@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/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,200 @@
1
- # Temporary Holding Version
1
+ # pg-conformance
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
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** — 4005 pairs, zero misses — 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 | cases | what it is for |
125
+ | --- | --- | --- |
126
+ | `objects` | 20 | creating each object kind from an empty schema |
127
+ | `hard-cases` | 90 | 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` | 12 | dependency ordering, with names chosen to defeat text matching |
129
+ | `migrations` | 52 | 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` | 6 | one schema written two ways — as a developer writes it and as PostgreSQL renders it — which a comparison must call identical |
131
+ | `data` | 12 | 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
+ `0.0.6` renamed the PHP namespace to `Akal\PgConformance`. A breaking change on
174
+ `0.x` should raise the minor, and that release was asked to — but the bump
175
+ detection read only the first line of the merge commit, which is
176
+ `Merge pull request …`, so the marker went unseen and a patch went out. The
177
+ detection is fixed; this is recorded because the version number cannot tell that
178
+ story on its own, and a reader wondering why a rename sits in a patch deserves
179
+ an answer.
180
+
181
+ Consumers pin through their lockfile as usual; a bot bumps that lockfile, so `npm ci` stays reproducible and still moves.
182
+
183
+ ## Local development
184
+
185
+ ```bash
186
+ npm link # in this repo
187
+ npm link @akal/pg-conformance # in the consumer
188
+ ```
189
+
190
+ Run the package's own tests against a real server:
191
+
192
+ ```bash
193
+ PGURL=postgresql://user:pass@127.0.0.1:5432/postgres npm test
194
+ ```
195
+
196
+ 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.
197
+
198
+ ## Licence
199
+
200
+ MIT
@@ -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
+ ]