diff --git a/docs/resources/table.md b/docs/resources/table.md index b78561a146..c137d245c9 100644 --- a/docs/resources/table.md +++ b/docs/resources/table.md @@ -59,6 +59,7 @@ resource "snowflake_table" "table" { name = "data" type = "text" nullable = false + collate = "en-ci" } column { @@ -115,6 +116,7 @@ Required: Optional: +- `collate` (String) Column collation, e.g. utf8 - `comment` (String) Column comment - `default` (Block List, Max: 1) Defines the column default value; note due to limitations of Snowflake's ALTER TABLE ADD/MODIFY COLUMN updates to default will not be applied (see [below for nested schema](#nestedblock--column--default)) - `identity` (Block List, Max: 1) Defines the identity start/step values for a column. **Note** Identity/default are mutually exclusive. (see [below for nested schema](#nestedblock--column--identity)) diff --git a/examples/resources/snowflake_table/resource.tf b/examples/resources/snowflake_table/resource.tf index 87c91b8ebf..d9bc531170 100644 --- a/examples/resources/snowflake_table/resource.tf +++ b/examples/resources/snowflake_table/resource.tf @@ -44,6 +44,7 @@ resource "snowflake_table" "table" { name = "data" type = "text" nullable = false + collate = "en-ci" } column { diff --git a/pkg/resources/table.go b/pkg/resources/table.go index 28daffd360..ae0b67da63 100644 --- a/pkg/resources/table.go +++ b/pkg/resources/table.go @@ -139,6 +139,12 @@ var tableSchema = map[string]*schema.Schema{ Default: "", Description: "Masking policy to apply on column. It has to be a fully qualified name.", }, + "collate": { + Type: schema.TypeString, + Optional: true, + Default: "", + Description: "Column collation, e.g. utf8", + }, }, }, }, @@ -254,6 +260,7 @@ type column struct { identity *columnIdentity comment string maskingPolicy string + collate string } type columns []column @@ -267,13 +274,14 @@ type changedColumn struct { dropedDefault bool changedComment bool changedMaskingPolicy bool + changedCollate bool } func (c columns) getChangedColumnProperties(new columns) (changed changedColumns) { changed = changedColumns{} for _, cO := range c { for _, cN := range new { - changeColumn := changedColumn{cN, false, false, false, false, false} + changeColumn := changedColumn{cN, false, false, false, false, false, false} if cO.name == cN.name && cO.dataType != cN.dataType { changeColumn.changedDataType = true } @@ -292,6 +300,10 @@ func (c columns) getChangedColumnProperties(new columns) (changed changedColumns changeColumn.changedMaskingPolicy = true } + if cO.name == cN.name && cO.collate != cN.collate { + changeColumn.changedCollate = true + } + changed = append(changed, changeColumn) } } @@ -362,6 +374,7 @@ func getColumn(from interface{}) (to column) { _default: cd, identity: id, comment: c["comment"].(string), + collate: c["collate"].(string), maskingPolicy: c["masking_policy"].(string), } } @@ -387,7 +400,7 @@ func getTableColumnRequest(from interface{}) *sdk.TableColumnRequest { if len(_default) == 1 { if c, ok := _default[0].(map[string]interface{})["constant"]; ok { if constant, ok := c.(string); ok && len(constant) > 0 { - if strings.Contains(_type, "CHAR") || _type == "STRING" || _type == "TEXT" { + if sdk.IsStringType(_type) { expression = snowflake.EscapeSnowflakeString(constant) } else { expression = constant @@ -422,6 +435,10 @@ func getTableColumnRequest(from interface{}) *sdk.TableColumnRequest { request.WithMaskingPolicy(sdk.NewColumnMaskingPolicyRequest(sdk.NewSchemaObjectIdentifierFromFullyQualifiedName(maskingPolicy))) } + if sdk.IsStringType(_type) { + request.WithCollate(sdk.String(c["collate"].(string))) + } + return request. WithNotNull(sdk.Bool(!c["nullable"].(bool))). WithComment(sdk.String(c["comment"].(string))) @@ -469,6 +486,10 @@ func toColumnConfig(descriptions []sdk.TableColumnDetails) []any { flat["comment"] = *td.Comment } + if td.Collation != nil { + flat["collate"] = *td.Collation + } + if td.PolicyName != nil { // TODO [SNOW-867240]: SHOW TABLE returns last part of id without double quotes... we have to quote it again. Move it to SDK. flat["masking_policy"] = sdk.NewSchemaObjectIdentifierFromFullyQualifiedName(*td.PolicyName).FullyQualifiedName() @@ -507,8 +528,7 @@ func toColumnDefaultConfig(td sdk.TableColumnDetails) map[string]any { return def } - columnType := strings.ToUpper(string(td.Type)) - if strings.Contains(columnType, "CHAR") || columnType == "STRING" || columnType == "TEXT" { + if sdk.IsStringType(string(td.Type)) { def["constant"] = snowflake.UnescapeSnowflakeString(defaultRaw) return def } @@ -765,7 +785,7 @@ func UpdateTable(d *schema.ResourceData, meta interface{}) error { return fmt.Errorf("failed to add column %v => Only adding a column as a constant is supported by Snowflake", cA.name) } var expression string - if strings.Contains(cA.dataType, "CHAR") || cA.dataType == "STRING" || cA.dataType == "TEXT" { + if sdk.IsStringType(cA.dataType) { expression = snowflake.EscapeSnowflakeString(*cA._default.constant) } else { expression = *cA._default.constant @@ -785,14 +805,18 @@ func UpdateTable(d *schema.ResourceData, meta interface{}) error { addRequest.WithComment(sdk.String(cA.comment)) } + if cA.collate != "" && sdk.IsStringType(cA.dataType) { + addRequest.WithCollate(sdk.String(cA.collate)) + } + err := client.Tables.Alter(ctx, sdk.NewAlterTableRequest(id).WithColumnAction(sdk.NewTableColumnActionRequest().WithAdd(addRequest))) if err != nil { return fmt.Errorf("error adding column: %w", err) } } for _, cA := range changed { - if cA.changedDataType { - err := client.Tables.Alter(ctx, sdk.NewAlterTableRequest(id).WithColumnAction(sdk.NewTableColumnActionRequest().WithAlter([]sdk.TableColumnAlterActionRequest{*sdk.NewTableColumnAlterActionRequest(fmt.Sprintf("\"%s\"", cA.newColumn.name)).WithType(sdk.Pointer(sdk.DataType(cA.newColumn.dataType)))}))) + if cA.changedDataType || cA.changedCollate { + err := client.Tables.Alter(ctx, sdk.NewAlterTableRequest(id).WithColumnAction(sdk.NewTableColumnActionRequest().WithAlter([]sdk.TableColumnAlterActionRequest{*sdk.NewTableColumnAlterActionRequest(fmt.Sprintf("\"%s\"", cA.newColumn.name)).WithType(sdk.Pointer(sdk.DataType(cA.newColumn.dataType))).WithCollate(sdk.String(cA.newColumn.collate))}))) if err != nil { return fmt.Errorf("error changing property on %v: err %w", d.Id(), err) } diff --git a/pkg/resources/table_acceptance_test.go b/pkg/resources/table_acceptance_test.go index cddc2b1dee..5c5c2bd460 100644 --- a/pkg/resources/table_acceptance_test.go +++ b/pkg/resources/table_acceptance_test.go @@ -4,6 +4,7 @@ import ( "context" "database/sql" "fmt" + "regexp" "strings" "testing" @@ -1228,6 +1229,165 @@ resource "snowflake_table" "test_table" { return fmt.Sprintf(s, name, databaseName, schemaName, name, databaseName, schemaName) } +func TestAcc_TableCollate(t *testing.T) { + accName := strings.ToUpper(acctest.RandStringFromCharSet(10, acctest.CharSetAlpha)) + + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: acc.TestAccProtoV6ProviderFactories, + PreCheck: func() { acc.TestAccPreCheck(t) }, + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.RequireAbove(tfversion.Version1_5_0), + }, + CheckDestroy: testAccCheckTableDestroy, + Steps: []resource.TestStep{ + { + Config: tableColumnWithCollate(accName, acc.TestDatabaseName, acc.TestSchemaName), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("snowflake_table.test_table", "name", accName), + resource.TestCheckResourceAttr("snowflake_table.test_table", "database", acc.TestDatabaseName), + resource.TestCheckResourceAttr("snowflake_table.test_table", "schema", acc.TestSchemaName), + resource.TestCheckResourceAttr("snowflake_table.test_table", "comment", "Terraform acceptance test"), + resource.TestCheckResourceAttr("snowflake_table.test_table", "column.#", "3"), + resource.TestCheckResourceAttr("snowflake_table.test_table", "column.0.name", "column1"), + resource.TestCheckResourceAttr("snowflake_table.test_table", "column.0.collate", "en"), + resource.TestCheckResourceAttr("snowflake_table.test_table", "column.1.name", "column2"), + resource.TestCheckResourceAttr("snowflake_table.test_table", "column.1.collate", ""), + resource.TestCheckResourceAttr("snowflake_table.test_table", "column.2.name", "column3"), + resource.TestCheckResourceAttr("snowflake_table.test_table", "column.2.collate", ""), + ), + }, + { + Config: alterTableColumnWithCollate(accName, acc.TestDatabaseName, acc.TestSchemaName), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("snowflake_table.test_table", "name", accName), + resource.TestCheckResourceAttr("snowflake_table.test_table", "database", acc.TestDatabaseName), + resource.TestCheckResourceAttr("snowflake_table.test_table", "schema", acc.TestSchemaName), + resource.TestCheckResourceAttr("snowflake_table.test_table", "comment", "Terraform acceptance test"), + resource.TestCheckResourceAttr("snowflake_table.test_table", "column.#", "4"), + resource.TestCheckResourceAttr("snowflake_table.test_table", "column.0.name", "column1"), + resource.TestCheckResourceAttr("snowflake_table.test_table", "column.0.collate", "en"), + resource.TestCheckResourceAttr("snowflake_table.test_table", "column.1.name", "column2"), + resource.TestCheckResourceAttr("snowflake_table.test_table", "column.1.collate", ""), + resource.TestCheckResourceAttr("snowflake_table.test_table", "column.2.name", "column3"), + resource.TestCheckResourceAttr("snowflake_table.test_table", "column.2.collate", ""), + resource.TestCheckResourceAttr("snowflake_table.test_table", "column.3.name", "column4"), + resource.TestCheckResourceAttr("snowflake_table.test_table", "column.3.collate", "utf8"), + ), + }, + { + Config: alterTableColumnWithIncompatibleCollate(accName, acc.TestDatabaseName, acc.TestSchemaName), + ExpectError: regexp.MustCompile("\"VARCHAR\\(200\\) COLLATE 'fr'\" because they have incompatible collations\\."), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("snowflake_table.test_table", "name", accName), + resource.TestCheckResourceAttr("snowflake_table.test_table", "database", acc.TestDatabaseName), + resource.TestCheckResourceAttr("snowflake_table.test_table", "schema", acc.TestSchemaName), + resource.TestCheckResourceAttr("snowflake_table.test_table", "comment", "Terraform acceptance test"), + resource.TestCheckResourceAttr("snowflake_table.test_table", "column.#", "4"), + resource.TestCheckResourceAttr("snowflake_table.test_table", "column.0.name", "column1"), + resource.TestCheckResourceAttr("snowflake_table.test_table", "column.0.collate", "en"), + resource.TestCheckResourceAttr("snowflake_table.test_table", "column.1.name", "column2"), + resource.TestCheckResourceAttr("snowflake_table.test_table", "column.1.collate", ""), + resource.TestCheckResourceAttr("snowflake_table.test_table", "column.2.name", "column3"), + resource.TestCheckResourceAttr("snowflake_table.test_table", "column.2.collate", ""), + resource.TestCheckResourceAttr("snowflake_table.test_table", "column.3.name", "column4"), + resource.TestCheckResourceAttr("snowflake_table.test_table", "column.3.collate", "utf8"), + ), + }, + }, + }) +} + +func tableColumnWithCollate(name string, databaseName string, schemaName string) string { + s := ` +resource "snowflake_table" "test_table" { + name = "%s" + database = "%s" + schema = "%s" + comment = "Terraform acceptance test" + + column { + name = "column1" + type = "VARCHAR(100)" + collate = "en" + } + column { + name = "column2" + type = "VARCHAR(100)" + collate = "" + } + column { + name = "column3" + type = "VARCHAR(100)" + } +} +` + return fmt.Sprintf(s, name, databaseName, schemaName) +} + +func alterTableColumnWithCollate(name string, databaseName string, schemaName string) string { + s := ` +resource "snowflake_table" "test_table" { + name = "%s" + database = "%s" + schema = "%s" + comment = "Terraform acceptance test" + + column { + name = "column1" + type = "VARCHAR(200)" + collate = "en" + } + column { + name = "column2" + type = "VARCHAR(200)" + collate = "" + } + column { + name = "column3" + type = "VARCHAR(200)" + } + column { + name = "column4" + type = "VARCHAR" + collate = "utf8" + } +} +` + return fmt.Sprintf(s, name, databaseName, schemaName) +} + +func alterTableColumnWithIncompatibleCollate(name string, databaseName string, schemaName string) string { + s := ` +resource "snowflake_table" "test_table" { + name = "%s" + database = "%s" + schema = "%s" + comment = "Terraform acceptance test" + + column { + name = "column1" + type = "VARCHAR(200)" + collate = "fr" + } + column { + name = "column2" + type = "VARCHAR(200)" + collate = "" + } + column { + name = "column3" + type = "VARCHAR(200)" + } + column { + name = "column4" + type = "VARCHAR" + collate = "utf8" + } +} +` + return fmt.Sprintf(s, name, databaseName, schemaName) +} + func TestAcc_TableRename(t *testing.T) { oldTableName := strings.ToUpper(acctest.RandStringFromCharSet(10, acctest.CharSetAlpha)) newTableName := strings.ToUpper(acctest.RandStringFromCharSet(10, acctest.CharSetAlpha)) diff --git a/pkg/sdk/data_types.go b/pkg/sdk/data_types.go index f95cc5afb0..a4af8157ac 100644 --- a/pkg/sdk/data_types.go +++ b/pkg/sdk/data_types.go @@ -91,3 +91,13 @@ func ToDataType(s string) (DataType, error) { return "", fmt.Errorf("invalid data type: %s", s) } + +func IsStringType(_type string) bool { + t := strings.ToUpper(_type) + return strings.HasPrefix(t, "STRING") || + strings.HasPrefix(t, "VARCHAR") || + strings.HasPrefix(t, "CHAR") || + strings.HasPrefix(t, "TEXT") || + strings.HasPrefix(t, "NVARCHAR") || + strings.HasPrefix(t, "NCHAR") +} diff --git a/pkg/sdk/data_types_test.go b/pkg/sdk/data_types_test.go index 21de63b921..7123fd4092 100644 --- a/pkg/sdk/data_types_test.go +++ b/pkg/sdk/data_types_test.go @@ -87,3 +87,52 @@ func TestToDataType(t *testing.T) { }) } } + +func TestIsStringType(t *testing.T) { + type test struct { + input string + want bool + } + + tests := []test{ + // case insensitive. + {input: "STRING", want: true}, + {input: "string", want: true}, + {input: "String", want: true}, + + // varchar types. + {input: "VARCHAR", want: true}, + {input: "NVARCHAR", want: true}, + {input: "NVARCHAR2", want: true}, + {input: "CHAR", want: true}, + {input: "NCHAR", want: true}, + {input: "CHAR VARYING", want: true}, + {input: "NCHAR VARYING", want: true}, + {input: "TEXT", want: true}, + + // with length + {input: "VARCHAR(100)", want: true}, + {input: "NVARCHAR(100)", want: true}, + {input: "NVARCHAR2(100)", want: true}, + {input: "CHAR(100)", want: true}, + {input: "NCHAR(100)", want: true}, + {input: "CHAR VARYING(100)", want: true}, + {input: "NCHAR VARYING(100)", want: true}, + {input: "TEXT(100)", want: true}, + + // binary is not string types. + {input: "binary", want: false}, + {input: "varbinary", want: false}, + + // other types + {input: "boolean", want: false}, + {input: "number", want: false}, + } + + for _, tc := range tests { + t.Run(tc.input, func(t *testing.T) { + got := IsStringType(tc.input) + require.Equal(t, tc.want, got) + }) + } +} diff --git a/pkg/sdk/tables.go b/pkg/sdk/tables.go index 2e3b124786..c9f762c451 100644 --- a/pkg/sdk/tables.go +++ b/pkg/sdk/tables.go @@ -4,6 +4,7 @@ import ( "context" "database/sql" "fmt" + "regexp" "strings" ) @@ -266,6 +267,7 @@ type TableColumnAddAction struct { IfNotExists *bool `ddl:"keyword" sql:"IF NOT EXISTS"` Name string `ddl:"keyword"` Type DataType `ddl:"keyword"` + Collate *string `ddl:"parameter,no_equals,single_quotes" sql:"COLLATE"` DefaultValue *ColumnDefaultValue `ddl:"keyword"` InlineConstraint *TableColumnAddInlineConstraint `ddl:"keyword"` MaskingPolicy *ColumnMaskingPolicy `ddl:"keyword"` @@ -294,11 +296,12 @@ type TableColumnAlterAction struct { column bool `ddl:"static" sql:"COLUMN"` Name string `ddl:"keyword"` - // One of + // One of (except Collate) DropDefault *bool `ddl:"keyword" sql:"DROP DEFAULT"` SetDefault *SequenceName `ddl:"parameter,no_equals" sql:"SET DEFAULT"` NotNullConstraint *TableColumnNotNullConstraint Type *DataType `ddl:"parameter,no_equals" sql:"SET DATA TYPE"` + Collate *string `ddl:"parameter,no_equals,single_quotes" sql:"COLLATE"` Comment *string `ddl:"parameter,no_equals,single_quotes" sql:"COMMENT"` UnsetComment *bool `ddl:"keyword" sql:"UNSET COMMENT"` } @@ -657,6 +660,7 @@ type TableColumnDetails struct { Expression *string Comment *string PolicyName *string + Collation *string } // tableColumnDetailsRow based on https://docs.snowflake.com/en/sql-reference/sql/desc-table @@ -675,13 +679,16 @@ type tableColumnDetailsRow struct { } func (r tableColumnDetailsRow) convert() *TableColumnDetails { + type_, collation := r.splitTypeAndCollation() + details := &TableColumnDetails{ Name: r.Name, - Type: r.Type, + Type: type_, Kind: r.Kind, IsNullable: r.IsNullable == "Y", IsPrimary: r.IsPrimary == "Y", IsUnique: r.IsUnique == "Y", + Collation: collation, } if r.Default.Valid { details.Default = String(r.Default.String) @@ -701,6 +708,18 @@ func (r tableColumnDetailsRow) convert() *TableColumnDetails { return details } +func (r tableColumnDetailsRow) splitTypeAndCollation() (DataType, *string) { + collateRegexp := regexp.MustCompile(`COLLATE +'([a-zA-Z0-9_-]*)'`) + matches := collateRegexp.FindStringSubmatch(string(r.Type)) + + if len(matches) == 2 { + collation := matches[1] + type_ := DataType(strings.TrimSpace(collateRegexp.ReplaceAllString(string(r.Type), ""))) + return type_, &collation + } + return r.Type, nil +} + type describeTableStageOptions struct { describeTable bool `ddl:"static" sql:"DESCRIBE TABLE"` name SchemaObjectIdentifier `ddl:"identifier"` diff --git a/pkg/sdk/tables_dto.go b/pkg/sdk/tables_dto.go index 504cab4fe5..65b82e1359 100644 --- a/pkg/sdk/tables_dto.go +++ b/pkg/sdk/tables_dto.go @@ -371,6 +371,7 @@ type TableColumnAddActionRequest struct { With *bool Tags []TagAssociation Comment *string + Collate *string } type TableColumnAddInlineConstraintRequest struct { @@ -400,6 +401,7 @@ type TableColumnAlterActionRequest struct { Type *DataType Comment *string UnsetComment *bool + Collate *string } type TableColumnAlterSetMaskingPolicyActionRequest struct { diff --git a/pkg/sdk/tables_dto_generated.go b/pkg/sdk/tables_dto_generated.go index 17ec7218a7..5efceac847 100644 --- a/pkg/sdk/tables_dto_generated.go +++ b/pkg/sdk/tables_dto_generated.go @@ -1192,6 +1192,11 @@ func (s *TableColumnAddActionRequest) WithComment(comment *string) *TableColumnA return s } +func (s *TableColumnAddActionRequest) WithCollate(collate *string) *TableColumnAddActionRequest { + s.Collate = collate + return s +} + func NewTableColumnAddInlineConstraintRequest() *TableColumnAddInlineConstraintRequest { return &TableColumnAddInlineConstraintRequest{} } @@ -1273,6 +1278,11 @@ func (s *TableColumnAlterActionRequest) WithComment(comment *string) *TableColum return s } +func (s *TableColumnAlterActionRequest) WithCollate(collate *string) *TableColumnAlterActionRequest { + s.Collate = collate + return s +} + func (s *TableColumnAlterActionRequest) WithUnsetComment(unsetComment *bool) *TableColumnAlterActionRequest { s.UnsetComment = unsetComment return s diff --git a/pkg/sdk/tables_impl.go b/pkg/sdk/tables_impl.go index 5a34f0ac8e..f320d0a0f6 100644 --- a/pkg/sdk/tables_impl.go +++ b/pkg/sdk/tables_impl.go @@ -394,6 +394,7 @@ func (r *TableColumnActionRequest) toOpts() *TableColumnAction { DefaultValue: defaultValue, InlineConstraint: inlineConstraint, Comment: r.Add.Comment, + Collate: r.Add.Collate, }, } } @@ -422,6 +423,7 @@ func (r *TableColumnActionRequest) toOpts() *TableColumnAction { NotNullConstraint: notNullConstraint, Type: alterAction.Type, Comment: alterAction.Comment, + Collate: alterAction.Collate, UnsetComment: alterAction.UnsetComment, }) } diff --git a/pkg/sdk/tables_test.go b/pkg/sdk/tables_test.go index 644c9594a7..9a9adb792b 100644 --- a/pkg/sdk/tables_test.go +++ b/pkg/sdk/tables_test.go @@ -965,7 +965,8 @@ func TestTableAlter(t *testing.T) { Add: &TableColumnAddAction{ IfNotExists: Bool(true), Name: columnName, - Type: DataTypeBoolean, + Type: DataTypeVARCHAR, + Collate: String("utf8"), DefaultValue: &ColumnDefaultValue{ Identity: &ColumnIdentity{ Start: 10, @@ -975,7 +976,7 @@ func TestTableAlter(t *testing.T) { }, }, } - assertOptsValidAndSQLEquals(t, opts, "ALTER TABLE %s ADD COLUMN IF NOT EXISTS NEXT_COLUMN BOOLEAN IDENTITY START 10 INCREMENT 1", id.FullyQualifiedName()) + assertOptsValidAndSQLEquals(t, opts, "ALTER TABLE %s ADD COLUMN IF NOT EXISTS NEXT_COLUMN VARCHAR COLLATE 'utf8' IDENTITY START 10 INCREMENT 1", id.FullyQualifiedName()) }) t.Run("rename column", func(t *testing.T) { @@ -1026,8 +1027,9 @@ func TestTableAlter(t *testing.T) { Comment: String("comment"), }, { - Name: columnTwoName, - Type: Pointer(DataTypeBoolean), + Name: columnTwoName, + Type: Pointer(DataTypeVARCHAR), + Collate: String("utf8"), }, { Name: columnTwoName, @@ -1044,7 +1046,7 @@ func TestTableAlter(t *testing.T) { Alter: actions, }, } - assertOptsValidAndSQLEquals(t, opts, "ALTER TABLE %s ALTER COLUMN COLUMN_1 DROP DEFAULT, COLUMN COLUMN_1 SET DEFAULT SEQUENCE_1.NEXTVAL, COLUMN COLUMN_1 UNSET COMMENT, COLUMN COLUMN_2 DROP DEFAULT, COLUMN COLUMN_2 SET DEFAULT SEQUENCE_2.NEXTVAL, COLUMN COLUMN_2 COMMENT 'comment', COLUMN COLUMN_2 SET DATA TYPE BOOLEAN, COLUMN COLUMN_2 DROP NOT NULL", id.FullyQualifiedName()) + assertOptsValidAndSQLEquals(t, opts, "ALTER TABLE %s ALTER COLUMN COLUMN_1 DROP DEFAULT, COLUMN COLUMN_1 SET DEFAULT SEQUENCE_1.NEXTVAL, COLUMN COLUMN_1 UNSET COMMENT, COLUMN COLUMN_2 DROP DEFAULT, COLUMN COLUMN_2 SET DEFAULT SEQUENCE_2.NEXTVAL, COLUMN COLUMN_2 COMMENT 'comment', COLUMN COLUMN_2 SET DATA TYPE VARCHAR COLLATE 'utf8', COLUMN COLUMN_2 DROP NOT NULL", id.FullyQualifiedName()) }) t.Run("alter: set masking policy", func(t *testing.T) { @@ -1526,6 +1528,58 @@ func TestTableDescribeColumns(t *testing.T) { }) } +func TestTableColumnDetailsRow_SplitTypeAndCollation(t *testing.T) { + t.Run("with utf8", func(t *testing.T) { + row := tableColumnDetailsRow{ + Type: DataType("VARCHAR(10) COLLATE 'utf8'"), + } + + actualType, actualCollation := row.splitTypeAndCollation() + assert.Equal(t, DataType("VARCHAR(10)"), actualType) + assert.Equal(t, "utf8", *actualCollation) + }) + + t.Run("with locale", func(t *testing.T) { + row := tableColumnDetailsRow{ + Type: DataType("VARCHAR(10) COLLATE 'en_US'"), + } + + actualType, actualCollation := row.splitTypeAndCollation() + assert.Equal(t, DataType("VARCHAR(10)"), actualType) + assert.Equal(t, "en_US", *actualCollation) + }) + + t.Run("with multiple specifiers", func(t *testing.T) { + row := tableColumnDetailsRow{ + Type: DataType("VARCHAR(10) COLLATE 'fr_CA-ai-pi-trim'"), + } + + actualType, actualCollation := row.splitTypeAndCollation() + assert.Equal(t, DataType("VARCHAR(10)"), actualType) + assert.Equal(t, "fr_CA-ai-pi-trim", *actualCollation) + }) + + t.Run("with empty collation", func(t *testing.T) { + row := tableColumnDetailsRow{ + Type: DataType("VARCHAR(10) COLLATE ''"), + } + + actualType, actualCollation := row.splitTypeAndCollation() + assert.Equal(t, DataType("VARCHAR(10)"), actualType) + assert.Equal(t, "", *actualCollation) + }) + + t.Run("without collation", func(t *testing.T) { + row := tableColumnDetailsRow{ + Type: DataType("NUMBER(38, 0)"), + } + + actualType, actualCollation := row.splitTypeAndCollation() + assert.Equal(t, DataType("NUMBER(38, 0)"), actualType) + assert.Nil(t, actualCollation) + }) +} + func TestTableDescribeStage(t *testing.T) { id := RandomSchemaObjectIdentifier() defaultOpts := func() *describeTableStageOptions {