@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.
Files changed (154) hide show
  1. package/README.md +13 -13
  2. package/agent-meta/manifest.json +2 -2
  3. package/agent-meta/skills/postgres-resource/SKILL.md +1 -1
  4. package/build/consts.d.ts +9 -0
  5. package/build/consts.d.ts.map +1 -1
  6. package/build/consts.js +6 -0
  7. package/build/consts.js.map +1 -1
  8. package/build/declarations/types.d.ts +12 -0
  9. package/build/declarations/types.d.ts.map +1 -0
  10. package/build/declarations/types.js +2 -0
  11. package/build/declarations/types.js.map +1 -0
  12. package/build/declarations.d.ts +5 -1
  13. package/build/declarations.d.ts.map +1 -1
  14. package/build/declarations.js +25 -15
  15. package/build/declarations.js.map +1 -1
  16. package/build/errors.d.ts +0 -20
  17. package/build/errors.d.ts.map +1 -1
  18. package/build/errors.js +1 -111
  19. package/build/errors.js.map +1 -1
  20. package/build/index.d.ts +3 -0
  21. package/build/index.d.ts.map +1 -1
  22. package/build/index.js +1 -0
  23. package/build/index.js.map +1 -1
  24. package/build/pg-error/types.d.ts +24 -0
  25. package/build/pg-error/types.d.ts.map +1 -0
  26. package/build/pg-error/types.js +2 -0
  27. package/build/pg-error/types.js.map +1 -0
  28. package/build/pg-error.d.ts +4 -0
  29. package/build/pg-error.d.ts.map +1 -0
  30. package/build/pg-error.js +100 -0
  31. package/build/pg-error.js.map +1 -0
  32. package/build/resource.d.ts.map +1 -1
  33. package/build/resource.js +15 -11
  34. package/build/resource.js.map +1 -1
  35. package/build/types.d.ts.map +1 -1
  36. package/build/utils/consts.local.d.ts +2 -0
  37. package/build/utils/consts.local.d.ts.map +1 -1
  38. package/build/utils/consts.local.js +2 -0
  39. package/build/utils/consts.local.js.map +1 -1
  40. package/build/utils/criteria/types.d.ts +31 -0
  41. package/build/utils/criteria/types.d.ts.map +1 -0
  42. package/build/utils/criteria/types.js +2 -0
  43. package/build/utils/criteria/types.js.map +1 -0
  44. package/build/utils/criteria.d.ts +3 -27
  45. package/build/utils/criteria.d.ts.map +1 -1
  46. package/build/utils/criteria.js +86 -104
  47. package/build/utils/criteria.js.map +1 -1
  48. package/build/utils/diff/types.d.ts +40 -0
  49. package/build/utils/diff/types.d.ts.map +1 -0
  50. package/build/utils/diff/types.js +2 -0
  51. package/build/utils/diff/types.js.map +1 -0
  52. package/build/utils/diff.d.ts +5 -35
  53. package/build/utils/diff.d.ts.map +1 -1
  54. package/build/utils/diff.js +320 -344
  55. package/build/utils/diff.js.map +1 -1
  56. package/build/utils/index.d.ts +9 -0
  57. package/build/utils/index.d.ts.map +1 -1
  58. package/build/utils/index.js.map +1 -1
  59. package/build/utils/introspect/types.d.ts +9 -0
  60. package/build/utils/introspect/types.d.ts.map +1 -0
  61. package/build/utils/introspect/types.js +2 -0
  62. package/build/utils/introspect/types.js.map +1 -0
  63. package/build/utils/introspect.d.ts +3 -3
  64. package/build/utils/introspect.d.ts.map +1 -1
  65. package/build/utils/introspect.js +41 -38
  66. package/build/utils/introspect.js.map +1 -1
  67. package/build/utils/life-cycle/types.d.ts +37 -0
  68. package/build/utils/life-cycle/types.d.ts.map +1 -0
  69. package/build/utils/life-cycle/types.js +2 -0
  70. package/build/utils/life-cycle/types.js.map +1 -0
  71. package/build/utils/life-cycle.d.ts +3 -33
  72. package/build/utils/life-cycle.d.ts.map +1 -1
  73. package/build/utils/life-cycle.js +88 -108
  74. package/build/utils/life-cycle.js.map +1 -1
  75. package/build/utils/marshal/types.d.ts +39 -0
  76. package/build/utils/marshal/types.d.ts.map +1 -0
  77. package/build/utils/marshal/types.js +2 -0
  78. package/build/utils/marshal/types.js.map +1 -0
  79. package/build/utils/marshal.d.ts +4 -32
  80. package/build/utils/marshal.d.ts.map +1 -1
  81. package/build/utils/marshal.js +121 -144
  82. package/build/utils/marshal.js.map +1 -1
  83. package/build/utils/migrations.d.ts.map +1 -1
  84. package/build/utils/migrations.js +41 -36
  85. package/build/utils/migrations.js.map +1 -1
  86. package/build/utils/name/types.d.ts +43 -0
  87. package/build/utils/name/types.d.ts.map +1 -0
  88. package/build/utils/name/types.js +2 -0
  89. package/build/utils/name/types.js.map +1 -0
  90. package/build/utils/name.d.ts +6 -35
  91. package/build/utils/name.d.ts.map +1 -1
  92. package/build/utils/name.js +38 -57
  93. package/build/utils/name.js.map +1 -1
  94. package/build/utils/schema/types.d.ts +22 -0
  95. package/build/utils/schema/types.d.ts.map +1 -0
  96. package/build/utils/schema/types.js +2 -0
  97. package/build/utils/schema/types.js.map +1 -0
  98. package/build/utils/schema.d.ts +4 -24
  99. package/build/utils/schema.d.ts.map +1 -1
  100. package/build/utils/schema.js +339 -352
  101. package/build/utils/schema.js.map +1 -1
  102. package/build/utils/sql/types.d.ts +25 -0
  103. package/build/utils/sql/types.d.ts.map +1 -0
  104. package/build/utils/sql/types.js +2 -0
  105. package/build/utils/sql/types.js.map +1 -0
  106. package/build/utils/sql.d.ts +4 -18
  107. package/build/utils/sql.d.ts.map +1 -1
  108. package/build/utils/sql.js +0 -0
  109. package/build/utils/sql.js.map +1 -1
  110. package/build/utils/sync/types.d.ts +25 -0
  111. package/build/utils/sync/types.d.ts.map +1 -0
  112. package/build/utils/sync/types.js +2 -0
  113. package/build/utils/sync/types.js.map +1 -0
  114. package/build/utils/sync.d.ts +2 -19
  115. package/build/utils/sync.d.ts.map +1 -1
  116. package/build/utils/sync.js +64 -71
  117. package/build/utils/sync.js.map +1 -1
  118. package/package.json +7 -7
  119. package/src/consts.ts +7 -0
  120. package/src/declarations/types.ts +12 -0
  121. package/src/declarations.ts +29 -14
  122. package/src/errors.ts +1 -121
  123. package/src/index.ts +3 -0
  124. package/src/pg-error/types.ts +23 -0
  125. package/src/pg-error.ts +114 -0
  126. package/src/resource.ts +17 -13
  127. package/src/types.ts +1 -0
  128. package/src/utils/consts.local.ts +3 -0
  129. package/src/utils/criteria/types.ts +32 -0
  130. package/src/utils/criteria.ts +95 -110
  131. package/src/utils/diff/types.ts +41 -0
  132. package/src/utils/diff.ts +330 -348
  133. package/src/utils/index.ts +9 -0
  134. package/src/utils/introspect/types.ts +9 -0
  135. package/src/utils/introspect.ts +51 -45
  136. package/src/utils/life-cycle/types.ts +41 -0
  137. package/src/utils/life-cycle.ts +102 -121
  138. package/src/utils/marshal/types.ts +40 -0
  139. package/src/utils/marshal.ts +129 -147
  140. package/src/utils/migrations.ts +39 -33
  141. package/src/utils/name/types.ts +44 -0
  142. package/src/utils/name.ts +47 -60
  143. package/src/utils/schema/types.ts +26 -0
  144. package/src/utils/schema.ts +359 -366
  145. package/src/utils/sql/types.ts +25 -0
  146. package/src/utils/sql.ts +0 -0
  147. package/src/utils/sync/types.ts +25 -0
  148. package/src/utils/sync.ts +62 -68
  149. package/tests/criteria.spec.ts +4 -1
  150. package/tests/diff.spec.ts +4 -1
  151. package/tests/errors.spec.ts +4 -3
  152. package/tests/name.spec.ts +5 -3
  153. package/tests/schema.spec.ts +3 -1
  154. package/tests/sql.spec.ts +20 -22
