diff --git a/MIGRATION_GUIDE.md b/MIGRATION_GUIDE.md index 6d89286111..c0925ce848 100644 --- a/MIGRATION_GUIDE.md +++ b/MIGRATION_GUIDE.md @@ -6,11 +6,29 @@ across different versions. ## v0.94.x ➞ v0.95.0 +### snowflake_view resource changes +New fields: + - `row_access_policy` + - `aggregation_policy` + - `change_tracking` + - `is_recursive` + - `is_temporary` +- added `show_output` field that holds the response from SHOW VIEWS. +- added `describe_output` field that holds the response from DESCRIBE VIEW. Note that one needs to grant sufficient privileges e.g. with [grant_ownership](https://registry.terraform.io/providers/Snowflake-Labs/snowflake/latest/docs/resources/grant_ownership) on the tables used in this view. Otherwise, this field is not filled. + +#### *(breaking change)* Removed fields from snowflake_view resource +Removed fields: +- `tag` +The value of this field will be removed from the state automatically. Please, use [tag_association](https://registry.terraform.io/providers/Snowflake-Labs/snowflake/latest/docs/resources/tag_association) instead. + +#### *(breaking change)* Required warehouse +For this resource, the provider now uses [policy references](https://docs.snowflake.com/en/sql-reference/functions/policy_references) which requires a warehouse in the connection. Please, make sure you have either set a DEFAULT_WAREHOUSE for the user, or specified a warehouse in the provider configuration. + ### Identifier changes #### *(breaking change)* resource identifiers for schema and streamlit During [identifiers rework](https://github.com/Snowflake-Labs/terraform-provider-snowflake/blob/main/ROADMAP.md#identifiers-rework) we decided to -migrate resource ids from pipe-separated to regular Snowflake identifiers (e.g. `|` -> `"".""`). +migrate resource ids from pipe-separated to regular Snowflake identifiers (e.g. `|` -> `"".""`). Exception to that rule will be identifiers that consist of multiple parts (like in the case of [grant_privileges_to_account_role](https://registry.terraform.io/providers/Snowflake-Labs/snowflake/latest/docs/resources/grant_privileges_to_account_role#import)'s resource id). The change was applied to already refactored resources (only in the case of `snowflake_schema` and `snowflake_streamlit` this will be a breaking change, because the rest of the objects are single part identifiers in the format of ``): - `snowflake_api_authentication_integration_with_authorization_code_grant` @@ -28,7 +46,7 @@ The change was applied to already refactored resources (only in the case of `sno - `snowflake_network_policy` - `snowflake_warehouse` -No change is required, the state will be migrated automatically. +No change is required, the state will be migrated automatically. The rest of the objects will be changed when working on them during [v1 object preparations](https://github.com/Snowflake-Labs/terraform-provider-snowflake/blob/main/ROADMAP.md#preparing-essential-ga-objects-for-the-provider-v1). #### *(breaking change)* diff suppress for identifier quoting diff --git a/docs/resources/account_role.md b/docs/resources/account_role.md index fccc6781b3..1d5a3eec4e 100644 --- a/docs/resources/account_role.md +++ b/docs/resources/account_role.md @@ -33,7 +33,7 @@ resource "snowflake_account_role" "complete" { ### Required -- `name` (String) +- `name` (String) Identifier for the role; must be unique for your account. Due to technical limitations (read more [here](https://github.com/Snowflake-Labs/terraform-provider-snowflake/blob/main/docs/technical-documentation/identifiers_rework_design_decisions.md#known-limitations-and-identifier-recommendations)), avoid using the following characters: `|`, `.`, `(`, `)`, `"` ### Optional diff --git a/docs/resources/api_authentication_integration_with_authorization_code_grant.md b/docs/resources/api_authentication_integration_with_authorization_code_grant.md index 66b97d42bf..af2ad46347 100644 --- a/docs/resources/api_authentication_integration_with_authorization_code_grant.md +++ b/docs/resources/api_authentication_integration_with_authorization_code_grant.md @@ -45,7 +45,7 @@ resource "snowflake_api_authentication_integration_with_authorization_code_grant ### Required - `enabled` (Boolean) Specifies whether this security integration is enabled or disabled. -- `name` (String) Specifies the identifier (i.e. name) for the integration. This value must be unique in your account. +- `name` (String) Specifies the identifier (i.e. name) for the integration. This value must be unique in your account. Due to technical limitations (read more [here](https://github.com/Snowflake-Labs/terraform-provider-snowflake/blob/main/docs/technical-documentation/identifiers_rework_design_decisions.md#known-limitations-and-identifier-recommendations)), avoid using the following characters: `|`, `.`, `(`, `)`, `"` - `oauth_client_id` (String) Specifies the client ID for the OAuth application in the external service. - `oauth_client_secret` (String) Specifies the client secret for the OAuth application in the ServiceNow instance from the previous step. The connector uses this to request an access token from the ServiceNow instance. diff --git a/docs/resources/api_authentication_integration_with_client_credentials.md b/docs/resources/api_authentication_integration_with_client_credentials.md index 0be7ff9b3c..3813877b62 100644 --- a/docs/resources/api_authentication_integration_with_client_credentials.md +++ b/docs/resources/api_authentication_integration_with_client_credentials.md @@ -43,7 +43,7 @@ resource "snowflake_api_authentication_integration_with_client_credentials" "tes ### Required - `enabled` (Boolean) Specifies whether this security integration is enabled or disabled. -- `name` (String) Specifies the identifier (i.e. name) for the integration. This value must be unique in your account. +- `name` (String) Specifies the identifier (i.e. name) for the integration. This value must be unique in your account. Due to technical limitations (read more [here](https://github.com/Snowflake-Labs/terraform-provider-snowflake/blob/main/docs/technical-documentation/identifiers_rework_design_decisions.md#known-limitations-and-identifier-recommendations)), avoid using the following characters: `|`, `.`, `(`, `)`, `"` - `oauth_client_id` (String) Specifies the client ID for the OAuth application in the external service. - `oauth_client_secret` (String) Specifies the client secret for the OAuth application in the ServiceNow instance from the previous step. The connector uses this to request an access token from the ServiceNow instance. diff --git a/docs/resources/api_authentication_integration_with_jwt_bearer.md b/docs/resources/api_authentication_integration_with_jwt_bearer.md index 194b37d0c8..9b2024cbd4 100644 --- a/docs/resources/api_authentication_integration_with_jwt_bearer.md +++ b/docs/resources/api_authentication_integration_with_jwt_bearer.md @@ -46,7 +46,7 @@ resource "snowflake_api_authentication_integration_with_jwt_bearer" "test" { ### Required - `enabled` (Boolean) Specifies whether this security integration is enabled or disabled. -- `name` (String) Specifies the identifier (i.e. name) for the integration. This value must be unique in your account. +- `name` (String) Specifies the identifier (i.e. name) for the integration. This value must be unique in your account. Due to technical limitations (read more [here](https://github.com/Snowflake-Labs/terraform-provider-snowflake/blob/main/docs/technical-documentation/identifiers_rework_design_decisions.md#known-limitations-and-identifier-recommendations)), avoid using the following characters: `|`, `.`, `(`, `)`, `"` - `oauth_assertion_issuer` (String) - `oauth_client_id` (String) Specifies the client ID for the OAuth application in the external service. - `oauth_client_secret` (String) Specifies the client secret for the OAuth application in the ServiceNow instance from the previous step. The connector uses this to request an access token from the ServiceNow instance. diff --git a/docs/resources/database.md b/docs/resources/database.md index a5b3202b09..a8a1596e1c 100644 --- a/docs/resources/database.md +++ b/docs/resources/database.md @@ -84,7 +84,7 @@ resource "snowflake_database" "primary" { ### Required -- `name` (String) Specifies the identifier for the database; must be unique for your account. As a best practice for [Database Replication and Failover](https://docs.snowflake.com/en/user-guide/db-replication-intro), it is recommended to give each secondary database the same name as its primary database. This practice supports referencing fully-qualified objects (i.e. '..') by other objects in the same database, such as querying a fully-qualified table name in a view. If a secondary database has a different name from the primary database, then these object references would break in the secondary database. +- `name` (String) Specifies the identifier for the database; must be unique for your account. As a best practice for [Database Replication and Failover](https://docs.snowflake.com/en/user-guide/db-replication-intro), it is recommended to give each secondary database the same name as its primary database. This practice supports referencing fully-qualified objects (i.e. '..') by other objects in the same database, such as querying a fully-qualified table name in a view. If a secondary database has a different name from the primary database, then these object references would break in the secondary database. Due to technical limitations (read more [here](https://github.com/Snowflake-Labs/terraform-provider-snowflake/blob/main/docs/technical-documentation/identifiers_rework_design_decisions.md#known-limitations-and-identifier-recommendations)), avoid using the following characters: `|`, `.`, `(`, `)`, `"` ### Optional diff --git a/docs/resources/external_oauth_integration.md b/docs/resources/external_oauth_integration.md index 076dcab088..b76fc63e59 100644 --- a/docs/resources/external_oauth_integration.md +++ b/docs/resources/external_oauth_integration.md @@ -70,7 +70,7 @@ resource "snowflake_external_oauth_integration" "test" { - `external_oauth_snowflake_user_mapping_attribute` (String) Indicates which Snowflake user record attribute should be used to map the access token to a Snowflake user record. Valid values are (case-insensitive): `LOGIN_NAME` | `EMAIL_ADDRESS`. - `external_oauth_token_user_mapping_claim` (Set of String) Specifies the access token claim or claims that can be used to map the access token to a Snowflake user record. If removed from the config, the resource is recreated. - `external_oauth_type` (String) Specifies the OAuth 2.0 authorization server to be Okta, Microsoft Azure AD, Ping Identity PingFederate, or a Custom OAuth 2.0 authorization server. Valid values are (case-insensitive): `OKTA` | `AZURE` | `PING_FEDERATE` | `CUSTOM`. -- `name` (String) Specifies the name of the External Oath integration. This name follows the rules for Object Identifiers. The name should be unique among security integrations in your account. +- `name` (String) Specifies the name of the External Oath integration. This name follows the rules for Object Identifiers. The name should be unique among security integrations in your account. Due to technical limitations (read more [here](https://github.com/Snowflake-Labs/terraform-provider-snowflake/blob/main/docs/technical-documentation/identifiers_rework_design_decisions.md#known-limitations-and-identifier-recommendations)), avoid using the following characters: `|`, `.`, `(`, `)`, `"` ### Optional diff --git a/docs/resources/network_policy.md b/docs/resources/network_policy.md index 917eb7e80f..c4e991ea6a 100644 --- a/docs/resources/network_policy.md +++ b/docs/resources/network_policy.md @@ -37,7 +37,7 @@ resource "snowflake_network_policy" "basic" { ### Required -- `name` (String) Specifies the identifier for the network policy; must be unique for the account in which the network policy is created. +- `name` (String) Specifies the identifier for the network policy; must be unique for the account in which the network policy is created. Due to technical limitations (read more [here](https://github.com/Snowflake-Labs/terraform-provider-snowflake/blob/main/docs/technical-documentation/identifiers_rework_design_decisions.md#known-limitations-and-identifier-recommendations)), avoid using the following characters: `|`, `.`, `(`, `)`, `"` ### Optional diff --git a/docs/resources/oauth_integration_for_custom_clients.md b/docs/resources/oauth_integration_for_custom_clients.md index 1903adffa3..c6f427e7b3 100644 --- a/docs/resources/oauth_integration_for_custom_clients.md +++ b/docs/resources/oauth_integration_for_custom_clients.md @@ -50,7 +50,7 @@ resource "snowflake_oauth_integration_for_custom_clients" "complete" { ### Required - `blocked_roles_list` (Set of String) A set of Snowflake roles that a user cannot explicitly consent to using after authenticating. -- `name` (String) Specifies the name of the OAuth integration. This name follows the rules for Object Identifiers. The name should be unique among security integrations in your account. +- `name` (String) Specifies the name of the OAuth integration. This name follows the rules for Object Identifiers. The name should be unique among security integrations in your account. Due to technical limitations (read more [here](https://github.com/Snowflake-Labs/terraform-provider-snowflake/blob/main/docs/technical-documentation/identifiers_rework_design_decisions.md#known-limitations-and-identifier-recommendations)), avoid using the following characters: `|`, `.`, `(`, `)`, `"` - `oauth_client_type` (String) Specifies the type of client being registered. Snowflake supports both confidential and public clients. Valid options are: `PUBLIC` | `CONFIDENTIAL`. - `oauth_redirect_uri` (String) Specifies the client URI. After a user is authenticated, the web browser is redirected to this URI. diff --git a/docs/resources/oauth_integration_for_partner_applications.md b/docs/resources/oauth_integration_for_partner_applications.md index e8180af06b..ae75802d54 100644 --- a/docs/resources/oauth_integration_for_partner_applications.md +++ b/docs/resources/oauth_integration_for_partner_applications.md @@ -43,7 +43,7 @@ resource "snowflake_oauth_integration_for_partner_applications" "test" { ### Required - `blocked_roles_list` (Set of String) A set of Snowflake roles that a user cannot explicitly consent to using after authenticating. -- `name` (String) Specifies the name of the OAuth integration. This name follows the rules for Object Identifiers. The name should be unique among security integrations in your account. +- `name` (String) Specifies the name of the OAuth integration. This name follows the rules for Object Identifiers. The name should be unique among security integrations in your account. Due to technical limitations (read more [here](https://github.com/Snowflake-Labs/terraform-provider-snowflake/blob/main/docs/technical-documentation/identifiers_rework_design_decisions.md#known-limitations-and-identifier-recommendations)), avoid using the following characters: `|`, `.`, `(`, `)`, `"` - `oauth_client` (String) Creates an OAuth interface between Snowflake and a partner application. Valid options are: `LOOKER` | `TABLEAU_DESKTOP` | `TABLEAU_SERVER`. ### Optional diff --git a/docs/resources/role.md b/docs/resources/role.md index df1ef5bf86..4b20e2d2c3 100644 --- a/docs/resources/role.md +++ b/docs/resources/role.md @@ -34,7 +34,7 @@ resource "snowflake_role" "complete" { ### Required -- `name` (String) +- `name` (String) Identifier for the role; must be unique for your account. Due to technical limitations (read more [here](https://github.com/Snowflake-Labs/terraform-provider-snowflake/blob/main/docs/technical-documentation/identifiers_rework_design_decisions.md#known-limitations-and-identifier-recommendations)), avoid using the following characters: `|`, `.`, `(`, `)`, `"` ### Optional diff --git a/docs/resources/saml2_integration.md b/docs/resources/saml2_integration.md index 2aa188bc9a..133eb33f4c 100644 --- a/docs/resources/saml2_integration.md +++ b/docs/resources/saml2_integration.md @@ -53,7 +53,7 @@ resource "snowflake_saml2_integration" "test" { ### Required -- `name` (String) Specifies the name of the SAML2 integration. This name follows the rules for Object Identifiers. The name should be unique among security integrations in your account. +- `name` (String) Specifies the name of the SAML2 integration. This name follows the rules for Object Identifiers. The name should be unique among security integrations in your account. Due to technical limitations (read more [here](https://github.com/Snowflake-Labs/terraform-provider-snowflake/blob/main/docs/technical-documentation/identifiers_rework_design_decisions.md#known-limitations-and-identifier-recommendations)), avoid using the following characters: `|`, `.`, `(`, `)`, `"` - `saml2_issuer` (String) The string containing the IdP EntityID / Issuer. - `saml2_provider` (String) The string describing the IdP. Valid options are: `OKTA` | `ADFS` | `CUSTOM`. - `saml2_sso_url` (String) The string containing the IdP SSO URL, where the user should be redirected by Snowflake (the Service Provider) with a SAML AuthnRequest message. diff --git a/docs/resources/scim_integration.md b/docs/resources/scim_integration.md index fdeabe3180..19985cf3e1 100644 --- a/docs/resources/scim_integration.md +++ b/docs/resources/scim_integration.md @@ -41,7 +41,7 @@ resource "snowflake_scim_integration" "test" { ### Required - `enabled` (Boolean) Specify whether the security integration is enabled. -- `name` (String) String that specifies the identifier (i.e. name) for the integration; must be unique in your account. +- `name` (String) String that specifies the identifier (i.e. name) for the integration; must be unique in your account. Due to technical limitations (read more [here](https://github.com/Snowflake-Labs/terraform-provider-snowflake/blob/main/docs/technical-documentation/identifiers_rework_design_decisions.md#known-limitations-and-identifier-recommendations)), avoid using the following characters: `|`, `.`, `(`, `)`, `"` - `run_as_role` (String) Specify the SCIM role in Snowflake that owns any users and roles that are imported from the identity provider into Snowflake using SCIM. Provider assumes that the specified role is already provided. Valid options are: `OKTA_PROVISIONER` | `AAD_PROVISIONER` | `GENERIC_SCIM_PROVISIONER`. - `scim_client` (String) Specifies the client type for the scim integration. Valid options are: `OKTA` | `AZURE` | `GENERIC`. diff --git a/docs/resources/secondary_database.md b/docs/resources/secondary_database.md index b5c2b865cf..8e3c22cd65 100644 --- a/docs/resources/secondary_database.md +++ b/docs/resources/secondary_database.md @@ -93,7 +93,7 @@ resource "snowflake_task" "refresh_secondary_database" { ### Required - `as_replica_of` (String) A fully qualified path to a database to create a replica from. A fully qualified path follows the format of `""."".""`. -- `name` (String) Specifies the identifier for the database; must be unique for your account. As a best practice for [Database Replication and Failover](https://docs.snowflake.com/en/user-guide/db-replication-intro), it is recommended to give each secondary database the same name as its primary database. This practice supports referencing fully-qualified objects (i.e. '..') by other objects in the same database, such as querying a fully-qualified table name in a view. If a secondary database has a different name from the primary database, then these object references would break in the secondary database. +- `name` (String) Specifies the identifier for the database; must be unique for your account. As a best practice for [Database Replication and Failover](https://docs.snowflake.com/en/user-guide/db-replication-intro), it is recommended to give each secondary database the same name as its primary database. This practice supports referencing fully-qualified objects (i.e. '..') by other objects in the same database, such as querying a fully-qualified table name in a view. If a secondary database has a different name from the primary database, then these object references would break in the secondary database. Due to technical limitations (read more [here](https://github.com/Snowflake-Labs/terraform-provider-snowflake/blob/main/docs/technical-documentation/identifiers_rework_design_decisions.md#known-limitations-and-identifier-recommendations)), avoid using the following characters: `|`, `.`, `(`, `)`, `"` ### Optional diff --git a/docs/resources/shared_database.md b/docs/resources/shared_database.md index 3daced23a2..0ed981c871 100644 --- a/docs/resources/shared_database.md +++ b/docs/resources/shared_database.md @@ -78,7 +78,7 @@ resource "snowflake_shared_database" "test" { ### Required - `from_share` (String) A fully qualified path to a share from which the database will be created. A fully qualified path follows the format of `""."".""`. -- `name` (String) Specifies the identifier for the database; must be unique for your account. +- `name` (String) Specifies the identifier for the database; must be unique for your account. Due to technical limitations (read more [here](https://github.com/Snowflake-Labs/terraform-provider-snowflake/blob/main/docs/technical-documentation/identifiers_rework_design_decisions.md#known-limitations-and-identifier-recommendations)), avoid using the following characters: `|`, `.`, `(`, `)`, `"` ### Optional diff --git a/docs/resources/streamlit.md b/docs/resources/streamlit.md index 88d5e8d12f..25b6d5a28d 100644 --- a/docs/resources/streamlit.md +++ b/docs/resources/streamlit.md @@ -63,7 +63,7 @@ resource "snowflake_streamlit" "streamlit" { - `describe_output` (List of Object) Outputs the result of `DESCRIBE STREAMLIT` for the given streamlit. (see [below for nested schema](#nestedatt--describe_output)) - `fully_qualified_name` (String) Fully qualified name of the resource. For more information, see [object name resolution](https://docs.snowflake.com/en/sql-reference/name-resolution). - `id` (String) The ID of this resource. -- `show_output` (List of Object) Outputs the result of `SHOW STREAMLIT` for the given streamli. (see [below for nested schema](#nestedatt--show_output)) +- `show_output` (List of Object) Outputs the result of `SHOW STREAMLIT` for the given streamlit. (see [below for nested schema](#nestedatt--show_output)) ### Nested Schema for `describe_output` diff --git a/docs/resources/view.md b/docs/resources/view.md index 7358c37189..a53ef624b5 100644 --- a/docs/resources/view.md +++ b/docs/resources/view.md @@ -2,31 +2,64 @@ page_title: "snowflake_view Resource - terraform-provider-snowflake" subcategory: "" description: |- - + Resource used to manage view objects. For more information, check view documentation https://docs.snowflake.com/en/sql-reference/sql/create-view. --- -# snowflake_view (Resource) +!> **V1 release candidate** This resource was reworked and is a release candidate for the V1. We do not expect significant changes in it before the V1. We will welcome any feedback and adjust the resource if needed. Any errors reported will be resolved with a higher priority. We encourage checking this resource out before the V1 release. Please follow the [migration guide](https://github.com/Snowflake-Labs/terraform-provider-snowflake/blob/main/MIGRATION_GUIDE.md#v094x--v0950) to use it. + +!> **Note about copy_grants** Fields like `is_recursive`, `is_temporary`, `copy_grants` and `statement` can not be ALTERed on Snowflake side (check [docs](https://docs.snowflake.com/en/sql-reference/sql/alter-view)), and a change means recreation of the resource. ForceNew can not be used because it does not preserve grants from `copy_grants`. Beware that even though a change is marked as update, the resource is recreated. +~> **Required warehouse** For this resource, the provider uses [policy references](https://docs.snowflake.com/en/sql-reference/functions/policy_references) which requires a warehouse in the connection. Please, make sure you have either set a DEFAULT_WAREHOUSE for the user, or specified a warehouse in the provider configuration. +# snowflake_view (Resource) + +Resource used to manage view objects. For more information, check [view documentation](https://docs.snowflake.com/en/sql-reference/sql/create-view). ## Example Usage ```terraform +# basic resource resource "snowflake_view" "view" { - database = "database" - schema = "schema" - name = "view" - - comment = "comment" + database = "database" + schema = "schema" + name = "view" + statement = <<-SQL + select * from foo; +SQL +} - statement = <<-SQL +# recursive view +resource "snowflake_view" "view" { + database = "database" + schema = "schema" + name = "view" + is_recursive = "true" + statement = <<-SQL select * from foo; SQL - or_replace = false - is_secure = false +} +# resource with attached policies +resource "snowflake_view" "test" { + database = "database" + schema = "schema" + name = "view" + comment = "comment" + is_secure = "true" + change_tracking = "true" + is_temporary = "true" + row_access_policy { + policy_name = "row_access_policy" + on = ["id"] + } + aggregation_policy { + policy_name = "aggregation_policy" + entity_key = ["id"] + } + statement = <<-SQL + select id from foo; +SQL } ``` - -> **Note** Instead of using fully_qualified_name, you can reference objects managed outside Terraform by constructing a correct ID, consult [identifiers guide](https://registry.terraform.io/providers/Snowflake-Labs/snowflake/latest/docs/guides/identifiers#new-computed-fully-qualified-name-field-in-resources). @@ -35,37 +68,88 @@ SQL ### Required -- `database` (String) The database in which to create the view. Don't use the | character. -- `name` (String) Specifies the identifier for the view; must be unique for the schema in which the view is created. Don't use the | character. -- `schema` (String) The schema in which to create the view. Don't use the | character. +- `database` (String) The database in which to create the view. Due to technical limitations (read more [here](https://github.com/Snowflake-Labs/terraform-provider-snowflake/blob/main/docs/technical-documentation/identifiers_rework_design_decisions.md#known-limitations-and-identifier-recommendations)), avoid using the following characters: `|`, `.`, `(`, `)`, `"` +- `name` (String) Specifies the identifier for the view; must be unique for the schema in which the view is created. Due to technical limitations (read more [here](https://github.com/Snowflake-Labs/terraform-provider-snowflake/blob/main/docs/technical-documentation/identifiers_rework_design_decisions.md#known-limitations-and-identifier-recommendations)), avoid using the following characters: `|`, `.`, `(`, `)`, `"` +- `schema` (String) The schema in which to create the view. Due to technical limitations (read more [here](https://github.com/Snowflake-Labs/terraform-provider-snowflake/blob/main/docs/technical-documentation/identifiers_rework_design_decisions.md#known-limitations-and-identifier-recommendations)), avoid using the following characters: `|`, `.`, `(`, `)`, `"` - `statement` (String) Specifies the query used to create the view. ### Optional +- `aggregation_policy` (Block List, Max: 1) Specifies the aggregation policy to set on a view. (see [below for nested schema](#nestedblock--aggregation_policy)) +- `change_tracking` (String) Specifies to enable or disable change tracking on the table. Available options are: "true" or "false". When the value is not set in the configuration the provider will put "default" there which means to use the Snowflake default for this value. - `comment` (String) Specifies a comment for the view. - `copy_grants` (Boolean) Retains the access permissions from the original view when a new view is created using the OR REPLACE clause. OR REPLACE must be set when COPY GRANTS is set. -- `is_secure` (Boolean) Specifies that the view is secure. By design, the Snowflake's `SHOW VIEWS` command does not provide information about secure views (consult [view usage notes](https://docs.snowflake.com/en/sql-reference/sql/create-view#usage-notes)) which is essential to manage/import view with Terraform. Use the role owning the view while managing secure views. +- `is_recursive` (String) Specifies that the view can refer to itself using recursive syntax without necessarily using a CTE (common table expression). Available options are: "true" or "false". When the value is not set in the configuration the provider will put "default" there which means to use the Snowflake default for this value. +- `is_secure` (String) Specifies that the view is secure. By design, the Snowflake's `SHOW VIEWS` command does not provide information about secure views (consult [view usage notes](https://docs.snowflake.com/en/sql-reference/sql/create-view#usage-notes)) which is essential to manage/import view with Terraform. Use the role owning the view while managing secure views. Available options are: "true" or "false". When the value is not set in the configuration the provider will put "default" there which means to use the Snowflake default for this value. +- `is_temporary` (String) Specifies that the view persists only for the duration of the session that you created it in. A temporary view and all its contents are dropped at the end of the session. In context of this provider, it means that it's dropped after a Terraform operation. This results in a permanent plan with object creation. Available options are: "true" or "false". When the value is not set in the configuration the provider will put "default" there which means to use the Snowflake default for this value. - `or_replace` (Boolean) Overwrites the View if it exists. -- `tag` (Block List, Deprecated) Definitions of a tag to associate with the resource. (see [below for nested schema](#nestedblock--tag)) +- `row_access_policy` (Block List, Max: 1) Specifies the row access policy to set on a view. (see [below for nested schema](#nestedblock--row_access_policy)) ### Read-Only -- `created_on` (String) The timestamp at which the view was created. +- `describe_output` (List of Object) Outputs the result of `DESCRIBE VIEW` for the given view. (see [below for nested schema](#nestedatt--describe_output)) - `fully_qualified_name` (String) Fully qualified name of the resource. For more information, see [object name resolution](https://docs.snowflake.com/en/sql-reference/name-resolution). - `id` (String) The ID of this resource. +- `show_output` (List of Object) Outputs the result of `SHOW VIEW` for the given view. (see [below for nested schema](#nestedatt--show_output)) - -### Nested Schema for `tag` + +### Nested Schema for `aggregation_policy` Required: -- `name` (String) Tag name, e.g. department. -- `value` (String) Tag value, e.g. marketing_info. +- `policy_name` (String) Aggregation policy name. Optional: -- `database` (String) Name of the database that the tag was created in. -- `schema` (String) Name of the schema that the tag was created in. +- `entity_key` (Set of String) Defines which columns uniquely identify an entity within the view. + + + +### Nested Schema for `row_access_policy` + +Required: + +- `on` (Set of String) Defines which columns are affected by the policy. +- `policy_name` (String) Row access policy name. + + + +### Nested Schema for `describe_output` + +Read-Only: + +- `check` (String) +- `comment` (String) +- `default` (String) +- `expression` (String) +- `is_nullable` (Boolean) +- `is_primary` (Boolean) +- `is_unique` (Boolean) +- `kind` (String) +- `name` (String) +- `policy_name` (String) +- `privacy_domain` (String) +- `type` (String) + + + +### Nested Schema for `show_output` + +Read-Only: + +- `change_tracking` (String) +- `comment` (String) +- `created_on` (String) +- `database_name` (String) +- `is_materialized` (Boolean) +- `is_secure` (Boolean) +- `kind` (String) +- `name` (String) +- `owner` (String) +- `owner_role_type` (String) +- `reserved` (String) +- `schema_name` (String) +- `text` (String) ## Import diff --git a/docs/resources/warehouse.md b/docs/resources/warehouse.md index 385f6a57d3..a651c63ae3 100644 --- a/docs/resources/warehouse.md +++ b/docs/resources/warehouse.md @@ -28,7 +28,7 @@ resource "snowflake_warehouse" "warehouse" { ### Required -- `name` (String) Identifier for the virtual warehouse; must be unique for your account. +- `name` (String) Identifier for the virtual warehouse; must be unique for your account. Due to technical limitations (read more [here](https://github.com/Snowflake-Labs/terraform-provider-snowflake/blob/main/docs/technical-documentation/identifiers_rework_design_decisions.md#known-limitations-and-identifier-recommendations)), avoid using the following characters: `|`, `.`, `(`, `)`, `"` ### Optional diff --git a/examples/resources/snowflake_view/resource.tf b/examples/resources/snowflake_view/resource.tf index afe7b9d94b..d4506dfcf7 100644 --- a/examples/resources/snowflake_view/resource.tf +++ b/examples/resources/snowflake_view/resource.tf @@ -1,13 +1,41 @@ +# basic resource resource "snowflake_view" "view" { - database = "database" - schema = "schema" - name = "view" - - comment = "comment" + database = "database" + schema = "schema" + name = "view" + statement = <<-SQL + select * from foo; +SQL +} - statement = <<-SQL +# recursive view +resource "snowflake_view" "view" { + database = "database" + schema = "schema" + name = "view" + is_recursive = "true" + statement = <<-SQL select * from foo; SQL - or_replace = false - is_secure = false +} +# resource with attached policies +resource "snowflake_view" "test" { + database = "database" + schema = "schema" + name = "view" + comment = "comment" + is_secure = "true" + change_tracking = "true" + is_temporary = "true" + row_access_policy { + policy_name = "row_access_policy" + on = ["id"] + } + aggregation_policy { + policy_name = "aggregation_policy" + entity_key = ["id"] + } + statement = <<-SQL + select id from foo; +SQL } diff --git a/pkg/acceptance/bettertestspoc/README.md b/pkg/acceptance/bettertestspoc/README.md index e17e502334..ed10d8eda1 100644 --- a/pkg/acceptance/bettertestspoc/README.md +++ b/pkg/acceptance/bettertestspoc/README.md @@ -300,7 +300,7 @@ it will result in: - Verify if all the config types are supported. - Consider a better implementation for the model conversion to config (TODO left in `config/config.go`). - Support additional methods for references in models (TODO left in `config/config.go`). -- Support depends_on in models (TODO left in `config/config.go`). +- Support depends_on in models so that it can be chained like other resource fields (TODO left in `config/config.go`). - Add a convenience function to concatenate multiple models (TODO left in `config/config.go`). - Add function to support using `ConfigFile:` in the acceptance tests (TODO left in `config/config.go`). - Replace `acceptance/snowflakechecks` with the new proposed Snowflake objects assertions. diff --git a/pkg/acceptance/bettertestspoc/assert/resourceassert/gen/resource_schema_def.go b/pkg/acceptance/bettertestspoc/assert/resourceassert/gen/resource_schema_def.go index 3e54aed96c..746291bbec 100644 --- a/pkg/acceptance/bettertestspoc/assert/resourceassert/gen/resource_schema_def.go +++ b/pkg/acceptance/bettertestspoc/assert/resourceassert/gen/resource_schema_def.go @@ -29,4 +29,8 @@ var allResourceSchemaDefs = []ResourceSchemaDef{ name: "User", schema: resources.User().Schema, }, + { + name: "View", + schema: resources.View().Schema, + }, } diff --git a/pkg/acceptance/bettertestspoc/assert/resourceassert/view_resource_gen.go b/pkg/acceptance/bettertestspoc/assert/resourceassert/view_resource_gen.go new file mode 100644 index 0000000000..ebac0c6239 --- /dev/null +++ b/pkg/acceptance/bettertestspoc/assert/resourceassert/view_resource_gen.go @@ -0,0 +1,197 @@ +// Code generated by assertions generator; DO NOT EDIT. + +package resourceassert + +import ( + "testing" + + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/acceptance/bettertestspoc/assert" +) + +type ViewResourceAssert struct { + *assert.ResourceAssert +} + +func ViewResource(t *testing.T, name string) *ViewResourceAssert { + t.Helper() + + return &ViewResourceAssert{ + ResourceAssert: assert.NewResourceAssert(name, "resource"), + } +} + +func ImportedViewResource(t *testing.T, id string) *ViewResourceAssert { + t.Helper() + + return &ViewResourceAssert{ + ResourceAssert: assert.NewImportedResourceAssert(id, "imported resource"), + } +} + +/////////////////////////////////// +// Attribute value string checks // +/////////////////////////////////// + +func (v *ViewResourceAssert) HasAggregationPolicyString(expected string) *ViewResourceAssert { + v.AddAssertion(assert.ValueSet("aggregation_policy", expected)) + return v +} + +func (v *ViewResourceAssert) HasChangeTrackingString(expected string) *ViewResourceAssert { + v.AddAssertion(assert.ValueSet("change_tracking", expected)) + return v +} + +func (v *ViewResourceAssert) HasColumnsString(expected string) *ViewResourceAssert { + v.AddAssertion(assert.ValueSet("columns", expected)) + return v +} + +func (v *ViewResourceAssert) HasCommentString(expected string) *ViewResourceAssert { + v.AddAssertion(assert.ValueSet("comment", expected)) + return v +} + +func (v *ViewResourceAssert) HasCopyGrantsString(expected string) *ViewResourceAssert { + v.AddAssertion(assert.ValueSet("copy_grants", expected)) + return v +} + +func (v *ViewResourceAssert) HasDataMetricFunctionsString(expected string) *ViewResourceAssert { + v.AddAssertion(assert.ValueSet("data_metric_functions", expected)) + return v +} + +func (v *ViewResourceAssert) HasDataMetricScheduleString(expected string) *ViewResourceAssert { + v.AddAssertion(assert.ValueSet("data_metric_schedule", expected)) + return v +} + +func (v *ViewResourceAssert) HasDatabaseString(expected string) *ViewResourceAssert { + v.AddAssertion(assert.ValueSet("database", expected)) + return v +} + +func (v *ViewResourceAssert) HasIsRecursiveString(expected string) *ViewResourceAssert { + v.AddAssertion(assert.ValueSet("is_recursive", expected)) + return v +} + +func (v *ViewResourceAssert) HasIsSecureString(expected string) *ViewResourceAssert { + v.AddAssertion(assert.ValueSet("is_secure", expected)) + return v +} + +func (v *ViewResourceAssert) HasIsTemporaryString(expected string) *ViewResourceAssert { + v.AddAssertion(assert.ValueSet("is_temporary", expected)) + return v +} + +func (v *ViewResourceAssert) HasNameString(expected string) *ViewResourceAssert { + v.AddAssertion(assert.ValueSet("name", expected)) + return v +} + +func (v *ViewResourceAssert) HasOrReplaceString(expected string) *ViewResourceAssert { + v.AddAssertion(assert.ValueSet("or_replace", expected)) + return v +} + +func (v *ViewResourceAssert) HasRowAccessPolicyString(expected string) *ViewResourceAssert { + v.AddAssertion(assert.ValueSet("row_access_policy", expected)) + return v +} + +func (v *ViewResourceAssert) HasSchemaString(expected string) *ViewResourceAssert { + v.AddAssertion(assert.ValueSet("schema", expected)) + return v +} + +func (v *ViewResourceAssert) HasStatementString(expected string) *ViewResourceAssert { + v.AddAssertion(assert.ValueSet("statement", expected)) + return v +} + +//////////////////////////// +// Attribute empty checks // +//////////////////////////// + +func (v *ViewResourceAssert) HasNoAggregationPolicy() *ViewResourceAssert { + v.AddAssertion(assert.ValueNotSet("aggregation_policy")) + return v +} + +func (v *ViewResourceAssert) HasNoChangeTracking() *ViewResourceAssert { + v.AddAssertion(assert.ValueNotSet("change_tracking")) + return v +} + +func (v *ViewResourceAssert) HasNoColumns() *ViewResourceAssert { + v.AddAssertion(assert.ValueNotSet("columns")) + return v +} + +func (v *ViewResourceAssert) HasNoComment() *ViewResourceAssert { + v.AddAssertion(assert.ValueNotSet("comment")) + return v +} + +func (v *ViewResourceAssert) HasNoCopyGrants() *ViewResourceAssert { + v.AddAssertion(assert.ValueNotSet("copy_grants")) + return v +} + +func (v *ViewResourceAssert) HasNoDataMetricFunctions() *ViewResourceAssert { + v.AddAssertion(assert.ValueNotSet("data_metric_functions")) + return v +} + +func (v *ViewResourceAssert) HasNoDataMetricSchedule() *ViewResourceAssert { + v.AddAssertion(assert.ValueNotSet("data_metric_schedule")) + return v +} + +func (v *ViewResourceAssert) HasNoDatabase() *ViewResourceAssert { + v.AddAssertion(assert.ValueNotSet("database")) + return v +} + +func (v *ViewResourceAssert) HasNoIsRecursive() *ViewResourceAssert { + v.AddAssertion(assert.ValueNotSet("is_recursive")) + return v +} + +func (v *ViewResourceAssert) HasNoIsSecure() *ViewResourceAssert { + v.AddAssertion(assert.ValueNotSet("is_secure")) + return v +} + +func (v *ViewResourceAssert) HasNoIsTemporary() *ViewResourceAssert { + v.AddAssertion(assert.ValueNotSet("is_temporary")) + return v +} + +func (v *ViewResourceAssert) HasNoName() *ViewResourceAssert { + v.AddAssertion(assert.ValueNotSet("name")) + return v +} + +func (v *ViewResourceAssert) HasNoOrReplace() *ViewResourceAssert { + v.AddAssertion(assert.ValueNotSet("or_replace")) + return v +} + +func (v *ViewResourceAssert) HasNoRowAccessPolicy() *ViewResourceAssert { + v.AddAssertion(assert.ValueNotSet("row_access_policy")) + return v +} + +func (v *ViewResourceAssert) HasNoSchema() *ViewResourceAssert { + v.AddAssertion(assert.ValueNotSet("schema")) + return v +} + +func (v *ViewResourceAssert) HasNoStatement() *ViewResourceAssert { + v.AddAssertion(assert.ValueNotSet("statement")) + return v +} diff --git a/pkg/acceptance/bettertestspoc/config/config.go b/pkg/acceptance/bettertestspoc/config/config.go index 1119f4d0bc..c1dea78d7e 100644 --- a/pkg/acceptance/bettertestspoc/config/config.go +++ b/pkg/acceptance/bettertestspoc/config/config.go @@ -11,7 +11,7 @@ import ( ) // TODO [SNOW-1501905]: add possibility to have reference to another object (e.g. WithResourceMonitorReference); new config.Variable impl? -// TODO [SNOW-1501905]: add possibility to have depends_on to other resources (in meta?) +// TODO [SNOW-1501905]: generate With/SetDependsOn for the resources to preserve builder pattern // TODO [SNOW-1501905]: add a convenience method to use multiple configs from multiple models // ResourceModel is the base interface all of our config models will implement. @@ -20,11 +20,14 @@ type ResourceModel interface { Resource() resources.Resource ResourceName() string SetResourceName(name string) + DependsOn() []string + SetDependsOn(values []string) } type ResourceModelMeta struct { - name string - resource resources.Resource + name string + resource resources.Resource + dependsOn []string } func (m *ResourceModelMeta) Resource() resources.Resource { @@ -39,6 +42,14 @@ func (m *ResourceModelMeta) SetResourceName(name string) { m.name = name } +func (m *ResourceModelMeta) DependsOn() []string { + return m.dependsOn +} + +func (m *ResourceModelMeta) SetDependsOn(values []string) { + m.dependsOn = values +} + // DefaultResourceName is exported to allow assertions against the resources using the default name. const DefaultResourceName = "test" @@ -70,6 +81,9 @@ func FromModel(t *testing.T, model ResourceModel) string { for k, v := range objMap { sb.WriteString(fmt.Sprintf("\t%s = %s\n", k, v)) } + if len(model.DependsOn()) > 0 { + sb.WriteString(fmt.Sprintf("\tdepends_on = [%s]\n", strings.Join(model.DependsOn(), ", "))) + } sb.WriteString(`}`) sb.WriteRune('\n') s := sb.String() diff --git a/pkg/acceptance/bettertestspoc/config/model/gen/templates/definition.tmpl b/pkg/acceptance/bettertestspoc/config/model/gen/templates/definition.tmpl index 76315ef0d7..274061be68 100644 --- a/pkg/acceptance/bettertestspoc/config/model/gen/templates/definition.tmpl +++ b/pkg/acceptance/bettertestspoc/config/model/gen/templates/definition.tmpl @@ -25,8 +25,10 @@ func {{ .Name }}( {{ $modelVar }} := &{{ $modelName }}{ResourceModelMeta: config.Meta(resourceName, resources.{{ .Name }})} {{ range .Attributes -}} {{- $attributeNameCamel := SnakeCaseToCamel .Name -}} - {{ if .Required }}{{ $modelVar }}.With{{ $attributeNameCamel }}({{ FirstLetterLowercase $attributeNameCamel }}) {{ end }} - {{- end }} + {{- if .Required -}} + {{ $modelVar }}.With{{ $attributeNameCamel }}({{ FirstLetterLowercase $attributeNameCamel }}) + {{ end -}} + {{- end -}} return {{ $modelVar }} } @@ -39,7 +41,9 @@ func {{ .Name }}WithDefaultMeta( {{ $modelVar }} := &{{ $modelName }}{ResourceModelMeta: config.DefaultMeta(resources.{{ .Name }})} {{ range .Attributes -}} {{- $attributeNameCamel := SnakeCaseToCamel .Name -}} - {{ if .Required }}{{ $modelVar }}.With{{ $attributeNameCamel }}({{ FirstLetterLowercase $attributeNameCamel }}) {{ end }} - {{- end }} + {{- if .Required -}} + {{ $modelVar }}.With{{ $attributeNameCamel }}({{ FirstLetterLowercase $attributeNameCamel }}) + {{ end -}} + {{- end -}} return {{ $modelVar }} } diff --git a/pkg/acceptance/bettertestspoc/config/model/view_model_ext.go b/pkg/acceptance/bettertestspoc/config/model/view_model_ext.go new file mode 100644 index 0000000000..f37cde3e33 --- /dev/null +++ b/pkg/acceptance/bettertestspoc/config/model/view_model_ext.go @@ -0,0 +1,6 @@ +package model + +func (v *ViewModel) WithDependsOn(values []string) *ViewModel { + v.SetDependsOn(values) + return v +} diff --git a/pkg/acceptance/bettertestspoc/config/model/view_model_gen.go b/pkg/acceptance/bettertestspoc/config/model/view_model_gen.go new file mode 100644 index 0000000000..8c1a2e0ff6 --- /dev/null +++ b/pkg/acceptance/bettertestspoc/config/model/view_model_gen.go @@ -0,0 +1,211 @@ +// Code generated by config model builder generator; DO NOT EDIT. + +package model + +import ( + tfconfig "github.com/hashicorp/terraform-plugin-testing/config" + + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/acceptance/bettertestspoc/config" + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/provider/resources" +) + +type ViewModel struct { + AggregationPolicy tfconfig.Variable `json:"aggregation_policy,omitempty"` + ChangeTracking tfconfig.Variable `json:"change_tracking,omitempty"` + Columns tfconfig.Variable `json:"columns,omitempty"` + Comment tfconfig.Variable `json:"comment,omitempty"` + CopyGrants tfconfig.Variable `json:"copy_grants,omitempty"` + DataMetricFunctions tfconfig.Variable `json:"data_metric_functions,omitempty"` + DataMetricSchedule tfconfig.Variable `json:"data_metric_schedule,omitempty"` + Database tfconfig.Variable `json:"database,omitempty"` + IsRecursive tfconfig.Variable `json:"is_recursive,omitempty"` + IsSecure tfconfig.Variable `json:"is_secure,omitempty"` + IsTemporary tfconfig.Variable `json:"is_temporary,omitempty"` + Name tfconfig.Variable `json:"name,omitempty"` + OrReplace tfconfig.Variable `json:"or_replace,omitempty"` + RowAccessPolicy tfconfig.Variable `json:"row_access_policy,omitempty"` + Schema tfconfig.Variable `json:"schema,omitempty"` + Statement tfconfig.Variable `json:"statement,omitempty"` + + *config.ResourceModelMeta +} + +///////////////////////////////////////////////// +// Basic builders (resource name and required) // +///////////////////////////////////////////////// + +func View( + resourceName string, + database string, name string, schema string, statement string, +) *ViewModel { + v := &ViewModel{ResourceModelMeta: config.Meta(resourceName, resources.View)} + v.WithDatabase(database) + v.WithName(name) + v.WithSchema(schema) + v.WithStatement(statement) + return v +} + +func ViewWithDefaultMeta( + database string, name string, schema string, statement string, +) *ViewModel { + v := &ViewModel{ResourceModelMeta: config.DefaultMeta(resources.View)} + v.WithDatabase(database) + v.WithName(name) + v.WithSchema(schema) + v.WithStatement(statement) + return v +} + +///////////////////////////////// +// below all the proper values // +///////////////////////////////// + +// aggregation_policy attribute type is not yet supported, so WithAggregationPolicy can't be generated + +func (v *ViewModel) WithChangeTracking(changeTracking string) *ViewModel { + v.ChangeTracking = tfconfig.StringVariable(changeTracking) + return v +} + +// columns attribute type is not yet supported, so WithColumns can't be generated + +func (v *ViewModel) WithComment(comment string) *ViewModel { + v.Comment = tfconfig.StringVariable(comment) + return v +} + +func (v *ViewModel) WithCopyGrants(copyGrants bool) *ViewModel { + v.CopyGrants = tfconfig.BoolVariable(copyGrants) + return v +} + +// data_metric_functions attribute type is not yet supported, so WithDataMetricFunctions can't be generated + +// data_metric_schedule attribute type is not yet supported, so WithDataMetricSchedule can't be generated + +func (v *ViewModel) WithDatabase(database string) *ViewModel { + v.Database = tfconfig.StringVariable(database) + return v +} + +func (v *ViewModel) WithIsRecursive(isRecursive string) *ViewModel { + v.IsRecursive = tfconfig.StringVariable(isRecursive) + return v +} + +func (v *ViewModel) WithIsSecure(isSecure string) *ViewModel { + v.IsSecure = tfconfig.StringVariable(isSecure) + return v +} + +func (v *ViewModel) WithIsTemporary(isTemporary string) *ViewModel { + v.IsTemporary = tfconfig.StringVariable(isTemporary) + return v +} + +func (v *ViewModel) WithName(name string) *ViewModel { + v.Name = tfconfig.StringVariable(name) + return v +} + +func (v *ViewModel) WithOrReplace(orReplace bool) *ViewModel { + v.OrReplace = tfconfig.BoolVariable(orReplace) + return v +} + +// row_access_policy attribute type is not yet supported, so WithRowAccessPolicy can't be generated + +func (v *ViewModel) WithSchema(schema string) *ViewModel { + v.Schema = tfconfig.StringVariable(schema) + return v +} + +func (v *ViewModel) WithStatement(statement string) *ViewModel { + v.Statement = tfconfig.StringVariable(statement) + return v +} + +////////////////////////////////////////// +// below it's possible to set any value // +////////////////////////////////////////// + +func (v *ViewModel) WithAggregationPolicyValue(value tfconfig.Variable) *ViewModel { + v.AggregationPolicy = value + return v +} + +func (v *ViewModel) WithChangeTrackingValue(value tfconfig.Variable) *ViewModel { + v.ChangeTracking = value + return v +} + +func (v *ViewModel) WithColumnsValue(value tfconfig.Variable) *ViewModel { + v.Columns = value + return v +} + +func (v *ViewModel) WithCommentValue(value tfconfig.Variable) *ViewModel { + v.Comment = value + return v +} + +func (v *ViewModel) WithCopyGrantsValue(value tfconfig.Variable) *ViewModel { + v.CopyGrants = value + return v +} + +func (v *ViewModel) WithDataMetricFunctionsValue(value tfconfig.Variable) *ViewModel { + v.DataMetricFunctions = value + return v +} + +func (v *ViewModel) WithDataMetricScheduleValue(value tfconfig.Variable) *ViewModel { + v.DataMetricSchedule = value + return v +} + +func (v *ViewModel) WithDatabaseValue(value tfconfig.Variable) *ViewModel { + v.Database = value + return v +} + +func (v *ViewModel) WithIsRecursiveValue(value tfconfig.Variable) *ViewModel { + v.IsRecursive = value + return v +} + +func (v *ViewModel) WithIsSecureValue(value tfconfig.Variable) *ViewModel { + v.IsSecure = value + return v +} + +func (v *ViewModel) WithIsTemporaryValue(value tfconfig.Variable) *ViewModel { + v.IsTemporary = value + return v +} + +func (v *ViewModel) WithNameValue(value tfconfig.Variable) *ViewModel { + v.Name = value + return v +} + +func (v *ViewModel) WithOrReplaceValue(value tfconfig.Variable) *ViewModel { + v.OrReplace = value + return v +} + +func (v *ViewModel) WithRowAccessPolicyValue(value tfconfig.Variable) *ViewModel { + v.RowAccessPolicy = value + return v +} + +func (v *ViewModel) WithSchemaValue(value tfconfig.Variable) *ViewModel { + v.Schema = value + return v +} + +func (v *ViewModel) WithStatementValue(value tfconfig.Variable) *ViewModel { + v.Statement = value + return v +} diff --git a/pkg/acceptance/helpers/aggregation_policy_client.go b/pkg/acceptance/helpers/aggregation_policy_client.go index d04114a55d..85f63fdb38 100644 --- a/pkg/acceptance/helpers/aggregation_policy_client.go +++ b/pkg/acceptance/helpers/aggregation_policy_client.go @@ -31,7 +31,7 @@ func (c *AggregationPolicyClient) CreateAggregationPolicy(t *testing.T) (sdk.Sch ctx := context.Background() id := c.ids.RandomSchemaObjectIdentifier() - _, err := c.client().ExecForTests(ctx, fmt.Sprintf(`CREATE AGGREGATION POLICY %s AS () RETURNS AGGREGATION_CONSTRAINT -> AGGREGATION_CONSTRAINT(MIN_GROUP_SIZE => 5)`, id.Name())) + _, err := c.client().ExecForTests(ctx, fmt.Sprintf(`CREATE AGGREGATION POLICY %s AS () RETURNS AGGREGATION_CONSTRAINT -> AGGREGATION_CONSTRAINT(MIN_GROUP_SIZE => 5)`, id.FullyQualifiedName())) require.NoError(t, err) return id, c.DropAggregationPolicyFunc(t, id) } @@ -41,7 +41,7 @@ func (c *AggregationPolicyClient) DropAggregationPolicyFunc(t *testing.T, id sdk ctx := context.Background() return func() { - _, err := c.client().ExecForTests(ctx, fmt.Sprintf(`DROP AGGREGATION POLICY IF EXISTS %s`, id.Name())) + _, err := c.client().ExecForTests(ctx, fmt.Sprintf(`DROP AGGREGATION POLICY IF EXISTS %s`, id.FullyQualifiedName())) require.NoError(t, err) } } diff --git a/pkg/acceptance/helpers/row_access_policy_client.go b/pkg/acceptance/helpers/row_access_policy_client.go index 29d8700f08..2ae9be2099 100644 --- a/pkg/acceptance/helpers/row_access_policy_client.go +++ b/pkg/acceptance/helpers/row_access_policy_client.go @@ -25,11 +25,16 @@ func (c *RowAccessPolicyClient) client() sdk.RowAccessPolicies { } func (c *RowAccessPolicyClient) CreateRowAccessPolicy(t *testing.T) (*sdk.RowAccessPolicy, func()) { + t.Helper() + return c.CreateRowAccessPolicyWithDataType(t, sdk.DataTypeNumber) +} + +func (c *RowAccessPolicyClient) CreateRowAccessPolicyWithDataType(t *testing.T, datatype sdk.DataType) (*sdk.RowAccessPolicy, func()) { t.Helper() ctx := context.Background() id := c.ids.RandomSchemaObjectIdentifier() - arg := sdk.NewCreateRowAccessPolicyArgsRequest("A", sdk.DataTypeNumber) + arg := sdk.NewCreateRowAccessPolicyArgsRequest("A", datatype) body := "true" createRequest := sdk.NewCreateRowAccessPolicyRequest(id, []sdk.CreateRowAccessPolicyArgsRequest{*arg}, body) diff --git a/pkg/acceptance/helpers/view_client.go b/pkg/acceptance/helpers/view_client.go index 0a1ae71632..ae6c9fb83d 100644 --- a/pkg/acceptance/helpers/view_client.go +++ b/pkg/acceptance/helpers/view_client.go @@ -52,6 +52,14 @@ func (c *ViewClient) RecreateView(t *testing.T, id sdk.SchemaObjectIdentifier, q return view } +func (c *ViewClient) Alter(t *testing.T, req *sdk.AlterViewRequest) { + t.Helper() + ctx := context.Background() + + err := c.client().Alter(ctx, req) + require.NoError(t, err) +} + func (c *ViewClient) DropViewFunc(t *testing.T, id sdk.SchemaObjectIdentifier) func() { t.Helper() ctx := context.Background() diff --git a/pkg/datasources/views_acceptance_test.go b/pkg/datasources/views_acceptance_test.go index 46b65564fb..ef5069cb31 100644 --- a/pkg/datasources/views_acceptance_test.go +++ b/pkg/datasources/views_acceptance_test.go @@ -5,15 +5,18 @@ import ( "testing" acc "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/acceptance" + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/acceptance/testenvs" + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/sdk" "github.com/hashicorp/terraform-plugin-testing/helper/resource" "github.com/hashicorp/terraform-plugin-testing/tfversion" ) +// TODO(SNOW-1423486): Fix using warehouse in all tests and remove unsetting testenvs.ConfigureClientOnce. func TestAcc_Views(t *testing.T) { - databaseName := acc.TestClient().Ids.Alpha() - schemaName := acc.TestClient().Ids.Alpha() - viewName := acc.TestClient().Ids.Alpha() + t.Setenv(string(testenvs.ConfigureClientOnce), "") + + viewId := acc.TestClient().Ids.RandomSchemaObjectIdentifier() resource.Test(t, resource.TestCase{ ProtoV6ProviderFactories: acc.TestAccProtoV6ProviderFactories, PreCheck: func() { acc.TestAccPreCheck(t) }, @@ -23,36 +26,32 @@ func TestAcc_Views(t *testing.T) { CheckDestroy: nil, Steps: []resource.TestStep{ { - Config: views(databaseName, schemaName, viewName), + Config: views(viewId), Check: resource.ComposeTestCheckFunc( - resource.TestCheckResourceAttr("data.snowflake_views.v", "database", databaseName), - resource.TestCheckResourceAttr("data.snowflake_views.v", "schema", schemaName), + resource.TestCheckResourceAttr("data.snowflake_views.v", "database", viewId.DatabaseName()), + resource.TestCheckResourceAttr("data.snowflake_views.v", "schema", viewId.SchemaName()), resource.TestCheckResourceAttrSet("data.snowflake_views.v", "views.#"), resource.TestCheckResourceAttr("data.snowflake_views.v", "views.#", "1"), - resource.TestCheckResourceAttr("data.snowflake_views.v", "views.0.name", viewName), + resource.TestCheckResourceAttr("data.snowflake_views.v", "views.0.name", viewId.Name()), ), }, }, }) } -func views(databaseName string, schemaName string, viewName string) string { +func views(viewId sdk.SchemaObjectIdentifier) string { return fmt.Sprintf(` - - resource snowflake_database "d" { - name = "%v" - } - - resource snowflake_schema "s"{ - name = "%v" - database = snowflake_database.d.name + resource "snowflake_unsafe_execute" "use_warehouse" { + execute = "USE WAREHOUSE \"%v\"" + revert = "SELECT 1" } resource snowflake_view "v"{ name = "%v" - database = snowflake_schema.s.database - schema = snowflake_schema.s.name + schema = "%v" + database = "%v" statement = "SELECT ROLE_NAME, ROLE_OWNER FROM INFORMATION_SCHEMA.APPLICABLE_ROLES where ROLE_OWNER like 'foo%%'" + depends_on = [snowflake_unsafe_execute.use_warehouse] } data snowflake_views "v" { @@ -60,5 +59,5 @@ func views(databaseName string, schemaName string, viewName string) string { schema = snowflake_view.v.schema depends_on = [snowflake_view.v] } - `, databaseName, schemaName, viewName) + `, acc.TestWarehouseName, viewId.Name(), viewId.SchemaName(), viewId.DatabaseName()) } diff --git a/pkg/resources/account_role.go b/pkg/resources/account_role.go index 0a9095ca5c..e7cc6f7fcf 100644 --- a/pkg/resources/account_role.go +++ b/pkg/resources/account_role.go @@ -22,6 +22,7 @@ var accountRoleSchema = map[string]*schema.Schema{ DiffSuppressFunc: suppressIdentifierQuoting, // TODO(SNOW-1495079): Uncomment once better identifier validation will be implemented // ValidateDiagFunc: IsValidIdentifier[sdk.AccountObjectIdentifier](), + Description: blocklistedCharactersFieldDescription("Identifier for the role; must be unique for your account."), }, "comment": { Type: schema.TypeString, diff --git a/pkg/resources/api_authentication_integration_common.go b/pkg/resources/api_authentication_integration_common.go index d21b3c943b..c505cd1bef 100644 --- a/pkg/resources/api_authentication_integration_common.go +++ b/pkg/resources/api_authentication_integration_common.go @@ -17,7 +17,7 @@ var apiAuthCommonSchema = map[string]*schema.Schema{ Type: schema.TypeString, Required: true, ForceNew: true, - Description: "Specifies the identifier (i.e. name) for the integration. This value must be unique in your account.", + Description: blocklistedCharactersFieldDescription("Specifies the identifier (i.e. name) for the integration. This value must be unique in your account."), DiffSuppressFunc: suppressIdentifierQuoting, }, "enabled": { diff --git a/pkg/resources/common.go b/pkg/resources/common.go index 05579effba..b74b467930 100644 --- a/pkg/resources/common.go +++ b/pkg/resources/common.go @@ -2,6 +2,7 @@ package resources import ( "context" + "regexp" "strings" "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/sdk" @@ -26,6 +27,8 @@ func DiffSuppressStatement(_, old, new string, _ *schema.ResourceData) bool { return strings.EqualFold(normalizeQuery(old), normalizeQuery(new)) } +var space = regexp.MustCompile(`\s+`) + func normalizeQuery(str string) string { return strings.TrimSpace(space.ReplaceAllString(str, " ")) } diff --git a/pkg/resources/database.go b/pkg/resources/database.go index cc06ee6e60..ec89d89db9 100644 --- a/pkg/resources/database.go +++ b/pkg/resources/database.go @@ -25,7 +25,7 @@ var databaseSchema = map[string]*schema.Schema{ "name": { Type: schema.TypeString, Required: true, - Description: "Specifies the identifier for the database; must be unique for your account. As a best practice for [Database Replication and Failover](https://docs.snowflake.com/en/user-guide/db-replication-intro), it is recommended to give each secondary database the same name as its primary database. This practice supports referencing fully-qualified objects (i.e. '..') by other objects in the same database, such as querying a fully-qualified table name in a view. If a secondary database has a different name from the primary database, then these object references would break in the secondary database.", + Description: blocklistedCharactersFieldDescription("Specifies the identifier for the database; must be unique for your account. As a best practice for [Database Replication and Failover](https://docs.snowflake.com/en/user-guide/db-replication-intro), it is recommended to give each secondary database the same name as its primary database. This practice supports referencing fully-qualified objects (i.e. '..') by other objects in the same database, such as querying a fully-qualified table name in a view. If a secondary database has a different name from the primary database, then these object references would break in the secondary database."), DiffSuppressFunc: suppressIdentifierQuoting, }, "drop_public_schema_on_creation": { diff --git a/pkg/resources/doc_helpers.go b/pkg/resources/doc_helpers.go index 9ec0200963..a7e8a278f1 100644 --- a/pkg/resources/doc_helpers.go +++ b/pkg/resources/doc_helpers.go @@ -13,6 +13,14 @@ func possibleValuesListed[T ~string](values []T) string { return strings.Join(valuesWrapped, " | ") } +func characterList(values []rune) string { + valuesWrapped := make([]string, len(values)) + for i, value := range values { + valuesWrapped[i] = fmt.Sprintf("`%c`", value) + } + return strings.Join(valuesWrapped, ", ") +} + func booleanStringFieldDescription(description string) string { return fmt.Sprintf(`%s Available options are: "%s" or "%s". When the value is not set in the configuration the provider will put "%s" there which means to use the Snowflake default for this value.`, description, BooleanTrue, BooleanFalse, BooleanDefault) } @@ -24,3 +32,7 @@ func externalChangesNotDetectedFieldDescription(description string) string { func withPrivilegedRolesDescription(description, paramName string) string { return fmt.Sprintf(`%s By default, this list includes the ACCOUNTADMIN, ORGADMIN and SECURITYADMIN roles. To remove these privileged roles from the list, use the ALTER ACCOUNT command to set the %s account parameter to FALSE. `, description, paramName) } + +func blocklistedCharactersFieldDescription(description string) string { + return fmt.Sprintf(`%s Due to technical limitations (read more [here](https://github.com/Snowflake-Labs/terraform-provider-snowflake/blob/main/docs/technical-documentation/identifiers_rework_design_decisions.md#known-limitations-and-identifier-recommendations)), avoid using the following characters: %s`, description, characterList([]rune{'|', '.', '(', ')', '"'})) +} diff --git a/pkg/resources/external_oauth_integration.go b/pkg/resources/external_oauth_integration.go index aa5408a68d..31aa448d47 100644 --- a/pkg/resources/external_oauth_integration.go +++ b/pkg/resources/external_oauth_integration.go @@ -27,7 +27,7 @@ var externalOauthIntegrationSchema = map[string]*schema.Schema{ Type: schema.TypeString, Required: true, ForceNew: true, - Description: "Specifies the name of the External Oath integration. This name follows the rules for Object Identifiers. The name should be unique among security integrations in your account.", + Description: blocklistedCharactersFieldDescription("Specifies the name of the External Oath integration. This name follows the rules for Object Identifiers. The name should be unique among security integrations in your account."), DiffSuppressFunc: suppressIdentifierQuoting, }, "external_oauth_type": { diff --git a/pkg/resources/grant_privileges_to_share_acceptance_test.go b/pkg/resources/grant_privileges_to_share_acceptance_test.go index 459ed7ae6d..78ae8a4a01 100644 --- a/pkg/resources/grant_privileges_to_share_acceptance_test.go +++ b/pkg/resources/grant_privileges_to_share_acceptance_test.go @@ -229,6 +229,8 @@ func TestAcc_GrantPrivilegesToShare_OnAllTablesInSchema(t *testing.T) { } func TestAcc_GrantPrivilegesToShare_OnView(t *testing.T) { + t.Setenv(string(testenvs.ConfigureClientOnce), "") + databaseId := acc.TestClient().Ids.RandomAccountObjectIdentifier() schemaId := acc.TestClient().Ids.RandomDatabaseObjectIdentifierInDatabase(databaseId) tableId := acc.TestClient().Ids.RandomSchemaObjectIdentifierInSchema(schemaId) diff --git a/pkg/resources/network_policy.go b/pkg/resources/network_policy.go index bd0bcd5950..d22c86eeee 100644 --- a/pkg/resources/network_policy.go +++ b/pkg/resources/network_policy.go @@ -21,7 +21,7 @@ var networkPolicySchema = map[string]*schema.Schema{ "name": { Type: schema.TypeString, Required: true, - Description: "Specifies the identifier for the network policy; must be unique for the account in which the network policy is created.", + Description: blocklistedCharactersFieldDescription("Specifies the identifier for the network policy; must be unique for the account in which the network policy is created."), DiffSuppressFunc: suppressIdentifierQuoting, }, "allowed_network_rule_list": { diff --git a/pkg/resources/oauth_integration_for_custom_clients.go b/pkg/resources/oauth_integration_for_custom_clients.go index 726d0aebdb..aa133197a3 100644 --- a/pkg/resources/oauth_integration_for_custom_clients.go +++ b/pkg/resources/oauth_integration_for_custom_clients.go @@ -25,7 +25,7 @@ var oauthIntegrationForCustomClientsSchema = map[string]*schema.Schema{ Type: schema.TypeString, Required: true, ForceNew: true, - Description: "Specifies the name of the OAuth integration. This name follows the rules for Object Identifiers. The name should be unique among security integrations in your account.", + Description: blocklistedCharactersFieldDescription("Specifies the name of the OAuth integration. This name follows the rules for Object Identifiers. The name should be unique among security integrations in your account."), DiffSuppressFunc: suppressIdentifierQuoting, }, "oauth_client_type": { diff --git a/pkg/resources/oauth_integration_for_partner_applications.go b/pkg/resources/oauth_integration_for_partner_applications.go index 848d33118f..9cf0dfcefc 100644 --- a/pkg/resources/oauth_integration_for_partner_applications.go +++ b/pkg/resources/oauth_integration_for_partner_applications.go @@ -26,7 +26,7 @@ var oauthIntegrationForPartnerApplicationsSchema = map[string]*schema.Schema{ Type: schema.TypeString, Required: true, ForceNew: true, - Description: "Specifies the name of the OAuth integration. This name follows the rules for Object Identifiers. The name should be unique among security integrations in your account.", + Description: blocklistedCharactersFieldDescription("Specifies the name of the OAuth integration. This name follows the rules for Object Identifiers. The name should be unique among security integrations in your account."), DiffSuppressFunc: suppressIdentifierQuoting, }, "oauth_client": { diff --git a/pkg/resources/saml2_integration.go b/pkg/resources/saml2_integration.go index dda8fad3bf..a460970f1f 100644 --- a/pkg/resources/saml2_integration.go +++ b/pkg/resources/saml2_integration.go @@ -25,7 +25,7 @@ var saml2IntegrationSchema = map[string]*schema.Schema{ Type: schema.TypeString, Required: true, ForceNew: true, - Description: "Specifies the name of the SAML2 integration. This name follows the rules for Object Identifiers. The name should be unique among security integrations in your account.", + Description: blocklistedCharactersFieldDescription("Specifies the name of the SAML2 integration. This name follows the rules for Object Identifiers. The name should be unique among security integrations in your account."), DiffSuppressFunc: suppressIdentifierQuoting, }, "enabled": { diff --git a/pkg/resources/scim_integration.go b/pkg/resources/scim_integration.go index a4c01ec40f..15fd779d52 100644 --- a/pkg/resources/scim_integration.go +++ b/pkg/resources/scim_integration.go @@ -25,7 +25,7 @@ var scimIntegrationSchema = map[string]*schema.Schema{ Type: schema.TypeString, Required: true, ForceNew: true, - Description: "String that specifies the identifier (i.e. name) for the integration; must be unique in your account.", + Description: blocklistedCharactersFieldDescription("String that specifies the identifier (i.e. name) for the integration; must be unique in your account."), DiffSuppressFunc: suppressIdentifierQuoting, }, "enabled": { diff --git a/pkg/resources/secondary_database.go b/pkg/resources/secondary_database.go index 15cdf7f703..361db4ab48 100644 --- a/pkg/resources/secondary_database.go +++ b/pkg/resources/secondary_database.go @@ -18,7 +18,7 @@ var secondaryDatabaseSchema = map[string]*schema.Schema{ "name": { Type: schema.TypeString, Required: true, - Description: "Specifies the identifier for the database; must be unique for your account. As a best practice for [Database Replication and Failover](https://docs.snowflake.com/en/user-guide/db-replication-intro), it is recommended to give each secondary database the same name as its primary database. This practice supports referencing fully-qualified objects (i.e. '..') by other objects in the same database, such as querying a fully-qualified table name in a view. If a secondary database has a different name from the primary database, then these object references would break in the secondary database.", + Description: blocklistedCharactersFieldDescription("Specifies the identifier for the database; must be unique for your account. As a best practice for [Database Replication and Failover](https://docs.snowflake.com/en/user-guide/db-replication-intro), it is recommended to give each secondary database the same name as its primary database. This practice supports referencing fully-qualified objects (i.e. '..') by other objects in the same database, such as querying a fully-qualified table name in a view. If a secondary database has a different name from the primary database, then these object references would break in the secondary database."), DiffSuppressFunc: suppressIdentifierQuoting, }, "as_replica_of": { diff --git a/pkg/resources/shared_database.go b/pkg/resources/shared_database.go index 02675867a5..b6da4eaf36 100644 --- a/pkg/resources/shared_database.go +++ b/pkg/resources/shared_database.go @@ -18,7 +18,7 @@ var sharedDatabaseSchema = map[string]*schema.Schema{ "name": { Type: schema.TypeString, Required: true, - Description: "Specifies the identifier for the database; must be unique for your account.", + Description: blocklistedCharactersFieldDescription("Specifies the identifier for the database; must be unique for your account."), DiffSuppressFunc: suppressIdentifierQuoting, }, "from_share": { diff --git a/pkg/resources/stream_acceptance_test.go b/pkg/resources/stream_acceptance_test.go index a85c6d67a0..1fccdd2e23 100644 --- a/pkg/resources/stream_acceptance_test.go +++ b/pkg/resources/stream_acceptance_test.go @@ -107,6 +107,9 @@ func TestAcc_Stream_OnTable(t *testing.T) { // proves issue https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/2672 func TestAcc_Stream_OnView(t *testing.T) { + // TODO(SNOW-1423486): Fix using warehouse in all tests and remove unsetting testenvs.ConfigureClientOnce + t.Setenv(string(testenvs.ConfigureClientOnce), "") + tableName := acc.TestClient().Ids.Alpha() viewName := acc.TestClient().Ids.Alpha() name := acc.TestClient().Ids.Alpha() @@ -289,6 +292,7 @@ resource "snowflake_view" "test" { database = "%[1]s" schema = "%[2]s" name = "%[4]s" + change_tracking = true statement = "select * from \"${snowflake_table.test.name}\"" } diff --git a/pkg/resources/streamlit.go b/pkg/resources/streamlit.go index 342b9e0c61..e386f10f6c 100644 --- a/pkg/resources/streamlit.go +++ b/pkg/resources/streamlit.go @@ -88,7 +88,7 @@ var streamlitSchema = map[string]*schema.Schema{ ShowOutputAttributeName: { Type: schema.TypeList, Computed: true, - Description: "Outputs the result of `SHOW STREAMLIT` for the given streamli.", + Description: "Outputs the result of `SHOW STREAMLIT` for the given streamlit.", Elem: &schema.Resource{ Schema: schemas.ShowStreamlitSchema, }, diff --git a/pkg/resources/testdata/TestAcc_View/basic/test.tf b/pkg/resources/testdata/TestAcc_View/basic/test.tf new file mode 100644 index 0000000000..74efa22d33 --- /dev/null +++ b/pkg/resources/testdata/TestAcc_View/basic/test.tf @@ -0,0 +1,6 @@ +resource "snowflake_view" "test" { + name = var.name + database = var.database + schema = var.schema + statement = var.statement +} diff --git a/pkg/resources/testdata/TestAcc_View_Tags/1/variables.tf b/pkg/resources/testdata/TestAcc_View/basic/variables.tf similarity index 65% rename from pkg/resources/testdata/TestAcc_View_Tags/1/variables.tf rename to pkg/resources/testdata/TestAcc_View/basic/variables.tf index 7d7074a5a9..5b5810d23d 100644 --- a/pkg/resources/testdata/TestAcc_View_Tags/1/variables.tf +++ b/pkg/resources/testdata/TestAcc_View/basic/variables.tf @@ -13,11 +13,3 @@ variable "schema" { variable "statement" { type = string } - -variable "tag1Name" { - type = string -} - -variable "tag2Name" { - type = string -} diff --git a/pkg/resources/testdata/TestAcc_View/basic_update/test.tf b/pkg/resources/testdata/TestAcc_View/basic_update/test.tf new file mode 100644 index 0000000000..2bd1ef8145 --- /dev/null +++ b/pkg/resources/testdata/TestAcc_View/basic_update/test.tf @@ -0,0 +1,16 @@ +resource "snowflake_view" "test" { + name = var.name + database = var.database + schema = var.schema + row_access_policy { + policy_name = var.row_access_policy + on = var.row_access_policy_on + + } + aggregation_policy { + policy_name = var.aggregation_policy + entity_key = var.aggregation_policy_entity_key + } + statement = var.statement + comment = var.comment +} diff --git a/pkg/resources/testdata/TestAcc_View/basic_update/variables.tf b/pkg/resources/testdata/TestAcc_View/basic_update/variables.tf new file mode 100644 index 0000000000..42cc6286e8 --- /dev/null +++ b/pkg/resources/testdata/TestAcc_View/basic_update/variables.tf @@ -0,0 +1,35 @@ +variable "name" { + type = string +} + +variable "database" { + type = string +} + +variable "schema" { + type = string +} + +variable "statement" { + type = string +} + +variable "row_access_policy" { + type = string +} + +variable "row_access_policy_on" { + type = list(string) +} + +variable "aggregation_policy" { + type = string +} + +variable "aggregation_policy_entity_key" { + type = list(string) +} + +variable "comment" { + type = string +} diff --git a/pkg/resources/testdata/TestAcc_View/complete/test.tf b/pkg/resources/testdata/TestAcc_View/complete/test.tf new file mode 100644 index 0000000000..45a4a42eb0 --- /dev/null +++ b/pkg/resources/testdata/TestAcc_View/complete/test.tf @@ -0,0 +1,27 @@ +resource "snowflake_view" "test" { + name = var.name + comment = var.comment + database = var.database + schema = var.schema + is_secure = var.is_secure + or_replace = var.or_replace + copy_grants = var.copy_grants + change_tracking = var.change_tracking + is_temporary = var.is_temporary + row_access_policy { + policy_name = var.row_access_policy + on = var.row_access_policy_on + + } + aggregation_policy { + policy_name = var.aggregation_policy + entity_key = var.aggregation_policy_entity_key + } + statement = var.statement + depends_on = [snowflake_unsafe_execute.use_warehouse] +} + +resource "snowflake_unsafe_execute" "use_warehouse" { + execute = "USE WAREHOUSE \"${var.warehouse}\"" + revert = "SELECT 1" +} diff --git a/pkg/resources/testdata/TestAcc_View/complete/variables.tf b/pkg/resources/testdata/TestAcc_View/complete/variables.tf new file mode 100644 index 0000000000..4423777db3 --- /dev/null +++ b/pkg/resources/testdata/TestAcc_View/complete/variables.tf @@ -0,0 +1,59 @@ +variable "name" { + type = string +} + +variable "comment" { + type = string +} + +variable "database" { + type = string +} + +variable "schema" { + type = string +} + +variable "is_secure" { + type = bool +} + +variable "change_tracking" { + type = string +} + +variable "or_replace" { + type = bool +} + +variable "copy_grants" { + type = bool +} + +variable "row_access_policy" { + type = string +} + +variable "is_temporary" { + type = string +} + +variable "row_access_policy_on" { + type = list(string) +} + +variable "aggregation_policy" { + type = string +} + +variable "aggregation_policy_entity_key" { + type = list(string) +} + +variable "statement" { + type = string +} + +variable "warehouse" { + type = string +} diff --git a/pkg/resources/testdata/TestAcc_View_Tags/1/test.tf b/pkg/resources/testdata/TestAcc_View_Tags/1/test.tf deleted file mode 100644 index b27dd3db42..0000000000 --- a/pkg/resources/testdata/TestAcc_View_Tags/1/test.tf +++ /dev/null @@ -1,28 +0,0 @@ -resource "snowflake_tag" "test_tag" { - name = var.tag1Name - database = var.database - schema = var.schema -} - -resource "snowflake_tag" "test_tag_2" { - name = var.tag2Name - database = var.database - schema = var.schema -} - -resource "snowflake_view" "test" { - name = var.name - database = var.database - schema = var.schema - is_secure = false - or_replace = false - copy_grants = false - statement = var.statement - - tag { - name = snowflake_tag.test_tag.name - schema = var.schema - database = var.database - value = "some_value" - } -} diff --git a/pkg/resources/testdata/TestAcc_View_Tags/2/test.tf b/pkg/resources/testdata/TestAcc_View_Tags/2/test.tf deleted file mode 100644 index 933fe9613e..0000000000 --- a/pkg/resources/testdata/TestAcc_View_Tags/2/test.tf +++ /dev/null @@ -1,28 +0,0 @@ -resource "snowflake_tag" "test_tag" { - name = var.tag1Name - database = var.database - schema = var.schema -} - -resource "snowflake_tag" "test_tag_2" { - name = var.tag2Name - database = var.database - schema = var.schema -} - -resource "snowflake_view" "test" { - name = var.name - database = var.database - schema = var.schema - is_secure = false - or_replace = false - copy_grants = false - statement = var.statement - - tag { - name = snowflake_tag.test_tag_2.name - schema = var.schema - database = var.database - value = "some_value" - } -} diff --git a/pkg/resources/testdata/TestAcc_View_Tags/2/variables.tf b/pkg/resources/testdata/TestAcc_View_Tags/2/variables.tf deleted file mode 100644 index 7d7074a5a9..0000000000 --- a/pkg/resources/testdata/TestAcc_View_Tags/2/variables.tf +++ /dev/null @@ -1,23 +0,0 @@ -variable "name" { - type = string -} - -variable "database" { - type = string -} - -variable "schema" { - type = string -} - -variable "statement" { - type = string -} - -variable "tag1Name" { - type = string -} - -variable "tag2Name" { - type = string -} diff --git a/pkg/resources/testdata/TestAcc_View_basic/test.tf b/pkg/resources/testdata/TestAcc_View_basic/test.tf deleted file mode 100644 index d333d83bd9..0000000000 --- a/pkg/resources/testdata/TestAcc_View_basic/test.tf +++ /dev/null @@ -1,10 +0,0 @@ -resource "snowflake_view" "test" { - name = var.name - comment = var.comment - database = var.database - schema = var.schema - is_secure = var.is_secure - or_replace = var.or_replace - copy_grants = var.copy_grants - statement = var.statement -} diff --git a/pkg/resources/testdata/TestAcc_View_basic/variables.tf b/pkg/resources/testdata/TestAcc_View_basic/variables.tf deleted file mode 100644 index b36067977a..0000000000 --- a/pkg/resources/testdata/TestAcc_View_basic/variables.tf +++ /dev/null @@ -1,31 +0,0 @@ -variable "name" { - type = string -} - -variable "comment" { - type = string -} - -variable "database" { - type = string -} - -variable "schema" { - type = string -} - -variable "is_secure" { - type = bool -} - -variable "or_replace" { - type = bool -} - -variable "copy_grants" { - type = bool -} - -variable "statement" { - type = string -} diff --git a/pkg/resources/view.go b/pkg/resources/view.go index 33700718ab..c61f823b1b 100644 --- a/pkg/resources/view.go +++ b/pkg/resources/view.go @@ -2,9 +2,11 @@ package resources import ( "context" + "errors" "fmt" "log" - "regexp" + "strconv" + "strings" "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/internal/provider" "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/schemas" @@ -12,29 +14,32 @@ import ( "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/helpers" "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/sdk" "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/snowflake" + "github.com/hashicorp/go-cty/cty" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/customdiff" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" ) -var space = regexp.MustCompile(`\s+`) - var viewSchema = map[string]*schema.Schema{ "name": { - Type: schema.TypeString, - Required: true, - Description: "Specifies the identifier for the view; must be unique for the schema in which the view is created. Don't use the | character.", + Type: schema.TypeString, + Required: true, + Description: blocklistedCharactersFieldDescription("Specifies the identifier for the view; must be unique for the schema in which the view is created."), + DiffSuppressFunc: suppressIdentifierQuoting, }, "database": { - Type: schema.TypeString, - Required: true, - Description: "The database in which to create the view. Don't use the | character.", - ForceNew: true, + Type: schema.TypeString, + Required: true, + Description: blocklistedCharactersFieldDescription("The database in which to create the view."), + ForceNew: true, + DiffSuppressFunc: suppressIdentifierQuoting, }, "schema": { - Type: schema.TypeString, - Required: true, - Description: "The schema in which to create the view. Don't use the | character.", - ForceNew: true, + Type: schema.TypeString, + Required: true, + Description: blocklistedCharactersFieldDescription("The schema in which to create the view."), + ForceNew: true, + DiffSuppressFunc: suppressIdentifierQuoting, }, "or_replace": { Type: schema.TypeBool, @@ -54,181 +59,515 @@ var viewSchema = map[string]*schema.Schema{ RequiredWith: []string{"or_replace"}, }, "is_secure": { - Type: schema.TypeBool, - Optional: true, - Default: false, - Description: "Specifies that the view is secure. By design, the Snowflake's `SHOW VIEWS` command does not provide information about secure views (consult [view usage notes](https://docs.snowflake.com/en/sql-reference/sql/create-view#usage-notes)) which is essential to manage/import view with Terraform. Use the role owning the view while managing secure views.", + Type: schema.TypeString, + Optional: true, + Default: BooleanDefault, + ValidateDiagFunc: validateBooleanString, + DiffSuppressFunc: IgnoreChangeToCurrentSnowflakeValueInShow("is_secure"), + Description: booleanStringFieldDescription("Specifies that the view is secure. By design, the Snowflake's `SHOW VIEWS` command does not provide information about secure views (consult [view usage notes](https://docs.snowflake.com/en/sql-reference/sql/create-view#usage-notes)) which is essential to manage/import view with Terraform. Use the role owning the view while managing secure views."), + }, + "is_temporary": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + Default: BooleanDefault, + ValidateDiagFunc: validateBooleanString, + Description: booleanStringFieldDescription("Specifies that the view persists only for the duration of the session that you created it in. A temporary view and all its contents are dropped at the end of the session. In context of this provider, it means that it's dropped after a Terraform operation. This results in a permanent plan with object creation."), }, + "is_recursive": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + Default: BooleanDefault, + ValidateDiagFunc: validateBooleanString, + Description: booleanStringFieldDescription("Specifies that the view can refer to itself using recursive syntax without necessarily using a CTE (common table expression)."), + }, + "change_tracking": { + Type: schema.TypeString, + Optional: true, + Default: BooleanDefault, + ValidateDiagFunc: validateBooleanString, + DiffSuppressFunc: IgnoreChangeToCurrentSnowflakeValueInShowWithMapping("change_tracking", func(x any) any { + return x.(string) == "ON" + }), + Description: booleanStringFieldDescription("Specifies to enable or disable change tracking on the table."), + }, + // TODO(next pr): support remaining fields + // "data_metric_functions": { + // Type: schema.TypeSet, + // Optional: true, + // Elem: &schema.Resource{ + // Schema: map[string]*schema.Schema{ + // "metric_name": { + // Type: schema.TypeString, + // Optional: true, + // Description: "Identifier of the data metric function to add to the table or view or drop from the table or view.", + // }, + // "column_name": { + // Type: schema.TypeString, + // Optional: true, + // Description: "The table or view columns on which to associate the data metric function. The data types of the columns must match the data types of the columns specified in the data metric function definition.", + // }, + // }, + // }, + // Description: "Data metric functions used for the view.", + // }, + // "data_metric_schedule": { + // Type: schema.TypeList, + // Optional: true, + // MaxItems: 1, + // Elem: &schema.Resource{ + // Schema: map[string]*schema.Schema{ + // "minutes": { + // Type: schema.TypeInt, + // Optional: true, + // Description: "Specifies an interval (in minutes) of wait time inserted between runs of the data metric function. Conflicts with `using_cron` and `trigger_on_changes`.", + // // TODO: move to sdk + // ValidateFunc: validation.IntInSlice([]int{5, 15, 30, 60, 720, 1440}), + // ConflictsWith: []string{"data_metric_schedule.using_cron", "data_metric_schedule.trigger_on_changes"}, + // }, + // "using_cron": { + // Type: schema.TypeString, + // Optional: true, + // Description: "Specifies a cron expression and time zone for periodically running the data metric function. Supports a subset of standard cron utility syntax. Conflicts with `minutes` and `trigger_on_changes`.", + // // TODO: validate? + // ConflictsWith: []string{"data_metric_schedule.minutes", "data_metric_schedule.trigger_on_changes"}, + // }, + // "trigger_on_changes": { + // Type: schema.TypeString, + // Optional: true, + // Default: BooleanDefault, + // Description: booleanStringFieldDescription("Specifies that the DMF runs when a DML operation modifies the table, such as inserting a new row or deleting a row. Conflicts with `minutes` and `using_cron`."), + // ConflictsWith: []string{"data_metric_schedule.minutes", "data_metric_schedule.using_cron"}, + // }, + // }, + // }, + // Description: "Specifies the schedule to run the data metric function periodically.", + // }, + // "columns": { + // Type: schema.TypeList, + // Optional: true, + // Elem: &schema.Resource{ + // Schema: map[string]*schema.Schema{ + // "column_name": { + // Type: schema.TypeString, + // Required: true, + // Description: "Specifies affected column name.", + // }, + // "masking_policy": { + // Type: schema.TypeList, + // Optional: true, + // Elem: &schema.Resource{ + // Schema: map[string]*schema.Schema{ + // // TODO: change to `name`? in other policies as well + // "policy_name": { + // Type: schema.TypeString, + // Required: true, + // Description: "Specifies the masking policy to set on a column.", + // }, + // "using": { + // Type: schema.TypeList, + // Optional: true, + // Elem: &schema.Schema{ + // Type: schema.TypeString, + // }, + // Description: "Specifies the arguments to pass into the conditional masking policy SQL expression. The first column in the list specifies the column for the policy conditions to mask or tokenize the data and must match the column to which the masking policy is set. The additional columns specify the columns to evaluate to determine whether to mask or tokenize the data in each row of the query result when a query is made on the first column. If the USING clause is omitted, Snowflake treats the conditional masking policy as a normal masking policy.", + // }, + // }, + // }, + // }, + // "projection_policy": { + // Type: schema.TypeString, + // Optional: true, + // DiffSuppressFunc: DiffSuppressStatement, + // Description: "Specifies the projection policy to set on a column.", + // }, + // "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, Description: "Specifies a comment for the view.", }, + "row_access_policy": { + Type: schema.TypeList, + MaxItems: 1, + Optional: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "policy_name": { + Type: schema.TypeString, + Required: true, + DiffSuppressFunc: suppressIdentifierQuoting, + Description: "Row access policy name.", + }, + "on": { + Type: schema.TypeSet, + Required: true, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + Description: "Defines which columns are affected by the policy.", + }, + }, + }, + Description: "Specifies the row access policy to set on a view.", + }, + "aggregation_policy": { + Type: schema.TypeList, + MaxItems: 1, + Optional: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "policy_name": { + Type: schema.TypeString, + Required: true, + DiffSuppressFunc: suppressIdentifierQuoting, + Description: "Aggregation policy name.", + }, + "entity_key": { + Type: schema.TypeSet, + Optional: true, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + Description: "Defines which columns uniquely identify an entity within the view.", + }, + }, + }, + Description: "Specifies the aggregation policy to set on a view.", + }, "statement": { Type: schema.TypeString, Required: true, Description: "Specifies the query used to create the view.", DiffSuppressFunc: DiffSuppressStatement, }, - "created_on": { - Type: schema.TypeString, + ShowOutputAttributeName: { + Type: schema.TypeList, + Computed: true, + Description: "Outputs the result of `SHOW VIEW` for the given view.", + Elem: &schema.Resource{ + Schema: schemas.ShowViewSchema, + }, + }, + DescribeOutputAttributeName: { + Type: schema.TypeList, Computed: true, - Description: "The timestamp at which the view was created.", + Description: "Outputs the result of `DESCRIBE VIEW` for the given view.", + Elem: &schema.Resource{ + Schema: schemas.ViewDescribeSchema, + }, }, - "tag": tagReferenceSchema, FullyQualifiedNameAttributeName: schemas.FullyQualifiedNameSchema, } // View returns a pointer to the resource representing a view. func View() *schema.Resource { return &schema.Resource{ - Create: CreateView, - Read: ReadView, - Update: UpdateView, - Delete: DeleteView, + SchemaVersion: 1, + + CreateContext: CreateView(false), + ReadContext: ReadView(true), + UpdateContext: UpdateView, + DeleteContext: DeleteView, + Description: "Resource used to manage view objects. For more information, check [view documentation](https://docs.snowflake.com/en/sql-reference/sql/create-view).", CustomizeDiff: customdiff.All( + ComputedIfAnyAttributeChanged(ShowOutputAttributeName, "comment", "change_tracking", "is_secure", "is_temporary", "is_recursive", "statement"), ComputedIfAnyAttributeChanged(FullyQualifiedNameAttributeName, "name"), ), Schema: viewSchema, Importer: &schema.ResourceImporter{ - StateContext: schema.ImportStatePassthroughContext, + StateContext: ImportView, + }, + + StateUpgraders: []schema.StateUpgrader{ + { + Version: 0, + // setting type to cty.EmptyObject is a bit hacky here but following https://developer.hashicorp.com/terraform/plugin/framework/migrating/resources/state-upgrade#sdkv2-1 would require lots of repetitive code; this should work with cty.EmptyObject + Type: cty.EmptyObject, + Upgrade: v0_94_1_ViewStateUpgrader, + }, }, } } -// CreateView implements schema.CreateFunc. -func CreateView(d *schema.ResourceData, meta interface{}) error { +func ImportView(ctx context.Context, d *schema.ResourceData, meta any) ([]*schema.ResourceData, error) { + log.Printf("[DEBUG] Starting view import") client := meta.(*provider.Context).Client - ctx := context.Background() - - databaseName := d.Get("database").(string) - schemaName := d.Get("schema").(string) - name := d.Get("name").(string) - id := sdk.NewSchemaObjectIdentifier(databaseName, schemaName, name) - - s := d.Get("statement").(string) - createRequest := sdk.NewCreateViewRequest(id, s) + id := helpers.DecodeSnowflakeID(d.Id()).(sdk.SchemaObjectIdentifier) - if v, ok := d.GetOk("or_replace"); ok && v.(bool) { - createRequest.WithOrReplace(true) + v, err := client.Views.ShowByID(ctx, id) + if err != nil { + return nil, err } - - if v, ok := d.GetOk("is_secure"); ok && v.(bool) { - createRequest.WithSecure(true) + if err := d.Set("name", v.Name); err != nil { + return nil, err } - if v, ok := d.GetOk("copy_grants"); ok && v.(bool) { - createRequest.WithCopyGrants(true) + if err := d.Set("change_tracking", booleanStringFromBool(v.IsChangeTracking())); err != nil { + return nil, err + } + if err := d.Set("is_recursive", booleanStringFromBool(v.IsRecursive())); err != nil { + return nil, err + } + if err := d.Set("is_secure", booleanStringFromBool(v.IsSecure)); err != nil { + return nil, err } + if err := d.Set("is_temporary", booleanStringFromBool(v.IsTemporary())); err != nil { + return nil, err + } + return []*schema.ResourceData{d}, nil +} - if v, ok := d.GetOk("comment"); ok { - createRequest.WithComment(v.(string)) +func CreateView(orReplace bool) schema.CreateContextFunc { + return func(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { + client := meta.(*provider.Context).Client + databaseName := d.Get("database").(string) + schemaName := d.Get("schema").(string) + name := d.Get("name").(string) + id := sdk.NewSchemaObjectIdentifier(databaseName, schemaName, name) + + statement := d.Get("statement").(string) + req := sdk.NewCreateViewRequest(id, statement) + + // TODO(next pr): remove or_replace field + if v := d.Get("or_replace"); v.(bool) || orReplace { + req.WithOrReplace(true) + } + + if v := d.Get("copy_grants"); v.(bool) { + req.WithCopyGrants(true) + } + + if v := d.Get("is_secure").(string); v != BooleanDefault { + parsed, err := strconv.ParseBool(v) + if err != nil { + return diag.FromErr(err) + } + req.WithSecure(parsed) + } + + if v := d.Get("is_temporary").(string); v != BooleanDefault { + parsed, err := strconv.ParseBool(v) + if err != nil { + return diag.FromErr(err) + } + req.WithTemporary(parsed) + } + + if v := d.Get("is_recursive").(string); v != BooleanDefault { + parsed, err := strconv.ParseBool(v) + if err != nil { + return diag.FromErr(err) + } + req.WithRecursive(parsed) + } + + if v := d.Get("comment").(string); len(v) > 0 { + req.WithComment(v) + } + + if v := d.Get("row_access_policy"); len(v.([]any)) > 0 { + req.WithRowAccessPolicy(*sdk.NewViewRowAccessPolicyRequest(extractPolicyWithColumns(v, "on"))) + } + + if v := d.Get("aggregation_policy"); len(v.([]any)) > 0 { + id, columns := extractPolicyWithColumns(v, "entity_key") + aggregationPolicyReq := sdk.NewViewAggregationPolicyRequest(id) + if len(columns) > 0 { + aggregationPolicyReq.WithEntityKey(columns) + } + req.WithAggregationPolicy(*aggregationPolicyReq) + } + + err := client.Views.Create(ctx, req) + if err != nil { + return diag.FromErr(fmt.Errorf("error creating view %v err = %w", id.Name(), err)) + } + + d.SetId(helpers.EncodeSnowflakeID(id)) + + if v := d.Get("change_tracking").(string); v != BooleanDefault { + parsed, err := booleanStringToBool(v) + if err != nil { + return diag.FromErr(err) + } + + err = client.Views.Alter(ctx, sdk.NewAlterViewRequest(id).WithSetChangeTracking(parsed)) + if err != nil { + return diag.FromErr(fmt.Errorf("error setting change_tracking in view %v err = %w", id.Name(), err)) + } + } + + return ReadView(false)(ctx, d, meta) } +} - err := client.Views.Create(ctx, createRequest) - if err != nil { - return fmt.Errorf("error creating view %v err = %w", name, err) +func extractPolicyWithColumns(v any, columnsKey string) (sdk.SchemaObjectIdentifier, []sdk.Column) { + policyConfig := v.([]any)[0].(map[string]any) + columnsRaw := expandStringList(policyConfig[columnsKey].(*schema.Set).List()) + columns := make([]sdk.Column, len(columnsRaw)) + for i := range columnsRaw { + columns[i] = sdk.Column{Value: columnsRaw[i]} } + return sdk.NewSchemaObjectIdentifierFromFullyQualifiedName(policyConfig["policy_name"].(string)), columns +} + +func ReadView(withExternalChangesMarking bool) schema.ReadContextFunc { + return func(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { + client := meta.(*provider.Context).Client + id := helpers.DecodeSnowflakeID(d.Id()).(sdk.SchemaObjectIdentifier) - // TODO [SNOW-1348118]: we have to set tags after creation because existing view extractor is not aware of TAG during CREATE - // Will be discussed with parser topic during resources redesign. - if _, ok := d.GetOk("tag"); ok { - err := client.Views.Alter(ctx, sdk.NewAlterViewRequest(id).WithSetTags(getPropertyTags(d, "tag"))) + view, err := client.Views.ShowByID(ctx, id) if err != nil { - return fmt.Errorf("error setting tags on view %v, err = %w", id, err) + if errors.Is(err, sdk.ErrObjectNotFound) { + d.SetId("") + return diag.Diagnostics{ + diag.Diagnostic{ + Severity: diag.Warning, + Summary: "Failed to query view. Marking the resource as removed.", + Detail: fmt.Sprintf("View: %s, Err: %s", id.FullyQualifiedName(), err), + }, + } + } + return diag.FromErr(err) } - } - d.SetId(helpers.EncodeSnowflakeID(id)) + if err = d.Set("name", view.Name); err != nil { + return diag.FromErr(err) + } + if err := d.Set(FullyQualifiedNameAttributeName, id.FullyQualifiedName()); err != nil { + return diag.FromErr(err) + } + if err = d.Set("database", view.DatabaseName); err != nil { + return diag.FromErr(err) + } + if err = d.Set("schema", view.SchemaName); err != nil { + return diag.FromErr(err) + } + if err = d.Set("copy_grants", view.HasCopyGrants()); err != nil { + return diag.FromErr(err) + } + if err = d.Set("comment", view.Comment); err != nil { + return diag.FromErr(err) + } + if withExternalChangesMarking { + if err = handleExternalChangesToObjectInShow(d, + showMapping{"is_secure", "is_secure", view.IsSecure, booleanStringFromBool(view.IsSecure), nil}, + showMapping{"text", "is_recursive", view.IsRecursive(), booleanStringFromBool(view.IsRecursive()), func(x any) any { + return strings.Contains(x.(string), "RECURSIVE") + }}, + showMapping{"text", "is_temporary", view.IsTemporary(), booleanStringFromBool(view.IsTemporary()), func(x any) any { + return strings.Contains(x.(string), "TEMPORARY") + }}, + showMapping{"change_tracking", "change_tracking", view.IsChangeTracking(), booleanStringFromBool(view.IsChangeTracking()), func(x any) any { + return x.(string) == "ON" + }}, + ); err != nil { + return diag.FromErr(err) + } + } + if err = setStateToValuesFromConfig(d, viewSchema, []string{ + "change_tracking", + "is_recursive", + "is_secure", + "is_temporary", + }); err != nil { + return diag.FromErr(err) + } - return ReadView(d, meta) -} + err = handlePolicyReferences(ctx, client, id, d) + if err != nil { + return diag.FromErr(err) + } + if view.Text != "" { + // Want to only capture the SELECT part of the query because before that is the CREATE part of the view. + extractor := snowflake.NewViewSelectStatementExtractor(view.Text) + statement, err := extractor.Extract() + if err != nil { + return diag.FromErr(err) + } + if err = d.Set("statement", statement); err != nil { + return diag.FromErr(err) + } + } else { + return diag.FromErr(fmt.Errorf("error reading view %v, err = %w, `text` is missing; if the view is secure then the role used by the provider must own the view (consult https://docs.snowflake.com/en/sql-reference/sql/create-view#usage-notes)", d.Id(), err)) + } -// ReadView implements schema.ReadFunc. -func ReadView(d *schema.ResourceData, meta interface{}) error { - client := meta.(*provider.Context).Client - ctx := context.Background() - id := helpers.DecodeSnowflakeID(d.Id()).(sdk.SchemaObjectIdentifier) + describeResult, err := client.Views.Describe(ctx, view.ID()) + if err != nil { + log.Printf("[DEBUG] describing view: %s, err: %s", id.FullyQualifiedName(), err) + } else { + if err = d.Set(DescribeOutputAttributeName, schemas.ViewDescriptionToSchema(describeResult)); err != nil { + return diag.FromErr(err) + } + } - view, err := client.Views.ShowByID(ctx, id) - if err != nil { - log.Printf("[DEBUG] view (%s) not found", d.Id()) - d.SetId("") + if err = d.Set(ShowOutputAttributeName, []map[string]any{schemas.ViewToSchema(view)}); err != nil { + return diag.FromErr(err) + } return nil } - if err := d.Set(FullyQualifiedNameAttributeName, id.FullyQualifiedName()); err != nil { - return err - } +} - if err = d.Set("name", view.Name); err != nil { - return err - } - if err = d.Set("is_secure", view.IsSecure); err != nil { - return err - } - if err = d.Set("copy_grants", view.HasCopyGrants()); err != nil { - return err - } - if err = d.Set("comment", view.Comment); err != nil { - return err +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) } - if err = d.Set("schema", view.SchemaName); err != nil { - return err + var aggregationPolicies []map[string]any + var rowAccessPolicies []map[string]any + for _, p := range policyRefs { + policyName := sdk.NewSchemaObjectIdentifier(*p.PolicyDb, *p.PolicySchema, p.PolicyName) + switch p.PolicyKind { + case string(sdk.PolicyKindAggregationPolicy): + var entityKey []string + if p.RefArgColumnNames != nil { + entityKey = sdk.ParseCommaSeparatedStringArray(*p.RefArgColumnNames, true) + } + aggregationPolicies = append(aggregationPolicies, map[string]any{ + "policy_name": policyName.FullyQualifiedName(), + "entity_key": entityKey, + }) + case string(sdk.PolicyKindRowAccessPolicy): + var on []string + if p.RefArgColumnNames != nil { + on = sdk.ParseCommaSeparatedStringArray(*p.RefArgColumnNames, true) + } + rowAccessPolicies = append(rowAccessPolicies, map[string]any{ + "policy_name": policyName.FullyQualifiedName(), + "on": on, + }) + default: + log.Printf("[WARN] unexpected policy kind %v in policy references returned from Snowflake", p.PolicyKind) + } } - if err = d.Set("database", view.DatabaseName); err != nil { + if err = d.Set("aggregation_policy", aggregationPolicies); err != nil { return err } - if err = d.Set("created_on", view.CreatedOn); err != nil { + if err = d.Set("row_access_policy", rowAccessPolicies); err != nil { return err } - - if view.Text != "" { - // Want to only capture the SELECT part of the query because before that is the CREATE part of the view. - extractor := snowflake.NewViewSelectStatementExtractor(view.Text) - substringOfQuery, err := extractor.Extract() - if err != nil { - return err - } - if err = d.Set("statement", substringOfQuery); err != nil { - return err - } - } else { - return fmt.Errorf("error reading view %v, err = %w, `text` is missing; if the view is secure then the role used by the provider must own the view (consult https://docs.snowflake.com/en/sql-reference/sql/create-view#usage-notes)", d.Id(), err) - } - - return nil + return err } -// UpdateView implements schema.UpdateFunc. -func UpdateView(d *schema.ResourceData, meta interface{}) error { +func UpdateView(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { client := meta.(*provider.Context).Client - ctx := context.Background() id := helpers.DecodeSnowflakeID(d.Id()).(sdk.SchemaObjectIdentifier) - // The only way to update the statement field in a view is to perform create or replace with the new statement. - // In case of any statement change, create or replace will be performed with all the old parameters, except statement - // and copy grants (which is always set to true to keep the permissions from the previous state). - if d.HasChange("statement") { - oldIsSecure, _ := d.GetChange("is_secure") - oldComment, _ := d.GetChange("comment") - oldTags, _ := d.GetChange("tag") - - createRequest := sdk.NewCreateViewRequest(id, d.Get("statement").(string)). - WithOrReplace(true). - WithCopyGrants(true). - WithComment(oldComment.(string)). - WithTag(getTagsFromList(oldTags.([]any))) - - if oldIsSecure.(bool) { - createRequest.WithSecure(true) - } - - err := client.Views.Create(ctx, createRequest) - if err != nil { - return fmt.Errorf("error when changing property on %v and performing create or replace to update view statements, err = %w", d.Id(), err) - } + // 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") { + return CreateView(true)(ctx, d, meta) } if d.HasChange("name") { @@ -236,7 +575,7 @@ func UpdateView(d *schema.ResourceData, meta interface{}) error { err := client.Views.Alter(ctx, sdk.NewAlterViewRequest(id).WithRenameTo(newId)) if err != nil { - return fmt.Errorf("error renaming view %v err = %w", d.Id(), err) + return diag.FromErr(fmt.Errorf("error renaming view %v err = %w", d.Id(), err)) } d.SetId(helpers.EncodeSnowflakeID(newId)) @@ -247,63 +586,114 @@ func UpdateView(d *schema.ResourceData, meta interface{}) error { if comment := d.Get("comment").(string); comment == "" { err := client.Views.Alter(ctx, sdk.NewAlterViewRequest(id).WithUnsetComment(true)) if err != nil { - return fmt.Errorf("error unsetting comment for view %v", d.Id()) + return diag.FromErr(fmt.Errorf("error unsetting comment for view %v", d.Id())) } } else { err := client.Views.Alter(ctx, sdk.NewAlterViewRequest(id).WithSetComment(comment)) if err != nil { - return fmt.Errorf("error updating comment for view %v", d.Id()) + return diag.FromErr(fmt.Errorf("error setting comment for view %v", d.Id())) } } } if d.HasChange("is_secure") { - if d.Get("is_secure").(bool) { - err := client.Views.Alter(ctx, sdk.NewAlterViewRequest(id).WithSetSecure(true)) + if v := d.Get("is_secure").(string); v != BooleanDefault { + parsed, err := booleanStringToBool(v) + if err != nil { + return diag.FromErr(err) + } + err = client.Views.Alter(ctx, sdk.NewAlterViewRequest(id).WithSetSecure(parsed)) if err != nil { - return fmt.Errorf("error setting secure for view %v", d.Id()) + return diag.FromErr(fmt.Errorf("error setting is_secure for view %v: %w", d.Id(), err)) } } else { err := client.Views.Alter(ctx, sdk.NewAlterViewRequest(id).WithUnsetSecure(true)) if err != nil { - return fmt.Errorf("error unsetting secure for view %v", d.Id()) + return diag.FromErr(fmt.Errorf("error unsetting is_secure for view %v: %w", d.Id(), err)) } } } - - if d.HasChange("tag") { - unsetTags, setTags := GetTagsDiff(d, "tag") - - if len(unsetTags) > 0 { - err := client.Views.Alter(ctx, sdk.NewAlterViewRequest(id).WithUnsetTags(unsetTags)) + if d.HasChange("change_tracking") { + if v := d.Get("change_tracking").(string); v != BooleanDefault { + parsed, err := booleanStringToBool(v) + if err != nil { + return diag.FromErr(err) + } + err = client.Views.Alter(ctx, sdk.NewAlterViewRequest(id).WithSetChangeTracking(parsed)) if err != nil { - return fmt.Errorf("error unsetting tags on %v, err = %w", d.Id(), err) + return diag.FromErr(fmt.Errorf("error setting change_tracking for view %v: %w", d.Id(), err)) + } + } else { + err := client.Views.Alter(ctx, sdk.NewAlterViewRequest(id).WithSetChangeTracking(false)) + if err != nil { + return diag.FromErr(fmt.Errorf("error unsetting change_tracking for view %v: %w", d.Id(), err)) } } + } + + if d.HasChange("row_access_policy") { + var addReq *sdk.ViewAddRowAccessPolicyRequest + var dropReq *sdk.ViewDropRowAccessPolicyRequest - if len(setTags) > 0 { - err := client.Views.Alter(ctx, sdk.NewAlterViewRequest(id).WithSetTags(setTags)) + oldRaw, newRaw := d.GetChange("row_access_policy") + if len(oldRaw.([]any)) > 0 { + oldId, _ := extractPolicyWithColumns(oldRaw, "on") + dropReq = sdk.NewViewDropRowAccessPolicyRequest(oldId) + } + if len(newRaw.([]any)) > 0 { + newId, newColumns := extractPolicyWithColumns(newRaw, "on") + addReq = sdk.NewViewAddRowAccessPolicyRequest(newId, newColumns) + } + req := sdk.NewAlterViewRequest(id) + if addReq != nil && dropReq != nil { // nolint + req.WithDropAndAddRowAccessPolicy(*sdk.NewViewDropAndAddRowAccessPolicyRequest(*dropReq, *addReq)) + } else if addReq != nil { + req.WithAddRowAccessPolicy(*addReq) + } else if dropReq != nil { + req.WithDropRowAccessPolicy(*dropReq) + } + err := client.Views.Alter(ctx, req) + if err != nil { + return diag.FromErr(fmt.Errorf("error altering row_access_policy for view %v: %w", d.Id(), err)) + } + } + if d.HasChange("aggregation_policy") { + if v, ok := d.GetOk("aggregation_policy"); ok { + newId, newColumns := extractPolicyWithColumns(v, "entity_key") + aggregationPolicyReq := sdk.NewViewSetAggregationPolicyRequest(newId) + if len(newColumns) > 0 { + aggregationPolicyReq.WithEntityKey(newColumns) + } + err := client.Views.Alter(ctx, sdk.NewAlterViewRequest(id).WithSetAggregationPolicy(*aggregationPolicyReq.WithForce(true))) + if err != nil { + return diag.FromErr(fmt.Errorf("error setting aggregation policy for view %v: %w", d.Id(), err)) + } + } else { + err := client.Views.Alter(ctx, sdk.NewAlterViewRequest(id).WithUnsetAggregationPolicy(*sdk.NewViewUnsetAggregationPolicyRequest())) if err != nil { - return fmt.Errorf("error setting tags on %v, err = %w", d.Id(), err) + return diag.FromErr(fmt.Errorf("error unsetting aggregation policy for view %v", d.Id())) } } } - return ReadView(d, meta) + return ReadView(false)(ctx, d, meta) } -// DeleteView implements schema.DeleteFunc. -func DeleteView(d *schema.ResourceData, meta interface{}) error { - client := meta.(*provider.Context).Client - ctx := context.Background() +func DeleteView(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { id := helpers.DecodeSnowflakeID(d.Id()).(sdk.SchemaObjectIdentifier) + client := meta.(*provider.Context).Client - err := client.Views.Drop(ctx, sdk.NewDropViewRequest(id)) + err := client.Views.Drop(ctx, sdk.NewDropViewRequest(id).WithIfExists(true)) if err != nil { - return err + return diag.Diagnostics{ + diag.Diagnostic{ + Severity: diag.Error, + Summary: "Error deleting view", + Detail: fmt.Sprintf("id %v err = %v", id.Name(), err), + }, + } } d.SetId("") - return nil } diff --git a/pkg/resources/view_acceptance_test.go b/pkg/resources/view_acceptance_test.go index b5149c8e7b..be3b810735 100644 --- a/pkg/resources/view_acceptance_test.go +++ b/pkg/resources/view_acceptance_test.go @@ -6,7 +6,15 @@ import ( "testing" acc "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/acceptance" - + accconfig "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/acceptance/bettertestspoc/config" + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/acceptance/testenvs" + + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/acceptance/bettertestspoc/assert" + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/acceptance/bettertestspoc/assert/resourceassert" + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/acceptance/bettertestspoc/assert/resourceshowoutputassert" + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/acceptance/bettertestspoc/config/model" + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/acceptance/importchecks" + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/helpers" "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/internal/snowflakeroles" "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/provider/resources" "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/sdk" @@ -16,193 +24,439 @@ import ( "github.com/hashicorp/terraform-plugin-testing/tfversion" ) -func TestAcc_View(t *testing.T) { - viewId := acc.TestClient().Ids.RandomSchemaObjectIdentifier() - accName := viewId.Name() - query := "SELECT ROLE_NAME, ROLE_OWNER FROM INFORMATION_SCHEMA.APPLICABLE_ROLES" - otherQuery := "SELECT ROLE_NAME, ROLE_OWNER FROM INFORMATION_SCHEMA.APPLICABLE_ROLES where ROLE_OWNER like 'foo%%'" - - m := func() map[string]config.Variable { - return map[string]config.Variable{ - "name": config.StringVariable(accName), - "database": config.StringVariable(acc.TestDatabaseName), - "schema": config.StringVariable(acc.TestSchemaName), - "comment": config.StringVariable("Terraform test resource"), - "is_secure": config.BoolVariable(true), - "or_replace": config.BoolVariable(false), - "copy_grants": config.BoolVariable(false), - "statement": config.StringVariable(query), +// TODO(SNOW-1423486): Fix using warehouse in all tests and remove unsetting testenvs.ConfigureClientOnce +// TODO(next pr): cleanup setting warehouse with unsafe_execute +func TestAcc_View_basic(t *testing.T) { + t.Setenv(string(testenvs.ConfigureClientOnce), "") + _ = testenvs.GetOrSkipTest(t, testenvs.EnableAcceptance) + acc.TestAccPreCheck(t) + + rowAccessPolicy, rowAccessPolicyCleanup := acc.TestClient().RowAccessPolicy.CreateRowAccessPolicyWithDataType(t, sdk.DataTypeVARCHAR) + t.Cleanup(rowAccessPolicyCleanup) + + aggregationPolicy, aggregationPolicyCleanup := acc.TestClient().AggregationPolicy.CreateAggregationPolicy(t) + t.Cleanup(aggregationPolicyCleanup) + + rowAccessPolicy2, rowAccessPolicy2Cleanup := acc.TestClient().RowAccessPolicy.CreateRowAccessPolicyWithDataType(t, sdk.DataTypeVARCHAR) + t.Cleanup(rowAccessPolicy2Cleanup) + + aggregationPolicy2, aggregationPolicy2Cleanup := acc.TestClient().AggregationPolicy.CreateAggregationPolicy(t) + t.Cleanup(aggregationPolicy2Cleanup) + + id := acc.TestClient().Ids.RandomSchemaObjectIdentifier() + statement := "SELECT ROLE_NAME, ROLE_OWNER FROM INFORMATION_SCHEMA.APPLICABLE_ROLES" + otherStatement := "SELECT ROLE_NAME, ROLE_OWNER FROM INFORMATION_SCHEMA.APPLICABLE_ROLES where ROLE_OWNER like 'foo%%'" + comment := "Terraform test resource'" + + viewModel := model.View("test", id.DatabaseName(), id.Name(), id.SchemaName(), statement) + viewModelWithDependency := model.View("test", id.DatabaseName(), id.Name(), id.SchemaName(), statement).WithDependsOn([]string{"snowflake_unsafe_execute.use_warehouse"}) + + // generators currently don't handle lists, so use the old way + basicUpdate := func(rap, ap sdk.SchemaObjectIdentifier, statement string) config.Variables { + return config.Variables{ + "name": config.StringVariable(id.Name()), + "database": config.StringVariable(id.DatabaseName()), + "schema": config.StringVariable(id.SchemaName()), + "statement": config.StringVariable(statement), + "row_access_policy": config.StringVariable(rap.FullyQualifiedName()), + "row_access_policy_on": config.ListVariable(config.StringVariable("ROLE_NAME")), + "aggregation_policy": config.StringVariable(ap.FullyQualifiedName()), + "aggregation_policy_entity_key": config.ListVariable(config.StringVariable("ROLE_NAME")), + "comment": config.StringVariable(comment), } } - m2 := m() - m2["comment"] = config.StringVariable("different comment") - m2["is_secure"] = config.BoolVariable(false) - m3 := m() - m3["comment"] = config.StringVariable("different comment") - m3["is_secure"] = config.BoolVariable(false) - m3["statement"] = config.StringVariable(otherQuery) resource.Test(t, resource.TestCase{ ProtoV6ProviderFactories: acc.TestAccProtoV6ProviderFactories, - PreCheck: func() { acc.TestAccPreCheck(t) }, TerraformVersionChecks: []tfversion.TerraformVersionCheck{ tfversion.RequireAbove(tfversion.Version1_5_0), }, CheckDestroy: acc.CheckDestroy(t, resources.View), Steps: []resource.TestStep{ + // without optionals { - ConfigDirectory: acc.ConfigurationDirectory("TestAcc_View_basic"), - ConfigVariables: m(), - Check: resource.ComposeTestCheckFunc( - resource.TestCheckResourceAttr("snowflake_view.test", "name", accName), - resource.TestCheckResourceAttr("snowflake_view.test", "statement", query), - resource.TestCheckResourceAttr("snowflake_view.test", "database", acc.TestDatabaseName), - resource.TestCheckResourceAttr("snowflake_view.test", "schema", acc.TestSchemaName), - resource.TestCheckResourceAttr("snowflake_view.test", "comment", "Terraform test resource"), - resource.TestCheckResourceAttr("snowflake_view.test", "copy_grants", "false"), - checkBool("snowflake_view.test", "is_secure", true), + Config: accconfig.FromModel(t, viewModelWithDependency) + useWarehouseConfig(acc.TestWarehouseName), + Check: assert.AssertThat(t, resourceassert.ViewResource(t, "snowflake_view.test"). + HasNameString(id.Name()). + HasStatementString(statement). + HasDatabaseString(id.DatabaseName()). + HasSchemaString(id.SchemaName())), + }, + // import - without optionals + { + Config: accconfig.FromModel(t, viewModel), + ResourceName: "snowflake_view.test", + ImportState: true, + ImportStateCheck: assert.AssertThatImport(t, assert.CheckImport(importchecks.TestCheckResourceAttrInstanceState(helpers.EncodeSnowflakeID(id), "name", id.Name())), + resourceassert.ImportedViewResource(t, helpers.EncodeSnowflakeID(id)). + HasNameString(id.Name()). + HasDatabaseString(id.DatabaseName()). + HasSchemaString(id.SchemaName()). + HasStatementString(statement)), + }, + // set policies externally + { + PreConfig: func() { + acc.TestClient().View.Alter(t, sdk.NewAlterViewRequest(id).WithAddRowAccessPolicy(*sdk.NewViewAddRowAccessPolicyRequest(rowAccessPolicy.ID(), []sdk.Column{{Value: "ROLE_NAME"}}))) + acc.TestClient().View.Alter(t, sdk.NewAlterViewRequest(id).WithSetAggregationPolicy(*sdk.NewViewSetAggregationPolicyRequest(aggregationPolicy))) + }, + Config: accconfig.FromModel(t, viewModel), + Check: assert.AssertThat(t, resourceassert.ViewResource(t, "snowflake_view.test"). + HasNameString(id.Name()). + HasStatementString(statement). + HasDatabaseString(id.DatabaseName()). + HasSchemaString(id.SchemaName()), + assert.Check(resource.TestCheckResourceAttr("snowflake_view.test", "aggregation_policy.#", "0")), + assert.Check(resource.TestCheckResourceAttr("snowflake_view.test", "row_access_policy.#", "0")), ), }, - // update parameters + // set other fields { - ConfigDirectory: acc.ConfigurationDirectory("TestAcc_View_basic"), - ConfigVariables: m2, - Check: resource.ComposeTestCheckFunc( - resource.TestCheckResourceAttr("snowflake_view.test", "name", accName), - resource.TestCheckResourceAttr("snowflake_view.test", "statement", query), - resource.TestCheckResourceAttr("snowflake_view.test", "database", acc.TestDatabaseName), - resource.TestCheckResourceAttr("snowflake_view.test", "schema", acc.TestSchemaName), - resource.TestCheckResourceAttr("snowflake_view.test", "comment", "different comment"), - resource.TestCheckResourceAttr("snowflake_view.test", "copy_grants", "false"), - checkBool("snowflake_view.test", "is_secure", false), + ConfigDirectory: acc.ConfigurationDirectory("TestAcc_View/basic_update"), + ConfigVariables: basicUpdate(rowAccessPolicy.ID(), aggregationPolicy, statement), + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectResourceAction("snowflake_view.test", plancheck.ResourceActionUpdate), + }, + }, + Check: assert.AssertThat(t, resourceassert.ViewResource(t, "snowflake_view.test"). + HasNameString(id.Name()). + HasStatementString(statement). + HasDatabaseString(id.DatabaseName()). + HasSchemaString(id.SchemaName()). + HasCommentString(comment), + assert.Check(resource.TestCheckResourceAttr("snowflake_view.test", "aggregation_policy.#", "1")), + assert.Check(resource.TestCheckResourceAttr("snowflake_view.test", "aggregation_policy.0.policy_name", aggregationPolicy.FullyQualifiedName())), + assert.Check(resource.TestCheckResourceAttr("snowflake_view.test", "aggregation_policy.0.entity_key.#", "1")), + assert.Check(resource.TestCheckResourceAttr("snowflake_view.test", "aggregation_policy.0.entity_key.0", "ROLE_NAME")), + assert.Check(resource.TestCheckResourceAttr("snowflake_view.test", "row_access_policy.#", "1")), + assert.Check(resource.TestCheckResourceAttr("snowflake_view.test", "row_access_policy.0.policy_name", rowAccessPolicy.ID().FullyQualifiedName())), + assert.Check(resource.TestCheckResourceAttr("snowflake_view.test", "row_access_policy.0.on.#", "1")), + assert.Check(resource.TestCheckResourceAttr("snowflake_view.test", "row_access_policy.0.on.0", "ROLE_NAME")), ), }, - // change statement + // change policies { - ConfigDirectory: acc.ConfigurationDirectory("TestAcc_View_basic"), - ConfigVariables: m3, - Check: resource.ComposeTestCheckFunc( - resource.TestCheckResourceAttr("snowflake_view.test", "name", accName), - resource.TestCheckResourceAttr("snowflake_view.test", "statement", otherQuery), - resource.TestCheckResourceAttr("snowflake_view.test", "database", acc.TestDatabaseName), - resource.TestCheckResourceAttr("snowflake_view.test", "schema", acc.TestSchemaName), - resource.TestCheckResourceAttr("snowflake_view.test", "comment", "different comment"), - // copy grants is currently set to true for recreation - resource.TestCheckResourceAttr("snowflake_view.test", "copy_grants", "true"), - checkBool("snowflake_view.test", "is_secure", false), + ConfigDirectory: acc.ConfigurationDirectory("TestAcc_View/basic_update"), + ConfigVariables: basicUpdate(rowAccessPolicy2.ID(), aggregationPolicy2, statement), + Check: assert.AssertThat(t, resourceassert.ViewResource(t, "snowflake_view.test"). + HasNameString(id.Name()). + HasStatementString(statement). + HasDatabaseString(id.DatabaseName()). + HasSchemaString(id.SchemaName()). + HasCommentString(comment), + assert.Check(resource.TestCheckResourceAttr("snowflake_view.test", "aggregation_policy.#", "1")), + assert.Check(resource.TestCheckResourceAttr("snowflake_view.test", "aggregation_policy.0.policy_name", aggregationPolicy2.FullyQualifiedName())), + assert.Check(resource.TestCheckResourceAttr("snowflake_view.test", "aggregation_policy.0.entity_key.#", "1")), + assert.Check(resource.TestCheckResourceAttr("snowflake_view.test", "aggregation_policy.0.entity_key.0", "ROLE_NAME")), + assert.Check(resource.TestCheckResourceAttr("snowflake_view.test", "row_access_policy.#", "1")), + assert.Check(resource.TestCheckResourceAttr("snowflake_view.test", "row_access_policy.0.policy_name", rowAccessPolicy2.ID().FullyQualifiedName())), + assert.Check(resource.TestCheckResourceAttr("snowflake_view.test", "row_access_policy.0.on.#", "1")), + assert.Check(resource.TestCheckResourceAttr("snowflake_view.test", "row_access_policy.0.on.0", "ROLE_NAME")), ), }, - // change statement externally + // change statement and policies + { + ConfigDirectory: acc.ConfigurationDirectory("TestAcc_View/basic_update"), + ConfigVariables: basicUpdate(rowAccessPolicy.ID(), aggregationPolicy, otherStatement), + Check: assert.AssertThat(t, resourceassert.ViewResource(t, "snowflake_view.test"). + HasNameString(id.Name()). + HasStatementString(otherStatement). + HasDatabaseString(id.DatabaseName()). + HasSchemaString(id.SchemaName()). + HasCommentString(comment), + assert.Check(resource.TestCheckResourceAttr("snowflake_view.test", "aggregation_policy.#", "1")), + assert.Check(resource.TestCheckResourceAttr("snowflake_view.test", "aggregation_policy.0.policy_name", aggregationPolicy.FullyQualifiedName())), + assert.Check(resource.TestCheckResourceAttr("snowflake_view.test", "aggregation_policy.0.entity_key.#", "1")), + assert.Check(resource.TestCheckResourceAttr("snowflake_view.test", "aggregation_policy.0.entity_key.0", "ROLE_NAME")), + assert.Check(resource.TestCheckResourceAttr("snowflake_view.test", "row_access_policy.#", "1")), + assert.Check(resource.TestCheckResourceAttr("snowflake_view.test", "row_access_policy.0.policy_name", rowAccessPolicy.ID().FullyQualifiedName())), + assert.Check(resource.TestCheckResourceAttr("snowflake_view.test", "row_access_policy.0.on.#", "1")), + assert.Check(resource.TestCheckResourceAttr("snowflake_view.test", "row_access_policy.0.on.0", "ROLE_NAME")), + ), + }, + // change statements externally { PreConfig: func() { - acc.TestClient().View.RecreateView(t, viewId, query) + acc.TestClient().View.RecreateView(t, id, statement) }, - ConfigDirectory: acc.ConfigurationDirectory("TestAcc_View_basic"), - ConfigVariables: m3, - Check: resource.ComposeTestCheckFunc( - resource.TestCheckResourceAttr("snowflake_view.test", "name", accName), - resource.TestCheckResourceAttr("snowflake_view.test", "statement", otherQuery), - resource.TestCheckResourceAttr("snowflake_view.test", "database", acc.TestDatabaseName), - resource.TestCheckResourceAttr("snowflake_view.test", "schema", acc.TestSchemaName), - resource.TestCheckResourceAttr("snowflake_view.test", "comment", "different comment"), - resource.TestCheckResourceAttr("snowflake_view.test", "copy_grants", "true"), - checkBool("snowflake_view.test", "is_secure", false), + ConfigDirectory: acc.ConfigurationDirectory("TestAcc_View/basic_update"), + ConfigVariables: basicUpdate(rowAccessPolicy.ID(), aggregationPolicy, otherStatement), + Check: assert.AssertThat(t, resourceassert.ViewResource(t, "snowflake_view.test"). + HasNameString(id.Name()). + HasStatementString(otherStatement). + HasDatabaseString(id.DatabaseName()). + HasSchemaString(id.SchemaName()). + HasCommentString(comment), + assert.Check(resource.TestCheckResourceAttr("snowflake_view.test", "aggregation_policy.#", "1")), + assert.Check(resource.TestCheckResourceAttr("snowflake_view.test", "aggregation_policy.0.policy_name", aggregationPolicy.FullyQualifiedName())), + assert.Check(resource.TestCheckResourceAttr("snowflake_view.test", "aggregation_policy.0.entity_key.#", "1")), + assert.Check(resource.TestCheckResourceAttr("snowflake_view.test", "aggregation_policy.0.entity_key.0", "ROLE_NAME")), + assert.Check(resource.TestCheckResourceAttr("snowflake_view.test", "row_access_policy.#", "1")), + assert.Check(resource.TestCheckResourceAttr("snowflake_view.test", "row_access_policy.0.policy_name", rowAccessPolicy.ID().FullyQualifiedName())), + assert.Check(resource.TestCheckResourceAttr("snowflake_view.test", "row_access_policy.0.on.#", "1")), + assert.Check(resource.TestCheckResourceAttr("snowflake_view.test", "row_access_policy.0.on.0", "ROLE_NAME")), + ), + }, + // unset policies externally + { + PreConfig: func() { + acc.TestClient().View.Alter(t, sdk.NewAlterViewRequest(id).WithDropAllRowAccessPolicies(true)) + acc.TestClient().View.Alter(t, sdk.NewAlterViewRequest(id).WithUnsetAggregationPolicy(*sdk.NewViewUnsetAggregationPolicyRequest())) + }, + ConfigDirectory: acc.ConfigurationDirectory("TestAcc_View/basic_update"), + ConfigVariables: basicUpdate(rowAccessPolicy.ID(), aggregationPolicy, otherStatement), + Check: assert.AssertThat(t, resourceassert.ViewResource(t, "snowflake_view.test"). + HasNameString(id.Name()). + HasStatementString(otherStatement). + HasDatabaseString(id.DatabaseName()). + HasSchemaString(id.SchemaName()). + HasCommentString(comment), + assert.Check(resource.TestCheckResourceAttr("snowflake_view.test", "aggregation_policy.#", "1")), + assert.Check(resource.TestCheckResourceAttr("snowflake_view.test", "aggregation_policy.0.policy_name", aggregationPolicy.FullyQualifiedName())), + assert.Check(resource.TestCheckResourceAttr("snowflake_view.test", "aggregation_policy.0.entity_key.#", "1")), + assert.Check(resource.TestCheckResourceAttr("snowflake_view.test", "aggregation_policy.0.entity_key.0", "ROLE_NAME")), + assert.Check(resource.TestCheckResourceAttr("snowflake_view.test", "row_access_policy.#", "1")), + assert.Check(resource.TestCheckResourceAttr("snowflake_view.test", "row_access_policy.0.policy_name", rowAccessPolicy.ID().FullyQualifiedName())), + assert.Check(resource.TestCheckResourceAttr("snowflake_view.test", "row_access_policy.0.on.#", "1")), + assert.Check(resource.TestCheckResourceAttr("snowflake_view.test", "row_access_policy.0.on.0", "ROLE_NAME")), + ), + }, + + // import - with optionals + { + ConfigDirectory: acc.ConfigurationDirectory("TestAcc_View/basic_update"), + ConfigVariables: basicUpdate(rowAccessPolicy.ID(), aggregationPolicy, otherStatement), + ResourceName: "snowflake_view.test", + ImportState: true, + ImportStateCheck: assert.AssertThatImport(t, assert.CheckImport(importchecks.TestCheckResourceAttrInstanceState(helpers.EncodeSnowflakeID(id), "name", id.Name())), + resourceassert.ImportedViewResource(t, helpers.EncodeSnowflakeID(id)). + HasNameString(id.Name()). + HasStatementString(otherStatement). + HasDatabaseString(id.DatabaseName()). + HasSchemaString(id.SchemaName()). + HasCommentString(comment). + HasIsSecureString("false"). + HasIsTemporaryString("false"). + HasChangeTrackingString("false"), + assert.CheckImport(importchecks.TestCheckResourceAttrInstanceState(helpers.EncodeSnowflakeID(id), "aggregation_policy.#", "1")), + assert.CheckImport(importchecks.TestCheckResourceAttrInstanceState(helpers.EncodeSnowflakeID(id), "aggregation_policy.0.policy_name", aggregationPolicy.FullyQualifiedName())), + assert.CheckImport(importchecks.TestCheckResourceAttrInstanceState(helpers.EncodeSnowflakeID(id), "aggregation_policy.0.entity_key.#", "1")), + assert.CheckImport(importchecks.TestCheckResourceAttrInstanceState(helpers.EncodeSnowflakeID(id), "aggregation_policy.0.entity_key.0", "ROLE_NAME")), + assert.CheckImport(importchecks.TestCheckResourceAttrInstanceState(helpers.EncodeSnowflakeID(id), "row_access_policy.#", "1")), + assert.CheckImport(importchecks.TestCheckResourceAttrInstanceState(helpers.EncodeSnowflakeID(id), "row_access_policy.0.policy_name", rowAccessPolicy.ID().FullyQualifiedName())), + assert.CheckImport(importchecks.TestCheckResourceAttrInstanceState(helpers.EncodeSnowflakeID(id), "row_access_policy.0.on.#", "1")), + assert.CheckImport(importchecks.TestCheckResourceAttrInstanceState(helpers.EncodeSnowflakeID(id), "row_access_policy.0.on.0", "ROLE_NAME")), + ), + }, + // unset + { + Config: accconfig.FromModel(t, viewModel.WithStatement(otherStatement)), + ResourceName: "snowflake_view.test", + Check: assert.AssertThat(t, resourceassert.ViewResource(t, "snowflake_view.test"). + HasNameString(id.Name()). + HasStatementString(otherStatement). + HasDatabaseString(id.DatabaseName()). + HasSchemaString(id.SchemaName()). + HasCommentString(""), + assert.Check(resource.TestCheckNoResourceAttr("snowflake_view.test", "aggregation_policy.#")), + assert.Check(resource.TestCheckNoResourceAttr("snowflake_view.test", "row_access_policy.#")), + ), + }, + // recreate - change is_recursive + { + Config: accconfig.FromModel(t, viewModel.WithIsRecursive("true")), + Check: assert.AssertThat(t, resourceassert.ViewResource(t, "snowflake_view.test"). + HasNameString(id.Name()). + HasStatementString(otherStatement). + HasDatabaseString(id.DatabaseName()). + HasSchemaString(id.SchemaName()). + HasCommentString(""). + HasIsRecursiveString("true"). + HasIsTemporaryString("default"). + HasChangeTrackingString("default"), + assert.Check(resource.TestCheckNoResourceAttr("snowflake_view.test", "aggregation_policy.#")), + assert.Check(resource.TestCheckNoResourceAttr("snowflake_view.test", "row_access_policy.#")), ), }, - // IMPORT + }, + }) +} + +func TestAcc_View_recursive(t *testing.T) { + t.Setenv(string(testenvs.ConfigureClientOnce), "") + _ = testenvs.GetOrSkipTest(t, testenvs.EnableAcceptance) + acc.TestAccPreCheck(t) + id := acc.TestClient().Ids.RandomSchemaObjectIdentifier() + statement := "SELECT ROLE_NAME, ROLE_OWNER FROM INFORMATION_SCHEMA.APPLICABLE_ROLES" + viewModel := model.View("test", id.DatabaseName(), id.Name(), id.SchemaName(), statement).WithDependsOn([]string{"snowflake_unsafe_execute.use_warehouse"}) + + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: acc.TestAccProtoV6ProviderFactories, + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.RequireAbove(tfversion.Version1_5_0), + }, + CheckDestroy: acc.CheckDestroy(t, resources.View), + Steps: []resource.TestStep{ + { + Config: accconfig.FromModel(t, viewModel.WithIsRecursive("true")) + useWarehouseConfig(acc.TestWarehouseName), + Check: assert.AssertThat(t, resourceassert.ViewResource(t, "snowflake_view.test"). + HasNameString(id.Name()). + HasStatementString(statement). + HasDatabaseString(id.DatabaseName()). + HasSchemaString(id.SchemaName()). + HasIsRecursiveString("true")), + }, { - ConfigVariables: m3, - ResourceName: "snowflake_view.test", - ImportState: true, - ImportStateVerify: true, - ImportStateVerifyIgnore: []string{"or_replace"}, + Config: accconfig.FromModel(t, viewModel.WithIsRecursive("true")) + useWarehouseConfig(acc.TestWarehouseName), + ResourceName: "snowflake_view.test", + ImportState: true, + ImportStateCheck: assert.AssertThatImport(t, assert.CheckImport(importchecks.TestCheckResourceAttrInstanceState(helpers.EncodeSnowflakeID(id), "name", id.Name())), + resourceassert.ImportedViewResource(t, helpers.EncodeSnowflakeID(id)). + HasNameString(id.Name()). + HasDatabaseString(id.DatabaseName()). + HasSchemaString(id.SchemaName()). + HasStatementString(statement). + HasIsRecursiveString("true")), }, }, }) } -func TestAcc_View_Tags(t *testing.T) { - viewName := acc.TestClient().Ids.Alpha() - tag1Name := acc.TestClient().Ids.Alpha() - tag2Name := acc.TestClient().Ids.Alpha() +func TestAcc_View_temporary(t *testing.T) { + t.Setenv(string(testenvs.ConfigureClientOnce), "") + _ = testenvs.GetOrSkipTest(t, testenvs.EnableAcceptance) + // we use one configured client, so a temporary view should be visible after creation + _ = testenvs.GetOrSkipTest(t, testenvs.ConfigureClientOnce) + 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).WithDependsOn([]string{"snowflake_unsafe_execute.use_warehouse"}) + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: acc.TestAccProtoV6ProviderFactories, + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.RequireAbove(tfversion.Version1_5_0), + }, + CheckDestroy: acc.CheckDestroy(t, resources.View), + Steps: []resource.TestStep{ + { + Config: accconfig.FromModel(t, viewModel.WithIsTemporary("true")) + useWarehouseConfig(acc.TestWarehouseName), + Check: assert.AssertThat(t, resourceassert.ViewResource(t, "snowflake_view.test"). + HasNameString(id.Name()). + HasStatementString(statement). + HasDatabaseString(id.DatabaseName()). + HasSchemaString(id.SchemaName()). + HasIsTemporaryString("true")), + }, + }, + }) +} - query := "SELECT ROLE_NAME, ROLE_OWNER FROM INFORMATION_SCHEMA.APPLICABLE_ROLES" +func TestAcc_View_complete(t *testing.T) { + t.Setenv(string(testenvs.ConfigureClientOnce), "") + _ = testenvs.GetOrSkipTest(t, testenvs.EnableAcceptance) + acc.TestAccPreCheck(t) + id := acc.TestClient().Ids.RandomSchemaObjectIdentifier() + // use a simple table to test change_tracking, otherwise it fails with: Change tracking is not supported on queries with joins of type '[LEFT_OUTER_JOIN]' + table, tableCleanup := acc.TestClient().Table.CreateTable(t) + t.Cleanup(tableCleanup) + statement := fmt.Sprintf("SELECT id FROM %s", table.ID().FullyQualifiedName()) + rowAccessPolicy, rowAccessPolicyCleanup := acc.TestClient().RowAccessPolicy.CreateRowAccessPolicyWithDataType(t, sdk.DataTypeNumber) + t.Cleanup(rowAccessPolicyCleanup) + + aggregationPolicy, aggregationPolicyCleanup := acc.TestClient().AggregationPolicy.CreateAggregationPolicy(t) + t.Cleanup(aggregationPolicyCleanup) m := func() map[string]config.Variable { return map[string]config.Variable{ - "name": config.StringVariable(viewName), - "database": config.StringVariable(acc.TestDatabaseName), - "schema": config.StringVariable(acc.TestSchemaName), - "statement": config.StringVariable(query), - "tag1Name": config.StringVariable(tag1Name), - "tag2Name": config.StringVariable(tag2Name), + "name": config.StringVariable(id.Name()), + "database": config.StringVariable(id.DatabaseName()), + "schema": config.StringVariable(id.SchemaName()), + "comment": config.StringVariable("Terraform test resource"), + "is_secure": config.BoolVariable(true), + "is_temporary": config.BoolVariable(false), + "or_replace": config.BoolVariable(false), + "copy_grants": config.BoolVariable(false), + "change_tracking": config.BoolVariable(true), + "row_access_policy": config.StringVariable(rowAccessPolicy.ID().FullyQualifiedName()), + "row_access_policy_on": config.ListVariable(config.StringVariable("ID")), + "aggregation_policy": config.StringVariable(aggregationPolicy.FullyQualifiedName()), + "aggregation_policy_entity_key": config.ListVariable(config.StringVariable("ID")), + "statement": config.StringVariable(statement), + "warehouse": config.StringVariable(acc.TestWarehouseName), } } - resource.Test(t, resource.TestCase{ ProtoV6ProviderFactories: acc.TestAccProtoV6ProviderFactories, - PreCheck: func() { acc.TestAccPreCheck(t) }, TerraformVersionChecks: []tfversion.TerraformVersionCheck{ tfversion.RequireAbove(tfversion.Version1_5_0), }, CheckDestroy: acc.CheckDestroy(t, resources.View), Steps: []resource.TestStep{ - // create tags { - ConfigDirectory: config.TestStepDirectory(), + ConfigDirectory: acc.ConfigurationDirectory("TestAcc_View/complete"), ConfigVariables: m(), - Check: resource.ComposeTestCheckFunc( - resource.TestCheckResourceAttr("snowflake_view.test", "name", viewName), - resource.TestCheckResourceAttr("snowflake_view.test", "tag.#", "1"), - resource.TestCheckResourceAttr("snowflake_view.test", "tag.0.name", tag1Name), - resource.TestCheckResourceAttr("snowflake_view.test", "tag.0.value", "some_value"), + Check: assert.AssertThat(t, resourceassert.ViewResource(t, "snowflake_view.test"). + HasNameString(id.Name()). + HasStatementString(statement). + HasDatabaseString(id.DatabaseName()). + HasSchemaString(id.SchemaName()). + HasCommentString("Terraform test resource"). + HasIsSecureString("true"). + HasIsTemporaryString("false"). + HasChangeTrackingString("true"), + assert.Check(resource.TestCheckResourceAttr("snowflake_view.test", "aggregation_policy.#", "1")), + assert.Check(resource.TestCheckResourceAttr("snowflake_view.test", "aggregation_policy.0.policy_name", aggregationPolicy.FullyQualifiedName())), + assert.Check(resource.TestCheckResourceAttr("snowflake_view.test", "aggregation_policy.0.entity_key.#", "1")), + assert.Check(resource.TestCheckResourceAttr("snowflake_view.test", "aggregation_policy.0.entity_key.0", "ID")), + assert.Check(resource.TestCheckResourceAttr("snowflake_view.test", "row_access_policy.#", "1")), + assert.Check(resource.TestCheckResourceAttr("snowflake_view.test", "row_access_policy.0.policy_name", rowAccessPolicy.ID().FullyQualifiedName())), + assert.Check(resource.TestCheckResourceAttr("snowflake_view.test", "row_access_policy.0.on.#", "1")), + assert.Check(resource.TestCheckResourceAttr("snowflake_view.test", "row_access_policy.0.on.0", "ID")), + resourceshowoutputassert.ViewShowOutput(t, "snowflake_view.test"). + HasName(id.Name()). + HasDatabaseName(id.DatabaseName()). + HasSchemaName(id.SchemaName()). + HasComment("Terraform test resource"). + HasIsSecure(true). + HasChangeTracking("ON"), ), }, - // update tags { - ConfigDirectory: config.TestStepDirectory(), + ConfigDirectory: acc.ConfigurationDirectory("TestAcc_View/complete"), ConfigVariables: m(), - Check: resource.ComposeTestCheckFunc( - resource.TestCheckResourceAttr("snowflake_view.test", "name", viewName), - resource.TestCheckResourceAttr("snowflake_view.test", "tag.#", "1"), - resource.TestCheckResourceAttr("snowflake_view.test", "tag.0.name", tag2Name), - resource.TestCheckResourceAttr("snowflake_view.test", "tag.0.value", "some_value"), + ResourceName: "snowflake_view.test", + ImportState: true, + ImportStateCheck: assert.AssertThatImport(t, assert.CheckImport(importchecks.TestCheckResourceAttrInstanceState(helpers.EncodeSnowflakeID(id), "name", id.Name())), + resourceassert.ImportedViewResource(t, helpers.EncodeSnowflakeID(id)). + HasNameString(id.Name()). + HasStatementString(statement). + HasDatabaseString(id.DatabaseName()). + HasSchemaString(id.SchemaName()). + HasCommentString("Terraform test resource"). + HasIsSecureString("true"). + HasIsTemporaryString("false").HasChangeTrackingString("true"), + assert.CheckImport(importchecks.TestCheckResourceAttrInstanceState(helpers.EncodeSnowflakeID(id), "aggregation_policy.#", "1")), + assert.CheckImport(importchecks.TestCheckResourceAttrInstanceState(helpers.EncodeSnowflakeID(id), "aggregation_policy.0.policy_name", aggregationPolicy.FullyQualifiedName())), + assert.CheckImport(importchecks.TestCheckResourceAttrInstanceState(helpers.EncodeSnowflakeID(id), "aggregation_policy.0.entity_key.#", "1")), + assert.CheckImport(importchecks.TestCheckResourceAttrInstanceState(helpers.EncodeSnowflakeID(id), "aggregation_policy.0.entity_key.0", "ID")), + assert.CheckImport(importchecks.TestCheckResourceAttrInstanceState(helpers.EncodeSnowflakeID(id), "row_access_policy.#", "1")), + assert.CheckImport(importchecks.TestCheckResourceAttrInstanceState(helpers.EncodeSnowflakeID(id), "row_access_policy.0.policy_name", rowAccessPolicy.ID().FullyQualifiedName())), + assert.CheckImport(importchecks.TestCheckResourceAttrInstanceState(helpers.EncodeSnowflakeID(id), "row_access_policy.0.on.#", "1")), + assert.CheckImport(importchecks.TestCheckResourceAttrInstanceState(helpers.EncodeSnowflakeID(id), "row_access_policy.0.on.0", "ID")), ), }, - // IMPORT - { - ConfigVariables: m(), - ResourceName: "snowflake_view.test", - ImportState: true, - ImportStateVerify: true, - ImportStateVerifyIgnore: []string{"or_replace", "tag"}, - }, }, }) } func TestAcc_View_Rename(t *testing.T) { - viewName := acc.TestClient().Ids.Alpha() - newViewName := acc.TestClient().Ids.Alpha() - viewId := sdk.NewSchemaObjectIdentifier(acc.TestDatabaseName, acc.TestSchemaName, viewName) - newViewId := sdk.NewSchemaObjectIdentifier(acc.TestDatabaseName, acc.TestSchemaName, newViewName) - query := "SELECT ROLE_NAME, ROLE_OWNER FROM INFORMATION_SCHEMA.APPLICABLE_ROLES" - - m := func() map[string]config.Variable { - return map[string]config.Variable{ - "name": config.StringVariable(viewName), - "database": config.StringVariable(acc.TestDatabaseName), - "schema": config.StringVariable(acc.TestSchemaName), - "comment": config.StringVariable("Terraform test resource"), - "is_secure": config.BoolVariable(true), - "or_replace": config.BoolVariable(false), - "copy_grants": config.BoolVariable(false), - "statement": config.StringVariable(query), - } - } - m2 := m() - m2["name"] = config.StringVariable(newViewName) - m2["comment"] = config.StringVariable("new comment") + 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").WithDependsOn([]string{"snowflake_unsafe_execute.use_warehouse"}) + newViewModel := model.View("test", newId.DatabaseName(), newId.Name(), newId.SchemaName(), statement).WithComment("foo") resource.Test(t, resource.TestCase{ ProtoV6ProviderFactories: acc.TestAccProtoV6ProviderFactories, @@ -213,26 +467,25 @@ func TestAcc_View_Rename(t *testing.T) { CheckDestroy: acc.CheckDestroy(t, resources.View), Steps: []resource.TestStep{ { - ConfigDirectory: acc.ConfigurationDirectory("TestAcc_View_basic"), - ConfigVariables: m(), - Check: resource.ComposeTestCheckFunc( - resource.TestCheckResourceAttr("snowflake_view.test", "name", viewName), - resource.TestCheckResourceAttr("snowflake_view.test", "fully_qualified_name", viewId.FullyQualifiedName()), + Config: accconfig.FromModel(t, viewModel) + useWarehouseConfig(acc.TestWarehouseName), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr("snowflake_view.test", "name", id.Name()), + resource.TestCheckResourceAttr("snowflake_view.test", "comment", "foo"), + resource.TestCheckResourceAttr("snowflake_view.test", "fully_qualified_name", id.FullyQualifiedName()), ), }, // rename with one param changed { - ConfigDirectory: acc.ConfigurationDirectory("TestAcc_View_basic"), - ConfigVariables: m2, + Config: accconfig.FromModel(t, newViewModel), ConfigPlanChecks: resource.ConfigPlanChecks{ PreApply: []plancheck.PlanCheck{ plancheck.ExpectResourceAction("snowflake_view.test", plancheck.ResourceActionUpdate), }, }, - Check: resource.ComposeTestCheckFunc( - resource.TestCheckResourceAttr("snowflake_view.test", "name", newViewName), - resource.TestCheckResourceAttr("snowflake_view.test", "comment", "new comment"), - resource.TestCheckResourceAttr("snowflake_view.test", "fully_qualified_name", newViewId.FullyQualifiedName()), + 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()), ), }, }, @@ -240,23 +493,12 @@ func TestAcc_View_Rename(t *testing.T) { } func TestAcc_ViewChangeCopyGrants(t *testing.T) { - accName := acc.TestClient().Ids.Alpha() + t.Setenv(string(testenvs.ConfigureClientOnce), "") + id := acc.TestClient().Ids.RandomSchemaObjectIdentifier() - m := func() map[string]config.Variable { - return map[string]config.Variable{ - "name": config.StringVariable(accName), - "database": config.StringVariable(acc.TestDatabaseName), - "schema": config.StringVariable(acc.TestSchemaName), - "comment": config.StringVariable("Terraform test resource"), - "is_secure": config.BoolVariable(true), - "or_replace": config.BoolVariable(false), - "copy_grants": config.BoolVariable(false), - "statement": config.StringVariable("SELECT ROLE_NAME, ROLE_OWNER FROM INFORMATION_SCHEMA.APPLICABLE_ROLES"), - } - } - m2 := m() - m2["copy_grants"] = config.BoolVariable(true) - m2["or_replace"] = config.BoolVariable(true) + statement := "SELECT ROLE_NAME, ROLE_OWNER FROM INFORMATION_SCHEMA.APPLICABLE_ROLES" + viewModel := model.View("test", id.DatabaseName(), id.Name(), id.SchemaName(), statement).WithIsSecure("true").WithOrReplace(false).WithCopyGrants(false). + WithDependsOn([]string{"snowflake_unsafe_execute.use_warehouse"}) var createdOn string @@ -269,15 +511,14 @@ func TestAcc_ViewChangeCopyGrants(t *testing.T) { CheckDestroy: acc.CheckDestroy(t, resources.View), Steps: []resource.TestStep{ { - ConfigDirectory: acc.ConfigurationDirectory("TestAcc_View_basic"), - ConfigVariables: m(), - Check: resource.ComposeTestCheckFunc( - resource.TestCheckResourceAttr("snowflake_view.test", "name", accName), - resource.TestCheckResourceAttr("snowflake_view.test", "database", acc.TestDatabaseName), - resource.TestCheckResourceAttr("snowflake_view.test", "comment", "Terraform test resource"), + Config: accconfig.FromModel(t, viewModel) + useWarehouseConfig(acc.TestWarehouseName), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr("snowflake_view.test", "name", id.Name()), + resource.TestCheckResourceAttr("snowflake_view.test", "database", id.DatabaseName()), resource.TestCheckResourceAttr("snowflake_view.test", "copy_grants", "false"), checkBool("snowflake_view.test", "is_secure", true), - resource.TestCheckResourceAttrWith("snowflake_view.test", "created_on", func(value string) error { + resource.TestCheckResourceAttr("snowflake_view.test", "show_output.#", "1"), + resource.TestCheckResourceAttrWith("snowflake_view.test", "show_output.0.created_on", func(value string) error { createdOn = value return nil }), @@ -285,10 +526,10 @@ func TestAcc_ViewChangeCopyGrants(t *testing.T) { }, // Checks that copy_grants changes don't trigger a drop { - ConfigDirectory: acc.ConfigurationDirectory("TestAcc_View_basic"), - ConfigVariables: m2, - Check: resource.ComposeTestCheckFunc( - resource.TestCheckResourceAttrWith("snowflake_view.test", "created_on", func(value string) error { + Config: accconfig.FromModel(t, viewModel.WithCopyGrants(true).WithOrReplace(true)) + useWarehouseConfig(acc.TestWarehouseName), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr("snowflake_view.test", "show_output.#", "1"), + resource.TestCheckResourceAttrWith("snowflake_view.test", "show_output.0.created_on", func(value string) error { if value != createdOn { return fmt.Errorf("view was recreated") } @@ -302,22 +543,12 @@ func TestAcc_ViewChangeCopyGrants(t *testing.T) { } func TestAcc_ViewChangeCopyGrantsReversed(t *testing.T) { - accName := acc.TestClient().Ids.Alpha() + t.Setenv(string(testenvs.ConfigureClientOnce), "") + id := acc.TestClient().Ids.RandomSchemaObjectIdentifier() - m := func() map[string]config.Variable { - return map[string]config.Variable{ - "name": config.StringVariable(accName), - "database": config.StringVariable(acc.TestDatabaseName), - "schema": config.StringVariable(acc.TestSchemaName), - "comment": config.StringVariable("Terraform test resource"), - "is_secure": config.BoolVariable(true), - "or_replace": config.BoolVariable(true), - "copy_grants": config.BoolVariable(true), - "statement": config.StringVariable("SELECT ROLE_NAME, ROLE_OWNER FROM INFORMATION_SCHEMA.APPLICABLE_ROLES"), - } - } - m2 := m() - m2["copy_grants"] = config.BoolVariable(false) + statement := "SELECT ROLE_NAME, ROLE_OWNER FROM INFORMATION_SCHEMA.APPLICABLE_ROLES" + viewModel := model.View("test", id.DatabaseName(), id.Name(), id.SchemaName(), statement).WithIsSecure("true").WithOrReplace(true).WithCopyGrants(true). + WithDependsOn([]string{"snowflake_unsafe_execute.use_warehouse"}) var createdOn string @@ -330,11 +561,11 @@ func TestAcc_ViewChangeCopyGrantsReversed(t *testing.T) { CheckDestroy: acc.CheckDestroy(t, resources.View), Steps: []resource.TestStep{ { - ConfigDirectory: acc.ConfigurationDirectory("TestAcc_View_basic"), - ConfigVariables: m(), - Check: resource.ComposeTestCheckFunc( + Config: accconfig.FromModel(t, viewModel) + useWarehouseConfig(acc.TestWarehouseName), + Check: resource.ComposeAggregateTestCheckFunc( resource.TestCheckResourceAttr("snowflake_view.test", "copy_grants", "true"), - resource.TestCheckResourceAttrWith("snowflake_view.test", "created_on", func(value string) error { + resource.TestCheckResourceAttr("snowflake_view.test", "show_output.#", "1"), + resource.TestCheckResourceAttrWith("snowflake_view.test", "show_output.0.created_on", func(value string) error { createdOn = value return nil }), @@ -342,10 +573,10 @@ func TestAcc_ViewChangeCopyGrantsReversed(t *testing.T) { ), }, { - ConfigDirectory: acc.ConfigurationDirectory("TestAcc_View_basic"), - ConfigVariables: m2, - Check: resource.ComposeTestCheckFunc( - resource.TestCheckResourceAttrWith("snowflake_view.test", "created_on", func(value string) error { + Config: accconfig.FromModel(t, viewModel.WithCopyGrants(false)) + useWarehouseConfig(acc.TestWarehouseName), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr("snowflake_view.test", "show_output.#", "1"), + resource.TestCheckResourceAttrWith("snowflake_view.test", "show_output.0.created_on", func(value string) error { if value != createdOn { return fmt.Errorf("view was recreated") } @@ -358,9 +589,11 @@ func TestAcc_ViewChangeCopyGrantsReversed(t *testing.T) { }) } -func TestAcc_ViewStatementUpdate(t *testing.T) { - tableName := acc.TestClient().Ids.Alpha() - viewName := acc.TestClient().Ids.Alpha() +func TestAcc_ViewCopyGrantsStatementUpdate(t *testing.T) { + t.Setenv(string(testenvs.ConfigureClientOnce), "") + _ = testenvs.GetOrSkipTest(t, testenvs.EnableAcceptance) + tableId := acc.TestClient().Ids.RandomSchemaObjectIdentifier() + viewId := acc.TestClient().Ids.RandomSchemaObjectIdentifier() resource.Test(t, resource.TestCase{ ProtoV6ProviderFactories: acc.TestAccProtoV6ProviderFactories, @@ -371,16 +604,16 @@ func TestAcc_ViewStatementUpdate(t *testing.T) { CheckDestroy: acc.CheckDestroy(t, resources.View), Steps: []resource.TestStep{ { - Config: viewConfigWithGrants(acc.TestDatabaseName, acc.TestSchemaName, tableName, viewName, `\"name\"`), - Check: resource.ComposeTestCheckFunc( + Config: viewConfigWithGrants(viewId, tableId, `\"name\"`) + useWarehouseConfig(acc.TestWarehouseName), + Check: resource.ComposeAggregateTestCheckFunc( // there should be more than one privilege, because we applied grant all privileges and initially there's always one which is ownership resource.TestCheckResourceAttr("data.snowflake_grants.grants", "grants.#", "2"), resource.TestCheckResourceAttr("data.snowflake_grants.grants", "grants.1.privilege", "SELECT"), ), }, { - Config: viewConfigWithGrants(acc.TestDatabaseName, acc.TestSchemaName, tableName, viewName, "*"), - Check: resource.ComposeTestCheckFunc( + Config: viewConfigWithGrants(viewId, tableId, "*") + useWarehouseConfig(acc.TestWarehouseName), + Check: resource.ComposeAggregateTestCheckFunc( resource.TestCheckResourceAttr("data.snowflake_grants.grants", "grants.#", "2"), resource.TestCheckResourceAttr("data.snowflake_grants.grants", "grants.1.privilege", "SELECT"), ), @@ -390,9 +623,10 @@ func TestAcc_ViewStatementUpdate(t *testing.T) { } func TestAcc_View_copyGrants(t *testing.T) { - accName := acc.TestClient().Ids.Alpha() - query := "SELECT ROLE_NAME, ROLE_OWNER FROM INFORMATION_SCHEMA.APPLICABLE_ROLES" - + t.Setenv(string(testenvs.ConfigureClientOnce), "") + id := acc.TestClient().Ids.RandomSchemaObjectIdentifier() + statement := "SELECT ROLE_NAME, ROLE_OWNER FROM INFORMATION_SCHEMA.APPLICABLE_ROLES" + viewModel := model.View("test", id.DatabaseName(), id.Name(), id.SchemaName(), statement).WithDependsOn([]string{"snowflake_unsafe_execute.use_warehouse"}) resource.Test(t, resource.TestCase{ ProtoV6ProviderFactories: acc.TestAccProtoV6ProviderFactories, PreCheck: func() { acc.TestAccPreCheck(t) }, @@ -402,19 +636,19 @@ func TestAcc_View_copyGrants(t *testing.T) { CheckDestroy: acc.CheckDestroy(t, resources.View), Steps: []resource.TestStep{ { - Config: viewConfigWithCopyGrants(acc.TestDatabaseName, acc.TestSchemaName, accName, query, true), + Config: accconfig.FromModel(t, viewModel.WithCopyGrants(true)) + useWarehouseConfig(acc.TestWarehouseName), ExpectError: regexp.MustCompile("all of `copy_grants,or_replace` must be specified"), }, { - Config: viewConfigWithCopyGrantsAndOrReplace(acc.TestDatabaseName, acc.TestSchemaName, accName, query, true, true), - Check: resource.ComposeTestCheckFunc( - resource.TestCheckResourceAttr("snowflake_view.test", "name", accName), + Config: accconfig.FromModel(t, viewModel.WithCopyGrants(true).WithOrReplace(true)) + useWarehouseConfig(acc.TestWarehouseName), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr("snowflake_view.test", "name", id.Name()), ), }, { - Config: viewConfigWithOrReplace(acc.TestDatabaseName, acc.TestSchemaName, accName, query, true), - Check: resource.ComposeTestCheckFunc( - resource.TestCheckResourceAttr("snowflake_view.test", "name", accName), + Config: accconfig.FromModel(t, viewModel.WithCopyGrants(false).WithOrReplace(true)) + useWarehouseConfig(acc.TestWarehouseName), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr("snowflake_view.test", "name", id.Name()), ), }, }, @@ -422,10 +656,11 @@ func TestAcc_View_copyGrants(t *testing.T) { } func TestAcc_View_Issue2640(t *testing.T) { - viewId := acc.TestClient().Ids.RandomSchemaObjectIdentifier() - viewName := viewId.Name() + t.Setenv(string(testenvs.ConfigureClientOnce), "") + id := acc.TestClient().Ids.RandomSchemaObjectIdentifier() part1 := "SELECT ROLE_NAME, ROLE_OWNER FROM INFORMATION_SCHEMA.APPLICABLE_ROLES" part2 := "SELECT ROLE_OWNER, ROLE_NAME FROM INFORMATION_SCHEMA.APPLICABLE_ROLES" + statement := fmt.Sprintf("%s\n\tunion\n%s\n", part1, part2) roleId := acc.TestClient().Ids.RandomAccountObjectIdentifier() resource.Test(t, resource.TestCase{ @@ -437,10 +672,10 @@ func TestAcc_View_Issue2640(t *testing.T) { CheckDestroy: acc.CheckDestroy(t, resources.View), Steps: []resource.TestStep{ { - Config: viewConfigWithMultilineUnionStatement(acc.TestDatabaseName, acc.TestSchemaName, viewName, part1, part2), - Check: resource.ComposeTestCheckFunc( - resource.TestCheckResourceAttr("snowflake_view.test", "name", viewName), - resource.TestCheckResourceAttr("snowflake_view.test", "statement", fmt.Sprintf("%s\n\tunion\n%s\n", part1, part2)), + Config: viewConfigWithMultilineUnionStatement(id, part1, part2) + useWarehouseConfig(acc.TestWarehouseName), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr("snowflake_view.test", "name", id.Name()), + resource.TestCheckResourceAttr("snowflake_view.test", "statement", statement), resource.TestCheckResourceAttr("snowflake_view.test", "database", acc.TestDatabaseName), resource.TestCheckResourceAttr("snowflake_view.test", "schema", acc.TestSchemaName), ), @@ -450,7 +685,7 @@ func TestAcc_View_Issue2640(t *testing.T) { PreConfig: func() { role, roleCleanup := acc.TestClient().Role.CreateRoleWithIdentifier(t, roleId) t.Cleanup(roleCleanup) - acc.TestClient().Role.GrantOwnershipOnSchemaObject(t, role.ID(), viewId, sdk.ObjectTypeView, sdk.Revoke) + acc.TestClient().Role.GrantOwnershipOnSchemaObject(t, role.ID(), id, sdk.ObjectTypeView, sdk.Revoke) }, ResourceName: "snowflake_view.test", ImportState: true, @@ -459,18 +694,95 @@ func TestAcc_View_Issue2640(t *testing.T) { // import with the proper role { PreConfig: func() { - acc.TestClient().Role.GrantOwnershipOnSchemaObject(t, snowflakeroles.Accountadmin, viewId, sdk.ObjectTypeView, sdk.Revoke) + acc.TestClient().Role.GrantOwnershipOnSchemaObject(t, snowflakeroles.Accountadmin, id, sdk.ObjectTypeView, sdk.Revoke) + }, + ResourceName: "snowflake_view.test", + ImportState: true, + ImportStateCheck: assert.AssertThatImport(t, assert.CheckImport(importchecks.TestCheckResourceAttrInstanceState(helpers.EncodeSnowflakeID(id), "name", id.Name())), + resourceassert.ImportedViewResource(t, helpers.EncodeSnowflakeID(id)). + HasNameString(id.Name()). + HasStatementString(statement). + HasDatabaseString(id.DatabaseName()). + HasSchemaString(id.SchemaName()), + ), + }, + }, + }) +} + +func TestAcc_view_migrateFromVersion_0_94_1(t *testing.T) { + t.Setenv(string(testenvs.ConfigureClientOnce), "") + _ = testenvs.GetOrSkipTest(t, testenvs.EnableAcceptance) + acc.TestAccPreCheck(t) + id := acc.TestClient().Ids.RandomSchemaObjectIdentifier() + 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).WithDependsOn([]string{"snowflake_unsafe_execute.use_warehouse"}) + + tag, tagCleanup := acc.TestClient().Tag.CreateTag(t) + t.Cleanup(tagCleanup) + + resource.Test(t, resource.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.RequireAbove(tfversion.Version1_5_0), + }, + + Steps: []resource.TestStep{ + { + ExternalProviders: map[string]resource.ExternalProvider{ + "snowflake": { + VersionConstraint: "=0.94.1", + Source: "Snowflake-Labs/snowflake", + }, }, - ResourceName: "snowflake_view.test", - ImportState: true, - ImportStateVerify: true, - ImportStateVerifyIgnore: []string{"or_replace", "created_on"}, + Config: viewv_0_94_1_WithTags(id, tag.SchemaName, tag.Name, "foo", statement), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr(resourceName, "name", id.Name()), + resource.TestCheckResourceAttr(resourceName, "tag.#", "1"), + resource.TestCheckResourceAttr(resourceName, "tag.0.name", tag.Name), + resource.TestCheckResourceAttr(resourceName, "tag.0.value", "foo"), + ), + }, + { + ProtoV6ProviderFactories: acc.TestAccProtoV6ProviderFactories, + Config: accconfig.FromModel(t, viewModel) + useWarehouseConfig(acc.TestWarehouseName), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr(resourceName, "name", id.Name()), + resource.TestCheckNoResourceAttr(resourceName, "tag.#"), + ), }, }, }) } -func viewConfigWithGrants(databaseName string, schemaName string, tableName string, viewName string, selectStatement string) string { +func useWarehouseConfig(name string) string { + return fmt.Sprintf(` +resource "snowflake_unsafe_execute" "use_warehouse" { + execute = "USE WAREHOUSE \"%s\"" + revert = "SELECT 1" +} +`, name) +} + +func viewv_0_94_1_WithTags(id sdk.SchemaObjectIdentifier, tagSchema, tagName, tagValue, statement string) string { + s := ` +resource "snowflake_view" "test" { + name = "%[1]s" + database = "%[2]s" + schema = "%[6]s" + statement = "%[7]s" + tag { + name = "%[4]s" + value = "%[5]s" + schema = "%[3]s" + database = "%[2]s" + } +} +` + return fmt.Sprintf(s, id.Name(), id.DatabaseName(), tagSchema, tagName, tagValue, id.SchemaName(), statement) +} + +func viewConfigWithGrants(viewId, tableId sdk.SchemaObjectIdentifier, selectStatement string) string { return fmt.Sprintf(` resource "snowflake_table" "table" { database = "%[1]s" @@ -484,7 +796,6 @@ resource "snowflake_table" "table" { } resource "snowflake_view" "test" { - depends_on = [snowflake_table.table] name = "%[4]s" comment = "created by terraform" database = "%[1]s" @@ -493,6 +804,7 @@ resource "snowflake_view" "test" { or_replace = true copy_grants = true is_secure = true + depends_on = [snowflake_unsafe_execute.use_warehouse, snowflake_table.table] } resource "snowflake_account_role" "test" { @@ -509,53 +821,16 @@ resource "snowflake_grant_privileges_to_account_role" "grant" { } data "snowflake_grants" "grants" { - depends_on = [snowflake_grant_privileges_to_account_role.grant, snowflake_view.test] + depends_on = [snowflake_grant_privileges_to_account_role.grant, snowflake_view.test, snowflake_unsafe_execute.use_warehouse] grants_on { object_name = "\"%[1]s\".\"%[2]s\".\"${snowflake_view.test.name}\"" object_type = "VIEW" } } - `, databaseName, schemaName, tableName, viewName, selectStatement) -} - -func viewConfigWithCopyGrants(databaseName string, schemaName string, name string, selectStatement string, copyGrants bool) string { - return fmt.Sprintf(` -resource "snowflake_view" "test" { - name = "%[3]s" - database = "%[1]s" - schema = "%[2]s" - statement = "%[4]s" - copy_grants = %[5]t -} - `, databaseName, schemaName, name, selectStatement, copyGrants) -} - -func viewConfigWithCopyGrantsAndOrReplace(databaseName string, schemaName string, name string, selectStatement string, copyGrants bool, orReplace bool) string { - return fmt.Sprintf(` -resource "snowflake_view" "test" { - name = "%[3]s" - database = "%[1]s" - schema = "%[2]s" - statement = "%[4]s" - copy_grants = %[5]t - or_replace = %[6]t -} - `, databaseName, schemaName, name, selectStatement, copyGrants, orReplace) -} - -func viewConfigWithOrReplace(databaseName string, schemaName string, name string, selectStatement string, orReplace bool) string { - return fmt.Sprintf(` -resource "snowflake_view" "test" { - name = "%[3]s" - database = "%[1]s" - schema = "%[2]s" - statement = "%[4]s" - or_replace = %[5]t -} - `, databaseName, schemaName, name, selectStatement, orReplace) + `, viewId.DatabaseName(), viewId.SchemaName(), tableId.Name(), viewId.Name(), selectStatement) } -func viewConfigWithMultilineUnionStatement(databaseName string, schemaName string, name string, part1 string, part2 string) string { +func viewConfigWithMultilineUnionStatement(id sdk.SchemaObjectIdentifier, part1 string, part2 string) string { return fmt.Sprintf(` resource "snowflake_view" "test" { name = "%[3]s" @@ -567,6 +842,7 @@ resource "snowflake_view" "test" { %[5]s SQL is_secure = true + depends_on = [snowflake_unsafe_execute.use_warehouse] } - `, databaseName, schemaName, name, part1, part2) + `, id.DatabaseName(), id.SchemaName(), id.Name(), part1, part2) } diff --git a/pkg/resources/view_state_upgraders.go b/pkg/resources/view_state_upgraders.go new file mode 100644 index 0000000000..f48b54c568 --- /dev/null +++ b/pkg/resources/view_state_upgraders.go @@ -0,0 +1,20 @@ +package resources + +import ( + "context" + "strconv" +) + +func v0_94_1_ViewStateUpgrader(ctx context.Context, rawState map[string]any, meta any) (map[string]any, error) { + if rawState == nil { + return rawState, nil + } + + if v, ok := rawState["is_secure"]; ok { + rawState["is_secure"] = strconv.FormatBool(v.(bool)) + } + + delete(rawState, "tag") + + return rawState, nil +} diff --git a/pkg/resources/warehouse.go b/pkg/resources/warehouse.go index 633dbfb0aa..9623cd947b 100644 --- a/pkg/resources/warehouse.go +++ b/pkg/resources/warehouse.go @@ -23,7 +23,7 @@ var warehouseSchema = map[string]*schema.Schema{ "name": { Type: schema.TypeString, Required: true, - Description: "Identifier for the virtual warehouse; must be unique for your account.", + Description: blocklistedCharactersFieldDescription("Identifier for the virtual warehouse; must be unique for your account."), DiffSuppressFunc: suppressIdentifierQuoting, }, "warehouse_type": { diff --git a/pkg/schemas/view.go b/pkg/schemas/view.go new file mode 100644 index 0000000000..6ada82a7fe --- /dev/null +++ b/pkg/schemas/view.go @@ -0,0 +1,78 @@ +package schemas + +import ( + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/sdk" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +var ViewDescribeSchema = map[string]*schema.Schema{ + "name": { + Type: schema.TypeString, + Computed: true, + }, + "type": { + Type: schema.TypeString, + Computed: true, + }, + "kind": { + Type: schema.TypeString, + Computed: true, + }, + "is_nullable": { + Type: schema.TypeBool, + Computed: true, + }, + "default": { + Type: schema.TypeString, + Computed: true, + }, + "is_primary": { + Type: schema.TypeBool, + Computed: true, + }, + "is_unique": { + Type: schema.TypeBool, + Computed: true, + }, + "check": { + Type: schema.TypeString, + Computed: true, + }, + "expression": { + Type: schema.TypeString, + Computed: true, + }, + "comment": { + Type: schema.TypeString, + Computed: true, + }, + "policy_name": { + Type: schema.TypeString, + Computed: true, + }, + "privacy_domain": { + Type: schema.TypeString, + Computed: true, + }, +} + +func ViewDescriptionToSchema(description []sdk.ViewDetails) []map[string]any { + result := make([]map[string]any, len(description)) + for i, row := range description { + result[i] = map[string]any{ + "name": row.Name, + "type": row.Type, + "kind": row.Kind, + "is_nullable": row.IsNullable, + "default": row.Default, + "is_primary": row.IsPrimary, + "is_unique": row.IsUnique, + "check": row.Check, + "expression": row.Expression, + "comment": row.Comment, + "policy_name": row.PolicyName, + "privacy_domain": row.PrivacyDomain, + } + } + return result +} diff --git a/pkg/sdk/policy_references.go b/pkg/sdk/policy_references.go index b5aeb6c361..9f5ee04e21 100644 --- a/pkg/sdk/policy_references.go +++ b/pkg/sdk/policy_references.go @@ -68,6 +68,14 @@ type policyReferenceFunctionArguments struct { refEntityDomain *PolicyEntityDomain `ddl:"parameter,single_quotes,arrow_equals" sql:"REF_ENTITY_DOMAIN"` } +// TODO: use PolicyKind in PolicyReference +type PolicyKind string + +const ( + PolicyKindAggregationPolicy PolicyKind = "AGGREGATION_POLICY" + PolicyKindRowAccessPolicy PolicyKind = "ROW_ACCESS_POLICY" +) + type PolicyReference struct { PolicyDb *string PolicySchema *string diff --git a/pkg/sdk/testint/views_gen_integration_test.go b/pkg/sdk/testint/views_gen_integration_test.go index 25e2e796ab..59e77f666b 100644 --- a/pkg/sdk/testint/views_gen_integration_test.go +++ b/pkg/sdk/testint/views_gen_integration_test.go @@ -186,8 +186,8 @@ func TestInt_Views(t *testing.T) { }). WithCopyGrants(true). WithComment("comment"). - WithRowAccessPolicy(*sdk.NewViewRowAccessPolicyRequest(rowAccessPolicy.ID(), []sdk.DoubleQuotedString{{Value: "column_with_comment"}})). - WithAggregationPolicy(*sdk.NewViewAggregationPolicyRequest(aggregationPolicy).WithEntityKey([]sdk.DoubleQuotedString{{Value: "column_with_comment"}})). + WithRowAccessPolicy(*sdk.NewViewRowAccessPolicyRequest(rowAccessPolicy.ID(), []sdk.Column{{Value: "column_with_comment"}})). + WithAggregationPolicy(*sdk.NewViewAggregationPolicyRequest(aggregationPolicy).WithEntityKey([]sdk.Column{{Value: "column_with_comment"}})). WithTag([]sdk.TagAssociation{{ Name: tag.ID(), Value: "v2", @@ -226,7 +226,7 @@ func TestInt_Views(t *testing.T) { WithRecursive(true). WithColumns([]sdk.ViewColumnRequest{ *sdk.NewViewColumnRequest("col1").WithMaskingPolicy( - *sdk.NewViewColumnMaskingPolicyRequest(maskingPolicy.ID()).WithUsing([]sdk.DoubleQuotedString{{Value: "col1"}}), + *sdk.NewViewColumnMaskingPolicyRequest(maskingPolicy.ID()).WithUsing([]sdk.Column{{Value: "col1"}}), ).WithProjectionPolicy( *sdk.NewViewColumnProjectionPolicyRequest(projectionPolicy), ), @@ -504,7 +504,7 @@ func TestInt_Views(t *testing.T) { id := view.ID() // add policy - alterRequest := sdk.NewAlterViewRequest(id).WithAddRowAccessPolicy(*sdk.NewViewAddRowAccessPolicyRequest(rowAccessPolicy.ID(), []sdk.DoubleQuotedString{{Value: "ID"}})) + alterRequest := sdk.NewAlterViewRequest(id).WithAddRowAccessPolicy(*sdk.NewViewAddRowAccessPolicyRequest(rowAccessPolicy.ID(), []sdk.Column{{Value: "ID"}})) err := client.Views.Alter(ctx, alterRequest) require.NoError(t, err) @@ -522,7 +522,7 @@ func TestInt_Views(t *testing.T) { require.Error(t, err, "no rows in result set") // add policy again - alterRequest = sdk.NewAlterViewRequest(id).WithAddRowAccessPolicy(*sdk.NewViewAddRowAccessPolicyRequest(rowAccessPolicy.ID(), []sdk.DoubleQuotedString{{Value: "ID"}})) + alterRequest = sdk.NewAlterViewRequest(id).WithAddRowAccessPolicy(*sdk.NewViewAddRowAccessPolicyRequest(rowAccessPolicy.ID(), []sdk.Column{{Value: "ID"}})) err = client.Views.Alter(ctx, alterRequest) require.NoError(t, err) @@ -533,7 +533,7 @@ func TestInt_Views(t *testing.T) { // drop and add other policy simultaneously alterRequest = sdk.NewAlterViewRequest(id).WithDropAndAddRowAccessPolicy(*sdk.NewViewDropAndAddRowAccessPolicyRequest( *sdk.NewViewDropRowAccessPolicyRequest(rowAccessPolicy.ID()), - *sdk.NewViewAddRowAccessPolicyRequest(rowAccessPolicy2.ID(), []sdk.DoubleQuotedString{{Value: "ID"}}), + *sdk.NewViewAddRowAccessPolicyRequest(rowAccessPolicy2.ID(), []sdk.Column{{Value: "ID"}}), )) err = client.Views.Alter(ctx, alterRequest) require.NoError(t, err) @@ -570,7 +570,7 @@ func TestInt_Views(t *testing.T) { alterRequest = sdk.NewAlterViewRequest(id).WithAddDataMetricFunction(*sdk.NewViewAddDataMetricFunctionRequest([]sdk.ViewDataMetricFunction{ { DataMetricFunction: dataMetricFunction, - On: []sdk.DoubleQuotedString{{Value: "ID"}}, + On: []sdk.Column{{Value: "ID"}}, }, })) err = client.Views.Alter(ctx, alterRequest) @@ -586,7 +586,7 @@ func TestInt_Views(t *testing.T) { alterRequest = sdk.NewAlterViewRequest(id).WithDropDataMetricFunction(*sdk.NewViewDropDataMetricFunctionRequest([]sdk.ViewDataMetricFunction{ { DataMetricFunction: dataMetricFunction, - On: []sdk.DoubleQuotedString{{Value: "ID"}}, + On: []sdk.Column{{Value: "ID"}}, }, })) err = client.Views.Alter(ctx, alterRequest) @@ -600,11 +600,11 @@ func TestInt_Views(t *testing.T) { alterRequest = sdk.NewAlterViewRequest(id).WithAddDataMetricFunction(*sdk.NewViewAddDataMetricFunctionRequest([]sdk.ViewDataMetricFunction{ { DataMetricFunction: dataMetricFunction, - On: []sdk.DoubleQuotedString{{Value: "ID"}}, + On: []sdk.Column{{Value: "ID"}}, }, { DataMetricFunction: dataMetricFunction2, - On: []sdk.DoubleQuotedString{{Value: "ID"}}, + On: []sdk.Column{{Value: "ID"}}, }, })) err = client.Views.Alter(ctx, alterRequest) @@ -636,7 +636,7 @@ func TestInt_Views(t *testing.T) { id := view.ID() // set policy - alterRequest := sdk.NewAlterViewRequest(id).WithSetAggregationPolicy(*sdk.NewViewSetAggregationPolicyRequest(aggregationPolicy).WithEntityKey([]sdk.DoubleQuotedString{{Value: "ID"}})) + alterRequest := sdk.NewAlterViewRequest(id).WithSetAggregationPolicy(*sdk.NewViewSetAggregationPolicyRequest(aggregationPolicy).WithEntityKey([]sdk.Column{{Value: "ID"}})) err := client.Views.Alter(ctx, alterRequest) require.NoError(t, err) @@ -648,7 +648,7 @@ func TestInt_Views(t *testing.T) { // set policy with force alterRequest = sdk.NewAlterViewRequest(id).WithSetAggregationPolicy(*sdk.NewViewSetAggregationPolicyRequest(aggregationPolicy2). - WithEntityKey([]sdk.DoubleQuotedString{{Value: "ID"}}). + WithEntityKey([]sdk.Column{{Value: "ID"}}). WithForce(true)) err = client.Views.Alter(ctx, alterRequest) require.NoError(t, err) diff --git a/pkg/sdk/views_def.go b/pkg/sdk/views_def.go index fef265a355..680407fb5a 100644 --- a/pkg/sdk/views_def.go +++ b/pkg/sdk/views_def.go @@ -63,7 +63,7 @@ var viewDetails = g.PlainStruct("ViewDetails"). OptionalText("PolicyName"). OptionalText("PrivacyDomain") -var doubleQuotedStringDef = g.NewQueryStruct("DoubleQuotedString"). +var columnDef = g.NewQueryStruct("Column"). Text("Value", g.KeywordOptions().Required().DoubleQuotes()) var viewMinute = g.NewQueryStruct("ViewMinute"). @@ -76,7 +76,7 @@ var viewUsingCron = g.NewQueryStruct("ViewUsingCron"). var dataMetricFunctionDef = g.NewQueryStruct("ViewDataMetricFunction"). Identifier("DataMetricFunction", g.KindOfT[SchemaObjectIdentifier](), g.IdentifierOptions().Required()). - ListAssignment("ON", "DoubleQuotedString", g.ParameterOptions().Required().NoEquals().Parentheses()). + ListAssignment("ON", "Column", g.ParameterOptions().Required().NoEquals().Parentheses()). WithValidation(g.ValidIdentifier, "DataMetricFunction") var viewColumn = g.NewQueryStruct("ViewColumn"). @@ -88,19 +88,19 @@ var viewColumn = g.NewQueryStruct("ViewColumn"). var viewColumnMaskingPolicy = g.NewQueryStruct("ViewColumnMaskingPolicy"). Identifier("MaskingPolicy", g.KindOfT[SchemaObjectIdentifier](), g.IdentifierOptions().SQL("MASKING POLICY").Required()). - ListAssignment("USING", "DoubleQuotedString", g.ParameterOptions().NoEquals().Parentheses()) + ListAssignment("USING", "Column", g.ParameterOptions().NoEquals().Parentheses()) var viewColumnProjectionPolicy = g.NewQueryStruct("ViewColumnProjectionPolicy"). Identifier("ProjectionPolicy", g.KindOfT[SchemaObjectIdentifier](), g.IdentifierOptions().SQL("PROJECTION POLICY").Required()) var viewRowAccessPolicy = g.NewQueryStruct("ViewRowAccessPolicy"). Identifier("RowAccessPolicy", g.KindOfT[SchemaObjectIdentifier](), g.IdentifierOptions().SQL("ROW ACCESS POLICY").Required()). - ListAssignment("ON", "DoubleQuotedString", g.ParameterOptions().Required().NoEquals().Parentheses()). + ListAssignment("ON", "Column", g.ParameterOptions().Required().NoEquals().Parentheses()). WithValidation(g.ValidIdentifier, "RowAccessPolicy") var viewAggregationPolicy = g.NewQueryStruct("ViewAggregationPolicy"). Identifier("AggregationPolicy", g.KindOfT[SchemaObjectIdentifier](), g.IdentifierOptions().SQL("AGGREGATION POLICY").Required()). - ListAssignment("ENTITY KEY", "DoubleQuotedString", g.ParameterOptions().NoEquals().Parentheses()). + ListAssignment("ENTITY KEY", "Column", g.ParameterOptions().NoEquals().Parentheses()). WithValidation(g.ValidIdentifier, "AggregationPolicy") var viewAddDataMetricFunction = g.NewQueryStruct("ViewAddDataMetricFunction"). @@ -124,7 +124,7 @@ var viewUnsetDataMetricSchedule = g.NewQueryStruct("ViewUnsetDataMetricSchedule" var viewAddRowAccessPolicy = g.NewQueryStruct("ViewAddRowAccessPolicy"). SQL("ADD"). Identifier("RowAccessPolicy", g.KindOfT[SchemaObjectIdentifier](), g.IdentifierOptions().SQL("ROW ACCESS POLICY").Required()). - ListAssignment("ON", "DoubleQuotedString", g.ParameterOptions().Required().NoEquals().Parentheses()). + ListAssignment("ON", "Column", g.ParameterOptions().Required().NoEquals().Parentheses()). WithValidation(g.ValidIdentifier, "RowAccessPolicy") var viewDropRowAccessPolicy = g.NewQueryStruct("ViewDropRowAccessPolicy"). @@ -139,7 +139,7 @@ var viewDropAndAddRowAccessPolicy = g.NewQueryStruct("ViewDropAndAddRowAccessPol var viewSetAggregationPolicy = g.NewQueryStruct("ViewSetAggregationPolicy"). SQL("SET"). Identifier("AggregationPolicy", g.KindOfT[SchemaObjectIdentifier](), g.IdentifierOptions().SQL("AGGREGATION POLICY").Required()). - ListAssignment("ENTITY KEY", "DoubleQuotedString", g.ParameterOptions().NoEquals().Parentheses()). + ListAssignment("ENTITY KEY", "Column", g.ParameterOptions().NoEquals().Parentheses()). OptionalSQL("FORCE"). WithValidation(g.ValidIdentifier, "AggregationPolicy") @@ -153,7 +153,7 @@ var viewSetColumnMaskingPolicy = g.NewQueryStruct("ViewSetColumnMaskingPolicy"). Text("Name", g.KeywordOptions().Required().DoubleQuotes()). SQL("SET"). Identifier("MaskingPolicy", g.KindOfT[SchemaObjectIdentifier](), g.IdentifierOptions().SQL("MASKING POLICY").Required()). - ListAssignment("USING", "DoubleQuotedString", g.ParameterOptions().NoEquals().Parentheses()). + ListAssignment("USING", "Column", g.ParameterOptions().NoEquals().Parentheses()). OptionalSQL("FORCE") var viewUnsetColumnMaskingPolicy = g.NewQueryStruct("ViewUnsetColumnMaskingPolicy"). @@ -267,7 +267,7 @@ var ViewsDef = g.NewInterface( "UnsetTagsOnColumn"). WithValidation(g.ConflictingFields, "IfExists", "SetSecure"). WithValidation(g.ConflictingFields, "IfExists", "UnsetSecure"), - doubleQuotedStringDef, + columnDef, dataMetricFunctionDef, ). DropOperation( diff --git a/pkg/sdk/views_dto_builders_gen.go b/pkg/sdk/views_dto_builders_gen.go index 47d65f0e29..d16848ec28 100644 --- a/pkg/sdk/views_dto_builders_gen.go +++ b/pkg/sdk/views_dto_builders_gen.go @@ -113,14 +113,14 @@ func NewViewColumnMaskingPolicyRequest( return &s } -func (s *ViewColumnMaskingPolicyRequest) WithUsing(Using []DoubleQuotedString) *ViewColumnMaskingPolicyRequest { +func (s *ViewColumnMaskingPolicyRequest) WithUsing(Using []Column) *ViewColumnMaskingPolicyRequest { s.Using = Using return s } func NewViewRowAccessPolicyRequest( RowAccessPolicy SchemaObjectIdentifier, - On []DoubleQuotedString, + On []Column, ) *ViewRowAccessPolicyRequest { s := ViewRowAccessPolicyRequest{} s.RowAccessPolicy = RowAccessPolicy @@ -136,7 +136,7 @@ func NewViewAggregationPolicyRequest( return &s } -func (s *ViewAggregationPolicyRequest) WithEntityKey(EntityKey []DoubleQuotedString) *ViewAggregationPolicyRequest { +func (s *ViewAggregationPolicyRequest) WithEntityKey(EntityKey []Column) *ViewAggregationPolicyRequest { s.EntityKey = EntityKey return s } @@ -331,7 +331,7 @@ func NewViewUnsetDataMetricScheduleRequest() *ViewUnsetDataMetricScheduleRequest func NewViewAddRowAccessPolicyRequest( RowAccessPolicy SchemaObjectIdentifier, - On []DoubleQuotedString, + On []Column, ) *ViewAddRowAccessPolicyRequest { s := ViewAddRowAccessPolicyRequest{} s.RowAccessPolicy = RowAccessPolicy @@ -365,7 +365,7 @@ func NewViewSetAggregationPolicyRequest( return &s } -func (s *ViewSetAggregationPolicyRequest) WithEntityKey(EntityKey []DoubleQuotedString) *ViewSetAggregationPolicyRequest { +func (s *ViewSetAggregationPolicyRequest) WithEntityKey(EntityKey []Column) *ViewSetAggregationPolicyRequest { s.EntityKey = EntityKey return s } @@ -389,7 +389,7 @@ func NewViewSetColumnMaskingPolicyRequest( return &s } -func (s *ViewSetColumnMaskingPolicyRequest) WithUsing(Using []DoubleQuotedString) *ViewSetColumnMaskingPolicyRequest { +func (s *ViewSetColumnMaskingPolicyRequest) WithUsing(Using []Column) *ViewSetColumnMaskingPolicyRequest { s.Using = Using return s } diff --git a/pkg/sdk/views_dto_gen.go b/pkg/sdk/views_dto_gen.go index b14b8f5083..4b089e63d3 100644 --- a/pkg/sdk/views_dto_gen.go +++ b/pkg/sdk/views_dto_gen.go @@ -44,17 +44,17 @@ type ViewColumnProjectionPolicyRequest struct { type ViewColumnMaskingPolicyRequest struct { MaskingPolicy SchemaObjectIdentifier // required - Using []DoubleQuotedString + Using []Column } type ViewRowAccessPolicyRequest struct { RowAccessPolicy SchemaObjectIdentifier // required - On []DoubleQuotedString // required + On []Column // required } type ViewAggregationPolicyRequest struct { AggregationPolicy SchemaObjectIdentifier // required - EntityKey []DoubleQuotedString + EntityKey []Column } type AlterViewRequest struct { @@ -112,7 +112,7 @@ type ViewUnsetDataMetricScheduleRequest struct{} type ViewAddRowAccessPolicyRequest struct { RowAccessPolicy SchemaObjectIdentifier // required - On []DoubleQuotedString // required + On []Column // required } type ViewDropRowAccessPolicyRequest struct { @@ -126,7 +126,7 @@ type ViewDropAndAddRowAccessPolicyRequest struct { type ViewSetAggregationPolicyRequest struct { AggregationPolicy SchemaObjectIdentifier // required - EntityKey []DoubleQuotedString + EntityKey []Column Force *bool } @@ -135,7 +135,7 @@ type ViewUnsetAggregationPolicyRequest struct{} type ViewSetColumnMaskingPolicyRequest struct { Name string // required MaskingPolicy SchemaObjectIdentifier // required - Using []DoubleQuotedString + Using []Column Force *bool } diff --git a/pkg/sdk/views_gen.go b/pkg/sdk/views_gen.go index 8b6ab59535..2c85f8b30a 100644 --- a/pkg/sdk/views_gen.go +++ b/pkg/sdk/views_gen.go @@ -46,15 +46,15 @@ type ViewColumnProjectionPolicy struct { } type ViewColumnMaskingPolicy struct { MaskingPolicy SchemaObjectIdentifier `ddl:"identifier" sql:"MASKING POLICY"` - Using []DoubleQuotedString `ddl:"parameter,parentheses,no_equals" sql:"USING"` + Using []Column `ddl:"parameter,parentheses,no_equals" sql:"USING"` } type ViewRowAccessPolicy struct { RowAccessPolicy SchemaObjectIdentifier `ddl:"identifier" sql:"ROW ACCESS POLICY"` - On []DoubleQuotedString `ddl:"parameter,parentheses,no_equals" sql:"ON"` + On []Column `ddl:"parameter,parentheses,no_equals" sql:"ON"` } type ViewAggregationPolicy struct { AggregationPolicy SchemaObjectIdentifier `ddl:"identifier" sql:"AGGREGATION POLICY"` - EntityKey []DoubleQuotedString `ddl:"parameter,parentheses,no_equals" sql:"ENTITY KEY"` + EntityKey []Column `ddl:"parameter,parentheses,no_equals" sql:"ENTITY KEY"` } // AlterViewOptions is based on https://docs.snowflake.com/en/sql-reference/sql/alter-view. @@ -88,12 +88,12 @@ type AlterViewOptions struct { SetTagsOnColumn *ViewSetColumnTags `ddl:"keyword"` UnsetTagsOnColumn *ViewUnsetColumnTags `ddl:"keyword"` } -type DoubleQuotedString struct { +type Column struct { Value string `ddl:"keyword,double_quotes"` } type ViewDataMetricFunction struct { DataMetricFunction SchemaObjectIdentifier `ddl:"identifier"` - On []DoubleQuotedString `ddl:"parameter,parentheses,no_equals" sql:"ON"` + On []Column `ddl:"parameter,parentheses,no_equals" sql:"ON"` } type ViewAddDataMetricFunction struct { add bool `ddl:"static" sql:"ADD"` @@ -128,7 +128,7 @@ type ViewUnsetDataMetricSchedule struct { type ViewAddRowAccessPolicy struct { add bool `ddl:"static" sql:"ADD"` RowAccessPolicy SchemaObjectIdentifier `ddl:"identifier" sql:"ROW ACCESS POLICY"` - On []DoubleQuotedString `ddl:"parameter,parentheses,no_equals" sql:"ON"` + On []Column `ddl:"parameter,parentheses,no_equals" sql:"ON"` } type ViewDropRowAccessPolicy struct { drop bool `ddl:"static" sql:"DROP"` @@ -141,7 +141,7 @@ type ViewDropAndAddRowAccessPolicy struct { type ViewSetAggregationPolicy struct { set bool `ddl:"static" sql:"SET"` AggregationPolicy SchemaObjectIdentifier `ddl:"identifier" sql:"AGGREGATION POLICY"` - EntityKey []DoubleQuotedString `ddl:"parameter,parentheses,no_equals" sql:"ENTITY KEY"` + EntityKey []Column `ddl:"parameter,parentheses,no_equals" sql:"ENTITY KEY"` Force *bool `ddl:"keyword" sql:"FORCE"` } type ViewUnsetAggregationPolicy struct { @@ -153,7 +153,7 @@ type ViewSetColumnMaskingPolicy struct { Name string `ddl:"keyword,double_quotes"` set bool `ddl:"static" sql:"SET"` MaskingPolicy SchemaObjectIdentifier `ddl:"identifier" sql:"MASKING POLICY"` - Using []DoubleQuotedString `ddl:"parameter,parentheses,no_equals" sql:"USING"` + Using []Column `ddl:"parameter,parentheses,no_equals" sql:"USING"` Force *bool `ddl:"keyword" sql:"FORCE"` } type ViewUnsetColumnMaskingPolicy struct { @@ -244,10 +244,23 @@ func (v *View) ID() SchemaObjectIdentifier { return NewSchemaObjectIdentifier(v.DatabaseName, v.SchemaName, v.Name) } +// TODO(SNOW-1636212): remove func (v *View) HasCopyGrants() bool { return strings.Contains(v.Text, " COPY GRANTS ") } +func (v *View) IsTemporary() bool { + return strings.Contains(v.Text, "TEMPORARY") +} + +func (v *View) IsRecursive() bool { + return strings.Contains(v.Text, "RECURSIVE") +} + +func (v *View) IsChangeTracking() bool { + return v.ChangeTracking == "ON" +} + // DescribeViewOptions is based on https://docs.snowflake.com/en/sql-reference/sql/desc-view. type DescribeViewOptions struct { describe bool `ddl:"static" sql:"DESCRIBE"` diff --git a/pkg/sdk/views_gen_test.go b/pkg/sdk/views_gen_test.go index d32e775e3a..94ef5d195d 100644 --- a/pkg/sdk/views_gen_test.go +++ b/pkg/sdk/views_gen_test.go @@ -47,7 +47,7 @@ func TestViews_Create(t *testing.T) { opts := defaultOpts() opts.RowAccessPolicy = &ViewRowAccessPolicy{ RowAccessPolicy: randomSchemaObjectIdentifier(), - On: []DoubleQuotedString{}, + On: []Column{}, } assertOptsInvalidJoinedErrors(t, opts, errNotSet("CreateViewOptions.RowAccessPolicy", "On")) }) @@ -101,7 +101,7 @@ func TestViews_Create(t *testing.T) { *NewViewColumnRequest("column_with_comment").WithComment("column 2 comment"), *NewViewColumnRequest("column").WithMaskingPolicy( *NewViewColumnMaskingPolicyRequest(maskingPolicy1Id). - WithUsing([]DoubleQuotedString{{"a"}, {"b"}}), + WithUsing([]Column{{"a"}, {"b"}}), ).WithTag([]TagAssociation{{Name: tag1Id, Value: "v1"}}), *NewViewColumnRequest("column 2").WithProjectionPolicy( *NewViewColumnProjectionPolicyRequest(maskingPolicy2Id), @@ -109,8 +109,8 @@ func TestViews_Create(t *testing.T) { }). WithCopyGrants(true). WithComment("comment"). - WithRowAccessPolicy(*NewViewRowAccessPolicyRequest(rowAccessPolicyId, []DoubleQuotedString{{"c"}, {"d"}})). - WithAggregationPolicy(*NewViewAggregationPolicyRequest(aggregationPolicyId).WithEntityKey([]DoubleQuotedString{{"column_with_comment"}})). + WithRowAccessPolicy(*NewViewRowAccessPolicyRequest(rowAccessPolicyId, []Column{{"c"}, {"d"}})). + WithAggregationPolicy(*NewViewAggregationPolicyRequest(aggregationPolicyId).WithEntityKey([]Column{{"column_with_comment"}})). WithTag([]TagAssociation{{ Name: tag2Id, Value: "v2", @@ -201,7 +201,7 @@ func TestViews_Alter(t *testing.T) { opts := defaultOpts() opts.AddRowAccessPolicy = &ViewAddRowAccessPolicy{ RowAccessPolicy: randomSchemaObjectIdentifier(), - On: []DoubleQuotedString{}, + On: []Column{}, } assertOptsInvalidJoinedErrors(t, opts, errNotSet("AlterViewOptions.AddRowAccessPolicy", "On")) }) @@ -240,7 +240,7 @@ func TestViews_Alter(t *testing.T) { }, Add: ViewAddRowAccessPolicy{ RowAccessPolicy: randomSchemaObjectIdentifier(), - On: []DoubleQuotedString{}, + On: []Column{}, }, } assertOptsInvalidJoinedErrors(t, opts, errNotSet("AlterViewOptions.DropAndAddRowAccessPolicy.Add", "On")) @@ -295,7 +295,7 @@ func TestViews_Alter(t *testing.T) { opts := defaultOpts() opts.AddDataMetricFunction = &ViewAddDataMetricFunction{ - DataMetricFunction: []ViewDataMetricFunction{{DataMetricFunction: dmfId, On: []DoubleQuotedString{{"foo"}}}}, + DataMetricFunction: []ViewDataMetricFunction{{DataMetricFunction: dmfId, On: []Column{{"foo"}}}}, } assertOptsValidAndSQLEquals(t, opts, "ALTER VIEW %s ADD DATA METRIC FUNCTION %s ON (\"foo\")", id.FullyQualifiedName(), dmfId.FullyQualifiedName()) }) @@ -305,7 +305,7 @@ func TestViews_Alter(t *testing.T) { opts := defaultOpts() opts.DropDataMetricFunction = &ViewDropDataMetricFunction{ - DataMetricFunction: []ViewDataMetricFunction{{DataMetricFunction: dmfId, On: []DoubleQuotedString{{"foo"}}}}, + DataMetricFunction: []ViewDataMetricFunction{{DataMetricFunction: dmfId, On: []Column{{"foo"}}}}, } assertOptsValidAndSQLEquals(t, opts, "ALTER VIEW %s DROP DATA METRIC FUNCTION %s ON (\"foo\")", id.FullyQualifiedName(), dmfId.FullyQualifiedName()) }) @@ -370,7 +370,7 @@ func TestViews_Alter(t *testing.T) { opts := defaultOpts() opts.AddRowAccessPolicy = &ViewAddRowAccessPolicy{ RowAccessPolicy: rowAccessPolicyId, - On: []DoubleQuotedString{{"a"}, {"b"}}, + On: []Column{{"a"}, {"b"}}, } assertOptsValidAndSQLEquals(t, opts, "ALTER VIEW %s ADD ROW ACCESS POLICY %s ON (\"a\", \"b\")", id.FullyQualifiedName(), rowAccessPolicyId.FullyQualifiedName()) }) @@ -396,7 +396,7 @@ func TestViews_Alter(t *testing.T) { }, Add: ViewAddRowAccessPolicy{ RowAccessPolicy: rowAccessPolicy2Id, - On: []DoubleQuotedString{{"a"}, {"b"}}, + On: []Column{{"a"}, {"b"}}, }, } assertOptsValidAndSQLEquals(t, opts, "ALTER VIEW %s DROP ROW ACCESS POLICY %s, ADD ROW ACCESS POLICY %s ON (\"a\", \"b\")", id.FullyQualifiedName(), rowAccessPolicy1Id.FullyQualifiedName(), rowAccessPolicy2Id.FullyQualifiedName()) @@ -414,7 +414,7 @@ func TestViews_Alter(t *testing.T) { opts := defaultOpts() opts.SetAggregationPolicy = &ViewSetAggregationPolicy{ AggregationPolicy: aggregationPolicyId, - EntityKey: []DoubleQuotedString{{"a"}, {"b"}}, + EntityKey: []Column{{"a"}, {"b"}}, Force: Bool(true), } assertOptsValidAndSQLEquals(t, opts, "ALTER VIEW %s SET AGGREGATION POLICY %s ENTITY KEY (\"a\", \"b\") FORCE", id.FullyQualifiedName(), aggregationPolicyId.FullyQualifiedName()) @@ -433,7 +433,7 @@ func TestViews_Alter(t *testing.T) { opts.SetMaskingPolicyOnColumn = &ViewSetColumnMaskingPolicy{ Name: "column", MaskingPolicy: maskingPolicyId, - Using: []DoubleQuotedString{{"a"}, {"b"}}, + Using: []Column{{"a"}, {"b"}}, Force: Bool(true), } assertOptsValidAndSQLEquals(t, opts, "ALTER VIEW %s ALTER COLUMN \"column\" SET MASKING POLICY %s USING (\"a\", \"b\") FORCE", id.FullyQualifiedName(), maskingPolicyId.FullyQualifiedName()) diff --git a/pkg/snowflake/parser.go b/pkg/snowflake/parser.go index e351a57b7c..63c97f1ffc 100644 --- a/pkg/snowflake/parser.go +++ b/pkg/snowflake/parser.go @@ -13,6 +13,7 @@ import ( // to support queries of the sort that are generated by this project. // // Also there is little error handling and we assume queries are well-formed. +// TODO (SNOW-1636212): remove type ViewSelectStatementExtractor struct { input []rune pos int @@ -25,7 +26,6 @@ func NewViewSelectStatementExtractor(input string) *ViewSelectStatementExtractor } func (e *ViewSelectStatementExtractor) Extract() (string, error) { - fmt.Printf("[DEBUG] extracting view query %s\n", string(e.input)) e.consumeSpace() e.consumeToken("create") e.consumeSpace() @@ -33,6 +33,8 @@ func (e *ViewSelectStatementExtractor) Extract() (string, error) { e.consumeSpace() e.consumeToken("secure") e.consumeSpace() + e.consumeToken("temporary") + e.consumeSpace() e.consumeToken("recursive") e.consumeSpace() e.consumeToken("view") @@ -47,12 +49,63 @@ func (e *ViewSelectStatementExtractor) Extract() (string, error) { e.consumeSpace() e.consumeComment() e.consumeSpace() + e.extractRowAccessPolicy() + e.extractAggregationPolicy() e.consumeToken("as") e.consumeSpace() + fmt.Printf("[DEBUG] extracted statement %s from view query %s\n", string(e.input[e.pos:]), string(e.input)) + return string(e.input[e.pos:]), nil } +func (e *ViewSelectStatementExtractor) extractRowAccessPolicy() { + ok := e.consumeToken("row access policy") + if !ok { + return + } + e.consumeSpace() + e.consumeID() + e.consumeSpace() + e.consumeToken("on") + e.consumeSpace() + e.extractIdentifierList() + e.consumeSpace() +} + +func (e *ViewSelectStatementExtractor) extractAggregationPolicy() { + ok := e.consumeToken("aggregation policy") + if !ok { + return + } + e.consumeSpace() + e.consumeID() + e.consumeSpace() + e.consumeToken("entity key") + e.consumeSpace() + e.extractIdentifierList() + e.consumeSpace() +} + +func (e *ViewSelectStatementExtractor) extractIdentifierList() { + e.consumeSpace() + if !e.consumeToken("(") { + return + } + for { + e.consumeSpace() + e.consumeID() + if e.input[e.pos-1] == ')' { + break + } + e.consumeSpace() + if e.consumeToken(")") { + break + } + } + e.consumeSpace() +} + func (e *ViewSelectStatementExtractor) ExtractMaterializedView() (string, error) { fmt.Printf("[DEBUG] extracting materialized view query: %s\n", string(e.input)) e.consumeSpace() @@ -195,7 +248,8 @@ func (e *ViewSelectStatementExtractor) consumeQuotedParameter(param string) { if escaped { //nolint:gocritic // todo: please fix this to pass gocritic escaped = false - } else if e.input[e.pos+found] == '\\' { + // There are two possible escape sequences: \' and '' + } else if e.input[e.pos+found] == '\\' || e.pos+found+1 < len(e.input) && string(e.input[e.pos+found:e.pos+found+2]) == "''" { escaped = true } else if e.input[e.pos+found] == '\'' { break diff --git a/pkg/snowflake/parser_test.go b/pkg/snowflake/parser_test.go index 14fa61fe4d..efa03aadb6 100644 --- a/pkg/snowflake/parser_test.go +++ b/pkg/snowflake/parser_test.go @@ -3,11 +3,14 @@ package snowflake import ( "fmt" "testing" + + "github.com/stretchr/testify/require" ) func TestViewSelectStatementExtractor_Extract(t *testing.T) { basic := "create view foo as select * from bar;" caps := "CREATE VIEW FOO AS SELECT * FROM BAR;" + commentWithSingleQuotes := "CREATE VIEW FOO COMMENT = 'test''' AS SELECT * FROM BAR;" parens := "create view foo as (select * from bar);" multiline := ` create view foo as @@ -32,7 +35,8 @@ from bar;` full := `CREATE SECURE VIEW "rgdxfmnfhh"."PUBLIC"."rgdxfmnfhh" COMMENT = 'Terraform test resource' AS SELECT ROLE_NAME, ROLE_OWNER FROM INFORMATION_SCHEMA.APPLICABLE_ROLES` issue2640 := `CREATE OR REPLACE SECURE VIEW "CLASSIFICATION" comment = 'Classification View of the union of classification tables' AS select * from AB1_SUBSCRIPTION.CLASSIFICATION.CLASSIFICATION union select * from AB2_SUBSCRIPTION.CLASSIFICATION.CLASSIFICATION` - + withRowAccessAndAggregationPolicy := `CREATE SECURE VIEW "rgdxfmnfhh"."PUBLIC"."rgdxfmnfhh" COMMENT = 'Terraform test resource' ROW ACCESS policy rap on (title, title2) AGGREGATION POLICY rap AS SELECT ROLE_NAME, ROLE_OWNER FROM INFORMATION_SCHEMA.APPLICABLE_ROLES` + withRowAccessAndAggregationPolicyWithEntityKey := `CREATE SECURE VIEW "rgdxfmnfhh"."PUBLIC"."rgdxfmnfhh" COMMENT = 'Terraform test resource' ROW ACCESS policy rap on (title, title2) AGGREGATION POLICY rap ENTITY KEY (foo, bar) AS SELECT ROLE_NAME, ROLE_OWNER FROM INFORMATION_SCHEMA.APPLICABLE_ROLES` type args struct { input string } @@ -44,6 +48,7 @@ from bar;` }{ {"basic", args{basic}, "select * from bar;", false}, {"caps", args{caps}, "SELECT * FROM BAR;", false}, + {"comment with single quotes", args{commentWithSingleQuotes}, "SELECT * FROM BAR;", false}, {"parens", args{parens}, "(select * from bar);", false}, {"multiline", args{multiline}, "select *\nfrom bar;", false}, {"multilineComment", args{multilineComment}, "-- comment\nselect *\nfrom bar;", false}, @@ -57,6 +62,8 @@ from bar;` {"identifier", args{identifier}, "select * from bar;", false}, {"full", args{full}, "SELECT ROLE_NAME, ROLE_OWNER FROM INFORMATION_SCHEMA.APPLICABLE_ROLES", false}, {"issue2640", args{issue2640}, "select * from AB1_SUBSCRIPTION.CLASSIFICATION.CLASSIFICATION union select * from AB2_SUBSCRIPTION.CLASSIFICATION.CLASSIFICATION", false}, + {"with row access policy and aggregation policy", args{withRowAccessAndAggregationPolicy}, "SELECT ROLE_NAME, ROLE_OWNER FROM INFORMATION_SCHEMA.APPLICABLE_ROLES", false}, + {"with row access policy and aggregation policy with entity key", args{withRowAccessAndAggregationPolicyWithEntityKey}, "SELECT ROLE_NAME, ROLE_OWNER FROM INFORMATION_SCHEMA.APPLICABLE_ROLES", false}, } for _, tt := range tests { tt := tt @@ -67,9 +74,7 @@ from bar;` t.Errorf("ViewSelectStatementExtractor.Extract() error = %v, wantErr %v", err, tt.wantErr) return } - if got != tt.want { - t.Errorf("ViewSelectStatementExtractor.Extract() = '%v', want '%v'", got, tt.want) - } + require.Equal(t, tt.want, got) }) } } diff --git a/templates/resources/view.md.tmpl b/templates/resources/view.md.tmpl new file mode 100644 index 0000000000..368bbfc15d --- /dev/null +++ b/templates/resources/view.md.tmpl @@ -0,0 +1,39 @@ +--- +page_title: "{{.Name}} {{.Type}} - {{.ProviderName}}" +subcategory: "" +description: |- +{{ if gt (len (split .Description "")) 1 -}} +{{ index (split .Description "") 1 | plainmarkdown | trimspace | prefixlines " " }} +{{- else -}} +{{ .Description | plainmarkdown | trimspace | prefixlines " " }} +{{- end }} +--- + +!> **V1 release candidate** This resource was reworked and is a release candidate for the V1. We do not expect significant changes in it before the V1. We will welcome any feedback and adjust the resource if needed. Any errors reported will be resolved with a higher priority. We encourage checking this resource out before the V1 release. Please follow the [migration guide](https://github.com/Snowflake-Labs/terraform-provider-snowflake/blob/main/MIGRATION_GUIDE.md#v094x--v0950) to use it. + +!> **Note about copy_grants** Fields like `is_recursive`, `is_temporary`, `copy_grants` and `statement` can not be ALTERed on Snowflake side (check [docs](https://docs.snowflake.com/en/sql-reference/sql/alter-view)), and a change means recreation of the resource. ForceNew can not be used because it does not preserve grants from `copy_grants`. Beware that even though a change is marked as update, the resource is recreated. + +~> **Required warehouse** For this resource, the provider uses [policy references](https://docs.snowflake.com/en/sql-reference/functions/policy_references) which requires a warehouse in the connection. Please, make sure you have either set a DEFAULT_WAREHOUSE for the user, or specified a warehouse in the provider configuration. + +# {{.Name}} ({{.Type}}) + +{{ .Description | trimspace }} + +{{ if .HasExample -}} +## Example Usage + +{{ tffile (printf "examples/resources/%s/resource.tf" .Name)}} +-> **Note** Instead of using fully_qualified_name, you can reference objects managed outside Terraform by constructing a correct ID, consult [identifiers guide](https://registry.terraform.io/providers/Snowflake-Labs/snowflake/latest/docs/guides/identifiers#new-computed-fully-qualified-name-field-in-resources). + + +{{- end }} + +{{ .SchemaMarkdown | trimspace }} +{{- if .HasImport }} + +## Import + +Import is supported using the following syntax: + +{{ codefile "shell" (printf "examples/resources/%s/import.sh" .Name)}} +{{- end }}