@akal/pg-conformance 0.0.0-stage → 0.0.16
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/LICENSE +21 -0
- package/README.md +191 -2
- package/corpus/data.json +122 -0
- package/corpus/equivalences.json +44 -0
- package/corpus/hard-cases.json +452 -0
- package/corpus/migrations.json +515 -0
- package/corpus/objects.json +202 -0
- package/corpus/ordering.json +201 -0
- package/fingerprint.sql +245 -0
- package/index.d.ts +147 -0
- package/index.js +87 -0
- package/package.json +46 -4
- package/src/Conformance.php +97 -0
- package/state.sql +234 -0
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.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.0.16",
|
|
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;
|