@akal/pg-conformance 0.0.0-stage → 0.0.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,202 @@
1
+ [
2
+ {
3
+ "id": "obj_create_table_pk",
4
+ "source_file": "objects",
5
+ "category": "create_table",
6
+ "table": "",
7
+ "before_sql": "",
8
+ "alter_sql": "CREATE TABLE o_books (id bigint PRIMARY KEY, title text NOT NULL);",
9
+ "description": "create a table with a primary key from nothing",
10
+ "skip_reason": null
11
+ },
12
+ {
13
+ "id": "obj_create_serial",
14
+ "source_file": "objects",
15
+ "category": "create_table",
16
+ "table": "",
17
+ "before_sql": "",
18
+ "alter_sql": "CREATE TABLE o_serial (id serial PRIMARY KEY, name text);",
19
+ "description": "serial column must round-trip with its owned sequence",
20
+ "skip_reason": null
21
+ },
22
+ {
23
+ "id": "obj_create_identity",
24
+ "source_file": "objects",
25
+ "category": "create_table",
26
+ "table": "",
27
+ "before_sql": "",
28
+ "alter_sql": "CREATE TABLE o_ident (id bigint GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, n text);",
29
+ "description": "identity column from nothing",
30
+ "skip_reason": null
31
+ },
32
+ {
33
+ "id": "obj_create_identity_increment",
34
+ "source_file": "objects",
35
+ "category": "create_table",
36
+ "table": "",
37
+ "before_sql": "",
38
+ "alter_sql": "CREATE TABLE o_ident2 (id bigint GENERATED ALWAYS AS IDENTITY (INCREMENT 10 START 100), n text);",
39
+ "description": "identity carrying non-default sequence options",
40
+ "skip_reason": null
41
+ },
42
+ {
43
+ "id": "obj_create_enum_column",
44
+ "source_file": "objects",
45
+ "category": "create_type",
46
+ "table": "",
47
+ "before_sql": "",
48
+ "alter_sql": "CREATE TYPE o_mood AS ENUM ('ok','bad'); CREATE TABLE o_t (id int PRIMARY KEY, m o_mood);",
49
+ "description": "enum type plus a column using it",
50
+ "skip_reason": null
51
+ },
52
+ {
53
+ "id": "obj_create_composite",
54
+ "source_file": "objects",
55
+ "category": "create_type",
56
+ "table": "",
57
+ "before_sql": "",
58
+ "alter_sql": "CREATE TYPE o_addr AS (street text, city text);",
59
+ "description": "composite type",
60
+ "skip_reason": null
61
+ },
62
+ {
63
+ "id": "obj_create_domain",
64
+ "source_file": "objects",
65
+ "category": "create_type",
66
+ "table": "",
67
+ "before_sql": "",
68
+ "alter_sql": "CREATE DOMAIN o_pos AS integer CHECK (VALUE > 0);",
69
+ "description": "domain with a constraint",
70
+ "skip_reason": null
71
+ },
72
+ {
73
+ "id": "obj_partitioned_range",
74
+ "source_file": "objects",
75
+ "category": "partition",
76
+ "table": "",
77
+ "before_sql": "",
78
+ "alter_sql": "CREATE TABLE o_sales (id bigint, d date NOT NULL, PRIMARY KEY (d,id)) PARTITION BY RANGE (d); CREATE TABLE o_sales_26 PARTITION OF o_sales FOR VALUES FROM ('2026-01-01') TO ('2027-01-01');",
79
+ "description": "partitioned parent plus an attached partition",
80
+ "skip_reason": null
81
+ },
82
+ {
83
+ "id": "obj_partitioned_list",
84
+ "source_file": "objects",
85
+ "category": "partition",
86
+ "table": "",
87
+ "before_sql": "",
88
+ "alter_sql": "CREATE TABLE o_reg (id bigint, r text NOT NULL, PRIMARY KEY (r,id)) PARTITION BY LIST (r); CREATE TABLE o_reg_emea PARTITION OF o_reg FOR VALUES IN ('emea');",
89
+ "description": "list partitioning",
90
+ "skip_reason": null
91
+ },
92
+ {
93
+ "id": "obj_partition_index",
94
+ "source_file": "objects",
95
+ "category": "partition",
96
+ "table": "",
97
+ "before_sql": "",
98
+ "alter_sql": "CREATE TABLE o_pidx (id bigint, d date NOT NULL, PRIMARY KEY (d,id)) PARTITION BY RANGE (d); CREATE TABLE o_pidx_26 PARTITION OF o_pidx FOR VALUES FROM ('2026-01-01') TO ('2027-01-01'); CREATE INDEX o_pidx_d ON o_pidx (d);",
99
+ "description": "index on a partitioned parent must reach its partitions",
100
+ "skip_reason": null
101
+ },
102
+ {
103
+ "id": "obj_create_view",
104
+ "source_file": "objects",
105
+ "category": "create_view",
106
+ "table": "",
107
+ "before_sql": "CREATE TABLE o_vt (id int PRIMARY KEY, name text);",
108
+ "alter_sql": "CREATE VIEW o_v AS SELECT id, name FROM o_vt;",
109
+ "description": "view over an existing table",
110
+ "skip_reason": null
111
+ },
112
+ {
113
+ "id": "obj_create_view_expr",
114
+ "source_file": "objects",
115
+ "category": "create_view",
116
+ "table": "",
117
+ "before_sql": "CREATE TABLE o_vt2 (id int PRIMARY KEY, a int, b int);",
118
+ "alter_sql": "CREATE VIEW o_v2 AS SELECT id, a + b AS total FROM o_vt2 WHERE a > 0;",
119
+ "description": "view with an expression and predicate",
120
+ "skip_reason": null
121
+ },
122
+ {
123
+ "id": "obj_create_function",
124
+ "source_file": "objects",
125
+ "category": "create_routine",
126
+ "table": "",
127
+ "before_sql": "",
128
+ "alter_sql": "CREATE FUNCTION o_add(a int, b int) RETURNS int LANGUAGE sql IMMUTABLE AS $$ SELECT a + b $$;",
129
+ "description": "plain function",
130
+ "skip_reason": null
131
+ },
132
+ {
133
+ "id": "obj_create_trigger",
134
+ "source_file": "objects",
135
+ "category": "create_trigger",
136
+ "table": "",
137
+ "before_sql": "CREATE TABLE o_tt (id int PRIMARY KEY, v text);",
138
+ "alter_sql": "CREATE FUNCTION o_touch() RETURNS trigger LANGUAGE plpgsql AS $$ BEGIN RETURN NEW; END $$; CREATE TRIGGER o_tt_bu BEFORE UPDATE ON o_tt FOR EACH ROW EXECUTE FUNCTION o_touch();",
139
+ "description": "trigger and the function it calls, created together",
140
+ "skip_reason": null
141
+ },
142
+ {
143
+ "id": "obj_trigger_on_partitioned",
144
+ "source_file": "objects",
145
+ "category": "create_trigger",
146
+ "table": "",
147
+ "before_sql": "",
148
+ "alter_sql": "CREATE TABLE o_pt (id bigint, d date NOT NULL, PRIMARY KEY (d,id)) PARTITION BY RANGE (d); CREATE TABLE o_pt_26 PARTITION OF o_pt FOR VALUES FROM ('2026-01-01') TO ('2027-01-01'); CREATE FUNCTION o_pt_f() RETURNS trigger LANGUAGE plpgsql AS $$ BEGIN RETURN NEW; END $$; CREATE TRIGGER o_pt_ai AFTER INSERT ON o_pt FOR EACH ROW EXECUTE FUNCTION o_pt_f();",
149
+ "description": "trigger on a partitioned parent propagates; the clone is not re-declared",
150
+ "skip_reason": null
151
+ },
152
+ {
153
+ "id": "obj_create_policy",
154
+ "source_file": "objects",
155
+ "category": "create_policy",
156
+ "table": "",
157
+ "before_sql": "CREATE TABLE o_sec (id int PRIMARY KEY, owner text);",
158
+ "alter_sql": "ALTER TABLE o_sec ENABLE ROW LEVEL SECURITY; CREATE POLICY o_sec_read ON o_sec FOR SELECT USING (true);",
159
+ "description": "row level security plus a policy",
160
+ "skip_reason": null
161
+ },
162
+ {
163
+ "id": "obj_create_sequence",
164
+ "source_file": "objects",
165
+ "category": "create_sequence",
166
+ "table": "",
167
+ "before_sql": "",
168
+ "alter_sql": "CREATE SEQUENCE o_seq START 100 INCREMENT 5 MINVALUE 100 MAXVALUE 100000 CYCLE;",
169
+ "description": "standalone sequence with non-default options",
170
+ "skip_reason": null
171
+ },
172
+ {
173
+ "id": "obj_create_matview",
174
+ "source_file": "objects",
175
+ "category": "create_view",
176
+ "table": "",
177
+ "before_sql": "CREATE TABLE o_mt (id int PRIMARY KEY, n int);",
178
+ "alter_sql": "CREATE MATERIALIZED VIEW o_mv AS SELECT id, n FROM o_mt WHERE n > 0;",
179
+ "description": "materialized view",
180
+ "skip_reason": null
181
+ },
182
+ {
183
+ "id": "obj_fk_between_tables",
184
+ "source_file": "objects",
185
+ "category": "create_table",
186
+ "table": "",
187
+ "before_sql": "",
188
+ "alter_sql": "CREATE TABLE o_parent (id bigint PRIMARY KEY); CREATE TABLE o_child (id bigint PRIMARY KEY, p bigint NOT NULL REFERENCES o_parent(id) ON DELETE CASCADE);",
189
+ "description": "two tables plus the foreign key between them, ordered correctly",
190
+ "skip_reason": null
191
+ },
192
+ {
193
+ "id": "obj_generated_column",
194
+ "source_file": "objects",
195
+ "category": "create_table",
196
+ "table": "",
197
+ "before_sql": "",
198
+ "alter_sql": "CREATE TABLE o_gen (id int PRIMARY KEY, a int, b int GENERATED ALWAYS AS (a * 2) STORED);",
199
+ "description": "generated stored column",
200
+ "skip_reason": null
201
+ }
202
+ ]
@@ -0,0 +1,201 @@
1
+ [
2
+ {
3
+ "id": "a_table_is_created_before_the_table_whose_foreign_key_points_at_it",
4
+ "description": "a table is created before the table whose foreign key points at it",
5
+ "statements": [
6
+ "CREATE TABLE user_profile (id int REFERENCES \"user\"(id), bio text);",
7
+ "CREATE TABLE \"user\" (id int PRIMARY KEY);"
8
+ ],
9
+ "requires": [
10
+ [
11
+ "CREATE TABLE \"user\"",
12
+ "CREATE TABLE user_profile"
13
+ ]
14
+ ]
15
+ },
16
+ {
17
+ "id": "quoted_names_that_collide_with_keywords_are_still_ordered",
18
+ "description": "quoted names that collide with keywords are still ordered",
19
+ "statements": [
20
+ "CREATE TABLE \"order\" (id int REFERENCES \"table\"(id));",
21
+ "CREATE TABLE \"table\" (id int PRIMARY KEY);"
22
+ ],
23
+ "requires": [
24
+ [
25
+ "CREATE TABLE \"table\"",
26
+ "CREATE TABLE \"order\""
27
+ ]
28
+ ]
29
+ },
30
+ {
31
+ "id": "a_domain_is_created_before_the_column_that_uses_it",
32
+ "description": "a domain is created before the column that uses it",
33
+ "statements": [
34
+ "CREATE TABLE d_t (id int, v money_amt);",
35
+ "CREATE DOMAIN money_amt AS numeric(10,2) CHECK (VALUE >= 0);"
36
+ ],
37
+ "requires": [
38
+ [
39
+ "CREATE DOMAIN money_amt",
40
+ "CREATE TABLE d_t"
41
+ ]
42
+ ]
43
+ },
44
+ {
45
+ "id": "a_function_is_created_before_the_check_constraint_that_calls_it",
46
+ "description": "a function is created before the CHECK constraint that calls it",
47
+ "statements": [
48
+ "CREATE TABLE c_t (id int, code text, CONSTRAINT c_ck CHECK (is_valid(code)));",
49
+ "CREATE FUNCTION is_valid(t text) RETURNS boolean LANGUAGE sql IMMUTABLE AS $$ SELECT length(t) > 2 $$;"
50
+ ],
51
+ "requires": [
52
+ [
53
+ "CREATE FUNCTION is_valid",
54
+ "CREATE TABLE c_t"
55
+ ]
56
+ ]
57
+ },
58
+ {
59
+ "id": "a_function_is_created_before_the_generated_column_that_calls_it",
60
+ "description": "a function is created before the generated column that calls it",
61
+ "statements": [
62
+ "CREATE TABLE g_t (id int, raw text, norm text GENERATED ALWAYS AS (norm_it(raw)) STORED);",
63
+ "CREATE FUNCTION norm_it(t text) RETURNS text LANGUAGE sql IMMUTABLE AS $$ SELECT lower(t) $$;"
64
+ ],
65
+ "requires": [
66
+ [
67
+ "CREATE FUNCTION norm_it",
68
+ "CREATE TABLE g_t"
69
+ ]
70
+ ]
71
+ },
72
+ {
73
+ "id": "a_composite_type_is_created_before_the_function_that_returns_it",
74
+ "description": "a composite type is created before the function that returns it",
75
+ "statements": [
76
+ "CREATE FUNCTION mk_addr() RETURNS addr_t LANGUAGE sql AS $$ SELECT ROW('a','b')::addr_t $$;",
77
+ "CREATE TYPE addr_t AS (street text, city text);"
78
+ ],
79
+ "requires": [
80
+ [
81
+ "CREATE TYPE addr_t",
82
+ "CREATE FUNCTION mk_addr"
83
+ ]
84
+ ]
85
+ },
86
+ {
87
+ "id": "a_view_that_selects_from_a_function_waits_for_the_function_and_its_table",
88
+ "description": "a view that selects from a function waits for the function and its table",
89
+ "statements": [
90
+ "CREATE VIEW v_top AS SELECT * FROM top_ids();",
91
+ "CREATE FUNCTION top_ids() RETURNS TABLE(id int) LANGUAGE sql AS $$ SELECT id FROM base_t $$;",
92
+ "CREATE TABLE base_t (id int);"
93
+ ],
94
+ "requires": [
95
+ [
96
+ "CREATE TABLE base_t",
97
+ "CREATE FUNCTION top_ids"
98
+ ],
99
+ [
100
+ "CREATE FUNCTION top_ids",
101
+ "CREATE VIEW v_top"
102
+ ]
103
+ ]
104
+ },
105
+ {
106
+ "id": "a_chain_of_views_is_created_from_the_base_outwards",
107
+ "description": "a chain of views is created from the base outwards",
108
+ "statements": [
109
+ "CREATE VIEW v3 AS SELECT id FROM v2;",
110
+ "CREATE VIEW v1 AS SELECT id FROM vbase;",
111
+ "CREATE VIEW v2 AS SELECT id FROM v1;",
112
+ "CREATE TABLE vbase (id int);"
113
+ ],
114
+ "requires": [
115
+ [
116
+ "CREATE TABLE vbase",
117
+ "CREATE VIEW v1"
118
+ ],
119
+ [
120
+ "CREATE VIEW v1",
121
+ "CREATE VIEW v2"
122
+ ],
123
+ [
124
+ "CREATE VIEW v2",
125
+ "CREATE VIEW v3"
126
+ ]
127
+ ]
128
+ },
129
+ {
130
+ "id": "a_partition_is_attached_before_the_index_on_its_parent",
131
+ "description": "a partition is attached before the index on its parent",
132
+ "statements": [
133
+ "CREATE INDEX p_i ON ONLY p_tbl (d);",
134
+ "CREATE TABLE p_26 PARTITION OF p_tbl FOR VALUES FROM ('2026-01-01') TO ('2027-01-01');",
135
+ "CREATE TABLE p_tbl (id int, d date) PARTITION BY RANGE (d);"
136
+ ],
137
+ "requires": [
138
+ [
139
+ "CREATE TABLE p_tbl",
140
+ "CREATE TABLE p_26"
141
+ ],
142
+ [
143
+ "CREATE TABLE p_26",
144
+ "CREATE INDEX p_i"
145
+ ]
146
+ ]
147
+ },
148
+ {
149
+ "id": "an_enum_is_created_before_the_column_defaulting_to_one_of_its_labels",
150
+ "description": "an enum is created before the column defaulting to one of its labels",
151
+ "statements": [
152
+ "CREATE TABLE e_t (id int, m mood_x NOT NULL DEFAULT 'ok');",
153
+ "CREATE TYPE mood_x AS ENUM ('sad','ok');"
154
+ ],
155
+ "requires": [
156
+ [
157
+ "CREATE TYPE mood_x",
158
+ "CREATE TABLE e_t"
159
+ ]
160
+ ]
161
+ },
162
+ {
163
+ "id": "a_three_table_foreign_key_chain_is_created_root_first",
164
+ "description": "a three-table foreign key chain is created root first",
165
+ "statements": [
166
+ "CREATE TABLE l3 (id int REFERENCES l2(id));",
167
+ "CREATE TABLE l1 (id int PRIMARY KEY);",
168
+ "CREATE TABLE l2 (id int PRIMARY KEY REFERENCES l1(id));"
169
+ ],
170
+ "requires": [
171
+ [
172
+ "CREATE TABLE l1",
173
+ "CREATE TABLE l2"
174
+ ],
175
+ [
176
+ "CREATE TABLE l2",
177
+ "CREATE TABLE l3"
178
+ ]
179
+ ]
180
+ },
181
+ {
182
+ "id": "a_trigger_waits_for_its_function_and_the_table_it_fires_on",
183
+ "description": "a trigger waits for its function and the table it fires on",
184
+ "statements": [
185
+ "CREATE TRIGGER t_aud AFTER INSERT ON src_t FOR EACH ROW EXECUTE FUNCTION log_it();",
186
+ "CREATE FUNCTION log_it() RETURNS trigger LANGUAGE plpgsql AS $$ BEGIN INSERT INTO audit_t VALUES (1); RETURN NEW; END $$;",
187
+ "CREATE TABLE src_t (id int);",
188
+ "CREATE TABLE audit_t (id int);"
189
+ ],
190
+ "requires": [
191
+ [
192
+ "CREATE TABLE src_t",
193
+ "CREATE TRIGGER t_aud"
194
+ ],
195
+ [
196
+ "CREATE FUNCTION log_it",
197
+ "CREATE TRIGGER t_aud"
198
+ ]
199
+ ]
200
+ }
201
+ ]
@@ -0,0 +1,245 @@
1
+ -- Structural fingerprint of a PostgreSQL schema.
2
+ --
3
+ -- One query that renders everything about a set of schemas which a migration
4
+ -- could get wrong, as sorted text. Two databases are structurally identical if
5
+ -- and only if this returns the same string for both.
6
+ --
7
+ -- Why the catalog and not information_schema
8
+ -- ------------------------------------------
9
+ -- information_schema is a standardised view over the catalog, and the standard
10
+ -- has no concept of most of what matters here. It cannot tell a partitioned
11
+ -- table from an ordinary one, has nowhere to put a partition bound, and does
12
+ -- not expose identity sequence options, generated-column storage, compression
13
+ -- or collation. Every one of those has hidden a real bug.
14
+ --
15
+ -- Why bodies, not names
16
+ -- --------------------
17
+ -- Objects that carry a body are compared by that body. Comparing names alone
18
+ -- reports a view whose predicate was inverted, a function whose implementation
19
+ -- was replaced, and a trigger moved from AFTER INSERT to BEFORE UPDATE as
20
+ -- identical — three schemas differing in the ways most likely to matter.
21
+ --
22
+ -- Where PostgreSQL can render an object canonically (pg_get_viewdef,
23
+ -- pg_get_constraintdef, pg_get_functiondef, pg_get_triggerdef, pg_get_expr)
24
+ -- that rendering is what gets compared, so both sides are produced by the same
25
+ -- server code and formatting can never manufacture a difference.
26
+ --
27
+ -- Definitions are flattened to one line. Entries are joined with newlines, so a
28
+ -- multi-line body would otherwise arrive as several unattributed entries —
29
+ -- losing the object name and making sort order depend on the body's layout.
30
+ --
31
+ -- Usage
32
+ -- -----
33
+ -- Replace the token __SCHEMAS__ with a quoted, comma-separated schema list,
34
+ -- e.g. 'public','app'. The language accessors do this for you and handle
35
+ -- quoting; do not interpolate user input by hand.
36
+ --
37
+ -- Requires PostgreSQL 14 or newer (pg_attribute.attcompression).
38
+
39
+ SELECT string_agg(line, E'\n' ORDER BY line) AS fingerprint FROM (
40
+
41
+ -- Relations. relkind distinguishes a partitioned table from an ordinary one;
42
+ -- relpartbound is the only place a partition's bound exists; relpersistence
43
+ -- carries UNLOGGED; reloptions carries fillfactor and friends.
44
+ SELECT format('rel %s.%s kind=%s persist=%s bound=%s opts=%s rls=%s force_rls=%s',
45
+ n.nspname, c.relname, c.relkind, c.relpersistence,
46
+ COALESCE(btrim(regexp_replace(pg_get_expr(c.relpartbound, c.oid), '\s+', ' ', 'g')), '-'),
47
+ COALESCE(array_to_string(c.reloptions, ','), '-'),
48
+ c.relrowsecurity, c.relforcerowsecurity) AS line
49
+ FROM pg_class c
50
+ JOIN pg_namespace n ON n.oid = c.relnamespace
51
+ WHERE n.nspname IN (__SCHEMAS__)
52
+ AND c.relkind IN ('r','p','v','m','f','S')
53
+
54
+ UNION ALL
55
+
56
+ -- Columns, read from pg_attribute so identity, generated, storage,
57
+ -- compression and collation are visible at all.
58
+ SELECT format('col %s.%s.%s type=%s notnull=%s default=%s ident=%s gen=%s store=%s compress=%s coll=%s',
59
+ n.nspname, c.relname, a.attname,
60
+ format_type(a.atttypid, a.atttypmod),
61
+ a.attnotnull,
62
+ COALESCE(btrim(regexp_replace(pg_get_expr(d.adbin, d.adrelid), '\s+', ' ', 'g')), '-'),
63
+ a.attidentity, a.attgenerated, a.attstorage,
64
+ COALESCE(a.attcompression::text, '-'),
65
+ COALESCE(co.collname, '-'))
66
+ FROM pg_attribute a
67
+ JOIN pg_class c ON c.oid = a.attrelid
68
+ JOIN pg_namespace n ON n.oid = c.relnamespace
69
+ LEFT JOIN pg_attrdef d ON d.adrelid = a.attrelid AND d.adnum = a.attnum
70
+ LEFT JOIN pg_collation co ON co.oid = a.attcollation
71
+ WHERE n.nspname IN (__SCHEMAS__)
72
+ AND a.attnum > 0 AND NOT a.attisdropped
73
+ AND c.relkind IN ('r','p','v','m','f')
74
+
75
+ UNION ALL
76
+
77
+ -- Sequence options. An identity column's sequence is part of the column: a
78
+ -- table rebuilt without INCREMENT and START silently restarts its counter.
79
+ SELECT format('seq %s.%s start=%s inc=%s min=%s max=%s cache=%s cycle=%s',
80
+ n.nspname, c.relname,
81
+ s.seqstart, s.seqincrement, s.seqmin, s.seqmax, s.seqcache, s.seqcycle)
82
+ FROM pg_sequence s
83
+ JOIN pg_class c ON c.oid = s.seqrelid
84
+ JOIN pg_namespace n ON n.oid = c.relnamespace
85
+ WHERE n.nspname IN (__SCHEMAS__)
86
+
87
+ UNION ALL
88
+
89
+ -- Constraints, including whether they are validated and how they defer —
90
+ -- a NOT VALID check and a validated one are not the same constraint.
91
+ SELECT format('con %s.%s.%s %s valid=%s deferrable=%s deferred=%s',
92
+ n.nspname, c.relname, con.conname,
93
+ btrim(regexp_replace(pg_get_constraintdef(con.oid), '\s+', ' ', 'g')),
94
+ con.convalidated, con.condeferrable, con.condeferred)
95
+ FROM pg_constraint con
96
+ JOIN pg_class c ON c.oid = con.conrelid
97
+ JOIN pg_namespace n ON n.oid = c.relnamespace
98
+ WHERE n.nspname IN (__SCHEMAS__)
99
+
100
+ UNION ALL
101
+
102
+ -- Indexes, per relation. An index created ON ONLY a partitioned parent never
103
+ -- reaches its partitions, and only a per-relation comparison shows that.
104
+ SELECT format('idx %s.%s %s', schemaname, indexname,
105
+ btrim(regexp_replace(indexdef, '\s+', ' ', 'g')))
106
+ FROM pg_indexes
107
+ WHERE schemaname IN (__SCHEMAS__)
108
+
109
+ UNION ALL
110
+
111
+ -- Views and materialised views, by body.
112
+ SELECT format('view %s.%s %s opts=%s',
113
+ n.nspname, c.relname,
114
+ btrim(regexp_replace(pg_get_viewdef(c.oid, true), '\s+', ' ', 'g')),
115
+ COALESCE(array_to_string(c.reloptions, ','), '-'))
116
+ FROM pg_class c
117
+ JOIN pg_namespace n ON n.oid = c.relnamespace
118
+ WHERE n.nspname IN (__SCHEMAS__)
119
+ AND c.relkind IN ('v','m')
120
+
121
+ UNION ALL
122
+
123
+ -- Routines, by body. prokind is filtered because pg_get_functiondef raises
124
+ -- an error on aggregate and window functions.
125
+ SELECT format('fn %s.%s(%s) %s',
126
+ n.nspname, p.proname,
127
+ pg_get_function_identity_arguments(p.oid),
128
+ btrim(regexp_replace(pg_get_functiondef(p.oid), '\s+', ' ', 'g')))
129
+ FROM pg_proc p
130
+ JOIN pg_namespace n ON n.oid = p.pronamespace
131
+ WHERE n.nspname IN (__SCHEMAS__)
132
+ AND p.prokind IN ('f','p')
133
+
134
+ UNION ALL
135
+
136
+ -- Triggers, by definition. tgparentid excludes the per-partition clones
137
+ -- PostgreSQL creates automatically, which are not independent objects.
138
+ SELECT format('trg %s.%s.%s %s',
139
+ n.nspname, c.relname, t.tgname,
140
+ btrim(regexp_replace(pg_get_triggerdef(t.oid), '\s+', ' ', 'g')))
141
+ FROM pg_trigger t
142
+ JOIN pg_class c ON c.oid = t.tgrelid
143
+ JOIN pg_namespace n ON n.oid = c.relnamespace
144
+ WHERE n.nspname IN (__SCHEMAS__)
145
+ AND NOT t.tgisinternal
146
+ AND t.tgparentid = 0
147
+
148
+ UNION ALL
149
+
150
+ -- Policies. USING alone is not the policy: the command, the roles and the
151
+ -- WITH CHECK expression all decide who may do what.
152
+ SELECT format('pol %s.%s.%s cmd=%s permissive=%s roles=%s using=%s check=%s',
153
+ schemaname, tablename, policyname, cmd, permissive,
154
+ array_to_string(roles, ','),
155
+ COALESCE(btrim(regexp_replace(qual, '\s+', ' ', 'g')), '-'),
156
+ COALESCE(btrim(regexp_replace(with_check, '\s+', ' ', 'g')), '-'))
157
+ FROM pg_policies
158
+ WHERE schemaname IN (__SCHEMAS__)
159
+
160
+ UNION ALL
161
+
162
+ -- Enums, domains and composite types. Enum label order is significant:
163
+ -- it decides comparison and ORDER BY.
164
+ --
165
+ -- Every kind's content is carried inline, the way enum labels are, because no
166
+ -- other section can reach it. The col section reads relations (relkind
167
+ -- r/p/v/m/f) and so never sees a standalone composite's attributes, which are
168
+ -- on a 'c'; the con section joins conrelid, while a domain's CHECK is keyed by
169
+ -- contypid and has no conrelid at all. Until these were here, a composite of
170
+ -- (street text, city text) and one of (postcode int) fingerprinted the same,
171
+ -- as did domains with CHECK (VALUE > 0) and CHECK (VALUE < -999) — the same
172
+ -- class of silent agreement this file exists to prevent.
173
+ --
174
+ -- Only CHECK constraints are read. PostgreSQL 17 began recording a domain's
175
+ -- NOT NULL as a constraint row of its own, and notnull= already carries it;
176
+ -- taking every row would both state it twice and make one logical domain
177
+ -- fingerprint differently either side of that release.
178
+ SELECT format('typ %s.%s kind=%s labels=%s default=%s notnull=%s base=%s attrs=%s checks=%s',
179
+ n.nspname, t.typname, t.typtype,
180
+ COALESCE((SELECT string_agg(e.enumlabel, ',' ORDER BY e.enumsortorder)
181
+ FROM pg_enum e WHERE e.enumtypid = t.oid), '-'),
182
+ COALESCE(btrim(regexp_replace(pg_get_expr(t.typdefaultbin, 0), '\s+', ' ', 'g')), '-'),
183
+ t.typnotnull,
184
+ COALESCE(CASE WHEN t.typtype = 'd'
185
+ THEN format_type(t.typbasetype, t.typtypmod) END, '-'),
186
+ COALESCE((SELECT string_agg(
187
+ format('%s %s', a.attname,
188
+ format_type(a.atttypid, a.atttypmod)),
189
+ ',' ORDER BY a.attnum)
190
+ FROM pg_attribute a
191
+ WHERE a.attrelid = t.typrelid
192
+ AND a.attnum > 0 AND NOT a.attisdropped), '-'),
193
+ COALESCE((SELECT string_agg(
194
+ format('%s %s', con.conname,
195
+ btrim(regexp_replace(
196
+ pg_get_constraintdef(con.oid), '\s+', ' ', 'g'))),
197
+ ',' ORDER BY con.conname)
198
+ FROM pg_constraint con
199
+ WHERE con.contypid = t.oid
200
+ AND con.contype = 'c'), '-'))
201
+ FROM pg_type t
202
+ JOIN pg_namespace n ON n.oid = t.typnamespace
203
+ WHERE n.nspname IN (__SCHEMAS__)
204
+ AND t.typtype IN ('e','d','c')
205
+ AND NOT EXISTS (
206
+ SELECT 1 FROM pg_class c
207
+ WHERE c.oid = t.typrelid AND c.relkind <> 'c'
208
+ )
209
+
210
+ UNION ALL
211
+
212
+ -- Inheritance, which is not implied by anything above.
213
+ SELECT format('inh %s.%s <- %s.%s',
214
+ pn.nspname, parent.relname, cn.nspname, child.relname)
215
+ FROM pg_inherits i
216
+ JOIN pg_class child ON child.oid = i.inhrelid
217
+ JOIN pg_class parent ON parent.oid = i.inhparent
218
+ JOIN pg_namespace cn ON cn.oid = child.relnamespace
219
+ JOIN pg_namespace pn ON pn.oid = parent.relnamespace
220
+ WHERE cn.nspname IN (__SCHEMAS__)
221
+
222
+ UNION ALL
223
+
224
+ -- Comments, on anything in the schemas: tables and columns, and functions,
225
+ -- types, constraints, triggers, policies and the schemas themselves. Named
226
+ -- as PostgreSQL identifies the object, so a column by its name: keyed by
227
+ -- its position, the same comment on a column added last on one side read
228
+ -- as a difference. An extension's objects are its own.
229
+ SELECT format('cmt %s %s %s', i.type, i.identity,
230
+ btrim(regexp_replace(d.description, '\s+', ' ', 'g')))
231
+ FROM pg_description d
232
+ CROSS JOIN LATERAL pg_identify_object(d.classoid, d.objoid, d.objsubid) i
233
+ WHERE (i.schema IN (__SCHEMAS__)
234
+ -- A trigger or a policy has no schema of its own: its table's.
235
+ OR (SELECT n.nspname FROM pg_class c JOIN pg_namespace n ON n.oid = c.relnamespace
236
+ WHERE c.oid = CASE d.classoid
237
+ WHEN 'pg_trigger'::regclass THEN (SELECT tgrelid FROM pg_trigger WHERE oid = d.objoid)
238
+ WHEN 'pg_policy'::regclass THEN (SELECT polrelid FROM pg_policy WHERE oid = d.objoid)
239
+ END) IN (__SCHEMAS__)
240
+ OR (d.classoid = 'pg_namespace'::regclass
241
+ AND d.objoid IN (SELECT oid FROM pg_namespace WHERE nspname IN (__SCHEMAS__))))
242
+ AND NOT EXISTS (SELECT 1 FROM pg_depend e
243
+ WHERE e.deptype = 'e' AND e.classid = d.classoid AND e.objid = d.objoid)
244
+
245
+ ) entries;