From a61f2a0e21442727e822c4bd08060adedf2ad325 Mon Sep 17 00:00:00 2001 From: ayusssmaan Date: Wed, 10 Jun 2026 15:00:47 +0530 Subject: [PATCH 1/4] fix: strip double-quotes from extracted sequence name in SERIAL ownership detection MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When a sequence has a mixed-case quoted name, the nextval() default expression stores it as nextval('schema."SeqName"'::regclass). The existing two-pass REGEXP_REPLACE extracted 'SeqName' with double-quotes still present, causing the col_table JOIN to compare '"SeqName"' against 'SeqName' (as stored in pg_sequences) — the join always failed for such sequences. Add a third REGEXP_REPLACE pass that strips leading/trailing double-quotes from the extracted sequence name so the col_table JOIN matches correctly. With this fix, OwnedByTable is correctly populated for quoted SERIAL sequences, the skip condition fires, and pgschema no longer emits a standalone CREATE SEQUENCE alongside the SERIAL-generated one — eliminating the _seq1 drift on fresh database deploys. --- ir/queries/queries.sql | 9 ++++++--- ir/queries/queries.sql.go | 9 ++++++--- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/ir/queries/queries.sql b/ir/queries/queries.sql index 062bc135..f1a6946a 100644 --- a/ir/queries/queries.sql +++ b/ir/queries/queries.sql @@ -972,12 +972,15 @@ LEFT JOIN pg_class dep_table ON d.refobjid = dep_table.oid LEFT JOIN pg_attribute dep_col ON dep_col.attrelid = dep_table.oid AND dep_col.attnum = d.refobjsubid -- Method 2: Find sequences used in column defaults (for nextval() patterns) LEFT JOIN ( - SELECT + SELECT col.table_name, col.column_name, REGEXP_REPLACE( - REGEXP_REPLACE(col.column_default, 'nextval\(''([^'']+)''.*\)', '\1'), - '^[^.]*\.', '' + REGEXP_REPLACE( + REGEXP_REPLACE(col.column_default, 'nextval\(''([^'']+)''.*\)', '\1'), + '^[^.]*\.', '' + ), + '^"(.*)"$', '\1' ) AS sequence_name FROM information_schema.columns col WHERE col.table_schema = $1 diff --git a/ir/queries/queries.sql.go b/ir/queries/queries.sql.go index 0a5e4848..ea0dd593 100644 --- a/ir/queries/queries.sql.go +++ b/ir/queries/queries.sql.go @@ -2732,12 +2732,15 @@ LEFT JOIN pg_depend d ON d.objid = c.oid AND d.classid = 'pg_class'::regclass AN LEFT JOIN pg_class dep_table ON d.refobjid = dep_table.oid LEFT JOIN pg_attribute dep_col ON dep_col.attrelid = dep_table.oid AND dep_col.attnum = d.refobjsubid LEFT JOIN ( - SELECT + SELECT col.table_name, col.column_name, REGEXP_REPLACE( - REGEXP_REPLACE(col.column_default, 'nextval\(''([^'']+)''.*\)', '\1'), - '^[^.]*\.', '' + REGEXP_REPLACE( + REGEXP_REPLACE(col.column_default, 'nextval\(''([^'']+)''.*\)', '\1'), + '^[^.]*\.', '' + ), + '^"(.*)"$', '\1' ) AS sequence_name FROM information_schema.columns col WHERE col.table_schema = $1 From f87a9a28b307f630d18d2ea355eed3ec2259dd4b Mon Sep 17 00:00:00 2001 From: ayusssmaan Date: Wed, 10 Jun 2026 15:25:29 +0530 Subject: [PATCH 2/4] fix: unescape doubled double-quotes in extracted sequence name After stripping outer quotes, a sequence name like My"Seq is stored in column_default as My""Seq (PostgreSQL SQL identifier escaping). Without unescaping, the col_table JOIN still fails for such names. Add REPLACE(..., '""', '"') as a final pass to unescape doubled quotes, as suggested in code review. --- ir/queries/queries.sql | 11 +++++++---- ir/queries/queries.sql.go | 11 +++++++---- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/ir/queries/queries.sql b/ir/queries/queries.sql index f1a6946a..bf467139 100644 --- a/ir/queries/queries.sql +++ b/ir/queries/queries.sql @@ -975,12 +975,15 @@ LEFT JOIN ( SELECT col.table_name, col.column_name, - REGEXP_REPLACE( + REPLACE( REGEXP_REPLACE( - REGEXP_REPLACE(col.column_default, 'nextval\(''([^'']+)''.*\)', '\1'), - '^[^.]*\.', '' + REGEXP_REPLACE( + REGEXP_REPLACE(col.column_default, 'nextval\(''([^'']+)''.*\)', '\1'), + '^[^.]*\.', '' + ), + '^"(.*)"$', '\1' ), - '^"(.*)"$', '\1' + '""', '"' ) AS sequence_name FROM information_schema.columns col WHERE col.table_schema = $1 diff --git a/ir/queries/queries.sql.go b/ir/queries/queries.sql.go index ea0dd593..68d6d5a0 100644 --- a/ir/queries/queries.sql.go +++ b/ir/queries/queries.sql.go @@ -2735,12 +2735,15 @@ LEFT JOIN ( SELECT col.table_name, col.column_name, - REGEXP_REPLACE( + REPLACE( REGEXP_REPLACE( - REGEXP_REPLACE(col.column_default, 'nextval\(''([^'']+)''.*\)', '\1'), - '^[^.]*\.', '' + REGEXP_REPLACE( + REGEXP_REPLACE(col.column_default, 'nextval\(''([^'']+)''.*\)', '\1'), + '^[^.]*\.', '' + ), + '^"(.*)"$', '\1' ), - '^"(.*)"$', '\1' + '""', '"' ) AS sequence_name FROM information_schema.columns col WHERE col.table_schema = $1 From 0cdc86ac6a14629aae39cc6d370da55de4693c6d Mon Sep 17 00:00:00 2001 From: ayusssmaan Date: Wed, 10 Jun 2026 15:29:14 +0530 Subject: [PATCH 3/4] fix: make schema-stripping pass quote-aware for dotted schema names The previous regex '^[^.]*\.' splits at the first dot, which breaks for quoted schemas containing a dot (e.g. "foo.bar"). Replace with a quote-aware alternative that matches either a full quoted identifier followed by a dot, or an unquoted prefix followed by a dot. --- ir/queries/queries.sql | 2 +- ir/queries/queries.sql.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/ir/queries/queries.sql b/ir/queries/queries.sql index bf467139..28dbac9d 100644 --- a/ir/queries/queries.sql +++ b/ir/queries/queries.sql @@ -979,7 +979,7 @@ LEFT JOIN ( REGEXP_REPLACE( REGEXP_REPLACE( REGEXP_REPLACE(col.column_default, 'nextval\(''([^'']+)''.*\)', '\1'), - '^[^.]*\.', '' + '^("([^"]|"")*"\.|[^.]*\.)', '' ), '^"(.*)"$', '\1' ), diff --git a/ir/queries/queries.sql.go b/ir/queries/queries.sql.go index 68d6d5a0..47015ead 100644 --- a/ir/queries/queries.sql.go +++ b/ir/queries/queries.sql.go @@ -2739,7 +2739,7 @@ LEFT JOIN ( REGEXP_REPLACE( REGEXP_REPLACE( REGEXP_REPLACE(col.column_default, 'nextval\(''([^'']+)''.*\)', '\1'), - '^[^.]*\.', '' + '^("([^"]|"")*"\.|[^.]*\.)', '' ), '^"(.*)"$', '\1' ), From be03f666e286533b2bbadec4eb06129b443b4949 Mon Sep 17 00:00:00 2001 From: ayusssmaan Date: Wed, 10 Jun 2026 20:03:57 +0530 Subject: [PATCH 4/4] test: add regression test for mixed-case SERIAL sequence ownership detection MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a query-level test that directly calls GetSequencesForSchema and asserts OwnedByTable/OwnedByColumn are populated for a SERIAL column with a mixed-case name ("orderId"). The pg_depend ownership edge is removed via OWNED BY NONE to force the column_default parsing fallback — the path that was broken before this fix. Without the fix, the test fails with: OwnedByTable = "", want "orders" Co-Authored-By: Claude Sonnet 4.6 --- ir/queries/queries_test.go | 71 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 71 insertions(+) create mode 100644 ir/queries/queries_test.go diff --git a/ir/queries/queries_test.go b/ir/queries/queries_test.go new file mode 100644 index 00000000..4264c38f --- /dev/null +++ b/ir/queries/queries_test.go @@ -0,0 +1,71 @@ +package queries_test + +import ( + "context" + "database/sql" + "strings" + "testing" + + "github.com/pgplex/pgschema/internal/postgres" + "github.com/pgplex/pgschema/ir/queries" + "github.com/pgplex/pgschema/testutil" +) + +var sharedTestPostgres *postgres.EmbeddedPostgres + +func TestMain(m *testing.M) { + sharedTestPostgres = testutil.SetupPostgres(nil) + defer sharedTestPostgres.Stop() + m.Run() +} + +func TestGetSequencesForSchemaDetectsMixedCaseSequenceInColumnDefault(t *testing.T) { + conn, _, _, _, _, _ := testutil.ConnectToPostgres(t, sharedTestPostgres) + defer conn.Close() + + ctx := context.Background() + if _, err := conn.ExecContext(ctx, `DROP TABLE IF EXISTS orders CASCADE`); err != nil { + t.Fatalf("failed to drop test table: %v", err) + } + if _, err := conn.ExecContext(ctx, `CREATE TABLE orders ("orderId" SERIAL PRIMARY KEY)`); err != nil { + t.Fatalf("failed to create test table: %v", err) + } + // Drop the pg_depend ownership edge that SERIAL creates automatically. + // GetSequencesForSchema detects ownership via two paths: pg_depend (primary) + // and column_default parsing (fallback). Without this, pg_depend resolves + // ownership before the column_default regex is ever reached, so the test + // would pass even with the broken regex. OWNED BY NONE forces the fallback + // path — the one that was broken for mixed-case identifiers before this fix. + if _, err := conn.ExecContext(ctx, `ALTER SEQUENCE "orders_orderId_seq" OWNED BY NONE`); err != nil { + t.Fatalf("failed to remove sequence ownership dependency: %v", err) + } + + rows, err := queries.New(conn).GetSequencesForSchema(ctx, sql.NullString{String: "public", Valid: true}) + if err != nil { + t.Fatalf("failed to get sequences for schema: %v", err) + } + + for _, row := range rows { + if row.SequenceName.String != "orders_orderId_seq" { + continue + } + + if !row.OwnedByTable.Valid || row.OwnedByTable.String != "orders" { + t.Fatalf("OwnedByTable = %q, want %q", row.OwnedByTable.String, "orders") + } + if !row.OwnedByColumn.Valid || row.OwnedByColumn.String != "orderId" { + t.Fatalf("OwnedByColumn = %q, want %q", row.OwnedByColumn.String, "orderId") + } + return + } + + t.Fatalf("sequence %q not found; got sequences: %s", "orders_orderId_seq", sequenceNames(rows)) +} + +func sequenceNames(rows []queries.GetSequencesForSchemaRow) string { + names := make([]string, 0, len(rows)) + for _, row := range rows { + names = append(names, row.SequenceName.String) + } + return strings.Join(names, ", ") +}