diff --git a/cmd/dump/extension_integration_test.go b/cmd/dump/extension_integration_test.go new file mode 100644 index 00000000..51848cba --- /dev/null +++ b/cmd/dump/extension_integration_test.go @@ -0,0 +1,116 @@ +package dump + +import ( + "context" + "fmt" + "testing" + + "github.com/pgplex/pgschema/ir" + "github.com/pgplex/pgschema/testutil" + "github.com/stretchr/testify/require" +) + +// Issue #595: pg_stat_statements' view was dumped while its required function +// was omitted. Neither definition belongs in an application schema dump. +func TestDumpExtensionMembers(t *testing.T) { + if testing.Short() { + t.Skip("Skipping integration test in short mode") + } + pg := testutil.SetupPostgres(t) + defer pg.Stop() + db, host, port, name, user, password := testutil.ConnectToPostgres(t, pg) + defer db.Close() + ctx := context.Background() + // Catalog inspection and view creation do not execute pg_stat_statements, + // so the bundled extension needs no shared_preload_libraries modification. + _, err := db.ExecContext(ctx, ` + CREATE EXTENSION pg_stat_statements; + CREATE TABLE app_requests (id integer PRIMARY KEY); + CREATE VIEW app_stats AS SELECT queryid FROM pg_stat_statements; + GRANT SELECT ON app_requests TO PUBLIC; + GRANT SELECT (queryid) ON pg_stat_statements TO PUBLIC; + `) + require.NoError(t, err) + config := &DumpConfig{ + Host: host, Port: port, DB: name, User: user, Password: password, + Schema: "public", NoComments: true, ConfigDir: t.TempDir(), + } + dumped, err := ExecuteDump(config) + require.NoError(t, err) + require.NotContains(t, dumped, "VIEW pg_stat_statements") + require.NotContains(t, dumped, "CREATE OR REPLACE FUNCTION pg_stat_statements") + require.NotContains(t, dumped, "ON TABLE pg_stat_statements") + require.Contains(t, dumped, "CREATE OR REPLACE VIEW app_stats") + require.Contains(t, dumped, "FROM pg_stat_statements") + require.Contains(t, dumped, "GRANT SELECT ON TABLE app_requests TO PUBLIC") + // Reload the unmodified native dump while the extension is still installed. + _, err = db.ExecContext(ctx, `DROP VIEW app_stats; DROP TABLE app_requests;`) + require.NoError(t, err) + _, err = db.ExecContext(ctx, dumped) + require.NoError(t, err) + roundtrip, err := ExecuteDump(config) + require.NoError(t, err) + require.Equal(t, dumped, roundtrip) +} + +// An application partition remains managed even when its parent is an +// extension member. Replaying its dump must preserve its own column rules. +func TestDumpExtensionPartitionOverrides(t *testing.T) { + if testing.Short() { + t.Skip("Skipping integration test in short mode") + } + pg := testutil.SetupPostgres(t) + defer pg.Stop() + db, host, port, name, user, password := testutil.ConnectToPostgres(t, pg) + defer db.Close() + ctx := context.Background() + _, err := db.ExecContext(ctx, `CREATE SCHEMA extension595; CREATE EXTENSION hstore SCHEMA extension595;`) + require.NoError(t, err) + for _, tc := range []struct{ parentSchema, childSchema string }{ + {"public", "public"}, + {"Extension Space", "App Space"}, + } { + t.Run(tc.childSchema, func(t *testing.T) { + parent := ir.QuoteIdentifier(tc.parentSchema) + `."Member Parent"` + child := ir.QuoteIdentifier(tc.childSchema) + `."App Child"` + _, err := db.ExecContext(ctx, fmt.Sprintf(` + CREATE SCHEMA IF NOT EXISTS %s; + CREATE SCHEMA IF NOT EXISTS %s; + CREATE TABLE %s ( + id integer NOT NULL, + priority integer DEFAULT 0, + notes text, + inherited integer DEFAULT 42 NOT NULL, + calculated integer GENERATED ALWAYS AS (id * 2) STORED + ) PARTITION BY RANGE (id); + ALTER EXTENSION hstore ADD TABLE %s; + CREATE TABLE %s PARTITION OF %s ( + priority DEFAULT 10, notes NOT NULL + ) FOR VALUES FROM (0) TO (100); + `, ir.QuoteIdentifier(tc.parentSchema), ir.QuoteIdentifier(tc.childSchema), parent, parent, child, parent)) + require.NoError(t, err) + config := &DumpConfig{Host: host, Port: port, DB: name, User: user, Password: password, + Schema: tc.childSchema, NoComments: true, QualifySchema: true, ConfigDir: t.TempDir()} + dumped, err := ExecuteDump(config) + require.NoError(t, err) + require.NotContains(t, dumped, `CREATE TABLE IF NOT EXISTS `+parent+` (`) + _, err = db.ExecContext(ctx, `DROP TABLE `+child) + require.NoError(t, err) + _, err = db.ExecContext(ctx, dumped) + require.NoError(t, err, dumped) + var priority, inherited, calculated int + err = db.QueryRowContext(ctx, `INSERT INTO `+child+` (id, notes) VALUES (1, 'kept') RETURNING priority, inherited, calculated`).Scan(&priority, &inherited, &calculated) + require.NoError(t, err) + require.Equal(t, 10, priority, "partition default must survive dump replay") + require.Equal(t, 42, inherited) + require.Equal(t, 2, calculated) + _, err = db.ExecContext(ctx, `INSERT INTO `+child+` (id) VALUES (2)`) + require.ErrorContains(t, err, "23502", "partition NOT NULL must survive dump replay") + roundtrip, err := ExecuteDump(config) + require.NoError(t, err) + require.Equal(t, dumped, roundtrip) + _, err = db.ExecContext(ctx, `DROP TABLE `+child+`; ALTER EXTENSION hstore DROP TABLE `+parent+`; DROP TABLE `+parent) + require.NoError(t, err) + }) + } +} diff --git a/cmd/plan/plan.go b/cmd/plan/plan.go index 0c66970e..c409fbb6 100644 --- a/cmd/plan/plan.go +++ b/cmd/plan/plan.go @@ -584,6 +584,13 @@ func normalizeSchemaNames(irData *ir.IR, fromSchema, toSchema string) { *column.GeneratedExpr = stripQualifiers(replaceString(*column.GeneratedExpr)) } } + // Unmanaged parent defaults must use the same schema context as + // child defaults, otherwise inherited expressions look like overrides. + for _, column := range table.PartitionParentColumns { + if column.DefaultValue != nil { + *column.DefaultValue = stripQualifiers(replaceString(*column.DefaultValue)) + } + } // Normalize schema names in indexes for _, index := range table.Indexes { diff --git a/cmd/plan/plan_test.go b/cmd/plan/plan_test.go index 1a8e7684..d5fa68e4 100644 --- a/cmd/plan/plan_test.go +++ b/cmd/plan/plan_test.go @@ -4,8 +4,10 @@ import ( "fmt" "os" "path/filepath" + "strings" "testing" + "github.com/pgplex/pgschema/internal/diff" "github.com/pgplex/pgschema/ir" "github.com/spf13/cobra" ) @@ -237,6 +239,32 @@ func TestNormalizeSchemaNames_StripsSameSchemaQualifiersFromViewDefinitions(t *t } } +func TestNormalizeSchemaNames_PreservesInheritedPartitionDefault(t *testing.T) { + const tempSchema = "pgschema_tmp_partition595" + childDefault, parentDefault := "public.member_default()", "public.member_default()" + child := &ir.Table{ + Schema: tempSchema, Name: "app_child", PartitionOf: "member_parent", + PartitionOfSchema: "public", PartitionBound: "FOR VALUES FROM (0) TO (10)", + Columns: []*ir.Column{{Name: "value", DefaultValue: &childDefault}}, + PartitionParentColumns: []*ir.Column{{Name: "value", DefaultValue: &parentDefault}}, + } + desired := &ir.IR{Schemas: map[string]*ir.Schema{ + tempSchema: {Name: tempSchema, Tables: map[string]*ir.Table{"app_child": child}}, + }} + normalizeSchemaNames(desired, tempSchema, "public") + changes := diff.GenerateMigration(ir.NewIR(), desired, "public") + var statements []string + for _, change := range changes { + for _, statement := range change.Statements { + statements = append(statements, statement.SQL) + } + } + sql := strings.Join(statements, "\n") + if !strings.Contains(sql, "PARTITION OF member_parent") || strings.Contains(sql, "DEFAULT") { + t.Fatalf("expected inherited default without a child override, got %s", sql) + } +} + func TestNormalizeSchemaNames_PreservesViewTableAliasesMatchingTargetSchema(t *testing.T) { irData := &ir.IR{ Schemas: map[string]*ir.Schema{ diff --git a/docs/cli/plan-db.mdx b/docs/cli/plan-db.mdx index 16e98582..3a290be8 100644 --- a/docs/cli/plan-db.mdx +++ b/docs/cli/plan-db.mdx @@ -65,6 +65,19 @@ pgschema apply \ pgschema does not manage extensions, so `dump` never emits `CREATE EXTENSION` and you should not add it to your schema files. Instead, the plan database must already have the extensions your schema uses, in the same schema as on the target. +Extension-owned objects are also unmanaged. `dump` omits their definitions and +privileges, and `plan` leaves them alone. This includes privileges changed after +installation, such as a custom grant on `spatial_ref_sys` or a revoked `PUBLIC` +grant on an extension function. Manage those privileges separately; do not put +extension-member `GRANT`/`REVOKE` statements in the desired schema. In an external +plan database those statements can affect the preinstalled extension itself, +outside the temporary schema, without representing a managed change. + +Application objects that use extension types or functions remain managed, +including their privileges. Membership is determined from PostgreSQL's dependency +catalog, not object names or the schema containing the extension. Unlike +`pg_dump`, pgschema does not export changes from an extension's initial privileges. + The default embedded plan database handles this for you: it installs every extension found on the target database before applying your schema. This covers all extensions bundled with PostgreSQL (`hstore`, `pg_trgm`, `citext`, `uuid-ossp`, `btree_gist`, ...). Third-party extensions such as `postgis` or `pgvector` are not bundled, so for those you need an external plan database. An external plan database is not mirrored automatically: install every extension your schema uses yourself, bundled ones included, in the same schema as on the target: diff --git a/docs/syntax/grant_revoke.mdx b/docs/syntax/grant_revoke.mdx index b447bc94..cb06b974 100644 --- a/docs/syntax/grant_revoke.mdx +++ b/docs/syntax/grant_revoke.mdx @@ -44,6 +44,11 @@ pgschema understands the following `GRANT`/`REVOKE` features: - **REVOKE GRANT OPTION FOR**: Revoke only the grant option while keeping the privilege - **PUBLIC**: Special grantee representing all roles +These features apply to application-owned objects. Extension-member privileges, +including custom grants and revoked defaults, are excluded from dump and plan +along with the extension-owned definitions. Manage them separately from desired +schema SQL; see [Using PostgreSQL Extensions](/cli/plan-db#using-postgresql-extensions). + ## Examples ### Grant table privileges diff --git a/internal/diff/table.go b/internal/diff/table.go index 478406ac..db8d1579 100644 --- a/internal/diff/table.go +++ b/internal/diff/table.go @@ -937,9 +937,13 @@ func generateTableSQL(table *ir.Table, targetSchema string, qualifySchema bool, // Detect per-child column overrides (DEFAULT, NOT NULL) by comparing against the parent. parentKey := parentSchema + "." + table.PartitionOf + parentColumns := table.PartitionParentColumns if parentTable, ok := allTables[parentKey]; ok { - parentCols := make(map[string]*ir.Column, len(parentTable.Columns)) - for _, col := range parentTable.Columns { + parentColumns = parentTable.Columns + } + if len(parentColumns) > 0 { + parentCols := make(map[string]*ir.Column, len(parentColumns)) + for _, col := range parentColumns { parentCols[col.Name] = col } for _, col := range table.Columns { diff --git a/ir/inspector.go b/ir/inspector.go index 2a350145..6233a088 100644 --- a/ir/inspector.go +++ b/ir/inspector.go @@ -140,6 +140,10 @@ func (i *Inspector) BuildIR(ctx context.Context, targetSchema string) (*IR, erro return nil, err } + if err := i.buildPartitionParentColumns(ctx, schema, targetSchema); err != nil { + return nil, fmt.Errorf("failed to build partition parent columns: %w", err) + } + // Load rows of data-managed tables now that columns, constraints, and // partitions are known. if err := i.buildRows(ctx, schema, targetSchema); err != nil { @@ -760,6 +764,42 @@ func (i *Inspector) buildPartitionMapping(ctx context.Context, schema *IR, targe return partitionMapping } +// buildPartitionParentColumns keeps unmanaged parents available for comparing +// inherited column properties, without adding their definitions to the IR. +func (i *Inspector) buildPartitionParentColumns(ctx context.Context, schema *IR, targetSchema string) error { + children := make(map[string]*Table) + for name, table := range schema.Schemas[targetSchema].Tables { + if table.PartitionOf == "" { + continue + } + parentSchema := table.PartitionOfSchema + if parentSchema == "" { + parentSchema = targetSchema + } + if s := schema.Schemas[parentSchema]; s != nil && s.Tables[table.PartitionOf] != nil { + continue + } + children[name] = table + } + if len(children) == 0 { + return nil + } + columns, err := i.queries.GetPartitionParentColumnsForSchema(ctx, targetSchema) + if err != nil { + return err + } + for _, col := range columns { + if table := children[col.ChildTable]; table != nil { + column := &Column{Name: col.ColumnName, IsNullable: col.IsNullable.Bool} + if col.ColumnDefault.Valid { + column.DefaultValue = &col.ColumnDefault.String + } + table.PartitionParentColumns = append(table.PartitionParentColumns, column) + } + } + return nil +} + // sortPrimaryKeyColumnsForPartitionedTable sorts primary key constraint columns // to ensure partition key columns come first func (i *Inspector) sortPrimaryKeyColumnsForPartitionedTable(constraint *Constraint, partitionKey string) { diff --git a/ir/ir.go b/ir/ir.go index bcff34c0..6bb360dc 100644 --- a/ir/ir.go +++ b/ir/ir.go @@ -75,6 +75,11 @@ type Table struct { // schema definition, so it is excluded from serialization (and thereby // from fingerprints and plan JSON). AllConstraintNames map[string]bool `json:"-"` + // PartitionParentColumns holds comparison metadata when a partition's + // parent is outside the managed IR, e.g. an extension member. It preserves + // child DEFAULT/NOT NULL overrides without managing or fingerprinting the + // parent itself. + PartitionParentColumns []*Column `json:"-"` // DataManaged is true when the table matches [data] in pgschema.toml and // its rows are part of the desired state. Rows holds those rows, in // DataColumns() order. Both are excluded from serialization, and therefore diff --git a/ir/normalize.go b/ir/normalize.go index f4ccdfb8..adf823b4 100644 --- a/ir/normalize.go +++ b/ir/normalize.go @@ -161,6 +161,11 @@ func normalizeTable(table *Table) { for _, column := range table.Columns { normalizeColumn(column, table.Schema) } + // Compare parent expressions in the child's schema context, just like its + // own defaults. The parent can live in a different (unmanaged) schema. + for _, column := range table.PartitionParentColumns { + normalizeColumn(column, table.Schema) + } // Normalize policies for _, policy := range table.Policies { @@ -1598,6 +1603,7 @@ func IsTextLikeType(typeName string) bool { // to avoid a perpetual spurious diff (issue #473): // - array-level cast: "col::text = ANY ((ARRAY['a'::varchar])::text[])" (wrapping paren + array cast) // - element-level: "col::text = ANY (ARRAY[('a'::varchar)::text])" (cast on each element) +// // Both collapse to "col::text IN ('a'::varchar)". func convertAnyArrayToIn(expr string) string { const anyMarker = " = ANY (" diff --git a/ir/queries/extension_members_test.go b/ir/queries/extension_members_test.go new file mode 100644 index 00000000..c8934783 --- /dev/null +++ b/ir/queries/extension_members_test.go @@ -0,0 +1,230 @@ +package queries_test + +import ( + "context" + "database/sql" + "encoding/json" + "fmt" + "testing" + + "github.com/pgplex/pgschema/ir/queries" + "github.com/pgplex/pgschema/testutil" + "github.com/stretchr/testify/require" +) + +// Extension membership belongs to a catalog object, not a name, schema or +// dependency on an extension type. Use real ALTER EXTENSION ADD edges so these +// regressions run with bundled PostgreSQL on every supported major version. +func TestExtensionMembers(t *testing.T) { + db, _, _, _, _, _ := testutil.ConnectToPostgres(t, sharedTestPostgres) + defer db.Close() + ctx := context.Background() + const schemaName = "extension595" + _, err := db.ExecContext(ctx, `CREATE SCHEMA "extension595"; + CREATE EXTENSION hstore SCHEMA "extension595";`) + require.NoError(t, err) + defer db.ExecContext(ctx, `DROP EXTENSION hstore CASCADE; DROP SCHEMA IF EXISTS "extension595" CASCADE`) + + for _, prefix := range []string{"member595", "app595"} { + _, err = db.ExecContext(ctx, fmt.Sprintf(` + SET search_path TO "extension595", pg_catalog; + CREATE TABLE %[1]s_table (id serial PRIMARY KEY, attrs hstore, checked integer CHECK (checked > 0)); + CREATE INDEX %[1]s_index ON %[1]s_table (checked); + CREATE VIEW %[1]s_view AS SELECT id FROM %[1]s_table; + CREATE MATERIALIZED VIEW %[1]s_matview AS SELECT id FROM %[1]s_table; + CREATE SEQUENCE %[1]s_sequence; + CREATE TYPE %[1]s_enum AS ENUM ('one', 'two'); + CREATE TYPE %[1]s_composite AS (id integer); + CREATE DOMAIN %[1]s_domain AS integer CHECK (VALUE > 0); + CREATE FUNCTION %[1]s_function() RETURNS integer LANGUAGE sql AS 'SELECT 1'; + CREATE PROCEDURE %[1]s_procedure() LANGUAGE sql AS 'SELECT 1'; + CREATE AGGREGATE %[1]s_aggregate(integer) (SFUNC=int4pl, STYPE=integer, INITCOND='0'); + CREATE FUNCTION %[1]s_trigger_function() RETURNS trigger LANGUAGE plpgsql AS 'BEGIN RETURN NEW; END'; + CREATE TRIGGER %[1]s_trigger BEFORE INSERT ON %[1]s_table FOR EACH ROW EXECUTE FUNCTION %[1]s_trigger_function(); + ALTER TABLE %[1]s_table ENABLE ROW LEVEL SECURITY; + CREATE POLICY %[1]s_policy ON %[1]s_table USING (id > 0); + CREATE TABLE %[1]s_partitioned (id integer) PARTITION BY RANGE (id); + CREATE TABLE %[1]s_partition PARTITION OF %[1]s_partitioned FOR VALUES FROM (0) TO (10); + GRANT SELECT ON %[1]s_table, %[1]s_view, %[1]s_matview TO PUBLIC; + GRANT SELECT (checked) ON %[1]s_table TO PUBLIC; + GRANT USAGE ON SEQUENCE %[1]s_sequence, %[1]s_table_id_seq TO PUBLIC; + REVOKE USAGE ON TYPE %[1]s_table FROM PUBLIC; + REVOKE EXECUTE ON FUNCTION %[1]s_function() FROM PUBLIC; + REVOKE EXECUTE ON PROCEDURE %[1]s_procedure() FROM PUBLIC; + REVOKE USAGE ON TYPE %[1]s_enum FROM PUBLIC; + `, prefix)) + require.NoError(t, err) + } + // Include a quoted member name and identically named application object in + // another schema: object names cannot be the membership identity. + _, err = db.ExecContext(ctx, ` + SET search_path TO "extension595", pg_catalog; + CREATE TABLE "member595.Quoted" (id integer); + ALTER EXTENSION hstore ADD TABLE "member595.Quoted"; + ALTER EXTENSION hstore ADD TABLE member595_table; + + ALTER EXTENSION hstore ADD VIEW member595_view; + ALTER EXTENSION hstore ADD MATERIALIZED VIEW member595_matview; + ALTER EXTENSION hstore ADD SEQUENCE member595_sequence; + ALTER EXTENSION hstore ADD TYPE member595_enum; + ALTER EXTENSION hstore ADD TYPE member595_composite; + ALTER EXTENSION hstore ADD DOMAIN member595_domain; + ALTER EXTENSION hstore ADD FUNCTION member595_function(); + ALTER EXTENSION hstore ADD PROCEDURE member595_procedure(); + ALTER EXTENSION hstore ADD AGGREGATE member595_aggregate(integer); + ALTER EXTENSION hstore ADD FUNCTION member595_trigger_function(); + ALTER EXTENSION hstore ADD TABLE member595_partitioned; + ALTER EXTENSION hstore ADD TABLE member595_partition; + -- An independently created partition is an application table, even if its + -- parent is an extension member. It still needs its PARTITION OF metadata. + CREATE TABLE app595_external_partition PARTITION OF member595_partitioned + FOR VALUES FROM (10) TO (20); + ALTER FUNCTION app595_function() DEPENDS ON EXTENSION hstore; + CREATE SCHEMA extension595_other; + CREATE TABLE extension595_other.member595_table (id integer); + ALTER EXTENSION hstore ADD SCHEMA extension595_other; + `) + require.NoError(t, err) + defer db.ExecContext(ctx, `DROP SCHEMA IF EXISTS extension595_other CASCADE`) + q := queries.New(db) + schema := sql.NullString{String: schemaName, Valid: true} + // Each getter must keep application objects and omit extension definitions, + // children and ACLs, including explicitly changed member ACLs. + tests := []struct { + name string + run func() (string, error) + }{ + {"GetTables", func() (string, error) { return extensionRows(q.GetTables(ctx)) }}, + {"GetTablesForSchema", func() (string, error) { return extensionRows(q.GetTablesForSchema(ctx, schema)) }}, + {"GetColumns", func() (string, error) { return extensionRows(q.GetColumns(ctx)) }}, + {"GetColumnsForSchema", func() (string, error) { return extensionRows(q.GetColumnsForSchema(ctx, schema)) }}, + {"GetConstraints", func() (string, error) { return extensionRows(q.GetConstraints(ctx)) }}, + {"GetConstraintsForSchema", func() (string, error) { return extensionRows(q.GetConstraintsForSchema(ctx, schema)) }}, + {"GetIndexes", func() (string, error) { return extensionRows(q.GetIndexes(ctx)) }}, + {"GetIndexesForSchema", func() (string, error) { return extensionRows(q.GetIndexesForSchema(ctx, schema)) }}, + {"GetSequences", func() (string, error) { return extensionRows(q.GetSequences(ctx)) }}, + {"GetSequencesForSchema", func() (string, error) { return extensionRows(q.GetSequencesForSchema(ctx, schema)) }}, + {"GetFunctions", func() (string, error) { return extensionRows(q.GetFunctions(ctx)) }}, + {"GetFunctionsForSchema", func() (string, error) { return extensionRows(q.GetFunctionsForSchema(ctx, schema)) }}, + {"GetProcedures", func() (string, error) { return extensionRows(q.GetProcedures(ctx)) }}, + {"GetProceduresForSchema", func() (string, error) { return extensionRows(q.GetProceduresForSchema(ctx, schema)) }}, + {"GetAggregates", func() (string, error) { return extensionRows(q.GetAggregates(ctx)) }}, + {"GetAggregatesForSchema", func() (string, error) { return extensionRows(q.GetAggregatesForSchema(ctx, schema)) }}, + {"GetViews", func() (string, error) { return extensionRows(q.GetViews(ctx)) }}, + {"GetViewsForSchema", func() (string, error) { return extensionRows(q.GetViewsForSchema(ctx, schema)) }}, + {"GetTypes", func() (string, error) { return extensionRows(q.GetTypes(ctx)) }}, + {"GetTypesForSchema", func() (string, error) { return extensionRows(q.GetTypesForSchema(ctx, schema)) }}, + {"GetEnumValues", func() (string, error) { return extensionRows(q.GetEnumValues(ctx)) }}, + {"GetEnumValuesForSchema", func() (string, error) { return extensionRows(q.GetEnumValuesForSchema(ctx, schema)) }}, + {"GetCompositeTypeColumns", func() (string, error) { return extensionRows(q.GetCompositeTypeColumns(ctx)) }}, + {"GetCompositeTypeColumnsForSchema", func() (string, error) { return extensionRows(q.GetCompositeTypeColumnsForSchema(ctx, schema)) }}, + {"GetDomains", func() (string, error) { return extensionRows(q.GetDomains(ctx)) }}, + {"GetDomainsForSchema", func() (string, error) { return extensionRows(q.GetDomainsForSchema(ctx, schema)) }}, + {"GetDomainConstraints", func() (string, error) { return extensionRows(q.GetDomainConstraints(ctx)) }}, + {"GetDomainConstraintsForSchema", func() (string, error) { return extensionRows(q.GetDomainConstraintsForSchema(ctx, schema)) }}, + {"GetRLSTables", func() (string, error) { return extensionRows(q.GetRLSTables(ctx)) }}, + {"GetRLSTablesForSchema", func() (string, error) { return extensionRows(q.GetRLSTablesForSchema(ctx, schemaName)) }}, + {"GetRLSPolicies", func() (string, error) { return extensionRows(q.GetRLSPolicies(ctx)) }}, + {"GetRLSPoliciesForSchema", func() (string, error) { return extensionRows(q.GetRLSPoliciesForSchema(ctx, schemaName)) }}, + {"GetTriggers", func() (string, error) { return extensionRows(q.GetTriggers(ctx)) }}, + {"GetTriggersForSchema", func() (string, error) { return extensionRows(q.GetTriggersForSchema(ctx, schema)) }}, + {"GetPartitionChildren", func() (string, error) { return extensionRows(q.GetPartitionChildren(ctx)) }}, + {"GetPartitionedTablesForSchema", func() (string, error) { return extensionRows(q.GetPartitionedTablesForSchema(ctx, schema)) }}, + {"GetPrivilegesForSchema", func() (string, error) { return extensionRows(q.GetPrivilegesForSchema(ctx, schema)) }}, + {"GetRevokedDefaultPrivilegesForSchema", func() (string, error) { return extensionRows(q.GetRevokedDefaultPrivilegesForSchema(ctx, schema)) }}, + {"GetColumnPrivilegesForSchema", func() (string, error) { return extensionRows(q.GetColumnPrivilegesForSchema(ctx, schema)) }}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + rows, err := tt.run() + require.NoError(t, err) + // Global getters also return our same-name control in the other schema. + require.NotContains(t, rows, `"member595.Quoted"`) + // Check member rows by their schema as well as name, so the cross-schema + // control remains a positive test rather than a name-based ignore. + var records []map[string]any + require.NoError(t, json.Unmarshal([]byte(rows), &records)) + for _, record := range records { + if record["table_schema"] == "extension595_other" { + continue + } + // A managed partition may reference a member parent. Only the + // child's identity determines whether this row is managed. + delete(record, "parent_table") + encoded, err := json.Marshal(record) + require.NoError(t, err) + require.NotContains(t, string(encoded), "member595", "extension member leaked") + } + require.Contains(t, rows, "app595", "dependent application objects must remain managed") + switch tt.name { + case "GetSequences", "GetSequencesForSchema": + require.Contains(t, rows, "app595_sequence") + require.Contains(t, rows, "app595_table_id_seq") + case "GetPrivilegesForSchema": + require.Contains(t, rows, "app595_table_id_seq") + require.Contains(t, rows, "app595_view") + require.Contains(t, rows, "app595_matview") + case "GetRevokedDefaultPrivilegesForSchema": + require.Contains(t, rows, "app595_table") + require.Contains(t, rows, "app595_function") + require.Contains(t, rows, "app595_procedure") + require.Contains(t, rows, "app595_enum") + case "GetPartitionChildren": + require.Contains(t, rows, "app595_external_partition") + require.Contains(t, rows, "member595_partitioned") + } + }) + } + rows, err := q.GetTablesForSchema(ctx, sql.NullString{String: "extension595_other", Valid: true}) + require.NoError(t, err) + require.Len(t, rows, 1) + require.Equal(t, "member595_table", rows[0].TableName) +} + +func extensionRows[T any](rows []T, err error) (string, error) { + if err != nil { + return "", err + } + b, err := json.Marshal(rows) + return string(b), err +} + +// OIDs are unique only within a catalog. Simulate a collision without waiting +// for an OID wraparound, and roll back every synthetic catalog edge immediately. +// SetupPostgres creates a fresh cluster with a superuser, as required here. +func TestExtensionMembershipCatalogIdentity(t *testing.T) { + db, _, _, _, _, _ := testutil.ConnectToPostgres(t, sharedTestPostgres) + defer db.Close() + ctx := context.Background() + _, err := db.ExecContext(ctx, `CREATE FUNCTION public.app595_identity() RETURNS integer LANGUAGE sql AS 'SELECT 1'`) + require.NoError(t, err) + defer db.ExecContext(ctx, `DROP FUNCTION public.app595_identity()`) + for _, tc := range []struct { + name, class, refclass string + subid, refsubid int + }{ + {"different catalog, same OID", "pg_class", "pg_extension", 0, 0}, + {"different referenced catalog", "pg_proc", "pg_namespace", 0, 0}, + {"different subobject", "pg_proc", "pg_extension", 1, 0}, + {"different referenced subobject", "pg_proc", "pg_extension", 0, 1}, + } { + t.Run(tc.name, func(t *testing.T) { + tx, err := db.BeginTx(ctx, nil) + require.NoError(t, err) + defer tx.Rollback() + _, err = tx.ExecContext(ctx, `INSERT INTO pg_catalog.pg_depend + (classid, objid, objsubid, refclassid, refobjid, refobjsubid, deptype) + VALUES ($1::regclass, 'public.app595_identity()'::regprocedure, $2, + $3::regclass, (SELECT oid FROM pg_extension WHERE extname='plpgsql'), $4, 'e')`, + tc.class, tc.subid, tc.refclass, tc.refsubid) + require.NoError(t, err) + q := queries.New(tx) + global, err := extensionRows(q.GetFunctions(ctx)) + require.NoError(t, err) + require.Contains(t, global, "app595_identity") + scoped, err := extensionRows(q.GetFunctionsForSchema(ctx, sql.NullString{String: "public", Valid: true})) + require.NoError(t, err) + require.Contains(t, scoped, "app595_identity") + }) + } +} diff --git a/ir/queries/queries.sql b/ir/queries/queries.sql index 864cbb5a..5f022cdc 100644 --- a/ir/queries/queries.sql +++ b/ir/queries/queries.sql @@ -1,3 +1,9 @@ +-- Extension members are identified by their full pg_depend catalog identity. +-- Only deptype 'e' denotes membership; ordinary dependencies (including 'x', +-- AUTO_EXTENSION) must stay managed. Definitions and member ACLs are excluded +-- consistently for dump and both sides of planning. Schemas themselves remain +-- inspectable because an extension schema can also contain application objects. + -- GetSchemas retrieves all user-defined schemas -- name: GetSchemas :many SELECT @@ -37,6 +43,15 @@ WHERE AND t.table_schema NOT LIKE 'pg_temp_%' AND t.table_schema NOT LIKE 'pg_toast_temp_%' AND t.table_type IN ('BASE TABLE', 'VIEW') + AND NOT EXISTS ( + SELECT 1 FROM pg_catalog.pg_depend dep + WHERE dep.classid = 'pg_catalog.pg_class'::regclass + AND dep.objid = c.oid + AND dep.objsubid = 0 + AND dep.refclassid = 'pg_catalog.pg_extension'::regclass + AND dep.refobjsubid = 0 + AND dep.deptype = 'e' + ) ORDER BY t.table_schema, t.table_name; -- GetTablesForSchema retrieves all tables in a specific schema with metadata @@ -54,6 +69,15 @@ LEFT JOIN pg_description d ON d.objoid = c.oid AND d.classoid = 'pg_class'::regc WHERE t.table_schema = $1 AND t.table_type IN ('BASE TABLE', 'VIEW') + AND NOT EXISTS ( + SELECT 1 FROM pg_catalog.pg_depend dep + WHERE dep.classid = 'pg_catalog.pg_class'::regclass + AND dep.objid = c.oid + AND dep.objsubid = 0 + AND dep.refclassid = 'pg_catalog.pg_extension'::regclass + AND dep.refobjsubid = 0 + AND dep.deptype = 'e' + ) ORDER BY t.table_name; -- GetColumns retrieves all columns for all tables @@ -129,6 +153,15 @@ WITH column_base AS ( c.table_schema NOT IN ('information_schema', 'pg_catalog', 'pg_toast') AND c.table_schema NOT LIKE 'pg_temp_%' AND c.table_schema NOT LIKE 'pg_toast_temp_%' + AND NOT EXISTS ( + SELECT 1 FROM pg_catalog.pg_depend dep + WHERE dep.classid = 'pg_catalog.pg_class'::regclass + AND dep.objid = cl.oid + AND dep.objsubid = 0 + AND dep.refclassid = 'pg_catalog.pg_extension'::regclass + AND dep.refobjsubid = 0 + AND dep.deptype = 'e' + ) ) SELECT cb.table_schema, @@ -250,6 +283,15 @@ WITH column_base AS ( LEFT JOIN pg_constraint nn ON nn.conrelid = cl.oid AND nn.contype = 'n' AND NOT nn.convalidated AND a.attnum = ANY(nn.conkey) WHERE c.table_schema = $1 + AND NOT EXISTS ( + SELECT 1 FROM pg_catalog.pg_depend dep + WHERE dep.classid = 'pg_catalog.pg_class'::regclass + AND dep.objid = cl.oid + AND dep.objsubid = 0 + AND dep.refclassid = 'pg_catalog.pg_extension'::regclass + AND dep.refobjsubid = 0 + AND dep.deptype = 'e' + ) ) SELECT cb.table_schema, @@ -392,6 +434,15 @@ WHERE n.nspname NOT IN ('information_schema', 'pg_catalog', 'pg_toast') -- only root constraints (conparentid = 0) and child-specific constraints -- are dumpable. PARTITION OF auto-creates the inherited copies. AND c.conparentid = 0 + AND NOT EXISTS ( + SELECT 1 FROM pg_catalog.pg_depend dep + WHERE dep.classid = 'pg_catalog.pg_class'::regclass + AND dep.objid = cl.oid + AND dep.objsubid = 0 + AND dep.refclassid = 'pg_catalog.pg_extension'::regclass + AND dep.refobjsubid = 0 + AND dep.deptype = 'e' + ) ORDER BY n.nspname, cl.relname, c.contype, c.conname, a.attnum; -- GetIndexes retrieves all indexes including regular and unique indexes created with CREATE INDEX @@ -430,6 +481,24 @@ WITH index_base AS ( AND n.nspname NOT IN ('information_schema', 'pg_catalog', 'pg_toast') AND n.nspname NOT LIKE 'pg_temp_%' AND n.nspname NOT LIKE 'pg_toast_temp_%' + AND NOT EXISTS ( + SELECT 1 FROM pg_catalog.pg_depend dep + WHERE dep.classid = 'pg_catalog.pg_class'::regclass + AND dep.objid = t.oid + AND dep.objsubid = 0 + AND dep.refclassid = 'pg_catalog.pg_extension'::regclass + AND dep.refobjsubid = 0 + AND dep.deptype = 'e' + ) + AND NOT EXISTS ( + SELECT 1 FROM pg_catalog.pg_depend dep + WHERE dep.classid = 'pg_catalog.pg_class'::regclass + AND dep.objid = i.oid + AND dep.objsubid = 0 + AND dep.refclassid = 'pg_catalog.pg_extension'::regclass + AND dep.refobjsubid = 0 + AND dep.deptype = 'e' + ) ) SELECT ib.schemaname, @@ -528,6 +597,24 @@ WITH index_base AS ( AND c.contype IN ('u', 'p', 'x') ) AND n.nspname = $1 + AND NOT EXISTS ( + SELECT 1 FROM pg_catalog.pg_depend dep + WHERE dep.classid = 'pg_catalog.pg_class'::regclass + AND dep.objid = t.oid + AND dep.objsubid = 0 + AND dep.refclassid = 'pg_catalog.pg_extension'::regclass + AND dep.refobjsubid = 0 + AND dep.deptype = 'e' + ) + AND NOT EXISTS ( + SELECT 1 FROM pg_catalog.pg_depend dep + WHERE dep.classid = 'pg_catalog.pg_class'::regclass + AND dep.objid = i.oid + AND dep.objsubid = 0 + AND dep.refclassid = 'pg_catalog.pg_extension'::regclass + AND dep.refobjsubid = 0 + AND dep.deptype = 'e' + ) ) SELECT ib.schemaname, @@ -575,11 +662,39 @@ SELECT maximum_value, increment, cycle_option -FROM information_schema.sequences +FROM information_schema.sequences s +JOIN pg_namespace n ON n.nspname = s.sequence_schema +JOIN pg_class c ON c.relnamespace = n.oid AND c.relname = s.sequence_name WHERE sequence_schema NOT IN ('information_schema', 'pg_catalog', 'pg_toast') AND sequence_schema NOT LIKE 'pg_temp_%' AND sequence_schema NOT LIKE 'pg_toast_temp_%' + AND NOT EXISTS ( + SELECT 1 FROM pg_catalog.pg_depend dep + WHERE dep.classid = 'pg_catalog.pg_class'::regclass + AND dep.objid = c.oid + AND dep.objsubid = 0 + AND dep.refclassid = 'pg_catalog.pg_extension'::regclass + AND dep.refobjsubid = 0 + AND dep.deptype = 'e' + ) + -- SERIAL/identity sequences inherit the owning extension table's boundary. + AND NOT EXISTS ( + SELECT 1 FROM pg_catalog.pg_depend owned + JOIN pg_catalog.pg_depend ext ON ext.objid = owned.refobjid + AND ext.classid = 'pg_catalog.pg_class'::regclass + AND ext.objsubid = 0 + AND ext.refclassid = 'pg_catalog.pg_extension'::regclass + AND ext.refobjsubid = 0 + AND ext.deptype = 'e' + WHERE c.relkind = 'S' + AND owned.classid = 'pg_catalog.pg_class'::regclass + AND owned.objid = c.oid + AND owned.objsubid = 0 + AND owned.refclassid = 'pg_catalog.pg_class'::regclass + AND owned.refobjsubid > 0 + AND owned.deptype IN ('a', 'i') + ) ORDER BY sequence_schema, sequence_name; -- GetFunctions retrieves all user-defined functions (excluding extension members) @@ -606,14 +721,21 @@ FROM information_schema.routines r LEFT JOIN pg_proc p ON p.proname = r.routine_name AND p.pronamespace = (SELECT oid FROM pg_namespace WHERE nspname = r.routine_schema) AND p.oid = (regexp_match(r.specific_name, '_(\d+)$'))[1]::oid -LEFT JOIN pg_depend d ON d.objid = p.oid AND d.deptype = 'e' LEFT JOIN pg_description desc_func ON desc_func.objoid = p.oid AND desc_func.classoid = 'pg_proc'::regclass WHERE r.routine_schema NOT IN ('information_schema', 'pg_catalog', 'pg_toast') AND r.routine_schema NOT LIKE 'pg_temp_%' AND r.routine_schema NOT LIKE 'pg_toast_temp_%' AND r.routine_type = 'FUNCTION' - AND d.objid IS NULL -- Exclude functions that are extension members + AND NOT EXISTS ( + SELECT 1 FROM pg_catalog.pg_depend dep + WHERE dep.classid = 'pg_catalog.pg_proc'::regclass + AND dep.objid = p.oid + AND dep.objsubid = 0 + AND dep.refclassid = 'pg_catalog.pg_extension'::regclass + AND dep.refobjsubid = 0 + AND dep.deptype = 'e' + ) ORDER BY r.routine_schema, r.routine_name; -- GetProcedures retrieves all user-defined procedures (excluding extension members) @@ -631,14 +753,21 @@ FROM information_schema.routines r LEFT JOIN pg_proc p ON p.proname = r.routine_name AND p.pronamespace = (SELECT oid FROM pg_namespace WHERE nspname = r.routine_schema) AND p.oid = (regexp_match(r.specific_name, '_(\d+)$'))[1]::oid -LEFT JOIN pg_depend d ON d.objid = p.oid AND d.deptype = 'e' LEFT JOIN pg_description desc_proc ON desc_proc.objoid = p.oid AND desc_proc.classoid = 'pg_proc'::regclass WHERE r.routine_schema NOT IN ('information_schema', 'pg_catalog', 'pg_toast') AND r.routine_schema NOT LIKE 'pg_temp_%' AND r.routine_schema NOT LIKE 'pg_toast_temp_%' AND r.routine_type = 'PROCEDURE' - AND d.objid IS NULL -- Exclude procedures that are extension members + AND NOT EXISTS ( + SELECT 1 FROM pg_catalog.pg_depend dep + WHERE dep.classid = 'pg_catalog.pg_proc'::regclass + AND dep.objid = p.oid + AND dep.objsubid = 0 + AND dep.refclassid = 'pg_catalog.pg_extension'::regclass + AND dep.refobjsubid = 0 + AND dep.deptype = 'e' + ) ORDER BY r.routine_schema, r.routine_name; -- GetAggregates retrieves all user-defined aggregates @@ -687,9 +816,14 @@ WHERE p.prokind = 'a' -- Only aggregates AND n.nspname NOT LIKE 'pg_temp_%' AND n.nspname NOT LIKE 'pg_toast_temp_%' AND NOT EXISTS ( - SELECT 1 FROM pg_depend dep - WHERE dep.objid = p.oid AND dep.deptype = 'e' - ) -- Exclude extension members + SELECT 1 FROM pg_catalog.pg_depend dep + WHERE dep.classid = 'pg_catalog.pg_proc'::regclass + AND dep.objid = p.oid + AND dep.objsubid = 0 + AND dep.refclassid = 'pg_catalog.pg_extension'::regclass + AND dep.refobjsubid = 0 + AND dep.deptype = 'e' + ) ORDER BY n.nspname, p.proname; -- GetViews retrieves all views and materialized views @@ -708,6 +842,15 @@ WHERE AND n.nspname NOT IN ('information_schema', 'pg_catalog', 'pg_toast') AND n.nspname NOT LIKE 'pg_temp_%' AND n.nspname NOT LIKE 'pg_toast_temp_%' + AND NOT EXISTS ( + SELECT 1 FROM pg_catalog.pg_depend dep + WHERE dep.classid = 'pg_catalog.pg_class'::regclass + AND dep.objid = c.oid + AND dep.objsubid = 0 + AND dep.refclassid = 'pg_catalog.pg_extension'::regclass + AND dep.refobjsubid = 0 + AND dep.deptype = 'e' + ) ORDER BY n.nspname, c.relname; @@ -731,6 +874,15 @@ WHERE t.typtype IN ('e', 'c') -- ENUM and composite types only AND n.nspname NOT LIKE 'pg_temp_%' AND n.nspname NOT LIKE 'pg_toast_temp_%' AND (t.typtype = 'e' OR (t.typtype = 'c' AND c.relkind = 'c')) -- For composite types, only include true composite types (not table types) + AND NOT EXISTS ( + SELECT 1 FROM pg_catalog.pg_depend dep + WHERE dep.classid = 'pg_catalog.pg_type'::regclass + AND dep.objid = t.oid + AND dep.objsubid = 0 + AND dep.refclassid = 'pg_catalog.pg_extension'::regclass + AND dep.refobjsubid = 0 + AND dep.deptype = 'e' + ) ORDER BY n.nspname, t.typname; -- GetEnumValues retrieves enum values for ENUM types @@ -746,6 +898,15 @@ JOIN pg_namespace n ON t.typnamespace = n.oid WHERE n.nspname NOT IN ('information_schema', 'pg_catalog', 'pg_toast') AND n.nspname NOT LIKE 'pg_temp_%' AND n.nspname NOT LIKE 'pg_toast_temp_%' + AND NOT EXISTS ( + SELECT 1 FROM pg_catalog.pg_depend dep + WHERE dep.classid = 'pg_catalog.pg_type'::regclass + AND dep.objid = t.oid + AND dep.objsubid = 0 + AND dep.refclassid = 'pg_catalog.pg_extension'::regclass + AND dep.refobjsubid = 0 + AND dep.deptype = 'e' + ) ORDER BY n.nspname, t.typname, e.enumsortorder; -- GetCompositeTypeColumns retrieves columns for composite types @@ -782,6 +943,15 @@ WHERE t.typtype = 'c' -- composite types only AND n.nspname NOT IN ('information_schema', 'pg_catalog', 'pg_toast') AND n.nspname NOT LIKE 'pg_temp_%' AND n.nspname NOT LIKE 'pg_toast_temp_%' + AND NOT EXISTS ( + SELECT 1 FROM pg_catalog.pg_depend dep + WHERE dep.classid = 'pg_catalog.pg_type'::regclass + AND dep.objid = t.oid + AND dep.objsubid = 0 + AND dep.refclassid = 'pg_catalog.pg_extension'::regclass + AND dep.refobjsubid = 0 + AND dep.deptype = 'e' + ) ORDER BY n.nspname, t.typname, a.attnum; -- GetTriggers retrieves all triggers @@ -795,11 +965,32 @@ SELECT action_statement, action_condition, action_orientation -FROM information_schema.triggers +FROM information_schema.triggers it +JOIN pg_namespace n ON n.nspname = it.trigger_schema +JOIN pg_class c ON c.relnamespace = n.oid AND c.relname = it.event_object_table +JOIN pg_trigger t ON t.tgrelid = c.oid AND t.tgname = it.trigger_name WHERE trigger_schema NOT IN ('information_schema', 'pg_catalog', 'pg_toast') AND trigger_schema NOT LIKE 'pg_temp_%' AND trigger_schema NOT LIKE 'pg_toast_temp_%' + AND NOT EXISTS ( + SELECT 1 FROM pg_catalog.pg_depend dep + WHERE dep.classid = 'pg_catalog.pg_class'::regclass + AND dep.objid = c.oid + AND dep.objsubid = 0 + AND dep.refclassid = 'pg_catalog.pg_extension'::regclass + AND dep.refobjsubid = 0 + AND dep.deptype = 'e' + ) + AND NOT EXISTS ( + SELECT 1 FROM pg_catalog.pg_depend dep + WHERE dep.classid = 'pg_catalog.pg_trigger'::regclass + AND dep.objid = t.oid + AND dep.objsubid = 0 + AND dep.refclassid = 'pg_catalog.pg_extension'::regclass + AND dep.refobjsubid = 0 + AND dep.deptype = 'e' + ) ORDER BY trigger_schema, event_object_table, trigger_name; -- GetViewDependencies retrieves view dependencies on tables and other views @@ -834,6 +1025,15 @@ WHERE AND n.nspname NOT LIKE 'pg_toast_temp_%' AND c.relkind IN ('r', 'p') -- ordinary and partitioned tables (issue #471) AND c.relrowsecurity = true + AND NOT EXISTS ( + SELECT 1 FROM pg_catalog.pg_depend dep + WHERE dep.classid = 'pg_catalog.pg_class'::regclass + AND dep.objid = c.oid + AND dep.objsubid = 0 + AND dep.refclassid = 'pg_catalog.pg_extension'::regclass + AND dep.refobjsubid = 0 + AND dep.deptype = 'e' + ) ORDER BY n.nspname, c.relname; -- GetRLSPolicies retrieves all row level security policies @@ -881,6 +1081,15 @@ WHERE n.nspname NOT IN ('information_schema', 'pg_catalog', 'pg_toast') AND n.nspname NOT LIKE 'pg_temp_%' AND n.nspname NOT LIKE 'pg_toast_temp_%' + AND NOT EXISTS ( + SELECT 1 FROM pg_catalog.pg_depend dep + WHERE dep.classid = 'pg_catalog.pg_class'::regclass + AND dep.objid = c.oid + AND dep.objsubid = 0 + AND dep.refclassid = 'pg_catalog.pg_extension'::regclass + AND dep.refobjsubid = 0 + AND dep.deptype = 'e' + ) ORDER BY n.nspname, c.relname, pol.polname; -- GetRLSTablesForSchema retrieves tables with row level security enabled for a specific schema @@ -896,6 +1105,15 @@ WHERE n.nspname = $1 AND c.relkind IN ('r', 'p') -- ordinary and partitioned tables (issue #471) AND c.relrowsecurity = true + AND NOT EXISTS ( + SELECT 1 FROM pg_catalog.pg_depend dep + WHERE dep.classid = 'pg_catalog.pg_class'::regclass + AND dep.objid = c.oid + AND dep.objsubid = 0 + AND dep.refclassid = 'pg_catalog.pg_extension'::regclass + AND dep.refobjsubid = 0 + AND dep.deptype = 'e' + ) ORDER BY n.nspname, c.relname; -- GetRLSPoliciesForSchema retrieves all row level security policies for a specific schema @@ -941,6 +1159,15 @@ LEFT JOIN LATERAL ( ) e ON true WHERE n.nspname = $1 + AND NOT EXISTS ( + SELECT 1 FROM pg_catalog.pg_depend dep + WHERE dep.classid = 'pg_catalog.pg_class'::regclass + AND dep.objid = c.oid + AND dep.objsubid = 0 + AND dep.refclassid = 'pg_catalog.pg_extension'::regclass + AND dep.refobjsubid = 0 + AND dep.deptype = 'e' + ) ORDER BY n.nspname, c.relname, pol.polname; -- GetDomains retrieves all user-defined domains @@ -974,6 +1201,15 @@ WHERE t.typtype = 'd' -- Domain types only AND n.nspname NOT IN ('information_schema', 'pg_catalog', 'pg_toast') AND n.nspname NOT LIKE 'pg_temp_%' AND n.nspname NOT LIKE 'pg_toast_temp_%' + AND NOT EXISTS ( + SELECT 1 FROM pg_catalog.pg_depend dep + WHERE dep.classid = 'pg_catalog.pg_type'::regclass + AND dep.objid = t.oid + AND dep.objsubid = 0 + AND dep.refclassid = 'pg_catalog.pg_extension'::regclass + AND dep.refobjsubid = 0 + AND dep.deptype = 'e' + ) ORDER BY n.nspname, t.typname; -- GetDomainConstraints retrieves constraints for domains @@ -990,6 +1226,15 @@ WHERE t.typtype = 'd' -- Domain types only AND n.nspname NOT IN ('information_schema', 'pg_catalog', 'pg_toast') AND n.nspname NOT LIKE 'pg_temp_%' AND n.nspname NOT LIKE 'pg_toast_temp_%' + AND NOT EXISTS ( + SELECT 1 FROM pg_catalog.pg_depend dep + WHERE dep.classid = 'pg_catalog.pg_type'::regclass + AND dep.objid = t.oid + AND dep.objsubid = 0 + AND dep.refclassid = 'pg_catalog.pg_extension'::regclass + AND dep.refobjsubid = 0 + AND dep.deptype = 'e' + ) ORDER BY n.nspname, t.typname, c.conname; -- GetPartitionedTablesForSchema retrieves partition information for partitioned tables in a specific schema @@ -1009,6 +1254,15 @@ JOIN pg_class c ON pt.partrelid = c.oid JOIN pg_namespace n ON c.relnamespace = n.oid JOIN pg_attribute a ON a.attrelid = pt.partrelid AND a.attnum = ANY(pt.partattrs) WHERE n.nspname = $1 + AND NOT EXISTS ( + SELECT 1 FROM pg_catalog.pg_depend dep + WHERE dep.classid = 'pg_catalog.pg_class'::regclass + AND dep.objid = c.oid + AND dep.objsubid = 0 + AND dep.refclassid = 'pg_catalog.pg_extension'::regclass + AND dep.refobjsubid = 0 + AND dep.deptype = 'e' + ) GROUP BY n.nspname, c.relname, pt.partstrat ORDER BY n.nspname, c.relname; @@ -1035,8 +1289,45 @@ WHERE pn.nspname NOT IN ('information_schema', 'pg_catalog', 'pg_toast') SELECT 1 FROM pg_partitioned_table pt WHERE pt.partrelid = pc.oid ) + AND NOT EXISTS ( + SELECT 1 FROM pg_catalog.pg_depend dep + WHERE dep.classid = 'pg_catalog.pg_class'::regclass + AND dep.objid = cc.oid + AND dep.objsubid = 0 + AND dep.refclassid = 'pg_catalog.pg_extension'::regclass + AND dep.refobjsubid = 0 + AND dep.deptype = 'e' + ) ORDER BY pn.nspname, pc.relname, cn.nspname, cc.relname; +-- Parent columns are comparison metadata for managed partitions whose parent +-- is not in the managed IR (for example, an extension member). Do not apply +-- extension-member filters here: these rows never become managed tables. +-- name: GetPartitionParentColumnsForSchema :many +SELECT + cc.relname AS child_table, + a.attname AS column_name, + NOT (a.attnotnull OR (t.typtype = 'd' AND t.typnotnull)) AS is_nullable, + ge.column_default +FROM pg_catalog.pg_inherits inh +JOIN pg_catalog.pg_class cc ON cc.oid = inh.inhrelid +JOIN pg_catalog.pg_namespace cn ON cn.oid = cc.relnamespace +JOIN pg_catalog.pg_attribute a ON a.attrelid = inh.inhparent +JOIN pg_catalog.pg_type t ON t.oid = a.atttypid +LEFT JOIN pg_catalog.pg_attrdef d ON d.adrelid = a.attrelid AND d.adnum = a.attnum +LEFT JOIN LATERAL ( + SELECT + set_config('search_path', 'pg_catalog', true) AS dummy, + CASE WHEN a.attgenerated IN ('s', 'v') THEN NULL + ELSE pg_catalog.pg_get_expr(d.adbin, d.adrelid) + END AS column_default +) ge ON true +WHERE cn.nspname = $1 + AND cc.relispartition + AND a.attnum > 0 + AND NOT a.attisdropped +ORDER BY cc.relname, a.attnum; + -- GetConstraintsForSchema retrieves all table constraints for a specific schema -- name: GetConstraintsForSchema :many @@ -1121,6 +1412,15 @@ WHERE n.nspname = $1 -- only root constraints (conparentid = 0) and child-specific constraints -- are dumpable. PARTITION OF auto-creates the inherited copies. AND c.conparentid = 0 + AND NOT EXISTS ( + SELECT 1 FROM pg_catalog.pg_depend dep + WHERE dep.classid = 'pg_catalog.pg_class'::regclass + AND dep.objid = cl.oid + AND dep.objsubid = 0 + AND dep.refclassid = 'pg_catalog.pg_extension'::regclass + AND dep.refobjsubid = 0 + AND dep.deptype = 'e' + ) ORDER BY n.nspname, cl.relname, c.contype, c.conname, a.attnum; -- GetSequencesForSchema retrieves all sequences for a specific schema @@ -1148,6 +1448,32 @@ 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 WHERE s.schemaname = $1 + AND NOT EXISTS ( + SELECT 1 FROM pg_catalog.pg_depend dep + WHERE dep.classid = 'pg_catalog.pg_class'::regclass + AND dep.objid = c.oid + AND dep.objsubid = 0 + AND dep.refclassid = 'pg_catalog.pg_extension'::regclass + AND dep.refobjsubid = 0 + AND dep.deptype = 'e' + ) + -- SERIAL/identity sequences inherit the owning extension table's boundary. + AND NOT EXISTS ( + SELECT 1 FROM pg_catalog.pg_depend owned + JOIN pg_catalog.pg_depend ext ON ext.objid = owned.refobjid + AND ext.classid = 'pg_catalog.pg_class'::regclass + AND ext.objsubid = 0 + AND ext.refclassid = 'pg_catalog.pg_extension'::regclass + AND ext.refobjsubid = 0 + AND ext.deptype = 'e' + WHERE c.relkind = 'S' + AND owned.classid = 'pg_catalog.pg_class'::regclass + AND owned.objid = c.oid + AND owned.objsubid = 0 + AND owned.refclassid = 'pg_catalog.pg_class'::regclass + AND owned.refobjsubid > 0 + AND owned.deptype IN ('a', 'i') + ) ORDER BY s.schemaname, s.sequencename; -- GetFunctionsForSchema retrieves all user-defined functions for a specific schema @@ -1182,11 +1508,18 @@ FROM information_schema.routines r LEFT JOIN pg_proc p ON p.proname = r.routine_name AND p.pronamespace = (SELECT oid FROM pg_namespace WHERE nspname = r.routine_schema) AND p.oid = (regexp_match(r.specific_name, '_(\d+)$'))[1]::oid -LEFT JOIN pg_depend d ON d.objid = p.oid AND d.deptype = 'e' LEFT JOIN pg_description desc_func ON desc_func.objoid = p.oid AND desc_func.classoid = 'pg_proc'::regclass WHERE r.routine_schema = $1 AND r.routine_type = 'FUNCTION' - AND d.objid IS NULL -- Exclude functions that are extension members + AND NOT EXISTS ( + SELECT 1 FROM pg_catalog.pg_depend dep + WHERE dep.classid = 'pg_catalog.pg_proc'::regclass + AND dep.objid = p.oid + AND dep.objsubid = 0 + AND dep.refclassid = 'pg_catalog.pg_extension'::regclass + AND dep.refobjsubid = 0 + AND dep.deptype = 'e' + ) ORDER BY r.routine_schema, r.routine_name; -- GetProceduresForSchema retrieves all user-defined procedures for a specific schema @@ -1209,11 +1542,18 @@ FROM information_schema.routines r LEFT JOIN pg_proc p ON p.proname = r.routine_name AND p.pronamespace = (SELECT oid FROM pg_namespace WHERE nspname = r.routine_schema) AND p.oid = (regexp_match(r.specific_name, '_(\d+)$'))[1]::oid -LEFT JOIN pg_depend d ON d.objid = p.oid AND d.deptype = 'e' LEFT JOIN pg_description desc_proc ON desc_proc.objoid = p.oid AND desc_proc.classoid = 'pg_proc'::regclass WHERE r.routine_schema = $1 AND r.routine_type = 'PROCEDURE' - AND d.objid IS NULL -- Exclude procedures that are extension members + AND NOT EXISTS ( + SELECT 1 FROM pg_catalog.pg_depend dep + WHERE dep.classid = 'pg_catalog.pg_proc'::regclass + AND dep.objid = p.oid + AND dep.objsubid = 0 + AND dep.refclassid = 'pg_catalog.pg_extension'::regclass + AND dep.refobjsubid = 0 + AND dep.deptype = 'e' + ) ORDER BY r.routine_schema, r.routine_name; -- GetAggregatesForSchema retrieves all user-defined aggregates for a specific schema. @@ -1322,9 +1662,14 @@ LEFT JOIN pg_description d ON d.objoid = p.oid AND d.classoid = 'pg_proc'::regcl WHERE p.prokind = 'a' -- Only aggregates AND n.nspname = $1 AND NOT EXISTS ( - SELECT 1 FROM pg_depend dep - WHERE dep.objid = p.oid AND dep.deptype = 'e' - ) -- Exclude extension members + SELECT 1 FROM pg_catalog.pg_depend dep + WHERE dep.classid = 'pg_catalog.pg_proc'::regclass + AND dep.objid = p.oid + AND dep.objsubid = 0 + AND dep.refclassid = 'pg_catalog.pg_extension'::regclass + AND dep.refobjsubid = 0 + AND dep.deptype = 'e' + ) ORDER BY n.nspname, p.proname; -- GetViewsForSchema retrieves all views and materialized views for a specific schema @@ -1347,6 +1692,15 @@ WITH view_definitions AS ( WHERE c.relkind IN ('v', 'm') -- views and materialized views AND n.nspname = $1 + AND NOT EXISTS ( + SELECT 1 FROM pg_catalog.pg_depend dep + WHERE dep.classid = 'pg_catalog.pg_class'::regclass + AND dep.objid = c.oid + AND dep.objsubid = 0 + AND dep.refclassid = 'pg_catalog.pg_extension'::regclass + AND dep.refobjsubid = 0 + AND dep.deptype = 'e' + ) ) SELECT vd.table_schema, @@ -1416,6 +1770,24 @@ WHERE n.nspname = $1 -- defined on a partitioned parent; pg_dump emits only the top-level trigger -- on the parent (tgparentid = 0). AND t.tgparentid = 0 + AND NOT EXISTS ( + SELECT 1 FROM pg_catalog.pg_depend dep + WHERE dep.classid = 'pg_catalog.pg_class'::regclass + AND dep.objid = c.oid + AND dep.objsubid = 0 + AND dep.refclassid = 'pg_catalog.pg_extension'::regclass + AND dep.refobjsubid = 0 + AND dep.deptype = 'e' + ) + AND NOT EXISTS ( + SELECT 1 FROM pg_catalog.pg_depend dep + WHERE dep.classid = 'pg_catalog.pg_trigger'::regclass + AND dep.objid = t.oid + AND dep.objsubid = 0 + AND dep.refclassid = 'pg_catalog.pg_extension'::regclass + AND dep.refobjsubid = 0 + AND dep.deptype = 'e' + ) ORDER BY n.nspname, c.relname, t.tgname; -- GetTypesForSchema retrieves all user-defined types for a specific schema @@ -1436,6 +1808,15 @@ LEFT JOIN pg_class c ON t.typrelid = c.oid WHERE t.typtype IN ('e', 'c') -- ENUM and composite types only AND n.nspname = $1 AND (t.typtype = 'e' OR (t.typtype = 'c' AND c.relkind = 'c')) -- For composite types, only include true composite types (not table types) + AND NOT EXISTS ( + SELECT 1 FROM pg_catalog.pg_depend dep + WHERE dep.classid = 'pg_catalog.pg_type'::regclass + AND dep.objid = t.oid + AND dep.objsubid = 0 + AND dep.refclassid = 'pg_catalog.pg_extension'::regclass + AND dep.refobjsubid = 0 + AND dep.deptype = 'e' + ) ORDER BY n.nspname, t.typname; -- GetDomainsForSchema retrieves all user-defined domains for a specific schema @@ -1467,6 +1848,15 @@ LEFT JOIN pg_namespace ben ON bet.typnamespace = ben.oid LEFT JOIN pg_description d ON d.objoid = t.oid AND d.classoid = 'pg_type'::regclass WHERE t.typtype = 'd' -- Domain types only AND n.nspname = $1 + AND NOT EXISTS ( + SELECT 1 FROM pg_catalog.pg_depend dep + WHERE dep.classid = 'pg_catalog.pg_type'::regclass + AND dep.objid = t.oid + AND dep.objsubid = 0 + AND dep.refclassid = 'pg_catalog.pg_extension'::regclass + AND dep.refobjsubid = 0 + AND dep.deptype = 'e' + ) ORDER BY n.nspname, t.typname; -- GetDomainConstraintsForSchema retrieves constraints for domains in a specific schema @@ -1481,6 +1871,15 @@ JOIN pg_type t ON c.contypid = t.oid JOIN pg_namespace n ON t.typnamespace = n.oid WHERE t.typtype = 'd' -- Domain types only AND n.nspname = $1 + AND NOT EXISTS ( + SELECT 1 FROM pg_catalog.pg_depend dep + WHERE dep.classid = 'pg_catalog.pg_type'::regclass + AND dep.objid = t.oid + AND dep.objsubid = 0 + AND dep.refclassid = 'pg_catalog.pg_extension'::regclass + AND dep.refobjsubid = 0 + AND dep.deptype = 'e' + ) ORDER BY n.nspname, t.typname, c.conname; -- GetEnumValuesForSchema retrieves enum values for ENUM types in a specific schema @@ -1494,6 +1893,15 @@ FROM pg_enum e JOIN pg_type t ON e.enumtypid = t.oid JOIN pg_namespace n ON t.typnamespace = n.oid WHERE n.nspname = $1 + AND NOT EXISTS ( + SELECT 1 FROM pg_catalog.pg_depend dep + WHERE dep.classid = 'pg_catalog.pg_type'::regclass + AND dep.objid = t.oid + AND dep.objsubid = 0 + AND dep.refclassid = 'pg_catalog.pg_extension'::regclass + AND dep.refobjsubid = 0 + AND dep.deptype = 'e' + ) ORDER BY n.nspname, t.typname, e.enumsortorder; -- GetCompositeTypeColumnsForSchema retrieves columns for composite types in a specific schema @@ -1528,6 +1936,15 @@ WHERE t.typtype = 'c' -- composite types only AND a.attnum > 0 -- exclude system columns AND NOT a.attisdropped -- exclude dropped columns AND n.nspname = $1 + AND NOT EXISTS ( + SELECT 1 FROM pg_catalog.pg_depend dep + WHERE dep.classid = 'pg_catalog.pg_type'::regclass + AND dep.objid = t.oid + AND dep.objsubid = 0 + AND dep.refclassid = 'pg_catalog.pg_extension'::regclass + AND dep.refobjsubid = 0 + AND dep.deptype = 'e' + ) ORDER BY n.nspname, t.typname, a.attnum; -- GetDefaultPrivilegesForSchema retrieves default privileges for a specific schema @@ -1583,8 +2000,34 @@ WITH acl_data AS ( FROM pg_class c JOIN pg_namespace n ON c.relnamespace = n.oid WHERE n.nspname = $1 + AND NOT EXISTS ( + SELECT 1 FROM pg_catalog.pg_depend dep + WHERE dep.classid = 'pg_catalog.pg_class'::regclass + AND dep.objid = c.oid + AND dep.objsubid = 0 + AND dep.refclassid = 'pg_catalog.pg_extension'::regclass + AND dep.refobjsubid = 0 + AND dep.deptype = 'e' + ) AND c.relkind IN ('r', 'v', 'm', 'S') AND c.relacl IS NOT NULL + -- SERIAL/identity sequences inherit the owning extension table's boundary. + AND NOT EXISTS ( + SELECT 1 FROM pg_catalog.pg_depend owned + JOIN pg_catalog.pg_depend ext ON ext.objid = owned.refobjid + AND ext.classid = 'pg_catalog.pg_class'::regclass + AND ext.objsubid = 0 + AND ext.refclassid = 'pg_catalog.pg_extension'::regclass + AND ext.refobjsubid = 0 + AND ext.deptype = 'e' + WHERE c.relkind = 'S' + AND owned.classid = 'pg_catalog.pg_class'::regclass + AND owned.objid = c.oid + AND owned.objsubid = 0 + AND owned.refclassid = 'pg_catalog.pg_class'::regclass + AND owned.refobjsubid > 0 + AND owned.deptype IN ('a', 'i') + ) UNION ALL @@ -1598,6 +2041,15 @@ WITH acl_data AS ( FROM pg_proc p JOIN pg_namespace n ON p.pronamespace = n.oid WHERE n.nspname = $1 + AND NOT EXISTS ( + SELECT 1 FROM pg_catalog.pg_depend dep + WHERE dep.classid = 'pg_catalog.pg_proc'::regclass + AND dep.objid = p.oid + AND dep.objsubid = 0 + AND dep.refclassid = 'pg_catalog.pg_extension'::regclass + AND dep.refobjsubid = 0 + AND dep.deptype = 'e' + ) AND p.prokind = 'f' AND p.proacl IS NOT NULL @@ -1613,6 +2065,15 @@ WITH acl_data AS ( FROM pg_proc p JOIN pg_namespace n ON p.pronamespace = n.oid WHERE n.nspname = $1 + AND NOT EXISTS ( + SELECT 1 FROM pg_catalog.pg_depend dep + WHERE dep.classid = 'pg_catalog.pg_proc'::regclass + AND dep.objid = p.oid + AND dep.objsubid = 0 + AND dep.refclassid = 'pg_catalog.pg_extension'::regclass + AND dep.refobjsubid = 0 + AND dep.deptype = 'e' + ) AND p.prokind = 'p' AND p.proacl IS NOT NULL @@ -1628,7 +2089,25 @@ WITH acl_data AS ( FROM pg_type t JOIN pg_namespace n ON t.typnamespace = n.oid WHERE n.nspname = $1 + AND NOT EXISTS ( + SELECT 1 FROM pg_catalog.pg_depend dep + WHERE dep.classid = 'pg_catalog.pg_type'::regclass + AND dep.objid = t.oid + AND dep.objsubid = 0 + AND dep.refclassid = 'pg_catalog.pg_extension'::regclass + AND dep.refobjsubid = 0 + AND dep.deptype = 'e' + ) AND t.typtype IN ('e', 'c', 'd') + AND NOT EXISTS ( + SELECT 1 FROM pg_catalog.pg_depend dep + WHERE dep.classid = 'pg_catalog.pg_class'::regclass + AND dep.objid = t.typrelid + AND dep.objsubid = 0 + AND dep.refclassid = 'pg_catalog.pg_extension'::regclass + AND dep.refobjsubid = 0 + AND dep.deptype = 'e' + ) AND t.typacl IS NOT NULL ) SELECT @@ -1653,6 +2132,15 @@ WITH objects_with_acl AS ( FROM pg_proc p JOIN pg_namespace n ON p.pronamespace = n.oid WHERE n.nspname = $1 + AND NOT EXISTS ( + SELECT 1 FROM pg_catalog.pg_depend dep + WHERE dep.classid = 'pg_catalog.pg_proc'::regclass + AND dep.objid = p.oid + AND dep.objsubid = 0 + AND dep.refclassid = 'pg_catalog.pg_extension'::regclass + AND dep.refobjsubid = 0 + AND dep.deptype = 'e' + ) AND p.prokind = 'f' UNION ALL @@ -1665,6 +2153,15 @@ WITH objects_with_acl AS ( FROM pg_proc p JOIN pg_namespace n ON p.pronamespace = n.oid WHERE n.nspname = $1 + AND NOT EXISTS ( + SELECT 1 FROM pg_catalog.pg_depend dep + WHERE dep.classid = 'pg_catalog.pg_proc'::regclass + AND dep.objid = p.oid + AND dep.objsubid = 0 + AND dep.refclassid = 'pg_catalog.pg_extension'::regclass + AND dep.refobjsubid = 0 + AND dep.deptype = 'e' + ) AND p.prokind = 'p' UNION ALL @@ -1677,7 +2174,25 @@ WITH objects_with_acl AS ( FROM pg_type t JOIN pg_namespace n ON t.typnamespace = n.oid WHERE n.nspname = $1 + AND NOT EXISTS ( + SELECT 1 FROM pg_catalog.pg_depend dep + WHERE dep.classid = 'pg_catalog.pg_type'::regclass + AND dep.objid = t.oid + AND dep.objsubid = 0 + AND dep.refclassid = 'pg_catalog.pg_extension'::regclass + AND dep.refobjsubid = 0 + AND dep.deptype = 'e' + ) AND t.typtype IN ('e', 'c', 'd') + AND NOT EXISTS ( + SELECT 1 FROM pg_catalog.pg_depend dep + WHERE dep.classid = 'pg_catalog.pg_class'::regclass + AND dep.objid = t.typrelid + AND dep.objsubid = 0 + AND dep.refclassid = 'pg_catalog.pg_extension'::regclass + AND dep.refobjsubid = 0 + AND dep.deptype = 'e' + ) ), public_grants AS ( SELECT @@ -1708,6 +2223,15 @@ WITH column_acls AS ( JOIN pg_class c ON a.attrelid = c.oid JOIN pg_namespace n ON c.relnamespace = n.oid WHERE n.nspname = $1 + AND NOT EXISTS ( + SELECT 1 FROM pg_catalog.pg_depend dep + WHERE dep.classid = 'pg_catalog.pg_class'::regclass + AND dep.objid = c.oid + AND dep.objsubid = 0 + AND dep.refclassid = 'pg_catalog.pg_extension'::regclass + AND dep.refobjsubid = 0 + AND dep.deptype = 'e' + ) AND c.relkind IN ('r', 'v', 'm') -- tables, views, materialized views AND a.attnum > 0 -- skip system columns AND NOT a.attisdropped @@ -1739,4 +2263,4 @@ JOIN pg_namespace referenced_ns ON referenced_proc.pronamespace = referenced_ns. WHERE d.classid = 'pg_proc'::regclass AND d.refclassid = 'pg_proc'::regclass AND d.deptype = 'n' - AND dependent_ns.nspname = $1; \ No newline at end of file + AND dependent_ns.nspname = $1; diff --git a/ir/queries/queries.sql.go b/ir/queries/queries.sql.go index d5dff9ad..f1783968 100644 --- a/ir/queries/queries.sql.go +++ b/ir/queries/queries.sql.go @@ -57,9 +57,14 @@ WHERE p.prokind = 'a' -- Only aggregates AND n.nspname NOT LIKE 'pg_temp_%' AND n.nspname NOT LIKE 'pg_toast_temp_%' AND NOT EXISTS ( - SELECT 1 FROM pg_depend dep - WHERE dep.objid = p.oid AND dep.deptype = 'e' - ) -- Exclude extension members + SELECT 1 FROM pg_catalog.pg_depend dep + WHERE dep.classid = 'pg_catalog.pg_proc'::regclass + AND dep.objid = p.oid + AND dep.objsubid = 0 + AND dep.refclassid = 'pg_catalog.pg_extension'::regclass + AND dep.refobjsubid = 0 + AND dep.deptype = 'e' + ) ORDER BY n.nspname, p.proname ` @@ -217,9 +222,14 @@ LEFT JOIN pg_description d ON d.objoid = p.oid AND d.classoid = 'pg_proc'::regcl WHERE p.prokind = 'a' -- Only aggregates AND n.nspname = $1 AND NOT EXISTS ( - SELECT 1 FROM pg_depend dep - WHERE dep.objid = p.oid AND dep.deptype = 'e' - ) -- Exclude extension members + SELECT 1 FROM pg_catalog.pg_depend dep + WHERE dep.classid = 'pg_catalog.pg_proc'::regclass + AND dep.objid = p.oid + AND dep.objsubid = 0 + AND dep.refclassid = 'pg_catalog.pg_extension'::regclass + AND dep.refobjsubid = 0 + AND dep.deptype = 'e' + ) ORDER BY n.nspname, p.proname ` @@ -318,6 +328,15 @@ WITH column_acls AS ( JOIN pg_class c ON a.attrelid = c.oid JOIN pg_namespace n ON c.relnamespace = n.oid WHERE n.nspname = $1 + AND NOT EXISTS ( + SELECT 1 FROM pg_catalog.pg_depend dep + WHERE dep.classid = 'pg_catalog.pg_class'::regclass + AND dep.objid = c.oid + AND dep.objsubid = 0 + AND dep.refclassid = 'pg_catalog.pg_extension'::regclass + AND dep.refobjsubid = 0 + AND dep.deptype = 'e' + ) AND c.relkind IN ('r', 'v', 'm') -- tables, views, materialized views AND a.attnum > 0 -- skip system columns AND NOT a.attisdropped @@ -444,6 +463,15 @@ WITH column_base AS ( c.table_schema NOT IN ('information_schema', 'pg_catalog', 'pg_toast') AND c.table_schema NOT LIKE 'pg_temp_%' AND c.table_schema NOT LIKE 'pg_toast_temp_%' + AND NOT EXISTS ( + SELECT 1 FROM pg_catalog.pg_depend dep + WHERE dep.classid = 'pg_catalog.pg_class'::regclass + AND dep.objid = cl.oid + AND dep.objsubid = 0 + AND dep.refclassid = 'pg_catalog.pg_extension'::regclass + AND dep.refobjsubid = 0 + AND dep.deptype = 'e' + ) ) SELECT cb.table_schema, @@ -639,6 +667,15 @@ WITH column_base AS ( LEFT JOIN pg_constraint nn ON nn.conrelid = cl.oid AND nn.contype = 'n' AND NOT nn.convalidated AND a.attnum = ANY(nn.conkey) WHERE c.table_schema = $1 + AND NOT EXISTS ( + SELECT 1 FROM pg_catalog.pg_depend dep + WHERE dep.classid = 'pg_catalog.pg_class'::regclass + AND dep.objid = cl.oid + AND dep.objsubid = 0 + AND dep.refclassid = 'pg_catalog.pg_extension'::regclass + AND dep.refobjsubid = 0 + AND dep.deptype = 'e' + ) ) SELECT cb.table_schema, @@ -808,6 +845,15 @@ WHERE t.typtype = 'c' -- composite types only AND n.nspname NOT IN ('information_schema', 'pg_catalog', 'pg_toast') AND n.nspname NOT LIKE 'pg_temp_%' AND n.nspname NOT LIKE 'pg_toast_temp_%' + AND NOT EXISTS ( + SELECT 1 FROM pg_catalog.pg_depend dep + WHERE dep.classid = 'pg_catalog.pg_type'::regclass + AND dep.objid = t.oid + AND dep.objsubid = 0 + AND dep.refclassid = 'pg_catalog.pg_extension'::regclass + AND dep.refobjsubid = 0 + AND dep.deptype = 'e' + ) ORDER BY n.nspname, t.typname, a.attnum ` @@ -880,6 +926,15 @@ WHERE t.typtype = 'c' -- composite types only AND a.attnum > 0 -- exclude system columns AND NOT a.attisdropped -- exclude dropped columns AND n.nspname = $1 + AND NOT EXISTS ( + SELECT 1 FROM pg_catalog.pg_depend dep + WHERE dep.classid = 'pg_catalog.pg_type'::regclass + AND dep.objid = t.oid + AND dep.objsubid = 0 + AND dep.refclassid = 'pg_catalog.pg_extension'::regclass + AND dep.refobjsubid = 0 + AND dep.deptype = 'e' + ) ORDER BY n.nspname, t.typname, a.attnum ` @@ -1001,6 +1056,15 @@ WHERE n.nspname NOT IN ('information_schema', 'pg_catalog', 'pg_toast') -- only root constraints (conparentid = 0) and child-specific constraints -- are dumpable. PARTITION OF auto-creates the inherited copies. AND c.conparentid = 0 + AND NOT EXISTS ( + SELECT 1 FROM pg_catalog.pg_depend dep + WHERE dep.classid = 'pg_catalog.pg_class'::regclass + AND dep.objid = cl.oid + AND dep.objsubid = 0 + AND dep.refclassid = 'pg_catalog.pg_extension'::regclass + AND dep.refobjsubid = 0 + AND dep.deptype = 'e' + ) ORDER BY n.nspname, cl.relname, c.contype, c.conname, a.attnum ` @@ -1154,6 +1218,15 @@ WHERE n.nspname = $1 -- only root constraints (conparentid = 0) and child-specific constraints -- are dumpable. PARTITION OF auto-creates the inherited copies. AND c.conparentid = 0 + AND NOT EXISTS ( + SELECT 1 FROM pg_catalog.pg_depend dep + WHERE dep.classid = 'pg_catalog.pg_class'::regclass + AND dep.objid = cl.oid + AND dep.objsubid = 0 + AND dep.refclassid = 'pg_catalog.pg_extension'::regclass + AND dep.refobjsubid = 0 + AND dep.deptype = 'e' + ) ORDER BY n.nspname, cl.relname, c.contype, c.conname, a.attnum ` @@ -1313,6 +1386,15 @@ WHERE t.typtype = 'd' -- Domain types only AND n.nspname NOT IN ('information_schema', 'pg_catalog', 'pg_toast') AND n.nspname NOT LIKE 'pg_temp_%' AND n.nspname NOT LIKE 'pg_toast_temp_%' + AND NOT EXISTS ( + SELECT 1 FROM pg_catalog.pg_depend dep + WHERE dep.classid = 'pg_catalog.pg_type'::regclass + AND dep.objid = t.oid + AND dep.objsubid = 0 + AND dep.refclassid = 'pg_catalog.pg_extension'::regclass + AND dep.refobjsubid = 0 + AND dep.deptype = 'e' + ) ORDER BY n.nspname, t.typname, c.conname ` @@ -1363,6 +1445,15 @@ JOIN pg_type t ON c.contypid = t.oid JOIN pg_namespace n ON t.typnamespace = n.oid WHERE t.typtype = 'd' -- Domain types only AND n.nspname = $1 + AND NOT EXISTS ( + SELECT 1 FROM pg_catalog.pg_depend dep + WHERE dep.classid = 'pg_catalog.pg_type'::regclass + AND dep.objid = t.oid + AND dep.objsubid = 0 + AND dep.refclassid = 'pg_catalog.pg_extension'::regclass + AND dep.refobjsubid = 0 + AND dep.deptype = 'e' + ) ORDER BY n.nspname, t.typname, c.conname ` @@ -1432,6 +1523,15 @@ WHERE t.typtype = 'd' -- Domain types only AND n.nspname NOT IN ('information_schema', 'pg_catalog', 'pg_toast') AND n.nspname NOT LIKE 'pg_temp_%' AND n.nspname NOT LIKE 'pg_toast_temp_%' + AND NOT EXISTS ( + SELECT 1 FROM pg_catalog.pg_depend dep + WHERE dep.classid = 'pg_catalog.pg_type'::regclass + AND dep.objid = t.oid + AND dep.objsubid = 0 + AND dep.refclassid = 'pg_catalog.pg_extension'::regclass + AND dep.refobjsubid = 0 + AND dep.deptype = 'e' + ) ORDER BY n.nspname, t.typname ` @@ -1503,6 +1603,15 @@ LEFT JOIN pg_namespace ben ON bet.typnamespace = ben.oid LEFT JOIN pg_description d ON d.objoid = t.oid AND d.classoid = 'pg_type'::regclass WHERE t.typtype = 'd' -- Domain types only AND n.nspname = $1 + AND NOT EXISTS ( + SELECT 1 FROM pg_catalog.pg_depend dep + WHERE dep.classid = 'pg_catalog.pg_type'::regclass + AND dep.objid = t.oid + AND dep.objsubid = 0 + AND dep.refclassid = 'pg_catalog.pg_extension'::regclass + AND dep.refobjsubid = 0 + AND dep.deptype = 'e' + ) ORDER BY n.nspname, t.typname ` @@ -1558,6 +1667,15 @@ JOIN pg_namespace n ON t.typnamespace = n.oid WHERE n.nspname NOT IN ('information_schema', 'pg_catalog', 'pg_toast') AND n.nspname NOT LIKE 'pg_temp_%' AND n.nspname NOT LIKE 'pg_toast_temp_%' + AND NOT EXISTS ( + SELECT 1 FROM pg_catalog.pg_depend dep + WHERE dep.classid = 'pg_catalog.pg_type'::regclass + AND dep.objid = t.oid + AND dep.objsubid = 0 + AND dep.refclassid = 'pg_catalog.pg_extension'::regclass + AND dep.refobjsubid = 0 + AND dep.deptype = 'e' + ) ORDER BY n.nspname, t.typname, e.enumsortorder ` @@ -1607,6 +1725,15 @@ FROM pg_enum e JOIN pg_type t ON e.enumtypid = t.oid JOIN pg_namespace n ON t.typnamespace = n.oid WHERE n.nspname = $1 + AND NOT EXISTS ( + SELECT 1 FROM pg_catalog.pg_depend dep + WHERE dep.classid = 'pg_catalog.pg_type'::regclass + AND dep.objid = t.oid + AND dep.objsubid = 0 + AND dep.refclassid = 'pg_catalog.pg_extension'::regclass + AND dep.refobjsubid = 0 + AND dep.deptype = 'e' + ) ORDER BY n.nspname, t.typname, e.enumsortorder ` @@ -1728,14 +1855,21 @@ FROM information_schema.routines r LEFT JOIN pg_proc p ON p.proname = r.routine_name AND p.pronamespace = (SELECT oid FROM pg_namespace WHERE nspname = r.routine_schema) AND p.oid = (regexp_match(r.specific_name, '_(\d+)$'))[1]::oid -LEFT JOIN pg_depend d ON d.objid = p.oid AND d.deptype = 'e' LEFT JOIN pg_description desc_func ON desc_func.objoid = p.oid AND desc_func.classoid = 'pg_proc'::regclass WHERE r.routine_schema NOT IN ('information_schema', 'pg_catalog', 'pg_toast') AND r.routine_schema NOT LIKE 'pg_temp_%' AND r.routine_schema NOT LIKE 'pg_toast_temp_%' AND r.routine_type = 'FUNCTION' - AND d.objid IS NULL -- Exclude functions that are extension members + AND NOT EXISTS ( + SELECT 1 FROM pg_catalog.pg_depend dep + WHERE dep.classid = 'pg_catalog.pg_proc'::regclass + AND dep.objid = p.oid + AND dep.objsubid = 0 + AND dep.refclassid = 'pg_catalog.pg_extension'::regclass + AND dep.refobjsubid = 0 + AND dep.deptype = 'e' + ) ORDER BY r.routine_schema, r.routine_name ` @@ -1822,11 +1956,18 @@ FROM information_schema.routines r LEFT JOIN pg_proc p ON p.proname = r.routine_name AND p.pronamespace = (SELECT oid FROM pg_namespace WHERE nspname = r.routine_schema) AND p.oid = (regexp_match(r.specific_name, '_(\d+)$'))[1]::oid -LEFT JOIN pg_depend d ON d.objid = p.oid AND d.deptype = 'e' LEFT JOIN pg_description desc_func ON desc_func.objoid = p.oid AND desc_func.classoid = 'pg_proc'::regclass WHERE r.routine_schema = $1 AND r.routine_type = 'FUNCTION' - AND d.objid IS NULL -- Exclude functions that are extension members + AND NOT EXISTS ( + SELECT 1 FROM pg_catalog.pg_depend dep + WHERE dep.classid = 'pg_catalog.pg_proc'::regclass + AND dep.objid = p.oid + AND dep.objsubid = 0 + AND dep.refclassid = 'pg_catalog.pg_extension'::regclass + AND dep.refobjsubid = 0 + AND dep.deptype = 'e' + ) ORDER BY r.routine_schema, r.routine_name ` @@ -1920,6 +2061,24 @@ WITH index_base AS ( AND n.nspname NOT IN ('information_schema', 'pg_catalog', 'pg_toast') AND n.nspname NOT LIKE 'pg_temp_%' AND n.nspname NOT LIKE 'pg_toast_temp_%' + AND NOT EXISTS ( + SELECT 1 FROM pg_catalog.pg_depend dep + WHERE dep.classid = 'pg_catalog.pg_class'::regclass + AND dep.objid = t.oid + AND dep.objsubid = 0 + AND dep.refclassid = 'pg_catalog.pg_extension'::regclass + AND dep.refobjsubid = 0 + AND dep.deptype = 'e' + ) + AND NOT EXISTS ( + SELECT 1 FROM pg_catalog.pg_depend dep + WHERE dep.classid = 'pg_catalog.pg_class'::regclass + AND dep.objid = i.oid + AND dep.objsubid = 0 + AND dep.refclassid = 'pg_catalog.pg_extension'::regclass + AND dep.refobjsubid = 0 + AND dep.deptype = 'e' + ) ) SELECT ib.schemaname, @@ -2066,6 +2225,24 @@ WITH index_base AS ( AND c.contype IN ('u', 'p', 'x') ) AND n.nspname = $1 + AND NOT EXISTS ( + SELECT 1 FROM pg_catalog.pg_depend dep + WHERE dep.classid = 'pg_catalog.pg_class'::regclass + AND dep.objid = t.oid + AND dep.objsubid = 0 + AND dep.refclassid = 'pg_catalog.pg_extension'::regclass + AND dep.refobjsubid = 0 + AND dep.deptype = 'e' + ) + AND NOT EXISTS ( + SELECT 1 FROM pg_catalog.pg_depend dep + WHERE dep.classid = 'pg_catalog.pg_class'::regclass + AND dep.objid = i.oid + AND dep.objsubid = 0 + AND dep.refclassid = 'pg_catalog.pg_extension'::regclass + AND dep.refobjsubid = 0 + AND dep.deptype = 'e' + ) ) SELECT ib.schemaname, @@ -2194,6 +2371,15 @@ WHERE pn.nspname NOT IN ('information_schema', 'pg_catalog', 'pg_toast') SELECT 1 FROM pg_partitioned_table pt WHERE pt.partrelid = pc.oid ) + AND NOT EXISTS ( + SELECT 1 FROM pg_catalog.pg_depend dep + WHERE dep.classid = 'pg_catalog.pg_class'::regclass + AND dep.objid = cc.oid + AND dep.objsubid = 0 + AND dep.refclassid = 'pg_catalog.pg_extension'::regclass + AND dep.refobjsubid = 0 + AND dep.deptype = 'e' + ) ORDER BY pn.nspname, pc.relname, cn.nspname, cc.relname ` @@ -2235,6 +2421,70 @@ func (q *Queries) GetPartitionChildren(ctx context.Context) ([]GetPartitionChild return items, nil } +const getPartitionParentColumnsForSchema = `-- name: GetPartitionParentColumnsForSchema :many +SELECT + cc.relname AS child_table, + a.attname AS column_name, + NOT (a.attnotnull OR (t.typtype = 'd' AND t.typnotnull)) AS is_nullable, + ge.column_default +FROM pg_catalog.pg_inherits inh +JOIN pg_catalog.pg_class cc ON cc.oid = inh.inhrelid +JOIN pg_catalog.pg_namespace cn ON cn.oid = cc.relnamespace +JOIN pg_catalog.pg_attribute a ON a.attrelid = inh.inhparent +JOIN pg_catalog.pg_type t ON t.oid = a.atttypid +LEFT JOIN pg_catalog.pg_attrdef d ON d.adrelid = a.attrelid AND d.adnum = a.attnum +LEFT JOIN LATERAL ( + SELECT + set_config('search_path', 'pg_catalog', true) AS dummy, + CASE WHEN a.attgenerated IN ('s', 'v') THEN NULL + ELSE pg_catalog.pg_get_expr(d.adbin, d.adrelid) + END AS column_default +) ge ON true +WHERE cn.nspname = $1 + AND cc.relispartition + AND a.attnum > 0 + AND NOT a.attisdropped +ORDER BY cc.relname, a.attnum +` + +type GetPartitionParentColumnsForSchemaRow struct { + ChildTable string `db:"child_table" json:"child_table"` + ColumnName string `db:"column_name" json:"column_name"` + IsNullable sql.NullBool `db:"is_nullable" json:"is_nullable"` + ColumnDefault sql.NullString `db:"column_default" json:"column_default"` +} + +// Parent columns are comparison metadata for managed partitions whose parent +// is not in the managed IR (for example, an extension member). Do not apply +// extension-member filters here: these rows never become managed tables. +func (q *Queries) GetPartitionParentColumnsForSchema(ctx context.Context, nspname string) ([]GetPartitionParentColumnsForSchemaRow, error) { + rows, err := q.db.QueryContext(ctx, getPartitionParentColumnsForSchema, nspname) + if err != nil { + return nil, err + } + defer rows.Close() + var items []GetPartitionParentColumnsForSchemaRow + for rows.Next() { + var i GetPartitionParentColumnsForSchemaRow + if err := rows.Scan( + &i.ChildTable, + &i.ColumnName, + &i.IsNullable, + &i.ColumnDefault, + ); 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 getPartitionedTablesForSchema = `-- name: GetPartitionedTablesForSchema :many SELECT n.nspname AS table_schema, @@ -2251,6 +2501,15 @@ JOIN pg_class c ON pt.partrelid = c.oid JOIN pg_namespace n ON c.relnamespace = n.oid JOIN pg_attribute a ON a.attrelid = pt.partrelid AND a.attnum = ANY(pt.partattrs) WHERE n.nspname = $1 + AND NOT EXISTS ( + SELECT 1 FROM pg_catalog.pg_depend dep + WHERE dep.classid = 'pg_catalog.pg_class'::regclass + AND dep.objid = c.oid + AND dep.objsubid = 0 + AND dep.refclassid = 'pg_catalog.pg_extension'::regclass + AND dep.refobjsubid = 0 + AND dep.deptype = 'e' + ) GROUP BY n.nspname, c.relname, pt.partstrat ORDER BY n.nspname, c.relname ` @@ -2308,8 +2567,34 @@ WITH acl_data AS ( FROM pg_class c JOIN pg_namespace n ON c.relnamespace = n.oid WHERE n.nspname = $1 + AND NOT EXISTS ( + SELECT 1 FROM pg_catalog.pg_depend dep + WHERE dep.classid = 'pg_catalog.pg_class'::regclass + AND dep.objid = c.oid + AND dep.objsubid = 0 + AND dep.refclassid = 'pg_catalog.pg_extension'::regclass + AND dep.refobjsubid = 0 + AND dep.deptype = 'e' + ) AND c.relkind IN ('r', 'v', 'm', 'S') AND c.relacl IS NOT NULL + -- SERIAL/identity sequences inherit the owning extension table's boundary. + AND NOT EXISTS ( + SELECT 1 FROM pg_catalog.pg_depend owned + JOIN pg_catalog.pg_depend ext ON ext.objid = owned.refobjid + AND ext.classid = 'pg_catalog.pg_class'::regclass + AND ext.objsubid = 0 + AND ext.refclassid = 'pg_catalog.pg_extension'::regclass + AND ext.refobjsubid = 0 + AND ext.deptype = 'e' + WHERE c.relkind = 'S' + AND owned.classid = 'pg_catalog.pg_class'::regclass + AND owned.objid = c.oid + AND owned.objsubid = 0 + AND owned.refclassid = 'pg_catalog.pg_class'::regclass + AND owned.refobjsubid > 0 + AND owned.deptype IN ('a', 'i') + ) UNION ALL @@ -2323,6 +2608,15 @@ WITH acl_data AS ( FROM pg_proc p JOIN pg_namespace n ON p.pronamespace = n.oid WHERE n.nspname = $1 + AND NOT EXISTS ( + SELECT 1 FROM pg_catalog.pg_depend dep + WHERE dep.classid = 'pg_catalog.pg_proc'::regclass + AND dep.objid = p.oid + AND dep.objsubid = 0 + AND dep.refclassid = 'pg_catalog.pg_extension'::regclass + AND dep.refobjsubid = 0 + AND dep.deptype = 'e' + ) AND p.prokind = 'f' AND p.proacl IS NOT NULL @@ -2338,6 +2632,15 @@ WITH acl_data AS ( FROM pg_proc p JOIN pg_namespace n ON p.pronamespace = n.oid WHERE n.nspname = $1 + AND NOT EXISTS ( + SELECT 1 FROM pg_catalog.pg_depend dep + WHERE dep.classid = 'pg_catalog.pg_proc'::regclass + AND dep.objid = p.oid + AND dep.objsubid = 0 + AND dep.refclassid = 'pg_catalog.pg_extension'::regclass + AND dep.refobjsubid = 0 + AND dep.deptype = 'e' + ) AND p.prokind = 'p' AND p.proacl IS NOT NULL @@ -2353,7 +2656,25 @@ WITH acl_data AS ( FROM pg_type t JOIN pg_namespace n ON t.typnamespace = n.oid WHERE n.nspname = $1 + AND NOT EXISTS ( + SELECT 1 FROM pg_catalog.pg_depend dep + WHERE dep.classid = 'pg_catalog.pg_type'::regclass + AND dep.objid = t.oid + AND dep.objsubid = 0 + AND dep.refclassid = 'pg_catalog.pg_extension'::regclass + AND dep.refobjsubid = 0 + AND dep.deptype = 'e' + ) AND t.typtype IN ('e', 'c', 'd') + AND NOT EXISTS ( + SELECT 1 FROM pg_catalog.pg_depend dep + WHERE dep.classid = 'pg_catalog.pg_class'::regclass + AND dep.objid = t.typrelid + AND dep.objsubid = 0 + AND dep.refclassid = 'pg_catalog.pg_extension'::regclass + AND dep.refobjsubid = 0 + AND dep.deptype = 'e' + ) AND t.typacl IS NOT NULL ) SELECT @@ -2424,14 +2745,21 @@ FROM information_schema.routines r LEFT JOIN pg_proc p ON p.proname = r.routine_name AND p.pronamespace = (SELECT oid FROM pg_namespace WHERE nspname = r.routine_schema) AND p.oid = (regexp_match(r.specific_name, '_(\d+)$'))[1]::oid -LEFT JOIN pg_depend d ON d.objid = p.oid AND d.deptype = 'e' LEFT JOIN pg_description desc_proc ON desc_proc.objoid = p.oid AND desc_proc.classoid = 'pg_proc'::regclass WHERE r.routine_schema NOT IN ('information_schema', 'pg_catalog', 'pg_toast') AND r.routine_schema NOT LIKE 'pg_temp_%' AND r.routine_schema NOT LIKE 'pg_toast_temp_%' AND r.routine_type = 'PROCEDURE' - AND d.objid IS NULL -- Exclude procedures that are extension members + AND NOT EXISTS ( + SELECT 1 FROM pg_catalog.pg_depend dep + WHERE dep.classid = 'pg_catalog.pg_proc'::regclass + AND dep.objid = p.oid + AND dep.objsubid = 0 + AND dep.refclassid = 'pg_catalog.pg_extension'::regclass + AND dep.refobjsubid = 0 + AND dep.deptype = 'e' + ) ORDER BY r.routine_schema, r.routine_name ` @@ -2498,11 +2826,18 @@ FROM information_schema.routines r LEFT JOIN pg_proc p ON p.proname = r.routine_name AND p.pronamespace = (SELECT oid FROM pg_namespace WHERE nspname = r.routine_schema) AND p.oid = (regexp_match(r.specific_name, '_(\d+)$'))[1]::oid -LEFT JOIN pg_depend d ON d.objid = p.oid AND d.deptype = 'e' LEFT JOIN pg_description desc_proc ON desc_proc.objoid = p.oid AND desc_proc.classoid = 'pg_proc'::regclass WHERE r.routine_schema = $1 AND r.routine_type = 'PROCEDURE' - AND d.objid IS NULL -- Exclude procedures that are extension members + AND NOT EXISTS ( + SELECT 1 FROM pg_catalog.pg_depend dep + WHERE dep.classid = 'pg_catalog.pg_proc'::regclass + AND dep.objid = p.oid + AND dep.objsubid = 0 + AND dep.refclassid = 'pg_catalog.pg_extension'::regclass + AND dep.refobjsubid = 0 + AND dep.deptype = 'e' + ) ORDER BY r.routine_schema, r.routine_name ` @@ -2587,6 +2922,15 @@ WHERE n.nspname NOT IN ('information_schema', 'pg_catalog', 'pg_toast') AND n.nspname NOT LIKE 'pg_temp_%' AND n.nspname NOT LIKE 'pg_toast_temp_%' + AND NOT EXISTS ( + SELECT 1 FROM pg_catalog.pg_depend dep + WHERE dep.classid = 'pg_catalog.pg_class'::regclass + AND dep.objid = c.oid + AND dep.objsubid = 0 + AND dep.refclassid = 'pg_catalog.pg_extension'::regclass + AND dep.refobjsubid = 0 + AND dep.deptype = 'e' + ) ORDER BY n.nspname, c.relname, pol.polname ` @@ -2681,6 +3025,15 @@ LEFT JOIN LATERAL ( ) e ON true WHERE n.nspname = $1 + AND NOT EXISTS ( + SELECT 1 FROM pg_catalog.pg_depend dep + WHERE dep.classid = 'pg_catalog.pg_class'::regclass + AND dep.objid = c.oid + AND dep.objsubid = 0 + AND dep.refclassid = 'pg_catalog.pg_extension'::regclass + AND dep.refobjsubid = 0 + AND dep.deptype = 'e' + ) ORDER BY n.nspname, c.relname, pol.polname ` @@ -2744,6 +3097,15 @@ WHERE AND n.nspname NOT LIKE 'pg_toast_temp_%' AND c.relkind IN ('r', 'p') -- ordinary and partitioned tables (issue #471) AND c.relrowsecurity = true + AND NOT EXISTS ( + SELECT 1 FROM pg_catalog.pg_depend dep + WHERE dep.classid = 'pg_catalog.pg_class'::regclass + AND dep.objid = c.oid + AND dep.objsubid = 0 + AND dep.refclassid = 'pg_catalog.pg_extension'::regclass + AND dep.refobjsubid = 0 + AND dep.deptype = 'e' + ) ORDER BY n.nspname, c.relname ` @@ -2795,6 +3157,15 @@ WHERE n.nspname = $1 AND c.relkind IN ('r', 'p') -- ordinary and partitioned tables (issue #471) AND c.relrowsecurity = true + AND NOT EXISTS ( + SELECT 1 FROM pg_catalog.pg_depend dep + WHERE dep.classid = 'pg_catalog.pg_class'::regclass + AND dep.objid = c.oid + AND dep.objsubid = 0 + AND dep.refclassid = 'pg_catalog.pg_extension'::regclass + AND dep.refobjsubid = 0 + AND dep.deptype = 'e' + ) ORDER BY n.nspname, c.relname ` @@ -2844,6 +3215,15 @@ WITH objects_with_acl AS ( FROM pg_proc p JOIN pg_namespace n ON p.pronamespace = n.oid WHERE n.nspname = $1 + AND NOT EXISTS ( + SELECT 1 FROM pg_catalog.pg_depend dep + WHERE dep.classid = 'pg_catalog.pg_proc'::regclass + AND dep.objid = p.oid + AND dep.objsubid = 0 + AND dep.refclassid = 'pg_catalog.pg_extension'::regclass + AND dep.refobjsubid = 0 + AND dep.deptype = 'e' + ) AND p.prokind = 'f' UNION ALL @@ -2856,6 +3236,15 @@ WITH objects_with_acl AS ( FROM pg_proc p JOIN pg_namespace n ON p.pronamespace = n.oid WHERE n.nspname = $1 + AND NOT EXISTS ( + SELECT 1 FROM pg_catalog.pg_depend dep + WHERE dep.classid = 'pg_catalog.pg_proc'::regclass + AND dep.objid = p.oid + AND dep.objsubid = 0 + AND dep.refclassid = 'pg_catalog.pg_extension'::regclass + AND dep.refobjsubid = 0 + AND dep.deptype = 'e' + ) AND p.prokind = 'p' UNION ALL @@ -2868,7 +3257,25 @@ WITH objects_with_acl AS ( FROM pg_type t JOIN pg_namespace n ON t.typnamespace = n.oid WHERE n.nspname = $1 + AND NOT EXISTS ( + SELECT 1 FROM pg_catalog.pg_depend dep + WHERE dep.classid = 'pg_catalog.pg_type'::regclass + AND dep.objid = t.oid + AND dep.objsubid = 0 + AND dep.refclassid = 'pg_catalog.pg_extension'::regclass + AND dep.refobjsubid = 0 + AND dep.deptype = 'e' + ) AND t.typtype IN ('e', 'c', 'd') + AND NOT EXISTS ( + SELECT 1 FROM pg_catalog.pg_depend dep + WHERE dep.classid = 'pg_catalog.pg_class'::regclass + AND dep.objid = t.typrelid + AND dep.objsubid = 0 + AND dep.refclassid = 'pg_catalog.pg_extension'::regclass + AND dep.refobjsubid = 0 + AND dep.deptype = 'e' + ) ), public_grants AS ( SELECT @@ -2937,6 +3344,7 @@ func (q *Queries) GetSchema(ctx context.Context, schemaName sql.NullString) (int } const getSchemas = `-- name: GetSchemas :many + SELECT schema_name FROM information_schema.schemata @@ -2947,6 +3355,11 @@ WHERE ORDER BY schema_name ` +// Extension members are identified by their full pg_depend catalog identity. +// Only deptype 'e' denotes membership; ordinary dependencies (including 'x', +// AUTO_EXTENSION) must stay managed. Definitions and member ACLs are excluded +// consistently for dump and both sides of planning. Schemas themselves remain +// inspectable because an extension schema can also contain application objects. // GetSchemas retrieves all user-defined schemas func (q *Queries) GetSchemas(ctx context.Context) ([]interface{}, error) { rows, err := q.db.QueryContext(ctx, getSchemas) @@ -2981,11 +3394,39 @@ SELECT maximum_value, increment, cycle_option -FROM information_schema.sequences +FROM information_schema.sequences s +JOIN pg_namespace n ON n.nspname = s.sequence_schema +JOIN pg_class c ON c.relnamespace = n.oid AND c.relname = s.sequence_name WHERE sequence_schema NOT IN ('information_schema', 'pg_catalog', 'pg_toast') AND sequence_schema NOT LIKE 'pg_temp_%' AND sequence_schema NOT LIKE 'pg_toast_temp_%' + AND NOT EXISTS ( + SELECT 1 FROM pg_catalog.pg_depend dep + WHERE dep.classid = 'pg_catalog.pg_class'::regclass + AND dep.objid = c.oid + AND dep.objsubid = 0 + AND dep.refclassid = 'pg_catalog.pg_extension'::regclass + AND dep.refobjsubid = 0 + AND dep.deptype = 'e' + ) + -- SERIAL/identity sequences inherit the owning extension table's boundary. + AND NOT EXISTS ( + SELECT 1 FROM pg_catalog.pg_depend owned + JOIN pg_catalog.pg_depend ext ON ext.objid = owned.refobjid + AND ext.classid = 'pg_catalog.pg_class'::regclass + AND ext.objsubid = 0 + AND ext.refclassid = 'pg_catalog.pg_extension'::regclass + AND ext.refobjsubid = 0 + AND ext.deptype = 'e' + WHERE c.relkind = 'S' + AND owned.classid = 'pg_catalog.pg_class'::regclass + AND owned.objid = c.oid + AND owned.objsubid = 0 + AND owned.refclassid = 'pg_catalog.pg_class'::regclass + AND owned.refobjsubid > 0 + AND owned.deptype IN ('a', 'i') + ) ORDER BY sequence_schema, sequence_name ` @@ -3054,6 +3495,32 @@ 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 WHERE s.schemaname = $1 + AND NOT EXISTS ( + SELECT 1 FROM pg_catalog.pg_depend dep + WHERE dep.classid = 'pg_catalog.pg_class'::regclass + AND dep.objid = c.oid + AND dep.objsubid = 0 + AND dep.refclassid = 'pg_catalog.pg_extension'::regclass + AND dep.refobjsubid = 0 + AND dep.deptype = 'e' + ) + -- SERIAL/identity sequences inherit the owning extension table's boundary. + AND NOT EXISTS ( + SELECT 1 FROM pg_catalog.pg_depend owned + JOIN pg_catalog.pg_depend ext ON ext.objid = owned.refobjid + AND ext.classid = 'pg_catalog.pg_class'::regclass + AND ext.objsubid = 0 + AND ext.refclassid = 'pg_catalog.pg_extension'::regclass + AND ext.refobjsubid = 0 + AND ext.deptype = 'e' + WHERE c.relkind = 'S' + AND owned.classid = 'pg_catalog.pg_class'::regclass + AND owned.objid = c.oid + AND owned.objsubid = 0 + AND owned.refclassid = 'pg_catalog.pg_class'::regclass + AND owned.refobjsubid > 0 + AND owned.deptype IN ('a', 'i') + ) ORDER BY s.schemaname, s.sequencename ` @@ -3128,6 +3595,15 @@ WHERE AND t.table_schema NOT LIKE 'pg_temp_%' AND t.table_schema NOT LIKE 'pg_toast_temp_%' AND t.table_type IN ('BASE TABLE', 'VIEW') + AND NOT EXISTS ( + SELECT 1 FROM pg_catalog.pg_depend dep + WHERE dep.classid = 'pg_catalog.pg_class'::regclass + AND dep.objid = c.oid + AND dep.objsubid = 0 + AND dep.refclassid = 'pg_catalog.pg_extension'::regclass + AND dep.refobjsubid = 0 + AND dep.deptype = 'e' + ) ORDER BY t.table_schema, t.table_name ` @@ -3183,6 +3659,15 @@ LEFT JOIN pg_description d ON d.objoid = c.oid AND d.classoid = 'pg_class'::regc WHERE t.table_schema = $1 AND t.table_type IN ('BASE TABLE', 'VIEW') + AND NOT EXISTS ( + SELECT 1 FROM pg_catalog.pg_depend dep + WHERE dep.classid = 'pg_catalog.pg_class'::regclass + AND dep.objid = c.oid + AND dep.objsubid = 0 + AND dep.refclassid = 'pg_catalog.pg_extension'::regclass + AND dep.refobjsubid = 0 + AND dep.deptype = 'e' + ) ORDER BY t.table_name ` @@ -3234,11 +3719,32 @@ SELECT action_statement, action_condition, action_orientation -FROM information_schema.triggers +FROM information_schema.triggers it +JOIN pg_namespace n ON n.nspname = it.trigger_schema +JOIN pg_class c ON c.relnamespace = n.oid AND c.relname = it.event_object_table +JOIN pg_trigger t ON t.tgrelid = c.oid AND t.tgname = it.trigger_name WHERE trigger_schema NOT IN ('information_schema', 'pg_catalog', 'pg_toast') AND trigger_schema NOT LIKE 'pg_temp_%' AND trigger_schema NOT LIKE 'pg_toast_temp_%' + AND NOT EXISTS ( + SELECT 1 FROM pg_catalog.pg_depend dep + WHERE dep.classid = 'pg_catalog.pg_class'::regclass + AND dep.objid = c.oid + AND dep.objsubid = 0 + AND dep.refclassid = 'pg_catalog.pg_extension'::regclass + AND dep.refobjsubid = 0 + AND dep.deptype = 'e' + ) + AND NOT EXISTS ( + SELECT 1 FROM pg_catalog.pg_depend dep + WHERE dep.classid = 'pg_catalog.pg_trigger'::regclass + AND dep.objid = t.oid + AND dep.objsubid = 0 + AND dep.refclassid = 'pg_catalog.pg_extension'::regclass + AND dep.refobjsubid = 0 + AND dep.deptype = 'e' + ) ORDER BY trigger_schema, event_object_table, trigger_name ` @@ -3322,6 +3828,24 @@ WHERE n.nspname = $1 -- defined on a partitioned parent; pg_dump emits only the top-level trigger -- on the parent (tgparentid = 0). AND t.tgparentid = 0 + AND NOT EXISTS ( + SELECT 1 FROM pg_catalog.pg_depend dep + WHERE dep.classid = 'pg_catalog.pg_class'::regclass + AND dep.objid = c.oid + AND dep.objsubid = 0 + AND dep.refclassid = 'pg_catalog.pg_extension'::regclass + AND dep.refobjsubid = 0 + AND dep.deptype = 'e' + ) + AND NOT EXISTS ( + SELECT 1 FROM pg_catalog.pg_depend dep + WHERE dep.classid = 'pg_catalog.pg_trigger'::regclass + AND dep.objid = t.oid + AND dep.objsubid = 0 + AND dep.refclassid = 'pg_catalog.pg_extension'::regclass + AND dep.refobjsubid = 0 + AND dep.deptype = 'e' + ) ORDER BY n.nspname, c.relname, t.tgname ` @@ -3412,6 +3936,15 @@ WHERE t.typtype IN ('e', 'c') -- ENUM and composite types only AND n.nspname NOT LIKE 'pg_temp_%' AND n.nspname NOT LIKE 'pg_toast_temp_%' AND (t.typtype = 'e' OR (t.typtype = 'c' AND c.relkind = 'c')) -- For composite types, only include true composite types (not table types) + AND NOT EXISTS ( + SELECT 1 FROM pg_catalog.pg_depend dep + WHERE dep.classid = 'pg_catalog.pg_type'::regclass + AND dep.objid = t.oid + AND dep.objsubid = 0 + AND dep.refclassid = 'pg_catalog.pg_extension'::regclass + AND dep.refobjsubid = 0 + AND dep.deptype = 'e' + ) ORDER BY n.nspname, t.typname ` @@ -3468,6 +4001,15 @@ LEFT JOIN pg_class c ON t.typrelid = c.oid WHERE t.typtype IN ('e', 'c') -- ENUM and composite types only AND n.nspname = $1 AND (t.typtype = 'e' OR (t.typtype = 'c' AND c.relkind = 'c')) -- For composite types, only include true composite types (not table types) + AND NOT EXISTS ( + SELECT 1 FROM pg_catalog.pg_depend dep + WHERE dep.classid = 'pg_catalog.pg_type'::regclass + AND dep.objid = t.oid + AND dep.objsubid = 0 + AND dep.refclassid = 'pg_catalog.pg_extension'::regclass + AND dep.refobjsubid = 0 + AND dep.deptype = 'e' + ) ORDER BY n.nspname, t.typname ` @@ -3575,6 +4117,15 @@ WHERE AND n.nspname NOT IN ('information_schema', 'pg_catalog', 'pg_toast') AND n.nspname NOT LIKE 'pg_temp_%' AND n.nspname NOT LIKE 'pg_toast_temp_%' + AND NOT EXISTS ( + SELECT 1 FROM pg_catalog.pg_depend dep + WHERE dep.classid = 'pg_catalog.pg_class'::regclass + AND dep.objid = c.oid + AND dep.objsubid = 0 + AND dep.refclassid = 'pg_catalog.pg_extension'::regclass + AND dep.refobjsubid = 0 + AND dep.deptype = 'e' + ) ORDER BY n.nspname, c.relname ` @@ -3632,6 +4183,15 @@ WITH view_definitions AS ( WHERE c.relkind IN ('v', 'm') -- views and materialized views AND n.nspname = $1 + AND NOT EXISTS ( + SELECT 1 FROM pg_catalog.pg_depend dep + WHERE dep.classid = 'pg_catalog.pg_class'::regclass + AND dep.objid = c.oid + AND dep.objsubid = 0 + AND dep.refclassid = 'pg_catalog.pg_extension'::regclass + AND dep.refobjsubid = 0 + AND dep.deptype = 'e' + ) ) SELECT vd.table_schema,