diff --git a/go.mod b/go.mod index bdad6a316be..1a2dbda5406 100644 --- a/go.mod +++ b/go.mod @@ -10,7 +10,7 @@ require ( github.com/mattn/go-sqlite3 v1.14.28 github.com/prometheus/common v0.66.1 github.com/stretchr/testify v1.11.1 - github.com/ydb-platform/ydb-go-sdk/v3 v3.125.0 + github.com/ydb-platform/ydb-go-sdk/v3 v3.125.4 github.com/zclconf/go-cty v1.14.4 github.com/zclconf/go-cty-yaml v1.1.0 golang.org/x/mod v0.30.0 diff --git a/go.sum b/go.sum index d12f9e83c9f..355bb38d781 100644 --- a/go.sum +++ b/go.sum @@ -107,8 +107,8 @@ github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/ydb-platform/ydb-go-genproto v0.0.0-20251222105147-0bf751469a4a h1:nRqONRrMFulP2bTWM2RRnPM1VDhWuBZg4ULXkG4xXdk= github.com/ydb-platform/ydb-go-genproto v0.0.0-20251222105147-0bf751469a4a/go.mod h1:Er+FePu1dNUieD+XTMDduGpQuCPssK5Q4BjF+IIXJ3I= -github.com/ydb-platform/ydb-go-sdk/v3 v3.125.0 h1:KPnGV2diuX1A4/1zXLO1UWHJokWC8yICzEfjdkSUWKo= -github.com/ydb-platform/ydb-go-sdk/v3 v3.125.0/go.mod h1:/LjMxb/rXmoGAAnImoqAFIlhO5ampHacbvDetQitCk4= +github.com/ydb-platform/ydb-go-sdk/v3 v3.125.4 h1:GAC7qeNgsibEJkUVzV4z06aBnHR4jqfXsFiQtrY40gI= +github.com/ydb-platform/ydb-go-sdk/v3 v3.125.4/go.mod h1:stS1mQYjbJvwwYaYzKyFY9eMiuVXWWXQA6T+SpOLg9c= github.com/zclconf/go-cty v1.14.4 h1:uXXczd9QDGsgu0i/QFR/hzI5NYCHLf6NQw/atrbnhq8= github.com/zclconf/go-cty v1.14.4/go.mod h1:VvMs5i0vgZdhYawQNq5kePSpLAoz8u1xvZgrPIxfnZE= github.com/zclconf/go-cty-yaml v1.1.0 h1:nP+jp0qPHv2IhUVqmQSzjvqAWcObN0KBkUl2rWBdig0= diff --git a/sql/ydb/attributes.go b/sql/ydb/attributes.go index b19390cdd73..8e7727e878a 100644 --- a/sql/ydb/attributes.go +++ b/sql/ydb/attributes.go @@ -8,9 +8,9 @@ package ydb import "ariga.io/atlas/sql/schema" -//[IndexAttributes] represents YDB-specific index attributes. +// [IndexAttributes] represents YDB-specific index attributes. type IndexAttributes struct { schema.Attr - Global bool // GLOBAL, LOCAL - Sync bool // SYNC, ASYNC + Async bool + CoverColumns []*schema.Column } diff --git a/sql/ydb/convert.go b/sql/ydb/convert.go index abfeacdda91..a4b62855180 100644 --- a/sql/ydb/convert.go +++ b/sql/ydb/convert.go @@ -23,7 +23,7 @@ func FormatType(typ schema.Type) (string, error) { ) switch t := typ.(type) { - case OptionalType: + case *OptionalType: formatted = t.T case *schema.BoolType: formatted = TypeBool @@ -41,7 +41,7 @@ func FormatType(typ schema.Type) (string, error) { formatted = TypeUtf8 case *schema.JSONType: formatted, err = formatJSONType(t) - case YsonType: + case *YsonType: formatted = t.T case *schema.UUIDType: formatted = TypeUUID @@ -61,69 +61,67 @@ func FormatType(typ schema.Type) (string, error) { return formatted, nil } -func formatIntegerType(t *schema.IntegerType) (string, error) { - typ := strings.ToLower(t.T) - switch typ { +func formatIntegerType(intType *schema.IntegerType) (string, error) { + switch typ := strings.ToLower(intType.T); typ { case TypeInt8: - if t.Unsigned { + if intType.Unsigned { return TypeUint8, nil } return TypeInt8, nil case TypeInt16: - if t.Unsigned { + if intType.Unsigned { return TypeUint16, nil } return TypeInt16, nil case TypeInt32: - if t.Unsigned { + if intType.Unsigned { return TypeUint32, nil } return TypeInt32, nil case TypeInt64: - if t.Unsigned { + if intType.Unsigned { return TypeUint64, nil } return TypeInt64, nil case TypeUint8, TypeUint16, TypeUint32, TypeUint64: return typ, nil default: - return "", fmt.Errorf("ydb: unsupported object identifier type: %q", t.T) + return "", fmt.Errorf("ydb: unsupported object identifier type: %q", intType.T) } } -func formatFloatType(t *schema.FloatType) (string, error) { - typ := strings.ToLower(t.T) - switch typ { +func formatFloatType(floatType *schema.FloatType) (string, error) { + switch typ := strings.ToLower(floatType.T); typ { case TypeFloat, TypeDouble: return typ, nil default: - return "", fmt.Errorf("ydb: unsupported object identifier type: %q", t.T) + return "", fmt.Errorf("ydb: unsupported object identifier type: %q", floatType.T) } } -func formatDecimalType(t *schema.DecimalType) (string, error) { - if t.Precision < 1 || t.Precision > 35 { - return "", fmt.Errorf("ydb: DECIMAL precision must be in [1, 35] range, but was %q", t.Precision) +func formatDecimalType(decType *schema.DecimalType) (string, error) { + if decType.Precision < 1 || decType.Precision > 35 { + return "", fmt.Errorf("ydb: DECIMAL precision must be in [1, 35] range, but was %q", decType.Precision) } - if t.Scale < 0 || t.Scale > t.Precision { - return "", fmt.Errorf("ydb: DECIMAL scale must be in [1, precision] range, but was %q", t.Precision) + if decType.Scale < 0 || decType.Scale > decType.Precision { + return "", fmt.Errorf("ydb: DECIMAL scale must be in [1, precision] range, but was %q", decType.Precision) } - return fmt.Sprintf("%s(%d,%d)", TypeDecimal, t.Precision, t.Scale), nil + return fmt.Sprintf("%s(%d,%d)", TypeDecimal, decType.Precision, decType.Scale), nil } -func formatJSONType(t *schema.JSONType) (string, error) { - typ := strings.ToLower(t.T) +func formatJSONType(jsonType *schema.JSONType) (string, error) { + typ := strings.ToLower(jsonType.T) switch typ { case TypeJSONDocument, TypeJSON: return typ, nil default: - return "", fmt.Errorf("ydb: unsupported object identifier type: %q", t.T) + return "", fmt.Errorf("ydb: unsupported object identifier type: %q", jsonType.T) } } -func formatTimeType(t *schema.TimeType) (string, error) { - switch typ := strings.ToLower(t.T); typ { +func formatTimeType(timeType *schema.TimeType) (string, error) { + switch typ := strings.ToLower(timeType.T); typ { case TypeDate, TypeDate32, TypeDateTime, @@ -140,7 +138,7 @@ func formatTimeType(t *schema.TimeType) (string, error) { TypeTzTimestamp64: return typ, nil default: - return "", fmt.Errorf("ydb: unsupported object identifier type: %q", t.T) + return "", fmt.Errorf("ydb: unsupported object identifier type: %q", timeType.T) } } diff --git a/sql/ydb/convert_test.go b/sql/ydb/convert_test.go index cf27bd5830d..676a1bdb8dd 100644 --- a/sql/ydb/convert_test.go +++ b/sql/ydb/convert_test.go @@ -69,7 +69,7 @@ func TestConvert_FormatType(t *testing.T) { {name: "jsondocument", typ: &schema.JSONType{T: TypeJSONDocument}, expected: TypeJSONDocument}, // YSON type - {name: "yson", typ: YsonType{T: TypeYson}, expected: TypeYson}, + {name: "yson", typ: &YsonType{T: TypeYson}, expected: TypeYson}, // UUID type {name: "uuid", typ: &schema.UUIDType{T: TypeUUID}, expected: TypeUUID}, @@ -93,7 +93,7 @@ func TestConvert_FormatType(t *testing.T) { {name: "tztimestamp64", typ: &schema.TimeType{T: TypeTzTimestamp64}, expected: TypeTzTimestamp64}, // Optional type - {name: "optional_int32", typ: OptionalType{T: "Optional", InnerType: &schema.IntegerType{T: TypeInt32}}, expected: "Optional"}, + {name: "optional_int32", typ: &OptionalType{T: "Optional", InnerType: &schema.IntegerType{T: TypeInt32}}, expected: "Optional"}, // Error cases {name: "unsupported_type", typ: &schema.UnsupportedType{T: "unknown"}, wantErr: true}, diff --git a/sql/ydb/diff.go b/sql/ydb/diff.go index 16e90dc28d5..c249f352363 100644 --- a/sql/ydb/diff.go +++ b/sql/ydb/diff.go @@ -17,7 +17,9 @@ import ( // DefaultDiff provides basic diffing capabilities for YDB dialect. // Note, it is recommended to call Open, create a new Driver and use its // Differ when a database connection is available. -var DefaultDiff schema.Differ = &sqlx.Diff{DiffDriver: &diff{&conn{ExecQuerier: sqlx.NoRows}}} +var DefaultDiff schema.Differ = &sqlx.Diff{ + DiffDriver: &diff{&conn{ExecQuerier: sqlx.NoRows}}, +} // A diff provides a YDB implementation for sqlx.DiffDriver. type diff struct { @@ -116,7 +118,7 @@ func (d *diff) typeChanged(from *schema.Column, to *schema.Column) (bool, error) } // defaultChanged reports if the default value of a column was changed. -func (d *diff) defaultChanged(from, to *schema.Column) bool { +func (d *diff) defaultChanged(from *schema.Column, to *schema.Column) bool { default1, ok1 := sqlx.DefaultValue(from) default2, ok2 := sqlx.DefaultValue(to) if ok1 != ok2 { @@ -126,13 +128,31 @@ func (d *diff) defaultChanged(from, to *schema.Column) bool { } // IndexAttrChanged reports if the index attributes were changed. -func (*diff) IndexAttrChanged(_, _ []schema.Attr) bool { - return false // unimplemented. +func (*diff) IndexAttrChanged(from, to []schema.Attr) bool { + var fromAttrs, toAttrs IndexAttributes + sqlx.Has(from, &fromAttrs) + sqlx.Has(to, &toAttrs) + + if fromAttrs.Async != toAttrs.Async { + return true + } + + if len(fromAttrs.CoverColumns) != len(toAttrs.CoverColumns) { + return true + } + + for i := range fromAttrs.CoverColumns { + if fromAttrs.CoverColumns[i].Name != toAttrs.CoverColumns[i].Name { + return true + } + } + return false } // IndexPartAttrChanged reports if the index-part attributes were changed. func (*diff) IndexPartAttrChanged(_, _ *schema.Index, _ int) bool { - return false // unimplemented. + // YDB doesn't have per-part attributes like collation, prefix, or operator classes. + return false } // IsGeneratedIndexName reports if the index name was generated by the database. diff --git a/sql/ydb/diff_test.go b/sql/ydb/diff_test.go index 4867da6d02c..bb52ba03b16 100644 --- a/sql/ydb/diff_test.go +++ b/sql/ydb/diff_test.go @@ -241,6 +241,70 @@ func TestDiff_TableDiff(t *testing.T) { }, } }(), + func() testcase { + var ( + from = &schema.Table{ + Name: "users", + Schema: &schema.Schema{Name: "local"}, + Columns: []*schema.Column{ + {Name: "id", Type: &schema.ColumnType{Type: &schema.IntegerType{T: TypeInt32}}}, + }, + } + to = &schema.Table{ + Name: "users", + Columns: []*schema.Column{ + {Name: "id", Type: &schema.ColumnType{Type: &schema.IntegerType{T: TypeInt32}}}, + }, + } + ) + from.Indexes = []*schema.Index{ + {Name: "idx_id", Table: from, Parts: []*schema.IndexPart{{SeqNo: 1, C: from.Columns[0]}}, Attrs: []schema.Attr{&IndexAttributes{Async: false}}}, + } + to.Indexes = []*schema.Index{ + {Name: "idx_id", Table: to, Parts: []*schema.IndexPart{{SeqNo: 1, C: to.Columns[0]}}, Attrs: []schema.Attr{&IndexAttributes{Async: true}}}, + } + return testcase{ + name: "modify index async", + from: from, + to: to, + wantChanges: []schema.Change{ + &schema.ModifyIndex{From: from.Indexes[0], To: to.Indexes[0], Change: schema.ChangeAttr}, + }, + } + }(), + func() testcase { + var ( + from = &schema.Table{ + Name: "users", + Schema: &schema.Schema{Name: "local"}, + Columns: []*schema.Column{ + {Name: "id", Type: &schema.ColumnType{Type: &schema.IntegerType{T: TypeInt32}}}, + {Name: "name", Type: &schema.ColumnType{Type: &schema.StringType{T: TypeUtf8}}}, + }, + } + to = &schema.Table{ + Name: "users", + Columns: []*schema.Column{ + {Name: "id", Type: &schema.ColumnType{Type: &schema.IntegerType{T: TypeInt32}}}, + {Name: "name", Type: &schema.ColumnType{Type: &schema.StringType{T: TypeUtf8}}}, + }, + } + ) + from.Indexes = []*schema.Index{ + {Name: "idx_id", Table: from, Parts: []*schema.IndexPart{{SeqNo: 1, C: from.Columns[0]}}}, + } + to.Indexes = []*schema.Index{ + {Name: "idx_id", Table: to, Parts: []*schema.IndexPart{{SeqNo: 1, C: to.Columns[0]}}, Attrs: []schema.Attr{&IndexAttributes{CoverColumns: []*schema.Column{to.Columns[1]}}}}, + } + return testcase{ + name: "modify index add cover columns", + from: from, + to: to, + wantChanges: []schema.Change{ + &schema.ModifyIndex{From: from.Indexes[0], To: to.Indexes[0], Change: schema.ChangeAttr}, + }, + } + }(), } for _, tt := range tests { @@ -406,3 +470,104 @@ func TestDiff_DefaultChanged(t *testing.T) { }) } } + +func TestDiff_IndexAttrChanged(t *testing.T) { + d := &diff{conn: &conn{}} + + col1 := &schema.Column{Name: "name"} + col2 := &schema.Column{Name: "email"} + + tests := []struct { + name string + from []schema.Attr + to []schema.Attr + changed bool + }{ + { + name: "no attributes", + from: nil, + to: nil, + }, + { + name: "add async attribute", + from: nil, + to: []schema.Attr{&IndexAttributes{Async: true}}, + changed: true, + }, + { + name: "remove async attribute", + from: []schema.Attr{&IndexAttributes{Async: true}}, + to: nil, + changed: true, + }, + { + name: "same async false", + from: []schema.Attr{&IndexAttributes{Async: false}}, + to: []schema.Attr{&IndexAttributes{Async: false}}, + }, + { + name: "same async true", + from: []schema.Attr{&IndexAttributes{Async: true}}, + to: []schema.Attr{&IndexAttributes{Async: true}}, + }, + { + name: "change async false to true", + from: []schema.Attr{&IndexAttributes{Async: false}}, + to: []schema.Attr{&IndexAttributes{Async: true}}, + changed: true, + }, + { + name: "change async true to false", + from: []schema.Attr{&IndexAttributes{Async: true}}, + to: []schema.Attr{&IndexAttributes{Async: false}}, + changed: true, + }, + { + name: "same cover columns", + from: []schema.Attr{&IndexAttributes{CoverColumns: []*schema.Column{col1}}}, + to: []schema.Attr{&IndexAttributes{CoverColumns: []*schema.Column{col1}}}, + }, + { + name: "add cover columns", + from: []schema.Attr{&IndexAttributes{}}, + to: []schema.Attr{&IndexAttributes{CoverColumns: []*schema.Column{col1}}}, + changed: true, + }, + { + name: "remove cover columns", + from: []schema.Attr{&IndexAttributes{CoverColumns: []*schema.Column{col1}}}, + to: []schema.Attr{&IndexAttributes{}}, + changed: true, + }, + { + name: "different cover columns count", + from: []schema.Attr{&IndexAttributes{CoverColumns: []*schema.Column{col1}}}, + to: []schema.Attr{&IndexAttributes{CoverColumns: []*schema.Column{col1, col2}}}, + changed: true, + }, + { + name: "different cover column names", + from: []schema.Attr{&IndexAttributes{CoverColumns: []*schema.Column{col1}}}, + to: []schema.Attr{&IndexAttributes{CoverColumns: []*schema.Column{col2}}}, + changed: true, + }, + { + name: "same async and cover columns", + from: []schema.Attr{&IndexAttributes{Async: true, CoverColumns: []*schema.Column{col1, col2}}}, + to: []schema.Attr{&IndexAttributes{Async: true, CoverColumns: []*schema.Column{col1, col2}}}, + }, + { + name: "same cover columns different async", + from: []schema.Attr{&IndexAttributes{Async: false, CoverColumns: []*schema.Column{col1}}}, + to: []schema.Attr{&IndexAttributes{Async: true, CoverColumns: []*schema.Column{col1}}}, + changed: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + changed := d.IndexAttrChanged(tt.from, tt.to) + require.Equal(t, tt.changed, changed) + }) + } +} diff --git a/sql/ydb/driver.go b/sql/ydb/driver.go index 36295cc89c5..b73e03753dc 100644 --- a/sql/ydb/driver.go +++ b/sql/ydb/driver.go @@ -64,9 +64,9 @@ func init() { } func opener(ctx context.Context, dsn *url.URL) (*sqlclient.Client, error) { - parser := parser{}.ParseURL(dsn) + url := parser{}.ParseURL(dsn) - nativeDriver, err := ydbSdk.Open(ctx, parser.DSN) + nativeDriver, err := ydbSdk.Open(ctx, url.DSN) if err != nil { return nil, err } @@ -74,14 +74,14 @@ func opener(ctx context.Context, dsn *url.URL) (*sqlclient.Client, error) { conn, err := ydbSdk.Connector( nativeDriver, ydbSdk.WithAutoDeclare(), - ydbSdk.WithTablePathPrefix(nativeDriver.Name()), + ydbSdk.WithQueryService(true), ) if err != nil { return nil, err } sqlDriver := sql.OpenDB(conn) - drv, err := Open(nativeDriver, sqlDriver) + migrateDriver, err := Open(nativeDriver, sqlDriver) if err != nil { if cerr := sqlDriver.Close(); cerr != nil { err = fmt.Errorf("%w: %v", err, cerr) @@ -89,15 +89,15 @@ func opener(ctx context.Context, dsn *url.URL) (*sqlclient.Client, error) { return nil, err } - if d, ok := drv.(*Driver); ok { - d.database = parser.Schema + if ydbDriver, ok := migrateDriver.(*Driver); ok { + ydbDriver.database = url.Schema } return &sqlclient.Client{ Name: DriverName, DB: sqlDriver, - URL: parser, - Driver: drv, + URL: url, + Driver: migrateDriver, }, nil } @@ -106,6 +106,7 @@ func Open(nativeDriver *ydbSdk.Driver, sqlDriver *sql.DB) (migrate.Driver, error c := &conn{ ExecQuerier: sqlDriver, nativeDriver: nativeDriver, + database: nativeDriver.Name(), } rows, err := sqlDriver.QueryContext(context.Background(), "SELECT version()") diff --git a/sql/ydb/inspect.go b/sql/ydb/inspect.go index 5ce21ede0cf..dd3038faab5 100644 --- a/sql/ydb/inspect.go +++ b/sql/ydb/inspect.go @@ -11,6 +11,7 @@ import ( "errors" "fmt" "slices" + "strings" "ariga.io/atlas/sql/internal/sqlx" "ariga.io/atlas/sql/schema" @@ -21,7 +22,8 @@ import ( // inspect provides a YDB implementation for schema.Inspector. type inspect struct { - database string + *conn + schemeClient scheme.Client tableClient table.Client } @@ -29,7 +31,7 @@ type inspect struct { // newInspect creates a new inspect from conn. func newInspect(c *conn) *inspect { return &inspect{ - database: c.database, + conn: c, schemeClient: c.nativeDriver.Scheme(), tableClient: c.nativeDriver.Table(), } @@ -38,7 +40,10 @@ func newInspect(c *conn) *inspect { var _ schema.Inspector = (*inspect)(nil) // InspectRealm returns schema descriptions of all resources in the given realm. -func (i *inspect) InspectRealm(ctx context.Context, opts *schema.InspectRealmOption) (*schema.Realm, error) { +func (i *inspect) InspectRealm( + ctx context.Context, + opts *schema.InspectRealmOption, +) (*schema.Realm, error) { schemas, err := i.schemas(ctx, opts) if err != nil { return nil, err @@ -102,7 +107,10 @@ func (i *inspect) InspectSchema( } // schemas returns the list of schemas in the database. -func (i *inspect) schemas(ctx context.Context, opts *schema.InspectRealmOption) ([]*schema.Schema, error) { +func (i *inspect) schemas( + ctx context.Context, + opts *schema.InspectRealmOption, +) ([]*schema.Schema, error) { var names []string if opts != nil && len(opts.Schemas) > 0 && opts.Schemas[0] != "" { names = opts.Schemas @@ -137,7 +145,9 @@ func (i *inspect) inspectTables( return err } for _, table := range schema.Tables { - tableDesc, err := i.tableClient.DescribeTable(ctx, table.Name) + fullPath := schema.Name + "/" + table.Name + + tableDesc, err := i.tableClient.DescribeTable(ctx, fullPath) if err != nil { return fmt.Errorf("ydb: failed describe table: %v", err) } @@ -159,8 +169,12 @@ type entryWithPath struct { } // tables queries and populates the tables in the schema. -func (i *inspect) tables(ctx context.Context, s *schema.Schema, opts *schema.InspectOptions) error { - rootPath := s.Name +func (i *inspect) tables( + ctx context.Context, + schem *schema.Schema, + opts *schema.InspectOptions, +) error { + rootPath := schem.Name rootDir, err := i.schemeClient.ListDirectory(ctx, rootPath) if err != nil { return fmt.Errorf("ydb: failed list directory: %v", err) @@ -181,13 +195,16 @@ func (i *inspect) tables(ctx context.Context, s *schema.Schema, opts *schema.Ins switch currEntry.Type { case scheme.EntryTable: + relativePath := strings.TrimPrefix(currEntry.fullPath, rootPath+"/") + shouldAdd := opts == nil || len(opts.Tables) == 0 || + slices.Contains(opts.Tables, relativePath) || slices.Contains(opts.Tables, currEntry.fullPath) if shouldAdd { - t := schema.NewTable(currEntry.fullPath) - s.AddTables(t) + t := schema.NewTable(relativePath) + schem.AddTables(t) } case scheme.EntryDirectory: @@ -276,8 +293,9 @@ func (i *inspect) indexes( // secondary indexes for _, idx := range tableDesc.Indexes { atlasIdx := &schema.Index{ - Name: idx.Name, - Table: table, + Name: idx.Name, + Table: table, + Unique: idx.Type == options.GlobalUniqueIndex(), } for _, columnName := range idx.IndexColumns { @@ -291,6 +309,20 @@ func (i *inspect) indexes( }) } + indexAttrs := &IndexAttributes{ + Async: idx.Type == options.GlobalAsyncIndex(), + } + + for _, dataCol := range idx.DataColumns { + column, ok := table.Column(dataCol) + if !ok { + return fmt.Errorf("ydb: cover column %q not found in table %q", dataCol, table.Name) + } + indexAttrs.CoverColumns = append(indexAttrs.CoverColumns, column) + } + + atlasIdx.Attrs = append(atlasIdx.Attrs, indexAttrs) + table.AddIndexes(atlasIdx) } diff --git a/sql/ydb/inspect_test.go b/sql/ydb/inspect_test.go index 81107e9a120..463836f77d8 100644 --- a/sql/ydb/inspect_test.go +++ b/sql/ydb/inspect_test.go @@ -106,7 +106,7 @@ func newTestInspect( tableClient table.Client, ) *inspect { return &inspect{ - database: database, + conn: &conn{database: database}, schemeClient: schemeClient, tableClient: tableClient, } @@ -144,7 +144,7 @@ func TestInspect_InspectSchema_Simple(t *testing.T) { require.NoError(t, err) require.Equal(t, "/local", s.Name) require.Len(t, s.Tables, 1) - require.Equal(t, "/local/users", s.Tables[0].Name) + require.Equal(t, "users", s.Tables[0].Name) require.Len(t, s.Tables[0].Columns, 2) require.NotNil(t, s.Tables[0].PrimaryKey) require.Equal(t, "PRIMARY", s.Tables[0].PrimaryKey.Name) @@ -337,8 +337,8 @@ func TestInspect_InspectSchema_NestedDirectories(t *testing.T) { require.Len(t, s.Tables, 2) tableNames := []string{s.Tables[0].Name, s.Tables[1].Name} - require.Contains(t, tableNames, "/local/users") - require.Contains(t, tableNames, "/local/app/settings") + require.Contains(t, tableNames, "users") + require.Contains(t, tableNames, "app/settings") } func TestInspect_InspectRealm(t *testing.T) { @@ -564,15 +564,15 @@ func TestInspect_TableFilter(t *testing.T) { ctx := context.Background() s, err := insp.InspectSchema(ctx, "/local", &schema.InspectOptions{ - Tables: []string{"/local/users", "/local/orders"}, + Tables: []string{"users", "orders"}, }) require.NoError(t, err) require.Len(t, s.Tables, 2) tableNames := []string{s.Tables[0].Name, s.Tables[1].Name} - require.Contains(t, tableNames, "/local/users") - require.Contains(t, tableNames, "/local/orders") - require.NotContains(t, tableNames, "/local/products") + require.Contains(t, tableNames, "users") + require.Contains(t, tableNames, "orders") + require.NotContains(t, tableNames, "products") } func TestInspect_EmptySchema(t *testing.T) { @@ -695,7 +695,7 @@ func TestInspect_DeeplyNestedDirectories(t *testing.T) { s, err := insp.InspectSchema(ctx, "/local", nil) require.NoError(t, err) require.Len(t, s.Tables, 1) - require.Equal(t, "/local/level1/level2/deep_table", s.Tables[0].Name) + require.Equal(t, "level1/level2/deep_table", s.Tables[0].Name) } func TestInspect_MixedEntryTypes(t *testing.T) { @@ -742,8 +742,8 @@ func TestInspect_MixedEntryTypes(t *testing.T) { require.Len(t, s.Tables, 2) tableNames := []string{s.Tables[0].Name, s.Tables[1].Name} - require.Contains(t, tableNames, "/local/users") - require.Contains(t, tableNames, "/local/subdir/orders") + require.Contains(t, tableNames, "users") + require.Contains(t, tableNames, "subdir/orders") } func TestInspect_ColumnTypeRawValue(t *testing.T) { diff --git a/sql/ydb/migrate.go b/sql/ydb/migrate.go index 8b5580d9ed1..ade3b8678e8 100644 --- a/sql/ydb/migrate.go +++ b/sql/ydb/migrate.go @@ -19,7 +19,9 @@ import ( // DefaultPlan provides basic planning capabilities for YDB dialect. // Note, it is recommended to call Open, create a new Driver and use its // migrate.PlanApplier when a database connection is available. -var DefaultPlan migrate.PlanApplier = &planApply{conn: &conn{ExecQuerier: sqlx.NoRows}} +var DefaultPlan migrate.PlanApplier = &planApply{ + conn: &conn{ExecQuerier: sqlx.NoRows}, +} // A planApply provides migration capabilities for schema elements. type planApply struct{ *conn } @@ -35,7 +37,7 @@ func (p *planApply) PlanChanges( conn: p.conn, Plan: migrate.Plan{ Name: name, - Transactional: true, + Transactional: false, }, } for _, opt := range opts { @@ -95,24 +97,41 @@ func (s *state) plan(changes []schema.Change) error { return nil } +// tablePath returns the full YDB path for a table. +func (s *state) tablePath(table *schema.Table) string { + if s.database == "" { + return table.Name + } + return s.database + "/" + table.Name +} + // addTable builds and executes the query for creating a table in a schema. func (s *state) addTable(addTable *schema.AddTable) error { var errs []string builder := s.Build("CREATE TABLE") - builder.Table(addTable.T) + if sqlx.Has(addTable.Extra, &schema.IfNotExists{}) { + builder.P("IF NOT EXISTS") + } + + builder.Ident(s.tablePath(addTable.T)) builder.WrapIndent(func(b *sqlx.Builder) { - b.MapIndent(addTable.T.Columns, func(i int, b *sqlx.Builder) { - if err := s.column(b, addTable.T.Columns[i]); err != nil { - errs = append(errs, err.Error()) - } - }) + b.MapIndent( + addTable.T.Columns, + func(i int, b *sqlx.Builder) { + if err := s.column(b, addTable.T.Columns[i]); err != nil { + errs = append(errs, err.Error()) + } + }, + ) + if primaryKey := addTable.T.PrimaryKey; primaryKey != nil { b.Comma().NL().P("PRIMARY KEY") s.indexParts(b, primaryKey.Parts) } else { errs = append(errs, "ydb: primary key is mandatory") } + // inline secondary indexes for _, idx := range addTable.T.Indexes { b.Comma().NL() @@ -125,7 +144,7 @@ func (s *state) addTable(addTable *schema.AddTable) error { } reverse := s.Build("DROP TABLE"). - Table(addTable.T). + Ident(s.tablePath(addTable.T)). String() s.append(&migrate.Change{ @@ -152,7 +171,7 @@ func (s *state) dropTable(drop *schema.DropTable) error { if sqlx.Has(drop.Extra, &schema.IfExists{}) { builder.P("IF EXISTS") } - builder.Table(drop.T) + builder.Ident(s.tablePath(drop.T)) // The reverse of 'DROP TABLE' might be a multi-statement operation reverse := func() any { @@ -229,114 +248,100 @@ func (s *state) modifyTable(modify *schema.ModifyTable) error { } // alterTable modifies the given table by executing on it a list of changes in one SQL statement. -func (s *state) alterTable(t *schema.Table, changes []schema.Change) error { +func (s *state) alterTable(table *schema.Table, changes []schema.Change) error { var reverse []schema.Change buildFunc := func(changes []schema.Change) (string, error) { - b := s.Build("ALTER TABLE").Table(t) - - err := b.MapCommaErr(changes, func(i int, builder *sqlx.Builder) error { - switch change := changes[i].(type) { - case *schema.AddColumn: - builder.P("ADD COLUMN") - if err := s.column(builder, change.C); err != nil { - return err + builder := s.Build("ALTER TABLE").Ident(s.tablePath(table)) + + err := builder.MapCommaErr( + changes, + func(i int, builder *sqlx.Builder) error { + switch change := changes[i].(type) { + case *schema.AddColumn: + builder.P("ADD COLUMN") + if err := s.column(builder, change.C); err != nil { + return err + } + reverse = append(reverse, &schema.DropColumn{C: change.C}) + + case *schema.DropColumn: + builder.P("DROP COLUMN").Ident(change.C.Name) + reverse = append(reverse, &schema.AddColumn{C: change.C}) } - reverse = append(reverse, &schema.DropColumn{C: change.C}) - - case *schema.DropColumn: - builder.P("DROP COLUMN").Ident(change.C.Name) - reverse = append(reverse, &schema.AddColumn{C: change.C}) - } - return nil - }) + return nil + }, + ) if err != nil { return "", err } - return b.String(), nil + return builder.String(), nil } - stmt, err := buildFunc(changes) + query, err := buildFunc(changes) if err != nil { - return fmt.Errorf("alter table %q: %v", t.Name, err) + return fmt.Errorf("alter table %q: %v", table.Name, err) } cmd := &migrate.Change{ - Cmd: stmt, + Cmd: query, Source: &schema.ModifyTable{ - T: t, + T: table, Changes: changes, }, - Comment: fmt.Sprintf("modify %q table", t.Name), + Comment: fmt.Sprintf("modify %q table", table.Name), } // Changes should be reverted in a reversed order they were created. sqlx.ReverseChanges(reverse) if cmd.Reverse, err = buildFunc(reverse); err != nil { - return fmt.Errorf("reverse alter table %q: %v", t.Name, err) + return fmt.Errorf("reverse alter table %q: %v", table.Name, err) } s.append(cmd) return nil } -func (s *state) addIndexes(src schema.Change, t *schema.Table, indexes ...*schema.AddIndex) error { +func (s *state) addIndexes(src schema.Change, table *schema.Table, indexes ...*schema.AddIndex) error { for _, add := range indexes { index := add.I - indexAttrs := IndexAttributes{} - hasAttrs := sqlx.Has(index.Attrs, &indexAttrs) - b := s.Build("ALTER TABLE"). - Table(t). + builder := s.Build("ALTER TABLE"). + Ident(s.tablePath(table)). P("ADD INDEX"). Ident(index.Name) - if hasAttrs && !indexAttrs.Global { - b.P("LOCAL") - } else { - b.P("GLOBAL") - } - - if index.Unique { - b.P("UNIQUE") - } - - if hasAttrs && !indexAttrs.Sync { - b.P("ASYNC") - } else { - b.P("SYNC") - } - - b.P("ON") - - s.indexParts(b, index.Parts) + s.buildIndexSpec(builder, index) reverseOp := s.Build("ALTER TABLE"). - Table(t). + Ident(s.tablePath(table)). P("DROP INDEX"). Ident(index.Name). String() s.append(&migrate.Change{ - Cmd: b.String(), + Cmd: builder.String(), Source: src, - Comment: fmt.Sprintf("create index %q to table: %q", index.Name, t.Name), + Comment: fmt.Sprintf("create index %q to table: %q", index.Name, table.Name), Reverse: reverseOp, }) } return nil } -func (s *state) dropIndexes(src schema.Change, t *schema.Table, drops ...*schema.DropIndex) error { +func (s *state) dropIndexes(src schema.Change, table *schema.Table, drops ...*schema.DropIndex) error { adds := make([]*schema.AddIndex, len(drops)) - for i, d := range drops { - adds[i] = &schema.AddIndex{I: d.I, Extra: d.Extra} + for i, drop := range drops { + adds[i] = &schema.AddIndex{ + I: drop.I, + Extra: drop.Extra, + } } reverseState := &state{conn: s.conn, PlanOptions: s.PlanOptions} - if err := reverseState.addIndexes(src, t, adds...); err != nil { + if err := reverseState.addIndexes(src, table, adds...); err != nil { return err } @@ -344,7 +349,7 @@ func (s *state) dropIndexes(src schema.Change, t *schema.Table, drops ...*schema s.append(&migrate.Change{ Cmd: reverseState.Changes[i].Reverse.(string), Source: src, - Comment: fmt.Sprintf("drop index %q from table: %q", add.I.Name, t.Name), + Comment: fmt.Sprintf("drop index %q from table: %q", add.I.Name, table.Name), Reverse: reverseState.Changes[i].Cmd, }) } @@ -353,57 +358,96 @@ func (s *state) dropIndexes(src schema.Change, t *schema.Table, drops ...*schema } // renameTable builds and appends the statement for renaming a table. -func (s *state) renameTable(c *schema.RenameTable) { +func (s *state) renameTable(rename *schema.RenameTable) { s.append(&migrate.Change{ - Source: c, - Comment: fmt.Sprintf("rename a table from %q to %q", c.From.Name, c.To.Name), - Cmd: s.Build("ALTER TABLE").Table(c.From).P("RENAME TO").Table(c.To).String(), - Reverse: s.Build("ALTER TABLE").Table(c.To).P("RENAME TO").Table(c.From).String(), + Source: rename, + Comment: fmt.Sprintf("rename a table from %q to %q", rename.From.Name, rename.To.Name), + Cmd: s.Build("ALTER TABLE").Ident(s.tablePath(rename.From)).P("RENAME TO").Ident(s.tablePath(rename.To)).String(), + Reverse: s.Build("ALTER TABLE").Ident(s.tablePath(rename.To)).P("RENAME TO").Ident(s.tablePath(rename.From)).String(), }) } // renameIndex builds and appends the statement for renaming an index. -func (s *state) renameIndex(modify *schema.ModifyTable, c *schema.RenameIndex) { +func (s *state) renameIndex(modify *schema.ModifyTable, rename *schema.RenameIndex) { s.append(&migrate.Change{ - Source: c, - Comment: fmt.Sprintf("rename an index from %q to %q", c.From.Name, c.To.Name), - Cmd: s.Build("ALTER TABLE").Table(modify.T).P("RENAME INDEX").Ident(c.From.Name).P("TO").Ident(c.To.Name).String(), - Reverse: s.Build("ALTER TABLE").Table(modify.T).P("RENAME INDEX").Ident(c.To.Name).P("TO").Ident(c.From.Name).String(), + Source: rename, + Comment: fmt.Sprintf("rename an index from %q to %q", rename.From.Name, rename.To.Name), + Cmd: s.Build("ALTER TABLE").Ident(s.tablePath(modify.T)).P("RENAME INDEX").Ident(rename.From.Name).P("TO").Ident(rename.To.Name).String(), + Reverse: s.Build("ALTER TABLE").Ident(s.tablePath(modify.T)).P("RENAME INDEX").Ident(rename.To.Name).P("TO").Ident(rename.From.Name).String(), }) } // column writes the column definition to the builder. -func (s *state) column(b *sqlx.Builder, c *schema.Column) error { - t, err := FormatType(c.Type.Type) +func (s *state) column(builder *sqlx.Builder, column *schema.Column) error { + t, err := FormatType(column.Type.Type) if err != nil { return err } - b.Ident(c.Name).P(t) + builder.Ident(column.Name).P(t) - if !c.Type.Null { - b.P("NOT NULL") + if !column.Type.Null { + builder.P("NOT NULL") } return nil } // indexDef writes an inline index definition for CREATE TABLE. -func (s *state) indexDef(b *sqlx.Builder, idx *schema.Index) { - b.P("INDEX").Ident(idx.Name).P("GLOBAL ON") - s.indexParts(b, idx.Parts) +func (s *state) indexDef(builder *sqlx.Builder, index *schema.Index) { + builder.P("INDEX").Ident(index.Name) + s.buildIndexSpec(builder, index) +} + +// buildIndexSpec writes the common index specification: +// GLOBAL [UNIQUE] [SYNC|ASYNC] ON (columns) [COVER (columns)]. +func (s *state) buildIndexSpec(builder *sqlx.Builder, idx *schema.Index) { + indexAttrs := IndexAttributes{} + hasAttrs := sqlx.Has(idx.Attrs, &indexAttrs) + + builder.P("GLOBAL") + + if idx.Unique { + builder.P("UNIQUE") + } + + if hasAttrs && indexAttrs.Async { + builder.P("ASYNC") + } else { + builder.P("SYNC") + } + + builder.P("ON") + s.indexParts(builder, idx.Parts) + + if hasAttrs && len(indexAttrs.CoverColumns) > 0 { + builder.P("COVER") + s.indexCoverColumns(builder, indexAttrs.CoverColumns) + } } // indexParts writes the index parts (columns) to the builder. -func (s *state) indexParts(b *sqlx.Builder, parts []*schema.IndexPart) { - b.Wrap(func(b *sqlx.Builder) { - b.MapComma(parts, func(i int, b *sqlx.Builder) { - switch part := parts[i]; { - case part.C != nil: - b.Ident(part.C.Name) - case part.X != nil: - b.WriteString(part.X.(*schema.RawExpr).X) - } - }) +func (s *state) indexParts(builder *sqlx.Builder, parts []*schema.IndexPart) { + builder.Wrap(func(b *sqlx.Builder) { + b.MapComma( + parts, + func(i int, builder *sqlx.Builder) { + if parts[i].C != nil { + builder.Ident(parts[i].C.Name) + } + }, + ) + }) +} + +// indexCoverColumns writes the cover columns to the builder. +func (s *state) indexCoverColumns(builder *sqlx.Builder, coverColumns []*schema.Column) { + builder.Wrap(func(b *sqlx.Builder) { + b.MapComma( + coverColumns, + func(i int, builder *sqlx.Builder) { + builder.Ident(coverColumns[i].Name) + }, + ) }) } diff --git a/sql/ydb/migrate_test.go b/sql/ydb/migrate_test.go index 180596e0b4d..dd2de1d4235 100644 --- a/sql/ydb/migrate_test.go +++ b/sql/ydb/migrate_test.go @@ -38,7 +38,7 @@ func TestPlanChanges_AddTable(t *testing.T) { }, }, wantPlan: &migrate.Plan{ - Transactional: true, + Transactional: false, Changes: []*migrate.Change{ { Cmd: "CREATE TABLE `users` (`id` int64 NOT NULL, `name` utf8 NOT NULL, PRIMARY KEY (`id`))", @@ -64,7 +64,7 @@ func TestPlanChanges_AddTable(t *testing.T) { }, }, wantPlan: &migrate.Plan{ - Transactional: true, + Transactional: false, Changes: []*migrate.Change{ { Cmd: "CREATE TABLE `users` (`id` int64 NOT NULL, `email` utf8, PRIMARY KEY (`id`))", @@ -91,7 +91,7 @@ func TestPlanChanges_AddTable(t *testing.T) { }, }, wantPlan: &migrate.Plan{ - Transactional: true, + Transactional: false, Changes: []*migrate.Change{ { Cmd: "CREATE TABLE `users` (`id` int64 NOT NULL, `name` utf8 NOT NULL, PRIMARY KEY (`id`), INDEX `idx_name` GLOBAL ON (`name`))", @@ -118,7 +118,7 @@ func TestPlanChanges_AddTable(t *testing.T) { }, }, wantPlan: &migrate.Plan{ - Transactional: true, + Transactional: false, Changes: []*migrate.Change{ { Cmd: "CREATE TABLE `order_items` (`order_id` int64 NOT NULL, `item_id` int64 NOT NULL, `quantity` int32 NOT NULL, PRIMARY KEY (`order_id`, `item_id`))", @@ -140,6 +140,32 @@ func TestPlanChanges_AddTable(t *testing.T) { }, wantErr: true, }, + { + name: "create table if not exists", + changes: []schema.Change{ + &schema.AddTable{ + T: schema.NewTable("users"). + AddColumns( + schema.NewColumn("id").SetType(&schema.IntegerType{T: TypeInt64}), + schema.NewColumn("name").SetType(&schema.StringType{T: TypeUtf8}), + ). + SetPrimaryKey(schema.NewPrimaryKey( + schema.NewColumn("id").SetType(&schema.IntegerType{T: TypeInt64}), + )), + Extra: []schema.Clause{&schema.IfNotExists{}}, + }, + }, + wantPlan: &migrate.Plan{ + Transactional: false, + Changes: []*migrate.Change{ + { + Cmd: "CREATE TABLE IF NOT EXISTS `users` (`id` int64 NOT NULL, `name` utf8 NOT NULL, PRIMARY KEY (`id`))", + Reverse: "DROP TABLE `users`", + Comment: `create "users" table`, + }, + }, + }, + }, { name: "table with various types", changes: []schema.Change{ @@ -159,7 +185,7 @@ func TestPlanChanges_AddTable(t *testing.T) { }, }, wantPlan: &migrate.Plan{ - Transactional: true, + Transactional: false, Changes: []*migrate.Change{ { Cmd: "CREATE TABLE `data` (`id` int64 NOT NULL, `flag` bool NOT NULL, `price` decimal(10,2) NOT NULL, `timestamp` timestamp NOT NULL, `data` json NOT NULL, PRIMARY KEY (`id`))", @@ -212,7 +238,7 @@ func TestPlanChanges_DropTable(t *testing.T) { }, }, wantPlan: &migrate.Plan{ - Transactional: true, + Transactional: false, Changes: []*migrate.Change{ { Cmd: "DROP TABLE `users`", @@ -238,7 +264,7 @@ func TestPlanChanges_DropTable(t *testing.T) { }, }, wantPlan: &migrate.Plan{ - Transactional: true, + Transactional: false, Changes: []*migrate.Change{ { Cmd: "DROP TABLE IF EXISTS `users`", @@ -361,7 +387,7 @@ func TestPlanChanges_AddColumn(t *testing.T) { }, }, wantPlan: &migrate.Plan{ - Transactional: true, + Transactional: false, Changes: []*migrate.Change{ { Cmd: "ALTER TABLE `users` ADD COLUMN `email` utf8 NOT NULL", @@ -384,7 +410,7 @@ func TestPlanChanges_AddColumn(t *testing.T) { }, }, wantPlan: &migrate.Plan{ - Transactional: true, + Transactional: false, Changes: []*migrate.Change{ { Cmd: "ALTER TABLE `users` ADD COLUMN `bio` utf8", @@ -410,7 +436,7 @@ func TestPlanChanges_AddColumn(t *testing.T) { }, }, wantPlan: &migrate.Plan{ - Transactional: true, + Transactional: false, Changes: []*migrate.Change{ { Cmd: "ALTER TABLE `users` ADD COLUMN `email` utf8 NOT NULL, ADD COLUMN `age` int32 NOT NULL", @@ -467,7 +493,7 @@ func TestPlanChanges_DropColumn(t *testing.T) { }, }, wantPlan: &migrate.Plan{ - Transactional: true, + Transactional: false, Changes: []*migrate.Change{ { Cmd: "ALTER TABLE `users` DROP COLUMN `email`", @@ -493,7 +519,7 @@ func TestPlanChanges_DropColumn(t *testing.T) { }, }, wantPlan: &migrate.Plan{ - Transactional: true, + Transactional: false, Changes: []*migrate.Change{ { Cmd: "ALTER TABLE `users` DROP COLUMN `name`, DROP COLUMN `email`", @@ -550,7 +576,7 @@ func TestPlanChanges_AddIndex(t *testing.T) { }, }, wantPlan: &migrate.Plan{ - Transactional: true, + Transactional: false, Changes: []*migrate.Change{ { Cmd: "ALTER TABLE `users` ADD INDEX `idx_name` GLOBAL SYNC ON (`name`)", @@ -573,7 +599,7 @@ func TestPlanChanges_AddIndex(t *testing.T) { }, }, wantPlan: &migrate.Plan{ - Transactional: true, + Transactional: false, Changes: []*migrate.Change{ { Cmd: "ALTER TABLE `users` ADD INDEX `idx_name_email` GLOBAL SYNC ON (`name`, `email`)", @@ -583,6 +609,103 @@ func TestPlanChanges_AddIndex(t *testing.T) { }, }, }, + { + name: "add async index", + changes: []schema.Change{ + &schema.ModifyTable{ + T: usersTable, + Changes: []schema.Change{ + &schema.AddIndex{ + I: func() *schema.Index { + idx := schema.NewIndex("idx_name_async").AddColumns(usersTable.Columns[1]) + idx.Attrs = append(idx.Attrs, &IndexAttributes{Async: true}) + return idx + }(), + }, + }, + }, + }, + wantPlan: &migrate.Plan{ + Transactional: false, + Changes: []*migrate.Change{ + { + Cmd: "ALTER TABLE `users` ADD INDEX `idx_name_async` GLOBAL ASYNC ON (`name`)", + Reverse: "ALTER TABLE `users` DROP INDEX `idx_name_async`", + Comment: `create index "idx_name_async" to table: "users"`, + }, + }, + }, + }, + { + name: "add index with cover columns", + changes: []schema.Change{ + &schema.ModifyTable{ + T: usersTable, + Changes: []schema.Change{ + &schema.AddIndex{ + I: func() *schema.Index { + idx := schema.NewIndex("idx_name_cover").AddColumns(usersTable.Columns[1]) + idx.Attrs = append( + idx.Attrs, + &IndexAttributes{ + CoverColumns: []*schema.Column{ + {Name: "email"}, + }, + }, + ) + return idx + }(), + }, + }, + }, + }, + wantPlan: &migrate.Plan{ + Transactional: false, + Changes: []*migrate.Change{ + { + Cmd: "ALTER TABLE `users` ADD INDEX `idx_name_cover` GLOBAL SYNC ON (`name`) COVER (`email`)", + Reverse: "ALTER TABLE `users` DROP INDEX `idx_name_cover`", + Comment: `create index "idx_name_cover" to table: "users"`, + }, + }, + }, + }, + { + name: "add async index with cover columns", + changes: []schema.Change{ + &schema.ModifyTable{ + T: usersTable, + Changes: []schema.Change{ + &schema.AddIndex{ + I: func() *schema.Index { + idx := schema.NewIndex("idx_name_async_cover").AddColumns(usersTable.Columns[1]) + idx.Attrs = append( + idx.Attrs, + &IndexAttributes{ + Async: true, + CoverColumns: []*schema.Column{ + {Name: "email"}, + {Name: "id"}, + }, + }, + ) + return idx + }(), + }, + }, + }, + }, + wantPlan: &migrate.Plan{ + Transactional: false, + Changes: []*migrate.Change{ + { + Cmd: "ALTER TABLE `users` ADD INDEX `idx_name_async_cover` GLOBAL ASYNC ON (`name`) COVER (`email`, `id`)", + Reverse: "ALTER TABLE `users` DROP INDEX `idx_name_async_cover`", + Comment: `create index "idx_name_async_cover" to table: "users"`, + }, + }, + }, + }, { name: "add multiple indexes", changes: []schema.Change{ @@ -599,7 +722,7 @@ func TestPlanChanges_AddIndex(t *testing.T) { }, }, wantPlan: &migrate.Plan{ - Transactional: true, + Transactional: false, Changes: []*migrate.Change{ { Cmd: "ALTER TABLE `users` ADD INDEX `idx_name` GLOBAL SYNC ON (`name`)", @@ -661,7 +784,7 @@ func TestPlanChanges_DropIndex(t *testing.T) { }, }, wantPlan: &migrate.Plan{ - Transactional: true, + Transactional: false, Changes: []*migrate.Change{ { Cmd: "ALTER TABLE `users` DROP INDEX `idx_name`", @@ -687,7 +810,7 @@ func TestPlanChanges_DropIndex(t *testing.T) { }, }, wantPlan: &migrate.Plan{ - Transactional: true, + Transactional: false, Changes: []*migrate.Change{ { Cmd: "ALTER TABLE `users` DROP INDEX `idx_name`", @@ -702,6 +825,67 @@ func TestPlanChanges_DropIndex(t *testing.T) { }, }, }, + { + name: "drop async index", + changes: []schema.Change{ + &schema.ModifyTable{ + T: usersTable, + Changes: []schema.Change{ + &schema.DropIndex{ + I: func() *schema.Index { + idx := schema.NewIndex("idx_name_async").AddColumns(usersTable.Columns[1]) + idx.Attrs = append(idx.Attrs, &IndexAttributes{Async: true}) + return idx + }(), + }, + }, + }, + }, + wantPlan: &migrate.Plan{ + Transactional: false, + Changes: []*migrate.Change{ + { + Cmd: "ALTER TABLE `users` DROP INDEX `idx_name_async`", + Reverse: "ALTER TABLE `users` ADD INDEX `idx_name_async` GLOBAL ASYNC ON (`name`)", + Comment: `drop index "idx_name_async" from table: "users"`, + }, + }, + }, + }, + { + name: "drop index with cover columns", + changes: []schema.Change{ + &schema.ModifyTable{ + T: usersTable, + Changes: []schema.Change{ + &schema.DropIndex{ + I: func() *schema.Index { + idx := schema.NewIndex("idx_name_cover").AddColumns(usersTable.Columns[1]) + idx.Attrs = append( + idx.Attrs, + &IndexAttributes{ + CoverColumns: []*schema.Column{ + {Name: "email"}, + }, + }, + ) + return idx + }(), + }, + }, + }, + }, + wantPlan: &migrate.Plan{ + Transactional: false, + Changes: []*migrate.Change{ + { + Cmd: "ALTER TABLE `users` DROP INDEX `idx_name_cover`", + Reverse: "ALTER TABLE `users` ADD INDEX `idx_name_cover` GLOBAL SYNC ON (`name`) COVER (`email`)", + Comment: `drop index "idx_name_cover" from table: "users"`, + }, + }, + }, + }, } for _, tt := range tests { @@ -750,7 +934,7 @@ func TestPlanChanges_ModifyIndex(t *testing.T) { }, }, wantPlan: &migrate.Plan{ - Transactional: true, + Transactional: false, Changes: []*migrate.Change{ { Cmd: "ALTER TABLE `users` DROP INDEX `idx_name`", @@ -813,7 +997,7 @@ func TestPlanChanges_RenameIndex(t *testing.T) { }, }, wantPlan: &migrate.Plan{ - Transactional: true, + Transactional: false, Changes: []*migrate.Change{ { Cmd: "ALTER TABLE `users` RENAME INDEX `idx_name` TO `idx_user_name`", diff --git a/sql/ydb/types.go b/sql/ydb/types.go index 53d4b4175ad..78a38bcccba 100644 --- a/sql/ydb/types.go +++ b/sql/ydb/types.go @@ -7,6 +7,8 @@ package ydb import ( + "errors" + "ariga.io/atlas/sql/schema" ) @@ -81,10 +83,15 @@ type ( ) // Creates [SerialType] from corresponding [schema.IntegerType] -func SerialFromInt(intType *schema.IntegerType) *SerialType { +func SerialFromInt(intType *schema.IntegerType) (*SerialType, error) { serialType := &SerialType{} - serialType.SetType(intType) - return serialType + + err := serialType.SetType(intType) + if err != nil { + return nil, err + } + + return serialType, nil } // Converts [SerialType] to corresponding [schema.IntegerType] @@ -102,7 +109,7 @@ func (s *SerialType) IntegerType() *schema.IntegerType { } // Sets [schema.IntegerType] as base underlying type for [SerialType] -func (s *SerialType) SetType(t *schema.IntegerType) { +func (s *SerialType) SetType(t *schema.IntegerType) error { switch t.T { case TypeInt8, TypeInt16: s.T = TypeSerial2 @@ -110,5 +117,8 @@ func (s *SerialType) SetType(t *schema.IntegerType) { s.T = TypeSerial4 case TypeInt64: s.T = TypeSerial8 + default: + return errors.New("ydb: auto increment only supports signed integers") } + return nil }