From 72901b993a99d452eb0353f8466b22383e970208 Mon Sep 17 00:00:00 2001 From: Jakub Michalak Date: Tue, 6 Aug 2024 12:02:02 +0200 Subject: [PATCH 01/14] Init --- MIGRATION_GUIDE.md | 15 +- docs/resources/streamlit.md | 2 +- docs/resources/view.md | 119 ++- examples/resources/snowflake_view/resource.tf | 44 +- .../resourceassert/gen/resource_schema_def.go | 4 + .../resourceassert/view_resource_gen.go | 197 +++++ .../model/gen/templates/definition.tmpl | 12 +- .../config/model/view_model_gen.go | 211 +++++ .../helpers/row_access_policy_client.go | 7 +- pkg/resources/common.go | 33 + pkg/resources/streamlit.go | 2 +- .../tag_masking_policy_association.go | 19 +- .../testdata/TestAcc_View/basic/test.tf | 6 + .../1 => TestAcc_View/basic}/variables.tf | 8 - .../TestAcc_View/basic_update/test.tf | 16 + .../TestAcc_View/basic_update/variables.tf | 35 + .../testdata/TestAcc_View/complete/test.tf | 21 + .../complete}/variables.tf | 24 + .../testdata/TestAcc_View_Tags/1/test.tf | 28 - .../testdata/TestAcc_View_Tags/2/test.tf | 28 - .../testdata/TestAcc_View_Tags/2/variables.tf | 23 - .../testdata/TestAcc_View_basic/test.tf | 10 - pkg/resources/view.go | 722 ++++++++++++++---- pkg/resources/view_acceptance_test.go | 628 ++++++++++----- pkg/resources/view_stage_upgraders.go | 20 + pkg/schemas/view.go | 78 ++ pkg/sdk/policy_references.go | 8 + pkg/sdk/testint/views_gen_integration_test.go | 24 +- pkg/sdk/views_def.go | 18 +- pkg/sdk/views_dto_builders_gen.go | 12 +- pkg/sdk/views_dto_gen.go | 12 +- pkg/sdk/views_gen.go | 29 +- pkg/sdk/views_gen_test.go | 24 +- pkg/snowflake/parser.go | 55 +- pkg/snowflake/parser_test.go | 11 +- templates/resources/view.md.tmpl | 32 + 36 files changed, 1989 insertions(+), 548 deletions(-) create mode 100644 pkg/acceptance/bettertestspoc/assert/resourceassert/view_resource_gen.go create mode 100644 pkg/acceptance/bettertestspoc/config/model/view_model_gen.go create mode 100644 pkg/resources/testdata/TestAcc_View/basic/test.tf rename pkg/resources/testdata/{TestAcc_View_Tags/1 => TestAcc_View/basic}/variables.tf (65%) create mode 100644 pkg/resources/testdata/TestAcc_View/basic_update/test.tf create mode 100644 pkg/resources/testdata/TestAcc_View/basic_update/variables.tf create mode 100644 pkg/resources/testdata/TestAcc_View/complete/test.tf rename pkg/resources/testdata/{TestAcc_View_basic => TestAcc_View/complete}/variables.tf (50%) delete mode 100644 pkg/resources/testdata/TestAcc_View_Tags/1/test.tf delete mode 100644 pkg/resources/testdata/TestAcc_View_Tags/2/test.tf delete mode 100644 pkg/resources/testdata/TestAcc_View_Tags/2/variables.tf delete mode 100644 pkg/resources/testdata/TestAcc_View_basic/test.tf create mode 100644 pkg/resources/view_stage_upgraders.go create mode 100644 pkg/schemas/view.go create mode 100644 templates/resources/view.md.tmpl diff --git a/MIGRATION_GUIDE.md b/MIGRATION_GUIDE.md index 30898f1c5b..c1b8c578fc 100644 --- a/MIGRATION_GUIDE.md +++ b/MIGRATION_GUIDE.md @@ -6,7 +6,20 @@ across different versions. ## v0.94.x ➞ v0.95.0 -### snowflake_warehouse resource changes +### snowflake_view resource changes +New fields: + - `row_access_policy` + - `aggregation_policy` + - `change_tracking` +- added `show_output` field that holds the response from SHOW VIEWS. +- added `describe_output` field that holds the response from DESCRIBE VIEW. Note that one needs to grant sufficient privileges e.g. with [grant_ownership](https://registry.terraform.io/providers/Snowflake-Labs/snowflake/latest/docs/resources/grant_ownership) on the tables used in this view. Otherwise, this field is not filled. + +#### *(breaking change)* Removed fields from snowflake_view resource +Removed fields: +- `tag` +The value of this field will be removed from the state automatically. Please, use [tag_association](https://registry.terraform.io/providers/Snowflake-Labs/snowflake/latest/docs/resources/tag_association) instead. + +### snowflake_user resource changes #### *(breaking change)* user parameters added to snowflake_user resource diff --git a/docs/resources/streamlit.md b/docs/resources/streamlit.md index 8071ba7bb5..31ecf669dc 100644 --- a/docs/resources/streamlit.md +++ b/docs/resources/streamlit.md @@ -60,7 +60,7 @@ resource "snowflake_streamlit" "streamlit" { - `describe_output` (List of Object) Outputs the result of `DESCRIBE STREAMLIT` for the given streamlit. (see [below for nested schema](#nestedatt--describe_output)) - `id` (String) The ID of this resource. -- `show_output` (List of Object) Outputs the result of `SHOW STREAMLIT` for the given streamli. (see [below for nested schema](#nestedatt--show_output)) +- `show_output` (List of Object) Outputs the result of `SHOW STREAMLIT` for the given streamlit. (see [below for nested schema](#nestedatt--show_output)) ### Nested Schema for `describe_output` diff --git a/docs/resources/view.md b/docs/resources/view.md index 04d41b8c03..f34ebeafb9 100644 --- a/docs/resources/view.md +++ b/docs/resources/view.md @@ -2,28 +2,58 @@ page_title: "snowflake_view Resource - terraform-provider-snowflake" subcategory: "" description: |- - + Resource used to manage view objects. For more information, check view documentation https://docs.snowflake.com/en/sql-reference/sql/create-view. --- -# snowflake_view (Resource) +!> **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#v094x--v0950) to use it. +# snowflake_view (Resource) +Resource used to manage view objects. For more information, check [view documentation](https://docs.snowflake.com/en/sql-reference/sql/create-view). ## Example Usage ```terraform +# basic resource resource "snowflake_view" "view" { - database = "database" - schema = "schema" - name = "view" - - comment = "comment" + database = "database" + schema = "schema" + name = "view" + statement = <<-SQL + select * from foo; +SQL +} - statement = <<-SQL +# recursive view +resource "snowflake_view" "view" { + database = "database" + schema = "schema" + name = "view" + is_recursive = "true" + statement = <<-SQL select * from foo; SQL - or_replace = false - is_secure = false +} +# resource with attached policies +resource "snowflake_view" "test" { + database = "database" + schema = "schema" + name = "view" + comment = "comment" + is_secure = "true" + change_tracking = "true" + is_temporary = "true" + row_access_policy { + policy_name = "row_access_policy" + on = ["id"] + } + aggregation_policy { + policy_name = "aggregation_policy" + entity_key = ["id"] + } + statement = <<-SQL + select id from foo; +SQL } ``` @@ -39,29 +69,80 @@ SQL ### Optional +- `aggregation_policy` (Block List, Max: 1) Specifies the aggregation policy to set on a view. (see [below for nested schema](#nestedblock--aggregation_policy)) +- `change_tracking` (String) Specifies to enable or disable change tracking on the table. Available options are: "true" or "false". When the value is not set in the configuration the provider will put "default" there which means to use the Snowflake default for this value. - `comment` (String) Specifies a comment for the view. - `copy_grants` (Boolean) Retains the access permissions from the original view when a new view is created using the OR REPLACE clause. OR REPLACE must be set when COPY GRANTS is set. -- `is_secure` (Boolean) Specifies that the view is secure. By design, the Snowflake's `SHOW VIEWS` command does not provide information about secure views (consult [view usage notes](https://docs.snowflake.com/en/sql-reference/sql/create-view#usage-notes)) which is essential to manage/import view with Terraform. Use the role owning the view while managing secure views. +- `is_recursive` (String) Specifies that the view can refer to itself using recursive syntax without necessarily using a CTE (common table expression). Available options are: "true" or "false". When the value is not set in the configuration the provider will put "default" there which means to use the Snowflake default for this value. +- `is_secure` (String) Specifies that the view is secure. By design, the Snowflake's `SHOW VIEWS` command does not provide information about secure views (consult [view usage notes](https://docs.snowflake.com/en/sql-reference/sql/create-view#usage-notes)) which is essential to manage/import view with Terraform. Use the role owning the view while managing secure views. Available options are: "true" or "false". When the value is not set in the configuration the provider will put "default" there which means to use the Snowflake default for this value. +- `is_temporary` (String) Specifies that the view persists only for the duration of the session that you created it in. A temporary view and all its contents are dropped at the end of the session. Available options are: "true" or "false". When the value is not set in the configuration the provider will put "default" there which means to use the Snowflake default for this value. - `or_replace` (Boolean) Overwrites the View if it exists. -- `tag` (Block List, Deprecated) Definitions of a tag to associate with the resource. (see [below for nested schema](#nestedblock--tag)) +- `row_access_policy` (Block List) Specifies the row access policy to set on a view. (see [below for nested schema](#nestedblock--row_access_policy)) ### Read-Only -- `created_on` (String) The timestamp at which the view was created. +- `describe_output` (List of Object) Outputs the result of `DESCRIBE VIEW` for the given view. (see [below for nested schema](#nestedatt--describe_output)) - `id` (String) The ID of this resource. +- `show_output` (List of Object) Outputs the result of `SHOW VIEW` for the given view. (see [below for nested schema](#nestedatt--show_output)) - -### Nested Schema for `tag` + +### Nested Schema for `aggregation_policy` Required: -- `name` (String) Tag name, e.g. department. -- `value` (String) Tag value, e.g. marketing_info. +- `policy_name` (String) Aggregation policy name. Optional: -- `database` (String) Name of the database that the tag was created in. -- `schema` (String) Name of the schema that the tag was created in. +- `entity_key` (Set of String) Defines which columns uniquely identify an entity within the view. + + + +### Nested Schema for `row_access_policy` + +Required: + +- `on` (Set of String) Defines which columns are affected by the policy. +- `policy_name` (String) Row access policy name. + + + +### Nested Schema for `describe_output` + +Read-Only: + +- `check` (String) +- `comment` (String) +- `default` (String) +- `expression` (String) +- `is_nullable` (Boolean) +- `is_primary` (Boolean) +- `is_unique` (Boolean) +- `kind` (String) +- `name` (String) +- `policy_name` (String) +- `privacy_domain` (String) +- `type` (String) + + + +### Nested Schema for `show_output` + +Read-Only: + +- `change_tracking` (String) +- `comment` (String) +- `created_on` (String) +- `database_name` (String) +- `is_materialized` (Boolean) +- `is_secure` (Boolean) +- `kind` (String) +- `name` (String) +- `owner` (String) +- `owner_role_type` (String) +- `reserved` (String) +- `schema_name` (String) +- `text` (String) ## Import diff --git a/examples/resources/snowflake_view/resource.tf b/examples/resources/snowflake_view/resource.tf index afe7b9d94b..d4506dfcf7 100644 --- a/examples/resources/snowflake_view/resource.tf +++ b/examples/resources/snowflake_view/resource.tf @@ -1,13 +1,41 @@ +# basic resource resource "snowflake_view" "view" { - database = "database" - schema = "schema" - name = "view" - - comment = "comment" + database = "database" + schema = "schema" + name = "view" + statement = <<-SQL + select * from foo; +SQL +} - statement = <<-SQL +# recursive view +resource "snowflake_view" "view" { + database = "database" + schema = "schema" + name = "view" + is_recursive = "true" + statement = <<-SQL select * from foo; SQL - or_replace = false - is_secure = false +} +# resource with attached policies +resource "snowflake_view" "test" { + database = "database" + schema = "schema" + name = "view" + comment = "comment" + is_secure = "true" + change_tracking = "true" + is_temporary = "true" + row_access_policy { + policy_name = "row_access_policy" + on = ["id"] + } + aggregation_policy { + policy_name = "aggregation_policy" + entity_key = ["id"] + } + statement = <<-SQL + select id from foo; +SQL } 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 3e54aed96c..746291bbec 100644 --- a/pkg/acceptance/bettertestspoc/assert/resourceassert/gen/resource_schema_def.go +++ b/pkg/acceptance/bettertestspoc/assert/resourceassert/gen/resource_schema_def.go @@ -29,4 +29,8 @@ var allResourceSchemaDefs = []ResourceSchemaDef{ name: "User", schema: resources.User().Schema, }, + { + name: "View", + schema: resources.View().Schema, + }, } diff --git a/pkg/acceptance/bettertestspoc/assert/resourceassert/view_resource_gen.go b/pkg/acceptance/bettertestspoc/assert/resourceassert/view_resource_gen.go new file mode 100644 index 0000000000..ebac0c6239 --- /dev/null +++ b/pkg/acceptance/bettertestspoc/assert/resourceassert/view_resource_gen.go @@ -0,0 +1,197 @@ +// Code generated by assertions generator; DO NOT EDIT. + +package resourceassert + +import ( + "testing" + + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/acceptance/bettertestspoc/assert" +) + +type ViewResourceAssert struct { + *assert.ResourceAssert +} + +func ViewResource(t *testing.T, name string) *ViewResourceAssert { + t.Helper() + + return &ViewResourceAssert{ + ResourceAssert: assert.NewResourceAssert(name, "resource"), + } +} + +func ImportedViewResource(t *testing.T, id string) *ViewResourceAssert { + t.Helper() + + return &ViewResourceAssert{ + ResourceAssert: assert.NewImportedResourceAssert(id, "imported resource"), + } +} + +/////////////////////////////////// +// Attribute value string checks // +/////////////////////////////////// + +func (v *ViewResourceAssert) HasAggregationPolicyString(expected string) *ViewResourceAssert { + v.AddAssertion(assert.ValueSet("aggregation_policy", expected)) + return v +} + +func (v *ViewResourceAssert) HasChangeTrackingString(expected string) *ViewResourceAssert { + v.AddAssertion(assert.ValueSet("change_tracking", expected)) + return v +} + +func (v *ViewResourceAssert) HasColumnsString(expected string) *ViewResourceAssert { + v.AddAssertion(assert.ValueSet("columns", expected)) + return v +} + +func (v *ViewResourceAssert) HasCommentString(expected string) *ViewResourceAssert { + v.AddAssertion(assert.ValueSet("comment", expected)) + return v +} + +func (v *ViewResourceAssert) HasCopyGrantsString(expected string) *ViewResourceAssert { + v.AddAssertion(assert.ValueSet("copy_grants", expected)) + return v +} + +func (v *ViewResourceAssert) HasDataMetricFunctionsString(expected string) *ViewResourceAssert { + v.AddAssertion(assert.ValueSet("data_metric_functions", expected)) + return v +} + +func (v *ViewResourceAssert) HasDataMetricScheduleString(expected string) *ViewResourceAssert { + v.AddAssertion(assert.ValueSet("data_metric_schedule", expected)) + return v +} + +func (v *ViewResourceAssert) HasDatabaseString(expected string) *ViewResourceAssert { + v.AddAssertion(assert.ValueSet("database", expected)) + return v +} + +func (v *ViewResourceAssert) HasIsRecursiveString(expected string) *ViewResourceAssert { + v.AddAssertion(assert.ValueSet("is_recursive", expected)) + return v +} + +func (v *ViewResourceAssert) HasIsSecureString(expected string) *ViewResourceAssert { + v.AddAssertion(assert.ValueSet("is_secure", expected)) + return v +} + +func (v *ViewResourceAssert) HasIsTemporaryString(expected string) *ViewResourceAssert { + v.AddAssertion(assert.ValueSet("is_temporary", expected)) + return v +} + +func (v *ViewResourceAssert) HasNameString(expected string) *ViewResourceAssert { + v.AddAssertion(assert.ValueSet("name", expected)) + return v +} + +func (v *ViewResourceAssert) HasOrReplaceString(expected string) *ViewResourceAssert { + v.AddAssertion(assert.ValueSet("or_replace", expected)) + return v +} + +func (v *ViewResourceAssert) HasRowAccessPolicyString(expected string) *ViewResourceAssert { + v.AddAssertion(assert.ValueSet("row_access_policy", expected)) + return v +} + +func (v *ViewResourceAssert) HasSchemaString(expected string) *ViewResourceAssert { + v.AddAssertion(assert.ValueSet("schema", expected)) + return v +} + +func (v *ViewResourceAssert) HasStatementString(expected string) *ViewResourceAssert { + v.AddAssertion(assert.ValueSet("statement", expected)) + return v +} + +//////////////////////////// +// Attribute empty checks // +//////////////////////////// + +func (v *ViewResourceAssert) HasNoAggregationPolicy() *ViewResourceAssert { + v.AddAssertion(assert.ValueNotSet("aggregation_policy")) + return v +} + +func (v *ViewResourceAssert) HasNoChangeTracking() *ViewResourceAssert { + v.AddAssertion(assert.ValueNotSet("change_tracking")) + return v +} + +func (v *ViewResourceAssert) HasNoColumns() *ViewResourceAssert { + v.AddAssertion(assert.ValueNotSet("columns")) + return v +} + +func (v *ViewResourceAssert) HasNoComment() *ViewResourceAssert { + v.AddAssertion(assert.ValueNotSet("comment")) + return v +} + +func (v *ViewResourceAssert) HasNoCopyGrants() *ViewResourceAssert { + v.AddAssertion(assert.ValueNotSet("copy_grants")) + return v +} + +func (v *ViewResourceAssert) HasNoDataMetricFunctions() *ViewResourceAssert { + v.AddAssertion(assert.ValueNotSet("data_metric_functions")) + return v +} + +func (v *ViewResourceAssert) HasNoDataMetricSchedule() *ViewResourceAssert { + v.AddAssertion(assert.ValueNotSet("data_metric_schedule")) + return v +} + +func (v *ViewResourceAssert) HasNoDatabase() *ViewResourceAssert { + v.AddAssertion(assert.ValueNotSet("database")) + return v +} + +func (v *ViewResourceAssert) HasNoIsRecursive() *ViewResourceAssert { + v.AddAssertion(assert.ValueNotSet("is_recursive")) + return v +} + +func (v *ViewResourceAssert) HasNoIsSecure() *ViewResourceAssert { + v.AddAssertion(assert.ValueNotSet("is_secure")) + return v +} + +func (v *ViewResourceAssert) HasNoIsTemporary() *ViewResourceAssert { + v.AddAssertion(assert.ValueNotSet("is_temporary")) + return v +} + +func (v *ViewResourceAssert) HasNoName() *ViewResourceAssert { + v.AddAssertion(assert.ValueNotSet("name")) + return v +} + +func (v *ViewResourceAssert) HasNoOrReplace() *ViewResourceAssert { + v.AddAssertion(assert.ValueNotSet("or_replace")) + return v +} + +func (v *ViewResourceAssert) HasNoRowAccessPolicy() *ViewResourceAssert { + v.AddAssertion(assert.ValueNotSet("row_access_policy")) + return v +} + +func (v *ViewResourceAssert) HasNoSchema() *ViewResourceAssert { + v.AddAssertion(assert.ValueNotSet("schema")) + return v +} + +func (v *ViewResourceAssert) HasNoStatement() *ViewResourceAssert { + v.AddAssertion(assert.ValueNotSet("statement")) + return v +} diff --git a/pkg/acceptance/bettertestspoc/config/model/gen/templates/definition.tmpl b/pkg/acceptance/bettertestspoc/config/model/gen/templates/definition.tmpl index 76315ef0d7..274061be68 100644 --- a/pkg/acceptance/bettertestspoc/config/model/gen/templates/definition.tmpl +++ b/pkg/acceptance/bettertestspoc/config/model/gen/templates/definition.tmpl @@ -25,8 +25,10 @@ func {{ .Name }}( {{ $modelVar }} := &{{ $modelName }}{ResourceModelMeta: config.Meta(resourceName, resources.{{ .Name }})} {{ range .Attributes -}} {{- $attributeNameCamel := SnakeCaseToCamel .Name -}} - {{ if .Required }}{{ $modelVar }}.With{{ $attributeNameCamel }}({{ FirstLetterLowercase $attributeNameCamel }}) {{ end }} - {{- end }} + {{- if .Required -}} + {{ $modelVar }}.With{{ $attributeNameCamel }}({{ FirstLetterLowercase $attributeNameCamel }}) + {{ end -}} + {{- end -}} return {{ $modelVar }} } @@ -39,7 +41,9 @@ func {{ .Name }}WithDefaultMeta( {{ $modelVar }} := &{{ $modelName }}{ResourceModelMeta: config.DefaultMeta(resources.{{ .Name }})} {{ range .Attributes -}} {{- $attributeNameCamel := SnakeCaseToCamel .Name -}} - {{ if .Required }}{{ $modelVar }}.With{{ $attributeNameCamel }}({{ FirstLetterLowercase $attributeNameCamel }}) {{ end }} - {{- end }} + {{- if .Required -}} + {{ $modelVar }}.With{{ $attributeNameCamel }}({{ FirstLetterLowercase $attributeNameCamel }}) + {{ end -}} + {{- end -}} return {{ $modelVar }} } diff --git a/pkg/acceptance/bettertestspoc/config/model/view_model_gen.go b/pkg/acceptance/bettertestspoc/config/model/view_model_gen.go new file mode 100644 index 0000000000..8c1a2e0ff6 --- /dev/null +++ b/pkg/acceptance/bettertestspoc/config/model/view_model_gen.go @@ -0,0 +1,211 @@ +// 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 ViewModel struct { + AggregationPolicy tfconfig.Variable `json:"aggregation_policy,omitempty"` + ChangeTracking tfconfig.Variable `json:"change_tracking,omitempty"` + Columns tfconfig.Variable `json:"columns,omitempty"` + Comment tfconfig.Variable `json:"comment,omitempty"` + CopyGrants tfconfig.Variable `json:"copy_grants,omitempty"` + DataMetricFunctions tfconfig.Variable `json:"data_metric_functions,omitempty"` + DataMetricSchedule tfconfig.Variable `json:"data_metric_schedule,omitempty"` + Database tfconfig.Variable `json:"database,omitempty"` + IsRecursive tfconfig.Variable `json:"is_recursive,omitempty"` + IsSecure tfconfig.Variable `json:"is_secure,omitempty"` + IsTemporary tfconfig.Variable `json:"is_temporary,omitempty"` + Name tfconfig.Variable `json:"name,omitempty"` + OrReplace tfconfig.Variable `json:"or_replace,omitempty"` + RowAccessPolicy tfconfig.Variable `json:"row_access_policy,omitempty"` + Schema tfconfig.Variable `json:"schema,omitempty"` + Statement tfconfig.Variable `json:"statement,omitempty"` + + *config.ResourceModelMeta +} + +///////////////////////////////////////////////// +// Basic builders (resource name and required) // +///////////////////////////////////////////////// + +func View( + resourceName string, + database string, name string, schema string, statement string, +) *ViewModel { + v := &ViewModel{ResourceModelMeta: config.Meta(resourceName, resources.View)} + v.WithDatabase(database) + v.WithName(name) + v.WithSchema(schema) + v.WithStatement(statement) + return v +} + +func ViewWithDefaultMeta( + database string, name string, schema string, statement string, +) *ViewModel { + v := &ViewModel{ResourceModelMeta: config.DefaultMeta(resources.View)} + v.WithDatabase(database) + v.WithName(name) + v.WithSchema(schema) + v.WithStatement(statement) + return v +} + +///////////////////////////////// +// below all the proper values // +///////////////////////////////// + +// aggregation_policy attribute type is not yet supported, so WithAggregationPolicy can't be generated + +func (v *ViewModel) WithChangeTracking(changeTracking string) *ViewModel { + v.ChangeTracking = tfconfig.StringVariable(changeTracking) + return v +} + +// columns attribute type is not yet supported, so WithColumns can't be generated + +func (v *ViewModel) WithComment(comment string) *ViewModel { + v.Comment = tfconfig.StringVariable(comment) + return v +} + +func (v *ViewModel) WithCopyGrants(copyGrants bool) *ViewModel { + v.CopyGrants = tfconfig.BoolVariable(copyGrants) + return v +} + +// data_metric_functions attribute type is not yet supported, so WithDataMetricFunctions can't be generated + +// data_metric_schedule attribute type is not yet supported, so WithDataMetricSchedule can't be generated + +func (v *ViewModel) WithDatabase(database string) *ViewModel { + v.Database = tfconfig.StringVariable(database) + return v +} + +func (v *ViewModel) WithIsRecursive(isRecursive string) *ViewModel { + v.IsRecursive = tfconfig.StringVariable(isRecursive) + return v +} + +func (v *ViewModel) WithIsSecure(isSecure string) *ViewModel { + v.IsSecure = tfconfig.StringVariable(isSecure) + return v +} + +func (v *ViewModel) WithIsTemporary(isTemporary string) *ViewModel { + v.IsTemporary = tfconfig.StringVariable(isTemporary) + return v +} + +func (v *ViewModel) WithName(name string) *ViewModel { + v.Name = tfconfig.StringVariable(name) + return v +} + +func (v *ViewModel) WithOrReplace(orReplace bool) *ViewModel { + v.OrReplace = tfconfig.BoolVariable(orReplace) + return v +} + +// row_access_policy attribute type is not yet supported, so WithRowAccessPolicy can't be generated + +func (v *ViewModel) WithSchema(schema string) *ViewModel { + v.Schema = tfconfig.StringVariable(schema) + return v +} + +func (v *ViewModel) WithStatement(statement string) *ViewModel { + v.Statement = tfconfig.StringVariable(statement) + return v +} + +////////////////////////////////////////// +// below it's possible to set any value // +////////////////////////////////////////// + +func (v *ViewModel) WithAggregationPolicyValue(value tfconfig.Variable) *ViewModel { + v.AggregationPolicy = value + return v +} + +func (v *ViewModel) WithChangeTrackingValue(value tfconfig.Variable) *ViewModel { + v.ChangeTracking = value + return v +} + +func (v *ViewModel) WithColumnsValue(value tfconfig.Variable) *ViewModel { + v.Columns = value + return v +} + +func (v *ViewModel) WithCommentValue(value tfconfig.Variable) *ViewModel { + v.Comment = value + return v +} + +func (v *ViewModel) WithCopyGrantsValue(value tfconfig.Variable) *ViewModel { + v.CopyGrants = value + return v +} + +func (v *ViewModel) WithDataMetricFunctionsValue(value tfconfig.Variable) *ViewModel { + v.DataMetricFunctions = value + return v +} + +func (v *ViewModel) WithDataMetricScheduleValue(value tfconfig.Variable) *ViewModel { + v.DataMetricSchedule = value + return v +} + +func (v *ViewModel) WithDatabaseValue(value tfconfig.Variable) *ViewModel { + v.Database = value + return v +} + +func (v *ViewModel) WithIsRecursiveValue(value tfconfig.Variable) *ViewModel { + v.IsRecursive = value + return v +} + +func (v *ViewModel) WithIsSecureValue(value tfconfig.Variable) *ViewModel { + v.IsSecure = value + return v +} + +func (v *ViewModel) WithIsTemporaryValue(value tfconfig.Variable) *ViewModel { + v.IsTemporary = value + return v +} + +func (v *ViewModel) WithNameValue(value tfconfig.Variable) *ViewModel { + v.Name = value + return v +} + +func (v *ViewModel) WithOrReplaceValue(value tfconfig.Variable) *ViewModel { + v.OrReplace = value + return v +} + +func (v *ViewModel) WithRowAccessPolicyValue(value tfconfig.Variable) *ViewModel { + v.RowAccessPolicy = value + return v +} + +func (v *ViewModel) WithSchemaValue(value tfconfig.Variable) *ViewModel { + v.Schema = value + return v +} + +func (v *ViewModel) WithStatementValue(value tfconfig.Variable) *ViewModel { + v.Statement = value + return v +} diff --git a/pkg/acceptance/helpers/row_access_policy_client.go b/pkg/acceptance/helpers/row_access_policy_client.go index 29d8700f08..2ae9be2099 100644 --- a/pkg/acceptance/helpers/row_access_policy_client.go +++ b/pkg/acceptance/helpers/row_access_policy_client.go @@ -25,11 +25,16 @@ func (c *RowAccessPolicyClient) client() sdk.RowAccessPolicies { } func (c *RowAccessPolicyClient) CreateRowAccessPolicy(t *testing.T) (*sdk.RowAccessPolicy, func()) { + t.Helper() + return c.CreateRowAccessPolicyWithDataType(t, sdk.DataTypeNumber) +} + +func (c *RowAccessPolicyClient) CreateRowAccessPolicyWithDataType(t *testing.T, datatype sdk.DataType) (*sdk.RowAccessPolicy, func()) { t.Helper() ctx := context.Background() id := c.ids.RandomSchemaObjectIdentifier() - arg := sdk.NewCreateRowAccessPolicyArgsRequest("A", sdk.DataTypeNumber) + arg := sdk.NewCreateRowAccessPolicyArgsRequest("A", datatype) body := "true" createRequest := sdk.NewCreateRowAccessPolicyRequest(id, []sdk.CreateRowAccessPolicyArgsRequest{*arg}, body) diff --git a/pkg/resources/common.go b/pkg/resources/common.go index a2a33a7c1d..1230b25b29 100644 --- a/pkg/resources/common.go +++ b/pkg/resources/common.go @@ -1,9 +1,14 @@ package resources import ( + "context" + "fmt" + "log" + "regexp" "strings" "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/helpers" + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/sdk" "github.com/hashicorp/go-cty/cty" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" ) @@ -22,6 +27,8 @@ func DiffSuppressStatement(_, old, new string, _ *schema.ResourceData) bool { return strings.EqualFold(normalizeQuery(old), normalizeQuery(new)) } +var space = regexp.MustCompile(`\s+`) + func normalizeQuery(str string) string { return strings.TrimSpace(space.ReplaceAllString(str, " ")) } @@ -61,3 +68,29 @@ func ctyValToSliceString(valueElems []cty.Value) []string { } return elems } + +func ensureWarehouse(ctx context.Context, client *sdk.Client) (func(), error) { + warehouse, err := client.ContextFunctions.CurrentWarehouse(ctx) + if err != nil { + return nil, err + } + if len(warehouse) > 0 { + // everything is fine, return a no-op function to avoid checking by callers + return func() {}, nil + } + randomWarehouseName := fmt.Sprintf("terraform-provider-snowflake-%v", helpers.RandomString()) + log.Printf("[DEBUG] no current warehouse set, creating a temporary warehouse %s", randomWarehouseName) + wid := sdk.NewAccountObjectIdentifier(randomWarehouseName) + if err := client.Warehouses.Create(ctx, wid, nil); err != nil { + return nil, err + } + cleanup := func() { + if err := client.Warehouses.Drop(ctx, wid, nil); err != nil { + log.Printf("[WARN] error cleaning up temp warehouse %v", err) + } + } + if err := client.Sessions.UseWarehouse(ctx, wid); err != nil { + return cleanup, err + } + return cleanup, nil +} diff --git a/pkg/resources/streamlit.go b/pkg/resources/streamlit.go index 3b7ff3aa04..d4b7b1171f 100644 --- a/pkg/resources/streamlit.go +++ b/pkg/resources/streamlit.go @@ -83,7 +83,7 @@ var streamlitSchema = map[string]*schema.Schema{ ShowOutputAttributeName: { Type: schema.TypeList, Computed: true, - Description: "Outputs the result of `SHOW STREAMLIT` for the given streamli.", + Description: "Outputs the result of `SHOW STREAMLIT` for the given streamlit.", Elem: &schema.Resource{ Schema: schemas.ShowStreamlitSchema, }, diff --git a/pkg/resources/tag_masking_policy_association.go b/pkg/resources/tag_masking_policy_association.go index f8f570f207..a376571363 100644 --- a/pkg/resources/tag_masking_policy_association.go +++ b/pkg/resources/tag_masking_policy_association.go @@ -132,26 +132,11 @@ func ReadContextTagMaskingPolicyAssociation(ctx context.Context, d *schema.Resou } // create temp warehouse to query the tag, and make sure to clean it up - warehouse, err := client.ContextFunctions.CurrentWarehouse(ctx) + cleanupWarehouse, err := ensureWarehouse(ctx, client) if err != nil { return diag.FromErr(err) } - if warehouse == "" { - log.Printf("[DEBUG] no current warehouse set, creating a temporary warehouse") - randomWarehouseName := fmt.Sprintf("terraform-provider-snowflake-%v", helpers.RandomString()) - wid := sdk.NewAccountObjectIdentifier(randomWarehouseName) - if err := client.Warehouses.Create(ctx, wid, nil); err != nil { - return diag.FromErr(err) - } - defer func() { - if err := client.Warehouses.Drop(ctx, wid, nil); err != nil { - log.Printf("[WARN] error cleaning up temp warehouse %v", err) - } - }() - if err := client.Sessions.UseWarehouse(ctx, wid); err != nil { - return diag.FromErr(err) - } - } + defer cleanupWarehouse() // show attached masking policy tid := sdk.NewSchemaObjectIdentifier(aid.TagDatabaseName, aid.TagSchemaName, aid.TagName) mid := sdk.NewSchemaObjectIdentifier(aid.MaskingPolicyDatabaseName, aid.MaskingPolicySchemaName, aid.MaskingPolicyName) diff --git a/pkg/resources/testdata/TestAcc_View/basic/test.tf b/pkg/resources/testdata/TestAcc_View/basic/test.tf new file mode 100644 index 0000000000..74efa22d33 --- /dev/null +++ b/pkg/resources/testdata/TestAcc_View/basic/test.tf @@ -0,0 +1,6 @@ +resource "snowflake_view" "test" { + name = var.name + database = var.database + schema = var.schema + statement = var.statement +} diff --git a/pkg/resources/testdata/TestAcc_View_Tags/1/variables.tf b/pkg/resources/testdata/TestAcc_View/basic/variables.tf similarity index 65% rename from pkg/resources/testdata/TestAcc_View_Tags/1/variables.tf rename to pkg/resources/testdata/TestAcc_View/basic/variables.tf index 7d7074a5a9..5b5810d23d 100644 --- a/pkg/resources/testdata/TestAcc_View_Tags/1/variables.tf +++ b/pkg/resources/testdata/TestAcc_View/basic/variables.tf @@ -13,11 +13,3 @@ variable "schema" { variable "statement" { type = string } - -variable "tag1Name" { - type = string -} - -variable "tag2Name" { - type = string -} diff --git a/pkg/resources/testdata/TestAcc_View/basic_update/test.tf b/pkg/resources/testdata/TestAcc_View/basic_update/test.tf new file mode 100644 index 0000000000..2bd1ef8145 --- /dev/null +++ b/pkg/resources/testdata/TestAcc_View/basic_update/test.tf @@ -0,0 +1,16 @@ +resource "snowflake_view" "test" { + name = var.name + database = var.database + schema = var.schema + row_access_policy { + policy_name = var.row_access_policy + on = var.row_access_policy_on + + } + aggregation_policy { + policy_name = var.aggregation_policy + entity_key = var.aggregation_policy_entity_key + } + statement = var.statement + comment = var.comment +} diff --git a/pkg/resources/testdata/TestAcc_View/basic_update/variables.tf b/pkg/resources/testdata/TestAcc_View/basic_update/variables.tf new file mode 100644 index 0000000000..42cc6286e8 --- /dev/null +++ b/pkg/resources/testdata/TestAcc_View/basic_update/variables.tf @@ -0,0 +1,35 @@ +variable "name" { + type = string +} + +variable "database" { + type = string +} + +variable "schema" { + type = string +} + +variable "statement" { + type = string +} + +variable "row_access_policy" { + type = string +} + +variable "row_access_policy_on" { + type = list(string) +} + +variable "aggregation_policy" { + type = string +} + +variable "aggregation_policy_entity_key" { + type = list(string) +} + +variable "comment" { + type = string +} diff --git a/pkg/resources/testdata/TestAcc_View/complete/test.tf b/pkg/resources/testdata/TestAcc_View/complete/test.tf new file mode 100644 index 0000000000..0af4ad36b3 --- /dev/null +++ b/pkg/resources/testdata/TestAcc_View/complete/test.tf @@ -0,0 +1,21 @@ +resource "snowflake_view" "test" { + name = var.name + comment = var.comment + database = var.database + schema = var.schema + is_secure = var.is_secure + or_replace = var.or_replace + copy_grants = var.copy_grants + change_tracking = var.change_tracking + is_temporary = var.is_temporary + row_access_policy { + policy_name = var.row_access_policy + on = var.row_access_policy_on + + } + aggregation_policy { + policy_name = var.aggregation_policy + entity_key = var.aggregation_policy_entity_key + } + statement = var.statement +} diff --git a/pkg/resources/testdata/TestAcc_View_basic/variables.tf b/pkg/resources/testdata/TestAcc_View/complete/variables.tf similarity index 50% rename from pkg/resources/testdata/TestAcc_View_basic/variables.tf rename to pkg/resources/testdata/TestAcc_View/complete/variables.tf index b36067977a..6fe755f654 100644 --- a/pkg/resources/testdata/TestAcc_View_basic/variables.tf +++ b/pkg/resources/testdata/TestAcc_View/complete/variables.tf @@ -18,6 +18,10 @@ variable "is_secure" { type = bool } +variable "change_tracking" { + type = string +} + variable "or_replace" { type = bool } @@ -26,6 +30,26 @@ variable "copy_grants" { type = bool } +variable "row_access_policy" { + type = string +} + +variable "is_temporary" { + type = string +} + +variable "row_access_policy_on" { + type = list(string) +} + +variable "aggregation_policy" { + type = string +} + +variable "aggregation_policy_entity_key" { + type = list(string) +} + variable "statement" { type = string } diff --git a/pkg/resources/testdata/TestAcc_View_Tags/1/test.tf b/pkg/resources/testdata/TestAcc_View_Tags/1/test.tf deleted file mode 100644 index b27dd3db42..0000000000 --- a/pkg/resources/testdata/TestAcc_View_Tags/1/test.tf +++ /dev/null @@ -1,28 +0,0 @@ -resource "snowflake_tag" "test_tag" { - name = var.tag1Name - database = var.database - schema = var.schema -} - -resource "snowflake_tag" "test_tag_2" { - name = var.tag2Name - database = var.database - schema = var.schema -} - -resource "snowflake_view" "test" { - name = var.name - database = var.database - schema = var.schema - is_secure = false - or_replace = false - copy_grants = false - statement = var.statement - - tag { - name = snowflake_tag.test_tag.name - schema = var.schema - database = var.database - value = "some_value" - } -} diff --git a/pkg/resources/testdata/TestAcc_View_Tags/2/test.tf b/pkg/resources/testdata/TestAcc_View_Tags/2/test.tf deleted file mode 100644 index 933fe9613e..0000000000 --- a/pkg/resources/testdata/TestAcc_View_Tags/2/test.tf +++ /dev/null @@ -1,28 +0,0 @@ -resource "snowflake_tag" "test_tag" { - name = var.tag1Name - database = var.database - schema = var.schema -} - -resource "snowflake_tag" "test_tag_2" { - name = var.tag2Name - database = var.database - schema = var.schema -} - -resource "snowflake_view" "test" { - name = var.name - database = var.database - schema = var.schema - is_secure = false - or_replace = false - copy_grants = false - statement = var.statement - - tag { - name = snowflake_tag.test_tag_2.name - schema = var.schema - database = var.database - value = "some_value" - } -} diff --git a/pkg/resources/testdata/TestAcc_View_Tags/2/variables.tf b/pkg/resources/testdata/TestAcc_View_Tags/2/variables.tf deleted file mode 100644 index 7d7074a5a9..0000000000 --- a/pkg/resources/testdata/TestAcc_View_Tags/2/variables.tf +++ /dev/null @@ -1,23 +0,0 @@ -variable "name" { - type = string -} - -variable "database" { - type = string -} - -variable "schema" { - type = string -} - -variable "statement" { - type = string -} - -variable "tag1Name" { - type = string -} - -variable "tag2Name" { - type = string -} diff --git a/pkg/resources/testdata/TestAcc_View_basic/test.tf b/pkg/resources/testdata/TestAcc_View_basic/test.tf deleted file mode 100644 index d333d83bd9..0000000000 --- a/pkg/resources/testdata/TestAcc_View_basic/test.tf +++ /dev/null @@ -1,10 +0,0 @@ -resource "snowflake_view" "test" { - name = var.name - comment = var.comment - database = var.database - schema = var.schema - is_secure = var.is_secure - or_replace = var.or_replace - copy_grants = var.copy_grants - statement = var.statement -} diff --git a/pkg/resources/view.go b/pkg/resources/view.go index 8fea269008..f7efe47c18 100644 --- a/pkg/resources/view.go +++ b/pkg/resources/view.go @@ -2,37 +2,44 @@ package resources import ( "context" + "errors" "fmt" "log" - "regexp" + "strconv" + "strings" "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/internal/provider" + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/schemas" "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/helpers" "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/sdk" "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/snowflake" + "github.com/hashicorp/go-cty/cty" + "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 space = regexp.MustCompile(`\s+`) - var viewSchema = map[string]*schema.Schema{ "name": { - Type: schema.TypeString, - Required: true, - Description: "Specifies the identifier for the view; must be unique for the schema in which the view is created. Don't use the | character.", + Type: schema.TypeString, + Required: true, + Description: "Specifies the identifier for the view; must be unique for the schema in which the view is created. Don't use the | character.", + DiffSuppressFunc: suppressIdentifierQuoting, }, "database": { - Type: schema.TypeString, - Required: true, - Description: "The database in which to create the view. Don't use the | character.", - ForceNew: true, + Type: schema.TypeString, + Required: true, + Description: "The database in which to create the view. Don't use the | character.", + ForceNew: true, + DiffSuppressFunc: suppressIdentifierQuoting, }, "schema": { - Type: schema.TypeString, - Required: true, - Description: "The schema in which to create the view. Don't use the | character.", - ForceNew: true, + Type: schema.TypeString, + Required: true, + Description: "The schema in which to create the view. Don't use the | character.", + ForceNew: true, + DiffSuppressFunc: suppressIdentifierQuoting, }, "or_replace": { Type: schema.TypeBool, @@ -52,172 +59,550 @@ var viewSchema = map[string]*schema.Schema{ RequiredWith: []string{"or_replace"}, }, "is_secure": { - Type: schema.TypeBool, - Optional: true, - Default: false, - Description: "Specifies that the view is secure. By design, the Snowflake's `SHOW VIEWS` command does not provide information about secure views (consult [view usage notes](https://docs.snowflake.com/en/sql-reference/sql/create-view#usage-notes)) which is essential to manage/import view with Terraform. Use the role owning the view while managing secure views.", + Type: schema.TypeString, + Optional: true, + Default: BooleanDefault, + ValidateDiagFunc: validateBooleanString, + DiffSuppressFunc: IgnoreChangeToCurrentSnowflakeValueInShow("is_secure"), + Description: booleanStringFieldDescription("Specifies that the view is secure. By design, the Snowflake's `SHOW VIEWS` command does not provide information about secure views (consult [view usage notes](https://docs.snowflake.com/en/sql-reference/sql/create-view#usage-notes)) which is essential to manage/import view with Terraform. Use the role owning the view while managing secure views."), }, + "is_temporary": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + Default: BooleanDefault, + ValidateDiagFunc: validateBooleanString, + Description: booleanStringFieldDescription("Specifies that the view persists only for the duration of the session that you created it in. A temporary view and all its contents are dropped at the end of the session."), + }, + "is_recursive": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + Default: BooleanDefault, + ValidateDiagFunc: validateBooleanString, + Description: booleanStringFieldDescription("Specifies that the view can refer to itself using recursive syntax without necessarily using a CTE (common table expression)."), + }, + "change_tracking": { + Type: schema.TypeString, + Optional: true, + Default: BooleanDefault, + ValidateDiagFunc: validateBooleanString, + DiffSuppressFunc: IgnoreChangeToCurrentSnowflakeValueInShowWithMapping("change_tracking", func(x any) any { + return x.(string) == "ON" + }), + // DiffSuppressFunc: IgnoreChangeToCurrentSnowflakeValueInShow("change_tracking"), + Description: booleanStringFieldDescription("Specifies to enable or disable change tracking on the table."), + }, + // TODO(next pr): support remaining fields + // "data_metric_functions": { + // Type: schema.TypeSet, + // Optional: true, + // Elem: &schema.Resource{ + // Schema: map[string]*schema.Schema{ + // "metric_name": { + // Type: schema.TypeString, + // Optional: true, + // Description: "Identifier of the data metric function to add to the table or view or drop from the table or view.", + // }, + // "column_name": { + // Type: schema.TypeString, + // Optional: true, + // Description: "The table or view columns on which to associate the data metric function. The data types of the columns must match the data types of the columns specified in the data metric function definition.", + // }, + // }, + // }, + // Description: "Data metric functions used for the view.", + // }, + // "data_metric_schedule": { + // Type: schema.TypeList, + // Optional: true, + // MaxItems: 1, + // Elem: &schema.Resource{ + // Schema: map[string]*schema.Schema{ + // "minutes": { + // Type: schema.TypeInt, + // Optional: true, + // Description: "Specifies an interval (in minutes) of wait time inserted between runs of the data metric function. Conflicts with `using_cron` and `trigger_on_changes`.", + // // TODO: move to sdk + // ValidateFunc: validation.IntInSlice([]int{5, 15, 30, 60, 720, 1440}), + // ConflictsWith: []string{"data_metric_schedule.using_cron", "data_metric_schedule.trigger_on_changes"}, + // }, + // "using_cron": { + // Type: schema.TypeString, + // Optional: true, + // Description: "Specifies a cron expression and time zone for periodically running the data metric function. Supports a subset of standard cron utility syntax. Conflicts with `minutes` and `trigger_on_changes`.", + // // TODO: validate? + // ConflictsWith: []string{"data_metric_schedule.minutes", "data_metric_schedule.trigger_on_changes"}, + // }, + // "trigger_on_changes": { + // Type: schema.TypeString, + // Optional: true, + // Default: BooleanDefault, + // Description: booleanStringFieldDescription("Specifies that the DMF runs when a DML operation modifies the table, such as inserting a new row or deleting a row. Conflicts with `minutes` and `using_cron`."), + // ConflictsWith: []string{"data_metric_schedule.minutes", "data_metric_schedule.using_cron"}, + // }, + // }, + // }, + // Description: "Specifies the schedule to run the data metric function periodically.", + // }, + // "columns": { + // Type: schema.TypeList, + // Optional: true, + // Elem: &schema.Resource{ + // Schema: map[string]*schema.Schema{ + // "column_name": { + // Type: schema.TypeString, + // Required: true, + // Description: "Specifies affected column name.", + // }, + // "masking_policy": { + // Type: schema.TypeList, + // Optional: true, + // Elem: &schema.Resource{ + // Schema: map[string]*schema.Schema{ + // // TODO: change to `name`? in other policies as well + // "policy_name": { + // Type: schema.TypeString, + // Required: true, + // Description: "Specifies the masking policy to set on a column.", + // }, + // "using": { + // Type: schema.TypeList, + // Optional: true, + // Elem: &schema.Schema{ + // Type: schema.TypeString, + // }, + // Description: "Specifies the arguments to pass into the conditional masking policy SQL expression. The first column in the list specifies the column for the policy conditions to mask or tokenize the data and must match the column to which the masking policy is set. The additional columns specify the columns to evaluate to determine whether to mask or tokenize the data in each row of the query result when a query is made on the first column. If the USING clause is omitted, Snowflake treats the conditional masking policy as a normal masking policy.", + // }, + // }, + // }, + // }, + // "projection_policy": { + // Type: schema.TypeString, + // Optional: true, + // DiffSuppressFunc: DiffSuppressStatement, + // Description: "Specifies the projection policy to set on a column.", + // }, + // }, + // }, + // Description: "If you want to change the name of a column or add a comment to a column in the new view, include a column list that specifies the column names and (if needed) comments about the columns. (You do not need to specify the data types of the columns.)", + // }, "comment": { Type: schema.TypeString, Optional: true, Description: "Specifies a comment for the view.", }, + "row_access_policy": { + Type: schema.TypeList, + Optional: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "policy_name": { + Type: schema.TypeString, + Required: true, + DiffSuppressFunc: suppressIdentifierQuoting, + Description: "Row access policy name.", + }, + "on": { + Type: schema.TypeSet, + Required: true, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + Description: "Defines which columns are affected by the policy.", + }, + }, + }, + Description: "Specifies the row access policy to set on a view.", + }, + "aggregation_policy": { + Type: schema.TypeList, + MaxItems: 1, + Optional: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "policy_name": { + Type: schema.TypeString, + Required: true, + DiffSuppressFunc: suppressIdentifierQuoting, + Description: "Aggregation policy name.", + }, + "entity_key": { + Type: schema.TypeSet, + Optional: true, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + Description: "Defines which columns uniquely identify an entity within the view.", + }, + }, + }, + Description: "Specifies the aggregation policy to set on a view.", + }, "statement": { Type: schema.TypeString, Required: true, Description: "Specifies the query used to create the view.", DiffSuppressFunc: DiffSuppressStatement, }, - "created_on": { - Type: schema.TypeString, + ShowOutputAttributeName: { + Type: schema.TypeList, Computed: true, - Description: "The timestamp at which the view was created.", + Description: "Outputs the result of `SHOW VIEW` for the given view.", + Elem: &schema.Resource{ + Schema: schemas.ShowViewSchema, + }, + }, + DescribeOutputAttributeName: { + Type: schema.TypeList, + Computed: true, + Description: "Outputs the result of `DESCRIBE VIEW` for the given view.", + Elem: &schema.Resource{ + Schema: schemas.ViewDescribeSchema, + }, }, - "tag": tagReferenceSchema, } // View returns a pointer to the resource representing a view. func View() *schema.Resource { return &schema.Resource{ - Create: CreateView, - Read: ReadView, - Update: UpdateView, - Delete: DeleteView, + SchemaVersion: 1, + + CreateContext: CreateView, + ReadContext: ReadView(true), + UpdateContext: UpdateView, + DeleteContext: DeleteView, + Description: "Resource used to manage view objects. For more information, check [view documentation](https://docs.snowflake.com/en/sql-reference/sql/create-view).", + + CustomizeDiff: customdiff.All( + ComputedIfAnyAttributeChanged(ShowOutputAttributeName, "name", "comment", "change_tracking", "is_secure", "statement"), + ), Schema: viewSchema, Importer: &schema.ResourceImporter{ - StateContext: schema.ImportStatePassthroughContext, + StateContext: ImportView, + }, + + StateUpgraders: []schema.StateUpgrader{ + { + Version: 0, + // setting type to cty.EmptyObject is a bit hacky here but following https://developer.hashicorp.com/terraform/plugin/framework/migrating/resources/state-upgrade#sdkv2-1 would require lots of repetitive code; this should work with cty.EmptyObject + Type: cty.EmptyObject, + Upgrade: v094ViewStateUpgrader, + }, }, } } -// CreateView implements schema.CreateFunc. -func CreateView(d *schema.ResourceData, meta interface{}) error { +func ImportView(ctx context.Context, d *schema.ResourceData, meta any) ([]*schema.ResourceData, error) { + log.Printf("[DEBUG] Starting view import") client := meta.(*provider.Context).Client - ctx := context.Background() - - databaseName := d.Get("database").(string) - schemaName := d.Get("schema").(string) - name := d.Get("name").(string) - id := sdk.NewSchemaObjectIdentifier(databaseName, schemaName, name) - - s := d.Get("statement").(string) - createRequest := sdk.NewCreateViewRequest(id, s) + id := helpers.DecodeSnowflakeID(d.Id()).(sdk.SchemaObjectIdentifier) - if v, ok := d.GetOk("or_replace"); ok && v.(bool) { - createRequest.WithOrReplace(true) + v, err := client.Views.ShowByID(ctx, id) + if err != nil { + return nil, err } - - if v, ok := d.GetOk("is_secure"); ok && v.(bool) { - createRequest.WithSecure(true) + if err := d.Set("name", v.Name); err != nil { + return nil, err } - if v, ok := d.GetOk("copy_grants"); ok && v.(bool) { - createRequest.WithCopyGrants(true) + if err := d.Set("change_tracking", booleanStringFromBool(v.IsChangeTracking())); err != nil { + return nil, err + } + if err := d.Set("is_recursive", booleanStringFromBool(v.IsRecursive())); err != nil { + return nil, err } + if err := d.Set("is_secure", booleanStringFromBool(v.IsSecure)); err != nil { + return nil, err + } + if err := d.Set("is_temporary", booleanStringFromBool(v.IsTemporary())); err != nil { + return nil, err + } + return []*schema.ResourceData{d}, nil +} - if v, ok := d.GetOk("comment"); ok { - createRequest.WithComment(v.(string)) +func CreateView(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { + client := meta.(*provider.Context).Client + + req, err := prepareCreateRequest(d) + if err != nil { + return diag.FromErr(err) } + id := req.GetName() - err := client.Views.Create(ctx, createRequest) + err = client.Views.Create(ctx, req) if err != nil { - return fmt.Errorf("error creating view %v err = %w", name, err) + return diag.FromErr(fmt.Errorf("error creating view %v err = %w", id.Name(), err)) } - // TODO [SNOW-1348118]: we have to set tags after creation because existing view extractor is not aware of TAG during CREATE - // Will be discussed with parser topic during resources redesign. - if _, ok := d.GetOk("tag"); ok { - err := client.Views.Alter(ctx, sdk.NewAlterViewRequest(id).WithSetTags(getPropertyTags(d, "tag"))) + d.SetId(helpers.EncodeSnowflakeID(id)) + + if v := d.Get("change_tracking").(string); v != BooleanDefault { + parsed, err := booleanStringToBool(v) + if err != nil { + return diag.FromErr(err) + } + + err = client.Views.Alter(ctx, sdk.NewAlterViewRequest(id).WithSetChangeTracking(parsed)) if err != nil { - return fmt.Errorf("error setting tags on view %v, err = %w", id, err) + return diag.FromErr(fmt.Errorf("error setting change_tracking in view %v err = %w", id.Name(), err)) } } - d.SetId(helpers.EncodeSnowflakeID(id)) + return ReadView(false)(ctx, d, meta) +} - return ReadView(d, meta) +type resourceDataGetter interface { + Get(string) any } -// ReadView implements schema.ReadFunc. -func ReadView(d *schema.ResourceData, meta interface{}) error { - client := meta.(*provider.Context).Client - ctx := context.Background() - id := helpers.DecodeSnowflakeID(d.Id()).(sdk.SchemaObjectIdentifier) +func prepareCreateRequest(d resourceDataGetter) (*sdk.CreateViewRequest, error) { + databaseName := d.Get("database").(string) + schemaName := d.Get("schema").(string) + name := d.Get("name").(string) + id := sdk.NewSchemaObjectIdentifier(databaseName, schemaName, name) - view, err := client.Views.ShowByID(ctx, id) - if err != nil { - log.Printf("[DEBUG] view (%s) not found", d.Id()) - d.SetId("") - return nil + statement := d.Get("statement").(string) + req := sdk.NewCreateViewRequest(id, statement) + + if v := d.Get("or_replace"); v.(bool) { + req.WithOrReplace(true) } - if err = d.Set("name", view.Name); err != nil { - return err + if v := d.Get("copy_grants"); v.(bool) { + req.WithCopyGrants(true) } - if err = d.Set("is_secure", view.IsSecure); err != nil { - return err + + if v := d.Get("is_secure").(string); v != BooleanDefault { + parsed, err := strconv.ParseBool(v) + if err != nil { + return nil, err + } + req.WithSecure(parsed) } - if err = d.Set("copy_grants", view.HasCopyGrants()); err != nil { - return err + + if v := d.Get("is_temporary").(string); v != BooleanDefault { + parsed, err := strconv.ParseBool(v) + if err != nil { + return nil, err + } + req.WithTemporary(parsed) } - if err = d.Set("comment", view.Comment); err != nil { - return err + + if v := d.Get("is_recursive").(string); v != BooleanDefault { + parsed, err := strconv.ParseBool(v) + if err != nil { + return nil, err + } + req.WithRecursive(parsed) } - if err = d.Set("schema", view.SchemaName); err != nil { - return err + + if v := d.Get("comment").(string); len(v) > 0 { + req.WithComment(v) } - if err = d.Set("database", view.DatabaseName); err != nil { - return err + + if v := d.Get("row_access_policy"); len(v.([]any)) > 0 { + req.WithRowAccessPolicy(*sdk.NewViewRowAccessPolicyRequest(extractPolicyWithColumns(v, "on"))) } - if err = d.Set("created_on", view.CreatedOn); err != nil { - return err + + if v := d.Get("aggregation_policy"); len(v.([]any)) > 0 { + id, columns := extractPolicyWithColumns(v, "entity_key") + aggregationPolicyReq := sdk.NewViewAggregationPolicyRequest(id) + if len(columns) > 0 { + aggregationPolicyReq.WithEntityKey(columns) + } + req.WithAggregationPolicy(*aggregationPolicyReq) + } + return req, nil +} + +func extractPolicyWithColumns(v any, columnsKey string) (sdk.SchemaObjectIdentifier, []sdk.Column) { + policyConfig := v.([]any)[0].(map[string]any) + columnsRaw := expandStringList(policyConfig[columnsKey].(*schema.Set).List()) + columns := make([]sdk.Column, len(columnsRaw)) + for i := range columnsRaw { + columns[i] = sdk.Column{Value: columnsRaw[i]} } + return sdk.NewSchemaObjectIdentifierFromFullyQualifiedName(policyConfig["policy_name"].(string)), columns +} + +func ReadView(withExternalChangesMarking bool) schema.ReadContextFunc { + return func(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { + client := meta.(*provider.Context).Client + id := helpers.DecodeSnowflakeID(d.Id()).(sdk.SchemaObjectIdentifier) + + view, err := client.Views.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 view. Marking the resource as removed.", + Detail: fmt.Sprintf("View: %s, Err: %s", id.FullyQualifiedName(), err), + }, + } + } + return diag.FromErr(err) + } + + if err = d.Set("name", view.Name); err != nil { + return diag.FromErr(err) + } + if err = d.Set("database", view.DatabaseName); err != nil { + return diag.FromErr(err) + } + if err = d.Set("schema", view.SchemaName); 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) + } + if withExternalChangesMarking { + if err = handleExternalChangesToObjectInShow(d, + showMapping{"is_secure", "is_secure", view.IsSecure, booleanStringFromBool(view.IsSecure), nil}, + showMapping{"text", "is_recursive", view.IsRecursive(), booleanStringFromBool(view.IsRecursive()), func(x any) any { + return strings.Contains(x.(string), "RECURSIVE") + }}, + showMapping{"text", "is_temporary", view.IsTemporary(), booleanStringFromBool(view.IsTemporary()), func(x any) any { + return strings.Contains(x.(string), "TEMPORARY") + }}, + showMapping{"change_tracking", "change_tracking", view.IsChangeTracking(), booleanStringFromBool(view.IsChangeTracking()), func(x any) any { + return x.(string) == "ON" + }}, + ); err != nil { + return diag.FromErr(err) + } + } + if err = setStateToValuesFromConfig(d, viewSchema, []string{ + "change_tracking", + "is_recursive", + "is_secure", + "is_temporary", + }); err != nil { + return diag.FromErr(err) + } - if view.Text != "" { - // Want to only capture the SELECT part of the query because before that is the CREATE part of the view. - extractor := snowflake.NewViewSelectStatementExtractor(view.Text) - substringOfQuery, err := extractor.Extract() + err = handlePolicyReferences(ctx, client, id, d) if err != nil { - return err + return diag.FromErr(err) } - if err = d.Set("statement", substringOfQuery); err != nil { - return err + if view.Text != "" { + // Want to only capture the SELECT part of the query because before that is the CREATE part of the view. + extractor := snowflake.NewViewSelectStatementExtractor(view.Text) + statement, err := extractor.Extract() + if err != nil { + return diag.FromErr(err) + } + if err = d.Set("statement", statement); err != nil { + return diag.FromErr(err) + } + } else { + return diag.FromErr(fmt.Errorf("error reading view %v, err = %w, `text` is missing; if the view is secure then the role used by the provider must own the view (consult https://docs.snowflake.com/en/sql-reference/sql/create-view#usage-notes)", d.Id(), err)) } - } else { - return fmt.Errorf("error reading view %v, err = %w, `text` is missing; if the view is secure then the role used by the provider must own the view (consult https://docs.snowflake.com/en/sql-reference/sql/create-view#usage-notes)", d.Id(), err) + + describeResult, err := client.Views.Describe(ctx, view.ID()) + if err != nil { + log.Printf("[DEBUG] describing view: %s, err: %s", id.FullyQualifiedName(), err) + } else { + if err = d.Set(DescribeOutputAttributeName, schemas.ViewDescriptionToSchema(describeResult)); err != nil { + return diag.FromErr(err) + } + } + + if err = d.Set(ShowOutputAttributeName, []map[string]any{schemas.ViewToSchema(view)}); err != nil { + return diag.FromErr(err) + } + return nil } +} - return nil +func handlePolicyReferences(ctx context.Context, client *sdk.Client, id sdk.SchemaObjectIdentifier, d *schema.ResourceData) error { + // ensure a warehouse is selected for the session to get policy references + cleanupWarehouse, err := ensureWarehouse(ctx, client) + if err != nil { + return err + } + defer cleanupWarehouse() + + policyRefs, err := client.PolicyReferences.GetForEntity(ctx, sdk.NewGetForEntityPolicyReferenceRequest(id, sdk.PolicyEntityDomainView)) + if err != nil { + return err + } + for _, p := range policyRefs { + policyName := sdk.NewSchemaObjectIdentifier(*p.PolicyDb, *p.PolicySchema, p.PolicyName) + switch p.PolicyKind { + case string(sdk.PolicyKindAggregationPolicy): + var entityKey []string + if p.RefArgColumnNames != nil { + entityKey = sdk.ParseCommaSeparatedStringArray(*p.RefArgColumnNames, true) + } + if err = d.Set("aggregation_policy", []map[string]any{{ + "policy_name": policyName.FullyQualifiedName(), + "entity_key": entityKey, + }}); err != nil { + return err + } + case string(sdk.PolicyKindRowAccessPolicy): + var on []string + if p.RefArgColumnNames != nil { + on = sdk.ParseCommaSeparatedStringArray(*p.RefArgColumnNames, true) + } + if err = d.Set("row_access_policy", []map[string]any{{ + "policy_name": policyName.FullyQualifiedName(), + "on": on, + }}); err != nil { + return err + } + default: + log.Printf("[WARN] unexpected policy kind %v in policy references returned from Snowflake", p.PolicyKind) + } + } + return err +} + +type viewWithReplaceDataResourceGetter struct { + d *schema.ResourceData +} + +func (g *viewWithReplaceDataResourceGetter) Get(name string) any { + old, new := g.d.GetChange(name) + if name == "statement" { + return new + } + return old } -// UpdateView implements schema.UpdateFunc. -func UpdateView(d *schema.ResourceData, meta interface{}) error { +func UpdateView(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { client := meta.(*provider.Context).Client - ctx := context.Background() id := helpers.DecodeSnowflakeID(d.Id()).(sdk.SchemaObjectIdentifier) - // The only way to update the statement field in a view is to perform create or replace with the new statement. - // In case of any statement change, create or replace will be performed with all the old parameters, except statement - // and copy grants (which is always set to true to keep the permissions from the previous state). + // change on this field can not be ForceNew because then view is dropped explicitly and copying grants does not have effect if d.HasChange("statement") { - oldIsSecure, _ := d.GetChange("is_secure") - oldComment, _ := d.GetChange("comment") - oldTags, _ := d.GetChange("tag") - - createRequest := sdk.NewCreateViewRequest(id, d.Get("statement").(string)). - WithOrReplace(true). - WithCopyGrants(true). - WithComment(oldComment.(string)). - WithTag(getTagsFromList(oldTags.([]any))) - - if oldIsSecure.(bool) { - createRequest.WithSecure(true) + // TODO: extract this common code with CreateView + req, err := prepareCreateRequest(&viewWithReplaceDataResourceGetter{d}) + if err != nil { + return diag.FromErr(err) } - err := client.Views.Create(ctx, createRequest) + err = client.Views.Create(ctx, req.WithOrReplace(true)) if err != nil { - return fmt.Errorf("error when changing property on %v and performing create or replace to update view statements, err = %w", d.Id(), err) + return diag.FromErr(fmt.Errorf("error when changing property on %v and performing create or replace to update view statements, err = %w", d.Id(), err)) + } + if v := d.Get("change_tracking").(string); v != BooleanDefault { + parsed, err := booleanStringToBool(v) + if err != nil { + return diag.FromErr(err) + } + + err = client.Views.Alter(ctx, sdk.NewAlterViewRequest(id).WithSetChangeTracking(parsed)) + if err != nil { + return diag.FromErr(fmt.Errorf("error setting change_tracking in view %v err = %w", id.Name(), err)) + } } } @@ -226,7 +611,7 @@ func UpdateView(d *schema.ResourceData, meta interface{}) error { err := client.Views.Alter(ctx, sdk.NewAlterViewRequest(id).WithRenameTo(newId)) if err != nil { - return fmt.Errorf("error renaming view %v err = %w", d.Id(), err) + return diag.FromErr(fmt.Errorf("error renaming view %v err = %w", d.Id(), err)) } d.SetId(helpers.EncodeSnowflakeID(newId)) @@ -237,63 +622,130 @@ func UpdateView(d *schema.ResourceData, meta interface{}) error { if comment := d.Get("comment").(string); comment == "" { err := client.Views.Alter(ctx, sdk.NewAlterViewRequest(id).WithUnsetComment(true)) if err != nil { - return fmt.Errorf("error unsetting comment for view %v", d.Id()) + return diag.FromErr(fmt.Errorf("error unsetting comment for view %v", d.Id())) } } else { err := client.Views.Alter(ctx, sdk.NewAlterViewRequest(id).WithSetComment(comment)) if err != nil { - return fmt.Errorf("error updating comment for view %v", d.Id()) + return diag.FromErr(fmt.Errorf("error setting comment for view %v", d.Id())) } } } if d.HasChange("is_secure") { - if d.Get("is_secure").(bool) { - err := client.Views.Alter(ctx, sdk.NewAlterViewRequest(id).WithSetSecure(true)) + if v := d.Get("is_secure").(string); v != BooleanDefault { + parsed, err := booleanStringToBool(v) if err != nil { - return fmt.Errorf("error setting secure for view %v", d.Id()) + return diag.FromErr(err) + } + if parsed { + err = client.Views.Alter(ctx, sdk.NewAlterViewRequest(id).WithSetSecure(parsed)) + } else { + err = client.Views.Alter(ctx, sdk.NewAlterViewRequest(id).WithUnsetSecure(parsed)) + } + if err != nil { + return diag.FromErr(fmt.Errorf("error setting is_secure for view %v: %w", d.Id(), err)) } } else { err := client.Views.Alter(ctx, sdk.NewAlterViewRequest(id).WithUnsetSecure(true)) if err != nil { - return fmt.Errorf("error unsetting secure for view %v", d.Id()) + return diag.FromErr(fmt.Errorf("error unsetting is_secure for view %v: %w", d.Id(), err)) } } } - - if d.HasChange("tag") { - unsetTags, setTags := GetTagsDiff(d, "tag") - - if len(unsetTags) > 0 { - err := client.Views.Alter(ctx, sdk.NewAlterViewRequest(id).WithUnsetTags(unsetTags)) + if d.HasChange("change_tracking") { + if v := d.Get("change_tracking").(string); v != BooleanDefault { + parsed, err := booleanStringToBool(v) + if err != nil { + return diag.FromErr(err) + } + err = client.Views.Alter(ctx, sdk.NewAlterViewRequest(id).WithSetChangeTracking(parsed)) if err != nil { - return fmt.Errorf("error unsetting tags on %v, err = %w", d.Id(), err) + return diag.FromErr(fmt.Errorf("error setting change_tracking for view %v: %w", d.Id(), err)) + } + } else { + err := client.Views.Alter(ctx, sdk.NewAlterViewRequest(id).WithSetChangeTracking(false)) + if err != nil { + return diag.FromErr(fmt.Errorf("error unsetting change_tracking for view %v: %w", d.Id(), err)) } } + } - if len(setTags) > 0 { - err := client.Views.Alter(ctx, sdk.NewAlterViewRequest(id).WithSetTags(setTags)) + if d.HasChange("row_access_policy") { + var addReq *sdk.ViewAddRowAccessPolicyRequest + var dropReq *sdk.ViewDropRowAccessPolicyRequest + if v, ok := d.GetOk("row_access_policy"); ok { + rowAccessPolicyConfig := v.([]any)[0].(map[string]any) + columnsRaw := expandStringList(rowAccessPolicyConfig["on"].(*schema.Set).List()) + columns := make([]sdk.Column, len(columnsRaw)) + for i := range columnsRaw { + columns[i] = sdk.Column{Value: columnsRaw[i]} + } + addReq = sdk.NewViewAddRowAccessPolicyRequest( + sdk.NewSchemaObjectIdentifierFromFullyQualifiedName(rowAccessPolicyConfig["policy_name"].(string)), + columns) + } + old, _ := d.GetChange("row_access_policy") + if len(old.([]any)) > 0 { + oldPolicy := old.([]any)[0].(map[string]any) + dropReq = sdk.NewViewDropRowAccessPolicyRequest(sdk.NewSchemaObjectIdentifierFromFullyQualifiedName(oldPolicy["policy_name"].(string))) + } + req := sdk.NewAlterViewRequest(id) + if addReq != nil && dropReq != nil { // nolint + req.WithDropAndAddRowAccessPolicy(*sdk.NewViewDropAndAddRowAccessPolicyRequest(*dropReq, *addReq)) + } else if addReq != nil { + req.WithAddRowAccessPolicy(*addReq) + } else if dropReq != nil { + req.WithDropRowAccessPolicy(*dropReq) + } + err := client.Views.Alter(ctx, req) + if err != nil { + return diag.FromErr(fmt.Errorf("error altering row_access_policy for view %v: %w", d.Id(), err)) + } + } + if d.HasChange("aggregation_policy") { + if v, ok := d.GetOk("aggregation_policy"); ok { + rowAccessPolicyConfig := v.([]any)[0].(map[string]any) + columnsRaw := expandStringList(rowAccessPolicyConfig["entity_key"].(*schema.Set).List()) + columns := make([]sdk.Column, len(columnsRaw)) + for i := range columnsRaw { + columns[i] = sdk.Column{Value: columnsRaw[i]} + } + aggregationPolicyReq := sdk.NewViewSetAggregationPolicyRequest( + sdk.NewSchemaObjectIdentifierFromFullyQualifiedName(rowAccessPolicyConfig["policy_name"].(string))).WithForce(true) + if len(columns) > 0 { + aggregationPolicyReq.WithEntityKey(columns) + } + err := client.Views.Alter(ctx, sdk.NewAlterViewRequest(id).WithSetAggregationPolicy(*aggregationPolicyReq)) + if err != nil { + return diag.FromErr(fmt.Errorf("error setting aggregation policy for view %v: %w", d.Id(), err)) + } + } else { + err := client.Views.Alter(ctx, sdk.NewAlterViewRequest(id).WithUnsetAggregationPolicy(*sdk.NewViewUnsetAggregationPolicyRequest())) if err != nil { - return fmt.Errorf("error setting tags on %v, err = %w", d.Id(), err) + return diag.FromErr(fmt.Errorf("error unsetting aggregation policy for view %v", d.Id())) } } } - return ReadView(d, meta) + return ReadView(false)(ctx, d, meta) } -// DeleteView implements schema.DeleteFunc. -func DeleteView(d *schema.ResourceData, meta interface{}) error { - client := meta.(*provider.Context).Client - ctx := context.Background() +func DeleteView(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { id := helpers.DecodeSnowflakeID(d.Id()).(sdk.SchemaObjectIdentifier) + client := meta.(*provider.Context).Client - err := client.Views.Drop(ctx, sdk.NewDropViewRequest(id)) + err := client.Views.Drop(ctx, sdk.NewDropViewRequest(id).WithIfExists(true)) if err != nil { - return err + return diag.Diagnostics{ + diag.Diagnostic{ + Severity: diag.Error, + Summary: "Error deleting view", + Detail: fmt.Sprintf("id %v err = %v", id.Name(), err), + }, + } } d.SetId("") - return nil } diff --git a/pkg/resources/view_acceptance_test.go b/pkg/resources/view_acceptance_test.go index dc553bb1b2..73370138fd 100644 --- a/pkg/resources/view_acceptance_test.go +++ b/pkg/resources/view_acceptance_test.go @@ -6,7 +6,15 @@ import ( "testing" acc "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/acceptance" - + accconfig "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/acceptance/bettertestspoc/config" + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/acceptance/testenvs" + + "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/model" + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/acceptance/importchecks" + "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" "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/sdk" @@ -16,31 +24,41 @@ import ( "github.com/hashicorp/terraform-plugin-testing/tfversion" ) -func TestAcc_View(t *testing.T) { - viewId := acc.TestClient().Ids.RandomSchemaObjectIdentifier() - accName := viewId.Name() - query := "SELECT ROLE_NAME, ROLE_OWNER FROM INFORMATION_SCHEMA.APPLICABLE_ROLES" - otherQuery := "SELECT ROLE_NAME, ROLE_OWNER FROM INFORMATION_SCHEMA.APPLICABLE_ROLES where ROLE_OWNER like 'foo%%'" +func TestAcc_View_basic(t *testing.T) { + acc.TestAccPreCheck(t) - m := func() map[string]config.Variable { - return map[string]config.Variable{ - "name": config.StringVariable(accName), - "database": config.StringVariable(acc.TestDatabaseName), - "schema": config.StringVariable(acc.TestSchemaName), - "comment": config.StringVariable("Terraform test resource"), - "is_secure": config.BoolVariable(true), - "or_replace": config.BoolVariable(false), - "copy_grants": config.BoolVariable(false), - "statement": config.StringVariable(query), + rowAccessPolicy, rowAccessPolicyCleanup := acc.TestClient().RowAccessPolicy.CreateRowAccessPolicyWithDataType(t, sdk.DataTypeVARCHAR) + t.Cleanup(rowAccessPolicyCleanup) + + aggregationPolicy, aggregationPolicyCleanup := acc.TestClient().AggregationPolicy.CreateAggregationPolicy(t) + t.Cleanup(aggregationPolicyCleanup) + + rowAccessPolicy2, rowAccessPolicy2Cleanup := acc.TestClient().RowAccessPolicy.CreateRowAccessPolicyWithDataType(t, sdk.DataTypeVARCHAR) + t.Cleanup(rowAccessPolicy2Cleanup) + + aggregationPolicy2, aggregationPolicy2Cleanup := acc.TestClient().AggregationPolicy.CreateAggregationPolicy(t) + t.Cleanup(aggregationPolicy2Cleanup) + + id := acc.TestClient().Ids.RandomSchemaObjectIdentifier() + statement := "SELECT ROLE_NAME, ROLE_OWNER FROM INFORMATION_SCHEMA.APPLICABLE_ROLES" + otherStatement := "SELECT ROLE_NAME, ROLE_OWNER FROM INFORMATION_SCHEMA.APPLICABLE_ROLES where ROLE_OWNER like 'foo%%'" + + viewModel := model.View("test", id.DatabaseName(), id.Name(), id.SchemaName(), statement) + + // generators currently don't handle lists, so use the old way + basicUpdate := func(rap, ap sdk.SchemaObjectIdentifier, statement string) config.Variables { + return config.Variables{ + "name": config.StringVariable(id.Name()), + "database": config.StringVariable(id.DatabaseName()), + "schema": config.StringVariable(id.SchemaName()), + "statement": config.StringVariable(statement), + "row_access_policy": config.StringVariable(rap.FullyQualifiedName()), + "row_access_policy_on": config.ListVariable(config.StringVariable("ROLE_NAME")), + "aggregation_policy": config.StringVariable(ap.FullyQualifiedName()), + "aggregation_policy_entity_key": config.ListVariable(config.StringVariable("ROLE_NAME")), + "comment": config.StringVariable("Terraform test resource"), } } - m2 := m() - m2["comment"] = config.StringVariable("different comment") - m2["is_secure"] = config.BoolVariable(false) - m3 := m() - m3["comment"] = config.StringVariable("different comment") - m3["is_secure"] = config.BoolVariable(false) - m3["statement"] = config.StringVariable(otherQuery) resource.Test(t, resource.TestCase{ ProtoV6ProviderFactories: acc.TestAccProtoV6ProviderFactories, @@ -50,158 +68,317 @@ func TestAcc_View(t *testing.T) { }, CheckDestroy: acc.CheckDestroy(t, resources.View), Steps: []resource.TestStep{ + // without optionals { - ConfigDirectory: acc.ConfigurationDirectory("TestAcc_View_basic"), - ConfigVariables: m(), - Check: resource.ComposeTestCheckFunc( - resource.TestCheckResourceAttr("snowflake_view.test", "name", accName), - resource.TestCheckResourceAttr("snowflake_view.test", "statement", query), - resource.TestCheckResourceAttr("snowflake_view.test", "database", acc.TestDatabaseName), - resource.TestCheckResourceAttr("snowflake_view.test", "schema", acc.TestSchemaName), - resource.TestCheckResourceAttr("snowflake_view.test", "comment", "Terraform test resource"), - resource.TestCheckResourceAttr("snowflake_view.test", "copy_grants", "false"), - checkBool("snowflake_view.test", "is_secure", true), + Config: accconfig.FromModel(t, viewModel), + Check: assert.AssertThat(t, resourceassert.ViewResource(t, "snowflake_view.test"). + HasNameString(id.Name()). + HasStatementString(statement). + HasDatabaseString(id.DatabaseName()). + HasSchemaString(id.SchemaName())), + }, + // import - without optionals + { + Config: accconfig.FromModel(t, viewModel), + ResourceName: "snowflake_view.test", + ImportState: true, + ImportStateCheck: assert.AssertThatImport(t, assert.CheckImport(importchecks.TestCheckResourceAttrInstanceState(helpers.EncodeSnowflakeID(id), "name", id.Name())), + resourceassert.ImportedViewResource(t, helpers.EncodeSnowflakeID(id)). + HasNameString(id.Name()). + HasDatabaseString(id.DatabaseName()). + HasSchemaString(id.SchemaName()). + HasStatementString(statement)), + }, + // set other fields + { + ConfigDirectory: acc.ConfigurationDirectory("TestAcc_View/basic_update"), + ConfigVariables: basicUpdate(rowAccessPolicy.ID(), aggregationPolicy, statement), + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectResourceAction("snowflake_view.test", plancheck.ResourceActionUpdate), + }, + }, + Check: assert.AssertThat(t, resourceassert.ViewResource(t, "snowflake_view.test"). + HasNameString(id.Name()). + HasStatementString(statement). + HasDatabaseString(id.DatabaseName()). + HasSchemaString(id.SchemaName()). + HasCommentString("Terraform test resource"), + assert.Check(resource.TestCheckResourceAttr("snowflake_view.test", "aggregation_policy.#", "1")), + assert.Check(resource.TestCheckResourceAttr("snowflake_view.test", "aggregation_policy.0.policy_name", aggregationPolicy.FullyQualifiedName())), + assert.Check(resource.TestCheckResourceAttr("snowflake_view.test", "aggregation_policy.0.entity_key.#", "1")), + assert.Check(resource.TestCheckResourceAttr("snowflake_view.test", "aggregation_policy.0.entity_key.0", "ROLE_NAME")), + assert.Check(resource.TestCheckResourceAttr("snowflake_view.test", "row_access_policy.#", "1")), + assert.Check(resource.TestCheckResourceAttr("snowflake_view.test", "row_access_policy.0.policy_name", rowAccessPolicy.ID().FullyQualifiedName())), + assert.Check(resource.TestCheckResourceAttr("snowflake_view.test", "row_access_policy.0.on.#", "1")), + assert.Check(resource.TestCheckResourceAttr("snowflake_view.test", "row_access_policy.0.on.0", "ROLE_NAME")), ), }, - // update parameters + // change policies { - ConfigDirectory: acc.ConfigurationDirectory("TestAcc_View_basic"), - ConfigVariables: m2, - Check: resource.ComposeTestCheckFunc( - resource.TestCheckResourceAttr("snowflake_view.test", "name", accName), - resource.TestCheckResourceAttr("snowflake_view.test", "statement", query), - resource.TestCheckResourceAttr("snowflake_view.test", "database", acc.TestDatabaseName), - resource.TestCheckResourceAttr("snowflake_view.test", "schema", acc.TestSchemaName), - resource.TestCheckResourceAttr("snowflake_view.test", "comment", "different comment"), - resource.TestCheckResourceAttr("snowflake_view.test", "copy_grants", "false"), - checkBool("snowflake_view.test", "is_secure", false), + ConfigDirectory: acc.ConfigurationDirectory("TestAcc_View/basic_update"), + ConfigVariables: basicUpdate(rowAccessPolicy2.ID(), aggregationPolicy2, statement), + Check: assert.AssertThat(t, resourceassert.ViewResource(t, "snowflake_view.test"). + HasNameString(id.Name()). + HasStatementString(statement). + HasDatabaseString(id.DatabaseName()). + HasSchemaString(id.SchemaName()). + HasCommentString("Terraform test resource"), + assert.Check(resource.TestCheckResourceAttr("snowflake_view.test", "aggregation_policy.#", "1")), + assert.Check(resource.TestCheckResourceAttr("snowflake_view.test", "aggregation_policy.0.policy_name", aggregationPolicy2.FullyQualifiedName())), + assert.Check(resource.TestCheckResourceAttr("snowflake_view.test", "aggregation_policy.0.entity_key.#", "1")), + assert.Check(resource.TestCheckResourceAttr("snowflake_view.test", "aggregation_policy.0.entity_key.0", "ROLE_NAME")), + assert.Check(resource.TestCheckResourceAttr("snowflake_view.test", "row_access_policy.#", "1")), + assert.Check(resource.TestCheckResourceAttr("snowflake_view.test", "row_access_policy.0.policy_name", rowAccessPolicy2.ID().FullyQualifiedName())), + assert.Check(resource.TestCheckResourceAttr("snowflake_view.test", "row_access_policy.0.on.#", "1")), + assert.Check(resource.TestCheckResourceAttr("snowflake_view.test", "row_access_policy.0.on.0", "ROLE_NAME")), ), }, - // change statement + // change statement and policies { - ConfigDirectory: acc.ConfigurationDirectory("TestAcc_View_basic"), - ConfigVariables: m3, - Check: resource.ComposeTestCheckFunc( - resource.TestCheckResourceAttr("snowflake_view.test", "name", accName), - resource.TestCheckResourceAttr("snowflake_view.test", "statement", otherQuery), - resource.TestCheckResourceAttr("snowflake_view.test", "database", acc.TestDatabaseName), - resource.TestCheckResourceAttr("snowflake_view.test", "schema", acc.TestSchemaName), - resource.TestCheckResourceAttr("snowflake_view.test", "comment", "different comment"), - // copy grants is currently set to true for recreation - resource.TestCheckResourceAttr("snowflake_view.test", "copy_grants", "true"), - checkBool("snowflake_view.test", "is_secure", false), + ConfigDirectory: acc.ConfigurationDirectory("TestAcc_View/basic_update"), + ConfigVariables: basicUpdate(rowAccessPolicy.ID(), aggregationPolicy, otherStatement), + Check: assert.AssertThat(t, resourceassert.ViewResource(t, "snowflake_view.test"). + HasNameString(id.Name()). + HasStatementString(otherStatement). + HasDatabaseString(id.DatabaseName()). + HasSchemaString(id.SchemaName()). + HasCommentString("Terraform test resource"), + assert.Check(resource.TestCheckResourceAttr("snowflake_view.test", "aggregation_policy.#", "1")), + assert.Check(resource.TestCheckResourceAttr("snowflake_view.test", "aggregation_policy.0.policy_name", aggregationPolicy.FullyQualifiedName())), + assert.Check(resource.TestCheckResourceAttr("snowflake_view.test", "aggregation_policy.0.entity_key.#", "1")), + assert.Check(resource.TestCheckResourceAttr("snowflake_view.test", "aggregation_policy.0.entity_key.0", "ROLE_NAME")), + assert.Check(resource.TestCheckResourceAttr("snowflake_view.test", "row_access_policy.#", "1")), + assert.Check(resource.TestCheckResourceAttr("snowflake_view.test", "row_access_policy.0.policy_name", rowAccessPolicy.ID().FullyQualifiedName())), + assert.Check(resource.TestCheckResourceAttr("snowflake_view.test", "row_access_policy.0.on.#", "1")), + assert.Check(resource.TestCheckResourceAttr("snowflake_view.test", "row_access_policy.0.on.0", "ROLE_NAME")), ), }, - // change statement externally + // change statements externally { PreConfig: func() { - acc.TestClient().View.RecreateView(t, viewId, query) + acc.TestClient().View.RecreateView(t, id, statement) }, - ConfigDirectory: acc.ConfigurationDirectory("TestAcc_View_basic"), - ConfigVariables: m3, - Check: resource.ComposeTestCheckFunc( - resource.TestCheckResourceAttr("snowflake_view.test", "name", accName), - resource.TestCheckResourceAttr("snowflake_view.test", "statement", otherQuery), - resource.TestCheckResourceAttr("snowflake_view.test", "database", acc.TestDatabaseName), - resource.TestCheckResourceAttr("snowflake_view.test", "schema", acc.TestSchemaName), - resource.TestCheckResourceAttr("snowflake_view.test", "comment", "different comment"), - resource.TestCheckResourceAttr("snowflake_view.test", "copy_grants", "true"), - checkBool("snowflake_view.test", "is_secure", false), + ConfigDirectory: acc.ConfigurationDirectory("TestAcc_View/basic_update"), + ConfigVariables: basicUpdate(rowAccessPolicy.ID(), aggregationPolicy, otherStatement), + Check: assert.AssertThat(t, resourceassert.ViewResource(t, "snowflake_view.test"). + HasNameString(id.Name()). + HasStatementString(otherStatement). + HasDatabaseString(id.DatabaseName()). + HasSchemaString(id.SchemaName()). + HasCommentString("Terraform test resource"), + assert.Check(resource.TestCheckResourceAttr("snowflake_view.test", "aggregation_policy.#", "1")), + assert.Check(resource.TestCheckResourceAttr("snowflake_view.test", "aggregation_policy.0.policy_name", aggregationPolicy.FullyQualifiedName())), + assert.Check(resource.TestCheckResourceAttr("snowflake_view.test", "aggregation_policy.0.entity_key.#", "1")), + assert.Check(resource.TestCheckResourceAttr("snowflake_view.test", "aggregation_policy.0.entity_key.0", "ROLE_NAME")), + assert.Check(resource.TestCheckResourceAttr("snowflake_view.test", "row_access_policy.#", "1")), + assert.Check(resource.TestCheckResourceAttr("snowflake_view.test", "row_access_policy.0.policy_name", rowAccessPolicy.ID().FullyQualifiedName())), + assert.Check(resource.TestCheckResourceAttr("snowflake_view.test", "row_access_policy.0.on.#", "1")), + assert.Check(resource.TestCheckResourceAttr("snowflake_view.test", "row_access_policy.0.on.0", "ROLE_NAME")), + ), + }, + // import - with optionals + { + ConfigDirectory: acc.ConfigurationDirectory("TestAcc_View/basic_update"), + ConfigVariables: basicUpdate(rowAccessPolicy.ID(), aggregationPolicy, otherStatement), + ResourceName: "snowflake_view.test", + ImportState: true, + ImportStateCheck: assert.AssertThatImport(t, assert.CheckImport(importchecks.TestCheckResourceAttrInstanceState(helpers.EncodeSnowflakeID(id), "name", id.Name())), + resourceassert.ImportedViewResource(t, helpers.EncodeSnowflakeID(id)). + HasNameString(id.Name()). + HasStatementString(otherStatement). + HasDatabaseString(id.DatabaseName()). + HasSchemaString(id.SchemaName()). + HasCommentString("Terraform test resource"). + HasIsSecureString("false"). + HasIsTemporaryString("false"). + HasChangeTrackingString("false"), + assert.CheckImport(importchecks.TestCheckResourceAttrInstanceState(helpers.EncodeSnowflakeID(id), "aggregation_policy.#", "1")), + assert.CheckImport(importchecks.TestCheckResourceAttrInstanceState(helpers.EncodeSnowflakeID(id), "aggregation_policy.0.policy_name", aggregationPolicy.FullyQualifiedName())), + assert.CheckImport(importchecks.TestCheckResourceAttrInstanceState(helpers.EncodeSnowflakeID(id), "aggregation_policy.0.entity_key.#", "1")), + assert.CheckImport(importchecks.TestCheckResourceAttrInstanceState(helpers.EncodeSnowflakeID(id), "aggregation_policy.0.entity_key.0", "ROLE_NAME")), + assert.CheckImport(importchecks.TestCheckResourceAttrInstanceState(helpers.EncodeSnowflakeID(id), "row_access_policy.#", "1")), + assert.CheckImport(importchecks.TestCheckResourceAttrInstanceState(helpers.EncodeSnowflakeID(id), "row_access_policy.0.policy_name", rowAccessPolicy.ID().FullyQualifiedName())), + assert.CheckImport(importchecks.TestCheckResourceAttrInstanceState(helpers.EncodeSnowflakeID(id), "row_access_policy.0.on.#", "1")), + assert.CheckImport(importchecks.TestCheckResourceAttrInstanceState(helpers.EncodeSnowflakeID(id), "row_access_policy.0.on.0", "ROLE_NAME")), ), }, - // IMPORT + // unset { - ConfigVariables: m3, - ResourceName: "snowflake_view.test", - ImportState: true, - ImportStateVerify: true, - ImportStateVerifyIgnore: []string{"or_replace"}, + Config: accconfig.FromModel(t, viewModel.WithStatement(otherStatement)), + ResourceName: "snowflake_view.test", + Check: assert.AssertThat(t, resourceassert.ViewResource(t, "snowflake_view.test"). + HasNameString(id.Name()). + HasStatementString(otherStatement). + HasDatabaseString(id.DatabaseName()). + HasSchemaString(id.SchemaName()). + HasCommentString(""), + assert.Check(resource.TestCheckNoResourceAttr("snowflake_view.test", "aggregation_policy.#")), + assert.Check(resource.TestCheckNoResourceAttr("snowflake_view.test", "row_access_policy.#")), + ), + }, + // recreate - change is_recursive + { + Config: accconfig.FromModel(t, viewModel.WithIsRecursive("true")), + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectResourceAction("snowflake_view.test", plancheck.ResourceActionDestroyBeforeCreate), + }, + }, + Check: assert.AssertThat(t, resourceassert.ViewResource(t, "snowflake_view.test"). + HasNameString(id.Name()). + HasStatementString(otherStatement). + HasDatabaseString(id.DatabaseName()). + HasSchemaString(id.SchemaName()). + HasCommentString(""). + HasIsRecursiveString("true"). + HasIsTemporaryString("default"). + HasChangeTrackingString("default"), + assert.Check(resource.TestCheckNoResourceAttr("snowflake_view.test", "aggregation_policy.#")), + assert.Check(resource.TestCheckNoResourceAttr("snowflake_view.test", "row_access_policy.#")), + ), }, }, }) } -func TestAcc_View_Tags(t *testing.T) { - viewName := acc.TestClient().Ids.Alpha() - tag1Name := acc.TestClient().Ids.Alpha() - tag2Name := acc.TestClient().Ids.Alpha() +func TestAcc_View_recursive(t *testing.T) { + acc.TestAccPreCheck(t) + id := acc.TestClient().Ids.RandomSchemaObjectIdentifier() + statement := "SELECT ROLE_NAME, ROLE_OWNER FROM INFORMATION_SCHEMA.APPLICABLE_ROLES" + viewModel := model.View("test", id.DatabaseName(), id.Name(), id.SchemaName(), statement) + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: acc.TestAccProtoV6ProviderFactories, + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.RequireAbove(tfversion.Version1_5_0), + }, + CheckDestroy: acc.CheckDestroy(t, resources.View), + Steps: []resource.TestStep{ + { + Config: accconfig.FromModel(t, viewModel.WithIsRecursive("true")), + Check: assert.AssertThat(t, resourceassert.ViewResource(t, "snowflake_view.test"). + HasNameString(id.Name()). + HasStatementString(statement). + HasDatabaseString(id.DatabaseName()). + HasSchemaString(id.SchemaName()). + HasIsRecursiveString("true")), + }, + { + Config: accconfig.FromModel(t, viewModel.WithIsRecursive("true")), + ResourceName: "snowflake_view.test", + ImportState: true, + ImportStateCheck: assert.AssertThatImport(t, assert.CheckImport(importchecks.TestCheckResourceAttrInstanceState(helpers.EncodeSnowflakeID(id), "name", id.Name())), + resourceassert.ImportedViewResource(t, helpers.EncodeSnowflakeID(id)). + HasNameString(id.Name()). + HasDatabaseString(id.DatabaseName()). + HasSchemaString(id.SchemaName()). + HasStatementString(statement). + HasIsRecursiveString("true")), + }, + }, + }) +} - query := "SELECT ROLE_NAME, ROLE_OWNER FROM INFORMATION_SCHEMA.APPLICABLE_ROLES" +func TestAcc_View_complete(t *testing.T) { + acc.TestAccPreCheck(t) + id := acc.TestClient().Ids.RandomSchemaObjectIdentifier() + // use a simple table to test change_tracking, otherwise it fails with: Change tracking is not supported on queries with joins of type '[LEFT_OUTER_JOIN]' + table, tableCleanup := acc.TestClient().Table.CreateTable(t) + t.Cleanup(tableCleanup) + statement := fmt.Sprintf("SELECT id FROM %s", table.ID().FullyQualifiedName()) + rowAccessPolicy, rowAccessPolicyCleanup := acc.TestClient().RowAccessPolicy.CreateRowAccessPolicyWithDataType(t, sdk.DataTypeNumber) + t.Cleanup(rowAccessPolicyCleanup) + + aggregationPolicy, aggregationPolicyCleanup := acc.TestClient().AggregationPolicy.CreateAggregationPolicy(t) + t.Cleanup(aggregationPolicyCleanup) m := func() map[string]config.Variable { return map[string]config.Variable{ - "name": config.StringVariable(viewName), - "database": config.StringVariable(acc.TestDatabaseName), - "schema": config.StringVariable(acc.TestSchemaName), - "statement": config.StringVariable(query), - "tag1Name": config.StringVariable(tag1Name), - "tag2Name": config.StringVariable(tag2Name), + "name": config.StringVariable(id.Name()), + "database": config.StringVariable(id.DatabaseName()), + "schema": config.StringVariable(id.SchemaName()), + "comment": config.StringVariable("Terraform test resource"), + "is_secure": config.BoolVariable(true), + "is_temporary": config.BoolVariable(false), + "or_replace": config.BoolVariable(false), + "copy_grants": config.BoolVariable(false), + "change_tracking": config.BoolVariable(true), + "row_access_policy": config.StringVariable(rowAccessPolicy.ID().FullyQualifiedName()), + "row_access_policy_on": config.ListVariable(config.StringVariable("ID")), + "aggregation_policy": config.StringVariable(aggregationPolicy.FullyQualifiedName()), + "aggregation_policy_entity_key": config.ListVariable(config.StringVariable("ID")), + "statement": config.StringVariable(statement), } } - 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.View), Steps: []resource.TestStep{ - // create tags { - ConfigDirectory: config.TestStepDirectory(), + ConfigDirectory: acc.ConfigurationDirectory("TestAcc_View/complete"), ConfigVariables: m(), - Check: resource.ComposeTestCheckFunc( - resource.TestCheckResourceAttr("snowflake_view.test", "name", viewName), - resource.TestCheckResourceAttr("snowflake_view.test", "tag.#", "1"), - resource.TestCheckResourceAttr("snowflake_view.test", "tag.0.name", tag1Name), - resource.TestCheckResourceAttr("snowflake_view.test", "tag.0.value", "some_value"), + Check: assert.AssertThat(t, resourceassert.ViewResource(t, "snowflake_view.test"). + HasNameString(id.Name()). + HasStatementString(statement). + HasDatabaseString(id.DatabaseName()). + HasSchemaString(id.SchemaName()). + HasCommentString("Terraform test resource"). + HasIsSecureString("true"). + HasIsTemporaryString("false"). + HasChangeTrackingString("true"), + assert.Check(resource.TestCheckResourceAttr("snowflake_view.test", "aggregation_policy.#", "1")), + assert.Check(resource.TestCheckResourceAttr("snowflake_view.test", "aggregation_policy.0.policy_name", aggregationPolicy.FullyQualifiedName())), + assert.Check(resource.TestCheckResourceAttr("snowflake_view.test", "aggregation_policy.0.entity_key.#", "1")), + assert.Check(resource.TestCheckResourceAttr("snowflake_view.test", "aggregation_policy.0.entity_key.0", "ID")), + assert.Check(resource.TestCheckResourceAttr("snowflake_view.test", "row_access_policy.#", "1")), + assert.Check(resource.TestCheckResourceAttr("snowflake_view.test", "row_access_policy.0.policy_name", rowAccessPolicy.ID().FullyQualifiedName())), + assert.Check(resource.TestCheckResourceAttr("snowflake_view.test", "row_access_policy.0.on.#", "1")), + assert.Check(resource.TestCheckResourceAttr("snowflake_view.test", "row_access_policy.0.on.0", "ID")), + resourceshowoutputassert.ViewShowOutput(t, "snowflake_view.test"). + HasName(id.Name()). + HasDatabaseName(id.DatabaseName()). + HasSchemaName(id.SchemaName()). + HasComment("Terraform test resource"). + HasIsSecure(true). + HasChangeTracking("ON"), ), }, - // update tags { - ConfigDirectory: config.TestStepDirectory(), + ConfigDirectory: acc.ConfigurationDirectory("TestAcc_View/complete"), ConfigVariables: m(), - Check: resource.ComposeTestCheckFunc( - resource.TestCheckResourceAttr("snowflake_view.test", "name", viewName), - resource.TestCheckResourceAttr("snowflake_view.test", "tag.#", "1"), - resource.TestCheckResourceAttr("snowflake_view.test", "tag.0.name", tag2Name), - resource.TestCheckResourceAttr("snowflake_view.test", "tag.0.value", "some_value"), + ResourceName: "snowflake_view.test", + ImportState: true, + ImportStateCheck: assert.AssertThatImport(t, assert.CheckImport(importchecks.TestCheckResourceAttrInstanceState(helpers.EncodeSnowflakeID(id), "name", id.Name())), + resourceassert.ImportedViewResource(t, helpers.EncodeSnowflakeID(id)). + HasNameString(id.Name()). + HasStatementString(statement). + HasDatabaseString(id.DatabaseName()). + HasSchemaString(id.SchemaName()). + HasCommentString("Terraform test resource"). + HasIsSecureString("true"). + HasIsTemporaryString("false").HasChangeTrackingString("true"), + assert.CheckImport(importchecks.TestCheckResourceAttrInstanceState(helpers.EncodeSnowflakeID(id), "aggregation_policy.#", "1")), + assert.CheckImport(importchecks.TestCheckResourceAttrInstanceState(helpers.EncodeSnowflakeID(id), "aggregation_policy.0.policy_name", aggregationPolicy.FullyQualifiedName())), + assert.CheckImport(importchecks.TestCheckResourceAttrInstanceState(helpers.EncodeSnowflakeID(id), "aggregation_policy.0.entity_key.#", "1")), + assert.CheckImport(importchecks.TestCheckResourceAttrInstanceState(helpers.EncodeSnowflakeID(id), "aggregation_policy.0.entity_key.0", "ID")), + assert.CheckImport(importchecks.TestCheckResourceAttrInstanceState(helpers.EncodeSnowflakeID(id), "row_access_policy.#", "1")), + assert.CheckImport(importchecks.TestCheckResourceAttrInstanceState(helpers.EncodeSnowflakeID(id), "row_access_policy.0.policy_name", rowAccessPolicy.ID().FullyQualifiedName())), + assert.CheckImport(importchecks.TestCheckResourceAttrInstanceState(helpers.EncodeSnowflakeID(id), "row_access_policy.0.on.#", "1")), + assert.CheckImport(importchecks.TestCheckResourceAttrInstanceState(helpers.EncodeSnowflakeID(id), "row_access_policy.0.on.0", "ID")), ), }, - // IMPORT - { - ConfigVariables: m(), - ResourceName: "snowflake_view.test", - ImportState: true, - ImportStateVerify: true, - ImportStateVerifyIgnore: []string{"or_replace", "tag"}, - }, }, }) } func TestAcc_View_Rename(t *testing.T) { - viewName := acc.TestClient().Ids.Alpha() - newViewName := acc.TestClient().Ids.Alpha() - query := "SELECT ROLE_NAME, ROLE_OWNER FROM INFORMATION_SCHEMA.APPLICABLE_ROLES" - - m := func() map[string]config.Variable { - return map[string]config.Variable{ - "name": config.StringVariable(viewName), - "database": config.StringVariable(acc.TestDatabaseName), - "schema": config.StringVariable(acc.TestSchemaName), - "comment": config.StringVariable("Terraform test resource"), - "is_secure": config.BoolVariable(true), - "or_replace": config.BoolVariable(false), - "copy_grants": config.BoolVariable(false), - "statement": config.StringVariable(query), - } - } - m2 := m() - m2["name"] = config.StringVariable(newViewName) - m2["comment"] = config.StringVariable("new comment") - + statement := "SELECT ROLE_NAME, ROLE_OWNER FROM INFORMATION_SCHEMA.APPLICABLE_ROLES" + id := acc.TestClient().Ids.RandomSchemaObjectIdentifier() + newId := acc.TestClient().Ids.RandomSchemaObjectIdentifier() + viewModel := model.View("test", id.DatabaseName(), id.Name(), id.SchemaName(), statement).WithComment("foo") resource.Test(t, resource.TestCase{ ProtoV6ProviderFactories: acc.TestAccProtoV6ProviderFactories, PreCheck: func() { acc.TestAccPreCheck(t) }, @@ -211,24 +388,23 @@ func TestAcc_View_Rename(t *testing.T) { CheckDestroy: acc.CheckDestroy(t, resources.View), Steps: []resource.TestStep{ { - ConfigDirectory: acc.ConfigurationDirectory("TestAcc_View_basic"), - ConfigVariables: m(), - Check: resource.ComposeTestCheckFunc( - resource.TestCheckResourceAttr("snowflake_view.test", "name", viewName), + Config: accconfig.FromModel(t, viewModel), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr("snowflake_view.test", "name", id.Name()), + resource.TestCheckResourceAttr("snowflake_view.test", "comment", "foo"), ), }, // rename with one param changed { - ConfigDirectory: acc.ConfigurationDirectory("TestAcc_View_basic"), - ConfigVariables: m2, + Config: accconfig.FromModel(t, model.View("test", newId.DatabaseName(), newId.Name(), newId.SchemaName(), statement).WithComment("foo")), ConfigPlanChecks: resource.ConfigPlanChecks{ PreApply: []plancheck.PlanCheck{ plancheck.ExpectResourceAction("snowflake_view.test", plancheck.ResourceActionUpdate), }, }, - Check: resource.ComposeTestCheckFunc( - resource.TestCheckResourceAttr("snowflake_view.test", "name", newViewName), - resource.TestCheckResourceAttr("snowflake_view.test", "comment", "new comment"), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr("snowflake_view.test", "name", newId.Name()), + resource.TestCheckResourceAttr("snowflake_view.test", "comment", "foo"), ), }, }, @@ -236,23 +412,10 @@ func TestAcc_View_Rename(t *testing.T) { } func TestAcc_ViewChangeCopyGrants(t *testing.T) { - accName := acc.TestClient().Ids.Alpha() + id := acc.TestClient().Ids.RandomSchemaObjectIdentifier() - m := func() map[string]config.Variable { - return map[string]config.Variable{ - "name": config.StringVariable(accName), - "database": config.StringVariable(acc.TestDatabaseName), - "schema": config.StringVariable(acc.TestSchemaName), - "comment": config.StringVariable("Terraform test resource"), - "is_secure": config.BoolVariable(true), - "or_replace": config.BoolVariable(false), - "copy_grants": config.BoolVariable(false), - "statement": config.StringVariable("SELECT ROLE_NAME, ROLE_OWNER FROM INFORMATION_SCHEMA.APPLICABLE_ROLES"), - } - } - m2 := m() - m2["copy_grants"] = config.BoolVariable(true) - m2["or_replace"] = config.BoolVariable(true) + statement := "SELECT ROLE_NAME, ROLE_OWNER FROM INFORMATION_SCHEMA.APPLICABLE_ROLES" + viewModel := model.View("test", id.DatabaseName(), id.Name(), id.SchemaName(), statement).WithIsSecure("true").WithOrReplace(false).WithCopyGrants(false) var createdOn string @@ -265,15 +428,14 @@ func TestAcc_ViewChangeCopyGrants(t *testing.T) { CheckDestroy: acc.CheckDestroy(t, resources.View), Steps: []resource.TestStep{ { - ConfigDirectory: acc.ConfigurationDirectory("TestAcc_View_basic"), - ConfigVariables: m(), - Check: resource.ComposeTestCheckFunc( - resource.TestCheckResourceAttr("snowflake_view.test", "name", accName), - resource.TestCheckResourceAttr("snowflake_view.test", "database", acc.TestDatabaseName), - resource.TestCheckResourceAttr("snowflake_view.test", "comment", "Terraform test resource"), + Config: accconfig.FromModel(t, viewModel), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr("snowflake_view.test", "name", id.Name()), + resource.TestCheckResourceAttr("snowflake_view.test", "database", id.DatabaseName()), resource.TestCheckResourceAttr("snowflake_view.test", "copy_grants", "false"), checkBool("snowflake_view.test", "is_secure", true), - resource.TestCheckResourceAttrWith("snowflake_view.test", "created_on", func(value string) error { + resource.TestCheckResourceAttr("snowflake_view.test", "show_output.#", "1"), + resource.TestCheckResourceAttrWith("snowflake_view.test", "show_output.0.created_on", func(value string) error { createdOn = value return nil }), @@ -281,10 +443,10 @@ func TestAcc_ViewChangeCopyGrants(t *testing.T) { }, // Checks that copy_grants changes don't trigger a drop { - ConfigDirectory: acc.ConfigurationDirectory("TestAcc_View_basic"), - ConfigVariables: m2, - Check: resource.ComposeTestCheckFunc( - resource.TestCheckResourceAttrWith("snowflake_view.test", "created_on", func(value string) error { + Config: accconfig.FromModel(t, viewModel.WithCopyGrants(true).WithOrReplace(true)), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr("snowflake_view.test", "show_output.#", "1"), + resource.TestCheckResourceAttrWith("snowflake_view.test", "show_output.0.created_on", func(value string) error { if value != createdOn { return fmt.Errorf("view was recreated") } @@ -298,22 +460,10 @@ func TestAcc_ViewChangeCopyGrants(t *testing.T) { } func TestAcc_ViewChangeCopyGrantsReversed(t *testing.T) { - accName := acc.TestClient().Ids.Alpha() + id := acc.TestClient().Ids.RandomSchemaObjectIdentifier() - m := func() map[string]config.Variable { - return map[string]config.Variable{ - "name": config.StringVariable(accName), - "database": config.StringVariable(acc.TestDatabaseName), - "schema": config.StringVariable(acc.TestSchemaName), - "comment": config.StringVariable("Terraform test resource"), - "is_secure": config.BoolVariable(true), - "or_replace": config.BoolVariable(true), - "copy_grants": config.BoolVariable(true), - "statement": config.StringVariable("SELECT ROLE_NAME, ROLE_OWNER FROM INFORMATION_SCHEMA.APPLICABLE_ROLES"), - } - } - m2 := m() - m2["copy_grants"] = config.BoolVariable(false) + statement := "SELECT ROLE_NAME, ROLE_OWNER FROM INFORMATION_SCHEMA.APPLICABLE_ROLES" + viewModel := model.View("test", id.DatabaseName(), id.Name(), id.SchemaName(), statement).WithIsSecure("true").WithOrReplace(true).WithCopyGrants(true) var createdOn string @@ -326,11 +476,11 @@ func TestAcc_ViewChangeCopyGrantsReversed(t *testing.T) { CheckDestroy: acc.CheckDestroy(t, resources.View), Steps: []resource.TestStep{ { - ConfigDirectory: acc.ConfigurationDirectory("TestAcc_View_basic"), - ConfigVariables: m(), - Check: resource.ComposeTestCheckFunc( + Config: accconfig.FromModel(t, viewModel), + Check: resource.ComposeAggregateTestCheckFunc( resource.TestCheckResourceAttr("snowflake_view.test", "copy_grants", "true"), - resource.TestCheckResourceAttrWith("snowflake_view.test", "created_on", func(value string) error { + resource.TestCheckResourceAttr("snowflake_view.test", "show_output.#", "1"), + resource.TestCheckResourceAttrWith("snowflake_view.test", "show_output.0.created_on", func(value string) error { createdOn = value return nil }), @@ -338,10 +488,10 @@ func TestAcc_ViewChangeCopyGrantsReversed(t *testing.T) { ), }, { - ConfigDirectory: acc.ConfigurationDirectory("TestAcc_View_basic"), - ConfigVariables: m2, - Check: resource.ComposeTestCheckFunc( - resource.TestCheckResourceAttrWith("snowflake_view.test", "created_on", func(value string) error { + Config: accconfig.FromModel(t, viewModel.WithCopyGrants(false)), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr("snowflake_view.test", "show_output.#", "1"), + resource.TestCheckResourceAttrWith("snowflake_view.test", "show_output.0.created_on", func(value string) error { if value != createdOn { return fmt.Errorf("view was recreated") } @@ -354,7 +504,7 @@ func TestAcc_ViewChangeCopyGrantsReversed(t *testing.T) { }) } -func TestAcc_ViewStatementUpdate(t *testing.T) { +func TestAcc_ViewCopyGrantsStatementUpdate(t *testing.T) { tableName := acc.TestClient().Ids.Alpha() viewName := acc.TestClient().Ids.Alpha() @@ -368,7 +518,7 @@ func TestAcc_ViewStatementUpdate(t *testing.T) { Steps: []resource.TestStep{ { Config: viewConfigWithGrants(acc.TestDatabaseName, acc.TestSchemaName, tableName, viewName, `\"name\"`), - Check: resource.ComposeTestCheckFunc( + 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"), @@ -376,7 +526,7 @@ func TestAcc_ViewStatementUpdate(t *testing.T) { }, { Config: viewConfigWithGrants(acc.TestDatabaseName, acc.TestSchemaName, tableName, viewName, "*"), - Check: resource.ComposeTestCheckFunc( + Check: resource.ComposeAggregateTestCheckFunc( resource.TestCheckResourceAttr("data.snowflake_grants.grants", "grants.#", "2"), resource.TestCheckResourceAttr("data.snowflake_grants.grants", "grants.1.privilege", "SELECT"), ), @@ -403,13 +553,13 @@ func TestAcc_View_copyGrants(t *testing.T) { }, { Config: viewConfigWithCopyGrantsAndOrReplace(acc.TestDatabaseName, acc.TestSchemaName, accName, query, true, true), - Check: resource.ComposeTestCheckFunc( + Check: resource.ComposeAggregateTestCheckFunc( resource.TestCheckResourceAttr("snowflake_view.test", "name", accName), ), }, { Config: viewConfigWithOrReplace(acc.TestDatabaseName, acc.TestSchemaName, accName, query, true), - Check: resource.ComposeTestCheckFunc( + Check: resource.ComposeAggregateTestCheckFunc( resource.TestCheckResourceAttr("snowflake_view.test", "name", accName), ), }, @@ -418,10 +568,10 @@ func TestAcc_View_copyGrants(t *testing.T) { } func TestAcc_View_Issue2640(t *testing.T) { - viewId := acc.TestClient().Ids.RandomSchemaObjectIdentifier() - viewName := viewId.Name() + id := acc.TestClient().Ids.RandomSchemaObjectIdentifier() part1 := "SELECT ROLE_NAME, ROLE_OWNER FROM INFORMATION_SCHEMA.APPLICABLE_ROLES" part2 := "SELECT ROLE_OWNER, ROLE_NAME FROM INFORMATION_SCHEMA.APPLICABLE_ROLES" + statement := fmt.Sprintf("%s\n\tunion\n%s\n", part1, part2) roleId := acc.TestClient().Ids.RandomAccountObjectIdentifier() resource.Test(t, resource.TestCase{ @@ -433,10 +583,10 @@ func TestAcc_View_Issue2640(t *testing.T) { CheckDestroy: acc.CheckDestroy(t, resources.View), Steps: []resource.TestStep{ { - Config: viewConfigWithMultilineUnionStatement(acc.TestDatabaseName, acc.TestSchemaName, viewName, part1, part2), - Check: resource.ComposeTestCheckFunc( - resource.TestCheckResourceAttr("snowflake_view.test", "name", viewName), - resource.TestCheckResourceAttr("snowflake_view.test", "statement", fmt.Sprintf("%s\n\tunion\n%s\n", part1, part2)), + Config: viewConfigWithMultilineUnionStatement(acc.TestDatabaseName, acc.TestSchemaName, id.Name(), part1, part2), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr("snowflake_view.test", "name", id.Name()), + resource.TestCheckResourceAttr("snowflake_view.test", "statement", statement), resource.TestCheckResourceAttr("snowflake_view.test", "database", acc.TestDatabaseName), resource.TestCheckResourceAttr("snowflake_view.test", "schema", acc.TestSchemaName), ), @@ -446,7 +596,7 @@ func TestAcc_View_Issue2640(t *testing.T) { PreConfig: func() { role, roleCleanup := acc.TestClient().Role.CreateRoleWithIdentifier(t, roleId) t.Cleanup(roleCleanup) - acc.TestClient().Role.GrantOwnershipOnSchemaObject(t, role.ID(), viewId, sdk.ObjectTypeView, sdk.Revoke) + acc.TestClient().Role.GrantOwnershipOnSchemaObject(t, role.ID(), id, sdk.ObjectTypeView, sdk.Revoke) }, ResourceName: "snowflake_view.test", ImportState: true, @@ -455,17 +605,85 @@ func TestAcc_View_Issue2640(t *testing.T) { // import with the proper role { PreConfig: func() { - acc.TestClient().Role.GrantOwnershipOnSchemaObject(t, snowflakeroles.Accountadmin, viewId, sdk.ObjectTypeView, sdk.Revoke) + acc.TestClient().Role.GrantOwnershipOnSchemaObject(t, snowflakeroles.Accountadmin, id, sdk.ObjectTypeView, sdk.Revoke) }, - ResourceName: "snowflake_view.test", - ImportState: true, - ImportStateVerify: true, - ImportStateVerifyIgnore: []string{"or_replace", "created_on"}, + ResourceName: "snowflake_view.test", + ImportState: true, + ImportStateCheck: assert.AssertThatImport(t, assert.CheckImport(importchecks.TestCheckResourceAttrInstanceState(helpers.EncodeSnowflakeID(id), "name", id.Name())), + resourceassert.ImportedViewResource(t, helpers.EncodeSnowflakeID(id)). + HasNameString(id.Name()). + HasStatementString(statement). + HasDatabaseString(id.DatabaseName()). + HasSchemaString(id.SchemaName()), + ), }, }, }) } +func TestAcc_view_migrateFromVersion094(t *testing.T) { + _ = testenvs.GetOrSkipTest(t, testenvs.EnableAcceptance) + acc.TestAccPreCheck(t) + id := acc.TestClient().Ids.RandomSchemaObjectIdentifier() + resourceName := "snowflake_view.test" + statement := "SELECT ROLE_NAME, ROLE_OWNER FROM INFORMATION_SCHEMA.APPLICABLE_ROLES" + viewModel := model.View("test", id.DatabaseName(), id.Name(), id.SchemaName(), statement) + + tag, tagCleanup := acc.TestClient().Tag.CreateTag(t) + t.Cleanup(tagCleanup) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { acc.TestAccPreCheck(t) }, + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.RequireAbove(tfversion.Version1_5_0), + }, + + Steps: []resource.TestStep{ + { + ExternalProviders: map[string]resource.ExternalProvider{ + "snowflake": { + VersionConstraint: "=0.94.0", + Source: "Snowflake-Labs/snowflake", + }, + }, + Config: viewv094WithTags(id, tag.SchemaName, tag.Name, "foo", statement), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr(resourceName, "name", id.Name()), + resource.TestCheckResourceAttr(resourceName, "tag.#", "1"), + resource.TestCheckResourceAttr(resourceName, "tag.0.name", tag.Name), + resource.TestCheckResourceAttr(resourceName, "tag.0.value", "foo"), + ), + }, + { + ProtoV6ProviderFactories: acc.TestAccProtoV6ProviderFactories, + Config: accconfig.FromModel(t, viewModel), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr(resourceName, "name", id.Name()), + resource.TestCheckNoResourceAttr(resourceName, "tag.#"), + ), + }, + }, + }) +} + +func viewv094WithTags(id sdk.SchemaObjectIdentifier, tagSchema, tagName, tagValue, statement string) string { + s := ` +resource "snowflake_view" "test" { + name = "%[1]s" + database = "%[2]s" + schema = "%[6]s" + statement = "%[7]s" + tag { + name = "%[4]s" + value = "%[5]s" + schema = "%[3]s" + database = "%[2]s" + } +} +` + return fmt.Sprintf(s, id.Name(), id.DatabaseName(), tagSchema, tagName, tagValue, id.SchemaName(), statement) +} + func viewConfigWithGrants(databaseName string, schemaName string, tableName string, viewName string, selectStatement string) string { return fmt.Sprintf(` resource "snowflake_table" "table" { diff --git a/pkg/resources/view_stage_upgraders.go b/pkg/resources/view_stage_upgraders.go new file mode 100644 index 0000000000..8883e2190f --- /dev/null +++ b/pkg/resources/view_stage_upgraders.go @@ -0,0 +1,20 @@ +package resources + +import ( + "context" + "strconv" +) + +func v094ViewStateUpgrader(ctx context.Context, rawState map[string]any, meta any) (map[string]any, error) { + if rawState == nil { + return rawState, nil + } + + if v, ok := rawState["is_secure"]; ok { + rawState["is_secure"] = strconv.FormatBool(v.(bool)) + } + + delete(rawState, "tag") + + return rawState, nil +} diff --git a/pkg/schemas/view.go b/pkg/schemas/view.go new file mode 100644 index 0000000000..6ada82a7fe --- /dev/null +++ b/pkg/schemas/view.go @@ -0,0 +1,78 @@ +package schemas + +import ( + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/sdk" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +var ViewDescribeSchema = map[string]*schema.Schema{ + "name": { + Type: schema.TypeString, + Computed: true, + }, + "type": { + Type: schema.TypeString, + Computed: true, + }, + "kind": { + Type: schema.TypeString, + Computed: true, + }, + "is_nullable": { + Type: schema.TypeBool, + Computed: true, + }, + "default": { + Type: schema.TypeString, + Computed: true, + }, + "is_primary": { + Type: schema.TypeBool, + Computed: true, + }, + "is_unique": { + Type: schema.TypeBool, + Computed: true, + }, + "check": { + Type: schema.TypeString, + Computed: true, + }, + "expression": { + Type: schema.TypeString, + Computed: true, + }, + "comment": { + Type: schema.TypeString, + Computed: true, + }, + "policy_name": { + Type: schema.TypeString, + Computed: true, + }, + "privacy_domain": { + Type: schema.TypeString, + Computed: true, + }, +} + +func ViewDescriptionToSchema(description []sdk.ViewDetails) []map[string]any { + result := make([]map[string]any, len(description)) + for i, row := range description { + result[i] = map[string]any{ + "name": row.Name, + "type": row.Type, + "kind": row.Kind, + "is_nullable": row.IsNullable, + "default": row.Default, + "is_primary": row.IsPrimary, + "is_unique": row.IsUnique, + "check": row.Check, + "expression": row.Expression, + "comment": row.Comment, + "policy_name": row.PolicyName, + "privacy_domain": row.PrivacyDomain, + } + } + return result +} diff --git a/pkg/sdk/policy_references.go b/pkg/sdk/policy_references.go index b5aeb6c361..9f5ee04e21 100644 --- a/pkg/sdk/policy_references.go +++ b/pkg/sdk/policy_references.go @@ -68,6 +68,14 @@ type policyReferenceFunctionArguments struct { refEntityDomain *PolicyEntityDomain `ddl:"parameter,single_quotes,arrow_equals" sql:"REF_ENTITY_DOMAIN"` } +// TODO: use PolicyKind in PolicyReference +type PolicyKind string + +const ( + PolicyKindAggregationPolicy PolicyKind = "AGGREGATION_POLICY" + PolicyKindRowAccessPolicy PolicyKind = "ROW_ACCESS_POLICY" +) + type PolicyReference struct { PolicyDb *string PolicySchema *string diff --git a/pkg/sdk/testint/views_gen_integration_test.go b/pkg/sdk/testint/views_gen_integration_test.go index 8d4510cdae..bef10a4911 100644 --- a/pkg/sdk/testint/views_gen_integration_test.go +++ b/pkg/sdk/testint/views_gen_integration_test.go @@ -186,8 +186,8 @@ func TestInt_Views(t *testing.T) { }). WithCopyGrants(true). WithComment("comment"). - WithRowAccessPolicy(*sdk.NewViewRowAccessPolicyRequest(rowAccessPolicy.ID(), []sdk.DoubleQuotedString{{Value: "column_with_comment"}})). - WithAggregationPolicy(*sdk.NewViewAggregationPolicyRequest(aggregationPolicy).WithEntityKey([]sdk.DoubleQuotedString{{Value: "column_with_comment"}})). + WithRowAccessPolicy(*sdk.NewViewRowAccessPolicyRequest(rowAccessPolicy.ID(), []sdk.Column{{Value: "column_with_comment"}})). + WithAggregationPolicy(*sdk.NewViewAggregationPolicyRequest(aggregationPolicy).WithEntityKey([]sdk.Column{{Value: "column_with_comment"}})). WithTag([]sdk.TagAssociation{{ Name: tag.ID(), Value: "v2", @@ -226,7 +226,7 @@ func TestInt_Views(t *testing.T) { WithRecursive(true). WithColumns([]sdk.ViewColumnRequest{ *sdk.NewViewColumnRequest("col1").WithMaskingPolicy( - *sdk.NewViewColumnMaskingPolicyRequest(maskingPolicy.ID()).WithUsing([]sdk.DoubleQuotedString{{Value: "col1"}}), + *sdk.NewViewColumnMaskingPolicyRequest(maskingPolicy.ID()).WithUsing([]sdk.Column{{Value: "col1"}}), ).WithProjectionPolicy( *sdk.NewViewColumnProjectionPolicyRequest(projectionPolicy), ), @@ -504,7 +504,7 @@ func TestInt_Views(t *testing.T) { id := view.ID() // add policy - alterRequest := sdk.NewAlterViewRequest(id).WithAddRowAccessPolicy(*sdk.NewViewAddRowAccessPolicyRequest(rowAccessPolicy.ID(), []sdk.DoubleQuotedString{{Value: "ID"}})) + alterRequest := sdk.NewAlterViewRequest(id).WithAddRowAccessPolicy(*sdk.NewViewAddRowAccessPolicyRequest(rowAccessPolicy.ID(), []sdk.Column{{Value: "ID"}})) err := client.Views.Alter(ctx, alterRequest) require.NoError(t, err) @@ -522,7 +522,7 @@ func TestInt_Views(t *testing.T) { require.Error(t, err, "no rows in result set") // add policy again - alterRequest = sdk.NewAlterViewRequest(id).WithAddRowAccessPolicy(*sdk.NewViewAddRowAccessPolicyRequest(rowAccessPolicy.ID(), []sdk.DoubleQuotedString{{Value: "ID"}})) + alterRequest = sdk.NewAlterViewRequest(id).WithAddRowAccessPolicy(*sdk.NewViewAddRowAccessPolicyRequest(rowAccessPolicy.ID(), []sdk.Column{{Value: "ID"}})) err = client.Views.Alter(ctx, alterRequest) require.NoError(t, err) @@ -533,7 +533,7 @@ func TestInt_Views(t *testing.T) { // drop and add other policy simultaneously alterRequest = sdk.NewAlterViewRequest(id).WithDropAndAddRowAccessPolicy(*sdk.NewViewDropAndAddRowAccessPolicyRequest( *sdk.NewViewDropRowAccessPolicyRequest(rowAccessPolicy.ID()), - *sdk.NewViewAddRowAccessPolicyRequest(rowAccessPolicy2.ID(), []sdk.DoubleQuotedString{{Value: "ID"}}), + *sdk.NewViewAddRowAccessPolicyRequest(rowAccessPolicy2.ID(), []sdk.Column{{Value: "ID"}}), )) err = client.Views.Alter(ctx, alterRequest) require.NoError(t, err) @@ -570,7 +570,7 @@ func TestInt_Views(t *testing.T) { alterRequest = sdk.NewAlterViewRequest(id).WithAddDataMetricFunction(*sdk.NewViewAddDataMetricFunctionRequest([]sdk.ViewDataMetricFunction{ { DataMetricFunction: dataMetricFunction, - On: []sdk.DoubleQuotedString{{Value: "ID"}}, + On: []sdk.Column{{Value: "ID"}}, }, })) err = client.Views.Alter(ctx, alterRequest) @@ -586,7 +586,7 @@ func TestInt_Views(t *testing.T) { alterRequest = sdk.NewAlterViewRequest(id).WithDropDataMetricFunction(*sdk.NewViewDropDataMetricFunctionRequest([]sdk.ViewDataMetricFunction{ { DataMetricFunction: dataMetricFunction, - On: []sdk.DoubleQuotedString{{Value: "ID"}}, + On: []sdk.Column{{Value: "ID"}}, }, })) err = client.Views.Alter(ctx, alterRequest) @@ -600,11 +600,11 @@ func TestInt_Views(t *testing.T) { alterRequest = sdk.NewAlterViewRequest(id).WithAddDataMetricFunction(*sdk.NewViewAddDataMetricFunctionRequest([]sdk.ViewDataMetricFunction{ { DataMetricFunction: dataMetricFunction, - On: []sdk.DoubleQuotedString{{Value: "ID"}}, + On: []sdk.Column{{Value: "ID"}}, }, { DataMetricFunction: dataMetricFunction2, - On: []sdk.DoubleQuotedString{{Value: "ID"}}, + On: []sdk.Column{{Value: "ID"}}, }, })) err = client.Views.Alter(ctx, alterRequest) @@ -636,7 +636,7 @@ func TestInt_Views(t *testing.T) { id := view.ID() // set policy - alterRequest := sdk.NewAlterViewRequest(id).WithSetAggregationPolicy(*sdk.NewViewSetAggregationPolicyRequest(aggregationPolicy).WithEntityKey([]sdk.DoubleQuotedString{{Value: "ID"}})) + alterRequest := sdk.NewAlterViewRequest(id).WithSetAggregationPolicy(*sdk.NewViewSetAggregationPolicyRequest(aggregationPolicy).WithEntityKey([]sdk.Column{{Value: "ID"}})) err := client.Views.Alter(ctx, alterRequest) require.NoError(t, err) @@ -648,7 +648,7 @@ func TestInt_Views(t *testing.T) { // set policy with force alterRequest = sdk.NewAlterViewRequest(id).WithSetAggregationPolicy(*sdk.NewViewSetAggregationPolicyRequest(aggregationPolicy2). - WithEntityKey([]sdk.DoubleQuotedString{{Value: "ID"}}). + WithEntityKey([]sdk.Column{{Value: "ID"}}). WithForce(true)) err = client.Views.Alter(ctx, alterRequest) require.NoError(t, err) diff --git a/pkg/sdk/views_def.go b/pkg/sdk/views_def.go index fef265a355..680407fb5a 100644 --- a/pkg/sdk/views_def.go +++ b/pkg/sdk/views_def.go @@ -63,7 +63,7 @@ var viewDetails = g.PlainStruct("ViewDetails"). OptionalText("PolicyName"). OptionalText("PrivacyDomain") -var doubleQuotedStringDef = g.NewQueryStruct("DoubleQuotedString"). +var columnDef = g.NewQueryStruct("Column"). Text("Value", g.KeywordOptions().Required().DoubleQuotes()) var viewMinute = g.NewQueryStruct("ViewMinute"). @@ -76,7 +76,7 @@ var viewUsingCron = g.NewQueryStruct("ViewUsingCron"). var dataMetricFunctionDef = g.NewQueryStruct("ViewDataMetricFunction"). Identifier("DataMetricFunction", g.KindOfT[SchemaObjectIdentifier](), g.IdentifierOptions().Required()). - ListAssignment("ON", "DoubleQuotedString", g.ParameterOptions().Required().NoEquals().Parentheses()). + ListAssignment("ON", "Column", g.ParameterOptions().Required().NoEquals().Parentheses()). WithValidation(g.ValidIdentifier, "DataMetricFunction") var viewColumn = g.NewQueryStruct("ViewColumn"). @@ -88,19 +88,19 @@ var viewColumn = g.NewQueryStruct("ViewColumn"). var viewColumnMaskingPolicy = g.NewQueryStruct("ViewColumnMaskingPolicy"). Identifier("MaskingPolicy", g.KindOfT[SchemaObjectIdentifier](), g.IdentifierOptions().SQL("MASKING POLICY").Required()). - ListAssignment("USING", "DoubleQuotedString", g.ParameterOptions().NoEquals().Parentheses()) + ListAssignment("USING", "Column", g.ParameterOptions().NoEquals().Parentheses()) var viewColumnProjectionPolicy = g.NewQueryStruct("ViewColumnProjectionPolicy"). Identifier("ProjectionPolicy", g.KindOfT[SchemaObjectIdentifier](), g.IdentifierOptions().SQL("PROJECTION POLICY").Required()) var viewRowAccessPolicy = g.NewQueryStruct("ViewRowAccessPolicy"). Identifier("RowAccessPolicy", g.KindOfT[SchemaObjectIdentifier](), g.IdentifierOptions().SQL("ROW ACCESS POLICY").Required()). - ListAssignment("ON", "DoubleQuotedString", g.ParameterOptions().Required().NoEquals().Parentheses()). + ListAssignment("ON", "Column", g.ParameterOptions().Required().NoEquals().Parentheses()). WithValidation(g.ValidIdentifier, "RowAccessPolicy") var viewAggregationPolicy = g.NewQueryStruct("ViewAggregationPolicy"). Identifier("AggregationPolicy", g.KindOfT[SchemaObjectIdentifier](), g.IdentifierOptions().SQL("AGGREGATION POLICY").Required()). - ListAssignment("ENTITY KEY", "DoubleQuotedString", g.ParameterOptions().NoEquals().Parentheses()). + ListAssignment("ENTITY KEY", "Column", g.ParameterOptions().NoEquals().Parentheses()). WithValidation(g.ValidIdentifier, "AggregationPolicy") var viewAddDataMetricFunction = g.NewQueryStruct("ViewAddDataMetricFunction"). @@ -124,7 +124,7 @@ var viewUnsetDataMetricSchedule = g.NewQueryStruct("ViewUnsetDataMetricSchedule" var viewAddRowAccessPolicy = g.NewQueryStruct("ViewAddRowAccessPolicy"). SQL("ADD"). Identifier("RowAccessPolicy", g.KindOfT[SchemaObjectIdentifier](), g.IdentifierOptions().SQL("ROW ACCESS POLICY").Required()). - ListAssignment("ON", "DoubleQuotedString", g.ParameterOptions().Required().NoEquals().Parentheses()). + ListAssignment("ON", "Column", g.ParameterOptions().Required().NoEquals().Parentheses()). WithValidation(g.ValidIdentifier, "RowAccessPolicy") var viewDropRowAccessPolicy = g.NewQueryStruct("ViewDropRowAccessPolicy"). @@ -139,7 +139,7 @@ var viewDropAndAddRowAccessPolicy = g.NewQueryStruct("ViewDropAndAddRowAccessPol var viewSetAggregationPolicy = g.NewQueryStruct("ViewSetAggregationPolicy"). SQL("SET"). Identifier("AggregationPolicy", g.KindOfT[SchemaObjectIdentifier](), g.IdentifierOptions().SQL("AGGREGATION POLICY").Required()). - ListAssignment("ENTITY KEY", "DoubleQuotedString", g.ParameterOptions().NoEquals().Parentheses()). + ListAssignment("ENTITY KEY", "Column", g.ParameterOptions().NoEquals().Parentheses()). OptionalSQL("FORCE"). WithValidation(g.ValidIdentifier, "AggregationPolicy") @@ -153,7 +153,7 @@ var viewSetColumnMaskingPolicy = g.NewQueryStruct("ViewSetColumnMaskingPolicy"). Text("Name", g.KeywordOptions().Required().DoubleQuotes()). SQL("SET"). Identifier("MaskingPolicy", g.KindOfT[SchemaObjectIdentifier](), g.IdentifierOptions().SQL("MASKING POLICY").Required()). - ListAssignment("USING", "DoubleQuotedString", g.ParameterOptions().NoEquals().Parentheses()). + ListAssignment("USING", "Column", g.ParameterOptions().NoEquals().Parentheses()). OptionalSQL("FORCE") var viewUnsetColumnMaskingPolicy = g.NewQueryStruct("ViewUnsetColumnMaskingPolicy"). @@ -267,7 +267,7 @@ var ViewsDef = g.NewInterface( "UnsetTagsOnColumn"). WithValidation(g.ConflictingFields, "IfExists", "SetSecure"). WithValidation(g.ConflictingFields, "IfExists", "UnsetSecure"), - doubleQuotedStringDef, + columnDef, dataMetricFunctionDef, ). DropOperation( diff --git a/pkg/sdk/views_dto_builders_gen.go b/pkg/sdk/views_dto_builders_gen.go index 47d65f0e29..d16848ec28 100644 --- a/pkg/sdk/views_dto_builders_gen.go +++ b/pkg/sdk/views_dto_builders_gen.go @@ -113,14 +113,14 @@ func NewViewColumnMaskingPolicyRequest( return &s } -func (s *ViewColumnMaskingPolicyRequest) WithUsing(Using []DoubleQuotedString) *ViewColumnMaskingPolicyRequest { +func (s *ViewColumnMaskingPolicyRequest) WithUsing(Using []Column) *ViewColumnMaskingPolicyRequest { s.Using = Using return s } func NewViewRowAccessPolicyRequest( RowAccessPolicy SchemaObjectIdentifier, - On []DoubleQuotedString, + On []Column, ) *ViewRowAccessPolicyRequest { s := ViewRowAccessPolicyRequest{} s.RowAccessPolicy = RowAccessPolicy @@ -136,7 +136,7 @@ func NewViewAggregationPolicyRequest( return &s } -func (s *ViewAggregationPolicyRequest) WithEntityKey(EntityKey []DoubleQuotedString) *ViewAggregationPolicyRequest { +func (s *ViewAggregationPolicyRequest) WithEntityKey(EntityKey []Column) *ViewAggregationPolicyRequest { s.EntityKey = EntityKey return s } @@ -331,7 +331,7 @@ func NewViewUnsetDataMetricScheduleRequest() *ViewUnsetDataMetricScheduleRequest func NewViewAddRowAccessPolicyRequest( RowAccessPolicy SchemaObjectIdentifier, - On []DoubleQuotedString, + On []Column, ) *ViewAddRowAccessPolicyRequest { s := ViewAddRowAccessPolicyRequest{} s.RowAccessPolicy = RowAccessPolicy @@ -365,7 +365,7 @@ func NewViewSetAggregationPolicyRequest( return &s } -func (s *ViewSetAggregationPolicyRequest) WithEntityKey(EntityKey []DoubleQuotedString) *ViewSetAggregationPolicyRequest { +func (s *ViewSetAggregationPolicyRequest) WithEntityKey(EntityKey []Column) *ViewSetAggregationPolicyRequest { s.EntityKey = EntityKey return s } @@ -389,7 +389,7 @@ func NewViewSetColumnMaskingPolicyRequest( return &s } -func (s *ViewSetColumnMaskingPolicyRequest) WithUsing(Using []DoubleQuotedString) *ViewSetColumnMaskingPolicyRequest { +func (s *ViewSetColumnMaskingPolicyRequest) WithUsing(Using []Column) *ViewSetColumnMaskingPolicyRequest { s.Using = Using return s } diff --git a/pkg/sdk/views_dto_gen.go b/pkg/sdk/views_dto_gen.go index b14b8f5083..4b089e63d3 100644 --- a/pkg/sdk/views_dto_gen.go +++ b/pkg/sdk/views_dto_gen.go @@ -44,17 +44,17 @@ type ViewColumnProjectionPolicyRequest struct { type ViewColumnMaskingPolicyRequest struct { MaskingPolicy SchemaObjectIdentifier // required - Using []DoubleQuotedString + Using []Column } type ViewRowAccessPolicyRequest struct { RowAccessPolicy SchemaObjectIdentifier // required - On []DoubleQuotedString // required + On []Column // required } type ViewAggregationPolicyRequest struct { AggregationPolicy SchemaObjectIdentifier // required - EntityKey []DoubleQuotedString + EntityKey []Column } type AlterViewRequest struct { @@ -112,7 +112,7 @@ type ViewUnsetDataMetricScheduleRequest struct{} type ViewAddRowAccessPolicyRequest struct { RowAccessPolicy SchemaObjectIdentifier // required - On []DoubleQuotedString // required + On []Column // required } type ViewDropRowAccessPolicyRequest struct { @@ -126,7 +126,7 @@ type ViewDropAndAddRowAccessPolicyRequest struct { type ViewSetAggregationPolicyRequest struct { AggregationPolicy SchemaObjectIdentifier // required - EntityKey []DoubleQuotedString + EntityKey []Column Force *bool } @@ -135,7 +135,7 @@ type ViewUnsetAggregationPolicyRequest struct{} type ViewSetColumnMaskingPolicyRequest struct { Name string // required MaskingPolicy SchemaObjectIdentifier // required - Using []DoubleQuotedString + Using []Column Force *bool } diff --git a/pkg/sdk/views_gen.go b/pkg/sdk/views_gen.go index 8b6ab59535..0f681eaabc 100644 --- a/pkg/sdk/views_gen.go +++ b/pkg/sdk/views_gen.go @@ -46,15 +46,15 @@ type ViewColumnProjectionPolicy struct { } type ViewColumnMaskingPolicy struct { MaskingPolicy SchemaObjectIdentifier `ddl:"identifier" sql:"MASKING POLICY"` - Using []DoubleQuotedString `ddl:"parameter,parentheses,no_equals" sql:"USING"` + Using []Column `ddl:"parameter,parentheses,no_equals" sql:"USING"` } type ViewRowAccessPolicy struct { RowAccessPolicy SchemaObjectIdentifier `ddl:"identifier" sql:"ROW ACCESS POLICY"` - On []DoubleQuotedString `ddl:"parameter,parentheses,no_equals" sql:"ON"` + On []Column `ddl:"parameter,parentheses,no_equals" sql:"ON"` } type ViewAggregationPolicy struct { AggregationPolicy SchemaObjectIdentifier `ddl:"identifier" sql:"AGGREGATION POLICY"` - EntityKey []DoubleQuotedString `ddl:"parameter,parentheses,no_equals" sql:"ENTITY KEY"` + EntityKey []Column `ddl:"parameter,parentheses,no_equals" sql:"ENTITY KEY"` } // AlterViewOptions is based on https://docs.snowflake.com/en/sql-reference/sql/alter-view. @@ -88,12 +88,12 @@ type AlterViewOptions struct { SetTagsOnColumn *ViewSetColumnTags `ddl:"keyword"` UnsetTagsOnColumn *ViewUnsetColumnTags `ddl:"keyword"` } -type DoubleQuotedString struct { +type Column struct { Value string `ddl:"keyword,double_quotes"` } type ViewDataMetricFunction struct { DataMetricFunction SchemaObjectIdentifier `ddl:"identifier"` - On []DoubleQuotedString `ddl:"parameter,parentheses,no_equals" sql:"ON"` + On []Column `ddl:"parameter,parentheses,no_equals" sql:"ON"` } type ViewAddDataMetricFunction struct { add bool `ddl:"static" sql:"ADD"` @@ -128,7 +128,7 @@ type ViewUnsetDataMetricSchedule struct { type ViewAddRowAccessPolicy struct { add bool `ddl:"static" sql:"ADD"` RowAccessPolicy SchemaObjectIdentifier `ddl:"identifier" sql:"ROW ACCESS POLICY"` - On []DoubleQuotedString `ddl:"parameter,parentheses,no_equals" sql:"ON"` + On []Column `ddl:"parameter,parentheses,no_equals" sql:"ON"` } type ViewDropRowAccessPolicy struct { drop bool `ddl:"static" sql:"DROP"` @@ -141,7 +141,7 @@ type ViewDropAndAddRowAccessPolicy struct { type ViewSetAggregationPolicy struct { set bool `ddl:"static" sql:"SET"` AggregationPolicy SchemaObjectIdentifier `ddl:"identifier" sql:"AGGREGATION POLICY"` - EntityKey []DoubleQuotedString `ddl:"parameter,parentheses,no_equals" sql:"ENTITY KEY"` + EntityKey []Column `ddl:"parameter,parentheses,no_equals" sql:"ENTITY KEY"` Force *bool `ddl:"keyword" sql:"FORCE"` } type ViewUnsetAggregationPolicy struct { @@ -153,7 +153,7 @@ type ViewSetColumnMaskingPolicy struct { Name string `ddl:"keyword,double_quotes"` set bool `ddl:"static" sql:"SET"` MaskingPolicy SchemaObjectIdentifier `ddl:"identifier" sql:"MASKING POLICY"` - Using []DoubleQuotedString `ddl:"parameter,parentheses,no_equals" sql:"USING"` + Using []Column `ddl:"parameter,parentheses,no_equals" sql:"USING"` Force *bool `ddl:"keyword" sql:"FORCE"` } type ViewUnsetColumnMaskingPolicy struct { @@ -248,6 +248,19 @@ func (v *View) HasCopyGrants() bool { return strings.Contains(v.Text, " COPY GRANTS ") } +// TODO: proper extraction +func (v *View) IsTemporary() bool { + return strings.Contains(v.Text, "TEMPORARY") +} + +func (v *View) IsRecursive() bool { + return strings.Contains(v.Text, "RECURSIVE") +} + +func (v *View) IsChangeTracking() bool { + return v.ChangeTracking == "ON" +} + // DescribeViewOptions is based on https://docs.snowflake.com/en/sql-reference/sql/desc-view. type DescribeViewOptions struct { describe bool `ddl:"static" sql:"DESCRIBE"` diff --git a/pkg/sdk/views_gen_test.go b/pkg/sdk/views_gen_test.go index d32e775e3a..94ef5d195d 100644 --- a/pkg/sdk/views_gen_test.go +++ b/pkg/sdk/views_gen_test.go @@ -47,7 +47,7 @@ func TestViews_Create(t *testing.T) { opts := defaultOpts() opts.RowAccessPolicy = &ViewRowAccessPolicy{ RowAccessPolicy: randomSchemaObjectIdentifier(), - On: []DoubleQuotedString{}, + On: []Column{}, } assertOptsInvalidJoinedErrors(t, opts, errNotSet("CreateViewOptions.RowAccessPolicy", "On")) }) @@ -101,7 +101,7 @@ func TestViews_Create(t *testing.T) { *NewViewColumnRequest("column_with_comment").WithComment("column 2 comment"), *NewViewColumnRequest("column").WithMaskingPolicy( *NewViewColumnMaskingPolicyRequest(maskingPolicy1Id). - WithUsing([]DoubleQuotedString{{"a"}, {"b"}}), + WithUsing([]Column{{"a"}, {"b"}}), ).WithTag([]TagAssociation{{Name: tag1Id, Value: "v1"}}), *NewViewColumnRequest("column 2").WithProjectionPolicy( *NewViewColumnProjectionPolicyRequest(maskingPolicy2Id), @@ -109,8 +109,8 @@ func TestViews_Create(t *testing.T) { }). WithCopyGrants(true). WithComment("comment"). - WithRowAccessPolicy(*NewViewRowAccessPolicyRequest(rowAccessPolicyId, []DoubleQuotedString{{"c"}, {"d"}})). - WithAggregationPolicy(*NewViewAggregationPolicyRequest(aggregationPolicyId).WithEntityKey([]DoubleQuotedString{{"column_with_comment"}})). + WithRowAccessPolicy(*NewViewRowAccessPolicyRequest(rowAccessPolicyId, []Column{{"c"}, {"d"}})). + WithAggregationPolicy(*NewViewAggregationPolicyRequest(aggregationPolicyId).WithEntityKey([]Column{{"column_with_comment"}})). WithTag([]TagAssociation{{ Name: tag2Id, Value: "v2", @@ -201,7 +201,7 @@ func TestViews_Alter(t *testing.T) { opts := defaultOpts() opts.AddRowAccessPolicy = &ViewAddRowAccessPolicy{ RowAccessPolicy: randomSchemaObjectIdentifier(), - On: []DoubleQuotedString{}, + On: []Column{}, } assertOptsInvalidJoinedErrors(t, opts, errNotSet("AlterViewOptions.AddRowAccessPolicy", "On")) }) @@ -240,7 +240,7 @@ func TestViews_Alter(t *testing.T) { }, Add: ViewAddRowAccessPolicy{ RowAccessPolicy: randomSchemaObjectIdentifier(), - On: []DoubleQuotedString{}, + On: []Column{}, }, } assertOptsInvalidJoinedErrors(t, opts, errNotSet("AlterViewOptions.DropAndAddRowAccessPolicy.Add", "On")) @@ -295,7 +295,7 @@ func TestViews_Alter(t *testing.T) { opts := defaultOpts() opts.AddDataMetricFunction = &ViewAddDataMetricFunction{ - DataMetricFunction: []ViewDataMetricFunction{{DataMetricFunction: dmfId, On: []DoubleQuotedString{{"foo"}}}}, + DataMetricFunction: []ViewDataMetricFunction{{DataMetricFunction: dmfId, On: []Column{{"foo"}}}}, } assertOptsValidAndSQLEquals(t, opts, "ALTER VIEW %s ADD DATA METRIC FUNCTION %s ON (\"foo\")", id.FullyQualifiedName(), dmfId.FullyQualifiedName()) }) @@ -305,7 +305,7 @@ func TestViews_Alter(t *testing.T) { opts := defaultOpts() opts.DropDataMetricFunction = &ViewDropDataMetricFunction{ - DataMetricFunction: []ViewDataMetricFunction{{DataMetricFunction: dmfId, On: []DoubleQuotedString{{"foo"}}}}, + DataMetricFunction: []ViewDataMetricFunction{{DataMetricFunction: dmfId, On: []Column{{"foo"}}}}, } assertOptsValidAndSQLEquals(t, opts, "ALTER VIEW %s DROP DATA METRIC FUNCTION %s ON (\"foo\")", id.FullyQualifiedName(), dmfId.FullyQualifiedName()) }) @@ -370,7 +370,7 @@ func TestViews_Alter(t *testing.T) { opts := defaultOpts() opts.AddRowAccessPolicy = &ViewAddRowAccessPolicy{ RowAccessPolicy: rowAccessPolicyId, - On: []DoubleQuotedString{{"a"}, {"b"}}, + On: []Column{{"a"}, {"b"}}, } assertOptsValidAndSQLEquals(t, opts, "ALTER VIEW %s ADD ROW ACCESS POLICY %s ON (\"a\", \"b\")", id.FullyQualifiedName(), rowAccessPolicyId.FullyQualifiedName()) }) @@ -396,7 +396,7 @@ func TestViews_Alter(t *testing.T) { }, Add: ViewAddRowAccessPolicy{ RowAccessPolicy: rowAccessPolicy2Id, - On: []DoubleQuotedString{{"a"}, {"b"}}, + On: []Column{{"a"}, {"b"}}, }, } assertOptsValidAndSQLEquals(t, opts, "ALTER VIEW %s DROP ROW ACCESS POLICY %s, ADD ROW ACCESS POLICY %s ON (\"a\", \"b\")", id.FullyQualifiedName(), rowAccessPolicy1Id.FullyQualifiedName(), rowAccessPolicy2Id.FullyQualifiedName()) @@ -414,7 +414,7 @@ func TestViews_Alter(t *testing.T) { opts := defaultOpts() opts.SetAggregationPolicy = &ViewSetAggregationPolicy{ AggregationPolicy: aggregationPolicyId, - EntityKey: []DoubleQuotedString{{"a"}, {"b"}}, + EntityKey: []Column{{"a"}, {"b"}}, Force: Bool(true), } assertOptsValidAndSQLEquals(t, opts, "ALTER VIEW %s SET AGGREGATION POLICY %s ENTITY KEY (\"a\", \"b\") FORCE", id.FullyQualifiedName(), aggregationPolicyId.FullyQualifiedName()) @@ -433,7 +433,7 @@ func TestViews_Alter(t *testing.T) { opts.SetMaskingPolicyOnColumn = &ViewSetColumnMaskingPolicy{ Name: "column", MaskingPolicy: maskingPolicyId, - Using: []DoubleQuotedString{{"a"}, {"b"}}, + Using: []Column{{"a"}, {"b"}}, Force: Bool(true), } assertOptsValidAndSQLEquals(t, opts, "ALTER VIEW %s ALTER COLUMN \"column\" SET MASKING POLICY %s USING (\"a\", \"b\") FORCE", id.FullyQualifiedName(), maskingPolicyId.FullyQualifiedName()) diff --git a/pkg/snowflake/parser.go b/pkg/snowflake/parser.go index e351a57b7c..69810021c5 100644 --- a/pkg/snowflake/parser.go +++ b/pkg/snowflake/parser.go @@ -25,7 +25,6 @@ func NewViewSelectStatementExtractor(input string) *ViewSelectStatementExtractor } func (e *ViewSelectStatementExtractor) Extract() (string, error) { - fmt.Printf("[DEBUG] extracting view query %s\n", string(e.input)) e.consumeSpace() e.consumeToken("create") e.consumeSpace() @@ -33,6 +32,8 @@ func (e *ViewSelectStatementExtractor) Extract() (string, error) { e.consumeSpace() e.consumeToken("secure") e.consumeSpace() + e.consumeToken("temporary") + e.consumeSpace() e.consumeToken("recursive") e.consumeSpace() e.consumeToken("view") @@ -40,19 +41,69 @@ func (e *ViewSelectStatementExtractor) Extract() (string, error) { e.consumeToken("if not exists") e.consumeSpace() e.consumeID() - // TODO column list e.consumeSpace() e.consumeToken("copy grants") e.consumeComment() e.consumeSpace() e.consumeComment() e.consumeSpace() + e.extractRowAccessPolicy() + e.extractAggregationPolicy() e.consumeToken("as") e.consumeSpace() + fmt.Printf("[DEBUG] extracted statement %s from view query %s\n", string(e.input[e.pos:]), string(e.input)) + return string(e.input[e.pos:]), nil } +func (e *ViewSelectStatementExtractor) extractRowAccessPolicy() { + ok := e.consumeToken("row access policy") + if !ok { + return + } + e.consumeSpace() + e.consumeID() + e.consumeSpace() + e.consumeToken("on") + e.consumeSpace() + e.extractIdentifierList() + e.consumeSpace() +} + +func (e *ViewSelectStatementExtractor) extractAggregationPolicy() { + ok := e.consumeToken("aggregation policy") + if !ok { + return + } + e.consumeSpace() + e.consumeID() + e.consumeSpace() + e.consumeToken("entity key") + e.consumeSpace() + e.extractIdentifierList() + e.consumeSpace() +} + +func (e *ViewSelectStatementExtractor) extractIdentifierList() { + e.consumeSpace() + if !e.consumeToken("(") { + return + } + for { + e.consumeSpace() + e.consumeID() + if e.input[e.pos-1] == ')' { + break + } + e.consumeSpace() + if e.consumeToken(")") { + break + } + } + e.consumeSpace() +} + func (e *ViewSelectStatementExtractor) ExtractMaterializedView() (string, error) { fmt.Printf("[DEBUG] extracting materialized view query: %s\n", string(e.input)) e.consumeSpace() diff --git a/pkg/snowflake/parser_test.go b/pkg/snowflake/parser_test.go index 14fa61fe4d..e603403864 100644 --- a/pkg/snowflake/parser_test.go +++ b/pkg/snowflake/parser_test.go @@ -3,6 +3,8 @@ package snowflake import ( "fmt" "testing" + + "github.com/stretchr/testify/require" ) func TestViewSelectStatementExtractor_Extract(t *testing.T) { @@ -32,7 +34,8 @@ from bar;` full := `CREATE SECURE VIEW "rgdxfmnfhh"."PUBLIC"."rgdxfmnfhh" COMMENT = 'Terraform test resource' AS SELECT ROLE_NAME, ROLE_OWNER FROM INFORMATION_SCHEMA.APPLICABLE_ROLES` issue2640 := `CREATE OR REPLACE SECURE VIEW "CLASSIFICATION" comment = 'Classification View of the union of classification tables' AS select * from AB1_SUBSCRIPTION.CLASSIFICATION.CLASSIFICATION union select * from AB2_SUBSCRIPTION.CLASSIFICATION.CLASSIFICATION` - + withRowAccessAndAggregationPolicy := `CREATE SECURE VIEW "rgdxfmnfhh"."PUBLIC"."rgdxfmnfhh" COMMENT = 'Terraform test resource' ROW ACCESS policy rap on (title, title2) AGGREGATION POLICY rap AS SELECT ROLE_NAME, ROLE_OWNER FROM INFORMATION_SCHEMA.APPLICABLE_ROLES` + withRowAccessAndAggregationPolicyWithEntityKey := `CREATE SECURE VIEW "rgdxfmnfhh"."PUBLIC"."rgdxfmnfhh" COMMENT = 'Terraform test resource' ROW ACCESS policy rap on (title, title2) AGGREGATION POLICY rap ENTITY KEY (foo, bar) AS SELECT ROLE_NAME, ROLE_OWNER FROM INFORMATION_SCHEMA.APPLICABLE_ROLES` type args struct { input string } @@ -57,6 +60,8 @@ from bar;` {"identifier", args{identifier}, "select * from bar;", false}, {"full", args{full}, "SELECT ROLE_NAME, ROLE_OWNER FROM INFORMATION_SCHEMA.APPLICABLE_ROLES", false}, {"issue2640", args{issue2640}, "select * from AB1_SUBSCRIPTION.CLASSIFICATION.CLASSIFICATION union select * from AB2_SUBSCRIPTION.CLASSIFICATION.CLASSIFICATION", false}, + {"with row access policy and aggregation policy", args{withRowAccessAndAggregationPolicy}, "SELECT ROLE_NAME, ROLE_OWNER FROM INFORMATION_SCHEMA.APPLICABLE_ROLES", false}, + {"with row access policy and aggregation policy with entity key", args{withRowAccessAndAggregationPolicyWithEntityKey}, "SELECT ROLE_NAME, ROLE_OWNER FROM INFORMATION_SCHEMA.APPLICABLE_ROLES", false}, } for _, tt := range tests { tt := tt @@ -67,9 +72,7 @@ from bar;` t.Errorf("ViewSelectStatementExtractor.Extract() error = %v, wantErr %v", err, tt.wantErr) return } - if got != tt.want { - t.Errorf("ViewSelectStatementExtractor.Extract() = '%v', want '%v'", got, tt.want) - } + require.Equal(t, tt.want, got) }) } } diff --git a/templates/resources/view.md.tmpl b/templates/resources/view.md.tmpl new file mode 100644 index 0000000000..9d371d4b57 --- /dev/null +++ b/templates/resources/view.md.tmpl @@ -0,0 +1,32 @@ +--- +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#v094x--v0950) to use it. + +# {{.Name}} ({{.Type}}) + +{{ .Description | trimspace }} + +{{ if .HasExample -}} +## Example Usage + +{{ tffile (printf "examples/resources/%s/resource.tf" .Name)}} +{{- end }} + +{{ .SchemaMarkdown | trimspace }} +{{- if .HasImport }} + +## Import + +Import is supported using the following syntax: + +{{ codefile "shell" (printf "examples/resources/%s/import.sh" .Name)}} +{{- end }} From 9024b1edd6b694bcd52fd5d3001e5f21ce0abf17 Mon Sep 17 00:00:00 2001 From: Jakub Michalak Date: Fri, 9 Aug 2024 12:13:31 +0200 Subject: [PATCH 02/14] Fix tests --- pkg/acceptance/helpers/aggregation_policy_client.go | 2 +- pkg/resources/stream_acceptance_test.go | 1 + pkg/resources/view_acceptance_test.go | 2 ++ .../{view_stage_upgraders.go => view_state_upgraders.go} | 0 4 files changed, 4 insertions(+), 1 deletion(-) rename pkg/resources/{view_stage_upgraders.go => view_state_upgraders.go} (100%) diff --git a/pkg/acceptance/helpers/aggregation_policy_client.go b/pkg/acceptance/helpers/aggregation_policy_client.go index d04114a55d..bac7d8714e 100644 --- a/pkg/acceptance/helpers/aggregation_policy_client.go +++ b/pkg/acceptance/helpers/aggregation_policy_client.go @@ -31,7 +31,7 @@ func (c *AggregationPolicyClient) CreateAggregationPolicy(t *testing.T) (sdk.Sch ctx := context.Background() id := c.ids.RandomSchemaObjectIdentifier() - _, err := c.client().ExecForTests(ctx, fmt.Sprintf(`CREATE AGGREGATION POLICY %s AS () RETURNS AGGREGATION_CONSTRAINT -> AGGREGATION_CONSTRAINT(MIN_GROUP_SIZE => 5)`, id.Name())) + _, err := c.client().ExecForTests(ctx, fmt.Sprintf(`CREATE AGGREGATION POLICY %s AS () RETURNS AGGREGATION_CONSTRAINT -> AGGREGATION_CONSTRAINT(MIN_GROUP_SIZE => 5)`, id.FullyQualifiedName())) require.NoError(t, err) return id, c.DropAggregationPolicyFunc(t, id) } diff --git a/pkg/resources/stream_acceptance_test.go b/pkg/resources/stream_acceptance_test.go index a13b17365d..c67628141c 100644 --- a/pkg/resources/stream_acceptance_test.go +++ b/pkg/resources/stream_acceptance_test.go @@ -287,6 +287,7 @@ resource "snowflake_view" "test" { database = "%[1]s" schema = "%[2]s" name = "%[4]s" + change_tracking = true statement = "select * from \"${snowflake_table.test.name}\"" } diff --git a/pkg/resources/view_acceptance_test.go b/pkg/resources/view_acceptance_test.go index 73370138fd..19e022f267 100644 --- a/pkg/resources/view_acceptance_test.go +++ b/pkg/resources/view_acceptance_test.go @@ -25,6 +25,7 @@ import ( ) func TestAcc_View_basic(t *testing.T) { + _ = testenvs.GetOrSkipTest(t, testenvs.EnableAcceptance) acc.TestAccPreCheck(t) rowAccessPolicy, rowAccessPolicyCleanup := acc.TestClient().RowAccessPolicy.CreateRowAccessPolicyWithDataType(t, sdk.DataTypeVARCHAR) @@ -280,6 +281,7 @@ func TestAcc_View_recursive(t *testing.T) { } func TestAcc_View_complete(t *testing.T) { + _ = testenvs.GetOrSkipTest(t, testenvs.EnableAcceptance) acc.TestAccPreCheck(t) id := acc.TestClient().Ids.RandomSchemaObjectIdentifier() // use a simple table to test change_tracking, otherwise it fails with: Change tracking is not supported on queries with joins of type '[LEFT_OUTER_JOIN]' diff --git a/pkg/resources/view_stage_upgraders.go b/pkg/resources/view_state_upgraders.go similarity index 100% rename from pkg/resources/view_stage_upgraders.go rename to pkg/resources/view_state_upgraders.go From 19b2eb2fb9b7cca9647f73e9c7544c7bfd0cf923 Mon Sep 17 00:00:00 2001 From: Jakub Michalak Date: Thu, 22 Aug 2024 12:53:16 +0200 Subject: [PATCH 03/14] Review suggestions --- MIGRATION_GUIDE.md | 4 +- docs/resources/view.md | 14 +-- pkg/acceptance/helpers/view_client.go | 8 ++ pkg/resources/doc_helpers.go | 8 ++ pkg/resources/special_values.go | 4 + .../tag_masking_policy_association.go | 19 +++- pkg/resources/view.go | 79 +++++++--------- pkg/resources/view_acceptance_test.go | 89 ++++++++++++++++--- pkg/resources/view_state_upgraders.go | 2 +- pkg/sdk/views_gen.go | 2 +- pkg/snowflake/parser.go | 2 + templates/resources/view.md.tmpl | 4 + 12 files changed, 166 insertions(+), 69 deletions(-) diff --git a/MIGRATION_GUIDE.md b/MIGRATION_GUIDE.md index c1b8c578fc..fbfb919c3a 100644 --- a/MIGRATION_GUIDE.md +++ b/MIGRATION_GUIDE.md @@ -11,6 +11,8 @@ New fields: - `row_access_policy` - `aggregation_policy` - `change_tracking` + - `is_recursive` + - `is_temporary` - added `show_output` field that holds the response from SHOW VIEWS. - added `describe_output` field that holds the response from DESCRIBE VIEW. Note that one needs to grant sufficient privileges e.g. with [grant_ownership](https://registry.terraform.io/providers/Snowflake-Labs/snowflake/latest/docs/resources/grant_ownership) on the tables used in this view. Otherwise, this field is not filled. @@ -474,7 +476,7 @@ resource "snowflake_database" "test" { } ``` -If you had `from_database` set, you should follow our [resource migration guide](https://github.com/Snowflake-Labs/terraform-provider-snowflake/blob/main/docs/technical-documentation/resource_migration.md) to remove +If you had `from_database` set, you should follow our [resource migration guide](https://github.com/Snowflake-Labs/terraform-provider-snowflake/blob/main/docs/technical-documentation/resource_migration.md) to remove the database from state to later import it in the newer version of the provider. Otherwise, it may cause issues when migrating to v0.93.0. For now, we're dropping the possibility to create a clone database from other databases. diff --git a/docs/resources/view.md b/docs/resources/view.md index f34ebeafb9..8778174acb 100644 --- a/docs/resources/view.md +++ b/docs/resources/view.md @@ -7,6 +7,10 @@ description: |- !> **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#v094x--v0950) to use it. +!> **Note about copy_grants** Fields like `is_recursive`, `is_temporary`, `copy_grants` and `statement` can not be altered, and a change means recreation of the resource. ForceNew can not be used because it does not preserve grants from `copy_grants`. Beware that even though a change is marked as update, the resource is recreated. + +~> **Required warehouse** For this resource, the provider uses [policy references](https://docs.snowflake.com/en/sql-reference/functions/policy_references) which requires a warehouse in the connection. Please, make sure you have either set a DEFAULT_WAREHOUSE for the user, or specified a warehouse in the configuration. + # snowflake_view (Resource) Resource used to manage view objects. For more information, check [view documentation](https://docs.snowflake.com/en/sql-reference/sql/create-view). @@ -62,9 +66,9 @@ SQL ### Required -- `database` (String) The database in which to create the view. Don't use the | character. -- `name` (String) Specifies the identifier for the view; must be unique for the schema in which the view is created. Don't use the | character. -- `schema` (String) The schema in which to create the view. Don't use the | character. +- `database` (String) The database in which to create the view. Due to technical limitations, don't use the following characters: `|` +- `name` (String) Specifies the identifier for the view; must be unique for the schema in which the view is created. Due to technical limitations, don't use the following characters: `|` +- `schema` (String) The schema in which to create the view. Due to technical limitations, don't use the following characters: `|` - `statement` (String) Specifies the query used to create the view. ### Optional @@ -75,9 +79,9 @@ SQL - `copy_grants` (Boolean) Retains the access permissions from the original view when a new view is created using the OR REPLACE clause. OR REPLACE must be set when COPY GRANTS is set. - `is_recursive` (String) Specifies that the view can refer to itself using recursive syntax without necessarily using a CTE (common table expression). Available options are: "true" or "false". When the value is not set in the configuration the provider will put "default" there which means to use the Snowflake default for this value. - `is_secure` (String) Specifies that the view is secure. By design, the Snowflake's `SHOW VIEWS` command does not provide information about secure views (consult [view usage notes](https://docs.snowflake.com/en/sql-reference/sql/create-view#usage-notes)) which is essential to manage/import view with Terraform. Use the role owning the view while managing secure views. Available options are: "true" or "false". When the value is not set in the configuration the provider will put "default" there which means to use the Snowflake default for this value. -- `is_temporary` (String) Specifies that the view persists only for the duration of the session that you created it in. A temporary view and all its contents are dropped at the end of the session. Available options are: "true" or "false". When the value is not set in the configuration the provider will put "default" there which means to use the Snowflake default for this value. +- `is_temporary` (String) Specifies that the view persists only for the duration of the session that you created it in. A temporary view and all its contents are dropped at the end of the session. In context of this provider, it means that it's dropped after a Terraform operation. This results in a permanent plan with object creation. Available options are: "true" or "false". When the value is not set in the configuration the provider will put "default" there which means to use the Snowflake default for this value. - `or_replace` (Boolean) Overwrites the View if it exists. -- `row_access_policy` (Block List) Specifies the row access policy to set on a view. (see [below for nested schema](#nestedblock--row_access_policy)) +- `row_access_policy` (Block List, Max: 1) Specifies the row access policy to set on a view. (see [below for nested schema](#nestedblock--row_access_policy)) ### Read-Only diff --git a/pkg/acceptance/helpers/view_client.go b/pkg/acceptance/helpers/view_client.go index 0a1ae71632..ae6c9fb83d 100644 --- a/pkg/acceptance/helpers/view_client.go +++ b/pkg/acceptance/helpers/view_client.go @@ -52,6 +52,14 @@ func (c *ViewClient) RecreateView(t *testing.T, id sdk.SchemaObjectIdentifier, q return view } +func (c *ViewClient) Alter(t *testing.T, req *sdk.AlterViewRequest) { + t.Helper() + ctx := context.Background() + + err := c.client().Alter(ctx, req) + require.NoError(t, err) +} + func (c *ViewClient) DropViewFunc(t *testing.T, id sdk.SchemaObjectIdentifier) func() { t.Helper() ctx := context.Background() diff --git a/pkg/resources/doc_helpers.go b/pkg/resources/doc_helpers.go index 020f70ce46..eabe5eaf34 100644 --- a/pkg/resources/doc_helpers.go +++ b/pkg/resources/doc_helpers.go @@ -12,3 +12,11 @@ func possibleValuesListed[T ~string](values []T) string { } return strings.Join(valuesWrapped, " | ") } + +func characterList(values []rune) string { + valuesWrapped := make([]string, len(values)) + for i, value := range values { + valuesWrapped[i] = fmt.Sprintf("`%c`", value) + } + return strings.Join(valuesWrapped, ", ") +} diff --git a/pkg/resources/special_values.go b/pkg/resources/special_values.go index 4a74508a8e..3b3693f2b2 100644 --- a/pkg/resources/special_values.go +++ b/pkg/resources/special_values.go @@ -45,3 +45,7 @@ func externalChangesNotDetectedFieldDescription(description string) string { func withPrivilegedRolesDescription(description, paramName string) string { return fmt.Sprintf(`%s By default, this list includes the ACCOUNTADMIN, ORGADMIN and SECURITYADMIN roles. To remove these privileged roles from the list, use the ALTER ACCOUNT command to set the %s account parameter to FALSE. `, description, paramName) } + +func blacklistedCharactersFieldDescription(description string, blacklistedCharacters []rune) string { + return fmt.Sprintf(`%s Due to technical limitations, don't use the following characters: %s`, description, characterList(blacklistedCharacters)) +} diff --git a/pkg/resources/tag_masking_policy_association.go b/pkg/resources/tag_masking_policy_association.go index a376571363..f8f570f207 100644 --- a/pkg/resources/tag_masking_policy_association.go +++ b/pkg/resources/tag_masking_policy_association.go @@ -132,11 +132,26 @@ func ReadContextTagMaskingPolicyAssociation(ctx context.Context, d *schema.Resou } // create temp warehouse to query the tag, and make sure to clean it up - cleanupWarehouse, err := ensureWarehouse(ctx, client) + warehouse, err := client.ContextFunctions.CurrentWarehouse(ctx) if err != nil { return diag.FromErr(err) } - defer cleanupWarehouse() + if warehouse == "" { + log.Printf("[DEBUG] no current warehouse set, creating a temporary warehouse") + randomWarehouseName := fmt.Sprintf("terraform-provider-snowflake-%v", helpers.RandomString()) + wid := sdk.NewAccountObjectIdentifier(randomWarehouseName) + if err := client.Warehouses.Create(ctx, wid, nil); err != nil { + return diag.FromErr(err) + } + defer func() { + if err := client.Warehouses.Drop(ctx, wid, nil); err != nil { + log.Printf("[WARN] error cleaning up temp warehouse %v", err) + } + }() + if err := client.Sessions.UseWarehouse(ctx, wid); err != nil { + return diag.FromErr(err) + } + } // show attached masking policy tid := sdk.NewSchemaObjectIdentifier(aid.TagDatabaseName, aid.TagSchemaName, aid.TagName) mid := sdk.NewSchemaObjectIdentifier(aid.MaskingPolicyDatabaseName, aid.MaskingPolicySchemaName, aid.MaskingPolicyName) diff --git a/pkg/resources/view.go b/pkg/resources/view.go index f7efe47c18..2a8f49e343 100644 --- a/pkg/resources/view.go +++ b/pkg/resources/view.go @@ -24,20 +24,20 @@ var viewSchema = map[string]*schema.Schema{ "name": { Type: schema.TypeString, Required: true, - Description: "Specifies the identifier for the view; must be unique for the schema in which the view is created. Don't use the | character.", + Description: blacklistedCharactersFieldDescription("Specifies the identifier for the view; must be unique for the schema in which the view is created.", []rune{'|'}), DiffSuppressFunc: suppressIdentifierQuoting, }, "database": { Type: schema.TypeString, Required: true, - Description: "The database in which to create the view. Don't use the | character.", + Description: blacklistedCharactersFieldDescription("The database in which to create the view.", []rune{'|'}), ForceNew: true, DiffSuppressFunc: suppressIdentifierQuoting, }, "schema": { Type: schema.TypeString, Required: true, - Description: "The schema in which to create the view. Don't use the | character.", + Description: blacklistedCharactersFieldDescription("The schema in which to create the view.", []rune{'|'}), ForceNew: true, DiffSuppressFunc: suppressIdentifierQuoting, }, @@ -72,7 +72,7 @@ var viewSchema = map[string]*schema.Schema{ ForceNew: true, Default: BooleanDefault, ValidateDiagFunc: validateBooleanString, - Description: booleanStringFieldDescription("Specifies that the view persists only for the duration of the session that you created it in. A temporary view and all its contents are dropped at the end of the session."), + Description: booleanStringFieldDescription("Specifies that the view persists only for the duration of the session that you created it in. A temporary view and all its contents are dropped at the end of the session. In context of this provider, it means that it's dropped after a Terraform operation. This results in a permanent plan with object creation."), }, "is_recursive": { Type: schema.TypeString, @@ -90,7 +90,6 @@ var viewSchema = map[string]*schema.Schema{ DiffSuppressFunc: IgnoreChangeToCurrentSnowflakeValueInShowWithMapping("change_tracking", func(x any) any { return x.(string) == "ON" }), - // DiffSuppressFunc: IgnoreChangeToCurrentSnowflakeValueInShow("change_tracking"), Description: booleanStringFieldDescription("Specifies to enable or disable change tracking on the table."), }, // TODO(next pr): support remaining fields @@ -183,6 +182,11 @@ var viewSchema = map[string]*schema.Schema{ // DiffSuppressFunc: DiffSuppressStatement, // Description: "Specifies the projection policy to set on a column.", // }, + // "comment": { + // Type: schema.TypeString, + // Optional: true, + // Description: "Specifies a comment for the column.", + // }, // }, // }, // Description: "If you want to change the name of a column or add a comment to a column in the new view, include a column list that specifies the column names and (if needed) comments about the columns. (You do not need to specify the data types of the columns.)", @@ -194,6 +198,7 @@ var viewSchema = map[string]*schema.Schema{ }, "row_access_policy": { Type: schema.TypeList, + MaxItems: 1, Optional: true, Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ @@ -275,7 +280,7 @@ func View() *schema.Resource { Description: "Resource used to manage view objects. For more information, check [view documentation](https://docs.snowflake.com/en/sql-reference/sql/create-view).", CustomizeDiff: customdiff.All( - ComputedIfAnyAttributeChanged(ShowOutputAttributeName, "name", "comment", "change_tracking", "is_secure", "statement"), + ComputedIfAnyAttributeChanged(ShowOutputAttributeName, "comment", "change_tracking", "is_secure", "is_temporary", "is_recursive", "statement"), ), Schema: viewSchema, @@ -288,7 +293,7 @@ func View() *schema.Resource { Version: 0, // setting type to cty.EmptyObject is a bit hacky here but following https://developer.hashicorp.com/terraform/plugin/framework/migrating/resources/state-upgrade#sdkv2-1 would require lots of repetitive code; this should work with cty.EmptyObject Type: cty.EmptyObject, - Upgrade: v094ViewStateUpgrader, + Upgrade: v0_94_1_ViewStateUpgrader, }, }, } @@ -353,11 +358,7 @@ func CreateView(ctx context.Context, d *schema.ResourceData, meta any) diag.Diag return ReadView(false)(ctx, d, meta) } -type resourceDataGetter interface { - Get(string) any -} - -func prepareCreateRequest(d resourceDataGetter) (*sdk.CreateViewRequest, error) { +func prepareCreateRequest(d *schema.ResourceData) (*sdk.CreateViewRequest, error) { databaseName := d.Get("database").(string) schemaName := d.Get("schema").(string) name := d.Get("name").(string) @@ -522,17 +523,12 @@ func ReadView(withExternalChangesMarking bool) schema.ReadContextFunc { } func handlePolicyReferences(ctx context.Context, client *sdk.Client, id sdk.SchemaObjectIdentifier, d *schema.ResourceData) error { - // ensure a warehouse is selected for the session to get policy references - cleanupWarehouse, err := ensureWarehouse(ctx, client) - if err != nil { - return err - } - defer cleanupWarehouse() - policyRefs, err := client.PolicyReferences.GetForEntity(ctx, sdk.NewGetForEntityPolicyReferenceRequest(id, sdk.PolicyEntityDomainView)) if err != nil { return err } + var aggregationPolicies []map[string]any + var rowAccessPolicies []map[string]any for _, p := range policyRefs { policyName := sdk.NewSchemaObjectIdentifier(*p.PolicyDb, *p.PolicySchema, p.PolicyName) switch p.PolicyKind { @@ -541,50 +537,39 @@ func handlePolicyReferences(ctx context.Context, client *sdk.Client, id sdk.Sche if p.RefArgColumnNames != nil { entityKey = sdk.ParseCommaSeparatedStringArray(*p.RefArgColumnNames, true) } - if err = d.Set("aggregation_policy", []map[string]any{{ + aggregationPolicies = append(aggregationPolicies, map[string]any{ "policy_name": policyName.FullyQualifiedName(), "entity_key": entityKey, - }}); err != nil { - return err - } + }) case string(sdk.PolicyKindRowAccessPolicy): var on []string if p.RefArgColumnNames != nil { on = sdk.ParseCommaSeparatedStringArray(*p.RefArgColumnNames, true) } - if err = d.Set("row_access_policy", []map[string]any{{ + rowAccessPolicies = append(rowAccessPolicies, map[string]any{ "policy_name": policyName.FullyQualifiedName(), "on": on, - }}); err != nil { - return err - } + }) default: log.Printf("[WARN] unexpected policy kind %v in policy references returned from Snowflake", p.PolicyKind) } } - return err -} - -type viewWithReplaceDataResourceGetter struct { - d *schema.ResourceData -} - -func (g *viewWithReplaceDataResourceGetter) Get(name string) any { - old, new := g.d.GetChange(name) - if name == "statement" { - return new + if err = d.Set("aggregation_policy", aggregationPolicies); err != nil { + return err } - return old + if err = d.Set("row_access_policy", rowAccessPolicies); err != nil { + return err + } + return err } func UpdateView(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { client := meta.(*provider.Context).Client id := helpers.DecodeSnowflakeID(d.Id()).(sdk.SchemaObjectIdentifier) - // change on this field can not be ForceNew because then view is dropped explicitly and copying grants does not have effect - if d.HasChange("statement") { - // TODO: extract this common code with CreateView - req, err := prepareCreateRequest(&viewWithReplaceDataResourceGetter{d}) + // change on these fields can not be ForceNew because then view is dropped explicitly and copying grants does not have effect + if d.HasChange("statement") || d.HasChange("is_temporary") || d.HasChange("is_recursive") || d.HasChange("copy_grant") { + req, err := prepareCreateRequest(d) if err != nil { return diag.FromErr(err) } @@ -604,6 +589,7 @@ func UpdateView(ctx context.Context, d *schema.ResourceData, meta any) diag.Diag return diag.FromErr(fmt.Errorf("error setting change_tracking in view %v err = %w", id.Name(), err)) } } + return ReadView(false)(ctx, d, meta) } if d.HasChange("name") { @@ -638,11 +624,7 @@ func UpdateView(ctx context.Context, d *schema.ResourceData, meta any) diag.Diag if err != nil { return diag.FromErr(err) } - if parsed { - err = client.Views.Alter(ctx, sdk.NewAlterViewRequest(id).WithSetSecure(parsed)) - } else { - err = client.Views.Alter(ctx, sdk.NewAlterViewRequest(id).WithUnsetSecure(parsed)) - } + err = client.Views.Alter(ctx, sdk.NewAlterViewRequest(id).WithSetSecure(parsed)) if err != nil { return diag.FromErr(fmt.Errorf("error setting is_secure for view %v: %w", d.Id(), err)) } @@ -676,6 +658,7 @@ func UpdateView(ctx context.Context, d *schema.ResourceData, meta any) diag.Diag var dropReq *sdk.ViewDropRowAccessPolicyRequest if v, ok := d.GetOk("row_access_policy"); ok { rowAccessPolicyConfig := v.([]any)[0].(map[string]any) + columnsRaw := expandStringList(rowAccessPolicyConfig["on"].(*schema.Set).List()) columns := make([]sdk.Column, len(columnsRaw)) for i := range columnsRaw { diff --git a/pkg/resources/view_acceptance_test.go b/pkg/resources/view_acceptance_test.go index 19e022f267..7668bc83db 100644 --- a/pkg/resources/view_acceptance_test.go +++ b/pkg/resources/view_acceptance_test.go @@ -63,7 +63,6 @@ func TestAcc_View_basic(t *testing.T) { resource.Test(t, resource.TestCase{ ProtoV6ProviderFactories: acc.TestAccProtoV6ProviderFactories, - PreCheck: func() { acc.TestAccPreCheck(t) }, TerraformVersionChecks: []tfversion.TerraformVersionCheck{ tfversion.RequireAbove(tfversion.Version1_5_0), }, @@ -90,6 +89,22 @@ func TestAcc_View_basic(t *testing.T) { HasSchemaString(id.SchemaName()). HasStatementString(statement)), }, + // set policies externally + { + PreConfig: func() { + acc.TestClient().View.Alter(t, sdk.NewAlterViewRequest(id).WithAddRowAccessPolicy(*sdk.NewViewAddRowAccessPolicyRequest(rowAccessPolicy.ID(), []sdk.Column{{Value: "ROLE_NAME"}}))) + acc.TestClient().View.Alter(t, sdk.NewAlterViewRequest(id).WithSetAggregationPolicy(*sdk.NewViewSetAggregationPolicyRequest(aggregationPolicy))) + }, + Config: accconfig.FromModel(t, viewModel), + Check: assert.AssertThat(t, resourceassert.ViewResource(t, "snowflake_view.test"). + HasNameString(id.Name()). + HasStatementString(statement). + HasDatabaseString(id.DatabaseName()). + HasSchemaString(id.SchemaName()), + assert.Check(resource.TestCheckResourceAttr("snowflake_view.test", "aggregation_policy.#", "0")), + assert.Check(resource.TestCheckResourceAttr("snowflake_view.test", "row_access_policy.#", "0")), + ), + }, // set other fields { ConfigDirectory: acc.ConfigurationDirectory("TestAcc_View/basic_update"), @@ -178,6 +193,31 @@ func TestAcc_View_basic(t *testing.T) { assert.Check(resource.TestCheckResourceAttr("snowflake_view.test", "row_access_policy.0.on.0", "ROLE_NAME")), ), }, + // unset policies externally + { + PreConfig: func() { + acc.TestClient().View.Alter(t, sdk.NewAlterViewRequest(id).WithDropAllRowAccessPolicies(true)) + acc.TestClient().View.Alter(t, sdk.NewAlterViewRequest(id).WithUnsetAggregationPolicy(*sdk.NewViewUnsetAggregationPolicyRequest())) + }, + ConfigDirectory: acc.ConfigurationDirectory("TestAcc_View/basic_update"), + ConfigVariables: basicUpdate(rowAccessPolicy.ID(), aggregationPolicy, otherStatement), + Check: assert.AssertThat(t, resourceassert.ViewResource(t, "snowflake_view.test"). + HasNameString(id.Name()). + HasStatementString(otherStatement). + HasDatabaseString(id.DatabaseName()). + HasSchemaString(id.SchemaName()). + HasCommentString("Terraform test resource"), + assert.Check(resource.TestCheckResourceAttr("snowflake_view.test", "aggregation_policy.#", "1")), + assert.Check(resource.TestCheckResourceAttr("snowflake_view.test", "aggregation_policy.0.policy_name", aggregationPolicy.FullyQualifiedName())), + assert.Check(resource.TestCheckResourceAttr("snowflake_view.test", "aggregation_policy.0.entity_key.#", "1")), + assert.Check(resource.TestCheckResourceAttr("snowflake_view.test", "aggregation_policy.0.entity_key.0", "ROLE_NAME")), + assert.Check(resource.TestCheckResourceAttr("snowflake_view.test", "row_access_policy.#", "1")), + assert.Check(resource.TestCheckResourceAttr("snowflake_view.test", "row_access_policy.0.policy_name", rowAccessPolicy.ID().FullyQualifiedName())), + assert.Check(resource.TestCheckResourceAttr("snowflake_view.test", "row_access_policy.0.on.#", "1")), + assert.Check(resource.TestCheckResourceAttr("snowflake_view.test", "row_access_policy.0.on.0", "ROLE_NAME")), + ), + }, + // import - with optionals { ConfigDirectory: acc.ConfigurationDirectory("TestAcc_View/basic_update"), @@ -221,11 +261,6 @@ func TestAcc_View_basic(t *testing.T) { // recreate - change is_recursive { Config: accconfig.FromModel(t, viewModel.WithIsRecursive("true")), - ConfigPlanChecks: resource.ConfigPlanChecks{ - PreApply: []plancheck.PlanCheck{ - plancheck.ExpectResourceAction("snowflake_view.test", plancheck.ResourceActionDestroyBeforeCreate), - }, - }, Check: assert.AssertThat(t, resourceassert.ViewResource(t, "snowflake_view.test"). HasNameString(id.Name()). HasStatementString(otherStatement). @@ -244,6 +279,7 @@ func TestAcc_View_basic(t *testing.T) { } func TestAcc_View_recursive(t *testing.T) { + _ = testenvs.GetOrSkipTest(t, testenvs.EnableAcceptance) acc.TestAccPreCheck(t) id := acc.TestClient().Ids.RandomSchemaObjectIdentifier() statement := "SELECT ROLE_NAME, ROLE_OWNER FROM INFORMATION_SCHEMA.APPLICABLE_ROLES" @@ -280,6 +316,38 @@ func TestAcc_View_recursive(t *testing.T) { }) } +func TestAcc_View_temporary(t *testing.T) { + _ = testenvs.GetOrSkipTest(t, testenvs.EnableAcceptance) + acc.TestAccPreCheck(t) + id := acc.TestClient().Ids.RandomSchemaObjectIdentifier() + statement := "SELECT ROLE_NAME, ROLE_OWNER FROM INFORMATION_SCHEMA.APPLICABLE_ROLES" + viewModel := model.View("test", id.DatabaseName(), id.Name(), id.SchemaName(), statement) + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: acc.TestAccProtoV6ProviderFactories, + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.RequireAbove(tfversion.Version1_5_0), + }, + CheckDestroy: acc.CheckDestroy(t, resources.View), + Steps: []resource.TestStep{ + { + Config: accconfig.FromModel(t, viewModel.WithIsTemporary("true")), + ExpectNonEmptyPlan: true, + ConfigPlanChecks: resource.ConfigPlanChecks{ + PostApplyPostRefresh: []plancheck.PlanCheck{ + plancheck.ExpectResourceAction("snowflake_view.test", plancheck.ResourceActionCreate), + }, + }, + Check: assert.AssertThat(t, resourceassert.ViewResource(t, "snowflake_view.test"). + HasNameString(id.Name()). + HasStatementString(statement). + HasDatabaseString(id.DatabaseName()). + HasSchemaString(id.SchemaName()). + HasIsTemporaryString("true")), + }, + }, + }) +} + func TestAcc_View_complete(t *testing.T) { _ = testenvs.GetOrSkipTest(t, testenvs.EnableAcceptance) acc.TestAccPreCheck(t) @@ -623,7 +691,7 @@ func TestAcc_View_Issue2640(t *testing.T) { }) } -func TestAcc_view_migrateFromVersion094(t *testing.T) { +func TestAcc_view_migrateFromVersion_0_94_1(t *testing.T) { _ = testenvs.GetOrSkipTest(t, testenvs.EnableAcceptance) acc.TestAccPreCheck(t) id := acc.TestClient().Ids.RandomSchemaObjectIdentifier() @@ -635,7 +703,6 @@ func TestAcc_view_migrateFromVersion094(t *testing.T) { t.Cleanup(tagCleanup) resource.Test(t, resource.TestCase{ - PreCheck: func() { acc.TestAccPreCheck(t) }, TerraformVersionChecks: []tfversion.TerraformVersionCheck{ tfversion.RequireAbove(tfversion.Version1_5_0), }, @@ -644,11 +711,11 @@ func TestAcc_view_migrateFromVersion094(t *testing.T) { { ExternalProviders: map[string]resource.ExternalProvider{ "snowflake": { - VersionConstraint: "=0.94.0", + VersionConstraint: "=0.94.1", Source: "Snowflake-Labs/snowflake", }, }, - Config: viewv094WithTags(id, tag.SchemaName, tag.Name, "foo", statement), + Config: viewv_0_94_1_WithTags(id, tag.SchemaName, tag.Name, "foo", statement), Check: resource.ComposeAggregateTestCheckFunc( resource.TestCheckResourceAttr(resourceName, "name", id.Name()), resource.TestCheckResourceAttr(resourceName, "tag.#", "1"), @@ -668,7 +735,7 @@ func TestAcc_view_migrateFromVersion094(t *testing.T) { }) } -func viewv094WithTags(id sdk.SchemaObjectIdentifier, tagSchema, tagName, tagValue, statement string) string { +func viewv_0_94_1_WithTags(id sdk.SchemaObjectIdentifier, tagSchema, tagName, tagValue, statement string) string { s := ` resource "snowflake_view" "test" { name = "%[1]s" diff --git a/pkg/resources/view_state_upgraders.go b/pkg/resources/view_state_upgraders.go index 8883e2190f..f48b54c568 100644 --- a/pkg/resources/view_state_upgraders.go +++ b/pkg/resources/view_state_upgraders.go @@ -5,7 +5,7 @@ import ( "strconv" ) -func v094ViewStateUpgrader(ctx context.Context, rawState map[string]any, meta any) (map[string]any, error) { +func v0_94_1_ViewStateUpgrader(ctx context.Context, rawState map[string]any, meta any) (map[string]any, error) { if rawState == nil { return rawState, nil } diff --git a/pkg/sdk/views_gen.go b/pkg/sdk/views_gen.go index 0f681eaabc..2c85f8b30a 100644 --- a/pkg/sdk/views_gen.go +++ b/pkg/sdk/views_gen.go @@ -244,11 +244,11 @@ func (v *View) ID() SchemaObjectIdentifier { return NewSchemaObjectIdentifier(v.DatabaseName, v.SchemaName, v.Name) } +// TODO(SNOW-1636212): remove func (v *View) HasCopyGrants() bool { return strings.Contains(v.Text, " COPY GRANTS ") } -// TODO: proper extraction func (v *View) IsTemporary() bool { return strings.Contains(v.Text, "TEMPORARY") } diff --git a/pkg/snowflake/parser.go b/pkg/snowflake/parser.go index 69810021c5..b1e402379e 100644 --- a/pkg/snowflake/parser.go +++ b/pkg/snowflake/parser.go @@ -13,6 +13,7 @@ import ( // to support queries of the sort that are generated by this project. // // Also there is little error handling and we assume queries are well-formed. +// TODO (SNOW-1636212): remove type ViewSelectStatementExtractor struct { input []rune pos int @@ -41,6 +42,7 @@ func (e *ViewSelectStatementExtractor) Extract() (string, error) { e.consumeToken("if not exists") e.consumeSpace() e.consumeID() + // TODO column list e.consumeSpace() e.consumeToken("copy grants") e.consumeComment() diff --git a/templates/resources/view.md.tmpl b/templates/resources/view.md.tmpl index 9d371d4b57..686247b30c 100644 --- a/templates/resources/view.md.tmpl +++ b/templates/resources/view.md.tmpl @@ -11,6 +11,10 @@ description: |- !> **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#v094x--v0950) to use it. +!> **Note about copy_grants** Fields like `is_recursive`, `is_temporary`, `copy_grants` and `statement` can not be altered, and a change means recreation of the resource. ForceNew can not be used because it does not preserve grants from `copy_grants`. Beware that even though a change is marked as update, the resource is recreated. + +~> **Required warehouse** For this resource, the provider uses [policy references](https://docs.snowflake.com/en/sql-reference/functions/policy_references) which requires a warehouse in the connection. Please, make sure you have either set a DEFAULT_WAREHOUSE for the user, or specified a warehouse in the configuration. + # {{.Name}} ({{.Type}}) {{ .Description | trimspace }} From b6f60f6eec68cd055f78adb35093cc94dfedb108 Mon Sep 17 00:00:00 2001 From: Jakub Michalak Date: Thu, 22 Aug 2024 15:03:08 +0200 Subject: [PATCH 04/14] Review suggestions --- pkg/resources/view.go | 190 +++++++++++++++++------------------------- 1 file changed, 75 insertions(+), 115 deletions(-) diff --git a/pkg/resources/view.go b/pkg/resources/view.go index 2a8f49e343..0c8e33d527 100644 --- a/pkg/resources/view.go +++ b/pkg/resources/view.go @@ -273,7 +273,7 @@ func View() *schema.Resource { return &schema.Resource{ SchemaVersion: 1, - CreateContext: CreateView, + CreateContext: CreateView(false), ReadContext: ReadView(true), UpdateContext: UpdateView, DeleteContext: DeleteView, @@ -327,95 +327,88 @@ func ImportView(ctx context.Context, d *schema.ResourceData, meta any) ([]*schem return []*schema.ResourceData{d}, nil } -func CreateView(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { - client := meta.(*provider.Context).Client - - req, err := prepareCreateRequest(d) - if err != nil { - return diag.FromErr(err) - } - id := req.GetName() - - err = client.Views.Create(ctx, req) - if err != nil { - return diag.FromErr(fmt.Errorf("error creating view %v err = %w", id.Name(), err)) - } +func CreateView(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) - d.SetId(helpers.EncodeSnowflakeID(id)) + statement := d.Get("statement").(string) + req := sdk.NewCreateViewRequest(id, statement) - if v := d.Get("change_tracking").(string); v != BooleanDefault { - parsed, err := booleanStringToBool(v) - if err != nil { - return diag.FromErr(err) + // TODO(next pr): remove or_replace field + if v := d.Get("or_replace"); v.(bool) || orReplace { + req.WithOrReplace(true) } - err = client.Views.Alter(ctx, sdk.NewAlterViewRequest(id).WithSetChangeTracking(parsed)) - if err != nil { - return diag.FromErr(fmt.Errorf("error setting change_tracking in view %v err = %w", id.Name(), err)) + if v := d.Get("copy_grants"); v.(bool) { + req.WithCopyGrants(true) } - } - return ReadView(false)(ctx, d, meta) -} - -func prepareCreateRequest(d *schema.ResourceData) (*sdk.CreateViewRequest, error) { - databaseName := d.Get("database").(string) - schemaName := d.Get("schema").(string) - name := d.Get("name").(string) - id := sdk.NewSchemaObjectIdentifier(databaseName, schemaName, name) + if v := d.Get("is_secure").(string); v != BooleanDefault { + parsed, err := strconv.ParseBool(v) + if err != nil { + return diag.FromErr(err) + } + req.WithSecure(parsed) + } - statement := d.Get("statement").(string) - req := sdk.NewCreateViewRequest(id, statement) + if v := d.Get("is_temporary").(string); v != BooleanDefault { + parsed, err := strconv.ParseBool(v) + if err != nil { + return diag.FromErr(err) + } + req.WithTemporary(parsed) + } - if v := d.Get("or_replace"); v.(bool) { - req.WithOrReplace(true) - } + if v := d.Get("is_recursive").(string); v != BooleanDefault { + parsed, err := strconv.ParseBool(v) + if err != nil { + return diag.FromErr(err) + } + req.WithRecursive(parsed) + } - if v := d.Get("copy_grants"); v.(bool) { - req.WithCopyGrants(true) - } + if v := d.Get("comment").(string); len(v) > 0 { + req.WithComment(v) + } - if v := d.Get("is_secure").(string); v != BooleanDefault { - parsed, err := strconv.ParseBool(v) - if err != nil { - return nil, err + if v := d.Get("row_access_policy"); len(v.([]any)) > 0 { + req.WithRowAccessPolicy(*sdk.NewViewRowAccessPolicyRequest(extractPolicyWithColumns(v, "on"))) } - req.WithSecure(parsed) - } - if v := d.Get("is_temporary").(string); v != BooleanDefault { - parsed, err := strconv.ParseBool(v) - if err != nil { - return nil, err + if v := d.Get("aggregation_policy"); len(v.([]any)) > 0 { + id, columns := extractPolicyWithColumns(v, "entity_key") + aggregationPolicyReq := sdk.NewViewAggregationPolicyRequest(id) + if len(columns) > 0 { + aggregationPolicyReq.WithEntityKey(columns) + } + req.WithAggregationPolicy(*aggregationPolicyReq) } - req.WithTemporary(parsed) - } - if v := d.Get("is_recursive").(string); v != BooleanDefault { - parsed, err := strconv.ParseBool(v) + err := client.Views.Create(ctx, req) if err != nil { - return nil, err + return diag.FromErr(fmt.Errorf("error creating view %v err = %w", id.Name(), err)) } - req.WithRecursive(parsed) - } - if v := d.Get("comment").(string); len(v) > 0 { - req.WithComment(v) - } + d.SetId(helpers.EncodeSnowflakeID(id)) - if v := d.Get("row_access_policy"); len(v.([]any)) > 0 { - req.WithRowAccessPolicy(*sdk.NewViewRowAccessPolicyRequest(extractPolicyWithColumns(v, "on"))) - } + if v := d.Get("change_tracking").(string); v != BooleanDefault { + parsed, err := booleanStringToBool(v) + if err != nil { + return diag.FromErr(err) + } - if v := d.Get("aggregation_policy"); len(v.([]any)) > 0 { - id, columns := extractPolicyWithColumns(v, "entity_key") - aggregationPolicyReq := sdk.NewViewAggregationPolicyRequest(id) - if len(columns) > 0 { - aggregationPolicyReq.WithEntityKey(columns) + err = client.Views.Alter(ctx, sdk.NewAlterViewRequest(id).WithSetChangeTracking(parsed)) + if err != nil { + return diag.FromErr(fmt.Errorf("error setting change_tracking in view %v err = %w", id.Name(), err)) + } } - req.WithAggregationPolicy(*aggregationPolicyReq) + + return ReadView(false)(ctx, d, meta) } - return req, nil } func extractPolicyWithColumns(v any, columnsKey string) (sdk.SchemaObjectIdentifier, []sdk.Column) { @@ -569,27 +562,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 d.HasChange("statement") || d.HasChange("is_temporary") || d.HasChange("is_recursive") || d.HasChange("copy_grant") { - req, err := prepareCreateRequest(d) - if err != nil { - return diag.FromErr(err) - } - - err = client.Views.Create(ctx, req.WithOrReplace(true)) - if err != nil { - return diag.FromErr(fmt.Errorf("error when changing property on %v and performing create or replace to update view statements, err = %w", d.Id(), err)) - } - if v := d.Get("change_tracking").(string); v != BooleanDefault { - parsed, err := booleanStringToBool(v) - if err != nil { - return diag.FromErr(err) - } - - err = client.Views.Alter(ctx, sdk.NewAlterViewRequest(id).WithSetChangeTracking(parsed)) - if err != nil { - return diag.FromErr(fmt.Errorf("error setting change_tracking in view %v err = %w", id.Name(), err)) - } - } - return ReadView(false)(ctx, d, meta) + return CreateView(true)(ctx, d, meta) } if d.HasChange("name") { @@ -656,22 +629,15 @@ func UpdateView(ctx context.Context, d *schema.ResourceData, meta any) diag.Diag if d.HasChange("row_access_policy") { var addReq *sdk.ViewAddRowAccessPolicyRequest var dropReq *sdk.ViewDropRowAccessPolicyRequest - if v, ok := d.GetOk("row_access_policy"); ok { - rowAccessPolicyConfig := v.([]any)[0].(map[string]any) - columnsRaw := expandStringList(rowAccessPolicyConfig["on"].(*schema.Set).List()) - columns := make([]sdk.Column, len(columnsRaw)) - for i := range columnsRaw { - columns[i] = sdk.Column{Value: columnsRaw[i]} - } - addReq = sdk.NewViewAddRowAccessPolicyRequest( - sdk.NewSchemaObjectIdentifierFromFullyQualifiedName(rowAccessPolicyConfig["policy_name"].(string)), - columns) + oldRaw, newRaw := d.GetChange("row_access_policy") + if len(oldRaw.([]any)) > 0 { + oldId, _ := extractPolicyWithColumns(oldRaw, "on") + dropReq = sdk.NewViewDropRowAccessPolicyRequest(oldId) } - old, _ := d.GetChange("row_access_policy") - if len(old.([]any)) > 0 { - oldPolicy := old.([]any)[0].(map[string]any) - dropReq = sdk.NewViewDropRowAccessPolicyRequest(sdk.NewSchemaObjectIdentifierFromFullyQualifiedName(oldPolicy["policy_name"].(string))) + if len(newRaw.([]any)) > 0 { + newId, newColumns := extractPolicyWithColumns(newRaw, "on") + addReq = sdk.NewViewAddRowAccessPolicyRequest(newId, newColumns) } req := sdk.NewAlterViewRequest(id) if addReq != nil && dropReq != nil { // nolint @@ -688,18 +654,12 @@ func UpdateView(ctx context.Context, d *schema.ResourceData, meta any) diag.Diag } if d.HasChange("aggregation_policy") { if v, ok := d.GetOk("aggregation_policy"); ok { - rowAccessPolicyConfig := v.([]any)[0].(map[string]any) - columnsRaw := expandStringList(rowAccessPolicyConfig["entity_key"].(*schema.Set).List()) - columns := make([]sdk.Column, len(columnsRaw)) - for i := range columnsRaw { - columns[i] = sdk.Column{Value: columnsRaw[i]} - } - aggregationPolicyReq := sdk.NewViewSetAggregationPolicyRequest( - sdk.NewSchemaObjectIdentifierFromFullyQualifiedName(rowAccessPolicyConfig["policy_name"].(string))).WithForce(true) - if len(columns) > 0 { - aggregationPolicyReq.WithEntityKey(columns) + newId, newColumns := extractPolicyWithColumns(v, "entity_key") + aggregationPolicyReq := sdk.NewViewSetAggregationPolicyRequest(newId) + if len(newColumns) > 0 { + aggregationPolicyReq.WithEntityKey(newColumns) } - err := client.Views.Alter(ctx, sdk.NewAlterViewRequest(id).WithSetAggregationPolicy(*aggregationPolicyReq)) + err := client.Views.Alter(ctx, sdk.NewAlterViewRequest(id).WithSetAggregationPolicy(*aggregationPolicyReq.WithForce(true))) if err != nil { return diag.FromErr(fmt.Errorf("error setting aggregation policy for view %v: %w", d.Id(), err)) } From 2422b666a4f86edee87072be6d46e419423d7bcb Mon Sep 17 00:00:00 2001 From: Jakub Michalak Date: Thu, 22 Aug 2024 15:28:41 +0200 Subject: [PATCH 05/14] Add a note about fully_qualified_name --- docs/resources/view.md | 2 ++ templates/resources/view.md.tmpl | 3 +++ 2 files changed, 5 insertions(+) diff --git a/docs/resources/view.md b/docs/resources/view.md index 53d7ba906a..cfba1e31bd 100644 --- a/docs/resources/view.md +++ b/docs/resources/view.md @@ -60,6 +60,8 @@ resource "snowflake_view" "test" { SQL } ``` +-> **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 diff --git a/templates/resources/view.md.tmpl b/templates/resources/view.md.tmpl index 686247b30c..67a79c7eb7 100644 --- a/templates/resources/view.md.tmpl +++ b/templates/resources/view.md.tmpl @@ -23,6 +23,9 @@ description: |- ## 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 }} From d066e546a3b34dcb2ec5626b355383a1170c08ee Mon Sep 17 00:00:00 2001 From: Jakub Michalak Date: Thu, 22 Aug 2024 15:53:29 +0200 Subject: [PATCH 06/14] Fix comments with single quotes --- pkg/resources/view_acceptance_test.go | 15 ++++++++------- pkg/snowflake/parser.go | 3 ++- pkg/snowflake/parser_test.go | 2 ++ 3 files changed, 12 insertions(+), 8 deletions(-) diff --git a/pkg/resources/view_acceptance_test.go b/pkg/resources/view_acceptance_test.go index 563e6c7a1e..f9a3955bec 100644 --- a/pkg/resources/view_acceptance_test.go +++ b/pkg/resources/view_acceptance_test.go @@ -43,6 +43,7 @@ func TestAcc_View_basic(t *testing.T) { id := acc.TestClient().Ids.RandomSchemaObjectIdentifier() statement := "SELECT ROLE_NAME, ROLE_OWNER FROM INFORMATION_SCHEMA.APPLICABLE_ROLES" otherStatement := "SELECT ROLE_NAME, ROLE_OWNER FROM INFORMATION_SCHEMA.APPLICABLE_ROLES where ROLE_OWNER like 'foo%%'" + comment := "Terraform test resource'" viewModel := model.View("test", id.DatabaseName(), id.Name(), id.SchemaName(), statement) @@ -57,7 +58,7 @@ func TestAcc_View_basic(t *testing.T) { "row_access_policy_on": config.ListVariable(config.StringVariable("ROLE_NAME")), "aggregation_policy": config.StringVariable(ap.FullyQualifiedName()), "aggregation_policy_entity_key": config.ListVariable(config.StringVariable("ROLE_NAME")), - "comment": config.StringVariable("Terraform test resource"), + "comment": config.StringVariable(comment), } } @@ -119,7 +120,7 @@ func TestAcc_View_basic(t *testing.T) { HasStatementString(statement). HasDatabaseString(id.DatabaseName()). HasSchemaString(id.SchemaName()). - HasCommentString("Terraform test resource"), + HasCommentString(comment), assert.Check(resource.TestCheckResourceAttr("snowflake_view.test", "aggregation_policy.#", "1")), assert.Check(resource.TestCheckResourceAttr("snowflake_view.test", "aggregation_policy.0.policy_name", aggregationPolicy.FullyQualifiedName())), assert.Check(resource.TestCheckResourceAttr("snowflake_view.test", "aggregation_policy.0.entity_key.#", "1")), @@ -139,7 +140,7 @@ func TestAcc_View_basic(t *testing.T) { HasStatementString(statement). HasDatabaseString(id.DatabaseName()). HasSchemaString(id.SchemaName()). - HasCommentString("Terraform test resource"), + HasCommentString(comment), assert.Check(resource.TestCheckResourceAttr("snowflake_view.test", "aggregation_policy.#", "1")), assert.Check(resource.TestCheckResourceAttr("snowflake_view.test", "aggregation_policy.0.policy_name", aggregationPolicy2.FullyQualifiedName())), assert.Check(resource.TestCheckResourceAttr("snowflake_view.test", "aggregation_policy.0.entity_key.#", "1")), @@ -159,7 +160,7 @@ func TestAcc_View_basic(t *testing.T) { HasStatementString(otherStatement). HasDatabaseString(id.DatabaseName()). HasSchemaString(id.SchemaName()). - HasCommentString("Terraform test resource"), + HasCommentString(comment), assert.Check(resource.TestCheckResourceAttr("snowflake_view.test", "aggregation_policy.#", "1")), assert.Check(resource.TestCheckResourceAttr("snowflake_view.test", "aggregation_policy.0.policy_name", aggregationPolicy.FullyQualifiedName())), assert.Check(resource.TestCheckResourceAttr("snowflake_view.test", "aggregation_policy.0.entity_key.#", "1")), @@ -182,7 +183,7 @@ func TestAcc_View_basic(t *testing.T) { HasStatementString(otherStatement). HasDatabaseString(id.DatabaseName()). HasSchemaString(id.SchemaName()). - HasCommentString("Terraform test resource"), + HasCommentString(comment), assert.Check(resource.TestCheckResourceAttr("snowflake_view.test", "aggregation_policy.#", "1")), assert.Check(resource.TestCheckResourceAttr("snowflake_view.test", "aggregation_policy.0.policy_name", aggregationPolicy.FullyQualifiedName())), assert.Check(resource.TestCheckResourceAttr("snowflake_view.test", "aggregation_policy.0.entity_key.#", "1")), @@ -206,7 +207,7 @@ func TestAcc_View_basic(t *testing.T) { HasStatementString(otherStatement). HasDatabaseString(id.DatabaseName()). HasSchemaString(id.SchemaName()). - HasCommentString("Terraform test resource"), + HasCommentString(comment), assert.Check(resource.TestCheckResourceAttr("snowflake_view.test", "aggregation_policy.#", "1")), assert.Check(resource.TestCheckResourceAttr("snowflake_view.test", "aggregation_policy.0.policy_name", aggregationPolicy.FullyQualifiedName())), assert.Check(resource.TestCheckResourceAttr("snowflake_view.test", "aggregation_policy.0.entity_key.#", "1")), @@ -230,7 +231,7 @@ func TestAcc_View_basic(t *testing.T) { HasStatementString(otherStatement). HasDatabaseString(id.DatabaseName()). HasSchemaString(id.SchemaName()). - HasCommentString("Terraform test resource"). + HasCommentString(comment). HasIsSecureString("false"). HasIsTemporaryString("false"). HasChangeTrackingString("false"), diff --git a/pkg/snowflake/parser.go b/pkg/snowflake/parser.go index b1e402379e..63c97f1ffc 100644 --- a/pkg/snowflake/parser.go +++ b/pkg/snowflake/parser.go @@ -248,7 +248,8 @@ func (e *ViewSelectStatementExtractor) consumeQuotedParameter(param string) { if escaped { //nolint:gocritic // todo: please fix this to pass gocritic escaped = false - } else if e.input[e.pos+found] == '\\' { + // There are two possible escape sequences: \' and '' + } else if e.input[e.pos+found] == '\\' || e.pos+found+1 < len(e.input) && string(e.input[e.pos+found:e.pos+found+2]) == "''" { escaped = true } else if e.input[e.pos+found] == '\'' { break diff --git a/pkg/snowflake/parser_test.go b/pkg/snowflake/parser_test.go index e603403864..efa03aadb6 100644 --- a/pkg/snowflake/parser_test.go +++ b/pkg/snowflake/parser_test.go @@ -10,6 +10,7 @@ import ( func TestViewSelectStatementExtractor_Extract(t *testing.T) { basic := "create view foo as select * from bar;" caps := "CREATE VIEW FOO AS SELECT * FROM BAR;" + commentWithSingleQuotes := "CREATE VIEW FOO COMMENT = 'test''' AS SELECT * FROM BAR;" parens := "create view foo as (select * from bar);" multiline := ` create view foo as @@ -47,6 +48,7 @@ from bar;` }{ {"basic", args{basic}, "select * from bar;", false}, {"caps", args{caps}, "SELECT * FROM BAR;", false}, + {"comment with single quotes", args{commentWithSingleQuotes}, "SELECT * FROM BAR;", false}, {"parens", args{parens}, "(select * from bar);", false}, {"multiline", args{multiline}, "select *\nfrom bar;", false}, {"multilineComment", args{multilineComment}, "-- comment\nselect *\nfrom bar;", false}, From f37244e5ba36b516c9e2ca6c210c3f2ad6d4aa6e Mon Sep 17 00:00:00 2001 From: Jakub Michalak Date: Fri, 23 Aug 2024 10:04:29 +0200 Subject: [PATCH 07/14] Review suggestions and updated docs for name field --- docs/resources/account_role.md | 2 +- ...tegration_with_authorization_code_grant.md | 2 +- ...ion_integration_with_client_credentials.md | 2 +- ...hentication_integration_with_jwt_bearer.md | 2 +- docs/resources/database.md | 2 +- docs/resources/external_oauth_integration.md | 2 +- docs/resources/network_policy.md | 2 +- .../oauth_integration_for_custom_clients.md | 2 +- ...th_integration_for_partner_applications.md | 2 +- docs/resources/role.md | 2 +- docs/resources/saml2_integration.md | 2 +- docs/resources/scim_integration.md | 2 +- docs/resources/secondary_database.md | 2 +- docs/resources/shared_database.md | 2 +- docs/resources/view.md | 10 +++---- docs/resources/warehouse.md | 2 +- pkg/resources/account_role.go | 1 + .../api_authentication_integration_common.go | 2 +- pkg/resources/common.go | 30 ------------------- pkg/resources/database.go | 2 +- pkg/resources/doc_helpers.go | 4 +-- pkg/resources/external_oauth_integration.go | 2 +- pkg/resources/network_policy.go | 2 +- .../oauth_integration_for_custom_clients.go | 2 +- ...th_integration_for_partner_applications.go | 2 +- pkg/resources/saml2_integration.go | 2 +- pkg/resources/scim_integration.go | 2 +- pkg/resources/secondary_database.go | 2 +- pkg/resources/shared_database.go | 2 +- pkg/resources/view.go | 6 ++-- pkg/resources/warehouse.go | 2 +- templates/resources/view.md.tmpl | 4 +-- 32 files changed, 39 insertions(+), 68 deletions(-) diff --git a/docs/resources/account_role.md b/docs/resources/account_role.md index 1be71bb81f..e754c835b2 100644 --- a/docs/resources/account_role.md +++ b/docs/resources/account_role.md @@ -31,7 +31,7 @@ resource "snowflake_account_role" "complete" { ### Required -- `name` (String) +- `name` (String) Identifier for the role; must be unique for your account. 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 diff --git a/docs/resources/api_authentication_integration_with_authorization_code_grant.md b/docs/resources/api_authentication_integration_with_authorization_code_grant.md index 76613bf024..bac4f8446d 100644 --- a/docs/resources/api_authentication_integration_with_authorization_code_grant.md +++ b/docs/resources/api_authentication_integration_with_authorization_code_grant.md @@ -43,7 +43,7 @@ resource "snowflake_api_authentication_integration_with_authorization_code_grant ### Required - `enabled` (Boolean) Specifies whether this security integration is enabled or disabled. -- `name` (String) Specifies the identifier (i.e. name) for the integration. This value must be unique in your account. +- `name` (String) Specifies the identifier (i.e. name) for the integration. This value must be unique in your account. 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: `|`, `.`, `(`, `)`, `"` - `oauth_client_id` (String) Specifies the client ID for the OAuth application in the external service. - `oauth_client_secret` (String) Specifies the client secret for the OAuth application in the ServiceNow instance from the previous step. The connector uses this to request an access token from the ServiceNow instance. diff --git a/docs/resources/api_authentication_integration_with_client_credentials.md b/docs/resources/api_authentication_integration_with_client_credentials.md index 9adeb93da8..d2d53bcdca 100644 --- a/docs/resources/api_authentication_integration_with_client_credentials.md +++ b/docs/resources/api_authentication_integration_with_client_credentials.md @@ -41,7 +41,7 @@ resource "snowflake_api_authentication_integration_with_client_credentials" "tes ### Required - `enabled` (Boolean) Specifies whether this security integration is enabled or disabled. -- `name` (String) Specifies the identifier (i.e. name) for the integration. This value must be unique in your account. +- `name` (String) Specifies the identifier (i.e. name) for the integration. This value must be unique in your account. 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: `|`, `.`, `(`, `)`, `"` - `oauth_client_id` (String) Specifies the client ID for the OAuth application in the external service. - `oauth_client_secret` (String) Specifies the client secret for the OAuth application in the ServiceNow instance from the previous step. The connector uses this to request an access token from the ServiceNow instance. diff --git a/docs/resources/api_authentication_integration_with_jwt_bearer.md b/docs/resources/api_authentication_integration_with_jwt_bearer.md index a804a4d54b..4dff5ea9db 100644 --- a/docs/resources/api_authentication_integration_with_jwt_bearer.md +++ b/docs/resources/api_authentication_integration_with_jwt_bearer.md @@ -44,7 +44,7 @@ resource "snowflake_api_authentication_integration_with_jwt_bearer" "test" { ### Required - `enabled` (Boolean) Specifies whether this security integration is enabled or disabled. -- `name` (String) Specifies the identifier (i.e. name) for the integration. This value must be unique in your account. +- `name` (String) Specifies the identifier (i.e. name) for the integration. This value must be unique in your account. 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: `|`, `.`, `(`, `)`, `"` - `oauth_assertion_issuer` (String) - `oauth_client_id` (String) Specifies the client ID for the OAuth application in the external service. - `oauth_client_secret` (String) Specifies the client secret for the OAuth application in the ServiceNow instance from the previous step. The connector uses this to request an access token from the ServiceNow instance. diff --git a/docs/resources/database.md b/docs/resources/database.md index fce5a16456..5338be0c06 100644 --- a/docs/resources/database.md +++ b/docs/resources/database.md @@ -82,7 +82,7 @@ resource "snowflake_database" "primary" { ### Required -- `name` (String) Specifies the identifier for the database; must be unique for your account. As a best practice for [Database Replication and Failover](https://docs.snowflake.com/en/user-guide/db-replication-intro), it is recommended to give each secondary database the same name as its primary database. This practice supports referencing fully-qualified objects (i.e. '..') by other objects in the same database, such as querying a fully-qualified table name in a view. If a secondary database has a different name from the primary database, then these object references would break in the secondary database. +- `name` (String) Specifies the identifier for the database; must be unique for your account. As a best practice for [Database Replication and Failover](https://docs.snowflake.com/en/user-guide/db-replication-intro), it is recommended to give each secondary database the same name as its primary database. This practice supports referencing fully-qualified objects (i.e. '..') by other objects in the same database, such as querying a fully-qualified table name in a view. If a secondary database has a different name from the primary database, then these object references would break in the secondary database. 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 diff --git a/docs/resources/external_oauth_integration.md b/docs/resources/external_oauth_integration.md index fc253a218e..caf5ebae29 100644 --- a/docs/resources/external_oauth_integration.md +++ b/docs/resources/external_oauth_integration.md @@ -68,7 +68,7 @@ resource "snowflake_external_oauth_integration" "test" { - `external_oauth_snowflake_user_mapping_attribute` (String) Indicates which Snowflake user record attribute should be used to map the access token to a Snowflake user record. Valid values are (case-insensitive): `LOGIN_NAME` | `EMAIL_ADDRESS`. - `external_oauth_token_user_mapping_claim` (Set of String) Specifies the access token claim or claims that can be used to map the access token to a Snowflake user record. If removed from the config, the resource is recreated. - `external_oauth_type` (String) Specifies the OAuth 2.0 authorization server to be Okta, Microsoft Azure AD, Ping Identity PingFederate, or a Custom OAuth 2.0 authorization server. Valid values are (case-insensitive): `OKTA` | `AZURE` | `PING_FEDERATE` | `CUSTOM`. -- `name` (String) Specifies the name of the External Oath integration. This name follows the rules for Object Identifiers. The name should be unique among security integrations in your account. +- `name` (String) Specifies the name of the External Oath integration. This name follows the rules for Object Identifiers. The name should be unique among security integrations in your account. 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 diff --git a/docs/resources/network_policy.md b/docs/resources/network_policy.md index db80cda3b9..220552f364 100644 --- a/docs/resources/network_policy.md +++ b/docs/resources/network_policy.md @@ -35,7 +35,7 @@ resource "snowflake_network_policy" "basic" { ### Required -- `name` (String) Specifies the identifier for the network policy; must be unique for the account in which the network policy is created. +- `name` (String) Specifies the identifier for the network policy; must be unique for the account in which the network policy 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: `|`, `.`, `(`, `)`, `"` ### Optional diff --git a/docs/resources/oauth_integration_for_custom_clients.md b/docs/resources/oauth_integration_for_custom_clients.md index 8c9f1f7c68..0c536d0198 100644 --- a/docs/resources/oauth_integration_for_custom_clients.md +++ b/docs/resources/oauth_integration_for_custom_clients.md @@ -48,7 +48,7 @@ resource "snowflake_oauth_integration_for_custom_clients" "complete" { ### Required - `blocked_roles_list` (Set of String) A set of Snowflake roles that a user cannot explicitly consent to using after authenticating. -- `name` (String) Specifies the name of the OAuth integration. This name follows the rules for Object Identifiers. The name should be unique among security integrations in your account. +- `name` (String) Specifies the name of the OAuth integration. This name follows the rules for Object Identifiers. The name should be unique among security integrations in your account. 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: `|`, `.`, `(`, `)`, `"` - `oauth_client_type` (String) Specifies the type of client being registered. Snowflake supports both confidential and public clients. Valid options are: `PUBLIC` | `CONFIDENTIAL`. - `oauth_redirect_uri` (String) Specifies the client URI. After a user is authenticated, the web browser is redirected to this URI. diff --git a/docs/resources/oauth_integration_for_partner_applications.md b/docs/resources/oauth_integration_for_partner_applications.md index 2e0c1505b6..164b3d066c 100644 --- a/docs/resources/oauth_integration_for_partner_applications.md +++ b/docs/resources/oauth_integration_for_partner_applications.md @@ -41,7 +41,7 @@ resource "snowflake_oauth_integration_for_partner_applications" "test" { ### Required - `blocked_roles_list` (Set of String) A set of Snowflake roles that a user cannot explicitly consent to using after authenticating. -- `name` (String) Specifies the name of the OAuth integration. This name follows the rules for Object Identifiers. The name should be unique among security integrations in your account. +- `name` (String) Specifies the name of the OAuth integration. This name follows the rules for Object Identifiers. The name should be unique among security integrations in your account. 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: `|`, `.`, `(`, `)`, `"` - `oauth_client` (String) Creates an OAuth interface between Snowflake and a partner application. Valid options are: `LOOKER` | `TABLEAU_DESKTOP` | `TABLEAU_SERVER`. ### Optional diff --git a/docs/resources/role.md b/docs/resources/role.md index f30eb5ac9f..a20a466924 100644 --- a/docs/resources/role.md +++ b/docs/resources/role.md @@ -31,7 +31,7 @@ resource "snowflake_role" "complete" { ### Required -- `name` (String) +- `name` (String) Identifier for the role; must be unique for your account. 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 diff --git a/docs/resources/saml2_integration.md b/docs/resources/saml2_integration.md index 38ebcacab8..a94afd2ff5 100644 --- a/docs/resources/saml2_integration.md +++ b/docs/resources/saml2_integration.md @@ -51,7 +51,7 @@ resource "snowflake_saml2_integration" "test" { ### Required -- `name` (String) Specifies the name of the SAML2 integration. This name follows the rules for Object Identifiers. The name should be unique among security integrations in your account. +- `name` (String) Specifies the name of the SAML2 integration. This name follows the rules for Object Identifiers. The name should be unique among security integrations in your account. 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: `|`, `.`, `(`, `)`, `"` - `saml2_issuer` (String) The string containing the IdP EntityID / Issuer. - `saml2_provider` (String) The string describing the IdP. Valid options are: `OKTA` | `ADFS` | `CUSTOM`. - `saml2_sso_url` (String) The string containing the IdP SSO URL, where the user should be redirected by Snowflake (the Service Provider) with a SAML AuthnRequest message. diff --git a/docs/resources/scim_integration.md b/docs/resources/scim_integration.md index ad939a7790..2a93a0141d 100644 --- a/docs/resources/scim_integration.md +++ b/docs/resources/scim_integration.md @@ -39,7 +39,7 @@ resource "snowflake_scim_integration" "test" { ### Required - `enabled` (Boolean) Specify whether the security integration is enabled. -- `name` (String) String that specifies the identifier (i.e. name) for the integration; must be unique in your account. +- `name` (String) String that specifies the identifier (i.e. name) for the integration; must be unique in your account. 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: `|`, `.`, `(`, `)`, `"` - `run_as_role` (String) Specify the SCIM role in Snowflake that owns any users and roles that are imported from the identity provider into Snowflake using SCIM. Provider assumes that the specified role is already provided. Valid options are: `OKTA_PROVISIONER` | `AAD_PROVISIONER` | `GENERIC_SCIM_PROVISIONER`. - `scim_client` (String) Specifies the client type for the scim integration. Valid options are: `OKTA` | `AZURE` | `GENERIC`. diff --git a/docs/resources/secondary_database.md b/docs/resources/secondary_database.md index 64cd5cabbf..4d7a927c21 100644 --- a/docs/resources/secondary_database.md +++ b/docs/resources/secondary_database.md @@ -91,7 +91,7 @@ resource "snowflake_task" "refresh_secondary_database" { ### Required - `as_replica_of` (String) A fully qualified path to a database to create a replica from. A fully qualified path follows the format of `""."".""`. -- `name` (String) Specifies the identifier for the database; must be unique for your account. As a best practice for [Database Replication and Failover](https://docs.snowflake.com/en/user-guide/db-replication-intro), it is recommended to give each secondary database the same name as its primary database. This practice supports referencing fully-qualified objects (i.e. '..') by other objects in the same database, such as querying a fully-qualified table name in a view. If a secondary database has a different name from the primary database, then these object references would break in the secondary database. +- `name` (String) Specifies the identifier for the database; must be unique for your account. As a best practice for [Database Replication and Failover](https://docs.snowflake.com/en/user-guide/db-replication-intro), it is recommended to give each secondary database the same name as its primary database. This practice supports referencing fully-qualified objects (i.e. '..') by other objects in the same database, such as querying a fully-qualified table name in a view. If a secondary database has a different name from the primary database, then these object references would break in the secondary database. 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 diff --git a/docs/resources/shared_database.md b/docs/resources/shared_database.md index 591563ac72..785873b4c9 100644 --- a/docs/resources/shared_database.md +++ b/docs/resources/shared_database.md @@ -76,7 +76,7 @@ resource "snowflake_shared_database" "test" { ### Required - `from_share` (String) A fully qualified path to a share from which the database will be created. A fully qualified path follows the format of `""."".""`. -- `name` (String) Specifies the identifier for the database; must be unique for your account. +- `name` (String) Specifies the identifier for the database; must be unique for your account. 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 diff --git a/docs/resources/view.md b/docs/resources/view.md index cfba1e31bd..dc34c5ebc6 100644 --- a/docs/resources/view.md +++ b/docs/resources/view.md @@ -7,9 +7,9 @@ description: |- !> **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#v094x--v0950) to use it. -!> **Note about copy_grants** Fields like `is_recursive`, `is_temporary`, `copy_grants` and `statement` can not be altered, and a change means recreation of the resource. ForceNew can not be used because it does not preserve grants from `copy_grants`. Beware that even though a change is marked as update, the resource is recreated. +!> **Note about copy_grants** Fields like `is_recursive`, `is_temporary`, `copy_grants` and `statement` can not be ALTERed on Snowflake side (check [docs](https://docs.snowflake.com/en/sql-reference/sql/alter-view)), and a change means recreation of the resource. ForceNew can not be used because it does not preserve grants from `copy_grants`. Beware that even though a change is marked as update, the resource is recreated. -~> **Required warehouse** For this resource, the provider uses [policy references](https://docs.snowflake.com/en/sql-reference/functions/policy_references) which requires a warehouse in the connection. Please, make sure you have either set a DEFAULT_WAREHOUSE for the user, or specified a warehouse in the configuration. +~> **Required warehouse** For this resource, the provider uses [policy references](https://docs.snowflake.com/en/sql-reference/functions/policy_references) which requires a warehouse in the connection. Please, make sure you have either set a DEFAULT_WAREHOUSE for the user, or specified a warehouse in the provider configuration. # snowflake_view (Resource) @@ -68,9 +68,9 @@ SQL ### Required -- `database` (String) The database in which to create the view. Due to technical limitations, don't use the following characters: `|` -- `name` (String) Specifies the identifier for the view; must be unique for the schema in which the view is created. Due to technical limitations, don't use the following characters: `|` -- `schema` (String) The schema in which to create the view. Due to technical limitations, don't use the following characters: `|` +- `database` (String) The database in which to create the view. +- `name` (String) Specifies the identifier for the view; must be unique for the schema in which the view 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 view. - `statement` (String) Specifies the query used to create the view. ### Optional diff --git a/docs/resources/warehouse.md b/docs/resources/warehouse.md index f4a9b55a8e..0417f034ca 100644 --- a/docs/resources/warehouse.md +++ b/docs/resources/warehouse.md @@ -26,7 +26,7 @@ resource "snowflake_warehouse" "warehouse" { ### Required -- `name` (String) Identifier for the virtual warehouse; must be unique for your account. +- `name` (String) Identifier for the virtual warehouse; must be unique for your account. 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 diff --git a/pkg/resources/account_role.go b/pkg/resources/account_role.go index 0626082501..25426282fb 100644 --- a/pkg/resources/account_role.go +++ b/pkg/resources/account_role.go @@ -21,6 +21,7 @@ var accountRoleSchema = map[string]*schema.Schema{ Required: true, // TODO(SNOW-999049): Uncomment once better identifier validation will be implemented // ValidateDiagFunc: IsValidIdentifier[sdk.AccountObjectIdentifier](), + Description: blocklistedCharactersFieldDescription("Identifier for the role; must be unique for your account."), }, "comment": { Type: schema.TypeString, diff --git a/pkg/resources/api_authentication_integration_common.go b/pkg/resources/api_authentication_integration_common.go index 66ad533649..5c4c19b6ac 100644 --- a/pkg/resources/api_authentication_integration_common.go +++ b/pkg/resources/api_authentication_integration_common.go @@ -16,7 +16,7 @@ var apiAuthCommonSchema = map[string]*schema.Schema{ Type: schema.TypeString, Required: true, ForceNew: true, - Description: "Specifies the identifier (i.e. name) for the integration. This value must be unique in your account.", + Description: blocklistedCharactersFieldDescription("Specifies the identifier (i.e. name) for the integration. This value must be unique in your account."), }, "enabled": { Type: schema.TypeBool, diff --git a/pkg/resources/common.go b/pkg/resources/common.go index e8e6d535b3..4524b05a27 100644 --- a/pkg/resources/common.go +++ b/pkg/resources/common.go @@ -1,14 +1,10 @@ package resources import ( - "context" - "fmt" - "log" "regexp" "strings" "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/helpers" - "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/sdk" "github.com/hashicorp/go-cty/cty" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" ) @@ -70,29 +66,3 @@ func ctyValToSliceString(valueElems []cty.Value) []string { } return elems } - -func ensureWarehouse(ctx context.Context, client *sdk.Client) (func(), error) { - warehouse, err := client.ContextFunctions.CurrentWarehouse(ctx) - if err != nil { - return nil, err - } - if len(warehouse) > 0 { - // everything is fine, return a no-op function to avoid checking by callers - return func() {}, nil - } - randomWarehouseName := fmt.Sprintf("terraform-provider-snowflake-%v", helpers.RandomString()) - log.Printf("[DEBUG] no current warehouse set, creating a temporary warehouse %s", randomWarehouseName) - wid := sdk.NewAccountObjectIdentifier(randomWarehouseName) - if err := client.Warehouses.Create(ctx, wid, nil); err != nil { - return nil, err - } - cleanup := func() { - if err := client.Warehouses.Drop(ctx, wid, nil); err != nil { - log.Printf("[WARN] error cleaning up temp warehouse %v", err) - } - } - if err := client.Sessions.UseWarehouse(ctx, wid); err != nil { - return cleanup, err - } - return cleanup, nil -} diff --git a/pkg/resources/database.go b/pkg/resources/database.go index 8090832e75..0e32b7e7fc 100644 --- a/pkg/resources/database.go +++ b/pkg/resources/database.go @@ -25,7 +25,7 @@ var databaseSchema = map[string]*schema.Schema{ "name": { Type: schema.TypeString, Required: true, - Description: "Specifies the identifier for the database; must be unique for your account. As a best practice for [Database Replication and Failover](https://docs.snowflake.com/en/user-guide/db-replication-intro), it is recommended to give each secondary database the same name as its primary database. This practice supports referencing fully-qualified objects (i.e. '..') by other objects in the same database, such as querying a fully-qualified table name in a view. If a secondary database has a different name from the primary database, then these object references would break in the secondary database.", + Description: blocklistedCharactersFieldDescription("Specifies the identifier for the database; must be unique for your account. As a best practice for [Database Replication and Failover](https://docs.snowflake.com/en/user-guide/db-replication-intro), it is recommended to give each secondary database the same name as its primary database. This practice supports referencing fully-qualified objects (i.e. '..') by other objects in the same database, such as querying a fully-qualified table name in a view. If a secondary database has a different name from the primary database, then these object references would break in the secondary database."), }, "drop_public_schema_on_creation": { Type: schema.TypeBool, diff --git a/pkg/resources/doc_helpers.go b/pkg/resources/doc_helpers.go index 92019aefd9..a7e8a278f1 100644 --- a/pkg/resources/doc_helpers.go +++ b/pkg/resources/doc_helpers.go @@ -33,6 +33,6 @@ func withPrivilegedRolesDescription(description, paramName string) string { return fmt.Sprintf(`%s By default, this list includes the ACCOUNTADMIN, ORGADMIN and SECURITYADMIN roles. To remove these privileged roles from the list, use the ALTER ACCOUNT command to set the %s account parameter to FALSE. `, description, paramName) } -func blacklistedCharactersFieldDescription(description string, blacklistedCharacters []rune) string { - return fmt.Sprintf(`%s Due to technical limitations, don't use the following characters: %s`, description, characterList(blacklistedCharacters)) +func blocklistedCharactersFieldDescription(description string) string { + return fmt.Sprintf(`%s 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: %s`, description, characterList([]rune{'|', '.', '(', ')', '"'})) } diff --git a/pkg/resources/external_oauth_integration.go b/pkg/resources/external_oauth_integration.go index 1b98e1f76d..162fc7b06d 100644 --- a/pkg/resources/external_oauth_integration.go +++ b/pkg/resources/external_oauth_integration.go @@ -26,7 +26,7 @@ var externalOauthIntegrationSchema = map[string]*schema.Schema{ Type: schema.TypeString, Required: true, ForceNew: true, - Description: "Specifies the name of the External Oath integration. This name follows the rules for Object Identifiers. The name should be unique among security integrations in your account.", + Description: blocklistedCharactersFieldDescription("Specifies the name of the External Oath integration. This name follows the rules for Object Identifiers. The name should be unique among security integrations in your account."), }, "external_oauth_type": { Type: schema.TypeString, diff --git a/pkg/resources/network_policy.go b/pkg/resources/network_policy.go index 381740912f..e4966554a2 100644 --- a/pkg/resources/network_policy.go +++ b/pkg/resources/network_policy.go @@ -21,7 +21,7 @@ var networkPolicySchema = map[string]*schema.Schema{ "name": { Type: schema.TypeString, Required: true, - Description: "Specifies the identifier for the network policy; must be unique for the account in which the network policy is created.", + Description: blocklistedCharactersFieldDescription("Specifies the identifier for the network policy; must be unique for the account in which the network policy is created."), }, "allowed_network_rule_list": { Type: schema.TypeSet, diff --git a/pkg/resources/oauth_integration_for_custom_clients.go b/pkg/resources/oauth_integration_for_custom_clients.go index c287b87f95..b2ef9bbc01 100644 --- a/pkg/resources/oauth_integration_for_custom_clients.go +++ b/pkg/resources/oauth_integration_for_custom_clients.go @@ -25,7 +25,7 @@ var oauthIntegrationForCustomClientsSchema = map[string]*schema.Schema{ Type: schema.TypeString, Required: true, ForceNew: true, - Description: "Specifies the name of the OAuth integration. This name follows the rules for Object Identifiers. The name should be unique among security integrations in your account.", + Description: blocklistedCharactersFieldDescription("Specifies the name of the OAuth integration. This name follows the rules for Object Identifiers. The name should be unique among security integrations in your account."), }, "oauth_client_type": { Type: schema.TypeString, diff --git a/pkg/resources/oauth_integration_for_partner_applications.go b/pkg/resources/oauth_integration_for_partner_applications.go index e419ecf6e6..8145cb331f 100644 --- a/pkg/resources/oauth_integration_for_partner_applications.go +++ b/pkg/resources/oauth_integration_for_partner_applications.go @@ -26,7 +26,7 @@ var oauthIntegrationForPartnerApplicationsSchema = map[string]*schema.Schema{ Type: schema.TypeString, Required: true, ForceNew: true, - Description: "Specifies the name of the OAuth integration. This name follows the rules for Object Identifiers. The name should be unique among security integrations in your account.", + Description: blocklistedCharactersFieldDescription("Specifies the name of the OAuth integration. This name follows the rules for Object Identifiers. The name should be unique among security integrations in your account."), }, "oauth_client": { Type: schema.TypeString, diff --git a/pkg/resources/saml2_integration.go b/pkg/resources/saml2_integration.go index 82a834d44e..2595cc4890 100644 --- a/pkg/resources/saml2_integration.go +++ b/pkg/resources/saml2_integration.go @@ -25,7 +25,7 @@ var saml2IntegrationSchema = map[string]*schema.Schema{ Type: schema.TypeString, Required: true, ForceNew: true, - Description: "Specifies the name of the SAML2 integration. This name follows the rules for Object Identifiers. The name should be unique among security integrations in your account.", + Description: blocklistedCharactersFieldDescription("Specifies the name of the SAML2 integration. This name follows the rules for Object Identifiers. The name should be unique among security integrations in your account."), }, "enabled": { Type: schema.TypeString, diff --git a/pkg/resources/scim_integration.go b/pkg/resources/scim_integration.go index 6fb91db044..df19ff348d 100644 --- a/pkg/resources/scim_integration.go +++ b/pkg/resources/scim_integration.go @@ -24,7 +24,7 @@ var scimIntegrationSchema = map[string]*schema.Schema{ Type: schema.TypeString, Required: true, ForceNew: true, - Description: "String that specifies the identifier (i.e. name) for the integration; must be unique in your account.", + Description: blocklistedCharactersFieldDescription("String that specifies the identifier (i.e. name) for the integration; must be unique in your account."), }, "enabled": { Type: schema.TypeBool, diff --git a/pkg/resources/secondary_database.go b/pkg/resources/secondary_database.go index 33ffcd4fec..6b294f2e34 100644 --- a/pkg/resources/secondary_database.go +++ b/pkg/resources/secondary_database.go @@ -18,7 +18,7 @@ var secondaryDatabaseSchema = map[string]*schema.Schema{ "name": { Type: schema.TypeString, Required: true, - Description: "Specifies the identifier for the database; must be unique for your account. As a best practice for [Database Replication and Failover](https://docs.snowflake.com/en/user-guide/db-replication-intro), it is recommended to give each secondary database the same name as its primary database. This practice supports referencing fully-qualified objects (i.e. '..') by other objects in the same database, such as querying a fully-qualified table name in a view. If a secondary database has a different name from the primary database, then these object references would break in the secondary database.", + Description: blocklistedCharactersFieldDescription("Specifies the identifier for the database; must be unique for your account. As a best practice for [Database Replication and Failover](https://docs.snowflake.com/en/user-guide/db-replication-intro), it is recommended to give each secondary database the same name as its primary database. This practice supports referencing fully-qualified objects (i.e. '..') by other objects in the same database, such as querying a fully-qualified table name in a view. If a secondary database has a different name from the primary database, then these object references would break in the secondary database."), }, "as_replica_of": { Type: schema.TypeString, diff --git a/pkg/resources/shared_database.go b/pkg/resources/shared_database.go index b3d3f8e72b..9e1b19a4d7 100644 --- a/pkg/resources/shared_database.go +++ b/pkg/resources/shared_database.go @@ -18,7 +18,7 @@ var sharedDatabaseSchema = map[string]*schema.Schema{ "name": { Type: schema.TypeString, Required: true, - Description: "Specifies the identifier for the database; must be unique for your account.", + Description: blocklistedCharactersFieldDescription("Specifies the identifier for the database; must be unique for your account."), }, "from_share": { Type: schema.TypeString, diff --git a/pkg/resources/view.go b/pkg/resources/view.go index d8b4c0170d..8724e1470e 100644 --- a/pkg/resources/view.go +++ b/pkg/resources/view.go @@ -24,20 +24,20 @@ var viewSchema = map[string]*schema.Schema{ "name": { Type: schema.TypeString, Required: true, - Description: blacklistedCharactersFieldDescription("Specifies the identifier for the view; must be unique for the schema in which the view is created.", []rune{'|'}), + Description: blocklistedCharactersFieldDescription("Specifies the identifier for the view; must be unique for the schema in which the view is created."), DiffSuppressFunc: suppressIdentifierQuoting, }, "database": { Type: schema.TypeString, Required: true, - Description: blacklistedCharactersFieldDescription("The database in which to create the view.", []rune{'|'}), + Description: "The database in which to create the view.", ForceNew: true, DiffSuppressFunc: suppressIdentifierQuoting, }, "schema": { Type: schema.TypeString, Required: true, - Description: blacklistedCharactersFieldDescription("The schema in which to create the view.", []rune{'|'}), + Description: "The schema in which to create the view.", ForceNew: true, DiffSuppressFunc: suppressIdentifierQuoting, }, diff --git a/pkg/resources/warehouse.go b/pkg/resources/warehouse.go index d054d26c08..147ace18e9 100644 --- a/pkg/resources/warehouse.go +++ b/pkg/resources/warehouse.go @@ -23,7 +23,7 @@ var warehouseSchema = map[string]*schema.Schema{ "name": { Type: schema.TypeString, Required: true, - Description: "Identifier for the virtual warehouse; must be unique for your account.", + Description: blocklistedCharactersFieldDescription("Identifier for the virtual warehouse; must be unique for your account."), }, "warehouse_type": { Type: schema.TypeString, diff --git a/templates/resources/view.md.tmpl b/templates/resources/view.md.tmpl index 67a79c7eb7..368bbfc15d 100644 --- a/templates/resources/view.md.tmpl +++ b/templates/resources/view.md.tmpl @@ -11,9 +11,9 @@ description: |- !> **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#v094x--v0950) to use it. -!> **Note about copy_grants** Fields like `is_recursive`, `is_temporary`, `copy_grants` and `statement` can not be altered, and a change means recreation of the resource. ForceNew can not be used because it does not preserve grants from `copy_grants`. Beware that even though a change is marked as update, the resource is recreated. +!> **Note about copy_grants** Fields like `is_recursive`, `is_temporary`, `copy_grants` and `statement` can not be ALTERed on Snowflake side (check [docs](https://docs.snowflake.com/en/sql-reference/sql/alter-view)), and a change means recreation of the resource. ForceNew can not be used because it does not preserve grants from `copy_grants`. Beware that even though a change is marked as update, the resource is recreated. -~> **Required warehouse** For this resource, the provider uses [policy references](https://docs.snowflake.com/en/sql-reference/functions/policy_references) which requires a warehouse in the connection. Please, make sure you have either set a DEFAULT_WAREHOUSE for the user, or specified a warehouse in the configuration. +~> **Required warehouse** For this resource, the provider uses [policy references](https://docs.snowflake.com/en/sql-reference/functions/policy_references) which requires a warehouse in the connection. Please, make sure you have either set a DEFAULT_WAREHOUSE for the user, or specified a warehouse in the provider configuration. # {{.Name}} ({{.Type}}) From 7488d9d72713b7aeb3416577f022267b0386d692 Mon Sep 17 00:00:00 2001 From: Jakub Michalak Date: Fri, 23 Aug 2024 10:42:30 +0200 Subject: [PATCH 08/14] Fix docs --- docs/resources/view.md | 3 --- 1 file changed, 3 deletions(-) diff --git a/docs/resources/view.md b/docs/resources/view.md index 97412f5a1b..dc34c5ebc6 100644 --- a/docs/resources/view.md +++ b/docs/resources/view.md @@ -63,9 +63,6 @@ SQL -> **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). --> **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 From 54e85ddd3b9e05448468fd1f1d5e4ba3b8752653 Mon Sep 17 00:00:00 2001 From: Jakub Michalak Date: Mon, 26 Aug 2024 17:36:26 +0200 Subject: [PATCH 09/14] Fix tests --- pkg/acceptance/bettertestspoc/README.md | 2 +- .../bettertestspoc/config/config.go | 20 ++- .../config/model/view_model_ext.go | 6 + .../testdata/TestAcc_View/complete/test.tf | 8 +- .../TestAcc_View/complete/variables.tf | 4 + pkg/resources/view.go | 2 +- pkg/resources/view_acceptance_test.go | 136 ++++++++---------- 7 files changed, 93 insertions(+), 85 deletions(-) create mode 100644 pkg/acceptance/bettertestspoc/config/model/view_model_ext.go diff --git a/pkg/acceptance/bettertestspoc/README.md b/pkg/acceptance/bettertestspoc/README.md index e17e502334..ed10d8eda1 100644 --- a/pkg/acceptance/bettertestspoc/README.md +++ b/pkg/acceptance/bettertestspoc/README.md @@ -300,7 +300,7 @@ it will result in: - Verify if all the config types are supported. - Consider a better implementation for the model conversion to config (TODO left in `config/config.go`). - Support additional methods for references in models (TODO left in `config/config.go`). -- Support depends_on in models (TODO left in `config/config.go`). +- Support depends_on in models so that it can be chained like other resource fields (TODO left in `config/config.go`). - Add a convenience function to concatenate multiple models (TODO left in `config/config.go`). - Add function to support using `ConfigFile:` in the acceptance tests (TODO left in `config/config.go`). - Replace `acceptance/snowflakechecks` with the new proposed Snowflake objects assertions. diff --git a/pkg/acceptance/bettertestspoc/config/config.go b/pkg/acceptance/bettertestspoc/config/config.go index 1119f4d0bc..c1dea78d7e 100644 --- a/pkg/acceptance/bettertestspoc/config/config.go +++ b/pkg/acceptance/bettertestspoc/config/config.go @@ -11,7 +11,7 @@ import ( ) // TODO [SNOW-1501905]: add possibility to have reference to another object (e.g. WithResourceMonitorReference); new config.Variable impl? -// TODO [SNOW-1501905]: add possibility to have depends_on to other resources (in meta?) +// TODO [SNOW-1501905]: generate With/SetDependsOn for the resources to preserve builder pattern // TODO [SNOW-1501905]: add a convenience method to use multiple configs from multiple models // ResourceModel is the base interface all of our config models will implement. @@ -20,11 +20,14 @@ type ResourceModel interface { Resource() resources.Resource ResourceName() string SetResourceName(name string) + DependsOn() []string + SetDependsOn(values []string) } type ResourceModelMeta struct { - name string - resource resources.Resource + name string + resource resources.Resource + dependsOn []string } func (m *ResourceModelMeta) Resource() resources.Resource { @@ -39,6 +42,14 @@ func (m *ResourceModelMeta) SetResourceName(name string) { m.name = name } +func (m *ResourceModelMeta) DependsOn() []string { + return m.dependsOn +} + +func (m *ResourceModelMeta) SetDependsOn(values []string) { + m.dependsOn = values +} + // DefaultResourceName is exported to allow assertions against the resources using the default name. const DefaultResourceName = "test" @@ -70,6 +81,9 @@ func FromModel(t *testing.T, model ResourceModel) string { for k, v := range objMap { sb.WriteString(fmt.Sprintf("\t%s = %s\n", k, v)) } + if len(model.DependsOn()) > 0 { + sb.WriteString(fmt.Sprintf("\tdepends_on = [%s]\n", strings.Join(model.DependsOn(), ", "))) + } sb.WriteString(`}`) sb.WriteRune('\n') s := sb.String() diff --git a/pkg/acceptance/bettertestspoc/config/model/view_model_ext.go b/pkg/acceptance/bettertestspoc/config/model/view_model_ext.go new file mode 100644 index 0000000000..f37cde3e33 --- /dev/null +++ b/pkg/acceptance/bettertestspoc/config/model/view_model_ext.go @@ -0,0 +1,6 @@ +package model + +func (v *ViewModel) WithDependsOn(values []string) *ViewModel { + v.SetDependsOn(values) + return v +} diff --git a/pkg/resources/testdata/TestAcc_View/complete/test.tf b/pkg/resources/testdata/TestAcc_View/complete/test.tf index 0af4ad36b3..45a4a42eb0 100644 --- a/pkg/resources/testdata/TestAcc_View/complete/test.tf +++ b/pkg/resources/testdata/TestAcc_View/complete/test.tf @@ -17,5 +17,11 @@ resource "snowflake_view" "test" { policy_name = var.aggregation_policy entity_key = var.aggregation_policy_entity_key } - statement = var.statement + statement = var.statement + depends_on = [snowflake_unsafe_execute.use_warehouse] +} + +resource "snowflake_unsafe_execute" "use_warehouse" { + execute = "USE WAREHOUSE \"${var.warehouse}\"" + revert = "SELECT 1" } diff --git a/pkg/resources/testdata/TestAcc_View/complete/variables.tf b/pkg/resources/testdata/TestAcc_View/complete/variables.tf index 6fe755f654..4423777db3 100644 --- a/pkg/resources/testdata/TestAcc_View/complete/variables.tf +++ b/pkg/resources/testdata/TestAcc_View/complete/variables.tf @@ -53,3 +53,7 @@ variable "aggregation_policy_entity_key" { variable "statement" { type = string } + +variable "warehouse" { + type = string +} diff --git a/pkg/resources/view.go b/pkg/resources/view.go index 8724e1470e..a035e8394b 100644 --- a/pkg/resources/view.go +++ b/pkg/resources/view.go @@ -523,7 +523,7 @@ func ReadView(withExternalChangesMarking bool) schema.ReadContextFunc { func handlePolicyReferences(ctx context.Context, client *sdk.Client, id sdk.SchemaObjectIdentifier, d *schema.ResourceData) error { policyRefs, err := client.PolicyReferences.GetForEntity(ctx, sdk.NewGetForEntityPolicyReferenceRequest(id, sdk.PolicyEntityDomainView)) if err != nil { - return err + return fmt.Errorf("getting policy references for view: %v", err) } var aggregationPolicies []map[string]any var rowAccessPolicies []map[string]any diff --git a/pkg/resources/view_acceptance_test.go b/pkg/resources/view_acceptance_test.go index f9a3955bec..612bd43959 100644 --- a/pkg/resources/view_acceptance_test.go +++ b/pkg/resources/view_acceptance_test.go @@ -24,6 +24,7 @@ import ( "github.com/hashicorp/terraform-plugin-testing/tfversion" ) +// TODO(SNOW-1423486): Fix using warehouse in all tests. func TestAcc_View_basic(t *testing.T) { _ = testenvs.GetOrSkipTest(t, testenvs.EnableAcceptance) acc.TestAccPreCheck(t) @@ -46,6 +47,7 @@ func TestAcc_View_basic(t *testing.T) { comment := "Terraform test resource'" viewModel := model.View("test", id.DatabaseName(), id.Name(), id.SchemaName(), statement) + viewModelWithDependency := model.View("test", id.DatabaseName(), id.Name(), id.SchemaName(), statement).WithDependsOn([]string{"snowflake_unsafe_execute.use_warehouse"}) // generators currently don't handle lists, so use the old way basicUpdate := func(rap, ap sdk.SchemaObjectIdentifier, statement string) config.Variables { @@ -71,7 +73,7 @@ func TestAcc_View_basic(t *testing.T) { Steps: []resource.TestStep{ // without optionals { - Config: accconfig.FromModel(t, viewModel), + Config: accconfig.FromModel(t, viewModelWithDependency) + useWarehouseConfig(acc.TestWarehouseName), Check: assert.AssertThat(t, resourceassert.ViewResource(t, "snowflake_view.test"). HasNameString(id.Name()). HasStatementString(statement). @@ -284,7 +286,8 @@ func TestAcc_View_recursive(t *testing.T) { acc.TestAccPreCheck(t) id := acc.TestClient().Ids.RandomSchemaObjectIdentifier() statement := "SELECT ROLE_NAME, ROLE_OWNER FROM INFORMATION_SCHEMA.APPLICABLE_ROLES" - viewModel := model.View("test", id.DatabaseName(), id.Name(), id.SchemaName(), statement) + viewModel := model.View("test", id.DatabaseName(), id.Name(), id.SchemaName(), statement).WithDependsOn([]string{"snowflake_unsafe_execute.use_warehouse"}) + resource.Test(t, resource.TestCase{ ProtoV6ProviderFactories: acc.TestAccProtoV6ProviderFactories, TerraformVersionChecks: []tfversion.TerraformVersionCheck{ @@ -293,7 +296,7 @@ func TestAcc_View_recursive(t *testing.T) { CheckDestroy: acc.CheckDestroy(t, resources.View), Steps: []resource.TestStep{ { - Config: accconfig.FromModel(t, viewModel.WithIsRecursive("true")), + Config: accconfig.FromModel(t, viewModel.WithIsRecursive("true")) + useWarehouseConfig(acc.TestWarehouseName), Check: assert.AssertThat(t, resourceassert.ViewResource(t, "snowflake_view.test"). HasNameString(id.Name()). HasStatementString(statement). @@ -302,7 +305,7 @@ func TestAcc_View_recursive(t *testing.T) { HasIsRecursiveString("true")), }, { - Config: accconfig.FromModel(t, viewModel.WithIsRecursive("true")), + Config: accconfig.FromModel(t, viewModel.WithIsRecursive("true")) + useWarehouseConfig(acc.TestWarehouseName), ResourceName: "snowflake_view.test", ImportState: true, ImportStateCheck: assert.AssertThatImport(t, assert.CheckImport(importchecks.TestCheckResourceAttrInstanceState(helpers.EncodeSnowflakeID(id), "name", id.Name())), @@ -319,10 +322,12 @@ func TestAcc_View_recursive(t *testing.T) { func TestAcc_View_temporary(t *testing.T) { _ = testenvs.GetOrSkipTest(t, testenvs.EnableAcceptance) + // we use one configured client, so a temporary view should be visible after creation + _ = testenvs.GetOrSkipTest(t, testenvs.ConfigureClientOnce) acc.TestAccPreCheck(t) id := acc.TestClient().Ids.RandomSchemaObjectIdentifier() statement := "SELECT ROLE_NAME, ROLE_OWNER FROM INFORMATION_SCHEMA.APPLICABLE_ROLES" - viewModel := model.View("test", id.DatabaseName(), id.Name(), id.SchemaName(), statement) + viewModel := model.View("test", id.DatabaseName(), id.Name(), id.SchemaName(), statement).WithDependsOn([]string{"snowflake_unsafe_execute.use_warehouse"}) resource.Test(t, resource.TestCase{ ProtoV6ProviderFactories: acc.TestAccProtoV6ProviderFactories, TerraformVersionChecks: []tfversion.TerraformVersionCheck{ @@ -331,13 +336,7 @@ func TestAcc_View_temporary(t *testing.T) { CheckDestroy: acc.CheckDestroy(t, resources.View), Steps: []resource.TestStep{ { - Config: accconfig.FromModel(t, viewModel.WithIsTemporary("true")), - ExpectNonEmptyPlan: true, - ConfigPlanChecks: resource.ConfigPlanChecks{ - PostApplyPostRefresh: []plancheck.PlanCheck{ - plancheck.ExpectResourceAction("snowflake_view.test", plancheck.ResourceActionCreate), - }, - }, + Config: accconfig.FromModel(t, viewModel.WithIsTemporary("true")) + useWarehouseConfig(acc.TestWarehouseName), Check: assert.AssertThat(t, resourceassert.ViewResource(t, "snowflake_view.test"). HasNameString(id.Name()). HasStatementString(statement). @@ -379,6 +378,7 @@ func TestAcc_View_complete(t *testing.T) { "aggregation_policy": config.StringVariable(aggregationPolicy.FullyQualifiedName()), "aggregation_policy_entity_key": config.ListVariable(config.StringVariable("ID")), "statement": config.StringVariable(statement), + "warehouse": config.StringVariable(acc.TestWarehouseName), } } resource.Test(t, resource.TestCase{ @@ -449,7 +449,9 @@ func TestAcc_View_Rename(t *testing.T) { statement := "SELECT ROLE_NAME, ROLE_OWNER FROM INFORMATION_SCHEMA.APPLICABLE_ROLES" id := acc.TestClient().Ids.RandomSchemaObjectIdentifier() newId := acc.TestClient().Ids.RandomSchemaObjectIdentifier() - viewModel := model.View("test", id.DatabaseName(), id.Name(), id.SchemaName(), statement).WithComment("foo") + viewModel := model.View("test", id.DatabaseName(), id.Name(), id.SchemaName(), statement).WithComment("foo").WithDependsOn([]string{"snowflake_unsafe_execute.use_warehouse"}) + newViewModel := model.View("test", newId.DatabaseName(), newId.Name(), newId.SchemaName(), statement).WithComment("foo") + resource.Test(t, resource.TestCase{ ProtoV6ProviderFactories: acc.TestAccProtoV6ProviderFactories, PreCheck: func() { acc.TestAccPreCheck(t) }, @@ -459,7 +461,7 @@ func TestAcc_View_Rename(t *testing.T) { CheckDestroy: acc.CheckDestroy(t, resources.View), Steps: []resource.TestStep{ { - Config: accconfig.FromModel(t, viewModel), + Config: accconfig.FromModel(t, viewModel) + useWarehouseConfig(acc.TestWarehouseName), Check: resource.ComposeAggregateTestCheckFunc( resource.TestCheckResourceAttr("snowflake_view.test", "name", id.Name()), resource.TestCheckResourceAttr("snowflake_view.test", "comment", "foo"), @@ -468,7 +470,7 @@ func TestAcc_View_Rename(t *testing.T) { }, // rename with one param changed { - Config: accconfig.FromModel(t, model.View("test", newId.DatabaseName(), newId.Name(), newId.SchemaName(), statement).WithComment("foo")), + Config: accconfig.FromModel(t, newViewModel), ConfigPlanChecks: resource.ConfigPlanChecks{ PreApply: []plancheck.PlanCheck{ plancheck.ExpectResourceAction("snowflake_view.test", plancheck.ResourceActionUpdate), @@ -488,7 +490,8 @@ func TestAcc_ViewChangeCopyGrants(t *testing.T) { id := acc.TestClient().Ids.RandomSchemaObjectIdentifier() statement := "SELECT ROLE_NAME, ROLE_OWNER FROM INFORMATION_SCHEMA.APPLICABLE_ROLES" - viewModel := model.View("test", id.DatabaseName(), id.Name(), id.SchemaName(), statement).WithIsSecure("true").WithOrReplace(false).WithCopyGrants(false) + viewModel := model.View("test", id.DatabaseName(), id.Name(), id.SchemaName(), statement).WithIsSecure("true").WithOrReplace(false).WithCopyGrants(false). + WithDependsOn([]string{"snowflake_unsafe_execute.use_warehouse"}) var createdOn string @@ -501,7 +504,7 @@ func TestAcc_ViewChangeCopyGrants(t *testing.T) { CheckDestroy: acc.CheckDestroy(t, resources.View), Steps: []resource.TestStep{ { - Config: accconfig.FromModel(t, viewModel), + Config: accconfig.FromModel(t, viewModel) + useWarehouseConfig(acc.TestWarehouseName), Check: resource.ComposeAggregateTestCheckFunc( resource.TestCheckResourceAttr("snowflake_view.test", "name", id.Name()), resource.TestCheckResourceAttr("snowflake_view.test", "database", id.DatabaseName()), @@ -516,7 +519,7 @@ func TestAcc_ViewChangeCopyGrants(t *testing.T) { }, // Checks that copy_grants changes don't trigger a drop { - Config: accconfig.FromModel(t, viewModel.WithCopyGrants(true).WithOrReplace(true)), + Config: accconfig.FromModel(t, viewModel.WithCopyGrants(true).WithOrReplace(true)) + useWarehouseConfig(acc.TestWarehouseName), Check: resource.ComposeAggregateTestCheckFunc( resource.TestCheckResourceAttr("snowflake_view.test", "show_output.#", "1"), resource.TestCheckResourceAttrWith("snowflake_view.test", "show_output.0.created_on", func(value string) error { @@ -536,7 +539,8 @@ func TestAcc_ViewChangeCopyGrantsReversed(t *testing.T) { id := acc.TestClient().Ids.RandomSchemaObjectIdentifier() statement := "SELECT ROLE_NAME, ROLE_OWNER FROM INFORMATION_SCHEMA.APPLICABLE_ROLES" - viewModel := model.View("test", id.DatabaseName(), id.Name(), id.SchemaName(), statement).WithIsSecure("true").WithOrReplace(true).WithCopyGrants(true) + viewModel := model.View("test", id.DatabaseName(), id.Name(), id.SchemaName(), statement).WithIsSecure("true").WithOrReplace(true).WithCopyGrants(true). + WithDependsOn([]string{"snowflake_unsafe_execute.use_warehouse"}) var createdOn string @@ -549,7 +553,7 @@ func TestAcc_ViewChangeCopyGrantsReversed(t *testing.T) { CheckDestroy: acc.CheckDestroy(t, resources.View), Steps: []resource.TestStep{ { - Config: accconfig.FromModel(t, viewModel), + Config: accconfig.FromModel(t, viewModel) + useWarehouseConfig(acc.TestWarehouseName), Check: resource.ComposeAggregateTestCheckFunc( resource.TestCheckResourceAttr("snowflake_view.test", "copy_grants", "true"), resource.TestCheckResourceAttr("snowflake_view.test", "show_output.#", "1"), @@ -561,7 +565,7 @@ func TestAcc_ViewChangeCopyGrantsReversed(t *testing.T) { ), }, { - Config: accconfig.FromModel(t, viewModel.WithCopyGrants(false)), + Config: accconfig.FromModel(t, viewModel.WithCopyGrants(false)) + useWarehouseConfig(acc.TestWarehouseName), Check: resource.ComposeAggregateTestCheckFunc( resource.TestCheckResourceAttr("snowflake_view.test", "show_output.#", "1"), resource.TestCheckResourceAttrWith("snowflake_view.test", "show_output.0.created_on", func(value string) error { @@ -578,8 +582,9 @@ func TestAcc_ViewChangeCopyGrantsReversed(t *testing.T) { } func TestAcc_ViewCopyGrantsStatementUpdate(t *testing.T) { - tableName := acc.TestClient().Ids.Alpha() - viewName := acc.TestClient().Ids.Alpha() + _ = testenvs.GetOrSkipTest(t, testenvs.EnableAcceptance) + tableId := acc.TestClient().Ids.RandomSchemaObjectIdentifier() + viewId := acc.TestClient().Ids.RandomSchemaObjectIdentifier() resource.Test(t, resource.TestCase{ ProtoV6ProviderFactories: acc.TestAccProtoV6ProviderFactories, @@ -590,7 +595,7 @@ func TestAcc_ViewCopyGrantsStatementUpdate(t *testing.T) { CheckDestroy: acc.CheckDestroy(t, resources.View), Steps: []resource.TestStep{ { - Config: viewConfigWithGrants(acc.TestDatabaseName, acc.TestSchemaName, tableName, viewName, `\"name\"`), + Config: viewConfigWithGrants(viewId, tableId, `\"name\"`) + useWarehouseConfig(acc.TestWarehouseName), 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"), @@ -598,7 +603,7 @@ func TestAcc_ViewCopyGrantsStatementUpdate(t *testing.T) { ), }, { - Config: viewConfigWithGrants(acc.TestDatabaseName, acc.TestSchemaName, tableName, viewName, "*"), + Config: viewConfigWithGrants(viewId, tableId, "*") + useWarehouseConfig(acc.TestWarehouseName), Check: resource.ComposeAggregateTestCheckFunc( resource.TestCheckResourceAttr("data.snowflake_grants.grants", "grants.#", "2"), resource.TestCheckResourceAttr("data.snowflake_grants.grants", "grants.1.privilege", "SELECT"), @@ -609,9 +614,9 @@ func TestAcc_ViewCopyGrantsStatementUpdate(t *testing.T) { } func TestAcc_View_copyGrants(t *testing.T) { - accName := acc.TestClient().Ids.Alpha() - query := "SELECT ROLE_NAME, ROLE_OWNER FROM INFORMATION_SCHEMA.APPLICABLE_ROLES" - + id := acc.TestClient().Ids.RandomSchemaObjectIdentifier() + statement := "SELECT ROLE_NAME, ROLE_OWNER FROM INFORMATION_SCHEMA.APPLICABLE_ROLES" + viewModel := model.View("test", id.DatabaseName(), id.Name(), id.SchemaName(), statement).WithDependsOn([]string{"snowflake_unsafe_execute.use_warehouse"}) resource.Test(t, resource.TestCase{ ProtoV6ProviderFactories: acc.TestAccProtoV6ProviderFactories, PreCheck: func() { acc.TestAccPreCheck(t) }, @@ -621,19 +626,19 @@ func TestAcc_View_copyGrants(t *testing.T) { CheckDestroy: acc.CheckDestroy(t, resources.View), Steps: []resource.TestStep{ { - Config: viewConfigWithCopyGrants(acc.TestDatabaseName, acc.TestSchemaName, accName, query, true), + Config: accconfig.FromModel(t, viewModel.WithCopyGrants(true)) + useWarehouseConfig(acc.TestWarehouseName), ExpectError: regexp.MustCompile("all of `copy_grants,or_replace` must be specified"), }, { - Config: viewConfigWithCopyGrantsAndOrReplace(acc.TestDatabaseName, acc.TestSchemaName, accName, query, true, true), + Config: accconfig.FromModel(t, viewModel.WithCopyGrants(true).WithOrReplace(true)) + useWarehouseConfig(acc.TestWarehouseName), Check: resource.ComposeAggregateTestCheckFunc( - resource.TestCheckResourceAttr("snowflake_view.test", "name", accName), + resource.TestCheckResourceAttr("snowflake_view.test", "name", id.Name()), ), }, { - Config: viewConfigWithOrReplace(acc.TestDatabaseName, acc.TestSchemaName, accName, query, true), + Config: accconfig.FromModel(t, viewModel.WithCopyGrants(false).WithOrReplace(true)) + useWarehouseConfig(acc.TestWarehouseName), Check: resource.ComposeAggregateTestCheckFunc( - resource.TestCheckResourceAttr("snowflake_view.test", "name", accName), + resource.TestCheckResourceAttr("snowflake_view.test", "name", id.Name()), ), }, }, @@ -656,7 +661,7 @@ func TestAcc_View_Issue2640(t *testing.T) { CheckDestroy: acc.CheckDestroy(t, resources.View), Steps: []resource.TestStep{ { - Config: viewConfigWithMultilineUnionStatement(acc.TestDatabaseName, acc.TestSchemaName, id.Name(), part1, part2), + Config: viewConfigWithMultilineUnionStatement(id, part1, part2) + useWarehouseConfig(acc.TestWarehouseName), Check: resource.ComposeAggregateTestCheckFunc( resource.TestCheckResourceAttr("snowflake_view.test", "name", id.Name()), resource.TestCheckResourceAttr("snowflake_view.test", "statement", statement), @@ -700,7 +705,7 @@ func TestAcc_view_migrateFromVersion_0_94_1(t *testing.T) { id := acc.TestClient().Ids.RandomSchemaObjectIdentifier() resourceName := "snowflake_view.test" statement := "SELECT ROLE_NAME, ROLE_OWNER FROM INFORMATION_SCHEMA.APPLICABLE_ROLES" - viewModel := model.View("test", id.DatabaseName(), id.Name(), id.SchemaName(), statement) + viewModel := model.View("test", id.DatabaseName(), id.Name(), id.SchemaName(), statement).WithDependsOn([]string{"snowflake_unsafe_execute.use_warehouse"}) tag, tagCleanup := acc.TestClient().Tag.CreateTag(t) t.Cleanup(tagCleanup) @@ -728,7 +733,7 @@ func TestAcc_view_migrateFromVersion_0_94_1(t *testing.T) { }, { ProtoV6ProviderFactories: acc.TestAccProtoV6ProviderFactories, - Config: accconfig.FromModel(t, viewModel), + Config: accconfig.FromModel(t, viewModel) + useWarehouseConfig(acc.TestWarehouseName), Check: resource.ComposeAggregateTestCheckFunc( resource.TestCheckResourceAttr(resourceName, "name", id.Name()), resource.TestCheckNoResourceAttr(resourceName, "tag.#"), @@ -738,6 +743,15 @@ func TestAcc_view_migrateFromVersion_0_94_1(t *testing.T) { }) } +func useWarehouseConfig(name string) string { + return fmt.Sprintf(` +resource "snowflake_unsafe_execute" "use_warehouse" { + execute = "USE WAREHOUSE \"%s\"" + revert = "SELECT 1" +} +`, name) +} + func viewv_0_94_1_WithTags(id sdk.SchemaObjectIdentifier, tagSchema, tagName, tagValue, statement string) string { s := ` resource "snowflake_view" "test" { @@ -756,7 +770,7 @@ resource "snowflake_view" "test" { return fmt.Sprintf(s, id.Name(), id.DatabaseName(), tagSchema, tagName, tagValue, id.SchemaName(), statement) } -func viewConfigWithGrants(databaseName string, schemaName string, tableName string, viewName string, selectStatement string) string { +func viewConfigWithGrants(viewId, tableId sdk.SchemaObjectIdentifier, selectStatement string) string { return fmt.Sprintf(` resource "snowflake_table" "table" { database = "%[1]s" @@ -770,7 +784,6 @@ resource "snowflake_table" "table" { } resource "snowflake_view" "test" { - depends_on = [snowflake_table.table] name = "%[4]s" comment = "created by terraform" database = "%[1]s" @@ -779,6 +792,7 @@ resource "snowflake_view" "test" { or_replace = true copy_grants = true is_secure = true + depends_on = [snowflake_unsafe_execute.use_warehouse, snowflake_table.table] } resource "snowflake_account_role" "test" { @@ -795,53 +809,16 @@ resource "snowflake_grant_privileges_to_account_role" "grant" { } data "snowflake_grants" "grants" { - depends_on = [snowflake_grant_privileges_to_account_role.grant, snowflake_view.test] + depends_on = [snowflake_grant_privileges_to_account_role.grant, snowflake_view.test, snowflake_unsafe_execute.use_warehouse] grants_on { object_name = "\"%[1]s\".\"%[2]s\".\"${snowflake_view.test.name}\"" object_type = "VIEW" } } - `, databaseName, schemaName, tableName, viewName, selectStatement) -} - -func viewConfigWithCopyGrants(databaseName string, schemaName string, name string, selectStatement string, copyGrants bool) string { - return fmt.Sprintf(` -resource "snowflake_view" "test" { - name = "%[3]s" - database = "%[1]s" - schema = "%[2]s" - statement = "%[4]s" - copy_grants = %[5]t -} - `, databaseName, schemaName, name, selectStatement, copyGrants) -} - -func viewConfigWithCopyGrantsAndOrReplace(databaseName string, schemaName string, name string, selectStatement string, copyGrants bool, orReplace bool) string { - return fmt.Sprintf(` -resource "snowflake_view" "test" { - name = "%[3]s" - database = "%[1]s" - schema = "%[2]s" - statement = "%[4]s" - copy_grants = %[5]t - or_replace = %[6]t -} - `, databaseName, schemaName, name, selectStatement, copyGrants, orReplace) -} - -func viewConfigWithOrReplace(databaseName string, schemaName string, name string, selectStatement string, orReplace bool) string { - return fmt.Sprintf(` -resource "snowflake_view" "test" { - name = "%[3]s" - database = "%[1]s" - schema = "%[2]s" - statement = "%[4]s" - or_replace = %[5]t -} - `, databaseName, schemaName, name, selectStatement, orReplace) + `, viewId.DatabaseName(), viewId.SchemaName(), tableId.Name(), viewId.Name(), selectStatement) } -func viewConfigWithMultilineUnionStatement(databaseName string, schemaName string, name string, part1 string, part2 string) string { +func viewConfigWithMultilineUnionStatement(id sdk.SchemaObjectIdentifier, part1 string, part2 string) string { return fmt.Sprintf(` resource "snowflake_view" "test" { name = "%[3]s" @@ -853,6 +830,7 @@ resource "snowflake_view" "test" { %[5]s SQL is_secure = true + depends_on = [snowflake_unsafe_execute.use_warehouse] } - `, databaseName, schemaName, name, part1, part2) + `, id.DatabaseName(), id.SchemaName(), id.Name(), part1, part2) } From 08c509378636ccad8008fa433ebb218fd9b900d1 Mon Sep 17 00:00:00 2001 From: Jakub Michalak Date: Tue, 27 Aug 2024 10:10:40 +0200 Subject: [PATCH 10/14] Fix tests --- pkg/acceptance/helpers/aggregation_policy_client.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/acceptance/helpers/aggregation_policy_client.go b/pkg/acceptance/helpers/aggregation_policy_client.go index bac7d8714e..85f63fdb38 100644 --- a/pkg/acceptance/helpers/aggregation_policy_client.go +++ b/pkg/acceptance/helpers/aggregation_policy_client.go @@ -41,7 +41,7 @@ func (c *AggregationPolicyClient) DropAggregationPolicyFunc(t *testing.T, id sdk ctx := context.Background() return func() { - _, err := c.client().ExecForTests(ctx, fmt.Sprintf(`DROP AGGREGATION POLICY IF EXISTS %s`, id.Name())) + _, err := c.client().ExecForTests(ctx, fmt.Sprintf(`DROP AGGREGATION POLICY IF EXISTS %s`, id.FullyQualifiedName())) require.NoError(t, err) } } From f316ff8e9d76a4bab24e1973696dfcd8f4be12db Mon Sep 17 00:00:00 2001 From: Jakub Michalak Date: Tue, 27 Aug 2024 11:16:07 +0200 Subject: [PATCH 11/14] Fix tests --- pkg/datasources/views_acceptance_test.go | 34 +++++++++++------------- 1 file changed, 15 insertions(+), 19 deletions(-) diff --git a/pkg/datasources/views_acceptance_test.go b/pkg/datasources/views_acceptance_test.go index 46b65564fb..3326d13aab 100644 --- a/pkg/datasources/views_acceptance_test.go +++ b/pkg/datasources/views_acceptance_test.go @@ -5,15 +5,15 @@ import ( "testing" acc "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/acceptance" + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/sdk" "github.com/hashicorp/terraform-plugin-testing/helper/resource" "github.com/hashicorp/terraform-plugin-testing/tfversion" ) +// TODO(SNOW-1423486): Fix using warehouse in all tests. func TestAcc_Views(t *testing.T) { - databaseName := acc.TestClient().Ids.Alpha() - schemaName := acc.TestClient().Ids.Alpha() - viewName := acc.TestClient().Ids.Alpha() + viewId := acc.TestClient().Ids.RandomSchemaObjectIdentifier() resource.Test(t, resource.TestCase{ ProtoV6ProviderFactories: acc.TestAccProtoV6ProviderFactories, PreCheck: func() { acc.TestAccPreCheck(t) }, @@ -23,36 +23,32 @@ func TestAcc_Views(t *testing.T) { CheckDestroy: nil, Steps: []resource.TestStep{ { - Config: views(databaseName, schemaName, viewName), + Config: views(viewId), Check: resource.ComposeTestCheckFunc( - resource.TestCheckResourceAttr("data.snowflake_views.v", "database", databaseName), - resource.TestCheckResourceAttr("data.snowflake_views.v", "schema", schemaName), + resource.TestCheckResourceAttr("data.snowflake_views.v", "database", viewId.DatabaseName()), + resource.TestCheckResourceAttr("data.snowflake_views.v", "schema", viewId.SchemaName()), resource.TestCheckResourceAttrSet("data.snowflake_views.v", "views.#"), resource.TestCheckResourceAttr("data.snowflake_views.v", "views.#", "1"), - resource.TestCheckResourceAttr("data.snowflake_views.v", "views.0.name", viewName), + resource.TestCheckResourceAttr("data.snowflake_views.v", "views.0.name", viewId.Name()), ), }, }, }) } -func views(databaseName string, schemaName string, viewName string) string { +func views(viewId sdk.SchemaObjectIdentifier) string { return fmt.Sprintf(` - - resource snowflake_database "d" { - name = "%v" - } - - resource snowflake_schema "s"{ - name = "%v" - database = snowflake_database.d.name + resource "snowflake_unsafe_execute" "use_warehouse" { + execute = "USE WAREHOUSE \"%v\"" + revert = "SELECT 1" } resource snowflake_view "v"{ name = "%v" - database = snowflake_schema.s.database - schema = snowflake_schema.s.name + schema = "%v" + database = "%v" statement = "SELECT ROLE_NAME, ROLE_OWNER FROM INFORMATION_SCHEMA.APPLICABLE_ROLES where ROLE_OWNER like 'foo%%'" + depends_on = [snowflake_unsafe_execute.use_warehouse] } data snowflake_views "v" { @@ -60,5 +56,5 @@ func views(databaseName string, schemaName string, viewName string) string { schema = snowflake_view.v.schema depends_on = [snowflake_view.v] } - `, databaseName, schemaName, viewName) + `, acc.TestWarehouseName, viewId.Name(), viewId.SchemaName(), viewId.DatabaseName()) } From c28d4e9265604aa2e0447b470ffb1f57495b6a49 Mon Sep 17 00:00:00 2001 From: Jakub Michalak Date: Tue, 27 Aug 2024 13:41:18 +0200 Subject: [PATCH 12/14] Fix reviewdog --- docs/resources/view.md | 4 ++-- pkg/resources/view.go | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/resources/view.md b/docs/resources/view.md index dc34c5ebc6..a53ef624b5 100644 --- a/docs/resources/view.md +++ b/docs/resources/view.md @@ -68,9 +68,9 @@ SQL ### Required -- `database` (String) The database in which to create the view. +- `database` (String) The database in which to create the view. 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 view; must be unique for the schema in which the view 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 view. +- `schema` (String) The schema in which to create the view. 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: `|`, `.`, `(`, `)`, `"` - `statement` (String) Specifies the query used to create the view. ### Optional diff --git a/pkg/resources/view.go b/pkg/resources/view.go index a035e8394b..c61f823b1b 100644 --- a/pkg/resources/view.go +++ b/pkg/resources/view.go @@ -30,14 +30,14 @@ var viewSchema = map[string]*schema.Schema{ "database": { Type: schema.TypeString, Required: true, - Description: "The database in which to create the view.", + Description: blocklistedCharactersFieldDescription("The database in which to create the view."), ForceNew: true, DiffSuppressFunc: suppressIdentifierQuoting, }, "schema": { Type: schema.TypeString, Required: true, - Description: "The schema in which to create the view.", + Description: blocklistedCharactersFieldDescription("The schema in which to create the view."), ForceNew: true, DiffSuppressFunc: suppressIdentifierQuoting, }, @@ -523,7 +523,7 @@ func ReadView(withExternalChangesMarking bool) schema.ReadContextFunc { func handlePolicyReferences(ctx context.Context, client *sdk.Client, id sdk.SchemaObjectIdentifier, d *schema.ResourceData) error { policyRefs, err := client.PolicyReferences.GetForEntity(ctx, sdk.NewGetForEntityPolicyReferenceRequest(id, sdk.PolicyEntityDomainView)) if err != nil { - return fmt.Errorf("getting policy references for view: %v", err) + return fmt.Errorf("getting policy references for view: %w", err) } var aggregationPolicies []map[string]any var rowAccessPolicies []map[string]any From d7955e73be81f9574547e8b16d9621039d357834 Mon Sep 17 00:00:00 2001 From: Jakub Michalak Date: Tue, 27 Aug 2024 16:07:47 +0200 Subject: [PATCH 13/14] Fix tests --- MIGRATION_GUIDE.md | 3 +++ pkg/datasources/views_acceptance_test.go | 5 ++++- .../grant_privileges_to_share_acceptance_test.go | 2 ++ pkg/resources/view_acceptance_test.go | 14 +++++++++++++- 4 files changed, 22 insertions(+), 2 deletions(-) diff --git a/MIGRATION_GUIDE.md b/MIGRATION_GUIDE.md index bf7dd7ac63..c0925ce848 100644 --- a/MIGRATION_GUIDE.md +++ b/MIGRATION_GUIDE.md @@ -21,6 +21,9 @@ Removed fields: - `tag` The value of this field will be removed from the state automatically. Please, use [tag_association](https://registry.terraform.io/providers/Snowflake-Labs/snowflake/latest/docs/resources/tag_association) instead. +#### *(breaking change)* Required warehouse +For this resource, the provider now uses [policy references](https://docs.snowflake.com/en/sql-reference/functions/policy_references) which requires a warehouse in the connection. Please, make sure you have either set a DEFAULT_WAREHOUSE for the user, or specified a warehouse in the provider configuration. + ### Identifier changes #### *(breaking change)* resource identifiers for schema and streamlit diff --git a/pkg/datasources/views_acceptance_test.go b/pkg/datasources/views_acceptance_test.go index 3326d13aab..ef5069cb31 100644 --- a/pkg/datasources/views_acceptance_test.go +++ b/pkg/datasources/views_acceptance_test.go @@ -5,14 +5,17 @@ import ( "testing" acc "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/acceptance" + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/acceptance/testenvs" "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/sdk" "github.com/hashicorp/terraform-plugin-testing/helper/resource" "github.com/hashicorp/terraform-plugin-testing/tfversion" ) -// TODO(SNOW-1423486): Fix using warehouse in all tests. +// TODO(SNOW-1423486): Fix using warehouse in all tests and remove unsetting testenvs.ConfigureClientOnce. func TestAcc_Views(t *testing.T) { + t.Setenv(string(testenvs.ConfigureClientOnce), "") + viewId := acc.TestClient().Ids.RandomSchemaObjectIdentifier() resource.Test(t, resource.TestCase{ ProtoV6ProviderFactories: acc.TestAccProtoV6ProviderFactories, diff --git a/pkg/resources/grant_privileges_to_share_acceptance_test.go b/pkg/resources/grant_privileges_to_share_acceptance_test.go index 459ed7ae6d..78ae8a4a01 100644 --- a/pkg/resources/grant_privileges_to_share_acceptance_test.go +++ b/pkg/resources/grant_privileges_to_share_acceptance_test.go @@ -229,6 +229,8 @@ func TestAcc_GrantPrivilegesToShare_OnAllTablesInSchema(t *testing.T) { } func TestAcc_GrantPrivilegesToShare_OnView(t *testing.T) { + t.Setenv(string(testenvs.ConfigureClientOnce), "") + databaseId := acc.TestClient().Ids.RandomAccountObjectIdentifier() schemaId := acc.TestClient().Ids.RandomDatabaseObjectIdentifierInDatabase(databaseId) tableId := acc.TestClient().Ids.RandomSchemaObjectIdentifierInSchema(schemaId) diff --git a/pkg/resources/view_acceptance_test.go b/pkg/resources/view_acceptance_test.go index 612bd43959..be3b810735 100644 --- a/pkg/resources/view_acceptance_test.go +++ b/pkg/resources/view_acceptance_test.go @@ -24,8 +24,10 @@ import ( "github.com/hashicorp/terraform-plugin-testing/tfversion" ) -// TODO(SNOW-1423486): Fix using warehouse in all tests. +// TODO(SNOW-1423486): Fix using warehouse in all tests and remove unsetting testenvs.ConfigureClientOnce +// TODO(next pr): cleanup setting warehouse with unsafe_execute func TestAcc_View_basic(t *testing.T) { + t.Setenv(string(testenvs.ConfigureClientOnce), "") _ = testenvs.GetOrSkipTest(t, testenvs.EnableAcceptance) acc.TestAccPreCheck(t) @@ -282,6 +284,7 @@ func TestAcc_View_basic(t *testing.T) { } func TestAcc_View_recursive(t *testing.T) { + t.Setenv(string(testenvs.ConfigureClientOnce), "") _ = testenvs.GetOrSkipTest(t, testenvs.EnableAcceptance) acc.TestAccPreCheck(t) id := acc.TestClient().Ids.RandomSchemaObjectIdentifier() @@ -321,6 +324,7 @@ func TestAcc_View_recursive(t *testing.T) { } func TestAcc_View_temporary(t *testing.T) { + t.Setenv(string(testenvs.ConfigureClientOnce), "") _ = testenvs.GetOrSkipTest(t, testenvs.EnableAcceptance) // we use one configured client, so a temporary view should be visible after creation _ = testenvs.GetOrSkipTest(t, testenvs.ConfigureClientOnce) @@ -349,6 +353,7 @@ func TestAcc_View_temporary(t *testing.T) { } func TestAcc_View_complete(t *testing.T) { + t.Setenv(string(testenvs.ConfigureClientOnce), "") _ = testenvs.GetOrSkipTest(t, testenvs.EnableAcceptance) acc.TestAccPreCheck(t) id := acc.TestClient().Ids.RandomSchemaObjectIdentifier() @@ -446,6 +451,7 @@ func TestAcc_View_complete(t *testing.T) { } func TestAcc_View_Rename(t *testing.T) { + t.Setenv(string(testenvs.ConfigureClientOnce), "") statement := "SELECT ROLE_NAME, ROLE_OWNER FROM INFORMATION_SCHEMA.APPLICABLE_ROLES" id := acc.TestClient().Ids.RandomSchemaObjectIdentifier() newId := acc.TestClient().Ids.RandomSchemaObjectIdentifier() @@ -487,6 +493,7 @@ func TestAcc_View_Rename(t *testing.T) { } func TestAcc_ViewChangeCopyGrants(t *testing.T) { + t.Setenv(string(testenvs.ConfigureClientOnce), "") id := acc.TestClient().Ids.RandomSchemaObjectIdentifier() statement := "SELECT ROLE_NAME, ROLE_OWNER FROM INFORMATION_SCHEMA.APPLICABLE_ROLES" @@ -536,6 +543,7 @@ func TestAcc_ViewChangeCopyGrants(t *testing.T) { } func TestAcc_ViewChangeCopyGrantsReversed(t *testing.T) { + t.Setenv(string(testenvs.ConfigureClientOnce), "") id := acc.TestClient().Ids.RandomSchemaObjectIdentifier() statement := "SELECT ROLE_NAME, ROLE_OWNER FROM INFORMATION_SCHEMA.APPLICABLE_ROLES" @@ -582,6 +590,7 @@ func TestAcc_ViewChangeCopyGrantsReversed(t *testing.T) { } func TestAcc_ViewCopyGrantsStatementUpdate(t *testing.T) { + t.Setenv(string(testenvs.ConfigureClientOnce), "") _ = testenvs.GetOrSkipTest(t, testenvs.EnableAcceptance) tableId := acc.TestClient().Ids.RandomSchemaObjectIdentifier() viewId := acc.TestClient().Ids.RandomSchemaObjectIdentifier() @@ -614,6 +623,7 @@ func TestAcc_ViewCopyGrantsStatementUpdate(t *testing.T) { } func TestAcc_View_copyGrants(t *testing.T) { + t.Setenv(string(testenvs.ConfigureClientOnce), "") id := acc.TestClient().Ids.RandomSchemaObjectIdentifier() statement := "SELECT ROLE_NAME, ROLE_OWNER FROM INFORMATION_SCHEMA.APPLICABLE_ROLES" viewModel := model.View("test", id.DatabaseName(), id.Name(), id.SchemaName(), statement).WithDependsOn([]string{"snowflake_unsafe_execute.use_warehouse"}) @@ -646,6 +656,7 @@ func TestAcc_View_copyGrants(t *testing.T) { } func TestAcc_View_Issue2640(t *testing.T) { + t.Setenv(string(testenvs.ConfigureClientOnce), "") id := acc.TestClient().Ids.RandomSchemaObjectIdentifier() part1 := "SELECT ROLE_NAME, ROLE_OWNER FROM INFORMATION_SCHEMA.APPLICABLE_ROLES" part2 := "SELECT ROLE_OWNER, ROLE_NAME FROM INFORMATION_SCHEMA.APPLICABLE_ROLES" @@ -700,6 +711,7 @@ func TestAcc_View_Issue2640(t *testing.T) { } func TestAcc_view_migrateFromVersion_0_94_1(t *testing.T) { + t.Setenv(string(testenvs.ConfigureClientOnce), "") _ = testenvs.GetOrSkipTest(t, testenvs.EnableAcceptance) acc.TestAccPreCheck(t) id := acc.TestClient().Ids.RandomSchemaObjectIdentifier() From 86228f2b99fc3c0a9436ea768e15c7cc6e2c0be5 Mon Sep 17 00:00:00 2001 From: Jakub Michalak Date: Wed, 28 Aug 2024 09:48:13 +0200 Subject: [PATCH 14/14] Fix tests --- pkg/resources/stream_acceptance_test.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pkg/resources/stream_acceptance_test.go b/pkg/resources/stream_acceptance_test.go index 58aa5edf6c..1fccdd2e23 100644 --- a/pkg/resources/stream_acceptance_test.go +++ b/pkg/resources/stream_acceptance_test.go @@ -107,6 +107,9 @@ func TestAcc_Stream_OnTable(t *testing.T) { // proves issue https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/2672 func TestAcc_Stream_OnView(t *testing.T) { + // TODO(SNOW-1423486): Fix using warehouse in all tests and remove unsetting testenvs.ConfigureClientOnce + t.Setenv(string(testenvs.ConfigureClientOnce), "") + tableName := acc.TestClient().Ids.Alpha() viewName := acc.TestClient().Ids.Alpha() name := acc.TestClient().Ids.Alpha()