@@ -1,386 +1,362 @@
1
1
  import { PgIndexMethod } from '../consts.js';
2
2
  import { PostgresPlaceholderError } from '../errors.js';
3
- import { qualify, quoteIdent } from './name.js';
3
+ import { pgNameHelper } from './name.js';
4
4
  import { CAST } from './consts.local.js';
5
- const literal = (value) => {
6
- if (value === null)
7
- return 'NULL';
8
- if (typeof value === 'number')
9
- return `${value}`;
10
- if (typeof value === 'boolean')
11
- return value ? 'TRUE' : 'FALSE';
12
- return `'${value.replace(/'/g, "''")}'`;
13
- };
14
- const defaultExpression = (column) => {
15
- if (column.defaultRaw != null)
16
- return column.defaultRaw;
17
- if (column.defaultLiteral !== undefined)
18
- return literal(column.defaultLiteral);
19
- return undefined;
20
- };
21
- /**
22
- * Postgres echoes defaults back with explicit casts — a declared `'active'` comes back as
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
- * Casts anywhere (a `varchar` id's `gen_random_uuid()::text` is stored as
31
- * `(gen_random_uuid())::character varying`) and the parentheses Postgres adds around them.
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
- return expression.toLowerCase().replace(CAST, '').replace(/[()]/g, '').replace(/\s+/g, ' ').trim();
34
- };
35
- /**
36
- * The form an index or constraint definition is compared in, so the statement this package emits
37
- * and the text Postgres reports for the object it created compare equal.
38
- *
39
- * Postgres deparses what it stores: it leaves an identifier unquoted when it can, casts every
40
- * literal (`''::text`), casts a `varchar` column inside an expression, wraps a partial predicate and
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
- * Compare the compiled specification against the live table and produce the ordered DDL
120
- * that converges one onto the other.
121
- *
122
- * Statement order is load bearing:
123
- * - `DROP COLUMN` is last, because a `USING` cast or a backfill may legitimately read a
124
- * column that is about to disappear.
125
- * - A primary key constraint is never dropped: it cascades into every referencing key and
126
- * can't be undone inside the same transaction once dependents exist.
127
- *
128
- * Foreign keys are not part of the plan at all — see {@link planForeignKeys}.
129
- */
130
- export const planSync = (spec, live, additive = false) => {
131
- if (!live.exists) {
132
- const constraints = [];
133
- if (spec.primaryKey.length > 0) {
134
- constraints.push(`CONSTRAINT ${quoteIdent(`${spec.table}_pkey`)} PRIMARY KEY (${columnList(spec.primaryKey)})`);
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
- for (const unique of spec.uniques) {
137
- constraints.push(`CONSTRAINT ${quoteIdent(unique.name)} UNIQUE (${columnList(unique.columns)})`);
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
- for (const check of spec.checks) {
140
- constraints.push(`CONSTRAINT ${quoteIdent(check.name)} CHECK (${check.expression})`);
86
+ if (targetTable == null) {
87
+ throw new PostgresPlaceholderError(`fk-no-target:${reference.property}`);
141
88
  }
142
- const statements = [{
143
- kind: 'create-table',
144
- target: spec.table,
145
- sql: `CREATE TABLE ${spec.qualified} (\n `
146
- + [...spec.columns.map(columnDefinition), ...constraints].join(',\n ')
147
- + '\n)'
148
- }];
149
- for (const entry of spec.indexes) {
150
- statements.push({ kind: 'create-index', target: entry.name, sql: indexStatement(spec, entry) });
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
- return { fresh: true, statements };
153
- }
154
- const statements = [];
155
- const byName = {};
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 expression = defaultExpression(column);
166
- statements.push({
167
- kind: 'add-column',
168
- target: column.column,
169
- sql: `ALTER TABLE ${spec.qualified} ADD COLUMN ${quoteIdent(column.column)} ${column.sqlType}`
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: 'set-not-null',
138
+ kind: 'add-column',
183
139
  target: column.column,
184
- sql: `ALTER TABLE ${spec.qualified} ALTER COLUMN ${quoteIdent(column.column)} SET NOT NULL`
140
+ sql: `ALTER TABLE ${spec.qualified} ADD COLUMN ${quoteIdent(column.column)} ${column.sqlType}`
141
+ + (expression != null ? ` DEFAULT ${expression}` : '')
185
142
  });
186
- }
187
- }
188
- /** 2-4. Type, nullability and default drift on columns that already exist. */
189
- for (const column of managed) {
190
- const existing = byName[column.column];
191
- if (existing == null) {
192
- continue;
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-default',
153
+ kind: 'set-not-null',
214
154
  target: column.column,
215
- sql: `ALTER TABLE ${spec.qualified} ALTER COLUMN ${quoteIdent(column.column)} DROP DEFAULT`
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
- const expression = defaultExpression(column);
228
- // Once the retype has dropped it, the default has to be re-stated even when it matches what
229
- // the column used to carry: the comparison is against the pre-plan introspection, which stops
230
- // describing the column the moment the plan drops its default.
231
- const defaultDrifted = defaultDropped
232
- ? expression != null
233
- : normalizeDefault(expression) !== normalizeDefault(existing.defaultExpr);
234
- if (defaultDrifted) {
235
- statements.push(expression != null
236
- ? {
237
- kind: 'set-default',
238
- target: column.column,
239
- sql: `ALTER TABLE ${spec.qualified} ALTER COLUMN ${quoteIdent(column.column)}`
240
- + ` SET DEFAULT ${expression}`
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
- kind: 'set-default',
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)} DROP DEFAULT`
194
+ sql: `ALTER TABLE ${spec.qualified} ALTER COLUMN ${quoteIdent(column.column)}`
195
+ + ` TYPE ${column.sqlType} USING ${using}`
246
196
  });
247
- }
248
- if (column.notNull !== existing.notNull) {
249
- if (column.notNull) {
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: 'backfill',
229
+ kind: column.notNull ? 'set-not-null' : 'drop-not-null',
252
230
  target: column.column,
253
- sql: `UPDATE ${spec.qualified} SET ${quoteIdent(column.column)}`
254
- + ` = ${expression ?? fallback(column)} WHERE ${quoteIdent(column.column)} IS NULL`
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
- /** 5. Unique and check constraints. Primary keys are deliberately excluded. */
266
- const desiredConstraints = {};
267
- for (const unique of spec.uniques) {
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
- statements.push({
310
- kind: 'add-constraint',
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 desired = desiredIndexes[existing.name];
326
- if (desired != null && normalizeDefinition(desired) === normalizeDefinition(existing.definition)) {
327
- continue;
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
- if (desired == null && (additive || !isGenerated(existing.name, spec))) {
330
- continue;
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
- statements.push({
333
- kind: 'drop-index',
334
- target: existing.name,
335
- sql: `DROP INDEX ${qualify(spec.schema, existing.name)}`
336
- });
337
- }
338
- for (const [name, statement] of Object.entries(desiredIndexes)) {
339
- const existing = live.indexes.find(entry => entry.name === name);
340
- if (existing != null && normalizeDefinition(existing.definition) === normalizeDefinition(statement)) {
341
- continue;
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
- if (constraintBacked.includes(name)) {
344
- continue;
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
- statements.push({ kind: 'create-index', target: name, sql: statement });
347
- }
348
- /** 7. Drops, last and only in full mode. */
349
- if (!additive) {
350
- const keep = [...managed.map(column => column.column), ...spec.unmanaged];
351
- for (const existing of live.columns) {
352
- if (keep.includes(existing.name) || existing.generated !== '') {
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-column',
304
+ kind: 'drop-index',
357
305
  target: existing.name,
358
- destructive: true,
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
- return { fresh: false, statements };
364
- };
365
- /** A name this package would have generated is a name this package may drop. */
366
- const isGenerated = (name, spec) => name.startsWith(`${spec.table}_`);
367
- /** Value used to fill an existing row before a column becomes `NOT NULL`. */
368
- const fallback = (column) => {
369
- if (column.array)
370
- return `'{}'::${column.sqlType}`;
371
- if (column.jsonb)
372
- return `'{}'::jsonb`;
373
- switch (column.jsonType) {
374
- case 'number':
375
- case 'integer':
376
- case 'bigint':
377
- return '0';
378
- case 'boolean':
379
- return 'FALSE';
380
- case 'date':
381
- return 'now()';
382
- default:
383
- return `''::${column.sqlType}`;
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