diff --git a/.goreleaser.yml b/.goreleaser.yml
index e35c0bb88b..181fb7f3d5 100644
--- a/.goreleaser.yml
+++ b/.goreleaser.yml
@@ -1,6 +1,11 @@
builds:
- env:
- - CGO_ENABLED=0
+ - >-
+ {{- if or (eq .Os "darwin") (eq .Os "windows") }}
+ CGO_ENABLED=1
+ {{- else }}
+ CGO_ENABLED=0
+ {{- end }}
goos:
- windows
- linux
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 181b28c7a2..ee487b4c9d 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,29 @@
# Changelog
+## [0.83.1](https://github.com/Snowflake-Labs/terraform-provider-snowflake/compare/v0.83.0...v0.83.1) (2024-01-12)
+
+
+### π **Bug fixes:**
+
+* Revert goreleaser mfa token caching ([#2343](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/2343)) ([9a98031](https://github.com/Snowflake-Labs/terraform-provider-snowflake/commit/9a9803107f395ca350bdbb9fb8dc0f17820b8eb6))
+
+## [0.83.0](https://github.com/Snowflake-Labs/terraform-provider-snowflake/compare/v0.82.0...v0.83.0) (2024-01-11)
+
+
+### π **What's new:**
+
+* Add create streamlit privilege to the SDK ([#2303](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/2303)) ([be01d5f](https://github.com/Snowflake-Labs/terraform-provider-snowflake/commit/be01d5fdab4f2d31db9c4c849b349e657c0352c8))
+* grant privileges to database role resource ([#2306](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/2306)) ([0311cf8](https://github.com/Snowflake-Labs/terraform-provider-snowflake/commit/0311cf8554f0fbd202b489a54c9428f55c52a490))
+
+
+### π **Bug fixes:**
+
+* Add secondary account and fix tests ([#2324](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/2324)) ([da6ca73](https://github.com/Snowflake-Labs/terraform-provider-snowflake/commit/da6ca733c7527c8918d1e1beb86d1641d94062ec))
+* external tables issues ([#2334](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/2334)) ([ae41691](https://github.com/Snowflake-Labs/terraform-provider-snowflake/commit/ae416917be72ab55c6f1b758dd7e06269831fabc))
+* Fix test because of the date ([#2312](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/2312)) ([9a9ea33](https://github.com/Snowflake-Labs/terraform-provider-snowflake/commit/9a9ea3331f090201c2d686ed7e726eb7d9cef926))
+* Fix warehouse read and resource monitor empty set ([#2319](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/2319)) ([05f96c6](https://github.com/Snowflake-Labs/terraform-provider-snowflake/commit/05f96c699e4e36db4b28380d9ac3577d4f50a709)), closes [#2318](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/2318) [#2316](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/2316)
+* goreleaser for mfa token caching ([#2320](https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/2320)) ([4fef709](https://github.com/Snowflake-Labs/terraform-provider-snowflake/commit/4fef709a376f5905effcc3439b1ad4cb9043ffca))
+
## [0.82.0](https://github.com/Snowflake-Labs/terraform-provider-snowflake/compare/v0.81.0...v0.82.0) (2023-12-21)
diff --git a/MIGRATION_GUIDE.md b/MIGRATION_GUIDE.md
index 690f3269f0..0501f1261a 100644
--- a/MIGRATION_GUIDE.md
+++ b/MIGRATION_GUIDE.md
@@ -88,3 +88,7 @@ provider "snowflake" {
params = {}
}
```
+
+#### *(behavior change)* authenticator (JWT)
+
+Before the change `authenticator` parameter did not have to be set for private key authentication and was deduced by the provider. The change is a result of the introduced configuration alignment with an underlying [gosnowflake driver](https://github.com/snowflakedb/gosnowflake). The authentication type is required there, and it defaults to user+password one. From this version, set `authenticator` to `JWT` explicitly.
diff --git a/Makefile b/Makefile
index d4a9d88bbb..50d82787f1 100644
--- a/Makefile
+++ b/Makefile
@@ -1,8 +1,8 @@
export SKIP_EMAIL_INTEGRATION_TESTS=true
-export SKIP_EXTERNAL_TABLE_TEST=true
export SKIP_NOTIFICATION_INTEGRATION_TESTS=true
export SKIP_SAML_INTEGRATION_TESTS=true
export SKIP_STREAM_TEST=true
+export SKIP_MANAGED_ACCOUNT_INT_TEST=true
export BASE_BINARY_NAME=terraform-provider-snowflake
export TERRAFORM_PLUGINS_DIR=$(HOME)/.terraform.d/plugins
export TERRAFORM_PLUGIN_LOCAL_INSTALL=$(TERRAFORM_PLUGINS_DIR)/$(BASE_BINARY_NAME)
diff --git a/README.md b/README.md
index 5b4e49a116..aed701d47f 100644
--- a/README.md
+++ b/README.md
@@ -66,59 +66,59 @@ Integration status - indicates if given resource / datasource is using new SDK.
| Object Type | SDK migration status | Resource name | Datasource name | Integration status |
|-------------------------------------|----------------------|------------------------------------------------|-------------------------------|--------------------|
| Account | β
| snowflake_account | snowflake_account | β
|
-| Managed Account | β | snowflake_managed_account | snowflake_managed_account | β |
+| Managed Account | π¨βπ» | snowflake_managed_account | snowflake_managed_account | β |
| User | β
| snowflake_user | snowflake_user | β
|
| Database Role | β
| snowflake_database_role | snowflake_database_role | β
|
| Role | β
| snowflake_role | snowflake_role | π¨βπ» |
-| Grant Privilege to Application Role | β | snowflake_grant_privileges_to_application_role | snowflake_grants | β |
-| Grant Privilege to Database Role | β
| snowflake_grant_privileges_to_database_role | snowflake_grants | π¨βπ» |
-| Grant Privilege to Role | β | snowflake_grant_privileges_to_role | snowflake_grants | β
|
-| Grant Role | β | snowflake_grant_role | snowflake_grants | β |
-| Grant Database Role | β
| snowflake_grant_database_role | snowflake_grants | β |
-| Grant Application Role | β | snowflake_grant_application_role | snowflake_grants | β |
+| Grant Privilege to Application Role | β
| snowflake_grant_privileges_to_application_role | snowflake_grants | β |
+| Grant Privilege to Database Role | β
| snowflake_grant_privileges_to_database_role | snowflake_grants | β
|
+| Grant Privilege to Role | β
| snowflake_grant_privileges_to_role | snowflake_grants | β
|
+| Grant Role | β
| snowflake_grant_role | snowflake_grants | π¨βπ» |
+| Grant Database Role | β
| snowflake_grant_database_role | snowflake_grants | π¨βπ» |
+| Grant Application Role | β
| snowflake_grant_application_role | snowflake_grants | β |
| Grant Privilege to Share | β
| snowflake_grant_privileges_to_share | snowflake_grants | β |
| Grant Ownership | β
| snowflake_grant_ownership | snowflake_grants | β |
| API Integration | β | snowflake_api_integration | snowflake_integrations | β |
| Notification Integration | β | snowflake_notification_integration | snowflake_integrations | β |
| Security Integration | β | snowflake_security_integration | snowflake_integrations | β |
-| Storage Integration | β | snowflake_storage_integration | snowflake_integrations | β |
+| Storage Integration | β
| snowflake_storage_integration | snowflake_integrations | β |
| Network Policy | β
| snowflake_network_policy | snowflake_network_policy | β
|
| Password Policy | β
| snowflake_password_policy | snowflake_password_policy | β
|
| Session Policy | β
| snowflake_session_policy | snowflake_session_policy | β |
| Replication Group | β | snowflake_replication_group | snowflake_replication_group | β |
| Failover Group | β
| snowflake_failover_group | snowflake_failover_group | β
|
| Connection | β | snowflake_connection | snowflake_connection | β |
-| Account Parameters | β
| snowflake_account_parameter | snowflake_parameters | β |
-| Session Parameters | β
| snowflake_session_parameter | snowflake_parameters | β |
-| Object Parameters | β
| snowflake_object_parameter | snowflake_parameters | β |
+| Account Parameters | β
| snowflake_account_parameter | snowflake_parameters | β
|
+| Session Parameters | β
| snowflake_session_parameter | snowflake_parameters | β
|
+| Object Parameters | β
| snowflake_object_parameter | snowflake_parameters | β
|
| Warehouse | β
| snowflake_warehouse | snowflake_warehouse | π¨ |
| Resource Monitor | β
| snowflake_resource_monitor | snowflake_resource_monitor | β
|
| Database | β
| snowflake_database | snowflake_database | β
|
| Schema | β
| snowflake_schema | snowflake_schema | β
|
| Share | β
| snowflake_share | snowflake_share | β
|
-| Table | π¨βπ» | snowflake_table | snowflake_table | β |
-| Dynamic Table | β
| snowflake_dynamic_table | snowflake_dynamic_table | β |
-| External Table | β
| snowflake_external_table | snowflake_external_table | β |
-| Event Table | β | snowflake_event_table | snowflake_event_table | β |
-| View | β | snowflake_view | snowflake_view | β |
+| Table | β
| snowflake_table | snowflake_table | β |
+| Dynamic Table | β
| snowflake_dynamic_table | snowflake_dynamic_table | β
|
+| External Table | β
| snowflake_external_table | snowflake_external_table | β
|
+| Event Table | β
| snowflake_event_table | snowflake_event_table | β |
+| View | β
| snowflake_view | snowflake_view | β |
| Materialized View | β | snowflake_materialized_view | snowflake_materialized_view | β |
| Sequence | β | snowflake_sequence | snowflake_sequence | β |
-| Function | β | snowflake_function | snowflake_function | β |
-| External Function | β | snowflake_external_function | snowflake_external_function | β |
-| Stored Procedure | β | snowflake_stored_procedure | snowflake_stored_procedure | β |
+| Function | β
| snowflake_function | snowflake_function | β |
+| External Function | β
| snowflake_external_function | snowflake_external_function | β |
+| Stored Procedure | β
| snowflake_stored_procedure | snowflake_stored_procedure | β |
| Stream | β
| snowflake_stream | snowflake_stream | β
|
-| Task | β
| snowflake_task | snowflake_task | β |
+| Task | β
| snowflake_task | snowflake_task | β
|
| Masking Policy | β
| snowflake_masking_policy | snowflake_masking_policy | β
|
-| Row Access Policy | β | snowflake_row_access_policy | snowflake_row_access_policy | β |
+| Row Access Policy | π¨βπ» | snowflake_row_access_policy | snowflake_row_access_policy | β |
| Tag | β
| snowflake_tag | snowflake_tag | β |
| Secret | β | snowflake_secret | snowflake_secret | β |
-| Stage | β | snowflake_stage | snowflake_stage | β |
+| Stage | π¨ | snowflake_stage | snowflake_stage | β |
| File Format | β
| snowflake_file_format | snowflake_file_format | β
|
| Pipe | β
| snowflake_pipe | snowflake_pipe | β
|
| Alert | β
| snowflake_alert | snowflake_alert | β
|
-| Application | β | snowflake_application | snowflake_application | β |
-| Application Package | β | snowflake_application_package | snowflake_application_package | β |
-| Application Role | β | snowflake_application_role | snowflake_application_role | β |
+| Application | π¨βπ» | snowflake_application | snowflake_application | β |
+| Application Package | π¨βπ» | snowflake_application_package | snowflake_application_package | β |
+| Application Role | β
| snowflake_application_role | snowflake_application_role | β |
| Streamlit | β | snowflake_streamlit | snowflake_streamlit | β |
| Versioned Schema | β | snowflake_versioned_schema | snowflake_versioned_schema | β |
| Tag Association | β | snowflake_tag_association | snowflake_tag_association | β |
diff --git a/docs/index.md b/docs/index.md
index fdbc20787f..50f3f883a8 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -52,7 +52,7 @@ provider "snowflake" {
### Optional
- `account` (String) Specifies your Snowflake account identifier assigned, by Snowflake. For information about account identifiers, see the [Snowflake documentation](https://docs.snowflake.com/en/user-guide/admin-account-identifier.html). Can also be sourced from the `SNOWFLAKE_ACCOUNT` environment variable. Required unless using `profile`.
-- `authenticator` (String) Specifies the [authentication type](https://pkg.go.dev/github.com/snowflakedb/gosnowflake#AuthType) to use when connecting to Snowflake. Valid values include: Snowflake, OAuth, ExternalBrowser, Okta, JWT, TokenAccessor, UsernamePasswordMFA. Can also be sourced from the `SNOWFLAKE_AUTHENTICATOR` environment variable.
+- `authenticator` (String) Specifies the [authentication type](https://pkg.go.dev/github.com/snowflakedb/gosnowflake#AuthType) to use when connecting to Snowflake. Valid values include: Snowflake, OAuth, ExternalBrowser, Okta, JWT, TokenAccessor, UsernamePasswordMFA. Can also be sourced from the `SNOWFLAKE_AUTHENTICATOR` environment variable. It has to be set explicitly to JWT for private key authentication.
- `browser_auth` (Boolean, Deprecated) Required when `oauth_refresh_token` is used. Can also be sourced from `SNOWFLAKE_USE_BROWSER_AUTH` environment variable.
- `client_ip` (String) IP address for network checks. Can also be sourced from the `SNOWFLAKE_CLIENT_IP` environment variable.
- `client_request_mfa_token` (Boolean) When true the MFA token is cached in the credential manager. True by default in Windows/OSX. False for Linux. Can also be sourced from the `SNOWFLAKE_CLIENT_REQUEST_MFA_TOKEN` environment variable.
diff --git a/docs/resources/external_table.md b/docs/resources/external_table.md
index 364f7fb3a9..d0c43febea 100644
--- a/docs/resources/external_table.md
+++ b/docs/resources/external_table.md
@@ -53,6 +53,7 @@ resource "snowflake_external_table" "external_table" {
- `partition_by` (List of String) Specifies any partition columns to evaluate for the external table.
- `pattern` (String) Specifies the file names and/or paths on the external stage to match.
- `refresh_on_create` (Boolean) Specifies weather to refresh when an external table is created.
+- `table_format` (String) Identifies the external table table type. For now, only "delta" for Delta Lake table format is supported.
- `tag` (Block List, Deprecated) Definitions of a tag to associate with the resource. (see [below for nested schema](#nestedblock--tag))
### Read-Only
diff --git a/docs/resources/grant_account_role.md b/docs/resources/grant_account_role.md
new file mode 100644
index 0000000000..1249406239
--- /dev/null
+++ b/docs/resources/grant_account_role.md
@@ -0,0 +1,75 @@
+---
+# generated by https://github.com/hashicorp/terraform-plugin-docs
+page_title: "snowflake_grant_account_role Resource - terraform-provider-snowflake"
+subcategory: ""
+description: |-
+
+---
+
+# snowflake_grant_account_role (Resource)
+
+
+
+## Example Usage
+
+```terraform
+##################################
+### grant account role to account role
+##################################
+
+resource "snowflake_role" "role" {
+ name = var.role_name
+}
+
+resource "snowflake_role" "parent_role" {
+ name = var.parent_role_name
+}
+
+resource "snowflake_grant_account_role" "g" {
+ role_name = snowflake_role.role.name
+ parent_role_name = snowflake_role.parent_role.name
+}
+
+
+##################################
+### grant account role to user
+##################################
+
+resource "snowflake_role" "role" {
+ name = var.role_name
+}
+
+resource "snowflake_user" "user" {
+ name = var.user_name
+}
+
+resource "snowflake_grant_account_role" "g" {
+ role_name = snowflake_role.role.name
+ user_name = snowflake_user.user.name
+}
+```
+
+
+## Schema
+
+### Required
+
+- `role_name` (String) The fully qualified name of the role which will be granted to the user or parent role.
+
+### Optional
+
+- `parent_role_name` (String) The fully qualified name of the parent role which will create a parent-child relationship between the roles.
+- `user_name` (String) The fully qualified name of the user on which specified role will be granted.
+
+### Read-Only
+
+- `id` (String) The ID of this resource.
+
+## Import
+
+Import is supported using the following syntax:
+
+```shell
+# format is role_name (string) | grantee_object_type (ROLE|USER) | grantee_name (string)
+terraform import "\"test_role\"|ROLE|\"test_parent_role\""
+```
diff --git a/docs/resources/grant_database_role.md b/docs/resources/grant_database_role.md
new file mode 100644
index 0000000000..e29b98994b
--- /dev/null
+++ b/docs/resources/grant_database_role.md
@@ -0,0 +1,87 @@
+---
+# generated by https://github.com/hashicorp/terraform-plugin-docs
+page_title: "snowflake_grant_database_role Resource - terraform-provider-snowflake"
+subcategory: ""
+description: |-
+
+---
+
+# snowflake_grant_database_role (Resource)
+
+
+
+## Example Usage
+
+```terraform
+##################################
+### grant database role to account role
+##################################
+
+resource "snowflake_database_role" "database_role" {
+ database = var.database
+ name = var.database_role_name
+}
+
+resource "snowflake_role" "parent_role" {
+ name = var.parent_role_name
+}
+
+resource "snowflake_grant_database_role" "g" {
+ database_role_name = "\"${var.database}\".\"${snowflake_database_role.database_role.name}\""
+ parent_role_name = snowflake_role.parent_role.name
+}
+
+##################################
+### grant database role to database role
+##################################
+
+resource "snowflake_database_role" "database_role" {
+ database = var.database
+ name = var.database_role_name
+}
+
+resource "snowflake_database_role" "parent_database_role" {
+ database = var.database
+ name = var.parent_database_role_name
+}
+
+resource "snowflake_grant_database_role" "g" {
+ database_role_name = "\"${var.database}\".\"${snowflake_database_role.database_role.name}\""
+ parent_database_role_name = "\"${var.database}\".\"${snowflake_database_role.parent_database_role.name}\""
+}
+
+##################################
+### grant database role to share
+##################################
+
+resource "snowflake_grant_database_role" "g" {
+ database_role_name = "\"${var.database}\".\"${snowflake_database_role.database_role.name}\""
+ share_name = snowflake_share.share.name
+}
+```
+
+
+## Schema
+
+### Required
+
+- `database_role_name` (String) The fully qualified name of the database role which will be granted to share or parent role.
+
+### Optional
+
+- `parent_database_role_name` (String) The fully qualified name of the parent database role which will create a parent-child relationship between the roles.
+- `parent_role_name` (String) The fully qualified name of the parent account role which will create a parent-child relationship between the roles.
+- `share_name` (String) The fully qualified name of the share on which privileges will be granted.
+
+### Read-Only
+
+- `id` (String) The ID of this resource.
+
+## Import
+
+Import is supported using the following syntax:
+
+```shell
+# format is database_role_name (string) | object_type (ROLE|DATABASE ROLE|SHARE) | grantee_name (string)
+terraform import "\"ABC\".\"test_db_role\"|ROLE|\"test_parent_role\""
+```
diff --git a/docs/resources/grant_privileges_to_database_role.md b/docs/resources/grant_privileges_to_database_role.md
new file mode 100644
index 0000000000..067ee9caea
--- /dev/null
+++ b/docs/resources/grant_privileges_to_database_role.md
@@ -0,0 +1,309 @@
+---
+# generated by https://github.com/hashicorp/terraform-plugin-docs
+page_title: "snowflake_grant_privileges_to_database_role Resource - terraform-provider-snowflake"
+subcategory: ""
+description: |-
+
+---
+
+
+!> **Warning** Be careful when using `always_apply` field. It will always produce a plan (even when no changes were made) and can be harmful in some setups. For more details why we decided to introduce it to go our document explaining those design decisions (coming soon).
+
+# snowflake_grant_privileges_to_database_role (Resource)
+
+
+
+## Example Usage
+
+```terraform
+resource "snowflake_database_role" "db_role" {
+ database = "database"
+ name = "db_role_name"
+}
+
+##################################
+### on database privileges
+##################################
+
+# list of privileges
+resource "snowflake_grant_privileges_to_database_role" "example" {
+ privileges = ["CREATE", "MONITOR"]
+ database_role_name = "\"${snowflake_database_role.db_role.database}\".\"${snowflake_database_role.db_role.name}\""
+ on_database = snowflake_database_role.db_role.database
+}
+
+# all privileges + grant option
+resource "snowflake_grant_privileges_to_database_role" "example" {
+ database_role_name = "\"${snowflake_database_role.db_role.database}\".\"${snowflake_database_role.db_role.name}\""
+ on_database = snowflake_database_role.db_role.database
+ all_privileges = true
+ with_grant_option = true
+}
+
+# all privileges + grant option + always apply
+resource "snowflake_grant_privileges_to_database_role" "example" {
+ database_role_name = "\"${snowflake_database_role.db_role.database}\".\"${snowflake_database_role.db_role.name}\""
+ on_database = snowflake_database_role.db_role.database
+ always_apply = true
+ all_privileges = true
+ with_grant_option = true
+}
+
+##################################
+### schema privileges
+##################################
+
+# list of privileges
+resource "snowflake_grant_privileges_to_database_role" "example" {
+ privileges = ["MODIFY", "CREATE TABLE"]
+ database_role_name = "\"${snowflake_database_role.db_role.database}\".\"${snowflake_database_role.db_role.name}\""
+ on_schema {
+ schema_name = "\"${snowflake_database_role.db_role.database}\".\"my_schema\"" # note this is a fully qualified name!
+ }
+}
+
+# all privileges + grant option
+resource "snowflake_grant_privileges_to_database_role" "example" {
+ database_role_name = "\"${snowflake_database_role.db_role.database}\".\"${snowflake_database_role.db_role.name}\""
+ on_schema {
+ schema_name = "\"${snowflake_database_role.db_role.database}\".\"my_schema\"" # note this is a fully qualified name!
+ }
+ all_privileges = true
+ with_grant_option = true
+}
+
+# all schemas in database
+resource "snowflake_grant_privileges_to_database_role" "example" {
+ privileges = ["MODIFY", "CREATE TABLE"]
+ database_role_name = "\"${snowflake_database_role.db_role.database}\".\"${snowflake_database_role.db_role.name}\""
+ on_schema {
+ all_schemas_in_database = snowflake_database_role.db_role.database
+ }
+}
+
+# future schemas in database
+resource "snowflake_grant_privileges_to_database_role" "example" {
+ privileges = ["MODIFY", "CREATE TABLE"]
+ database_role_name = "\"${snowflake_database_role.db_role.database}\".\"${snowflake_database_role.db_role.name}\""
+ on_schema {
+ future_schemas_in_database = snowflake_database_role.db_role.database
+ }
+}
+
+##################################
+### schema object privileges
+##################################
+
+# list of privileges
+resource "snowflake_grant_privileges_to_database_role" "example" {
+ privileges = ["SELECT", "REFERENCES"]
+ database_role_name = "\"${snowflake_database_role.db_role.database}\".\"${snowflake_database_role.db_role.name}\""
+ on_schema_object {
+ object_type = "VIEW"
+ object_name = "\"${snowflake_database_role.db_role.database}\".\"my_schema\".\"my_view\"" # note this is a fully qualified name!
+ }
+}
+
+# all privileges + grant option
+resource "snowflake_grant_privileges_to_database_role" "example" {
+ database_role_name = "\"${snowflake_database_role.db_role.database}\".\"${snowflake_database_role.db_role.name}\""
+ on_schema_object {
+ object_type = "VIEW"
+ object_name = "\"${snowflake_database_role.db_role.database}\".\"my_schema\".\"my_view\"" # note this is a fully qualified name!
+ }
+ all_privileges = true
+ with_grant_option = true
+}
+
+# all in database
+resource "snowflake_grant_privileges_to_database_role" "example" {
+ privileges = ["SELECT", "INSERT"]
+ database_role_name = "\"${snowflake_database_role.db_role.database}\".\"${snowflake_database_role.db_role.name}\""
+ on_schema_object {
+ all {
+ object_type_plural = "TABLES"
+ in_database = snowflake_database_role.db_role.database
+ }
+ }
+}
+
+# all in schema
+resource "snowflake_grant_privileges_to_database_role" "example" {
+ privileges = ["SELECT", "INSERT"]
+ database_role_name = "\"${snowflake_database_role.db_role.database}\".\"${snowflake_database_role.db_role.name}\""
+ on_schema_object {
+ all {
+ object_type_plural = "TABLES"
+ in_schema = "\"${snowflake_database_role.db_role.database}\".\"my_schema\"" # note this is a fully qualified name!
+ }
+ }
+}
+
+# future in database
+resource "snowflake_grant_privileges_to_database_role" "example" {
+ privileges = ["SELECT", "INSERT"]
+ database_role_name = "\"${snowflake_database_role.db_role.database}\".\"${snowflake_database_role.db_role.name}\""
+ on_schema_object {
+ future {
+ object_type_plural = "TABLES"
+ in_database = snowflake_database_role.db_role.database
+ }
+ }
+}
+
+# future in schema
+resource "snowflake_grant_privileges_to_database_role" "example" {
+ privileges = ["SELECT", "INSERT"]
+ database_role_name = "\"${snowflake_database_role.db_role.database}\".\"${snowflake_database_role.db_role.name}\""
+ on_schema_object {
+ future {
+ object_type_plural = "TABLES"
+ in_schema = "\"${snowflake_database_role.db_role.database}\".\"my_schema\"" # note this is a fully qualified name!
+ }
+ }
+}
+```
+
+
+## Schema
+
+### Required
+
+- `database_role_name` (String) The fully qualified name of the database role to which privileges will be granted.
+
+### Optional
+
+- `all_privileges` (Boolean) Grant all privileges on the database role.
+- `always_apply` (Boolean) If true, the resource will always produce a βplanβ and on βapplyβ it will re-grant defined privileges. It is supposed to be used only in βgrant privileges on all Xβs in database / schema Yβ or βgrant all privileges to Xβ scenarios to make sure that every new object in a given database / schema is granted by the account role and every new privilege is granted to the database role. Important note: this flag is not compliant with the Terraform assumptions of the config being eventually convergent (producing an empty plan).
+- `always_apply_trigger` (String) This field should not be set and its main purpose is to achieve the functionality described by always_apply field. This is value will be flipped to the opposite value on every terraform apply, thus creating a new plan that will re-apply grants.
+- `on_database` (String) The fully qualified name of the database on which privileges will be granted.
+- `on_schema` (Block List, Max: 1) Specifies the schema on which privileges will be granted. (see [below for nested schema](#nestedblock--on_schema))
+- `on_schema_object` (Block List, Max: 1) Specifies the schema object on which privileges will be granted. (see [below for nested schema](#nestedblock--on_schema_object))
+- `privileges` (Set of String) The privileges to grant on the database role.
+- `with_grant_option` (Boolean) If specified, allows the recipient role to grant the privileges to other roles.
+
+### Read-Only
+
+- `id` (String) The ID of this resource.
+
+
+### Nested Schema for `on_schema`
+
+Optional:
+
+- `all_schemas_in_database` (String) The fully qualified name of the database.
+- `future_schemas_in_database` (String) The fully qualified name of the database.
+- `schema_name` (String) The fully qualified name of the schema.
+
+
+
+### Nested Schema for `on_schema_object`
+
+Optional:
+
+- `all` (Block List, Max: 1) Configures the privilege to be granted on all objects in either a database or schema. (see [below for nested schema](#nestedblock--on_schema_object--all))
+- `future` (Block List, Max: 1) Configures the privilege to be granted on future objects in either a database or schema. (see [below for nested schema](#nestedblock--on_schema_object--future))
+- `object_name` (String) The fully qualified name of the object on which privileges will be granted.
+- `object_type` (String) The object type of the schema object on which privileges will be granted. Valid values are: ALERT | DYNAMIC TABLE | EVENT TABLE | FILE FORMAT | FUNCTION | PROCEDURE | SECRET | SEQUENCE | PIPE | MASKING POLICY | PASSWORD POLICY | ROW ACCESS POLICY | SESSION POLICY | TAG | STAGE | STREAM | TABLE | EXTERNAL TABLE | TASK | VIEW | MATERIALIZED VIEW | NETWORK RULE | PACKAGES POLICY | ICEBERG TABLE
+
+
+### Nested Schema for `on_schema_object.all`
+
+Required:
+
+- `object_type_plural` (String) The plural object type of the schema object on which privileges will be granted. Valid values are: ALERTS | DYNAMIC TABLES | EVENT TABLES | FILE FORMATS | FUNCTIONS | PROCEDURES | SECRETS | SEQUENCES | PIPES | MASKING POLICIES | PASSWORD POLICIES | ROW ACCESS POLICIES | SESSION POLICIES | TAGS | STAGES | STREAMS | TABLES | EXTERNAL TABLES | TASKS | VIEWS | MATERIALIZED VIEWS | NETWORK RULES | PACKAGES POLICIES | ICEBERG TABLES
+
+Optional:
+
+- `in_database` (String)
+- `in_schema` (String)
+
+
+
+### Nested Schema for `on_schema_object.future`
+
+Required:
+
+- `object_type_plural` (String) The plural object type of the schema object on which privileges will be granted. Valid values are: ALERTS | DYNAMIC TABLES | EVENT TABLES | FILE FORMATS | FUNCTIONS | PROCEDURES | SECRETS | SEQUENCES | PIPES | MASKING POLICIES | PASSWORD POLICIES | ROW ACCESS POLICIES | SESSION POLICIES | TAGS | STAGES | STREAMS | TABLES | EXTERNAL TABLES | TASKS | VIEWS | MATERIALIZED VIEWS | NETWORK RULES | PACKAGES POLICIES | ICEBERG TABLES
+
+Optional:
+
+- `in_database` (String)
+- `in_schema` (String)
+
+## Import
+
+~> **Note** All the ..._name parts should be fully qualified names, e.g. for database object it is `"".""`
+~> **Note** To import all_privileges write ALL or ALL PRIVILEGES in place of ``
+
+Import is supported using the following syntax:
+
+`terraform import "|||||"`
+
+where:
+- database_role_name - fully qualified identifier
+- with_grant_option - boolean
+- always_apply - boolean
+- privileges - list of privileges, comma separated; to import all_privileges write "ALL" or "ALL PRIVILEGES"
+- grant_type - enum
+- grant_data - enum data
+
+It has varying number of parts, depending on grant_type. All the possible types are:
+
+### OnDatabase
+`terraform import "||||OnDatabase|"`
+
+### OnSchema
+
+On schema contains inner types for all options.
+
+#### OnSchema
+`terraform import "||||OnSchema|OnSchema|"`
+
+#### OnAllSchemasInDatabase
+`terraform import "||||OnSchema|OnAllSchemasInDatabase|"`
+
+#### OnFutureSchemasInDatabase
+`terraform import "||||OnSchema|OnFutureSchemasInDatabase|"`
+
+### OnSchemaObject
+
+On schema object contains inner types for all options.
+
+#### OnObject
+`terraform import "||||OnSchemaObject|OnObject||"`
+
+#### OnAll
+
+On all contains inner types for all options.
+
+##### InDatabase
+`terraform import "||||OnSchemaObject|OnAll||InDatabase|"`
+
+##### InSchema
+`terraform import "||||OnSchemaObject|OnAll||InSchema|"`
+
+#### OnFuture
+
+On future contains inner types for all options.
+
+##### InDatabase
+`terraform import "||||OnSchemaObject|OnFuture||InDatabase|"`
+
+##### InSchema
+`terraform import "||||OnSchemaObject|OnFuture||InSchema|"`
+
+### Import examples
+
+#### Grant all privileges OnDatabase
+`terraform import "\"test_db\".\"test_db_role\"|false|false|ALL|OnDatabase|\"test_db\""`
+
+#### Grant list of privileges OnAllSchemasInDatabase
+`terraform import "\"test_db\".\"test_db_role\"|false|false|CREATE TAG,CREATE TABLE|OnSchema|OnAllSchemasInDatabase|\"test_db\""`
+
+#### Grant list of privileges on table
+`terraform import "\"test_db\".\"test_db_role\"|false|false|SELECT,DELETE,INSERT|OnSchemaObject|OnObject|TABLE|\"test_db\".\"test_schema\".\"test_table\""`
+
+#### Grant list of privileges OnAll tables in schema
+`terraform import "\"test_db\".\"test_db_role\"|false|false|SELECT,DELETE,INSERT|OnSchemaObject|OnAll|TABLES|InSchema|\"test_db\".\"test_schema\""`
+
diff --git a/examples/resources/snowflake_grant_account_role/import.sh b/examples/resources/snowflake_grant_account_role/import.sh
new file mode 100644
index 0000000000..f308c94fa5
--- /dev/null
+++ b/examples/resources/snowflake_grant_account_role/import.sh
@@ -0,0 +1,2 @@
+# format is role_name (string) | grantee_object_type (ROLE|USER) | grantee_name (string)
+terraform import "\"test_role\"|ROLE|\"test_parent_role\""
diff --git a/examples/resources/snowflake_grant_account_role/resource.tf b/examples/resources/snowflake_grant_account_role/resource.tf
new file mode 100644
index 0000000000..75e7891cc9
--- /dev/null
+++ b/examples/resources/snowflake_grant_account_role/resource.tf
@@ -0,0 +1,34 @@
+##################################
+### grant account role to account role
+##################################
+
+resource "snowflake_role" "role" {
+ name = var.role_name
+}
+
+resource "snowflake_role" "parent_role" {
+ name = var.parent_role_name
+}
+
+resource "snowflake_grant_account_role" "g" {
+ role_name = snowflake_role.role.name
+ parent_role_name = snowflake_role.parent_role.name
+}
+
+
+##################################
+### grant account role to user
+##################################
+
+resource "snowflake_role" "role" {
+ name = var.role_name
+}
+
+resource "snowflake_user" "user" {
+ name = var.user_name
+}
+
+resource "snowflake_grant_account_role" "g" {
+ role_name = snowflake_role.role.name
+ user_name = snowflake_user.user.name
+}
diff --git a/examples/resources/snowflake_grant_database_role/import.sh b/examples/resources/snowflake_grant_database_role/import.sh
new file mode 100644
index 0000000000..878d3c901a
--- /dev/null
+++ b/examples/resources/snowflake_grant_database_role/import.sh
@@ -0,0 +1,2 @@
+# format is database_role_name (string) | object_type (ROLE|DATABASE ROLE|SHARE) | grantee_name (string)
+terraform import "\"ABC\".\"test_db_role\"|ROLE|\"test_parent_role\""
diff --git a/examples/resources/snowflake_grant_database_role/resource.tf b/examples/resources/snowflake_grant_database_role/resource.tf
new file mode 100644
index 0000000000..f3f331cf05
--- /dev/null
+++ b/examples/resources/snowflake_grant_database_role/resource.tf
@@ -0,0 +1,45 @@
+##################################
+### grant database role to account role
+##################################
+
+resource "snowflake_database_role" "database_role" {
+ database = var.database
+ name = var.database_role_name
+}
+
+resource "snowflake_role" "parent_role" {
+ name = var.parent_role_name
+}
+
+resource "snowflake_grant_database_role" "g" {
+ database_role_name = "\"${var.database}\".\"${snowflake_database_role.database_role.name}\""
+ parent_role_name = snowflake_role.parent_role.name
+}
+
+##################################
+### grant database role to database role
+##################################
+
+resource "snowflake_database_role" "database_role" {
+ database = var.database
+ name = var.database_role_name
+}
+
+resource "snowflake_database_role" "parent_database_role" {
+ database = var.database
+ name = var.parent_database_role_name
+}
+
+resource "snowflake_grant_database_role" "g" {
+ database_role_name = "\"${var.database}\".\"${snowflake_database_role.database_role.name}\""
+ parent_database_role_name = "\"${var.database}\".\"${snowflake_database_role.parent_database_role.name}\""
+}
+
+##################################
+### grant database role to share
+##################################
+
+resource "snowflake_grant_database_role" "g" {
+ database_role_name = "\"${var.database}\".\"${snowflake_database_role.database_role.name}\""
+ share_name = snowflake_share.share.name
+}
diff --git a/examples/resources/snowflake_grant_privileges_to_database_role/resource.tf b/examples/resources/snowflake_grant_privileges_to_database_role/resource.tf
new file mode 100644
index 0000000000..a5de908db6
--- /dev/null
+++ b/examples/resources/snowflake_grant_privileges_to_database_role/resource.tf
@@ -0,0 +1,146 @@
+resource "snowflake_database_role" "db_role" {
+ database = "database"
+ name = "db_role_name"
+}
+
+##################################
+### on database privileges
+##################################
+
+# list of privileges
+resource "snowflake_grant_privileges_to_database_role" "example" {
+ privileges = ["CREATE", "MONITOR"]
+ database_role_name = "\"${snowflake_database_role.db_role.database}\".\"${snowflake_database_role.db_role.name}\""
+ on_database = snowflake_database_role.db_role.database
+}
+
+# all privileges + grant option
+resource "snowflake_grant_privileges_to_database_role" "example" {
+ database_role_name = "\"${snowflake_database_role.db_role.database}\".\"${snowflake_database_role.db_role.name}\""
+ on_database = snowflake_database_role.db_role.database
+ all_privileges = true
+ with_grant_option = true
+}
+
+# all privileges + grant option + always apply
+resource "snowflake_grant_privileges_to_database_role" "example" {
+ database_role_name = "\"${snowflake_database_role.db_role.database}\".\"${snowflake_database_role.db_role.name}\""
+ on_database = snowflake_database_role.db_role.database
+ always_apply = true
+ all_privileges = true
+ with_grant_option = true
+}
+
+##################################
+### schema privileges
+##################################
+
+# list of privileges
+resource "snowflake_grant_privileges_to_database_role" "example" {
+ privileges = ["MODIFY", "CREATE TABLE"]
+ database_role_name = "\"${snowflake_database_role.db_role.database}\".\"${snowflake_database_role.db_role.name}\""
+ on_schema {
+ schema_name = "\"${snowflake_database_role.db_role.database}\".\"my_schema\"" # note this is a fully qualified name!
+ }
+}
+
+# all privileges + grant option
+resource "snowflake_grant_privileges_to_database_role" "example" {
+ database_role_name = "\"${snowflake_database_role.db_role.database}\".\"${snowflake_database_role.db_role.name}\""
+ on_schema {
+ schema_name = "\"${snowflake_database_role.db_role.database}\".\"my_schema\"" # note this is a fully qualified name!
+ }
+ all_privileges = true
+ with_grant_option = true
+}
+
+# all schemas in database
+resource "snowflake_grant_privileges_to_database_role" "example" {
+ privileges = ["MODIFY", "CREATE TABLE"]
+ database_role_name = "\"${snowflake_database_role.db_role.database}\".\"${snowflake_database_role.db_role.name}\""
+ on_schema {
+ all_schemas_in_database = snowflake_database_role.db_role.database
+ }
+}
+
+# future schemas in database
+resource "snowflake_grant_privileges_to_database_role" "example" {
+ privileges = ["MODIFY", "CREATE TABLE"]
+ database_role_name = "\"${snowflake_database_role.db_role.database}\".\"${snowflake_database_role.db_role.name}\""
+ on_schema {
+ future_schemas_in_database = snowflake_database_role.db_role.database
+ }
+}
+
+##################################
+### schema object privileges
+##################################
+
+# list of privileges
+resource "snowflake_grant_privileges_to_database_role" "example" {
+ privileges = ["SELECT", "REFERENCES"]
+ database_role_name = "\"${snowflake_database_role.db_role.database}\".\"${snowflake_database_role.db_role.name}\""
+ on_schema_object {
+ object_type = "VIEW"
+ object_name = "\"${snowflake_database_role.db_role.database}\".\"my_schema\".\"my_view\"" # note this is a fully qualified name!
+ }
+}
+
+# all privileges + grant option
+resource "snowflake_grant_privileges_to_database_role" "example" {
+ database_role_name = "\"${snowflake_database_role.db_role.database}\".\"${snowflake_database_role.db_role.name}\""
+ on_schema_object {
+ object_type = "VIEW"
+ object_name = "\"${snowflake_database_role.db_role.database}\".\"my_schema\".\"my_view\"" # note this is a fully qualified name!
+ }
+ all_privileges = true
+ with_grant_option = true
+}
+
+# all in database
+resource "snowflake_grant_privileges_to_database_role" "example" {
+ privileges = ["SELECT", "INSERT"]
+ database_role_name = "\"${snowflake_database_role.db_role.database}\".\"${snowflake_database_role.db_role.name}\""
+ on_schema_object {
+ all {
+ object_type_plural = "TABLES"
+ in_database = snowflake_database_role.db_role.database
+ }
+ }
+}
+
+# all in schema
+resource "snowflake_grant_privileges_to_database_role" "example" {
+ privileges = ["SELECT", "INSERT"]
+ database_role_name = "\"${snowflake_database_role.db_role.database}\".\"${snowflake_database_role.db_role.name}\""
+ on_schema_object {
+ all {
+ object_type_plural = "TABLES"
+ in_schema = "\"${snowflake_database_role.db_role.database}\".\"my_schema\"" # note this is a fully qualified name!
+ }
+ }
+}
+
+# future in database
+resource "snowflake_grant_privileges_to_database_role" "example" {
+ privileges = ["SELECT", "INSERT"]
+ database_role_name = "\"${snowflake_database_role.db_role.database}\".\"${snowflake_database_role.db_role.name}\""
+ on_schema_object {
+ future {
+ object_type_plural = "TABLES"
+ in_database = snowflake_database_role.db_role.database
+ }
+ }
+}
+
+# future in schema
+resource "snowflake_grant_privileges_to_database_role" "example" {
+ privileges = ["SELECT", "INSERT"]
+ database_role_name = "\"${snowflake_database_role.db_role.database}\".\"${snowflake_database_role.db_role.name}\""
+ on_schema_object {
+ future {
+ object_type_plural = "TABLES"
+ in_schema = "\"${snowflake_database_role.db_role.database}\".\"my_schema\"" # note this is a fully qualified name!
+ }
+ }
+}
diff --git a/framework/provider/resource_monitor_resource.go b/framework/provider/resource_monitor_resource.go
index 921387a376..588c509e19 100644
--- a/framework/provider/resource_monitor_resource.go
+++ b/framework/provider/resource_monitor_resource.go
@@ -736,7 +736,7 @@ func (r *ResourceMonitorResource) update(ctx context.Context, plan *resourceMoni
for _, e := range elements {
notifiedUsers = append(notifiedUsers, sdk.NotifiedUser{Name: e.ValueString()})
}
- opts.NotifyUsers = &sdk.NotifyUsers{
+ opts.Set.NotifyUsers = &sdk.NotifyUsers{
Users: notifiedUsers,
}
}
diff --git a/pkg/acceptance/testing.go b/pkg/acceptance/testing.go
index 0cd29e41f4..d6725ca491 100644
--- a/pkg/acceptance/testing.go
+++ b/pkg/acceptance/testing.go
@@ -91,3 +91,11 @@ func ConfigurationDirectory(directory string) func(config.TestStepConfigRequest)
return filepath.Join("testdata", directory)
}
}
+
+// ConfigurationInnerDirectory is similar to ConfigurationSameAsStepN, but instead of index-based directories,
+// you can choose a particular one by name.
+func ConfigurationInnerDirectory(innerDirectory string) func(config.TestStepConfigRequest) string {
+ return func(req config.TestStepConfigRequest) string {
+ return filepath.Join("testdata", req.TestName, innerDirectory)
+ }
+}
diff --git a/pkg/provider/provider.go b/pkg/provider/provider.go
index f51cb5e706..00486c65bb 100644
--- a/pkg/provider/provider.go
+++ b/pkg/provider/provider.go
@@ -105,7 +105,7 @@ func Provider() *schema.Provider {
},
"authenticator": {
Type: schema.TypeString,
- Description: "Specifies the [authentication type](https://pkg.go.dev/github.com/snowflakedb/gosnowflake#AuthType) to use when connecting to Snowflake. Valid values include: Snowflake, OAuth, ExternalBrowser, Okta, JWT, TokenAccessor, UsernamePasswordMFA. Can also be sourced from the `SNOWFLAKE_AUTHENTICATOR` environment variable.",
+ Description: "Specifies the [authentication type](https://pkg.go.dev/github.com/snowflakedb/gosnowflake#AuthType) to use when connecting to Snowflake. Valid values include: Snowflake, OAuth, ExternalBrowser, Okta, JWT, TokenAccessor, UsernamePasswordMFA. Can also be sourced from the `SNOWFLAKE_AUTHENTICATOR` environment variable. It has to be set explicitly to JWT for private key authentication.",
Optional: true,
DefaultFunc: schema.EnvDefaultFunc("SNOWFLAKE_AUTHENTICATOR", nil),
ValidateFunc: func(val interface{}, key string) (warns []string, errs []error) {
@@ -443,7 +443,10 @@ func getResources() map[string]*schema.Resource {
"snowflake_failover_group": resources.FailoverGroup(),
"snowflake_file_format": resources.FileFormat(),
"snowflake_function": resources.Function(),
+ "snowflake_grant_account_role": resources.GrantAccountRole(),
+ "snowflake_grant_database_role": resources.GrantDatabaseRole(),
"snowflake_grant_privileges_to_role": resources.GrantPrivilegesToRole(),
+ "snowflake_grant_privileges_to_database_role": resources.GrantPrivilegesToDatabaseRole(),
"snowflake_managed_account": resources.ManagedAccount(),
"snowflake_masking_policy": resources.MaskingPolicy(),
"snowflake_materialized_view": resources.MaterializedView(),
diff --git a/pkg/resources/dynamic_table_acceptance_test.go b/pkg/resources/dynamic_table_acceptance_test.go
index a685561706..6a59b1c37d 100644
--- a/pkg/resources/dynamic_table_acceptance_test.go
+++ b/pkg/resources/dynamic_table_acceptance_test.go
@@ -301,6 +301,7 @@ func testAccCheckDynamicTableDestroy(s *terraform.State) error {
return nil
}
+// TODO [SNOW-926148]: currently this dynamic table is not cleaned in the test; it is removed when the whole database is removed - this currently happens in a sweeper
func createDynamicTableOutsideTerraform(t *testing.T, schemaName string, dynamicTableName string, query string) {
t.Helper()
client, err := sdk.NewDefaultClient()
diff --git a/pkg/resources/email_notification_integration_acceptance_test.go b/pkg/resources/email_notification_integration_acceptance_test.go
index ace8bcaf9c..11c29a06b2 100644
--- a/pkg/resources/email_notification_integration_acceptance_test.go
+++ b/pkg/resources/email_notification_integration_acceptance_test.go
@@ -10,7 +10,7 @@ import (
"github.com/hashicorp/terraform-plugin-testing/helper/resource"
)
-// TODO: use email of our service user
+// TODO [SNOW-1007539]: use email of our service user
func TestAcc_EmailNotificationIntegration(t *testing.T) {
emailIntegrationName := strings.ToUpper(acctest.RandStringFromCharSet(10, acctest.CharSetAlpha))
verifiedEmail := "artur.sawicki@snowflake.com"
diff --git a/pkg/resources/external_table.go b/pkg/resources/external_table.go
index 7a4aad4e8a..f9d73c4237 100644
--- a/pkg/resources/external_table.go
+++ b/pkg/resources/external_table.go
@@ -6,6 +6,8 @@ import (
"fmt"
"log"
+ "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation"
+
"github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/helpers"
"github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/sdk"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
@@ -30,6 +32,13 @@ var externalTableSchema = map[string]*schema.Schema{
ForceNew: true,
Description: "The database in which to create the external table.",
},
+ "table_format": {
+ Type: schema.TypeString,
+ Optional: true,
+ ForceNew: true,
+ Description: `Identifies the external table table type. For now, only "delta" for Delta Lake table format is supported.`,
+ ValidateFunc: validation.StringInSlice([]string{"delta"}, true),
+ },
"column": {
Type: schema.TypeList,
Required: true,
@@ -152,7 +161,6 @@ func CreateExternalTable(d *schema.ResourceData, meta any) error {
id := sdk.NewSchemaObjectIdentifier(database, schema, name)
location := d.Get("location").(string)
fileFormat := d.Get("file_format").(string)
- req := sdk.NewCreateExternalTableRequest(id, location).WithRawFileFormat(&fileFormat)
tableColumns := d.Get("column").([]any)
columnRequests := make([]*sdk.ExternalTableColumnRequest, len(tableColumns))
@@ -161,51 +169,82 @@ func CreateExternalTable(d *schema.ResourceData, meta any) error {
for key, val := range col.(map[string]any) {
columnDef[key] = val.(string)
}
-
- name := columnDef["name"]
- dataTypeString := columnDef["type"]
- dataType, err := sdk.ToDataType(dataTypeString)
- if err != nil {
- return fmt.Errorf(`failed to parse datatype: %s`, dataTypeString)
- }
- as := columnDef["as"]
- columnRequests[i] = sdk.NewExternalTableColumnRequest(name, dataType, as)
+ columnRequests[i] = sdk.NewExternalTableColumnRequest(
+ columnDef["name"],
+ sdk.DataType(columnDef["type"]),
+ columnDef["as"],
+ )
}
- req.WithColumns(columnRequests)
-
- req.WithAutoRefresh(sdk.Bool(d.Get("auto_refresh").(bool)))
- req.WithRefreshOnCreate(sdk.Bool(d.Get("refresh_on_create").(bool)))
- req.WithCopyGrants(sdk.Bool(d.Get("copy_grants").(bool)))
+ autoRefresh := sdk.Bool(d.Get("auto_refresh").(bool))
+ refreshOnCreate := sdk.Bool(d.Get("refresh_on_create").(bool))
+ copyGrants := sdk.Bool(d.Get("copy_grants").(bool))
+ var partitionBy []string
if v, ok := d.GetOk("partition_by"); ok {
- partitionBy := expandStringList(v.([]any))
- req.WithPartitionBy(partitionBy)
+ partitionBy = expandStringList(v.([]any))
}
+ var pattern *string
if v, ok := d.GetOk("pattern"); ok {
- req.WithPattern(sdk.String(v.(string)))
+ pattern = sdk.String(v.(string))
}
+ var awsSnsTopic *string
if v, ok := d.GetOk("aws_sns_topic"); ok {
- req.WithAwsSnsTopic(sdk.String(v.(string)))
+ awsSnsTopic = sdk.String(v.(string))
}
+ var comment *string
if v, ok := d.GetOk("comment"); ok {
- req.WithComment(sdk.String(v.(string)))
+ comment = sdk.String(v.(string))
}
+ var tagAssociationRequests []*sdk.TagAssociationRequest
if _, ok := d.GetOk("tag"); ok {
tagAssociations := getPropertyTags(d, "tag")
- tagAssociationRequests := make([]*sdk.TagAssociationRequest, len(tagAssociations))
+ tagAssociationRequests = make([]*sdk.TagAssociationRequest, len(tagAssociations))
for i, t := range tagAssociations {
tagAssociationRequests[i] = sdk.NewTagAssociationRequest(t.Name, t.Value)
}
- req.WithTag(tagAssociationRequests)
}
- if err := client.ExternalTables.Create(ctx, req); err != nil {
- return err
+ switch {
+ case d.Get("table_format").(string) == "delta":
+ err := client.ExternalTables.CreateDeltaLake(
+ ctx,
+ sdk.NewCreateDeltaLakeExternalTableRequest(id, location).
+ WithColumns(columnRequests).
+ WithPartitionBy(partitionBy).
+ WithRefreshOnCreate(refreshOnCreate).
+ WithAutoRefresh(autoRefresh).
+ WithRawFileFormat(&fileFormat).
+ WithCopyGrants(copyGrants).
+ WithComment(comment).
+ WithTag(tagAssociationRequests),
+ )
+ if err != nil {
+ return err
+ }
+ default:
+ err := client.ExternalTables.Create(
+ ctx,
+ sdk.NewCreateExternalTableRequest(id, location).
+ WithColumns(columnRequests).
+ WithPartitionBy(partitionBy).
+ WithRefreshOnCreate(refreshOnCreate).
+ WithAutoRefresh(autoRefresh).
+ WithPattern(pattern).
+ WithRawFileFormat(&fileFormat).
+ WithAwsSnsTopic(awsSnsTopic).
+ WithCopyGrants(copyGrants).
+ WithComment(comment).
+ WithTag(tagAssociationRequests),
+ )
+ if err != nil {
+ return err
+ }
}
+
d.SetId(helpers.EncodeSnowflakeID(id))
return ReadExternalTable(d, meta)
diff --git a/pkg/resources/external_table_acceptance_test.go b/pkg/resources/external_table_acceptance_test.go
index 1cb407b126..556eb49b70 100644
--- a/pkg/resources/external_table_acceptance_test.go
+++ b/pkg/resources/external_table_acceptance_test.go
@@ -3,11 +3,17 @@ package resources_test
import (
"context"
"database/sql"
+ "encoding/json"
"fmt"
+ "log"
"os"
+ "slices"
"strings"
"testing"
+ "github.com/hashicorp/terraform-plugin-testing/plancheck"
+ "github.com/stretchr/testify/require"
+
"github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/sdk"
"github.com/hashicorp/terraform-plugin-testing/config"
"github.com/hashicorp/terraform-plugin-testing/terraform"
@@ -19,27 +25,128 @@ import (
)
func TestAcc_ExternalTable_basic(t *testing.T) {
- env := os.Getenv("SKIP_EXTERNAL_TABLE_TEST")
- if env != "" {
- t.Skip("Skipping TestAcc_ExternalTable")
+ shouldSkip, awsBucketURL, awsKeyId, awsSecretKey := externalTableTestEnvs()
+ if shouldSkip {
+ t.Skip("Skipping TestAcc_ExternalTable_basic")
}
+
name := strings.ToUpper(acctest.RandStringFromCharSet(10, acctest.CharSetAlpha))
- bucketURL := os.Getenv("AWS_EXTERNAL_BUCKET_URL")
- if bucketURL == "" {
- t.Skip("Skipping TestAcc_ExternalTable")
+ resourceName := "snowflake_external_table.test_table"
+
+ innerDirectory := "/external_tables_test_data/"
+ configVariables := map[string]config.Variable{
+ "name": config.StringVariable(name),
+ "location": config.StringVariable(awsBucketURL),
+ "aws_key_id": config.StringVariable(awsKeyId),
+ "aws_secret_key": config.StringVariable(awsSecretKey),
+ "database": config.StringVariable(acc.TestDatabaseName),
+ "schema": config.StringVariable(acc.TestSchemaName),
}
- roleName := os.Getenv("AWS_EXTERNAL_ROLE_NAME")
- if roleName == "" {
- t.Skip("Skipping TestAcc_ExternalTable")
+
+ data, err := json.Marshal([]struct {
+ Name string `json:"name"`
+ Age int `json:"age"`
+ }{
+ {
+ Name: "one",
+ Age: 11,
+ },
+ {
+ Name: "two",
+ Age: 22,
+ },
+ {
+ Name: "three",
+ Age: 33,
+ },
+ })
+ require.NoError(t, err)
+
+ resource.Test(t, resource.TestCase{
+ ProtoV6ProviderFactories: acc.TestAccProtoV6ProviderFactories,
+ PreCheck: func() { acc.TestAccPreCheck(t) },
+ TerraformVersionChecks: []tfversion.TerraformVersionCheck{
+ tfversion.RequireAbove(tfversion.Version1_5_0),
+ },
+ CheckDestroy: testAccCheckExternalTableDestroy,
+ Steps: []resource.TestStep{
+ {
+ ConfigDirectory: config.TestStepDirectory(),
+ ConfigVariables: configVariables,
+ },
+ {
+ PreConfig: func() {
+ publishExternalTablesTestData(sdk.NewSchemaObjectIdentifier(acc.TestDatabaseName, acc.TestSchemaName, name), data)
+ },
+ ConfigDirectory: config.TestStepDirectory(),
+ ConfigVariables: configVariables,
+ Check: resource.ComposeTestCheckFunc(
+ resource.TestCheckResourceAttr(resourceName, "name", name),
+ resource.TestCheckResourceAttr(resourceName, "database", acc.TestDatabaseName),
+ resource.TestCheckResourceAttr(resourceName, "schema", acc.TestSchemaName),
+ resource.TestCheckResourceAttr(resourceName, "location", fmt.Sprintf(`@"%s"."%s"."%s"%s`, acc.TestDatabaseName, acc.TestSchemaName, name, innerDirectory)),
+ resource.TestCheckResourceAttr(resourceName, "file_format", "TYPE = JSON, STRIP_OUTER_ARRAY = TRUE"),
+ resource.TestCheckResourceAttr(resourceName, "comment", "Terraform acceptance test"),
+ resource.TestCheckResourceAttr(resourceName, "column.#", "2"),
+ resource.TestCheckResourceAttr(resourceName, "column.0.name", "name"),
+ resource.TestCheckResourceAttr(resourceName, "column.0.type", "string"),
+ resource.TestCheckResourceAttr(resourceName, "column.0.as", "value:name::string"),
+ resource.TestCheckResourceAttr(resourceName, "column.1.name", "age"),
+ resource.TestCheckResourceAttr(resourceName, "column.1.type", "number"),
+ resource.TestCheckResourceAttr(resourceName, "column.1.as", "value:age::number"),
+ ),
+ },
+ {
+ ConfigDirectory: acc.ConfigurationSameAsStepN(2),
+ ConfigVariables: configVariables,
+ Check: externalTableContainsData(name, func(rows []map[string]*any) bool {
+ expectedNames := []string{"one", "two", "three"}
+ names := make([]string, 3)
+ for i, row := range rows {
+ nameValue, ok := row["NAME"]
+ if !ok {
+ return false
+ }
+
+ if nameValue == nil {
+ return false
+ }
+
+ nameStringValue, ok := (*nameValue).(string)
+ if !ok {
+ return false
+ }
+
+ names[i] = nameStringValue
+ }
+
+ return !slices.ContainsFunc(expectedNames, func(expectedName string) bool {
+ return !slices.Contains(names, expectedName)
+ })
+ }),
+ },
+ },
+ })
+}
+
+// proves https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/2310 is fixed
+func TestAcc_ExternalTable_CorrectDataTypes(t *testing.T) {
+ shouldSkip, awsBucketURL, awsKeyId, awsSecretKey := externalTableTestEnvs()
+ if shouldSkip {
+ t.Skip("Skipping TestAcc_ExternalTable_CorrectDataTypes")
}
+
+ name := strings.ToUpper(acctest.RandStringFromCharSet(10, acctest.CharSetAlpha))
resourceName := "snowflake_external_table.test_table"
+ innerDirectory := "/external_tables_test_data/"
configVariables := map[string]config.Variable{
- "name": config.StringVariable(name),
- "location": config.StringVariable(bucketURL),
- "aws_arn": config.StringVariable(roleName),
- "database": config.StringVariable(acc.TestDatabaseName),
- "schema": config.StringVariable(acc.TestSchemaName),
+ "name": config.StringVariable(name),
+ "location": config.StringVariable(awsBucketURL),
+ "aws_key_id": config.StringVariable(awsKeyId),
+ "aws_secret_key": config.StringVariable(awsSecretKey),
+ "database": config.StringVariable(acc.TestDatabaseName),
+ "schema": config.StringVariable(acc.TestSchemaName),
}
resource.Test(t, resource.TestCase{
@@ -53,26 +160,286 @@ func TestAcc_ExternalTable_basic(t *testing.T) {
{
ConfigDirectory: config.TestNameDirectory(),
ConfigVariables: configVariables,
+ ConfigPlanChecks: resource.ConfigPlanChecks{
+ PostApplyPostRefresh: []plancheck.PlanCheck{
+ plancheck.ExpectEmptyPlan(),
+ },
+ },
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttr(resourceName, "name", name),
resource.TestCheckResourceAttr(resourceName, "database", acc.TestDatabaseName),
resource.TestCheckResourceAttr(resourceName, "schema", acc.TestSchemaName),
- resource.TestCheckResourceAttr(resourceName, "location", fmt.Sprintf(`@"%s"."%s"."%s"`, acc.TestDatabaseName, acc.TestSchemaName, name)),
- resource.TestCheckResourceAttr(resourceName, "file_format", "TYPE = CSV"),
+ resource.TestCheckResourceAttr(resourceName, "location", fmt.Sprintf(`@"%s"."%s"."%s"%s`, acc.TestDatabaseName, acc.TestSchemaName, name, innerDirectory)),
+ resource.TestCheckResourceAttr(resourceName, "file_format", "TYPE = JSON, STRIP_OUTER_ARRAY = TRUE"),
resource.TestCheckResourceAttr(resourceName, "comment", "Terraform acceptance test"),
resource.TestCheckResourceAttr(resourceName, "column.#", "2"),
- resource.TestCheckResourceAttr(resourceName, "column[0].name", "column1"),
- resource.TestCheckResourceAttr(resourceName, "column[0].type", "STRING"),
- resource.TestCheckResourceAttr(resourceName, "column[0].as", "TO_VARCHAR(TO_TIMESTAMP_NTZ(value:unix_timestamp_property::NUMBER, 3), 'yyyy-mm-dd-hh')"),
- resource.TestCheckResourceAttr(resourceName, "column[1].name", "column2"),
- resource.TestCheckResourceAttr(resourceName, "column[1].type", "TIMESTAMP_NTZ(9)"),
- resource.TestCheckResourceAttr(resourceName, "column[1].as", "($1:\"CreatedDate\"::timestamp)"),
+ resource.TestCheckResourceAttr(resourceName, "column.0.name", "name"),
+ resource.TestCheckResourceAttr(resourceName, "column.0.type", "varchar(200)"),
+ resource.TestCheckResourceAttr(resourceName, "column.0.as", "value:name::string"),
+ resource.TestCheckResourceAttr(resourceName, "column.1.name", "age"),
+ resource.TestCheckResourceAttr(resourceName, "column.1.type", "number(2, 2)"),
+ resource.TestCheckResourceAttr(resourceName, "column.1.as", "value:age::number"),
+ expectTableToHaveColumnDataTypes(name, []sdk.DataType{
+ sdk.DataTypeVariant,
+ "VARCHAR(200)",
+ "NUMBER(2,2)",
+ }),
+ ),
+ },
+ },
+ })
+}
+
+// proves https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/2293 is fixed
+func TestAcc_ExternalTable_CanCreateWithPartitions(t *testing.T) {
+ shouldSkip, awsBucketURL, awsKeyId, awsSecretKey := externalTableTestEnvs()
+ if shouldSkip {
+ t.Skip("Skipping TestAcc_ExternalTable_CanCreateWithPartitions")
+ }
+
+ name := strings.ToUpper(acctest.RandStringFromCharSet(10, acctest.CharSetAlpha))
+ resourceName := "snowflake_external_table.test_table"
+
+ innerDirectory := "/external_tables_test_data/"
+ configVariables := map[string]config.Variable{
+ "name": config.StringVariable(name),
+ "location": config.StringVariable(awsBucketURL),
+ "aws_key_id": config.StringVariable(awsKeyId),
+ "aws_secret_key": config.StringVariable(awsSecretKey),
+ "database": config.StringVariable(acc.TestDatabaseName),
+ "schema": config.StringVariable(acc.TestSchemaName),
+ }
+
+ resource.Test(t, resource.TestCase{
+ ProtoV6ProviderFactories: acc.TestAccProtoV6ProviderFactories,
+ PreCheck: func() { acc.TestAccPreCheck(t) },
+ TerraformVersionChecks: []tfversion.TerraformVersionCheck{
+ tfversion.RequireAbove(tfversion.Version1_5_0),
+ },
+ CheckDestroy: testAccCheckExternalTableDestroy,
+ Steps: []resource.TestStep{
+ {
+ ConfigDirectory: config.TestNameDirectory(),
+ ConfigVariables: configVariables,
+ ConfigPlanChecks: resource.ConfigPlanChecks{
+ PostApplyPostRefresh: []plancheck.PlanCheck{
+ plancheck.ExpectEmptyPlan(),
+ },
+ },
+ Check: resource.ComposeTestCheckFunc(
+ resource.TestCheckResourceAttr(resourceName, "name", name),
+ resource.TestCheckResourceAttr(resourceName, "database", acc.TestDatabaseName),
+ resource.TestCheckResourceAttr(resourceName, "schema", acc.TestSchemaName),
+ resource.TestCheckResourceAttr(resourceName, "location", fmt.Sprintf(`@"%s"."%s"."%s"%s`, acc.TestDatabaseName, acc.TestSchemaName, name, innerDirectory)),
+ resource.TestCheckResourceAttr(resourceName, "file_format", "TYPE = JSON, STRIP_OUTER_ARRAY = TRUE"),
+ resource.TestCheckResourceAttr(resourceName, "comment", "Terraform acceptance test"),
+ resource.TestCheckResourceAttr(resourceName, "partition_by.#", "1"),
+ resource.TestCheckResourceAttr(resourceName, "partition_by.0", "filename"),
+ resource.TestCheckResourceAttr(resourceName, "column.#", "3"),
+ resource.TestCheckResourceAttr(resourceName, "column.0.name", "filename"),
+ resource.TestCheckResourceAttr(resourceName, "column.0.type", "string"),
+ resource.TestCheckResourceAttr(resourceName, "column.0.as", "metadata$filename"),
+ resource.TestCheckResourceAttr(resourceName, "column.1.name", "name"),
+ resource.TestCheckResourceAttr(resourceName, "column.1.type", "varchar(200)"),
+ resource.TestCheckResourceAttr(resourceName, "column.1.as", "value:name::string"),
+ resource.TestCheckResourceAttr(resourceName, "column.2.name", "age"),
+ resource.TestCheckResourceAttr(resourceName, "column.2.type", "number(2, 2)"),
+ resource.TestCheckResourceAttr(resourceName, "column.2.as", "value:age::number"),
+ expectTableDDLContains(name, "partition by (FILENAME)"),
),
},
},
})
}
+// proves https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/1564 is implemented
+func TestAcc_ExternalTable_DeltaLake(t *testing.T) {
+ shouldSkip, awsBucketURL, awsKeyId, awsSecretKey := externalTableTestEnvs()
+ if shouldSkip {
+ t.Skip("Skipping TestAcc_ExternalTable_DeltaLake")
+ }
+
+ name := strings.ToUpper(acctest.RandStringFromCharSet(10, acctest.CharSetAlpha))
+ resourceName := "snowflake_external_table.test_table"
+
+ innerDirectory := "/external_tables_test_data/"
+ configVariables := map[string]config.Variable{
+ "name": config.StringVariable(name),
+ "location": config.StringVariable(awsBucketURL),
+ "aws_key_id": config.StringVariable(awsKeyId),
+ "aws_secret_key": config.StringVariable(awsSecretKey),
+ "database": config.StringVariable(acc.TestDatabaseName),
+ "schema": config.StringVariable(acc.TestSchemaName),
+ }
+
+ resource.Test(t, resource.TestCase{
+ ProtoV6ProviderFactories: acc.TestAccProtoV6ProviderFactories,
+ PreCheck: func() { acc.TestAccPreCheck(t) },
+ TerraformVersionChecks: []tfversion.TerraformVersionCheck{
+ tfversion.RequireAbove(tfversion.Version1_5_0),
+ },
+ CheckDestroy: testAccCheckExternalTableDestroy,
+ Steps: []resource.TestStep{
+ {
+ ConfigDirectory: config.TestNameDirectory(),
+ ConfigVariables: configVariables,
+ ConfigPlanChecks: resource.ConfigPlanChecks{
+ PostApplyPostRefresh: []plancheck.PlanCheck{
+ plancheck.ExpectEmptyPlan(),
+ },
+ },
+ Check: resource.ComposeTestCheckFunc(
+ resource.TestCheckResourceAttr(resourceName, "name", name),
+ resource.TestCheckResourceAttr(resourceName, "database", acc.TestDatabaseName),
+ resource.TestCheckResourceAttr(resourceName, "schema", acc.TestSchemaName),
+ resource.TestCheckResourceAttr(resourceName, "location", fmt.Sprintf(`@"%s"."%s"."%s"%s`, acc.TestDatabaseName, acc.TestSchemaName, name, innerDirectory)),
+ resource.TestCheckResourceAttr(resourceName, "file_format", "TYPE = PARQUET"),
+ resource.TestCheckResourceAttr(resourceName, "comment", "Terraform acceptance test"),
+ resource.TestCheckResourceAttr(resourceName, "table_format", "delta"),
+ resource.TestCheckResourceAttr(resourceName, "partition_by.#", "1"),
+ resource.TestCheckResourceAttr(resourceName, "partition_by.0", "filename"),
+ resource.TestCheckResourceAttr(resourceName, "column.#", "2"),
+ resource.TestCheckResourceAttr(resourceName, "column.0.name", "filename"),
+ resource.TestCheckResourceAttr(resourceName, "column.0.type", "string"),
+ resource.TestCheckResourceAttr(resourceName, "column.0.as", "metadata$filename"),
+ resource.TestCheckResourceAttr(resourceName, "column.1.name", "name"),
+ resource.TestCheckResourceAttr(resourceName, "column.1.type", "string"),
+ resource.TestCheckResourceAttr(resourceName, "column.1.as", "value:name::string"),
+ func(state *terraform.State) error {
+ client := sdk.NewClientFromDB(acc.TestAccProvider.Meta().(*sql.DB))
+ ctx := context.Background()
+ id := sdk.NewSchemaObjectIdentifier(acc.TestDatabaseName, acc.TestSchemaName, name)
+ result, err := client.ExternalTables.ShowByID(ctx, sdk.NewShowExternalTableByIDRequest(id))
+ if err != nil {
+ return err
+ }
+ if result.TableFormat != "DELTA" {
+ return fmt.Errorf("expeted table_format: DELTA, got: %s", result.TableFormat)
+ }
+ return nil
+ },
+ ),
+ },
+ },
+ })
+}
+
+func externalTableTestEnvs() (bool, string, string, string) {
+ shouldSkip := os.Getenv("SKIP_EXTERNAL_TABLE_TEST")
+ awsBucketURL := os.Getenv("AWS_EXTERNAL_BUCKET_URL")
+ awsKeyId := os.Getenv("AWS_EXTERNAL_KEY_ID")
+ awsSecretKey := os.Getenv("AWS_EXTERNAL_SECRET_KEY")
+ return shouldSkip != "" || awsBucketURL == "" || awsKeyId == "" || awsSecretKey == "", awsBucketURL, awsKeyId, awsSecretKey
+}
+
+func externalTableContainsData(name string, contains func(rows []map[string]*any) bool) func(state *terraform.State) error {
+ return func(state *terraform.State) error {
+ client := sdk.NewClientFromDB(acc.TestAccProvider.Meta().(*sql.DB))
+ ctx := context.Background()
+ id := sdk.NewSchemaObjectIdentifier(acc.TestDatabaseName, acc.TestSchemaName, name)
+ rows, err := client.QueryUnsafe(ctx, fmt.Sprintf("select * from %s", id.FullyQualifiedName()))
+ if err != nil {
+ return err
+ }
+
+ jsonRows, err := json.MarshalIndent(rows, "", " ")
+ if err != nil {
+ return err
+ }
+ log.Printf("Retrieved rows for %s: %v", id.FullyQualifiedName(), string(jsonRows))
+
+ if !contains(rows) {
+ return fmt.Errorf("unexpected data returned by external table %s", id.FullyQualifiedName())
+ }
+
+ return nil
+ }
+}
+
+func publishExternalTablesTestData(stageName sdk.SchemaObjectIdentifier, data []byte) {
+ client, err := sdk.NewDefaultClient()
+ if err != nil {
+ log.Fatal(err)
+ }
+ ctx := context.Background()
+
+ _, err = client.ExecForTests(ctx, fmt.Sprintf(`copy into @%s/external_tables_test_data/test_data from (select parse_json('%s')) overwrite = true`, stageName.FullyQualifiedName(), string(data)))
+ if err != nil {
+ log.Fatal(err)
+ }
+}
+
+func expectTableToHaveColumnDataTypes(tableName string, expectedDataTypes []sdk.DataType) func(s *terraform.State) error {
+ return func(s *terraform.State) error {
+ client := sdk.NewClientFromDB(acc.TestAccProvider.Meta().(*sql.DB))
+ ctx := context.Background()
+ id := sdk.NewSchemaObjectIdentifier(acc.TestDatabaseName, acc.TestSchemaName, tableName)
+ columnsDesc, err := client.ExternalTables.DescribeColumns(ctx, sdk.NewDescribeExternalTableColumnsRequest(id))
+ if err != nil {
+ return err
+ }
+
+ actualTableDataTypes := make([]sdk.DataType, len(columnsDesc))
+ for i, desc := range columnsDesc {
+ actualTableDataTypes[i] = desc.Type
+ }
+
+ slices.SortFunc(expectedDataTypes, func(a, b sdk.DataType) int {
+ return strings.Compare(string(a), string(b))
+ })
+ slices.SortFunc(actualTableDataTypes, func(a, b sdk.DataType) int {
+ return strings.Compare(string(a), string(b))
+ })
+
+ if !slices.Equal(expectedDataTypes, actualTableDataTypes) {
+ return fmt.Errorf("expected table %s to have columns with data types: %v, got: %v", tableName, expectedDataTypes, actualTableDataTypes)
+ }
+
+ return nil
+ }
+}
+
+func expectTableDDLContains(tableName string, substr string) func(s *terraform.State) error {
+ return func(s *terraform.State) error {
+ client := sdk.NewClientFromDB(acc.TestAccProvider.Meta().(*sql.DB))
+ ctx := context.Background()
+ id := sdk.NewSchemaObjectIdentifier(acc.TestDatabaseName, acc.TestSchemaName, tableName)
+
+ rows, err := client.QueryUnsafe(ctx, fmt.Sprintf("select get_ddl('table', '%s')", id.FullyQualifiedName()))
+ if err != nil {
+ return err
+ }
+
+ if len(rows) != 1 {
+ return fmt.Errorf("unexpectedly returned more than one row: %d", len(rows))
+ }
+
+ row := rows[0]
+
+ if len(row) != 1 {
+ return fmt.Errorf("unexpectedly returned more than one columns: %d", len(row))
+ }
+
+ for _, v := range row {
+ if v == nil {
+ return fmt.Errorf("unexpectedly row value of ddl is nil")
+ }
+
+ ddl, ok := (*v).(string)
+
+ if !ok {
+ return fmt.Errorf("unexpectedly ddl is not type string")
+ }
+
+ if !strings.Contains(ddl, substr) {
+ return fmt.Errorf("expected '%s' to be a substring of '%s'", substr, ddl)
+ }
+ }
+
+ return nil
+ }
+}
+
func testAccCheckExternalTableDestroy(s *terraform.State) error {
db := acc.TestAccProvider.Meta().(*sql.DB)
client := sdk.NewClientFromDB(db)
diff --git a/pkg/resources/grant_account_role.go b/pkg/resources/grant_account_role.go
new file mode 100644
index 0000000000..65e5f4952a
--- /dev/null
+++ b/pkg/resources/grant_account_role.go
@@ -0,0 +1,181 @@
+package resources
+
+import (
+ "context"
+ "database/sql"
+ "fmt"
+ "log"
+ "strings"
+
+ "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/helpers"
+ "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/sdk"
+ "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
+)
+
+var grantAccountRoleSchema = map[string]*schema.Schema{
+ "role_name": {
+ Type: schema.TypeString,
+ Required: true,
+ Description: "The fully qualified name of the role which will be granted to the user or parent role.",
+ ForceNew: true,
+ ValidateDiagFunc: IsValidIdentifier[sdk.AccountObjectIdentifier](),
+ },
+ "user_name": {
+ Type: schema.TypeString,
+ Optional: true,
+ Description: "The fully qualified name of the user on which specified role will be granted.",
+ ForceNew: true,
+ ValidateDiagFunc: IsValidIdentifier[sdk.AccountObjectIdentifier](),
+ ExactlyOneOf: []string{
+ "user_name",
+ "parent_role_name",
+ },
+ },
+ "parent_role_name": {
+ Type: schema.TypeString,
+ Optional: true,
+ Description: "The fully qualified name of the parent role which will create a parent-child relationship between the roles.",
+ ForceNew: true,
+ ValidateDiagFunc: IsValidIdentifier[sdk.AccountObjectIdentifier](),
+ ExactlyOneOf: []string{
+ "user_name",
+ "parent_role_name",
+ },
+ },
+}
+
+func GrantAccountRole() *schema.Resource {
+ return &schema.Resource{
+ Create: CreateGrantAccountRole,
+ Read: ReadGrantAccountRole,
+ Delete: DeleteGrantAccountRole,
+ Schema: grantAccountRoleSchema,
+ Importer: &schema.ResourceImporter{
+ StateContext: func(ctx context.Context, d *schema.ResourceData, m interface{}) ([]*schema.ResourceData, error) {
+ parts := strings.Split(d.Id(), helpers.IDDelimiter)
+ if len(parts) != 3 {
+ return nil, fmt.Errorf("invalid ID specified: %v, expected ||", d.Id())
+ }
+ if err := d.Set("role_name", strings.Trim(parts[0], "\"")); err != nil {
+ return nil, err
+ }
+ switch parts[1] {
+ case "ROLE":
+ if err := d.Set("parent_role_name", strings.Trim(parts[2], "\"")); err != nil {
+ return nil, err
+ }
+ case "USER":
+ if err := d.Set("user_name", strings.Trim(parts[2], "\"")); err != nil {
+ return nil, err
+ }
+ default:
+ return nil, fmt.Errorf("invalid object type specified: %v, expected ROLE or USER", parts[1])
+ }
+
+ return []*schema.ResourceData{d}, nil
+ },
+ },
+ }
+}
+
+// CreateGrantAccountRole implements schema.CreateFunc.
+func CreateGrantAccountRole(d *schema.ResourceData, meta interface{}) error {
+ db := meta.(*sql.DB)
+ client := sdk.NewClientFromDB(db)
+ ctx := context.Background()
+ roleName := d.Get("role_name").(string)
+ roleIdentifier := sdk.NewAccountObjectIdentifierFromFullyQualifiedName(roleName)
+ // format of snowflakeResourceID is |