Skip to content

Commit 0283fe8

Browse files
authored
fix: add workarounds for duckdb's limitation on sequences (#333)
1 parent 0c978f0 commit 0283fe8

File tree

5 files changed

+204
-38
lines changed

5 files changed

+204
-38
lines changed

.github/workflows/bats-tests.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ jobs:
3434
3535
pip3 install "sqlglot[rs]" pyarrow pandas
3636
37-
curl -LJO https://github.com/duckdb/duckdb/releases/download/v1.1.3/duckdb_cli-linux-amd64.zip
37+
curl -LJO https://github.com/duckdb/duckdb/releases/latest/download/duckdb_cli-linux-amd64.zip
3838
unzip duckdb_cli-linux-amd64.zip
3939
chmod +x duckdb
4040
sudo mv duckdb /usr/local/bin
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
name: MySQL Copy Instance Test
2+
3+
on:
4+
push:
5+
branches: [ "main" ]
6+
pull_request:
7+
branches: [ "main" ]
8+
9+
jobs:
10+
copy-instance-test:
11+
runs-on: ubuntu-latest
12+
services:
13+
source:
14+
image: mysql:lts
15+
env:
16+
MYSQL_ROOT_PASSWORD: root
17+
ports:
18+
- 13306:3306
19+
options: >-
20+
--health-cmd="mysqladmin ping"
21+
--health-interval=10s
22+
--health-timeout=5s
23+
--health-retries=3
24+
25+
steps:
26+
- uses: actions/checkout@v4
27+
28+
- name: Set up Go
29+
uses: actions/setup-go@v5
30+
with:
31+
go-version: '1.23'
32+
33+
- name: Set up Python
34+
uses: actions/setup-python@v5
35+
with:
36+
python-version: '3.13'
37+
38+
- name: Install system packages
39+
uses: awalsh128/cache-apt-pkgs-action@latest
40+
with:
41+
packages: libnsl2 # required by MySQL Shell
42+
version: 1.1
43+
44+
- name: Install dependencies
45+
run: |
46+
go get .
47+
48+
pip3 install "sqlglot[rs]"
49+
50+
curl -LJO https://dev.mysql.com/get/Downloads/MySQL-Shell/mysql-shell_9.1.0-1debian12_amd64.deb
51+
sudo dpkg -i ./mysql-shell_9.1.0-1debian12_amd64.deb
52+
53+
- name: Setup test data in source MySQL
54+
run: |
55+
mysqlsh -hlocalhost -P13306 -uroot -proot --sql -e "
56+
CREATE DATABASE testdb;
57+
USE testdb;
58+
CREATE TABLE users (
59+
id INT AUTO_INCREMENT PRIMARY KEY,
60+
name VARCHAR(100)
61+
);
62+
INSERT INTO users (name) VALUES ('test1'), ('test2'), ('test3');
63+
-- Make a gap in the id sequence
64+
INSERT INTO users VALUES (100, 'test100');
65+
INSERT INTO users (name) VALUES ('test101');
66+
67+
-- A table with non-default starting auto_increment value
68+
CREATE TABLE items (
69+
id INT AUTO_INCREMENT PRIMARY KEY,
70+
name VARCHAR(100)
71+
) AUTO_INCREMENT=1000;
72+
73+
INSERT INTO items (name) VALUES ('item1'), ('item2'), ('item3');
74+
"
75+
76+
- name: Build and start MyDuck Server
77+
run: |
78+
go build -v
79+
./myduckserver &
80+
sleep 5
81+
82+
- name: Run copy-instance test
83+
run: |
84+
# Set local_infile to true to allow loading data from files
85+
mysqlsh -uroot --no-password --sql -e "SET GLOBAL local_infile = 1;"
86+
87+
# Copy the data from source MySQL to MyDuck
88+
mysqlsh -hlocalhost -P13306 -uroot -proot \
89+
-- util copy-instance "mysql://root:@127.0.0.1:3306" \
90+
--users false --ignore-version true
91+
92+
# Verify the data was copied
93+
for table in users items; do
94+
mysqlsh -hlocalhost -P13306 -uroot -proot --sql -e "
95+
SELECT * FROM testdb.$table ORDER BY id;
96+
" | tee source_data_$table.tsv
97+
mysqlsh -uroot --no-password --sql -e "
98+
SELECT * FROM testdb.$table ORDER BY id;
99+
" | tee copied_data_$table.tsv
100+
101+
diff source_data_$table.tsv copied_data_$table.tsv
102+
done
103+
104+

