From 7060e2d1669d35a8cf78a2e12d99d2dde5c01d96 Mon Sep 17 00:00:00 2001 From: Scott Winkler Date: Thu, 28 Sep 2023 15:53:06 -0700 Subject: [PATCH] tags sdk refactoring --- pkg/sdk/client.go | 10 +- pkg/sdk/helper_test.go | 5 +- pkg/sdk/tags.go | 148 +++++++++++++++- pkg/sdk/tags_dto.go | 34 ++++ pkg/sdk/tags_dto_builders.go | 69 ++++++++ pkg/sdk/tags_impl.go | 84 +++++++++ pkg/sdk/tags_integration_test.go | 81 +++++++++ pkg/sdk/tags_test.go | 286 +++++++++++++++++++++++++++++++ pkg/sdk/tags_validations.go | 79 +++++++++ 9 files changed, 783 insertions(+), 13 deletions(-) create mode 100644 pkg/sdk/tags_dto.go create mode 100644 pkg/sdk/tags_dto_builders.go create mode 100644 pkg/sdk/tags_impl.go create mode 100644 pkg/sdk/tags_integration_test.go create mode 100644 pkg/sdk/tags_test.go create mode 100644 pkg/sdk/tags_validations.go diff --git a/pkg/sdk/client.go b/pkg/sdk/client.go index 37ea9ef7a4..f6cb4122e1 100644 --- a/pkg/sdk/client.go +++ b/pkg/sdk/client.go @@ -29,8 +29,8 @@ type Client struct { Accounts Accounts Alerts Alerts Comments Comments - Databases Databases DatabaseRoles DatabaseRoles + Databases Databases ExternalTables ExternalTables FailoverGroups FailoverGroups FileFormats FileFormats @@ -42,11 +42,12 @@ type Client struct { Pipes Pipes ResourceMonitors ResourceMonitors Roles Roles + Schemas Schemas SessionPolicies SessionPolicies Sessions Sessions Shares Shares + Tags Tags Users Users - Schemas Schemas Warehouses Warehouses } @@ -128,8 +129,8 @@ func (c *Client) initialize() { c.Comments = &comments{client: c} c.ContextFunctions = &contextFunctions{client: c} c.ConversionFunctions = &conversionFunctions{client: c} - c.Databases = &databases{client: c} c.DatabaseRoles = &databaseRoles{client: c} + c.Databases = &databases{client: c} c.ExternalTables = &externalTables{client: c} c.FailoverGroups = &failoverGroups{client: c} c.FileFormats = &fileFormats{client: c} @@ -142,11 +143,12 @@ func (c *Client) initialize() { c.ReplicationFunctions = &replicationFunctions{client: c} c.ResourceMonitors = &resourceMonitors{client: c} c.Roles = &roles{client: c} + c.Schemas = &schemas{client: c} c.SessionPolicies = &sessionPolicies{client: c} c.Sessions = &sessions{client: c} c.Shares = &shares{client: c} - c.Schemas = &schemas{client: c} c.SystemFunctions = &systemFunctions{client: c} + c.Tags = &tags{client: c} c.Users = &users{client: c} c.Warehouses = &warehouses{client: c} } diff --git a/pkg/sdk/helper_test.go b/pkg/sdk/helper_test.go index 8205696c48..0aea02a17c 100644 --- a/pkg/sdk/helper_test.go +++ b/pkg/sdk/helper_test.go @@ -388,13 +388,14 @@ func createTable(t *testing.T, client *Client, database *Database, schema *Schem func createTag(t *testing.T, client *Client, database *Database, schema *Schema) (*Tag, func()) { t.Helper() - return createTagWithOptions(t, client, database, schema, &TagCreateOptions{}) + return createTagWithOptions(t, client, database, schema) } -func createTagWithOptions(t *testing.T, client *Client, database *Database, schema *Schema, _ *TagCreateOptions) (*Tag, func()) { +func createTagWithOptions(t *testing.T, client *Client, database *Database, schema *Schema) (*Tag, func()) { t.Helper() name := randomStringRange(t, 8, 28) ctx := context.Background() + _, err := client.exec(ctx, fmt.Sprintf("CREATE TAG \"%s\".\"%s\".\"%s\"", database.Name, schema.Name, name)) require.NoError(t, err) return &Tag{ diff --git a/pkg/sdk/tags.go b/pkg/sdk/tags.go index 01149d93c1..9c0e7378f1 100644 --- a/pkg/sdk/tags.go +++ b/pkg/sdk/tags.go @@ -1,18 +1,152 @@ package sdk -// placeholder for the real implementation. -type TagCreateOptions struct{} +import ( + "context" + "database/sql" + "strings" + "time" +) + +type Tags interface { + Create(ctx context.Context, request *CreateTagRequest) error + Show(ctx context.Context, opts *ShowTagRequest) ([]Tag, error) + ShowByID(ctx context.Context, id AccountObjectIdentifier) (*Tag, error) + Drop(ctx context.Context, request *DropTagRequest) error + Undrop(ctx context.Context, request *UndropTagRequest) error +} + +// createTagOptions is based on https://docs.snowflake.com/en/sql-reference/sql/create-tag +type createTagOptions struct { + create bool `ddl:"static" sql:"CREATE"` + OrReplace *bool `ddl:"keyword" sql:"OR REPLACE"` + tag string `ddl:"static" sql:"TAG"` + IfNotExists *bool `ddl:"keyword" sql:"IF NOT EXISTS"` + name AccountObjectIdentifier `ddl:"identifier"` + Comment *string `ddl:"parameter,single_quotes" sql:"COMMENT"` + AllowedValues *AllowedValues `ddl:"keyword" sql:"ALLOWED_VALUES"` +} + +type AllowedValues struct { + Values []AllowedValue `ddl:"list,comma"` +} + +type AllowedValue struct { + Value string `ddl:"keyword,single_quotes"` +} + +// showTagOptions is based on https://docs.snowflake.com/en/sql-reference/sql/show-tags +type showTagOptions struct { + show bool `ddl:"static" sql:"SHOW"` + tag bool `ddl:"static" sql:"TAGS"` + Like *Like `ddl:"keyword" sql:"LIKE"` + In *In `ddl:"keyword" sql:"IN"` +} type Tag struct { - DatabaseName string - SchemaName string - Name string + CreatedOn time.Time + Name string + DatabaseName string + SchemaName string + Owner string + Comment string + AllowedValues []string + OwnerRole string } func (v *Tag) ID() SchemaObjectIdentifier { return NewSchemaObjectIdentifier(v.DatabaseName, v.SchemaName, v.Name) } -func (v *Tag) ObjectType() ObjectType { - return ObjectTypeTag +type tagRow struct { + CreatedOn time.Time `db:"created_on"` + Name string `db:"name"` + DatabaseName string `db:"database_name"` + SchemaName string `db:"schema_name"` + Owner string `db:"owner"` + Comment string `db:"comment"` + AllowedValues sql.NullString `db:"allowed_values"` + OwnerRoleType string `db:"owner_role_type"` +} + +func (tr tagRow) convert() *Tag { + t := &Tag{ + CreatedOn: tr.CreatedOn, + Name: tr.Name, + DatabaseName: tr.DatabaseName, + SchemaName: tr.SchemaName, + Owner: tr.Owner, + Comment: tr.Comment, + OwnerRole: tr.OwnerRoleType, + } + if tr.AllowedValues.Valid { + s := strings.Trim(tr.AllowedValues.String, "[]") // remove brackets + items := strings.Split(s, ",") + values := make([]string, len(items)) + for i, item := range items { + values[i] = strings.Trim(item, `"`) // remove quotes + } + t.AllowedValues = values + } + return t +} + +type TagSetMaskingPolicies struct { + MaskingPolicies []TagMaskingPolicy `ddl:"list,comma"` + Force *bool `ddl:"keyword" sql:"FORCE"` +} + +type TagUnsetMaskingPolicies struct { + MaskingPolicies []TagMaskingPolicy `ddl:"list,comma"` +} + +type TagMaskingPolicy struct { + Name string `ddl:"parameter,no_equals" sql:"MASKING POLICY"` +} + +type TagSet struct { + MaskingPolicies *TagSetMaskingPolicies `ddl:"keyword"` + Comment *string `ddl:"parameter,single_quotes" sql:"COMMENT"` +} + +type TagUnset struct { + MaskingPolicies *TagUnsetMaskingPolicies `ddl:"keyword"` + AllowedValues *bool `ddl:"keyword" sql:"ALLOWED_VALUES"` + Comment *bool `ddl:"keyword" sql:"COMMENT"` +} + +type TagAdd struct { + AllowedValues *AllowedValues `ddl:"keyword" sql:"ALLOWED_VALUES"` +} + +type TagDrop struct { + AllowedValues *AllowedValues `ddl:"keyword" sql:"ALLOWED_VALUES"` +} + +// alterTagOptions is based on https://docs.snowflake.com/en/sql-reference/sql/alter-tag +type alterTagOptions struct { + alter bool `ddl:"static" sql:"ALTER"` + tag string `ddl:"static" sql:"TAG"` + name AccountObjectIdentifier `ddl:"identifier"` + + // One of + Add *TagAdd `ddl:"keyword" sql:"ADD"` + Drop *TagDrop `ddl:"keyword" sql:"DROP"` + Set *TagSet `ddl:"keyword" sql:"SET"` + Unset *TagUnset `ddl:"keyword" sql:"UNSET"` + RenameTo *string `ddl:"parameter,no_equals" sql:"RENAME TO"` +} + +// dropTagOptions is based on https://docs.snowflake.com/en/sql-reference/sql/drop-tag +type dropTagOptions struct { + drop bool `ddl:"static" sql:"DROP"` + tag string `ddl:"static" sql:"TAG"` + IfNotExists *bool `ddl:"keyword" sql:"IF NOT EXISTS"` + name AccountObjectIdentifier `ddl:"identifier"` +} + +// undropTagOptions is based on https://docs.snowflake.com/en/sql-reference/sql/undrop-tag +type undropTagOptions struct { + undrop bool `ddl:"static" sql:"UNDROP"` + tag string `ddl:"static" sql:"TAG"` + name AccountObjectIdentifier `ddl:"identifier"` } diff --git a/pkg/sdk/tags_dto.go b/pkg/sdk/tags_dto.go new file mode 100644 index 0000000000..3c34efce98 --- /dev/null +++ b/pkg/sdk/tags_dto.go @@ -0,0 +1,34 @@ +package sdk + +var ( + _ optionsProvider[createTagOptions] = new(CreateTagRequest) + _ optionsProvider[showTagOptions] = new(ShowTagRequest) + _ optionsProvider[dropTagOptions] = new(DropTagRequest) + _ optionsProvider[undropTagOptions] = new(UndropTagRequest) +) + +type CreateTagRequest struct { + orReplace bool + ifNotExists bool + + name AccountObjectIdentifier // required + + // One of + comment *string + allowedValues *AllowedValues +} + +type ShowTagRequest struct { + like *Like + in *In +} + +type DropTagRequest struct { + ifNotExists bool + + name AccountObjectIdentifier // required +} + +type UndropTagRequest struct { + name AccountObjectIdentifier // required +} diff --git a/pkg/sdk/tags_dto_builders.go b/pkg/sdk/tags_dto_builders.go new file mode 100644 index 0000000000..144e7c51d8 --- /dev/null +++ b/pkg/sdk/tags_dto_builders.go @@ -0,0 +1,69 @@ +package sdk + +func NewCreateTagRequest(name AccountObjectIdentifier) *CreateTagRequest { + s := CreateTagRequest{} + s.name = name + return &s +} + +func (s *CreateTagRequest) WithOrReplace(orReplace bool) *CreateTagRequest { + s.orReplace = orReplace + return s +} + +func (s *CreateTagRequest) WithIfNotExists(ifNotExists bool) *CreateTagRequest { + s.ifNotExists = ifNotExists + return s +} + +func (s *CreateTagRequest) WithComment(comment *string) *CreateTagRequest { + s.comment = comment + return s +} + +func (s *CreateTagRequest) WithAllowedValues(values []string) *CreateTagRequest { + if len(values) > 0 { + s.allowedValues = &AllowedValues{ + Values: make([]AllowedValue, 0, len(values)), + } + for _, value := range values { + s.allowedValues.Values = append(s.allowedValues.Values, AllowedValue{ + Value: value, + }) + } + } + return s +} + +func NewShowTagRequest() *ShowTagRequest { + return &ShowTagRequest{} +} + +func (s *ShowTagRequest) WithLike(pattern string) *ShowTagRequest { + s.like = &Like{ + Pattern: String(pattern), + } + return s +} + +func (s *ShowTagRequest) WithIn(in *In) *ShowTagRequest { + s.in = in + return s +} + +func NewDropTagRequest(name AccountObjectIdentifier) *DropTagRequest { + s := DropTagRequest{} + s.name = name + return &s +} + +func (s *DropTagRequest) WithIfNotExists(ifNotExists bool) *DropTagRequest { + s.ifNotExists = ifNotExists + return s +} + +func NewUndropTagRequest(name AccountObjectIdentifier) *UndropTagRequest { + s := UndropTagRequest{} + s.name = name + return &s +} diff --git a/pkg/sdk/tags_impl.go b/pkg/sdk/tags_impl.go new file mode 100644 index 0000000000..318c0f05e2 --- /dev/null +++ b/pkg/sdk/tags_impl.go @@ -0,0 +1,84 @@ +package sdk + +import "context" + +var _ Tags = (*tags)(nil) + +type tags struct { + client *Client +} + +func (v *tags) Create(ctx context.Context, request *CreateTagRequest) error { + opts := request.toOpts() + return validateAndExec(v.client, ctx, opts) +} + +func (v *tags) Show(ctx context.Context, request *ShowTagRequest) ([]Tag, error) { + opts := request.toOpts() + rows, err := validateAndQuery[tagRow](v.client, ctx, opts) + if err != nil { + return nil, err + } + result := convertRows[tagRow, Tag](rows) + return result, nil +} + +func (v *tags) ShowByID(ctx context.Context, id AccountObjectIdentifier) (*Tag, error) { + request := NewShowTagRequest().WithLike(id.Name()) + tags, err := v.Show(ctx, request) + if err != nil { + return nil, err + } + return findOne(tags, func(r Tag) bool { return r.Name == id.Name() }) +} + +func (v *tags) Drop(ctx context.Context, request *DropTagRequest) error { + opts := request.toOpts() + return validateAndExec(v.client, ctx, opts) +} + +func (v *tags) Undrop(ctx context.Context, request *UndropTagRequest) error { + opts := request.toOpts() + return validateAndExec(v.client, ctx, opts) +} + +func (s *CreateTagRequest) toOpts() *createTagOptions { + opts := createTagOptions{ + OrReplace: Bool(s.orReplace), + IfNotExists: Bool(s.ifNotExists), + name: s.name, + } + if s.comment != nil { + opts.Comment = s.comment + } + if s.allowedValues != nil { + opts.AllowedValues = s.allowedValues + } + return &opts +} + +func (s *ShowTagRequest) toOpts() *showTagOptions { + opts := showTagOptions{} + if s.like != nil { + opts.Like = s.like + } + if s.in != nil { + opts.In = s.in + } + return &opts +} + +func (s *DropTagRequest) toOpts() *dropTagOptions { + opts := dropTagOptions{ + IfNotExists: Bool(s.ifNotExists), + name: s.name, + } + return &opts +} + +func (s *UndropTagRequest) toOpts() *undropTagOptions { + opts := undropTagOptions{ + name: s.name, + } + return &opts +} diff --git a/pkg/sdk/tags_integration_test.go b/pkg/sdk/tags_integration_test.go new file mode 100644 index 0000000000..c2629f0caa --- /dev/null +++ b/pkg/sdk/tags_integration_test.go @@ -0,0 +1,81 @@ +package sdk + +import ( + "context" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestInt_TagCreate(t *testing.T) { + client := testClient(t) + + databaseTest, databaseCleanup := createDatabase(t, client) + t.Cleanup(databaseCleanup) + _, schemaCleanup := createSchema(t, client, databaseTest) + t.Cleanup(schemaCleanup) + + ctx := context.Background() + t.Run("create with comment", func(t *testing.T) { + name := randomAccountObjectIdentifier(t) + comment := randomComment(t) + err := client.Tags.Create(ctx, NewCreateTagRequest(name).WithOrReplace(true).WithComment(&comment)) + require.NoError(t, err) + t.Cleanup(func() { + err = client.Tags.Drop(ctx, NewDropTagRequest(name)) + require.NoError(t, err) + }) + entities, err := client.Tags.Show(ctx, NewShowTagRequest().WithLike(name.Name())) + require.NoError(t, err) + require.Equal(t, 1, len(entities)) + + entity := entities[0] + require.Equal(t, name.Name(), entity.Name) + require.Equal(t, comment, entity.Comment) + }) + + t.Run("create with one allowed value", func(t *testing.T) { + name := randomAccountObjectIdentifier(t) + values := []string{"value1"} + err := client.Tags.Create(ctx, NewCreateTagRequest(name).WithOrReplace(true).WithAllowedValues(values)) + require.NoError(t, err) + t.Cleanup(func() { + err = client.Tags.Drop(ctx, NewDropTagRequest(name)) + require.NoError(t, err) + }) + entities, err := client.Tags.Show(ctx, NewShowTagRequest().WithLike(name.Name())) + require.NoError(t, err) + require.Equal(t, 1, len(entities)) + + entity := entities[0] + require.Equal(t, name.Name(), entity.Name) + require.Equal(t, values, entity.AllowedValues) + }) + + t.Run("create with two allowed values", func(t *testing.T) { + name := randomAccountObjectIdentifier(t) + values := []string{"value1", "value2"} + err := client.Tags.Create(ctx, NewCreateTagRequest(name).WithOrReplace(true).WithAllowedValues(values)) + require.NoError(t, err) + t.Cleanup(func() { + err = client.Tags.Drop(ctx, NewDropTagRequest(name)) + require.NoError(t, err) + }) + entities, err := client.Tags.Show(ctx, NewShowTagRequest().WithLike(name.Name())) + require.NoError(t, err) + require.Equal(t, 1, len(entities)) + + entity := entities[0] + require.Equal(t, name.Name(), entity.Name) + require.Equal(t, values, entity.AllowedValues) + }) + + t.Run("create with comment and allowed values", func(t *testing.T) { + name := randomAccountObjectIdentifier(t) + comment := randomComment(t) + values := []string{"value1"} + err := client.Tags.Create(ctx, NewCreateTagRequest(name).WithOrReplace(true).WithComment(&comment).WithAllowedValues(values)) + expected := "fields [Comment AllowedValues] are incompatible and cannot be set at once" + require.Equal(t, expected, err.Error()) + }) +} diff --git a/pkg/sdk/tags_test.go b/pkg/sdk/tags_test.go new file mode 100644 index 0000000000..d96923d151 --- /dev/null +++ b/pkg/sdk/tags_test.go @@ -0,0 +1,286 @@ +package sdk + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestTagCreate(t *testing.T) { + t.Run("create with allowed values", func(t *testing.T) { + opts := &createTagOptions{ + OrReplace: Bool(true), + name: AccountObjectIdentifier{ + name: "tag", + }, + AllowedValues: &AllowedValues{ + Values: []AllowedValue{ + { + Value: "value1", + }, + { + Value: "value2", + }, + }, + }, + } + actual, err := structToSQL(opts) + require.NoError(t, err) + expected := `CREATE OR REPLACE TAG "tag" ALLOWED_VALUES 'value1', 'value2'` + assert.Equal(t, expected, actual) + }) + + t.Run("create with comment", func(t *testing.T) { + opts := &createTagOptions{ + OrReplace: Bool(true), + name: AccountObjectIdentifier{ + name: "tag", + }, + Comment: String("comment"), + } + actual, err := structToSQL(opts) + require.NoError(t, err) + expected := `CREATE OR REPLACE TAG "tag" COMMENT = 'comment'` + assert.Equal(t, expected, actual) + }) + + t.Run("create with not exists", func(t *testing.T) { + opts := &createTagOptions{ + IfNotExists: Bool(true), + name: AccountObjectIdentifier{ + name: "tag", + }, + Comment: String("comment"), + } + actual, err := structToSQL(opts) + require.NoError(t, err) + expected := `CREATE TAG IF NOT EXISTS "tag" COMMENT = 'comment'` + assert.Equal(t, expected, actual) + }) +} + +func TestTagDrop(t *testing.T) { + t.Run("drop with name", func(t *testing.T) { + opts := &dropTagOptions{ + name: NewAccountObjectIdentifier("test"), + } + actual, err := structToSQL(opts) + require.NoError(t, err) + expected := `DROP TAG "test"` + assert.Equal(t, expected, actual) + }) +} + +func TestTagUndrop(t *testing.T) { + t.Run("undrop with name", func(t *testing.T) { + opts := &undropTagOptions{ + name: NewAccountObjectIdentifier("test"), + } + actual, err := structToSQL(opts) + require.NoError(t, err) + expected := `UNDROP TAG "test"` + assert.Equal(t, expected, actual) + }) +} + +func TestTagShow(t *testing.T) { + t.Run("show with empty options", func(t *testing.T) { + opts := &showTagOptions{} + actual, err := structToSQL(opts) + require.NoError(t, err) + expected := `SHOW TAGS` + assert.Equal(t, expected, actual) + }) + + t.Run("show with like", func(t *testing.T) { + opts := &showTagOptions{ + Like: &Like{ + Pattern: String("test"), + }, + } + actual, err := structToSQL(opts) + require.NoError(t, err) + expected := `SHOW TAGS LIKE 'test'` + assert.Equal(t, expected, actual) + }) +} + +func TestTagAlter(t *testing.T) { + t.Run("alter with rename to", func(t *testing.T) { + opts := &alterTagOptions{ + name: NewAccountObjectIdentifier("test"), + RenameTo: String("test2"), + } + actual, err := structToSQL(opts) + require.NoError(t, err) + expected := `ALTER TAG "test" RENAME TO test2` + assert.Equal(t, expected, actual) + }) + + t.Run("alter with add", func(t *testing.T) { + opts := &alterTagOptions{ + name: NewAccountObjectIdentifier("test"), + Add: &TagAdd{ + AllowedValues: &AllowedValues{ + Values: []AllowedValue{ + { + Value: "value1", + }, + { + Value: "value2", + }, + }, + }, + }, + } + actual, err := structToSQL(opts) + require.NoError(t, err) + expected := `ALTER TAG "test" ADD ALLOWED_VALUES 'value1', 'value2'` + assert.Equal(t, expected, actual) + }) + + t.Run("alter with drop", func(t *testing.T) { + opts := &alterTagOptions{ + name: NewAccountObjectIdentifier("test"), + Drop: &TagDrop{ + AllowedValues: &AllowedValues{ + Values: []AllowedValue{ + { + Value: "value1", + }, + { + Value: "value2", + }, + }, + }, + }, + } + actual, err := structToSQL(opts) + require.NoError(t, err) + expected := `ALTER TAG "test" DROP ALLOWED_VALUES 'value1', 'value2'` + assert.Equal(t, expected, actual) + }) + + t.Run("alter with unset allowed values", func(t *testing.T) { + opts := &alterTagOptions{ + name: NewAccountObjectIdentifier("test"), + Unset: &TagUnset{ + AllowedValues: Bool(true), + }, + } + actual, err := structToSQL(opts) + require.NoError(t, err) + expected := `ALTER TAG "test" UNSET ALLOWED_VALUES` + assert.Equal(t, expected, actual) + }) + + t.Run("alter with set masking policy", func(t *testing.T) { + opts := &alterTagOptions{ + name: NewAccountObjectIdentifier("test"), + Set: &TagSet{ + MaskingPolicies: &TagSetMaskingPolicies{ + MaskingPolicies: []TagMaskingPolicy{ + { + Name: "policy1", + }, + }, + }, + }, + } + actual, err := structToSQL(opts) + require.NoError(t, err) + expected := `ALTER TAG "test" SET MASKING POLICY policy1` + assert.Equal(t, expected, actual) + }) + + t.Run("alter with set masking policies", func(t *testing.T) { + opts := &alterTagOptions{ + name: NewAccountObjectIdentifier("test"), + Set: &TagSet{ + MaskingPolicies: &TagSetMaskingPolicies{ + MaskingPolicies: []TagMaskingPolicy{ + { + Name: "policy1", + }, + { + Name: "policy2", + }, + }, + Force: Bool(true), + }, + }, + } + actual, err := structToSQL(opts) + require.NoError(t, err) + expected := `ALTER TAG "test" SET MASKING POLICY policy1, MASKING POLICY policy2 FORCE` + assert.Equal(t, expected, actual) + }) + + t.Run("alter with unset masking policy", func(t *testing.T) { + opts := &alterTagOptions{ + name: NewAccountObjectIdentifier("test"), + Unset: &TagUnset{ + MaskingPolicies: &TagUnsetMaskingPolicies{ + MaskingPolicies: []TagMaskingPolicy{ + { + Name: "policy1", + }, + }, + }, + }, + } + actual, err := structToSQL(opts) + require.NoError(t, err) + expected := `ALTER TAG "test" UNSET MASKING POLICY policy1` + assert.Equal(t, expected, actual) + }) + + t.Run("alter with unset masking policies", func(t *testing.T) { + opts := &alterTagOptions{ + name: NewAccountObjectIdentifier("test"), + Unset: &TagUnset{ + MaskingPolicies: &TagUnsetMaskingPolicies{ + MaskingPolicies: []TagMaskingPolicy{ + { + Name: "policy1", + }, + { + Name: "policy2", + }, + }, + }, + }, + } + actual, err := structToSQL(opts) + require.NoError(t, err) + expected := `ALTER TAG "test" UNSET MASKING POLICY policy1, MASKING POLICY policy2` + assert.Equal(t, expected, actual) + }) + + t.Run("alter with set comment", func(t *testing.T) { + opts := &alterTagOptions{ + name: NewAccountObjectIdentifier("test"), + Set: &TagSet{ + Comment: String("comment"), + }, + } + actual, err := structToSQL(opts) + require.NoError(t, err) + expected := `ALTER TAG "test" SET COMMENT = 'comment'` + assert.Equal(t, expected, actual) + }) + + t.Run("alter with unset comment", func(t *testing.T) { + opts := &alterTagOptions{ + name: NewAccountObjectIdentifier("test"), + Unset: &TagUnset{ + Comment: Bool(true), + }, + } + actual, err := structToSQL(opts) + require.NoError(t, err) + expected := `ALTER TAG "test" UNSET COMMENT` + assert.Equal(t, expected, actual) + }) +} diff --git a/pkg/sdk/tags_validations.go b/pkg/sdk/tags_validations.go new file mode 100644 index 0000000000..f0243a0c51 --- /dev/null +++ b/pkg/sdk/tags_validations.go @@ -0,0 +1,79 @@ +package sdk + +import ( + "errors" + "fmt" +) + +var ( + _ validatable = new(createTagOptions) + _ validatable = new(dropTagOptions) + _ validatable = new(showTagOptions) + _ validatable = new(undropTagOptions) + _ validatable = new(AllowedValues) +) + +func (opts *createTagOptions) validate() error { + if opts == nil { + return errors.Join(errNilOptions) + } + var errs []error + if !validObjectidentifier(opts.name) { + errs = append(errs, errInvalidObjectIdentifier) + } + if everyValueSet(opts.OrReplace, opts.IfNotExists) && *opts.OrReplace && *opts.IfNotExists { + errs = append(errs, errOneOf("OrReplace", "IfNotExists")) + } + if valueSet(opts.Comment) && valueSet(opts.AllowedValues) { + errs = append(errs, errOneOf("Comment", "AllowedValues")) + } + if valueSet(opts.AllowedValues) { + if err := opts.AllowedValues.validate(); err != nil { + errs = append(errs, err) + } + } + return errors.Join(errs...) +} + +func (v *AllowedValues) validate() error { + if ok := validateIntInRange(len(v.Values), 1, 50); !ok { + return fmt.Errorf("Number of the AllowedValues must be between 1 and 50") + } + return nil +} + +func (opts *showTagOptions) validate() error { + if opts == nil { + return errors.Join(errNilOptions) + } + var errs []error + if valueSet(opts.Like) && !valueSet(opts.Like.Pattern) { + errs = append(errs, errPatternRequiredForLikeKeyword) + } + if valueSet(opts.In) && !exactlyOneValueSet(opts.In.Account, opts.In.Database, opts.In.Schema) { + errs = append(errs, errScopeRequiredForInKeyword) + } + return errors.Join(errs...) +} + +func (opts *dropTagOptions) validate() error { + if opts == nil { + return errors.Join(errNilOptions) + } + var errs []error + if !validObjectidentifier(opts.name) { + errs = append(errs, errInvalidObjectIdentifier) + } + return errors.Join(errs...) +} + +func (opts *undropTagOptions) validate() error { + if opts == nil { + return errors.Join(errNilOptions) + } + var errs []error + if !validObjectidentifier(opts.name) { + errs = append(errs, errInvalidObjectIdentifier) + } + return errors.Join(errs...) +}