@owlmeans/postgres-resource 0.1.18-rc.42 → 0.1.18-rc.43
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/README.md +13 -13
- package/agent-meta/manifest.json +2 -2
- package/agent-meta/skills/postgres-resource/SKILL.md +1 -1
- package/build/consts.d.ts +9 -0
- package/build/consts.d.ts.map +1 -1
- package/build/consts.js +6 -0
- package/build/consts.js.map +1 -1
- package/build/declarations/types.d.ts +12 -0
- package/build/declarations/types.d.ts.map +1 -0
- package/build/declarations/types.js +2 -0
- package/build/declarations/types.js.map +1 -0
- package/build/declarations.d.ts +5 -1
- package/build/declarations.d.ts.map +1 -1
- package/build/declarations.js +25 -15
- package/build/declarations.js.map +1 -1
- package/build/errors.d.ts +0 -20
- package/build/errors.d.ts.map +1 -1
- package/build/errors.js +1 -111
- package/build/errors.js.map +1 -1
- package/build/index.d.ts +3 -0
- package/build/index.d.ts.map +1 -1
- package/build/index.js +1 -0
- package/build/index.js.map +1 -1
- package/build/pg-error/types.d.ts +24 -0
- package/build/pg-error/types.d.ts.map +1 -0
- package/build/pg-error/types.js +2 -0
- package/build/pg-error/types.js.map +1 -0
- package/build/pg-error.d.ts +4 -0
- package/build/pg-error.d.ts.map +1 -0
- package/build/pg-error.js +100 -0
- package/build/pg-error.js.map +1 -0
- package/build/resource.d.ts.map +1 -1
- package/build/resource.js +15 -11
- package/build/resource.js.map +1 -1
- package/build/types.d.ts.map +1 -1
- package/build/utils/consts.local.d.ts +2 -0
- package/build/utils/consts.local.d.ts.map +1 -1
- package/build/utils/consts.local.js +2 -0
- package/build/utils/consts.local.js.map +1 -1
- package/build/utils/criteria/types.d.ts +31 -0
- package/build/utils/criteria/types.d.ts.map +1 -0
- package/build/utils/criteria/types.js +2 -0
- package/build/utils/criteria/types.js.map +1 -0
- package/build/utils/criteria.d.ts +3 -27
- package/build/utils/criteria.d.ts.map +1 -1
- package/build/utils/criteria.js +86 -104
- package/build/utils/criteria.js.map +1 -1
- package/build/utils/diff/types.d.ts +40 -0
- package/build/utils/diff/types.d.ts.map +1 -0
- package/build/utils/diff/types.js +2 -0
- package/build/utils/diff/types.js.map +1 -0
- package/build/utils/diff.d.ts +5 -35
- package/build/utils/diff.d.ts.map +1 -1
- package/build/utils/diff.js +320 -344
- package/build/utils/diff.js.map +1 -1
- package/build/utils/index.d.ts +9 -0
- package/build/utils/index.d.ts.map +1 -1
- package/build/utils/index.js.map +1 -1
- package/build/utils/introspect/types.d.ts +9 -0
- package/build/utils/introspect/types.d.ts.map +1 -0
- package/build/utils/introspect/types.js +2 -0
- package/build/utils/introspect/types.js.map +1 -0
- package/build/utils/introspect.d.ts +3 -3
- package/build/utils/introspect.d.ts.map +1 -1
- package/build/utils/introspect.js +41 -38
- package/build/utils/introspect.js.map +1 -1
- package/build/utils/life-cycle/types.d.ts +37 -0
- package/build/utils/life-cycle/types.d.ts.map +1 -0
- package/build/utils/life-cycle/types.js +2 -0
- package/build/utils/life-cycle/types.js.map +1 -0
- package/build/utils/life-cycle.d.ts +3 -33
- package/build/utils/life-cycle.d.ts.map +1 -1
- package/build/utils/life-cycle.js +88 -108
- package/build/utils/life-cycle.js.map +1 -1
- package/build/utils/marshal/types.d.ts +39 -0
- package/build/utils/marshal/types.d.ts.map +1 -0
- package/build/utils/marshal/types.js +2 -0
- package/build/utils/marshal/types.js.map +1 -0
- package/build/utils/marshal.d.ts +4 -32
- package/build/utils/marshal.d.ts.map +1 -1
- package/build/utils/marshal.js +121 -144
- package/build/utils/marshal.js.map +1 -1
- package/build/utils/migrations.d.ts.map +1 -1
- package/build/utils/migrations.js +41 -36
- package/build/utils/migrations.js.map +1 -1
- package/build/utils/name/types.d.ts +43 -0
- package/build/utils/name/types.d.ts.map +1 -0
- package/build/utils/name/types.js +2 -0
- package/build/utils/name/types.js.map +1 -0
- package/build/utils/name.d.ts +6 -35
- package/build/utils/name.d.ts.map +1 -1
- package/build/utils/name.js +38 -57
- package/build/utils/name.js.map +1 -1
- package/build/utils/schema/types.d.ts +22 -0
- package/build/utils/schema/types.d.ts.map +1 -0
- package/build/utils/schema/types.js +2 -0
- package/build/utils/schema/types.js.map +1 -0
- package/build/utils/schema.d.ts +4 -24
- package/build/utils/schema.d.ts.map +1 -1
- package/build/utils/schema.js +339 -352
- package/build/utils/schema.js.map +1 -1
- package/build/utils/sql/types.d.ts +25 -0
- package/build/utils/sql/types.d.ts.map +1 -0
- package/build/utils/sql/types.js +2 -0
- package/build/utils/sql/types.js.map +1 -0
- package/build/utils/sql.d.ts +4 -18
- package/build/utils/sql.d.ts.map +1 -1
- package/build/utils/sql.js +0 -0
- package/build/utils/sql.js.map +1 -1
- package/build/utils/sync/types.d.ts +25 -0
- package/build/utils/sync/types.d.ts.map +1 -0
- package/build/utils/sync/types.js +2 -0
- package/build/utils/sync/types.js.map +1 -0
- package/build/utils/sync.d.ts +2 -19
- package/build/utils/sync.d.ts.map +1 -1
- package/build/utils/sync.js +64 -71
- package/build/utils/sync.js.map +1 -1
- package/package.json +7 -7
- package/src/consts.ts +7 -0
- package/src/declarations/types.ts +12 -0
- package/src/declarations.ts +29 -14
- package/src/errors.ts +1 -121
- package/src/index.ts +3 -0
- package/src/pg-error/types.ts +23 -0
- package/src/pg-error.ts +114 -0
- package/src/resource.ts +17 -13
- package/src/types.ts +1 -0
- package/src/utils/consts.local.ts +3 -0
- package/src/utils/criteria/types.ts +32 -0
- package/src/utils/criteria.ts +95 -110
- package/src/utils/diff/types.ts +41 -0
- package/src/utils/diff.ts +330 -348
- package/src/utils/index.ts +9 -0
- package/src/utils/introspect/types.ts +9 -0
- package/src/utils/introspect.ts +51 -45
- package/src/utils/life-cycle/types.ts +41 -0
- package/src/utils/life-cycle.ts +102 -121
- package/src/utils/marshal/types.ts +40 -0
- package/src/utils/marshal.ts +129 -147
- package/src/utils/migrations.ts +39 -33
- package/src/utils/name/types.ts +44 -0
- package/src/utils/name.ts +47 -60
- package/src/utils/schema/types.ts +26 -0
- package/src/utils/schema.ts +359 -366
- package/src/utils/sql/types.ts +25 -0
- package/src/utils/sql.ts +0 -0
- package/src/utils/sync/types.ts +25 -0
- package/src/utils/sync.ts +62 -68
- package/tests/criteria.spec.ts +4 -1
- package/tests/diff.spec.ts +4 -1
- package/tests/errors.spec.ts +4 -3
- package/tests/name.spec.ts +5 -3
- package/tests/schema.spec.ts +3 -1
- package/tests/sql.spec.ts +20 -22
package/build/utils/diff.js
CHANGED
|
@@ -1,386 +1,362 @@
|
|
|
1
1
|
import { PgIndexMethod } from '../consts.js';
|
|
2
2
|
import { PostgresPlaceholderError } from '../errors.js';
|
|
3
|
-
import {
|
|
3
|
+
import { pgNameHelper } from './name.js';
|
|
4
4
|
import { CAST } from './consts.local.js';
|
|
5
|
-
const
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
* `'active'::text`. Compare without them, so an unchanged default doesn't look like drift on
|
|
24
|
-
* every boot.
|
|
25
|
-
*/
|
|
26
|
-
const normalizeDefault = (expression) => {
|
|
27
|
-
if (expression == null)
|
|
28
|
-
return null;
|
|
5
|
+
export const createPgDiffHelper = () => {
|
|
6
|
+
const { qualify, quoteIdent } = pgNameHelper;
|
|
7
|
+
const literal = (value) => {
|
|
8
|
+
if (value === null)
|
|
9
|
+
return 'NULL';
|
|
10
|
+
if (typeof value === 'number')
|
|
11
|
+
return `${value}`;
|
|
12
|
+
if (typeof value === 'boolean')
|
|
13
|
+
return value ? 'TRUE' : 'FALSE';
|
|
14
|
+
return `'${value.replace(/'/g, "''")}'`;
|
|
15
|
+
};
|
|
16
|
+
const defaultExpression = (column) => {
|
|
17
|
+
if (column.defaultRaw != null)
|
|
18
|
+
return column.defaultRaw;
|
|
19
|
+
if (column.defaultLiteral !== undefined)
|
|
20
|
+
return literal(column.defaultLiteral);
|
|
21
|
+
return undefined;
|
|
22
|
+
};
|
|
29
23
|
/**
|
|
30
|
-
*
|
|
31
|
-
* `
|
|
24
|
+
* Postgres echoes defaults back with explicit casts — a declared `'active'` comes back as
|
|
25
|
+
* `'active'::text`. Compare without them, so an unchanged default doesn't look like drift on
|
|
26
|
+
* every boot.
|
|
32
27
|
*/
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
* each of its operator expressions in parentheses, and rewrites `x IN (…)` as `x = ANY (ARRAY[…])`.
|
|
42
|
-
* Comparing the raw texts made every boot drop and recreate every declared index and every enum
|
|
43
|
-
* `CHECK`. So both sides lose case, quotes, casts, whitespace and parentheses, and `= ANY (ARRAY[…])`
|
|
44
|
-
* reads as `IN (…)`. What that cannot tell apart — two definitions differing only in how their
|
|
45
|
-
* operators group — is a change to make under a new name.
|
|
46
|
-
*/
|
|
47
|
-
export const canonicalDefinition = (definition) => definition
|
|
48
|
-
.toLowerCase()
|
|
49
|
-
.replace(/"/g, '')
|
|
50
|
-
.replace(CAST, '')
|
|
51
|
-
.replace(/[\s()]/g, '')
|
|
52
|
-
.replace(/=anyarray\[([^\]]*)\]/g, 'in$1');
|
|
53
|
-
const normalizeDefinition = canonicalDefinition;
|
|
54
|
-
const columnList = (columns) => columns.map(quoteIdent).join(', ');
|
|
55
|
-
const indexStatement = (spec, entry) => {
|
|
56
|
-
const target = entry.expression != null
|
|
57
|
-
? `(${entry.expression})`
|
|
58
|
-
: `(${columnList(entry.columns)})`;
|
|
59
|
-
const where = entry.where != null ? ` WHERE ${entry.where}` : '';
|
|
60
|
-
return `CREATE ${entry.unique === true ? 'UNIQUE ' : ''}INDEX ${quoteIdent(entry.name)}`
|
|
61
|
-
+ ` ON ${spec.qualified} USING ${entry.method ?? PgIndexMethod.BTree} ${target}${where}`;
|
|
62
|
-
};
|
|
63
|
-
const columnDefinition = (column) => {
|
|
64
|
-
const parts = [quoteIdent(column.column), column.sqlType];
|
|
65
|
-
if (column.notNull)
|
|
66
|
-
parts.push('NOT NULL');
|
|
67
|
-
const expression = defaultExpression(column);
|
|
68
|
-
if (expression != null)
|
|
69
|
-
parts.push(`DEFAULT ${expression}`);
|
|
70
|
-
return parts.join(' ');
|
|
71
|
-
};
|
|
72
|
-
/**
|
|
73
|
-
* Foreign keys are planned separately from the rest of the table, and applied only once
|
|
74
|
-
* every resource has initialized: a key points at a table another resource owns, and that
|
|
75
|
-
* resource's own `init()` may not have run yet.
|
|
76
|
-
*
|
|
77
|
-
* @throws {PostgresPlaceholderError} when a target can't be resolved.
|
|
78
|
-
*/
|
|
79
|
-
export const planForeignKeys = (spec, context) => spec.references.map(reference => {
|
|
80
|
-
const column = reference.property != null ? spec.byProperty[reference.property] : undefined;
|
|
81
|
-
if (column == null) {
|
|
82
|
-
throw new PostgresPlaceholderError(`fk-unknown-property:${reference.property}`);
|
|
83
|
-
}
|
|
84
|
-
let targetSchema = reference.schema ?? spec.schema;
|
|
85
|
-
let targetTable = reference.table;
|
|
86
|
-
if (reference.resource != null) {
|
|
87
|
-
if (context == null) {
|
|
88
|
-
throw new PostgresPlaceholderError(`fk-no-context:${reference.resource}`);
|
|
89
|
-
}
|
|
90
|
-
let target;
|
|
91
|
-
try {
|
|
92
|
-
target = context.resource(reference.resource);
|
|
93
|
-
}
|
|
94
|
-
catch {
|
|
95
|
-
throw new PostgresPlaceholderError(`fk-unknown-resource:${reference.resource}`);
|
|
96
|
-
}
|
|
97
|
-
if (target.table == null) {
|
|
98
|
-
throw new PostgresPlaceholderError(`fk-uninitialized:${reference.resource}`);
|
|
99
|
-
}
|
|
100
|
-
targetSchema = target.table.schema;
|
|
101
|
-
targetTable = target.table.table;
|
|
102
|
-
}
|
|
103
|
-
if (targetTable == null) {
|
|
104
|
-
throw new PostgresPlaceholderError(`fk-no-target:${reference.property}`);
|
|
105
|
-
}
|
|
106
|
-
const actions = [
|
|
107
|
-
reference.onDelete != null ? ` ON DELETE ${reference.onDelete}` : '',
|
|
108
|
-
reference.onUpdate != null ? ` ON UPDATE ${reference.onUpdate}` : ''
|
|
109
|
-
].join('');
|
|
110
|
-
return {
|
|
111
|
-
kind: 'add-foreign-key',
|
|
112
|
-
target: reference.name,
|
|
113
|
-
sql: `ALTER TABLE ${spec.qualified} ADD CONSTRAINT ${quoteIdent(reference.name)}`
|
|
114
|
-
+ ` FOREIGN KEY (${quoteIdent(column.column)})`
|
|
115
|
-
+ ` REFERENCES ${qualify(targetSchema, targetTable)} (${quoteIdent(reference.column)})${actions}`
|
|
28
|
+
const normalizeDefault = (expression) => {
|
|
29
|
+
if (expression == null)
|
|
30
|
+
return null;
|
|
31
|
+
/**
|
|
32
|
+
* Casts anywhere (a `varchar` id's `gen_random_uuid()::text` is stored as
|
|
33
|
+
* `(gen_random_uuid())::character varying`) and the parentheses Postgres adds around them.
|
|
34
|
+
*/
|
|
35
|
+
return expression.toLowerCase().replace(CAST, '').replace(/[()]/g, '').replace(/\s+/g, ' ').trim();
|
|
116
36
|
};
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
37
|
+
const canonicalDefinition = (definition) => definition
|
|
38
|
+
.toLowerCase()
|
|
39
|
+
.replace(/"/g, '')
|
|
40
|
+
.replace(CAST, '')
|
|
41
|
+
.replace(/[\s()]/g, '')
|
|
42
|
+
.replace(/=anyarray\[([^\]]*)\]/g, 'in$1');
|
|
43
|
+
const normalizeDefinition = canonicalDefinition;
|
|
44
|
+
const columnList = (columns) => columns.map(quoteIdent).join(', ');
|
|
45
|
+
const indexStatement = (spec, entry) => {
|
|
46
|
+
const target = entry.expression != null
|
|
47
|
+
? `(${entry.expression})`
|
|
48
|
+
: `(${columnList(entry.columns)})`;
|
|
49
|
+
const where = entry.where != null ? ` WHERE ${entry.where}` : '';
|
|
50
|
+
return `CREATE ${entry.unique === true ? 'UNIQUE ' : ''}INDEX ${quoteIdent(entry.name)}`
|
|
51
|
+
+ ` ON ${spec.qualified} USING ${entry.method ?? PgIndexMethod.BTree} ${target}${where}`;
|
|
52
|
+
};
|
|
53
|
+
const columnDefinition = (column) => {
|
|
54
|
+
const parts = [quoteIdent(column.column), column.sqlType];
|
|
55
|
+
if (column.notNull)
|
|
56
|
+
parts.push('NOT NULL');
|
|
57
|
+
const expression = defaultExpression(column);
|
|
58
|
+
if (expression != null)
|
|
59
|
+
parts.push(`DEFAULT ${expression}`);
|
|
60
|
+
return parts.join(' ');
|
|
61
|
+
};
|
|
62
|
+
const planForeignKeys = (spec, context) => spec.references.map(reference => {
|
|
63
|
+
const column = reference.property != null ? spec.byProperty[reference.property] : undefined;
|
|
64
|
+
if (column == null) {
|
|
65
|
+
throw new PostgresPlaceholderError(`fk-unknown-property:${reference.property}`);
|
|
135
66
|
}
|
|
136
|
-
|
|
137
|
-
|
|
67
|
+
let targetSchema = reference.schema ?? spec.schema;
|
|
68
|
+
let targetTable = reference.table;
|
|
69
|
+
if (reference.resource != null) {
|
|
70
|
+
if (context == null) {
|
|
71
|
+
throw new PostgresPlaceholderError(`fk-no-context:${reference.resource}`);
|
|
72
|
+
}
|
|
73
|
+
let target;
|
|
74
|
+
try {
|
|
75
|
+
target = context.resource(reference.resource);
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
throw new PostgresPlaceholderError(`fk-unknown-resource:${reference.resource}`);
|
|
79
|
+
}
|
|
80
|
+
if (target.table == null) {
|
|
81
|
+
throw new PostgresPlaceholderError(`fk-uninitialized:${reference.resource}`);
|
|
82
|
+
}
|
|
83
|
+
targetSchema = target.table.schema;
|
|
84
|
+
targetTable = target.table.table;
|
|
138
85
|
}
|
|
139
|
-
|
|
140
|
-
|
|
86
|
+
if (targetTable == null) {
|
|
87
|
+
throw new PostgresPlaceholderError(`fk-no-target:${reference.property}`);
|
|
141
88
|
}
|
|
142
|
-
const
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
89
|
+
const actions = [
|
|
90
|
+
reference.onDelete != null ? ` ON DELETE ${reference.onDelete}` : '',
|
|
91
|
+
reference.onUpdate != null ? ` ON UPDATE ${reference.onUpdate}` : ''
|
|
92
|
+
].join('');
|
|
93
|
+
return {
|
|
94
|
+
kind: 'add-foreign-key',
|
|
95
|
+
target: reference.name,
|
|
96
|
+
sql: `ALTER TABLE ${spec.qualified} ADD CONSTRAINT ${quoteIdent(reference.name)}`
|
|
97
|
+
+ ` FOREIGN KEY (${quoteIdent(column.column)})`
|
|
98
|
+
+ ` REFERENCES ${qualify(targetSchema, targetTable)} (${quoteIdent(reference.column)})${actions}`
|
|
99
|
+
};
|
|
100
|
+
});
|
|
101
|
+
const planSync = (spec, live, additive = false) => {
|
|
102
|
+
if (!live.exists) {
|
|
103
|
+
const constraints = [];
|
|
104
|
+
if (spec.primaryKey.length > 0) {
|
|
105
|
+
constraints.push(`CONSTRAINT ${quoteIdent(`${spec.table}_pkey`)} PRIMARY KEY (${columnList(spec.primaryKey)})`);
|
|
106
|
+
}
|
|
107
|
+
for (const unique of spec.uniques) {
|
|
108
|
+
constraints.push(`CONSTRAINT ${quoteIdent(unique.name)} UNIQUE (${columnList(unique.columns)})`);
|
|
109
|
+
}
|
|
110
|
+
for (const check of spec.checks) {
|
|
111
|
+
constraints.push(`CONSTRAINT ${quoteIdent(check.name)} CHECK (${check.expression})`);
|
|
112
|
+
}
|
|
113
|
+
const statements = [{
|
|
114
|
+
kind: 'create-table',
|
|
115
|
+
target: spec.table,
|
|
116
|
+
sql: `CREATE TABLE ${spec.qualified} (\n `
|
|
117
|
+
+ [...spec.columns.map(columnDefinition), ...constraints].join(',\n ')
|
|
118
|
+
+ '\n)'
|
|
119
|
+
}];
|
|
120
|
+
for (const entry of spec.indexes) {
|
|
121
|
+
statements.push({ kind: 'create-index', target: entry.name, sql: indexStatement(spec, entry) });
|
|
122
|
+
}
|
|
123
|
+
return { fresh: true, statements };
|
|
151
124
|
}
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
for (const column of live.columns) {
|
|
157
|
-
byName[column.name] = column;
|
|
158
|
-
}
|
|
159
|
-
const managed = spec.columns.filter(column => column.managed);
|
|
160
|
-
/** 1. Columns the table doesn't have yet — always added nullable so existing rows survive. */
|
|
161
|
-
for (const column of managed) {
|
|
162
|
-
if (byName[column.column] != null) {
|
|
163
|
-
continue;
|
|
125
|
+
const statements = [];
|
|
126
|
+
const byName = {};
|
|
127
|
+
for (const column of live.columns) {
|
|
128
|
+
byName[column.name] = column;
|
|
164
129
|
}
|
|
165
|
-
const
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
+ (expression != null ? ` DEFAULT ${expression}` : '')
|
|
171
|
-
});
|
|
172
|
-
if (column.notNull) {
|
|
173
|
-
if (expression == null) {
|
|
174
|
-
statements.push({
|
|
175
|
-
kind: 'backfill',
|
|
176
|
-
target: column.column,
|
|
177
|
-
sql: `UPDATE ${spec.qualified} SET ${quoteIdent(column.column)} = ${fallback(column)}`
|
|
178
|
-
+ ` WHERE ${quoteIdent(column.column)} IS NULL`
|
|
179
|
-
});
|
|
130
|
+
const managed = spec.columns.filter(column => column.managed);
|
|
131
|
+
/** 1. Columns the table doesn't have yet — always added nullable so existing rows survive. */
|
|
132
|
+
for (const column of managed) {
|
|
133
|
+
if (byName[column.column] != null) {
|
|
134
|
+
continue;
|
|
180
135
|
}
|
|
136
|
+
const expression = defaultExpression(column);
|
|
181
137
|
statements.push({
|
|
182
|
-
kind: '
|
|
138
|
+
kind: 'add-column',
|
|
183
139
|
target: column.column,
|
|
184
|
-
sql: `ALTER TABLE ${spec.qualified}
|
|
140
|
+
sql: `ALTER TABLE ${spec.qualified} ADD COLUMN ${quoteIdent(column.column)} ${column.sqlType}`
|
|
141
|
+
+ (expression != null ? ` DEFAULT ${expression}` : '')
|
|
185
142
|
});
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
* A retype drops the column's default first.
|
|
196
|
-
*
|
|
197
|
-
* Postgres refuses `ALTER COLUMN … TYPE` outright when the column carries a DEFAULT it cannot
|
|
198
|
-
* cast to the new type — `42804 default for column "x" cannot be cast automatically` — and it
|
|
199
|
-
* refuses it before looking at a single row, so the `USING` clause never gets a chance to
|
|
200
|
-
* help. The whole plan runs in one transaction, so that refusal aborts the entire
|
|
201
|
-
* reconciliation: the table keeps its old shape and the resource fails every boot with an
|
|
202
|
-
* error about a column it is trying to fix. The standard remedy is to take the default out of
|
|
203
|
-
* the way, retype, and put it back — which is what the default-drift block below does, from
|
|
204
|
-
* the spec rather than from whatever the column happened to be carrying.
|
|
205
|
-
*
|
|
206
|
-
* The `id` column is the one that meets this in practice: it is created with a
|
|
207
|
-
* `gen_random_uuid()::text` default, so any change to its declared type hits this path.
|
|
208
|
-
*/
|
|
209
|
-
let defaultDropped = false;
|
|
210
|
-
if (!additive && existing.type !== column.sqlType) {
|
|
211
|
-
if (existing.defaultExpr != null) {
|
|
143
|
+
if (column.notNull) {
|
|
144
|
+
if (expression == null) {
|
|
145
|
+
statements.push({
|
|
146
|
+
kind: 'backfill',
|
|
147
|
+
target: column.column,
|
|
148
|
+
sql: `UPDATE ${spec.qualified} SET ${quoteIdent(column.column)} = ${fallback(column)}`
|
|
149
|
+
+ ` WHERE ${quoteIdent(column.column)} IS NULL`
|
|
150
|
+
});
|
|
151
|
+
}
|
|
212
152
|
statements.push({
|
|
213
|
-
kind: 'set-
|
|
153
|
+
kind: 'set-not-null',
|
|
214
154
|
target: column.column,
|
|
215
|
-
sql: `ALTER TABLE ${spec.qualified} ALTER COLUMN ${quoteIdent(column.column)}
|
|
155
|
+
sql: `ALTER TABLE ${spec.qualified} ALTER COLUMN ${quoteIdent(column.column)} SET NOT NULL`
|
|
216
156
|
});
|
|
217
|
-
defaultDropped = true;
|
|
218
157
|
}
|
|
219
|
-
const using = column.using ?? `${quoteIdent(column.column)}::${column.sqlType}`;
|
|
220
|
-
statements.push({
|
|
221
|
-
kind: 'alter-type',
|
|
222
|
-
target: column.column,
|
|
223
|
-
sql: `ALTER TABLE ${spec.qualified} ALTER COLUMN ${quoteIdent(column.column)}`
|
|
224
|
-
+ ` TYPE ${column.sqlType} USING ${using}`
|
|
225
|
-
});
|
|
226
158
|
}
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
159
|
+
/** 2-4. Type, nullability and default drift on columns that already exist. */
|
|
160
|
+
for (const column of managed) {
|
|
161
|
+
const existing = byName[column.column];
|
|
162
|
+
if (existing == null) {
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* A retype drops the column's default first.
|
|
167
|
+
*
|
|
168
|
+
* Postgres refuses `ALTER COLUMN … TYPE` outright when the column carries a DEFAULT it cannot
|
|
169
|
+
* cast to the new type — `42804 default for column "x" cannot be cast automatically` — and it
|
|
170
|
+
* refuses it before looking at a single row, so the `USING` clause never gets a chance to
|
|
171
|
+
* help. The whole plan runs in one transaction, so that refusal aborts the entire
|
|
172
|
+
* reconciliation: the table keeps its old shape and the resource fails every boot with an
|
|
173
|
+
* error about a column it is trying to fix. The standard remedy is to take the default out of
|
|
174
|
+
* the way, retype, and put it back — which is what the default-drift block below does, from
|
|
175
|
+
* the spec rather than from whatever the column happened to be carrying.
|
|
176
|
+
*
|
|
177
|
+
* The `id` column is the one that meets this in practice: it is created with a
|
|
178
|
+
* `gen_random_uuid()::text` default, so any change to its declared type hits this path.
|
|
179
|
+
*/
|
|
180
|
+
let defaultDropped = false;
|
|
181
|
+
if (!additive && existing.type !== column.sqlType) {
|
|
182
|
+
if (existing.defaultExpr != null) {
|
|
183
|
+
statements.push({
|
|
184
|
+
kind: 'set-default',
|
|
185
|
+
target: column.column,
|
|
186
|
+
sql: `ALTER TABLE ${spec.qualified} ALTER COLUMN ${quoteIdent(column.column)} DROP DEFAULT`
|
|
187
|
+
});
|
|
188
|
+
defaultDropped = true;
|
|
241
189
|
}
|
|
242
|
-
|
|
243
|
-
|
|
190
|
+
const using = column.using ?? `${quoteIdent(column.column)}::${column.sqlType}`;
|
|
191
|
+
statements.push({
|
|
192
|
+
kind: 'alter-type',
|
|
244
193
|
target: column.column,
|
|
245
|
-
sql: `ALTER TABLE ${spec.qualified} ALTER COLUMN ${quoteIdent(column.column)}
|
|
194
|
+
sql: `ALTER TABLE ${spec.qualified} ALTER COLUMN ${quoteIdent(column.column)}`
|
|
195
|
+
+ ` TYPE ${column.sqlType} USING ${using}`
|
|
246
196
|
});
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
197
|
+
}
|
|
198
|
+
const expression = defaultExpression(column);
|
|
199
|
+
// Once the retype has dropped it, the default has to be re-stated even when it matches what
|
|
200
|
+
// the column used to carry: the comparison is against the pre-plan introspection, which stops
|
|
201
|
+
// describing the column the moment the plan drops its default.
|
|
202
|
+
const defaultDrifted = defaultDropped
|
|
203
|
+
? expression != null
|
|
204
|
+
: normalizeDefault(expression) !== normalizeDefault(existing.defaultExpr);
|
|
205
|
+
if (defaultDrifted) {
|
|
206
|
+
statements.push(expression != null
|
|
207
|
+
? {
|
|
208
|
+
kind: 'set-default',
|
|
209
|
+
target: column.column,
|
|
210
|
+
sql: `ALTER TABLE ${spec.qualified} ALTER COLUMN ${quoteIdent(column.column)}`
|
|
211
|
+
+ ` SET DEFAULT ${expression}`
|
|
212
|
+
}
|
|
213
|
+
: {
|
|
214
|
+
kind: 'set-default',
|
|
215
|
+
target: column.column,
|
|
216
|
+
sql: `ALTER TABLE ${spec.qualified} ALTER COLUMN ${quoteIdent(column.column)} DROP DEFAULT`
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
if (column.notNull !== existing.notNull) {
|
|
220
|
+
if (column.notNull) {
|
|
221
|
+
statements.push({
|
|
222
|
+
kind: 'backfill',
|
|
223
|
+
target: column.column,
|
|
224
|
+
sql: `UPDATE ${spec.qualified} SET ${quoteIdent(column.column)}`
|
|
225
|
+
+ ` = ${expression ?? fallback(column)} WHERE ${quoteIdent(column.column)} IS NULL`
|
|
226
|
+
});
|
|
227
|
+
}
|
|
250
228
|
statements.push({
|
|
251
|
-
kind: '
|
|
229
|
+
kind: column.notNull ? 'set-not-null' : 'drop-not-null',
|
|
252
230
|
target: column.column,
|
|
253
|
-
sql: `
|
|
254
|
-
+ `
|
|
231
|
+
sql: `ALTER TABLE ${spec.qualified} ALTER COLUMN ${quoteIdent(column.column)}`
|
|
232
|
+
+ ` ${column.notNull ? 'SET' : 'DROP'} NOT NULL`
|
|
255
233
|
});
|
|
256
234
|
}
|
|
257
|
-
statements.push({
|
|
258
|
-
kind: column.notNull ? 'set-not-null' : 'drop-not-null',
|
|
259
|
-
target: column.column,
|
|
260
|
-
sql: `ALTER TABLE ${spec.qualified} ALTER COLUMN ${quoteIdent(column.column)}`
|
|
261
|
-
+ ` ${column.notNull ? 'SET' : 'DROP'} NOT NULL`
|
|
262
|
-
});
|
|
263
235
|
}
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
desiredConstraints[unique.name] = `UNIQUE (${columnList(unique.columns)})`;
|
|
269
|
-
}
|
|
270
|
-
for (const check of spec.checks) {
|
|
271
|
-
desiredConstraints[check.name] = `CHECK ((${check.expression}))`;
|
|
272
|
-
}
|
|
273
|
-
const liveConstraints = {};
|
|
274
|
-
for (const constraint of live.constraints) {
|
|
275
|
-
// Primary and foreign keys have their own paths. A NOT NULL (`n`) is a constraint row since
|
|
276
|
-
// Postgres 18 (`<table>_<column>_not_null`) and is owned by the column step above: dropping it by
|
|
277
|
-
// name is refused on a primary key column, and on any other it would undo a `required` property.
|
|
278
|
-
if (constraint.type === 'p' || constraint.type === 'f' || constraint.type === 'n') {
|
|
279
|
-
continue;
|
|
280
|
-
}
|
|
281
|
-
liveConstraints[constraint.name] = constraint.definition;
|
|
282
|
-
}
|
|
283
|
-
for (const [name, definition] of Object.entries(liveConstraints)) {
|
|
284
|
-
const desired = desiredConstraints[name];
|
|
285
|
-
if (desired != null && normalizeDefinition(desired) === normalizeDefinition(definition)) {
|
|
286
|
-
continue;
|
|
287
|
-
}
|
|
288
|
-
if (desired == null && additive) {
|
|
289
|
-
continue;
|
|
290
|
-
}
|
|
291
|
-
if (desired == null && !isGenerated(name, spec)) {
|
|
292
|
-
/** A constraint nobody declared was added by hand — leave it alone. */
|
|
293
|
-
continue;
|
|
294
|
-
}
|
|
295
|
-
statements.push({
|
|
296
|
-
kind: 'drop-constraint',
|
|
297
|
-
target: name,
|
|
298
|
-
sql: `ALTER TABLE ${spec.qualified} DROP CONSTRAINT ${quoteIdent(name)}`
|
|
299
|
-
});
|
|
300
|
-
}
|
|
301
|
-
for (const [name, definition] of Object.entries(desiredConstraints)) {
|
|
302
|
-
const existing = liveConstraints[name];
|
|
303
|
-
if (existing != null && normalizeDefinition(existing) === normalizeDefinition(definition)) {
|
|
304
|
-
continue;
|
|
305
|
-
}
|
|
306
|
-
if (existing != null && additive) {
|
|
307
|
-
continue;
|
|
236
|
+
/** 5. Unique and check constraints. Primary keys are deliberately excluded. */
|
|
237
|
+
const desiredConstraints = {};
|
|
238
|
+
for (const unique of spec.uniques) {
|
|
239
|
+
desiredConstraints[unique.name] = `UNIQUE (${columnList(unique.columns)})`;
|
|
308
240
|
}
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
target: name,
|
|
312
|
-
sql: `ALTER TABLE ${spec.qualified} ADD CONSTRAINT ${quoteIdent(name)} ${definition}`
|
|
313
|
-
});
|
|
314
|
-
}
|
|
315
|
-
/** 6. Indexes. Constraint backed indexes are owned by their constraint, not by us. */
|
|
316
|
-
const constraintBacked = live.constraints.map(constraint => constraint.name);
|
|
317
|
-
const desiredIndexes = {};
|
|
318
|
-
for (const entry of spec.indexes) {
|
|
319
|
-
desiredIndexes[entry.name] = indexStatement(spec, entry);
|
|
320
|
-
}
|
|
321
|
-
for (const existing of live.indexes) {
|
|
322
|
-
if (constraintBacked.includes(existing.name)) {
|
|
323
|
-
continue;
|
|
241
|
+
for (const check of spec.checks) {
|
|
242
|
+
desiredConstraints[check.name] = `CHECK ((${check.expression}))`;
|
|
324
243
|
}
|
|
325
|
-
const
|
|
326
|
-
|
|
327
|
-
|
|
244
|
+
const liveConstraints = {};
|
|
245
|
+
for (const constraint of live.constraints) {
|
|
246
|
+
// Primary and foreign keys have their own paths. A NOT NULL (`n`) is a constraint row since
|
|
247
|
+
// Postgres 18 (`<table>_<column>_not_null`) and is owned by the column step above: dropping it by
|
|
248
|
+
// name is refused on a primary key column, and on any other it would undo a `required` property.
|
|
249
|
+
if (constraint.type === 'p' || constraint.type === 'f' || constraint.type === 'n') {
|
|
250
|
+
continue;
|
|
251
|
+
}
|
|
252
|
+
liveConstraints[constraint.name] = constraint.definition;
|
|
328
253
|
}
|
|
329
|
-
|
|
330
|
-
|
|
254
|
+
for (const [name, definition] of Object.entries(liveConstraints)) {
|
|
255
|
+
const desired = desiredConstraints[name];
|
|
256
|
+
if (desired != null && normalizeDefinition(desired) === normalizeDefinition(definition)) {
|
|
257
|
+
continue;
|
|
258
|
+
}
|
|
259
|
+
if (desired == null && additive) {
|
|
260
|
+
continue;
|
|
261
|
+
}
|
|
262
|
+
if (desired == null && !isGenerated(name, spec)) {
|
|
263
|
+
/** A constraint nobody declared was added by hand — leave it alone. */
|
|
264
|
+
continue;
|
|
265
|
+
}
|
|
266
|
+
statements.push({
|
|
267
|
+
kind: 'drop-constraint',
|
|
268
|
+
target: name,
|
|
269
|
+
sql: `ALTER TABLE ${spec.qualified} DROP CONSTRAINT ${quoteIdent(name)}`
|
|
270
|
+
});
|
|
331
271
|
}
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
272
|
+
for (const [name, definition] of Object.entries(desiredConstraints)) {
|
|
273
|
+
const existing = liveConstraints[name];
|
|
274
|
+
if (existing != null && normalizeDefinition(existing) === normalizeDefinition(definition)) {
|
|
275
|
+
continue;
|
|
276
|
+
}
|
|
277
|
+
if (existing != null && additive) {
|
|
278
|
+
continue;
|
|
279
|
+
}
|
|
280
|
+
statements.push({
|
|
281
|
+
kind: 'add-constraint',
|
|
282
|
+
target: name,
|
|
283
|
+
sql: `ALTER TABLE ${spec.qualified} ADD CONSTRAINT ${quoteIdent(name)} ${definition}`
|
|
284
|
+
});
|
|
342
285
|
}
|
|
343
|
-
|
|
344
|
-
|
|
286
|
+
/** 6. Indexes. Constraint backed indexes are owned by their constraint, not by us. */
|
|
287
|
+
const constraintBacked = live.constraints.map(constraint => constraint.name);
|
|
288
|
+
const desiredIndexes = {};
|
|
289
|
+
for (const entry of spec.indexes) {
|
|
290
|
+
desiredIndexes[entry.name] = indexStatement(spec, entry);
|
|
345
291
|
}
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
292
|
+
for (const existing of live.indexes) {
|
|
293
|
+
if (constraintBacked.includes(existing.name)) {
|
|
294
|
+
continue;
|
|
295
|
+
}
|
|
296
|
+
const desired = desiredIndexes[existing.name];
|
|
297
|
+
if (desired != null && normalizeDefinition(desired) === normalizeDefinition(existing.definition)) {
|
|
298
|
+
continue;
|
|
299
|
+
}
|
|
300
|
+
if (desired == null && (additive || !isGenerated(existing.name, spec))) {
|
|
353
301
|
continue;
|
|
354
302
|
}
|
|
355
303
|
statements.push({
|
|
356
|
-
kind: 'drop-
|
|
304
|
+
kind: 'drop-index',
|
|
357
305
|
target: existing.name,
|
|
358
|
-
|
|
359
|
-
sql: `ALTER TABLE ${spec.qualified} DROP COLUMN ${quoteIdent(existing.name)}`
|
|
306
|
+
sql: `DROP INDEX ${qualify(spec.schema, existing.name)}`
|
|
360
307
|
});
|
|
361
308
|
}
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
309
|
+
for (const [name, statement] of Object.entries(desiredIndexes)) {
|
|
310
|
+
const existing = live.indexes.find(entry => entry.name === name);
|
|
311
|
+
if (existing != null && normalizeDefinition(existing.definition) === normalizeDefinition(statement)) {
|
|
312
|
+
continue;
|
|
313
|
+
}
|
|
314
|
+
if (constraintBacked.includes(name)) {
|
|
315
|
+
continue;
|
|
316
|
+
}
|
|
317
|
+
statements.push({ kind: 'create-index', target: name, sql: statement });
|
|
318
|
+
}
|
|
319
|
+
/** 7. Drops, last and only in full mode. */
|
|
320
|
+
if (!additive) {
|
|
321
|
+
const keep = [...managed.map(column => column.column), ...spec.unmanaged];
|
|
322
|
+
for (const existing of live.columns) {
|
|
323
|
+
if (keep.includes(existing.name) || existing.generated !== '') {
|
|
324
|
+
continue;
|
|
325
|
+
}
|
|
326
|
+
statements.push({
|
|
327
|
+
kind: 'drop-column',
|
|
328
|
+
target: existing.name,
|
|
329
|
+
destructive: true,
|
|
330
|
+
sql: `ALTER TABLE ${spec.qualified} DROP COLUMN ${quoteIdent(existing.name)}`
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
return { fresh: false, statements };
|
|
335
|
+
};
|
|
336
|
+
/** A name this package would have generated is a name this package may drop. */
|
|
337
|
+
const isGenerated = (name, spec) => name.startsWith(`${spec.table}_`);
|
|
338
|
+
/** Value used to fill an existing row before a column becomes `NOT NULL`. */
|
|
339
|
+
const fallback = (column) => {
|
|
340
|
+
if (column.array)
|
|
341
|
+
return `'{}'::${column.sqlType}`;
|
|
342
|
+
if (column.jsonb)
|
|
343
|
+
return `'{}'::jsonb`;
|
|
344
|
+
switch (column.jsonType) {
|
|
345
|
+
case 'number':
|
|
346
|
+
case 'integer':
|
|
347
|
+
case 'bigint':
|
|
348
|
+
return '0';
|
|
349
|
+
case 'boolean':
|
|
350
|
+
return 'FALSE';
|
|
351
|
+
case 'date':
|
|
352
|
+
return 'now()';
|
|
353
|
+
default:
|
|
354
|
+
return `''::${column.sqlType}`;
|
|
355
|
+
}
|
|
356
|
+
};
|
|
357
|
+
return { canonicalDefinition, planForeignKeys, planSync };
|
|
385
358
|
};
|
|
359
|
+
export const pgDiffHelper = createPgDiffHelper();
|
|
360
|
+
/** @deprecated compat:factory-refactor — use `pgDiffHelper.planSync(…)` */
|
|
361
|
+
export const planSync = (spec, live, additive = false) => pgDiffHelper.planSync(spec, live, additive);
|
|
386
362
|
//# sourceMappingURL=diff.js.map
|