From 2d3f7cc5cd6b1a2cc2e1408a399fc0d41a8b8ea4 Mon Sep 17 00:00:00 2001 From: Ivan Giuliani Date: Mon, 27 Apr 2026 10:40:02 +0100 Subject: [PATCH 1/3] test: reproduce name type dumped as char[] Adds a failing integration test confirming the inspector misclassifies columns declared as `name` as `char[]`, because pg_type.typelem is non-zero for `name` (points to char) even though `name` is not an array. --- cmd/dump/dump_integration_test.go | 39 +++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/cmd/dump/dump_integration_test.go b/cmd/dump/dump_integration_test.go index ffccff4d..f1fb8209 100644 --- a/cmd/dump/dump_integration_test.go +++ b/cmd/dump/dump_integration_test.go @@ -144,6 +144,45 @@ func TestDumpCommand_Issue191FunctionProcedureOverload(t *testing.T) { runExactMatchTest(t, "issue_191_function_procedure_overload") } +// Reproduces a bug where a column declared as `name` is dumped as `char[]`. +// The inspector classifies any base type with pg_type.typelem <> 0 as an array, +// but the `name` type has typelem = 18 (the OID of "char") despite not being an array. +func TestDumpCommand_NameTypeNotDumpedAsCharArray(t *testing.T) { + if testing.Short() { + t.Skip("Skipping integration test in short mode") + } + + embeddedPG := testutil.SetupPostgres(t) + defer embeddedPG.Stop() + + conn, host, port, dbname, user, password := testutil.ConnectToPostgres(t, embeddedPG) + defer conn.Close() + + _, err := conn.ExecContext(context.Background(), `CREATE TABLE pgschema_name_repro (n name);`) + if err != nil { + t.Fatalf("Failed to create table: %v", err) + } + + output, err := ExecuteDump(&DumpConfig{ + Host: host, + Port: port, + DB: dbname, + User: user, + Password: password, + Schema: "public", + }) + if err != nil { + t.Fatalf("Dump command failed: %v", err) + } + + if strings.Contains(output, "char[]") { + t.Errorf("Dump output should not contain char[] for a name column.\nOutput:\n%s", output) + } + if !strings.Contains(output, "n name") { + t.Errorf("Dump output should contain `n name` column declaration.\nOutput:\n%s", output) + } +} + func TestDumpCommand_Issue318CrossSchemaComment(t *testing.T) { if testing.Short() { t.Skip("Skipping integration test in short mode") From 81e88e3acd8dcf33dce4681b6291f7dcc63dbf05 Mon Sep 17 00:00:00 2001 From: Ivan Giuliani Date: Mon, 27 Apr 2026 11:05:32 +0100 Subject: [PATCH 2/3] chore: refresh sqlc-generated queries Picks up two regenerator-driven changes that had drifted from the checked-in output: - IsPeriod is now sql.NullBool because sqlc no longer infers non-null from COALESCE((... ->> 'conperiod')::boolean, false). Updated inspector.go to use .Bool when reading the field. - GetFunctionDependencies is emitted in source-file order (between GetEnumValuesForSchema and GetFunctions) rather than at the end. No behavior changes. --- ir/inspector.go | 2 +- ir/queries/queries.sql.go | 124 +++++++++++++++++++------------------- 2 files changed, 63 insertions(+), 63 deletions(-) diff --git a/ir/inspector.go b/ir/inspector.go index 10625958..88397f09 100644 --- a/ir/inspector.go +++ b/ir/inspector.go @@ -492,7 +492,7 @@ func (i *Inspector) buildConstraints(ctx context.Context, schema *IR, targetSche Type: cType, Columns: []*ConstraintColumn{}, NoInherit: constraint.NoInherit, - IsTemporal: constraint.IsPeriod, // PG18 temporal constraint (WITHOUT OVERLAPS / PERIOD) + IsTemporal: constraint.IsPeriod.Bool, // PG18 temporal constraint (WITHOUT OVERLAPS / PERIOD) } // Handle foreign key references diff --git a/ir/queries/queries.sql.go b/ir/queries/queries.sql.go index 2ba5e76c..4ce82325 100644 --- a/ir/queries/queries.sql.go +++ b/ir/queries/queries.sql.go @@ -830,7 +830,7 @@ type GetConstraintsRow struct { Deferrable bool `db:"deferrable" json:"deferrable"` InitiallyDeferred bool `db:"initially_deferred" json:"initially_deferred"` IsValid bool `db:"is_valid" json:"is_valid"` - IsPeriod bool `db:"is_period" json:"is_period"` + IsPeriod sql.NullBool `db:"is_period" json:"is_period"` NoInherit bool `db:"no_inherit" json:"no_inherit"` } @@ -949,7 +949,7 @@ type GetConstraintsForSchemaRow struct { Deferrable bool `db:"deferrable" json:"deferrable"` InitiallyDeferred bool `db:"initially_deferred" json:"initially_deferred"` IsValid bool `db:"is_valid" json:"is_valid"` - IsPeriod bool `db:"is_period" json:"is_period"` + IsPeriod sql.NullBool `db:"is_period" json:"is_period"` NoInherit bool `db:"no_inherit" json:"no_inherit"` } @@ -1386,6 +1386,65 @@ func (q *Queries) GetEnumValuesForSchema(ctx context.Context, dollar_1 sql.NullS return items, nil } +const getFunctionDependencies = `-- name: GetFunctionDependencies :many +SELECT + dependent_ns.nspname AS dependent_schema, + dependent_proc.proname AS dependent_name, + pg_get_function_identity_arguments(dependent_proc.oid) AS dependent_args, + referenced_ns.nspname AS referenced_schema, + referenced_proc.proname AS referenced_name, + pg_get_function_identity_arguments(referenced_proc.oid) AS referenced_args +FROM pg_depend d +JOIN pg_proc dependent_proc ON d.objid = dependent_proc.oid +JOIN pg_namespace dependent_ns ON dependent_proc.pronamespace = dependent_ns.oid +JOIN pg_proc referenced_proc ON d.refobjid = referenced_proc.oid +JOIN pg_namespace referenced_ns ON referenced_proc.pronamespace = referenced_ns.oid +WHERE d.classid = 'pg_proc'::regclass + AND d.refclassid = 'pg_proc'::regclass + AND d.deptype = 'n' + AND dependent_ns.nspname = $1 +` + +type GetFunctionDependenciesRow struct { + DependentSchema string `db:"dependent_schema" json:"dependent_schema"` + DependentName string `db:"dependent_name" json:"dependent_name"` + DependentArgs sql.NullString `db:"dependent_args" json:"dependent_args"` + ReferencedSchema string `db:"referenced_schema" json:"referenced_schema"` + ReferencedName string `db:"referenced_name" json:"referenced_name"` + ReferencedArgs sql.NullString `db:"referenced_args" json:"referenced_args"` +} + +// GetFunctionDependencies retrieves function-to-function dependencies for topological sorting +func (q *Queries) GetFunctionDependencies(ctx context.Context, dollar_1 sql.NullString) ([]GetFunctionDependenciesRow, error) { + rows, err := q.db.QueryContext(ctx, getFunctionDependencies, dollar_1) + if err != nil { + return nil, err + } + defer rows.Close() + var items []GetFunctionDependenciesRow + for rows.Next() { + var i GetFunctionDependenciesRow + if err := rows.Scan( + &i.DependentSchema, + &i.DependentName, + &i.DependentArgs, + &i.ReferencedSchema, + &i.ReferencedName, + &i.ReferencedArgs, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + const getFunctions = `-- name: GetFunctions :many SELECT r.routine_schema, @@ -2783,7 +2842,7 @@ func (q *Queries) GetTables(ctx context.Context) ([]GetTablesRow, error) { } const getTablesForSchema = `-- name: GetTablesForSchema :many -SELECT +SELECT t.table_schema, t.table_name, t.table_type, @@ -3283,62 +3342,3 @@ func (q *Queries) GetViewsForSchema(ctx context.Context, dollar_1 sql.NullString } return items, nil } - -const getFunctionDependencies = `-- name: GetFunctionDependencies :many -SELECT - dependent_ns.nspname AS dependent_schema, - dependent_proc.proname AS dependent_name, - pg_get_function_identity_arguments(dependent_proc.oid) AS dependent_args, - referenced_ns.nspname AS referenced_schema, - referenced_proc.proname AS referenced_name, - pg_get_function_identity_arguments(referenced_proc.oid) AS referenced_args -FROM pg_depend d -JOIN pg_proc dependent_proc ON d.objid = dependent_proc.oid -JOIN pg_namespace dependent_ns ON dependent_proc.pronamespace = dependent_ns.oid -JOIN pg_proc referenced_proc ON d.refobjid = referenced_proc.oid -JOIN pg_namespace referenced_ns ON referenced_proc.pronamespace = referenced_ns.oid -WHERE d.classid = 'pg_proc'::regclass - AND d.refclassid = 'pg_proc'::regclass - AND d.deptype = 'n' - AND dependent_ns.nspname = $1 -` - -type GetFunctionDependenciesRow struct { - DependentSchema string `db:"dependent_schema" json:"dependent_schema"` - DependentName string `db:"dependent_name" json:"dependent_name"` - DependentArgs sql.NullString `db:"dependent_args" json:"dependent_args"` - ReferencedSchema string `db:"referenced_schema" json:"referenced_schema"` - ReferencedName string `db:"referenced_name" json:"referenced_name"` - ReferencedArgs sql.NullString `db:"referenced_args" json:"referenced_args"` -} - -// GetFunctionDependencies retrieves function-to-function dependencies for topological sorting -func (q *Queries) GetFunctionDependencies(ctx context.Context, dollar_1 sql.NullString) ([]GetFunctionDependenciesRow, error) { - rows, err := q.db.QueryContext(ctx, getFunctionDependencies, dollar_1) - if err != nil { - return nil, err - } - defer rows.Close() - var items []GetFunctionDependenciesRow - for rows.Next() { - var i GetFunctionDependenciesRow - if err := rows.Scan( - &i.DependentSchema, - &i.DependentName, - &i.DependentArgs, - &i.ReferencedSchema, - &i.ReferencedName, - &i.ReferencedArgs, - ); err != nil { - return nil, err - } - items = append(items, i) - } - if err := rows.Close(); err != nil { - return nil, err - } - if err := rows.Err(); err != nil { - return nil, err - } - return items, nil -} From 12d19802dc594682382ee8046244ae4f283e8d00 Mon Sep 17 00:00:00 2001 From: Ivan Giuliani Date: Mon, 27 Apr 2026 11:05:48 +0100 Subject: [PATCH 3/3] fix: dump columns declared as `name` no longer reported as `char[]` The inspector treated any base type with pg_type.typelem <> 0 as an array. The `name` type is a non-array fixed-length type whose typelem points to char (OID 18), so it was misclassified and dumped as `char[]`. Switch the predicate to typcategory = 'A', which is PostgreSQL's intended way to identify array types. --- ir/queries/queries.sql | 8 ++++++-- ir/queries/queries.sql.go | 8 ++++++-- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/ir/queries/queries.sql b/ir/queries/queries.sql index 4c23aa4e..d4ed8859 100644 --- a/ir/queries/queries.sql +++ b/ir/queries/queries.sql @@ -79,8 +79,10 @@ WITH column_base AS ( CASE WHEN dn.nspname = c.table_schema THEN dt.typname ELSE dn.nspname || '.' || dt.typname END - WHEN dt.typtype = 'b' AND dt.typelem <> 0 THEN + WHEN dt.typtype = 'b' AND dt.typcategory = 'A' THEN -- Array types: apply same schema qualification logic to element type + -- Use typcategory = 'A' rather than typelem <> 0; the latter is true + -- for non-array fixed-length types like name (typelem points to char). CASE WHEN en.nspname = 'pg_catalog' THEN et.typname || '[]' WHEN en.nspname = c.table_schema THEN et.typname || '[]' @@ -195,8 +197,10 @@ WITH column_base AS ( CASE WHEN dn.nspname = c.table_schema THEN dt.typname ELSE dn.nspname || '.' || dt.typname END - WHEN dt.typtype = 'b' AND dt.typelem <> 0 THEN + WHEN dt.typtype = 'b' AND dt.typcategory = 'A' THEN -- Array types: apply same schema qualification logic to element type + -- Use typcategory = 'A' rather than typelem <> 0; the latter is true + -- for non-array fixed-length types like name (typelem points to char). CASE WHEN en.nspname = 'pg_catalog' THEN et.typname || '[]' WHEN en.nspname = c.table_schema THEN et.typname || '[]' diff --git a/ir/queries/queries.sql.go b/ir/queries/queries.sql.go index 4ce82325..428617c6 100644 --- a/ir/queries/queries.sql.go +++ b/ir/queries/queries.sql.go @@ -278,8 +278,10 @@ WITH column_base AS ( CASE WHEN dn.nspname = c.table_schema THEN dt.typname ELSE dn.nspname || '.' || dt.typname END - WHEN dt.typtype = 'b' AND dt.typelem <> 0 THEN + WHEN dt.typtype = 'b' AND dt.typcategory = 'A' THEN -- Array types: apply same schema qualification logic to element type + -- Use typcategory = 'A' rather than typelem <> 0; the latter is true + -- for non-array fixed-length types like name (typelem points to char). CASE WHEN en.nspname = 'pg_catalog' THEN et.typname || '[]' WHEN en.nspname = c.table_schema THEN et.typname || '[]' @@ -466,8 +468,10 @@ WITH column_base AS ( CASE WHEN dn.nspname = c.table_schema THEN dt.typname ELSE dn.nspname || '.' || dt.typname END - WHEN dt.typtype = 'b' AND dt.typelem <> 0 THEN + WHEN dt.typtype = 'b' AND dt.typcategory = 'A' THEN -- Array types: apply same schema qualification logic to element type + -- Use typcategory = 'A' rather than typelem <> 0; the latter is true + -- for non-array fixed-length types like name (typelem points to char). CASE WHEN en.nspname = 'pg_catalog' THEN et.typname || '[]' WHEN en.nspname = c.table_schema THEN et.typname || '[]'