diff --git a/CREATING_ISSUES.md b/CREATING_ISSUES.md index d6572c5c9a..87d00a1a92 100644 --- a/CREATING_ISSUES.md +++ b/CREATING_ISSUES.md @@ -202,7 +202,20 @@ resource "snowflake_grant_privileges_to_account_role" "grant_on_procedure" { account_role_name = snowflake_account_role.name on_schema_object { object_type = "PROCEDURE" - object_name = "\"${snowflake_database.database.name}\".\"${snowflake_schema.schema.name}\".\"${snowflake_procedure.procedure.name}(NUMBER, VARCHAR)\"" + object_name = "\"${snowflake_database.database.name}\".\"${snowflake_schema.schema.name}\".\"${snowflake_procedure.procedure.name}\"(NUMBER, VARCHAR)" + } +} +``` + +If you manage the procedure in Terraform, you can use `fully_qualified_name` field: + +```terraform +resource "snowflake_grant_privileges_to_account_role" "grant_on_procedure" { + privileges = ["USAGE"] + account_role_name = snowflake_account_role.name + on_schema_object { + object_type = "PROCEDURE" + object_name = snowflake_procedure.procedure_name.fully_qualified_name } } ``` diff --git a/MIGRATION_GUIDE.md b/MIGRATION_GUIDE.md index 697e7e47c6..a6145e2b1f 100644 --- a/MIGRATION_GUIDE.md +++ b/MIGRATION_GUIDE.md @@ -9,15 +9,53 @@ across different versions. ## v0.97.0 ➞ v0.98.0 +### *(behavior change)* handling copy_grants +Currently, resources like `snowflake_view`, `snowflake_stream_on_table`, `snowflake_stream_on_external_table` and `snowflake_stream_on_directory_table` support `copy_grants` field corresponding with `COPY GRANTS` during `CREATE`. The current behavior is that, when a change leading for recreation is detected (meaning a change that can not be handled by ALTER, but only by `CREATE OR REPLACE`), `COPY GRANTS` are used during recreation when `copy_grants` is set to `true`. Changing this field without changes in other field results in a noop because in this case there is no need to recreate a resource. + +### *(new feature)* recovering stale streams +Starting from this version, the provider detects stale streams for `snowflake_stream_on_table`, `snowflake_stream_on_external_table` and `snowflake_stream_on_directory_table` and recreates them (optionally with `copy_grants`) to recover them. To handle this correctly, a new computed-only field `stale` has been added to these resource, indicating whether a stream is stale. + +### *(new feature)* snowflake_stream_on_directory_table resource +Continuing changes made in [v0.97](#v0960--v0970), the new resource `snowflake_stream_on_directory_table` has been introduced to replace the previous `snowflake_stream` for streams on directory tables. + +To use the new `stream_on_directory_table`, change the old `stream` from +```terraform +resource "snowflake_stream" "stream" { + name = "stream" + schema = "schema" + database = "database" + + on_stage = snowflake_stage.stage.fully_qualified_name + + comment = "A stream." +} +``` + +to + +```terraform +resource "snowflake_stream_on_directory_table" "stream" { + name = "stream" + schema = "schema" + database = "database" + + stage = snowflake_stage.stage.fully_qualified_name + + comment = "A stream." +} +``` + +Then, follow our [Resource migration guide](https://github.com/Snowflake-Labs/terraform-provider-snowflake/blob/main/docs/technical-documentation/resource_migration.md). + ### *(new feature)* Secret resources Added a new secrets resources for managing secrets. -We decided to split each secret flow into individual resources. +We decided to split each secret flow into individual resources. This segregation was based on the secret flows in CREATE SECRET. i.e.: - `snowflake_secret_with_client_credentials` - `snowflake_secret_with_authorization_code_grant` - `snowflake_secret_with_basic_authentication` - `snowflake_secret_with_generic_string` - + See reference [docs](https://docs.snowflake.com/en/sql-reference/sql/create-secret). @@ -25,7 +63,7 @@ See reference [docs](https://docs.snowflake.com/en/sql-reference/sql/create-secr ### *(new feature)* snowflake_stream_on_table, snowflake_stream_on_external_table resource -To enhance clarity and functionality, the new resources `snowflake_stream_on_table` and `snowflake_stream_on_external_table` have been introduced to replace the previous `snowflake_stream`. Recognizing that the old resource carried multiple responsibilities within a single entity, we opted to divide it into more specialized resources. +To enhance clarity and functionality, the new resources `snowflake_stream_on_table`, `snowflake_stream_on_external_table` and `snowflake_stream_on_directory_table` have been introduced to replace the previous `snowflake_stream`. Recognizing that the old resource carried multiple responsibilities within a single entity, we opted to divide it into more specialized resources. The newly introduced resources are aligned with the latest Snowflake documentation at the time of implementation, and adhere to our [new conventions](#general-changes). This segregation was based on the object on which the stream is created. The mapping between SQL statements and the resources is the following: - `ON TABLE ` -> `snowflake_stream_on_table` @@ -49,7 +87,7 @@ resource "snowflake_stream" "stream" { to -``` +```terraform resource "snowflake_stream_on_table" "stream" { name = "stream" schema = "schema" @@ -62,6 +100,7 @@ resource "snowflake_stream_on_table" "stream" { } ``` + Then, follow our [Resource migration guide](https://github.com/Snowflake-Labs/terraform-provider-snowflake/blob/main/docs/technical-documentation/resource_migration.md). ### *(new feature)* new snowflake_service_user and snowflake_legacy_service_user resources @@ -97,7 +136,7 @@ resource "snowflake_user" "service_user" { lifecycle { ignore_changes = [user_type] } - + name = "Snowflake Service User" login_name = "service_user" email = "service_user@snowflake.example" diff --git a/Makefile b/Makefile index e81c234249..c8feb876b8 100644 --- a/Makefile +++ b/Makefile @@ -76,6 +76,9 @@ test-architecture: ## check architecture constraints between packages test-client: ## runs test that checks sdk.Client without instrumentedsql SF_TF_NO_INSTRUMENTED_SQL=1 SF_TF_GOSNOWFLAKE_LOG_LEVEL=debug go test ./pkg/sdk/internal/client/... -v +test-object-renaming: ## runs tests in object_renaming_acceptance_test.go + TEST_SF_TF_ENABLE_OBJECT_RENAMING=1 go test ./pkg/resources/object_renaming_acceptace_test.go -v + test-acceptance-%: ## run acceptance tests for the given resource only, e.g. test-acceptance-Warehouse TF_ACC=1 TF_LOG=DEBUG SF_TF_ACC_TEST_CONFIGURE_CLIENT_ONCE=true go test -run ^TestAcc_$*_ -v -timeout=20m ./pkg/resources diff --git a/docs/guides/identifiers.md b/docs/guides/identifiers.md index 1813be44e0..0af6936acf 100644 --- a/docs/guides/identifiers.md +++ b/docs/guides/identifiers.md @@ -15,7 +15,7 @@ For example, instead of writing ``` object_name = “\”${snowflake_table.database}\”.\”${snowflake_table.schema}\”.\”${snowflake_table.name}\”” # for procedures -object_name = “\”${snowflake_procedure.database}\”.\”${snowflake_procedure.schema}\”.\”${snowflake_procedure.name}(NUMBER, VARCHAR)\”” +object_name = “\”${snowflake_procedure.database}\”.\”${snowflake_procedure.schema}\”.\”${snowflake_procedure.name}\"(NUMBER, VARCHAR)” ``` now we can write diff --git a/docs/resources/stream_on_directory_table.md b/docs/resources/stream_on_directory_table.md new file mode 100644 index 0000000000..490ba2eae1 --- /dev/null +++ b/docs/resources/stream_on_directory_table.md @@ -0,0 +1,126 @@ +--- +page_title: "snowflake_stream_on_directory_table Resource - terraform-provider-snowflake" +subcategory: "" +description: |- + Resource used to manage streams on directory tables. For more information, check stream documentation https://docs.snowflake.com/en/sql-reference/sql/create-stream. +--- + +!> **V1 release candidate** This resource was reworked and is a release candidate for the V1. We do not expect significant changes in it before the V1. We will welcome any feedback and adjust the resource if needed. Any errors reported will be resolved with a higher priority. We encourage checking this resource out before the V1 release. Please follow the [migration guide](https://github.com/Snowflake-Labs/terraform-provider-snowflake/blob/main/MIGRATION_GUIDE.md#v0970--v0980) to use it. + +# snowflake_stream_on_directory_table (Resource) + +Resource used to manage streams on directory tables. For more information, check [stream documentation](https://docs.snowflake.com/en/sql-reference/sql/create-stream). + +## Example Usage + +```terraform +resource "snowflake_stage" "example_stage" { + name = "EXAMPLE_STAGE" + url = "s3://com.example.bucket/prefix" + database = "EXAMPLE_DB" + schema = "EXAMPLE_SCHEMA" + credentials = "AWS_KEY_ID='${var.example_aws_key_id}' AWS_SECRET_KEY='${var.example_aws_secret_key}'" +} + +# basic resource +resource "snowflake_stream_on_directory_table" "stream" { + name = "stream" + schema = "schema" + database = "database" + + stage = snowflake_stage.stage.fully_qualified_name +} + + +# resource with more fields set +resource "snowflake_stream_on_directory_table" "stream" { + name = "stream" + schema = "schema" + database = "database" + + copy_grants = true + stage = snowflake_stage.stage.fully_qualified_name + + at { + statement = "8e5d0ca9-005e-44e6-b858-a8f5b37c5726" + } + + comment = "A stream." +} +``` +-> **Note** Instead of using fully_qualified_name, you can reference objects managed outside Terraform by constructing a correct ID, consult [identifiers guide](https://registry.terraform.io/providers/Snowflake-Labs/snowflake/latest/docs/guides/identifiers#new-computed-fully-qualified-name-field-in-resources). + + + +## Schema + +### Required + +- `database` (String) The database in which to create the stream. Due to technical limitations (read more [here](https://github.com/Snowflake-Labs/terraform-provider-snowflake/blob/main/docs/technical-documentation/identifiers_rework_design_decisions.md#known-limitations-and-identifier-recommendations)), avoid using the following characters: `|`, `.`, `(`, `)`, `"` +- `name` (String) Specifies the identifier for the stream; must be unique for the database and schema in which the stream is created. Due to technical limitations (read more [here](https://github.com/Snowflake-Labs/terraform-provider-snowflake/blob/main/docs/technical-documentation/identifiers_rework_design_decisions.md#known-limitations-and-identifier-recommendations)), avoid using the following characters: `|`, `.`, `(`, `)`, `"` +- `schema` (String) The schema in which to create the stream. Due to technical limitations (read more [here](https://github.com/Snowflake-Labs/terraform-provider-snowflake/blob/main/docs/technical-documentation/identifiers_rework_design_decisions.md#known-limitations-and-identifier-recommendations)), avoid using the following characters: `|`, `.`, `(`, `)`, `"` +- `stage` (String) Specifies an identifier for the stage the stream will monitor. Due to Snowflake limitations, the provider can not read the stage's database and schema. For stages, Snowflake returns only partially qualified name instead of fully qualified name. Please use stages located in the same schema as the stream. Due to technical limitations (read more [here](https://github.com/Snowflake-Labs/terraform-provider-snowflake/blob/main/docs/technical-documentation/identifiers_rework_design_decisions.md#known-limitations-and-identifier-recommendations)), avoid using the following characters: `|`, `.`, `(`, `)`, `"` + +### Optional + +- `comment` (String) Specifies a comment for the stream. +- `copy_grants` (Boolean) Retains the access permissions from the original stream when a stream is recreated using the OR REPLACE clause. That is sometimes used when the provider detects changes for fields that can not be changed by ALTER. This value will not have any effect when creating a new stream. + +### Read-Only + +- `describe_output` (List of Object) Outputs the result of `DESCRIBE STREAM` for the given stream. (see [below for nested schema](#nestedatt--describe_output)) +- `fully_qualified_name` (String) Fully qualified name of the resource. For more information, see [object name resolution](https://docs.snowflake.com/en/sql-reference/name-resolution). +- `id` (String) The ID of this resource. +- `show_output` (List of Object) Outputs the result of `SHOW STREAMS` for the given stream. (see [below for nested schema](#nestedatt--show_output)) +- `stale` (Boolean) Indicated if the stream is stale. When Terraform detects that the stream is stale, the stream is recreated with `CREATE OR REPLACE`. Read more on stream staleness in Snowflake [docs](https://docs.snowflake.com/en/user-guide/streams-intro#data-retention-period-and-staleness). + + +### Nested Schema for `describe_output` + +Read-Only: + +- `base_tables` (List of String) +- `comment` (String) +- `created_on` (String) +- `database_name` (String) +- `invalid_reason` (String) +- `mode` (String) +- `name` (String) +- `owner` (String) +- `owner_role_type` (String) +- `schema_name` (String) +- `source_type` (String) +- `stale` (Boolean) +- `stale_after` (String) +- `table_name` (String) +- `type` (String) + + + +### Nested Schema for `show_output` + +Read-Only: + +- `base_tables` (List of String) +- `comment` (String) +- `created_on` (String) +- `database_name` (String) +- `invalid_reason` (String) +- `mode` (String) +- `name` (String) +- `owner` (String) +- `owner_role_type` (String) +- `schema_name` (String) +- `source_type` (String) +- `stale` (Boolean) +- `stale_after` (String) +- `table_name` (String) +- `type` (String) + +## Import + +Import is supported using the following syntax: + +```shell +terraform import snowflake_stream_on_directory_table.example '""."".""' +``` diff --git a/docs/resources/stream_on_external_table.md b/docs/resources/stream_on_external_table.md index 2899aa7a8e..3b773cbfcd 100644 --- a/docs/resources/stream_on_external_table.md +++ b/docs/resources/stream_on_external_table.md @@ -89,6 +89,7 @@ resource "snowflake_stream_on_external_table" "stream" { - `fully_qualified_name` (String) Fully qualified name of the resource. For more information, see [object name resolution](https://docs.snowflake.com/en/sql-reference/name-resolution). - `id` (String) The ID of this resource. - `show_output` (List of Object) Outputs the result of `SHOW STREAMS` for the given stream. (see [below for nested schema](#nestedatt--show_output)) +- `stale` (Boolean) Indicated if the stream is stale. When Terraform detects that the stream is stale, the stream is recreated with `CREATE OR REPLACE`. Read more on stream staleness in Snowflake [docs](https://docs.snowflake.com/en/user-guide/streams-intro#data-retention-period-and-staleness). ### Nested Schema for `at` @@ -128,7 +129,7 @@ Read-Only: - `owner_role_type` (String) - `schema_name` (String) - `source_type` (String) -- `stale` (String) +- `stale` (Boolean) - `stale_after` (String) - `table_name` (String) - `type` (String) @@ -150,7 +151,7 @@ Read-Only: - `owner_role_type` (String) - `schema_name` (String) - `source_type` (String) -- `stale` (String) +- `stale` (Boolean) - `stale_after` (String) - `table_name` (String) - `type` (String) diff --git a/docs/resources/stream_on_table.md b/docs/resources/stream_on_table.md index 3f999386b8..3af9472bd3 100644 --- a/docs/resources/stream_on_table.md +++ b/docs/resources/stream_on_table.md @@ -74,6 +74,7 @@ resource "snowflake_stream_on_table" "stream" { - `fully_qualified_name` (String) Fully qualified name of the resource. For more information, see [object name resolution](https://docs.snowflake.com/en/sql-reference/name-resolution). - `id` (String) The ID of this resource. - `show_output` (List of Object) Outputs the result of `SHOW STREAMS` for the given stream. (see [below for nested schema](#nestedatt--show_output)) +- `stale` (Boolean) Indicated if the stream is stale. When Terraform detects that the stream is stale, the stream is recreated with `CREATE OR REPLACE`. Read more on stream staleness in Snowflake [docs](https://docs.snowflake.com/en/user-guide/streams-intro#data-retention-period-and-staleness). ### Nested Schema for `at` @@ -113,7 +114,7 @@ Read-Only: - `owner_role_type` (String) - `schema_name` (String) - `source_type` (String) -- `stale` (String) +- `stale` (Boolean) - `stale_after` (String) - `table_name` (String) - `type` (String) @@ -135,7 +136,7 @@ Read-Only: - `owner_role_type` (String) - `schema_name` (String) - `source_type` (String) -- `stale` (String) +- `stale` (Boolean) - `stale_after` (String) - `table_name` (String) - `type` (String) diff --git a/examples/resources/snowflake_stream_on_directory_table/import.sh b/examples/resources/snowflake_stream_on_directory_table/import.sh new file mode 100644 index 0000000000..2362eb9eca --- /dev/null +++ b/examples/resources/snowflake_stream_on_directory_table/import.sh @@ -0,0 +1 @@ +terraform import snowflake_stream_on_directory_table.example '""."".""' diff --git a/examples/resources/snowflake_stream_on_directory_table/resource.tf b/examples/resources/snowflake_stream_on_directory_table/resource.tf new file mode 100644 index 0000000000..ab85c22f29 --- /dev/null +++ b/examples/resources/snowflake_stream_on_directory_table/resource.tf @@ -0,0 +1,33 @@ +resource "snowflake_stage" "example_stage" { + name = "EXAMPLE_STAGE" + url = "s3://com.example.bucket/prefix" + database = "EXAMPLE_DB" + schema = "EXAMPLE_SCHEMA" + credentials = "AWS_KEY_ID='${var.example_aws_key_id}' AWS_SECRET_KEY='${var.example_aws_secret_key}'" +} + +# basic resource +resource "snowflake_stream_on_directory_table" "stream" { + name = "stream" + schema = "schema" + database = "database" + + stage = snowflake_stage.stage.fully_qualified_name +} + + +# resource with more fields set +resource "snowflake_stream_on_directory_table" "stream" { + name = "stream" + schema = "schema" + database = "database" + + copy_grants = true + stage = snowflake_stage.stage.fully_qualified_name + + at { + statement = "8e5d0ca9-005e-44e6-b858-a8f5b37c5726" + } + + comment = "A stream." +} diff --git a/pkg/acceptance/bettertestspoc/assert/objectassert/stream_snowflake_ext.go b/pkg/acceptance/bettertestspoc/assert/objectassert/stream_snowflake_ext.go index 400b8497c6..52f6cbb124 100644 --- a/pkg/acceptance/bettertestspoc/assert/objectassert/stream_snowflake_ext.go +++ b/pkg/acceptance/bettertestspoc/assert/objectassert/stream_snowflake_ext.go @@ -63,7 +63,11 @@ func (s *StreamAssert) HasBaseTables(expected ...sdk.SchemaObjectIdentifier) *St } var errs []error for _, wantId := range expected { - if !slices.ContainsFunc(o.BaseTables, func(gotId sdk.SchemaObjectIdentifier) bool { + if !slices.ContainsFunc(o.BaseTables, func(gotName string) bool { + gotId, err := sdk.ParseSchemaObjectIdentifier(gotName) + if err != nil { + errs = append(errs, err) + } return wantId.FullyQualifiedName() == gotId.FullyQualifiedName() }) { errs = append(errs, fmt.Errorf("expected id: %s, to be in the list ids: %v", wantId.FullyQualifiedName(), o.BaseTables)) @@ -74,6 +78,23 @@ func (s *StreamAssert) HasBaseTables(expected ...sdk.SchemaObjectIdentifier) *St return s } +func (s *StreamAssert) HasBaseTablesPartiallyQualified(expected ...string) *StreamAssert { + s.AddAssertion(func(t *testing.T, o *sdk.Stream) error { + t.Helper() + if len(o.BaseTables) != len(expected) { + return fmt.Errorf("expected base tables length: %v; got: %v", len(expected), len(o.BaseTables)) + } + var errs []error + for _, wantName := range expected { + if !slices.Contains(o.BaseTables, wantName) { + errs = append(errs, fmt.Errorf("expected name: %s, to be in the list ids: %v", wantName, o.BaseTables)) + } + } + return errors.Join(errs...) + }) + return s +} + func (s *StreamAssert) HasMode(expected sdk.StreamMode) *StreamAssert { s.AddAssertion(func(t *testing.T, o *sdk.Stream) error { t.Helper() diff --git a/pkg/acceptance/bettertestspoc/assert/objectassert/stream_snowflake_gen.go b/pkg/acceptance/bettertestspoc/assert/objectassert/stream_snowflake_gen.go index 460b5355a7..3ad178a8b8 100644 --- a/pkg/acceptance/bettertestspoc/assert/objectassert/stream_snowflake_gen.go +++ b/pkg/acceptance/bettertestspoc/assert/objectassert/stream_snowflake_gen.go @@ -131,14 +131,11 @@ func (s *StreamAssert) HasType(expected string) *StreamAssert { return s } -func (s *StreamAssert) HasStale(expected string) *StreamAssert { +func (s *StreamAssert) HasStale(expected bool) *StreamAssert { s.AddAssertion(func(t *testing.T, o *sdk.Stream) error { t.Helper() - if o.Stale == nil { - return fmt.Errorf("expected stale to have value; got: nil") - } - if *o.Stale != expected { - return fmt.Errorf("expected stale: %v; got: %v", expected, *o.Stale) + if o.Stale != expected { + return fmt.Errorf("expected stale: %v; got: %v", expected, o.Stale) } return nil }) diff --git a/pkg/acceptance/bettertestspoc/assert/resourceassert/gen/resource_schema_def.go b/pkg/acceptance/bettertestspoc/assert/resourceassert/gen/resource_schema_def.go index afe751fd60..c51ed96d0e 100644 --- a/pkg/acceptance/bettertestspoc/assert/resourceassert/gen/resource_schema_def.go +++ b/pkg/acceptance/bettertestspoc/assert/resourceassert/gen/resource_schema_def.go @@ -41,6 +41,10 @@ var allResourceSchemaDefs = []ResourceSchemaDef{ name: "View", schema: resources.View().Schema, }, + { + name: "Database", + schema: resources.Database().Schema, + }, { name: "DatabaseRole", schema: resources.DatabaseRole().Schema, @@ -53,6 +57,10 @@ var allResourceSchemaDefs = []ResourceSchemaDef{ name: "RowAccessPolicy", schema: resources.RowAccessPolicy().Schema, }, + { + name: "Schema", + schema: resources.Schema().Schema, + }, { name: "MaskingPolicy", schema: resources.MaskingPolicy().Schema, @@ -80,5 +88,8 @@ var allResourceSchemaDefs = []ResourceSchemaDef{ { name: "SecretWithGenericString", schema: resources.SecretWithGenericString().Schema, + }, + name: "StreamOnDirectoryTable", + schema: resources.StreamOnDirectoryTable().Schema, }, } diff --git a/pkg/acceptance/bettertestspoc/assert/resourceassert/stream_on_directory_table_resource_gen.go b/pkg/acceptance/bettertestspoc/assert/resourceassert/stream_on_directory_table_resource_gen.go new file mode 100644 index 0000000000..8e4cd10770 --- /dev/null +++ b/pkg/acceptance/bettertestspoc/assert/resourceassert/stream_on_directory_table_resource_gen.go @@ -0,0 +1,117 @@ +// Code generated by assertions generator; DO NOT EDIT. + +package resourceassert + +import ( + "testing" + + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/acceptance/bettertestspoc/assert" +) + +type StreamOnDirectoryTableResourceAssert struct { + *assert.ResourceAssert +} + +func StreamOnDirectoryTableResource(t *testing.T, name string) *StreamOnDirectoryTableResourceAssert { + t.Helper() + + return &StreamOnDirectoryTableResourceAssert{ + ResourceAssert: assert.NewResourceAssert(name, "resource"), + } +} + +func ImportedStreamOnDirectoryTableResource(t *testing.T, id string) *StreamOnDirectoryTableResourceAssert { + t.Helper() + + return &StreamOnDirectoryTableResourceAssert{ + ResourceAssert: assert.NewImportedResourceAssert(id, "imported resource"), + } +} + +/////////////////////////////////// +// Attribute value string checks // +/////////////////////////////////// + +func (s *StreamOnDirectoryTableResourceAssert) HasCommentString(expected string) *StreamOnDirectoryTableResourceAssert { + s.AddAssertion(assert.ValueSet("comment", expected)) + return s +} + +func (s *StreamOnDirectoryTableResourceAssert) HasCopyGrantsString(expected string) *StreamOnDirectoryTableResourceAssert { + s.AddAssertion(assert.ValueSet("copy_grants", expected)) + return s +} + +func (s *StreamOnDirectoryTableResourceAssert) HasDatabaseString(expected string) *StreamOnDirectoryTableResourceAssert { + s.AddAssertion(assert.ValueSet("database", expected)) + return s +} + +func (s *StreamOnDirectoryTableResourceAssert) HasFullyQualifiedNameString(expected string) *StreamOnDirectoryTableResourceAssert { + s.AddAssertion(assert.ValueSet("fully_qualified_name", expected)) + return s +} + +func (s *StreamOnDirectoryTableResourceAssert) HasNameString(expected string) *StreamOnDirectoryTableResourceAssert { + s.AddAssertion(assert.ValueSet("name", expected)) + return s +} + +func (s *StreamOnDirectoryTableResourceAssert) HasSchemaString(expected string) *StreamOnDirectoryTableResourceAssert { + s.AddAssertion(assert.ValueSet("schema", expected)) + return s +} + +func (s *StreamOnDirectoryTableResourceAssert) HasStageString(expected string) *StreamOnDirectoryTableResourceAssert { + s.AddAssertion(assert.ValueSet("stage", expected)) + return s +} + +func (s *StreamOnDirectoryTableResourceAssert) HasStaleString(expected string) *StreamOnDirectoryTableResourceAssert { + s.AddAssertion(assert.ValueSet("stale", expected)) + return s +} + +//////////////////////////// +// Attribute empty checks // +//////////////////////////// + +func (s *StreamOnDirectoryTableResourceAssert) HasNoComment() *StreamOnDirectoryTableResourceAssert { + s.AddAssertion(assert.ValueNotSet("comment")) + return s +} + +func (s *StreamOnDirectoryTableResourceAssert) HasNoCopyGrants() *StreamOnDirectoryTableResourceAssert { + s.AddAssertion(assert.ValueNotSet("copy_grants")) + return s +} + +func (s *StreamOnDirectoryTableResourceAssert) HasNoDatabase() *StreamOnDirectoryTableResourceAssert { + s.AddAssertion(assert.ValueNotSet("database")) + return s +} + +func (s *StreamOnDirectoryTableResourceAssert) HasNoFullyQualifiedName() *StreamOnDirectoryTableResourceAssert { + s.AddAssertion(assert.ValueNotSet("fully_qualified_name")) + return s +} + +func (s *StreamOnDirectoryTableResourceAssert) HasNoName() *StreamOnDirectoryTableResourceAssert { + s.AddAssertion(assert.ValueNotSet("name")) + return s +} + +func (s *StreamOnDirectoryTableResourceAssert) HasNoSchema() *StreamOnDirectoryTableResourceAssert { + s.AddAssertion(assert.ValueNotSet("schema")) + return s +} + +func (s *StreamOnDirectoryTableResourceAssert) HasNoStage() *StreamOnDirectoryTableResourceAssert { + s.AddAssertion(assert.ValueNotSet("stage")) + return s +} + +func (s *StreamOnDirectoryTableResourceAssert) HasNoStale() *StreamOnDirectoryTableResourceAssert { + s.AddAssertion(assert.ValueNotSet("stale")) + return s +} diff --git a/pkg/acceptance/bettertestspoc/assert/resourceassert/stream_on_external_table_resource_gen.go b/pkg/acceptance/bettertestspoc/assert/resourceassert/stream_on_external_table_resource_gen.go index f8e6f4f749..78b963026d 100644 --- a/pkg/acceptance/bettertestspoc/assert/resourceassert/stream_on_external_table_resource_gen.go +++ b/pkg/acceptance/bettertestspoc/assert/resourceassert/stream_on_external_table_resource_gen.go @@ -82,6 +82,11 @@ func (s *StreamOnExternalTableResourceAssert) HasSchemaString(expected string) * return s } +func (s *StreamOnExternalTableResourceAssert) HasStaleString(expected string) *StreamOnExternalTableResourceAssert { + s.AddAssertion(assert.ValueSet("stale", expected)) + return s +} + //////////////////////////// // Attribute empty checks // //////////////////////////// @@ -135,3 +140,8 @@ func (s *StreamOnExternalTableResourceAssert) HasNoSchema() *StreamOnExternalTab s.AddAssertion(assert.ValueNotSet("schema")) return s } + +func (s *StreamOnExternalTableResourceAssert) HasNoStale() *StreamOnExternalTableResourceAssert { + s.AddAssertion(assert.ValueNotSet("stale")) + return s +} diff --git a/pkg/acceptance/bettertestspoc/assert/resourceassert/stream_on_table_resource_gen.go b/pkg/acceptance/bettertestspoc/assert/resourceassert/stream_on_table_resource_gen.go index ad83af307c..4175844bf4 100644 --- a/pkg/acceptance/bettertestspoc/assert/resourceassert/stream_on_table_resource_gen.go +++ b/pkg/acceptance/bettertestspoc/assert/resourceassert/stream_on_table_resource_gen.go @@ -82,6 +82,11 @@ func (s *StreamOnTableResourceAssert) HasShowInitialRowsString(expected string) return s } +func (s *StreamOnTableResourceAssert) HasStaleString(expected string) *StreamOnTableResourceAssert { + s.AddAssertion(assert.ValueSet("stale", expected)) + return s +} + func (s *StreamOnTableResourceAssert) HasTableString(expected string) *StreamOnTableResourceAssert { s.AddAssertion(assert.ValueSet("table", expected)) return s @@ -141,6 +146,11 @@ func (s *StreamOnTableResourceAssert) HasNoShowInitialRows() *StreamOnTableResou return s } +func (s *StreamOnTableResourceAssert) HasNoStale() *StreamOnTableResourceAssert { + s.AddAssertion(assert.ValueNotSet("stale")) + return s +} + func (s *StreamOnTableResourceAssert) HasNoTable() *StreamOnTableResourceAssert { s.AddAssertion(assert.ValueNotSet("table")) return s diff --git a/pkg/acceptance/bettertestspoc/assert/resourceshowoutputassert/stream_show_output_ext.go b/pkg/acceptance/bettertestspoc/assert/resourceshowoutputassert/stream_show_output_ext.go index 9c350e1809..84756872f6 100644 --- a/pkg/acceptance/bettertestspoc/assert/resourceshowoutputassert/stream_show_output_ext.go +++ b/pkg/acceptance/bettertestspoc/assert/resourceshowoutputassert/stream_show_output_ext.go @@ -1,15 +1,25 @@ -// Code generated by assertions generator; DO NOT EDIT. - package resourceshowoutputassert import ( "fmt" "strconv" + "testing" "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/acceptance/bettertestspoc/assert" "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/sdk" ) +// StreamsDatasourceShowOutput is a temporary workaround to have better show output assertions in data source acceptance tests. +func StreamsDatasourceShowOutput(t *testing.T, name string) *StreamShowOutputAssert { + t.Helper() + + s := StreamShowOutputAssert{ + ResourceAssert: assert.NewDatasourceAssert("data."+name, "show_output", "streams.0."), + } + s.AddAssertion(assert.ValueSet("show_output.#", "1")) + return &s +} + func (s *StreamShowOutputAssert) HasCreatedOnNotEmpty() *StreamShowOutputAssert { s.AddAssertion(assert.ResourceShowOutputValuePresent("created_on")) return s @@ -27,3 +37,11 @@ func (s *StreamShowOutputAssert) HasBaseTables(ids []sdk.SchemaObjectIdentifier) } return s } + +func (s *StreamShowOutputAssert) HasBaseTablesPartiallyQualified(ids ...string) *StreamShowOutputAssert { + s.AddAssertion(assert.ResourceShowOutputValueSet("base_tables.#", strconv.FormatInt(int64(len(ids)), 10))) + for i := range ids { + s.AddAssertion(assert.ResourceShowOutputValueSet(fmt.Sprintf("base_tables.%d", i), ids[i])) + } + return s +} diff --git a/pkg/acceptance/bettertestspoc/config/config.go b/pkg/acceptance/bettertestspoc/config/config.go index 3135e9a2a8..a47a2a43d2 100644 --- a/pkg/acceptance/bettertestspoc/config/config.go +++ b/pkg/acceptance/bettertestspoc/config/config.go @@ -25,7 +25,7 @@ type ResourceModel interface { SetResourceName(name string) ResourceReference() string DependsOn() []string - SetDependsOn(values []string) + SetDependsOn(values ...string) } type ResourceModelMeta struct { @@ -54,7 +54,7 @@ func (m *ResourceModelMeta) DependsOn() []string { return m.dependsOn } -func (m *ResourceModelMeta) SetDependsOn(values []string) { +func (m *ResourceModelMeta) SetDependsOn(values ...string) { m.dependsOn = values } @@ -99,6 +99,15 @@ func FromModel(t *testing.T, model ResourceModel) string { return s } +func FromModels(t *testing.T, models ...ResourceModel) string { + t.Helper() + var sb strings.Builder + for _, model := range models { + sb.WriteString(FromModel(t, model) + "\n") + } + return sb.String() +} + // ConfigVariablesFromModel constructs config.Variables needed in acceptance tests that are using ConfigVariables in // combination with ConfigDirectory. It's necessary for cases not supported by FromModel, like lists of objects. func ConfigVariablesFromModel(t *testing.T, model ResourceModel) tfconfig.Variables { diff --git a/pkg/acceptance/bettertestspoc/config/model/database_model_gen.go b/pkg/acceptance/bettertestspoc/config/model/database_model_gen.go new file mode 100644 index 0000000000..813b6b96e3 --- /dev/null +++ b/pkg/acceptance/bettertestspoc/config/model/database_model_gen.go @@ -0,0 +1,283 @@ +// Code generated by config model builder generator; DO NOT EDIT. + +package model + +import ( + tfconfig "github.com/hashicorp/terraform-plugin-testing/config" + + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/acceptance/bettertestspoc/config" + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/provider/resources" +) + +type DatabaseModel struct { + Catalog tfconfig.Variable `json:"catalog,omitempty"` + Comment tfconfig.Variable `json:"comment,omitempty"` + DataRetentionTimeInDays tfconfig.Variable `json:"data_retention_time_in_days,omitempty"` + DefaultDdlCollation tfconfig.Variable `json:"default_ddl_collation,omitempty"` + DropPublicSchemaOnCreation tfconfig.Variable `json:"drop_public_schema_on_creation,omitempty"` + EnableConsoleOutput tfconfig.Variable `json:"enable_console_output,omitempty"` + ExternalVolume tfconfig.Variable `json:"external_volume,omitempty"` + FullyQualifiedName tfconfig.Variable `json:"fully_qualified_name,omitempty"` + IsTransient tfconfig.Variable `json:"is_transient,omitempty"` + LogLevel tfconfig.Variable `json:"log_level,omitempty"` + MaxDataExtensionTimeInDays tfconfig.Variable `json:"max_data_extension_time_in_days,omitempty"` + Name tfconfig.Variable `json:"name,omitempty"` + QuotedIdentifiersIgnoreCase tfconfig.Variable `json:"quoted_identifiers_ignore_case,omitempty"` + ReplaceInvalidCharacters tfconfig.Variable `json:"replace_invalid_characters,omitempty"` + Replication tfconfig.Variable `json:"replication,omitempty"` + StorageSerializationPolicy tfconfig.Variable `json:"storage_serialization_policy,omitempty"` + SuspendTaskAfterNumFailures tfconfig.Variable `json:"suspend_task_after_num_failures,omitempty"` + TaskAutoRetryAttempts tfconfig.Variable `json:"task_auto_retry_attempts,omitempty"` + TraceLevel tfconfig.Variable `json:"trace_level,omitempty"` + UserTaskManagedInitialWarehouseSize tfconfig.Variable `json:"user_task_managed_initial_warehouse_size,omitempty"` + UserTaskMinimumTriggerIntervalInSeconds tfconfig.Variable `json:"user_task_minimum_trigger_interval_in_seconds,omitempty"` + UserTaskTimeoutMs tfconfig.Variable `json:"user_task_timeout_ms,omitempty"` + + *config.ResourceModelMeta +} + +///////////////////////////////////////////////// +// Basic builders (resource name and required) // +///////////////////////////////////////////////// + +func Database( + resourceName string, + name string, +) *DatabaseModel { + d := &DatabaseModel{ResourceModelMeta: config.Meta(resourceName, resources.Database)} + d.WithName(name) + return d +} + +func DatabaseWithDefaultMeta( + name string, +) *DatabaseModel { + d := &DatabaseModel{ResourceModelMeta: config.DefaultMeta(resources.Database)} + d.WithName(name) + return d +} + +///////////////////////////////// +// below all the proper values // +///////////////////////////////// + +func (d *DatabaseModel) WithCatalog(catalog string) *DatabaseModel { + d.Catalog = tfconfig.StringVariable(catalog) + return d +} + +func (d *DatabaseModel) WithComment(comment string) *DatabaseModel { + d.Comment = tfconfig.StringVariable(comment) + return d +} + +func (d *DatabaseModel) WithDataRetentionTimeInDays(dataRetentionTimeInDays int) *DatabaseModel { + d.DataRetentionTimeInDays = tfconfig.IntegerVariable(dataRetentionTimeInDays) + return d +} + +func (d *DatabaseModel) WithDefaultDdlCollation(defaultDdlCollation string) *DatabaseModel { + d.DefaultDdlCollation = tfconfig.StringVariable(defaultDdlCollation) + return d +} + +func (d *DatabaseModel) WithDropPublicSchemaOnCreation(dropPublicSchemaOnCreation bool) *DatabaseModel { + d.DropPublicSchemaOnCreation = tfconfig.BoolVariable(dropPublicSchemaOnCreation) + return d +} + +func (d *DatabaseModel) WithEnableConsoleOutput(enableConsoleOutput bool) *DatabaseModel { + d.EnableConsoleOutput = tfconfig.BoolVariable(enableConsoleOutput) + return d +} + +func (d *DatabaseModel) WithExternalVolume(externalVolume string) *DatabaseModel { + d.ExternalVolume = tfconfig.StringVariable(externalVolume) + return d +} + +func (d *DatabaseModel) WithFullyQualifiedName(fullyQualifiedName string) *DatabaseModel { + d.FullyQualifiedName = tfconfig.StringVariable(fullyQualifiedName) + return d +} + +func (d *DatabaseModel) WithIsTransient(isTransient bool) *DatabaseModel { + d.IsTransient = tfconfig.BoolVariable(isTransient) + return d +} + +func (d *DatabaseModel) WithLogLevel(logLevel string) *DatabaseModel { + d.LogLevel = tfconfig.StringVariable(logLevel) + return d +} + +func (d *DatabaseModel) WithMaxDataExtensionTimeInDays(maxDataExtensionTimeInDays int) *DatabaseModel { + d.MaxDataExtensionTimeInDays = tfconfig.IntegerVariable(maxDataExtensionTimeInDays) + return d +} + +func (d *DatabaseModel) WithName(name string) *DatabaseModel { + d.Name = tfconfig.StringVariable(name) + return d +} + +func (d *DatabaseModel) WithQuotedIdentifiersIgnoreCase(quotedIdentifiersIgnoreCase bool) *DatabaseModel { + d.QuotedIdentifiersIgnoreCase = tfconfig.BoolVariable(quotedIdentifiersIgnoreCase) + return d +} + +func (d *DatabaseModel) WithReplaceInvalidCharacters(replaceInvalidCharacters bool) *DatabaseModel { + d.ReplaceInvalidCharacters = tfconfig.BoolVariable(replaceInvalidCharacters) + return d +} + +// replication attribute type is not yet supported, so WithReplication can't be generated + +func (d *DatabaseModel) WithStorageSerializationPolicy(storageSerializationPolicy string) *DatabaseModel { + d.StorageSerializationPolicy = tfconfig.StringVariable(storageSerializationPolicy) + return d +} + +func (d *DatabaseModel) WithSuspendTaskAfterNumFailures(suspendTaskAfterNumFailures int) *DatabaseModel { + d.SuspendTaskAfterNumFailures = tfconfig.IntegerVariable(suspendTaskAfterNumFailures) + return d +} + +func (d *DatabaseModel) WithTaskAutoRetryAttempts(taskAutoRetryAttempts int) *DatabaseModel { + d.TaskAutoRetryAttempts = tfconfig.IntegerVariable(taskAutoRetryAttempts) + return d +} + +func (d *DatabaseModel) WithTraceLevel(traceLevel string) *DatabaseModel { + d.TraceLevel = tfconfig.StringVariable(traceLevel) + return d +} + +func (d *DatabaseModel) WithUserTaskManagedInitialWarehouseSize(userTaskManagedInitialWarehouseSize string) *DatabaseModel { + d.UserTaskManagedInitialWarehouseSize = tfconfig.StringVariable(userTaskManagedInitialWarehouseSize) + return d +} + +func (d *DatabaseModel) WithUserTaskMinimumTriggerIntervalInSeconds(userTaskMinimumTriggerIntervalInSeconds int) *DatabaseModel { + d.UserTaskMinimumTriggerIntervalInSeconds = tfconfig.IntegerVariable(userTaskMinimumTriggerIntervalInSeconds) + return d +} + +func (d *DatabaseModel) WithUserTaskTimeoutMs(userTaskTimeoutMs int) *DatabaseModel { + d.UserTaskTimeoutMs = tfconfig.IntegerVariable(userTaskTimeoutMs) + return d +} + +////////////////////////////////////////// +// below it's possible to set any value // +////////////////////////////////////////// + +func (d *DatabaseModel) WithCatalogValue(value tfconfig.Variable) *DatabaseModel { + d.Catalog = value + return d +} + +func (d *DatabaseModel) WithCommentValue(value tfconfig.Variable) *DatabaseModel { + d.Comment = value + return d +} + +func (d *DatabaseModel) WithDataRetentionTimeInDaysValue(value tfconfig.Variable) *DatabaseModel { + d.DataRetentionTimeInDays = value + return d +} + +func (d *DatabaseModel) WithDefaultDdlCollationValue(value tfconfig.Variable) *DatabaseModel { + d.DefaultDdlCollation = value + return d +} + +func (d *DatabaseModel) WithDropPublicSchemaOnCreationValue(value tfconfig.Variable) *DatabaseModel { + d.DropPublicSchemaOnCreation = value + return d +} + +func (d *DatabaseModel) WithEnableConsoleOutputValue(value tfconfig.Variable) *DatabaseModel { + d.EnableConsoleOutput = value + return d +} + +func (d *DatabaseModel) WithExternalVolumeValue(value tfconfig.Variable) *DatabaseModel { + d.ExternalVolume = value + return d +} + +func (d *DatabaseModel) WithFullyQualifiedNameValue(value tfconfig.Variable) *DatabaseModel { + d.FullyQualifiedName = value + return d +} + +func (d *DatabaseModel) WithIsTransientValue(value tfconfig.Variable) *DatabaseModel { + d.IsTransient = value + return d +} + +func (d *DatabaseModel) WithLogLevelValue(value tfconfig.Variable) *DatabaseModel { + d.LogLevel = value + return d +} + +func (d *DatabaseModel) WithMaxDataExtensionTimeInDaysValue(value tfconfig.Variable) *DatabaseModel { + d.MaxDataExtensionTimeInDays = value + return d +} + +func (d *DatabaseModel) WithNameValue(value tfconfig.Variable) *DatabaseModel { + d.Name = value + return d +} + +func (d *DatabaseModel) WithQuotedIdentifiersIgnoreCaseValue(value tfconfig.Variable) *DatabaseModel { + d.QuotedIdentifiersIgnoreCase = value + return d +} + +func (d *DatabaseModel) WithReplaceInvalidCharactersValue(value tfconfig.Variable) *DatabaseModel { + d.ReplaceInvalidCharacters = value + return d +} + +func (d *DatabaseModel) WithReplicationValue(value tfconfig.Variable) *DatabaseModel { + d.Replication = value + return d +} + +func (d *DatabaseModel) WithStorageSerializationPolicyValue(value tfconfig.Variable) *DatabaseModel { + d.StorageSerializationPolicy = value + return d +} + +func (d *DatabaseModel) WithSuspendTaskAfterNumFailuresValue(value tfconfig.Variable) *DatabaseModel { + d.SuspendTaskAfterNumFailures = value + return d +} + +func (d *DatabaseModel) WithTaskAutoRetryAttemptsValue(value tfconfig.Variable) *DatabaseModel { + d.TaskAutoRetryAttempts = value + return d +} + +func (d *DatabaseModel) WithTraceLevelValue(value tfconfig.Variable) *DatabaseModel { + d.TraceLevel = value + return d +} + +func (d *DatabaseModel) WithUserTaskManagedInitialWarehouseSizeValue(value tfconfig.Variable) *DatabaseModel { + d.UserTaskManagedInitialWarehouseSize = value + return d +} + +func (d *DatabaseModel) WithUserTaskMinimumTriggerIntervalInSecondsValue(value tfconfig.Variable) *DatabaseModel { + d.UserTaskMinimumTriggerIntervalInSeconds = value + return d +} + +func (d *DatabaseModel) WithUserTaskTimeoutMsValue(value tfconfig.Variable) *DatabaseModel { + d.UserTaskTimeoutMs = value + return d +} diff --git a/pkg/acceptance/bettertestspoc/config/model/schema_model_gen.go b/pkg/acceptance/bettertestspoc/config/model/schema_model_gen.go new file mode 100644 index 0000000000..e83ac5058d --- /dev/null +++ b/pkg/acceptance/bettertestspoc/config/model/schema_model_gen.go @@ -0,0 +1,301 @@ +// Code generated by config model builder generator; DO NOT EDIT. + +package model + +import ( + tfconfig "github.com/hashicorp/terraform-plugin-testing/config" + + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/acceptance/bettertestspoc/config" + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/provider/resources" +) + +type SchemaModel struct { + Catalog tfconfig.Variable `json:"catalog,omitempty"` + Comment tfconfig.Variable `json:"comment,omitempty"` + DataRetentionTimeInDays tfconfig.Variable `json:"data_retention_time_in_days,omitempty"` + Database tfconfig.Variable `json:"database,omitempty"` + DefaultDdlCollation tfconfig.Variable `json:"default_ddl_collation,omitempty"` + EnableConsoleOutput tfconfig.Variable `json:"enable_console_output,omitempty"` + ExternalVolume tfconfig.Variable `json:"external_volume,omitempty"` + FullyQualifiedName tfconfig.Variable `json:"fully_qualified_name,omitempty"` + IsTransient tfconfig.Variable `json:"is_transient,omitempty"` + LogLevel tfconfig.Variable `json:"log_level,omitempty"` + MaxDataExtensionTimeInDays tfconfig.Variable `json:"max_data_extension_time_in_days,omitempty"` + Name tfconfig.Variable `json:"name,omitempty"` + PipeExecutionPaused tfconfig.Variable `json:"pipe_execution_paused,omitempty"` + QuotedIdentifiersIgnoreCase tfconfig.Variable `json:"quoted_identifiers_ignore_case,omitempty"` + ReplaceInvalidCharacters tfconfig.Variable `json:"replace_invalid_characters,omitempty"` + StorageSerializationPolicy tfconfig.Variable `json:"storage_serialization_policy,omitempty"` + SuspendTaskAfterNumFailures tfconfig.Variable `json:"suspend_task_after_num_failures,omitempty"` + TaskAutoRetryAttempts tfconfig.Variable `json:"task_auto_retry_attempts,omitempty"` + TraceLevel tfconfig.Variable `json:"trace_level,omitempty"` + UserTaskManagedInitialWarehouseSize tfconfig.Variable `json:"user_task_managed_initial_warehouse_size,omitempty"` + UserTaskMinimumTriggerIntervalInSeconds tfconfig.Variable `json:"user_task_minimum_trigger_interval_in_seconds,omitempty"` + UserTaskTimeoutMs tfconfig.Variable `json:"user_task_timeout_ms,omitempty"` + WithManagedAccess tfconfig.Variable `json:"with_managed_access,omitempty"` + + *config.ResourceModelMeta +} + +///////////////////////////////////////////////// +// Basic builders (resource name and required) // +///////////////////////////////////////////////// + +func Schema( + resourceName string, + database string, + name string, +) *SchemaModel { + s := &SchemaModel{ResourceModelMeta: config.Meta(resourceName, resources.Schema)} + s.WithDatabase(database) + s.WithName(name) + return s +} + +func SchemaWithDefaultMeta( + database string, + name string, +) *SchemaModel { + s := &SchemaModel{ResourceModelMeta: config.DefaultMeta(resources.Schema)} + s.WithDatabase(database) + s.WithName(name) + return s +} + +///////////////////////////////// +// below all the proper values // +///////////////////////////////// + +func (s *SchemaModel) WithCatalog(catalog string) *SchemaModel { + s.Catalog = tfconfig.StringVariable(catalog) + return s +} + +func (s *SchemaModel) WithComment(comment string) *SchemaModel { + s.Comment = tfconfig.StringVariable(comment) + return s +} + +func (s *SchemaModel) WithDataRetentionTimeInDays(dataRetentionTimeInDays int) *SchemaModel { + s.DataRetentionTimeInDays = tfconfig.IntegerVariable(dataRetentionTimeInDays) + return s +} + +func (s *SchemaModel) WithDatabase(database string) *SchemaModel { + s.Database = tfconfig.StringVariable(database) + return s +} + +func (s *SchemaModel) WithDefaultDdlCollation(defaultDdlCollation string) *SchemaModel { + s.DefaultDdlCollation = tfconfig.StringVariable(defaultDdlCollation) + return s +} + +func (s *SchemaModel) WithEnableConsoleOutput(enableConsoleOutput bool) *SchemaModel { + s.EnableConsoleOutput = tfconfig.BoolVariable(enableConsoleOutput) + return s +} + +func (s *SchemaModel) WithExternalVolume(externalVolume string) *SchemaModel { + s.ExternalVolume = tfconfig.StringVariable(externalVolume) + return s +} + +func (s *SchemaModel) WithFullyQualifiedName(fullyQualifiedName string) *SchemaModel { + s.FullyQualifiedName = tfconfig.StringVariable(fullyQualifiedName) + return s +} + +func (s *SchemaModel) WithIsTransient(isTransient string) *SchemaModel { + s.IsTransient = tfconfig.StringVariable(isTransient) + return s +} + +func (s *SchemaModel) WithLogLevel(logLevel string) *SchemaModel { + s.LogLevel = tfconfig.StringVariable(logLevel) + return s +} + +func (s *SchemaModel) WithMaxDataExtensionTimeInDays(maxDataExtensionTimeInDays int) *SchemaModel { + s.MaxDataExtensionTimeInDays = tfconfig.IntegerVariable(maxDataExtensionTimeInDays) + return s +} + +func (s *SchemaModel) WithName(name string) *SchemaModel { + s.Name = tfconfig.StringVariable(name) + return s +} + +func (s *SchemaModel) WithPipeExecutionPaused(pipeExecutionPaused bool) *SchemaModel { + s.PipeExecutionPaused = tfconfig.BoolVariable(pipeExecutionPaused) + return s +} + +func (s *SchemaModel) WithQuotedIdentifiersIgnoreCase(quotedIdentifiersIgnoreCase bool) *SchemaModel { + s.QuotedIdentifiersIgnoreCase = tfconfig.BoolVariable(quotedIdentifiersIgnoreCase) + return s +} + +func (s *SchemaModel) WithReplaceInvalidCharacters(replaceInvalidCharacters bool) *SchemaModel { + s.ReplaceInvalidCharacters = tfconfig.BoolVariable(replaceInvalidCharacters) + return s +} + +func (s *SchemaModel) WithStorageSerializationPolicy(storageSerializationPolicy string) *SchemaModel { + s.StorageSerializationPolicy = tfconfig.StringVariable(storageSerializationPolicy) + return s +} + +func (s *SchemaModel) WithSuspendTaskAfterNumFailures(suspendTaskAfterNumFailures int) *SchemaModel { + s.SuspendTaskAfterNumFailures = tfconfig.IntegerVariable(suspendTaskAfterNumFailures) + return s +} + +func (s *SchemaModel) WithTaskAutoRetryAttempts(taskAutoRetryAttempts int) *SchemaModel { + s.TaskAutoRetryAttempts = tfconfig.IntegerVariable(taskAutoRetryAttempts) + return s +} + +func (s *SchemaModel) WithTraceLevel(traceLevel string) *SchemaModel { + s.TraceLevel = tfconfig.StringVariable(traceLevel) + return s +} + +func (s *SchemaModel) WithUserTaskManagedInitialWarehouseSize(userTaskManagedInitialWarehouseSize string) *SchemaModel { + s.UserTaskManagedInitialWarehouseSize = tfconfig.StringVariable(userTaskManagedInitialWarehouseSize) + return s +} + +func (s *SchemaModel) WithUserTaskMinimumTriggerIntervalInSeconds(userTaskMinimumTriggerIntervalInSeconds int) *SchemaModel { + s.UserTaskMinimumTriggerIntervalInSeconds = tfconfig.IntegerVariable(userTaskMinimumTriggerIntervalInSeconds) + return s +} + +func (s *SchemaModel) WithUserTaskTimeoutMs(userTaskTimeoutMs int) *SchemaModel { + s.UserTaskTimeoutMs = tfconfig.IntegerVariable(userTaskTimeoutMs) + return s +} + +func (s *SchemaModel) WithWithManagedAccess(withManagedAccess string) *SchemaModel { + s.WithManagedAccess = tfconfig.StringVariable(withManagedAccess) + return s +} + +////////////////////////////////////////// +// below it's possible to set any value // +////////////////////////////////////////// + +func (s *SchemaModel) WithCatalogValue(value tfconfig.Variable) *SchemaModel { + s.Catalog = value + return s +} + +func (s *SchemaModel) WithCommentValue(value tfconfig.Variable) *SchemaModel { + s.Comment = value + return s +} + +func (s *SchemaModel) WithDataRetentionTimeInDaysValue(value tfconfig.Variable) *SchemaModel { + s.DataRetentionTimeInDays = value + return s +} + +func (s *SchemaModel) WithDatabaseValue(value tfconfig.Variable) *SchemaModel { + s.Database = value + return s +} + +func (s *SchemaModel) WithDefaultDdlCollationValue(value tfconfig.Variable) *SchemaModel { + s.DefaultDdlCollation = value + return s +} + +func (s *SchemaModel) WithEnableConsoleOutputValue(value tfconfig.Variable) *SchemaModel { + s.EnableConsoleOutput = value + return s +} + +func (s *SchemaModel) WithExternalVolumeValue(value tfconfig.Variable) *SchemaModel { + s.ExternalVolume = value + return s +} + +func (s *SchemaModel) WithFullyQualifiedNameValue(value tfconfig.Variable) *SchemaModel { + s.FullyQualifiedName = value + return s +} + +func (s *SchemaModel) WithIsTransientValue(value tfconfig.Variable) *SchemaModel { + s.IsTransient = value + return s +} + +func (s *SchemaModel) WithLogLevelValue(value tfconfig.Variable) *SchemaModel { + s.LogLevel = value + return s +} + +func (s *SchemaModel) WithMaxDataExtensionTimeInDaysValue(value tfconfig.Variable) *SchemaModel { + s.MaxDataExtensionTimeInDays = value + return s +} + +func (s *SchemaModel) WithNameValue(value tfconfig.Variable) *SchemaModel { + s.Name = value + return s +} + +func (s *SchemaModel) WithPipeExecutionPausedValue(value tfconfig.Variable) *SchemaModel { + s.PipeExecutionPaused = value + return s +} + +func (s *SchemaModel) WithQuotedIdentifiersIgnoreCaseValue(value tfconfig.Variable) *SchemaModel { + s.QuotedIdentifiersIgnoreCase = value + return s +} + +func (s *SchemaModel) WithReplaceInvalidCharactersValue(value tfconfig.Variable) *SchemaModel { + s.ReplaceInvalidCharacters = value + return s +} + +func (s *SchemaModel) WithStorageSerializationPolicyValue(value tfconfig.Variable) *SchemaModel { + s.StorageSerializationPolicy = value + return s +} + +func (s *SchemaModel) WithSuspendTaskAfterNumFailuresValue(value tfconfig.Variable) *SchemaModel { + s.SuspendTaskAfterNumFailures = value + return s +} + +func (s *SchemaModel) WithTaskAutoRetryAttemptsValue(value tfconfig.Variable) *SchemaModel { + s.TaskAutoRetryAttempts = value + return s +} + +func (s *SchemaModel) WithTraceLevelValue(value tfconfig.Variable) *SchemaModel { + s.TraceLevel = value + return s +} + +func (s *SchemaModel) WithUserTaskManagedInitialWarehouseSizeValue(value tfconfig.Variable) *SchemaModel { + s.UserTaskManagedInitialWarehouseSize = value + return s +} + +func (s *SchemaModel) WithUserTaskMinimumTriggerIntervalInSecondsValue(value tfconfig.Variable) *SchemaModel { + s.UserTaskMinimumTriggerIntervalInSeconds = value + return s +} + +func (s *SchemaModel) WithUserTaskTimeoutMsValue(value tfconfig.Variable) *SchemaModel { + s.UserTaskTimeoutMs = value + return s +} + +func (s *SchemaModel) WithWithManagedAccessValue(value tfconfig.Variable) *SchemaModel { + s.WithManagedAccess = value + return s +} diff --git a/pkg/acceptance/bettertestspoc/config/model/stream_on_directory_table_model_gen.go b/pkg/acceptance/bettertestspoc/config/model/stream_on_directory_table_model_gen.go new file mode 100644 index 0000000000..4956e940c9 --- /dev/null +++ b/pkg/acceptance/bettertestspoc/config/model/stream_on_directory_table_model_gen.go @@ -0,0 +1,133 @@ +// Code generated by config model builder generator; DO NOT EDIT. + +package model + +import ( + tfconfig "github.com/hashicorp/terraform-plugin-testing/config" + + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/acceptance/bettertestspoc/config" + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/provider/resources" +) + +type StreamOnDirectoryTableModel struct { + Comment tfconfig.Variable `json:"comment,omitempty"` + CopyGrants tfconfig.Variable `json:"copy_grants,omitempty"` + Database tfconfig.Variable `json:"database,omitempty"` + FullyQualifiedName tfconfig.Variable `json:"fully_qualified_name,omitempty"` + Name tfconfig.Variable `json:"name,omitempty"` + Schema tfconfig.Variable `json:"schema,omitempty"` + Stage tfconfig.Variable `json:"stage,omitempty"` + + *config.ResourceModelMeta +} + +///////////////////////////////////////////////// +// Basic builders (resource name and required) // +///////////////////////////////////////////////// + +func StreamOnDirectoryTable( + resourceName string, + database string, + name string, + schema string, + stage string, +) *StreamOnDirectoryTableModel { + s := &StreamOnDirectoryTableModel{ResourceModelMeta: config.Meta(resourceName, resources.StreamOnDirectoryTable)} + s.WithDatabase(database) + s.WithName(name) + s.WithSchema(schema) + s.WithStage(stage) + return s +} + +func StreamOnDirectoryTableWithDefaultMeta( + database string, + name string, + schema string, + stage string, +) *StreamOnDirectoryTableModel { + s := &StreamOnDirectoryTableModel{ResourceModelMeta: config.DefaultMeta(resources.StreamOnDirectoryTable)} + s.WithDatabase(database) + s.WithName(name) + s.WithSchema(schema) + s.WithStage(stage) + return s +} + +///////////////////////////////// +// below all the proper values // +///////////////////////////////// + +func (s *StreamOnDirectoryTableModel) WithComment(comment string) *StreamOnDirectoryTableModel { + s.Comment = tfconfig.StringVariable(comment) + return s +} + +func (s *StreamOnDirectoryTableModel) WithCopyGrants(copyGrants bool) *StreamOnDirectoryTableModel { + s.CopyGrants = tfconfig.BoolVariable(copyGrants) + return s +} + +func (s *StreamOnDirectoryTableModel) WithDatabase(database string) *StreamOnDirectoryTableModel { + s.Database = tfconfig.StringVariable(database) + return s +} + +func (s *StreamOnDirectoryTableModel) WithFullyQualifiedName(fullyQualifiedName string) *StreamOnDirectoryTableModel { + s.FullyQualifiedName = tfconfig.StringVariable(fullyQualifiedName) + return s +} + +func (s *StreamOnDirectoryTableModel) WithName(name string) *StreamOnDirectoryTableModel { + s.Name = tfconfig.StringVariable(name) + return s +} + +func (s *StreamOnDirectoryTableModel) WithSchema(schema string) *StreamOnDirectoryTableModel { + s.Schema = tfconfig.StringVariable(schema) + return s +} + +func (s *StreamOnDirectoryTableModel) WithStage(stage string) *StreamOnDirectoryTableModel { + s.Stage = tfconfig.StringVariable(stage) + return s +} + +////////////////////////////////////////// +// below it's possible to set any value // +////////////////////////////////////////// + +func (s *StreamOnDirectoryTableModel) WithCommentValue(value tfconfig.Variable) *StreamOnDirectoryTableModel { + s.Comment = value + return s +} + +func (s *StreamOnDirectoryTableModel) WithCopyGrantsValue(value tfconfig.Variable) *StreamOnDirectoryTableModel { + s.CopyGrants = value + return s +} + +func (s *StreamOnDirectoryTableModel) WithDatabaseValue(value tfconfig.Variable) *StreamOnDirectoryTableModel { + s.Database = value + return s +} + +func (s *StreamOnDirectoryTableModel) WithFullyQualifiedNameValue(value tfconfig.Variable) *StreamOnDirectoryTableModel { + s.FullyQualifiedName = value + return s +} + +func (s *StreamOnDirectoryTableModel) WithNameValue(value tfconfig.Variable) *StreamOnDirectoryTableModel { + s.Name = value + return s +} + +func (s *StreamOnDirectoryTableModel) WithSchemaValue(value tfconfig.Variable) *StreamOnDirectoryTableModel { + s.Schema = value + return s +} + +func (s *StreamOnDirectoryTableModel) WithStageValue(value tfconfig.Variable) *StreamOnDirectoryTableModel { + s.Stage = value + return s +} diff --git a/pkg/acceptance/bettertestspoc/config/model/view_model_ext.go b/pkg/acceptance/bettertestspoc/config/model/view_model_ext.go index f37cde3e33..db546a67f6 100644 --- a/pkg/acceptance/bettertestspoc/config/model/view_model_ext.go +++ b/pkg/acceptance/bettertestspoc/config/model/view_model_ext.go @@ -1,6 +1,6 @@ package model -func (v *ViewModel) WithDependsOn(values []string) *ViewModel { - v.SetDependsOn(values) +func (v *ViewModel) WithDependsOn(values ...string) *ViewModel { + v.SetDependsOn(values...) return v } diff --git a/pkg/acceptance/check_destroy.go b/pkg/acceptance/check_destroy.go index 74e4195b74..85985f0188 100644 --- a/pkg/acceptance/check_destroy.go +++ b/pkg/acceptance/check_destroy.go @@ -225,6 +225,9 @@ var showByIdFunctions = map[resources.Resource]showByIdFunc{ resources.Stream: func(ctx context.Context, client *sdk.Client, id sdk.ObjectIdentifier) error { return runShowById(ctx, id, client.Streams.ShowByID) }, + resources.StreamOnDirectoryTable: func(ctx context.Context, client *sdk.Client, id sdk.ObjectIdentifier) error { + return runShowById(ctx, id, client.Streams.ShowByID) + }, resources.StreamOnExternalTable: func(ctx context.Context, client *sdk.Client, id sdk.ObjectIdentifier) error { return runShowById(ctx, id, client.Streams.ShowByID) }, diff --git a/pkg/acceptance/helpers/database_client.go b/pkg/acceptance/helpers/database_client.go index a0ea93ebab..640dbb896f 100644 --- a/pkg/acceptance/helpers/database_client.go +++ b/pkg/acceptance/helpers/database_client.go @@ -48,6 +48,14 @@ func (c *DatabaseClient) CreateDatabaseWithOptions(t *testing.T, id sdk.AccountO return database, c.DropDatabaseFunc(t, id) } +func (c *DatabaseClient) Alter(t *testing.T, id sdk.AccountObjectIdentifier, opts *sdk.AlterDatabaseOptions) { + t.Helper() + ctx := context.Background() + + err := c.client().Alter(ctx, id, opts) + require.NoError(t, err) +} + func (c *DatabaseClient) DropDatabaseFunc(t *testing.T, id sdk.AccountObjectIdentifier) func() { t.Helper() return func() { require.NoError(t, c.DropDatabase(t, id)) } @@ -184,11 +192,3 @@ func (c *DatabaseClient) ShowAllReplicationDatabases(t *testing.T) ([]sdk.Replic return c.context.client.ReplicationFunctions.ShowReplicationDatabases(ctx, nil) } - -func (c *DatabaseClient) Alter(t *testing.T, id sdk.AccountObjectIdentifier, opts *sdk.AlterDatabaseOptions) { - t.Helper() - ctx := context.Background() - - err := c.client().Alter(ctx, id, opts) - require.NoError(t, err) -} diff --git a/pkg/acceptance/helpers/external_table_client.go b/pkg/acceptance/helpers/external_table_client.go index 8c5396f014..5374b9b5cb 100644 --- a/pkg/acceptance/helpers/external_table_client.go +++ b/pkg/acceptance/helpers/external_table_client.go @@ -42,6 +42,14 @@ func (c *ExternalTableClient) CreateWithLocation(t *testing.T, location string) return c.CreateWithRequest(t, req) } +func (c *ExternalTableClient) CreateInSchemaWithLocation(t *testing.T, location string, schemaId sdk.DatabaseObjectIdentifier) (*sdk.ExternalTable, func()) { + t.Helper() + + req := sdk.NewCreateExternalTableRequest(c.ids.RandomSchemaObjectIdentifierInSchema(schemaId), location).WithFileFormat(*sdk.NewExternalTableFileFormatRequest().WithFileFormatType(sdk.ExternalTableFileFormatTypeJSON)).WithColumns([]*sdk.ExternalTableColumnRequest{sdk.NewExternalTableColumnRequest("id", sdk.DataTypeNumber, "value:time::int")}) + + return c.CreateWithRequest(t, req) +} + func (c *ExternalTableClient) CreateWithRequest(t *testing.T, req *sdk.CreateExternalTableRequest) (*sdk.ExternalTable, func()) { t.Helper() ctx := context.Background() diff --git a/pkg/acceptance/helpers/schema_client.go b/pkg/acceptance/helpers/schema_client.go index 7c58b90560..c20f7a58fb 100644 --- a/pkg/acceptance/helpers/schema_client.go +++ b/pkg/acceptance/helpers/schema_client.go @@ -55,6 +55,14 @@ func (c *SchemaClient) CreateSchemaWithOpts(t *testing.T, id sdk.DatabaseObjectI return schema, c.DropSchemaFunc(t, id) } +func (c *SchemaClient) Alter(t *testing.T, id sdk.DatabaseObjectIdentifier, opts *sdk.AlterSchemaOptions) { + t.Helper() + ctx := context.Background() + + err := c.client().Alter(ctx, id, opts) + require.NoError(t, err) +} + func (c *SchemaClient) DropSchemaFunc(t *testing.T, id sdk.DatabaseObjectIdentifier) func() { t.Helper() ctx := context.Background() @@ -75,18 +83,14 @@ func (c *SchemaClient) UseDefaultSchema(t *testing.T) { require.NoError(t, err) } -func (c *SchemaClient) UpdateDataRetentionTime(t *testing.T, id sdk.DatabaseObjectIdentifier, days int) func() { +func (c *SchemaClient) UpdateDataRetentionTime(t *testing.T, id sdk.DatabaseObjectIdentifier, days int) { t.Helper() - ctx := context.Background() - return func() { - err := c.client().Alter(ctx, id, &sdk.AlterSchemaOptions{ - Set: &sdk.SchemaSet{ - DataRetentionTimeInDays: sdk.Int(days), - }, - }) - require.NoError(t, err) - } + c.Alter(t, id, &sdk.AlterSchemaOptions{ + Set: &sdk.SchemaSet{ + DataRetentionTimeInDays: sdk.Int(days), + }, + }) } func (c *SchemaClient) Show(t *testing.T, id sdk.DatabaseObjectIdentifier) (*sdk.Schema, error) { @@ -104,11 +108,3 @@ func (c *SchemaClient) ShowWithOptions(t *testing.T, opts *sdk.ShowSchemaOptions require.NoError(t, err) return schemas } - -func (c *SchemaClient) Alter(t *testing.T, id sdk.DatabaseObjectIdentifier, opts *sdk.AlterSchemaOptions) { - t.Helper() - ctx := context.Background() - - err := c.client().Alter(ctx, id, opts) - require.NoError(t, err) -} diff --git a/pkg/acceptance/testenvs/testing_environment_variables.go b/pkg/acceptance/testenvs/testing_environment_variables.go index 7bc1a3e082..7b6e22a50b 100644 --- a/pkg/acceptance/testenvs/testing_environment_variables.go +++ b/pkg/acceptance/testenvs/testing_environment_variables.go @@ -26,8 +26,9 @@ const ( AzureExternalSasToken env = "TEST_SF_TF_AZURE_EXTERNAL_SAS_TOKEN" // #nosec G101 GcsExternalBuckerUrl env = "TEST_SF_TF_GCS_EXTERNAL_BUCKET_URL" - SkipManagedAccountTest env = "TEST_SF_TF_SKIP_MANAGED_ACCOUNT_TEST" - SkipSamlIntegrationTest env = "TEST_SF_TF_SKIP_SAML_INTEGRATION_TEST" + EnableObjectRenamingTest env = "TEST_SF_TF_ENABLE_OBJECT_RENAMING" + SkipManagedAccountTest env = "TEST_SF_TF_SKIP_MANAGED_ACCOUNT_TEST" + SkipSamlIntegrationTest env = "TEST_SF_TF_SKIP_SAML_INTEGRATION_TEST" EnableAcceptance env = resource.EnvTfAcc EnableSweep env = "TEST_SF_TF_ENABLE_SWEEP" diff --git a/pkg/provider/provider.go b/pkg/provider/provider.go index 30cdccfe38..36fbc19943 100644 --- a/pkg/provider/provider.go +++ b/pkg/provider/provider.go @@ -484,6 +484,7 @@ func getResources() map[string]*schema.Resource { "snowflake_stage": resources.Stage(), "snowflake_storage_integration": resources.StorageIntegration(), "snowflake_stream": resources.Stream(), + "snowflake_stream_on_directory_table": resources.StreamOnDirectoryTable(), "snowflake_stream_on_external_table": resources.StreamOnExternalTable(), "snowflake_stream_on_table": resources.StreamOnTable(), "snowflake_streamlit": resources.Streamlit(), diff --git a/pkg/provider/resources/resources.go b/pkg/provider/resources/resources.go index 6730843d38..1de4d34f7f 100644 --- a/pkg/provider/resources/resources.go +++ b/pkg/provider/resources/resources.go @@ -52,6 +52,7 @@ const ( Stage resource = "snowflake_stage" StorageIntegration resource = "snowflake_storage_integration" Stream resource = "snowflake_stream" + StreamOnDirectoryTable resource = "snowflake_stream_on_directory_table" StreamOnExternalTable resource = "snowflake_stream_on_external_table" StreamOnTable resource = "snowflake_stream_on_table" Streamlit resource = "snowflake_streamlit" diff --git a/pkg/resources/custom_diffs.go b/pkg/resources/custom_diffs.go index 94485490f2..a71cd8b93e 100644 --- a/pkg/resources/custom_diffs.go +++ b/pkg/resources/custom_diffs.go @@ -221,9 +221,20 @@ func RecreateWhenSecretTypeChangedExternally(secretType sdk.SecretType) schema.C } if isRefreshTokenExpiryTimeEmpty { return errors.Join(diff.SetNew("secret_type", ""), diff.ForceNew("secret_type")) - } + } } } return nil } } + +// RecreateWhenStreamIsStale detects when the stream is stale, and sets a `false` value for `stale` field. +// This means that the provider can detect that change in `stale` from `true` to `false`, where `false` is our desired state. +func RecreateWhenStreamIsStale() schema.CustomizeDiffFunc { + return func(_ context.Context, diff *schema.ResourceDiff, _ interface{}) error { + if old, _ := diff.GetChange("stale"); old.(bool) { + return diff.SetNew("stale", false) + } + return nil + } +} diff --git a/pkg/resources/diff_suppressions.go b/pkg/resources/diff_suppressions.go index 3e23be3ee0..597529e4ec 100644 --- a/pkg/resources/diff_suppressions.go +++ b/pkg/resources/diff_suppressions.go @@ -222,6 +222,22 @@ func suppressIdentifierQuoting(_, oldValue, newValue string, _ *schema.ResourceD return slices.Equal(oldId, newId) } +func suppressIdentifierQuotingPartiallyQualifiedName(_, oldValue, newValue string, _ *schema.ResourceData) bool { + if oldValue == "" || newValue == "" { + return false + } + + oldId, err := sdk.ParseIdentifierString(oldValue) + if err != nil { + return false + } + newId, err := sdk.ParseIdentifierString(newValue) + if err != nil { + return false + } + return newId[len(newId)-1] == oldId[len(oldId)-1] +} + // IgnoreNewEmptyListOrSubfields suppresses the diff if `new` list is empty or compared subfield is ignored. Subfields can be nested. func IgnoreNewEmptyListOrSubfields(ignoredSubfields ...string) schema.SchemaDiffSuppressFunc { return func(k, old, new string, _ *schema.ResourceData) bool { diff --git a/pkg/resources/object_renaming_acceptance_test.go b/pkg/resources/object_renaming_acceptance_test.go new file mode 100644 index 0000000000..a74523e3eb --- /dev/null +++ b/pkg/resources/object_renaming_acceptance_test.go @@ -0,0 +1,1366 @@ +package resources_test + +import ( + "fmt" + "regexp" + "strconv" + "strings" + "testing" + + acc "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/acceptance" + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/acceptance/bettertestspoc/config" + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/acceptance/bettertestspoc/config/model" + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/acceptance/testenvs" + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/provider/resources" + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/sdk" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/plancheck" + "github.com/hashicorp/terraform-plugin-testing/tfversion" +) + +/* +The following tests are showing the behavior of the provider in cases where objects higher in the hierarchy +like database or schema are renamed when the objects lower in the hierarchy are in the Terraform configuration. +For more information check TODO(SNOW-1672319): link public document. + +Shallow hierarchy (database + schema) +- is in config - renamed internally - with implicit dependency (works) + +- is in config - renamed internally - without dependency - after rename schema referencing old database name (fails in Read and then it's failing to remove itself in Delete) +- is in config - renamed internally - without dependency - after rename schema referencing new database name (fails in Read and then it's failing to remove itself in Delete) + +- is in config - renamed internally - with depends_on - after rename schema referencing old database name (fails in Read and then it's failing to remove itself in Delete) +- is in config - renamed internally - with depends_on - after rename schema referencing new database name (works) + +- is in config - renamed externally - with implicit dependency - database holding the same name in config (works; creates a new database with a schema next to the already existing renamed database and schema) +- is in config - renamed externally - with implicit dependency - database holding the new name in config (fails to create database, because it already exists) + +- is in config - renamed externally - without dependency - after rename database referencing old name and schema referencing old database name (non-deterministic results depending on the Terraform execution order that seems to be different with every run) +- is in config - renamed externally - without dependency - after rename database referencing old name and schema referencing old database name - check impact of the resource order on the plan (seems to fail pretty consistently in Delete because database is dropped before schema) +- is in config - renamed externally - without dependency - after rename database referencing old name and schema referencing new database name (fails because schema resource tries to create a new schema that already exists in renamed database) +- is in config - renamed externally - without dependency - after rename database referencing new name and schema referencing old database name (fails because database resource tried to create database that already exists) +- is in config - renamed externally - without dependency - after rename database referencing new name and schema referencing new database name (fails because database resource tried to create database that already exists) + +- is in config - renamed externally - with depends_on - after rename database referencing old name and schema referencing old database name (works; creates a new database with a schema next to the already existing renamed database and schema) +- is in config - renamed externally - with depends_on - after rename database referencing old name and schema referencing new database name (fails because schema resource tries to create a new schema that already exists in renamed database) +- is in config - renamed externally - with depends_on - after rename database referencing new name and schema referencing old database name (fails because database resource tried to create database that already exists) +- is in config - renamed externally - with depends_on - after rename database referencing new name and schema referencing new database name (fails because database resource tried to create database that already exists) + +- is not in config - renamed externally - referencing old database name (fails because it tries to create a new schema on non-existing database) +- is not in config - renamed externally - referencing new database name (fails because schema resource tries to create a new schema that already exists in renamed database) + +Deep hierarchy (database + schema + table) + +- are in config - database renamed internally - with database implicit dependency - with no schema dependency - with database to schema implicit dependency (fails because table is created before schema) +- are in config - database renamed internally - with database implicit dependency - with implicit schema dependency - with database to schema implicit dependency (works) +- are in config - database renamed internally - with database implicit dependency - with schema depends_on - with database to schema implicit dependency (works) + +- are in config - database renamed internally - with database implicit dependency - with no schema dependency - with database to schema depends_on dependency (fails because table is created before schema) +- are in config - database renamed internally - with database implicit dependency - with implicit schema dependency - with database to schema depends_on dependency (works) +- are in config - database renamed internally - with database implicit dependency - with schema depends_on - with database to schema depends_on dependency (works) + +- are in config - database renamed internally - with database implicit dependency - with no schema dependency - with database to schema no dependency (fails during delete because database is deleted before schema) +- are in config - database renamed internally - with database implicit dependency - with implicit schema dependency - with database to schema no dependency (fails to drop schema after database rename) +- are in config - database renamed internally - with database implicit dependency - with schema depends_on - with database to schema no dependency (fails to drop schema after database rename) + +- are in config - database renamed internally - with no database dependency - with no schema dependency (fails because table is created before schema) +- are in config - database renamed internally - with no database dependency - with implicit schema dependency (works) +- are in config - database renamed internally - with no database dependency - with schema depends_on (works) + +- are in config - database renamed internally - with database depends_on - with no schema dependency (fails because table is created before schema) +- are in config - database renamed internally - with database depends_on - with implicit schema dependency (works) +- are in config - database renamed internally - with database depends_on - with schema depends_on (works) + +------------------------------------------------------------------------------------------------------------------------ + +- are in config - schema renamed internally - with database implicit dependency - with no schema dependency (fails because table is created before schema) +- are in config - schema renamed internally - with database implicit dependency - with implicit schema dependency (works) +- are in config - schema renamed internally - with database implicit dependency - with schema depends_on (works) + +- are in config - schema renamed internally - with no database dependency - with no schema dependency (fails because table is created before schema) +- are in config - schema renamed internally - with no database dependency - with implicit schema dependency (works) +- are in config - schema renamed internally - with no database dependency - with schema depends_on (works) + +- are in config - schema renamed internally - with database depends_on - with no schema dependency (fails because table is created before schema) +- are in config - schema renamed internally - with database depends_on - with implicit schema dependency (works) +- are in config - schema renamed internally - with database depends_on - with schema depends_on (works) + +------------------------------------------------------------------------------------------------------------------------ + +- are in config - database renamed externally - with database implicit dependency - with no schema dependency (fails because table is created before schema) +- are in config - database renamed externally - with database implicit dependency - with implicit schema dependency (fails because tries to create database when it's already there after rename) +- are in config - database renamed externally - with database implicit dependency - with schema depends_on (fails because tries to create database when it's already there after rename) + +- are in config - database renamed externally - with no database dependency - with no schema dependency (fails because table is created before schema) +- are in config - database renamed externally - with no database dependency - with implicit schema dependency (fails because tries to create database when it's already there after rename) +- are in config - database renamed externally - with no database dependency - with schema depends_on (fails because tries to create database when it's already there after rename) + +- are in config - database renamed externally - with database depends_on - with no schema dependency (fails because table is created before schema) +- are in config - database renamed externally - with database depends_on - with implicit schema dependency (fails because tries to create database when it's already there after rename) +- are in config - database renamed externally - with database depends_on - with schema depends_on (fails because tries to create database when it's already there after rename) + +------------------------------------------------------------------------------------------------------------------------ + +- are in config - schema renamed externally - with database implicit dependency - with no schema dependency (fails because table is created before schema) +- are in config - schema renamed externally - with database implicit dependency - with implicit schema dependency (fails because tries to create database when it's already there after rename) +- are in config - schema renamed externally - with database implicit dependency - with schema depends_on (fails because tries to create database when it's already there after rename) + +- are in config - schema renamed externally - with no database dependency - with no schema dependency (fails because table is created before schema) +- are in config - schema renamed externally - with no database dependency - with implicit schema dependency (fails because tries to create database when it's already there after rename) +- are in config - schema renamed externally - with no database dependency - with schema depends_on (fails because tries to create database when it's already there after rename) + +- are in config - schema renamed externally - with database depends_on - with no schema dependency (fails because table is created before schema) +- are in config - schema renamed externally - with database depends_on - with implicit schema dependency (fails because tries to create database when it's already there after rename) +- are in config - schema renamed externally - with database depends_on - with schema depends_on (fails because tries to create database when it's already there after rename) + +------------------------------------------------------------------------------------------------------------------------ + +- are not in config - database renamed externally - referencing old database name (fails because tries to create table on non-existing database) +- are not in config - database renamed externally - referencing new database name (fails because tries to create table that already exists in the renamed database) + +- are not in config - schema renamed externally - referencing old schema name (fails because tries to create table on non-existing schema) +- are not in config - schema renamed externally - referencing new schema name (fails because tries to create table that already exists in the renamed schema) + +# The list of test cases that were not added: +- (Deep hierarchy) More test cases with varying dependencies between resources +- (Deep hierarchy) Add test cases where old database is referenced to see if hierarchy recreation is possible +- (Deep hierarchy) More test cases could be added when database and schema are renamed at the same time +- (Deep hierarchy) More test cases could be added when either database or schema are in the config +*/ + +type DependencyType string + +const ( + ImplicitDependency DependencyType = "implicit" + DependsOnDependency DependencyType = "depends_on" + NoDependency DependencyType = "no_dependency" +) + +func TestAcc_ShallowHierarchy_IsInConfig_RenamedInternally_WithImplicitDependency(t *testing.T) { + _ = testenvs.GetOrSkipTest(t, testenvs.EnableObjectRenamingTest) + _ = testenvs.GetOrSkipTest(t, testenvs.EnableAcceptance) + acc.TestAccPreCheck(t) + + databaseId := acc.TestClient().Ids.RandomAccountObjectIdentifier() + newDatabaseId := acc.TestClient().Ids.RandomAccountObjectIdentifier() + schemaName := acc.TestClient().Ids.Alpha() + + databaseConfigModel := model.Database("test", databaseId.Name()) + databaseConfigModelWithNewId := model.Database("test", newDatabaseId.Name()) + + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: acc.TestAccProtoV6ProviderFactories, + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.RequireAbove(tfversion.Version1_5_0), + }, + CheckDestroy: acc.CheckDestroy(t, resources.Schema), + Steps: []resource.TestStep{ + { + Config: config.FromModel(t, databaseConfigModel) + configSchemaWithDatabaseReference(databaseConfigModel.ResourceReference(), schemaName), + }, + { + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectResourceAction("snowflake_database.test", plancheck.ResourceActionUpdate), + plancheck.ExpectResourceAction("snowflake_schema.test", plancheck.ResourceActionDestroyBeforeCreate), + }, + }, + Config: config.FromModel(t, databaseConfigModelWithNewId) + configSchemaWithDatabaseReference(databaseConfigModelWithNewId.ResourceReference(), schemaName), + }, + }, + }) +} + +func TestAcc_ShallowHierarchy_IsInConfig_RenamedInternally_WithoutDependency_AfterRenameSchemaReferencingOldDatabaseName(t *testing.T) { + // Error happens during schema's Read operation and then Delete operation (schema cannot be removed). + t.Skip("Not able to handle the error produced by Delete operation that results in test always failing") + _ = testenvs.GetOrSkipTest(t, testenvs.EnableObjectRenamingTest) + _ = testenvs.GetOrSkipTest(t, testenvs.EnableAcceptance) + acc.TestAccPreCheck(t) + + databaseId := acc.TestClient().Ids.RandomAccountObjectIdentifier() + newDatabaseId := acc.TestClient().Ids.RandomAccountObjectIdentifier() + schemaName := acc.TestClient().Ids.Alpha() + + databaseConfigModel := model.Database("test", databaseId.Name()) + databaseConfigModelWithNewId := model.Database("test", newDatabaseId.Name()) + + schemaModelConfig := model.Schema("test", databaseId.Name(), schemaName) + + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: acc.TestAccProtoV6ProviderFactories, + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.RequireAbove(tfversion.Version1_5_0), + }, + CheckDestroy: acc.CheckDestroy(t, resources.Schema), + Steps: []resource.TestStep{ + { + Config: config.FromModels(t, databaseConfigModel, schemaModelConfig), + }, + { + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectResourceAction("snowflake_database.test", plancheck.ResourceActionUpdate), + plancheck.ExpectResourceAction("snowflake_schema.test", plancheck.ResourceActionNoop), + }, + }, + Config: config.FromModels(t, databaseConfigModelWithNewId, schemaModelConfig), + ExpectError: regexp.MustCompile("does not exist or not authorized"), + }, + }, + }) +} + +func TestAcc_ShallowHierarchy_IsInConfig_RenamedInternally_WithoutDependency_AfterRenameSchemaReferencingNewDatabaseName(t *testing.T) { + // Error happens during schema's Read operation and then Delete operation (schema cannot be removed). + t.Skip("Not able to handle the error produced by Delete operation that results in test always failing") + _ = testenvs.GetOrSkipTest(t, testenvs.EnableObjectRenamingTest) + _ = testenvs.GetOrSkipTest(t, testenvs.EnableAcceptance) + acc.TestAccPreCheck(t) + + databaseId := acc.TestClient().Ids.RandomAccountObjectIdentifier() + newDatabaseId := acc.TestClient().Ids.RandomAccountObjectIdentifier() + schemaName := acc.TestClient().Ids.Alpha() + + databaseConfigModel := model.Database("test", databaseId.Name()) + databaseConfigModelWithNewId := model.Database("test", newDatabaseId.Name()) + + schemaModelConfig := model.Schema("test", databaseId.Name(), schemaName) + schemaModelConfigWithNewDatabaseId := model.Schema("test", newDatabaseId.Name(), schemaName) + + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: acc.TestAccProtoV6ProviderFactories, + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.RequireAbove(tfversion.Version1_5_0), + }, + CheckDestroy: acc.CheckDestroy(t, resources.Schema), + Steps: []resource.TestStep{ + { + Config: config.FromModels(t, databaseConfigModel, schemaModelConfig), + }, + { + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectResourceAction("snowflake_database.test", plancheck.ResourceActionUpdate), + plancheck.ExpectResourceAction("snowflake_schema.test", plancheck.ResourceActionDestroyBeforeCreate), + }, + }, + Config: config.FromModels(t, databaseConfigModelWithNewId, schemaModelConfigWithNewDatabaseId), + ExpectError: regexp.MustCompile("does not exist or not authorized"), + }, + }, + }) +} + +func TestAcc_ShallowHierarchy_IsInConfig_RenamedInternally_WithDependsOn_AfterRenameSchemaReferencingOldDatabaseName(t *testing.T) { + // Error happens during schema's Read operation and then Delete operation (schema cannot be removed). + t.Skip("Not able to handle the error produced by Delete operation that results in test always failing") + _ = testenvs.GetOrSkipTest(t, testenvs.EnableObjectRenamingTest) + _ = testenvs.GetOrSkipTest(t, testenvs.EnableAcceptance) + acc.TestAccPreCheck(t) + + databaseId := acc.TestClient().Ids.RandomAccountObjectIdentifier() + newDatabaseId := acc.TestClient().Ids.RandomAccountObjectIdentifier() + schemaName := acc.TestClient().Ids.Alpha() + + databaseConfigModel := model.Database("test", databaseId.Name()) + databaseConfigModelWithNewId := model.Database("test", newDatabaseId.Name()) + + schemaModelConfig := model.Schema("test", databaseId.Name(), schemaName) + schemaModelConfig.SetDependsOn(databaseConfigModel.ResourceReference()) + + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: acc.TestAccProtoV6ProviderFactories, + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.RequireAbove(tfversion.Version1_5_0), + }, + CheckDestroy: resource.ComposeAggregateTestCheckFunc(), + Steps: []resource.TestStep{ + { + Config: config.FromModels(t, databaseConfigModel, schemaModelConfig), + }, + { + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectResourceAction("snowflake_database.test", plancheck.ResourceActionUpdate), + plancheck.ExpectResourceAction("snowflake_schema.test", plancheck.ResourceActionNoop), + }, + }, + Config: config.FromModels(t, databaseConfigModelWithNewId, schemaModelConfig), + ExpectError: regexp.MustCompile("does not exist or not authorized"), + }, + }, + }) +} + +func TestAcc_ShallowHierarchy_IsInConfig_RenamedInternally_WithDependsOn_AfterRenameSchemaReferencingNewDatabaseName(t *testing.T) { + _ = testenvs.GetOrSkipTest(t, testenvs.EnableObjectRenamingTest) + _ = testenvs.GetOrSkipTest(t, testenvs.EnableAcceptance) + acc.TestAccPreCheck(t) + + databaseId := acc.TestClient().Ids.RandomAccountObjectIdentifier() + newDatabaseId := acc.TestClient().Ids.RandomAccountObjectIdentifier() + schemaName := acc.TestClient().Ids.Alpha() + + databaseConfigModel := model.Database("test", databaseId.Name()) + databaseConfigModelWithNewId := model.Database("test", newDatabaseId.Name()) + + schemaModelConfig := model.Schema("test", databaseId.Name(), schemaName) + schemaModelConfig.SetDependsOn(databaseConfigModel.ResourceReference()) + schemaModelConfigWithNewDatabaseId := model.Schema("test", newDatabaseId.Name(), schemaName) + schemaModelConfigWithNewDatabaseId.SetDependsOn(databaseConfigModelWithNewId.ResourceReference()) + + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: acc.TestAccProtoV6ProviderFactories, + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.RequireAbove(tfversion.Version1_5_0), + }, + CheckDestroy: acc.CheckDestroy(t, resources.Schema), + Steps: []resource.TestStep{ + { + Config: config.FromModels(t, databaseConfigModel, schemaModelConfig), + }, + { + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectResourceAction("snowflake_database.test", plancheck.ResourceActionUpdate), + plancheck.ExpectResourceAction("snowflake_schema.test", plancheck.ResourceActionDestroyBeforeCreate), + }, + }, + Config: config.FromModels(t, databaseConfigModelWithNewId, schemaModelConfigWithNewDatabaseId), + }, + }, + }) +} + +func TestAcc_ShallowHierarchy_IsInConfig_RenamedExternally_WithImplicitDependency_DatabaseHoldingTheOldNameInConfig(t *testing.T) { + _ = testenvs.GetOrSkipTest(t, testenvs.EnableObjectRenamingTest) + _ = testenvs.GetOrSkipTest(t, testenvs.EnableAcceptance) + acc.TestAccPreCheck(t) + + databaseId := acc.TestClient().Ids.RandomAccountObjectIdentifier() + newDatabaseId := acc.TestClient().Ids.RandomAccountObjectIdentifier() + schemaName := acc.TestClient().Ids.Alpha() + + databaseConfigModel := model.Database("test", databaseId.Name()) + + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: acc.TestAccProtoV6ProviderFactories, + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.RequireAbove(tfversion.Version1_5_0), + }, + CheckDestroy: acc.CheckDestroy(t, resources.Schema), + Steps: []resource.TestStep{ + { + Config: config.FromModel(t, databaseConfigModel) + configSchemaWithDatabaseReference(databaseConfigModel.ResourceReference(), schemaName), + }, + { + PreConfig: func() { + acc.TestClient().Database.Alter(t, databaseId, &sdk.AlterDatabaseOptions{ + NewName: &newDatabaseId, + }) + t.Cleanup(acc.TestClient().Database.DropDatabaseFunc(t, newDatabaseId)) + }, + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectResourceAction("snowflake_database.test", plancheck.ResourceActionCreate), + plancheck.ExpectResourceAction("snowflake_schema.test", plancheck.ResourceActionCreate), + }, + }, + Config: config.FromModel(t, databaseConfigModel) + configSchemaWithDatabaseReference(databaseConfigModel.ResourceReference(), schemaName), + }, + }, + }) +} + +func TestAcc_ShallowHierarchy_IsInConfig_RenamedExternally_WithImplicitDependency_DatabaseHoldingTheNewNameInConfig(t *testing.T) { + _ = testenvs.GetOrSkipTest(t, testenvs.EnableObjectRenamingTest) + _ = testenvs.GetOrSkipTest(t, testenvs.EnableAcceptance) + acc.TestAccPreCheck(t) + + databaseId := acc.TestClient().Ids.RandomAccountObjectIdentifier() + newDatabaseId := acc.TestClient().Ids.RandomAccountObjectIdentifier() + schemaName := acc.TestClient().Ids.Alpha() + + databaseConfigModel := model.Database("test", databaseId.Name()) + databaseConfigModelWithNewId := model.Database("test", newDatabaseId.Name()) + + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: acc.TestAccProtoV6ProviderFactories, + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.RequireAbove(tfversion.Version1_5_0), + }, + CheckDestroy: acc.CheckDestroy(t, resources.Schema), + Steps: []resource.TestStep{ + { + Config: config.FromModel(t, databaseConfigModel) + configSchemaWithDatabaseReference(databaseConfigModel.ResourceReference(), schemaName), + }, + { + PreConfig: func() { + acc.TestClient().Database.Alter(t, databaseId, &sdk.AlterDatabaseOptions{ + NewName: &newDatabaseId, + }) + t.Cleanup(acc.TestClient().Database.DropDatabaseFunc(t, newDatabaseId)) + }, + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectResourceAction("snowflake_database.test", plancheck.ResourceActionCreate), // Create is expected, because in refresh Read before apply the database is removing the unknown database from the state using d.SetId("") after failed ShowByID + plancheck.ExpectResourceAction("snowflake_schema.test", plancheck.ResourceActionCreate), // Create is expected, because in refresh Read before apply the schema is removing the unknown schema from the state using d.SetId("") after failed ShowByID + }, + }, + Config: config.FromModel(t, databaseConfigModelWithNewId) + configSchemaWithDatabaseReference(databaseConfigModelWithNewId.ResourceReference(), schemaName), + ExpectError: regexp.MustCompile(fmt.Sprintf(`Object '%s' already exists`, newDatabaseId.Name())), + }, + }, + }) +} + +func TestAcc_ShallowHierarchy_IsInConfig_RenamedExternally_WithoutDependency_AfterRenameDatabaseReferencingOldNameAndSchemaReferencingOldDatabaseName(t *testing.T) { + t.Skip("Test results are inconsistent because Terraform execution order is non-deterministic") + _ = testenvs.GetOrSkipTest(t, testenvs.EnableObjectRenamingTest) + _ = testenvs.GetOrSkipTest(t, testenvs.EnableAcceptance) + acc.TestAccPreCheck(t) + + databaseId := acc.TestClient().Ids.RandomAccountObjectIdentifier() + schemaName := acc.TestClient().Ids.Alpha() + + databaseConfigModel := model.Database("test", databaseId.Name()) + schemaModelConfig := model.Schema("test", databaseId.Name(), schemaName) + + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: acc.TestAccProtoV6ProviderFactories, + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.RequireAbove(tfversion.Version1_5_0), + }, + CheckDestroy: acc.CheckDestroy(t, resources.Schema), + Steps: []resource.TestStep{ + { + Config: config.FromModels(t, databaseConfigModel, schemaModelConfig), + }, + { + // This step has inconsistent results, and it depends on the Terraform execution order which seems to be non-deterministic in this case + PreConfig: func() { + newDatabaseId := acc.TestClient().Ids.RandomAccountObjectIdentifier() + acc.TestClient().Database.Alter(t, databaseId, &sdk.AlterDatabaseOptions{NewName: &newDatabaseId}) + t.Cleanup(acc.TestClient().Database.DropDatabaseFunc(t, newDatabaseId)) + }, + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectResourceAction("snowflake_database.test", plancheck.ResourceActionCreate), + plancheck.ExpectResourceAction("snowflake_schema.test", plancheck.ResourceActionCreate), + }, + }, + Config: config.FromModels(t, databaseConfigModel, schemaModelConfig), + // ExpectError: regexp.MustCompile("does not exist or not authorized"), + }, + }, + }) +} + +// This test checks if the order of the configuration resources has any impact on the order of resource execution (it seems to have no impact). +func TestAcc_ShallowHierarchy_IsInConfig_RenamedExternally_WithoutDependency_AfterRenameDatabaseReferencingOldNameAndSchemaReferencingOldDatabaseName_ConfigOrderSwap(t *testing.T) { + t.Skip("Test results are inconsistent because Terraform execution order is non-deterministic") + // Although the above applies, it seems to be consistently failing on delete operation after the test (because the database is dropped before schema). + _ = testenvs.GetOrSkipTest(t, testenvs.EnableObjectRenamingTest) + _ = testenvs.GetOrSkipTest(t, testenvs.EnableAcceptance) + acc.TestAccPreCheck(t) + + databaseId := acc.TestClient().Ids.RandomAccountObjectIdentifier() + schemaName := acc.TestClient().Ids.Alpha() + + databaseConfigModel := model.Database("test", databaseId.Name()) + schemaModelConfig := model.Schema("test", databaseId.Name(), schemaName) + + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: acc.TestAccProtoV6ProviderFactories, + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.RequireAbove(tfversion.Version1_5_0), + }, + CheckDestroy: acc.CheckDestroy(t, resources.Schema), + Steps: []resource.TestStep{ + { + Config: config.FromModels(t, schemaModelConfig, databaseConfigModel), + }, + { + PreConfig: func() { + newDatabaseId := acc.TestClient().Ids.RandomAccountObjectIdentifier() + acc.TestClient().Database.Alter(t, databaseId, &sdk.AlterDatabaseOptions{NewName: &newDatabaseId}) + t.Cleanup(acc.TestClient().Database.DropDatabaseFunc(t, newDatabaseId)) + }, + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectResourceAction("snowflake_database.test", plancheck.ResourceActionCreate), + plancheck.ExpectResourceAction("snowflake_schema.test", plancheck.ResourceActionCreate), + }, + }, + Config: config.FromModels(t, schemaModelConfig, databaseConfigModel), + ExpectError: regexp.MustCompile("does not exist or not authorized"), + }, + }, + }) +} + +func TestAcc_ShallowHierarchy_IsInConfig_RenamedExternally_WithoutDependency_AfterRenameDatabaseReferencingOldNameAndSchemaReferencingNewDatabaseName(t *testing.T) { + _ = testenvs.GetOrSkipTest(t, testenvs.EnableObjectRenamingTest) + _ = testenvs.GetOrSkipTest(t, testenvs.EnableAcceptance) + acc.TestAccPreCheck(t) + + databaseId := acc.TestClient().Ids.RandomAccountObjectIdentifier() + newDatabaseId := acc.TestClient().Ids.RandomAccountObjectIdentifier() + schemaName := acc.TestClient().Ids.Alpha() + + databaseConfigModel := model.Database("test", databaseId.Name()) + schemaModelConfig := model.Schema("test", databaseId.Name(), schemaName) + schemaModelConfigWithNewDatabaseId := model.Schema("test", newDatabaseId.Name(), schemaName) + + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: acc.TestAccProtoV6ProviderFactories, + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.RequireAbove(tfversion.Version1_5_0), + }, + CheckDestroy: acc.CheckDestroy(t, resources.Schema), + Steps: []resource.TestStep{ + { + Config: config.FromModels(t, databaseConfigModel, schemaModelConfig), + }, + { + PreConfig: func() { + acc.TestClient().Database.Alter(t, databaseId, &sdk.AlterDatabaseOptions{NewName: &newDatabaseId}) + t.Cleanup(acc.TestClient().Database.DropDatabaseFunc(t, newDatabaseId)) + }, + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectResourceAction("snowflake_database.test", plancheck.ResourceActionCreate), + plancheck.ExpectResourceAction("snowflake_schema.test", plancheck.ResourceActionCreate), + }, + }, + Config: config.FromModels(t, databaseConfigModel, schemaModelConfigWithNewDatabaseId), + ExpectError: regexp.MustCompile("Failed to create schema"), // already exists (because we try to create a schema on the renamed database that already has the schema that was previously created by terraform and wasn't removed) + }, + }, + }) +} + +func TestAcc_ShallowHierarchy_IsInConfig_RenamedExternally_WithoutDependency_AfterRenameDatabaseReferencingNewNameAndSchemaReferencingOldDatabaseName(t *testing.T) { + _ = testenvs.GetOrSkipTest(t, testenvs.EnableObjectRenamingTest) + _ = testenvs.GetOrSkipTest(t, testenvs.EnableAcceptance) + acc.TestAccPreCheck(t) + + databaseId := acc.TestClient().Ids.RandomAccountObjectIdentifier() + newDatabaseId := acc.TestClient().Ids.RandomAccountObjectIdentifier() + schemaName := acc.TestClient().Ids.Alpha() + + databaseConfigModel := model.Database("test", databaseId.Name()) + databaseConfigModelWithNewId := model.Database("test", newDatabaseId.Name()) + schemaModelConfig := model.Schema("test", databaseId.Name(), schemaName) + + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: acc.TestAccProtoV6ProviderFactories, + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.RequireAbove(tfversion.Version1_5_0), + }, + CheckDestroy: acc.CheckDestroy(t, resources.Schema), + Steps: []resource.TestStep{ + { + Config: config.FromModels(t, databaseConfigModel, schemaModelConfig), + }, + { + PreConfig: func() { + acc.TestClient().Database.Alter(t, databaseId, &sdk.AlterDatabaseOptions{NewName: &newDatabaseId}) + t.Cleanup(acc.TestClient().Database.DropDatabaseFunc(t, newDatabaseId)) + }, + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectResourceAction("snowflake_database.test", plancheck.ResourceActionCreate), + plancheck.ExpectResourceAction("snowflake_schema.test", plancheck.ResourceActionCreate), + }, + }, + Config: config.FromModels(t, databaseConfigModelWithNewId, schemaModelConfig), + ExpectError: regexp.MustCompile(fmt.Sprintf(`Object '%s' already exists`, newDatabaseId.Name())), + }, + }, + }) +} + +func TestAcc_ShallowHierarchy_IsInConfig_RenamedExternally_WithoutDependency_AfterRenameDatabaseReferencingNewNameAndSchemaReferencingNewDatabaseName(t *testing.T) { + _ = testenvs.GetOrSkipTest(t, testenvs.EnableObjectRenamingTest) + _ = testenvs.GetOrSkipTest(t, testenvs.EnableAcceptance) + acc.TestAccPreCheck(t) + + databaseId := acc.TestClient().Ids.RandomAccountObjectIdentifier() + newDatabaseId := acc.TestClient().Ids.RandomAccountObjectIdentifier() + schemaName := acc.TestClient().Ids.Alpha() + + databaseConfigModel := model.Database("test", databaseId.Name()) + databaseConfigModelWithNewId := model.Database("test", newDatabaseId.Name()) + schemaModelConfig := model.Schema("test", databaseId.Name(), schemaName) + schemaModelConfigWithNewDatabaseId := model.Schema("test", newDatabaseId.Name(), schemaName) + + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: acc.TestAccProtoV6ProviderFactories, + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.RequireAbove(tfversion.Version1_5_0), + }, + CheckDestroy: acc.CheckDestroy(t, resources.Schema), + Steps: []resource.TestStep{ + { + Config: config.FromModels(t, databaseConfigModel, schemaModelConfig), + }, + { + PreConfig: func() { + acc.TestClient().Database.Alter(t, databaseId, &sdk.AlterDatabaseOptions{NewName: &newDatabaseId}) + t.Cleanup(acc.TestClient().Database.DropDatabaseFunc(t, newDatabaseId)) + }, + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectResourceAction("snowflake_database.test", plancheck.ResourceActionCreate), + plancheck.ExpectResourceAction("snowflake_schema.test", plancheck.ResourceActionCreate), + }, + }, + Config: config.FromModels(t, databaseConfigModelWithNewId, schemaModelConfigWithNewDatabaseId), + ExpectError: regexp.MustCompile(fmt.Sprintf(`Object '%s' already exists`, newDatabaseId.Name())), + }, + }, + }) +} + +func TestAcc_ShallowHierarchy_IsInConfig_RenamedExternally_WithDependsOn_AfterRenameDatabaseReferencingOldNameAndSchemaReferencingOldDatabaseName(t *testing.T) { + _ = testenvs.GetOrSkipTest(t, testenvs.EnableObjectRenamingTest) + _ = testenvs.GetOrSkipTest(t, testenvs.EnableAcceptance) + acc.TestAccPreCheck(t) + + databaseId := acc.TestClient().Ids.RandomAccountObjectIdentifier() + schemaName := acc.TestClient().Ids.Alpha() + + databaseConfigModel := model.Database("test", databaseId.Name()) + schemaModelConfig := model.Schema("test", databaseId.Name(), schemaName) + schemaModelConfig.SetDependsOn(databaseConfigModel.ResourceReference()) + + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: acc.TestAccProtoV6ProviderFactories, + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.RequireAbove(tfversion.Version1_5_0), + }, + CheckDestroy: acc.CheckDestroy(t, resources.Schema), + Steps: []resource.TestStep{ + { + Config: config.FromModels(t, databaseConfigModel, schemaModelConfig), + }, + { + PreConfig: func() { + newDatabaseId := acc.TestClient().Ids.RandomAccountObjectIdentifier() + acc.TestClient().Database.Alter(t, databaseId, &sdk.AlterDatabaseOptions{NewName: &newDatabaseId}) + t.Cleanup(acc.TestClient().Database.DropDatabaseFunc(t, newDatabaseId)) + }, + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectResourceAction("snowflake_database.test", plancheck.ResourceActionCreate), + plancheck.ExpectResourceAction("snowflake_schema.test", plancheck.ResourceActionCreate), + }, + }, + Config: config.FromModels(t, databaseConfigModel, schemaModelConfig), + }, + }, + }) +} + +func TestAcc_ShallowHierarchy_IsInConfig_RenamedExternally_WithDependsOn_AfterRenameDatabaseReferencingOldNameAndSchemaReferencingNewDatabaseName(t *testing.T) { + _ = testenvs.GetOrSkipTest(t, testenvs.EnableObjectRenamingTest) + _ = testenvs.GetOrSkipTest(t, testenvs.EnableAcceptance) + acc.TestAccPreCheck(t) + + databaseId := acc.TestClient().Ids.RandomAccountObjectIdentifier() + newDatabaseId := acc.TestClient().Ids.RandomAccountObjectIdentifier() + schemaName := acc.TestClient().Ids.Alpha() + + databaseConfigModel := model.Database("test", databaseId.Name()) + schemaModelConfig := model.Schema("test", databaseId.Name(), schemaName) + schemaModelConfig.SetDependsOn(databaseConfigModel.ResourceReference()) + schemaModelConfigWithNewDatabaseId := model.Schema("test", newDatabaseId.Name(), schemaName) + schemaModelConfigWithNewDatabaseId.SetDependsOn(databaseConfigModel.ResourceReference()) + + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: acc.TestAccProtoV6ProviderFactories, + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.RequireAbove(tfversion.Version1_5_0), + }, + CheckDestroy: acc.CheckDestroy(t, resources.Schema), + Steps: []resource.TestStep{ + { + Config: config.FromModels(t, databaseConfigModel, schemaModelConfig), + }, + { + PreConfig: func() { + acc.TestClient().Database.Alter(t, databaseId, &sdk.AlterDatabaseOptions{NewName: &newDatabaseId}) + t.Cleanup(acc.TestClient().Database.DropDatabaseFunc(t, newDatabaseId)) + }, + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectResourceAction("snowflake_database.test", plancheck.ResourceActionCreate), + plancheck.ExpectResourceAction("snowflake_schema.test", plancheck.ResourceActionCreate), + }, + }, + Config: config.FromModels(t, databaseConfigModel, schemaModelConfigWithNewDatabaseId), + ExpectError: regexp.MustCompile("Failed to create schema"), // already exists + }, + }, + }) +} + +func TestAcc_ShallowHierarchy_IsInConfig_RenamedExternally_WithDependsOn_AfterRenameDatabaseReferencingNewNameAndSchemaReferencingOldDatabaseName(t *testing.T) { + _ = testenvs.GetOrSkipTest(t, testenvs.EnableObjectRenamingTest) + _ = testenvs.GetOrSkipTest(t, testenvs.EnableAcceptance) + acc.TestAccPreCheck(t) + + databaseId := acc.TestClient().Ids.RandomAccountObjectIdentifier() + newDatabaseId := acc.TestClient().Ids.RandomAccountObjectIdentifier() + schemaName := acc.TestClient().Ids.Alpha() + + databaseConfigModel := model.Database("test", databaseId.Name()) + databaseConfigModelWithNewId := model.Database("test", newDatabaseId.Name()) + schemaModelConfig := model.Schema("test", databaseId.Name(), schemaName) + schemaModelConfig.SetDependsOn(databaseConfigModel.ResourceReference()) + + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: acc.TestAccProtoV6ProviderFactories, + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.RequireAbove(tfversion.Version1_5_0), + }, + CheckDestroy: acc.CheckDestroy(t, resources.Schema), + Steps: []resource.TestStep{ + { + Config: config.FromModels(t, databaseConfigModel, schemaModelConfig), + }, + { + PreConfig: func() { + acc.TestClient().Database.Alter(t, databaseId, &sdk.AlterDatabaseOptions{NewName: &newDatabaseId}) + t.Cleanup(acc.TestClient().Database.DropDatabaseFunc(t, newDatabaseId)) + }, + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectResourceAction("snowflake_database.test", plancheck.ResourceActionCreate), + plancheck.ExpectResourceAction("snowflake_schema.test", plancheck.ResourceActionCreate), + }, + }, + Config: config.FromModels(t, databaseConfigModelWithNewId, schemaModelConfig), + ExpectError: regexp.MustCompile(fmt.Sprintf(`Object '%s' already exists`, newDatabaseId.Name())), + }, + }, + }) +} + +func TestAcc_ShallowHierarchy_IsInConfig_RenamedExternally_WithDependsOn_AfterRenameDatabaseReferencingNewNameAndSchemaReferencingNewDatabaseName(t *testing.T) { + _ = testenvs.GetOrSkipTest(t, testenvs.EnableObjectRenamingTest) + _ = testenvs.GetOrSkipTest(t, testenvs.EnableAcceptance) + acc.TestAccPreCheck(t) + + databaseId := acc.TestClient().Ids.RandomAccountObjectIdentifier() + newDatabaseId := acc.TestClient().Ids.RandomAccountObjectIdentifier() + schemaName := acc.TestClient().Ids.Alpha() + + databaseConfigModel := model.Database("test", databaseId.Name()) + databaseConfigModelWithNewId := model.Database("test", newDatabaseId.Name()) + schemaModelConfig := model.Schema("test", databaseId.Name(), schemaName) + schemaModelConfig.SetDependsOn(databaseConfigModel.ResourceReference()) + schemaModelConfigWithNewDatabaseId := model.Schema("test", newDatabaseId.Name(), schemaName) + schemaModelConfigWithNewDatabaseId.SetDependsOn(databaseConfigModelWithNewId.ResourceReference()) + + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: acc.TestAccProtoV6ProviderFactories, + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.RequireAbove(tfversion.Version1_5_0), + }, + CheckDestroy: acc.CheckDestroy(t, resources.Schema), + Steps: []resource.TestStep{ + { + Config: config.FromModels(t, databaseConfigModel, schemaModelConfig), + }, + { + PreConfig: func() { + acc.TestClient().Database.Alter(t, databaseId, &sdk.AlterDatabaseOptions{NewName: &newDatabaseId}) + t.Cleanup(acc.TestClient().Database.DropDatabaseFunc(t, newDatabaseId)) + }, + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectResourceAction("snowflake_database.test", plancheck.ResourceActionCreate), + plancheck.ExpectResourceAction("snowflake_schema.test", plancheck.ResourceActionCreate), + }, + }, + Config: config.FromModels(t, databaseConfigModelWithNewId, schemaModelConfigWithNewDatabaseId), + ExpectError: regexp.MustCompile(fmt.Sprintf(`Object '%s' already exists`, newDatabaseId.Name())), + }, + }, + }) +} + +func TestAcc_ShallowHierarchy_IsNotInConfig_RenamedExternally_ReferencingOldName(t *testing.T) { + _ = testenvs.GetOrSkipTest(t, testenvs.EnableObjectRenamingTest) + _ = testenvs.GetOrSkipTest(t, testenvs.EnableAcceptance) + acc.TestAccPreCheck(t) + + databaseId := acc.TestClient().Ids.RandomAccountObjectIdentifier() + newDatabaseId := acc.TestClient().Ids.RandomAccountObjectIdentifier() + schemaName := acc.TestClient().Ids.Alpha() + schemaModelConfig := model.Schema("test", databaseId.Name(), schemaName) + + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: acc.TestAccProtoV6ProviderFactories, + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.RequireAbove(tfversion.Version1_5_0), + }, + CheckDestroy: acc.CheckDestroy(t, resources.Schema), + Steps: []resource.TestStep{ + { + PreConfig: func() { + _, databaseCleanup := acc.TestClient().Database.CreateDatabaseWithIdentifier(t, databaseId) + t.Cleanup(databaseCleanup) + }, + Config: config.FromModel(t, schemaModelConfig), + }, + { + PreConfig: func() { + acc.TestClient().Database.Alter(t, databaseId, &sdk.AlterDatabaseOptions{NewName: &newDatabaseId}) + t.Cleanup(acc.TestClient().Database.DropDatabaseFunc(t, newDatabaseId)) + }, + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectResourceAction("snowflake_schema.test", plancheck.ResourceActionCreate), + }, + }, + Config: config.FromModel(t, schemaModelConfig), + ExpectError: regexp.MustCompile("object does not exist or not authorized"), + }, + }, + }) +} + +func TestAcc_ShallowHierarchy_IsNotInConfig_RenamedExternally_ReferencingNewName(t *testing.T) { + _ = testenvs.GetOrSkipTest(t, testenvs.EnableObjectRenamingTest) + _ = testenvs.GetOrSkipTest(t, testenvs.EnableAcceptance) + acc.TestAccPreCheck(t) + + databaseId := acc.TestClient().Ids.RandomAccountObjectIdentifier() + newDatabaseId := acc.TestClient().Ids.RandomAccountObjectIdentifier() + schemaName := acc.TestClient().Ids.Alpha() + schemaModelConfig := model.Schema("test", databaseId.Name(), schemaName) + schemaModelConfigWithNewDatabaseId := model.Schema("test", newDatabaseId.Name(), schemaName) + + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: acc.TestAccProtoV6ProviderFactories, + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.RequireAbove(tfversion.Version1_5_0), + }, + CheckDestroy: acc.CheckDestroy(t, resources.Schema), + Steps: []resource.TestStep{ + { + PreConfig: func() { + _, databaseCleanup := acc.TestClient().Database.CreateDatabaseWithIdentifier(t, databaseId) + t.Cleanup(databaseCleanup) + }, + Config: config.FromModel(t, schemaModelConfig), + }, + { + PreConfig: func() { + acc.TestClient().Database.Alter(t, databaseId, &sdk.AlterDatabaseOptions{NewName: &newDatabaseId}) + t.Cleanup(acc.TestClient().Database.DropDatabaseFunc(t, newDatabaseId)) + }, + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectResourceAction("snowflake_schema.test", plancheck.ResourceActionCreate), + }, + }, + Config: config.FromModel(t, schemaModelConfigWithNewDatabaseId), + ExpectError: regexp.MustCompile("Failed to create schema"), // already exists + }, + }, + }) +} + +func configSchemaWithDatabaseReference(databaseReference string, schemaName string) string { + return fmt.Sprintf(` +resource "snowflake_schema" "test" { + database = %[1]s.name + name = "%[2]s" +} +`, databaseReference, schemaName) +} + +func TestAcc_DeepHierarchy_AreInConfig_DatabaseRenamedInternally(t *testing.T) { + _ = testenvs.GetOrSkipTest(t, testenvs.EnableObjectRenamingTest) + _ = testenvs.GetOrSkipTest(t, testenvs.EnableAcceptance) + acc.TestAccPreCheck(t) + + testCases := []struct { + DatabaseDependency DependencyType + SchemaDependency DependencyType + DatabaseInSchemaDependency DependencyType + ExpectedFirstStepError *regexp.Regexp + }{ + {DatabaseDependency: ImplicitDependency, SchemaDependency: NoDependency, DatabaseInSchemaDependency: ImplicitDependency, ExpectedFirstStepError: regexp.MustCompile("error creating table")}, // tries to create table before schema + {DatabaseDependency: ImplicitDependency, SchemaDependency: ImplicitDependency, DatabaseInSchemaDependency: ImplicitDependency}, + {DatabaseDependency: ImplicitDependency, SchemaDependency: DependsOnDependency, DatabaseInSchemaDependency: ImplicitDependency}, + + {DatabaseDependency: ImplicitDependency, SchemaDependency: NoDependency, DatabaseInSchemaDependency: DependsOnDependency, ExpectedFirstStepError: regexp.MustCompile("error creating table")}, // tries to create table before schema + {DatabaseDependency: ImplicitDependency, SchemaDependency: ImplicitDependency, DatabaseInSchemaDependency: DependsOnDependency}, + {DatabaseDependency: ImplicitDependency, SchemaDependency: DependsOnDependency, DatabaseInSchemaDependency: DependsOnDependency}, + + // {DatabaseDependency: ImplicitDependency, SchemaDependency: NoDependency, DatabaseInSchemaDependency: NoDependency}, // fails after incorrect execution order (tries to drop schema after database was dropped); cannot assert + // {DatabaseDependency: ImplicitDependency, SchemaDependency: ImplicitDependency, DatabaseInSchemaDependency: NoDependency}, // tries to drop schema after database name was changed; cannot assert + // {DatabaseDependency: ImplicitDependency, SchemaDependency: DependsOnDependency, DatabaseInSchemaDependency: NoDependency}, // tries to drop schema after database name was changed; cannot assert + + {DatabaseDependency: DependsOnDependency, SchemaDependency: NoDependency, DatabaseInSchemaDependency: ImplicitDependency, ExpectedFirstStepError: regexp.MustCompile("error creating table")}, // tries to create table before schema + {DatabaseDependency: DependsOnDependency, SchemaDependency: ImplicitDependency, DatabaseInSchemaDependency: ImplicitDependency}, + {DatabaseDependency: DependsOnDependency, SchemaDependency: DependsOnDependency, DatabaseInSchemaDependency: ImplicitDependency}, + + {DatabaseDependency: NoDependency, SchemaDependency: NoDependency, DatabaseInSchemaDependency: ImplicitDependency, ExpectedFirstStepError: regexp.MustCompile("error creating table")}, // tries to create table before schema + {DatabaseDependency: NoDependency, SchemaDependency: ImplicitDependency, DatabaseInSchemaDependency: ImplicitDependency}, + {DatabaseDependency: NoDependency, SchemaDependency: DependsOnDependency, DatabaseInSchemaDependency: ImplicitDependency}, + } + + for _, testCase := range testCases { + t.Run(fmt.Sprintf("TestAcc_ database dependency: %s, schema dependency: %s, database in schema dependency: %s", testCase.DatabaseDependency, testCase.SchemaDependency, testCase.DatabaseInSchemaDependency), func(t *testing.T) { + databaseId := acc.TestClient().Ids.RandomAccountObjectIdentifier() + newDatabaseId := acc.TestClient().Ids.RandomAccountObjectIdentifier() + schemaName := acc.TestClient().Ids.Alpha() + tableName := acc.TestClient().Ids.Alpha() + + databaseConfigModel := model.Database("test", databaseId.Name()) + databaseConfigModelWithNewId := model.Database("test", newDatabaseId.Name()) + + testSteps := []resource.TestStep{ + { + Config: config.FromModel(t, databaseConfigModel) + + configSchemaWithReferences(t, databaseConfigModel.ResourceReference(), testCase.DatabaseInSchemaDependency, databaseId.Name(), schemaName) + + configTableWithReferences(t, databaseConfigModel.ResourceReference(), testCase.DatabaseDependency, "snowflake_schema.test", testCase.SchemaDependency, databaseId.Name(), schemaName, tableName), + ExpectError: testCase.ExpectedFirstStepError, + }, + } + + if testCase.ExpectedFirstStepError == nil { + testSteps = append(testSteps, + resource.TestStep{ + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectResourceAction("snowflake_database.test", plancheck.ResourceActionUpdate), + plancheck.ExpectResourceAction("snowflake_schema.test", plancheck.ResourceActionDestroyBeforeCreate), + plancheck.ExpectResourceAction("snowflake_table.test", plancheck.ResourceActionDestroyBeforeCreate), + }, + }, + Config: config.FromModel(t, databaseConfigModelWithNewId) + + configSchemaWithReferences(t, databaseConfigModelWithNewId.ResourceReference(), testCase.DatabaseInSchemaDependency, newDatabaseId.Name(), schemaName) + + configTableWithReferences(t, databaseConfigModelWithNewId.ResourceReference(), testCase.DatabaseDependency, "snowflake_schema.test", testCase.SchemaDependency, newDatabaseId.Name(), schemaName, tableName), + }, + ) + } + + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: acc.TestAccProtoV6ProviderFactories, + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.RequireAbove(tfversion.Version1_5_0), + }, + CheckDestroy: acc.CheckDestroy(t, resources.Table), + Steps: testSteps, + }) + }) + } +} + +func TestAcc_DeepHierarchy_AreInConfig_SchemaRenamedInternally(t *testing.T) { + _ = testenvs.GetOrSkipTest(t, testenvs.EnableObjectRenamingTest) + _ = testenvs.GetOrSkipTest(t, testenvs.EnableAcceptance) + acc.TestAccPreCheck(t) + + testCases := []struct { + DatabaseDependency DependencyType + SchemaDependency DependencyType + ExpectedFirstStepError *regexp.Regexp + }{ + {DatabaseDependency: ImplicitDependency, SchemaDependency: NoDependency, ExpectedFirstStepError: regexp.MustCompile("error creating table")}, // tries to create table before schema + {DatabaseDependency: ImplicitDependency, SchemaDependency: ImplicitDependency}, + {DatabaseDependency: ImplicitDependency, SchemaDependency: DependsOnDependency}, + + {DatabaseDependency: DependsOnDependency, SchemaDependency: NoDependency, ExpectedFirstStepError: regexp.MustCompile("error creating table")}, // tries to create table before schema + {DatabaseDependency: DependsOnDependency, SchemaDependency: ImplicitDependency}, + {DatabaseDependency: DependsOnDependency, SchemaDependency: DependsOnDependency}, + + {DatabaseDependency: NoDependency, SchemaDependency: NoDependency, ExpectedFirstStepError: regexp.MustCompile("error creating table")}, // tries to create table before schema + {DatabaseDependency: NoDependency, SchemaDependency: ImplicitDependency}, + {DatabaseDependency: NoDependency, SchemaDependency: DependsOnDependency}, + } + + for _, testCase := range testCases { + t.Run(fmt.Sprintf("TestAcc_ database dependency: %s, schema dependency: %s", testCase.DatabaseDependency, testCase.SchemaDependency), func(t *testing.T) { + databaseId := acc.TestClient().Ids.RandomAccountObjectIdentifier() + schemaName := acc.TestClient().Ids.Alpha() + newSchemaName := acc.TestClient().Ids.Alpha() + tableName := acc.TestClient().Ids.Alpha() + + databaseConfigModel := model.Database("test", databaseId.Name()) + + testSteps := []resource.TestStep{ + { + Config: config.FromModel(t, databaseConfigModel) + + configSchemaWithReferences(t, databaseConfigModel.ResourceReference(), ImplicitDependency, databaseId.Name(), schemaName) + + configTableWithReferences(t, databaseConfigModel.ResourceReference(), testCase.DatabaseDependency, "snowflake_schema.test", testCase.SchemaDependency, databaseId.Name(), schemaName, tableName), + ExpectError: testCase.ExpectedFirstStepError, + }, + } + + if testCase.ExpectedFirstStepError == nil { + testSteps = append(testSteps, + resource.TestStep{ + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectResourceAction("snowflake_database.test", plancheck.ResourceActionNoop), + plancheck.ExpectResourceAction("snowflake_schema.test", plancheck.ResourceActionUpdate), + plancheck.ExpectResourceAction("snowflake_table.test", plancheck.ResourceActionDestroyBeforeCreate), + }, + }, + Config: config.FromModel(t, databaseConfigModel) + + configSchemaWithReferences(t, databaseConfigModel.ResourceReference(), ImplicitDependency, databaseId.Name(), newSchemaName) + + configTableWithReferences(t, databaseConfigModel.ResourceReference(), testCase.DatabaseDependency, "snowflake_schema.test", testCase.SchemaDependency, databaseId.Name(), newSchemaName, tableName), + }, + ) + } + + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: acc.TestAccProtoV6ProviderFactories, + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.RequireAbove(tfversion.Version1_5_0), + }, + CheckDestroy: acc.CheckDestroy(t, resources.Table), + Steps: testSteps, + }) + }) + } +} + +func TestAcc_DeepHierarchy_AreInConfig_DatabaseRenamedExternally(t *testing.T) { + _ = testenvs.GetOrSkipTest(t, testenvs.EnableObjectRenamingTest) + _ = testenvs.GetOrSkipTest(t, testenvs.EnableAcceptance) + acc.TestAccPreCheck(t) + + testCases := []struct { + DatabaseDependency DependencyType + SchemaDependency DependencyType + DatabaseInSchemaDependency DependencyType + ExpectedFirstStepError *regexp.Regexp + ExpectedSecondStepError *regexp.Regexp + }{ + {DatabaseDependency: ImplicitDependency, SchemaDependency: NoDependency, DatabaseInSchemaDependency: ImplicitDependency, ExpectedFirstStepError: regexp.MustCompile("error creating table")}, // tries to create table before schema + {DatabaseDependency: ImplicitDependency, SchemaDependency: ImplicitDependency, DatabaseInSchemaDependency: ImplicitDependency, ExpectedSecondStepError: regexp.MustCompile("already exists")}, // tries to create a database when it's already there + {DatabaseDependency: ImplicitDependency, SchemaDependency: DependsOnDependency, DatabaseInSchemaDependency: ImplicitDependency, ExpectedSecondStepError: regexp.MustCompile("already exists")}, // tries to create a database when it's already there + + {DatabaseDependency: DependsOnDependency, SchemaDependency: NoDependency, DatabaseInSchemaDependency: ImplicitDependency, ExpectedFirstStepError: regexp.MustCompile("error creating table")}, // tries to create table before schema + {DatabaseDependency: DependsOnDependency, SchemaDependency: ImplicitDependency, DatabaseInSchemaDependency: ImplicitDependency, ExpectedSecondStepError: regexp.MustCompile("already exists")}, // tries to create a database when it's already there + {DatabaseDependency: DependsOnDependency, SchemaDependency: DependsOnDependency, DatabaseInSchemaDependency: ImplicitDependency, ExpectedSecondStepError: regexp.MustCompile("already exists")}, // tries to create a database when it's already there + + {DatabaseDependency: NoDependency, SchemaDependency: NoDependency, DatabaseInSchemaDependency: ImplicitDependency, ExpectedFirstStepError: regexp.MustCompile("error creating table")}, // tries to create table before schema + {DatabaseDependency: NoDependency, SchemaDependency: ImplicitDependency, DatabaseInSchemaDependency: ImplicitDependency, ExpectedSecondStepError: regexp.MustCompile("already exists")}, // tries to create a database when it's already there + {DatabaseDependency: NoDependency, SchemaDependency: DependsOnDependency, DatabaseInSchemaDependency: ImplicitDependency, ExpectedSecondStepError: regexp.MustCompile("already exists")}, // tries to create a database when it's already there + } + + for _, testCase := range testCases { + t.Run(fmt.Sprintf("TestAcc_ database dependency: %s, schema dependency: %s, database in schema dependency: %s", testCase.DatabaseDependency, testCase.SchemaDependency, testCase.DatabaseInSchemaDependency), func(t *testing.T) { + databaseId := acc.TestClient().Ids.RandomAccountObjectIdentifier() + newDatabaseId := acc.TestClient().Ids.RandomAccountObjectIdentifier() + schemaName := acc.TestClient().Ids.Alpha() + tableName := acc.TestClient().Ids.Alpha() + + databaseConfigModel := model.Database("test", databaseId.Name()) + databaseConfigModelWithNewId := model.Database("test", newDatabaseId.Name()) + + testSteps := []resource.TestStep{ + { + Config: config.FromModel(t, databaseConfigModel) + + configSchemaWithReferences(t, databaseConfigModel.ResourceReference(), testCase.DatabaseInSchemaDependency, databaseId.Name(), schemaName) + + configTableWithReferences(t, databaseConfigModel.ResourceReference(), testCase.DatabaseDependency, "snowflake_schema.test", testCase.SchemaDependency, databaseId.Name(), schemaName, tableName), + ExpectError: testCase.ExpectedFirstStepError, + }, + } + + if testCase.ExpectedFirstStepError == nil { + testSteps = append(testSteps, resource.TestStep{ + PreConfig: func() { + acc.TestClient().Database.Alter(t, databaseId, &sdk.AlterDatabaseOptions{ + NewName: &newDatabaseId, + }) + }, + Config: config.FromModel(t, databaseConfigModelWithNewId) + + configSchemaWithReferences(t, databaseConfigModelWithNewId.ResourceReference(), testCase.DatabaseInSchemaDependency, newDatabaseId.Name(), schemaName) + + configTableWithReferences(t, databaseConfigModelWithNewId.ResourceReference(), testCase.DatabaseDependency, "snowflake_schema.test", testCase.SchemaDependency, newDatabaseId.Name(), schemaName, tableName), + ExpectError: testCase.ExpectedSecondStepError, + }, + ) + } + + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: acc.TestAccProtoV6ProviderFactories, + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.RequireAbove(tfversion.Version1_5_0), + }, + CheckDestroy: acc.CheckDestroy(t, resources.Table), + Steps: testSteps, + }) + }) + } +} + +func TestAcc_DeepHierarchy_AreInConfig_SchemaRenamedExternally(t *testing.T) { + _ = testenvs.GetOrSkipTest(t, testenvs.EnableObjectRenamingTest) + _ = testenvs.GetOrSkipTest(t, testenvs.EnableAcceptance) + acc.TestAccPreCheck(t) + + testCases := []struct { + DatabaseDependency DependencyType + SchemaDependency DependencyType + DatabaseInSchemaDependency DependencyType + ExpectedFirstStepError *regexp.Regexp + ExpectedSecondStepError *regexp.Regexp + }{ + {DatabaseDependency: ImplicitDependency, SchemaDependency: NoDependency, DatabaseInSchemaDependency: ImplicitDependency, ExpectedFirstStepError: regexp.MustCompile("error creating table")}, // tries to create table before schema + {DatabaseDependency: ImplicitDependency, SchemaDependency: ImplicitDependency, DatabaseInSchemaDependency: ImplicitDependency, ExpectedSecondStepError: regexp.MustCompile("already exists")}, // tries to create a database when it's already there + {DatabaseDependency: ImplicitDependency, SchemaDependency: DependsOnDependency, DatabaseInSchemaDependency: ImplicitDependency, ExpectedSecondStepError: regexp.MustCompile("already exists")}, // tries to create a database when it's already there + + {DatabaseDependency: DependsOnDependency, SchemaDependency: NoDependency, DatabaseInSchemaDependency: ImplicitDependency, ExpectedFirstStepError: regexp.MustCompile("error creating table")}, // tries to create table before schema + {DatabaseDependency: DependsOnDependency, SchemaDependency: ImplicitDependency, DatabaseInSchemaDependency: ImplicitDependency, ExpectedSecondStepError: regexp.MustCompile("already exists")}, // tries to create a database when it's already there + {DatabaseDependency: DependsOnDependency, SchemaDependency: DependsOnDependency, DatabaseInSchemaDependency: ImplicitDependency, ExpectedSecondStepError: regexp.MustCompile("already exists")}, // tries to create a database when it's already there + + {DatabaseDependency: NoDependency, SchemaDependency: NoDependency, DatabaseInSchemaDependency: ImplicitDependency, ExpectedFirstStepError: regexp.MustCompile("error creating table")}, // tries to create table before schema + {DatabaseDependency: NoDependency, SchemaDependency: ImplicitDependency, DatabaseInSchemaDependency: ImplicitDependency, ExpectedSecondStepError: regexp.MustCompile("already exists")}, // tries to create a database when it's already there + {DatabaseDependency: NoDependency, SchemaDependency: DependsOnDependency, DatabaseInSchemaDependency: ImplicitDependency, ExpectedSecondStepError: regexp.MustCompile("already exists")}, // tries to create a database when it's already there + } + + for _, testCase := range testCases { + t.Run(fmt.Sprintf("TestAcc_ database dependency: %s, schema dependency: %s, database in schema dependency: %s", testCase.DatabaseDependency, testCase.SchemaDependency, testCase.DatabaseInSchemaDependency), func(t *testing.T) { + databaseId := acc.TestClient().Ids.RandomAccountObjectIdentifier() + schemaId := acc.TestClient().Ids.RandomDatabaseObjectIdentifierInDatabase(databaseId) + newSchemaId := acc.TestClient().Ids.RandomDatabaseObjectIdentifierInDatabase(databaseId) + tableName := acc.TestClient().Ids.Alpha() + + databaseConfigModel := model.Database("test", databaseId.Name()) + + testSteps := []resource.TestStep{ + { + Config: config.FromModel(t, databaseConfigModel) + + configSchemaWithReferences(t, databaseConfigModel.ResourceReference(), testCase.DatabaseInSchemaDependency, databaseId.Name(), schemaId.Name()) + + configTableWithReferences(t, databaseConfigModel.ResourceReference(), testCase.DatabaseDependency, "snowflake_schema.test", testCase.SchemaDependency, databaseId.Name(), schemaId.Name(), tableName), + ExpectError: testCase.ExpectedFirstStepError, + }, + } + + if testCase.ExpectedFirstStepError == nil { + testSteps = append(testSteps, resource.TestStep{ + PreConfig: func() { + acc.TestClient().Schema.Alter(t, schemaId, &sdk.AlterSchemaOptions{ + NewName: &newSchemaId, + }) + }, + Config: config.FromModel(t, databaseConfigModel) + + configSchemaWithReferences(t, databaseConfigModel.ResourceReference(), testCase.DatabaseInSchemaDependency, databaseId.Name(), newSchemaId.Name()) + + configTableWithReferences(t, databaseConfigModel.ResourceReference(), testCase.DatabaseDependency, "snowflake_schema.test", testCase.SchemaDependency, databaseId.Name(), newSchemaId.Name(), tableName), + ExpectError: testCase.ExpectedSecondStepError, + }, + ) + } + + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: acc.TestAccProtoV6ProviderFactories, + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.RequireAbove(tfversion.Version1_5_0), + }, + CheckDestroy: acc.CheckDestroy(t, resources.Table), + Steps: testSteps, + }) + }) + } +} + +func TestAcc_DeepHierarchy_AreNotInConfig_DatabaseRenamedExternally(t *testing.T) { + _ = testenvs.GetOrSkipTest(t, testenvs.EnableObjectRenamingTest) + _ = testenvs.GetOrSkipTest(t, testenvs.EnableAcceptance) + acc.TestAccPreCheck(t) + + testCases := []struct { + UseNewDatabaseNameAfterRename bool + ExpectedSecondStepError *regexp.Regexp + }{ + {UseNewDatabaseNameAfterRename: true, ExpectedSecondStepError: regexp.MustCompile("already exists")}, + {UseNewDatabaseNameAfterRename: false, ExpectedSecondStepError: regexp.MustCompile("object does not exist or not authorized")}, + } + + for _, testCase := range testCases { + t.Run(fmt.Sprintf("TestAcc_ use new database after rename: %t", testCase.UseNewDatabaseNameAfterRename), func(t *testing.T) { + newDatabaseId := acc.TestClient().Ids.RandomAccountObjectIdentifier() + tableName := acc.TestClient().Ids.Alpha() + + database, databaseCleanup := acc.TestClient().Database.CreateDatabase(t) + t.Cleanup(databaseCleanup) + + // not cleaning up, because the schema will be dropped with the database anyway + schema, _ := acc.TestClient().Schema.CreateSchemaInDatabase(t, database.ID()) + + var secondStepDatabaseName string + if testCase.UseNewDatabaseNameAfterRename { + secondStepDatabaseName = newDatabaseId.Name() + } else { + secondStepDatabaseName = database.ID().Name() + } + + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: acc.TestAccProtoV6ProviderFactories, + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.RequireAbove(tfversion.Version1_5_0), + }, + CheckDestroy: acc.CheckDestroy(t, resources.Table), + Steps: []resource.TestStep{ + { + Config: configTableWithReferences(t, "", NoDependency, "", NoDependency, database.ID().Name(), schema.ID().Name(), tableName), + }, + { + PreConfig: func() { + acc.TestClient().Database.Alter(t, database.ID(), &sdk.AlterDatabaseOptions{ + NewName: &newDatabaseId, + }) + t.Cleanup(acc.TestClient().Database.DropDatabaseFunc(t, newDatabaseId)) + }, + Config: configTableWithReferences(t, "", NoDependency, "", NoDependency, secondStepDatabaseName, schema.ID().Name(), tableName), + ExpectError: testCase.ExpectedSecondStepError, + }, + }, + }) + }) + } +} + +func TestAcc_DeepHierarchy_AreNotInConfig_SchemaRenamedExternally(t *testing.T) { + _ = testenvs.GetOrSkipTest(t, testenvs.EnableObjectRenamingTest) + _ = testenvs.GetOrSkipTest(t, testenvs.EnableAcceptance) + acc.TestAccPreCheck(t) + + testCases := []struct { + UseNewSchemaNameAfterRename bool + ExpectedSecondStepError *regexp.Regexp + }{ + {UseNewSchemaNameAfterRename: true, ExpectedSecondStepError: regexp.MustCompile("already exists")}, + {UseNewSchemaNameAfterRename: false, ExpectedSecondStepError: regexp.MustCompile("object does not exist or not authorized")}, + } + + for _, testCase := range testCases { + t.Run(fmt.Sprintf("TestAcc_ use new database after rename: %t", testCase.UseNewSchemaNameAfterRename), func(t *testing.T) { + database, databaseCleanup := acc.TestClient().Database.CreateDatabase(t) + t.Cleanup(databaseCleanup) + + newSchemaId := acc.TestClient().Ids.RandomDatabaseObjectIdentifierInDatabase(database.ID()) + tableName := acc.TestClient().Ids.Alpha() + + // not cleaning up, because the schema will be dropped with the database anyway + schema, _ := acc.TestClient().Schema.CreateSchemaInDatabase(t, database.ID()) + + var secondStepSchemaName string + if testCase.UseNewSchemaNameAfterRename { + secondStepSchemaName = newSchemaId.Name() + } else { + secondStepSchemaName = schema.ID().Name() + } + + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: acc.TestAccProtoV6ProviderFactories, + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.RequireAbove(tfversion.Version1_5_0), + }, + CheckDestroy: acc.CheckDestroy(t, resources.Table), + Steps: []resource.TestStep{ + { + Config: configTableWithReferences(t, "", NoDependency, "", NoDependency, database.ID().Name(), schema.ID().Name(), tableName), + }, + { + PreConfig: func() { + acc.TestClient().Schema.Alter(t, schema.ID(), &sdk.AlterSchemaOptions{ + NewName: &newSchemaId, + }) + }, + Config: configTableWithReferences(t, "", NoDependency, "", NoDependency, database.ID().Name(), secondStepSchemaName, tableName), + ExpectError: testCase.ExpectedSecondStepError, + }, + }, + }) + }) + } +} + +func configSchemaWithReferences(t *testing.T, databaseReference string, databaseDependencyType DependencyType, databaseName string, schemaName string) string { + t.Helper() + switch databaseDependencyType { + case ImplicitDependency: + return fmt.Sprintf(` +resource "snowflake_schema" "test" { + database = %[1]s.name + name = "%[2]s" +} +`, databaseReference, schemaName) + case DependsOnDependency: + return fmt.Sprintf(` +resource "snowflake_schema" "test" { + depends_on = [%[1]s] + database = "%[2]s" + name = "%[3]s" +} +`, databaseReference, databaseName, schemaName) + case NoDependency: + return fmt.Sprintf(` +resource "snowflake_schema" "test" { + database = "%[1]s" + name = "%[2]s" +} +`, databaseName, schemaName) + default: + t.Fatalf("configSchemaWithReferences: unknown database reference type: %s", databaseDependencyType) + return "" + } +} + +func configTableWithReferences(t *testing.T, databaseReference string, databaseDependencyType DependencyType, schemaReference string, schemaDependencyType DependencyType, databaseName string, schemaName string, tableName string) string { + t.Helper() + builder := new(strings.Builder) + builder.WriteString("resource \"snowflake_table\" \"test\" {\n") + + dependsOn := make([]string, 0) + database := "" + schema := "" + + switch databaseDependencyType { + case ImplicitDependency: + database = fmt.Sprintf("%s.name", databaseReference) + case DependsOnDependency: + dependsOn = append(dependsOn, databaseReference) + database = strconv.Quote(databaseName) + case NoDependency: + database = strconv.Quote(databaseName) + } + + switch schemaDependencyType { + case ImplicitDependency: + schema = fmt.Sprintf("%s.name", schemaReference) + case DependsOnDependency: + dependsOn = append(dependsOn, schemaReference) + schema = strconv.Quote(schemaName) + case NoDependency: + schema = strconv.Quote(schemaName) + } + + if len(dependsOn) > 0 { + builder.WriteString(fmt.Sprintf("depends_on = [%s]\n", strings.Join(dependsOn, ", "))) + } + builder.WriteString(fmt.Sprintf("database = %s\n", database)) + builder.WriteString(fmt.Sprintf("schema = %s\n", schema)) + builder.WriteString(fmt.Sprintf("name = \"%s\"\n", tableName)) + builder.WriteString(` +column { + type = "NUMBER(38,0)" + name = "N" +} +`) + builder.WriteString(`}`) + return builder.String() +} diff --git a/pkg/resources/resource_helpers_create.go b/pkg/resources/resource_helpers_create.go index e1d12cbc17..5ca92130bb 100644 --- a/pkg/resources/resource_helpers_create.go +++ b/pkg/resources/resource_helpers_create.go @@ -65,7 +65,7 @@ func attributeDirectValueCreate[T any](d *schema.ResourceData, key string, creat func copyGrantsAttributeCreate(d *schema.ResourceData, isOrReplace bool, orReplaceField, copyGrantsField **bool) error { if isOrReplace { *orReplaceField = sdk.Bool(true) - if d.Get("copy_grants").(bool) { + if d.GetRawConfig().AsValueMap()["copy_grants"].True() { *copyGrantsField = sdk.Bool(true) } } diff --git a/pkg/resources/schema_acceptance_test.go b/pkg/resources/schema_acceptance_test.go index e2b41dbb8b..03551b68f5 100644 --- a/pkg/resources/schema_acceptance_test.go +++ b/pkg/resources/schema_acceptance_test.go @@ -765,7 +765,9 @@ func TestAcc_Schema_DefaultDataRetentionTime_SetOutsideOfTerraform(t *testing.T) ), }, { - PreConfig: acc.TestClient().Schema.UpdateDataRetentionTime(t, id, 20), + PreConfig: func() { + acc.TestClient().Schema.UpdateDataRetentionTime(t, id, 20) + }, ConfigDirectory: acc.ConfigurationDirectory("TestAcc_Schema_DefaultDataRetentionTime/WithoutDataRetentionSet"), ConfigVariables: configVariablesWithoutSchemaDataRetentionTime(), Check: resource.ComposeTestCheckFunc( diff --git a/pkg/resources/stream_common.go b/pkg/resources/stream_common.go index 93257bfe93..b928bc743e 100644 --- a/pkg/resources/stream_common.go +++ b/pkg/resources/stream_common.go @@ -2,6 +2,7 @@ package resources import ( "context" + "errors" "fmt" "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/internal/provider" @@ -38,9 +39,13 @@ var streamCommonSchema = map[string]*schema.Schema{ Optional: true, Default: false, Description: "Retains the access permissions from the original stream when a stream is recreated using the OR REPLACE clause. That is sometimes used when the provider detects changes for fields that can not be changed by ALTER. This value will not have any effect when creating a new stream.", - DiffSuppressFunc: func(k, oldValue, newValue string, d *schema.ResourceData) bool { - return oldValue != "" && oldValue != newValue - }, + // Changing ONLY copy grants should have no effect. It is only used as an "option" during CREATE OR REPLACE - when other attributes change, it's not an object state. There is no point in recreating the object when only this field is changed. + DiffSuppressFunc: IgnoreAfterCreation, + }, + "stale": { + Type: schema.TypeBool, + Computed: true, + Description: "Indicated if the stream is stale. When Terraform detects that the stream is stale, the stream is recreated with `CREATE OR REPLACE`. Read more on stream staleness in Snowflake [docs](https://docs.snowflake.com/en/user-guide/streams-intro#data-retention-period-and-staleness).", }, "comment": { Type: schema.TypeString, @@ -196,19 +201,11 @@ func handleStreamRead(d *schema.ResourceData, stream *sdk.Stream, streamDescription *sdk.Stream, ) error { - if err := d.Set("comment", stream.Comment); err != nil { - return err - } - - if err := d.Set(ShowOutputAttributeName, []map[string]any{schemas.StreamToSchema(stream)}); err != nil { - return err - } - if err := d.Set(DescribeOutputAttributeName, []map[string]any{schemas.StreamDescriptionToSchema(*streamDescription)}); err != nil { - return err - } - if err := d.Set(FullyQualifiedNameAttributeName, id.FullyQualifiedName()); err != nil { - return err - } - - return nil + return errors.Join( + d.Set("comment", stream.Comment), + d.Set(ShowOutputAttributeName, []map[string]any{schemas.StreamToSchema(stream)}), + d.Set(DescribeOutputAttributeName, []map[string]any{schemas.StreamDescriptionToSchema(*streamDescription)}), + d.Set(FullyQualifiedNameAttributeName, id.FullyQualifiedName()), + d.Set("stale", stream.Stale), + ) } diff --git a/pkg/resources/stream_on_directory_table.go b/pkg/resources/stream_on_directory_table.go new file mode 100644 index 0000000000..f96d41ca56 --- /dev/null +++ b/pkg/resources/stream_on_directory_table.go @@ -0,0 +1,164 @@ +package resources + +import ( + "context" + "errors" + "fmt" + "log" + + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/internal/provider" + + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/helpers" + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/sdk" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/customdiff" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +var streamOnDirectoryTableSchema = func() map[string]*schema.Schema { + streamOnDirectoryTable := map[string]*schema.Schema{ + "stage": { + Type: schema.TypeString, + Required: true, + Description: blocklistedCharactersFieldDescription("Specifies an identifier for the stage the stream will monitor. Due to Snowflake limitations, the provider can not read the stage's database and schema. For stages, Snowflake returns only partially qualified name instead of fully qualified name. Please use stages located in the same schema as the stream."), + // TODO (SNOW-1733130): the returned value is not a fully qualified name + DiffSuppressFunc: SuppressIfAny(suppressIdentifierQuotingPartiallyQualifiedName, IgnoreChangeToCurrentSnowflakeValueInShow("stage")), + ValidateDiagFunc: IsValidIdentifier[sdk.SchemaObjectIdentifier](), + }, + } + return helpers.MergeMaps(streamCommonSchema, streamOnDirectoryTable) +}() + +func StreamOnDirectoryTable() *schema.Resource { + return &schema.Resource{ + CreateContext: CreateStreamOnDirectoryTable(false), + ReadContext: ReadStreamOnDirectoryTable(true), + UpdateContext: UpdateStreamOnDirectoryTable, + DeleteContext: DeleteStreamContext, + Description: "Resource used to manage streams on directory tables. For more information, check [stream documentation](https://docs.snowflake.com/en/sql-reference/sql/create-stream).", + + CustomizeDiff: customdiff.All( + ComputedIfAnyAttributeChanged(streamOnDirectoryTableSchema, ShowOutputAttributeName, "stage", "comment"), + ComputedIfAnyAttributeChanged(streamOnDirectoryTableSchema, DescribeOutputAttributeName, "stage", "comment"), + RecreateWhenStreamIsStale(), + ), + + Schema: streamOnDirectoryTableSchema, + + Importer: &schema.ResourceImporter{ + StateContext: ImportName[sdk.SchemaObjectIdentifier], + }, + } +} + +func CreateStreamOnDirectoryTable(orReplace bool) schema.CreateContextFunc { + return func(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { + client := meta.(*provider.Context).Client + databaseName := d.Get("database").(string) + schemaName := d.Get("schema").(string) + name := d.Get("name").(string) + id := sdk.NewSchemaObjectIdentifier(databaseName, schemaName, name) + + stageIdRaw := d.Get("stage").(string) + stageId, err := sdk.ParseSchemaObjectIdentifier(stageIdRaw) + if err != nil { + return diag.FromErr(err) + } + + req := sdk.NewCreateOnDirectoryTableStreamRequest(id, stageId) + + errs := errors.Join( + copyGrantsAttributeCreate(d, orReplace, &req.OrReplace, &req.CopyGrants), + stringAttributeCreate(d, "comment", &req.Comment), + ) + if errs != nil { + return diag.FromErr(errs) + } + + err = client.Streams.CreateOnDirectoryTable(ctx, req) + if err != nil { + return diag.FromErr(err) + } + d.SetId(helpers.EncodeResourceIdentifier(id)) + + return ReadStreamOnDirectoryTable(false)(ctx, d, meta) + } +} + +func ReadStreamOnDirectoryTable(withDirectoryChangesMarking bool) schema.ReadContextFunc { + return func(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { + client := meta.(*provider.Context).Client + id, err := sdk.ParseSchemaObjectIdentifier(d.Id()) + if err != nil { + return diag.FromErr(err) + } + stream, err := client.Streams.ShowByID(ctx, id) + if err != nil { + if errors.Is(err, sdk.ErrObjectNotFound) { + d.SetId("") + return diag.Diagnostics{ + diag.Diagnostic{ + Severity: diag.Warning, + Summary: "Failed to query stream. Marking the resource as removed.", + Detail: fmt.Sprintf("stream name: %s, Err: %s", id.FullyQualifiedName(), err), + }, + } + } + return diag.FromErr(err) + } + // TODO (SNOW-1733130): the returned value is not a fully qualified name + if stream.TableName == nil { + return diag.Diagnostics{ + diag.Diagnostic{ + Severity: diag.Error, + Summary: "Could not parse stage id", + Detail: fmt.Sprintf("stream name: %s", id.FullyQualifiedName()), + }, + } + } + if err := d.Set("stage", *stream.TableName); err != nil { + return diag.FromErr(err) + } + streamDescription, err := client.Streams.Describe(ctx, id) + if err != nil { + return diag.FromErr(err) + } + if err := handleStreamRead(d, id, stream, streamDescription); err != nil { + return diag.FromErr(err) + } + + return nil + } +} + +func UpdateStreamOnDirectoryTable(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { + client := meta.(*provider.Context).Client + id, err := sdk.ParseSchemaObjectIdentifier(d.Id()) + if err != nil { + return diag.FromErr(err) + } + + // change on these fields can not be ForceNew because then the object is dropped explicitly and copying grants does not have effect + // recreate when the stream is stale - see https://community.snowflake.com/s/article/using-tasks-to-avoid-stale-streams-when-incoming-data-is-empty + if keys := changedKeys(d, "stage", "stale"); len(keys) > 0 { + log.Printf("[DEBUG] Detected change on %q, recreating...", keys) + return CreateStreamOnDirectoryTable(true)(ctx, d, meta) + } + + if d.HasChange("comment") { + comment := d.Get("comment").(string) + if comment == "" { + err := client.Streams.Alter(ctx, sdk.NewAlterStreamRequest(id).WithUnsetComment(true)) + if err != nil { + return diag.FromErr(err) + } + } else { + err := client.Streams.Alter(ctx, sdk.NewAlterStreamRequest(id).WithSetComment(comment)) + if err != nil { + return diag.FromErr(err) + } + } + } + + return ReadStreamOnDirectoryTable(false)(ctx, d, meta) +} diff --git a/pkg/resources/stream_on_directory_table_acceptance_test.go b/pkg/resources/stream_on_directory_table_acceptance_test.go new file mode 100644 index 0000000000..bb82e8745c --- /dev/null +++ b/pkg/resources/stream_on_directory_table_acceptance_test.go @@ -0,0 +1,461 @@ +package resources_test + +import ( + "fmt" + "regexp" + "testing" + + acc "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/acceptance" + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/acceptance/bettertestspoc/assert" + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/acceptance/bettertestspoc/assert/resourceassert" + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/acceptance/bettertestspoc/assert/resourceshowoutputassert" + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/acceptance/bettertestspoc/config" + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/acceptance/bettertestspoc/config/model" + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/acceptance/testenvs" + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/helpers" + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/internal/snowflakeroles" + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/provider/resources" + r "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/resources" + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/sdk" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/plancheck" + "github.com/hashicorp/terraform-plugin-testing/tfversion" +) + +func TestAcc_StreamOnDirectoryTable_Basic(t *testing.T) { + _ = testenvs.GetOrSkipTest(t, testenvs.EnableAcceptance) + acc.TestAccPreCheck(t) + id := acc.TestClient().Ids.RandomSchemaObjectIdentifier() + resourceId := helpers.EncodeResourceIdentifier(id) + resourceName := "snowflake_stream_on_directory_table.test" + + stage, cleanupStage := acc.TestClient().Stage.CreateStageWithDirectory(t) + t.Cleanup(cleanupStage) + + baseModel := func() *model.StreamOnDirectoryTableModel { + return model.StreamOnDirectoryTable("test", id.DatabaseName(), id.Name(), id.SchemaName(), stage.ID().FullyQualifiedName()) + } + + modelWithExtraFields := baseModel(). + WithCopyGrants(true). + WithComment("foo") + + modelWithExtraFieldsModified := baseModel(). + WithCopyGrants(true). + WithComment("bar") + + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: acc.TestAccProtoV6ProviderFactories, + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.RequireAbove(tfversion.Version1_5_0), + }, + CheckDestroy: acc.CheckDestroy(t, resources.StreamOnDirectoryTable), + Steps: []resource.TestStep{ + // without optionals + { + Config: config.FromModel(t, baseModel()), + Check: assert.AssertThat(t, resourceassert.StreamOnDirectoryTableResource(t, resourceName). + HasNameString(id.Name()). + HasDatabaseString(id.DatabaseName()). + HasSchemaString(id.SchemaName()). + HasFullyQualifiedNameString(id.FullyQualifiedName()). + HasStageString(stage.ID().Name()), + resourceshowoutputassert.StreamShowOutput(t, resourceName). + HasCreatedOnNotEmpty(). + HasName(id.Name()). + HasDatabaseName(id.DatabaseName()). + HasSchemaName(id.SchemaName()). + HasOwner(snowflakeroles.Accountadmin.Name()). + HasTableName(stage.ID().Name()). + HasSourceType(sdk.StreamSourceTypeStage). + HasBaseTablesPartiallyQualified(stage.ID().Name()). + HasType("DELTA"). + HasStale("false"). + HasMode(sdk.StreamModeDefault). + HasStaleAfterNotEmpty(). + HasInvalidReason("N/A"). + HasOwnerRoleType("ROLE"), + assert.Check(resource.TestCheckResourceAttrSet(resourceName, "describe_output.0.created_on")), + assert.Check(resource.TestCheckResourceAttr(resourceName, "describe_output.0.name", id.Name())), + assert.Check(resource.TestCheckResourceAttr(resourceName, "describe_output.0.database_name", id.DatabaseName())), + assert.Check(resource.TestCheckResourceAttr(resourceName, "describe_output.0.schema_name", id.SchemaName())), + assert.Check(resource.TestCheckResourceAttr(resourceName, "describe_output.0.owner", snowflakeroles.Accountadmin.Name())), + assert.Check(resource.TestCheckResourceAttr(resourceName, "describe_output.0.comment", "")), + assert.Check(resource.TestCheckResourceAttr(resourceName, "describe_output.0.table_name", stage.ID().Name())), + assert.Check(resource.TestCheckResourceAttr(resourceName, "describe_output.0.source_type", string(sdk.StreamSourceTypeStage))), + assert.Check(resource.TestCheckResourceAttr(resourceName, "describe_output.0.base_tables.#", "1")), + assert.Check(resource.TestCheckResourceAttr(resourceName, "describe_output.0.base_tables.0", stage.ID().Name())), + assert.Check(resource.TestCheckResourceAttr(resourceName, "describe_output.0.type", "DELTA")), + assert.Check(resource.TestCheckResourceAttr(resourceName, "describe_output.0.stale", "false")), + assert.Check(resource.TestCheckResourceAttr(resourceName, "describe_output.0.mode", string(sdk.StreamModeDefault))), + assert.Check(resource.TestCheckResourceAttrSet(resourceName, "describe_output.0.stale_after")), + assert.Check(resource.TestCheckResourceAttr(resourceName, "describe_output.0.owner_role_type", "ROLE")), + ), + }, + // import without optionals + { + Config: config.FromModel(t, baseModel()), + ResourceName: resourceName, + ImportState: true, + ImportStateCheck: assert.AssertThatImport(t, + resourceassert.ImportedStreamOnDirectoryTableResource(t, resourceId). + HasNameString(id.Name()). + HasDatabaseString(id.DatabaseName()). + HasSchemaString(id.SchemaName()). + HasFullyQualifiedNameString(id.FullyQualifiedName()). + HasStageString(stage.ID().Name()), + ), + }, + // set all fields + { + Config: config.FromModel(t, modelWithExtraFields), + Check: assert.AssertThat(t, resourceassert.StreamOnDirectoryTableResource(t, resourceName). + HasNameString(id.Name()). + HasDatabaseString(id.DatabaseName()). + HasSchemaString(id.SchemaName()). + HasFullyQualifiedNameString(id.FullyQualifiedName()). + HasStageString(stage.ID().Name()), + resourceshowoutputassert.StreamShowOutput(t, resourceName). + HasCreatedOnNotEmpty(). + HasName(id.Name()). + HasDatabaseName(id.DatabaseName()). + HasSchemaName(id.SchemaName()). + HasOwner(snowflakeroles.Accountadmin.Name()). + HasTableName(stage.ID().Name()). + HasSourceType(sdk.StreamSourceTypeStage). + HasBaseTablesPartiallyQualified(stage.ID().Name()). + HasType("DELTA"). + HasStale("false"). + HasMode(sdk.StreamModeDefault). + HasStaleAfterNotEmpty(). + HasInvalidReason("N/A"). + HasComment("foo"). + HasOwnerRoleType("ROLE"), + assert.Check(resource.TestCheckResourceAttrSet(resourceName, "describe_output.0.created_on")), + assert.Check(resource.TestCheckResourceAttr(resourceName, "describe_output.0.name", id.Name())), + assert.Check(resource.TestCheckResourceAttr(resourceName, "describe_output.0.database_name", id.DatabaseName())), + assert.Check(resource.TestCheckResourceAttr(resourceName, "describe_output.0.schema_name", id.SchemaName())), + assert.Check(resource.TestCheckResourceAttr(resourceName, "describe_output.0.owner", snowflakeroles.Accountadmin.Name())), + assert.Check(resource.TestCheckResourceAttr(resourceName, "describe_output.0.comment", "foo")), + assert.Check(resource.TestCheckResourceAttr(resourceName, "describe_output.0.table_name", stage.ID().Name())), + assert.Check(resource.TestCheckResourceAttr(resourceName, "describe_output.0.source_type", string(sdk.StreamSourceTypeStage))), + assert.Check(resource.TestCheckResourceAttr(resourceName, "describe_output.0.base_tables.#", "1")), + assert.Check(resource.TestCheckResourceAttr(resourceName, "describe_output.0.base_tables.0", stage.ID().Name())), + assert.Check(resource.TestCheckResourceAttr(resourceName, "describe_output.0.type", "DELTA")), + assert.Check(resource.TestCheckResourceAttr(resourceName, "describe_output.0.stale", "false")), + assert.Check(resource.TestCheckResourceAttr(resourceName, "describe_output.0.mode", string(sdk.StreamModeDefault))), + assert.Check(resource.TestCheckResourceAttrSet(resourceName, "describe_output.0.stale_after")), + assert.Check(resource.TestCheckResourceAttr(resourceName, "describe_output.0.owner_role_type", "ROLE")), + ), + }, + // external change + { + PreConfig: func() { + acc.TestClient().Stream.Alter(t, sdk.NewAlterStreamRequest(id).WithSetComment("bar")) + }, + Config: config.FromModel(t, modelWithExtraFields), + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectResourceAction(resourceName, plancheck.ResourceActionUpdate), + }, + }, + Check: assert.AssertThat(t, resourceassert.StreamOnDirectoryTableResource(t, resourceName). + HasNameString(id.Name()). + HasDatabaseString(id.DatabaseName()). + HasSchemaString(id.SchemaName()). + HasFullyQualifiedNameString(id.FullyQualifiedName()). + HasStageString(stage.ID().Name()), + resourceshowoutputassert.StreamShowOutput(t, resourceName). + HasCreatedOnNotEmpty(). + HasName(id.Name()). + HasDatabaseName(id.DatabaseName()). + HasSchemaName(id.SchemaName()). + HasOwner(snowflakeroles.Accountadmin.Name()). + HasTableName(stage.ID().Name()). + HasSourceType(sdk.StreamSourceTypeStage). + HasBaseTablesPartiallyQualified(stage.ID().Name()). + HasType("DELTA"). + HasStale("false"). + HasMode(sdk.StreamModeDefault). + HasStaleAfterNotEmpty(). + HasInvalidReason("N/A"). + HasComment("foo"). + HasOwnerRoleType("ROLE"), + assert.Check(resource.TestCheckResourceAttrSet(resourceName, "describe_output.0.created_on")), + assert.Check(resource.TestCheckResourceAttr(resourceName, "describe_output.0.name", id.Name())), + assert.Check(resource.TestCheckResourceAttr(resourceName, "describe_output.0.database_name", id.DatabaseName())), + assert.Check(resource.TestCheckResourceAttr(resourceName, "describe_output.0.schema_name", id.SchemaName())), + assert.Check(resource.TestCheckResourceAttr(resourceName, "describe_output.0.owner", snowflakeroles.Accountadmin.Name())), + assert.Check(resource.TestCheckResourceAttr(resourceName, "describe_output.0.comment", "foo")), + assert.Check(resource.TestCheckResourceAttr(resourceName, "describe_output.0.table_name", stage.ID().Name())), + assert.Check(resource.TestCheckResourceAttr(resourceName, "describe_output.0.source_type", string(sdk.StreamSourceTypeStage))), + assert.Check(resource.TestCheckResourceAttr(resourceName, "describe_output.0.base_tables.#", "1")), + assert.Check(resource.TestCheckResourceAttr(resourceName, "describe_output.0.base_tables.0", stage.ID().Name())), + assert.Check(resource.TestCheckResourceAttr(resourceName, "describe_output.0.type", "DELTA")), + assert.Check(resource.TestCheckResourceAttr(resourceName, "describe_output.0.stale", "false")), + assert.Check(resource.TestCheckResourceAttr(resourceName, "describe_output.0.mode", string(sdk.StreamModeDefault))), + assert.Check(resource.TestCheckResourceAttrSet(resourceName, "describe_output.0.stale_after")), + assert.Check(resource.TestCheckResourceAttr(resourceName, "describe_output.0.owner_role_type", "ROLE")), + ), + }, + // update fields + { + Config: config.FromModel(t, modelWithExtraFieldsModified), + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectResourceAction(resourceName, plancheck.ResourceActionUpdate), + }, + }, + Check: assert.AssertThat(t, resourceassert.StreamOnDirectoryTableResource(t, resourceName). + HasNameString(id.Name()). + HasDatabaseString(id.DatabaseName()). + HasSchemaString(id.SchemaName()). + HasFullyQualifiedNameString(id.FullyQualifiedName()). + HasStageString(stage.ID().Name()), + resourceshowoutputassert.StreamShowOutput(t, resourceName). + HasCreatedOnNotEmpty(). + HasName(id.Name()). + HasDatabaseName(id.DatabaseName()). + HasSchemaName(id.SchemaName()). + HasOwner(snowflakeroles.Accountadmin.Name()). + HasTableName(stage.ID().Name()). + HasSourceType(sdk.StreamSourceTypeStage). + HasBaseTablesPartiallyQualified(stage.ID().Name()). + HasType("DELTA"). + HasStale("false"). + HasMode(sdk.StreamModeDefault). + HasStaleAfterNotEmpty(). + HasInvalidReason("N/A"). + HasComment("bar"). + HasOwnerRoleType("ROLE"), + assert.Check(resource.TestCheckResourceAttrSet(resourceName, "describe_output.0.created_on")), + assert.Check(resource.TestCheckResourceAttr(resourceName, "describe_output.0.name", id.Name())), + assert.Check(resource.TestCheckResourceAttr(resourceName, "describe_output.0.database_name", id.DatabaseName())), + assert.Check(resource.TestCheckResourceAttr(resourceName, "describe_output.0.schema_name", id.SchemaName())), + assert.Check(resource.TestCheckResourceAttr(resourceName, "describe_output.0.owner", snowflakeroles.Accountadmin.Name())), + assert.Check(resource.TestCheckResourceAttr(resourceName, "describe_output.0.comment", "bar")), + assert.Check(resource.TestCheckResourceAttr(resourceName, "describe_output.0.table_name", stage.ID().Name())), + assert.Check(resource.TestCheckResourceAttr(resourceName, "describe_output.0.source_type", string(sdk.StreamSourceTypeStage))), + assert.Check(resource.TestCheckResourceAttr(resourceName, "describe_output.0.base_tables.#", "1")), + assert.Check(resource.TestCheckResourceAttr(resourceName, "describe_output.0.base_tables.0", stage.ID().Name())), + assert.Check(resource.TestCheckResourceAttr(resourceName, "describe_output.0.type", "DELTA")), + assert.Check(resource.TestCheckResourceAttr(resourceName, "describe_output.0.stale", "false")), + assert.Check(resource.TestCheckResourceAttr(resourceName, "describe_output.0.mode", string(sdk.StreamModeDefault))), + assert.Check(resource.TestCheckResourceAttrSet(resourceName, "describe_output.0.stale_after")), + assert.Check(resource.TestCheckResourceAttr(resourceName, "describe_output.0.owner_role_type", "ROLE")), + ), + }, + // import + { + Config: config.FromModel(t, modelWithExtraFieldsModified), + ResourceName: resourceName, + ImportState: true, + ImportStateCheck: assert.AssertThatImport(t, + resourceassert.ImportedStreamOnDirectoryTableResource(t, resourceId). + HasNameString(id.Name()). + HasDatabaseString(id.DatabaseName()). + HasSchemaString(id.SchemaName()). + HasFullyQualifiedNameString(id.FullyQualifiedName()). + HasStageString(stage.ID().Name()). + HasCommentString("bar"), + ), + }, + }, + }) +} + +func TestAcc_StreamOnDirectoryTable_CopyGrants(t *testing.T) { + _ = testenvs.GetOrSkipTest(t, testenvs.EnableAcceptance) + acc.TestAccPreCheck(t) + id := acc.TestClient().Ids.RandomSchemaObjectIdentifier() + resourceName := "snowflake_stream_on_directory_table.test" + + var createdOn string + + stage, cleanupStage := acc.TestClient().Stage.CreateStageWithDirectory(t) + t.Cleanup(cleanupStage) + + model := model.StreamOnDirectoryTable("test", id.DatabaseName(), id.Name(), id.SchemaName(), stage.ID().FullyQualifiedName()) + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: acc.TestAccProtoV6ProviderFactories, + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.RequireAbove(tfversion.Version1_5_0), + }, + CheckDestroy: acc.CheckDestroy(t, resources.StreamOnDirectoryTable), + Steps: []resource.TestStep{ + { + Config: config.FromModel(t, model.WithCopyGrants(true)), + Check: assert.AssertThat(t, resourceassert.StreamOnTableResource(t, resourceName). + HasNameString(id.Name()), + assert.Check(resource.TestCheckResourceAttrWith(resourceName, "show_output.0.created_on", func(value string) error { + createdOn = value + return nil + })), + ), + }, + { + Config: config.FromModel(t, model.WithCopyGrants(false)), + Check: assert.AssertThat(t, resourceassert.StreamOnTableResource(t, resourceName). + HasNameString(id.Name()), + assert.Check(resource.TestCheckResourceAttrWith(resourceName, "show_output.0.created_on", func(value string) error { + if value != createdOn { + return fmt.Errorf("stream was recreated") + } + return nil + })), + ), + }, + { + Config: config.FromModel(t, model.WithCopyGrants(true)), + Check: assert.AssertThat(t, resourceassert.StreamOnTableResource(t, resourceName). + HasNameString(id.Name()), + assert.Check(resource.TestCheckResourceAttrWith(resourceName, "show_output.0.created_on", func(value string) error { + if value != createdOn { + return fmt.Errorf("stream was recreated") + } + return nil + })), + ), + }, + }, + }) +} + +func TestAcc_StreamOnDirectoryTable_CheckGrantsAfterRecreation(t *testing.T) { + _ = testenvs.GetOrSkipTest(t, testenvs.EnableAcceptance) + acc.TestAccPreCheck(t) + id := acc.TestClient().Ids.RandomSchemaObjectIdentifier() + resourceName := "snowflake_stream_on_directory_table.test" + + stage, cleanupStage := acc.TestClient().Stage.CreateStageWithDirectory(t) + t.Cleanup(cleanupStage) + + stage2, cleanupStage2 := acc.TestClient().Stage.CreateStageWithDirectory(t) + t.Cleanup(cleanupStage2) + + role, cleanupRole := acc.TestClient().Role.CreateRole(t) + t.Cleanup(cleanupRole) + + model1 := model.StreamOnDirectoryTable("test", id.DatabaseName(), id.Name(), id.SchemaName(), stage.ID().FullyQualifiedName()).WithCopyGrants(true) + model1WithoutCopyGrants := model.StreamOnDirectoryTable("test", id.DatabaseName(), id.Name(), id.SchemaName(), stage.ID().FullyQualifiedName()) + model2 := model.StreamOnDirectoryTable("test", id.DatabaseName(), id.Name(), id.SchemaName(), stage2.ID().FullyQualifiedName()).WithCopyGrants(true) + + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: acc.TestAccProtoV6ProviderFactories, + PreCheck: func() { acc.TestAccPreCheck(t) }, + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.RequireAbove(tfversion.Version1_5_0), + }, + CheckDestroy: acc.CheckDestroy(t, resources.StreamOnDirectoryTable), + Steps: []resource.TestStep{ + { + Config: config.FromModel(t, model1) + grantStreamPrivilegesConfig(resourceName, role.ID()), + Check: resource.ComposeAggregateTestCheckFunc( + // there should be more than one privilege, because we applied grant all privileges and initially there's always one which is ownership + resource.TestCheckResourceAttr("data.snowflake_grants.grants", "grants.#", "2"), + resource.TestCheckResourceAttr("data.snowflake_grants.grants", "grants.1.privilege", "SELECT"), + ), + }, + { + Config: config.FromModel(t, model2) + grantStreamPrivilegesConfig(resourceName, role.ID()), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr("data.snowflake_grants.grants", "grants.#", "2"), + resource.TestCheckResourceAttr("data.snowflake_grants.grants", "grants.1.privilege", "SELECT"), + ), + }, + { + Config: config.FromModel(t, model1WithoutCopyGrants) + grantStreamPrivilegesConfig(resourceName, role.ID()), + ExpectNonEmptyPlan: true, + ConfigPlanChecks: resource.ConfigPlanChecks{ + PostApplyPostRefresh: []plancheck.PlanCheck{ + plancheck.ExpectResourceAction("snowflake_grant_privileges_to_account_role.grant", plancheck.ResourceActionUpdate), + }, + }, + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr("data.snowflake_grants.grants", "grants.#", "1"), + ), + }, + }, + }) +} + +func grantStreamPrivilegesConfig(resourceName string, roleId sdk.AccountObjectIdentifier) string { + return fmt.Sprintf(` +resource "snowflake_grant_privileges_to_account_role" "grant" { + privileges = ["SELECT"] + account_role_name = %[1]s + on_schema_object { + object_type = "STREAM" + object_name = %[2]s.fully_qualified_name + } +} + +data "snowflake_grants" "grants" { + depends_on = [snowflake_grant_privileges_to_account_role.grant, %[2]s] + grants_on { + object_type = "STREAM" + object_name = %[2]s.fully_qualified_name + } +}`, roleId.FullyQualifiedName(), resourceName) +} + +// TODO (SNOW-1737932): Setting schema parameters related to retention time seems to have no affect on streams on directory tables. +// Adjust this test after this is fixed on Snowflake side. +func TestAcc_StreamOnDirectoryTable_RecreateWhenStale(t *testing.T) { + _ = testenvs.GetOrSkipTest(t, testenvs.EnableAcceptance) + acc.TestAccPreCheck(t) + resourceName := "snowflake_stream_on_directory_table.test" + + schema, cleanupSchema := acc.TestClient().Schema.CreateSchemaWithOpts(t, + acc.TestClient().Ids.RandomDatabaseObjectIdentifierInDatabase(acc.TestClient().Ids.DatabaseId()), + &sdk.CreateSchemaOptions{ + DataRetentionTimeInDays: sdk.Pointer(0), + MaxDataExtensionTimeInDays: sdk.Pointer(0), + }, + ) + t.Cleanup(cleanupSchema) + id := acc.TestClient().Ids.RandomSchemaObjectIdentifierInSchema(schema.ID()) + + stage, cleanupStage := acc.TestClient().Stage.CreateStageWithDirectory(t) + t.Cleanup(cleanupStage) + + model := model.StreamOnDirectoryTable("test", id.DatabaseName(), id.Name(), id.SchemaName(), stage.ID().FullyQualifiedName()) + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: acc.TestAccProtoV6ProviderFactories, + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.RequireAbove(tfversion.Version1_5_0), + }, + CheckDestroy: acc.CheckDestroy(t, resources.StreamOnDirectoryTable), + Steps: []resource.TestStep{ + { + Config: config.FromModel(t, model), + Check: assert.AssertThat(t, resourceassert.StreamOnDirectoryTableResource(t, resourceName). + HasNameString(id.Name()). + HasStaleString(r.BooleanFalse), + assert.Check(resource.TestCheckResourceAttr(resourceName, "show_output.0.stale", "false")), + ), + }, + }, + }) +} + +func TestAcc_StreamOnDirectoryTable_InvalidConfiguration(t *testing.T) { + id := acc.TestClient().Ids.RandomSchemaObjectIdentifier() + + modelWithInvalidStageId := model.StreamOnDirectoryTable("test", id.DatabaseName(), id.Name(), id.SchemaName(), "invalid") + + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: acc.TestAccProtoV6ProviderFactories, + PreCheck: func() { acc.TestAccPreCheck(t) }, + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.RequireAbove(tfversion.Version1_5_0), + }, + Steps: []resource.TestStep{ + // invalid stage id + { + Config: config.FromModel(t, modelWithInvalidStageId), + ExpectError: regexp.MustCompile("Error: Invalid identifier type"), + }, + }, + }) +} diff --git a/pkg/resources/stream_on_external_table.go b/pkg/resources/stream_on_external_table.go index 4524c8082e..9262e3595e 100644 --- a/pkg/resources/stream_on_external_table.go +++ b/pkg/resources/stream_on_external_table.go @@ -51,6 +51,7 @@ func StreamOnExternalTable() *schema.Resource { CustomizeDiff: customdiff.All( ComputedIfAnyAttributeChanged(streamOnExternalTableSchema, ShowOutputAttributeName, "external_table", "insert_only", "comment"), ComputedIfAnyAttributeChanged(streamOnExternalTableSchema, DescribeOutputAttributeName, "external_table", "insert_only", "comment"), + RecreateWhenStreamIsStale(), ), Schema: streamOnExternalTableSchema, @@ -73,13 +74,7 @@ func ImportStreamOnExternalTable(ctx context.Context, d *schema.ResourceData, me if err != nil { return nil, err } - if err := d.Set("name", id.Name()); err != nil { - return nil, err - } - if err := d.Set("database", id.DatabaseName()); err != nil { - return nil, err - } - if err := d.Set("schema", id.SchemaName()); err != nil { + if _, err := ImportName[sdk.SchemaObjectIdentifier](context.Background(), d, nil); err != nil { return nil, err } if err := d.Set("insert_only", booleanStringFromBool(v.IsInsertOnly())); err != nil { @@ -154,7 +149,7 @@ func ReadStreamOnExternalTable(withExternalChangesMarking bool) schema.ReadConte return diag.Diagnostics{ diag.Diagnostic{ Severity: diag.Error, - Summary: "Failed to parse table ID in Read.", + Summary: "Failed to parse external table ID in Read.", Detail: fmt.Sprintf("stream name: %s, Err: %s", id.FullyQualifiedName(), err), }, } @@ -199,7 +194,8 @@ func UpdateStreamOnExternalTable(ctx context.Context, d *schema.ResourceData, me } // change on these fields can not be ForceNew because then the object is dropped explicitly and copying grants does not have effect - if keys := changedKeys(d, "external_table", "insert_only", "at", "before"); len(keys) > 0 { + // recreate when the stream is stale - see https://community.snowflake.com/s/article/using-tasks-to-avoid-stale-streams-when-incoming-data-is-empty + if keys := changedKeys(d, "external_table", "insert_only", "at", "before", "stale"); len(keys) > 0 { log.Printf("[DEBUG] Detected change on %q, recreating...", keys) return CreateStreamOnExternalTable(true)(ctx, d, meta) } diff --git a/pkg/resources/stream_on_external_table_acceptance_test.go b/pkg/resources/stream_on_external_table_acceptance_test.go index 2611ffb69f..c926a507b9 100644 --- a/pkg/resources/stream_on_external_table_acceptance_test.go +++ b/pkg/resources/stream_on_external_table_acceptance_test.go @@ -7,17 +7,20 @@ import ( acc "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/acceptance" "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/acceptance/bettertestspoc/assert" + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/acceptance/bettertestspoc/assert/objectassert" "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/acceptance/bettertestspoc/assert/resourceassert" "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/acceptance/bettertestspoc/assert/resourceshowoutputassert" "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/acceptance/bettertestspoc/config" tfconfig "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/acceptance/bettertestspoc/config" "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/acceptance/bettertestspoc/config/model" + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/acceptance/planchecks" "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/acceptance/testenvs" "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/helpers" "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/internal/snowflakeroles" "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/provider/resources" r "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/resources" "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/sdk" + tfjson "github.com/hashicorp/terraform-json" pluginconfig "github.com/hashicorp/terraform-plugin-testing/config" "github.com/hashicorp/terraform-plugin-testing/helper/resource" "github.com/hashicorp/terraform-plugin-testing/plancheck" @@ -326,7 +329,7 @@ func TestAcc_StreamOnExternalTable_Basic(t *testing.T) { assert.Check(resource.TestCheckResourceAttr(resourceName, "describe_output.0.owner_role_type", "ROLE")), assert.Check(resource.TestCheckResourceAttrWith(resourceName, "show_output.0.created_on", func(value string) error { if value == createdOn { - return fmt.Errorf("view was not recreated") + return fmt.Errorf("stream was not recreated") } return nil })), @@ -392,7 +395,7 @@ func TestAcc_StreamOnExternalTable_CopyGrants(t *testing.T) { HasNameString(id.Name()), assert.Check(resource.TestCheckResourceAttrWith(resourceName, "show_output.0.created_on", func(value string) error { if value != createdOn { - return fmt.Errorf("view was recreated") + return fmt.Errorf("stream was recreated") } return nil })), @@ -404,7 +407,242 @@ func TestAcc_StreamOnExternalTable_CopyGrants(t *testing.T) { HasNameString(id.Name()), assert.Check(resource.TestCheckResourceAttrWith(resourceName, "show_output.0.created_on", func(value string) error { if value != createdOn { - return fmt.Errorf("view was recreated") + return fmt.Errorf("stream was recreated") + } + return nil + })), + ), + }, + }, + }) +} + +func TestAcc_StreamOnExternalTable_CheckGrantsAfterRecreation(t *testing.T) { + _ = testenvs.GetOrSkipTest(t, testenvs.EnableAcceptance) + acc.TestAccPreCheck(t) + id := acc.TestClient().Ids.RandomSchemaObjectIdentifier() + resourceName := "snowflake_stream_on_external_table.test" + + stageID := acc.TestClient().Ids.RandomSchemaObjectIdentifier() + stageLocation := fmt.Sprintf("@%s", stageID.FullyQualifiedName()) + _, stageCleanup := acc.TestClient().Stage.CreateStageWithURL(t, stageID) + t.Cleanup(stageCleanup) + + externalTable, externalTableCleanup := acc.TestClient().ExternalTable.CreateWithLocation(t, stageLocation) + t.Cleanup(externalTableCleanup) + + externalTable2, externalTableCleanup2 := acc.TestClient().ExternalTable.CreateWithLocation(t, stageLocation) + t.Cleanup(externalTableCleanup2) + + role, cleanupRole := acc.TestClient().Role.CreateRole(t) + t.Cleanup(cleanupRole) + + model1 := model.StreamOnExternalTable("test", id.DatabaseName(), externalTable.ID().FullyQualifiedName(), id.Name(), id.SchemaName()). + WithInsertOnly(r.BooleanTrue). + WithCopyGrants(true) + model1WithoutCopyGrants := model.StreamOnExternalTable("test", id.DatabaseName(), externalTable.ID().FullyQualifiedName(), id.Name(), id.SchemaName()). + WithInsertOnly(r.BooleanTrue) + model2 := model.StreamOnExternalTable("test", id.DatabaseName(), externalTable2.ID().FullyQualifiedName(), id.Name(), id.SchemaName()). + WithInsertOnly(r.BooleanTrue). + WithCopyGrants(true) + + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: acc.TestAccProtoV6ProviderFactories, + PreCheck: func() { acc.TestAccPreCheck(t) }, + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.RequireAbove(tfversion.Version1_5_0), + }, + CheckDestroy: acc.CheckDestroy(t, resources.StreamOnExternalTable), + Steps: []resource.TestStep{ + { + Config: config.FromModel(t, model1) + grantStreamPrivilegesConfig(resourceName, role.ID()), + Check: resource.ComposeAggregateTestCheckFunc( + // there should be more than one privilege, because we applied grant all privileges and initially there's always one which is ownership + resource.TestCheckResourceAttr("data.snowflake_grants.grants", "grants.#", "2"), + resource.TestCheckResourceAttr("data.snowflake_grants.grants", "grants.1.privilege", "SELECT"), + ), + }, + { + Config: config.FromModel(t, model2) + grantStreamPrivilegesConfig(resourceName, role.ID()), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr("data.snowflake_grants.grants", "grants.#", "2"), + resource.TestCheckResourceAttr("data.snowflake_grants.grants", "grants.1.privilege", "SELECT"), + ), + }, + { + Config: config.FromModel(t, model1WithoutCopyGrants) + grantStreamPrivilegesConfig(resourceName, role.ID()), + ExpectNonEmptyPlan: true, + ConfigPlanChecks: resource.ConfigPlanChecks{ + PostApplyPostRefresh: []plancheck.PlanCheck{ + plancheck.ExpectResourceAction("snowflake_grant_privileges_to_account_role.grant", plancheck.ResourceActionUpdate), + }, + }, + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr("data.snowflake_grants.grants", "grants.#", "1"), + ), + }, + }, + }) +} + +func TestAcc_StreamOnExternalTable_PermadiffWhenIsStaleAndHasNoRetentionTime(t *testing.T) { + _ = testenvs.GetOrSkipTest(t, testenvs.EnableAcceptance) + acc.TestAccPreCheck(t) + resourceName := "snowflake_stream_on_external_table.test" + + schema, cleanupSchema := acc.TestClient().Schema.CreateSchemaWithOpts(t, + acc.TestClient().Ids.RandomDatabaseObjectIdentifierInDatabase(acc.TestClient().Ids.DatabaseId()), + &sdk.CreateSchemaOptions{ + DataRetentionTimeInDays: sdk.Pointer(0), + MaxDataExtensionTimeInDays: sdk.Pointer(0), + }, + ) + t.Cleanup(cleanupSchema) + id := acc.TestClient().Ids.RandomSchemaObjectIdentifierInSchema(schema.ID()) + + stageID := acc.TestClient().Ids.RandomSchemaObjectIdentifierInSchema(schema.ID()) + stageLocation := fmt.Sprintf("@%s", stageID.FullyQualifiedName()) + _, stageCleanup := acc.TestClient().Stage.CreateStageWithURL(t, stageID) + t.Cleanup(stageCleanup) + + externalTable, externalTableCleanup := acc.TestClient().ExternalTable.CreateInSchemaWithLocation(t, stageLocation, schema.ID()) + t.Cleanup(externalTableCleanup) + + var createdOn string + + model := model.StreamOnExternalTable("test", id.DatabaseName(), externalTable.ID().FullyQualifiedName(), id.Name(), id.SchemaName()).WithInsertOnly(r.BooleanTrue) + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: acc.TestAccProtoV6ProviderFactories, + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.RequireAbove(tfversion.Version1_5_0), + }, + CheckDestroy: acc.CheckDestroy(t, resources.StreamOnExternalTable), + Steps: []resource.TestStep{ + // check that stale state is marked properly and forces an update + { + Config: config.FromModel(t, model), + ConfigPlanChecks: resource.ConfigPlanChecks{ + PostApplyPostRefresh: []plancheck.PlanCheck{ + plancheck.ExpectResourceAction(resourceName, plancheck.ResourceActionUpdate), + planchecks.ExpectChange(resourceName, "stale", tfjson.ActionUpdate, sdk.String(r.BooleanTrue), sdk.String(r.BooleanFalse)), + }, + }, + ExpectNonEmptyPlan: true, + Check: assert.AssertThat(t, resourceassert.StreamOnExternalTableResource(t, resourceName). + HasNameString(id.Name()). + HasStaleString(r.BooleanTrue), + assert.Check(resource.TestCheckResourceAttr(resourceName, "show_output.0.stale", "true")), + assert.Check(resource.TestCheckResourceAttrWith(resourceName, "show_output.0.created_on", func(value string) error { + createdOn = value + return nil + })), + ), + }, + // check that the resource was recreated + // note that it is stale again because we still have schema parameters set to 0, this results in a permadiff + { + Config: config.FromModel(t, model), + ConfigPlanChecks: resource.ConfigPlanChecks{ + PostApplyPostRefresh: []plancheck.PlanCheck{ + plancheck.ExpectResourceAction(resourceName, plancheck.ResourceActionUpdate), + planchecks.ExpectChange(resourceName, "stale", tfjson.ActionUpdate, sdk.String(r.BooleanTrue), sdk.String(r.BooleanFalse)), + }, + }, + ExpectNonEmptyPlan: true, + Check: assert.AssertThat(t, resourceassert.StreamOnExternalTableResource(t, resourceName). + HasNameString(id.Name()). + HasStaleString(r.BooleanTrue), + assert.Check(resource.TestCheckResourceAttr(resourceName, "show_output.0.stale", "true")), + assert.Check(resource.TestCheckResourceAttrWith(resourceName, "show_output.0.created_on", func(value string) error { + if value == createdOn { + return fmt.Errorf("stream was not recreated") + } + return nil + })), + ), + }, + }, + }) +} + +func TestAcc_StreamOnExternalTable_StaleWithExternalChanges(t *testing.T) { + _ = testenvs.GetOrSkipTest(t, testenvs.EnableAcceptance) + acc.TestAccPreCheck(t) + resourceName := "snowflake_stream_on_external_table.test" + + schema, cleanupSchema := acc.TestClient().Schema.CreateSchemaWithOpts(t, + acc.TestClient().Ids.RandomDatabaseObjectIdentifierInDatabase(acc.TestClient().Ids.DatabaseId()), + &sdk.CreateSchemaOptions{ + DataRetentionTimeInDays: sdk.Pointer(1), + MaxDataExtensionTimeInDays: sdk.Pointer(1), + }, + ) + t.Cleanup(cleanupSchema) + id := acc.TestClient().Ids.RandomSchemaObjectIdentifierInSchema(schema.ID()) + + stageID := acc.TestClient().Ids.RandomSchemaObjectIdentifierInSchema(schema.ID()) + stageLocation := fmt.Sprintf("@%s", stageID.FullyQualifiedName()) + _, stageCleanup := acc.TestClient().Stage.CreateStageWithURL(t, stageID) + t.Cleanup(stageCleanup) + + externalTable, externalTableCleanup := acc.TestClient().ExternalTable.CreateInSchemaWithLocation(t, stageLocation, schema.ID()) + t.Cleanup(externalTableCleanup) + + var createdOn string + + model := model.StreamOnExternalTable("test", id.DatabaseName(), externalTable.ID().FullyQualifiedName(), id.Name(), id.SchemaName()).WithInsertOnly(r.BooleanTrue) + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: acc.TestAccProtoV6ProviderFactories, + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.RequireAbove(tfversion.Version1_5_0), + }, + CheckDestroy: acc.CheckDestroy(t, resources.StreamOnExternalTable), + Steps: []resource.TestStep{ + // initial creation does not lead to stale stream + { + Config: config.FromModel(t, model), + Check: assert.AssertThat(t, resourceassert.StreamOnExternalTableResource(t, resourceName). + HasNameString(id.Name()). + HasStaleString(r.BooleanFalse), + assert.Check(resource.TestCheckResourceAttr(resourceName, "show_output.0.stale", "false")), + assert.Check(resource.TestCheckResourceAttrWith(resourceName, "show_output.0.created_on", func(value string) error { + createdOn = value + return nil + })), + ), + }, + // changing the value externally on schema + { + PreConfig: func() { + acc.TestClient().Schema.Alter(t, schema.ID(), &sdk.AlterSchemaOptions{ + Set: &sdk.SchemaSet{ + DataRetentionTimeInDays: sdk.Int(0), + MaxDataExtensionTimeInDays: sdk.Int(0), + }, + }) + assert.AssertThatObject(t, objectassert.Stream(t, id). + HasName(id.Name()). + HasStale(true), + ) + acc.TestClient().Schema.Alter(t, schema.ID(), &sdk.AlterSchemaOptions{ + Set: &sdk.SchemaSet{ + DataRetentionTimeInDays: sdk.Int(1), + MaxDataExtensionTimeInDays: sdk.Int(1), + }, + }) + assert.AssertThatObject(t, objectassert.Stream(t, id). + HasName(id.Name()). + HasStale(false), + ) + }, + Config: config.FromModel(t, model), + Check: assert.AssertThat(t, resourceassert.StreamOnExternalTableResource(t, resourceName). + HasNameString(id.Name()). + HasStaleString(r.BooleanFalse), + assert.Check(resource.TestCheckResourceAttr(resourceName, "show_output.0.stale", "false")), + assert.Check(resource.TestCheckResourceAttrWith(resourceName, "show_output.0.created_on", func(value string) error { + if value != createdOn { + return fmt.Errorf("stream was recreated") } return nil })), @@ -644,7 +882,7 @@ func TestAcc_StreamOnExternalTable_InvalidConfiguration(t *testing.T) { ConfigVariables: tfconfig.ConfigVariablesFromModel(t, modelWithAt), ExpectError: regexp.MustCompile("Error: Invalid combination of arguments"), }, - // invalid table id + // invalid external table id { Config: config.FromModel(t, modelWithInvalidExternalTableId), ExpectError: regexp.MustCompile("Error: Invalid identifier type"), diff --git a/pkg/resources/stream_on_table.go b/pkg/resources/stream_on_table.go index b8241a6e99..4fdcceba23 100644 --- a/pkg/resources/stream_on_table.go +++ b/pkg/resources/stream_on_table.go @@ -58,6 +58,7 @@ func StreamOnTable() *schema.Resource { CustomizeDiff: customdiff.All( ComputedIfAnyAttributeChanged(streamOnTableSchema, ShowOutputAttributeName, "table", "append_only", "comment"), ComputedIfAnyAttributeChanged(streamOnTableSchema, DescribeOutputAttributeName, "table", "append_only", "comment"), + RecreateWhenStreamIsStale(), ), Schema: streamOnTableSchema, @@ -80,13 +81,7 @@ func ImportStreamOnTable(ctx context.Context, d *schema.ResourceData, meta any) if err != nil { return nil, err } - if err := d.Set("name", id.Name()); err != nil { - return nil, err - } - if err := d.Set("database", id.DatabaseName()); err != nil { - return nil, err - } - if err := d.Set("schema", id.SchemaName()); err != nil { + if _, err := ImportName[sdk.SchemaObjectIdentifier](context.Background(), d, nil); err != nil { return nil, err } if err := d.Set("append_only", booleanStringFromBool(v.IsAppendOnly())); err != nil { @@ -207,7 +202,8 @@ func UpdateStreamOnTable(ctx context.Context, d *schema.ResourceData, meta any) } // change on these fields can not be ForceNew because then the object is dropped explicitly and copying grants does not have effect - if keys := changedKeys(d, "table", "append_only", "at", "before", "show_initial_rows"); len(keys) > 0 { + // recreate when the stream is stale - see https://community.snowflake.com/s/article/using-tasks-to-avoid-stale-streams-when-incoming-data-is-empty + if keys := changedKeys(d, "table", "append_only", "at", "before", "show_initial_rows", "stale"); len(keys) > 0 { log.Printf("[DEBUG] Detected change on %q, recreating...", keys) return CreateStreamOnTable(true)(ctx, d, meta) } diff --git a/pkg/resources/stream_on_table_acceptance_test.go b/pkg/resources/stream_on_table_acceptance_test.go index bfd5edafa3..5542c09531 100644 --- a/pkg/resources/stream_on_table_acceptance_test.go +++ b/pkg/resources/stream_on_table_acceptance_test.go @@ -7,17 +7,20 @@ import ( acc "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/acceptance" "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/acceptance/bettertestspoc/assert" + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/acceptance/bettertestspoc/assert/objectassert" "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/acceptance/bettertestspoc/assert/resourceassert" "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/acceptance/bettertestspoc/assert/resourceshowoutputassert" "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/acceptance/bettertestspoc/config" tfconfig "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/acceptance/bettertestspoc/config" "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/acceptance/bettertestspoc/config/model" + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/acceptance/planchecks" "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/acceptance/testenvs" "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/helpers" "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/internal/snowflakeroles" "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/provider/resources" r "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/resources" "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/sdk" + tfjson "github.com/hashicorp/terraform-json" pluginconfig "github.com/hashicorp/terraform-plugin-testing/config" "github.com/hashicorp/terraform-plugin-testing/helper/resource" "github.com/hashicorp/terraform-plugin-testing/plancheck" @@ -316,7 +319,7 @@ func TestAcc_StreamOnTable_CopyGrants(t *testing.T) { HasNameString(id.Name()), assert.Check(resource.TestCheckResourceAttrWith(resourceName, "show_output.0.created_on", func(value string) error { if value != createdOn { - return fmt.Errorf("view was recreated") + return fmt.Errorf("stream was recreated") } return nil })), @@ -328,7 +331,233 @@ func TestAcc_StreamOnTable_CopyGrants(t *testing.T) { HasNameString(id.Name()), assert.Check(resource.TestCheckResourceAttrWith(resourceName, "show_output.0.created_on", func(value string) error { if value != createdOn { - return fmt.Errorf("view was recreated") + return fmt.Errorf("stream was recreated") + } + return nil + })), + ), + }, + }, + }) +} + +func TestAcc_StreamOnTable_CheckGrantsAfterRecreation(t *testing.T) { + _ = testenvs.GetOrSkipTest(t, testenvs.EnableAcceptance) + acc.TestAccPreCheck(t) + id := acc.TestClient().Ids.RandomSchemaObjectIdentifier() + resourceName := "snowflake_stream_on_table.test" + + table, cleanupTable := acc.TestClient().Table.CreateWithChangeTracking(t) + t.Cleanup(cleanupTable) + + table2, cleanupTable2 := acc.TestClient().Table.CreateWithChangeTracking(t) + t.Cleanup(cleanupTable2) + + role, cleanupRole := acc.TestClient().Role.CreateRole(t) + t.Cleanup(cleanupRole) + + model1 := model.StreamOnTable("test", id.DatabaseName(), id.Name(), id.SchemaName(), table.ID().FullyQualifiedName()). + WithCopyGrants(true) + model1WithoutCopyGrants := model.StreamOnTable("test", id.DatabaseName(), id.Name(), id.SchemaName(), table.ID().FullyQualifiedName()) + model2 := model.StreamOnTable("test", id.DatabaseName(), id.Name(), id.SchemaName(), table2.ID().FullyQualifiedName()). + WithCopyGrants(true) + + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: acc.TestAccProtoV6ProviderFactories, + PreCheck: func() { acc.TestAccPreCheck(t) }, + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.RequireAbove(tfversion.Version1_5_0), + }, + CheckDestroy: acc.CheckDestroy(t, resources.StreamOnExternalTable), + Steps: []resource.TestStep{ + { + Config: config.FromModel(t, model1) + grantStreamPrivilegesConfig(resourceName, role.ID()), + Check: resource.ComposeAggregateTestCheckFunc( + // there should be more than one privilege, because we applied grant all privileges and initially there's always one which is ownership + resource.TestCheckResourceAttr("data.snowflake_grants.grants", "grants.#", "2"), + resource.TestCheckResourceAttr("data.snowflake_grants.grants", "grants.1.privilege", "SELECT"), + ), + }, + { + Config: config.FromModel(t, model2) + grantStreamPrivilegesConfig(resourceName, role.ID()), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr("data.snowflake_grants.grants", "grants.#", "2"), + resource.TestCheckResourceAttr("data.snowflake_grants.grants", "grants.1.privilege", "SELECT"), + ), + }, + { + Config: config.FromModel(t, model1WithoutCopyGrants) + grantStreamPrivilegesConfig(resourceName, role.ID()), + ExpectNonEmptyPlan: true, + ConfigPlanChecks: resource.ConfigPlanChecks{ + PostApplyPostRefresh: []plancheck.PlanCheck{ + plancheck.ExpectResourceAction("snowflake_grant_privileges_to_account_role.grant", plancheck.ResourceActionUpdate), + }, + }, + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr("data.snowflake_grants.grants", "grants.#", "1"), + ), + }, + }, + }) +} + +func TestAcc_StreamOnTable_PermadiffWhenIsStaleAndHasNoRetentionTime(t *testing.T) { + _ = testenvs.GetOrSkipTest(t, testenvs.EnableAcceptance) + acc.TestAccPreCheck(t) + resourceName := "snowflake_stream_on_table.test" + + schema, cleanupSchema := acc.TestClient().Schema.CreateSchemaWithOpts(t, + acc.TestClient().Ids.RandomDatabaseObjectIdentifierInDatabase(acc.TestClient().Ids.DatabaseId()), + &sdk.CreateSchemaOptions{ + DataRetentionTimeInDays: sdk.Pointer(0), + MaxDataExtensionTimeInDays: sdk.Pointer(0), + }, + ) + t.Cleanup(cleanupSchema) + id := acc.TestClient().Ids.RandomSchemaObjectIdentifierInSchema(schema.ID()) + + columns := []sdk.TableColumnRequest{ + *sdk.NewTableColumnRequest("id", "NUMBER"), + } + + table, cleanupTable := acc.TestClient().Table.CreateWithRequest(t, sdk.NewCreateTableRequest(acc.TestClient().Ids.RandomSchemaObjectIdentifierInSchema(schema.ID()), columns).WithChangeTracking(sdk.Pointer(true))) + t.Cleanup(cleanupTable) + + var createdOn string + + model := model.StreamOnTable("test", id.DatabaseName(), id.Name(), id.SchemaName(), table.ID().FullyQualifiedName()) + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: acc.TestAccProtoV6ProviderFactories, + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.RequireAbove(tfversion.Version1_5_0), + }, + CheckDestroy: acc.CheckDestroy(t, resources.StreamOnTable), + Steps: []resource.TestStep{ + // check that stale state is marked properly and forces an update + { + Config: config.FromModel(t, model), + ConfigPlanChecks: resource.ConfigPlanChecks{ + PostApplyPostRefresh: []plancheck.PlanCheck{ + plancheck.ExpectResourceAction(resourceName, plancheck.ResourceActionUpdate), + planchecks.ExpectChange(resourceName, "stale", tfjson.ActionUpdate, sdk.String(r.BooleanTrue), sdk.String(r.BooleanFalse)), + }, + }, + ExpectNonEmptyPlan: true, + Check: assert.AssertThat(t, resourceassert.StreamOnTableResource(t, resourceName). + HasNameString(id.Name()). + HasStaleString(r.BooleanTrue), + assert.Check(resource.TestCheckResourceAttr(resourceName, "show_output.0.stale", "true")), + assert.Check(resource.TestCheckResourceAttrWith(resourceName, "show_output.0.created_on", func(value string) error { + createdOn = value + return nil + })), + ), + }, + // check that the resource was recreated + // note that it is stale again because we still have schema parameters set to 0 + { + Config: config.FromModel(t, model), + ConfigPlanChecks: resource.ConfigPlanChecks{ + PostApplyPostRefresh: []plancheck.PlanCheck{ + plancheck.ExpectResourceAction(resourceName, plancheck.ResourceActionUpdate), + planchecks.ExpectChange(resourceName, "stale", tfjson.ActionUpdate, sdk.String(r.BooleanTrue), sdk.String(r.BooleanFalse)), + }, + }, + ExpectNonEmptyPlan: true, + Check: assert.AssertThat(t, resourceassert.StreamOnTableResource(t, resourceName). + HasNameString(id.Name()). + HasStaleString(r.BooleanTrue), + assert.Check(resource.TestCheckResourceAttr(resourceName, "show_output.0.stale", "true")), + assert.Check(resource.TestCheckResourceAttrWith(resourceName, "show_output.0.created_on", func(value string) error { + if value == createdOn { + return fmt.Errorf("stream was not recreated") + } + return nil + })), + ), + }, + }, + }) +} + +func TestAcc_StreamOnTable_StaleWithExternalChanges(t *testing.T) { + _ = testenvs.GetOrSkipTest(t, testenvs.EnableAcceptance) + acc.TestAccPreCheck(t) + resourceName := "snowflake_stream_on_table.test" + + schema, cleanupSchema := acc.TestClient().Schema.CreateSchemaWithOpts(t, + acc.TestClient().Ids.RandomDatabaseObjectIdentifierInDatabase(acc.TestClient().Ids.DatabaseId()), + &sdk.CreateSchemaOptions{ + DataRetentionTimeInDays: sdk.Pointer(1), + MaxDataExtensionTimeInDays: sdk.Pointer(1), + }, + ) + t.Cleanup(cleanupSchema) + id := acc.TestClient().Ids.RandomSchemaObjectIdentifierInSchema(schema.ID()) + + columns := []sdk.TableColumnRequest{ + *sdk.NewTableColumnRequest("id", "NUMBER"), + } + + table, cleanupTable := acc.TestClient().Table.CreateWithRequest(t, sdk.NewCreateTableRequest(acc.TestClient().Ids.RandomSchemaObjectIdentifierInSchema(schema.ID()), columns).WithChangeTracking(sdk.Pointer(true))) + t.Cleanup(cleanupTable) + + var createdOn string + + model := model.StreamOnTable("test", id.DatabaseName(), id.Name(), id.SchemaName(), table.ID().FullyQualifiedName()) + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: acc.TestAccProtoV6ProviderFactories, + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.RequireAbove(tfversion.Version1_5_0), + }, + CheckDestroy: acc.CheckDestroy(t, resources.StreamOnTable), + Steps: []resource.TestStep{ + // initial creation does not lead to stale stream + { + Config: config.FromModel(t, model), + Check: assert.AssertThat(t, resourceassert.StreamOnTableResource(t, resourceName). + HasNameString(id.Name()). + HasStaleString(r.BooleanFalse), + assert.Check(resource.TestCheckResourceAttr(resourceName, "show_output.0.stale", "false")), + assert.Check(resource.TestCheckResourceAttrWith(resourceName, "show_output.0.created_on", func(value string) error { + createdOn = value + return nil + })), + ), + }, + // changing the value externally on schema + { + PreConfig: func() { + acc.TestClient().Schema.Alter(t, schema.ID(), &sdk.AlterSchemaOptions{ + Set: &sdk.SchemaSet{ + DataRetentionTimeInDays: sdk.Int(0), + MaxDataExtensionTimeInDays: sdk.Int(0), + }, + }) + assert.AssertThatObject(t, objectassert.Stream(t, id). + HasName(id.Name()). + HasStale(true), + ) + + acc.TestClient().Schema.Alter(t, schema.ID(), &sdk.AlterSchemaOptions{ + Set: &sdk.SchemaSet{ + DataRetentionTimeInDays: sdk.Int(1), + MaxDataExtensionTimeInDays: sdk.Int(1), + }, + }) + assert.AssertThatObject(t, objectassert.Stream(t, id). + HasName(id.Name()). + HasStale(false), + ) + }, + Config: config.FromModel(t, model), + Check: assert.AssertThat(t, resourceassert.StreamOnTableResource(t, resourceName). + HasNameString(id.Name()). + HasStaleString(r.BooleanFalse), + assert.Check(resource.TestCheckResourceAttr(resourceName, "show_output.0.stale", "false")), + assert.Check(resource.TestCheckResourceAttrWith(resourceName, "show_output.0.created_on", func(value string) error { + if value != createdOn { + return fmt.Errorf("stream was recreated") } return nil })), diff --git a/pkg/resources/view.go b/pkg/resources/view.go index 3020e6d752..7c5b6d680f 100644 --- a/pkg/resources/view.go +++ b/pkg/resources/view.go @@ -44,13 +44,11 @@ var viewSchema = map[string]*schema.Schema{ DiffSuppressFunc: suppressIdentifierQuoting, }, "copy_grants": { - Type: schema.TypeBool, - Optional: true, - Default: false, - Description: "Retains the access permissions from the original view when a new view is created using the OR REPLACE clause.", - DiffSuppressFunc: func(k, oldValue, newValue string, d *schema.ResourceData) bool { - return oldValue != "" && oldValue != newValue - }, + Type: schema.TypeBool, + Optional: true, + Default: false, + Description: "Retains the access permissions from the original view when a new view is created using the OR REPLACE clause.", + DiffSuppressFunc: IgnoreAfterCreation, }, "is_secure": { Type: schema.TypeString, @@ -354,12 +352,8 @@ func CreateView(orReplace bool) schema.CreateContextFunc { statement := d.Get("statement").(string) req := sdk.NewCreateViewRequest(id, statement) - if orReplace { - req.WithOrReplace(true) - } - - if v := d.Get("copy_grants"); v.(bool) { - req.WithCopyGrants(true).WithOrReplace(true) + if err := copyGrantsAttributeCreate(d, orReplace, &req.OrReplace, &req.CopyGrants); err != nil { + return diag.FromErr(err) } if v := d.Get("is_secure").(string); v != BooleanDefault { @@ -611,9 +605,6 @@ func ReadView(withExternalChangesMarking bool) schema.ReadContextFunc { if err := d.Set(FullyQualifiedNameAttributeName, id.FullyQualifiedName()); err != nil { return diag.FromErr(err) } - if err = d.Set("copy_grants", view.HasCopyGrants()); err != nil { - return diag.FromErr(err) - } if err = d.Set("comment", view.Comment); err != nil { return diag.FromErr(err) } @@ -865,7 +856,7 @@ func UpdateView(ctx context.Context, d *schema.ResourceData, meta any) diag.Diag } // change on these fields can not be ForceNew because then view is dropped explicitly and copying grants does not have effect - if keys := changedKeys(d, "statement", "is_temporary", "is_recursive", "copy_grant", "column"); len(keys) > 0 { + if keys := changedKeys(d, "statement", "is_temporary", "is_recursive", "column"); len(keys) > 0 { log.Printf("[DEBUG] Detected change on %q, recreating...", keys) return CreateView(true)(ctx, d, meta) } diff --git a/pkg/resources/view_acceptance_test.go b/pkg/resources/view_acceptance_test.go index 016add5fac..462f4ee6b8 100644 --- a/pkg/resources/view_acceptance_test.go +++ b/pkg/resources/view_acceptance_test.go @@ -923,6 +923,7 @@ func TestAcc_View_Issue3073(t *testing.T) { }) } +// fixes https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/3073#issuecomment-2392250469 func TestAcc_View_IncorrectColumnsWithOrReplace(t *testing.T) { t.Setenv(string(testenvs.ConfigureClientOnce), "") statement := `SELECT ROLE_NAME as "role_name", ROLE_OWNER as "role_owner" FROM INFORMATION_SCHEMA.APPLICABLE_ROLES` @@ -1113,11 +1114,17 @@ func TestAcc_ViewChangeCopyGrantsReversed(t *testing.T) { }) } -func TestAcc_ViewCopyGrantsStatementUpdate(t *testing.T) { +func TestAcc_View_CheckGrantsAfterRecreation(t *testing.T) { t.Setenv(string(testenvs.ConfigureClientOnce), "") _ = testenvs.GetOrSkipTest(t, testenvs.EnableAcceptance) - tableId := acc.TestClient().Ids.RandomSchemaObjectIdentifier() - viewId := acc.TestClient().Ids.RandomSchemaObjectIdentifier() + acc.TestAccPreCheck(t) + id := acc.TestClient().Ids.RandomSchemaObjectIdentifier() + + table, cleanupTable := acc.TestClient().Table.Create(t) + t.Cleanup(cleanupTable) + + role, cleanupRole := acc.TestClient().Role.CreateRole(t) + t.Cleanup(cleanupRole) resource.Test(t, resource.TestCase{ ProtoV6ProviderFactories: acc.TestAccProtoV6ProviderFactories, @@ -1128,7 +1135,7 @@ func TestAcc_ViewCopyGrantsStatementUpdate(t *testing.T) { CheckDestroy: acc.CheckDestroy(t, resources.View), Steps: []resource.TestStep{ { - Config: viewConfigWithGrants(viewId, tableId, `\"name\"`), + Config: viewConfigWithGrants(id, table.ID(), "id", role.ID(), true), Check: resource.ComposeAggregateTestCheckFunc( // there should be more than one privilege, because we applied grant all privileges and initially there's always one which is ownership resource.TestCheckResourceAttr("data.snowflake_grants.grants", "grants.#", "2"), @@ -1136,12 +1143,25 @@ func TestAcc_ViewCopyGrantsStatementUpdate(t *testing.T) { ), }, { - Config: viewConfigWithGrants(viewId, tableId, "*"), + Config: viewConfigWithGrants(id, table.ID(), "*", role.ID(), true), Check: resource.ComposeAggregateTestCheckFunc( resource.TestCheckResourceAttr("data.snowflake_grants.grants", "grants.#", "2"), resource.TestCheckResourceAttr("data.snowflake_grants.grants", "grants.1.privilege", "SELECT"), ), }, + // Recreate without copy grants. Now we expect changes because the grants are still in the config. + { + Config: viewConfigWithGrants(id, table.ID(), "id", role.ID(), false), + ExpectNonEmptyPlan: true, + ConfigPlanChecks: resource.ConfigPlanChecks{ + PostApplyPostRefresh: []plancheck.PlanCheck{ + plancheck.ExpectResourceAction("snowflake_grant_privileges_to_account_role.grant", plancheck.ResourceActionUpdate), + }, + }, + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr("data.snowflake_grants.grants", "grants.#", "1"), + ), + }, }, }) } @@ -1281,26 +1301,15 @@ resource "snowflake_view" "test" { return fmt.Sprintf(s, id.Name(), id.DatabaseName(), tagSchema, tagName, tagValue, id.SchemaName(), statement) } -func viewConfigWithGrants(viewId, tableId sdk.SchemaObjectIdentifier, selectStatement string) string { +func viewConfigWithGrants(viewId, tableId sdk.SchemaObjectIdentifier, selectStatement string, roleId sdk.AccountObjectIdentifier, copyGrants bool) string { return fmt.Sprintf(` -resource "snowflake_table" "table" { - database = "%[1]s" - schema = "%[2]s" - name = "%[3]s" - - column { - name = "name" - type = "text" - } -} - resource "snowflake_view" "test" { name = "%[4]s" comment = "created by terraform" database = "%[1]s" schema = "%[2]s" - statement = "select %[5]s from \"%[1]s\".\"%[2]s\".\"${snowflake_table.table.name}\"" - copy_grants = true + statement = "select %[5]s from \"%[1]s\".\"%[2]s\".\"%[3]s\"" + copy_grants = %[7]t is_secure = true column { @@ -1308,27 +1317,23 @@ resource "snowflake_view" "test" { } } -resource "snowflake_account_role" "test" { - name = "test" -} - resource "snowflake_grant_privileges_to_account_role" "grant" { privileges = ["SELECT"] - account_role_name = snowflake_account_role.test.name + account_role_name = %[6]s on_schema_object { object_type = "VIEW" - object_name = "\"%[1]s\".\"%[2]s\".\"${snowflake_view.test.name}\"" + object_name = snowflake_view.test.fully_qualified_name } } data "snowflake_grants" "grants" { depends_on = [snowflake_grant_privileges_to_account_role.grant, snowflake_view.test] grants_on { - object_name = "\"%[1]s\".\"%[2]s\".\"${snowflake_view.test.name}\"" object_type = "VIEW" + object_name = snowflake_view.test.fully_qualified_name } } - `, viewId.DatabaseName(), viewId.SchemaName(), tableId.Name(), viewId.Name(), selectStatement) + `, viewId.DatabaseName(), viewId.SchemaName(), tableId.Name(), viewId.Name(), selectStatement, roleId.FullyQualifiedName(), copyGrants) } func viewConfigWithMultilineUnionStatement(id sdk.SchemaObjectIdentifier, part1 string, part2 string) string { diff --git a/pkg/schemas/stream.go b/pkg/schemas/stream.go index fc49e25a71..9b80bc9c07 100644 --- a/pkg/schemas/stream.go +++ b/pkg/schemas/stream.go @@ -1,5 +1,3 @@ -// Code generated by sdk-to-schema generator; DO NOT EDIT. - package schemas import ( @@ -56,7 +54,7 @@ var DescribeStreamSchema = map[string]*schema.Schema{ Computed: true, }, "stale": { - Type: schema.TypeString, + Type: schema.TypeBool, Computed: true, }, "mode": { @@ -79,6 +77,7 @@ var DescribeStreamSchema = map[string]*schema.Schema{ var _ = ShowStreamSchema +// TODO(SNOW-1733130): Remove the logic for stage handling. Use schema object identifiers in the schema. func StreamDescriptionToSchema(stream sdk.Stream) map[string]any { streamSchema := make(map[string]any) streamSchema["created_on"] = stream.CreatedOn.String() @@ -92,25 +91,38 @@ func StreamDescriptionToSchema(stream sdk.Stream) map[string]any { streamSchema["comment"] = stream.Comment } if stream.TableName != nil { - tableId, err := sdk.ParseSchemaObjectIdentifier(*stream.TableName) - if err != nil { - log.Printf("[DEBUG] could not parse table ID: %v", err) + if stream.SourceType != nil && *stream.SourceType == sdk.StreamSourceTypeStage { + streamSchema["table_name"] = *stream.TableName } else { - streamSchema["table_name"] = tableId.FullyQualifiedName() + tableId, err := sdk.ParseSchemaObjectIdentifier(*stream.TableName) + if err != nil { + log.Printf("[DEBUG] could not parse table ID: %v", err) + } else { + streamSchema["table_name"] = tableId.FullyQualifiedName() + } } } if stream.SourceType != nil { streamSchema["source_type"] = stream.SourceType } if stream.BaseTables != nil { - streamSchema["base_tables"] = collections.Map(stream.BaseTables, sdk.SchemaObjectIdentifier.FullyQualifiedName) + if stream.SourceType != nil && *stream.SourceType == sdk.StreamSourceTypeStage { + streamSchema["base_tables"] = stream.BaseTables + } else { + streamSchema["base_tables"] = collections.Map(stream.BaseTables, func(s string) string { + id, err := sdk.ParseSchemaObjectIdentifier(s) + if err != nil { + log.Printf("[DEBUG] could not parse base table ID: %v", err) + return "" + } + return id.FullyQualifiedName() + }) + } } if stream.Type != nil { streamSchema["type"] = stream.Type } - if stream.Stale != nil { - streamSchema["stale"] = stream.Stale - } + streamSchema["stale"] = stream.Stale if stream.Mode != nil { streamSchema["mode"] = stream.Mode } diff --git a/pkg/schemas/stream_gen.go b/pkg/schemas/stream_gen.go index bd9bed5c85..73af3de9c5 100644 --- a/pkg/schemas/stream_gen.go +++ b/pkg/schemas/stream_gen.go @@ -56,7 +56,7 @@ var ShowStreamSchema = map[string]*schema.Schema{ Computed: true, }, "stale": { - Type: schema.TypeString, + Type: schema.TypeBool, Computed: true, }, "mode": { @@ -80,6 +80,7 @@ var ShowStreamSchema = map[string]*schema.Schema{ var _ = ShowStreamSchema // Adjusted manually. +// TODO(SNOW-1733130): Remove the logic for stage handling. Use schema object identifiers in the schema. func StreamToSchema(stream *sdk.Stream) map[string]any { streamSchema := make(map[string]any) streamSchema["created_on"] = stream.CreatedOn.String() @@ -93,25 +94,38 @@ func StreamToSchema(stream *sdk.Stream) map[string]any { streamSchema["comment"] = stream.Comment } if stream.TableName != nil { - tableId, err := sdk.ParseSchemaObjectIdentifier(*stream.TableName) - if err != nil { - log.Printf("[DEBUG] could not parse table ID: %v", err) + if stream.SourceType != nil && *stream.SourceType == sdk.StreamSourceTypeStage { + streamSchema["table_name"] = *stream.TableName } else { - streamSchema["table_name"] = tableId.FullyQualifiedName() + tableId, err := sdk.ParseSchemaObjectIdentifier(*stream.TableName) + if err != nil { + log.Printf("[DEBUG] could not parse table ID: %v", err) + } else { + streamSchema["table_name"] = tableId.FullyQualifiedName() + } } } if stream.SourceType != nil { streamSchema["source_type"] = stream.SourceType } if stream.BaseTables != nil { - streamSchema["base_tables"] = collections.Map(stream.BaseTables, sdk.SchemaObjectIdentifier.FullyQualifiedName) + if stream.SourceType != nil && *stream.SourceType == sdk.StreamSourceTypeStage { + streamSchema["base_tables"] = stream.BaseTables + } else { + streamSchema["base_tables"] = collections.Map(stream.BaseTables, func(s string) string { + id, err := sdk.ParseSchemaObjectIdentifier(s) + if err != nil { + log.Printf("[DEBUG] could not parse base table ID: %v", err) + return "" + } + return id.FullyQualifiedName() + }) + } } if stream.Type != nil { streamSchema["type"] = stream.Type } - if stream.Stale != nil { - streamSchema["stale"] = stream.Stale - } + streamSchema["stale"] = stream.Stale if stream.Mode != nil { streamSchema["mode"] = stream.Mode } diff --git a/pkg/sdk/streams_def.go b/pkg/sdk/streams_def.go index 53a612d624..a67098574f 100644 --- a/pkg/sdk/streams_def.go +++ b/pkg/sdk/streams_def.go @@ -76,7 +76,7 @@ var ( Field("source_type", "sql.NullString"). Field("base_tables", "sql.NullString"). Field("type", "sql.NullString"). - Field("stale", "sql.NullString"). + Field("stale", "string"). Field("mode", "sql.NullString"). Field("stale_after", "sql.NullTime"). Field("invalid_reason", "sql.NullString"). @@ -91,9 +91,9 @@ var ( Field("Comment", "*string"). Field("TableName", "*string"). Field("SourceType", "*StreamSourceType"). - Field("BaseTables", "[]SchemaObjectIdentifier"). + Field("BaseTables", "[]string"). Field("Type", "*string"). - Field("Stale", "*string"). + Field("Stale", "bool"). Field("Mode", "*StreamMode"). Field("StaleAfter", "*time.Time"). Field("InvalidReason", "*string"). diff --git a/pkg/sdk/streams_gen.go b/pkg/sdk/streams_gen.go index 41e2eef921..10ba15c344 100644 --- a/pkg/sdk/streams_gen.go +++ b/pkg/sdk/streams_gen.go @@ -148,7 +148,7 @@ type showStreamsDbRow struct { SourceType sql.NullString `db:"source_type"` BaseTables sql.NullString `db:"base_tables"` Type sql.NullString `db:"type"` - Stale sql.NullString `db:"stale"` + Stale string `db:"stale"` Mode sql.NullString `db:"mode"` StaleAfter sql.NullTime `db:"stale_after"` InvalidReason sql.NullString `db:"invalid_reason"` @@ -164,9 +164,9 @@ type Stream struct { Comment *string TableName *string SourceType *StreamSourceType - BaseTables []SchemaObjectIdentifier + BaseTables []string Type *string - Stale *string + Stale bool Mode *StreamMode StaleAfter *time.Time InvalidReason *string diff --git a/pkg/sdk/streams_impl_gen.go b/pkg/sdk/streams_impl_gen.go index 2299666164..6612925b60 100644 --- a/pkg/sdk/streams_impl_gen.go +++ b/pkg/sdk/streams_impl_gen.go @@ -229,6 +229,7 @@ func (r showStreamsDbRow) convert() *Stream { Name: r.Name, DatabaseName: r.DatabaseName, SchemaName: r.SchemaName, + Stale: r.Stale == "true", } if r.StaleAfter.Valid { s.StaleAfter = &r.StaleAfter.Time @@ -251,19 +252,11 @@ func (r showStreamsDbRow) convert() *Stream { } } if r.BaseTables.Valid { - baseTables, err := ParseCommaSeparatedSchemaObjectIdentifierArray(r.BaseTables.String) - if err != nil { - log.Printf("[DEBUG] error converting show stream: %v", err) - } else { - s.BaseTables = baseTables - } + s.BaseTables = ParseCommaSeparatedStringArray(r.BaseTables.String, false) } if r.Type.Valid { s.Type = &r.Type.String } - if r.Stale.Valid { - s.Stale = &r.Stale.String - } if r.Mode.Valid { mode, err := ToStreamMode(r.Mode.String) if err != nil { diff --git a/pkg/sdk/testint/errors_integration_test.go b/pkg/sdk/testint/errors_integration_test.go new file mode 100644 index 0000000000..4d01e1f44c --- /dev/null +++ b/pkg/sdk/testint/errors_integration_test.go @@ -0,0 +1,146 @@ +package testint + +import ( + "context" + "testing" + + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/sdk" + "github.com/stretchr/testify/assert" +) + +func schemaObjectShowByIDWrapper[T any](showByIdFn func(context.Context, sdk.SchemaObjectIdentifier) (*T, error)) func(context.Context, sdk.SchemaObjectIdentifier) error { + return func(ctx context.Context, id sdk.SchemaObjectIdentifier) error { + _, err := showByIdFn(ctx, id) + return err + } +} + +func schemaObjectWithArgumentsShowByIDWrapper[T any](showByIdFn func(context.Context, sdk.SchemaObjectIdentifierWithArguments) (*T, error)) func(context.Context, sdk.SchemaObjectIdentifierWithArguments) error { + return func(ctx context.Context, id sdk.SchemaObjectIdentifierWithArguments) error { + _, err := showByIdFn(ctx, id) + return err + } +} + +func TestInt_ShowSchemaObjectInNonExistingDatabase(t *testing.T) { + doesNotExistOrNotAuthorized := sdk.ErrObjectNotExistOrAuthorized.Error() // Database '\"non-existing-database\"' does not exist or not authorized + doesNotExistOrOperationCannotBePerformed := "Object does not exist, or operation cannot be performed" + + testCases := []struct { + ObjectType sdk.ObjectType + ExpectedErr string + ShowFn func(context.Context, sdk.SchemaObjectIdentifier) error + }{ + // Only object types that use IN SCHEMA in their ShowByID implementation + {ObjectType: sdk.ObjectTypeTable, ExpectedErr: doesNotExistOrOperationCannotBePerformed, ShowFn: schemaObjectShowByIDWrapper(testClient(t).Tables.ShowByID)}, + {ObjectType: sdk.ObjectTypeDynamicTable, ExpectedErr: doesNotExistOrOperationCannotBePerformed, ShowFn: schemaObjectShowByIDWrapper(testClient(t).DynamicTables.ShowByID)}, + {ObjectType: sdk.ObjectTypeCortexSearchService, ExpectedErr: doesNotExistOrNotAuthorized, ShowFn: schemaObjectShowByIDWrapper(testClient(t).CortexSearchServices.ShowByID)}, + {ObjectType: sdk.ObjectTypeExternalTable, ExpectedErr: doesNotExistOrOperationCannotBePerformed, ShowFn: schemaObjectShowByIDWrapper(testClient(t).ExternalTables.ShowByID)}, + {ObjectType: sdk.ObjectTypeEventTable, ExpectedErr: doesNotExistOrOperationCannotBePerformed, ShowFn: schemaObjectShowByIDWrapper(testClient(t).EventTables.ShowByID)}, + {ObjectType: sdk.ObjectTypeView, ExpectedErr: doesNotExistOrOperationCannotBePerformed, ShowFn: schemaObjectShowByIDWrapper(testClient(t).Views.ShowByID)}, + {ObjectType: sdk.ObjectTypeMaterializedView, ExpectedErr: doesNotExistOrNotAuthorized, ShowFn: schemaObjectShowByIDWrapper(testClient(t).MaterializedViews.ShowByID)}, + {ObjectType: sdk.ObjectTypeSequence, ExpectedErr: doesNotExistOrNotAuthorized, ShowFn: schemaObjectShowByIDWrapper(testClient(t).Sequences.ShowByID)}, + {ObjectType: sdk.ObjectTypeStream, ExpectedErr: doesNotExistOrNotAuthorized, ShowFn: schemaObjectShowByIDWrapper(testClient(t).Streams.ShowByID)}, + {ObjectType: sdk.ObjectTypeTask, ExpectedErr: doesNotExistOrNotAuthorized, ShowFn: schemaObjectShowByIDWrapper(testClient(t).Tasks.ShowByID)}, + {ObjectType: sdk.ObjectTypeMaskingPolicy, ExpectedErr: doesNotExistOrNotAuthorized, ShowFn: schemaObjectShowByIDWrapper(testClient(t).MaskingPolicies.ShowByID)}, + {ObjectType: sdk.ObjectTypeRowAccessPolicy, ExpectedErr: doesNotExistOrNotAuthorized, ShowFn: schemaObjectShowByIDWrapper(testClient(t).RowAccessPolicies.ShowByID)}, + {ObjectType: sdk.ObjectTypeTag, ExpectedErr: doesNotExistOrNotAuthorized, ShowFn: schemaObjectShowByIDWrapper(testClient(t).Tags.ShowByID)}, + {ObjectType: sdk.ObjectTypeSecret, ExpectedErr: doesNotExistOrNotAuthorized, ShowFn: schemaObjectShowByIDWrapper(testClient(t).Secrets.ShowByID)}, + {ObjectType: sdk.ObjectTypeStage, ExpectedErr: doesNotExistOrOperationCannotBePerformed, ShowFn: schemaObjectShowByIDWrapper(testClient(t).Stages.ShowByID)}, + {ObjectType: sdk.ObjectTypeFileFormat, ExpectedErr: doesNotExistOrOperationCannotBePerformed, ShowFn: schemaObjectShowByIDWrapper(testClient(t).FileFormats.ShowByID)}, + {ObjectType: sdk.ObjectTypePipe, ExpectedErr: doesNotExistOrNotAuthorized, ShowFn: schemaObjectShowByIDWrapper(testClient(t).Pipes.ShowByID)}, + {ObjectType: sdk.ObjectTypeAlert, ExpectedErr: doesNotExistOrNotAuthorized, ShowFn: schemaObjectShowByIDWrapper(testClient(t).Alerts.ShowByID)}, + {ObjectType: sdk.ObjectTypeStreamlit, ExpectedErr: doesNotExistOrOperationCannotBePerformed, ShowFn: schemaObjectShowByIDWrapper(testClient(t).Streamlits.ShowByID)}, + {ObjectType: sdk.ObjectTypeNetworkRule, ExpectedErr: doesNotExistOrOperationCannotBePerformed, ShowFn: schemaObjectShowByIDWrapper(testClient(t).NetworkRules.ShowByID)}, + {ObjectType: sdk.ObjectTypeAuthenticationPolicy, ExpectedErr: doesNotExistOrNotAuthorized, ShowFn: schemaObjectShowByIDWrapper(testClient(t).AuthenticationPolicies.ShowByID)}, + } + + for _, tt := range testCases { + t.Run(tt.ObjectType.String(), func(t *testing.T) { + ctx := context.Background() + err := tt.ShowFn(ctx, sdk.NewSchemaObjectIdentifier("non-existing-database", "non-existing-schema", "non-existing-schema-object")) + assert.ErrorContains(t, err, tt.ExpectedErr) + }) + } + + schemaObjectWithArgumentsTestCases := []struct { + ObjectType sdk.ObjectType + ExpectedErr string + ShowFn func(context.Context, sdk.SchemaObjectIdentifierWithArguments) error + }{ + {ObjectType: sdk.ObjectTypeFunction, ExpectedErr: doesNotExistOrNotAuthorized, ShowFn: schemaObjectWithArgumentsShowByIDWrapper(testClient(t).Functions.ShowByID)}, + {ObjectType: sdk.ObjectTypeExternalFunction, ExpectedErr: doesNotExistOrNotAuthorized, ShowFn: schemaObjectWithArgumentsShowByIDWrapper(testClient(t).ExternalFunctions.ShowByID)}, + {ObjectType: sdk.ObjectTypeProcedure, ExpectedErr: doesNotExistOrNotAuthorized, ShowFn: schemaObjectWithArgumentsShowByIDWrapper(testClient(t).Procedures.ShowByID)}, + } + + for _, tt := range schemaObjectWithArgumentsTestCases { + t.Run(tt.ObjectType.String(), func(t *testing.T) { + ctx := context.Background() + err := tt.ShowFn(ctx, sdk.NewSchemaObjectIdentifierWithArguments("non-existing-database", "non-existing-schema", "non-existing-schema-object")) + assert.ErrorContains(t, err, tt.ExpectedErr) + }) + } +} + +func TestInt_ShowSchemaObjectInNonExistingSchema(t *testing.T) { + doesNotExistOrNotAuthorized := sdk.ErrObjectNotExistOrAuthorized.Error() // Schema '\"non-existing-schema\"' does not exist or not authorized + doesNotExistOrOperationCannotBePerformed := "Object does not exist, or operation cannot be performed" + + database, databaseCleanup := testClientHelper().Database.CreateDatabase(t) + t.Cleanup(databaseCleanup) + + testCases := []struct { + ObjectType sdk.ObjectType + ExpectedErr string + ShowFn func(context.Context, sdk.SchemaObjectIdentifier) error + }{ + // Only object types that use IN SCHEMA in their ShowByID implementation + {ObjectType: sdk.ObjectTypeTable, ExpectedErr: doesNotExistOrOperationCannotBePerformed, ShowFn: schemaObjectShowByIDWrapper(testClient(t).Tables.ShowByID)}, + {ObjectType: sdk.ObjectTypeDynamicTable, ExpectedErr: doesNotExistOrOperationCannotBePerformed, ShowFn: schemaObjectShowByIDWrapper(testClient(t).DynamicTables.ShowByID)}, + {ObjectType: sdk.ObjectTypeCortexSearchService, ExpectedErr: doesNotExistOrNotAuthorized, ShowFn: schemaObjectShowByIDWrapper(testClient(t).CortexSearchServices.ShowByID)}, + {ObjectType: sdk.ObjectTypeExternalTable, ExpectedErr: doesNotExistOrOperationCannotBePerformed, ShowFn: schemaObjectShowByIDWrapper(testClient(t).ExternalTables.ShowByID)}, + {ObjectType: sdk.ObjectTypeEventTable, ExpectedErr: doesNotExistOrOperationCannotBePerformed, ShowFn: schemaObjectShowByIDWrapper(testClient(t).EventTables.ShowByID)}, + {ObjectType: sdk.ObjectTypeView, ExpectedErr: doesNotExistOrOperationCannotBePerformed, ShowFn: schemaObjectShowByIDWrapper(testClient(t).Views.ShowByID)}, + {ObjectType: sdk.ObjectTypeMaterializedView, ExpectedErr: doesNotExistOrNotAuthorized, ShowFn: schemaObjectShowByIDWrapper(testClient(t).MaterializedViews.ShowByID)}, + {ObjectType: sdk.ObjectTypeSequence, ExpectedErr: doesNotExistOrNotAuthorized, ShowFn: schemaObjectShowByIDWrapper(testClient(t).Sequences.ShowByID)}, + {ObjectType: sdk.ObjectTypeStream, ExpectedErr: doesNotExistOrNotAuthorized, ShowFn: schemaObjectShowByIDWrapper(testClient(t).Streams.ShowByID)}, + {ObjectType: sdk.ObjectTypeTask, ExpectedErr: doesNotExistOrNotAuthorized, ShowFn: schemaObjectShowByIDWrapper(testClient(t).Tasks.ShowByID)}, + {ObjectType: sdk.ObjectTypeMaskingPolicy, ExpectedErr: doesNotExistOrNotAuthorized, ShowFn: schemaObjectShowByIDWrapper(testClient(t).MaskingPolicies.ShowByID)}, + {ObjectType: sdk.ObjectTypeRowAccessPolicy, ExpectedErr: doesNotExistOrNotAuthorized, ShowFn: schemaObjectShowByIDWrapper(testClient(t).RowAccessPolicies.ShowByID)}, + {ObjectType: sdk.ObjectTypeTag, ExpectedErr: doesNotExistOrNotAuthorized, ShowFn: schemaObjectShowByIDWrapper(testClient(t).Tags.ShowByID)}, + {ObjectType: sdk.ObjectTypeSecret, ExpectedErr: doesNotExistOrNotAuthorized, ShowFn: schemaObjectShowByIDWrapper(testClient(t).Secrets.ShowByID)}, + {ObjectType: sdk.ObjectTypeStage, ExpectedErr: doesNotExistOrOperationCannotBePerformed, ShowFn: schemaObjectShowByIDWrapper(testClient(t).Stages.ShowByID)}, + {ObjectType: sdk.ObjectTypeFileFormat, ExpectedErr: doesNotExistOrOperationCannotBePerformed, ShowFn: schemaObjectShowByIDWrapper(testClient(t).FileFormats.ShowByID)}, + {ObjectType: sdk.ObjectTypePipe, ExpectedErr: doesNotExistOrNotAuthorized, ShowFn: schemaObjectShowByIDWrapper(testClient(t).Pipes.ShowByID)}, + {ObjectType: sdk.ObjectTypeAlert, ExpectedErr: doesNotExistOrNotAuthorized, ShowFn: schemaObjectShowByIDWrapper(testClient(t).Alerts.ShowByID)}, + {ObjectType: sdk.ObjectTypeStreamlit, ExpectedErr: doesNotExistOrOperationCannotBePerformed, ShowFn: schemaObjectShowByIDWrapper(testClient(t).Streamlits.ShowByID)}, + {ObjectType: sdk.ObjectTypeNetworkRule, ExpectedErr: doesNotExistOrOperationCannotBePerformed, ShowFn: schemaObjectShowByIDWrapper(testClient(t).NetworkRules.ShowByID)}, + {ObjectType: sdk.ObjectTypeAuthenticationPolicy, ExpectedErr: doesNotExistOrNotAuthorized, ShowFn: schemaObjectShowByIDWrapper(testClient(t).AuthenticationPolicies.ShowByID)}, + } + + for _, tt := range testCases { + t.Run(tt.ObjectType.String(), func(t *testing.T) { + ctx := context.Background() + err := tt.ShowFn(ctx, sdk.NewSchemaObjectIdentifier(database.ID().Name(), "non-existing-schema", "non-existing-schema-object")) + assert.ErrorContains(t, err, tt.ExpectedErr) + }) + } + + schemaObjectWithArgumentsTestCases := []struct { + ObjectType sdk.ObjectType + ExpectedErr string + ShowFn func(context.Context, sdk.SchemaObjectIdentifierWithArguments) error + }{ + {ObjectType: sdk.ObjectTypeFunction, ExpectedErr: doesNotExistOrNotAuthorized, ShowFn: schemaObjectWithArgumentsShowByIDWrapper(testClient(t).Functions.ShowByID)}, + {ObjectType: sdk.ObjectTypeExternalFunction, ExpectedErr: doesNotExistOrNotAuthorized, ShowFn: schemaObjectWithArgumentsShowByIDWrapper(testClient(t).ExternalFunctions.ShowByID)}, + {ObjectType: sdk.ObjectTypeProcedure, ExpectedErr: doesNotExistOrNotAuthorized, ShowFn: schemaObjectWithArgumentsShowByIDWrapper(testClient(t).Procedures.ShowByID)}, + } + + for _, tt := range schemaObjectWithArgumentsTestCases { + t.Run(tt.ObjectType.String(), func(t *testing.T) { + ctx := context.Background() + err := tt.ShowFn(ctx, sdk.NewSchemaObjectIdentifierWithArguments(database.ID().Name(), "non-existing-schema", "non-existing-schema-object")) + assert.ErrorContains(t, err, tt.ExpectedErr) + }) + } +} diff --git a/pkg/sdk/testint/streams_gen_integration_test.go b/pkg/sdk/testint/streams_gen_integration_test.go index 79f30b23c6..2e99ca4f05 100644 --- a/pkg/sdk/testint/streams_gen_integration_test.go +++ b/pkg/sdk/testint/streams_gen_integration_test.go @@ -153,7 +153,8 @@ func TestInt_Streams(t *testing.T) { HasComment("some comment"). HasSourceType(sdk.StreamSourceTypeStage). HasMode(sdk.StreamModeDefault). - HasStageName(stage.ID().Name()), + HasStageName(stage.ID().Name()). + HasBaseTablesPartiallyQualified(stage.ID().Name()), ) }) diff --git a/templates/guides/identifiers.md.tmpl b/templates/guides/identifiers.md.tmpl index 1813be44e0..0af6936acf 100644 --- a/templates/guides/identifiers.md.tmpl +++ b/templates/guides/identifiers.md.tmpl @@ -15,7 +15,7 @@ For example, instead of writing ``` object_name = “\”${snowflake_table.database}\”.\”${snowflake_table.schema}\”.\”${snowflake_table.name}\”” # for procedures -object_name = “\”${snowflake_procedure.database}\”.\”${snowflake_procedure.schema}\”.\”${snowflake_procedure.name}(NUMBER, VARCHAR)\”” +object_name = “\”${snowflake_procedure.database}\”.\”${snowflake_procedure.schema}\”.\”${snowflake_procedure.name}\"(NUMBER, VARCHAR)” ``` now we can write diff --git a/templates/resources/stream_on_directory_table.md.tmpl b/templates/resources/stream_on_directory_table.md.tmpl new file mode 100644 index 0000000000..be9cb3fb69 --- /dev/null +++ b/templates/resources/stream_on_directory_table.md.tmpl @@ -0,0 +1,35 @@ +--- +page_title: "{{.Name}} {{.Type}} - {{.ProviderName}}" +subcategory: "" +description: |- +{{ if gt (len (split .Description "")) 1 -}} +{{ index (split .Description "") 1 | plainmarkdown | trimspace | prefixlines " " }} +{{- else -}} +{{ .Description | plainmarkdown | trimspace | prefixlines " " }} +{{- end }} +--- + +!> **V1 release candidate** This resource was reworked and is a release candidate for the V1. We do not expect significant changes in it before the V1. We will welcome any feedback and adjust the resource if needed. Any errors reported will be resolved with a higher priority. We encourage checking this resource out before the V1 release. Please follow the [migration guide](https://github.com/Snowflake-Labs/terraform-provider-snowflake/blob/main/MIGRATION_GUIDE.md#v0970--v0980) to use it. + +# {{.Name}} ({{.Type}}) + +{{ .Description | trimspace }} + +{{ if .HasExample -}} +## Example Usage + +{{ tffile (printf "examples/resources/%s/resource.tf" .Name)}} +-> **Note** Instead of using fully_qualified_name, you can reference objects managed outside Terraform by constructing a correct ID, consult [identifiers guide](https://registry.terraform.io/providers/Snowflake-Labs/snowflake/latest/docs/guides/identifiers#new-computed-fully-qualified-name-field-in-resources). + + +{{- end }} + +{{ .SchemaMarkdown | trimspace }} +{{- if .HasImport }} + +## Import + +Import is supported using the following syntax: + +{{ codefile "shell" (printf "examples/resources/%s/import.sh" .Name)}} +{{- end }} diff --git a/v1-preparations/REMAINING_GA_OBJECTS.MD b/v1-preparations/REMAINING_GA_OBJECTS.MD index 06a8527927..8c8c01ce83 100644 --- a/v1-preparations/REMAINING_GA_OBJECTS.MD +++ b/v1-preparations/REMAINING_GA_OBJECTS.MD @@ -17,7 +17,7 @@ Known issues lists open issues touching the given object. Note that some of thes | API INTEGRATION | ❌ | [#2772](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/2772), [#1445](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/1445) | | APPLICATION | ❌ | - | | APPLICATION PACKAGE | ❌ | - | -| APPLICATION ROLE | ❌ | - | +| APPLICATION ROLE | ❌ | [#3134](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/3134) - | | CONNECTION | ❌ | - | | EXTERNAL ACCESS INTEGRATION | ❌ | [#2546](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/2546) | | FAILOVER GROUP | ❌ | [#2516](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/2516), [#2332](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/2332), [#1418](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/1418) | @@ -31,7 +31,7 @@ Known issues lists open issues touching the given object. Note that some of thes | EVENT TABLE | ❌ | [#1888](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/1888) | | EXTERNAL FUNCTION | ❌ | [#1901](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/1901) | | EXTERNAL TABLE | ❌ | [#2881](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/2881), [#1564](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/1564), [#1537](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/1537), [#1416](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/1416), [#1040](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/1040) | -| FILE FORMAT | ❌ | [#2154](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/2154), [#1984](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/1984), [#1820](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/1820), [#1760](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/1760), [#1614](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/1614), [#1613](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/1613), [#1609](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/1609), [#1461](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/1461) | +| FILE FORMAT | ❌ | [#3115](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/3115), [#2154](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/2154), [#1984](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/1984), [#1820](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/1820), [#1760](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/1760), [#1614](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/1614), [#1613](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/1613), [#1609](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/1609), [#1461](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/1461) | | MATERIALIZED VIEW | ❌ | [#2397](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/2397), [#1218](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/1218) | | NETWORK RULE | ❌ | [#2593](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/2593), [#2482](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/2482) | | PACKAGES POLICY | ❌ | - |