diff --git a/MIGRATION_GUIDE.md b/MIGRATION_GUIDE.md index eec752d556..389131aff3 100644 --- a/MIGRATION_GUIDE.md +++ b/MIGRATION_GUIDE.md @@ -23,6 +23,22 @@ across different versions. - `comment` - `owner_role_type` +### snowflake_views data source changes +New filtering options: +- `in` +- `like` +- `starts_with` +- `limit` +- `with_describe` + +New output fields +- `show_output` +- `describe_output` + +Breaking changes: +- `database` and `schema` are right now under `in` field +- `views` field now organizes output of show under `show_output` field and the output of describe under `describe_output` field. + ### snowflake_view resource changes New fields: - `row_access_policy` @@ -32,6 +48,7 @@ New fields: - `is_temporary` - `data_metric_schedule` - `data_metric_function` + - `column` - 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. diff --git a/docs/data-sources/views.md b/docs/data-sources/views.md index 95eb43bbe3..9c61f6d351 100644 --- a/docs/data-sources/views.md +++ b/docs/data-sources/views.md @@ -2,12 +2,12 @@ page_title: "snowflake_views Data Source - terraform-provider-snowflake" subcategory: "" description: |- - + Datasource used to get details of filtered views. Filtering is aligned with the current possibilities for SHOW VIEWS https://docs.snowflake.com/en/sql-reference/sql/show-views query (only like is supported). The results of SHOW and DESCRIBE are encapsulated in one output collection views. --- # snowflake_views (Data Source) - +Datasource used to get details of filtered views. Filtering is aligned with the current possibilities for [SHOW VIEWS](https://docs.snowflake.com/en/sql-reference/sql/show-views) query (only `like` is supported). The results of SHOW and DESCRIBE are encapsulated in one output collection `views`. ## Example Usage @@ -21,22 +21,83 @@ data "snowflake_views" "current" { ## Schema -### Required +### Optional -- `database` (String) The database from which to return the schemas from. -- `schema` (String) The schema from which to return the views from. +- `in` (Block List, Max: 1) IN clause to filter the list of views (see [below for nested schema](#nestedblock--in)) +- `like` (String) Filters the output with **case-insensitive** pattern, with support for SQL wildcard characters (`%` and `_`). +- `limit` (Block List, Max: 1) Limits the number of rows returned. If the `limit.from` is set, then the limit wll start from the first element matched by the expression. The expression is only used to match with the first element, later on the elements are not matched by the prefix, but you can enforce a certain pattern with `starts_with` or `like`. (see [below for nested schema](#nestedblock--limit)) +- `starts_with` (String) Filters the output with **case-sensitive** characters indicating the beginning of the object name. +- `with_describe` (Boolean) Runs DESC VIEW for each view returned by SHOW VIEWS. The output of describe is saved to the description field. By default this value is set to true. ### Read-Only - `id` (String) The ID of this resource. -- `views` (List of Object) The views in the schema (see [below for nested schema](#nestedatt--views)) +- `views` (List of Object) Holds the aggregated output of all views details queries. (see [below for nested schema](#nestedatt--views)) + + +### Nested Schema for `in` + +Optional: + +- `account` (Boolean) Returns records for the entire account. +- `database` (String) Returns records for the current database in use or for a specified database. +- `schema` (String) Returns records for the current schema in use or a specified schema. Use fully qualified name. + + + +### Nested Schema for `limit` + +Required: + +- `rows` (Number) The maximum number of rows to return. + +Optional: + +- `from` (String) Specifies a **case-sensitive** pattern that is used to match object name. After the first match, the limit on the number of rows will be applied. + ### Nested Schema for `views` Read-Only: +- `describe_output` (List of Object) (see [below for nested schema](#nestedobjatt--views--describe_output)) +- `show_output` (List of Object) (see [below for nested schema](#nestedobjatt--views--show_output)) + + +### Nested Schema for `views.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 `views.show_output` + +Read-Only: + +- `change_tracking` (String) - `comment` (String) -- `database` (String) +- `created_on` (String) +- `database_name` (String) +- `is_materialized` (Boolean) +- `is_secure` (Boolean) +- `kind` (String) - `name` (String) -- `schema` (String) +- `owner` (String) +- `owner_role_type` (String) +- `reserved` (String) +- `schema_name` (String) +- `text` (String) diff --git a/docs/resources/view.md b/docs/resources/view.md index e3f4c3dc52..ab2682b498 100644 --- a/docs/resources/view.md +++ b/docs/resources/view.md @@ -38,7 +38,7 @@ resource "snowflake_view" "view" { select * from foo; SQL } -# resource with attached policies and data metric functions +# resource with attached policies, columns and data metric functions resource "snowflake_view" "test" { database = "database" schema = "schema" @@ -47,6 +47,20 @@ resource "snowflake_view" "test" { is_secure = "true" change_tracking = "true" is_temporary = "true" + column { + column_name = "id" + comment = "column comment" + } + column { + column_name = "address" + projection_policy { + policy_name = "projection_policy" + } + masking_policy { + policy_name = "masking_policy" + using = ["address"] + } + } row_access_policy { policy_name = "row_access_policy" on = ["id"] @@ -63,7 +77,7 @@ resource "snowflake_view" "test" { using_cron = "15 * * * * UTC" } statement = <<-SQL - SELECT id FROM TABLE; + SELECT id, address FROM TABLE; SQL } ``` @@ -84,6 +98,7 @@ SQL - `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. +- `column` (Block List) 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.) (see [below for nested schema](#nestedblock--column)) - `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. - `data_metric_function` (Block Set) Data metric functions used for the view. (see [below for nested schema](#nestedblock--data_metric_function)) @@ -112,6 +127,40 @@ Optional: - `entity_key` (Set of String) Defines which columns uniquely identify an entity within the view. + +### Nested Schema for `column` + +Required: + +- `column_name` (String) Specifies affected column name. + +Optional: + +- `comment` (String) Specifies a comment for the column. +- `masking_policy` (Block List, Max: 1) (see [below for nested schema](#nestedblock--column--masking_policy)) +- `projection_policy` (Block List, Max: 1) (see [below for nested schema](#nestedblock--column--projection_policy)) + + +### Nested Schema for `column.masking_policy` + +Required: + +- `policy_name` (String) Specifies the masking policy to set on a column. + +Optional: + +- `using` (List of String) 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. + + + +### Nested Schema for `column.projection_policy` + +Required: + +- `policy_name` (String) Specifies the projection policy to set on a column. + + + ### Nested Schema for `data_metric_function` @@ -119,6 +168,7 @@ Required: - `function_name` (String) Identifier of the data metric function to add to the table or view or drop from the table or view. This function identifier must be provided without arguments in parenthesis. - `on` (Set of String) 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. +- `schedule_status` (String) The status of the metrics association. Valid values are: `STARTED` | `SUSPENDED`. When status of a data metric function is changed, it is being reassigned with `DROP DATA METRIC FUNCTION` and `ADD DATA METRIC FUNCTION`, and then its status is changed by `MODIFY DATA METRIC FUNCTION` diff --git a/examples/resources/snowflake_view/resource.tf b/examples/resources/snowflake_view/resource.tf index de20fb54cb..b41c2c308d 100644 --- a/examples/resources/snowflake_view/resource.tf +++ b/examples/resources/snowflake_view/resource.tf @@ -18,7 +18,7 @@ resource "snowflake_view" "view" { select * from foo; SQL } -# resource with attached policies and data metric functions +# resource with attached policies, columns and data metric functions resource "snowflake_view" "test" { database = "database" schema = "schema" @@ -27,6 +27,20 @@ resource "snowflake_view" "test" { is_secure = "true" change_tracking = "true" is_temporary = "true" + column { + column_name = "id" + comment = "column comment" + } + column { + column_name = "address" + projection_policy { + policy_name = "projection_policy" + } + masking_policy { + policy_name = "masking_policy" + using = ["address"] + } + } row_access_policy { policy_name = "row_access_policy" on = ["id"] @@ -43,6 +57,6 @@ resource "snowflake_view" "test" { using_cron = "15 * * * * UTC" } statement = <<-SQL - SELECT id FROM TABLE; + SELECT id, address FROM TABLE; SQL } diff --git a/pkg/acceptance/bettertestspoc/assert/objectassert/view_snowflake_ext.go b/pkg/acceptance/bettertestspoc/assert/objectassert/view_snowflake_ext.go index 05598f49ad..494bdadc1a 100644 --- a/pkg/acceptance/bettertestspoc/assert/objectassert/view_snowflake_ext.go +++ b/pkg/acceptance/bettertestspoc/assert/objectassert/view_snowflake_ext.go @@ -2,8 +2,11 @@ package objectassert import ( "fmt" + "slices" "testing" + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/acceptance/helpers" + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/sdk" ) @@ -28,3 +31,71 @@ func (v *ViewAssert) HasNonEmptyText() *ViewAssert { }) return v } + +func (v *ViewAssert) HasNoRowAccessPolicyReferences(client *helpers.TestClient) *ViewAssert { + return v.hasNoPolicyReference(client, sdk.PolicyKindRowAccessPolicy) +} + +func (v *ViewAssert) HasNoAggregationPolicyReferences(client *helpers.TestClient) *ViewAssert { + return v.hasNoPolicyReference(client, sdk.PolicyKindAggregationPolicy) +} + +func (v *ViewAssert) HasNoMaskingPolicyReferences(client *helpers.TestClient) *ViewAssert { + return v.hasNoPolicyReference(client, sdk.PolicyKindMaskingPolicy) +} + +func (v *ViewAssert) HasNoProjectionPolicyReferences(client *helpers.TestClient) *ViewAssert { + return v.hasNoPolicyReference(client, sdk.PolicyKindProjectionPolicy) +} + +func (v *ViewAssert) hasNoPolicyReference(client *helpers.TestClient, kind sdk.PolicyKind) *ViewAssert { + v.AddAssertion(func(t *testing.T, o *sdk.View) error { + t.Helper() + refs, err := client.PolicyReferences.GetPolicyReferences(t, o.ID(), sdk.ObjectTypeView) + if err != nil { + return err + } + refs = slices.DeleteFunc(refs, func(reference helpers.PolicyReference) bool { + return reference.PolicyKind != string(kind) + }) + if len(refs) > 0 { + return fmt.Errorf("expected no %s policy references; got: %v", kind, refs) + } + return nil + }) + return v +} + +func (v *ViewAssert) HasRowAccessPolicyReferences(client *helpers.TestClient, n int) *ViewAssert { + return v.hasPolicyReference(client, sdk.PolicyKindRowAccessPolicy, n) +} + +func (v *ViewAssert) HasAggregationPolicyReferences(client *helpers.TestClient, n int) *ViewAssert { + return v.hasPolicyReference(client, sdk.PolicyKindAggregationPolicy, n) +} + +func (v *ViewAssert) HasMaskingPolicyReferences(client *helpers.TestClient, n int) *ViewAssert { + return v.hasPolicyReference(client, sdk.PolicyKindMaskingPolicy, n) +} + +func (v *ViewAssert) HasProjectionPolicyReferences(client *helpers.TestClient, n int) *ViewAssert { + return v.hasPolicyReference(client, sdk.PolicyKindProjectionPolicy, n) +} + +func (v *ViewAssert) hasPolicyReference(client *helpers.TestClient, kind sdk.PolicyKind, n int) *ViewAssert { + v.AddAssertion(func(t *testing.T, o *sdk.View) error { + t.Helper() + refs, err := client.PolicyReferences.GetPolicyReferences(t, o.ID(), sdk.ObjectTypeView) + if err != nil { + return err + } + refs = slices.DeleteFunc(refs, func(reference helpers.PolicyReference) bool { + return reference.PolicyKind != string(kind) + }) + if len(refs) != n { + return fmt.Errorf("expected %d %s policy references; got: %d, %v", n, kind, len(refs), refs) + } + return nil + }) + return v +} diff --git a/pkg/acceptance/bettertestspoc/assert/resourceassert/view_resource_ext.go b/pkg/acceptance/bettertestspoc/assert/resourceassert/view_resource_ext.go new file mode 100644 index 0000000000..12736a6a30 --- /dev/null +++ b/pkg/acceptance/bettertestspoc/assert/resourceassert/view_resource_ext.go @@ -0,0 +1,52 @@ +package resourceassert + +import ( + "strconv" + + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/acceptance/bettertestspoc/assert" +) + +func (v *ViewResourceAssert) HasColumnLength(len int) *ViewResourceAssert { + v.AddAssertion(assert.ValueSet("column.#", strconv.FormatInt(int64(len), 10))) + return v +} + +func (v *ViewResourceAssert) HasAggregationPolicyLength(len int) *ViewResourceAssert { + v.AddAssertion(assert.ValueSet("aggregation_policy.#", strconv.FormatInt(int64(len), 10))) + return v +} + +func (v *ViewResourceAssert) HasRowAccessPolicyLength(len int) *ViewResourceAssert { + v.AddAssertion(assert.ValueSet("row_access_policy.#", strconv.FormatInt(int64(len), 10))) + return v +} + +func (v *ViewResourceAssert) HasDataMetricScheduleLength(len int) *ViewResourceAssert { + v.AddAssertion(assert.ValueSet("data_metric_schedule.#", strconv.FormatInt(int64(len), 10))) + return v +} + +func (v *ViewResourceAssert) HasDataMetricFunctionLength(len int) *ViewResourceAssert { + v.AddAssertion(assert.ValueSet("data_metric_function.#", strconv.FormatInt(int64(len), 10))) + return v +} + +func (v *ViewResourceAssert) HasNoAggregationPolicyByLength() *ViewResourceAssert { + v.AddAssertion(assert.ValueNotSet("aggregation_policy.#")) + return v +} + +func (v *ViewResourceAssert) HasNoRowAccessPolicyByLength() *ViewResourceAssert { + v.AddAssertion(assert.ValueNotSet("row_access_policy.#")) + return v +} + +func (v *ViewResourceAssert) HasNoDataMetricScheduleByLength() *ViewResourceAssert { + v.AddAssertion(assert.ValueNotSet("data_metric_schedule.#")) + return v +} + +func (v *ViewResourceAssert) HasNoDataMetricFunctionByLength() *ViewResourceAssert { + v.AddAssertion(assert.ValueNotSet("data_metric_function.#")) + return v +} diff --git a/pkg/acceptance/helpers/projection_policy_client.go b/pkg/acceptance/helpers/projection_policy_client.go index 4adf3c07d4..e79b5d28b5 100644 --- a/pkg/acceptance/helpers/projection_policy_client.go +++ b/pkg/acceptance/helpers/projection_policy_client.go @@ -31,7 +31,7 @@ func (c *ProjectionPolicyClient) CreateProjectionPolicy(t *testing.T) (sdk.Schem ctx := context.Background() id := c.ids.RandomSchemaObjectIdentifier() - _, err := c.client().ExecForTests(ctx, fmt.Sprintf(`CREATE PROJECTION POLICY %s AS () RETURNS PROJECTION_CONSTRAINT -> PROJECTION_CONSTRAINT(ALLOW => false)`, id.Name())) + _, err := c.client().ExecForTests(ctx, fmt.Sprintf(`CREATE PROJECTION POLICY %s AS () RETURNS PROJECTION_CONSTRAINT -> PROJECTION_CONSTRAINT(ALLOW => false)`, id.FullyQualifiedName())) require.NoError(t, err) return id, c.DropProjectionPolicyFunc(t, id) } @@ -41,7 +41,7 @@ func (c *ProjectionPolicyClient) DropProjectionPolicyFunc(t *testing.T, id sdk.S ctx := context.Background() return func() { - _, err := c.client().ExecForTests(ctx, fmt.Sprintf(`DROP PROJECTION POLICY IF EXISTS %s`, id.Name())) + _, err := c.client().ExecForTests(ctx, fmt.Sprintf(`DROP PROJECTION POLICY IF EXISTS %s`, id.FullyQualifiedName())) require.NoError(t, err) } } diff --git a/pkg/architests/resources_acceptance_tests_arch_test.go b/pkg/architests/resources_acceptance_tests_arch_test.go index 7e3ddad5ca..3cfef473cc 100644 --- a/pkg/architests/resources_acceptance_tests_arch_test.go +++ b/pkg/architests/resources_acceptance_tests_arch_test.go @@ -26,7 +26,12 @@ func TestArchCheck_AcceptanceTests_Resources(t *testing.T) { }) t.Run("there are no acceptance tests in other test files in the directory", func(t *testing.T) { - otherTestFiles := resourcesFiles.Filter(architest.FileNameFilterWithExclusionsProvider(architest.TestFileRegex, architest.AcceptanceTestFileRegex, regexp.MustCompile("helpers_test.go"))) + otherTestFiles := resourcesFiles.Filter(architest.FileNameFilterWithExclusionsProvider( + architest.TestFileRegex, + architest.AcceptanceTestFileRegex, + regexp.MustCompile("helpers_test.go"), + regexp.MustCompile("view_test.go"), + )) otherTestFiles.All(func(file *architest.File) { file.ExportedMethods().All(func(method *architest.Method) { diff --git a/pkg/datasources/streamlits.go b/pkg/datasources/streamlits.go index ef71fb8c62..4fed061263 100644 --- a/pkg/datasources/streamlits.go +++ b/pkg/datasources/streamlits.go @@ -88,7 +88,7 @@ var streamlitsSchema = map[string]*schema.Schema{ resources.DescribeOutputAttributeName: { Type: schema.TypeList, Computed: true, - Description: "Holds the output of DESCRIBE STREAMLITS.", + Description: "Holds the output of DESCRIBE STREAMLIT.", Elem: &schema.Resource{ Schema: schemas.DescribeStreamlitSchema, }, diff --git a/pkg/datasources/views.go b/pkg/datasources/views.go index 2d17aa7d59..fa99c1becf 100644 --- a/pkg/datasources/views.go +++ b/pkg/datasources/views.go @@ -2,48 +2,102 @@ package datasources import ( "context" - "log" "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/internal/provider" + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/resources" + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/schemas" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" - "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/helpers" "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/sdk" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" ) var viewsSchema = map[string]*schema.Schema{ - "database": { + "with_describe": { + Type: schema.TypeBool, + Optional: true, + Default: true, + Description: "Runs DESC VIEW for each view returned by SHOW VIEWS. The output of describe is saved to the description field. By default this value is set to true.", + }, + "in": { + Type: schema.TypeList, + Optional: true, + Description: "IN clause to filter the list of views", + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "account": { + Type: schema.TypeBool, + Optional: true, + Description: "Returns records for the entire account.", + ExactlyOneOf: []string{"in.0.account", "in.0.database", "in.0.schema"}, + }, + "database": { + Type: schema.TypeString, + Optional: true, + Description: "Returns records for the current database in use or for a specified database.", + ExactlyOneOf: []string{"in.0.account", "in.0.database", "in.0.schema"}, + }, + "schema": { + Type: schema.TypeString, + Optional: true, + Description: "Returns records for the current schema in use or a specified schema. Use fully qualified name.", + ExactlyOneOf: []string{"in.0.account", "in.0.database", "in.0.schema"}, + }, + }, + }, + }, + "like": { Type: schema.TypeString, - Required: true, - Description: "The database from which to return the schemas from.", + Optional: true, + Description: "Filters the output with **case-insensitive** pattern, with support for SQL wildcard characters (`%` and `_`).", }, - "schema": { + "starts_with": { Type: schema.TypeString, - Required: true, - Description: "The schema from which to return the views from.", + Optional: true, + Description: "Filters the output with **case-sensitive** characters indicating the beginning of the object name.", }, - "views": { + "limit": { Type: schema.TypeList, - Computed: true, - Description: "The views in the schema", + Optional: true, + Description: "Limits the number of rows returned. If the `limit.from` is set, then the limit wll start from the first element matched by the expression. The expression is only used to match with the first element, later on the elements are not matched by the prefix, but you can enforce a certain pattern with `starts_with` or `like`.", + MaxItems: 1, Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ - "name": { - Type: schema.TypeString, - Computed: true, + "rows": { + Type: schema.TypeInt, + Required: true, + Description: "The maximum number of rows to return.", }, - "database": { - Type: schema.TypeString, - Computed: true, + "from": { + Type: schema.TypeString, + Optional: true, + Description: "Specifies a **case-sensitive** pattern that is used to match object name. After the first match, the limit on the number of rows will be applied.", }, - "schema": { - Type: schema.TypeString, - Computed: true, + }, + }, + }, + "views": { + Type: schema.TypeList, + Computed: true, + Description: "Holds the aggregated output of all views details queries.", + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + resources.ShowOutputAttributeName: { + Type: schema.TypeList, + Computed: true, + Description: "Holds the output of SHOW VIEWS.", + Elem: &schema.Resource{ + Schema: schemas.ShowViewSchema, + }, }, - "comment": { - Type: schema.TypeString, - Optional: true, - Computed: true, + resources.DescribeOutputAttributeName: { + Type: schema.TypeList, + Computed: true, + Description: "Holds the output of DESCRIBE VIEW.", + Elem: &schema.Resource{ + Schema: schemas.ViewDescribeSchema, + }, }, }, }, @@ -52,38 +106,91 @@ var viewsSchema = map[string]*schema.Schema{ func Views() *schema.Resource { return &schema.Resource{ - Read: ReadViews, - Schema: viewsSchema, + ReadContext: ReadViews, + Schema: viewsSchema, + Description: "Datasource used to get details of filtered views. Filtering is aligned with the current possibilities for [SHOW VIEWS](https://docs.snowflake.com/en/sql-reference/sql/show-views) query (only `like` is supported). The results of SHOW and DESCRIBE are encapsulated in one output collection `views`.", } } -func ReadViews(d *schema.ResourceData, meta interface{}) error { +func ReadViews(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { client := meta.(*provider.Context).Client - ctx := context.Background() - databaseName := d.Get("database").(string) - schemaName := d.Get("schema").(string) - - schemaId := sdk.NewDatabaseObjectIdentifier(databaseName, schemaName) - extractedViews, err := client.Views.Show(ctx, sdk.NewShowViewRequest().WithIn( - sdk.ExtendedIn{In: sdk.In{Schema: schemaId}}, - )) + req := sdk.NewShowViewRequest() + + if v, ok := d.GetOk("in"); ok { + in := v.([]any)[0].(map[string]any) + if v, ok := in["account"]; ok && v.(bool) { + req.WithIn(sdk.ExtendedIn{In: sdk.In{Account: sdk.Bool(true)}}) + } + if v, ok := in["database"]; ok { + database := v.(string) + if database != "" { + req.WithIn(sdk.ExtendedIn{In: sdk.In{Database: sdk.NewAccountObjectIdentifier(database)}}) + } + } + if v, ok := in["schema"]; ok { + schema := v.(string) + if schema != "" { + schemaId, err := sdk.ParseDatabaseObjectIdentifier(schema) + if err != nil { + return diag.FromErr(err) + } + req.WithIn(sdk.ExtendedIn{In: sdk.In{Schema: schemaId}}) + } + } + } + + if likePattern, ok := d.GetOk("like"); ok { + req.WithLike(sdk.Like{ + Pattern: sdk.String(likePattern.(string)), + }) + } + + if v, ok := d.GetOk("starts_with"); ok { + req.WithStartsWith(v.(string)) + } + + if v, ok := d.GetOk("limit"); ok { + l := v.([]any)[0].(map[string]any) + limit := sdk.LimitFrom{} + if v, ok := l["rows"]; ok { + rows := v.(int) + limit.Rows = sdk.Int(rows) + } + if v, ok := l["from"]; ok { + from := v.(string) + limit.From = sdk.String(from) + } + req.WithLimit(limit) + } + + views, err := client.Views.Show(ctx, req) if err != nil { - log.Printf("[DEBUG] failed when searching views in schema (%s), err = %s", schemaId.FullyQualifiedName(), err.Error()) - d.SetId("") - return nil + return diag.FromErr(err) } - views := make([]map[string]any, len(extractedViews)) + d.SetId("views_read") + + flattenedViews := make([]map[string]any, len(views)) + for i, view := range views { + view := view + var viewDescriptions []map[string]any + if d.Get("with_describe").(bool) { + describeOutput, err := client.Views.Describe(ctx, view.ID()) + if err != nil { + return diag.FromErr(err) + } + viewDescriptions = schemas.ViewDescriptionToSchema(describeOutput) + } - for i, view := range extractedViews { - views[i] = map[string]any{ - "name": view.Name, - "database": view.DatabaseName, - "schema": view.SchemaName, - "comment": view.Comment, + flattenedViews[i] = map[string]any{ + resources.ShowOutputAttributeName: []map[string]any{schemas.ViewToSchema(&view)}, + resources.DescribeOutputAttributeName: viewDescriptions, } } - d.SetId(helpers.EncodeSnowflakeID(databaseName, schemaName)) - return d.Set("views", views) + if err := d.Set("views", flattenedViews); err != nil { + return diag.FromErr(err) + } + + return nil } diff --git a/pkg/datasources/views_acceptance_test.go b/pkg/datasources/views_acceptance_test.go index 7edce8f234..ed4cb48a3a 100644 --- a/pkg/datasources/views_acceptance_test.go +++ b/pkg/datasources/views_acceptance_test.go @@ -6,8 +6,6 @@ import ( 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" ) @@ -16,7 +14,12 @@ import ( func TestAcc_Views(t *testing.T) { t.Setenv(string(testenvs.ConfigureClientOnce), "") - viewId := acc.TestClient().Ids.RandomSchemaObjectIdentifier() + schemaId := acc.TestClient().Ids.RandomDatabaseObjectIdentifier() + + viewNamePrefix := acc.TestClient().Ids.Alpha() + viewName := viewNamePrefix + "1" + acc.TestClient().Ids.Alpha() + viewName2 := viewNamePrefix + "2" + acc.TestClient().Ids.Alpha() + resource.Test(t, resource.TestCase{ ProtoV6ProviderFactories: acc.TestAccProtoV6ProviderFactories, PreCheck: func() { acc.TestAccPreCheck(t) }, @@ -26,32 +29,98 @@ func TestAcc_Views(t *testing.T) { CheckDestroy: nil, Steps: []resource.TestStep{ { - Config: views(viewId), - Check: resource.ComposeTestCheckFunc( - 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", viewId.Name()), + Config: views(acc.TestDatabaseName, acc.TestSchemaName, schemaId.Name(), viewName, viewName2, viewNamePrefix+"%"), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr("data.snowflake_views.in_schema", "views.#", "1"), + + resource.TestCheckResourceAttr("data.snowflake_views.in_schema", "views.0.show_output.0.name", viewName2), + resource.TestCheckResourceAttrSet("data.snowflake_views.in_schema", "views.0.show_output.0.created_on"), + resource.TestCheckResourceAttr("data.snowflake_views.in_schema", "views.0.show_output.0.kind", ""), + resource.TestCheckResourceAttr("data.snowflake_views.in_schema", "views.0.show_output.0.reserved", ""), + resource.TestCheckResourceAttr("data.snowflake_views.in_schema", "views.0.show_output.0.database_name", schemaId.DatabaseName()), + resource.TestCheckResourceAttr("data.snowflake_views.in_schema", "views.0.show_output.0.schema_name", schemaId.Name()), + resource.TestCheckResourceAttrSet("data.snowflake_views.in_schema", "views.0.show_output.0.owner"), + resource.TestCheckResourceAttr("data.snowflake_views.in_schema", "views.0.show_output.0.comment", ""), + resource.TestCheckResourceAttrSet("data.snowflake_views.in_schema", "views.0.show_output.0.text"), + resource.TestCheckResourceAttr("data.snowflake_views.in_schema", "views.0.show_output.0.is_secure", "false"), + resource.TestCheckResourceAttr("data.snowflake_views.in_schema", "views.0.show_output.0.is_materialized", "false"), + resource.TestCheckResourceAttr("data.snowflake_views.in_schema", "views.0.show_output.0.owner_role_type", "ROLE"), + resource.TestCheckResourceAttr("data.snowflake_views.in_schema", "views.0.show_output.0.change_tracking", "OFF"), + + resource.TestCheckResourceAttr("data.snowflake_views.in_schema", "views.0.describe_output.#", "2"), + resource.TestCheckResourceAttr("data.snowflake_views.in_schema", "views.0.describe_output.0.name", "ROLE_NAME"), + resource.TestCheckResourceAttrSet("data.snowflake_views.in_schema", "views.0.describe_output.0.type"), + resource.TestCheckResourceAttr("data.snowflake_views.in_schema", "views.0.describe_output.0.kind", "COLUMN"), + resource.TestCheckResourceAttr("data.snowflake_views.in_schema", "views.0.describe_output.0.is_nullable", "true"), + resource.TestCheckResourceAttr("data.snowflake_views.in_schema", "views.0.describe_output.0.default", ""), + resource.TestCheckResourceAttr("data.snowflake_views.in_schema", "views.0.describe_output.0.is_primary", "false"), + resource.TestCheckResourceAttr("data.snowflake_views.in_schema", "views.0.describe_output.0.is_unique", "false"), + resource.TestCheckResourceAttr("data.snowflake_views.in_schema", "views.0.describe_output.0.check", ""), + resource.TestCheckResourceAttr("data.snowflake_views.in_schema", "views.0.describe_output.0.expression", ""), + resource.TestCheckResourceAttr("data.snowflake_views.in_schema", "views.0.describe_output.0.comment", ""), + resource.TestCheckResourceAttr("data.snowflake_views.in_schema", "views.0.describe_output.0.policy_name", ""), + resource.TestCheckNoResourceAttr("data.snowflake_views.in_schema", "views.0.describe_output.0.policy_domain"), + resource.TestCheckResourceAttr("data.snowflake_views.in_schema", "views.0.describe_output.1.name", "ROLE_OWNER"), + + resource.TestCheckResourceAttr("data.snowflake_views.filtering", "views.#", "1"), + resource.TestCheckResourceAttr("data.snowflake_views.filtering", "views.0.show_output.0.name", viewName2), ), }, }, }) } -func views(viewId sdk.SchemaObjectIdentifier) string { +func views(databaseName, defaultSchemaName, schemaName, view1Name, view2Name, viewPrefix string) string { return fmt.Sprintf(` - resource snowflake_view "v"{ - name = "%v" - schema = "%v" - database = "%v" + resource snowflake_schema "test" { + database = "%[1]v" + name = "%[3]v" + } + + resource snowflake_view "v1"{ + database = "%[1]v" + schema = "%[2]v" + name = "%[4]v" + statement = "SELECT ROLE_NAME, ROLE_OWNER FROM INFORMATION_SCHEMA.APPLICABLE_ROLES where ROLE_OWNER like 'foo%%'" + column { + column_name = "ROLE_NAME" + } + column { + column_name = "ROLE_OWNER" + } + } + + resource snowflake_view "v2"{ + database = snowflake_schema.test.database + schema = snowflake_schema.test.name + name = "%[5]v" statement = "SELECT ROLE_NAME, ROLE_OWNER FROM INFORMATION_SCHEMA.APPLICABLE_ROLES where ROLE_OWNER like 'foo%%'" + column { + column_name = "ROLE_NAME" + } + column { + column_name = "ROLE_OWNER" + } + } + + data snowflake_views "in_schema" { + depends_on = [ snowflake_view.v1, snowflake_view.v2 ] + in { + schema = snowflake_schema.test.fully_qualified_name + } } - data snowflake_views "v" { - database = snowflake_view.v.database - schema = snowflake_view.v.schema - depends_on = [snowflake_view.v] + data snowflake_views "filtering" { + depends_on = [ snowflake_view.v1, snowflake_view.v2 ] + in { + database = snowflake_schema.test.database + } + like = "%[6]v" + starts_with = trimsuffix("%[6]v", "%%") + limit { + rows = 1 + from = snowflake_view.v1.name + } } - `, viewId.Name(), viewId.SchemaName(), viewId.DatabaseName()) + `, databaseName, defaultSchemaName, schemaName, view1Name, view2Name, viewPrefix) } diff --git a/pkg/resources/resource.go b/pkg/resources/resource.go index 2a7437cad9..a4e1494f67 100644 --- a/pkg/resources/resource.go +++ b/pkg/resources/resource.go @@ -9,6 +9,10 @@ import ( "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" ) +type ResourceValueSetter interface { + Set(string, any) error +} + func DeleteResource(t string, builder func(string) *snowflake.Builder) func(*schema.ResourceData, interface{}) error { return func(d *schema.ResourceData, meta interface{}) error { client := meta.(*provider.Context).Client diff --git a/pkg/resources/stream_acceptance_test.go b/pkg/resources/stream_acceptance_test.go index 1fccdd2e23..2e595fea91 100644 --- a/pkg/resources/stream_acceptance_test.go +++ b/pkg/resources/stream_acceptance_test.go @@ -295,6 +295,12 @@ resource "snowflake_view" "test" { change_tracking = true statement = "select * from \"${snowflake_table.test.name}\"" + column { + column_name = "column1" + } + column { + column_name = "column2" + } } resource "snowflake_stream" "test_stream" { diff --git a/pkg/resources/testdata/TestAcc_GrantPrivilegesToShare/OnView/test.tf b/pkg/resources/testdata/TestAcc_GrantPrivilegesToShare/OnView/test.tf index 931f79a9cf..f2b35dabb4 100644 --- a/pkg/resources/testdata/TestAcc_GrantPrivilegesToShare/OnView/test.tf +++ b/pkg/resources/testdata/TestAcc_GrantPrivilegesToShare/OnView/test.tf @@ -28,6 +28,9 @@ resource "snowflake_view" "test" { schema = snowflake_schema.test.name is_secure = true statement = "select \"id\" from \"${snowflake_database.test.name}\".\"${snowflake_schema.test.name}\".\"${snowflake_table.test.name}\"" + column { + column_name = "id" + } } resource "snowflake_grant_privileges_to_share" "test_setup" { diff --git a/pkg/resources/testdata/TestAcc_GrantPrivilegesToShare/OnView_NoGrant/test.tf b/pkg/resources/testdata/TestAcc_GrantPrivilegesToShare/OnView_NoGrant/test.tf index 1dcf526eed..4a4ec953c4 100644 --- a/pkg/resources/testdata/TestAcc_GrantPrivilegesToShare/OnView_NoGrant/test.tf +++ b/pkg/resources/testdata/TestAcc_GrantPrivilegesToShare/OnView_NoGrant/test.tf @@ -28,4 +28,7 @@ resource "snowflake_view" "test" { schema = snowflake_schema.test.name is_secure = true statement = "select \"id\" from \"${snowflake_database.test.name}\".\"${snowflake_schema.test.name}\".\"${snowflake_table.test.name}\"" + column { + column_name = "id" + } } diff --git a/pkg/resources/testdata/TestAcc_View/basic/test.tf b/pkg/resources/testdata/TestAcc_View/basic/test.tf index 74efa22d33..905a79328a 100644 --- a/pkg/resources/testdata/TestAcc_View/basic/test.tf +++ b/pkg/resources/testdata/TestAcc_View/basic/test.tf @@ -3,4 +3,11 @@ resource "snowflake_view" "test" { database = var.database schema = var.schema statement = var.statement + + dynamic "column" { + for_each = var.columns + content { + column_name = column.value["column_name"] + } + } } diff --git a/pkg/resources/testdata/TestAcc_View/basic/variables.tf b/pkg/resources/testdata/TestAcc_View/basic/variables.tf index 5b5810d23d..2219f130a5 100644 --- a/pkg/resources/testdata/TestAcc_View/basic/variables.tf +++ b/pkg/resources/testdata/TestAcc_View/basic/variables.tf @@ -13,3 +13,7 @@ variable "schema" { variable "statement" { type = string } + +variable "columns" { + type = set(map(string)) +} diff --git a/pkg/resources/testdata/TestAcc_View/basic_copy_grants/test.tf b/pkg/resources/testdata/TestAcc_View/basic_copy_grants/test.tf new file mode 100644 index 0000000000..cdc7295f9d --- /dev/null +++ b/pkg/resources/testdata/TestAcc_View/basic_copy_grants/test.tf @@ -0,0 +1,15 @@ +resource "snowflake_view" "test" { + name = var.name + database = var.database + schema = var.schema + statement = var.statement + copy_grants = var.copy_grants + is_secure = var.is_secure + + dynamic "column" { + for_each = var.columns + content { + column_name = column.value["column_name"] + } + } +} diff --git a/pkg/resources/testdata/TestAcc_View/basic_copy_grants/variables.tf b/pkg/resources/testdata/TestAcc_View/basic_copy_grants/variables.tf new file mode 100644 index 0000000000..86e63c4564 --- /dev/null +++ b/pkg/resources/testdata/TestAcc_View/basic_copy_grants/variables.tf @@ -0,0 +1,27 @@ +variable "name" { + type = string +} + +variable "database" { + type = string +} + +variable "schema" { + type = string +} + +variable "statement" { + type = string +} + +variable "copy_grants" { + type = bool +} + +variable "is_secure" { + type = bool +} + +variable "columns" { + type = set(map(string)) +} diff --git a/pkg/resources/testdata/TestAcc_View/basic_is_recursive/test.tf b/pkg/resources/testdata/TestAcc_View/basic_is_recursive/test.tf new file mode 100644 index 0000000000..42308b4d48 --- /dev/null +++ b/pkg/resources/testdata/TestAcc_View/basic_is_recursive/test.tf @@ -0,0 +1,14 @@ +resource "snowflake_view" "test" { + name = var.name + database = var.database + schema = var.schema + statement = var.statement + is_recursive = var.is_recursive + + dynamic "column" { + for_each = var.columns + content { + column_name = column.value["column_name"] + } + } +} diff --git a/pkg/resources/testdata/TestAcc_View/basic_is_recursive/variables.tf b/pkg/resources/testdata/TestAcc_View/basic_is_recursive/variables.tf new file mode 100644 index 0000000000..b38898bbcf --- /dev/null +++ b/pkg/resources/testdata/TestAcc_View/basic_is_recursive/variables.tf @@ -0,0 +1,23 @@ +variable "name" { + type = string +} + +variable "database" { + type = string +} + +variable "schema" { + type = string +} + +variable "statement" { + type = string +} + +variable "is_recursive" { + type = bool +} + +variable "columns" { + type = set(map(string)) +} diff --git a/pkg/resources/testdata/TestAcc_View/basic_update/test.tf b/pkg/resources/testdata/TestAcc_View/basic_update/test.tf index e403c93692..2df97a8f62 100644 --- a/pkg/resources/testdata/TestAcc_View/basic_update/test.tf +++ b/pkg/resources/testdata/TestAcc_View/basic_update/test.tf @@ -2,18 +2,26 @@ resource "snowflake_view" "test" { name = var.name database = var.database schema = var.schema + + dynamic "column" { + for_each = var.columns + content { + column_name = column.value["column_name"] + } + } + 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 } data_metric_function { - function_name = var.data_metric_function - on = var.data_metric_function_on + function_name = var.data_metric_function + on = var.data_metric_function_on + schedule_status = var.schedule_status } data_metric_schedule { using_cron = var.data_metric_schedule_using_cron diff --git a/pkg/resources/testdata/TestAcc_View/basic_update/variables.tf b/pkg/resources/testdata/TestAcc_View/basic_update/variables.tf index e2da9f2f40..4b814dd06d 100644 --- a/pkg/resources/testdata/TestAcc_View/basic_update/variables.tf +++ b/pkg/resources/testdata/TestAcc_View/basic_update/variables.tf @@ -45,3 +45,11 @@ variable "data_metric_function" { variable "data_metric_function_on" { type = list(string) } + +variable "schedule_status" { + type = string +} + +variable "columns" { + type = set(map(string)) +} diff --git a/pkg/resources/testdata/TestAcc_View/columns/test.tf b/pkg/resources/testdata/TestAcc_View/columns/test.tf new file mode 100644 index 0000000000..fd5b201fe7 --- /dev/null +++ b/pkg/resources/testdata/TestAcc_View/columns/test.tf @@ -0,0 +1,23 @@ +resource "snowflake_view" "test" { + name = var.name + database = var.database + schema = var.schema + statement = var.statement + + column { + column_name = "ID" + + projection_policy { + policy_name = var.projection_name + } + + masking_policy { + policy_name = var.masking_name + using = var.masking_using + } + } + + column { + column_name = "FOO" + } +} diff --git a/pkg/resources/testdata/TestAcc_View/columns/variables.tf b/pkg/resources/testdata/TestAcc_View/columns/variables.tf new file mode 100644 index 0000000000..ba6e4bfe0d --- /dev/null +++ b/pkg/resources/testdata/TestAcc_View/columns/variables.tf @@ -0,0 +1,27 @@ +variable "name" { + type = string +} + +variable "database" { + type = string +} + +variable "schema" { + type = string +} + +variable "statement" { + type = string +} + +variable "projection_name" { + type = string +} + +variable "masking_name" { + type = string +} + +variable "masking_using" { + type = list(string) +} diff --git a/pkg/resources/testdata/TestAcc_View/complete/test.tf b/pkg/resources/testdata/TestAcc_View/complete/test.tf index 6e4c53c023..33c05fdb69 100644 --- a/pkg/resources/testdata/TestAcc_View/complete/test.tf +++ b/pkg/resources/testdata/TestAcc_View/complete/test.tf @@ -7,9 +7,24 @@ resource "snowflake_view" "test" { copy_grants = var.copy_grants change_tracking = var.change_tracking is_temporary = var.is_temporary + column { + column_name = var.column1_name + comment = var.column1_comment + } + column { + column_name = var.column2_name + projection_policy { + policy_name = var.column2_projection_policy + } + masking_policy { + policy_name = var.column2_masking_policy + using = var.column2_masking_policy_using + } + } data_metric_function { - function_name = var.data_metric_function - on = var.data_metric_function_on + function_name = var.data_metric_function + on = var.data_metric_function_on + schedule_status = "STARTED" } data_metric_schedule { using_cron = var.data_metric_schedule_using_cron diff --git a/pkg/resources/testdata/TestAcc_View/complete/variables.tf b/pkg/resources/testdata/TestAcc_View/complete/variables.tf index 4cdf99c64b..02d4158484 100644 --- a/pkg/resources/testdata/TestAcc_View/complete/variables.tf +++ b/pkg/resources/testdata/TestAcc_View/complete/variables.tf @@ -65,3 +65,26 @@ variable "data_metric_function" { variable "data_metric_function_on" { type = list(string) } + +variable "column1_name" { + type = string +} + +variable "column1_comment" { + type = string +} +variable "column2_name" { + type = string +} + +variable "column2_masking_policy" { + type = string +} + +variable "column2_masking_policy_using" { + type = list(string) +} + +variable "column2_projection_policy" { + type = string +} diff --git a/pkg/resources/view.go b/pkg/resources/view.go index e9e929f2da..e5a832f5c8 100644 --- a/pkg/resources/view.go +++ b/pkg/resources/view.go @@ -5,9 +5,11 @@ import ( "errors" "fmt" "log" + "slices" "strconv" "strings" + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/internal/collections" "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/internal/provider" "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/schemas" @@ -101,19 +103,13 @@ var viewSchema = map[string]*schema.Schema{ }, 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.", }, - // TODO (SNOW-1348118 - next pr) - // "schedule_status": { - // Type: schema.TypeString, - // Optional: true, - // ValidateDiagFunc: sdkValidation(sdk.ToAllowedDataMetricScheduleStatusOption), - // Description: fmt.Sprintf("The status of the metrics association. Valid values are: %v. When status of a data metric function is changed, it is being reassigned with `DROP DATA METRIC FUNCTION` and `ADD DATA METRIC FUNCTION`, and then its status is changed by `MODIFY DATA METRIC FUNCTION` ", possibleValuesListed(sdk.AllAllowedDataMetricScheduleStatusOptions)), - // DiffSuppressFunc: SuppressIfAny(NormalizeAndCompare(sdk.ToAllowedDataMetricScheduleStatusOption), func(_, oldValue, newValue string, _ *schema.ResourceData) bool { - // if newValue == "" { - // return true - // } - // return false - // }), - // }, + "schedule_status": { + Type: schema.TypeString, + Required: true, + ValidateDiagFunc: sdkValidation(sdk.ToAllowedDataMetricScheduleStatusOption), + Description: fmt.Sprintf("The status of the metrics association. Valid values are: %v. When status of a data metric function is changed, it is being reassigned with `DROP DATA METRIC FUNCTION` and `ADD DATA METRIC FUNCTION`, and then its status is changed by `MODIFY DATA METRIC FUNCTION` ", possibleValuesListed(sdk.AllAllowedDataMetricScheduleStatusOptions)), + DiffSuppressFunc: SuppressIfAny(NormalizeAndCompare(sdk.ToAllowedDataMetricScheduleStatusOption)), + }, }, }, Description: "Data metric functions used for the view.", @@ -143,53 +139,63 @@ var viewSchema = map[string]*schema.Schema{ Description: "Specifies the schedule to run the data metric functions periodically.", RequiredWith: []string{"data_metric_function"}, }, - // TODO (SNOW-1348118 - next pr): add columns - // "column": { - // 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{ - // "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.", - // }, - // "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.)", - // }, + "column": { + 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, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "policy_name": { + Type: schema.TypeString, + Required: true, + DiffSuppressFunc: suppressIdentifierQuoting, + 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.TypeList, + Optional: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "policy_name": { + Type: schema.TypeString, + Required: true, + DiffSuppressFunc: suppressIdentifierQuoting, + 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.)", + }, "comment": { Type: schema.TypeString, Optional: true, @@ -383,8 +389,16 @@ func CreateView(orReplace bool) schema.CreateContextFunc { req.WithComment(v) } + if v := d.Get("column"); len(v.([]any)) > 0 { + columns, err := extractColumns(v) + if err != nil { + return diag.FromErr(err) + } + req.WithColumns(columns) + } + if v := d.Get("row_access_policy"); len(v.([]any)) > 0 { - id, columns, err := extractPolicyWithColumns(v, "on") + id, columns, err := extractPolicyWithColumnsSet(v, "on") if err != nil { return diag.FromErr(err) } @@ -392,7 +406,7 @@ func CreateView(orReplace bool) schema.CreateContextFunc { } if v := d.Get("aggregation_policy"); len(v.([]any)) > 0 { - id, columns, err := extractPolicyWithColumns(v, "entity_key") + id, columns, err := extractPolicyWithColumnsSet(v, "entity_key") if err != nil { return diag.FromErr(err) } @@ -452,48 +466,96 @@ func CreateView(orReplace bool) schema.CreateContextFunc { if err != nil { return diag.FromErr(fmt.Errorf("error adding data matric functions in view %v err = %w", id.Name(), err)) } - // TODO (SNOW-1348118 - next pr) - // changeSchedule := make([]sdk.ViewModifyDataMetricFunction, 0, len(addedRaw)) - // for i := range addedRaw { - // if addedRaw[i].ScheduleStatus != "" { - // expectedStatus, err := sdk.ToAllowedDataMetricScheduleStatusOption(addedRaw[i].ScheduleStatus) - // if err != nil { - // return diag.FromErr(err) - // } - // var statusCmd sdk.ViewDataMetricScheduleStatusOperationOption - // switch expectedStatus { - // case sdk.DataMetricScheduleStatusStarted: - // statusCmd = sdk.ViewDataMetricScheduleStatusOperationResume - // case sdk.DataMetricScheduleStatusSuspended: - // statusCmd = sdk.ViewDataMetricScheduleStatusOperationSuspend - // default: - // return diag.FromErr(fmt.Errorf("unexpected data metric function status: %v", expectedStatus)) - // } - // changeSchedule = append(changeSchedule, sdk.ViewModifyDataMetricFunction{ - // DataMetricFunction: addedRaw[i].DataMetricFunction, - // On: addedRaw[i].On, - // ViewDataMetricScheduleStatusOperationOption: statusCmd, - // }) - // } - // } - // if len(changeSchedule) > 0 { - // err = client.Views.Alter(ctx, sdk.NewAlterViewRequest(id).WithModifyDataMetricFunction(*sdk.NewViewModifyDataMetricFunctionsRequest(changeSchedule))) - // if err != nil { - // return diag.FromErr(fmt.Errorf("error adding data matric functions in view %v err = %w", id.Name(), err)) - // } - // } + changeSchedule := make([]sdk.ViewModifyDataMetricFunction, 0, len(addedRaw)) + for i := range addedRaw { + if addedRaw[i].ScheduleStatus != "" { + expectedStatus, err := sdk.ToAllowedDataMetricScheduleStatusOption(addedRaw[i].ScheduleStatus) + if err != nil { + return diag.FromErr(err) + } + var statusCmd sdk.ViewDataMetricScheduleStatusOperationOption + switch expectedStatus { + case sdk.DataMetricScheduleStatusStarted: + statusCmd = sdk.ViewDataMetricScheduleStatusOperationResume + case sdk.DataMetricScheduleStatusSuspended: + statusCmd = sdk.ViewDataMetricScheduleStatusOperationSuspend + default: + return diag.FromErr(fmt.Errorf("unexpected data metric function status: %v", expectedStatus)) + } + changeSchedule = append(changeSchedule, sdk.ViewModifyDataMetricFunction{ + DataMetricFunction: addedRaw[i].DataMetricFunction, + On: addedRaw[i].On, + ViewDataMetricScheduleStatusOperationOption: statusCmd, + }) + } + } + if len(changeSchedule) > 0 { + err = client.Views.Alter(ctx, sdk.NewAlterViewRequest(id).WithModifyDataMetricFunction(*sdk.NewViewModifyDataMetricFunctionsRequest(changeSchedule))) + if err != nil { + return diag.FromErr(fmt.Errorf("error adding data matric functions in view %v err = %w", id.Name(), err)) + } + } } return ReadView(false)(ctx, d, meta) } } -func extractPolicyWithColumns(v any, columnsKey string) (sdk.SchemaObjectIdentifier, []sdk.Column, error) { +func extractColumns(v any) ([]sdk.ViewColumnRequest, error) { + _, ok := v.([]any) + if v == nil || !ok { + return nil, fmt.Errorf("unable to extract columns, input is either nil or non expected type (%T): %v", v, v) + } + columns := make([]sdk.ViewColumnRequest, len(v.([]any))) + for i, columnConfigRaw := range v.([]any) { + columnConfig, ok := columnConfigRaw.(map[string]any) + if !ok { + return nil, fmt.Errorf("unable to extract column, non expected type of %T: %v", columnConfigRaw, columnConfigRaw) + } + + columnName, ok := columnConfig["column_name"] + if !ok { + return nil, fmt.Errorf("unable to extract column, missing column_name key in column") + } + columnsReq := *sdk.NewViewColumnRequest(columnName.(string)) + + projectionPolicy, ok := columnConfig["projection_policy"] + if ok && len(projectionPolicy.([]any)) > 0 { + projectionPolicyId, _, err := extractPolicyWithColumnsSet(projectionPolicy, "") + if err != nil { + return nil, err + } + columnsReq.WithProjectionPolicy(*sdk.NewViewColumnProjectionPolicyRequest(projectionPolicyId)) + } + + maskingPolicy, ok := columnConfig["masking_policy"] + if ok && len(maskingPolicy.([]any)) > 0 { + maskingPolicyId, maskingPolicyColumns, err := extractPolicyWithColumnsList(maskingPolicy, "using") + if err != nil { + return nil, err + } + columnsReq.WithMaskingPolicy(*sdk.NewViewColumnMaskingPolicyRequest(maskingPolicyId).WithUsing(maskingPolicyColumns)) + } + + comment, ok := columnConfig["comment"] + if ok && len(comment.(string)) > 0 { + columnsReq.WithComment(comment.(string)) + } + + columns[i] = columnsReq + } + return columns, nil +} + +func extractPolicyWithColumnsSet(v any, columnsKey string) (sdk.SchemaObjectIdentifier, []sdk.Column, error) { policyConfig := v.([]any)[0].(map[string]any) id, err := sdk.ParseSchemaObjectIdentifier(policyConfig["policy_name"].(string)) if err != nil { return sdk.SchemaObjectIdentifier{}, nil, err } + if policyConfig[columnsKey] == nil { + return id, nil, nil + } columnsRaw := expandStringList(policyConfig[columnsKey].(*schema.Set).List()) columns := make([]sdk.Column, len(columnsRaw)) for i := range columnsRaw { @@ -502,6 +564,23 @@ func extractPolicyWithColumns(v any, columnsKey string) (sdk.SchemaObjectIdentif return id, columns, nil } +func extractPolicyWithColumnsList(v any, columnsKey string) (sdk.SchemaObjectIdentifier, []sdk.Column, error) { + policyConfig := v.([]any)[0].(map[string]any) + id, err := sdk.ParseSchemaObjectIdentifier(policyConfig["policy_name"].(string)) + if err != nil { + return sdk.SchemaObjectIdentifier{}, nil, err + } + if policyConfig[columnsKey] == nil { + return id, nil, fmt.Errorf("unable to extract policy with column list, unable to find columnsKey: %s", columnsKey) + } + columnsRaw := expandStringList(policyConfig[columnsKey].([]any)) + columns := make([]sdk.Column, len(columnsRaw)) + for i := range columnsRaw { + columns[i] = sdk.Column{Value: columnsRaw[i]} + } + return id, columns, nil +} + func ReadView(withExternalChangesMarking bool) schema.ReadContextFunc { return func(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { client := meta.(*provider.Context).Client @@ -558,8 +637,11 @@ func ReadView(withExternalChangesMarking bool) schema.ReadContextFunc { }); err != nil { return diag.FromErr(err) } - - err = handlePolicyReferences(ctx, client, id, d) + policyRefs, err := client.PolicyReferences.GetForEntity(ctx, sdk.NewGetForEntityPolicyReferenceRequest(id, sdk.PolicyEntityDomainView)) + if err != nil { + return diag.FromErr(fmt.Errorf("getting policy references for view: %w", err)) + } + err = handlePolicyReferences(policyRefs, d) if err != nil { return diag.FromErr(err) } @@ -588,6 +670,10 @@ func ReadView(withExternalChangesMarking bool) schema.ReadContextFunc { if err = d.Set(DescribeOutputAttributeName, schemas.ViewDescriptionToSchema(describeResult)); err != nil { return diag.FromErr(err) } + err = handleColumns(d, describeResult, policyRefs) + if err != nil { + return diag.FromErr(err) + } } if err = d.Set(ShowOutputAttributeName, []map[string]any{schemas.ViewToSchema(view)}); err != nil { @@ -597,11 +683,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: %w", err) - } +func handlePolicyReferences(policyRefs []sdk.PolicyReference, d *schema.ResourceData) error { var aggregationPolicies []map[string]any var rowAccessPolicies []map[string]any for _, p := range policyRefs { @@ -626,16 +708,16 @@ func handlePolicyReferences(ctx context.Context, client *sdk.Client, id sdk.Sche "on": on, }) default: - log.Printf("[WARN] unexpected policy kind %v in policy references returned from Snowflake", p.PolicyKind) + log.Printf("[DEBUG] unexpected policy kind %v in policy references returned from Snowflake", p.PolicyKind) } } - if err = d.Set("aggregation_policy", aggregationPolicies); err != nil { + if err := d.Set("aggregation_policy", aggregationPolicies); err != nil { return err } - if err = d.Set("row_access_policy", rowAccessPolicies); err != nil { + if err := d.Set("row_access_policy", rowAccessPolicies); err != nil { return err } - return err + return nil } func handleDataMetricFunctions(ctx context.Context, client *sdk.Client, id sdk.SchemaObjectIdentifier, d *schema.ResourceData) error { @@ -654,22 +736,21 @@ func handleDataMetricFunctions(ctx context.Context, client *sdk.Client, id sdk.S for _, v := range dmfRef.RefArguments { columns = append(columns, v.Name) } - // TODO (SNOW-1348118 - next pr) - // var scheduleStatus sdk.DataMetricScheduleStatusOption - // status, err := sdk.ToDataMetricScheduleStatusOption(dmfRef.ScheduleStatus) - // if err != nil { - // return err - // } - // if slices.Contains(sdk.AllDataMetricScheduleStatusStartedOptions, status) { - // scheduleStatus = sdk.DataMetricScheduleStatusStarted - // } - // if slices.Contains(sdk.AllDataMetricScheduleStatusSuspendedOptions, status) { - // scheduleStatus = sdk.DataMetricScheduleStatusSuspended - // } + var scheduleStatus sdk.DataMetricScheduleStatusOption + status, err := sdk.ToDataMetricScheduleStatusOption(dmfRef.ScheduleStatus) + if err != nil { + return err + } + if slices.Contains(sdk.AllDataMetricScheduleStatusStartedOptions, status) { + scheduleStatus = sdk.DataMetricScheduleStatusStarted + } + if slices.Contains(sdk.AllDataMetricScheduleStatusSuspendedOptions, status) { + scheduleStatus = sdk.DataMetricScheduleStatusSuspended + } dataMetricFunctions[i] = map[string]any{ - "function_name": dmfName.FullyQualifiedName(), - "on": columns, - // "schedule_status": string(scheduleStatus), + "function_name": dmfName.FullyQualifiedName(), + "on": columns, + "schedule_status": string(scheduleStatus), } schedule = dmfRef.Schedule } @@ -684,6 +765,57 @@ func handleDataMetricFunctions(ctx context.Context, client *sdk.Client, id sdk.S }) } +func handleColumns(d ResourceValueSetter, columns []sdk.ViewDetails, policyRefs []sdk.PolicyReference) error { + if len(columns) == 0 { + return d.Set("column", nil) + } + columnsRaw := make([]map[string]any, len(columns)) + for i, column := range columns { + columnsRaw[i] = map[string]any{ + "column_name": column.Name, + } + if column.Comment != nil { + columnsRaw[i]["comment"] = *column.Comment + } else { + columnsRaw[i]["comment"] = nil + } + projectionPolicy, err := collections.FindFirst(policyRefs, func(r sdk.PolicyReference) bool { + return r.PolicyKind == sdk.PolicyKindProjectionPolicy && r.RefColumnName != nil && *r.RefColumnName == column.Name + }) + if err == nil { + if projectionPolicy.PolicyDb != nil && projectionPolicy.PolicySchema != nil { + columnsRaw[i]["projection_policy"] = []map[string]any{ + { + "policy_name": sdk.NewSchemaObjectIdentifier(*projectionPolicy.PolicyDb, *projectionPolicy.PolicySchema, projectionPolicy.PolicyName).FullyQualifiedName(), + }, + } + } else { + log.Printf("could not store projection policy name: policy db and schema can not be empty") + } + } + maskingPolicy, err := collections.FindFirst(policyRefs, func(r sdk.PolicyReference) bool { + return r.PolicyKind == sdk.PolicyKindMaskingPolicy && r.RefColumnName != nil && *r.RefColumnName == column.Name + }) + if err == nil { + if maskingPolicy.PolicyDb != nil && maskingPolicy.PolicySchema != nil { + var usingArgs []string + if maskingPolicy.RefArgColumnNames != nil { + usingArgs = sdk.ParseCommaSeparatedStringArray(*maskingPolicy.RefArgColumnNames, true) + } + columnsRaw[i]["masking_policy"] = []map[string]any{ + { + "policy_name": sdk.NewSchemaObjectIdentifier(*maskingPolicy.PolicyDb, *maskingPolicy.PolicySchema, maskingPolicy.PolicyName).FullyQualifiedName(), + "using": append([]string{*maskingPolicy.RefColumnName}, usingArgs...), + }, + } + } else { + log.Printf("could not store masking policy name: policy db and schema can not be empty") + } + } + } + return d.Set("column", columnsRaw) +} + type ViewDataMetricFunctionConfig struct { DataMetricFunction sdk.SchemaObjectIdentifier On []sdk.Column @@ -705,8 +837,7 @@ func extractDataMetricFunctions(v any) (dmfs []ViewDataMetricFunctionConfig, err dmfs = append(dmfs, ViewDataMetricFunctionConfig{ DataMetricFunction: id, On: columns, - // TODO (SNOW-1348118 - next pr) - // ScheduleStatus: config["schedule_status"].(string), + ScheduleStatus: config["schedule_status"].(string), }) } return @@ -730,8 +861,8 @@ 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") { - log.Printf("[DEBUG] Detected change on %q, recreating...", changedKeys(d, []string{"statement", "is_temporary", "is_recursive", "copy_grant"})) + if d.HasChange("statement") || d.HasChange("is_temporary") || d.HasChange("is_recursive") || d.HasChange("copy_grant") || d.HasChange("column") { + log.Printf("[DEBUG] Detected change on %q, recreating...", changedKeys(d, []string{"statement", "is_temporary", "is_recursive", "copy_grant", "column"})) return CreateView(true)(ctx, d, meta) } @@ -820,20 +951,37 @@ func UpdateView(ctx context.Context, d *schema.ResourceData, meta any) diag.Diag if d.HasChange("data_metric_function") { old, new := d.GetChange("data_metric_function") removedRaw, addedRaw := old.(*schema.Set).List(), new.(*schema.Set).List() - addedConfig, err := extractDataMetricFunctions(addedRaw) + addedConfigs, err := extractDataMetricFunctions(addedRaw) if err != nil { return diag.FromErr(err) } - removedConfig, err := extractDataMetricFunctions(removedRaw) + removedConfigs, err := extractDataMetricFunctions(removedRaw) if err != nil { return diag.FromErr(err) } - if len(removedConfig) > 0 { - removed := make([]sdk.ViewDataMetricFunction, len(removedConfig)) - for i := range removedConfig { + + addedConfigsCopy := slices.Clone(addedConfigs) + statusChangeConfig := make([]ViewDataMetricFunctionConfig, 0) + + for i, addedConfig := range addedConfigsCopy { + removedConfigDeleteIndex := slices.IndexFunc(removedConfigs, func(removedConfig ViewDataMetricFunctionConfig) bool { + return slices.Equal(addedConfig.On, removedConfig.On) && + addedConfig.DataMetricFunction.FullyQualifiedName() == removedConfig.DataMetricFunction.FullyQualifiedName() && + addedConfig.ScheduleStatus != removedConfig.ScheduleStatus + }) + if removedConfigDeleteIndex != -1 { + addedConfigs = append(addedConfigs[:i], addedConfigs[i+1:]...) + removedConfigs = append(removedConfigs[:removedConfigDeleteIndex], removedConfigs[removedConfigDeleteIndex+1:]...) + statusChangeConfig = append(statusChangeConfig, addedConfigsCopy[i]) + } + } + + if len(removedConfigs) > 0 { + removed := make([]sdk.ViewDataMetricFunction, len(removedConfigs)) + for i := range removedConfigs { removed[i] = sdk.ViewDataMetricFunction{ - DataMetricFunction: removedConfig[i].DataMetricFunction, - On: removedConfig[i].On, + DataMetricFunction: removedConfigs[i].DataMetricFunction, + On: removedConfigs[i].On, } } err := client.Views.Alter(ctx, sdk.NewAlterViewRequest(id).WithDropDataMetricFunction(*sdk.NewViewDropDataMetricFunctionRequest(removed))) @@ -841,12 +989,13 @@ func UpdateView(ctx context.Context, d *schema.ResourceData, meta any) diag.Diag return diag.FromErr(fmt.Errorf("error adding data matric functions in view %v err = %w", id.Name(), err)) } } - if len(addedConfig) > 0 { - added := make([]sdk.ViewDataMetricFunction, len(addedConfig)) - for i := range addedConfig { + + if len(addedConfigs) > 0 { + added := make([]sdk.ViewDataMetricFunction, len(addedConfigs)) + for i := range addedConfigs { added[i] = sdk.ViewDataMetricFunction{ - DataMetricFunction: addedConfig[i].DataMetricFunction, - On: addedConfig[i].On, + DataMetricFunction: addedConfigs[i].DataMetricFunction, + On: addedConfigs[i].On, } } err := client.Views.Alter(ctx, sdk.NewAlterViewRequest(id).WithAddDataMetricFunction(*sdk.NewViewAddDataMetricFunctionRequest(added))) @@ -854,6 +1003,36 @@ func UpdateView(ctx context.Context, d *schema.ResourceData, meta any) diag.Diag return diag.FromErr(fmt.Errorf("error adding data matric functions in view %v err = %w", id.Name(), err)) } } + + if len(statusChangeConfig) > 0 { + changeSchedule := make([]sdk.ViewModifyDataMetricFunction, 0, len(statusChangeConfig)) + for i := range statusChangeConfig { + if statusChangeConfig[i].ScheduleStatus != "" { + expectedStatus, err := sdk.ToAllowedDataMetricScheduleStatusOption(statusChangeConfig[i].ScheduleStatus) + if err != nil { + return diag.FromErr(err) + } + var statusCmd sdk.ViewDataMetricScheduleStatusOperationOption + switch expectedStatus { + case sdk.DataMetricScheduleStatusStarted: + statusCmd = sdk.ViewDataMetricScheduleStatusOperationResume + case sdk.DataMetricScheduleStatusSuspended: + statusCmd = sdk.ViewDataMetricScheduleStatusOperationSuspend + default: + return diag.FromErr(fmt.Errorf("unexpected data metric function status: %v", expectedStatus)) + } + changeSchedule = append(changeSchedule, sdk.ViewModifyDataMetricFunction{ + DataMetricFunction: statusChangeConfig[i].DataMetricFunction, + On: statusChangeConfig[i].On, + ViewDataMetricScheduleStatusOperationOption: statusCmd, + }) + } + } + err = client.Views.Alter(ctx, sdk.NewAlterViewRequest(id).WithModifyDataMetricFunction(*sdk.NewViewModifyDataMetricFunctionsRequest(changeSchedule))) + if err != nil { + return diag.FromErr(fmt.Errorf("error adding data matric functions in view %v err = %w", id.Name(), err)) + } + } } if d.HasChange("row_access_policy") { @@ -862,14 +1041,14 @@ func UpdateView(ctx context.Context, d *schema.ResourceData, meta any) diag.Diag oldRaw, newRaw := d.GetChange("row_access_policy") if len(oldRaw.([]any)) > 0 { - oldId, _, err := extractPolicyWithColumns(oldRaw, "on") + oldId, _, err := extractPolicyWithColumnsSet(oldRaw, "on") if err != nil { return diag.FromErr(err) } dropReq = sdk.NewViewDropRowAccessPolicyRequest(oldId) } if len(newRaw.([]any)) > 0 { - newId, newColumns, err := extractPolicyWithColumns(newRaw, "on") + newId, newColumns, err := extractPolicyWithColumnsSet(newRaw, "on") if err != nil { return diag.FromErr(err) } @@ -890,7 +1069,7 @@ 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 { - newId, newColumns, err := extractPolicyWithColumns(v, "entity_key") + newId, newColumns, err := extractPolicyWithColumnsSet(v, "entity_key") if err != nil { return diag.FromErr(err) } diff --git a/pkg/resources/view_acceptance_test.go b/pkg/resources/view_acceptance_test.go index 8c5cd24590..0a41c72ebe 100644 --- a/pkg/resources/view_acceptance_test.go +++ b/pkg/resources/view_acceptance_test.go @@ -5,6 +5,11 @@ import ( "regexp" "testing" + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/acceptance/bettertestspoc/assert/objectassert" + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/internal/collections" + + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/acceptance/helpers/random" + 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" @@ -45,7 +50,8 @@ func TestAcc_View_basic(t *testing.T) { functionId := sdk.NewSchemaObjectIdentifier("SNOWFLAKE", "CORE", "AVG") function2Id := sdk.NewSchemaObjectIdentifier("SNOWFLAKE", "CORE", "MAX") - cron, cron2 := "10 * * * * UTC", "20 * * * * UTC" + cron := "10 * * * * UTC" + cron2 := "20 * * * * UTC" id := acc.TestClient().Ids.RandomSchemaObjectIdentifier() resourceId := helpers.EncodeResourceIdentifier(id) @@ -56,27 +62,52 @@ func TestAcc_View_basic(t *testing.T) { t.Cleanup(tableCleanup) statement := fmt.Sprintf("SELECT id, foo FROM %s", table.ID().FullyQualifiedName()) otherStatement := fmt.Sprintf("SELECT foo, id FROM %s", table.ID().FullyQualifiedName()) - comment := "Terraform test resource'" + comment := random.Comment() - viewModel := model.View("test", id.DatabaseName(), id.Name(), id.SchemaName(), statement) - viewModelWithDependency := model.View("test", id.DatabaseName(), id.Name(), id.SchemaName(), statement) + // generators currently don't handle lists of objects, so use the old way + basicView := func(configStatement string) config.Variables { + return config.Variables{ + "name": config.StringVariable(id.Name()), + "database": config.StringVariable(id.DatabaseName()), + "schema": config.StringVariable(id.SchemaName()), + "statement": config.StringVariable(configStatement), + "columns": config.SetVariable( + config.MapVariable(map[string]config.Variable{ + "column_name": config.StringVariable("ID"), + }), + config.MapVariable(map[string]config.Variable{ + "column_name": config.StringVariable("FOO"), + }), + ), + } + } + basicViewWithIsRecursive := basicView(otherStatement) + basicViewWithIsRecursive["is_recursive"] = config.BoolVariable(true) - // generators currently don't handle lists, so use the old way + // generators currently don't handle lists of objects, so use the old way basicUpdate := func(rap, ap, functionId sdk.SchemaObjectIdentifier, statement, cron string, scheduleStatus sdk.DataMetricScheduleStatusOption) 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("ID")), - "aggregation_policy": config.StringVariable(ap.FullyQualifiedName()), - "aggregation_policy_entity_key": config.ListVariable(config.StringVariable("ID")), - "data_metric_function": config.StringVariable(functionId.FullyQualifiedName()), - "data_metric_function_on": config.ListVariable(config.StringVariable("ID")), - "data_metric_function_schedule_status": config.StringVariable(string(scheduleStatus)), - "data_metric_schedule_using_cron": config.StringVariable(cron), - "comment": config.StringVariable(comment), + "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("ID")), + "aggregation_policy": config.StringVariable(ap.FullyQualifiedName()), + "aggregation_policy_entity_key": config.ListVariable(config.StringVariable("ID")), + "data_metric_function": config.StringVariable(functionId.FullyQualifiedName()), + "data_metric_function_on": config.ListVariable(config.StringVariable("ID")), + "data_metric_schedule_using_cron": config.StringVariable(cron), + "comment": config.StringVariable(comment), + "schedule_status": config.StringVariable(string(scheduleStatus)), + "columns": config.SetVariable( + config.MapVariable(map[string]config.Variable{ + "column_name": config.StringVariable("ID"), + }), + config.MapVariable(map[string]config.Variable{ + "column_name": config.StringVariable("FOO"), + }), + ), } } @@ -89,24 +120,31 @@ func TestAcc_View_basic(t *testing.T) { Steps: []resource.TestStep{ // without optionals { - Config: accconfig.FromModel(t, viewModelWithDependency), - Check: assert.AssertThat(t, resourceassert.ViewResource(t, "snowflake_view.test"). - HasNameString(id.Name()). - HasStatementString(statement). - HasDatabaseString(id.DatabaseName()). - HasSchemaString(id.SchemaName())), + ConfigDirectory: acc.ConfigurationDirectory("TestAcc_View/basic"), + ConfigVariables: basicView(statement), + Check: assert.AssertThat(t, + resourceassert.ViewResource(t, "snowflake_view.test"). + HasNameString(id.Name()). + HasStatementString(statement). + HasDatabaseString(id.DatabaseName()). + HasSchemaString(id.SchemaName()). + HasColumnLength(2), + ), }, // import - without optionals { - Config: accconfig.FromModel(t, viewModel), - ResourceName: "snowflake_view.test", - ImportState: true, - ImportStateCheck: assert.AssertThatImport(t, assert.CheckImport(importchecks.TestCheckResourceAttrInstanceState(resourceId, "name", id.Name())), + ConfigDirectory: acc.ConfigurationDirectory("TestAcc_View/basic"), + ConfigVariables: basicView(statement), + ResourceName: "snowflake_view.test", + ImportState: true, + ImportStateCheck: assert.AssertThatImport(t, resourceassert.ImportedViewResource(t, resourceId). HasNameString(id.Name()). HasDatabaseString(id.DatabaseName()). HasSchemaString(id.SchemaName()). - HasStatementString(statement)), + HasStatementString(statement). + HasColumnLength(2), + ), }, // set policies and dmfs externally { @@ -121,16 +159,18 @@ func TestAcc_View_basic(t *testing.T) { }, }))) }, - 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")), - assert.Check(resource.TestCheckResourceAttr("snowflake_view.test", "data_metric_schedule.#", "0")), - assert.Check(resource.TestCheckResourceAttr("snowflake_view.test", "data_metric_function.#", "0")), + ConfigDirectory: acc.ConfigurationDirectory("TestAcc_View/basic"), + ConfigVariables: basicView(statement), + Check: assert.AssertThat(t, + resourceassert.ViewResource(t, "snowflake_view.test"). + HasNameString(id.Name()). + HasStatementString(statement). + HasDatabaseString(id.DatabaseName()). + HasSchemaString(id.SchemaName()). + HasAggregationPolicyLength(0). + HasRowAccessPolicyLength(0). + HasDataMetricScheduleLength(0). + HasDataMetricFunctionLength(0), ), }, // set other fields @@ -147,19 +187,19 @@ func TestAcc_View_basic(t *testing.T) { HasStatementString(statement). HasDatabaseString(id.DatabaseName()). HasSchemaString(id.SchemaName()). - HasCommentString(comment), - assert.Check(resource.TestCheckResourceAttr("snowflake_view.test", "aggregation_policy.#", "1")), + HasCommentString(comment). + HasAggregationPolicyLength(1). + HasRowAccessPolicyLength(1). + HasDataMetricScheduleLength(1). + HasDataMetricFunctionLength(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")), - assert.Check(resource.TestCheckResourceAttr("snowflake_view.test", "data_metric_schedule.#", "1")), assert.Check(resource.TestCheckResourceAttr("snowflake_view.test", "data_metric_schedule.0.using_cron", cron)), assert.Check(resource.TestCheckResourceAttr("snowflake_view.test", "data_metric_schedule.0.minutes", "0")), - assert.Check(resource.TestCheckResourceAttr("snowflake_view.test", "data_metric_function.#", "1")), assert.Check(resource.TestCheckResourceAttr("snowflake_view.test", "data_metric_function.0.function_name", functionId.FullyQualifiedName())), assert.Check(resource.TestCheckResourceAttr("snowflake_view.test", "data_metric_function.0.on.#", "1")), assert.Check(resource.TestCheckResourceAttr("snowflake_view.test", "data_metric_function.0.on.0", "ID")), @@ -174,20 +214,20 @@ func TestAcc_View_basic(t *testing.T) { HasStatementString(statement). HasDatabaseString(id.DatabaseName()). HasSchemaString(id.SchemaName()). - HasCommentString(comment), - assert.Check(resource.TestCheckResourceAttr("snowflake_view.test", "aggregation_policy.#", "1")), + HasCommentString(comment). + HasAggregationPolicyLength(1). + HasRowAccessPolicyLength(1). + HasDataMetricScheduleLength(1). + HasDataMetricFunctionLength(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", "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", 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", "ID")), - assert.Check(resource.TestCheckResourceAttr("snowflake_view.test", "data_metric_schedule.#", "1")), assert.Check(resource.TestCheckResourceAttr("snowflake_view.test", "data_metric_schedule.0.using_cron", cron2)), - assert.Check(resource.TestCheckResourceAttr("snowflake_view.test", "data_metric_schedule.0.schedule_status", string(sdk.DataMetricScheduleStatusStarted))), assert.Check(resource.TestCheckResourceAttr("snowflake_view.test", "data_metric_schedule.0.minutes", "0")), - assert.Check(resource.TestCheckResourceAttr("snowflake_view.test", "data_metric_function.#", "1")), + assert.Check(resource.TestCheckResourceAttr("snowflake_view.test", "data_metric_function.0.schedule_status", string(sdk.DataMetricScheduleStatusStarted))), assert.Check(resource.TestCheckResourceAttr("snowflake_view.test", "data_metric_function.0.function_name", function2Id.FullyQualifiedName())), assert.Check(resource.TestCheckResourceAttr("snowflake_view.test", "data_metric_function.0.on.#", "1")), assert.Check(resource.TestCheckResourceAttr("snowflake_view.test", "data_metric_function.0.on.0", "ID")), @@ -202,20 +242,20 @@ func TestAcc_View_basic(t *testing.T) { HasStatementString(statement). HasDatabaseString(id.DatabaseName()). HasSchemaString(id.SchemaName()). - HasCommentString(comment), - assert.Check(resource.TestCheckResourceAttr("snowflake_view.test", "aggregation_policy.#", "1")), + HasCommentString(comment). + HasAggregationPolicyLength(1). + HasRowAccessPolicyLength(1). + HasDataMetricScheduleLength(1). + HasDataMetricFunctionLength(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", "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", 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", "ID")), - assert.Check(resource.TestCheckResourceAttr("snowflake_view.test", "data_metric_schedule.#", "1")), assert.Check(resource.TestCheckResourceAttr("snowflake_view.test", "data_metric_schedule.0.using_cron", cron2)), - assert.Check(resource.TestCheckResourceAttr("snowflake_view.test", "data_metric_schedule.0.schedule_status", string(sdk.DataMetricScheduleStatusSuspended))), assert.Check(resource.TestCheckResourceAttr("snowflake_view.test", "data_metric_schedule.0.minutes", "0")), - assert.Check(resource.TestCheckResourceAttr("snowflake_view.test", "data_metric_function.#", "1")), + assert.Check(resource.TestCheckResourceAttr("snowflake_view.test", "data_metric_function.0.schedule_status", string(sdk.DataMetricScheduleStatusSuspended))), assert.Check(resource.TestCheckResourceAttr("snowflake_view.test", "data_metric_function.0.function_name", function2Id.FullyQualifiedName())), assert.Check(resource.TestCheckResourceAttr("snowflake_view.test", "data_metric_function.0.on.#", "1")), assert.Check(resource.TestCheckResourceAttr("snowflake_view.test", "data_metric_function.0.on.0", "ID")), @@ -230,19 +270,19 @@ func TestAcc_View_basic(t *testing.T) { HasStatementString(otherStatement). HasDatabaseString(id.DatabaseName()). HasSchemaString(id.SchemaName()). - HasCommentString(comment), - assert.Check(resource.TestCheckResourceAttr("snowflake_view.test", "aggregation_policy.#", "1")), + HasCommentString(comment). + HasAggregationPolicyLength(1). + HasRowAccessPolicyLength(1). + HasDataMetricScheduleLength(1). + HasDataMetricFunctionLength(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")), - assert.Check(resource.TestCheckResourceAttr("snowflake_view.test", "data_metric_schedule.#", "1")), assert.Check(resource.TestCheckResourceAttr("snowflake_view.test", "data_metric_schedule.0.using_cron", cron)), assert.Check(resource.TestCheckResourceAttr("snowflake_view.test", "data_metric_schedule.0.minutes", "0")), - assert.Check(resource.TestCheckResourceAttr("snowflake_view.test", "data_metric_function.#", "1")), assert.Check(resource.TestCheckResourceAttr("snowflake_view.test", "data_metric_function.0.function_name", functionId.FullyQualifiedName())), assert.Check(resource.TestCheckResourceAttr("snowflake_view.test", "data_metric_function.0.on.#", "1")), assert.Check(resource.TestCheckResourceAttr("snowflake_view.test", "data_metric_function.0.on.0", "ID")), @@ -260,19 +300,19 @@ func TestAcc_View_basic(t *testing.T) { HasStatementString(otherStatement). HasDatabaseString(id.DatabaseName()). HasSchemaString(id.SchemaName()). - HasCommentString(comment), - assert.Check(resource.TestCheckResourceAttr("snowflake_view.test", "aggregation_policy.#", "1")), + HasCommentString(comment). + HasAggregationPolicyLength(1). + HasRowAccessPolicyLength(1). + HasDataMetricScheduleLength(1). + HasDataMetricFunctionLength(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")), - assert.Check(resource.TestCheckResourceAttr("snowflake_view.test", "data_metric_schedule.#", "1")), assert.Check(resource.TestCheckResourceAttr("snowflake_view.test", "data_metric_schedule.0.using_cron", cron)), assert.Check(resource.TestCheckResourceAttr("snowflake_view.test", "data_metric_schedule.0.minutes", "0")), - assert.Check(resource.TestCheckResourceAttr("snowflake_view.test", "data_metric_function.#", "1")), assert.Check(resource.TestCheckResourceAttr("snowflake_view.test", "data_metric_function.0.function_name", functionId.FullyQualifiedName())), assert.Check(resource.TestCheckResourceAttr("snowflake_view.test", "data_metric_function.0.on.#", "1")), assert.Check(resource.TestCheckResourceAttr("snowflake_view.test", "data_metric_function.0.on.0", "ID")), @@ -291,25 +331,24 @@ func TestAcc_View_basic(t *testing.T) { HasStatementString(otherStatement). HasDatabaseString(id.DatabaseName()). HasSchemaString(id.SchemaName()). - HasCommentString(comment), - assert.Check(resource.TestCheckResourceAttr("snowflake_view.test", "aggregation_policy.#", "1")), + HasCommentString(comment). + HasAggregationPolicyLength(1). + HasRowAccessPolicyLength(1). + HasDataMetricScheduleLength(1). + HasDataMetricFunctionLength(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")), - assert.Check(resource.TestCheckResourceAttr("snowflake_view.test", "data_metric_schedule.#", "1")), assert.Check(resource.TestCheckResourceAttr("snowflake_view.test", "data_metric_schedule.0.using_cron", cron)), assert.Check(resource.TestCheckResourceAttr("snowflake_view.test", "data_metric_schedule.0.minutes", "0")), - assert.Check(resource.TestCheckResourceAttr("snowflake_view.test", "data_metric_function.#", "1")), assert.Check(resource.TestCheckResourceAttr("snowflake_view.test", "data_metric_function.0.function_name", functionId.FullyQualifiedName())), assert.Check(resource.TestCheckResourceAttr("snowflake_view.test", "data_metric_function.0.on.#", "1")), assert.Check(resource.TestCheckResourceAttr("snowflake_view.test", "data_metric_function.0.on.0", "ID")), ), }, - // import - with optionals { ConfigDirectory: acc.ConfigurationDirectory("TestAcc_View/basic_update"), @@ -325,12 +364,12 @@ func TestAcc_View_basic(t *testing.T) { HasCommentString(comment). HasIsSecureString("false"). HasIsTemporaryString("false"). - HasChangeTrackingString("false"), - assert.CheckImport(importchecks.TestCheckResourceAttrInstanceState(resourceId, "aggregation_policy.#", "1")), + HasChangeTrackingString("false"). + HasAggregationPolicyLength(1). + HasRowAccessPolicyLength(1), assert.CheckImport(importchecks.TestCheckResourceAttrInstanceState(resourceId, "aggregation_policy.0.policy_name", aggregationPolicy.FullyQualifiedName())), assert.CheckImport(importchecks.TestCheckResourceAttrInstanceState(resourceId, "aggregation_policy.0.entity_key.#", "1")), assert.CheckImport(importchecks.TestCheckResourceAttrInstanceState(resourceId, "aggregation_policy.0.entity_key.0", "ID")), - assert.CheckImport(importchecks.TestCheckResourceAttrInstanceState(resourceId, "row_access_policy.#", "1")), assert.CheckImport(importchecks.TestCheckResourceAttrInstanceState(resourceId, "row_access_policy.0.policy_name", rowAccessPolicy.ID().FullyQualifiedName())), assert.CheckImport(importchecks.TestCheckResourceAttrInstanceState(resourceId, "row_access_policy.0.on.#", "1")), assert.CheckImport(importchecks.TestCheckResourceAttrInstanceState(resourceId, "row_access_policy.0.on.0", "ID")), @@ -338,23 +377,25 @@ func TestAcc_View_basic(t *testing.T) { }, // unset { - Config: accconfig.FromModel(t, viewModel.WithStatement(otherStatement)), - ResourceName: "snowflake_view.test", + ConfigDirectory: acc.ConfigurationDirectory("TestAcc_View/basic"), + ConfigVariables: basicView(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.#")), - assert.Check(resource.TestCheckNoResourceAttr("snowflake_view.test", "data_metric_schedule.#")), - assert.Check(resource.TestCheckNoResourceAttr("snowflake_view.test", "data_metric_function.#")), + HasCommentString(""). + HasNoAggregationPolicyByLength(). + HasNoRowAccessPolicyByLength(). + HasNoDataMetricScheduleByLength(). + HasNoDataMetricFunctionByLength(), ), }, // recreate - change is_recursive { - Config: accconfig.FromModel(t, viewModel.WithIsRecursive("true")), + ConfigDirectory: acc.ConfigurationDirectory("TestAcc_View/basic_is_recursive"), + ConfigVariables: basicViewWithIsRecursive, Check: assert.AssertThat(t, resourceassert.ViewResource(t, "snowflake_view.test"). HasNameString(id.Name()). HasStatementString(otherStatement). @@ -363,11 +404,11 @@ func TestAcc_View_basic(t *testing.T) { 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.#")), - assert.Check(resource.TestCheckNoResourceAttr("snowflake_view.test", "data_metric_schedule.#")), - assert.Check(resource.TestCheckNoResourceAttr("snowflake_view.test", "data_metric_function.#")), + HasChangeTrackingString("default"). + HasNoAggregationPolicyByLength(). + HasNoRowAccessPolicyByLength(). + HasNoDataMetricScheduleByLength(). + HasNoDataMetricFunctionByLength(), ), }, }, @@ -380,7 +421,21 @@ 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) + basicView := config.Variables{ + "name": config.StringVariable(id.Name()), + "database": config.StringVariable(id.DatabaseName()), + "schema": config.StringVariable(id.SchemaName()), + "statement": config.StringVariable(statement), + "is_recursive": config.BoolVariable(true), + "columns": config.SetVariable( + config.MapVariable(map[string]config.Variable{ + "column_name": config.StringVariable("ROLE_NAME"), + }), + config.MapVariable(map[string]config.Variable{ + "column_name": config.StringVariable("ROLE_OWNER"), + }), + ), + } resource.Test(t, resource.TestCase{ ProtoV6ProviderFactories: acc.TestAccProtoV6ProviderFactories, @@ -390,7 +445,8 @@ func TestAcc_View_recursive(t *testing.T) { CheckDestroy: acc.CheckDestroy(t, resources.View), Steps: []resource.TestStep{ { - Config: accconfig.FromModel(t, viewModel.WithIsRecursive("true")), + ConfigDirectory: acc.ConfigurationDirectory("TestAcc_View/basic_is_recursive"), + ConfigVariables: basicView, Check: assert.AssertThat(t, resourceassert.ViewResource(t, "snowflake_view.test"). HasNameString(id.Name()). HasStatementString(statement). @@ -399,10 +455,11 @@ func TestAcc_View_recursive(t *testing.T) { HasIsRecursiveString("true")), }, { - Config: accconfig.FromModel(t, viewModel.WithIsRecursive("true")), - ResourceName: "snowflake_view.test", - ImportState: true, - ImportStateCheck: assert.AssertThatImport(t, assert.CheckImport(importchecks.TestCheckResourceAttrInstanceState(helpers.EncodeResourceIdentifier(id), "name", id.Name())), + ConfigDirectory: acc.ConfigurationDirectory("TestAcc_View/basic_is_recursive"), + ConfigVariables: basicView, + ResourceName: "snowflake_view.test", + ImportState: true, + ImportStateCheck: assert.AssertThatImport(t, resourceassert.ImportedViewResource(t, helpers.EncodeResourceIdentifier(id)). HasNameString(id.Name()). HasDatabaseString(id.DatabaseName()). @@ -464,7 +521,27 @@ func TestAcc_View_complete(t *testing.T) { projectionPolicy, projectionPolicyCleanup := acc.TestClient().ProjectionPolicy.CreateProjectionPolicy(t) t.Cleanup(projectionPolicyCleanup) - maskingPolicy, maskingPolicyCleanup := acc.TestClient().MaskingPolicy.CreateMaskingPolicyIdentity(t, sdk.DataTypeNumber) + maskingPolicy, maskingPolicyCleanup := acc.TestClient().MaskingPolicy.CreateMaskingPolicyWithOptions(t, + acc.TestClient().Ids.SchemaId(), + []sdk.TableColumnSignature{ + { + Name: "One", + Type: sdk.DataTypeNumber, + }, + { + Name: "Two", + Type: sdk.DataTypeNumber, + }, + }, + sdk.DataTypeNumber, + ` +case + when One > 0 then One + else Two +end;; +`, + new(sdk.CreateMaskingPolicyOptions), + ) t.Cleanup(maskingPolicyCleanup) functionId := sdk.NewSchemaObjectIdentifier("SNOWFLAKE", "CORE", "AVG") @@ -485,10 +562,12 @@ func TestAcc_View_complete(t *testing.T) { "aggregation_policy_entity_key": config.ListVariable(config.StringVariable("ID")), "statement": config.StringVariable(statement), "warehouse": config.StringVariable(acc.TestWarehouseName), - "column_name": config.StringVariable("ID"), - "masking_policy": config.StringVariable(maskingPolicy.ID().FullyQualifiedName()), - "masking_policy_using": config.ListVariable(config.StringVariable("ID")), - "projection_policy": config.StringVariable(projectionPolicy.FullyQualifiedName()), + "column1_name": config.StringVariable("ID"), + "column1_comment": config.StringVariable("col comment"), + "column2_name": config.StringVariable("FOO"), + "column2_masking_policy": config.StringVariable(maskingPolicy.ID().FullyQualifiedName()), + "column2_masking_policy_using": config.ListVariable(config.StringVariable("FOO"), config.StringVariable("ID")), + "column2_projection_policy": config.StringVariable(projectionPolicy.FullyQualifiedName()), "data_metric_function": config.StringVariable(functionId.FullyQualifiedName()), "data_metric_function_on": config.ListVariable(config.StringVariable("ID")), "data_metric_schedule_using_cron": config.StringVariable("5 * * * * UTC"), @@ -512,22 +591,32 @@ func TestAcc_View_complete(t *testing.T) { HasCommentString("Terraform test resource"). HasIsSecureString("true"). HasIsTemporaryString("false"). - HasChangeTrackingString("true"), - assert.Check(resource.TestCheckResourceAttr("snowflake_view.test", "data_metric_schedule.#", "1")), + HasChangeTrackingString("true"). + HasDataMetricScheduleLength(1). + HasDataMetricFunctionLength(1). + HasAggregationPolicyLength(1). + HasRowAccessPolicyLength(1). + HasColumnLength(2), assert.Check(resource.TestCheckResourceAttr("snowflake_view.test", "data_metric_schedule.0.using_cron", "5 * * * * UTC")), assert.Check(resource.TestCheckResourceAttr("snowflake_view.test", "data_metric_schedule.0.minutes", "0")), - assert.Check(resource.TestCheckResourceAttr("snowflake_view.test", "data_metric_function.#", "1")), assert.Check(resource.TestCheckResourceAttr("snowflake_view.test", "data_metric_function.0.function_name", functionId.FullyQualifiedName())), assert.Check(resource.TestCheckResourceAttr("snowflake_view.test", "data_metric_function.0.on.#", "1")), assert.Check(resource.TestCheckResourceAttr("snowflake_view.test", "data_metric_function.0.on.0", "ID")), - 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")), + assert.Check(resource.TestCheckResourceAttr("snowflake_view.test", "column.0.column_name", "ID")), + assert.Check(resource.TestCheckResourceAttr("snowflake_view.test", "column.0.masking_policy.#", "0")), + assert.Check(resource.TestCheckResourceAttr("snowflake_view.test", "column.0.projection_policy.#", "0")), + assert.Check(resource.TestCheckResourceAttr("snowflake_view.test", "column.0.comment", "col comment")), + assert.Check(resource.TestCheckResourceAttr("snowflake_view.test", "column.1.column_name", "FOO")), + assert.Check(resource.TestCheckResourceAttr("snowflake_view.test", "column.1.masking_policy.#", "1")), + assert.Check(resource.TestCheckResourceAttr("snowflake_view.test", "column.1.masking_policy.0.policy_name", maskingPolicy.ID().FullyQualifiedName())), + assert.Check(resource.TestCheckResourceAttr("snowflake_view.test", "column.1.projection_policy.#", "1")), + assert.Check(resource.TestCheckResourceAttr("snowflake_view.test", "column.1.projection_policy.0.policy_name", projectionPolicy.FullyQualifiedName())), resourceshowoutputassert.ViewShowOutput(t, "snowflake_view.test"). HasName(id.Name()). HasDatabaseName(id.DatabaseName()). @@ -550,19 +639,20 @@ func TestAcc_View_complete(t *testing.T) { HasSchemaString(id.SchemaName()). HasCommentString("Terraform test resource"). HasIsSecureString("true"). - HasIsTemporaryString("false").HasChangeTrackingString("true"), - assert.CheckImport(importchecks.TestCheckResourceAttrInstanceState(resourceId, "data_metric_schedule.#", "1")), + HasIsTemporaryString("false"). + HasChangeTrackingString("true"). + HasDataMetricScheduleLength(1). + HasDataMetricFunctionLength(1). + HasAggregationPolicyLength(1). + HasRowAccessPolicyLength(1), assert.CheckImport(importchecks.TestCheckResourceAttrInstanceState(resourceId, "data_metric_schedule.0.using_cron", "5 * * * * UTC")), assert.CheckImport(importchecks.TestCheckResourceAttrInstanceState(resourceId, "data_metric_schedule.0.minutes", "0")), - assert.CheckImport(importchecks.TestCheckResourceAttrInstanceState(resourceId, "data_metric_function.#", "1")), assert.CheckImport(importchecks.TestCheckResourceAttrInstanceState(resourceId, "data_metric_function.0.function_name", functionId.FullyQualifiedName())), assert.CheckImport(importchecks.TestCheckResourceAttrInstanceState(resourceId, "data_metric_function.0.on.#", "1")), assert.CheckImport(importchecks.TestCheckResourceAttrInstanceState(resourceId, "data_metric_function.0.on.0", "ID")), - assert.CheckImport(importchecks.TestCheckResourceAttrInstanceState(resourceId, "aggregation_policy.#", "1")), assert.CheckImport(importchecks.TestCheckResourceAttrInstanceState(resourceId, "aggregation_policy.0.policy_name", aggregationPolicy.FullyQualifiedName())), assert.CheckImport(importchecks.TestCheckResourceAttrInstanceState(resourceId, "aggregation_policy.0.entity_key.#", "1")), assert.CheckImport(importchecks.TestCheckResourceAttrInstanceState(resourceId, "aggregation_policy.0.entity_key.0", "ID")), - assert.CheckImport(importchecks.TestCheckResourceAttrInstanceState(resourceId, "row_access_policy.#", "1")), assert.CheckImport(importchecks.TestCheckResourceAttrInstanceState(resourceId, "row_access_policy.0.policy_name", rowAccessPolicy.ID().FullyQualifiedName())), assert.CheckImport(importchecks.TestCheckResourceAttrInstanceState(resourceId, "row_access_policy.0.on.#", "1")), assert.CheckImport(importchecks.TestCheckResourceAttrInstanceState(resourceId, "row_access_policy.0.on.0", "ID")), @@ -572,13 +662,174 @@ func TestAcc_View_complete(t *testing.T) { }) } +func TestAcc_View_columns(t *testing.T) { + t.Setenv(string(testenvs.ConfigureClientOnce), "") + _ = testenvs.GetOrSkipTest(t, testenvs.EnableAcceptance) + acc.TestAccPreCheck(t) + + id := acc.TestClient().Ids.RandomSchemaObjectIdentifier() + table, tableCleanup := acc.TestClient().Table.CreateTableWithColumns(t, []sdk.TableColumnRequest{ + *sdk.NewTableColumnRequest("id", sdk.DataTypeNumber), + *sdk.NewTableColumnRequest("foo", sdk.DataTypeNumber), + *sdk.NewTableColumnRequest("bar", sdk.DataTypeNumber), + }) + t.Cleanup(tableCleanup) + statement := fmt.Sprintf("SELECT id, foo FROM %s", table.ID().FullyQualifiedName()) + + maskingPolicy, maskingPolicyCleanup := acc.TestClient().MaskingPolicy.CreateMaskingPolicyWithOptions(t, + acc.TestClient().Ids.SchemaId(), + []sdk.TableColumnSignature{ + { + Name: "One", + Type: sdk.DataTypeNumber, + }, + }, + sdk.DataTypeNumber, + ` +case + when One > 0 then One + else 0 +end;; +`, + new(sdk.CreateMaskingPolicyOptions), + ) + t.Cleanup(maskingPolicyCleanup) + + projectionPolicy, projectionPolicyCleanup := acc.TestClient().ProjectionPolicy.CreateProjectionPolicy(t) + t.Cleanup(projectionPolicyCleanup) + + // generators currently don't handle lists of objects, so use the old way + basicView := func(columns ...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), + "columns": config.SetVariable( + collections.Map(columns, func(columnName string) config.Variable { + return config.MapVariable(map[string]config.Variable{ + "column_name": config.StringVariable(columnName), + }) + })..., + ), + } + } + + basicViewWithPolicies := func() config.Variables { + conf := basicView("ID", "FOO") + delete(conf, "columns") + conf["projection_name"] = config.StringVariable(projectionPolicy.FullyQualifiedName()) + conf["masking_name"] = config.StringVariable(maskingPolicy.ID().FullyQualifiedName()) + conf["masking_using"] = config.ListVariable(config.StringVariable("ID")) + return conf + } + + 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{ + // Columns without policies + { + ConfigDirectory: acc.ConfigurationDirectory("TestAcc_View/basic"), + ConfigVariables: basicView("ID", "FOO"), + Check: assert.AssertThat(t, + resourceassert.ViewResource(t, "snowflake_view.test"). + HasNameString(id.Name()). + HasStatementString(statement). + HasDatabaseString(id.DatabaseName()). + HasSchemaString(id.SchemaName()). + HasColumnLength(2), + ), + }, + // Columns with policies added externally + { + ConfigDirectory: acc.ConfigurationDirectory("TestAcc_View/basic"), + ConfigVariables: basicView("ID", "FOO"), + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectResourceAction("snowflake_view.test", plancheck.ResourceActionUpdate), + }, + }, + PreConfig: func() { + acc.TestClient().View.Alter(t, sdk.NewAlterViewRequest(id).WithSetMaskingPolicyOnColumn(*sdk.NewViewSetColumnMaskingPolicyRequest("ID", maskingPolicy.ID()).WithUsing([]sdk.Column{{Value: "ID"}}))) + acc.TestClient().View.Alter(t, sdk.NewAlterViewRequest(id).WithSetProjectionPolicyOnColumn(*sdk.NewViewSetProjectionPolicyRequest("ID", projectionPolicy).WithForce(true))) + }, + Check: assert.AssertThat(t, + resourceassert.ViewResource(t, "snowflake_view.test"). + HasNameString(id.Name()). + HasStatementString(statement). + HasDatabaseString(id.DatabaseName()). + HasSchemaString(id.SchemaName()). + HasColumnLength(2), + objectassert.View(t, id). + HasNoMaskingPolicyReferences(acc.TestClient()). + HasNoProjectionPolicyReferences(acc.TestClient()), + ), + }, + // With all policies on columns + { + ConfigDirectory: acc.ConfigurationDirectory("TestAcc_View/columns"), + ConfigVariables: basicViewWithPolicies(), + Check: assert.AssertThat(t, + resourceassert.ViewResource(t, "snowflake_view.test"). + HasNameString(id.Name()). + HasStatementString(statement). + HasDatabaseString(id.DatabaseName()). + HasSchemaString(id.SchemaName()). + HasColumnLength(2), + objectassert.View(t, id). + HasMaskingPolicyReferences(acc.TestClient(), 1). + HasProjectionPolicyReferences(acc.TestClient(), 1), + ), + }, + // Remove policies on columns externally + { + ConfigDirectory: acc.ConfigurationDirectory("TestAcc_View/columns"), + ConfigVariables: basicViewWithPolicies(), + PreConfig: func() { + acc.TestClient().View.Alter(t, sdk.NewAlterViewRequest(id).WithUnsetMaskingPolicyOnColumn(*sdk.NewViewUnsetColumnMaskingPolicyRequest("ID"))) + acc.TestClient().View.Alter(t, sdk.NewAlterViewRequest(id).WithUnsetProjectionPolicyOnColumn(*sdk.NewViewUnsetProjectionPolicyRequest("ID"))) + }, + Check: assert.AssertThat(t, + resourceassert.ViewResource(t, "snowflake_view.test"). + HasNameString(id.Name()). + HasStatementString(statement). + HasDatabaseString(id.DatabaseName()). + HasSchemaString(id.SchemaName()). + HasColumnLength(2), + objectassert.View(t, id). + HasMaskingPolicyReferences(acc.TestClient(), 1). + HasProjectionPolicyReferences(acc.TestClient(), 1), + ), + }, + }, + }) +} + 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() - viewModel := model.View("test", id.DatabaseName(), id.Name(), id.SchemaName(), statement).WithComment("foo") - newViewModel := model.View("test", newId.DatabaseName(), newId.Name(), newId.SchemaName(), statement).WithComment("foo") + viewConfig := func(identifier sdk.SchemaObjectIdentifier) config.Variables { + return config.Variables{ + "name": config.StringVariable(identifier.Name()), + "database": config.StringVariable(identifier.DatabaseName()), + "schema": config.StringVariable(identifier.SchemaName()), + "statement": config.StringVariable(statement), + "columns": config.SetVariable( + config.MapVariable(map[string]config.Variable{ + "column_name": config.StringVariable("ROLE_NAME"), + }), + config.MapVariable(map[string]config.Variable{ + "column_name": config.StringVariable("ROLE_OWNER"), + }), + ), + } + } resource.Test(t, resource.TestCase{ ProtoV6ProviderFactories: acc.TestAccProtoV6ProviderFactories, @@ -589,16 +840,17 @@ func TestAcc_View_Rename(t *testing.T) { CheckDestroy: acc.CheckDestroy(t, resources.View), Steps: []resource.TestStep{ { - Config: accconfig.FromModel(t, viewModel), + ConfigDirectory: acc.ConfigurationDirectory("TestAcc_View/basic"), + ConfigVariables: viewConfig(id), Check: resource.ComposeAggregateTestCheckFunc( resource.TestCheckResourceAttr("snowflake_view.test", "name", id.Name()), - resource.TestCheckResourceAttr("snowflake_view.test", "comment", "foo"), resource.TestCheckResourceAttr("snowflake_view.test", "fully_qualified_name", id.FullyQualifiedName()), ), }, // rename with one param changed { - Config: accconfig.FromModel(t, newViewModel), + ConfigDirectory: acc.ConfigurationDirectory("TestAcc_View/basic"), + ConfigVariables: viewConfig(newId), ConfigPlanChecks: resource.ConfigPlanChecks{ PreApply: []plancheck.PlanCheck{ plancheck.ExpectResourceAction("snowflake_view.test", plancheck.ResourceActionUpdate), @@ -606,7 +858,6 @@ func TestAcc_View_Rename(t *testing.T) { }, Check: resource.ComposeAggregateTestCheckFunc( resource.TestCheckResourceAttr("snowflake_view.test", "name", newId.Name()), - resource.TestCheckResourceAttr("snowflake_view.test", "comment", "foo"), resource.TestCheckResourceAttr("snowflake_view.test", "fully_qualified_name", newId.FullyQualifiedName()), ), }, @@ -619,7 +870,24 @@ 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").WithCopyGrants(false) + viewConfig := func(copyGrants bool) config.Variables { + return config.Variables{ + "name": config.StringVariable(id.Name()), + "database": config.StringVariable(id.DatabaseName()), + "schema": config.StringVariable(id.SchemaName()), + "statement": config.StringVariable(statement), + "copy_grants": config.BoolVariable(copyGrants), + "is_secure": config.BoolVariable(true), + "columns": config.SetVariable( + config.MapVariable(map[string]config.Variable{ + "column_name": config.StringVariable("ID"), + }), + config.MapVariable(map[string]config.Variable{ + "column_name": config.StringVariable("FOO"), + }), + ), + } + } var createdOn string @@ -632,7 +900,8 @@ func TestAcc_ViewChangeCopyGrants(t *testing.T) { CheckDestroy: acc.CheckDestroy(t, resources.View), Steps: []resource.TestStep{ { - Config: accconfig.FromModel(t, viewModel), + ConfigDirectory: acc.ConfigurationDirectory("TestAcc_View/basic_copy_grants"), + ConfigVariables: viewConfig(false), Check: resource.ComposeAggregateTestCheckFunc( resource.TestCheckResourceAttr("snowflake_view.test", "name", id.Name()), resource.TestCheckResourceAttr("snowflake_view.test", "database", id.DatabaseName()), @@ -647,7 +916,8 @@ func TestAcc_ViewChangeCopyGrants(t *testing.T) { }, // Checks that copy_grants changes don't trigger a drop { - Config: accconfig.FromModel(t, viewModel.WithCopyGrants(true)), + ConfigDirectory: acc.ConfigurationDirectory("TestAcc_View/basic_copy_grants"), + ConfigVariables: viewConfig(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 { @@ -668,7 +938,25 @@ 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").WithCopyGrants(true) + viewConfig := func(copyGrants bool) config.Variables { + return config.Variables{ + "name": config.StringVariable(id.Name()), + "database": config.StringVariable(id.DatabaseName()), + "schema": config.StringVariable(id.SchemaName()), + "statement": config.StringVariable(statement), + "copy_grants": config.BoolVariable(copyGrants), + "is_secure": config.BoolVariable(true), + "columns": config.SetVariable( + config.MapVariable(map[string]config.Variable{ + "column_name": config.StringVariable("ID"), + }), + config.MapVariable(map[string]config.Variable{ + "column_name": config.StringVariable("FOO"), + }), + ), + } + } + var createdOn string resource.Test(t, resource.TestCase{ @@ -680,7 +968,8 @@ func TestAcc_ViewChangeCopyGrantsReversed(t *testing.T) { CheckDestroy: acc.CheckDestroy(t, resources.View), Steps: []resource.TestStep{ { - Config: accconfig.FromModel(t, viewModel), + ConfigDirectory: acc.ConfigurationDirectory("TestAcc_View/basic_copy_grants"), + ConfigVariables: viewConfig(true), Check: resource.ComposeAggregateTestCheckFunc( resource.TestCheckResourceAttr("snowflake_view.test", "copy_grants", "true"), resource.TestCheckResourceAttr("snowflake_view.test", "show_output.#", "1"), @@ -692,7 +981,8 @@ func TestAcc_ViewChangeCopyGrantsReversed(t *testing.T) { ), }, { - Config: accconfig.FromModel(t, viewModel.WithCopyGrants(false)), + ConfigDirectory: acc.ConfigurationDirectory("TestAcc_View/basic_copy_grants"), + ConfigVariables: viewConfig(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 { @@ -741,30 +1031,6 @@ 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) - 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{ - { - Config: accconfig.FromModel(t, viewModel.WithCopyGrants(true)), - Check: resource.ComposeAggregateTestCheckFunc( - resource.TestCheckResourceAttr("snowflake_view.test", "name", id.Name()), - resource.TestCheckResourceAttr("snowflake_view.test", "copy_grants", "true"), - ), - }, - }, - }) -} - func TestAcc_View_Issue2640(t *testing.T) { t.Setenv(string(testenvs.ConfigureClientOnce), "") id := acc.TestClient().Ids.RandomSchemaObjectIdentifier() @@ -827,7 +1093,20 @@ 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) + viewConfig := config.Variables{ + "name": config.StringVariable(id.Name()), + "database": config.StringVariable(id.DatabaseName()), + "schema": config.StringVariable(id.SchemaName()), + "statement": config.StringVariable(statement), + "columns": config.SetVariable( + config.MapVariable(map[string]config.Variable{ + "column_name": config.StringVariable("ROLE_NAME"), + }), + config.MapVariable(map[string]config.Variable{ + "column_name": config.StringVariable("ROLE_OWNER"), + }), + ), + } tag, tagCleanup := acc.TestClient().Tag.CreateTag(t) t.Cleanup(tagCleanup) @@ -856,7 +1135,8 @@ func TestAcc_view_migrateFromVersion_0_94_1(t *testing.T) { }, { ProtoV6ProviderFactories: acc.TestAccProtoV6ProviderFactories, - Config: accconfig.FromModel(t, viewModel), + ConfigDirectory: acc.ConfigurationDirectory("TestAcc_View/basic"), + ConfigVariables: viewConfig, Check: resource.ComposeAggregateTestCheckFunc( resource.TestCheckResourceAttr(resourceName, "name", id.Name()), resource.TestCheckNoResourceAttr(resourceName, "tag.#"), @@ -907,6 +1187,10 @@ resource "snowflake_view" "test" { statement = "select %[5]s from \"%[1]s\".\"%[2]s\".\"${snowflake_table.table.name}\"" copy_grants = true is_secure = true + + column { + column_name = "%[5]s" + } } resource "snowflake_account_role" "test" { @@ -944,6 +1228,12 @@ resource "snowflake_view" "test" { %[5]s SQL is_secure = true + column { + column_name = "ROLE_OWNER" + } + column { + column_name = "ROLE_NAME" + } } `, id.DatabaseName(), id.SchemaName(), id.Name(), part1, part2) } diff --git a/pkg/resources/view_test.go b/pkg/resources/view_test.go new file mode 100644 index 0000000000..875fd01391 --- /dev/null +++ b/pkg/resources/view_test.go @@ -0,0 +1,348 @@ +package resources + +import ( + "fmt" + "reflect" + "testing" + + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/sdk" + "github.com/stretchr/testify/assert" +) + +type testResourceValueSetter struct { + internalMap map[string]any +} + +func newTestResourceValueSetter() *testResourceValueSetter { + return &testResourceValueSetter{ + internalMap: make(map[string]any), + } +} + +func (s *testResourceValueSetter) Set(key string, value any) error { + s.internalMap[key] = value + return nil +} + +func Test_handleColumns(t *testing.T) { + testCases := []struct { + InputColumns []sdk.ViewDetails + InputPolicyReferences []sdk.PolicyReference + Expected map[string]any + }{ + { + InputColumns: []sdk.ViewDetails{}, + InputPolicyReferences: []sdk.PolicyReference{}, + Expected: map[string]any{ + "column": nil, + }, + }, + { + InputColumns: []sdk.ViewDetails{ + { + Name: "name", + Comment: nil, + }, + }, + InputPolicyReferences: []sdk.PolicyReference{}, + Expected: map[string]any{ + "column": []map[string]any{ + { + "column_name": "name", + "comment": nil, + }, + }, + }, + }, + { + InputColumns: []sdk.ViewDetails{ + { + Name: "name", + Comment: sdk.String("comment"), + }, + }, + InputPolicyReferences: []sdk.PolicyReference{}, + Expected: map[string]any{ + "column": []map[string]any{ + { + "column_name": "name", + "comment": "comment", + }, + }, + }, + }, + { + InputColumns: []sdk.ViewDetails{ + { + Name: "name", + Comment: sdk.String("comment"), + }, + { + Name: "name2", + Comment: sdk.String("comment2"), + }, + }, + InputPolicyReferences: []sdk.PolicyReference{}, + Expected: map[string]any{ + "column": []map[string]any{ + { + "column_name": "name", + "comment": "comment", + }, + { + "column_name": "name2", + "comment": "comment2", + }, + }, + }, + }, + { + InputColumns: []sdk.ViewDetails{ + { + Name: "name", + Comment: sdk.String("comment"), + }, + { + Name: "name2", + Comment: sdk.String("comment2"), + }, + }, + InputPolicyReferences: []sdk.PolicyReference{ + { + PolicyDb: sdk.String("db"), + PolicySchema: sdk.String("sch"), + PolicyName: "policyName", + PolicyKind: sdk.PolicyKindProjectionPolicy, + RefColumnName: sdk.String("name"), + }, + }, + Expected: map[string]any{ + "column": []map[string]any{ + { + "column_name": "name", + "comment": "comment", + "projection_policy": []map[string]any{ + { + "policy_name": sdk.NewSchemaObjectIdentifier("db", "sch", "policyName").FullyQualifiedName(), + }, + }, + }, + { + "column_name": "name2", + "comment": "comment2", + }, + }, + }, + }, + { + InputColumns: []sdk.ViewDetails{ + { + Name: "name", + Comment: sdk.String("comment"), + }, + { + Name: "name2", + Comment: sdk.String("comment2"), + }, + }, + InputPolicyReferences: []sdk.PolicyReference{ + { + PolicyDb: sdk.String("db"), + PolicySchema: sdk.String("sch"), + PolicyName: "policyName", + PolicyKind: sdk.PolicyKindProjectionPolicy, + RefColumnName: sdk.String("name"), + }, + { + PolicyDb: sdk.String("db"), + PolicySchema: sdk.String("sch"), + PolicyName: "policyName2", + PolicyKind: sdk.PolicyKindMaskingPolicy, + RefColumnName: sdk.String("name"), + RefArgColumnNames: sdk.String("[one,two]"), + }, + }, + Expected: map[string]any{ + "column": []map[string]any{ + { + "column_name": "name", + "comment": "comment", + "projection_policy": []map[string]any{ + { + "policy_name": sdk.NewSchemaObjectIdentifier("db", "sch", "policyName").FullyQualifiedName(), + }, + }, + "masking_policy": []map[string]any{ + { + "policy_name": sdk.NewSchemaObjectIdentifier("db", "sch", "policyName2").FullyQualifiedName(), + "using": []string{"name", "one", "two"}, + }, + }, + }, + { + "column_name": "name2", + "comment": "comment2", + }, + }, + }, + }, + } + + for i, tc := range testCases { + t.Run(fmt.Sprintf("handle columns(%d): %v - %v", i, tc.InputColumns, tc.InputPolicyReferences), func(t *testing.T) { + valueSetter := newTestResourceValueSetter() + err := handleColumns(valueSetter, tc.InputColumns, tc.InputPolicyReferences) + assert.Nil(t, err) + assert.Equal(t, tc.Expected, valueSetter.internalMap) + }) + } +} + +func Test_extractColumns(t *testing.T) { + testCases := []struct { + Input any + Expected []sdk.ViewColumnRequest + Error string + }{ + { + Input: "", + Error: "unable to extract columns, input is either nil or non expected type (string): ", + }, + { + Input: nil, + Error: "unable to extract columns, input is either nil or non expected type (): ", + }, + { + Input: []any{""}, + Error: "unable to extract column, non expected type of string: ", + }, + { + Input: []any{ + map[string]any{}, + }, + Error: "unable to extract column, missing column_name key in column", + }, + { + Input: []any{ + map[string]any{ + "column_name": "abc", + }, + }, + Expected: []sdk.ViewColumnRequest{ + *sdk.NewViewColumnRequest("abc"), + }, + }, + { + Input: []any{ + map[string]any{ + "column_name": "abc", + }, + map[string]any{ + "column_name": "cba", + }, + }, + Expected: []sdk.ViewColumnRequest{ + *sdk.NewViewColumnRequest("abc"), + *sdk.NewViewColumnRequest("cba"), + }, + }, + { + Input: []any{ + map[string]any{ + "column_name": "abc", + "projection_policy": []any{ + map[string]any{ + "policy_name": "db.sch.proj", + }, + }, + "masking_policy": []any{ + map[string]any{ + "policy_name": "db.sch.mask", + "using": []any{"one", "two"}, + }, + }, + }, + map[string]any{ + "column_name": "cba", + }, + }, + Expected: []sdk.ViewColumnRequest{ + *sdk.NewViewColumnRequest("abc"). + WithProjectionPolicy(*sdk.NewViewColumnProjectionPolicyRequest(sdk.NewSchemaObjectIdentifier("db", "sch", "proj"))). + WithMaskingPolicy(*sdk.NewViewColumnMaskingPolicyRequest(sdk.NewSchemaObjectIdentifier("db", "sch", "mask")).WithUsing([]sdk.Column{{Value: "one"}, {Value: "two"}})), + *sdk.NewViewColumnRequest("cba"), + }, + }, + } + + for i, tc := range testCases { + t.Run(fmt.Sprintf("%d: %s", i, tc.Input), func(t *testing.T) { + req, err := extractColumns(tc.Input) + + if tc.Error != "" { + assert.Nil(t, req) + assert.NotNil(t, err) + assert.Contains(t, err.Error(), tc.Error) + } else { + assert.True(t, reflect.DeepEqual(tc.Expected, req)) + assert.Nil(t, err) + } + }) + } +} + +func Test_extractPolicyWithColumnsList(t *testing.T) { + testCases := []struct { + Input any + ColumnKey string + ExpectedId sdk.SchemaObjectIdentifier + ExpectedColumns []sdk.Column + Error string + }{ + { + Input: []any{ + map[string]any{ + "policy_name": "db.sch.pol", + "using": []any{"one", "two"}, + }, + }, + ColumnKey: "non-existing", + Error: "unable to extract policy with column list, unable to find columnsKey: non-existing", + }, + { + Input: []any{ + map[string]any{ + "policy_name": "db.sch.pol", + }, + }, + ColumnKey: "using", + Error: "unable to extract policy with column list, unable to find columnsKey: using", + }, + { + Input: []any{ + map[string]any{ + "policy_name": "db.sch.pol", + "using": []any{"one", "two"}, + }, + }, + ColumnKey: "using", + ExpectedId: sdk.NewSchemaObjectIdentifier("db", "sch", "pol"), + ExpectedColumns: []sdk.Column{{Value: "one"}, {Value: "two"}}, + }, + } + + for i, tc := range testCases { + t.Run(fmt.Sprintf("%d: %s", i, tc.Input), func(t *testing.T) { + id, cols, err := extractPolicyWithColumnsList(tc.Input, tc.ColumnKey) + + if tc.Error != "" { + assert.NotNil(t, err) + assert.Contains(t, err.Error(), tc.Error) + } else { + assert.Nil(t, err) + assert.Equal(t, tc.ExpectedId, id) + assert.Equal(t, tc.ExpectedColumns, cols) + } + }) + } +} diff --git a/pkg/sdk/data_metric_function_references_def.go b/pkg/sdk/data_metric_function_references_def.go index ea24d761b3..1de2263ac6 100644 --- a/pkg/sdk/data_metric_function_references_def.go +++ b/pkg/sdk/data_metric_function_references_def.go @@ -46,6 +46,7 @@ var AllDataMetricScheduleStatusSuspendedOptions = []DataMetricScheduleStatusOpti DataMetricScheduleStatusSuspendedTableColumnDoesNotExistOrNotAuthorized, DataMetricScheduleStatusSuspendedInsufficientPrivilegeToExecuteDataMetricFunction, DataMetricScheduleStatusSuspendedActiveEventTableDoesNotExistOrNotAuthorized, + DataMetricScheduleStatusSuspendedByUserAction, } func ToAllowedDataMetricScheduleStatusOption(s string) (DataMetricScheduleStatusOption, error) { diff --git a/pkg/sdk/policy_references.go b/pkg/sdk/policy_references.go index 8decc63793..8dbc6dfdf9 100644 --- a/pkg/sdk/policy_references.go +++ b/pkg/sdk/policy_references.go @@ -75,6 +75,7 @@ const ( PolicyKindRowAccessPolicy PolicyKind = "ROW_ACCESS_POLICY" PolicyKindPasswordPolicy PolicyKind = "PASSWORD_POLICY" PolicyKindMaskingPolicy PolicyKind = "MASKING_POLICY" + PolicyKindProjectionPolicy PolicyKind = "PROJECTION_POLICY" ) type PolicyReference struct { diff --git a/pkg/snowflake/parser.go b/pkg/snowflake/parser.go index 63c97f1ffc..2913e93a7b 100644 --- a/pkg/snowflake/parser.go +++ b/pkg/snowflake/parser.go @@ -2,6 +2,7 @@ package snowflake import ( "fmt" + "log" "strings" "unicode" ) @@ -42,24 +43,70 @@ func (e *ViewSelectStatementExtractor) Extract() (string, error) { e.consumeToken("if not exists") e.consumeSpace() e.consumeID() - // TODO column list + e.consumeSpace() + e.consumeColumns() e.consumeSpace() e.consumeToken("copy grants") e.consumeComment() e.consumeSpace() e.consumeComment() e.consumeSpace() - e.extractRowAccessPolicy() - e.extractAggregationPolicy() + e.consumeRowAccessPolicy() + e.consumeAggregationPolicy() e.consumeToken("as") e.consumeSpace() - fmt.Printf("[DEBUG] extracted statement %s from view query %s\n", string(e.input[e.pos:]), string(e.input)) + log.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() { +func (e *ViewSelectStatementExtractor) consumeColumns() { + ok := e.consumeToken("(") + if !ok { + return + } + for { + isLast := e.consumeColumn() + if isLast { + break + } + } +} + +func (e *ViewSelectStatementExtractor) consumeColumn() (isLast bool) { + e.consumeSpace() + e.consumeID() + if e.input[e.pos-1] == ')' { + isLast = true + } + e.consumeSpace() + ok := e.consumeToken("projection policy") + if ok { + e.consumeSpace() + e.consumeID() + if e.input[e.pos-1] == ')' { + isLast = true + } + e.consumeSpace() + } + ok = e.consumeToken("masking policy") + if ok { + e.consumeSpace() + e.consumeID() + e.consumeSpace() + e.consumeToken("using") + e.consumeSpace() + e.consumeIdentifierList() + if string(e.input[e.pos-2:e.pos]) == "))" { + isLast = true + } + e.consumeSpace() + } + return +} + +func (e *ViewSelectStatementExtractor) consumeRowAccessPolicy() { ok := e.consumeToken("row access policy") if !ok { return @@ -69,11 +116,11 @@ func (e *ViewSelectStatementExtractor) extractRowAccessPolicy() { e.consumeSpace() e.consumeToken("on") e.consumeSpace() - e.extractIdentifierList() + e.consumeIdentifierList() e.consumeSpace() } -func (e *ViewSelectStatementExtractor) extractAggregationPolicy() { +func (e *ViewSelectStatementExtractor) consumeAggregationPolicy() { ok := e.consumeToken("aggregation policy") if !ok { return @@ -83,11 +130,11 @@ func (e *ViewSelectStatementExtractor) extractAggregationPolicy() { e.consumeSpace() e.consumeToken("entity key") e.consumeSpace() - e.extractIdentifierList() + e.consumeIdentifierList() e.consumeSpace() } -func (e *ViewSelectStatementExtractor) extractIdentifierList() { +func (e *ViewSelectStatementExtractor) consumeIdentifierList() { e.consumeSpace() if !e.consumeToken("(") { return @@ -95,7 +142,7 @@ func (e *ViewSelectStatementExtractor) extractIdentifierList() { for { e.consumeSpace() e.consumeID() - if e.input[e.pos-1] == ')' { + if e.input[e.pos-1] == ')' || strings.HasSuffix(string(e.input[e.pos-2:e.pos]), "),") { break } e.consumeSpace() diff --git a/pkg/snowflake/parser_test.go b/pkg/snowflake/parser_test.go index efa03aadb6..e4b0f1627e 100644 --- a/pkg/snowflake/parser_test.go +++ b/pkg/snowflake/parser_test.go @@ -37,6 +37,7 @@ from bar;` 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` + allFields := `CREATE OR REPLACE SECURE TEMPORARY VIEW "rgdxfmnfhh"."PUBLIC"."rgdxfmnfhh" (id MASKING POLICY mp USING ("col1", "cond1") PROJECTION POLICY pp COMMENT = 'asdf', foo MASKING POLICY mp USING ("col1", "cond1")) 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 } @@ -64,6 +65,7 @@ from bar;` {"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}, + {"all fields", args{allFields}, "SELECT ROLE_NAME, ROLE_OWNER FROM INFORMATION_SCHEMA.APPLICABLE_ROLES", false}, } for _, tt := range tests { tt := tt