diff --git a/pkg/snapshot/generator/postgres/schema/pgdumprestore/snapshot_pg_dump_restore_generator.go b/pkg/snapshot/generator/postgres/schema/pgdumprestore/snapshot_pg_dump_restore_generator.go index bde75cba..970f7e4e 100644 --- a/pkg/snapshot/generator/postgres/schema/pgdumprestore/snapshot_pg_dump_restore_generator.go +++ b/pkg/snapshot/generator/postgres/schema/pgdumprestore/snapshot_pg_dump_restore_generator.go @@ -833,6 +833,13 @@ func (s *SnapshotGenerator) parseDump(d []byte) *dump { case strings.HasPrefix(line, "ALTER TABLE") && isClusterOnAlterTable(line): indicesAndConstraints.WriteString(line) indicesAndConstraints.WriteString("\n\n") + case isAttachPartitionIndexStatement(line): + // ATTACH PARTITION references the parent index, which is created in + // the indices and constraints section. Restored in place it runs + // before that index exists, fails with "relation does not exist", + // and is never retried, leaving the parent index invalid. + indicesAndConstraints.WriteString(line) + indicesAndConstraints.WriteString("\n\n") case strings.HasPrefix(line, "ALTER TABLE") && strings.Contains(line, "REPLICA IDENTITY"): // REPLICA IDENTITY lines should be in the indicesAndConstraints section // since they reference constraints/indices that are also there @@ -1024,6 +1031,14 @@ func isIndexStatement(line string) bool { strings.HasPrefix(line, "CREATE UNIQUE INDEX") } +// isAttachPartitionIndexStatement reports whether the line attaches a partition +// index to its parent. pg_dump emits these for partitioned tables, after the +// CREATE INDEX statements they depend on. +func isAttachPartitionIndexStatement(line string) bool { + return strings.HasPrefix(line, "ALTER INDEX") && + strings.Contains(line, "ATTACH PARTITION") +} + func materializedViewName(line string) (string, bool) { if !strings.HasPrefix(line, "CREATE MATERIALIZED VIEW ") { return "", false diff --git a/pkg/snapshot/generator/postgres/schema/pgdumprestore/snapshot_pg_dump_restore_generator_test.go b/pkg/snapshot/generator/postgres/schema/pgdumprestore/snapshot_pg_dump_restore_generator_test.go index bed86b15..201819e0 100644 --- a/pkg/snapshot/generator/postgres/schema/pgdumprestore/snapshot_pg_dump_restore_generator_test.go +++ b/pkg/snapshot/generator/postgres/schema/pgdumprestore/snapshot_pg_dump_restore_generator_test.go @@ -1764,6 +1764,44 @@ ALTER TABLE public.example_table CLUSTER ON example_table_created_at_idx; strings.Index(string(dump.indicesAndConstraints), "CLUSTER ON example_table_created_at_idx")) } +func TestSnapshotGenerator_parseDumpMovesAttachPartitionToConstraints(t *testing.T) { + t.Parallel() + + dumpBytes := []byte(`CREATE TABLE public.events ( + event_id bigint NOT NULL, + partition_id integer NOT NULL +) +PARTITION BY LIST (partition_id); + +CREATE TABLE public.events_0 ( + event_id bigint NOT NULL, + partition_id integer NOT NULL +); + +ALTER TABLE ONLY public.events ATTACH PARTITION public.events_0 FOR VALUES IN (0); + +CREATE INDEX events_partition_id_idx ON ONLY public.events USING btree (partition_id); + +CREATE INDEX events_0_partition_id_idx ON public.events_0 USING btree (partition_id); + +ALTER INDEX public.events_partition_id_idx ATTACH PARTITION public.events_0_partition_id_idx; +`) + + dump := (&SnapshotGenerator{}).parseDump(dumpBytes) + + // the table attachment stays with the tables, the index attachment does not + require.Contains(t, string(dump.filtered), "ALTER TABLE ONLY public.events ATTACH PARTITION public.events_0") + require.NotContains(t, string(dump.filtered), "ALTER INDEX public.events_partition_id_idx ATTACH PARTITION") + + require.Contains(t, string(dump.indicesAndConstraints), "ALTER INDEX public.events_partition_id_idx ATTACH PARTITION public.events_0_partition_id_idx;") + + // and it has to land after the parent index it references, or it fails the + // same way it did when it was restored in place + require.Less(t, + strings.Index(string(dump.indicesAndConstraints), "CREATE INDEX events_partition_id_idx"), + strings.Index(string(dump.indicesAndConstraints), "ALTER INDEX public.events_partition_id_idx ATTACH PARTITION")) +} + func TestSnapshotGenerator_parseDumpMovesMaterializedViewIndexesToViews(t *testing.T) { t.Parallel()