@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/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Akal Software Ltd
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,192 @@
|
|
|
1
|
-
#
|
|
1
|
+
# pg-conformance
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
A PostgreSQL schema **fingerprint** and a **DDL conformance corpus**, shared by tools that need to agree on whether two schemas are the same.
|
|
4
|
+
|
|
5
|
+
Data only. No runner, no database driver, no opinion about how you test.
|
|
6
|
+
|
|
7
|
+
## Why this exists
|
|
8
|
+
|
|
9
|
+
Two tools that compare PostgreSQL schemas each had their own answer to "are these the same?". They diverged, quietly, and one of them ended up reporting these pairs as identical:
|
|
10
|
+
|
|
11
|
+
| | one side | the other |
|
|
12
|
+
| --- | --- | --- |
|
|
13
|
+
| view | `SELECT n FROM t WHERE n > 0` | `... WHERE n < 0` |
|
|
14
|
+
| function | `RETURNS int AS $$ SELECT 1 $$` | `... SELECT 999 $$` |
|
|
15
|
+
| trigger | `AFTER INSERT ON t` | `BEFORE UPDATE ON t` |
|
|
16
|
+
| identity | `GENERATED ALWAYS AS IDENTITY (INCREMENT 10 START 100)` | `GENERATED ALWAYS AS IDENTITY` |
|
|
17
|
+
|
|
18
|
+
None of those is exotic. Each was invisible because that fingerprint compared objects by *name* and read columns from `information_schema`, which cannot express identity options, generated columns, storage, compression or collation — and has nowhere to put a partition bound.
|
|
19
|
+
|
|
20
|
+
The fix is not a better fingerprint in each tool. It is one fingerprint.
|
|
21
|
+
|
|
22
|
+
## Install
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
npm install @akal/pg-conformance
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
**PHP consumers install the same npm package.** This is not published to
|
|
29
|
+
Packagist, so `composer require` will not find it — the npm tarball ships
|
|
30
|
+
`src/Conformance.php`, which is how DBDiff consumes it:
|
|
31
|
+
|
|
32
|
+
```php
|
|
33
|
+
require_once 'node_modules/@akal/pg-conformance/src/Conformance.php';
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
One package means one version number for both languages, which matters more
|
|
37
|
+
here than idiomatic installation: a corpus whose whole job is to be the single
|
|
38
|
+
shared answer to "are these schemas the same?" should not be publishable at two
|
|
39
|
+
different versions at once. `composer.json` is kept for its autoload map and for
|
|
40
|
+
requiring this from git if you need to.
|
|
41
|
+
|
|
42
|
+
## Use
|
|
43
|
+
|
|
44
|
+
```js
|
|
45
|
+
import { fingerprintSql, loadCorpus } from '@akal/pg-conformance'
|
|
46
|
+
|
|
47
|
+
const sql = fingerprintSql(['public'])
|
|
48
|
+
const a = await query(sourceDb, sql)
|
|
49
|
+
const b = await query(targetDb, sql)
|
|
50
|
+
if (a !== b) { /* the schemas differ */ }
|
|
51
|
+
|
|
52
|
+
for (const testCase of loadCorpus('hard-cases')) {
|
|
53
|
+
// testCase.sql builds the objects; testCase.minPgVersion gates it
|
|
54
|
+
}
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
```php
|
|
58
|
+
use Akal\PgConformance\Conformance;
|
|
59
|
+
|
|
60
|
+
$sql = Conformance::fingerprintSql(['public']);
|
|
61
|
+
$cases = Conformance::loadCorpus('hard-cases');
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Schema names are quoted by the accessor, not by you — the query embeds them as SQL literals, so that is the one place an injection could enter. A name that is not a plain identifier is rejected rather than escaped.
|
|
65
|
+
|
|
66
|
+
If you shell out to `psql`, use `fingerprintSqlPath` and substitute `__SCHEMAS__` yourself.
|
|
67
|
+
|
|
68
|
+
## Schema state
|
|
69
|
+
|
|
70
|
+
`fingerprintSql()` answers *are these the same?*. `stateSql()` answers *what is there?* — the same catalog knowledge shaped as a JSON document, so a consumer can compute its own diff instead of trusting someone else's idea of what changed.
|
|
71
|
+
|
|
72
|
+
```js
|
|
73
|
+
import { stateSql } from '@akal/pg-conformance'
|
|
74
|
+
|
|
75
|
+
const before = JSON.parse(await query(db, stateSql(['public'])))
|
|
76
|
+
// ... apply a migration ...
|
|
77
|
+
const after = JSON.parse(await query(db, stateSql(['public'])))
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
```json
|
|
81
|
+
{
|
|
82
|
+
"meta": { "server_version_num": 170011, "schemas": ["public"] },
|
|
83
|
+
"tables": [{
|
|
84
|
+
"schema": "public", "name": "orders", "kind": "partitioned_table",
|
|
85
|
+
"unlogged": false, "partition_by": "RANGE (created_at)",
|
|
86
|
+
"options": ["fillfactor=70"], "rls_enabled": true,
|
|
87
|
+
"columns": [{
|
|
88
|
+
"name": "id", "type": "bigint", "not_null": true,
|
|
89
|
+
"identity": "always",
|
|
90
|
+
"identity_options": { "start": 100, "increment": 10, "cycle": false },
|
|
91
|
+
"storage": "plain", "compression": null, "collation": null
|
|
92
|
+
}],
|
|
93
|
+
"constraints": [...], "indexes": [...], "policies": [...], "triggers": [...]
|
|
94
|
+
}],
|
|
95
|
+
"views": [...], "sequences": [...], "routines": [...],
|
|
96
|
+
"types": [...], "extensions": [...]
|
|
97
|
+
}
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
Two conventions, both learned from getting them wrong:
|
|
101
|
+
|
|
102
|
+
**Values are semantic, not catalog shorthand.** `attstorage` `'x'` is reported as `"extended"`, `attcompression` `'l'` as `"lz4"`, `attidentity` `'a'` as `"always"`. A consumer should not have to memorise single letters.
|
|
103
|
+
|
|
104
|
+
**Inherited defaults are `null`, not spelled out.** A column that merely uses the database collation reports `null` rather than `"default"` — otherwise every text column in an unchanged schema reads as different.
|
|
105
|
+
|
|
106
|
+
The state document is verified to distinguish **every pair of corpus schemas the fingerprint distinguishes**, so adopting it loses nothing the fingerprint already caught. It is also byte-stable: identical schemas produce identical documents.
|
|
107
|
+
|
|
108
|
+
Existing schema APIs are not a substitute. `information_schema` cannot express a partition bound, identity sequence options, storage, compression or collation, and `postgres-meta` reads `relkind`/`relrowsecurity` but not `relpartbound`, `relpersistence` or `reloptions`, no identity options, and does not model sequences at all.
|
|
109
|
+
|
|
110
|
+
## What the fingerprint covers
|
|
111
|
+
|
|
112
|
+
Relations (kind, persistence, partition bound, storage options, RLS) · columns (type, nullability, default, identity, generated, storage, compression, collation) · sequence options · constraints (definition, validated, deferrable) · indexes, per relation · views and materialised views, **by body** · routines, **by body** · triggers, **by definition** · policies (command, roles, `USING`, `WITH CHECK`) · enums, domains and composite types · inheritance · comments.
|
|
113
|
+
|
|
114
|
+
Two principles decide the content:
|
|
115
|
+
|
|
116
|
+
**Read the catalog, not `information_schema`.** The standard has no concept of most of what matters here.
|
|
117
|
+
|
|
118
|
+
**Compare bodies, not names.** Where PostgreSQL can render an object canonically — `pg_get_viewdef`, `pg_get_constraintdef`, `pg_get_functiondef`, `pg_get_triggerdef` — that rendering is what gets compared, so both sides come from the same server code and formatting can never manufacture a difference.
|
|
119
|
+
|
|
120
|
+
Definitions are flattened to one line, because entries are newline-joined and a multi-line body would otherwise arrive as several unattributed entries.
|
|
121
|
+
|
|
122
|
+
## The corpora
|
|
123
|
+
|
|
124
|
+
| corpus | what it is for |
|
|
125
|
+
| --- | --- |
|
|
126
|
+
| `objects` | creating each object kind from an empty schema |
|
|
127
|
+
| `hard-cases` | DDL that is awkward to reproduce — identity options, generated columns, exclusion constraints, partitioning of all three strategies and multi-level, inheritance, collations, storage and TOAST parameters, compression, every index method, interval and range types, domains over domains, function overloads, `INSTEAD OF` and constraint triggers, restrictive policies |
|
|
128
|
+
| `ordering` | dependency ordering, with names chosen to defeat text matching |
|
|
129
|
+
| `migrations` | schema changes a migration tool must make in both directions — enum labels removed or reordered under views, policies and keys; identity, serial and storage changes; generated columns; types and functions created before, and dropped after, what uses them; cross-schema and multi-column foreign keys; objects in other schemas and in quoted ones; overloads, partitions, RLS, triggers, sequences and constraints |
|
|
130
|
+
| `equivalences` | one schema written two ways — as a developer writes it and as PostgreSQL renders it — which a comparison must call identical |
|
|
131
|
+
| `data` | rows a data migration must carry over both ways — text needing quoting, JSON, arrays, binary, numeric extremes, time zones, composite and missing keys, identity and generated columns, enums, domains and other types |
|
|
132
|
+
|
|
133
|
+
`ordering` cases give statements in an order that does **not** apply, plus the precedences any correct order must satisfy — a property rather than one expected permutation, so a sorter's tie-breaking can change without invalidating the case.
|
|
134
|
+
|
|
135
|
+
`migrations` cases give a `before` and an `after` schema. A tool migrating either into the other must leave it identical to a database built from the other directly, and its `preserve` queries must return the same rows on the migrated database as they did before it was migrated. They read only what a correct migration keeps — never a generated column whose expression changes, which it must recompute. These are the shapes found to produce SQL that is valid but cannot run, or that runs and loses something.
|
|
136
|
+
|
|
137
|
+
`equivalences` exist because PostgreSQL does not render every expression the same way twice: `status IN ('draft', 'active')` on a `varchar` column renders as `ARRAY[...]::text[]`, and recreated from that, as `ARRAY[(...)::text, ...]`. A dump, a restore or a generated migration changes the text and not the schema. **The fingerprint does not yet call these pairs identical** — it compares renderings — so a consumer comparing a database with a copy of it has to re-render one side (the package's own test reports this as a to-do).
|
|
138
|
+
|
|
139
|
+
`data` cases give one `schema` and two sets of rows. Migrating the `before` rows into the `after` rows, or back, must leave each `compare` query returning what it does on a database built from the other side directly. They are the values found to be quoted wrongly, or not handled at all, by a data diff.
|
|
140
|
+
|
|
141
|
+
Cases carry `minPgVersion` where they need a particular server.
|
|
142
|
+
|
|
143
|
+
## Requirements
|
|
144
|
+
|
|
145
|
+
PostgreSQL 14 or newer. Tested against 14, 15, 16, 17 and 18 on every change.
|
|
146
|
+
|
|
147
|
+
## Releases
|
|
148
|
+
|
|
149
|
+
Every merge to `main` publishes a patch automatically, so consumers track it
|
|
150
|
+
without ceremony. To ask for more than a patch, use any of these — the last one
|
|
151
|
+
wins, and is the one to rely on:
|
|
152
|
+
|
|
153
|
+
| How | Where |
|
|
154
|
+
| --- | --- |
|
|
155
|
+
| `[minor]` / `[major]` | anywhere in the commit message |
|
|
156
|
+
| `[minor]` / `[major]` | in the pull request title |
|
|
157
|
+
| `release:minor` / `release:major` | a label on the pull request |
|
|
158
|
+
|
|
159
|
+
`[skip release]` opts out.
|
|
160
|
+
|
|
161
|
+
This package is `0.x` while the fingerprint settles. **Depend on it with
|
|
162
|
+
`~0.0.x`, not `^0.0.x`** — under semver a caret on a `0.0.z` version allows no
|
|
163
|
+
updates at all, so `^0.0.6` is an exact pin and you would never receive a
|
|
164
|
+
release:
|
|
165
|
+
|
|
166
|
+
```jsonc
|
|
167
|
+
"@akal/pg-conformance": "~0.0.6" // tracks 0.0.7, 0.0.8, ...
|
|
168
|
+
"@akal/pg-conformance": "^0.0.6" // pinned to exactly 0.0.6
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
Once it reaches `0.1.0`, `^0.1.0` tracks the `0.1.x` line as you would expect.
|
|
172
|
+
|
|
173
|
+
Consumers pin through their lockfile as usual; a bot bumps that lockfile, so `npm ci` stays reproducible and still moves.
|
|
174
|
+
|
|
175
|
+
## Local development
|
|
176
|
+
|
|
177
|
+
```bash
|
|
178
|
+
npm link # in this repo
|
|
179
|
+
npm link @akal/pg-conformance # in the consumer
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
Run the package's own tests against a real server:
|
|
183
|
+
|
|
184
|
+
```bash
|
|
185
|
+
PGURL=postgresql://user:pass@127.0.0.1:5432/postgres npm test
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
Without `PGURL` the database half skips and only the accessors are checked. The database half is the half that matters: it asserts the fingerprint *discriminates*, since one that returned a constant would pass every consumer's suite while proving nothing.
|
|
189
|
+
|
|
190
|
+
## Licence
|
|
191
|
+
|
|
192
|
+
MIT
|
package/corpus/data.json
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
[
|
|
2
|
+
{
|
|
3
|
+
"id": "text_escaping",
|
|
4
|
+
"description": "Text a migration must quote rather than mangle: single and double quotes, backslashes, newlines and tabs, non-ASCII and emoji, and an empty string beside a NULL.",
|
|
5
|
+
"schema": "CREATE TABLE t (id int PRIMARY KEY, v text);",
|
|
6
|
+
"before": "INSERT INTO t VALUES (1, 'plain'), (2, 'to be deleted'), (3, NULL);",
|
|
7
|
+
"after": "INSERT INTO t VALUES (1, E'it''s a \"quote\" and a back\\\\slash'), (3, ''), (4, E'line1\\nline2\\ttab'), (5, 'ünïcödé 🎉'), (6, E'trailing backslash \\\\');",
|
|
8
|
+
"compare": [
|
|
9
|
+
"SELECT id, v, v IS NULL FROM t ORDER BY id"
|
|
10
|
+
]
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
"id": "json_values",
|
|
14
|
+
"description": "json and jsonb holding quotes, backslashes, nesting, a bare string and a JSON null — distinct from an SQL NULL.",
|
|
15
|
+
"schema": "CREATE TABLE j (id int PRIMARY KEY, a json, b jsonb);",
|
|
16
|
+
"before": "INSERT INTO j VALUES (1, '{\"k\": 1}', '{\"k\": 1}'), (2, NULL, NULL);",
|
|
17
|
+
"after": "INSERT INTO j VALUES (1, '{\"q\": \"it''s \\\"quoted\\\"\", \"n\": [1, {\"x\": null}]}', '{\"q\": \"back\\\\slash\", \"z\": true}'), (2, '\"just a string\"', 'null'), (3, '[]', '{}');",
|
|
18
|
+
"compare": [
|
|
19
|
+
"SELECT id, a::text, b::text, a IS NULL, b IS NULL FROM j ORDER BY id"
|
|
20
|
+
]
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
"id": "array_values",
|
|
24
|
+
"description": "Arrays whose elements need quoting — commas, braces, quotes, spaces — with NULL elements, an empty array and a two-dimensional one.",
|
|
25
|
+
"schema": "CREATE TABLE a (id int PRIMARY KEY, t text[], m int[]);",
|
|
26
|
+
"before": "INSERT INTO a VALUES (1, '{x}', '{1}');",
|
|
27
|
+
"after": "INSERT INTO a VALUES (1, ARRAY['a,b', '{c}', 'd\"e', 'f g', NULL, ''], '{{1,2},{3,4}}'), (2, '{}', NULL);",
|
|
28
|
+
"compare": [
|
|
29
|
+
"SELECT id, t::text, m::text FROM a ORDER BY id"
|
|
30
|
+
]
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
"id": "binary_values",
|
|
34
|
+
"description": "bytea, including a zero byte, high bytes and an empty value. A driver may hand these back as a stream rather than a string.",
|
|
35
|
+
"schema": "CREATE TABLE b (id int PRIMARY KEY, v bytea);",
|
|
36
|
+
"before": "INSERT INTO b VALUES (1, '\\x00');",
|
|
37
|
+
"after": "INSERT INTO b VALUES (1, '\\x00ff10'), (2, '\\x'), (3, decode('68656c6c6f00776f726c64', 'hex'));",
|
|
38
|
+
"compare": [
|
|
39
|
+
"SELECT id, encode(v, 'hex') FROM b ORDER BY id"
|
|
40
|
+
]
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
"id": "numeric_extremes",
|
|
44
|
+
"description": "Values that lose something if they pass through a float or an unquoted literal: high-precision numerics, NaN and infinities, the smallest double, and bigint at both ends.",
|
|
45
|
+
"schema": "CREATE TABLE n (id int PRIMARY KEY, d numeric, f float8, i bigint);",
|
|
46
|
+
"before": "INSERT INTO n VALUES (1, 1, 1, 1);",
|
|
47
|
+
"after": "INSERT INTO n VALUES (1, 123456789012345678901234567890.123456789, 'NaN', 9223372036854775807), (2, -0.000000001, '-Infinity', -9223372036854775808), (3, 'NaN', 1e-300, 0);",
|
|
48
|
+
"compare": [
|
|
49
|
+
"SELECT id, d::text, f::text, i::text FROM n ORDER BY id"
|
|
50
|
+
]
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
"id": "time_values",
|
|
54
|
+
"description": "timestamptz written in other zones, infinite timestamps and dates, intervals with months and seconds, and a time with a zone.",
|
|
55
|
+
"schema": "CREATE TABLE tm (id int PRIMARY KEY, ts timestamptz, d date, iv interval, tz timetz);",
|
|
56
|
+
"before": "INSERT INTO tm VALUES (1, '2020-01-01 00:00+00', '2020-01-01', '1 day', '10:00+00');",
|
|
57
|
+
"after": "INSERT INTO tm VALUES (1, '2026-01-01 10:00+05:30', 'infinity', '1 year 2 months 3 days 04:05:06.789', '23:59:59.999+14'), (2, '-infinity', '0001-01-01', '-5 years', NULL);",
|
|
58
|
+
"compare": [
|
|
59
|
+
"SELECT id, ts AT TIME ZONE 'UTC', d::text, iv::text, tz::text FROM tm ORDER BY id"
|
|
60
|
+
]
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
"id": "composite_key",
|
|
64
|
+
"description": "A two-column primary key: rows updated, inserted and deleted by both parts of the key, one of which shares a value with another row.",
|
|
65
|
+
"schema": "CREATE TABLE c (a int, b text, v text, PRIMARY KEY (a, b));",
|
|
66
|
+
"before": "INSERT INTO c VALUES (1, 'x', 'uno'), (3, 'z', 'three');",
|
|
67
|
+
"after": "INSERT INTO c VALUES (1, 'x', 'one'), (1, 'y', 'two'), (2, 'x', NULL);",
|
|
68
|
+
"compare": [
|
|
69
|
+
"SELECT a, b, v FROM c ORDER BY a, b"
|
|
70
|
+
]
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
"id": "no_primary_key",
|
|
74
|
+
"description": "A table with no key at all, holding duplicate rows. Rows can only be told apart by their values, and a duplicate counts.",
|
|
75
|
+
"schema": "CREATE TABLE k (x int, y text);",
|
|
76
|
+
"before": "INSERT INTO k VALUES (1, 'a'), (3, 'c');",
|
|
77
|
+
"after": "INSERT INTO k VALUES (1, 'a'), (1, 'a'), (2, NULL);",
|
|
78
|
+
"compare": [
|
|
79
|
+
"SELECT x, y, count(*) FROM k GROUP BY x, y ORDER BY x, y"
|
|
80
|
+
]
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
"id": "identity_always",
|
|
84
|
+
"description": "Rows in a table whose key is GENERATED ALWAYS AS IDENTITY: inserting them with their ids needs OVERRIDING SYSTEM VALUE.",
|
|
85
|
+
"schema": "CREATE TABLE ia (id int GENERATED ALWAYS AS IDENTITY PRIMARY KEY, v text);",
|
|
86
|
+
"before": "INSERT INTO ia (v) VALUES ('one');",
|
|
87
|
+
"after": "INSERT INTO ia (id, v) OVERRIDING SYSTEM VALUE VALUES (1, 'first'), (7, 'seventh');",
|
|
88
|
+
"compare": [
|
|
89
|
+
"SELECT id, v FROM ia ORDER BY id"
|
|
90
|
+
]
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
"id": "generated_column",
|
|
94
|
+
"description": "A table with a stored generated column. Its values follow from the others: a migration writing them is refused by the server.",
|
|
95
|
+
"schema": "CREATE TABLE g (id int PRIMARY KEY, a int, twice int GENERATED ALWAYS AS (a * 2) STORED);",
|
|
96
|
+
"before": "INSERT INTO g (id, a) VALUES (1, 1);",
|
|
97
|
+
"after": "INSERT INTO g (id, a) VALUES (1, 5), (2, 7);",
|
|
98
|
+
"compare": [
|
|
99
|
+
"SELECT id, a, twice FROM g ORDER BY id"
|
|
100
|
+
]
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
"id": "enum_and_domain_values",
|
|
104
|
+
"description": "Columns typed by an enum and by a domain over text.",
|
|
105
|
+
"schema": "CREATE TYPE mood AS ENUM ('sad', 'ok', 'happy'); CREATE DOMAIN code AS text CHECK (VALUE ~ '^[A-Z]+$'); CREATE TABLE ed (id int PRIMARY KEY, m mood, c code);",
|
|
106
|
+
"before": "INSERT INTO ed VALUES (1, 'sad', 'AB');",
|
|
107
|
+
"after": "INSERT INTO ed VALUES (1, 'happy', 'XYZ'), (2, NULL, 'Q');",
|
|
108
|
+
"compare": [
|
|
109
|
+
"SELECT id, m::text, c::text FROM ed ORDER BY id"
|
|
110
|
+
]
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
"id": "assorted_types",
|
|
114
|
+
"description": "uuid, boolean, inet, macaddr, bit strings, money and a range.",
|
|
115
|
+
"schema": "CREATE TABLE at (id int PRIMARY KEY, u uuid, ok boolean, ip inet, mac macaddr, bits bit varying(8), price money, r int4range);",
|
|
116
|
+
"before": "INSERT INTO at VALUES (1, NULL, false, NULL, NULL, NULL, NULL, NULL);",
|
|
117
|
+
"after": "INSERT INTO at VALUES (1, 'a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11', true, '192.168.0.1/24', '08:00:2b:01:02:03', B'1011', 12.34, '[1,10)'), (2, NULL, NULL, '::1', NULL, B'', 0, 'empty');",
|
|
118
|
+
"compare": [
|
|
119
|
+
"SELECT id, u::text, ok::text, ip::text, mac::text, bits::text, price::numeric::text, r::text FROM at ORDER BY id"
|
|
120
|
+
]
|
|
121
|
+
}
|
|
122
|
+
]
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
[
|
|
2
|
+
{
|
|
3
|
+
"id": "in_list_on_varchar_check",
|
|
4
|
+
"kind": "check",
|
|
5
|
+
"description": "A CHECK constraint with IN (...) on a varchar column, as written and as PostgreSQL renders it. Recreated from that rendering it renders differently again (ARRAY[...]::text[] becomes ARRAY[(...)::text, ...]), so a dump, restore or migration changes the text, not the schema.",
|
|
6
|
+
"written": "CREATE TABLE t (status varchar(20), n int, CONSTRAINT c CHECK (status IN ('draft', 'active')));",
|
|
7
|
+
"rendered": "CREATE TABLE t (status varchar(20), n int, CONSTRAINT c CHECK (((status)::text = ANY ((ARRAY['draft'::character varying, 'active'::character varying])::text[]))));"
|
|
8
|
+
},
|
|
9
|
+
{
|
|
10
|
+
"id": "in_list_on_varchar_index",
|
|
11
|
+
"kind": "index",
|
|
12
|
+
"description": "A partial index with IN (...) on a varchar column, as written and as PostgreSQL renders it. Recreated from that rendering it renders differently again (ARRAY[...]::text[] becomes ARRAY[(...)::text, ...]), so a dump, restore or migration changes the text, not the schema.",
|
|
13
|
+
"written": "CREATE TABLE t (status varchar(20), n int); CREATE INDEX i ON t (n) WHERE status IN ('draft', 'active');",
|
|
14
|
+
"rendered": "CREATE TABLE t (status varchar(20), n int); CREATE INDEX i ON t (n) WHERE ((status)::text = ANY ((ARRAY['draft'::character varying, 'active'::character varying])::text[]));"
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
"id": "in_list_on_varchar_policy",
|
|
18
|
+
"kind": "policy",
|
|
19
|
+
"description": "A RLS policy with IN (...) on a varchar column, as written and as PostgreSQL renders it. Recreated from that rendering it renders differently again (ARRAY[...]::text[] becomes ARRAY[(...)::text, ...]), so a dump, restore or migration changes the text, not the schema.",
|
|
20
|
+
"written": "CREATE TABLE t (status varchar(20)); ALTER TABLE t ENABLE ROW LEVEL SECURITY; CREATE POLICY p ON t USING (status IN ('draft', 'active'));",
|
|
21
|
+
"rendered": "CREATE TABLE t (status varchar(20)); ALTER TABLE t ENABLE ROW LEVEL SECURITY; CREATE POLICY p ON t USING (((status)::text = ANY ((ARRAY['draft'::character varying, 'active'::character varying])::text[])));"
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
"id": "in_list_on_varchar_view",
|
|
25
|
+
"kind": "view",
|
|
26
|
+
"description": "A view with IN (...) on a varchar column, as written and as PostgreSQL renders it. Recreated from that rendering it renders differently again (ARRAY[...]::text[] becomes ARRAY[(...)::text, ...]), so a dump, restore or migration changes the text, not the schema.",
|
|
27
|
+
"written": "CREATE TABLE t (status varchar(20)); CREATE VIEW v AS SELECT * FROM t WHERE status IN ('draft', 'active');",
|
|
28
|
+
"rendered": "CREATE TABLE t (status varchar(20)); CREATE VIEW v AS SELECT t.status FROM t WHERE ((t.status)::text = ANY ((ARRAY['draft'::character varying, 'active'::character varying])::text[]));"
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
"id": "in_list_on_varchar_domain",
|
|
32
|
+
"kind": "domain",
|
|
33
|
+
"description": "A domain CHECK with IN (...) on a varchar column, as written and as PostgreSQL renders it. Recreated from that rendering it renders differently again (ARRAY[...]::text[] becomes ARRAY[(...)::text, ...]), so a dump, restore or migration changes the text, not the schema.",
|
|
34
|
+
"written": "CREATE DOMAIN d AS varchar(20) CHECK (VALUE IN ('draft', 'active')); CREATE TABLE t (status d);",
|
|
35
|
+
"rendered": "CREATE DOMAIN d AS varchar(20) CHECK (((VALUE)::text = ANY ((ARRAY['draft'::character varying, 'active'::character varying])::text[]))); CREATE TABLE t (status d);"
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
"id": "in_list_on_varchar_generated",
|
|
39
|
+
"kind": "generated",
|
|
40
|
+
"description": "A stored generated column with IN (...) on a varchar column, as written and as PostgreSQL renders it. Recreated from that rendering it renders differently again (ARRAY[...]::text[] becomes ARRAY[(...)::text, ...]), so a dump, restore or migration changes the text, not the schema.",
|
|
41
|
+
"written": "CREATE TABLE t (status varchar(20), ok boolean GENERATED ALWAYS AS (status IN ('draft', 'active')) STORED);",
|
|
42
|
+
"rendered": "CREATE TABLE t (status varchar(20), ok boolean GENERATED ALWAYS AS (((status)::text = ANY ((ARRAY['draft'::character varying, 'active'::character varying])::text[]))) STORED);"
|
|
43
|
+
}
|
|
44
|
+
]
|