@happyvertical/smrt-core 0.41.0 → 0.42.1
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 +1 -1
- package/agents/schema-paths.md +79 -8
- package/dist/change-feed.d.ts.map +1 -1
- package/dist/change-feed.js +2 -2
- package/dist/change-feed.js.map +1 -1
- package/dist/change-signals.d.ts.map +1 -1
- package/dist/change-signals.js +8 -1
- package/dist/change-signals.js.map +1 -1
- package/dist/class.d.ts.map +1 -1
- package/dist/class.js +10 -9
- package/dist/class.js.map +1 -1
- package/dist/collection.d.ts.map +1 -1
- package/dist/collection.js +2 -2
- package/dist/collection.js.map +1 -1
- package/dist/dispatch/collections/Dispatches.d.ts.map +1 -1
- package/dist/dispatch/collections/Dispatches.js +5 -5
- package/dist/dispatch/collections/Dispatches.js.map +1 -1
- package/dist/dispatch/models/Dispatch.d.ts +1 -1
- package/dist/dispatch/models/Dispatch.d.ts.map +1 -1
- package/dist/dispatch/models/Dispatch.js +3 -2
- package/dist/dispatch/models/Dispatch.js.map +1 -1
- package/dist/dispatch/models/DispatchSubscription.d.ts +1 -1
- package/dist/dispatch/models/DispatchSubscription.d.ts.map +1 -1
- package/dist/dispatch/models/DispatchSubscription.js +2 -1
- package/dist/dispatch/models/DispatchSubscription.js.map +1 -1
- package/dist/embeddings/storage.d.ts.map +1 -1
- package/dist/embeddings/storage.js +2 -1
- package/dist/embeddings/storage.js.map +1 -1
- package/dist/learning/memory.d.ts.map +1 -1
- package/dist/learning/memory.js +5 -4
- package/dist/learning/memory.js.map +1 -1
- package/dist/manifest/static-manifest.js +1 -1
- package/dist/manifest/static-manifest.js.map +1 -1
- package/dist/manifest/store.js +1 -1
- package/dist/manifest.json +1 -1
- package/dist/migrations/differ.js +2 -2
- package/dist/migrations/differ.js.map +1 -1
- package/dist/migrations/index.d.ts +1 -0
- package/dist/migrations/index.d.ts.map +1 -1
- package/dist/migrations/index.js +2 -1
- package/dist/migrations/integer-width.d.ts +99 -0
- package/dist/migrations/integer-width.d.ts.map +1 -0
- package/dist/migrations/integer-width.js +237 -0
- package/dist/migrations/integer-width.js.map +1 -0
- package/dist/migrations/minor-units.d.ts +2 -2
- package/dist/migrations/minor-units.d.ts.map +1 -1
- package/dist/migrations/minor-units.js +16 -14
- package/dist/migrations/minor-units.js.map +1 -1
- package/dist/migrations/tracker.d.ts.map +1 -1
- package/dist/migrations/tracker.js +28 -20
- package/dist/migrations/tracker.js.map +1 -1
- package/dist/migrations.js +2 -1
- package/dist/schema/ddl/duckdb-strategy.d.ts.map +1 -1
- package/dist/schema/ddl/duckdb-strategy.js +1 -0
- package/dist/schema/ddl/duckdb-strategy.js.map +1 -1
- package/dist/schema/ddl/postgres-strategy.d.ts.map +1 -1
- package/dist/schema/ddl/postgres-strategy.js +2 -0
- package/dist/schema/ddl/postgres-strategy.js.map +1 -1
- package/dist/schema/generator.d.ts +1 -33
- package/dist/schema/generator.d.ts.map +1 -1
- package/dist/schema/generator.js +1 -150
- package/dist/schema/generator.js.map +1 -1
- package/dist/schema/index.d.ts +1 -2
- package/dist/schema/index.d.ts.map +1 -1
- package/dist/schema/index.js +1 -2
- package/dist/schema/live-parity.d.ts +3 -1
- package/dist/schema/live-parity.d.ts.map +1 -1
- package/dist/schema/live-parity.js +19 -0
- package/dist/schema/live-parity.js.map +1 -1
- package/dist/schema/types.d.ts +17 -10
- package/dist/schema/types.d.ts.map +1 -1
- package/dist/schema.js +1 -2
- package/dist/smrt-knowledge.json +7 -7
- package/dist/system/retention.d.ts.map +1 -1
- package/dist/system/retention.js +3 -3
- package/dist/system/retention.js.map +1 -1
- package/dist/system/schema.d.ts +2 -1
- package/dist/system/schema.d.ts.map +1 -1
- package/dist/system/schema.js +4 -2
- package/dist/system/schema.js.map +1 -1
- package/dist/utils/safe-integer.d.ts +19 -0
- package/dist/utils/safe-integer.d.ts.map +1 -0
- package/dist/utils/safe-integer.js +31 -0
- package/dist/utils/safe-integer.js.map +1 -0
- package/dist/utils.d.ts +2 -1
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +6 -3
- package/dist/utils.js.map +1 -1
- package/dist/vite-plugin/index.d.ts.map +1 -1
- package/dist/vite-plugin/index.js +0 -53
- package/dist/vite-plugin/index.js.map +1 -1
- package/package.json +4 -4
- package/dist/schema/override-system.d.ts +0 -43
- package/dist/schema/override-system.d.ts.map +0 -1
- package/dist/schema/override-system.js +0 -205
- package/dist/schema/override-system.js.map +0 -1
package/dist/migrations/index.js
CHANGED
|
@@ -2,9 +2,10 @@ import { parsePostgresTimeoutMs } from "../postgres-timeouts.js";
|
|
|
2
2
|
import { SQLITE_REBUILD_TABLE_PREFIX, buildSqliteRebuildStatements, isSqliteRebuildPlaceholder, planSqliteTableRebuilds, rewriteSqliteCreateTable, sqliteRebuildPlaceholderSql } from "./sqlite-rebuild.js";
|
|
3
3
|
import { SchemaComparer, canonicalizeDefault, generateSchemaDiff, getSQLFromDiff, hasActionableChanges, isAdvisoryOnlyChange, isInfoOnlyChange, isManualOrAdvisoryChange, uniqueColumnIndexName } from "./differ.js";
|
|
4
4
|
import { BackfillTableUnavailableError, BackfillTracker } from "./backfill-tracker.js";
|
|
5
|
+
import { buildIntegerWidthStatements, buildIntegerWidthTableStatements, collectIntegerWidthTargets, preflightIntegerWidthWidening, widenIntegerColumnsToBigInt } from "./integer-width.js";
|
|
5
6
|
import { computeChecksum, normalizeSQL, shortChecksum, verifyChecksum } from "./checksum.js";
|
|
6
7
|
import { MigrationGenerator, createMigrationDefinition, generateMigrationSequence, generateMigrationTimestamp } from "./generator.js";
|
|
7
8
|
import { MinorUnitsPreflightError, buildMinorUnitsStatements, preflightMinorUnitsRescale, rescaleMoneyColumnsToMinorUnits } from "./minor-units.js";
|
|
8
9
|
import { MigrationTracker, buildConcurrentIndexPlan, extractCreatedIndexName, planPostgresStatements } from "./tracker.js";
|
|
9
10
|
import { getPendingSchemaStatements, migrateSmrtSchemas } from "./orchestrate.js";
|
|
10
|
-
export { BackfillTableUnavailableError, BackfillTracker, MigrationGenerator, MigrationTracker, MinorUnitsPreflightError, SQLITE_REBUILD_TABLE_PREFIX, SchemaComparer, buildConcurrentIndexPlan, buildMinorUnitsStatements, buildSqliteRebuildStatements, canonicalizeDefault, computeChecksum, createMigrationDefinition, extractCreatedIndexName, generateMigrationSequence, generateMigrationTimestamp, generateSchemaDiff, getPendingSchemaStatements, getSQLFromDiff, hasActionableChanges, isAdvisoryOnlyChange, isInfoOnlyChange, isManualOrAdvisoryChange, isSqliteRebuildPlaceholder, migrateSmrtSchemas, normalizeSQL, parsePostgresTimeoutMs, planPostgresStatements, planSqliteTableRebuilds, preflightMinorUnitsRescale, rescaleMoneyColumnsToMinorUnits, rewriteSqliteCreateTable, shortChecksum, sqliteRebuildPlaceholderSql, uniqueColumnIndexName, verifyChecksum };
|
|
11
|
+
export { BackfillTableUnavailableError, BackfillTracker, MigrationGenerator, MigrationTracker, MinorUnitsPreflightError, SQLITE_REBUILD_TABLE_PREFIX, SchemaComparer, buildConcurrentIndexPlan, buildIntegerWidthStatements, buildIntegerWidthTableStatements, buildMinorUnitsStatements, buildSqliteRebuildStatements, canonicalizeDefault, collectIntegerWidthTargets, computeChecksum, createMigrationDefinition, extractCreatedIndexName, generateMigrationSequence, generateMigrationTimestamp, generateSchemaDiff, getPendingSchemaStatements, getSQLFromDiff, hasActionableChanges, isAdvisoryOnlyChange, isInfoOnlyChange, isManualOrAdvisoryChange, isSqliteRebuildPlaceholder, migrateSmrtSchemas, normalizeSQL, parsePostgresTimeoutMs, planPostgresStatements, planSqliteTableRebuilds, preflightIntegerWidthWidening, preflightMinorUnitsRescale, rescaleMoneyColumnsToMinorUnits, rewriteSqliteCreateTable, shortChecksum, sqliteRebuildPlaceholderSql, uniqueColumnIndexName, verifyChecksum, widenIntegerColumnsToBigInt };
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { DatabaseInterface } from '@happyvertical/sql';
|
|
2
|
+
import { SchemaDefinition } from '../schema/types.js';
|
|
3
|
+
/** A table and its SMRT-owned integer columns. */
|
|
4
|
+
export interface IntegerWidthTarget {
|
|
5
|
+
table: string;
|
|
6
|
+
columns: string[];
|
|
7
|
+
}
|
|
8
|
+
/** The width state of one declared integer column. */
|
|
9
|
+
export type IntegerWidthColumnState = 'pending' | 'current' | 'missing' | 'unexpected';
|
|
10
|
+
/** Preflight finding for one declared integer column. */
|
|
11
|
+
export interface IntegerWidthColumnReport {
|
|
12
|
+
table: string;
|
|
13
|
+
column: string;
|
|
14
|
+
state: IntegerWidthColumnState;
|
|
15
|
+
/** Live `information_schema.columns.data_type`, when the column exists. */
|
|
16
|
+
declaredType: string | null;
|
|
17
|
+
}
|
|
18
|
+
/** Preflight findings for a single SMRT-owned table. */
|
|
19
|
+
export interface IntegerWidthTableReport {
|
|
20
|
+
table: string;
|
|
21
|
+
/** Number of rows when at least one column still needs widening. */
|
|
22
|
+
rowCount: number | null;
|
|
23
|
+
columns: IntegerWidthColumnReport[];
|
|
24
|
+
}
|
|
25
|
+
/** Read-only assessment of a deployment's legacy int4 columns. */
|
|
26
|
+
export interface IntegerWidthPreflightResult {
|
|
27
|
+
engine: string;
|
|
28
|
+
/** False for SQLite and adapters without a native 32-bit integer width. */
|
|
29
|
+
supported: boolean;
|
|
30
|
+
tables: IntegerWidthTableReport[];
|
|
31
|
+
pendingColumns: number;
|
|
32
|
+
currentColumns: number;
|
|
33
|
+
missingColumns: number;
|
|
34
|
+
unexpectedColumns: number;
|
|
35
|
+
/** Tables that need a maintenance-window table rewrite. */
|
|
36
|
+
pendingTables: number;
|
|
37
|
+
/** Human-readable output suitable for an operator change record. */
|
|
38
|
+
summary: string;
|
|
39
|
+
}
|
|
40
|
+
/** Shared options for the read-only preflight and mutating widening pass. */
|
|
41
|
+
export interface IntegerWidthOptions {
|
|
42
|
+
/** Adapter hint when its URL does not identify the engine. */
|
|
43
|
+
engineHint?: string;
|
|
44
|
+
}
|
|
45
|
+
/** Options that make the widening pass safely repeatable. */
|
|
46
|
+
export interface IntegerWidthWidenOptions extends IntegerWidthOptions {
|
|
47
|
+
/** Stable `_smrt_backfills` audit marker, unique to this migration owner. */
|
|
48
|
+
backfillName: string;
|
|
49
|
+
/** Package recorded alongside the marker. */
|
|
50
|
+
packageName?: string;
|
|
51
|
+
/** PostgreSQL lock timeout in milliseconds (defaults to 30 seconds). */
|
|
52
|
+
lockTimeout?: number;
|
|
53
|
+
/** PostgreSQL statement timeout in milliseconds (defaults to 60 seconds). */
|
|
54
|
+
statementTimeout?: number;
|
|
55
|
+
}
|
|
56
|
+
/** Result of {@link widenIntegerColumnsToBigInt}. */
|
|
57
|
+
export interface IntegerWidthWidenResult {
|
|
58
|
+
/** False when no legacy columns remain or the engine is unsupported. */
|
|
59
|
+
ran: boolean;
|
|
60
|
+
preflight: IntegerWidthPreflightResult;
|
|
61
|
+
widenedColumns: Array<{
|
|
62
|
+
table: string;
|
|
63
|
+
column: string;
|
|
64
|
+
}>;
|
|
65
|
+
statements: string[];
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Collect every integer column owned by loaded application schemas and, when
|
|
69
|
+
* requested, by hand-written core system-table DDL.
|
|
70
|
+
*/
|
|
71
|
+
export declare function collectIntegerWidthTargets(schemas: Record<string, SchemaDefinition>, options?: {
|
|
72
|
+
includeSystemTables?: boolean;
|
|
73
|
+
}): IntegerWidthTarget[];
|
|
74
|
+
/** Build the exact, lossless ALTER statement for one target column. */
|
|
75
|
+
export declare function buildIntegerWidthStatements(engine: string, table: string, column: string): string[];
|
|
76
|
+
/**
|
|
77
|
+
* Build lossless widening statements for every pending column in one table.
|
|
78
|
+
*
|
|
79
|
+
* PostgreSQL performs a table rewrite for an integer-width change, so all of
|
|
80
|
+
* one table's pending columns must share one ALTER TABLE and one lock window.
|
|
81
|
+
* DuckDB keeps its one-column form because its ALTER COLUMN grammar is not
|
|
82
|
+
* equivalently composable across supported adapter versions.
|
|
83
|
+
*/
|
|
84
|
+
export declare function buildIntegerWidthTableStatements(engine: string, table: string, columns: string[]): string[];
|
|
85
|
+
/**
|
|
86
|
+
* Inspect explicit SMRT-owned integer targets without changing the database.
|
|
87
|
+
*
|
|
88
|
+
* PostgreSQL and DuckDB expose their live widths through
|
|
89
|
+
* `information_schema.columns`. SQLite is deliberately a no-op because its
|
|
90
|
+
* INTEGER storage class already holds signed 64-bit values.
|
|
91
|
+
*/
|
|
92
|
+
export declare function preflightIntegerWidthWidening(db: DatabaseInterface, targets: IntegerWidthTarget[], options?: IntegerWidthOptions): Promise<IntegerWidthPreflightResult>;
|
|
93
|
+
/**
|
|
94
|
+
* Widen legacy int4 columns to BIGINT and record a marker after every ALTER
|
|
95
|
+
* succeeds. This is intentionally never called by normal schema migration or
|
|
96
|
+
* framework bootstrap: PostgreSQL rewrites each altered table.
|
|
97
|
+
*/
|
|
98
|
+
export declare function widenIntegerColumnsToBigInt(db: DatabaseInterface, targets: IntegerWidthTarget[], options: IntegerWidthWidenOptions): Promise<IntegerWidthWidenResult>;
|
|
99
|
+
//# sourceMappingURL=integer-width.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"integer-width.d.ts","sourceRoot":"","sources":["../../src/migrations/integer-width.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
import { toSafeInteger } from "../utils/safe-integer.js";
|
|
2
|
+
import { detectEngine } from "../schema/ddl/index.js";
|
|
3
|
+
import { getSystemTableShapes } from "../schema/system-table-shapes.js";
|
|
4
|
+
import { parsePostgresTimeoutMs } from "../postgres-timeouts.js";
|
|
5
|
+
import { BackfillTracker } from "./backfill-tracker.js";
|
|
6
|
+
//#region src/migrations/integer-width.ts
|
|
7
|
+
var DEFAULT_POSTGRES_LOCK_TIMEOUT_MS = 3e4;
|
|
8
|
+
var DEFAULT_POSTGRES_STATEMENT_TIMEOUT_MS = 6e4;
|
|
9
|
+
/**
|
|
10
|
+
* Collect every integer column owned by loaded application schemas and, when
|
|
11
|
+
* requested, by hand-written core system-table DDL.
|
|
12
|
+
*/
|
|
13
|
+
function collectIntegerWidthTargets(schemas, options = {}) {
|
|
14
|
+
const byTable = /* @__PURE__ */ new Map();
|
|
15
|
+
const add = (table, column) => {
|
|
16
|
+
if (!byTable.has(table)) byTable.set(table, /* @__PURE__ */ new Set());
|
|
17
|
+
byTable.get(table)?.add(column);
|
|
18
|
+
};
|
|
19
|
+
for (const [key, schema] of Object.entries(schemas)) {
|
|
20
|
+
const table = schema?.tableName || key;
|
|
21
|
+
for (const [column, definition] of Object.entries(schema?.columns ?? {})) if (definition.type === "INTEGER") add(table, column);
|
|
22
|
+
}
|
|
23
|
+
if (options.includeSystemTables ?? true) {
|
|
24
|
+
for (const shape of getSystemTableShapes("postgres").values()) for (const column of shape.columns) if (isBigIntType(column.type)) add(shape.tableName, column.name);
|
|
25
|
+
}
|
|
26
|
+
return [...byTable].map(([table, columns]) => ({
|
|
27
|
+
table,
|
|
28
|
+
columns: [...columns].sort()
|
|
29
|
+
})).sort((left, right) => left.table.localeCompare(right.table));
|
|
30
|
+
}
|
|
31
|
+
/** Build the exact, lossless ALTER statement for one target column. */
|
|
32
|
+
function buildIntegerWidthStatements(engine, table, column) {
|
|
33
|
+
return buildIntegerWidthTableStatements(engine, table, [column]);
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Build lossless widening statements for every pending column in one table.
|
|
37
|
+
*
|
|
38
|
+
* PostgreSQL performs a table rewrite for an integer-width change, so all of
|
|
39
|
+
* one table's pending columns must share one ALTER TABLE and one lock window.
|
|
40
|
+
* DuckDB keeps its one-column form because its ALTER COLUMN grammar is not
|
|
41
|
+
* equivalently composable across supported adapter versions.
|
|
42
|
+
*/
|
|
43
|
+
function buildIntegerWidthTableStatements(engine, table, columns) {
|
|
44
|
+
const quotedTable = quoteIdentifier(table);
|
|
45
|
+
const quotedColumns = columns.map(quoteIdentifier);
|
|
46
|
+
if (quotedColumns.length === 0) return [];
|
|
47
|
+
if (engine !== "postgres" && engine !== "duckdb") return [];
|
|
48
|
+
if (engine === "postgres") return [`ALTER TABLE ${quotedTable} ${quotedColumns.map((column) => `ALTER COLUMN ${column} TYPE BIGINT`).join(", ")}`];
|
|
49
|
+
return quotedColumns.map((column) => `ALTER TABLE ${quotedTable} ALTER COLUMN ${column} TYPE BIGINT`);
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Inspect explicit SMRT-owned integer targets without changing the database.
|
|
53
|
+
*
|
|
54
|
+
* PostgreSQL and DuckDB expose their live widths through
|
|
55
|
+
* `information_schema.columns`. SQLite is deliberately a no-op because its
|
|
56
|
+
* INTEGER storage class already holds signed 64-bit values.
|
|
57
|
+
*/
|
|
58
|
+
async function preflightIntegerWidthWidening(db, targets, options = {}) {
|
|
59
|
+
const engine = resolveEngine(db, options.engineHint);
|
|
60
|
+
const normalizedTargets = normalizeTargets(targets);
|
|
61
|
+
if (engine !== "postgres" && engine !== "duckdb") return {
|
|
62
|
+
engine,
|
|
63
|
+
supported: false,
|
|
64
|
+
tables: [],
|
|
65
|
+
pendingColumns: 0,
|
|
66
|
+
currentColumns: 0,
|
|
67
|
+
missingColumns: 0,
|
|
68
|
+
unexpectedColumns: 0,
|
|
69
|
+
pendingTables: 0,
|
|
70
|
+
summary: `Integer-width widening preflight (engine=${engine}): not applicable; this engine does not expose a distinct native int4 storage type.`
|
|
71
|
+
};
|
|
72
|
+
const tables = [];
|
|
73
|
+
for (const target of normalizedTargets) {
|
|
74
|
+
const types = await readColumnTypes(db, engine, target.table);
|
|
75
|
+
const columns = target.columns.map((column) => {
|
|
76
|
+
const declaredType = types.get(column) ?? null;
|
|
77
|
+
return {
|
|
78
|
+
table: target.table,
|
|
79
|
+
column,
|
|
80
|
+
declaredType,
|
|
81
|
+
state: classifyIntegerWidth(declaredType)
|
|
82
|
+
};
|
|
83
|
+
});
|
|
84
|
+
const needsWidening = columns.some((column) => column.state === "pending");
|
|
85
|
+
tables.push({
|
|
86
|
+
table: target.table,
|
|
87
|
+
rowCount: needsWidening ? await countRows(db, target.table) : null,
|
|
88
|
+
columns
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
const allColumns = tables.flatMap((table) => table.columns);
|
|
92
|
+
const result = {
|
|
93
|
+
engine,
|
|
94
|
+
supported: true,
|
|
95
|
+
tables,
|
|
96
|
+
pendingColumns: allColumns.filter((column) => column.state === "pending").length,
|
|
97
|
+
currentColumns: allColumns.filter((column) => column.state === "current").length,
|
|
98
|
+
missingColumns: allColumns.filter((column) => column.state === "missing").length,
|
|
99
|
+
unexpectedColumns: allColumns.filter((column) => column.state === "unexpected").length,
|
|
100
|
+
pendingTables: tables.filter((table) => table.columns.some((column) => column.state === "pending")).length
|
|
101
|
+
};
|
|
102
|
+
return {
|
|
103
|
+
...result,
|
|
104
|
+
summary: renderSummary(result)
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Widen legacy int4 columns to BIGINT and record a marker after every ALTER
|
|
109
|
+
* succeeds. This is intentionally never called by normal schema migration or
|
|
110
|
+
* framework bootstrap: PostgreSQL rewrites each altered table.
|
|
111
|
+
*/
|
|
112
|
+
async function widenIntegerColumnsToBigInt(db, targets, options) {
|
|
113
|
+
const preflight = await preflightIntegerWidthWidening(db, targets, options);
|
|
114
|
+
if (!preflight.supported) return {
|
|
115
|
+
ran: false,
|
|
116
|
+
preflight,
|
|
117
|
+
widenedColumns: [],
|
|
118
|
+
statements: []
|
|
119
|
+
};
|
|
120
|
+
if (preflight.unexpectedColumns > 0) throw new Error("Integer-width widening refused because declared integer columns have unexpected live types. Resolve ordinary schema drift first.\n" + preflight.summary);
|
|
121
|
+
if (preflight.pendingColumns === 0) return {
|
|
122
|
+
ran: false,
|
|
123
|
+
preflight,
|
|
124
|
+
widenedColumns: [],
|
|
125
|
+
statements: []
|
|
126
|
+
};
|
|
127
|
+
const statements = [];
|
|
128
|
+
const widenedColumns = [];
|
|
129
|
+
await new BackfillTracker({ db }).initialize();
|
|
130
|
+
const applyWidening = async (writeDb) => {
|
|
131
|
+
for (const table of preflight.tables) {
|
|
132
|
+
const pendingColumns = table.columns.filter((column) => column.state === "pending").map((column) => column.column);
|
|
133
|
+
if (pendingColumns.length === 0) continue;
|
|
134
|
+
for (const sql of buildIntegerWidthTableStatements(preflight.engine, table.table, pendingColumns)) {
|
|
135
|
+
await writeDb.query(sql);
|
|
136
|
+
statements.push(sql);
|
|
137
|
+
}
|
|
138
|
+
widenedColumns.push(...pendingColumns.map((column) => ({
|
|
139
|
+
table: table.table,
|
|
140
|
+
column
|
|
141
|
+
})));
|
|
142
|
+
}
|
|
143
|
+
await new BackfillTracker({ db: writeDb }).recordApplied(options.backfillName, {
|
|
144
|
+
description: "Widened legacy int4 SMRT columns to BIGINT after #2373 changed fresh-schema defaults.",
|
|
145
|
+
packageName: options.packageName
|
|
146
|
+
});
|
|
147
|
+
};
|
|
148
|
+
if (preflight.engine === "postgres") {
|
|
149
|
+
if (!db.transaction) throw new Error("Integer-width widening on PostgreSQL requires transaction support.");
|
|
150
|
+
const lockTimeout = parsePostgresTimeoutMs(options.lockTimeout, DEFAULT_POSTGRES_LOCK_TIMEOUT_MS);
|
|
151
|
+
const statementTimeout = parsePostgresTimeoutMs(options.statementTimeout, DEFAULT_POSTGRES_STATEMENT_TIMEOUT_MS);
|
|
152
|
+
await db.transaction(async (tx) => {
|
|
153
|
+
const txDb = {
|
|
154
|
+
...tx,
|
|
155
|
+
transaction: async (callback) => callback(tx)
|
|
156
|
+
};
|
|
157
|
+
BackfillTracker.inheritInitialization(txDb, db);
|
|
158
|
+
await txDb.query(`SET LOCAL lock_timeout = '${formatPostgresTimeout(lockTimeout)}'`);
|
|
159
|
+
await txDb.query(`SET LOCAL statement_timeout = '${formatPostgresTimeout(statementTimeout)}'`);
|
|
160
|
+
await applyWidening(txDb);
|
|
161
|
+
});
|
|
162
|
+
} else await applyWidening(db);
|
|
163
|
+
return {
|
|
164
|
+
ran: true,
|
|
165
|
+
preflight,
|
|
166
|
+
widenedColumns,
|
|
167
|
+
statements
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
function resolveEngine(db, engineHint) {
|
|
171
|
+
const dbWithConfig = db;
|
|
172
|
+
return detectEngine(db.url || dbWithConfig.config?.url || "", engineHint);
|
|
173
|
+
}
|
|
174
|
+
function normalizeTargets(targets) {
|
|
175
|
+
const byTable = /* @__PURE__ */ new Map();
|
|
176
|
+
for (const target of targets) {
|
|
177
|
+
quoteIdentifier(target.table);
|
|
178
|
+
if (!byTable.has(target.table)) byTable.set(target.table, /* @__PURE__ */ new Set());
|
|
179
|
+
for (const column of target.columns) {
|
|
180
|
+
quoteIdentifier(column);
|
|
181
|
+
byTable.get(target.table)?.add(column);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
return [...byTable].map(([table, columns]) => ({
|
|
185
|
+
table,
|
|
186
|
+
columns: [...columns].sort()
|
|
187
|
+
})).sort((left, right) => left.table.localeCompare(right.table));
|
|
188
|
+
}
|
|
189
|
+
function quoteIdentifier(name) {
|
|
190
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(name)) throw new Error(`Integer-width widening: '${name}' is not a plain SQL identifier.`);
|
|
191
|
+
return `"${name}"`;
|
|
192
|
+
}
|
|
193
|
+
function formatPostgresTimeout(milliseconds) {
|
|
194
|
+
return `${milliseconds}ms`;
|
|
195
|
+
}
|
|
196
|
+
function isLegacyInt4Type(type) {
|
|
197
|
+
return /^(INTEGER|INT|INT4)$/i.test(type.trim());
|
|
198
|
+
}
|
|
199
|
+
function isBigIntType(type) {
|
|
200
|
+
return /^(BIGINT|INT8)$/i.test(type.trim());
|
|
201
|
+
}
|
|
202
|
+
function classifyIntegerWidth(type) {
|
|
203
|
+
if (type === null) return "missing";
|
|
204
|
+
if (isLegacyInt4Type(type)) return "pending";
|
|
205
|
+
if (isBigIntType(type)) return "current";
|
|
206
|
+
return "unexpected";
|
|
207
|
+
}
|
|
208
|
+
async function readColumnTypes(db, engine, table) {
|
|
209
|
+
const result = await db.query(engine === "postgres" ? `SELECT column_name, data_type
|
|
210
|
+
FROM information_schema.columns
|
|
211
|
+
WHERE table_schema = current_schema() AND table_name = ?` : `SELECT column_name, data_type
|
|
212
|
+
FROM information_schema.columns
|
|
213
|
+
WHERE table_name = ?`, table);
|
|
214
|
+
const types = /* @__PURE__ */ new Map();
|
|
215
|
+
for (const row of result.rows) if (typeof row.column_name === "string") types.set(row.column_name, String(row.data_type ?? ""));
|
|
216
|
+
return types;
|
|
217
|
+
}
|
|
218
|
+
async function countRows(db, table) {
|
|
219
|
+
return toSafeInteger((await db.query(`SELECT COUNT(*) AS row_count FROM ${quoteIdentifier(table)}`)).rows?.[0]?.row_count ?? 0, `Integer-width row count for ${table}`);
|
|
220
|
+
}
|
|
221
|
+
function renderSummary(result) {
|
|
222
|
+
const lines = [
|
|
223
|
+
`Integer-width widening preflight (engine=${result.engine})`,
|
|
224
|
+
` tables: ${result.pendingTables} pending`,
|
|
225
|
+
` columns: ${result.pendingColumns} pending, ${result.currentColumns} already BIGINT, ${result.missingColumns} missing, ${result.unexpectedColumns} unexpected`
|
|
226
|
+
];
|
|
227
|
+
for (const table of result.tables) {
|
|
228
|
+
const pending = table.columns.filter((column) => column.state === "pending");
|
|
229
|
+
if (pending.length === 0) continue;
|
|
230
|
+
lines.push(` - ${table.table}: ${table.rowCount ?? 0} row(s); ${pending.map((column) => `${column.column} (${column.declaredType})`).join(", ")}`);
|
|
231
|
+
}
|
|
232
|
+
return lines.join("\n");
|
|
233
|
+
}
|
|
234
|
+
//#endregion
|
|
235
|
+
export { buildIntegerWidthStatements, buildIntegerWidthTableStatements, collectIntegerWidthTargets, preflightIntegerWidthWidening, widenIntegerColumnsToBigInt };
|
|
236
|
+
|
|
237
|
+
//# sourceMappingURL=integer-width.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"integer-width.js","names":[],"sources":["../../src/migrations/integer-width.ts"],"sourcesContent":["/**\n * Explicit one-time widening for deployments created before #2373.\n *\n * Fresh PostgreSQL and DuckDB schemas now materialize SMRT's abstract\n * `INTEGER` as `BIGINT`. The migration differ deliberately treats int4 and\n * int8 as equivalent, so existing deployments need this separate, operator\n * initiated maintenance-window migration rather than an automatic repair.\n */\n\nimport type { DatabaseInterface } from '@happyvertical/sql';\nimport { parsePostgresTimeoutMs } from '../postgres-timeouts.js';\nimport { detectEngine } from '../schema/ddl/index.js';\nimport { getSystemTableShapes } from '../schema/system-table-shapes.js';\nimport type { SchemaDefinition } from '../schema/types.js';\nimport { toSafeInteger } from '../utils/safe-integer.js';\nimport { BackfillTracker } from './backfill-tracker.js';\n\nconst DEFAULT_POSTGRES_LOCK_TIMEOUT_MS = 30_000;\nconst DEFAULT_POSTGRES_STATEMENT_TIMEOUT_MS = 60_000;\n\n/** A table and its SMRT-owned integer columns. */\nexport interface IntegerWidthTarget {\n table: string;\n columns: string[];\n}\n\n/** The width state of one declared integer column. */\nexport type IntegerWidthColumnState =\n | 'pending'\n | 'current'\n | 'missing'\n | 'unexpected';\n\n/** Preflight finding for one declared integer column. */\nexport interface IntegerWidthColumnReport {\n table: string;\n column: string;\n state: IntegerWidthColumnState;\n /** Live `information_schema.columns.data_type`, when the column exists. */\n declaredType: string | null;\n}\n\n/** Preflight findings for a single SMRT-owned table. */\nexport interface IntegerWidthTableReport {\n table: string;\n /** Number of rows when at least one column still needs widening. */\n rowCount: number | null;\n columns: IntegerWidthColumnReport[];\n}\n\n/** Read-only assessment of a deployment's legacy int4 columns. */\nexport interface IntegerWidthPreflightResult {\n engine: string;\n /** False for SQLite and adapters without a native 32-bit integer width. */\n supported: boolean;\n tables: IntegerWidthTableReport[];\n pendingColumns: number;\n currentColumns: number;\n missingColumns: number;\n unexpectedColumns: number;\n /** Tables that need a maintenance-window table rewrite. */\n pendingTables: number;\n /** Human-readable output suitable for an operator change record. */\n summary: string;\n}\n\n/** Shared options for the read-only preflight and mutating widening pass. */\nexport interface IntegerWidthOptions {\n /** Adapter hint when its URL does not identify the engine. */\n engineHint?: string;\n}\n\n/** Options that make the widening pass safely repeatable. */\nexport interface IntegerWidthWidenOptions extends IntegerWidthOptions {\n /** Stable `_smrt_backfills` audit marker, unique to this migration owner. */\n backfillName: string;\n /** Package recorded alongside the marker. */\n packageName?: string;\n /** PostgreSQL lock timeout in milliseconds (defaults to 30 seconds). */\n lockTimeout?: number;\n /** PostgreSQL statement timeout in milliseconds (defaults to 60 seconds). */\n statementTimeout?: number;\n}\n\n/** Result of {@link widenIntegerColumnsToBigInt}. */\nexport interface IntegerWidthWidenResult {\n /** False when no legacy columns remain or the engine is unsupported. */\n ran: boolean;\n preflight: IntegerWidthPreflightResult;\n widenedColumns: Array<{ table: string; column: string }>;\n statements: string[];\n}\n\n/**\n * Collect every integer column owned by loaded application schemas and, when\n * requested, by hand-written core system-table DDL.\n */\nexport function collectIntegerWidthTargets(\n schemas: Record<string, SchemaDefinition>,\n options: { includeSystemTables?: boolean } = {},\n): IntegerWidthTarget[] {\n const byTable = new Map<string, Set<string>>();\n const add = (table: string, column: string) => {\n if (!byTable.has(table)) byTable.set(table, new Set());\n byTable.get(table)?.add(column);\n };\n\n for (const [key, schema] of Object.entries(schemas)) {\n const table = schema?.tableName || key;\n for (const [column, definition] of Object.entries(schema?.columns ?? {})) {\n if (definition.type === 'INTEGER') add(table, column);\n }\n }\n\n if (options.includeSystemTables ?? true) {\n for (const shape of getSystemTableShapes('postgres').values()) {\n for (const column of shape.columns) {\n if (isBigIntType(column.type)) add(shape.tableName, column.name);\n }\n }\n }\n\n return [...byTable]\n .map(([table, columns]) => ({ table, columns: [...columns].sort() }))\n .sort((left, right) => left.table.localeCompare(right.table));\n}\n\n/** Build the exact, lossless ALTER statement for one target column. */\nexport function buildIntegerWidthStatements(\n engine: string,\n table: string,\n column: string,\n): string[] {\n return buildIntegerWidthTableStatements(engine, table, [column]);\n}\n\n/**\n * Build lossless widening statements for every pending column in one table.\n *\n * PostgreSQL performs a table rewrite for an integer-width change, so all of\n * one table's pending columns must share one ALTER TABLE and one lock window.\n * DuckDB keeps its one-column form because its ALTER COLUMN grammar is not\n * equivalently composable across supported adapter versions.\n */\nexport function buildIntegerWidthTableStatements(\n engine: string,\n table: string,\n columns: string[],\n): string[] {\n const quotedTable = quoteIdentifier(table);\n const quotedColumns = columns.map(quoteIdentifier);\n if (quotedColumns.length === 0) return [];\n if (engine !== 'postgres' && engine !== 'duckdb') return [];\n if (engine === 'postgres') {\n return [\n `ALTER TABLE ${quotedTable} ${quotedColumns\n .map((column) => `ALTER COLUMN ${column} TYPE BIGINT`)\n .join(', ')}`,\n ];\n }\n return quotedColumns.map(\n (column) => `ALTER TABLE ${quotedTable} ALTER COLUMN ${column} TYPE BIGINT`,\n );\n}\n\n/**\n * Inspect explicit SMRT-owned integer targets without changing the database.\n *\n * PostgreSQL and DuckDB expose their live widths through\n * `information_schema.columns`. SQLite is deliberately a no-op because its\n * INTEGER storage class already holds signed 64-bit values.\n */\nexport async function preflightIntegerWidthWidening(\n db: DatabaseInterface,\n targets: IntegerWidthTarget[],\n options: IntegerWidthOptions = {},\n): Promise<IntegerWidthPreflightResult> {\n const engine = resolveEngine(db, options.engineHint);\n const normalizedTargets = normalizeTargets(targets);\n if (engine !== 'postgres' && engine !== 'duckdb') {\n const result = {\n engine,\n supported: false,\n tables: [],\n pendingColumns: 0,\n currentColumns: 0,\n missingColumns: 0,\n unexpectedColumns: 0,\n pendingTables: 0,\n };\n return {\n ...result,\n summary:\n `Integer-width widening preflight (engine=${engine}): not applicable; ` +\n 'this engine does not expose a distinct native int4 storage type.',\n };\n }\n\n const tables: IntegerWidthTableReport[] = [];\n for (const target of normalizedTargets) {\n const types = await readColumnTypes(db, engine, target.table);\n const columns = target.columns.map((column) => {\n const declaredType = types.get(column) ?? null;\n return {\n table: target.table,\n column,\n declaredType,\n state: classifyIntegerWidth(declaredType),\n };\n });\n const needsWidening = columns.some((column) => column.state === 'pending');\n tables.push({\n table: target.table,\n rowCount: needsWidening ? await countRows(db, target.table) : null,\n columns,\n });\n }\n\n const allColumns = tables.flatMap((table) => table.columns);\n const result = {\n engine,\n supported: true,\n tables,\n pendingColumns: allColumns.filter((column) => column.state === 'pending')\n .length,\n currentColumns: allColumns.filter((column) => column.state === 'current')\n .length,\n missingColumns: allColumns.filter((column) => column.state === 'missing')\n .length,\n unexpectedColumns: allColumns.filter(\n (column) => column.state === 'unexpected',\n ).length,\n pendingTables: tables.filter((table) =>\n table.columns.some((column) => column.state === 'pending'),\n ).length,\n };\n return { ...result, summary: renderSummary(result) };\n}\n\n/**\n * Widen legacy int4 columns to BIGINT and record a marker after every ALTER\n * succeeds. This is intentionally never called by normal schema migration or\n * framework bootstrap: PostgreSQL rewrites each altered table.\n */\nexport async function widenIntegerColumnsToBigInt(\n db: DatabaseInterface,\n targets: IntegerWidthTarget[],\n options: IntegerWidthWidenOptions,\n): Promise<IntegerWidthWidenResult> {\n const preflight = await preflightIntegerWidthWidening(db, targets, options);\n if (!preflight.supported) {\n return { ran: false, preflight, widenedColumns: [], statements: [] };\n }\n if (preflight.unexpectedColumns > 0) {\n throw new Error(\n 'Integer-width widening refused because declared integer columns have unexpected live types. Resolve ordinary schema drift first.\\n' +\n preflight.summary,\n );\n }\n\n // Current declared widths are the idempotency guard. A fixed marker cannot\n // safely be the guard because a later successful discovery may reveal a\n // package/table absent from the earlier target set.\n if (preflight.pendingColumns === 0) {\n return { ran: false, preflight, widenedColumns: [], statements: [] };\n }\n\n const statements: string[] = [];\n const widenedColumns: Array<{ table: string; column: string }> = [];\n await new BackfillTracker({ db }).initialize();\n\n const applyWidening = async (writeDb: DatabaseInterface) => {\n for (const table of preflight.tables) {\n const pendingColumns = table.columns\n .filter((column) => column.state === 'pending')\n .map((column) => column.column);\n if (pendingColumns.length === 0) continue;\n for (const sql of buildIntegerWidthTableStatements(\n preflight.engine,\n table.table,\n pendingColumns,\n )) {\n await writeDb.query(sql);\n statements.push(sql);\n }\n widenedColumns.push(\n ...pendingColumns.map((column) => ({ table: table.table, column })),\n );\n }\n\n await new BackfillTracker({ db: writeDb }).recordApplied(\n options.backfillName,\n {\n description:\n 'Widened legacy int4 SMRT columns to BIGINT after #2373 changed fresh-schema defaults.',\n packageName: options.packageName,\n },\n );\n };\n\n if (preflight.engine === 'postgres') {\n if (!db.transaction) {\n throw new Error(\n 'Integer-width widening on PostgreSQL requires transaction support.',\n );\n }\n const lockTimeout = parsePostgresTimeoutMs(\n options.lockTimeout,\n DEFAULT_POSTGRES_LOCK_TIMEOUT_MS,\n );\n const statementTimeout = parsePostgresTimeoutMs(\n options.statementTimeout,\n DEFAULT_POSTGRES_STATEMENT_TIMEOUT_MS,\n );\n await db.transaction(async (tx) => {\n const txDb = {\n ...tx,\n transaction: async <T>(\n callback: (transactionDb: DatabaseInterface) => Promise<T>,\n ) => callback(tx as DatabaseInterface),\n } as DatabaseInterface;\n BackfillTracker.inheritInitialization(txDb, db);\n await txDb.query(\n `SET LOCAL lock_timeout = '${formatPostgresTimeout(lockTimeout)}'`,\n );\n await txDb.query(\n `SET LOCAL statement_timeout = '${formatPostgresTimeout(statementTimeout)}'`,\n );\n await applyWidening(txDb);\n });\n } else {\n await applyWidening(db);\n }\n\n return { ran: true, preflight, widenedColumns, statements };\n}\n\nfunction resolveEngine(db: DatabaseInterface, engineHint?: string): string {\n const dbWithConfig = db as DatabaseInterface & { config?: { url?: string } };\n return detectEngine(db.url || dbWithConfig.config?.url || '', engineHint);\n}\n\nfunction normalizeTargets(targets: IntegerWidthTarget[]): IntegerWidthTarget[] {\n const byTable = new Map<string, Set<string>>();\n for (const target of targets) {\n quoteIdentifier(target.table);\n if (!byTable.has(target.table)) byTable.set(target.table, new Set());\n for (const column of target.columns) {\n quoteIdentifier(column);\n byTable.get(target.table)?.add(column);\n }\n }\n return [...byTable]\n .map(([table, columns]) => ({ table, columns: [...columns].sort() }))\n .sort((left, right) => left.table.localeCompare(right.table));\n}\n\nfunction quoteIdentifier(name: string): string {\n if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(name)) {\n throw new Error(\n `Integer-width widening: '${name}' is not a plain SQL identifier.`,\n );\n }\n return `\"${name}\"`;\n}\n\nfunction formatPostgresTimeout(milliseconds: number): string {\n return `${milliseconds}ms`;\n}\n\nfunction isLegacyInt4Type(type: string): boolean {\n return /^(INTEGER|INT|INT4)$/i.test(type.trim());\n}\n\nfunction isBigIntType(type: string): boolean {\n return /^(BIGINT|INT8)$/i.test(type.trim());\n}\n\nfunction classifyIntegerWidth(type: string | null): IntegerWidthColumnState {\n if (type === null) return 'missing';\n if (isLegacyInt4Type(type)) return 'pending';\n if (isBigIntType(type)) return 'current';\n return 'unexpected';\n}\n\nasync function readColumnTypes(\n db: DatabaseInterface,\n engine: string,\n table: string,\n): Promise<Map<string, string>> {\n const result = await db.query(\n engine === 'postgres'\n ? `SELECT column_name, data_type\n FROM information_schema.columns\n WHERE table_schema = current_schema() AND table_name = ?`\n : `SELECT column_name, data_type\n FROM information_schema.columns\n WHERE table_name = ?`,\n table,\n );\n const types = new Map<string, string>();\n for (const row of result.rows as {\n column_name?: unknown;\n data_type?: unknown;\n }[]) {\n if (typeof row.column_name === 'string') {\n types.set(row.column_name, String(row.data_type ?? ''));\n }\n }\n return types;\n}\n\nasync function countRows(\n db: DatabaseInterface,\n table: string,\n): Promise<number> {\n const result = await db.query(\n `SELECT COUNT(*) AS row_count FROM ${quoteIdentifier(table)}`,\n );\n return toSafeInteger(\n result.rows?.[0]?.row_count ?? 0,\n `Integer-width row count for ${table}`,\n );\n}\n\nfunction renderSummary(\n result: Omit<IntegerWidthPreflightResult, 'summary'>,\n): string {\n const lines = [\n `Integer-width widening preflight (engine=${result.engine})`,\n ` tables: ${result.pendingTables} pending`,\n ` columns: ${result.pendingColumns} pending, ${result.currentColumns} already BIGINT, ${result.missingColumns} missing, ${result.unexpectedColumns} unexpected`,\n ];\n for (const table of result.tables) {\n const pending = table.columns.filter(\n (column) => column.state === 'pending',\n );\n if (pending.length === 0) continue;\n lines.push(\n ` - ${table.table}: ${table.rowCount ?? 0} row(s); ${pending\n .map((column) => `${column.column} (${column.declaredType})`)\n .join(', ')}`,\n );\n }\n return lines.join('\\n');\n}\n"],"mappings":"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"}
|
|
@@ -34,7 +34,7 @@ export interface MinorUnitsColumnReport {
|
|
|
34
34
|
inspectedRows: number;
|
|
35
35
|
/** Values whose scaled form is not a whole number — rounding loses money. */
|
|
36
36
|
nonIntegral: MinorUnitsProblemRow[];
|
|
37
|
-
/** Values whose scaled form
|
|
37
|
+
/** Values whose scaled form exceeds JavaScript's safe integer range. */
|
|
38
38
|
overflow: MinorUnitsProblemRow[];
|
|
39
39
|
}
|
|
40
40
|
/** Result of {@link preflightMinorUnitsRescale}. */
|
|
@@ -50,7 +50,7 @@ export interface MinorUnitsPreflightResult {
|
|
|
50
50
|
missingColumns: number;
|
|
51
51
|
/** Total rows that would be rounded. */
|
|
52
52
|
nonIntegralRows: number;
|
|
53
|
-
/** Total rows that would
|
|
53
|
+
/** Total rows that would exceed JavaScript's safe integer range. */
|
|
54
54
|
overflowRows: number;
|
|
55
55
|
/** No row would lose information. */
|
|
56
56
|
ok: boolean;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"minor-units.d.ts","sourceRoot":"","sources":["../../src/migrations/minor-units.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"minor-units.d.ts","sourceRoot":"","sources":["../../src/migrations/minor-units.ts"],"names":[],"mappings":"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"}
|
|
@@ -6,20 +6,22 @@ import { createLogger } from "@happyvertical/logger";
|
|
|
6
6
|
* Money-column rescale: floating-point major units → integer minor units (#2401).
|
|
7
7
|
*
|
|
8
8
|
* SMRT's rule is that money is exact and is stored as **integer minor units**
|
|
9
|
-
*
|
|
9
|
+
* at an application-defined scale — `$19.99` is `1999` at a cents scale.
|
|
10
|
+
* Packages that predate the rule
|
|
10
11
|
* declared their money fields `= 0.0`, which compiles to a floating-point
|
|
11
12
|
* column holding *major* units. Flipping the declaration to `= 0` changes the
|
|
12
13
|
* column type, and REAL→INTEGER is deliberately **not** one of the differ's
|
|
13
|
-
* whitelisted upgrades: an automatic `ALTER … TYPE
|
|
14
|
-
* `19.99` to `19`, silently destroying
|
|
14
|
+
* whitelisted upgrades: an automatic `ALTER … TYPE BIGINT` would truncate
|
|
15
|
+
* `19.99` to `19`, silently destroying minor units on every row.
|
|
15
16
|
*
|
|
16
17
|
* So the conversion is an explicit, opt-in migration with two halves:
|
|
17
18
|
*
|
|
18
19
|
* 1. {@link preflightMinorUnitsRescale} — read-only. Reports, per column,
|
|
19
20
|
* whether it still needs converting, and lists the rows that would lose
|
|
20
21
|
* information: values whose scaled form is not a whole number (a half-cent
|
|
21
|
-
* that has to be rounded away) and values that
|
|
22
|
-
* once multiplied by the scale. Run it against a production
|
|
22
|
+
* that has to be rounded away) and values that exceed JavaScript's safe
|
|
23
|
+
* integer range once multiplied by the scale. Run it against a production
|
|
24
|
+
* snapshot and
|
|
23
25
|
* read the summary before converting anything.
|
|
24
26
|
* 2. {@link rescaleMoneyColumnsToMinorUnits} — the conversion itself, guarded
|
|
25
27
|
* by a {@link BackfillTracker} marker so re-running it can never multiply a
|
|
@@ -29,8 +31,8 @@ import { createLogger } from "@happyvertical/logger";
|
|
|
29
31
|
*
|
|
30
32
|
* | Engine | Column type | Values |
|
|
31
33
|
* | --- | --- | --- |
|
|
32
|
-
* | PostgreSQL | `ALTER COLUMN … TYPE
|
|
33
|
-
* | DuckDB | `ALTER COLUMN … TYPE
|
|
34
|
+
* | PostgreSQL | `ALTER COLUMN … TYPE BIGINT USING round(col * scale)` | converted by the cast |
|
|
35
|
+
* | DuckDB | `ALTER COLUMN … TYPE BIGINT USING round(col * scale)` | converted by the cast |
|
|
34
36
|
* | SQLite | **not changed here** — SQLite cannot alter a column's declared type in place | rescaled with an `UPDATE`, values become exact integers |
|
|
35
37
|
*
|
|
36
38
|
* SQLite's declared type only sets an affinity, so a rescaled column already
|
|
@@ -40,9 +42,9 @@ import { createLogger } from "@happyvertical/logger";
|
|
|
40
42
|
* pretending the column type moved.
|
|
41
43
|
*/
|
|
42
44
|
var logger = createLogger({ level: "info" });
|
|
43
|
-
/**
|
|
44
|
-
var
|
|
45
|
-
var
|
|
45
|
+
/** Hydrated integers must remain exactly representable in JavaScript. */
|
|
46
|
+
var MIN_SAFE_INTEGER = -Number.MAX_SAFE_INTEGER;
|
|
47
|
+
var MAX_SAFE_INTEGER = Number.MAX_SAFE_INTEGER;
|
|
46
48
|
/**
|
|
47
49
|
* How far a scaled value may sit from a whole number and still count as
|
|
48
50
|
* integral.
|
|
@@ -78,7 +80,7 @@ function roundHalfAwayFromZero(value) {
|
|
|
78
80
|
var MinorUnitsPreflightError = class extends Error {
|
|
79
81
|
preflight;
|
|
80
82
|
constructor(preflight) {
|
|
81
|
-
super(`Money minor-units rescale refused: ${preflight.nonIntegralRows} row(s) would be rounded and ${preflight.overflowRows} row(s) would overflow
|
|
83
|
+
super(`Money minor-units rescale refused: ${preflight.nonIntegralRows} row(s) would be rounded and ${preflight.overflowRows} row(s) would overflow JavaScript's safe integer range. Fix the data or pass \`force: true\` to accept the rounding.
|
|
82
84
|
` + preflight.summary);
|
|
83
85
|
this.preflight = preflight;
|
|
84
86
|
this.name = "MinorUnitsPreflightError";
|
|
@@ -172,7 +174,7 @@ async function inspectColumn(db, table, column, scale, maxProblemRows, withId) {
|
|
|
172
174
|
scaled
|
|
173
175
|
});
|
|
174
176
|
}
|
|
175
|
-
if (rounded >
|
|
177
|
+
if (rounded > MAX_SAFE_INTEGER || rounded < MIN_SAFE_INTEGER) {
|
|
176
178
|
if (overflow.length < maxProblemRows) overflow.push({
|
|
177
179
|
id,
|
|
178
180
|
value,
|
|
@@ -191,7 +193,7 @@ function renderSummary(result) {
|
|
|
191
193
|
`Money minor-units preflight (engine=${result.engine}, scale=${result.scale})`,
|
|
192
194
|
` columns: ${result.pendingColumns} pending, ${result.convertedColumns} already integer, ${result.missingColumns} missing`,
|
|
193
195
|
` rows that would be rounded: ${result.nonIntegralRows}`,
|
|
194
|
-
` rows that would
|
|
196
|
+
` rows that would exceed JavaScript's safe integer range: ${result.overflowRows}`
|
|
195
197
|
];
|
|
196
198
|
for (const column of result.columns) {
|
|
197
199
|
const label = `${column.table}.${column.column}`;
|
|
@@ -229,7 +231,7 @@ function buildMinorUnitsStatements(engine, table, column, scale) {
|
|
|
229
231
|
const scaled = engine === "postgres" ? `round((${quotedColumn})::numeric * ${scale})` : `round(${quotedColumn} * ${scale})`;
|
|
230
232
|
return [
|
|
231
233
|
`ALTER TABLE ${quotedTable} ALTER COLUMN ${quotedColumn} DROP DEFAULT`,
|
|
232
|
-
`ALTER TABLE ${quotedTable} ALTER COLUMN ${quotedColumn} TYPE
|
|
234
|
+
`ALTER TABLE ${quotedTable} ALTER COLUMN ${quotedColumn} TYPE BIGINT USING ${scaled}`,
|
|
233
235
|
`ALTER TABLE ${quotedTable} ALTER COLUMN ${quotedColumn} SET DEFAULT 0`
|
|
234
236
|
];
|
|
235
237
|
}
|