catalog/database.go

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -73,18 +73,29 @@ func (d *Database) GetTableInsensitive(ctx *sql.Context, tblName string) (sql.Ta
7373
func (d *Database) tablesInsensitive(ctx *sql.Context, pattern string) ([]*Table, error) {
7474
tables, err := d.findTables(ctx, pattern)
7575
if err != nil {
76+
ctx.GetLogger().WithFields(logrus.Fields{
77+
"catalog": d.catalog,
78+
"database": d.name,
79+
"pattern": pattern,
80+
}).WithError(err).Error("Failed to find tables")
7681
return nil, err
7782
}
7883
for _, t := range tables {
7984
if err := t.withSchema(ctx); err != nil {
85+
ctx.GetLogger().WithFields(logrus.Fields{
86+
"catalog": d.catalog,
87+
"database": d.name,
88+
"pattern": pattern,
89+
"table": t.Name(),
90+
}).WithError(err).Error("Failed to get table schema")
8091
return nil, err
8192
}
8293
}
8394
return tables, nil
8495
}
8596

8697
func (d *Database) findTables(ctx *sql.Context, pattern string) ([]*Table, error) {
87-
rows, err := adapter.QueryCatalog(ctx, "SELECT DISTINCT table_name, comment FROM duckdb_tables() where (database_name = ? and schema_name = ? and table_name ILIKE ?) or (database_name = 'temp' and schema_name = 'main' and table_name ILIKE ?)", d.catalog, d.name, pattern, pattern)
98+
rows, err := adapter.QueryCatalog(ctx, "SELECT DISTINCT table_name, comment FROM duckdb_tables() WHERE (database_name = ? AND schema_name = ? AND table_name ILIKE ?) OR (temporary IS TRUE AND table_name ILIKE ?)", d.catalog, d.name, pattern, pattern)
8899
if err != nil {
89100
return nil, ErrDuckDB.New(err)
90101
}
@@ -113,7 +124,6 @@ func (d *Database) Name() string {
113124
}
114125

115126
func (d *Database) createAllTable(ctx *sql.Context, name string, schema sql.PrimaryKeySchema, collation sql.CollationID, comment string, temporary bool) error {
116-
117127
var columns []string
118128
var columnCommentSQLs []string
119129
var fullTableName string

catalog/table.go

Lines changed: 87 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,14 @@ func getPrimaryKeyOrdinals(ctx *sql.Context, catalogName, dbName, tableName stri
199199
return ordinals
200200
}
201201

202+
func getCreateSequence(temporary bool, sequenceName string) (createStmt, fullName string) {
203+
if temporary {
204+
return `CREATE TEMP SEQUENCE "` + sequenceName + `"`, `temp.main."` + sequenceName + `"`
205+
}
206+
fullName = InternalSchemas.SYS.Schema + `."` + sequenceName + `"`
207+
return `CREATE SEQUENCE ` + fullName, fullName
208+
}
209+
202210
// AddColumn implements sql.AlterableTable.
203211
func (t *Table) AddColumn(ctx *sql.Context, column *sql.Column, order *sql.ColumnOrder) error {
204212
t.mu.Lock()
@@ -215,7 +223,7 @@ func (t *Table) AddColumn(ctx *sql.Context, column *sql.Column, order *sql.Colum
215223
sql := `ALTER TABLE ` + FullTableName(t.db.catalog, t.db.name, t.name) + ` ADD COLUMN ` + QuoteIdentifierANSI(column.Name) + ` ` + typ.name
216224

217225
temporary := t.db.catalog == "temp"
218-
var sequenceName, fullSequenceName string
226+
var sequenceName, fullSequenceName, createSequenceStmt string
219227

220228
if column.Default != nil {
221229
typ.mysql.Default = column.Default.String()
@@ -233,24 +241,13 @@ func (t *Table) AddColumn(ctx *sql.Context, column *sql.Column, order *sql.Colum
233241
return err
234242
}
235243
sequenceName = SequenceNamePrefix + uuid.String()
236-
if temporary {
237-
fullSequenceName = `temp.main."` + sequenceName + `"`
238-
} else {
239-
fullSequenceName = InternalSchemas.SYS.Schema + `."` + sequenceName + `"`
240-
}
244+
createSequenceStmt, fullSequenceName = getCreateSequence(temporary, sequenceName)
245+
sqls = append(sqls, createSequenceStmt)
241246

242247
defaultExpr := `nextval('` + fullSequenceName + `')`
243248
sql += " DEFAULT " + defaultExpr
244249
}
245250

246-
if column.AutoIncrement {
247-
if temporary {
248-
sqls = append(sqls, `CREATE TEMP SEQUENCE "`+sequenceName+`"`)
249-
} else {
250-
sqls = append(sqls, `CREATE SEQUENCE `+fullSequenceName)
251-
}
252-
}
253-
254251
sqls = append(sqls, sql)
255252

256253
// DuckDB does not support constraints in ALTER TABLE ADD COLUMN statement,
@@ -387,7 +384,7 @@ func (t *Table) ModifyColumn(ctx *sql.Context, columnName string, column *sql.Co
387384
tableInfoChanged := false
388385

389386
temporary := t.db.catalog == "temp"
390-
var sequenceName, fullSequenceName string
387+
var sequenceName, fullSequenceName, createSequenceStmt string
391388

392389
// Handle AUTO_INCREMENT changes
393390
if !oldColumn.AutoIncrement && column.AutoIncrement {
@@ -398,17 +395,8 @@ func (t *Table) ModifyColumn(ctx *sql.Context, columnName string, column *sql.Co
398395
return err
399396
}
400397
sequenceName = SequenceNamePrefix + uuid.String()
401-
if temporary {
402-
fullSequenceName = `temp.main."` + sequenceName + `"`
403-
} else {
404-
fullSequenceName = InternalSchemas.SYS.Schema + `."` + sequenceName + `"`
405-
}
406-
407-
if temporary {
408-
sqls = append(sqls, `CREATE TEMP SEQUENCE "`+sequenceName+`"`)
409-
} else {
410-
sqls = append(sqls, `CREATE SEQUENCE `+fullSequenceName)
411-
}
398+
createSequenceStmt, fullSequenceName = getCreateSequence(temporary, sequenceName)
399+
sqls = append(sqls, createSequenceStmt)
412400
sqls = append(sqls, baseSQL+` SET DEFAULT nextval('`+fullSequenceName+`')`)
413401

414402
// Update table comment with sequence info
@@ -782,7 +770,18 @@ func (t *Table) PeekNextAutoIncrementValue(ctx *sql.Context) (uint64, error) {
782770
var val uint64
783771
err := adapter.QueryRowCatalog(ctx, `SELECT currval('`+t.comment.Meta.Sequence+`') + 1`).Scan(&val)
784772
if err != nil {
785-
return 0, ErrDuckDB.New(err)
773+
// https://duckdb.org/docs/sql/statements/create_sequence.html#selecting-the-current-value
774+
// > Note that the nextval function must have already been called before calling currval,
775+
// > otherwise a Serialization Error (sequence is not yet defined in this session) will be thrown.
776+
if !strings.Contains(err.Error(), "sequence is not yet defined in this session") {
777+
return 0, ErrDuckDB.New(err)
778+
}
779+
// If the sequence has not been used yet, we can get the start value from the sequence.
780+
// See getCreateSequence() for the sequence name format.
781+
err = adapter.QueryRowCatalog(ctx, `SELECT start_value FROM duckdb_sequences() WHERE concat(schema_name, '."', sequence_name, '"') = '`+t.comment.Meta.Sequence+`'`).Scan(&val)
782+
if err != nil {
783+
return 0, ErrDuckDB.New(err)
784+
}
786785
}
787786

788787
return val, nil
@@ -834,8 +833,67 @@ func (t *Table) AutoIncrementSetter(ctx *sql.Context) sql.AutoIncrementSetter {
834833

835834
// setAutoIncrementValue is a helper function to update the sequence value
836835
func (t *Table) setAutoIncrementValue(ctx *sql.Context, value uint64) error {
837-
_, err := adapter.ExecCatalog(ctx, `CREATE OR REPLACE SEQUENCE `+t.comment.Meta.Sequence+` START WITH `+strconv.FormatUint(value, 10))
838-
return err
836+
// DuckDB does not support setting the sequence value directly,
837+
// so we need to recreate the sequence with the new start value.
838+
//
839+
// _, err := adapter.ExecCatalog(ctx, `CREATE OR REPLACE SEQUENCE `+t.comment.Meta.Sequence+` START WITH `+strconv.FormatUint(value, 10))
840+
//
841+
// However, `CREATE OR REPLACE` leads to a Dependency Error,
842+
// while `ALTER TABLE ... ALTER COLUMN ... DROP DEFAULT` deos not remove the dependency:
843+
// https://github.com/duckdb/duckdb/issues/15399
844+
// So we create a new sequence with the new start value and change the auto_increment column to use the new sequence.
845+
846+
// Find the column with the auto_increment property
847+
var autoIncrementColumn *sql.Column
848+
for _, column := range t.schema.Schema {
849+
if column.AutoIncrement {
850+
autoIncrementColumn = column
851+
break
852+
}
853+
}
854+
if autoIncrementColumn == nil {
855+
return sql.ErrNoAutoIncrementCol
856+
}
857+
858+
// Generate a random sequence name.
859+
uuid, err := uuid.NewRandom()
860+
if err != nil {
861+
return err
862+
}
863+
sequenceName := SequenceNamePrefix + uuid.String()
864+
865+
// Create a new sequence with the new start value
866+
temporary := t.db.catalog == "temp"
867+
createSequenceStmt, fullSequenceName := getCreateSequence(temporary, sequenceName)
868+
_, err = adapter.Exec(ctx, createSequenceStmt+` START WITH `+strconv.FormatUint(value, 10))
869+
if err != nil {
870+
return ErrDuckDB.New(err)
871+
}
872+
873+
// Update the auto_increment column to use the new sequence
874+
alterStmt := `ALTER TABLE ` + FullTableName(t.db.catalog, t.db.name, t.name) +
875+
` ALTER COLUMN ` + QuoteIdentifierANSI(autoIncrementColumn.Name) +
876+
` SET DEFAULT nextval('` + fullSequenceName + `')`
877+
if _, err = adapter.Exec(ctx, alterStmt); err != nil {
878+
return ErrDuckDB.New(err)
879+
}
880+
881+
// Drop the old sequence
882+
// https://github.com/duckdb/duckdb/issues/15399
883+
// if _, err = adapter.Exec(ctx, "DROP SEQUENCE " + t.comment.Meta.Sequence); err != nil {
884+
// return ErrDuckDB.New(err)
885+
// }
886+
887+
// Update the table comment with the new sequence name
888+
tableInfo := t.comment.Meta
889+
tableInfo.Sequence = fullSequenceName
890+
comment := NewCommentWithMeta(t.comment.Text, tableInfo)
891+
if _, err = adapter.Exec(ctx, `COMMENT ON TABLE `+FullTableName(t.db.catalog, t.db.name, t.name)+` IS '`+comment.Encode()+`'`); err != nil {
892+
return ErrDuckDB.New(err)
893+
}
894+
895+
t.comment.Meta.Sequence = fullSequenceName
896+
return t.withSchema(ctx)
839897
}
840898

841899
// autoIncrementSetter implements the AutoIncrementSetter interface

main_test.go

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1164,12 +1164,6 @@ func TestCreateTable(t *testing.T) {
11641164
"insert_into_t1_(b)_values_(1),_(2)",
11651165
"show_create_table_t1",
11661166
"select_*_from_t1_order_by_b",
1167-
"table_with_auto_increment_table_option",
1168-
"create_table_t1_(i_int)_auto_increment=10;",
1169-
"create_table_t2_(i_int_auto_increment_primary_key)_auto_increment=10;",
1170-
"show_create_table_t2",
1171-
"insert_into_t2_values_(null),_(null),_(null)",
1172-
"select_*_from_t2",
11731167
}
11741168

11751169
// Patch auto-generated queries that are known to fail

0 commit comments

Comments
 (0)