diff --git a/cmd/dump/dump_integration_test.go b/cmd/dump/dump_integration_test.go index 8c2011cf..70a35847 100644 --- a/cmd/dump/dump_integration_test.go +++ b/cmd/dump/dump_integration_test.go @@ -116,6 +116,74 @@ func TestDumpCommand_Issue307ViewDependencyOrder(t *testing.T) { runExactMatchTest(t, "issue_307_view_dependency_order") } +func TestDumpCommand_Issue318CrossSchemaComment(t *testing.T) { + if testing.Short() { + t.Skip("Skipping integration test in short mode") + } + + // Setup PostgreSQL + embeddedPG := testutil.SetupPostgres(t) + defer embeddedPG.Stop() + + // Connect to database + conn, host, port, dbname, user, password := testutil.ConnectToPostgres(t, embeddedPG) + defer conn.Close() + + // Read and execute the setup SQL that creates two schemas with same-named tables + setupPath := "../../testdata/dump/issue_318_cross_schema_comment/setup.sql" + setupContent, err := os.ReadFile(setupPath) + if err != nil { + t.Fatalf("Failed to read %s: %v", setupPath, err) + } + + _, err = conn.ExecContext(context.Background(), string(setupContent)) + if err != nil { + t.Fatalf("Failed to execute setup.sql: %v", err) + } + + // Dump each schema and verify comments are correctly attributed + tests := []struct { + schema string + tableComment string + colComment string + }{ + {"alpha", "Alpha account table", "Alpha account name"}, + {"beta", "Beta account table", "Beta account name"}, + } + + for _, tc := range tests { + t.Run(tc.schema, func(t *testing.T) { + config := &DumpConfig{ + Host: host, + Port: port, + DB: dbname, + User: user, + Password: password, + Schema: tc.schema, + MultiFile: false, + File: "", + } + + output, err := ExecuteDump(config) + if err != nil { + t.Fatalf("Dump command failed for schema %s: %v", tc.schema, err) + } + + // Verify table comment + expectedTableComment := fmt.Sprintf("COMMENT ON TABLE account IS '%s';", tc.tableComment) + if !strings.Contains(output, expectedTableComment) { + t.Errorf("Schema %s: expected table comment %q not found in output:\n%s", tc.schema, expectedTableComment, output) + } + + // Verify column comment + expectedColComment := fmt.Sprintf("COMMENT ON COLUMN account.name IS '%s';", tc.colComment) + if !strings.Contains(output, expectedColComment) { + t.Errorf("Schema %s: expected column comment %q not found in output:\n%s", tc.schema, expectedColComment, output) + } + }) + } +} + func TestDumpCommand_Issue307MultiFileViewDependencyOrder(t *testing.T) { if testing.Short() { t.Skip("Skipping integration test in short mode") diff --git a/ir/queries/queries.sql b/ir/queries/queries.sql index 822df23f..75d5b92a 100644 --- a/ir/queries/queries.sql +++ b/ir/queries/queries.sql @@ -28,10 +28,10 @@ SELECT t.table_type, COALESCE(d.description, '') AS table_comment FROM information_schema.tables t -LEFT JOIN pg_class c ON c.relname = t.table_name -LEFT JOIN pg_namespace n ON c.relnamespace = n.oid AND n.nspname = t.table_schema +LEFT JOIN pg_namespace n ON n.nspname = t.table_schema +LEFT JOIN pg_class c ON c.relname = t.table_name AND c.relnamespace = n.oid LEFT JOIN pg_description d ON d.objoid = c.oid AND d.classoid = 'pg_class'::regclass AND d.objsubid = 0 -WHERE +WHERE t.table_schema NOT IN ('information_schema', 'pg_catalog', 'pg_toast') AND t.table_schema NOT LIKE 'pg_temp_%' AND t.table_schema NOT LIKE 'pg_toast_temp_%' @@ -40,16 +40,16 @@ ORDER BY t.table_schema, t.table_name; -- GetTablesForSchema retrieves all tables in a specific schema with metadata -- name: GetTablesForSchema :many -SELECT +SELECT t.table_schema, t.table_name, t.table_type, COALESCE(d.description, '') AS table_comment FROM information_schema.tables t -LEFT JOIN pg_class c ON c.relname = t.table_name -LEFT JOIN pg_namespace n ON c.relnamespace = n.oid AND n.nspname = t.table_schema +LEFT JOIN pg_namespace n ON n.nspname = t.table_schema +LEFT JOIN pg_class c ON c.relname = t.table_name AND c.relnamespace = n.oid LEFT JOIN pg_description d ON d.objoid = c.oid AND d.classoid = 'pg_class'::regclass AND d.objsubid = 0 -WHERE +WHERE t.table_schema = $1 AND t.table_type IN ('BASE TABLE', 'VIEW') ORDER BY t.table_name; @@ -109,8 +109,8 @@ WITH column_base AS ( ad.adbin, ad.adrelid FROM information_schema.columns c - LEFT JOIN pg_class cl ON cl.relname = c.table_name - LEFT JOIN pg_namespace n ON cl.relnamespace = n.oid AND n.nspname = c.table_schema + LEFT JOIN pg_namespace n ON n.nspname = c.table_schema + LEFT JOIN pg_class cl ON cl.relname = c.table_name AND cl.relnamespace = n.oid LEFT JOIN pg_description d ON d.objoid = cl.oid AND d.classoid = 'pg_class'::regclass AND d.objsubid = c.ordinal_position LEFT JOIN pg_attribute a ON a.attrelid = cl.oid AND a.attname = c.column_name LEFT JOIN pg_attrdef ad ON ad.adrelid = a.attrelid AND ad.adnum = a.attnum @@ -939,8 +939,8 @@ SELECT COALESCE(dep_table.relname, col_table.table_name) AS owned_by_table, COALESCE(dep_col.attname, col_table.column_name) AS owned_by_column FROM pg_sequences s -LEFT JOIN pg_class c ON c.relname = s.sequencename -LEFT JOIN pg_namespace n ON c.relnamespace = n.oid AND n.nspname = s.schemaname +LEFT JOIN pg_namespace n ON n.nspname = s.schemaname +LEFT JOIN pg_class c ON c.relname = s.sequencename AND c.relnamespace = n.oid -- Method 1: Try to find dependency relationship (for proper SERIAL columns) LEFT JOIN pg_depend d ON d.objid = c.oid AND d.classid = 'pg_class'::regclass AND d.deptype IN ('a', 'i') LEFT JOIN pg_class dep_table ON d.refobjid = dep_table.oid diff --git a/ir/queries/queries.sql.go b/ir/queries/queries.sql.go index acd44bfe..dc31afe9 100644 --- a/ir/queries/queries.sql.go +++ b/ir/queries/queries.sql.go @@ -308,8 +308,8 @@ WITH column_base AS ( ad.adbin, ad.adrelid FROM information_schema.columns c - LEFT JOIN pg_class cl ON cl.relname = c.table_name - LEFT JOIN pg_namespace n ON cl.relnamespace = n.oid AND n.nspname = c.table_schema + LEFT JOIN pg_namespace n ON n.nspname = c.table_schema + LEFT JOIN pg_class cl ON cl.relname = c.table_name AND cl.relnamespace = n.oid LEFT JOIN pg_description d ON d.objoid = cl.oid AND d.classoid = 'pg_class'::regclass AND d.objsubid = c.ordinal_position LEFT JOIN pg_attribute a ON a.attrelid = cl.oid AND a.attname = c.column_name LEFT JOIN pg_attrdef ad ON ad.adrelid = a.attrelid AND ad.adnum = a.attnum @@ -2622,8 +2622,8 @@ SELECT COALESCE(dep_table.relname, col_table.table_name) AS owned_by_table, COALESCE(dep_col.attname, col_table.column_name) AS owned_by_column FROM pg_sequences s -LEFT JOIN pg_class c ON c.relname = s.sequencename -LEFT JOIN pg_namespace n ON c.relnamespace = n.oid AND n.nspname = s.schemaname +LEFT JOIN pg_namespace n ON n.nspname = s.schemaname +LEFT JOIN pg_class c ON c.relname = s.sequencename AND c.relnamespace = n.oid LEFT JOIN pg_depend d ON d.objid = c.oid AND d.classid = 'pg_class'::regclass AND d.deptype IN ('a', 'i') 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 @@ -2702,10 +2702,10 @@ SELECT t.table_type, COALESCE(d.description, '') AS table_comment FROM information_schema.tables t -LEFT JOIN pg_class c ON c.relname = t.table_name -LEFT JOIN pg_namespace n ON c.relnamespace = n.oid AND n.nspname = t.table_schema +LEFT JOIN pg_namespace n ON n.nspname = t.table_schema +LEFT JOIN pg_class c ON c.relname = t.table_name AND c.relnamespace = n.oid LEFT JOIN pg_description d ON d.objoid = c.oid AND d.classoid = 'pg_class'::regclass AND d.objsubid = 0 -WHERE +WHERE t.table_schema NOT IN ('information_schema', 'pg_catalog', 'pg_toast') AND t.table_schema NOT LIKE 'pg_temp_%' AND t.table_schema NOT LIKE 'pg_toast_temp_%' @@ -2756,10 +2756,10 @@ SELECT t.table_type, COALESCE(d.description, '') AS table_comment FROM information_schema.tables t -LEFT JOIN pg_class c ON c.relname = t.table_name -LEFT JOIN pg_namespace n ON c.relnamespace = n.oid AND n.nspname = t.table_schema +LEFT JOIN pg_namespace n ON n.nspname = t.table_schema +LEFT JOIN pg_class c ON c.relname = t.table_name AND c.relnamespace = n.oid LEFT JOIN pg_description d ON d.objoid = c.oid AND d.classoid = 'pg_class'::regclass AND d.objsubid = 0 -WHERE +WHERE t.table_schema = $1 AND t.table_type IN ('BASE TABLE', 'VIEW') ORDER BY t.table_name diff --git a/testdata/dump/issue_318_cross_schema_comment/manifest.json b/testdata/dump/issue_318_cross_schema_comment/manifest.json new file mode 100644 index 00000000..e19255c8 --- /dev/null +++ b/testdata/dump/issue_318_cross_schema_comment/manifest.json @@ -0,0 +1,9 @@ +{ + "name": "issue_318_cross_schema_comment", + "description": "Test case for wrong table/column comments when same table name exists in multiple schemas (GitHub issue #318)", + "source": "https://github.com/pgplex/pgschema/issues/318", + "notes": [ + "Reproduces the bug where pg_class join on relname alone (without relnamespace) causes wrong comment attribution", + "Tests that table and column comments are correctly scoped to their own schema when dumping" + ] +} diff --git a/testdata/dump/issue_318_cross_schema_comment/setup.sql b/testdata/dump/issue_318_cross_schema_comment/setup.sql new file mode 100644 index 00000000..3c6cc700 --- /dev/null +++ b/testdata/dump/issue_318_cross_schema_comment/setup.sql @@ -0,0 +1,24 @@ +-- +-- Setup: Two schemas with identically-named tables but different comments. +-- This reproduces GitHub issue #318 where the buggy pg_class join on relname +-- alone (without relnamespace) can cause wrong comment attribution. +-- + +CREATE SCHEMA alpha; +CREATE SCHEMA beta; + +CREATE TABLE alpha.account ( + id serial PRIMARY KEY, + name text NOT NULL +); + +COMMENT ON TABLE alpha.account IS 'Alpha account table'; +COMMENT ON COLUMN alpha.account.name IS 'Alpha account name'; + +CREATE TABLE beta.account ( + id serial PRIMARY KEY, + name text NOT NULL +); + +COMMENT ON TABLE beta.account IS 'Beta account table'; +COMMENT ON COLUMN beta.account.name IS 'Beta account name';