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