@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.
package/index.d.ts ADDED
@@ -0,0 +1,147 @@
1
+ /** A DDL case that builds objects from an empty schema. */
2
+ export interface ObjectCase {
3
+ id: string
4
+ sql: string
5
+ minPgVersion?: number
6
+ }
7
+
8
+ /** A case exercising a renderer against DDL that is awkward to reproduce. */
9
+ export interface HardCase {
10
+ id: string
11
+ sql: string
12
+ minPgVersion?: number
13
+ }
14
+
15
+ /**
16
+ * A case for a dependency-ordering algorithm.
17
+ *
18
+ * `statements` are given in an order that does not apply. `requires` lists
19
+ * `[before, after]` pairs, matched as substrings, that any correct order must
20
+ * satisfy — a precedence rather than one expected permutation, so tie-breaking
21
+ * can change without invalidating the case.
22
+ */
23
+ export interface OrderingCase {
24
+ id: string
25
+ description: string
26
+ statements: string[]
27
+ requires: Array<[string, string]>
28
+ }
29
+
30
+ /**
31
+ * A schema change any migration tool must make in both directions: from
32
+ * `before` to `after` and back, leaving each identical to a database built
33
+ * directly from it. `preserve` queries must return the same rows before and
34
+ * after migrating — the data a correct migration keeps.
35
+ */
36
+ export interface MigrationCase {
37
+ id: string
38
+ category:
39
+ | 'enum' | 'identity' | 'serial' | 'storage' | 'generated' | 'dependants'
40
+ | 'creation_order' | 'removal_order' | 'foreign_key'
41
+ | 'schemas' | 'routines' | 'partitions' | 'rls' | 'triggers' | 'types' | 'sequences' | 'constraints'
42
+ description: string
43
+ before: string
44
+ after: string
45
+ preserve?: string[]
46
+ minPgVersion?: number
47
+ }
48
+
49
+ /**
50
+ * One schema written two ways: as a developer writes it and as PostgreSQL
51
+ * renders it. A tool comparing schemas must call the pair identical.
52
+ */
53
+ export interface EquivalenceCase {
54
+ id: string
55
+ kind: 'check' | 'index' | 'policy' | 'view' | 'domain' | 'generated'
56
+ description: string
57
+ written: string
58
+ rendered: string
59
+ }
60
+
61
+ /**
62
+ * Rows a data migration must carry over, both ways. `schema` builds the same
63
+ * structure on both sides; `before` holds the target's rows and `after` the
64
+ * source's. Migrating either into the other must leave every `compare` query
65
+ * returning what it returns on a database built from the other directly.
66
+ */
67
+ export interface DataCase {
68
+ id: string
69
+ description: string
70
+ schema: string
71
+ before: string
72
+ after: string
73
+ compare: string[]
74
+ minPgVersion?: number
75
+ }
76
+
77
+ export interface CorpusByName {
78
+ objects: ObjectCase[]
79
+ 'hard-cases': HardCase[]
80
+ ordering: OrderingCase[]
81
+ migrations: MigrationCase[]
82
+ equivalences: EquivalenceCase[]
83
+ data: DataCase[]
84
+ }
85
+
86
+ /** One relation and everything attached to it. */
87
+ export interface StateTable {
88
+ schema: string
89
+ name: string
90
+ kind: 'table' | 'partitioned_table' | 'foreign_table'
91
+ unlogged: boolean
92
+ partition_of: string | null
93
+ partition_bound: string | null
94
+ partition_by: string | null
95
+ inherits: string[] | null
96
+ options: string[] | null
97
+ rls_enabled: boolean
98
+ rls_forced: boolean
99
+ comment: string | null
100
+ columns: StateColumn[] | null
101
+ constraints: Array<{ name: string; type: string; definition: string; validated: boolean; deferrable: boolean; deferred: boolean }> | null
102
+ indexes: Array<{ name: string; definition: string }> | null
103
+ policies: Array<{ name: string; command: string; permissive: boolean; roles: string[] | null; using: string | null; with_check: string | null }> | null
104
+ triggers: Array<{ name: string; definition: string }> | null
105
+ }
106
+
107
+ export interface StateColumn {
108
+ name: string
109
+ position: number
110
+ type: string
111
+ not_null: boolean
112
+ default: string | null
113
+ identity: 'always' | 'by_default' | null
114
+ identity_options: { start: number; increment: number; min: number; max: number; cache: number; cycle: boolean } | null
115
+ generated: 'stored' | null
116
+ storage: 'plain' | 'external' | 'main' | 'extended' | null
117
+ storage_is_default: boolean
118
+ compression: 'lz4' | 'pglz' | null
119
+ /** Null when inherited rather than set explicitly. */
120
+ collation: string | null
121
+ comment: string | null
122
+ }
123
+
124
+ /**
125
+ * A schema as data, rather than as an answer to "are these the same?".
126
+ *
127
+ * Sourced from pg_catalog, so it can express what information_schema cannot:
128
+ * partition bounds, identity sequence options, storage, compression and
129
+ * whether a collation was chosen or inherited.
130
+ */
131
+ export interface SchemaState {
132
+ meta: { server_version_num: number; schemas: string[] }
133
+ tables: StateTable[]
134
+ views: Array<{ schema: string; name: string; materialized: boolean; definition: string; options: string[] | null; comment: string | null; columns: Array<{ name: string; type: string }> | null }>
135
+ sequences: Array<{ schema: string; name: string; type: string; start: number; increment: number; min: number; max: number; cache: number; cycle: boolean; owned_by: string | null }>
136
+ routines: Array<{ schema: string; name: string; kind: 'function' | 'procedure'; arguments: string; definition: string; comment: string | null }>
137
+ types: Array<{ schema: string; name: string; kind: 'enum' | 'domain' | 'composite'; labels: string[] | null; base_type: string | null; not_null: boolean; default: string | null; constraints: string[] | null; attributes: Array<{ name: string; type: string }> | null }>
138
+ extensions: Array<{ name: string; version: string }>
139
+ }
140
+
141
+ export declare const stateSqlPath: string
142
+ export declare function stateSql(schemas?: string[]): string
143
+
144
+ export declare const fingerprintSqlPath: string
145
+ export declare function fingerprintSql(schemas?: string[]): string
146
+ export declare const corpora: Array<keyof CorpusByName>
147
+ export declare function loadCorpus<K extends keyof CorpusByName>(name: K): CorpusByName[K]
package/index.js ADDED
@@ -0,0 +1,87 @@
1
+ /**
2
+ * @akal/pg-conformance
3
+ *
4
+ * Data only: a schema fingerprint query and a corpus of DDL cases. There is no
5
+ * runner here on purpose — property-based and example-based harnesses
6
+ * legitimately differ, and each consumer already has one. What must not differ
7
+ * is the definition of "are these two schemas the same", which is why that
8
+ * lives in exactly one place.
9
+ */
10
+ import { readFileSync } from 'node:fs'
11
+ import { fileURLToPath } from 'node:url'
12
+ import { dirname, join } from 'node:path'
13
+
14
+ const HERE = dirname(fileURLToPath(import.meta.url))
15
+
16
+ /** Absolute path to the fingerprint query, for consumers that shell out to psql. */
17
+ export const fingerprintSqlPath = join(HERE, 'fingerprint.sql')
18
+
19
+ /**
20
+ * The fingerprint query, with the schema list substituted in.
21
+ *
22
+ * Schema names are quoted here rather than by the caller: the query embeds them
23
+ * as SQL literals, so this is the one place an injection could be introduced.
24
+ * A name containing a single quote is rejected rather than escaped, because no
25
+ * legitimate schema in this context has one and silently accepting it would
26
+ * hide a caller bug.
27
+ */
28
+ export function fingerprintSql(schemas = ['public']) {
29
+ return readFileSync(fingerprintSqlPath, 'utf8').replaceAll('__SCHEMAS__', schemaList(schemas))
30
+ }
31
+
32
+ /**
33
+ * Quote a schema list for embedding as SQL literals.
34
+ *
35
+ * Any name PostgreSQL accepts is accepted here — `App Data`, `Ünïcode` — and
36
+ * quoted by doubling its single quotes, which makes it a literal that cannot
37
+ * end early. Restricting names to plain identifiers instead made a schema
38
+ * named with a space impossible to compare at all.
39
+ *
40
+ * A backslash is still rejected: with standard_conforming_strings off it would
41
+ * be an escape inside the literal, and no sane schema name needs one. So is
42
+ * NUL, which no PostgreSQL name can contain. This is the single place either
43
+ * query could take an injection.
44
+ */
45
+ function schemaList(schemas) {
46
+ if (!Array.isArray(schemas) || schemas.length === 0) {
47
+ throw new TypeError('expected a non-empty array of schema names')
48
+ }
49
+ for (const s of schemas) {
50
+ if (typeof s !== 'string' || s.length === 0 || /[\\\0]/.test(s)) {
51
+ throw new TypeError(`unsupported schema name ${JSON.stringify(s)}`)
52
+ }
53
+ }
54
+ return schemas.map(s => `'${s.replaceAll("'", "''")}'`).join(',')
55
+ }
56
+
57
+ /** Absolute path to the state query, for consumers that shell out to psql. */
58
+ export const stateSqlPath = join(HERE, 'state.sql')
59
+
60
+ /**
61
+ * The schema-state query, with the schema list substituted in.
62
+ *
63
+ * Where the fingerprint answers "are these the same?", this answers "what is
64
+ * there?" — the same catalog knowledge shaped as a JSON document, so a
65
+ * consumer can compute its own diff rather than trusting someone else's idea
66
+ * of what changed. Schema names are validated and quoted here for the same
67
+ * reason as in fingerprintSql.
68
+ */
69
+ export function stateSql(schemas = ['public']) {
70
+ return readFileSync(stateSqlPath, 'utf8').replaceAll('__SCHEMAS__', schemaList(schemas))
71
+ }
72
+
73
+ /** Corpus names that ship with this package. */
74
+ export const corpora = ['objects', 'hard-cases', 'ordering', 'migrations', 'equivalences', 'data']
75
+
76
+ /**
77
+ * Load one corpus by name.
78
+ *
79
+ * Cases are plain data — SQL plus metadata — so a harness in any language can
80
+ * consume them without agreeing on how they should be run.
81
+ */
82
+ export function loadCorpus(name) {
83
+ if (!corpora.includes(name)) {
84
+ throw new Error(`loadCorpus: unknown corpus ${JSON.stringify(name)}; expected one of ${corpora.join(', ')}`)
85
+ }
86
+ return JSON.parse(readFileSync(join(HERE, 'corpus', `${name}.json`), 'utf8'))
87
+ }
package/package.json CHANGED
@@ -1,6 +1,48 @@
1
1
  {
2
2
  "name": "@akal/pg-conformance",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.0.15",
4
+ "description": "A PostgreSQL schema fingerprint and DDL conformance corpus, shared by tools that need to agree on whether two schemas are the same.",
5
+ "type": "module",
6
+ "main": "index.js",
7
+ "types": "index.d.ts",
8
+ "exports": {
9
+ ".": "./index.js",
10
+ "./fingerprint.sql": "./fingerprint.sql",
11
+ "./corpus/*.json": "./corpus/*.json"
12
+ },
13
+ "files": [
14
+ "index.js",
15
+ "index.d.ts",
16
+ "fingerprint.sql",
17
+ "state.sql",
18
+ "corpus",
19
+ "src"
20
+ ],
21
+ "scripts": {
22
+ "test": "node --test test/*.test.js"
23
+ },
24
+ "keywords": [
25
+ "postgresql",
26
+ "postgres",
27
+ "schema",
28
+ "migration",
29
+ "diff",
30
+ "conformance",
31
+ "fingerprint"
32
+ ],
33
+ "license": "MIT",
34
+ "publishConfig": {
35
+ "access": "public"
36
+ },
37
+ "repository": {
38
+ "type": "git",
39
+ "url": "git+https://github.com/akalsoftware/pg-conformance.git"
40
+ },
41
+ "homepage": "https://github.com/akalsoftware/pg-conformance#readme",
42
+ "bugs": {
43
+ "url": "https://github.com/akalsoftware/pg-conformance/issues"
44
+ },
45
+ "engines": {
46
+ "node": ">=18"
47
+ }
48
+ }
@@ -0,0 +1,97 @@
1
+ <?php
2
+
3
+ declare(strict_types=1);
4
+
5
+ namespace Akal\PgConformance;
6
+
7
+ /**
8
+ * Accessor for the shared conformance data.
9
+ *
10
+ * The PHP and JavaScript accessors deliberately expose the same three things —
11
+ * the fingerprint query, its path, and the corpora — so that a harness written
12
+ * in either language is asking the same question of the database.
13
+ */
14
+ final class Conformance
15
+ {
16
+ /** Path to the fingerprint query, for callers that shell out to psql. */
17
+ public static function fingerprintSqlPath(): string
18
+ {
19
+ return dirname(__DIR__) . '/fingerprint.sql';
20
+ }
21
+
22
+ /**
23
+ * The fingerprint query with the schema list substituted in.
24
+ *
25
+ * Schema names are quoted here rather than by the caller, because the query
26
+ * embeds them as SQL literals. A name that is not a plain identifier is
27
+ * rejected rather than escaped: none is legitimate here, and accepting one
28
+ * quietly would hide a bug in the caller.
29
+ *
30
+ * @param list<string> $schemas
31
+ */
32
+ public static function fingerprintSql(array $schemas = ['public']): string
33
+ {
34
+ return str_replace('__SCHEMAS__', self::schemaList($schemas), (string) file_get_contents(self::fingerprintSqlPath()));
35
+ }
36
+
37
+ /**
38
+ * Quote a schema list for embedding as SQL literals.
39
+ *
40
+ * Rejects anything that is not a plain identifier rather than escaping it:
41
+ * none is legitimate here, and accepting one quietly would hide a caller
42
+ * bug. The single place either query could take an injection.
43
+ *
44
+ * @param list<string> $schemas
45
+ */
46
+ private static function schemaList(array $schemas): string
47
+ {
48
+ if ($schemas === []) {
49
+ throw new \InvalidArgumentException('expected at least one schema name');
50
+ }
51
+
52
+ foreach ($schemas as $schema) {
53
+ if (preg_match('/^[A-Za-z_][A-Za-z0-9_$]*$/', $schema) !== 1) {
54
+ throw new \InvalidArgumentException(sprintf('unsupported schema name "%s"', $schema));
55
+ }
56
+ }
57
+
58
+ return implode(',', array_map(static fn (string $s): string => "'$s'", $schemas));
59
+ }
60
+
61
+ /** Path to the state query, for callers that shell out to psql. */
62
+ public static function stateSqlPath(): string
63
+ {
64
+ return dirname(__DIR__) . '/state.sql';
65
+ }
66
+
67
+ /**
68
+ * The schema-state query with the schema list substituted in.
69
+ *
70
+ * @param list<string> $schemas
71
+ */
72
+ public static function stateSql(array $schemas = ['public']): string
73
+ {
74
+ return str_replace('__SCHEMAS__', self::schemaList($schemas), (string) file_get_contents(self::stateSqlPath()));
75
+ }
76
+
77
+ /** Corpus names that ship with this package. */
78
+ public const CORPORA = ['objects', 'hard-cases', 'ordering', 'migrations', 'equivalences'];
79
+
80
+ /**
81
+ * Load one corpus by name.
82
+ *
83
+ * @return array<int, array<string, mixed>>
84
+ */
85
+ public static function loadCorpus(string $name): array
86
+ {
87
+ if (!in_array($name, self::CORPORA, true)) {
88
+ throw new \InvalidArgumentException(
89
+ sprintf('loadCorpus: unknown corpus "%s"; expected one of %s', $name, implode(', ', self::CORPORA))
90
+ );
91
+ }
92
+
93
+ $path = dirname(__DIR__) . "/corpus/$name.json";
94
+
95
+ return json_decode((string) file_get_contents($path), true, 512, JSON_THROW_ON_ERROR);
96
+ }
97
+ }
package/state.sql ADDED
@@ -0,0 +1,234 @@
1
+ -- Structural state of a PostgreSQL schema, as one JSON document.
2
+ --
3
+ -- The fingerprint answers "are these the same?". This answers "what is there?"
4
+ -- — the same catalog knowledge, shaped for a consumer to read rather than for
5
+ -- a string comparison. A tool holding two of these can compute its own diff
6
+ -- without trusting anyone else's idea of what changed.
7
+ --
8
+ -- Losslessness is the whole point, and it is why this reads pg_catalog rather
9
+ -- than information_schema or an existing schema API. Neither can express a
10
+ -- partition bound, an identity column's sequence options, per-column storage,
11
+ -- compression, or whether a collation was set or inherited. Every one of those
12
+ -- has hidden a real bug, so a state document blind to them would be a step
13
+ -- backwards from the fingerprint it sits beside.
14
+ --
15
+ -- Two conventions, both learned from getting them wrong:
16
+ --
17
+ -- Values are semantic, not catalog shorthand. attstorage 'x' is reported as
18
+ -- "extended" and attcompression 'l' as "lz4", because a consumer should not
19
+ -- have to memorise single letters that mean nothing outside pg_catalog.
20
+ --
21
+ -- Inherited defaults are null, not spelled out. A column that merely uses
22
+ -- the database collation reports null rather than "default", or every text
23
+ -- column in an unchanged schema would read as different.
24
+ --
25
+ -- Arrays are ordered so two runs over identical schemas produce byte-identical
26
+ -- output; jsonb sorts object keys itself.
27
+ --
28
+ -- Usage: replace __SCHEMAS__ with a quoted, comma-separated schema list. The
29
+ -- language accessors do this and handle quoting.
30
+ --
31
+ -- Requires PostgreSQL 14 or newer.
32
+
33
+ SELECT jsonb_pretty(jsonb_build_object(
34
+
35
+ 'meta', jsonb_build_object(
36
+ 'server_version_num', current_setting('server_version_num')::int,
37
+ 'schemas', (SELECT jsonb_agg(s ORDER BY s) FROM unnest(ARRAY[__SCHEMAS__]) AS s)
38
+ ),
39
+
40
+ -- Tables, partitioned tables and foreign tables, with their columns inline:
41
+ -- a column has no meaning apart from its relation, and nesting keeps a
42
+ -- consumer from having to re-join them.
43
+ 'tables', COALESCE((
44
+ SELECT jsonb_agg(t ORDER BY t->>'schema', t->>'name') FROM (
45
+ SELECT jsonb_build_object(
46
+ 'schema', n.nspname,
47
+ 'name', c.relname,
48
+ 'kind', CASE c.relkind WHEN 'r' THEN 'table' WHEN 'p' THEN 'partitioned_table'
49
+ WHEN 'f' THEN 'foreign_table' END,
50
+ 'unlogged', c.relpersistence = 'u',
51
+ 'partition_of', (SELECT p.relname FROM pg_inherits i
52
+ JOIN pg_class p ON p.oid = i.inhparent
53
+ WHERE i.inhrelid = c.oid AND c.relispartition),
54
+ 'partition_bound', pg_get_expr(c.relpartbound, c.oid),
55
+ 'partition_by', CASE WHEN c.relkind = 'p' THEN pg_get_partkeydef(c.oid) END,
56
+ 'inherits', (SELECT jsonb_agg(p.relname ORDER BY p.relname)
57
+ FROM pg_inherits i JOIN pg_class p ON p.oid = i.inhparent
58
+ WHERE i.inhrelid = c.oid AND NOT c.relispartition),
59
+ 'options', (SELECT jsonb_agg(o ORDER BY o) FROM unnest(c.reloptions) AS o),
60
+ 'rls_enabled', c.relrowsecurity,
61
+ 'rls_forced', c.relforcerowsecurity,
62
+ 'comment', obj_description(c.oid, 'pg_class'),
63
+ 'columns', (
64
+ SELECT jsonb_agg(jsonb_build_object(
65
+ 'name', a.attname,
66
+ 'position', a.attnum,
67
+ 'type', format_type(a.atttypid, a.atttypmod),
68
+ 'not_null', a.attnotnull,
69
+ 'default', pg_get_expr(d.adbin, d.adrelid),
70
+ 'identity', CASE a.attidentity WHEN 'a' THEN 'always'
71
+ WHEN 'd' THEN 'by_default' END,
72
+ 'identity_options', (
73
+ SELECT jsonb_build_object('start', s.seqstart, 'increment', s.seqincrement,
74
+ 'min', s.seqmin, 'max', s.seqmax,
75
+ 'cache', s.seqcache, 'cycle', s.seqcycle)
76
+ FROM pg_sequence s
77
+ WHERE a.attidentity <> ''
78
+ AND s.seqrelid = pg_get_serial_sequence(
79
+ format('%I.%I', n.nspname, c.relname), a.attname)::regclass),
80
+ 'generated', CASE a.attgenerated WHEN 's' THEN 'stored' END,
81
+ 'storage', CASE a.attstorage WHEN 'p' THEN 'plain' WHEN 'e' THEN 'external'
82
+ WHEN 'm' THEN 'main' WHEN 'x' THEN 'extended' END,
83
+ -- Null unless it differs from the type's own default, so a storage
84
+ -- setting nobody chose does not read as a deliberate one.
85
+ 'storage_is_default', a.attstorage = tp.typstorage,
86
+ 'compression', CASE a.attcompression WHEN 'l' THEN 'lz4'
87
+ WHEN 'p' THEN 'pglz' END,
88
+ -- An inherited collation is not a property of the column.
89
+ 'collation', NULLIF(co.collname, 'default'),
90
+ 'comment', col_description(c.oid, a.attnum)
91
+ ) ORDER BY a.attnum)
92
+ FROM pg_attribute a
93
+ JOIN pg_type tp ON tp.oid = a.atttypid
94
+ LEFT JOIN pg_attrdef d ON d.adrelid = a.attrelid AND d.adnum = a.attnum
95
+ LEFT JOIN pg_collation co ON co.oid = a.attcollation
96
+ WHERE a.attrelid = c.oid AND a.attnum > 0 AND NOT a.attisdropped),
97
+ 'constraints', (
98
+ SELECT jsonb_agg(jsonb_build_object(
99
+ 'name', con.conname,
100
+ 'type', CASE con.contype WHEN 'p' THEN 'primary_key' WHEN 'f' THEN 'foreign_key'
101
+ WHEN 'u' THEN 'unique' WHEN 'c' THEN 'check'
102
+ WHEN 'x' THEN 'exclusion' WHEN 'n' THEN 'not_null' END,
103
+ 'definition', pg_get_constraintdef(con.oid),
104
+ 'validated', con.convalidated,
105
+ 'deferrable', con.condeferrable,
106
+ 'deferred', con.condeferred
107
+ ) ORDER BY con.conname)
108
+ FROM pg_constraint con WHERE con.conrelid = c.oid),
109
+ 'indexes', (
110
+ SELECT jsonb_agg(jsonb_build_object(
111
+ 'name', i.indexname, 'definition', i.indexdef
112
+ ) ORDER BY i.indexname)
113
+ FROM pg_indexes i
114
+ WHERE i.schemaname = n.nspname AND i.tablename = c.relname),
115
+ 'policies', (
116
+ SELECT jsonb_agg(jsonb_build_object(
117
+ 'name', p.polname,
118
+ 'command', CASE p.polcmd WHEN 'r' THEN 'select' WHEN 'a' THEN 'insert'
119
+ WHEN 'w' THEN 'update' WHEN 'd' THEN 'delete'
120
+ WHEN '*' THEN 'all' END,
121
+ 'permissive', p.polpermissive,
122
+ 'roles', (SELECT jsonb_agg(r.rolname ORDER BY r.rolname)
123
+ FROM unnest(p.polroles) AS ro JOIN pg_roles r ON r.oid = ro),
124
+ 'using', pg_get_expr(p.polqual, p.polrelid),
125
+ 'with_check', pg_get_expr(p.polwithcheck, p.polrelid)
126
+ ) ORDER BY p.polname)
127
+ FROM pg_policy p WHERE p.polrelid = c.oid),
128
+ 'triggers', (
129
+ SELECT jsonb_agg(jsonb_build_object(
130
+ 'name', tg.tgname, 'definition', pg_get_triggerdef(tg.oid)
131
+ ) ORDER BY tg.tgname)
132
+ FROM pg_trigger tg
133
+ WHERE tg.tgrelid = c.oid AND NOT tg.tgisinternal AND tg.tgparentid = 0)
134
+ ) AS t
135
+ FROM pg_class c JOIN pg_namespace n ON n.oid = c.relnamespace
136
+ WHERE n.nspname IN (__SCHEMAS__) AND c.relkind IN ('r','p','f')
137
+ ) q), '[]'::jsonb),
138
+
139
+ 'views', COALESCE((
140
+ SELECT jsonb_agg(v ORDER BY v->>'schema', v->>'name') FROM (
141
+ SELECT jsonb_build_object(
142
+ 'schema', n.nspname, 'name', c.relname,
143
+ 'materialized', c.relkind = 'm',
144
+ 'definition', pg_get_viewdef(c.oid, true),
145
+ 'options', (SELECT jsonb_agg(o ORDER BY o) FROM unnest(c.reloptions) AS o),
146
+ 'comment', obj_description(c.oid, 'pg_class'),
147
+ 'columns', (SELECT jsonb_agg(jsonb_build_object(
148
+ 'name', a.attname, 'type', format_type(a.atttypid, a.atttypmod))
149
+ ORDER BY a.attnum)
150
+ FROM pg_attribute a
151
+ WHERE a.attrelid = c.oid AND a.attnum > 0 AND NOT a.attisdropped)
152
+ ) AS v
153
+ FROM pg_class c JOIN pg_namespace n ON n.oid = c.relnamespace
154
+ WHERE n.nspname IN (__SCHEMAS__) AND c.relkind IN ('v','m')
155
+ ) q), '[]'::jsonb),
156
+
157
+ -- Every sequence, with owned_by naming the column that brought it into being
158
+ -- so a consumer can tell one it must reproduce from one a column already
159
+ -- creates. An identity column's options are also reported on that column.
160
+ --
161
+ -- Both kinds of ownership count, and PostgreSQL records them differently: a
162
+ -- serial column's sequence depends on it with deptype 'a', an identity
163
+ -- column's with 'i'. Reading only 'a' reported every identity sequence as
164
+ -- standalone.
165
+ 'sequences', COALESCE((
166
+ SELECT jsonb_agg(s ORDER BY s->>'schema', s->>'name') FROM (
167
+ SELECT jsonb_build_object(
168
+ 'schema', n.nspname, 'name', c.relname,
169
+ 'type', format_type(sq.seqtypid, NULL),
170
+ 'start', sq.seqstart, 'increment', sq.seqincrement,
171
+ 'min', sq.seqmin, 'max', sq.seqmax, 'cache', sq.seqcache, 'cycle', sq.seqcycle,
172
+ 'owned_by', (SELECT format('%I.%I', dc.relname, da.attname)
173
+ FROM pg_depend dep
174
+ JOIN pg_class dc ON dc.oid = dep.refobjid
175
+ JOIN pg_attribute da ON da.attrelid = dep.refobjid
176
+ AND da.attnum = dep.refobjsubid
177
+ WHERE dep.objid = c.oid
178
+ AND dep.classid = 'pg_class'::regclass
179
+ AND dep.refclassid = 'pg_class'::regclass
180
+ AND dep.refobjsubid > 0
181
+ AND dep.deptype IN ('a', 'i') LIMIT 1)
182
+ ) AS s
183
+ FROM pg_sequence sq
184
+ JOIN pg_class c ON c.oid = sq.seqrelid
185
+ JOIN pg_namespace n ON n.oid = c.relnamespace
186
+ WHERE n.nspname IN (__SCHEMAS__)
187
+ ) q), '[]'::jsonb),
188
+
189
+ 'routines', COALESCE((
190
+ SELECT jsonb_agg(f ORDER BY f->>'schema', f->>'name', f->>'arguments') FROM (
191
+ SELECT jsonb_build_object(
192
+ 'schema', n.nspname, 'name', p.proname,
193
+ 'kind', CASE p.prokind WHEN 'p' THEN 'procedure' ELSE 'function' END,
194
+ 'arguments', pg_get_function_identity_arguments(p.oid),
195
+ 'definition', pg_get_functiondef(p.oid),
196
+ 'comment', obj_description(p.oid, 'pg_proc')
197
+ ) AS f
198
+ FROM pg_proc p JOIN pg_namespace n ON n.oid = p.pronamespace
199
+ WHERE n.nspname IN (__SCHEMAS__) AND p.prokind IN ('f','p')
200
+ ) q), '[]'::jsonb),
201
+
202
+ 'types', COALESCE((
203
+ SELECT jsonb_agg(t ORDER BY t->>'schema', t->>'name') FROM (
204
+ SELECT jsonb_build_object(
205
+ 'schema', n.nspname, 'name', t.typname,
206
+ 'kind', CASE t.typtype WHEN 'e' THEN 'enum' WHEN 'd' THEN 'domain'
207
+ WHEN 'c' THEN 'composite' END,
208
+ 'labels', (SELECT jsonb_agg(e.enumlabel ORDER BY e.enumsortorder)
209
+ FROM pg_enum e WHERE e.enumtypid = t.oid),
210
+ 'base_type', CASE WHEN t.typtype = 'd'
211
+ THEN format_type(t.typbasetype, t.typtypmod) END,
212
+ 'not_null', t.typnotnull,
213
+ 'default', pg_get_expr(t.typdefaultbin, 0),
214
+ 'constraints', (SELECT jsonb_agg(pg_get_constraintdef(con.oid) ORDER BY con.conname)
215
+ FROM pg_constraint con WHERE con.contypid = t.oid),
216
+ 'attributes', (SELECT jsonb_agg(jsonb_build_object(
217
+ 'name', a.attname, 'type', format_type(a.atttypid, a.atttypmod))
218
+ ORDER BY a.attnum)
219
+ FROM pg_attribute a
220
+ WHERE a.attrelid = t.typrelid AND a.attnum > 0 AND NOT a.attisdropped)
221
+ ) AS t
222
+ FROM pg_type t JOIN pg_namespace n ON n.oid = t.typnamespace
223
+ WHERE n.nspname IN (__SCHEMAS__) AND t.typtype IN ('e','d','c')
224
+ AND NOT EXISTS (SELECT 1 FROM pg_class c
225
+ WHERE c.oid = t.typrelid AND c.relkind <> 'c')
226
+ ) q), '[]'::jsonb),
227
+
228
+ 'extensions', COALESCE((
229
+ SELECT jsonb_agg(jsonb_build_object('name', e.extname, 'version', e.extversion)
230
+ ORDER BY e.extname)
231
+ FROM pg_extension e JOIN pg_namespace n ON n.oid = e.extnamespace
232
+ WHERE n.nspname IN (__SCHEMAS__)), '[]'::jsonb)
233
+
234
+ )) AS state;