From 1b8271aae7c7e00fc56b034dbaaf388df88c61ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20BERSAC?= Date: Fri, 12 Dec 2025 13:01:16 +0100 Subject: [PATCH 1/9] dev: Ensure mysql is up when running bats locally --- docker-compose.yml | 3 +++ test/bats.sh | 1 + 2 files changed, 4 insertions(+) diff --git a/docker-compose.yml b/docker-compose.yml index 2a6e9d30..811b1a5d 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -56,6 +56,9 @@ services: PGHOST: "postgres" HISTFILE: "/workspace/test/.bash_history" depends_on: + # Don't depends on mysql because mysql.bats includes a SHUTDOWN. + # If bats depends on mysql, docker kills bats once mysql is down. + # Start mysql manually before running bats. As in .circle/config.yml and test/bats.sh. - "${ORAHOST-oracle23}" - postgres entrypoint: /workspace/test/cli/entrypoint.sh diff --git a/test/bats.sh b/test/bats.sh index 40acd84b..5e184b1c 100755 --- a/test/bats.sh +++ b/test/bats.sh @@ -7,4 +7,5 @@ rm -rf test/results/ # Configure compose and bats to use dev databases. export ORAHOST=oracle18 # CI=true keeps results files. +docker compose up -d mysql exec docker compose run --rm -e CI=true bats "$@" -- GitLab From 4704680a8c4e54099c84bea7f9d6139e5c2a94cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20BERSAC?= Date: Mon, 8 Dec 2025 13:48:35 +0100 Subject: [PATCH 2/9] ora: Annotate temporary tables once --- internal/oracle/convert.go | 1 + 1 file changed, 1 insertion(+) diff --git a/internal/oracle/convert.go b/internal/oracle/convert.go index db4d85ef..cf43a496 100644 --- a/internal/oracle/convert.go +++ b/internal/oracle/convert.go @@ -27,6 +27,7 @@ func (ctg oractg) Convert() any { continue } tgt.Annotate("not implemented conversion: temporary table", 1) + break } tgt.Sequences = convert.Sequences(ctg.Sequences) -- GitLab From 579736c89ca8e7d14b3967fff37aed072712c532 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20BERSAC?= Date: Mon, 8 Dec 2025 15:21:30 +0100 Subject: [PATCH 3/9] dump: Limit constraint to table --- internal/catalog/sql/alter-table-keys.sql | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/catalog/sql/alter-table-keys.sql b/internal/catalog/sql/alter-table-keys.sql index 561befd2..f888d1a1 100644 --- a/internal/catalog/sql/alter-table-keys.sql +++ b/internal/catalog/sql/alter-table-keys.sql @@ -4,7 +4,7 @@ -- Name: {{ $.Name }} {{ .Name }}; Type: CONSTRAINT; Schema: {{ $.Schema }}; Owner: - -- -ALTER TABLE {{ identifier $.Schema $.Name }} +ALTER TABLE ONLY {{ identifier $.Schema $.Name }} ADD CONSTRAINT {{ identifier .Name }}{{ if .Primary }} PRIMARY KEY{{ else if .Unique }} UNIQUE{{ end }} ( {{- range $i, $s := .Columns }} {{- if gt $i 0 }}, {{ end }} -- GitLab From 36f94d4b650e2614a4f1b764c6747c4793af6874 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20BERSAC?= Date: Fri, 12 Dec 2025 14:15:35 +0100 Subject: [PATCH 4/9] dispatch: Create scoped checkpoint --- internal/dispatch/plan.go | 30 ++++++++++++++++++------------ internal/dispatch/plan_test.go | 4 ++-- internal/mysql/dump.go | 6 +++--- internal/oracle/dump.go | 6 +++--- 4 files changed, 26 insertions(+), 20 deletions(-) diff --git a/internal/dispatch/plan.go b/internal/dispatch/plan.go index 6df2d6db..bf9634f2 100644 --- a/internal/dispatch/plan.go +++ b/internal/dispatch/plan.go @@ -75,6 +75,10 @@ func (p *Plan) Add(t Task) { } p.tasks[id] = t + // Remove reqs from this task to use this task instead. + p.pointedTasks = slices.DeleteFunc(p.pointedTasks, func(id string) bool { + return slices.Contains(reqs, id) + }) p.pointedTasks = append(p.pointedTasks, id) if len(reqs) == 0 { @@ -94,23 +98,25 @@ func (p *Plan) Add(t Task) { } } -// Checkpoint add a checkpoint to the plan +// Checkpoint add a waiter to the plan +// +// nil reqs creates a checkpoint on all previous tasks. // -// Checkpoint add a no-op task that require all the -// previously added tasks. -func (p *Plan) Checkpoint(id string) { - checkpoint := noOpTask{ +// Use this to wait for a section of plan. e.g. pre-data, data, etc. +func (p *Plan) Checkpoint(id string, reqs []string) { + if len(reqs) == 0 { + reqs = p.pointedTasks + p.pointedTasks = nil + } + checkpoint := &noOpTask{ Header: Header{ - Id: id, - Priority: 0, - Reqs: p.pointedTasks, + Id: id, + Reqs: reqs, }, } - // pointedTasks is emptied - p.pointedTasks = nil p.Add(checkpoint) - // checkpoint Has been added so it is the only task in pointedTasks - // for further checkpoint + // Adds checkpoint as a regular task to ends in pointedTasks + // for next checkpoint. } // Execute runs tasks concurrently diff --git a/internal/dispatch/plan_test.go b/internal/dispatch/plan_test.go index f96f7262..f6d55397 100644 --- a/internal/dispatch/plan_test.go +++ b/internal/dispatch/plan_test.go @@ -210,7 +210,7 @@ func TestCheckpoint(t *testing.T) { }, fn: run, }) - p.Checkpoint("CP") + p.Checkpoint("CP", nil) p.Add(task{ Header: dispatch.Header{ Id: "e", @@ -230,7 +230,7 @@ func TestCheckpoint(t *testing.T) { }, fn: run, }) - p.Checkpoint("CP2") + p.Checkpoint("CP2", nil) p.Add(task{ Header: dispatch.Header{ Id: "g", diff --git a/internal/mysql/dump.go b/internal/mysql/dump.go index aae98f3e..dc66cc5c 100644 --- a/internal/mysql/dump.go +++ b/internal/mysql/dump.go @@ -16,7 +16,7 @@ import ( func (ctg *myctg) Dump(source any) *dispatch.Plan { p := dispatch.New() - p.Checkpoint("init") + p.Checkpoint("init", nil) ctg.Tables = slices.DeleteFunc(ctg.Tables, func(t catalog.Table) bool { src := catalog.Find[catalog.Table](source, convert.SourceMap[t.ObjectPath]) @@ -29,12 +29,12 @@ func (ctg *myctg) Dump(source any) *dispatch.Plan { if project.Current.DumpParams.PreData { ctg.predata(p) - p.Checkpoint("predata") + p.Checkpoint("predata", nil) } if project.Current.DumpParams.Data { ctg.data(p, source) - p.Checkpoint("data") + p.Checkpoint("data", nil) } if project.Current.DumpParams.PostData { diff --git a/internal/oracle/dump.go b/internal/oracle/dump.go index cc1b8f6a..96773a53 100644 --- a/internal/oracle/dump.go +++ b/internal/oracle/dump.go @@ -26,14 +26,14 @@ func (ctg *oractg) Dump(source any) *dispatch.Plan { ) p.Add(t) } - p.Checkpoint("init") + p.Checkpoint("init", nil) if project.Current.DumpParams.PreData { ctg.predata(p) - p.Checkpoint("predata") + p.Checkpoint("predata", nil) } if project.Current.DumpParams.Data { ctg.data(p, source) - p.Checkpoint("data") + p.Checkpoint("data", nil) if srcMdl.Before("12.1") { t := database.ExecuteSourceTask( "drop-package-support", []string{"data"}, -- GitLab From 7097facdddc0a488e6e62638eb3ec8d8539cdaf1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20BERSAC?= Date: Fri, 12 Dec 2025 11:20:32 +0100 Subject: [PATCH 5/9] dump: Create key on their own transaction --- internal/catalog/catalog.go | 30 +++++++++++++++-------- internal/catalog/constraints.go | 11 +++++++-- internal/catalog/sql/alter-table-keys.sql | 14 ----------- internal/catalog/sql/create-key.sql | 11 +++++++++ internal/catalog/table.go | 4 --- internal/dispatch/mock.go | 4 +++ 6 files changed, 44 insertions(+), 30 deletions(-) delete mode 100644 internal/catalog/sql/alter-table-keys.sql create mode 100644 internal/catalog/sql/create-key.sql diff --git a/internal/catalog/catalog.go b/internal/catalog/catalog.go index 3ff534b2..f02f28e1 100644 --- a/internal/catalog/catalog.go +++ b/internal/catalog/catalog.go @@ -125,23 +125,33 @@ func (ctg Catalog) AlterTablesKeys(p dispatch.TaskAdder) { continue } - if len(table.Keys) == 0 { - continue - } - var reqs []string if project.Current.DumpParams.Data { reqs = append(reqs, table.BuildPath("copy")) } else if project.Current.DumpParams.PreData { reqs = append(reqs, table.BuildPath("create")) + } else { + reqs = append(reqs, "init") } - p.Add(database.ExecuteTask( - table.BuildPath("keys"), table.Priority(), reqs, - table.AlterKeys(), - "Create table keys.", - "name", qualifiedName(table.Schema, table.Name), - )) + for _, key := range table.Keys { + if Exclude(key.ObjectPath) { + slog.Debug("Skipping ALTER of excluded key.", "path", key.ObjectPath) + continue + } + id := table.BuildPath(key.BuildPath("create")) + p.Add(database.ExecuteTask( + id, table.Priority(), reqs, + key.Create(table), + "Create table key.", + "name", qualifiedName(table.Schema, table.Name), + "key", key.Name, + )) + // chain tasks of same table. + reqs = []string{id} + } + + p.Checkpoint(table.BuildPath("keys"), reqs) } } diff --git a/internal/catalog/constraints.go b/internal/catalog/constraints.go index 7956f7b9..064940a2 100644 --- a/internal/catalog/constraints.go +++ b/internal/catalog/constraints.go @@ -34,8 +34,15 @@ func (k Key) String() string { return b.String() } -func (k Key) BuildPath() string { - return fmt.Sprintf("Keys/%s", k.Name) +func (k Key) BuildPath(tail ...string) string { + return strings.Join(append([]string{"Keys", k.Name}, tail...), "/") +} + +func (k Key) Create(t Table) string { + return Render("create-key.sql", map[string]any{ + "Key": k, + "Table": t, + }) } func RowToKey(r *sql.Rows) (a Association, err error) { diff --git a/internal/catalog/sql/alter-table-keys.sql b/internal/catalog/sql/alter-table-keys.sql deleted file mode 100644 index f888d1a1..00000000 --- a/internal/catalog/sql/alter-table-keys.sql +++ /dev/null @@ -1,14 +0,0 @@ -{{ range .Keys }} -{{- if not (exclude .ObjectPath) }} --- --- Name: {{ $.Name }} {{ .Name }}; Type: CONSTRAINT; Schema: {{ $.Schema }}; Owner: - --- - -ALTER TABLE ONLY {{ identifier $.Schema $.Name }} - ADD CONSTRAINT {{ identifier .Name }}{{ if .Primary }} PRIMARY KEY{{ else if .Unique }} UNIQUE{{ end }} ( -{{- range $i, $s := .Columns }} -{{- if gt $i 0 }}, {{ end }} -{{- identifier . }} -{{- end }}); -{{- end }} -{{- end }} diff --git a/internal/catalog/sql/create-key.sql b/internal/catalog/sql/create-key.sql new file mode 100644 index 00000000..cc5ffa3f --- /dev/null +++ b/internal/catalog/sql/create-key.sql @@ -0,0 +1,11 @@ + +-- +-- Name: {{ .Table.Name }} {{ .Key.Name }}; Type: CONSTRAINT; Schema: {{ .Table.Schema }}; Owner: - +-- + +ALTER TABLE ONLY {{ identifier .Table.Schema .Table.Name }} + ADD CONSTRAINT {{ identifier .Key.Name }}{{ if .Key.Primary }} PRIMARY KEY{{ else if .Key.Unique }} UNIQUE{{ end }} ( +{{- range $i, $s := .Key.Columns }} +{{- if gt $i 0 }}, {{ end }} +{{- identifier . }} +{{- end }}); diff --git a/internal/catalog/table.go b/internal/catalog/table.go index adc7d4ca..49a19993 100644 --- a/internal/catalog/table.go +++ b/internal/catalog/table.go @@ -90,10 +90,6 @@ func (t Table) CreateIndex(idx Index) string { }) } -func (t Table) AlterKeys() string { - return Render("alter-table-keys.sql", t) -} - func (t Table) AlterDefaults() string { return Render("alter-table-default.sql", t) } diff --git a/internal/dispatch/mock.go b/internal/dispatch/mock.go index 1b3fa41d..3987b75a 100644 --- a/internal/dispatch/mock.go +++ b/internal/dispatch/mock.go @@ -2,6 +2,7 @@ package dispatch type TaskAdder interface { Add(Task) + Checkpoint(string, []string) } var _ TaskAdder = New() @@ -14,3 +15,6 @@ func (p Mock) Add(t Task) { id, _, _ := t.Describe() p[id] = t } + +func (Mock) Checkpoint(_ string, _ []string) { +} -- GitLab From 4a30c8911c1b3a5935b922f5f1b0d7f4a2d670bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20BERSAC?= Date: Fri, 12 Dec 2025 11:24:24 +0100 Subject: [PATCH 6/9] dump: Serialize index creates per table --- internal/catalog/catalog.go | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/internal/catalog/catalog.go b/internal/catalog/catalog.go index f02f28e1..47182d14 100644 --- a/internal/catalog/catalog.go +++ b/internal/catalog/catalog.go @@ -157,29 +157,32 @@ func (ctg Catalog) AlterTablesKeys(p dispatch.TaskAdder) { func (ctg Catalog) CreateIndexes(p dispatch.TaskAdder) { for _, table := range ctg.Tables { - var reqs []string - - if len(table.Keys) > 0 { - reqs = append(reqs, table.BuildPath("keys")) - } else if project.Current.DumpParams.Data { - reqs = append(reqs, table.BuildPath("copy")) - } else if project.Current.DumpParams.PreData { - reqs = append(reqs, table.BuildPath("create")) + if Exclude(table.ObjectPath) { + continue } + var reqs []string + reqs = append(reqs, table.BuildPath("keys")) + for _, idx := range table.Indexes { if Exclude(idx.ObjectPath) { slog.Debug("Skipping CREATE of excluded index.", "path", idx.ObjectPath) continue } + id := table.BuildPath(idx.BuildPath("create")) p.Add(database.ExecuteTask( - table.BuildPath(idx.BuildPath("create")), table.Priority(), reqs, + id, table.Priority(), reqs, table.CreateIndex(idx), "Create Index.", "table", qualifiedName(table.Schema, table.Name), "name", idx.Name, )) + // Serialize index creations on same table + reqs = []string{id} } + + // Even if no index, create an index milestone. + p.Checkpoint(table.BuildPath("indexes"), reqs) } } -- GitLab From e21398adb0ac039848dc0e155724d197884e915b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20BERSAC?= Date: Mon, 8 Dec 2025 15:27:52 +0100 Subject: [PATCH 7/9] convert: Exclude foreign key on partitionned table --- CHANGELOG.md | 6 +++++- internal/convert/table.go | 14 ++++++++++---- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index aa60d4dc..fea0168c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,11 @@ See [features](./features.md) for overall progression of PostgreSQL Migrator. - Inspect `V$INSTANCE`. Requires new privilege `SELECT ON V_$INSTANCE`. -*UI:* +**Engine:** + +- Drop foreign key on partitionned table. Annotate unimplemented. + +**UI:** - Fix drawer annotation badge. - Scope search to current catalog. diff --git a/internal/convert/table.go b/internal/convert/table.go index ddafd130..748d76cd 100644 --- a/internal/convert/table.go +++ b/internal/convert/table.go @@ -101,15 +101,20 @@ func Tables(srcTables []catalog.Table) []catalog.Table { t.Keys[i] = k } - t.ForeignKeys = make([]catalog.ForeignKey, len(src.ForeignKeys)) - for i, srcFK := range src.ForeignKeys { + var fkeys []catalog.ForeignKey + t.ForeignKeys = nil + for _, srcFK := range src.ForeignKeys { + ft := catalog.Find[catalog.Table](ctg, srcFK.ForeignPath()) + if ft.PartitionBy != "" { + t.Annotate("unimplemented conversion: foreign key to partitionned table", 1) + continue + } fk := srcFK // copy fk.Name = Identifier(srcFK.ObjectPath, srcFK.Name) fk.ObjectPath = t.BuildPath(fk.BuildPath()) SourceMap[fk.ObjectPath] = srcFK.ObjectPath // Apply local and foreign columns renaming. - ft := catalog.Find[catalog.Table](ctg, srcFK.ForeignPath()) for j, name := range srcFK.Columns { fk.Columns[j] = colMap[name].Name @@ -121,12 +126,13 @@ func Tables(srcTables []catalog.Table) []catalog.Table { fk.ForeignTableName = Identifier(ft.ObjectPath, fk.ForeignTableName) fk.ForeignTableSchema = Identifier(fk.ForeignTableSchema, fk.ForeignTableSchema) fk.ObjectDefinition = fk.String() - t.ForeignKeys[i] = fk + fkeys = append(fkeys, fk) } if len(src.ForeignKeys) > 0 { t.Annotate("not implemented dump: foreign keys", 1) } + t.ForeignKeys = fkeys t.Indexes = nil indexes: -- GitLab From b548b9c319f7a040de335b89f60385a27661e71f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20BERSAC?= Date: Mon, 8 Dec 2025 15:28:23 +0100 Subject: [PATCH 8/9] convert: Rename foreign key --- CHANGELOG.md | 1 + internal/convert/table.go | 14 ++++++++++++-- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fea0168c..ef01e4b0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ See [features](./features.md) for overall progression of PostgreSQL Migrator. **Engine:** - Drop foreign key on partitionned table. Annotate unimplemented. +- Rename foreign keys. **UI:** diff --git a/internal/convert/table.go b/internal/convert/table.go index 748d76cd..e1665ceb 100644 --- a/internal/convert/table.go +++ b/internal/convert/table.go @@ -110,13 +110,23 @@ func Tables(srcTables []catalog.Table) []catalog.Table { continue } fk := srcFK // copy - fk.Name = Identifier(srcFK.ObjectPath, srcFK.Name) + if fk.RegenerateName { + for j, name := range fk.Columns { + fk.Columns[j] = colMap[name].Name + } + fk.Name = fmt.Sprintf("%s_%s_fkey", t.Name, strings.Join(fk.Columns, "_")) + slog.Debug("Regenerating foreign key identifier.", "from", srcFK.Name, "to", fk.Name, "path", srcFK.ObjectPath) + } else { + fk.Name = Identifier(srcFK.ObjectPath, srcFK.Name) + } fk.ObjectPath = t.BuildPath(fk.BuildPath()) SourceMap[fk.ObjectPath] = srcFK.ObjectPath // Apply local and foreign columns renaming. for j, name := range srcFK.Columns { - fk.Columns[j] = colMap[name].Name + if !fk.RegenerateName { + fk.Columns[j] = colMap[name].Name + } fc := ft.Column(srcFK.ForeignColumns[j]) // Rerun foreign column renaming. -- GitLab From d8738693a8ac0159b12ae87fd16aa744fe8b2ad5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20BERSAC?= Date: Mon, 8 Dec 2025 15:28:54 +0100 Subject: [PATCH 9/9] dump: Foreign keys --- CHANGELOG.md | 1 + docs/references/dump.md | 15 +++++++++ internal/catalog/catalog.go | 35 +++++++++++++++++++++ internal/catalog/constraints.go | 11 +++++-- internal/catalog/sql/create-foreign-key.sql | 15 +++++++++ internal/convert/table.go | 4 --- internal/mysql/dump.go | 4 +-- internal/oracle/dump.go | 4 +-- test/cli/mysql.bats | 7 +++++ test/cli/oracle.bats | 7 +++++ 10 files changed, 93 insertions(+), 10 deletions(-) create mode 100644 internal/catalog/sql/create-foreign-key.sql diff --git a/CHANGELOG.md b/CHANGELOG.md index ef01e4b0..9cef0a45 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ See [features](./features.md) for overall progression of PostgreSQL Migrator. **Engine:** +- Dump foreign keys. - Drop foreign key on partitionned table. Annotate unimplemented. - Rename foreign keys. diff --git a/docs/references/dump.md b/docs/references/dump.md index 68f7ccb7..aa6e6c48 100644 --- a/docs/references/dump.md +++ b/docs/references/dump.md @@ -73,3 +73,18 @@ Chunk size varies seconding source DBMS, see table below. | longblob | 128kB | PostgreSQL Migrator inlines long columns for tables with average row size below 64KiB. + + +## Table constraints + +PostgreSQL Migrator serializes creation of table constraints: + +- primary key +- unique keys, one by one. +- indexes, one by one. +- foreign keys, one by one. + +The dump scheduler parallelize constraints of different tables, +but not constraints on the same table. +This prevents deadlocks. +Each constraint runs in its own transaction. diff --git a/internal/catalog/catalog.go b/internal/catalog/catalog.go index 47182d14..8e80f1d7 100644 --- a/internal/catalog/catalog.go +++ b/internal/catalog/catalog.go @@ -186,6 +186,41 @@ func (ctg Catalog) CreateIndexes(p dispatch.TaskAdder) { } } +func (ctg Catalog) AlterTablesForeignKeys(p dispatch.TaskAdder) { + for _, table := range ctg.Tables { + if Exclude(table.ObjectPath) { + continue + } + + var reqs []string + // First foreign key depends on table indexes. + reqs = append(reqs, table.BuildPath("indexes")) + + for _, fk := range table.ForeignKeys { + if Exclude(fk.ObjectPath) { + slog.Debug("Skipping ALTER of excluded foreign key", "path", fk.ObjectPath) + continue + } + id := table.BuildPath(fk.BuildPath("create")) + // Also depends on foreign table indexes. + reqs = append(reqs, fk.ForeignPath("indexes")) + p.Add(database.ExecuteTask( + id, table.Priority(), reqs, + fk.Create(table), + "Create foreign key.", + "name", table, + "fk", fk, + )) + + // Next foreign key depends only on previous fk. + reqs = []string{id} + } + + // Even if no foreign key, create a foreign key milestone. + p.Checkpoint(table.BuildPath("fk"), reqs) + } +} + func (ctg Catalog) AlterTablesDefaults(p dispatch.TaskAdder) { var reqs []string if project.Current.DumpParams.PreData { diff --git a/internal/catalog/constraints.go b/internal/catalog/constraints.go index 064940a2..4f658e3e 100644 --- a/internal/catalog/constraints.go +++ b/internal/catalog/constraints.go @@ -120,8 +120,8 @@ func (fk ForeignKey) String() string { return b.String() } -func (fk ForeignKey) BuildPath() string { - return fmt.Sprintf("ForeignKeys/%s", fk.Name) +func (fk ForeignKey) BuildPath(tail ...string) string { + return strings.Join(append([]string{"ForeignKeys", fk.Name}, tail...), "/") } func (fk ForeignKey) ForeignPath(tail ...string) string { @@ -130,6 +130,13 @@ func (fk ForeignKey) ForeignPath(tail ...string) string { return strings.Join(elem, "/") } +func (fk ForeignKey) Create(t Table) string { + return Render("create-foreign-key.sql", map[string]any{ + "Key": fk, + "Table": t, + }) +} + func RowToForeignKey(r *sql.Rows) (a Association, err error) { var fk ForeignKey fk.ObjectType = "ForeignKey" diff --git a/internal/catalog/sql/create-foreign-key.sql b/internal/catalog/sql/create-foreign-key.sql new file mode 100644 index 00000000..5997bd3e --- /dev/null +++ b/internal/catalog/sql/create-foreign-key.sql @@ -0,0 +1,15 @@ + +-- +-- Name: {{ .Table.Name }} {{ .Key.Name }}; Type: FK CONSTRAINT; Schema: {{ .Table.Schema }}; Owner: - +-- + +ALTER TABLE ONLY {{ identifier .Table.Schema .Table.Name }} + ADD CONSTRAINT {{ identifier .Key.Name }} FOREIGN KEY ( +{{- range $i, $s := .Key.Columns }} +{{- if gt $i 0 }}, {{ end }} +{{- identifier . }} +{{- end }}) REFERENCES {{ identifier .Key.ForeignTableSchema .Key.ForeignTableName }} ( +{{- range $i, $s := .Key.ForeignColumns }} +{{- if gt $i 0 }}, {{ end }} +{{- identifier . }} +{{- end }}); diff --git a/internal/convert/table.go b/internal/convert/table.go index e1665ceb..e162fc25 100644 --- a/internal/convert/table.go +++ b/internal/convert/table.go @@ -138,10 +138,6 @@ func Tables(srcTables []catalog.Table) []catalog.Table { fk.ObjectDefinition = fk.String() fkeys = append(fkeys, fk) } - - if len(src.ForeignKeys) > 0 { - t.Annotate("not implemented dump: foreign keys", 1) - } t.ForeignKeys = fkeys t.Indexes = nil diff --git a/internal/mysql/dump.go b/internal/mysql/dump.go index dc66cc5c..f7cd912e 100644 --- a/internal/mysql/dump.go +++ b/internal/mysql/dump.go @@ -67,8 +67,8 @@ func (ctg myctg) data(p *dispatch.Plan, source any) { func (ctg myctg) postdata(p *dispatch.Plan) { dispatch.Number = dispatch.PostData ctg.AlterTablesKeys(p) - // Indexes depend on table keys - ctg.CreateIndexes(p) + ctg.CreateIndexes(p) // Indexes depend on table keys + ctg.AlterTablesForeignKeys(p) // FK depends on indexes ctg.AlterTablesDefaults(p) } diff --git a/internal/oracle/dump.go b/internal/oracle/dump.go index 96773a53..d462b3a6 100644 --- a/internal/oracle/dump.go +++ b/internal/oracle/dump.go @@ -81,7 +81,7 @@ func (ctg oractg) data(p *dispatch.Plan, source any) { func (ctg oractg) postdata(p *dispatch.Plan) { dispatch.Number = dispatch.PostData ctg.AlterTablesKeys(p) - // Indexes depend on table keys - ctg.CreateIndexes(p) + ctg.CreateIndexes(p) // Indexes depend on table keys + ctg.AlterTablesForeignKeys(p) // FK depends on indexes ctg.AlterTablesDefaults(p) } diff --git a/test/cli/mysql.bats b/test/cli/mysql.bats index d3fa1a92..8793063c 100644 --- a/test/cli/mysql.bats +++ b/test/cli/mysql.bats @@ -200,4 +200,11 @@ teardown_file() { WHERE conname = 'actor_pkey'; EOSQL assert_output "t" + + run psql -tA <<-EOSQL + SELECT confrelid IS NOT NULL + FROM pg_catalog.pg_constraint + WHERE conname = 'address_city_id_fkey'; + EOSQL + assert_output "t" } diff --git a/test/cli/oracle.bats b/test/cli/oracle.bats index e65b6761..83a2df28 100644 --- a/test/cli/oracle.bats +++ b/test/cli/oracle.bats @@ -209,4 +209,11 @@ setup_file() { WHERE conname = 'pk_actor'; EOSQL assert_output "t" + + run psql -tA <<-EOSQL + SELECT confrelid IS NOT NULL + FROM pg_catalog.pg_constraint + WHERE conname = 'fk_address_city'; + EOSQL + assert_output "t" } -- GitLab