From 4c22b89e82a608375fd78993134cf3a0b45b985b Mon Sep 17 00:00:00 2001 From: Nathan Gaberel Date: Tue, 23 May 2023 14:04:02 -0700 Subject: [PATCH] chore: Account SDK (#1822) * Add comments SDK. * Add CurrentRegion context function. * Add accounts SDK. * Use Account SDK in resource. --- pkg/resources/account.go | 122 +++--- pkg/sdk/accounts.go | 375 ++++++++++++++++++ pkg/sdk/accounts_test.go | 211 ++++++++++ pkg/sdk/client.go | 4 + pkg/sdk/comments.go | 76 ++++ pkg/sdk/comments_integration_test.go | 43 ++ pkg/sdk/comments_test.go | 49 +++ pkg/sdk/context_functions.go | 12 + pkg/sdk/context_functions_integration_test.go | 9 + pkg/sdk/object_types.go | 1 + 10 files changed, 853 insertions(+), 49 deletions(-) create mode 100644 pkg/sdk/accounts.go create mode 100644 pkg/sdk/accounts_test.go create mode 100644 pkg/sdk/comments.go create mode 100644 pkg/sdk/comments_integration_test.go create mode 100644 pkg/sdk/comments_test.go diff --git a/pkg/resources/account.go b/pkg/resources/account.go index 55f5fee83c..c073b6bbdd 100644 --- a/pkg/resources/account.go +++ b/pkg/resources/account.go @@ -1,11 +1,13 @@ package resources import ( + "context" "database/sql" "fmt" "strings" - "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/snowflake" + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/helpers" + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/sdk" snowflakeValidation "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/validation" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" @@ -101,7 +103,7 @@ var accountSchema = map[string]*schema.Schema{ Required: true, ForceNew: true, Description: "[Snowflake Edition](https://docs.snowflake.com/en/user-guide/intro-editions.html) of the account. Valid values are: STANDARD | ENTERPRISE | BUSINESS_CRITICAL", - ValidateFunc: validation.StringInSlice([]string{"STANDARD", "ENTERPRISE", "BUSINESS_CRITICAL"}, false), + ValidateFunc: validation.StringInSlice([]string{string(sdk.EditionStandard), string(sdk.EditionEnterprise), string(sdk.EditionBusinessCritical)}, false), }, "first_name": { Type: schema.TypeString, @@ -215,101 +217,114 @@ func Account() *schema.Resource { // CreateAccount implements schema.CreateFunc. func CreateAccount(d *schema.ResourceData, meta interface{}) error { db := meta.(*sql.DB) - // get required fields. + client := sdk.NewClientFromDB(db) + ctx := context.Background() + name := d.Get("name").(string) - adminName := d.Get("admin_name").(string) - email := d.Get("email").(string) - edition := d.Get("edition").(string) + objectIdentifier := sdk.NewAccountObjectIdentifier(name) - builder := snowflake.NewCreateAccountBuilder(name, adminName, email, snowflake.AccountEdition(edition), db) + createOptions := &sdk.AccountCreateOptions{ + AdminName: d.Get("admin_name").(string), + Email: d.Get("email").(string), + Edition: d.Get("edition").(sdk.AccountEdition), + } // get optional fields. if v, ok := d.GetOk("admin_password"); ok { - builder.WithAdminPassword(v.(string)) + createOptions.AdminPassword = sdk.String(v.(string)) } if v, ok := d.GetOk("admin_rsa_public_key"); ok { - builder.WithAdminRSAPublicKey(v.(string)) + createOptions.AdminRSAPublicKey = sdk.String(v.(string)) } if v, ok := d.GetOk("first_name"); ok { - builder.WithFirstName(v.(string)) + createOptions.FirstName = sdk.String(v.(string)) } if v, ok := d.GetOk("last_name"); ok { - builder.WithLastName(v.(string)) + createOptions.LastName = sdk.String(v.(string)) } if v, ok := d.GetOk("must_change_password"); ok { - builder.WithMustChangePassword(v.(bool)) + createOptions.MustChangePassword = sdk.Bool(v.(bool)) } if v, ok := d.GetOk("region_group"); ok { - builder.WithRegionGroup(v.(string)) + createOptions.RegionGroup = sdk.String(v.(string)) } else { // For organizations that have accounts in multiple region groups, returns . so we need to split on "." - currentAccount, err := snowflake.ReadCurrentAccount(db) + currentRegion, err := client.ContextFunctions.CurrentRegion(ctx) if err != nil { return err } - regionParts := strings.Split(currentAccount.Region, ".") + regionParts := strings.Split(currentRegion, ".") if len(regionParts) == 2 { - builder.WithRegionGroup(regionParts[0]) + createOptions.RegionGroup = sdk.String(regionParts[0]) } } if v, ok := d.GetOk("region"); ok { - builder.WithRegion(v.(string)) + createOptions.Region = sdk.String(v.(string)) } else { // For organizations that have accounts in multiple region groups, returns . so we need to split on "." - currentAccount, err := snowflake.ReadCurrentAccount(db) + currentRegion, err := client.ContextFunctions.CurrentRegion(ctx) if err != nil { return err } - regionParts := strings.Split(currentAccount.Region, ".") + regionParts := strings.Split(currentRegion, ".") if len(regionParts) == 2 { - builder.WithRegion(regionParts[1]) + createOptions.Region = sdk.String(regionParts[1]) } else { - builder.WithRegion(currentAccount.Region) + createOptions.Region = sdk.String(currentRegion) } } if v, ok := d.GetOk("comment"); ok { - builder.WithComment(v.(string)) + createOptions.Comment = sdk.String(v.(string)) } - account, err := builder.Create() + + err := client.Accounts.Create(ctx, objectIdentifier, createOptions) + if err != nil { + return err + } + + account, err := client.Accounts.ShowByID(ctx, objectIdentifier) if err != nil { return err } - d.SetId(account.AccountLocator.String) + d.SetId(helpers.EncodeSnowflakeID(account.AccountLocator)) return nil } // ReadAccount implements schema.ReadFunc. func ReadAccount(d *schema.ResourceData, meta interface{}) error { db := meta.(*sql.DB) - accountLocator := d.Id() + client := sdk.NewClientFromDB(db) + ctx := context.Background() - account, err := snowflake.ShowAccount(db, accountLocator) + id := helpers.DecodeSnowflakeID(d.Id()).(sdk.AccountObjectIdentifier) + + acc, err := client.Accounts.ShowByID(ctx, id) if err != nil { - return fmt.Errorf("error reading account: %w", err) + return err } - err = d.Set("name", account.AccountName.String) - if err != nil { + + if err = d.Set("name", acc.AccountName); err != nil { return fmt.Errorf("error setting name: %w", err) } - err = d.Set("edition", account.Edition.String) - if err != nil { + + if err = d.Set("edition", acc.Edition); err != nil { return fmt.Errorf("error setting edition: %w", err) } - err = d.Set("region_group", account.RegionGroup.String) - if err != nil { + + if err = d.Set("region_group", acc.RegionGroup); err != nil { return fmt.Errorf("error setting region_group: %w", err) } - err = d.Set("region", account.SnowflakeRegion.String) - if err != nil { + + if err = d.Set("region", acc.SnowflakeRegion); err != nil { return fmt.Errorf("error setting region: %w", err) } - err = d.Set("comment", account.Comment.String) - if err != nil { + + if err = d.Set("comment", acc.Comment); err != nil { return fmt.Errorf("error setting comment: %w", err) } - err = d.Set("is_org_admin", account.IsOrgAdmin.Bool) - if err != nil { + + if err = d.Set("is_org_admin", acc.IsOrgAdmin); err != nil { return fmt.Errorf("error setting is_org_admin: %w", err) } @@ -319,22 +334,31 @@ func ReadAccount(d *schema.ResourceData, meta interface{}) error { // UpdateAccount implements schema.UpdateFunc. func UpdateAccount(d *schema.ResourceData, meta interface{}) error { db := meta.(*sql.DB) - accountLocator := d.Id() - account, err := snowflake.ShowAccount(db, accountLocator) - if err != nil { - return fmt.Errorf("error reading account: %w", err) - } + client := sdk.NewClientFromDB(db) + ctx := context.Background() - builder := snowflake.NewAlterAccountBuilder(account.AccountName.String, db) - if d.HasChange("comment") { - err := builder.SetComment(d.Get("comment").(string)) + id := helpers.DecodeSnowflakeID(d.Id()).(sdk.AccountObjectIdentifier) + + // Rename + if d.HasChange("name") { + newName := sdk.NewAccountObjectIdentifier(d.Get("name").(string)) + err := client.Accounts.Alter(ctx, &sdk.AccountAlterOptions{ + Name: &id, + NewName: newName, + }) if err != nil { return err } + d.SetId(helpers.EncodeSnowflakeID(newName)) } - if d.HasChange("name") { - err := builder.Rename(d.Get("name").(string)) + // Change comment + if d.HasChange("comment") { + err := client.Comments.Set(ctx, &sdk.SetCommentOpts{ + ObjectType: sdk.ObjectTypeAccount, + ObjectName: id, + Value: sdk.String(d.Get("comment").(string)), + }) if err != nil { return err } diff --git a/pkg/sdk/accounts.go b/pkg/sdk/accounts.go new file mode 100644 index 0000000000..c4018c9512 --- /dev/null +++ b/pkg/sdk/accounts.go @@ -0,0 +1,375 @@ +package sdk + +import ( + "context" + "fmt" + "time" +) + +type Accounts interface { + // Create creates an account. + Create(ctx context.Context, name AccountObjectIdentifier, opts *AccountCreateOptions) error + // Alter modifies an existing account + Alter(ctx context.Context, opts *AccountAlterOptions) error + // Show returns a list of accounts. + Show(ctx context.Context, opts *AccountShowOptions) ([]*Account, error) + // ShowByID returns an account by ID + ShowByID(ctx context.Context, id AccountObjectIdentifier) (*Account, error) +} + +var _ Accounts = (*accounts)(nil) + +type accounts struct { + client *Client +} + +type AccountEdition string + +var ( + EditionStandard AccountEdition = "STANDARD" + EditionEnterprise AccountEdition = "ENTERPRISE" + EditionBusinessCritical AccountEdition = "BUSINESS_CRITICAL" +) + +type AccountCreateOptions struct { + create bool `ddl:"static" db:"CREATE"` //lint:ignore U1000 This is used in the ddl tag + account bool `ddl:"static" db:"ACCOUNT"` //lint:ignore U1000 This is used in the ddl tag + name AccountObjectIdentifier `ddl:"identifier"` + + // Object properties + AdminName string `ddl:"parameter,single_quotes" db:"ADMIN_NAME"` + AdminPassword *string `ddl:"parameter,single_quotes" db:"ADMIN_PASSWORD"` + AdminRSAPublicKey *string `ddl:"parameter,single_quotes" db:"ADMIN_RSA_PUBLIC_KEY"` + FirstName *string `ddl:"parameter,single_quotes" db:"FIRST_NAME"` + LastName *string `ddl:"parameter,single_quotes" db:"LAST_NAME"` + Email string `ddl:"parameter,single_quotes" db:"EMAIL"` + MustChangePassword *bool `ddl:"parameter" db:"MUST_CHANGE_PASSWORD"` + Edition AccountEdition `ddl:"parameter" db:"EDITION"` + RegionGroup *string `ddl:"parameter,single_quotes" db:"REGION_GROUP"` + Region *string `ddl:"parameter,single_quotes" db:"REGION"` + Comment *string `ddl:"parameter,single_quotes" db:"COMMENT"` +} + +func (opts *AccountCreateOptions) validate() error { + if opts.AdminName == "" { + return fmt.Errorf("AdminName is required") + } + if !anyValueSet(opts.AdminPassword, opts.AdminRSAPublicKey) { + return fmt.Errorf("at least one of AdminPassword or AdminRSAPublicKey must be set") + } + if opts.Email == "" { + return fmt.Errorf("Email is required") + } + if opts.Edition == "" { + return fmt.Errorf("Edition is required") + } + return nil +} + +func (c *accounts) Create(ctx context.Context, name AccountObjectIdentifier, opts *AccountCreateOptions) error { + if opts == nil { + opts = &AccountCreateOptions{} + } + opts.name = name + if err := opts.validate(); err != nil { + return err + } + stmt, err := structToSQL(opts) + if err != nil { + return err + } + _, err = c.client.exec(ctx, stmt) + return err +} + +type AccountAlterOptions struct { + alter bool `ddl:"static" db:"ALTER"` //lint:ignore U1000 This is used in the ddl tag + account bool `ddl:"static" db:"ACCOUNT"` //lint:ignore U1000 This is used in the ddl tag + Name *AccountObjectIdentifier `ddl:"identifier"` + + Set *AccountSet `ddl:"keyword" db:"SET"` + Unset *AccountUnset `ddl:"list,no_parentheses" db:"UNSET"` + + ResourceMonitor AccountObjectIdentifier `ddl:"identifier,equals" db:"SET RESOURCE_MONITOR"` + PasswordPolicy SchemaObjectIdentifier `ddl:"identifier" db:"SET PASSWORD POLICY"` + SessionPolicy SchemaObjectIdentifier `ddl:"identifier" db:"SET SESSION POLICY"` + UnsetPasswordPolicy *bool `ddl:"keyword" db:"UNSET PASSWORD POLICY"` + UnsetSessionPolicy *bool `ddl:"keyword" db:"UNSET SESSION POLICY"` + + SetTag []TagAssociation `ddl:"keyword" db:"SET TAG"` + UnsetTag []ObjectIdentifier `ddl:"keyword" db:"UNSET TAG"` + + NewName AccountObjectIdentifier `ddl:"identifier" db:"RENAME TO"` + SaveOldURL *bool `ddl:"parameter" db:"SAVE_OLD_URL"` + DropOldURL *bool `ddl:"keyword" db:"DROP OLD URL"` +} + +func (opts *AccountAlterOptions) validate() error { + if ok := exactlyOneValueSet( + opts.Set, + opts.Unset, + opts.ResourceMonitor, + opts.PasswordPolicy, + opts.SessionPolicy, + opts.UnsetPasswordPolicy, + opts.UnsetSessionPolicy, + opts.SetTag, + opts.UnsetTag, + opts.NewName, + opts.DropOldURL); !ok { + return fmt.Errorf("exactly one of Set, Unset, ResourceMonitor, PasswordPolicy, SessionPolicy, UnsetPasswordPolicy, UnsetSessionPolicy, SetTag, UnsetTag, NewName or DropOldURL must be set") + } + if (valueSet(opts.NewName) || valueSet(opts.DropOldURL)) && !valueSet(opts.Name) { + return fmt.Errorf("Name must be set when using NewName or DropOldURL") + } + + return nil +} + +type AccountSet struct { + // Account params + AllowIdToken *bool `ddl:"parameter" db:"ALLOW_ID_TOKEN"` + ClientEncryptionKeySize *int `ddl:"parameter" db:"CLIENT_ENCRYPTION_KEY_SIZE"` + EnableInternalStagesPrivatelink *bool `ddl:"parameter" db:"ENABLE_INTERNAL_STAGES_PRIVATELINK"` + ExternalOauthAddPrivilegedRolesToBlockedList *bool `ddl:"parameter" db:"EXTERNAL_OAUTH_ADD_PRIVILEGED_ROLES_TO_BLOCKED_LIST"` + InitialReplicationSizeLimitInTb *int `ddl:"parameter" db:"INITIAL_REPLICATION_SIZE_LIMIT_IN_TB"` + NetworkPolicy *string `ddl:"parameter,single_quotes" db:"NETWORK_POLICY"` + PeriodicDataRekeying *bool `ddl:"parameter" db:"PERIODIC_DATA_REKEYING"` + PreventUnloadToInlineUrl *bool `ddl:"parameter" db:"PREVENT_UNLOAD_TO_INLINE_URL"` + PreventUnloadToInternalStages *bool `ddl:"parameter" db:"PREVENT_UNLOAD_TO_INTERNAL_STAGES"` + RequireStorageIntegrationForStageCreation *bool `ddl:"parameter" db:"REQUIRE_STORAGE_INTEGRATION_FOR_STAGE_CREATION"` + RequireStorageIntegrationForStageOperation *bool `ddl:"parameter" db:"REQUIRE_STORAGE_INTEGRATION_FOR_STAGE_OPERATION"` + SsoLoginPage *bool `ddl:"parameter" db:"SSO_LOGIN_PAGE"` + + // User params + EnableUnredactedQuerySyntaxError *bool `ddl:"parameter" db:"ENABLE_UNREDACTED_QUERY_SYNTAX_ERROR"` + + // Object params + DataRetentionTimeInDays *int `ddl:"parameter" db:"DATA_RETENTION_TIME_IN_DAYS"` + MaxDataExtensionTimeInDays *int `ddl:"parameter" db:"MAX_DATA_EXTENSION_TIME_IN_DAYS"` + DefaultDdlCollation *string `ddl:"parameter,single_quotes" db:"DEFAULT_DDL_COLLATION"` + MaxConcurrencyLevel *int `ddl:"parameter" db:"MAX_CONCURRENCY_LEVEL"` + PipeExecutionPaused *bool `ddl:"parameter" db:"PIPE_EXECUTION_PAUSED"` + StatementQueuedTimeoutInSeconds *int `ddl:"parameter" db:"STATEMENT_QUEUED_TIMEOUT_IN_SECONDS"` + StatementTimeoutInSeconds *int `ddl:"parameter" db:"STATEMENT_TIMEOUT_IN_SECONDS"` + + // Session params + AbortDetachedQuery *bool `ddl:"parameter" db:"ABORT_DETACHED_QUERY"` + Autocommit *bool `ddl:"parameter" db:"AUTOCOMMIT"` + BinaryInputFormat *string `ddl:"parameter,single_quotes" db:"BINARY_INPUT_FORMAT"` + BinaryOutputFormat *string `ddl:"parameter,single_quotes" db:"BINARY_OUTPUT_FORMAT"` + DateInputFormat *string `ddl:"parameter,single_quotes" db:"DATE_INPUT_FORMAT"` + DateOutputFormat *string `ddl:"parameter,single_quotes" db:"DATE_OUTPUT_FORMAT"` + ErrorOnNondeterministicMerge *bool `ddl:"parameter" db:"ERROR_ON_NONDETERMINISTIC_MERGE"` + ErrorOnNondeterministicUpdate *bool `ddl:"parameter" db:"ERROR_ON_NONDETERMINISTIC_UPDATE"` + JsonIndent *int `ddl:"parameter" db:"JSON_INDENT"` + LockTimeout *int `ddl:"parameter" db:"LOCK_TIMEOUT"` + QueryTag *string `ddl:"parameter,single_quotes" db:"QUERY_TAG"` + RowsPerResultset *int `ddl:"parameter" db:"ROWS_PER_RESULTSET"` + SimulatedDataSharingConsumer *string `ddl:"parameter,single_quotes" db:"SIMULATED_DATA_SHARING_CONSUMER"` + StrictJsonOutput *bool `ddl:"parameter" db:"STRICT_JSON_OUTPUT"` + TimestampDayIsAlways24h *bool `ddl:"parameter" db:"TIMESTAMP_DAY_IS_ALWAYS_24H"` + TimestampInputFormat *string `ddl:"parameter,single_quotes" db:"TIMESTAMP_INPUT_FORMAT"` + TimestampLtzOutputFormat *string `ddl:"parameter,single_quotes" db:"TIMESTAMP_LTZ_OUTPUT_FORMAT"` + TimestampNtzOutputFormat *string `ddl:"parameter,single_quotes" db:"TIMESTAMP_NTZ_OUTPUT_FORMAT"` + TimestampOutputFormat *string `ddl:"parameter,single_quotes" db:"TIMESTAMP_OUTPUT_FORMAT"` + TimestampTypeMapping *string `ddl:"parameter,single_quotes" db:"TIMESTAMP_TYPE_MAPPING"` + TimestampTzOutputFormat *string `ddl:"parameter,single_quotes" db:"TIMESTAMP_TZ_OUTPUT_FORMAT"` + Timezone *string `ddl:"parameter,single_quotes" db:"TIMEZONE"` + TimeInputFormat *string `ddl:"parameter,single_quotes" db:"TIME_INPUT_FORMAT"` + TimeOutputFormat *string `ddl:"parameter,single_quotes" db:"TIME_OUTPUT_FORMAT"` + TransactionDefaultIsolationLevel *string `ddl:"parameter,single_quotes" db:"TRANSACTION_DEFAULT_ISOLATION_LEVEL"` + TwoDigitCenturyStart *int `ddl:"parameter" db:"TWO_DIGIT_CENTURY_START"` + UnsupportedDdlAction *string `ddl:"parameter,single_quotes" db:"UNSUPPORTED_DDL_ACTION"` + UseCachedResult *bool `ddl:"parameter" db:"USE_CACHED_RESULT"` + WeekOfYearPolicy *int `ddl:"parameter" db:"WEEK_OF_YEAR_POLICY"` + WeekStart *int `ddl:"parameter" db:"WEEK_START"` +} + +type AccountUnset struct { + // Account params + AllowIdToken *bool `ddl:"keyword" db:"ALLOW_ID_TOKEN"` + ClientEncryptionKeySize *bool `ddl:"keyword" db:"CLIENT_ENCRYPTION_KEY_SIZE"` + EnableInternalStagesPrivatelink *bool `ddl:"keyword" db:"ENABLE_INTERNAL_STAGES_PRIVATELINK"` + ExternalOauthAddPrivilegedRolesToBlockedList *bool `ddl:"keyword" db:"EXTERNAL_OAUTH_ADD_PRIVILEGED_ROLES_TO_BLOCKED_LIST"` + InitialReplicationSizeLimitInTb *bool `ddl:"keyword" db:"INITIAL_REPLICATION_SIZE_LIMIT_IN_TB"` + NetworkPolicy *bool `ddl:"keyword" db:"NETWORK_POLICY"` + PeriodicDataRekeying *bool `ddl:"keyword" db:"PERIODIC_DATA_REKEYING"` + PreventUnloadToInlineUrl *bool `ddl:"keyword" db:"PREVENT_UNLOAD_TO_INLINE_URL"` + PreventUnloadToInternalStages *bool `ddl:"keyword" db:"PREVENT_UNLOAD_TO_INTERNAL_STAGES"` + RequireStorageIntegrationForStageCreation *bool `ddl:"keyword" db:"REQUIRE_STORAGE_INTEGRATION_FOR_STAGE_CREATION"` + RequireStorageIntegrationForStageOperation *bool `ddl:"keyword" db:"REQUIRE_STORAGE_INTEGRATION_FOR_STAGE_OPERATION"` + SsoLoginPage *bool `ddl:"keyword" db:"SSO_LOGIN_PAGE"` + + // Object params + DataRetentionTimeInDays *bool `ddl:"keyword" db:"DATA_RETENTION_TIME_IN_DAYS"` + MaxDataExtensionTimeInDays *bool `ddl:"keyword" db:"MAX_DATA_EXTENSION_TIME_IN_DAYS"` + DefaultDdlCollation *bool `ddl:"keyword" db:"DEFAULT_DDL_COLLATION"` + MaxConcurrencyLevel *bool `ddl:"keyword" db:"MAX_CONCURRENCY_LEVEL"` + PipeExecutionPaused *bool `ddl:"keyword" db:"PIPE_EXECUTION_PAUSED"` + StatementQueuedTimeoutInSeconds *bool `ddl:"keyword" db:"STATEMENT_QUEUED_TIMEOUT_IN_SECONDS"` + StatementTimeoutInSeconds *bool `ddl:"keyword" db:"STATEMENT_TIMEOUT_IN_SECONDS"` + + // Session params + AbortDetachedQuery *bool `ddl:"keyword" db:"ABORT_DETACHED_QUERY"` + Autocommit *bool `ddl:"keyword" db:"AUTOCOMMIT"` + BinaryInputFormat *bool `ddl:"keyword" db:"BINARY_INPUT_FORMAT"` + BinaryOutputFormat *bool `ddl:"keyword" db:"BINARY_OUTPUT_FORMAT"` + DateInputFormat *bool `ddl:"keyword" db:"DATE_INPUT_FORMAT"` + DateOutputFormat *bool `ddl:"keyword" db:"DATE_OUTPUT_FORMAT"` + ErrorOnNondeterministicMerge *bool `ddl:"keyword" db:"ERROR_ON_NONDETERMINISTIC_MERGE"` + ErrorOnNondeterministicUpdate *bool `ddl:"keyword" db:"ERROR_ON_NONDETERMINISTIC_UPDATE"` + JsonIndent *bool `ddl:"keyword" db:"JSON_INDENT"` + LockTimeout *bool `ddl:"keyword" db:"LOCK_TIMEOUT"` + QueryTag *bool `ddl:"keyword" db:"QUERY_TAG"` + RowsPerResultset *bool `ddl:"keyword" db:"ROWS_PER_RESULTSET"` + SimulatedDataSharingConsumer *bool `ddl:"keyword" db:"SIMULATED_DATA_SHARING_CONSUMER"` + StrictJsonOutput *bool `ddl:"keyword" db:"STRICT_JSON_OUTPUT"` + TimestampDayIsAlways24h *bool `ddl:"keyword" db:"TIMESTAMP_DAY_IS_ALWAYS_24H"` + TimestampInputFormat *bool `ddl:"keyword" db:"TIMESTAMP_INPUT_FORMAT"` + TimestampLtzOutputFormat *bool `ddl:"keyword" db:"TIMESTAMP_LTZ_OUTPUT_FORMAT"` + TimestampNtzOutputFormat *bool `ddl:"keyword" db:"TIMESTAMP_NTZ_OUTPUT_FORMAT"` + TimestampOutputFormat *bool `ddl:"keyword" db:"TIMESTAMP_OUTPUT_FORMAT"` + TimestampTypeMapping *bool `ddl:"keyword" db:"TIMESTAMP_TYPE_MAPPING"` + TimestampTzOutputFormat *bool `ddl:"keyword" db:"TIMESTAMP_TZ_OUTPUT_FORMAT"` + Timezone *bool `ddl:"keyword" db:"TIMEZONE"` + TimeInputFormat *bool `ddl:"keyword" db:"TIME_INPUT_FORMAT"` + TimeOutputFormat *bool `ddl:"keyword" db:"TIME_OUTPUT_FORMAT"` + TransactionDefaultIsolationLevel *bool `ddl:"keyword" db:"TRANSACTION_DEFAULT_ISOLATION_LEVEL"` + TwoDigitCenturyStart *bool `ddl:"keyword" db:"TWO_DIGIT_CENTURY_START"` + UnsupportedDdlAction *bool `ddl:"keyword" db:"UNSUPPORTED_DDL_ACTION"` + UseCachedResult *bool `ddl:"keyword" db:"USE_CACHED_RESULT"` + WeekOfYearPolicy *bool `ddl:"keyword" db:"WEEK_OF_YEAR_POLICY"` + WeekStart *bool `ddl:"keyword" db:"WEEK_START"` +} + +func (c *accounts) Alter(ctx context.Context, opts *AccountAlterOptions) error { + if opts == nil { + opts = &AccountAlterOptions{} + } + if err := opts.validate(); err != nil { + return err + } + sql, err := structToSQL(opts) + if err != nil { + return err + } + _, err = c.client.exec(ctx, sql) + return err +} + +type AccountShowOptions struct { + show bool `ddl:"static" db:"SHOW"` //lint:ignore U1000 This is used in the ddl tag + accounts bool `ddl:"static" db:"ORGANIZATION ACCOUNTS"` //lint:ignore U1000 This is used in the ddl tag + Like *Like `ddl:"keyword" db:"LIKE"` +} + +func (opts *AccountShowOptions) validate() error { + return nil +} + +type Account struct { + OrganizationName string + AccountName string + RegionGroup string + SnowflakeRegion string + Edition string + AccountUrl string + CreatedOn time.Time + Comment string + AccountLocator string + AccountLocatorUrl string + ManagedAccounts string + ConsumptionBillingEntityName string + MarketplaceConsumerBillingEntityName string + MarketplaceProviderBillingEntityName string + OldAccountUrl string + IsOrgAdmin bool +} + +type accountDBRow struct { + OrganizationName string `db:"ORGANIZATION_NAME"` + AccountName string `db:"ACCOUNT_NAME"` + RegionGroup string `db:"REGION_GROUP"` + SnowflakeRegion string `db:"SNOWFLAKE_REGION"` + Edition string `db:"EDITION "` + AccountUrl string `db:"ACCOUNT_URL"` + CreatedOn time.Time `db:"CREATED_ON"` + Comment string `db:"COMMENT"` + AccountLocator string `db:"ACCOUNT_LOCATOR"` + AccountLocatorUrl string `db:"ACCOUNT_LOCATOR_URL"` + ManagedAccounts string `db:"MANAGED_ACCOUNTS"` + ConsumptionBillingEntityName string `db:"CONSUMPTION_BILLING_ENTITY_NAME"` + MarketplaceConsumerBillingEntityName string `db:"MARKETPLACE_CONSUMER_BILLING_ENTITY_NAME"` + MarketplaceProviderBillingEntityName string `db:"MARKETPLACE_PROVIDER_BILLING_ENTITY_NAME"` + OldAccountUrl string `db:"OLD_ACCOUNT_URL"` + IsOrgAdmin bool `db:"IS_ORG_ADMIN"` +} + +func (row accountDBRow) toAccount() *Account { + acc := &Account{ + OrganizationName: row.OrganizationName, + AccountName: row.AccountName, + RegionGroup: row.RegionGroup, + SnowflakeRegion: row.SnowflakeRegion, + Edition: row.Edition, + AccountUrl: row.AccountUrl, + CreatedOn: row.CreatedOn, + Comment: row.Comment, + AccountLocator: row.AccountLocator, + AccountLocatorUrl: row.AccountLocatorUrl, + ManagedAccounts: row.ManagedAccounts, + ConsumptionBillingEntityName: row.ConsumptionBillingEntityName, + MarketplaceConsumerBillingEntityName: row.MarketplaceConsumerBillingEntityName, + MarketplaceProviderBillingEntityName: row.MarketplaceProviderBillingEntityName, + OldAccountUrl: row.OldAccountUrl, + IsOrgAdmin: row.IsOrgAdmin, + } + return acc +} + +func (c *accounts) Show(ctx context.Context, opts *AccountShowOptions) ([]*Account, error) { + if opts == nil { + opts = &AccountShowOptions{} + } + if err := opts.validate(); err != nil { + return nil, err + } + sql, err := structToSQL(opts) + if err != nil { + return nil, err + } + dest := []accountDBRow{} + err = c.client.query(ctx, &dest, sql) + if err != nil { + return nil, err + } + resultList := make([]*Account, len(dest)) + for i, row := range dest { + resultList[i] = row.toAccount() + } + + return resultList, nil +} + +func (c *accounts) ShowByID(ctx context.Context, id AccountObjectIdentifier) (*Account, error) { + accounts, err := c.Show(ctx, &AccountShowOptions{ + Like: &Like{ + Pattern: String(id.Name()), + }, + }) + if err != nil { + return nil, err + } + + for _, account := range accounts { + if account.ID().name == id.Name() { + return account, nil + } + } + return nil, ErrObjectNotExistOrAuthorized +} + +func (v *Account) ID() AccountObjectIdentifier { + return NewAccountObjectIdentifier(v.AccountName) +} diff --git a/pkg/sdk/accounts_test.go b/pkg/sdk/accounts_test.go new file mode 100644 index 0000000000..4267d7c037 --- /dev/null +++ b/pkg/sdk/accounts_test.go @@ -0,0 +1,211 @@ +package sdk + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/stretchr/testify/require" +) + +func TestAccountCreate(t *testing.T) { + t.Run("simplest case", func(t *testing.T) { + opts := &AccountCreateOptions{ + name: AccountObjectIdentifier{ + name: "newaccount", + }, + AdminName: "someadmin", + AdminPassword: String("v3rys3cr3t"), + Email: "admin@example.com", + Edition: EditionBusinessCritical, + } + actual, err := structToSQL(opts) + require.NoError(t, err) + expected := `CREATE ACCOUNT "newaccount" ADMIN_NAME = 'someadmin' ADMIN_PASSWORD = 'v3rys3cr3t' EMAIL = 'admin@example.com' EDITION = BUSINESS_CRITICAL` + assert.Equal(t, expected, actual) + }) + + t.Run("every option", func(t *testing.T) { + opts := &AccountCreateOptions{ + name: AccountObjectIdentifier{ + name: "newaccount", + }, + AdminName: "someadmin", + AdminRSAPublicKey: String("s3cr3tk3y"), + FirstName: String("Ad"), + LastName: String("Min"), + Email: "admin@example.com", + MustChangePassword: Bool(true), + Edition: EditionBusinessCritical, + RegionGroup: String("groupid"), + Region: String("regionid"), + Comment: String("Test account"), + } + actual, err := structToSQL(opts) + require.NoError(t, err) + expected := `CREATE ACCOUNT "newaccount" ADMIN_NAME = 'someadmin' ADMIN_RSA_PUBLIC_KEY = 's3cr3tk3y' FIRST_NAME = 'Ad' LAST_NAME = 'Min' EMAIL = 'admin@example.com' MUST_CHANGE_PASSWORD = true EDITION = BUSINESS_CRITICAL REGION_GROUP = 'groupid' REGION = 'regionid' COMMENT = 'Test account'` + assert.Equal(t, expected, actual) + }) +} + +func TestAccountAlter(t *testing.T) { + t.Run("with set params", func(t *testing.T) { + opts := &AccountAlterOptions{ + Set: &AccountSet{ + ClientEncryptionKeySize: Int(20), + PreventUnloadToInternalStages: Bool(true), + MaxDataExtensionTimeInDays: Int(30), + JsonIndent: Int(40), + TimestampOutputFormat: String("hello"), + }, + } + actual, err := structToSQL(opts) + require.NoError(t, err) + expected := `ALTER ACCOUNT SET CLIENT_ENCRYPTION_KEY_SIZE = 20 PREVENT_UNLOAD_TO_INTERNAL_STAGES = true MAX_DATA_EXTENSION_TIME_IN_DAYS = 30 JSON_INDENT = 40 TIMESTAMP_OUTPUT_FORMAT = 'hello'` + assert.Equal(t, expected, actual) + }) + + t.Run("with unset params", func(t *testing.T) { + opts := &AccountAlterOptions{ + Unset: &AccountUnset{ + InitialReplicationSizeLimitInTb: Bool(true), + SsoLoginPage: Bool(true), + DefaultDdlCollation: Bool(true), + SimulatedDataSharingConsumer: Bool(true), + Timezone: Bool(true), + }, + } + actual, err := structToSQL(opts) + require.NoError(t, err) + expected := `ALTER ACCOUNT UNSET INITIAL_REPLICATION_SIZE_LIMIT_IN_TB,SSO_LOGIN_PAGE,DEFAULT_DDL_COLLATION,SIMULATED_DATA_SHARING_CONSUMER,TIMEZONE` + assert.Equal(t, expected, actual) + }) + + t.Run("with set resource monitor", func(t *testing.T) { + opts := &AccountAlterOptions{ + ResourceMonitor: NewAccountObjectIdentifier("mymonitor"), + } + actual, err := structToSQL(opts) + require.NoError(t, err) + expected := `ALTER ACCOUNT SET RESOURCE_MONITOR = "mymonitor"` + assert.Equal(t, expected, actual) + }) + + t.Run("with set password policy", func(t *testing.T) { + opts := &AccountAlterOptions{ + PasswordPolicy: NewSchemaObjectIdentifier("db", "schema", "passpol"), + } + actual, err := structToSQL(opts) + require.NoError(t, err) + expected := `ALTER ACCOUNT SET PASSWORD POLICY "db"."schema"."passpol"` + assert.Equal(t, expected, actual) + }) + + t.Run("with set session policy", func(t *testing.T) { + opts := &AccountAlterOptions{ + SessionPolicy: NewSchemaObjectIdentifier("db", "schema", "sesspol"), + } + actual, err := structToSQL(opts) + require.NoError(t, err) + expected := `ALTER ACCOUNT SET SESSION POLICY "db"."schema"."sesspol"` + assert.Equal(t, expected, actual) + }) + + t.Run("with unset password policy", func(t *testing.T) { + opts := &AccountAlterOptions{ + UnsetPasswordPolicy: Bool(true), + } + actual, err := structToSQL(opts) + require.NoError(t, err) + expected := `ALTER ACCOUNT UNSET PASSWORD POLICY` + assert.Equal(t, expected, actual) + }) + + t.Run("with unset session policy", func(t *testing.T) { + opts := &AccountAlterOptions{ + UnsetSessionPolicy: Bool(true), + } + actual, err := structToSQL(opts) + require.NoError(t, err) + expected := `ALTER ACCOUNT UNSET SESSION POLICY` + assert.Equal(t, expected, actual) + }) + + t.Run("with set tag", func(t *testing.T) { + opts := &AccountAlterOptions{ + SetTag: []TagAssociation{ + { + Name: NewSchemaObjectIdentifier("db", "schema", "tag1"), + Value: "v1", + }, + { + Name: NewSchemaObjectIdentifier("db", "schema", "tag2"), + Value: "v2", + }, + }, + } + actual, err := structToSQL(opts) + require.NoError(t, err) + expected := `ALTER ACCOUNT SET TAG "db"."schema"."tag1" = 'v1',"db"."schema"."tag2" = 'v2'` + assert.Equal(t, expected, actual) + }) + + t.Run("with unset tag", func(t *testing.T) { + opts := &AccountAlterOptions{ + UnsetTag: []ObjectIdentifier{ + NewSchemaObjectIdentifier("db", "schema", "tag1"), + }, + } + actual, err := structToSQL(opts) + require.NoError(t, err) + expected := `ALTER ACCOUNT UNSET TAG "db"."schema"."tag1"` + assert.Equal(t, expected, actual) + }) + + t.Run("rename", func(t *testing.T) { + oldName := NewAccountObjectIdentifier("oldname") + opts := &AccountAlterOptions{ + Name: &oldName, + NewName: NewAccountObjectIdentifier("newname"), + SaveOldURL: Bool(false), + } + actual, err := structToSQL(opts) + require.NoError(t, err) + expected := `ALTER ACCOUNT "oldname" RENAME TO "newname" SAVE_OLD_URL = false` + assert.Equal(t, expected, actual) + }) + + t.Run("drop old url", func(t *testing.T) { + oldName := NewAccountObjectIdentifier("oldname") + opts := &AccountAlterOptions{ + Name: &oldName, + DropOldURL: Bool(true), + } + actual, err := structToSQL(opts) + require.NoError(t, err) + expected := `ALTER ACCOUNT "oldname" DROP OLD URL` + assert.Equal(t, expected, actual) + }) +} + +func TestAccountShow(t *testing.T) { + t.Run("empty options", func(t *testing.T) { + opts := &AccountShowOptions{} + actual, err := structToSQL(opts) + require.NoError(t, err) + expected := `SHOW ORGANIZATION ACCOUNTS` + assert.Equal(t, expected, actual) + }) + + t.Run("with like", func(t *testing.T) { + opts := &AccountShowOptions{ + Like: &Like{ + Pattern: String("myaccount"), + }, + } + actual, err := structToSQL(opts) + require.NoError(t, err) + expected := `SHOW ORGANIZATION ACCOUNTS LIKE 'myaccount'` + assert.Equal(t, expected, actual) + }) +} diff --git a/pkg/sdk/client.go b/pkg/sdk/client.go index 865d08208b..80edf2cf89 100644 --- a/pkg/sdk/client.go +++ b/pkg/sdk/client.go @@ -18,6 +18,8 @@ type Client struct { db *sqlx.DB dryRun bool + Accounts Accounts + Comments Comments ContextFunctions ContextFunctions Databases Databases Grants Grants @@ -94,6 +96,8 @@ func NewClientFromDB(db *sql.DB) *Client { } func (c *Client) initialize() { + c.Accounts = &accounts{client: c} + c.Comments = &comments{client: c} c.ContextFunctions = &contextFunctions{client: c} c.Databases = &databases{client: c} c.Grants = &grants{client: c} diff --git a/pkg/sdk/comments.go b/pkg/sdk/comments.go new file mode 100644 index 0000000000..4c0b5a40f4 --- /dev/null +++ b/pkg/sdk/comments.go @@ -0,0 +1,76 @@ +package sdk + +import ( + "context" +) + +type Comments interface { + Set(ctx context.Context, opts *SetCommentOpts) error + SetColumn(ctx context.Context, opts *SetColumnCommentOpts) error +} + +type comments struct { + client *Client +} + +var _ Comments = (*comments)(nil) + +type SetCommentOpts struct { + comment bool `ddl:"static" db:"COMMENT"` + IfExists *bool `ddl:"keyword" db:"IF EXISTS"` + on bool `ddl:"static" db:"ON"` + ObjectType ObjectType `ddl:"keyword"` + ObjectName ObjectIdentifier `ddl:"identifier"` + Value *string `ddl:"parameter,single_quotes,no_equals" db:"IS"` +} + +func (opts *SetCommentOpts) validate() error { + return nil +} + +func (c *comments) Set(ctx context.Context, opts *SetCommentOpts) error { + if opts == nil { + opts = &SetCommentOpts{} + } + // opts.name = name + if err := opts.validate(); err != nil { + return err + } + stmt, err := structToSQL(opts) + if err != nil { + return err + } + _, err = c.client.exec(ctx, stmt) + return err +} + +type SetColumnCommentOpts struct { + comment bool `ddl:"static" db:"COMMENT"` + IfExists *bool `ddl:"keyword" db:"IF EXISTS"` + on bool `ddl:"static" db:"ON"` + Column ObjectIdentifier `ddl:"identifier" db:"COLUMN"` + Value *string `ddl:"parameter,single_quotes,no_equals" db:"IS"` +} + +func (opts *SetColumnCommentOpts) validate() error { + return nil +} + +func (c *comments) SetColumn(ctx context.Context, opts *SetColumnCommentOpts) error { + if opts == nil { + opts = &SetColumnCommentOpts{} + } + // We only want to render table.column, not the fully qualified name with database and schema. + if v, ok := opts.Column.(TableColumnIdentifier); ok { + opts.Column = NewSchemaIdentifier(v.tableName, v.columnName) + } + if err := opts.validate(); err != nil { + return err + } + stmt, err := structToSQL(opts) + if err != nil { + return err + } + _, err = c.client.exec(ctx, stmt) + return err +} diff --git a/pkg/sdk/comments_integration_test.go b/pkg/sdk/comments_integration_test.go new file mode 100644 index 0000000000..cd6747b3f1 --- /dev/null +++ b/pkg/sdk/comments_integration_test.go @@ -0,0 +1,43 @@ +package sdk + +import ( + "context" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestInt_Comment(t *testing.T) { + client := testClient(t) + ctx := context.Background() + + testWarehouse, warehouseCleanup := createWarehouse(t, client) + t.Cleanup(warehouseCleanup) + + t.Run("set", func(t *testing.T) { + comment := randomComment(t) + err := client.Comments.Set(ctx, &SetCommentOpts{ + ObjectType: ObjectTypeWarehouse, + ObjectName: testWarehouse.ID(), + Value: String(comment), + }) + require.NoError(t, err) + wh, err := client.Warehouses.ShowByID(ctx, testWarehouse.ID()) + require.NoError(t, err) + assert.Equal(t, comment, wh.Comment) + }) + + // TODO: uncomment once we can create tables/columns + // t.Run("set column", func(t *testing.T) { + // comment := randomComment(t) + // err := client.Comments.SetColumn(ctx, &SetColumnCommentOpts{ + // Column: testWarehouse.ID(), + // Value: String(comment), + // }) + // require.NoError(t, err) + // wh, err := client.Warehouses.ShowByID(ctx, testWarehouse.ID()) + // require.NoError(t, err) + // assert.Equal(t, comment, wh.Comment) + // }) +} diff --git a/pkg/sdk/comments_test.go b/pkg/sdk/comments_test.go new file mode 100644 index 0000000000..45a15a3cfe --- /dev/null +++ b/pkg/sdk/comments_test.go @@ -0,0 +1,49 @@ +package sdk + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/stretchr/testify/require" +) + +func TestComments(t *testing.T) { + t.Run("set on schema", func(t *testing.T) { + id := NewSchemaIdentifier("db1", "schema2") + opts := &SetCommentOpts{ + ObjectType: ObjectTypeSchema, + ObjectName: &id, + Value: String("mycomment"), + } + actual, err := structToSQL(opts) + require.NoError(t, err) + expected := `COMMENT ON SCHEMA "db1"."schema2" IS 'mycomment'` + assert.Equal(t, expected, actual) + }) + + t.Run("set if exists", func(t *testing.T) { + id := NewAccountObjectIdentifier("maskpol") + opts := &SetCommentOpts{ + IfExists: Bool(true), + ObjectType: ObjectTypeMaskingPolicy, + ObjectName: &id, + Value: String("mycomment2"), + } + actual, err := structToSQL(opts) + require.NoError(t, err) + expected := `COMMENT IF EXISTS ON MASKING POLICY "maskpol" IS 'mycomment2'` + assert.Equal(t, expected, actual) + }) + + t.Run("set column comment", func(t *testing.T) { + opts := &SetColumnCommentOpts{ + Column: NewSchemaIdentifier("table3", "column4"), + Value: String("mycomment3"), + } + actual, err := structToSQL(opts) + require.NoError(t, err) + expected := `COMMENT ON COLUMN "table3"."column4" IS 'mycomment3'` + assert.Equal(t, expected, actual) + }) +} diff --git a/pkg/sdk/context_functions.go b/pkg/sdk/context_functions.go index 83799a4edc..f4f79e768c 100644 --- a/pkg/sdk/context_functions.go +++ b/pkg/sdk/context_functions.go @@ -8,6 +8,7 @@ import ( type ContextFunctions interface { // Session functions. CurrentAccount(ctx context.Context) (string, error) + CurrentRegion(ctx context.Context) (string, error) CurrentSession(ctx context.Context) (string, error) // Session Object functions. @@ -34,6 +35,17 @@ func (c *contextFunctions) CurrentAccount(ctx context.Context) (string, error) { return s.CurrentAccount, nil } +func (c *contextFunctions) CurrentRegion(ctx context.Context) (string, error) { + s := &struct { + CurrentRegion string `db:"CURRENT_REGION"` + }{} + err := c.client.queryOne(ctx, s, "SELECT CURRENT_REGION() AS CURRENT_REGION") + if err != nil { + return "", err + } + return s.CurrentRegion, nil +} + func (c *contextFunctions) CurrentSession(ctx context.Context) (string, error) { s := &struct { CurrentSession string `db:"CURRENT_SESSION"` diff --git a/pkg/sdk/context_functions_integration_test.go b/pkg/sdk/context_functions_integration_test.go index e2b1ba85dd..d24609cd7f 100644 --- a/pkg/sdk/context_functions_integration_test.go +++ b/pkg/sdk/context_functions_integration_test.go @@ -17,6 +17,15 @@ func TestInt_CurrentAccount(t *testing.T) { assert.NotEmpty(t, account) } +func TestInt_CurrentRegion(t *testing.T) { + client := testClient(t) + ctx := context.Background() + + region, err := client.ContextFunctions.CurrentRegion(ctx) + require.NoError(t, err) + assert.NotEmpty(t, region) +} + func TestInt_CurrentSession(t *testing.T) { client := testClient(t) ctx := context.Background() diff --git a/pkg/sdk/object_types.go b/pkg/sdk/object_types.go index 0a226c2c37..5dadc83731 100644 --- a/pkg/sdk/object_types.go +++ b/pkg/sdk/object_types.go @@ -16,6 +16,7 @@ type Object struct { type ObjectType string const ( + ObjectTypeAccount ObjectType = "ACCOUNT" ObjectTypeAccountParameter ObjectType = "ACCOUNT PARAMETER" ObjectTypeDatabase ObjectType = "DATABASE" ObjectTypeFailoverGroup ObjectType = "FAILOVER GROUP"