From 6fb703b291c9806f3ec85175d8497ffb4913e7ce Mon Sep 17 00:00:00 2001 From: danilov6083 Date: Thu, 15 Jan 2026 23:21:08 +0300 Subject: [PATCH 01/15] sql/ydb: fixed empty database path --- sql/ydb/driver.go | 1 + sql/ydb/inspect.go | 5 +++-- sql/ydb/inspect_test.go | 2 +- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/sql/ydb/driver.go b/sql/ydb/driver.go index 36295cc89c5..25a1f93c8e4 100644 --- a/sql/ydb/driver.go +++ b/sql/ydb/driver.go @@ -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..445c8af7950 100644 --- a/sql/ydb/inspect.go +++ b/sql/ydb/inspect.go @@ -21,7 +21,8 @@ import ( // inspect provides a YDB implementation for schema.Inspector. type inspect struct { - database string + *conn + schemeClient scheme.Client tableClient table.Client } @@ -29,7 +30,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(), } diff --git a/sql/ydb/inspect_test.go b/sql/ydb/inspect_test.go index 81107e9a120..530e13953b2 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, } From 97382bf16113764ba3757139ce5fc0e2183a44ce Mon Sep 17 00:00:00 2001 From: danilov6083 Date: Thu, 15 Jan 2026 23:38:49 +0300 Subject: [PATCH 02/15] sql/ydb: fixed type conversion --- sql/ydb/convert.go | 4 ++-- sql/ydb/convert_test.go | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/sql/ydb/convert.go b/sql/ydb/convert.go index abfeacdda91..051cc1939b9 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 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}, From 566063862c025a6e0712db277d40106212e2052b Mon Sep 17 00:00:00 2001 From: danilov6083 Date: Thu, 15 Jan 2026 23:56:34 +0300 Subject: [PATCH 03/15] sql/ydb: handled case when unsigned int is passed to serial conversion --- sql/ydb/types.go | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) 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 } From 8b32657a1f1fd9f77f5fdd738ebde6e34142820e Mon Sep 17 00:00:00 2001 From: danilov6083 Date: Fri, 16 Jan 2026 00:15:35 +0300 Subject: [PATCH 04/15] sql/ydb: turned off transactional ddl for ydb --- sql/ydb/migrate.go | 2 +- sql/ydb/migrate_test.go | 38 +++++++++++++++++++------------------- 2 files changed, 20 insertions(+), 20 deletions(-) diff --git a/sql/ydb/migrate.go b/sql/ydb/migrate.go index 8b5580d9ed1..c618d2aa38f 100644 --- a/sql/ydb/migrate.go +++ b/sql/ydb/migrate.go @@ -35,7 +35,7 @@ func (p *planApply) PlanChanges( conn: p.conn, Plan: migrate.Plan{ Name: name, - Transactional: true, + Transactional: false, }, } for _, opt := range opts { diff --git a/sql/ydb/migrate_test.go b/sql/ydb/migrate_test.go index 180596e0b4d..5472bf856ee 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`))", @@ -159,7 +159,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 +212,7 @@ func TestPlanChanges_DropTable(t *testing.T) { }, }, wantPlan: &migrate.Plan{ - Transactional: true, + Transactional: false, Changes: []*migrate.Change{ { Cmd: "DROP TABLE `users`", @@ -238,7 +238,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 +361,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 +384,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 +410,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 +467,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 +493,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 +550,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 +573,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`)", @@ -599,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` GLOBAL SYNC ON (`name`)", @@ -661,7 +661,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 +687,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`", @@ -750,7 +750,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 +813,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`", From a1388088e96c401f44751ba23ad801510b5c14e6 Mon Sep 17 00:00:00 2001 From: danilov6083 Date: Fri, 16 Jan 2026 00:50:47 +0300 Subject: [PATCH 05/15] sql/ydb: tried to fix ydb query mode --- sql/ydb/migrate.go | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/sql/ydb/migrate.go b/sql/ydb/migrate.go index c618d2aa38f..405aaf29a64 100644 --- a/sql/ydb/migrate.go +++ b/sql/ydb/migrate.go @@ -14,12 +14,15 @@ import ( "ariga.io/atlas/sql/internal/sqlx" "ariga.io/atlas/sql/migrate" "ariga.io/atlas/sql/schema" + ydbSdk "github.com/ydb-platform/ydb-go-sdk/v3" ) // 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 } @@ -58,7 +61,9 @@ func (p *planApply) ApplyChanges( changes []schema.Change, opts ...migrate.PlanOption, ) error { - return sqlx.ApplyChanges(ctx, changes, p, opts...) + // YDB requires DDL statements to be executed via scheme queries + queryModeCtx := ydbSdk.WithQueryMode(ctx, ydbSdk.SchemeQueryMode) + return sqlx.ApplyChanges(queryModeCtx, changes, p, opts...) } // state represents the state of a planning. It is not part of From e6ef71cd053bfd0d86ffb34e91ff03ed5558d64f Mon Sep 17 00:00:00 2001 From: danilov6083 Date: Fri, 16 Jan 2026 15:07:22 +0300 Subject: [PATCH 06/15] sql/ydb: enabled query service for database/sql driver --- sql/ydb/driver.go | 1 + sql/ydb/migrate.go | 5 +---- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/sql/ydb/driver.go b/sql/ydb/driver.go index 25a1f93c8e4..95f8418f0eb 100644 --- a/sql/ydb/driver.go +++ b/sql/ydb/driver.go @@ -75,6 +75,7 @@ func opener(ctx context.Context, dsn *url.URL) (*sqlclient.Client, error) { nativeDriver, ydbSdk.WithAutoDeclare(), ydbSdk.WithTablePathPrefix(nativeDriver.Name()), + ydbSdk.WithQueryService(true), ) if err != nil { return nil, err diff --git a/sql/ydb/migrate.go b/sql/ydb/migrate.go index 405aaf29a64..979f96aa2e9 100644 --- a/sql/ydb/migrate.go +++ b/sql/ydb/migrate.go @@ -14,7 +14,6 @@ import ( "ariga.io/atlas/sql/internal/sqlx" "ariga.io/atlas/sql/migrate" "ariga.io/atlas/sql/schema" - ydbSdk "github.com/ydb-platform/ydb-go-sdk/v3" ) // DefaultPlan provides basic planning capabilities for YDB dialect. @@ -61,9 +60,7 @@ func (p *planApply) ApplyChanges( changes []schema.Change, opts ...migrate.PlanOption, ) error { - // YDB requires DDL statements to be executed via scheme queries - queryModeCtx := ydbSdk.WithQueryMode(ctx, ydbSdk.SchemeQueryMode) - return sqlx.ApplyChanges(queryModeCtx, changes, p, opts...) + return sqlx.ApplyChanges(ctx, changes, p, opts...) } // state represents the state of a planning. It is not part of From 971b6a07d21af992fdcef588eaab353c11f64c09 Mon Sep 17 00:00:00 2001 From: danilov6083 Date: Mon, 19 Jan 2026 22:10:37 +0300 Subject: [PATCH 07/15] sql/ydb: removed sync index type and added cover columns --- sql/ydb/attributes.go | 6 +- sql/ydb/migrate.go | 52 +++++++------ sql/ydb/migrate_test.go | 158 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 190 insertions(+), 26 deletions(-) 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/migrate.go b/sql/ydb/migrate.go index 979f96aa2e9..205350b498a 100644 --- a/sql/ydb/migrate.go +++ b/sql/ydb/migrate.go @@ -290,30 +290,30 @@ func (s *state) addIndexes(src schema.Change, t *schema.Table, indexes ...*schem indexAttrs := IndexAttributes{} hasAttrs := sqlx.Has(index.Attrs, &indexAttrs) - b := s.Build("ALTER TABLE"). + builder := s.Build("ALTER TABLE"). Table(t). P("ADD INDEX"). - Ident(index.Name) - - if hasAttrs && !indexAttrs.Global { - b.P("LOCAL") - } else { - b.P("GLOBAL") - } + Ident(index.Name). + P("GLOBAL") if index.Unique { - b.P("UNIQUE") + builder.P("UNIQUE") } - if hasAttrs && !indexAttrs.Sync { - b.P("ASYNC") + if hasAttrs && indexAttrs.Async { + builder.P("ASYNC") } else { - b.P("SYNC") + builder.P("SYNC") } - b.P("ON") + builder.P("ON") + + s.indexParts(builder, index.Parts) - s.indexParts(b, index.Parts) + if hasAttrs && len(indexAttrs.CoverColumns) > 0 { + builder.P("COVER") + s.indexCoverColumns(builder, indexAttrs.CoverColumns) + } reverseOp := s.Build("ALTER TABLE"). Table(t). @@ -322,7 +322,7 @@ func (s *state) addIndexes(src schema.Change, t *schema.Table, indexes ...*schem 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), Reverse: reverseOp, @@ -396,19 +396,25 @@ func (s *state) indexDef(b *sqlx.Builder, idx *schema.Index) { } // 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) + }) + }) +} + // append adds changes to the plan. func (s *state) append(c ...*migrate.Change) { s.Changes = append(s.Changes, c...) diff --git a/sql/ydb/migrate_test.go b/sql/ydb/migrate_test.go index 5472bf856ee..7d80ba4895a 100644 --- a/sql/ydb/migrate_test.go +++ b/sql/ydb/migrate_test.go @@ -583,6 +583,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{ @@ -702,6 +799,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 { From 7b003c4facaf14d40d20126e8ded9c6ac1a4eb18 Mon Sep 17 00:00:00 2001 From: danilov6083 Date: Tue, 20 Jan 2026 22:18:10 +0300 Subject: [PATCH 08/15] sl/ydb: fixed database path handling and table creation --- sql/ydb/driver.go | 1 - sql/ydb/inspect.go | 12 ++++++++++-- sql/ydb/inspect_test.go | 20 ++++++++++---------- sql/ydb/migrate.go | 32 ++++++++++++++++++++++---------- sql/ydb/migrate_test.go | 26 ++++++++++++++++++++++++++ 5 files changed, 68 insertions(+), 23 deletions(-) diff --git a/sql/ydb/driver.go b/sql/ydb/driver.go index 95f8418f0eb..eeee315bbac 100644 --- a/sql/ydb/driver.go +++ b/sql/ydb/driver.go @@ -74,7 +74,6 @@ 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 { diff --git a/sql/ydb/inspect.go b/sql/ydb/inspect.go index 445c8af7950..fc367fb027b 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" @@ -138,7 +139,11 @@ func (i *inspect) inspectTables( return err } for _, table := range schema.Tables { - tableDesc, err := i.tableClient.DescribeTable(ctx, table.Name) + // table.Name is a relative path (e.g., "users" or "dir1/users"), + // but DescribeTable needs the full path (e.g., "/local/users"). + // schema.Name contains the database path (e.g., "/local"). + fullPath := schema.Name + "/" + table.Name + tableDesc, err := i.tableClient.DescribeTable(ctx, fullPath) if err != nil { return fmt.Errorf("ydb: failed describe table: %v", err) } @@ -182,12 +187,15 @@ 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) + t := schema.NewTable(relativePath) s.AddTables(t) } diff --git a/sql/ydb/inspect_test.go b/sql/ydb/inspect_test.go index 530e13953b2..463836f77d8 100644 --- a/sql/ydb/inspect_test.go +++ b/sql/ydb/inspect_test.go @@ -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 205350b498a..d054fbdb1db 100644 --- a/sql/ydb/migrate.go +++ b/sql/ydb/migrate.go @@ -97,12 +97,24 @@ func (s *state) plan(changes []schema.Change) error { return nil } +// tablePath returns the full YDB path for a table. +func (s *state) tablePath(t *schema.Table) string { + if s.database == "" { + return t.Name + } + return s.database + "/" + t.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 { @@ -127,7 +139,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{ @@ -154,7 +166,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 { @@ -235,7 +247,7 @@ func (s *state) alterTable(t *schema.Table, changes []schema.Change) error { var reverse []schema.Change buildFunc := func(changes []schema.Change) (string, error) { - b := s.Build("ALTER TABLE").Table(t) + b := s.Build("ALTER TABLE").Ident(s.tablePath(t)) err := b.MapCommaErr(changes, func(i int, builder *sqlx.Builder) error { switch change := changes[i].(type) { @@ -291,7 +303,7 @@ func (s *state) addIndexes(src schema.Change, t *schema.Table, indexes ...*schem hasAttrs := sqlx.Has(index.Attrs, &indexAttrs) builder := s.Build("ALTER TABLE"). - Table(t). + Ident(s.tablePath(t)). P("ADD INDEX"). Ident(index.Name). P("GLOBAL") @@ -316,7 +328,7 @@ func (s *state) addIndexes(src schema.Change, t *schema.Table, indexes ...*schem } reverseOp := s.Build("ALTER TABLE"). - Table(t). + Ident(s.tablePath(t)). P("DROP INDEX"). Ident(index.Name). String() @@ -359,8 +371,8 @@ func (s *state) renameTable(c *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(), + Cmd: s.Build("ALTER TABLE").Ident(s.tablePath(c.From)).P("RENAME TO").Ident(s.tablePath(c.To)).String(), + Reverse: s.Build("ALTER TABLE").Ident(s.tablePath(c.To)).P("RENAME TO").Ident(s.tablePath(c.From)).String(), }) } @@ -369,8 +381,8 @@ func (s *state) renameIndex(modify *schema.ModifyTable, c *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(), + Cmd: s.Build("ALTER TABLE").Ident(s.tablePath(modify.T)).P("RENAME INDEX").Ident(c.From.Name).P("TO").Ident(c.To.Name).String(), + Reverse: s.Build("ALTER TABLE").Ident(s.tablePath(modify.T)).P("RENAME INDEX").Ident(c.To.Name).P("TO").Ident(c.From.Name).String(), }) } diff --git a/sql/ydb/migrate_test.go b/sql/ydb/migrate_test.go index 7d80ba4895a..dd2de1d4235 100644 --- a/sql/ydb/migrate_test.go +++ b/sql/ydb/migrate_test.go @@ -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{ From 9920761fb1b12bac478fccc556fd477f2e19627b Mon Sep 17 00:00:00 2001 From: danilov6083 Date: Fri, 23 Jan 2026 20:17:58 +0300 Subject: [PATCH 09/15] sql/ydb: code style fixes --- sql/ydb/convert.go | 46 +++++++------- sql/ydb/diff.go | 6 +- sql/ydb/driver.go | 14 ++--- sql/ydb/inspect.go | 24 ++++--- sql/ydb/migrate.go | 151 +++++++++++++++++++++++++-------------------- 5 files changed, 133 insertions(+), 108 deletions(-) diff --git a/sql/ydb/convert.go b/sql/ydb/convert.go index 051cc1939b9..a4b62855180 100644 --- a/sql/ydb/convert.go +++ b/sql/ydb/convert.go @@ -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/diff.go b/sql/ydb/diff.go index 16e90dc28d5..888cea4042d 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 { diff --git a/sql/ydb/driver.go b/sql/ydb/driver.go index eeee315bbac..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 } @@ -81,7 +81,7 @@ func opener(ctx context.Context, dsn *url.URL) (*sqlclient.Client, error) { } 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 } diff --git a/sql/ydb/inspect.go b/sql/ydb/inspect.go index fc367fb027b..9d5fec422d1 100644 --- a/sql/ydb/inspect.go +++ b/sql/ydb/inspect.go @@ -40,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 @@ -104,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 @@ -139,10 +145,8 @@ func (i *inspect) inspectTables( return err } for _, table := range schema.Tables { - // table.Name is a relative path (e.g., "users" or "dir1/users"), - // but DescribeTable needs the full path (e.g., "/local/users"). - // schema.Name contains the database path (e.g., "/local"). fullPath := schema.Name + "/" + table.Name + tableDesc, err := i.tableClient.DescribeTable(ctx, fullPath) if err != nil { return fmt.Errorf("ydb: failed describe table: %v", err) @@ -165,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) @@ -196,7 +204,7 @@ func (i *inspect) tables(ctx context.Context, s *schema.Schema, opts *schema.Ins if shouldAdd { t := schema.NewTable(relativePath) - s.AddTables(t) + schem.AddTables(t) } case scheme.EntryDirectory: diff --git a/sql/ydb/migrate.go b/sql/ydb/migrate.go index d054fbdb1db..68c442e7aaf 100644 --- a/sql/ydb/migrate.go +++ b/sql/ydb/migrate.go @@ -98,11 +98,11 @@ func (s *state) plan(changes []schema.Change) error { } // tablePath returns the full YDB path for a table. -func (s *state) tablePath(t *schema.Table) string { +func (s *state) tablePath(table *schema.Table) string { if s.database == "" { - return t.Name + return table.Name } - return s.database + "/" + t.Name + return s.database + "/" + table.Name } // addTable builds and executes the query for creating a table in a schema. @@ -116,17 +116,22 @@ func (s *state) addTable(addTable *schema.AddTable) error { 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() @@ -243,67 +248,70 @@ 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").Ident(s.tablePath(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) builder := s.Build("ALTER TABLE"). - Ident(s.tablePath(t)). + Ident(s.tablePath(table)). P("ADD INDEX"). Ident(index.Name). P("GLOBAL") @@ -328,7 +336,7 @@ func (s *state) addIndexes(src schema.Change, t *schema.Table, indexes ...*schem } reverseOp := s.Build("ALTER TABLE"). - Ident(s.tablePath(t)). + Ident(s.tablePath(table)). P("DROP INDEX"). Ident(index.Name). String() @@ -336,21 +344,24 @@ func (s *state) addIndexes(src schema.Change, t *schema.Table, indexes ...*schem s.append(&migrate.Change{ 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 } @@ -358,7 +369,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, }) } @@ -367,63 +378,69 @@ 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").Ident(s.tablePath(c.From)).P("RENAME TO").Ident(s.tablePath(c.To)).String(), - Reverse: s.Build("ALTER TABLE").Ident(s.tablePath(c.To)).P("RENAME TO").Ident(s.tablePath(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").Ident(s.tablePath(modify.T)).P("RENAME INDEX").Ident(c.From.Name).P("TO").Ident(c.To.Name).String(), - Reverse: s.Build("ALTER TABLE").Ident(s.tablePath(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, idx *schema.Index) { + builder.P("INDEX").Ident(idx.Name).P("GLOBAL ON") + s.indexParts(builder, idx.Parts) } // indexParts writes the index parts (columns) to the builder. 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) - } - }) + 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) - }) + b.MapComma( + coverColumns, + func(i int, builder *sqlx.Builder) { + builder.Ident(coverColumns[i].Name) + }, + ) }) } From d43ce9389c846d95dc99ee8f267aea135ea1882e Mon Sep 17 00:00:00 2001 From: danilov6083 Date: Sat, 24 Jan 2026 01:13:05 +0300 Subject: [PATCH 10/15] sql/ydb: added index diff logic --- sql/ydb/diff.go | 29 +++++++- sql/ydb/diff_test.go | 165 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 191 insertions(+), 3 deletions(-) diff --git a/sql/ydb/diff.go b/sql/ydb/diff.go index 888cea4042d..93ab90c2779 100644 --- a/sql/ydb/diff.go +++ b/sql/ydb/diff.go @@ -128,13 +128,36 @@ func (d *diff) defaultChanged(from *schema.Column, 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 fromA, toA IndexAttributes + fromHas, toHas := sqlx.Has(from, &fromA), sqlx.Has(to, &toA) + if fromHas != toHas { + return true + } + if !fromHas { + return false + } + + if fromA.Async != toA.Async { + return true + } + + if len(fromA.CoverColumns) != len(toA.CoverColumns) { + return true + } + + for i := range fromA.CoverColumns { + if fromA.CoverColumns[i].Name != toA.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) + }) + } +} From c3bfab89267d5f7ef9e193841bf003ba2ebb0499 Mon Sep 17 00:00:00 2001 From: danilov6083 Date: Sat, 24 Jan 2026 01:25:51 +0300 Subject: [PATCH 11/15] sql/ydb: fixed index introspection --- sql/ydb/diff.go | 17 +++++++++-------- sql/ydb/inspect.go | 14 ++++++++++++++ 2 files changed, 23 insertions(+), 8 deletions(-) diff --git a/sql/ydb/diff.go b/sql/ydb/diff.go index 93ab90c2779..86101383b55 100644 --- a/sql/ydb/diff.go +++ b/sql/ydb/diff.go @@ -129,25 +129,26 @@ func (d *diff) defaultChanged(from *schema.Column, to *schema.Column) bool { // IndexAttrChanged reports if the index attributes were changed. func (*diff) IndexAttrChanged(from, to []schema.Attr) bool { - var fromA, toA IndexAttributes - fromHas, toHas := sqlx.Has(from, &fromA), sqlx.Has(to, &toA) - if fromHas != toHas { + var fromAttrs, toAttrs IndexAttributes + hasFrom, hasTo := sqlx.Has(from, &fromAttrs), sqlx.Has(to, &toAttrs) + + if hasFrom != hasTo { return true } - if !fromHas { + if !hasFrom { return false } - if fromA.Async != toA.Async { + if fromAttrs.Async != toAttrs.Async { return true } - if len(fromA.CoverColumns) != len(toA.CoverColumns) { + if len(fromAttrs.CoverColumns) != len(toAttrs.CoverColumns) { return true } - for i := range fromA.CoverColumns { - if fromA.CoverColumns[i].Name != toA.CoverColumns[i].Name { + for i := range fromAttrs.CoverColumns { + if fromAttrs.CoverColumns[i].Name != toAttrs.CoverColumns[i].Name { return true } } diff --git a/sql/ydb/inspect.go b/sql/ydb/inspect.go index 9d5fec422d1..f91f94eeef4 100644 --- a/sql/ydb/inspect.go +++ b/sql/ydb/inspect.go @@ -308,6 +308,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) + } + if indexAttrs.Async || len(indexAttrs.CoverColumns) > 0 { + atlasIdx.Attrs = append(atlasIdx.Attrs, indexAttrs) + } + table.AddIndexes(atlasIdx) } From 9cba6f2e412f89e190a036bb650d905abf797afb Mon Sep 17 00:00:00 2001 From: danilov6083 Date: Sat, 24 Jan 2026 01:31:07 +0300 Subject: [PATCH 12/15] sql/ydb: tried to fix diff logic again --- sql/ydb/diff.go | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/sql/ydb/diff.go b/sql/ydb/diff.go index 86101383b55..c249f352363 100644 --- a/sql/ydb/diff.go +++ b/sql/ydb/diff.go @@ -130,14 +130,8 @@ func (d *diff) defaultChanged(from *schema.Column, to *schema.Column) bool { // IndexAttrChanged reports if the index attributes were changed. func (*diff) IndexAttrChanged(from, to []schema.Attr) bool { var fromAttrs, toAttrs IndexAttributes - hasFrom, hasTo := sqlx.Has(from, &fromAttrs), sqlx.Has(to, &toAttrs) - - if hasFrom != hasTo { - return true - } - if !hasFrom { - return false - } + sqlx.Has(from, &fromAttrs) + sqlx.Has(to, &toAttrs) if fromAttrs.Async != toAttrs.Async { return true From d835b0a126d469d069e273be1384e62ea5b973f6 Mon Sep 17 00:00:00 2001 From: danilov6083 Date: Sat, 24 Jan 2026 02:23:36 +0300 Subject: [PATCH 13/15] sql/ydb: fixed introspection of index uniqueness --- go.mod | 2 ++ go.sum | 4 ++-- sql/ydb/inspect.go | 11 ++++++----- 3 files changed, 10 insertions(+), 7 deletions(-) diff --git a/go.mod b/go.mod index bdad6a316be..86c7911b01c 100644 --- a/go.mod +++ b/go.mod @@ -42,3 +42,5 @@ require ( google.golang.org/grpc v1.78.0 // indirect google.golang.org/protobuf v1.36.11 // indirect ) + +replace github.com/ydb-platform/ydb-go-sdk/v3 => github.com/LostImagin4tion/ydb-go-sdk/v3 v3.0.1-lostimagin4tion diff --git a/go.sum b/go.sum index d12f9e83c9f..fd1ce739546 100644 --- a/go.sum +++ b/go.sum @@ -3,6 +3,8 @@ cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMT github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/DATA-DOG/go-sqlmock v1.5.0 h1:Shsta01QNfFxHCfpW6YH2STWB0MudeXXEWMr20OEh60= github.com/DATA-DOG/go-sqlmock v1.5.0/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM= +github.com/LostImagin4tion/ydb-go-sdk/v3 v3.0.1-lostimagin4tion h1:vGGqY93e5JImp1Wnt7+8brPDvir/u1m/3NqacWeJYVQ= +github.com/LostImagin4tion/ydb-go-sdk/v3 v3.0.1-lostimagin4tion/go.mod h1:stS1mQYjbJvwwYaYzKyFY9eMiuVXWWXQA6T+SpOLg9c= github.com/agext/levenshtein v1.2.1 h1:QmvMAjj2aEICytGiWzmxoE0x2KZvE0fvmqMOfy2tjT8= github.com/agext/levenshtein v1.2.1/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558= github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= @@ -107,8 +109,6 @@ 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/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/inspect.go b/sql/ydb/inspect.go index f91f94eeef4..dd3038faab5 100644 --- a/sql/ydb/inspect.go +++ b/sql/ydb/inspect.go @@ -293,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 { @@ -311,6 +312,7 @@ func (i *inspect) indexes( indexAttrs := &IndexAttributes{ Async: idx.Type == options.GlobalAsyncIndex(), } + for _, dataCol := range idx.DataColumns { column, ok := table.Column(dataCol) if !ok { @@ -318,9 +320,8 @@ func (i *inspect) indexes( } indexAttrs.CoverColumns = append(indexAttrs.CoverColumns, column) } - if indexAttrs.Async || len(indexAttrs.CoverColumns) > 0 { - atlasIdx.Attrs = append(atlasIdx.Attrs, indexAttrs) - } + + atlasIdx.Attrs = append(atlasIdx.Attrs, indexAttrs) table.AddIndexes(atlasIdx) } From 549e243aa30a250ce11c85c2918cb6441052a35d Mon Sep 17 00:00:00 2001 From: danilov6083 Date: Sat, 24 Jan 2026 03:36:28 +0300 Subject: [PATCH 14/15] sql/ydb: fixed index definition in create table (i hope so....) --- sql/ydb/migrate.go | 55 ++++++++++++++++++++++++++-------------------- 1 file changed, 31 insertions(+), 24 deletions(-) diff --git a/sql/ydb/migrate.go b/sql/ydb/migrate.go index 68c442e7aaf..ade3b8678e8 100644 --- a/sql/ydb/migrate.go +++ b/sql/ydb/migrate.go @@ -307,33 +307,13 @@ func (s *state) alterTable(table *schema.Table, changes []schema.Change) 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) builder := s.Build("ALTER TABLE"). Ident(s.tablePath(table)). P("ADD INDEX"). - Ident(index.Name). - P("GLOBAL") - - if index.Unique { - builder.P("UNIQUE") - } - - if hasAttrs && indexAttrs.Async { - builder.P("ASYNC") - } else { - builder.P("SYNC") - } - - builder.P("ON") + Ident(index.Name) - s.indexParts(builder, index.Parts) - - if hasAttrs && len(indexAttrs.CoverColumns) > 0 { - builder.P("COVER") - s.indexCoverColumns(builder, indexAttrs.CoverColumns) - } + s.buildIndexSpec(builder, index) reverseOp := s.Build("ALTER TABLE"). Ident(s.tablePath(table)). @@ -413,9 +393,36 @@ func (s *state) column(builder *sqlx.Builder, column *schema.Column) error { } // indexDef writes an inline index definition for CREATE TABLE. -func (s *state) indexDef(builder *sqlx.Builder, idx *schema.Index) { - builder.P("INDEX").Ident(idx.Name).P("GLOBAL ON") +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. From 4211acb955c7647a7699078db735399448552eee Mon Sep 17 00:00:00 2001 From: danilov6083 Date: Wed, 28 Jan 2026 04:02:55 +0300 Subject: [PATCH 15/15] bump ydb-go-sdk --- go.mod | 4 +--- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/go.mod b/go.mod index 86c7911b01c..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 @@ -42,5 +42,3 @@ require ( google.golang.org/grpc v1.78.0 // indirect google.golang.org/protobuf v1.36.11 // indirect ) - -replace github.com/ydb-platform/ydb-go-sdk/v3 => github.com/LostImagin4tion/ydb-go-sdk/v3 v3.0.1-lostimagin4tion diff --git a/go.sum b/go.sum index fd1ce739546..355bb38d781 100644 --- a/go.sum +++ b/go.sum @@ -3,8 +3,6 @@ cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMT github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/DATA-DOG/go-sqlmock v1.5.0 h1:Shsta01QNfFxHCfpW6YH2STWB0MudeXXEWMr20OEh60= github.com/DATA-DOG/go-sqlmock v1.5.0/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM= -github.com/LostImagin4tion/ydb-go-sdk/v3 v3.0.1-lostimagin4tion h1:vGGqY93e5JImp1Wnt7+8brPDvir/u1m/3NqacWeJYVQ= -github.com/LostImagin4tion/ydb-go-sdk/v3 v3.0.1-lostimagin4tion/go.mod h1:stS1mQYjbJvwwYaYzKyFY9eMiuVXWWXQA6T+SpOLg9c= github.com/agext/levenshtein v1.2.1 h1:QmvMAjj2aEICytGiWzmxoE0x2KZvE0fvmqMOfy2tjT8= github.com/agext/levenshtein v1.2.1/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558= github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= @@ -109,6 +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.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=