@happyvertical/smrt-core 0.51.38 → 0.52.0
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/AGENTS.md +8 -0
- package/agents/schema-paths.md +90 -0
- package/dist/cascade.d.ts.map +1 -1
- package/dist/cascade.js +2 -1
- package/dist/cascade.js.map +1 -1
- package/dist/collection.d.ts +10 -1
- package/dist/collection.d.ts.map +1 -1
- package/dist/collection.js +14 -4
- package/dist/collection.js.map +1 -1
- package/dist/errors.d.ts +18 -0
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +21 -0
- package/dist/errors.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/manifest/static-manifest.d.ts.map +1 -1
- package/dist/manifest/static-manifest.js +9 -1
- package/dist/manifest/static-manifest.js.map +1 -1
- package/dist/manifest/store.js +1 -1
- package/dist/manifest/store.js.map +1 -1
- package/dist/manifest.json +9 -1
- package/dist/migrations/differ.d.ts +58 -0
- package/dist/migrations/differ.d.ts.map +1 -1
- package/dist/migrations/differ.js +187 -32
- package/dist/migrations/differ.js.map +1 -1
- package/dist/migrations/null-equal-indexes.js +1 -1
- package/dist/migrations/orchestrate.d.ts +7 -0
- package/dist/migrations/orchestrate.d.ts.map +1 -1
- package/dist/migrations/orchestrate.js +4 -2
- package/dist/migrations/orchestrate.js.map +1 -1
- package/dist/migrations/tracker.d.ts +29 -1
- package/dist/migrations/tracker.d.ts.map +1 -1
- package/dist/migrations/tracker.js +92 -7
- package/dist/migrations/tracker.js.map +1 -1
- package/dist/object.d.ts +128 -0
- package/dist/object.d.ts.map +1 -1
- package/dist/object.js +333 -44
- package/dist/object.js.map +1 -1
- package/dist/registry/schema-builder.d.ts.map +1 -1
- package/dist/registry/schema-builder.js +7 -5
- package/dist/registry/schema-builder.js.map +1 -1
- package/dist/registry.d.ts +68 -4
- package/dist/registry.d.ts.map +1 -1
- package/dist/registry.js +119 -8
- package/dist/registry.js.map +1 -1
- package/dist/scanner/index.d.ts +1 -1
- package/dist/scanner/index.d.ts.map +1 -1
- package/dist/scanner/manifest-generator.d.ts +3 -2
- package/dist/scanner/manifest-generator.d.ts.map +1 -1
- package/dist/scanner/manifest-generator.js +12 -8
- package/dist/scanner/manifest-generator.js.map +1 -1
- package/dist/scanner/types.d.ts +21 -0
- package/dist/scanner/types.d.ts.map +1 -1
- package/dist/scanner/types.js.map +1 -1
- package/dist/schema/conflict-target.d.ts +68 -11
- package/dist/schema/conflict-target.d.ts.map +1 -1
- package/dist/schema/conflict-target.js +68 -12
- package/dist/schema/conflict-target.js.map +1 -1
- package/dist/schema/foreign-key-ddl.d.ts +17 -1
- package/dist/schema/foreign-key-ddl.d.ts.map +1 -1
- package/dist/schema/foreign-key-ddl.js +29 -3
- package/dist/schema/foreign-key-ddl.js.map +1 -1
- package/dist/schema/foreign-key-policy.d.ts +8 -0
- package/dist/schema/foreign-key-policy.d.ts.map +1 -1
- package/dist/schema/foreign-key-policy.js +2 -1
- package/dist/schema/foreign-key-policy.js.map +1 -1
- package/dist/schema/generator.d.ts +7 -0
- package/dist/schema/generator.d.ts.map +1 -1
- package/dist/schema/generator.js +18 -10
- package/dist/schema/generator.js.map +1 -1
- package/dist/schema/text-cast-probe.d.ts +50 -1
- package/dist/schema/text-cast-probe.d.ts.map +1 -1
- package/dist/schema/text-cast-probe.js +46 -11
- package/dist/schema/text-cast-probe.js.map +1 -1
- package/dist/schema/types.d.ts +6 -0
- package/dist/schema/types.d.ts.map +1 -1
- package/dist/smrt-knowledge.json +12 -7
- package/package.json +10 -10
|
@@ -25,9 +25,22 @@ import type { DatabaseInterface } from '@happyvertical/sql';
|
|
|
25
25
|
/** Outcome of a server-side cast-safety probe over one column's non-null values. */
|
|
26
26
|
export type ShapeProbeResult = {
|
|
27
27
|
status: 'clean';
|
|
28
|
+
emptyCount?: number;
|
|
28
29
|
} | {
|
|
29
30
|
status: 'dirty';
|
|
31
|
+
/** Every non-null value that does not cast, empty text included. */
|
|
30
32
|
count: number;
|
|
33
|
+
/**
|
|
34
|
+
* How many of `count` are empty or whitespace-only text (#3226). When
|
|
35
|
+
* it equals `count` (and there is no `reason`), empty text is the only
|
|
36
|
+
* obstacle and the opt-in `emptyTextAsNull` conversion can store those
|
|
37
|
+
* values as NULL.
|
|
38
|
+
*/
|
|
39
|
+
emptyCount?: number;
|
|
40
|
+
/**
|
|
41
|
+
* One offending value, preferring a non-empty one; `''` when empty
|
|
42
|
+
* text is the only obstacle. Display it with {@link maskSampleValue}.
|
|
43
|
+
*/
|
|
31
44
|
sample?: string;
|
|
32
45
|
/**
|
|
33
46
|
* `duplicate_keys`: every value casts, but `count` JSON objects carry
|
|
@@ -49,6 +62,27 @@ export type ShapeProbeResult = {
|
|
|
49
62
|
* into logs/CI output (`created_at`/`_meta_data` values are user data).
|
|
50
63
|
*/
|
|
51
64
|
export declare function maskSampleValue(value: string): string;
|
|
65
|
+
/**
|
|
66
|
+
* SQL predicate: the text form of `column` is empty or whitespace-only
|
|
67
|
+
* (#3226). `column` must already be a quoted identifier.
|
|
68
|
+
*/
|
|
69
|
+
export declare function renderEmptyTextPredicate(column: string): string;
|
|
70
|
+
/**
|
|
71
|
+
* SQL expression: the text form of `column`, with empty or whitespace-only
|
|
72
|
+
* text as NULL (#3226). The cast target of the opt-in empty-text-as-null
|
|
73
|
+
* conversions. `column` must already be a quoted identifier.
|
|
74
|
+
*/
|
|
75
|
+
export declare function renderEmptyTextAsNullExpression(column: string): string;
|
|
76
|
+
/** Options for {@link probeCastSafety}. */
|
|
77
|
+
export interface ProbeCastSafetyOptions {
|
|
78
|
+
/**
|
|
79
|
+
* The caller may store empty text as NULL (#3226). For `jsonb`, a column
|
|
80
|
+
* whose only non-castable values are empty text then still gets the
|
|
81
|
+
* duplicate-key preservation walk over its remaining values, so the
|
|
82
|
+
* opt-in conversion is never offered on an unverified column.
|
|
83
|
+
*/
|
|
84
|
+
emptyTextAsNull?: boolean;
|
|
85
|
+
}
|
|
52
86
|
/**
|
|
53
87
|
* Probe every non-null value of a candidate `text` (or, for `jsonb`, native
|
|
54
88
|
* `json`) column for whether it can cast losslessly to `timestamptz` or
|
|
@@ -66,7 +100,17 @@ export declare function maskSampleValue(value: string): string;
|
|
|
66
100
|
*
|
|
67
101
|
* SMRT never coerces or discards data on an unproven assumption.
|
|
68
102
|
*/
|
|
69
|
-
export declare function probeCastSafety(db: DatabaseInterface, tableName: string, columnName: string, targetType: 'timestamptz' | 'jsonb'): Promise<ShapeProbeResult>;
|
|
103
|
+
export declare function probeCastSafety(db: DatabaseInterface, tableName: string, columnName: string, targetType: 'timestamptz' | 'jsonb', options?: ProbeCastSafetyOptions): Promise<ShapeProbeResult>;
|
|
104
|
+
/**
|
|
105
|
+
* Whether a probe found non-castable values that are all empty or
|
|
106
|
+
* whitespace-only text (#3226) -- the one shape the opt-in
|
|
107
|
+
* empty-text-as-null conversion can repair.
|
|
108
|
+
*/
|
|
109
|
+
export declare function isEmptyTextOnlyProbe(result: ShapeProbeResult | undefined): result is Extract<ShapeProbeResult, {
|
|
110
|
+
status: 'dirty';
|
|
111
|
+
}> & {
|
|
112
|
+
emptyCount: number;
|
|
113
|
+
};
|
|
70
114
|
/**
|
|
71
115
|
* Column-conversion options shared by {@link renderTimestamptzColumnConversion}
|
|
72
116
|
* and {@link renderJsonbColumnConversion}.
|
|
@@ -88,6 +132,11 @@ export declare function probeCastSafety(db: DatabaseInterface, tableName: string
|
|
|
88
132
|
interface ColumnConversionOptions {
|
|
89
133
|
hasLiveDefault?: boolean;
|
|
90
134
|
manifestDefaultValue?: unknown;
|
|
135
|
+
/**
|
|
136
|
+
* Store empty or whitespace-only text as NULL (#3226). Only for a
|
|
137
|
+
* nullable column whose probe found empty text as the sole obstacle.
|
|
138
|
+
*/
|
|
139
|
+
emptyTextAsNull?: boolean;
|
|
91
140
|
}
|
|
92
141
|
/**
|
|
93
142
|
* Render the one-time PostgreSQL conversion of a legacy `text` column to
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"text-cast-probe.d.ts","sourceRoot":"","sources":["../../src/schema/text-cast-probe.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AAG5D,oFAAoF;AACpF,MAAM,MAAM,gBAAgB,GACxB;IAAE,MAAM,EAAE,OAAO,CAAA;CAAE,
|
|
1
|
+
{"version":3,"file":"text-cast-probe.d.ts","sourceRoot":"","sources":["../../src/schema/text-cast-probe.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AAG5D,oFAAoF;AACpF,MAAM,MAAM,gBAAgB,GACxB;IAAE,MAAM,EAAE,OAAO,CAAC;IAAC,UAAU,CAAC,EAAE,MAAM,CAAA;CAAE,GACxC;IACE,MAAM,EAAE,OAAO,CAAC;IAChB,oEAAoE;IACpE,KAAK,EAAE,MAAM,CAAC;IACd;;;;;OAKG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB;;;OAGG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB;;;;;;OAMG;IACH,MAAM,CAAC,EAAE,gBAAgB,GAAG,yBAAyB,CAAC;IACtD,4EAA4E;IAC5E,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB,GACD;IAAE,MAAM,EAAE,aAAa,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC;AAE9C;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAKrD;AAED;;;GAGG;AACH,wBAAgB,wBAAwB,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAE/D;AAED;;;;GAIG;AACH,wBAAgB,+BAA+B,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAEtE;AAED,2CAA2C;AAC3C,MAAM,WAAW,sBAAsB;IACrC;;;;;OAKG;IACH,eAAe,CAAC,EAAE,OAAO,CAAC;CAC3B;AA4ID;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAsB,eAAe,CACnC,EAAE,EAAE,iBAAiB,EACrB,SAAS,EAAE,MAAM,EACjB,UAAU,EAAE,MAAM,EAClB,UAAU,EAAE,aAAa,GAAG,OAAO,EACnC,OAAO,GAAE,sBAA2B,GACnC,OAAO,CAAC,gBAAgB,CAAC,CAqB3B;AAED;;;;GAIG;AACH,wBAAgB,oBAAoB,CAClC,MAAM,EAAE,gBAAgB,GAAG,SAAS,GACnC,MAAM,IAAI,OAAO,CAAC,gBAAgB,EAAE;IAAE,MAAM,EAAE,OAAO,CAAA;CAAE,CAAC,GAAG;IAC5D,UAAU,EAAE,MAAM,CAAC;CACpB,CAQA;AA+JD;;;;;;;;;;;;;;;;;GAiBG;AACH,UAAU,uBAAuB;IAC/B,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,oBAAoB,CAAC,EAAE,OAAO,CAAC;IAC/B;;;OAGG;IACH,eAAe,CAAC,EAAE,OAAO,CAAC;CAC3B;AAED;;;;GAIG;AACH,wBAAgB,iCAAiC,CAC/C,SAAS,EAAE,MAAM,EACjB,UAAU,EAAE,MAAM,EAClB,OAAO,GAAE,uBAA4B,GACpC,MAAM,EAAE,CAyBV;AAED;;;;GAIG;AACH,wBAAgB,2BAA2B,CACzC,SAAS,EAAE,MAAM,EACjB,UAAU,EAAE,MAAM,EAClB,OAAO,GAAE,uBAA4B,GACpC,MAAM,EAAE,CAyBV"}
|
|
@@ -10,6 +10,21 @@ function maskSampleValue(value) {
|
|
|
10
10
|
if (value.length <= 6) return `${"•".repeat(value.length)} (length ${value.length})`;
|
|
11
11
|
return `${value.slice(0, 3)}…${value.slice(-3)} (length ${value.length})`;
|
|
12
12
|
}
|
|
13
|
+
/**
|
|
14
|
+
* SQL predicate: the text form of `column` is empty or whitespace-only
|
|
15
|
+
* (#3226). `column` must already be a quoted identifier.
|
|
16
|
+
*/
|
|
17
|
+
function renderEmptyTextPredicate(column) {
|
|
18
|
+
return `btrim(${column}::text, E' \\t\\n\\r') = ''`;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* SQL expression: the text form of `column`, with empty or whitespace-only
|
|
22
|
+
* text as NULL (#3226). The cast target of the opt-in empty-text-as-null
|
|
23
|
+
* conversions. `column` must already be a quoted identifier.
|
|
24
|
+
*/
|
|
25
|
+
function renderEmptyTextAsNullExpression(column) {
|
|
26
|
+
return `CASE WHEN ${renderEmptyTextPredicate(column)} THEN NULL ELSE ${column}::text END`;
|
|
27
|
+
}
|
|
13
28
|
/** Session-scoped helper function name; `pg_temp` keeps it off the real schema. */
|
|
14
29
|
var PROBE_FUNCTION = "pg_temp.smrt_probe_cast_ok";
|
|
15
30
|
/**
|
|
@@ -54,8 +69,7 @@ function renderCreateProbeFunctionSql() {
|
|
|
54
69
|
function renderProbeQuerySql(tableName, columnName, targetType) {
|
|
55
70
|
const column = quoteIdentifier(columnName);
|
|
56
71
|
const table = quoteIdentifier(tableName);
|
|
57
|
-
|
|
58
|
-
return `SELECT count(*) AS invalid_count, min(CASE WHEN NOT ${isValid} THEN ${column}::text END) AS sample_value FROM ${table} WHERE ${column} IS NOT NULL AND NOT ${isValid}`;
|
|
72
|
+
return `SELECT count(*) FILTER (WHERE probe.is_empty OR NOT probe.is_valid) AS invalid_count, count(*) FILTER (WHERE probe.is_empty) AS empty_count, min(probe.value) FILTER (WHERE NOT probe.is_empty AND NOT probe.is_valid) AS sample_value FROM (SELECT candidate.value, candidate.is_empty, CASE WHEN candidate.is_empty THEN true ELSE ${`${PROBE_FUNCTION}(candidate.value, '${targetType}')`} END AS is_valid FROM (SELECT ${column}::text AS value, ${renderEmptyTextPredicate(column)} AS is_empty FROM ${table} WHERE ${column} IS NOT NULL) AS candidate) AS probe`;
|
|
59
73
|
}
|
|
60
74
|
/**
|
|
61
75
|
* One set-based jsonb cast over every non-null value: `count(expr)` forces
|
|
@@ -73,11 +87,15 @@ function classifyProbeRows(rows) {
|
|
|
73
87
|
reason: "probe returned a non-numeric count"
|
|
74
88
|
};
|
|
75
89
|
if (count === 0) return { status: "clean" };
|
|
76
|
-
const
|
|
90
|
+
const rawEmptyCount = Number(rows[0]?.empty_count);
|
|
91
|
+
const emptyCount = rows[0]?.empty_count !== void 0 && Number.isFinite(rawEmptyCount) ? rawEmptyCount : void 0;
|
|
92
|
+
const rawSample = rows[0]?.sample_value;
|
|
93
|
+
const sample = typeof rawSample === "string" ? rawSample : emptyCount !== void 0 && emptyCount > 0 ? "" : void 0;
|
|
77
94
|
return {
|
|
78
95
|
status: "dirty",
|
|
79
96
|
count,
|
|
80
|
-
...
|
|
97
|
+
...emptyCount !== void 0 ? { emptyCount } : {},
|
|
98
|
+
...sample !== void 0 ? { sample } : {}
|
|
81
99
|
};
|
|
82
100
|
}
|
|
83
101
|
/**
|
|
@@ -97,10 +115,27 @@ function classifyProbeRows(rows) {
|
|
|
97
115
|
*
|
|
98
116
|
* SMRT never coerces or discards data on an unproven assumption.
|
|
99
117
|
*/
|
|
100
|
-
async function probeCastSafety(db, tableName, columnName, targetType) {
|
|
118
|
+
async function probeCastSafety(db, tableName, columnName, targetType, options = {}) {
|
|
101
119
|
const castResult = await probeCastOnly(db, tableName, columnName, targetType);
|
|
102
|
-
if (targetType !== "jsonb"
|
|
103
|
-
return probeJsonbKeyPreservation(db, tableName, columnName);
|
|
120
|
+
if (targetType !== "jsonb") return castResult;
|
|
121
|
+
if (castResult.status === "clean") return probeJsonbKeyPreservation(db, tableName, columnName);
|
|
122
|
+
if (options.emptyTextAsNull === true && isEmptyTextOnlyProbe(castResult)) {
|
|
123
|
+
const preservation = await probeJsonbKeyPreservation(db, tableName, columnName);
|
|
124
|
+
if (preservation.status === "dirty") return {
|
|
125
|
+
...preservation,
|
|
126
|
+
emptyCount: castResult.emptyCount
|
|
127
|
+
};
|
|
128
|
+
return castResult;
|
|
129
|
+
}
|
|
130
|
+
return castResult;
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Whether a probe found non-castable values that are all empty or
|
|
134
|
+
* whitespace-only text (#3226) -- the one shape the opt-in
|
|
135
|
+
* empty-text-as-null conversion can repair.
|
|
136
|
+
*/
|
|
137
|
+
function isEmptyTextOnlyProbe(result) {
|
|
138
|
+
return result?.status === "dirty" && result.reason === void 0 && result.emptyCount !== void 0 && result.emptyCount > 0 && result.emptyCount === result.count;
|
|
104
139
|
}
|
|
105
140
|
/**
|
|
106
141
|
* #3041 review finding: a successful jsonb cast proves castability, not
|
|
@@ -140,7 +175,7 @@ function renderDuplicateKeyQuerySql(tableName, columnName) {
|
|
|
140
175
|
const column = quoteIdentifier(columnName);
|
|
141
176
|
const table = quoteIdentifier(tableName);
|
|
142
177
|
const containers = "json_typeof(value) IN ('object', 'array')";
|
|
143
|
-
return `WITH RECURSIVE nodes(v) AS (SELECT (${column}::text)::json FROM ${table} WHERE ${column} IS NOT NULL AND json_typeof((${column}::text)::json) IN ('object', 'array') UNION ALL SELECT child.value FROM nodes CROSS JOIN LATERAL (SELECT value FROM json_each(CASE WHEN json_typeof(nodes.v) = 'object' THEN nodes.v ELSE '{}'::json END) WHERE ${containers} UNION ALL SELECT value FROM json_array_elements(CASE WHEN json_typeof(nodes.v) = 'array' THEN nodes.v ELSE '[]'::json END) WHERE ${containers}) AS child) SELECT count(*) AS invalid_count, min(v::text) AS sample_value FROM nodes WHERE json_typeof(v) = 'object' AND EXISTS (SELECT 1 FROM json_object_keys(v) AS k(key) GROUP BY k.key HAVING count(*) > 1)`;
|
|
178
|
+
return `WITH RECURSIVE nodes(v) AS (SELECT (${column}::text)::json FROM ${table} WHERE ${column} IS NOT NULL AND CASE WHEN ${renderEmptyTextPredicate(column)} THEN false ELSE json_typeof((${column}::text)::json) IN ('object', 'array') END UNION ALL SELECT child.value FROM nodes CROSS JOIN LATERAL (SELECT value FROM json_each(CASE WHEN json_typeof(nodes.v) = 'object' THEN nodes.v ELSE '{}'::json END) WHERE ${containers} UNION ALL SELECT value FROM json_array_elements(CASE WHEN json_typeof(nodes.v) = 'array' THEN nodes.v ELSE '[]'::json END) WHERE ${containers}) AS child) SELECT count(*) AS invalid_count, min(v::text) AS sample_value FROM nodes WHERE json_typeof(v) = 'object' AND EXISTS (SELECT 1 FROM json_object_keys(v) AS k(key) GROUP BY k.key HAVING count(*) > 1)`;
|
|
144
179
|
}
|
|
145
180
|
/**
|
|
146
181
|
* Upper bound for the preservation walk. A walk that does not finish (this
|
|
@@ -186,7 +221,7 @@ function renderTimestamptzColumnConversion(tableName, columnName, options = {})
|
|
|
186
221
|
const hasManifestDefault = options.manifestDefaultValue !== void 0;
|
|
187
222
|
const statements = [];
|
|
188
223
|
if (options.hasLiveDefault || hasManifestDefault) statements.push(`ALTER TABLE ${table} ALTER COLUMN ${column} DROP DEFAULT`);
|
|
189
|
-
statements.push(`ALTER TABLE ${table} ALTER COLUMN ${column} TYPE timestamptz USING ${column}::timestamptz`);
|
|
224
|
+
statements.push(`ALTER TABLE ${table} ALTER COLUMN ${column} TYPE timestamptz USING ${options.emptyTextAsNull ? `(${renderEmptyTextAsNullExpression(column)})` : column}::timestamptz`);
|
|
190
225
|
if (hasManifestDefault) {
|
|
191
226
|
const formattedDefault = formatDefaultValue(options.manifestDefaultValue, "TIMESTAMP");
|
|
192
227
|
statements.push(`ALTER TABLE ${table} ALTER COLUMN ${column} SET DEFAULT ${formattedDefault}`);
|
|
@@ -204,7 +239,7 @@ function renderJsonbColumnConversion(tableName, columnName, options = {}) {
|
|
|
204
239
|
const hasManifestDefault = options.manifestDefaultValue !== void 0;
|
|
205
240
|
const statements = [];
|
|
206
241
|
if (options.hasLiveDefault || hasManifestDefault) statements.push(`ALTER TABLE ${table} ALTER COLUMN ${column} DROP DEFAULT`);
|
|
207
|
-
statements.push(`ALTER TABLE ${table} ALTER COLUMN ${column} TYPE jsonb USING ${column}::jsonb`);
|
|
242
|
+
statements.push(`ALTER TABLE ${table} ALTER COLUMN ${column} TYPE jsonb USING ${options.emptyTextAsNull ? `(${renderEmptyTextAsNullExpression(column)})` : column}::jsonb`);
|
|
208
243
|
if (hasManifestDefault) {
|
|
209
244
|
const formattedDefault = formatDefaultValue(options.manifestDefaultValue, "JSON");
|
|
210
245
|
statements.push(`ALTER TABLE ${table} ALTER COLUMN ${column} SET DEFAULT ${formattedDefault}::jsonb`);
|
|
@@ -212,6 +247,6 @@ function renderJsonbColumnConversion(tableName, columnName, options = {}) {
|
|
|
212
247
|
return statements;
|
|
213
248
|
}
|
|
214
249
|
//#endregion
|
|
215
|
-
export { maskSampleValue, probeCastSafety, renderJsonbColumnConversion, renderTimestamptzColumnConversion };
|
|
250
|
+
export { isEmptyTextOnlyProbe, maskSampleValue, probeCastSafety, renderEmptyTextAsNullExpression, renderEmptyTextPredicate, renderJsonbColumnConversion, renderTimestamptzColumnConversion };
|
|
216
251
|
|
|
217
252
|
//# sourceMappingURL=text-cast-probe.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"text-cast-probe.js","names":[],"sources":["../../src/schema/text-cast-probe.ts"],"sourcesContent":["/**\n * Shared \"probe, then cast\" helpers for text-column type convergence (#2771,\n * #2772).\n *\n * Two drift shapes on a table SMRT itself creates share one problem: a live\n * `text` column holds values that would parse losslessly into the manifest's\n * declared PostgreSQL type (`timestamptz` or `jsonb`), but the differ cannot\n * know that without looking at the data. `probeCastSafety()` answers that with\n * a genuine, exception-safe cast attempt — not a shape heuristic — over every\n * non-null value, and returns one sample so a fail-closed diagnostic can name\n * the offending value without dumping the whole column into JS.\n *\n * PostgreSQL has no `TRY_CAST`/safe-cast builtin before 17 (`pg_input_is_valid`),\n * and this fleet targets 16 (see `.github/workflows/postgres-tests.yml`), so a\n * real per-row safe cast needs PL/pgSQL's exception handling. That handler is\n * created as a session-scoped (`pg_temp`) function, which is why the create\n * statement and the probe query run inside one `db.transaction()` — the two\n * must land on the same physical connection, and a pooled adapter only\n * guarantees that within one transaction. `target_type` is never derived from\n * column data (always the literal `'jsonb'`/`'timestamptz'` this module\n * passes), so the dynamic `EXECUTE format(...)` it builds cannot be steered by\n * a row value; `%L` quotes the probed value as a literal.\n */\n\nimport type { DatabaseInterface } from '@happyvertical/sql';\nimport { formatDefaultValue, quoteIdentifier } from './sql-identifiers.js';\n\n/** Outcome of a server-side cast-safety probe over one column's non-null values. */\nexport type ShapeProbeResult =\n | { status: 'clean' }\n | {\n status: 'dirty';\n count: number;\n sample?: string;\n /**\n * `duplicate_keys`: every value casts, but `count` JSON objects carry\n * duplicate keys that `jsonb` would silently collapse (#3041).\n * `preservation_unverified`: every value casts, but the duplicate-key\n * walk did not finish (its bound, a lock wait); `count` is 0 and no\n * conversion may be offered, but the decision must stay visible.\n */\n reason?: 'duplicate_keys' | 'preservation_unverified';\n /** Why the preservation walk did not finish (`preservation_unverified`). */\n detail?: string;\n }\n | { status: 'unavailable'; reason: string };\n\n/**\n * Mask a probed sample value for display in a diagnostic message: keep just\n * enough of the value to recognize it without echoing a full record verbatim\n * into logs/CI output (`created_at`/`_meta_data` values are user data).\n */\nexport function maskSampleValue(value: string): string {\n if (value.length === 0) return '(empty)';\n if (value.length <= 6)\n return `${'•'.repeat(value.length)} (length ${value.length})`;\n return `${value.slice(0, 3)}…${value.slice(-3)} (length ${value.length})`;\n}\n\n/** Session-scoped helper function name; `pg_temp` keeps it off the real schema. */\nconst PROBE_FUNCTION = 'pg_temp.smrt_probe_cast_ok';\n\n/**\n * PostgreSQL's \"special\" date/time input values (see the \"Special Values\"\n * table in the PostgreSQL docs). Every one of these parses successfully to\n * `timestamptz` — `probeCastSafety`'s real-cast test alone would call a text\n * column literally holding the string `now` or `infinity` \"clean\" — but\n * casting them evaluates to the *migration's* execution time or an infinite\n * sentinel, silently destroying the original value with no error and no\n * advisory. They must never be treated as safe, regardless of what a bare\n * cast attempt reports.\n */\nconst TIMESTAMPTZ_SPECIAL_VALUES = new Set([\n 'epoch',\n 'infinity',\n '-infinity',\n 'now',\n 'today',\n 'tomorrow',\n 'yesterday',\n 'allballs',\n]);\n\n/**\n * A `timestamptz` cast is only safe to apply unattended when the source text\n * already carries an explicit UTC offset (`Z` or `+HH[:MM]`/`-HH[:MM]`) —\n * without one, PostgreSQL interprets the value under the *migrating\n * session's* `TimeZone` setting, so the exact same text can cast to a\n * different instant on a different run/server. That ambiguity is exactly\n * what `--legacy-timezone=UTC` exists to resolve explicitly; the probe must\n * fail closed (not silently guess UTC) so naive values route to that opt-in\n * path instead of being auto-converged.\n */\n// PostgreSQL's ARE (\"advanced regular expression\") dialect, matched with the\n// case-insensitive `~*` operator server-side — not a JS RegExp, and not\n// derived from one, since the two dialects diverge on escaping/anchoring.\nconst TIMESTAMPTZ_EXPLICIT_OFFSET_SQL_PATTERN =\n '^[0-9]{4}-[0-9]{2}-[0-9]{2}([ T][0-9]{2}:[0-9]{2}(:[0-9]{2}(\\\\.[0-9]+)?)?)?[[:space:]]*(Z|[+-][0-9]{2}(:?[0-9]{2})?)$';\n\n/**\n * `target` is one of this module's own literal type names — never data —\n * so building the DDL by interpolation is safe; there is no column, table,\n * or row value in this string.\n */\nfunction renderCreateProbeFunctionSql(): string {\n const specialValuesArray = [...TIMESTAMPTZ_SPECIAL_VALUES]\n .map((value) => `'${value}'`)\n .join(',');\n return (\n `CREATE OR REPLACE FUNCTION ${PROBE_FUNCTION}(value text, target_type text) ` +\n 'RETURNS boolean LANGUAGE plpgsql AS $$ ' +\n 'BEGIN ' +\n 'IF value IS NULL THEN RETURN true; END IF; ' +\n \"IF target_type = 'timestamptz' THEN \" +\n `IF lower(trim(value)) = ANY (ARRAY[${specialValuesArray}]) THEN RETURN false; END IF; ` +\n `IF trim(value) !~* '${TIMESTAMPTZ_EXPLICIT_OFFSET_SQL_PATTERN}' THEN RETURN false; END IF; ` +\n 'END IF; ' +\n \"EXECUTE format('SELECT %L::%s', value, target_type); \" +\n 'RETURN true; ' +\n 'EXCEPTION WHEN OTHERS THEN RETURN false; ' +\n 'END; $$'\n );\n}\n\nfunction renderProbeQuerySql(\n tableName: string,\n columnName: string,\n targetType: 'timestamptz' | 'jsonb',\n): string {\n const column = quoteIdentifier(columnName);\n const table = quoteIdentifier(tableName);\n const isValid = `${PROBE_FUNCTION}(${column}::text, '${targetType}')`;\n return (\n `SELECT count(*) AS invalid_count, ` +\n `min(CASE WHEN NOT ${isValid} THEN ${column}::text END) AS sample_value ` +\n `FROM ${table} WHERE ${column} IS NOT NULL AND NOT ${isValid}`\n );\n}\n\n/**\n * One set-based jsonb cast over every non-null value: `count(expr)` forces\n * the cast for each row, and any value jsonb rejects raises instead of\n * returning, so success proves every value castable (not that the\n * conversion preserves it -- see {@link probeJsonbKeyPreservation}).\n */\nfunction renderSetCastQuerySql(tableName: string, columnName: string): string {\n const column = quoteIdentifier(columnName);\n const table = quoteIdentifier(tableName);\n return `SELECT count((${column}::text)::jsonb) AS cast_count FROM ${table}`;\n}\n\nfunction classifyProbeRows(\n rows: { invalid_count?: unknown; sample_value?: unknown }[],\n): ShapeProbeResult {\n const count = Number(rows[0]?.invalid_count);\n if (!Number.isFinite(count)) {\n return {\n status: 'unavailable',\n reason: 'probe returned a non-numeric count',\n };\n }\n if (count === 0) return { status: 'clean' };\n const sample = rows[0]?.sample_value;\n return {\n status: 'dirty',\n count,\n ...(typeof sample === 'string' ? { sample } : {}),\n };\n}\n\n/**\n * Probe every non-null value of a candidate `text` (or, for `jsonb`, native\n * `json`) column for whether it can cast losslessly to `timestamptz` or\n * `jsonb`, in two stages:\n *\n * 1. Cast: a real, exception-safe cast attempt (not a shape heuristic) so an\n * invalid-calendar timestamp or a structurally-malformed-but-bracket-\n * balanced JSON document is caught the same as an obviously wrong value.\n * Any failure to run it (adapter without transaction support, a missing\n * table mid-run, a test double without a realistic response) resolves to\n * `unavailable` -- callers must not treat that as \"clean\".\n * 2. Preservation (`jsonb` only, after a clean cast): duplicate object keys\n * are `dirty` with reason `duplicate_keys`; a walk that does not finish is\n * `dirty` with reason `preservation_unverified` and `count: 0`.\n *\n * SMRT never coerces or discards data on an unproven assumption.\n */\nexport async function probeCastSafety(\n db: DatabaseInterface,\n tableName: string,\n columnName: string,\n targetType: 'timestamptz' | 'jsonb',\n): Promise<ShapeProbeResult> {\n const castResult = await probeCastOnly(db, tableName, columnName, targetType);\n if (targetType !== 'jsonb' || castResult.status !== 'clean') {\n return castResult;\n }\n return probeJsonbKeyPreservation(db, tableName, columnName);\n}\n\n/**\n * #3041 review finding: a successful jsonb cast proves castability, not\n * preservation. `jsonb` keeps only the last of duplicate object keys (it\n * also drops key order and insignificant whitespace, which SMRT never\n * treats as data). Walk every container node, nested ones included, and\n * look for a key that appears more than once in an object's own `json` key\n * list (no jsonb re-parse); any such object makes the column dirty so the conversion stays a fail-closed\n * advisory instead of a silent, irreversible rewrite. The result columns\n * reuse the cast probe's names so one classifier reads both.\n */\nasync function probeJsonbKeyPreservation(\n db: DatabaseInterface,\n tableName: string,\n columnName: string,\n): Promise<ShapeProbeResult> {\n if (!db.transaction) {\n return {\n status: 'unavailable',\n reason: 'adapter does not support transactions',\n };\n }\n try {\n const result = await db.transaction(async (tx) => {\n await tx.query(\n `SET LOCAL statement_timeout = '${PRESERVATION_PROBE_TIMEOUT}'`,\n );\n return tx.query(renderDuplicateKeyQuerySql(tableName, columnName));\n });\n const rows = (Array.isArray(result) ? result : (result?.rows ?? [])) as {\n invalid_count?: unknown;\n sample_value?: unknown;\n }[];\n const classified = classifyProbeRows(rows);\n return classified.status === 'dirty'\n ? { ...classified, reason: 'duplicate_keys' }\n : classified;\n } catch (error) {\n // #3041 review finding: the cast already proved the column castable, so\n // a walk that ran and gave up (typically the timeout below) is not the\n // \"could not probe\" state `unavailable` means -- returning it would\n // silently withdraw the conversion. Fail closed, visibly.\n return {\n status: 'dirty',\n count: 0,\n reason: 'preservation_unverified',\n detail: error instanceof Error ? error.message : String(error),\n };\n }\n}\n\nfunction renderDuplicateKeyQuerySql(\n tableName: string,\n columnName: string,\n): string {\n const column = quoteIdentifier(columnName);\n const table = quoteIdentifier(tableName);\n // Only containers recurse (scalars are pruned at the edge), and each\n // object node is checked from its own key list -- a duplicate is a key\n // that appears more than once -- so no subtree is re-parsed as jsonb and\n // the walk stays linear in the number of container nodes.\n const containers = \"json_typeof(value) IN ('object', 'array')\";\n return (\n 'WITH RECURSIVE nodes(v) AS (' +\n `SELECT (${column}::text)::json FROM ${table} ` +\n `WHERE ${column} IS NOT NULL AND json_typeof((${column}::text)::json) IN ('object', 'array') ` +\n 'UNION ALL ' +\n 'SELECT child.value FROM nodes CROSS JOIN LATERAL (' +\n \"SELECT value FROM json_each(CASE WHEN json_typeof(nodes.v) = 'object' THEN nodes.v ELSE '{}'::json END) \" +\n `WHERE ${containers} ` +\n 'UNION ALL ' +\n \"SELECT value FROM json_array_elements(CASE WHEN json_typeof(nodes.v) = 'array' THEN nodes.v ELSE '[]'::json END) \" +\n `WHERE ${containers}` +\n ') AS child) ' +\n 'SELECT count(*) AS invalid_count, min(v::text) AS sample_value FROM nodes ' +\n \"WHERE json_typeof(v) = 'object' AND EXISTS (\" +\n 'SELECT 1 FROM json_object_keys(v) AS k(key) GROUP BY k.key HAVING count(*) > 1)'\n );\n}\n\n/**\n * Upper bound for the preservation walk. A walk that does not finish (this\n * timeout, a lock wait) resolves to `dirty` with reason\n * `preservation_unverified`: the differ then emits a visible warning\n * advisory with no conversion SQL (never a silent collapse, and never the\n * silent withdrawal `unavailable` would mean).\n */\nconst PRESERVATION_PROBE_TIMEOUT = '60s';\n\nasync function probeCastOnly(\n db: DatabaseInterface,\n tableName: string,\n columnName: string,\n targetType: 'timestamptz' | 'jsonb',\n): Promise<ShapeProbeResult> {\n if (!db.transaction) {\n return {\n status: 'unavailable',\n reason: 'adapter does not support transactions',\n };\n }\n if (targetType === 'jsonb') {\n // #3041 review finding: the per-row PL/pgSQL probe opens a\n // subtransaction per value, and native `json` columns are the large\n // legacy `_meta_data`-style ones. A single set-based cast that succeeds\n // proves every value castable (preservation is checked separately, see\n // `probeJsonbKeyPreservation`); only a failed cast falls through to the\n // per-row probe for the count and sample.\n try {\n const castCount = await db.transaction(async (tx) => {\n const result = await tx.query(\n renderSetCastQuerySql(tableName, columnName),\n );\n const rows = (\n Array.isArray(result) ? result : (result?.rows ?? [])\n ) as {\n cast_count?: unknown;\n }[];\n return rows[0]?.cast_count;\n });\n // Only a realistic answer counts as proof; anything else (an adapter\n // or test double that does not return the count) takes the per-row\n // probe below instead.\n if (\n castCount !== undefined &&\n castCount !== null &&\n Number.isFinite(Number(castCount))\n ) {\n return { status: 'clean' };\n }\n } catch {\n // Fall through: the per-row probe classifies the failure (dirty\n // values, or unavailable for a missing table / unrealistic adapter).\n }\n }\n try {\n return await db.transaction(async (tx) => {\n await tx.query(renderCreateProbeFunctionSql());\n const result = await tx.query(\n renderProbeQuerySql(tableName, columnName, targetType),\n );\n const rows = (Array.isArray(result) ? result : (result?.rows ?? [])) as {\n invalid_count?: unknown;\n sample_value?: unknown;\n }[];\n return classifyProbeRows(rows);\n });\n } catch (error) {\n return {\n status: 'unavailable',\n reason: error instanceof Error ? error.message : String(error),\n };\n }\n}\n\n/**\n * Column-conversion options shared by {@link renderTimestamptzColumnConversion}\n * and {@link renderJsonbColumnConversion}.\n *\n * `hasLiveDefault` and `manifestDefaultValue` are independent: PostgreSQL\n * rejects `ALTER COLUMN ... TYPE` outright whenever the column has *any*\n * existing default that cannot be auto-cast to the target type — this is\n * true whether or not the manifest itself declares a default (a legacy\n * `text` column with `DEFAULT ''` blocks the ALTER exactly the same as one\n * the manifest also wants a default on), so `DROP DEFAULT` must be gated on\n * the *live* default. `SET DEFAULT` afterward must be gated on the\n * *manifest* default instead: re-establishing a live-only default the\n * manifest no longer declares would silently resurrect drift\n * `compareColumnConstraints` is supposed to report and let an operator\n * choose to drop (see #2771/#2772 review findings: DROP/TYPE/SET DEFAULT\n * mirrors the pre-existing `generateTypeUpgradeSQL` path, and the DROP gate\n * must key off live state PostgreSQL itself enforces, not manifest intent).\n */\ninterface ColumnConversionOptions {\n hasLiveDefault?: boolean;\n manifestDefaultValue?: unknown;\n}\n\n/**\n * Render the one-time PostgreSQL conversion of a legacy `text` column to\n * native `timestamptz`, once {@link probeCastSafety} has confirmed every\n * non-null value casts safely.\n */\nexport function renderTimestamptzColumnConversion(\n tableName: string,\n columnName: string,\n options: ColumnConversionOptions = {},\n): string[] {\n const table = quoteIdentifier(tableName);\n const column = quoteIdentifier(columnName);\n const hasManifestDefault = options.manifestDefaultValue !== undefined;\n const statements: string[] = [];\n if (options.hasLiveDefault || hasManifestDefault) {\n statements.push(`ALTER TABLE ${table} ALTER COLUMN ${column} DROP DEFAULT`);\n }\n statements.push(\n `ALTER TABLE ${table} ALTER COLUMN ${column} TYPE timestamptz USING ${column}::timestamptz`,\n );\n if (hasManifestDefault) {\n const formattedDefault = formatDefaultValue(\n options.manifestDefaultValue,\n 'TIMESTAMP',\n );\n statements.push(\n `ALTER TABLE ${table} ALTER COLUMN ${column} SET DEFAULT ${formattedDefault}`,\n );\n }\n return statements;\n}\n\n/**\n * Render the one-time PostgreSQL conversion of a legacy `text` column to\n * native `jsonb`, once {@link probeCastSafety} has confirmed every non-null\n * value casts safely.\n */\nexport function renderJsonbColumnConversion(\n tableName: string,\n columnName: string,\n options: ColumnConversionOptions = {},\n): string[] {\n const table = quoteIdentifier(tableName);\n const column = quoteIdentifier(columnName);\n const hasManifestDefault = options.manifestDefaultValue !== undefined;\n const statements: string[] = [];\n if (options.hasLiveDefault || hasManifestDefault) {\n statements.push(`ALTER TABLE ${table} ALTER COLUMN ${column} DROP DEFAULT`);\n }\n statements.push(\n `ALTER TABLE ${table} ALTER COLUMN ${column} TYPE jsonb USING ${column}::jsonb`,\n );\n if (hasManifestDefault) {\n const formattedDefault = formatDefaultValue(\n options.manifestDefaultValue,\n 'JSON',\n );\n statements.push(\n `ALTER TABLE ${table} ALTER COLUMN ${column} SET DEFAULT ${formattedDefault}::jsonb`,\n );\n }\n return statements;\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"text-cast-probe.js","names":[],"sources":["../../src/schema/text-cast-probe.ts"],"sourcesContent":["/**\n * Shared \"probe, then cast\" helpers for text-column type convergence (#2771,\n * #2772).\n *\n * Two drift shapes on a table SMRT itself creates share one problem: a live\n * `text` column holds values that would parse losslessly into the manifest's\n * declared PostgreSQL type (`timestamptz` or `jsonb`), but the differ cannot\n * know that without looking at the data. `probeCastSafety()` answers that with\n * a genuine, exception-safe cast attempt — not a shape heuristic — over every\n * non-null value, and returns one sample so a fail-closed diagnostic can name\n * the offending value without dumping the whole column into JS.\n *\n * PostgreSQL has no `TRY_CAST`/safe-cast builtin before 17 (`pg_input_is_valid`),\n * and this fleet targets 16 (see `.github/workflows/postgres-tests.yml`), so a\n * real per-row safe cast needs PL/pgSQL's exception handling. That handler is\n * created as a session-scoped (`pg_temp`) function, which is why the create\n * statement and the probe query run inside one `db.transaction()` — the two\n * must land on the same physical connection, and a pooled adapter only\n * guarantees that within one transaction. `target_type` is never derived from\n * column data (always the literal `'jsonb'`/`'timestamptz'` this module\n * passes), so the dynamic `EXECUTE format(...)` it builds cannot be steered by\n * a row value; `%L` quotes the probed value as a literal.\n */\n\nimport type { DatabaseInterface } from '@happyvertical/sql';\nimport { formatDefaultValue, quoteIdentifier } from './sql-identifiers.js';\n\n/** Outcome of a server-side cast-safety probe over one column's non-null values. */\nexport type ShapeProbeResult =\n | { status: 'clean'; emptyCount?: number }\n | {\n status: 'dirty';\n /** Every non-null value that does not cast, empty text included. */\n count: number;\n /**\n * How many of `count` are empty or whitespace-only text (#3226). When\n * it equals `count` (and there is no `reason`), empty text is the only\n * obstacle and the opt-in `emptyTextAsNull` conversion can store those\n * values as NULL.\n */\n emptyCount?: number;\n /**\n * One offending value, preferring a non-empty one; `''` when empty\n * text is the only obstacle. Display it with {@link maskSampleValue}.\n */\n sample?: string;\n /**\n * `duplicate_keys`: every value casts, but `count` JSON objects carry\n * duplicate keys that `jsonb` would silently collapse (#3041).\n * `preservation_unverified`: every value casts, but the duplicate-key\n * walk did not finish (its bound, a lock wait); `count` is 0 and no\n * conversion may be offered, but the decision must stay visible.\n */\n reason?: 'duplicate_keys' | 'preservation_unverified';\n /** Why the preservation walk did not finish (`preservation_unverified`). */\n detail?: string;\n }\n | { status: 'unavailable'; reason: string };\n\n/**\n * Mask a probed sample value for display in a diagnostic message: keep just\n * enough of the value to recognize it without echoing a full record verbatim\n * into logs/CI output (`created_at`/`_meta_data` values are user data).\n */\nexport function maskSampleValue(value: string): string {\n if (value.length === 0) return '(empty)';\n if (value.length <= 6)\n return `${'•'.repeat(value.length)} (length ${value.length})`;\n return `${value.slice(0, 3)}…${value.slice(-3)} (length ${value.length})`;\n}\n\n/**\n * SQL predicate: the text form of `column` is empty or whitespace-only\n * (#3226). `column` must already be a quoted identifier.\n */\nexport function renderEmptyTextPredicate(column: string): string {\n return `btrim(${column}::text, E' \\\\t\\\\n\\\\r') = ''`;\n}\n\n/**\n * SQL expression: the text form of `column`, with empty or whitespace-only\n * text as NULL (#3226). The cast target of the opt-in empty-text-as-null\n * conversions. `column` must already be a quoted identifier.\n */\nexport function renderEmptyTextAsNullExpression(column: string): string {\n return `CASE WHEN ${renderEmptyTextPredicate(column)} THEN NULL ELSE ${column}::text END`;\n}\n\n/** Options for {@link probeCastSafety}. */\nexport interface ProbeCastSafetyOptions {\n /**\n * The caller may store empty text as NULL (#3226). For `jsonb`, a column\n * whose only non-castable values are empty text then still gets the\n * duplicate-key preservation walk over its remaining values, so the\n * opt-in conversion is never offered on an unverified column.\n */\n emptyTextAsNull?: boolean;\n}\n\n/** Session-scoped helper function name; `pg_temp` keeps it off the real schema. */\nconst PROBE_FUNCTION = 'pg_temp.smrt_probe_cast_ok';\n\n/**\n * PostgreSQL's \"special\" date/time input values (see the \"Special Values\"\n * table in the PostgreSQL docs). Every one of these parses successfully to\n * `timestamptz` — `probeCastSafety`'s real-cast test alone would call a text\n * column literally holding the string `now` or `infinity` \"clean\" — but\n * casting them evaluates to the *migration's* execution time or an infinite\n * sentinel, silently destroying the original value with no error and no\n * advisory. They must never be treated as safe, regardless of what a bare\n * cast attempt reports.\n */\nconst TIMESTAMPTZ_SPECIAL_VALUES = new Set([\n 'epoch',\n 'infinity',\n '-infinity',\n 'now',\n 'today',\n 'tomorrow',\n 'yesterday',\n 'allballs',\n]);\n\n/**\n * A `timestamptz` cast is only safe to apply unattended when the source text\n * already carries an explicit UTC offset (`Z` or `+HH[:MM]`/`-HH[:MM]`) —\n * without one, PostgreSQL interprets the value under the *migrating\n * session's* `TimeZone` setting, so the exact same text can cast to a\n * different instant on a different run/server. That ambiguity is exactly\n * what `--legacy-timezone=UTC` exists to resolve explicitly; the probe must\n * fail closed (not silently guess UTC) so naive values route to that opt-in\n * path instead of being auto-converged.\n */\n// PostgreSQL's ARE (\"advanced regular expression\") dialect, matched with the\n// case-insensitive `~*` operator server-side — not a JS RegExp, and not\n// derived from one, since the two dialects diverge on escaping/anchoring.\nconst TIMESTAMPTZ_EXPLICIT_OFFSET_SQL_PATTERN =\n '^[0-9]{4}-[0-9]{2}-[0-9]{2}([ T][0-9]{2}:[0-9]{2}(:[0-9]{2}(\\\\.[0-9]+)?)?)?[[:space:]]*(Z|[+-][0-9]{2}(:?[0-9]{2})?)$';\n\n/**\n * `target` is one of this module's own literal type names — never data —\n * so building the DDL by interpolation is safe; there is no column, table,\n * or row value in this string.\n */\nfunction renderCreateProbeFunctionSql(): string {\n const specialValuesArray = [...TIMESTAMPTZ_SPECIAL_VALUES]\n .map((value) => `'${value}'`)\n .join(',');\n return (\n `CREATE OR REPLACE FUNCTION ${PROBE_FUNCTION}(value text, target_type text) ` +\n 'RETURNS boolean LANGUAGE plpgsql AS $$ ' +\n 'BEGIN ' +\n 'IF value IS NULL THEN RETURN true; END IF; ' +\n \"IF target_type = 'timestamptz' THEN \" +\n `IF lower(trim(value)) = ANY (ARRAY[${specialValuesArray}]) THEN RETURN false; END IF; ` +\n `IF trim(value) !~* '${TIMESTAMPTZ_EXPLICIT_OFFSET_SQL_PATTERN}' THEN RETURN false; END IF; ` +\n 'END IF; ' +\n \"EXECUTE format('SELECT %L::%s', value, target_type); \" +\n 'RETURN true; ' +\n 'EXCEPTION WHEN OTHERS THEN RETURN false; ' +\n 'END; $$'\n );\n}\n\nfunction renderProbeQuerySql(\n tableName: string,\n columnName: string,\n targetType: 'timestamptz' | 'jsonb',\n): string {\n const column = quoteIdentifier(columnName);\n const table = quoteIdentifier(tableName);\n // #3226: empty/whitespace-only text is counted on its own (it never casts\n // to either target) and kept out of the sample, so the diagnostic shows a\n // genuinely malformed value when there is one. The cast probe runs once\n // per non-empty value.\n const isValid = `${PROBE_FUNCTION}(candidate.value, '${targetType}')`;\n return (\n 'SELECT ' +\n 'count(*) FILTER (WHERE probe.is_empty OR NOT probe.is_valid) AS invalid_count, ' +\n 'count(*) FILTER (WHERE probe.is_empty) AS empty_count, ' +\n 'min(probe.value) FILTER (WHERE NOT probe.is_empty AND NOT probe.is_valid) AS sample_value ' +\n 'FROM (SELECT candidate.value, candidate.is_empty, ' +\n `CASE WHEN candidate.is_empty THEN true ELSE ${isValid} END AS is_valid ` +\n `FROM (SELECT ${column}::text AS value, ${renderEmptyTextPredicate(column)} AS is_empty ` +\n `FROM ${table} WHERE ${column} IS NOT NULL) AS candidate) AS probe`\n );\n}\n\n/**\n * One set-based jsonb cast over every non-null value: `count(expr)` forces\n * the cast for each row, and any value jsonb rejects raises instead of\n * returning, so success proves every value castable (not that the\n * conversion preserves it -- see {@link probeJsonbKeyPreservation}).\n */\nfunction renderSetCastQuerySql(tableName: string, columnName: string): string {\n const column = quoteIdentifier(columnName);\n const table = quoteIdentifier(tableName);\n return `SELECT count((${column}::text)::jsonb) AS cast_count FROM ${table}`;\n}\n\nfunction classifyProbeRows(\n rows: {\n invalid_count?: unknown;\n empty_count?: unknown;\n sample_value?: unknown;\n }[],\n): ShapeProbeResult {\n const count = Number(rows[0]?.invalid_count);\n if (!Number.isFinite(count)) {\n return {\n status: 'unavailable',\n reason: 'probe returned a non-numeric count',\n };\n }\n if (count === 0) return { status: 'clean' };\n const rawEmptyCount = Number(rows[0]?.empty_count);\n const emptyCount =\n rows[0]?.empty_count !== undefined && Number.isFinite(rawEmptyCount)\n ? rawEmptyCount\n : undefined;\n const rawSample = rows[0]?.sample_value;\n // #3226: when empty text is the only obstacle there is no non-empty\n // sample; report `''` so the diagnostic names the cause.\n const sample =\n typeof rawSample === 'string'\n ? rawSample\n : emptyCount !== undefined && emptyCount > 0\n ? ''\n : undefined;\n return {\n status: 'dirty',\n count,\n ...(emptyCount !== undefined ? { emptyCount } : {}),\n ...(sample !== undefined ? { sample } : {}),\n };\n}\n\n/**\n * Probe every non-null value of a candidate `text` (or, for `jsonb`, native\n * `json`) column for whether it can cast losslessly to `timestamptz` or\n * `jsonb`, in two stages:\n *\n * 1. Cast: a real, exception-safe cast attempt (not a shape heuristic) so an\n * invalid-calendar timestamp or a structurally-malformed-but-bracket-\n * balanced JSON document is caught the same as an obviously wrong value.\n * Any failure to run it (adapter without transaction support, a missing\n * table mid-run, a test double without a realistic response) resolves to\n * `unavailable` -- callers must not treat that as \"clean\".\n * 2. Preservation (`jsonb` only, after a clean cast): duplicate object keys\n * are `dirty` with reason `duplicate_keys`; a walk that does not finish is\n * `dirty` with reason `preservation_unverified` and `count: 0`.\n *\n * SMRT never coerces or discards data on an unproven assumption.\n */\nexport async function probeCastSafety(\n db: DatabaseInterface,\n tableName: string,\n columnName: string,\n targetType: 'timestamptz' | 'jsonb',\n options: ProbeCastSafetyOptions = {},\n): Promise<ShapeProbeResult> {\n const castResult = await probeCastOnly(db, tableName, columnName, targetType);\n if (targetType !== 'jsonb') return castResult;\n if (castResult.status === 'clean') {\n return probeJsonbKeyPreservation(db, tableName, columnName);\n }\n // #3226: empty text is the only obstacle and the caller may store it as\n // NULL -- the remaining values still need the preservation walk before a\n // conversion can be offered.\n if (options.emptyTextAsNull === true && isEmptyTextOnlyProbe(castResult)) {\n const preservation = await probeJsonbKeyPreservation(\n db,\n tableName,\n columnName,\n );\n if (preservation.status === 'dirty') {\n return { ...preservation, emptyCount: castResult.emptyCount };\n }\n return castResult;\n }\n return castResult;\n}\n\n/**\n * Whether a probe found non-castable values that are all empty or\n * whitespace-only text (#3226) -- the one shape the opt-in\n * empty-text-as-null conversion can repair.\n */\nexport function isEmptyTextOnlyProbe(\n result: ShapeProbeResult | undefined,\n): result is Extract<ShapeProbeResult, { status: 'dirty' }> & {\n emptyCount: number;\n} {\n return (\n result?.status === 'dirty' &&\n result.reason === undefined &&\n result.emptyCount !== undefined &&\n result.emptyCount > 0 &&\n result.emptyCount === result.count\n );\n}\n\n/**\n * #3041 review finding: a successful jsonb cast proves castability, not\n * preservation. `jsonb` keeps only the last of duplicate object keys (it\n * also drops key order and insignificant whitespace, which SMRT never\n * treats as data). Walk every container node, nested ones included, and\n * look for a key that appears more than once in an object's own `json` key\n * list (no jsonb re-parse); any such object makes the column dirty so the conversion stays a fail-closed\n * advisory instead of a silent, irreversible rewrite. The result columns\n * reuse the cast probe's names so one classifier reads both.\n */\nasync function probeJsonbKeyPreservation(\n db: DatabaseInterface,\n tableName: string,\n columnName: string,\n): Promise<ShapeProbeResult> {\n if (!db.transaction) {\n return {\n status: 'unavailable',\n reason: 'adapter does not support transactions',\n };\n }\n try {\n const result = await db.transaction(async (tx) => {\n await tx.query(\n `SET LOCAL statement_timeout = '${PRESERVATION_PROBE_TIMEOUT}'`,\n );\n return tx.query(renderDuplicateKeyQuerySql(tableName, columnName));\n });\n const rows = (Array.isArray(result) ? result : (result?.rows ?? [])) as {\n invalid_count?: unknown;\n sample_value?: unknown;\n }[];\n const classified = classifyProbeRows(rows);\n return classified.status === 'dirty'\n ? { ...classified, reason: 'duplicate_keys' }\n : classified;\n } catch (error) {\n // #3041 review finding: the cast already proved the column castable, so\n // a walk that ran and gave up (typically the timeout below) is not the\n // \"could not probe\" state `unavailable` means -- returning it would\n // silently withdraw the conversion. Fail closed, visibly.\n return {\n status: 'dirty',\n count: 0,\n reason: 'preservation_unverified',\n detail: error instanceof Error ? error.message : String(error),\n };\n }\n}\n\nfunction renderDuplicateKeyQuerySql(\n tableName: string,\n columnName: string,\n): string {\n const column = quoteIdentifier(columnName);\n const table = quoteIdentifier(tableName);\n // Only containers recurse (scalars are pruned at the edge), and each\n // object node is checked from its own key list -- a duplicate is a key\n // that appears more than once -- so no subtree is re-parsed as jsonb and\n // the walk stays linear in the number of container nodes.\n const containers = \"json_typeof(value) IN ('object', 'array')\";\n return (\n 'WITH RECURSIVE nodes(v) AS (' +\n `SELECT (${column}::text)::json FROM ${table} ` +\n // #3226: empty text (which becomes NULL under the opt-in conversion)\n // never reaches the json cast; a CASE keeps that evaluation order.\n `WHERE ${column} IS NOT NULL AND CASE WHEN ${renderEmptyTextPredicate(column)} THEN false ` +\n `ELSE json_typeof((${column}::text)::json) IN ('object', 'array') END ` +\n 'UNION ALL ' +\n 'SELECT child.value FROM nodes CROSS JOIN LATERAL (' +\n \"SELECT value FROM json_each(CASE WHEN json_typeof(nodes.v) = 'object' THEN nodes.v ELSE '{}'::json END) \" +\n `WHERE ${containers} ` +\n 'UNION ALL ' +\n \"SELECT value FROM json_array_elements(CASE WHEN json_typeof(nodes.v) = 'array' THEN nodes.v ELSE '[]'::json END) \" +\n `WHERE ${containers}` +\n ') AS child) ' +\n 'SELECT count(*) AS invalid_count, min(v::text) AS sample_value FROM nodes ' +\n \"WHERE json_typeof(v) = 'object' AND EXISTS (\" +\n 'SELECT 1 FROM json_object_keys(v) AS k(key) GROUP BY k.key HAVING count(*) > 1)'\n );\n}\n\n/**\n * Upper bound for the preservation walk. A walk that does not finish (this\n * timeout, a lock wait) resolves to `dirty` with reason\n * `preservation_unverified`: the differ then emits a visible warning\n * advisory with no conversion SQL (never a silent collapse, and never the\n * silent withdrawal `unavailable` would mean).\n */\nconst PRESERVATION_PROBE_TIMEOUT = '60s';\n\nasync function probeCastOnly(\n db: DatabaseInterface,\n tableName: string,\n columnName: string,\n targetType: 'timestamptz' | 'jsonb',\n): Promise<ShapeProbeResult> {\n if (!db.transaction) {\n return {\n status: 'unavailable',\n reason: 'adapter does not support transactions',\n };\n }\n if (targetType === 'jsonb') {\n // #3041 review finding: the per-row PL/pgSQL probe opens a\n // subtransaction per value, and native `json` columns are the large\n // legacy `_meta_data`-style ones. A single set-based cast that succeeds\n // proves every value castable (preservation is checked separately, see\n // `probeJsonbKeyPreservation`); only a failed cast falls through to the\n // per-row probe for the count and sample.\n try {\n const castCount = await db.transaction(async (tx) => {\n const result = await tx.query(\n renderSetCastQuerySql(tableName, columnName),\n );\n const rows = (\n Array.isArray(result) ? result : (result?.rows ?? [])\n ) as {\n cast_count?: unknown;\n }[];\n return rows[0]?.cast_count;\n });\n // Only a realistic answer counts as proof; anything else (an adapter\n // or test double that does not return the count) takes the per-row\n // probe below instead.\n if (\n castCount !== undefined &&\n castCount !== null &&\n Number.isFinite(Number(castCount))\n ) {\n return { status: 'clean' };\n }\n } catch {\n // Fall through: the per-row probe classifies the failure (dirty\n // values, or unavailable for a missing table / unrealistic adapter).\n }\n }\n try {\n return await db.transaction(async (tx) => {\n await tx.query(renderCreateProbeFunctionSql());\n const result = await tx.query(\n renderProbeQuerySql(tableName, columnName, targetType),\n );\n const rows = (Array.isArray(result) ? result : (result?.rows ?? [])) as {\n invalid_count?: unknown;\n sample_value?: unknown;\n }[];\n return classifyProbeRows(rows);\n });\n } catch (error) {\n return {\n status: 'unavailable',\n reason: error instanceof Error ? error.message : String(error),\n };\n }\n}\n\n/**\n * Column-conversion options shared by {@link renderTimestamptzColumnConversion}\n * and {@link renderJsonbColumnConversion}.\n *\n * `hasLiveDefault` and `manifestDefaultValue` are independent: PostgreSQL\n * rejects `ALTER COLUMN ... TYPE` outright whenever the column has *any*\n * existing default that cannot be auto-cast to the target type — this is\n * true whether or not the manifest itself declares a default (a legacy\n * `text` column with `DEFAULT ''` blocks the ALTER exactly the same as one\n * the manifest also wants a default on), so `DROP DEFAULT` must be gated on\n * the *live* default. `SET DEFAULT` afterward must be gated on the\n * *manifest* default instead: re-establishing a live-only default the\n * manifest no longer declares would silently resurrect drift\n * `compareColumnConstraints` is supposed to report and let an operator\n * choose to drop (see #2771/#2772 review findings: DROP/TYPE/SET DEFAULT\n * mirrors the pre-existing `generateTypeUpgradeSQL` path, and the DROP gate\n * must key off live state PostgreSQL itself enforces, not manifest intent).\n */\ninterface ColumnConversionOptions {\n hasLiveDefault?: boolean;\n manifestDefaultValue?: unknown;\n /**\n * Store empty or whitespace-only text as NULL (#3226). Only for a\n * nullable column whose probe found empty text as the sole obstacle.\n */\n emptyTextAsNull?: boolean;\n}\n\n/**\n * Render the one-time PostgreSQL conversion of a legacy `text` column to\n * native `timestamptz`, once {@link probeCastSafety} has confirmed every\n * non-null value casts safely.\n */\nexport function renderTimestamptzColumnConversion(\n tableName: string,\n columnName: string,\n options: ColumnConversionOptions = {},\n): string[] {\n const table = quoteIdentifier(tableName);\n const column = quoteIdentifier(columnName);\n const hasManifestDefault = options.manifestDefaultValue !== undefined;\n const statements: string[] = [];\n if (options.hasLiveDefault || hasManifestDefault) {\n statements.push(`ALTER TABLE ${table} ALTER COLUMN ${column} DROP DEFAULT`);\n }\n statements.push(\n `ALTER TABLE ${table} ALTER COLUMN ${column} TYPE timestamptz USING ${\n options.emptyTextAsNull\n ? `(${renderEmptyTextAsNullExpression(column)})`\n : column\n }::timestamptz`,\n );\n if (hasManifestDefault) {\n const formattedDefault = formatDefaultValue(\n options.manifestDefaultValue,\n 'TIMESTAMP',\n );\n statements.push(\n `ALTER TABLE ${table} ALTER COLUMN ${column} SET DEFAULT ${formattedDefault}`,\n );\n }\n return statements;\n}\n\n/**\n * Render the one-time PostgreSQL conversion of a legacy `text` column to\n * native `jsonb`, once {@link probeCastSafety} has confirmed every non-null\n * value casts safely.\n */\nexport function renderJsonbColumnConversion(\n tableName: string,\n columnName: string,\n options: ColumnConversionOptions = {},\n): string[] {\n const table = quoteIdentifier(tableName);\n const column = quoteIdentifier(columnName);\n const hasManifestDefault = options.manifestDefaultValue !== undefined;\n const statements: string[] = [];\n if (options.hasLiveDefault || hasManifestDefault) {\n statements.push(`ALTER TABLE ${table} ALTER COLUMN ${column} DROP DEFAULT`);\n }\n statements.push(\n `ALTER TABLE ${table} ALTER COLUMN ${column} TYPE jsonb USING ${\n options.emptyTextAsNull\n ? `(${renderEmptyTextAsNullExpression(column)})`\n : column\n }::jsonb`,\n );\n if (hasManifestDefault) {\n const formattedDefault = formatDefaultValue(\n options.manifestDefaultValue,\n 'JSON',\n );\n statements.push(\n `ALTER TABLE ${table} ALTER COLUMN ${column} SET DEFAULT ${formattedDefault}::jsonb`,\n );\n }\n return statements;\n}\n"],"mappings":"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"}
|
package/dist/schema/types.d.ts
CHANGED
|
@@ -388,6 +388,12 @@ export interface SchemaChange {
|
|
|
388
388
|
* so would withhold unrelated DDL on that column permanently.
|
|
389
389
|
*/
|
|
390
390
|
engineUnsupported?: boolean;
|
|
391
|
+
/**
|
|
392
|
+
* Operator-facing note on an executable change that does more than its
|
|
393
|
+
* type pair says -- e.g. a `type_upgrade` that stores empty text as NULL
|
|
394
|
+
* under `emptyTextAsNull` (#3226). Printed by `db:diff`/`db:migrate`.
|
|
395
|
+
*/
|
|
396
|
+
note?: string;
|
|
391
397
|
/** Generated SQL statement */
|
|
392
398
|
sql?: string;
|
|
393
399
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/schema/types.ts"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/schema/types.ts"],"names":[],"mappings":"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"}
|