@happyvertical/smrt-core 0.40.70 → 0.41.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.
Files changed (157) hide show
  1. package/AGENTS.md +2 -2
  2. package/agents/change-feed.md +1 -1
  3. package/agents/schema-paths.md +487 -29
  4. package/dist/browser.js +5 -4
  5. package/dist/cascade.d.ts +120 -0
  6. package/dist/cascade.d.ts.map +1 -0
  7. package/dist/cascade.js +430 -0
  8. package/dist/cascade.js.map +1 -0
  9. package/dist/change-feed.d.ts +34 -2
  10. package/dist/change-feed.d.ts.map +1 -1
  11. package/dist/change-feed.js +52 -11
  12. package/dist/change-feed.js.map +1 -1
  13. package/dist/class.d.ts +36 -3
  14. package/dist/class.d.ts.map +1 -1
  15. package/dist/class.js +59 -5
  16. package/dist/class.js.map +1 -1
  17. package/dist/collection-cache.js +0 -0
  18. package/dist/collection-cache.js.map +1 -1
  19. package/dist/collection.d.ts +29 -0
  20. package/dist/collection.d.ts.map +1 -1
  21. package/dist/collection.js +66 -14
  22. package/dist/collection.js.map +1 -1
  23. package/dist/config.d.ts +10 -0
  24. package/dist/config.d.ts.map +1 -1
  25. package/dist/config.js.map +1 -1
  26. package/dist/decorators/index.d.ts +63 -5
  27. package/dist/decorators/index.d.ts.map +1 -1
  28. package/dist/decorators/index.js +37 -5
  29. package/dist/decorators/index.js.map +1 -1
  30. package/dist/dispatch/collections/Dispatches.d.ts.map +1 -1
  31. package/dist/dispatch/collections/Dispatches.js +19 -4
  32. package/dist/dispatch/collections/Dispatches.js.map +1 -1
  33. package/dist/dispatch/types.d.ts +5 -0
  34. package/dist/dispatch/types.d.ts.map +1 -1
  35. package/dist/embedded-write-queue.d.ts +46 -0
  36. package/dist/embedded-write-queue.d.ts.map +1 -0
  37. package/dist/embedded-write-queue.js +66 -0
  38. package/dist/embedded-write-queue.js.map +1 -0
  39. package/dist/embeddings/storage.d.ts +7 -0
  40. package/dist/embeddings/storage.d.ts.map +1 -1
  41. package/dist/embeddings/storage.js +29 -12
  42. package/dist/embeddings/storage.js.map +1 -1
  43. package/dist/errors.d.ts +1 -1
  44. package/dist/hierarchical.js +1 -1
  45. package/dist/index.d.ts +2 -1
  46. package/dist/index.d.ts.map +1 -1
  47. package/dist/index.js +8 -6
  48. package/dist/interceptors.d.ts +21 -0
  49. package/dist/interceptors.d.ts.map +1 -1
  50. package/dist/interceptors.js +27 -1
  51. package/dist/interceptors.js.map +1 -1
  52. package/dist/manifest/generator.d.ts.map +1 -1
  53. package/dist/manifest/generator.js +4 -7
  54. package/dist/manifest/generator.js.map +1 -1
  55. package/dist/manifest/static-manifest.js +10 -10
  56. package/dist/manifest/static-manifest.js.map +1 -1
  57. package/dist/manifest/store.js +1 -1
  58. package/dist/manifest/store.js.map +1 -1
  59. package/dist/manifest.json +19 -19
  60. package/dist/migrations/differ.d.ts +9 -1
  61. package/dist/migrations/differ.d.ts.map +1 -1
  62. package/dist/migrations/differ.js +10 -5
  63. package/dist/migrations/differ.js.map +1 -1
  64. package/dist/migrations/index.d.ts +1 -0
  65. package/dist/migrations/index.d.ts.map +1 -1
  66. package/dist/migrations/index.js +2 -1
  67. package/dist/migrations/minor-units.d.ts +162 -0
  68. package/dist/migrations/minor-units.d.ts.map +1 -0
  69. package/dist/migrations/minor-units.js +381 -0
  70. package/dist/migrations/minor-units.js.map +1 -0
  71. package/dist/migrations.js +2 -1
  72. package/dist/object.d.ts +98 -5
  73. package/dist/object.d.ts.map +1 -1
  74. package/dist/object.js +138 -30
  75. package/dist/object.js.map +1 -1
  76. package/dist/query-bounds.d.ts +5 -1
  77. package/dist/query-bounds.d.ts.map +1 -1
  78. package/dist/query-bounds.js +5 -1
  79. package/dist/query-bounds.js.map +1 -1
  80. package/dist/registry/class-registration.d.ts.map +1 -1
  81. package/dist/registry/class-registration.js +3 -1
  82. package/dist/registry/class-registration.js.map +1 -1
  83. package/dist/registry/manifest-field-merge.d.ts +12 -0
  84. package/dist/registry/manifest-field-merge.d.ts.map +1 -1
  85. package/dist/registry/manifest-field-merge.js +14 -2
  86. package/dist/registry/manifest-field-merge.js.map +1 -1
  87. package/dist/registry/schema-builder.d.ts +22 -1
  88. package/dist/registry/schema-builder.d.ts.map +1 -1
  89. package/dist/registry/schema-builder.js +205 -165
  90. package/dist/registry/schema-builder.js.map +1 -1
  91. package/dist/registry/types.d.ts +3 -2
  92. package/dist/registry/types.d.ts.map +1 -1
  93. package/dist/registry.d.ts +41 -46
  94. package/dist/registry.d.ts.map +1 -1
  95. package/dist/registry.js +61 -83
  96. package/dist/registry.js.map +1 -1
  97. package/dist/scanner/manifest-generator.d.ts +45 -0
  98. package/dist/scanner/manifest-generator.d.ts.map +1 -1
  99. package/dist/scanner/manifest-generator.js +92 -28
  100. package/dist/scanner/manifest-generator.js.map +1 -1
  101. package/dist/schema/conflict-target.d.ts +104 -0
  102. package/dist/schema/conflict-target.d.ts.map +1 -0
  103. package/dist/schema/conflict-target.js +129 -0
  104. package/dist/schema/conflict-target.js.map +1 -0
  105. package/dist/schema/ddl/duckdb-strategy.d.ts.map +1 -1
  106. package/dist/schema/ddl/duckdb-strategy.js +2 -1
  107. package/dist/schema/ddl/duckdb-strategy.js.map +1 -1
  108. package/dist/schema/ddl/postgres-strategy.d.ts.map +1 -1
  109. package/dist/schema/ddl/postgres-strategy.js +12 -1
  110. package/dist/schema/ddl/postgres-strategy.js.map +1 -1
  111. package/dist/schema/generator.d.ts +252 -16
  112. package/dist/schema/generator.d.ts.map +1 -1
  113. package/dist/schema/generator.js +411 -106
  114. package/dist/schema/generator.js.map +1 -1
  115. package/dist/schema/index-utils.d.ts +120 -0
  116. package/dist/schema/index-utils.d.ts.map +1 -1
  117. package/dist/schema/index-utils.js +242 -1
  118. package/dist/schema/index-utils.js.map +1 -1
  119. package/dist/schema/index.d.ts +1 -1
  120. package/dist/schema/index.d.ts.map +1 -1
  121. package/dist/schema/index.js +2 -2
  122. package/dist/schema/live-parity.d.ts.map +1 -1
  123. package/dist/schema/live-parity.js +4 -2
  124. package/dist/schema/live-parity.js.map +1 -1
  125. package/dist/schema/system-table-shapes.d.ts +13 -0
  126. package/dist/schema/system-table-shapes.d.ts.map +1 -1
  127. package/dist/schema/system-table-shapes.js +14 -1
  128. package/dist/schema/system-table-shapes.js.map +1 -1
  129. package/dist/schema/utils.d.ts +1 -1
  130. package/dist/schema/utils.d.ts.map +1 -1
  131. package/dist/schema/utils.js +3 -3
  132. package/dist/schema/utils.js.map +1 -1
  133. package/dist/schema.js +2 -2
  134. package/dist/smrt-knowledge.json +9 -9
  135. package/dist/system/compatibility.d.ts +42 -0
  136. package/dist/system/compatibility.d.ts.map +1 -1
  137. package/dist/system/compatibility.js +182 -9
  138. package/dist/system/compatibility.js.map +1 -1
  139. package/dist/system/index.d.ts +1 -0
  140. package/dist/system/index.d.ts.map +1 -1
  141. package/dist/system/index.js +3 -2
  142. package/dist/system/retention.d.ts +237 -0
  143. package/dist/system/retention.d.ts.map +1 -0
  144. package/dist/system/retention.js +497 -0
  145. package/dist/system/retention.js.map +1 -0
  146. package/dist/system/schema.d.ts +100 -15
  147. package/dist/system/schema.d.ts.map +1 -1
  148. package/dist/system/schema.js +81 -45
  149. package/dist/system/schema.js.map +1 -1
  150. package/dist/system/types.d.ts +0 -2
  151. package/dist/system/types.d.ts.map +1 -1
  152. package/dist/testing/database.d.ts.map +1 -1
  153. package/dist/testing/database.js.map +1 -1
  154. package/dist/vite-plugin/index.d.ts.map +1 -1
  155. package/dist/vite-plugin/index.js +4 -9
  156. package/dist/vite-plugin/index.js.map +1 -1
  157. package/package.json +4 -4
@@ -0,0 +1,162 @@
1
+ import { DatabaseInterface } from './types.js';
2
+ /** A table and the money columns on it that must move to minor units. */
3
+ export interface MoneyColumnTarget {
4
+ /** Physical table name, e.g. `payments`. */
5
+ table: string;
6
+ /** Physical column names, e.g. `['amount', 'native_amount']`. */
7
+ columns: string[];
8
+ }
9
+ /** What the preflight found about one column. */
10
+ export type MinorUnitsColumnState =
11
+ /** Floating-point today: needs converting. */
12
+ 'pending'
13
+ /** Already an integer column: nothing to do. */
14
+ | 'converted'
15
+ /** Table or column absent (package not installed, older schema). */
16
+ | 'missing';
17
+ /** One row that cannot be converted without losing information. */
18
+ export interface MinorUnitsProblemRow {
19
+ /** Primary key, when the table has an `id` column. */
20
+ id: string | null;
21
+ /** The stored major-unit value. */
22
+ value: number;
23
+ /** `value * scale`, before rounding. */
24
+ scaled: number;
25
+ }
26
+ /** Preflight findings for a single column. */
27
+ export interface MinorUnitsColumnReport {
28
+ table: string;
29
+ column: string;
30
+ state: MinorUnitsColumnState;
31
+ /** Declared column type as the database reports it, when resolvable. */
32
+ declaredType: string | null;
33
+ /** Non-null values inspected. `0` unless `state` is `pending`. */
34
+ inspectedRows: number;
35
+ /** Values whose scaled form is not a whole number — rounding loses money. */
36
+ nonIntegral: MinorUnitsProblemRow[];
37
+ /** Values whose scaled form falls outside PostgreSQL `int4`. */
38
+ overflow: MinorUnitsProblemRow[];
39
+ }
40
+ /** Result of {@link preflightMinorUnitsRescale}. */
41
+ export interface MinorUnitsPreflightResult {
42
+ engine: string;
43
+ scale: number;
44
+ columns: MinorUnitsColumnReport[];
45
+ /** Columns still holding floating-point major units. */
46
+ pendingColumns: number;
47
+ /** Columns already integer. */
48
+ convertedColumns: number;
49
+ /** Columns whose table/column does not exist. */
50
+ missingColumns: number;
51
+ /** Total rows that would be rounded. */
52
+ nonIntegralRows: number;
53
+ /** Total rows that would overflow `int4`. */
54
+ overflowRows: number;
55
+ /** No row would lose information. */
56
+ ok: boolean;
57
+ /** Human-readable report, suitable for pasting into a migration log or PR. */
58
+ summary: string;
59
+ }
60
+ /** Options shared by the preflight and the rescale. */
61
+ export interface MinorUnitsOptions {
62
+ /** Minor units per major unit. Default `100` (cents). */
63
+ scale?: number;
64
+ /** Engine hint for adapters whose URL does not identify the engine. */
65
+ engineHint?: string;
66
+ /** Problem rows recorded per column before truncating. Default `20`. */
67
+ maxProblemRows?: number;
68
+ }
69
+ /** Options for {@link rescaleMoneyColumnsToMinorUnits}. */
70
+ export interface MinorUnitsRescaleOptions extends MinorUnitsOptions {
71
+ /**
72
+ * Stable marker name recorded in `_smrt_backfills`, e.g.
73
+ * `@happyvertical/smrt-commerce:money-minor-units:v1`.
74
+ *
75
+ * This is what makes the migration idempotent on SQLite, where the rescale
76
+ * is an `UPDATE` no column type can distinguish from an unconverted table.
77
+ */
78
+ backfillName: string;
79
+ /** Package recorded alongside the marker. */
80
+ packageName?: string;
81
+ /**
82
+ * Convert even when the preflight found rows that would be rounded or
83
+ * overflow. Off by default: silently rounding a half-cent away is exactly
84
+ * the failure this migration exists to prevent.
85
+ */
86
+ force?: boolean;
87
+ }
88
+ /** Result of {@link rescaleMoneyColumnsToMinorUnits}. */
89
+ export interface MinorUnitsRescaleResult {
90
+ /** `false` when the backfill marker was already present. */
91
+ ran: boolean;
92
+ engine: string;
93
+ scale: number;
94
+ /** The preflight that gated the run. */
95
+ preflight: MinorUnitsPreflightResult;
96
+ /** Columns whose values were rescaled. */
97
+ rescaledColumns: Array<{
98
+ table: string;
99
+ column: string;
100
+ }>;
101
+ /** Statements executed, in order — useful for logs and tests. */
102
+ statements: string[];
103
+ /**
104
+ * Columns whose values are now minor units but whose *declared* type is
105
+ * still floating-point. Only ever populated on SQLite, which cannot alter a
106
+ * column type in place; clearing it needs the table-rebuild path (#2370).
107
+ */
108
+ declaredTypeChangePending: Array<{
109
+ table: string;
110
+ column: string;
111
+ }>;
112
+ }
113
+ /**
114
+ * A money-losing preflight finding blocked the conversion.
115
+ *
116
+ * Carries the full preflight so a caller can print the summary, fix the data,
117
+ * and retry rather than re-deriving what went wrong.
118
+ */
119
+ export declare class MinorUnitsPreflightError extends Error {
120
+ readonly preflight: MinorUnitsPreflightResult;
121
+ constructor(preflight: MinorUnitsPreflightResult);
122
+ }
123
+ /**
124
+ * The statements that convert one column, for one engine.
125
+ *
126
+ * Exported so the emitted DDL can be asserted without a live PostgreSQL or
127
+ * DuckDB connection; each owning package's `test:postgres` lane then proves the
128
+ * same SQL runs against a real server.
129
+ *
130
+ * @param engine - `postgres`, `duckdb`, or anything SQLite-shaped.
131
+ * @param table - Physical table name.
132
+ * @param column - Physical column name.
133
+ * @param scale - Minor units per major unit.
134
+ * @returns Statements in execution order.
135
+ */
136
+ export declare function buildMinorUnitsStatements(engine: string, table: string, column: string, scale: number): string[];
137
+ /**
138
+ * Report what {@link rescaleMoneyColumnsToMinorUnits} would do, without
139
+ * writing anything.
140
+ *
141
+ * @param db - Root database handle.
142
+ * @param targets - Tables and money columns owned by the calling package.
143
+ * @param options - Scale, engine hint, problem-row cap.
144
+ * @returns Per-column state plus the rows that would lose information.
145
+ */
146
+ export declare function preflightMinorUnitsRescale(db: DatabaseInterface, targets: MoneyColumnTarget[], options?: MinorUnitsOptions): Promise<MinorUnitsPreflightResult>;
147
+ /**
148
+ * Convert floating-point major-unit money columns to integer minor units.
149
+ *
150
+ * Idempotent: the run is wrapped in a {@link BackfillTracker} marker, so a
151
+ * second call is a no-op even on SQLite where the column type cannot record
152
+ * that the conversion already happened.
153
+ *
154
+ * @param db - Root database handle (must support `query`).
155
+ * @param targets - Tables and money columns owned by the calling package.
156
+ * @param options - Marker name, scale, and whether to convert despite
157
+ * information-losing rows.
158
+ * @throws {MinorUnitsPreflightError} when the preflight found rows that would
159
+ * be rounded or overflow and `force` was not set.
160
+ */
161
+ export declare function rescaleMoneyColumnsToMinorUnits(db: DatabaseInterface, targets: MoneyColumnTarget[], options: MinorUnitsRescaleOptions): Promise<MinorUnitsRescaleResult>;
162
+ //# sourceMappingURL=minor-units.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"minor-units.d.ts","sourceRoot":"","sources":["../../src/migrations/minor-units.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,381 @@
1
+ import { detectEngine } from "../schema/ddl/index.js";
2
+ import { BackfillTracker } from "./backfill-tracker.js";
3
+ import { createLogger } from "@happyvertical/logger";
4
+ //#region src/migrations/minor-units.ts
5
+ /**
6
+ * Money-column rescale: floating-point major units → integer minor units (#2401).
7
+ *
8
+ * SMRT's rule is that money is exact and is stored as **integer minor units**
9
+ * (cents, satoshis) — `$19.99` is `1999`. Packages that predate the rule
10
+ * declared their money fields `= 0.0`, which compiles to a floating-point
11
+ * column holding *major* units. Flipping the declaration to `= 0` changes the
12
+ * column type, and REAL→INTEGER is deliberately **not** one of the differ's
13
+ * whitelisted upgrades: an automatic `ALTER … TYPE integer` would truncate
14
+ * `19.99` to `19`, silently destroying 99 cents on every row.
15
+ *
16
+ * So the conversion is an explicit, opt-in migration with two halves:
17
+ *
18
+ * 1. {@link preflightMinorUnitsRescale} — read-only. Reports, per column,
19
+ * whether it still needs converting, and lists the rows that would lose
20
+ * information: values whose scaled form is not a whole number (a half-cent
21
+ * that has to be rounded away) and values that overflow PostgreSQL's `int4`
22
+ * once multiplied by the scale. Run it against a production snapshot and
23
+ * read the summary before converting anything.
24
+ * 2. {@link rescaleMoneyColumnsToMinorUnits} — the conversion itself, guarded
25
+ * by a {@link BackfillTracker} marker so re-running it can never multiply a
26
+ * table by 100 twice.
27
+ *
28
+ * Per-engine behaviour:
29
+ *
30
+ * | Engine | Column type | Values |
31
+ * | --- | --- | --- |
32
+ * | PostgreSQL | `ALTER COLUMN … TYPE integer USING round(col * scale)` | converted by the cast |
33
+ * | DuckDB | `ALTER COLUMN … TYPE INTEGER USING round(col * scale)` | converted by the cast |
34
+ * | SQLite | **not changed here** — SQLite cannot alter a column's declared type in place | rescaled with an `UPDATE`, values become exact integers |
35
+ *
36
+ * SQLite's declared type only sets an affinity, so a rescaled column already
37
+ * behaves as integer minor units; bringing the *declaration* into line needs
38
+ * the table-rebuild path (#2370). The result flags that with
39
+ * {@link MinorUnitsRescaleResult.declaredTypeChangePending} rather than
40
+ * pretending the column type moved.
41
+ */
42
+ var logger = createLogger({ level: "info" });
43
+ /** PostgreSQL `int4` bounds — the ceiling this conversion has to respect. */
44
+ var INT4_MIN = -2147483648;
45
+ var INT4_MAX = 2147483647;
46
+ /**
47
+ * How far a scaled value may sit from a whole number and still count as
48
+ * integral.
49
+ *
50
+ * `19.99 * 100` is `1998.9999999999998` in IEEE-754, so a zero tolerance would
51
+ * report every ordinary two-decimal price as non-integral. A genuine half-cent
52
+ * is off by `0.5`, five orders of magnitude above this bound, so the two cases
53
+ * never blur.
54
+ */
55
+ var INTEGRALITY_TOLERANCE = 1e-6;
56
+ /** Default minor units per major unit — cents. */
57
+ var DEFAULT_SCALE = 100;
58
+ /**
59
+ * Round half away from zero, the way SQL `round()` does — and the way
60
+ * `Math.round` does not.
61
+ *
62
+ * `Math.round` rounds halves toward +∞, so `Math.round(-0.5)` is `-0` while
63
+ * SQLite's and DuckDB's `round(-0.5)` is `-1`. Money here can legitimately be
64
+ * negative (`BillingAdjustment.amount` is a signed credit), so a preflight that
65
+ * predicted the conversion with `Math.round` would report a value one minor
66
+ * unit above what the database actually stores. The PostgreSQL DDL casts to
67
+ * `numeric` for the same reason — see {@link buildMinorUnitsStatements}.
68
+ */
69
+ function roundHalfAwayFromZero(value) {
70
+ return Math.sign(value) * Math.round(Math.abs(value));
71
+ }
72
+ /**
73
+ * A money-losing preflight finding blocked the conversion.
74
+ *
75
+ * Carries the full preflight so a caller can print the summary, fix the data,
76
+ * and retry rather than re-deriving what went wrong.
77
+ */
78
+ var MinorUnitsPreflightError = class extends Error {
79
+ preflight;
80
+ constructor(preflight) {
81
+ super(`Money minor-units rescale refused: ${preflight.nonIntegralRows} row(s) would be rounded and ${preflight.overflowRows} row(s) would overflow int4. Fix the data or pass \`force: true\` to accept the rounding.
82
+ ` + preflight.summary);
83
+ this.preflight = preflight;
84
+ this.name = "MinorUnitsPreflightError";
85
+ }
86
+ };
87
+ function resolveDatabaseUrl(db) {
88
+ const dbWithConfig = db;
89
+ return db.url || dbWithConfig.config?.url || "";
90
+ }
91
+ function resolveEngine(db, engineHint) {
92
+ return typeof db.exportTable === "function" ? "json" : detectEngine(resolveDatabaseUrl(db), engineHint);
93
+ }
94
+ /**
95
+ * Quote an identifier for DDL.
96
+ *
97
+ * Table and column names here come from a package's own migration module, not
98
+ * from user input, but a rejected embedded quote is cheaper than trusting that
99
+ * forever.
100
+ */
101
+ function quoteIdentifier(name) {
102
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(name)) throw new Error(`Money minor-units rescale: '${name}' is not a plain SQL identifier.`);
103
+ return `"${name}"`;
104
+ }
105
+ /** Is this declared type a floating-point one that still needs converting? */
106
+ function isFloatingType(declaredType) {
107
+ return /^(REAL|FLOAT|DOUBLE|DECIMAL|NUMERIC|NUMBER)/i.test(declaredType.trim());
108
+ }
109
+ /** Is this declared type already an integer one? */
110
+ function isIntegerType(declaredType) {
111
+ return /^(INTEGER|INT|INT4|INT8|BIGINT|SMALLINT|TINYINT|HUGEINT)\b/i.test(declaredType.trim());
112
+ }
113
+ /**
114
+ * Read declared column types for one table.
115
+ *
116
+ * PostgreSQL and DuckDB both expose `information_schema.columns`; SQLite does
117
+ * not, so it uses `PRAGMA table_info`. Returns an empty map when the table does
118
+ * not exist, which the caller reports as `missing` rather than an error — a
119
+ * package's migration lists its own tables, and a deployment may legitimately
120
+ * not have created all of them yet.
121
+ */
122
+ async function readColumnTypes(db, engine, table) {
123
+ const types = /* @__PURE__ */ new Map();
124
+ try {
125
+ if (engine === "postgres" || engine === "duckdb") {
126
+ const result = await db.query("SELECT column_name, data_type FROM information_schema.columns WHERE table_name = ?", table);
127
+ for (const row of result.rows) if (row.column_name) types.set(row.column_name, row.data_type ?? "");
128
+ return types;
129
+ }
130
+ const result = await db.query(`PRAGMA table_info(${quoteIdentifier(table)})`);
131
+ for (const row of result.rows) if (row.name) types.set(row.name, row.type ?? "");
132
+ } catch (error) {
133
+ logger.debug(`[minor-units] Could not introspect ${table}; treating its columns as missing`, { error: error instanceof Error ? error.message : String(error) });
134
+ }
135
+ return types;
136
+ }
137
+ /** Does this table expose an `id` column we can name in problem rows? */
138
+ function hasIdColumn(types) {
139
+ return types.has("id");
140
+ }
141
+ function toFiniteNumber(value) {
142
+ if (value == null) return null;
143
+ const parsed = typeof value === "number" ? value : Number(value);
144
+ return Number.isFinite(parsed) ? parsed : null;
145
+ }
146
+ /**
147
+ * Inspect every non-null value in one floating-point money column.
148
+ *
149
+ * Rows are read rather than aggregated in SQL because the integrality test has
150
+ * to survive three dialects' differing `round`/modulo semantics, and a money
151
+ * table small enough to be worth converting by hand is small enough to scan.
152
+ */
153
+ async function inspectColumn(db, table, column, scale, maxProblemRows, withId) {
154
+ const quotedTable = quoteIdentifier(table);
155
+ const quotedColumn = quoteIdentifier(column);
156
+ const selection = withId ? `"id", ${quotedColumn} AS value` : `${quotedColumn} AS value`;
157
+ const result = await db.query(`SELECT ${selection} FROM ${quotedTable} WHERE ${quotedColumn} IS NOT NULL`);
158
+ const nonIntegral = [];
159
+ const overflow = [];
160
+ let inspectedRows = 0;
161
+ for (const row of result.rows) {
162
+ const value = toFiniteNumber(row.value);
163
+ if (value === null) continue;
164
+ inspectedRows += 1;
165
+ const scaled = value * scale;
166
+ const rounded = roundHalfAwayFromZero(scaled);
167
+ const id = row.id == null ? null : String(row.id);
168
+ if (Math.abs(scaled - rounded) > INTEGRALITY_TOLERANCE) {
169
+ if (nonIntegral.length < maxProblemRows) nonIntegral.push({
170
+ id,
171
+ value,
172
+ scaled
173
+ });
174
+ }
175
+ if (rounded > INT4_MAX || rounded < INT4_MIN) {
176
+ if (overflow.length < maxProblemRows) overflow.push({
177
+ id,
178
+ value,
179
+ scaled
180
+ });
181
+ }
182
+ }
183
+ return {
184
+ inspectedRows,
185
+ nonIntegral,
186
+ overflow
187
+ };
188
+ }
189
+ function renderSummary(result) {
190
+ const lines = [
191
+ `Money minor-units preflight (engine=${result.engine}, scale=${result.scale})`,
192
+ ` columns: ${result.pendingColumns} pending, ${result.convertedColumns} already integer, ${result.missingColumns} missing`,
193
+ ` rows that would be rounded: ${result.nonIntegralRows}`,
194
+ ` rows that would overflow int4: ${result.overflowRows}`
195
+ ];
196
+ for (const column of result.columns) {
197
+ const label = `${column.table}.${column.column}`;
198
+ if (column.state === "missing") {
199
+ lines.push(` - ${label}: missing`);
200
+ continue;
201
+ }
202
+ if (column.state === "converted") {
203
+ lines.push(` - ${label}: already integer (${column.declaredType ?? "unknown"})`);
204
+ continue;
205
+ }
206
+ lines.push(` - ${label}: pending (${column.declaredType ?? "unknown"}), ${column.inspectedRows} row(s) inspected, ${column.nonIntegral.length} non-integral, ${column.overflow.length} overflow`);
207
+ for (const row of column.nonIntegral) lines.push(` non-integral id=${row.id ?? "<no id>"} value=${row.value} scaled=${row.scaled}`);
208
+ for (const row of column.overflow) lines.push(` overflow id=${row.id ?? "<no id>"} value=${row.value} scaled=${row.scaled}`);
209
+ }
210
+ return lines.join("\n");
211
+ }
212
+ /**
213
+ * The statements that convert one column, for one engine.
214
+ *
215
+ * Exported so the emitted DDL can be asserted without a live PostgreSQL or
216
+ * DuckDB connection; each owning package's `test:postgres` lane then proves the
217
+ * same SQL runs against a real server.
218
+ *
219
+ * @param engine - `postgres`, `duckdb`, or anything SQLite-shaped.
220
+ * @param table - Physical table name.
221
+ * @param column - Physical column name.
222
+ * @param scale - Minor units per major unit.
223
+ * @returns Statements in execution order.
224
+ */
225
+ function buildMinorUnitsStatements(engine, table, column, scale) {
226
+ const quotedTable = quoteIdentifier(table);
227
+ const quotedColumn = quoteIdentifier(column);
228
+ if (engine === "postgres" || engine === "duckdb") {
229
+ const scaled = engine === "postgres" ? `round((${quotedColumn})::numeric * ${scale})` : `round(${quotedColumn} * ${scale})`;
230
+ return [
231
+ `ALTER TABLE ${quotedTable} ALTER COLUMN ${quotedColumn} DROP DEFAULT`,
232
+ `ALTER TABLE ${quotedTable} ALTER COLUMN ${quotedColumn} TYPE INTEGER USING ${scaled}`,
233
+ `ALTER TABLE ${quotedTable} ALTER COLUMN ${quotedColumn} SET DEFAULT 0`
234
+ ];
235
+ }
236
+ return [`UPDATE ${quotedTable} SET ${quotedColumn} = CAST(round(${quotedColumn} * ${scale}) AS INTEGER) WHERE ${quotedColumn} IS NOT NULL`];
237
+ }
238
+ /**
239
+ * Report what {@link rescaleMoneyColumnsToMinorUnits} would do, without
240
+ * writing anything.
241
+ *
242
+ * @param db - Root database handle.
243
+ * @param targets - Tables and money columns owned by the calling package.
244
+ * @param options - Scale, engine hint, problem-row cap.
245
+ * @returns Per-column state plus the rows that would lose information.
246
+ */
247
+ async function preflightMinorUnitsRescale(db, targets, options = {}) {
248
+ const scale = options.scale ?? DEFAULT_SCALE;
249
+ if (!Number.isInteger(scale) || scale <= 0) throw new Error(`Money minor-units rescale: scale must be a positive integer (got ${scale}).`);
250
+ const engine = resolveEngine(db, options.engineHint);
251
+ const maxProblemRows = options.maxProblemRows ?? 20;
252
+ for (const target of targets) {
253
+ quoteIdentifier(target.table);
254
+ for (const column of target.columns) quoteIdentifier(column);
255
+ }
256
+ const columns = [];
257
+ for (const target of targets) {
258
+ const types = await readColumnTypes(db, engine, target.table);
259
+ const withId = hasIdColumn(types);
260
+ for (const column of target.columns) {
261
+ const declaredType = types.get(column) ?? null;
262
+ if (declaredType === null) {
263
+ columns.push({
264
+ table: target.table,
265
+ column,
266
+ state: "missing",
267
+ declaredType: null,
268
+ inspectedRows: 0,
269
+ nonIntegral: [],
270
+ overflow: []
271
+ });
272
+ continue;
273
+ }
274
+ if (isIntegerType(declaredType) || !isFloatingType(declaredType)) {
275
+ columns.push({
276
+ table: target.table,
277
+ column,
278
+ state: "converted",
279
+ declaredType,
280
+ inspectedRows: 0,
281
+ nonIntegral: [],
282
+ overflow: []
283
+ });
284
+ continue;
285
+ }
286
+ const inspected = await inspectColumn(db, target.table, column, scale, maxProblemRows, withId);
287
+ columns.push({
288
+ table: target.table,
289
+ column,
290
+ state: "pending",
291
+ declaredType,
292
+ ...inspected
293
+ });
294
+ }
295
+ }
296
+ const base = {
297
+ engine,
298
+ scale,
299
+ columns,
300
+ pendingColumns: columns.filter((c) => c.state === "pending").length,
301
+ convertedColumns: columns.filter((c) => c.state === "converted").length,
302
+ missingColumns: columns.filter((c) => c.state === "missing").length,
303
+ nonIntegralRows: columns.reduce((sum, c) => sum + c.nonIntegral.length, 0),
304
+ overflowRows: columns.reduce((sum, c) => sum + c.overflow.length, 0)
305
+ };
306
+ const ok = base.nonIntegralRows === 0 && base.overflowRows === 0;
307
+ return {
308
+ ...base,
309
+ ok,
310
+ summary: renderSummary({
311
+ ...base,
312
+ ok
313
+ })
314
+ };
315
+ }
316
+ /**
317
+ * Convert floating-point major-unit money columns to integer minor units.
318
+ *
319
+ * Idempotent: the run is wrapped in a {@link BackfillTracker} marker, so a
320
+ * second call is a no-op even on SQLite where the column type cannot record
321
+ * that the conversion already happened.
322
+ *
323
+ * @param db - Root database handle (must support `query`).
324
+ * @param targets - Tables and money columns owned by the calling package.
325
+ * @param options - Marker name, scale, and whether to convert despite
326
+ * information-losing rows.
327
+ * @throws {MinorUnitsPreflightError} when the preflight found rows that would
328
+ * be rounded or overflow and `force` was not set.
329
+ */
330
+ async function rescaleMoneyColumnsToMinorUnits(db, targets, options) {
331
+ const scale = options.scale ?? DEFAULT_SCALE;
332
+ const engine = resolveEngine(db, options.engineHint);
333
+ const tracker = new BackfillTracker({ db });
334
+ if (await tracker.isApplied(options.backfillName)) return {
335
+ ran: false,
336
+ engine,
337
+ scale,
338
+ preflight: await preflightMinorUnitsRescale(db, targets, options),
339
+ rescaledColumns: [],
340
+ statements: [],
341
+ declaredTypeChangePending: []
342
+ };
343
+ const preflight = await preflightMinorUnitsRescale(db, targets, options);
344
+ if (!preflight.ok && !options.force) throw new MinorUnitsPreflightError(preflight);
345
+ const statements = [];
346
+ const rescaledColumns = [];
347
+ const declaredTypeChangePending = [];
348
+ const altersColumnType = engine === "postgres" || engine === "duckdb";
349
+ for (const report of preflight.columns) {
350
+ if (report.state !== "pending") continue;
351
+ for (const sql of buildMinorUnitsStatements(engine, report.table, report.column, scale)) {
352
+ await db.query(sql);
353
+ statements.push(sql);
354
+ }
355
+ if (!altersColumnType) declaredTypeChangePending.push({
356
+ table: report.table,
357
+ column: report.column
358
+ });
359
+ rescaledColumns.push({
360
+ table: report.table,
361
+ column: report.column
362
+ });
363
+ }
364
+ await tracker.recordApplied(options.backfillName, {
365
+ description: `Money columns rescaled from major units to integer minor units (scale ${scale}).`,
366
+ packageName: options.packageName
367
+ });
368
+ return {
369
+ ran: true,
370
+ engine,
371
+ scale,
372
+ preflight,
373
+ rescaledColumns,
374
+ statements,
375
+ declaredTypeChangePending
376
+ };
377
+ }
378
+ //#endregion
379
+ export { MinorUnitsPreflightError, buildMinorUnitsStatements, preflightMinorUnitsRescale, rescaleMoneyColumnsToMinorUnits };
380
+
381
+ //# sourceMappingURL=minor-units.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"minor-units.js","names":[],"sources":["../../src/migrations/minor-units.ts"],"sourcesContent":["/**\n * Money-column rescale: floating-point major units → integer minor units (#2401).\n *\n * SMRT's rule is that money is exact and is stored as **integer minor units**\n * (cents, satoshis) — `$19.99` is `1999`. Packages that predate the rule\n * declared their money fields `= 0.0`, which compiles to a floating-point\n * column holding *major* units. Flipping the declaration to `= 0` changes the\n * column type, and REAL→INTEGER is deliberately **not** one of the differ's\n * whitelisted upgrades: an automatic `ALTER … TYPE integer` would truncate\n * `19.99` to `19`, silently destroying 99 cents on every row.\n *\n * So the conversion is an explicit, opt-in migration with two halves:\n *\n * 1. {@link preflightMinorUnitsRescale} — read-only. Reports, per column,\n * whether it still needs converting, and lists the rows that would lose\n * information: values whose scaled form is not a whole number (a half-cent\n * that has to be rounded away) and values that overflow PostgreSQL's `int4`\n * once multiplied by the scale. Run it against a production snapshot and\n * read the summary before converting anything.\n * 2. {@link rescaleMoneyColumnsToMinorUnits} — the conversion itself, guarded\n * by a {@link BackfillTracker} marker so re-running it can never multiply a\n * table by 100 twice.\n *\n * Per-engine behaviour:\n *\n * | Engine | Column type | Values |\n * | --- | --- | --- |\n * | PostgreSQL | `ALTER COLUMN … TYPE integer USING round(col * scale)` | converted by the cast |\n * | DuckDB | `ALTER COLUMN … TYPE INTEGER USING round(col * scale)` | converted by the cast |\n * | SQLite | **not changed here** — SQLite cannot alter a column's declared type in place | rescaled with an `UPDATE`, values become exact integers |\n *\n * SQLite's declared type only sets an affinity, so a rescaled column already\n * behaves as integer minor units; bringing the *declaration* into line needs\n * the table-rebuild path (#2370). The result flags that with\n * {@link MinorUnitsRescaleResult.declaredTypeChangePending} rather than\n * pretending the column type moved.\n */\n\nimport { createLogger } from '@happyvertical/logger';\nimport { detectEngine } from '../schema/ddl/index.js';\nimport { BackfillTracker } from './backfill-tracker.js';\nimport type { DatabaseInterface } from './types.js';\n\nconst logger = createLogger({ level: 'info' });\n\n/** PostgreSQL `int4` bounds — the ceiling this conversion has to respect. */\nconst INT4_MIN = -2147483648;\nconst INT4_MAX = 2147483647;\n\n/**\n * How far a scaled value may sit from a whole number and still count as\n * integral.\n *\n * `19.99 * 100` is `1998.9999999999998` in IEEE-754, so a zero tolerance would\n * report every ordinary two-decimal price as non-integral. A genuine half-cent\n * is off by `0.5`, five orders of magnitude above this bound, so the two cases\n * never blur.\n */\nconst INTEGRALITY_TOLERANCE = 1e-6;\n\n/** Default minor units per major unit — cents. */\nconst DEFAULT_SCALE = 100;\n\n/**\n * Round half away from zero, the way SQL `round()` does — and the way\n * `Math.round` does not.\n *\n * `Math.round` rounds halves toward +∞, so `Math.round(-0.5)` is `-0` while\n * SQLite's and DuckDB's `round(-0.5)` is `-1`. Money here can legitimately be\n * negative (`BillingAdjustment.amount` is a signed credit), so a preflight that\n * predicted the conversion with `Math.round` would report a value one minor\n * unit above what the database actually stores. The PostgreSQL DDL casts to\n * `numeric` for the same reason — see {@link buildMinorUnitsStatements}.\n */\nfunction roundHalfAwayFromZero(value: number): number {\n return Math.sign(value) * Math.round(Math.abs(value));\n}\n\n/** A table and the money columns on it that must move to minor units. */\nexport interface MoneyColumnTarget {\n /** Physical table name, e.g. `payments`. */\n table: string;\n /** Physical column names, e.g. `['amount', 'native_amount']`. */\n columns: string[];\n}\n\n/** What the preflight found about one column. */\nexport type MinorUnitsColumnState =\n /** Floating-point today: needs converting. */\n | 'pending'\n /** Already an integer column: nothing to do. */\n | 'converted'\n /** Table or column absent (package not installed, older schema). */\n | 'missing';\n\n/** One row that cannot be converted without losing information. */\nexport interface MinorUnitsProblemRow {\n /** Primary key, when the table has an `id` column. */\n id: string | null;\n /** The stored major-unit value. */\n value: number;\n /** `value * scale`, before rounding. */\n scaled: number;\n}\n\n/** Preflight findings for a single column. */\nexport interface MinorUnitsColumnReport {\n table: string;\n column: string;\n state: MinorUnitsColumnState;\n /** Declared column type as the database reports it, when resolvable. */\n declaredType: string | null;\n /** Non-null values inspected. `0` unless `state` is `pending`. */\n inspectedRows: number;\n /** Values whose scaled form is not a whole number — rounding loses money. */\n nonIntegral: MinorUnitsProblemRow[];\n /** Values whose scaled form falls outside PostgreSQL `int4`. */\n overflow: MinorUnitsProblemRow[];\n}\n\n/** Result of {@link preflightMinorUnitsRescale}. */\nexport interface MinorUnitsPreflightResult {\n engine: string;\n scale: number;\n columns: MinorUnitsColumnReport[];\n /** Columns still holding floating-point major units. */\n pendingColumns: number;\n /** Columns already integer. */\n convertedColumns: number;\n /** Columns whose table/column does not exist. */\n missingColumns: number;\n /** Total rows that would be rounded. */\n nonIntegralRows: number;\n /** Total rows that would overflow `int4`. */\n overflowRows: number;\n /** No row would lose information. */\n ok: boolean;\n /** Human-readable report, suitable for pasting into a migration log or PR. */\n summary: string;\n}\n\n/** Options shared by the preflight and the rescale. */\nexport interface MinorUnitsOptions {\n /** Minor units per major unit. Default `100` (cents). */\n scale?: number;\n /** Engine hint for adapters whose URL does not identify the engine. */\n engineHint?: string;\n /** Problem rows recorded per column before truncating. Default `20`. */\n maxProblemRows?: number;\n}\n\n/** Options for {@link rescaleMoneyColumnsToMinorUnits}. */\nexport interface MinorUnitsRescaleOptions extends MinorUnitsOptions {\n /**\n * Stable marker name recorded in `_smrt_backfills`, e.g.\n * `@happyvertical/smrt-commerce:money-minor-units:v1`.\n *\n * This is what makes the migration idempotent on SQLite, where the rescale\n * is an `UPDATE` no column type can distinguish from an unconverted table.\n */\n backfillName: string;\n /** Package recorded alongside the marker. */\n packageName?: string;\n /**\n * Convert even when the preflight found rows that would be rounded or\n * overflow. Off by default: silently rounding a half-cent away is exactly\n * the failure this migration exists to prevent.\n */\n force?: boolean;\n}\n\n/** Result of {@link rescaleMoneyColumnsToMinorUnits}. */\nexport interface MinorUnitsRescaleResult {\n /** `false` when the backfill marker was already present. */\n ran: boolean;\n engine: string;\n scale: number;\n /** The preflight that gated the run. */\n preflight: MinorUnitsPreflightResult;\n /** Columns whose values were rescaled. */\n rescaledColumns: Array<{ table: string; column: string }>;\n /** Statements executed, in order — useful for logs and tests. */\n statements: string[];\n /**\n * Columns whose values are now minor units but whose *declared* type is\n * still floating-point. Only ever populated on SQLite, which cannot alter a\n * column type in place; clearing it needs the table-rebuild path (#2370).\n */\n declaredTypeChangePending: Array<{ table: string; column: string }>;\n}\n\n/**\n * A money-losing preflight finding blocked the conversion.\n *\n * Carries the full preflight so a caller can print the summary, fix the data,\n * and retry rather than re-deriving what went wrong.\n */\nexport class MinorUnitsPreflightError extends Error {\n constructor(readonly preflight: MinorUnitsPreflightResult) {\n super(\n `Money minor-units rescale refused: ${preflight.nonIntegralRows} row(s) ` +\n `would be rounded and ${preflight.overflowRows} row(s) would overflow ` +\n 'int4. Fix the data or pass `force: true` to accept the rounding.\\n' +\n preflight.summary,\n );\n this.name = 'MinorUnitsPreflightError';\n }\n}\n\nfunction resolveDatabaseUrl(db: DatabaseInterface): string {\n const dbWithConfig = db as DatabaseInterface & { config?: { url?: string } };\n return db.url || dbWithConfig.config?.url || '';\n}\n\nfunction resolveEngine(db: DatabaseInterface, engineHint?: string): string {\n return typeof (db as { exportTable?: unknown }).exportTable === 'function'\n ? 'json'\n : detectEngine(resolveDatabaseUrl(db), engineHint);\n}\n\n/**\n * Quote an identifier for DDL.\n *\n * Table and column names here come from a package's own migration module, not\n * from user input, but a rejected embedded quote is cheaper than trusting that\n * forever.\n */\nfunction quoteIdentifier(name: string): string {\n if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(name)) {\n throw new Error(\n `Money minor-units rescale: '${name}' is not a plain SQL identifier.`,\n );\n }\n return `\"${name}\"`;\n}\n\n/** Is this declared type a floating-point one that still needs converting? */\nfunction isFloatingType(declaredType: string): boolean {\n return /^(REAL|FLOAT|DOUBLE|DECIMAL|NUMERIC|NUMBER)/i.test(\n declaredType.trim(),\n );\n}\n\n/** Is this declared type already an integer one? */\nfunction isIntegerType(declaredType: string): boolean {\n return /^(INTEGER|INT|INT4|INT8|BIGINT|SMALLINT|TINYINT|HUGEINT)\\b/i.test(\n declaredType.trim(),\n );\n}\n\n/**\n * Read declared column types for one table.\n *\n * PostgreSQL and DuckDB both expose `information_schema.columns`; SQLite does\n * not, so it uses `PRAGMA table_info`. Returns an empty map when the table does\n * not exist, which the caller reports as `missing` rather than an error — a\n * package's migration lists its own tables, and a deployment may legitimately\n * not have created all of them yet.\n */\nasync function readColumnTypes(\n db: DatabaseInterface,\n engine: string,\n table: string,\n): Promise<Map<string, string>> {\n const types = new Map<string, string>();\n try {\n if (engine === 'postgres' || engine === 'duckdb') {\n const result = await db.query(\n 'SELECT column_name, data_type FROM information_schema.columns WHERE table_name = ?',\n table,\n );\n for (const row of result.rows as {\n column_name?: string;\n data_type?: string;\n }[]) {\n if (row.column_name) types.set(row.column_name, row.data_type ?? '');\n }\n return types;\n }\n // SQLite. `PRAGMA table_info` does not accept a bound parameter, and the\n // identifier has already been validated by `quoteIdentifier`.\n const result = await db.query(\n `PRAGMA table_info(${quoteIdentifier(table)})`,\n );\n for (const row of result.rows as { name?: string; type?: string }[]) {\n if (row.name) types.set(row.name, row.type ?? '');\n }\n } catch (error) {\n logger.debug(\n `[minor-units] Could not introspect ${table}; treating its columns as missing`,\n { error: error instanceof Error ? error.message : String(error) },\n );\n }\n return types;\n}\n\n/** Does this table expose an `id` column we can name in problem rows? */\nfunction hasIdColumn(types: Map<string, string>): boolean {\n return types.has('id');\n}\n\nfunction toFiniteNumber(value: unknown): number | null {\n if (value == null) return null;\n const parsed = typeof value === 'number' ? value : Number(value);\n return Number.isFinite(parsed) ? parsed : null;\n}\n\n/**\n * Inspect every non-null value in one floating-point money column.\n *\n * Rows are read rather than aggregated in SQL because the integrality test has\n * to survive three dialects' differing `round`/modulo semantics, and a money\n * table small enough to be worth converting by hand is small enough to scan.\n */\nasync function inspectColumn(\n db: DatabaseInterface,\n table: string,\n column: string,\n scale: number,\n maxProblemRows: number,\n withId: boolean,\n): Promise<{\n inspectedRows: number;\n nonIntegral: MinorUnitsProblemRow[];\n overflow: MinorUnitsProblemRow[];\n}> {\n const quotedTable = quoteIdentifier(table);\n const quotedColumn = quoteIdentifier(column);\n const selection = withId\n ? `\"id\", ${quotedColumn} AS value`\n : `${quotedColumn} AS value`;\n const result = await db.query(\n `SELECT ${selection} FROM ${quotedTable} WHERE ${quotedColumn} IS NOT NULL`,\n );\n\n const nonIntegral: MinorUnitsProblemRow[] = [];\n const overflow: MinorUnitsProblemRow[] = [];\n let inspectedRows = 0;\n\n for (const row of result.rows as { id?: unknown; value?: unknown }[]) {\n const value = toFiniteNumber(row.value);\n if (value === null) continue;\n inspectedRows += 1;\n const scaled = value * scale;\n const rounded = roundHalfAwayFromZero(scaled);\n const id = row.id == null ? null : String(row.id);\n if (Math.abs(scaled - rounded) > INTEGRALITY_TOLERANCE) {\n if (nonIntegral.length < maxProblemRows) {\n nonIntegral.push({ id, value, scaled });\n }\n }\n if (rounded > INT4_MAX || rounded < INT4_MIN) {\n if (overflow.length < maxProblemRows) {\n overflow.push({ id, value, scaled });\n }\n }\n }\n\n return { inspectedRows, nonIntegral, overflow };\n}\n\nfunction renderSummary(result: Omit<MinorUnitsPreflightResult, 'summary'>) {\n const lines: string[] = [\n `Money minor-units preflight (engine=${result.engine}, scale=${result.scale})`,\n ` columns: ${result.pendingColumns} pending, ${result.convertedColumns} already integer, ${result.missingColumns} missing`,\n ` rows that would be rounded: ${result.nonIntegralRows}`,\n ` rows that would overflow int4: ${result.overflowRows}`,\n ];\n for (const column of result.columns) {\n const label = `${column.table}.${column.column}`;\n if (column.state === 'missing') {\n lines.push(` - ${label}: missing`);\n continue;\n }\n if (column.state === 'converted') {\n lines.push(\n ` - ${label}: already integer (${column.declaredType ?? 'unknown'})`,\n );\n continue;\n }\n lines.push(\n ` - ${label}: pending (${column.declaredType ?? 'unknown'}), ` +\n `${column.inspectedRows} row(s) inspected, ` +\n `${column.nonIntegral.length} non-integral, ${column.overflow.length} overflow`,\n );\n for (const row of column.nonIntegral) {\n lines.push(\n ` non-integral id=${row.id ?? '<no id>'} value=${row.value} scaled=${row.scaled}`,\n );\n }\n for (const row of column.overflow) {\n lines.push(\n ` overflow id=${row.id ?? '<no id>'} value=${row.value} scaled=${row.scaled}`,\n );\n }\n }\n return lines.join('\\n');\n}\n\n/**\n * The statements that convert one column, for one engine.\n *\n * Exported so the emitted DDL can be asserted without a live PostgreSQL or\n * DuckDB connection; each owning package's `test:postgres` lane then proves the\n * same SQL runs against a real server.\n *\n * @param engine - `postgres`, `duckdb`, or anything SQLite-shaped.\n * @param table - Physical table name.\n * @param column - Physical column name.\n * @param scale - Minor units per major unit.\n * @returns Statements in execution order.\n */\nexport function buildMinorUnitsStatements(\n engine: string,\n table: string,\n column: string,\n scale: number,\n): string[] {\n const quotedTable = quoteIdentifier(table);\n const quotedColumn = quoteIdentifier(column);\n if (engine === 'postgres' || engine === 'duckdb') {\n // PostgreSQL's `round(double precision)` is banker's rounding — it breaks\n // ties to even, so `round(-0.5)` is `-0` and `round(2.5)` is `2`. Casting\n // to `numeric` first selects the half-away-from-zero overload, which is\n // what SQLite and DuckDB do and what the preflight predicts. Money can be\n // negative here (a billing credit), so the tie-break is not academic.\n const scaled =\n engine === 'postgres'\n ? `round((${quotedColumn})::numeric * ${scale})`\n : `round(${quotedColumn} * ${scale})`;\n // A `0.0` DEFAULT is not assignable to an integer column on PostgreSQL, so\n // it is dropped before the type change and restored after it. `0` is the\n // framework default for every money field.\n return [\n `ALTER TABLE ${quotedTable} ALTER COLUMN ${quotedColumn} DROP DEFAULT`,\n `ALTER TABLE ${quotedTable} ALTER COLUMN ${quotedColumn} TYPE INTEGER ` +\n `USING ${scaled}`,\n `ALTER TABLE ${quotedTable} ALTER COLUMN ${quotedColumn} SET DEFAULT 0`,\n ];\n }\n // SQLite: the declared type is only an affinity and cannot be altered in\n // place, so rescale the values and leave the declaration to the table-rebuild\n // path (#2370).\n return [\n `UPDATE ${quotedTable} SET ${quotedColumn} = CAST(round(${quotedColumn} * ${scale}) AS INTEGER) ` +\n `WHERE ${quotedColumn} IS NOT NULL`,\n ];\n}\n\n/**\n * Report what {@link rescaleMoneyColumnsToMinorUnits} would do, without\n * writing anything.\n *\n * @param db - Root database handle.\n * @param targets - Tables and money columns owned by the calling package.\n * @param options - Scale, engine hint, problem-row cap.\n * @returns Per-column state plus the rows that would lose information.\n */\nexport async function preflightMinorUnitsRescale(\n db: DatabaseInterface,\n targets: MoneyColumnTarget[],\n options: MinorUnitsOptions = {},\n): Promise<MinorUnitsPreflightResult> {\n const scale = options.scale ?? DEFAULT_SCALE;\n if (!Number.isInteger(scale) || scale <= 0) {\n throw new Error(\n `Money minor-units rescale: scale must be a positive integer (got ${scale}).`,\n );\n }\n const engine = resolveEngine(db, options.engineHint);\n const maxProblemRows = options.maxProblemRows ?? 20;\n\n // Validate every identifier up front. Introspection swallows errors so an\n // absent table reads as `missing`, and a malformed name must not disappear\n // down that same path — it is a bug in the caller's migration module.\n for (const target of targets) {\n quoteIdentifier(target.table);\n for (const column of target.columns) quoteIdentifier(column);\n }\n\n const columns: MinorUnitsColumnReport[] = [];\n for (const target of targets) {\n const types = await readColumnTypes(db, engine, target.table);\n const withId = hasIdColumn(types);\n for (const column of target.columns) {\n const declaredType = types.get(column) ?? null;\n if (declaredType === null) {\n columns.push({\n table: target.table,\n column,\n state: 'missing',\n declaredType: null,\n inspectedRows: 0,\n nonIntegral: [],\n overflow: [],\n });\n continue;\n }\n if (isIntegerType(declaredType) || !isFloatingType(declaredType)) {\n // Anything that is not floating-point is either already converted or a\n // type this migration has no business rewriting.\n columns.push({\n table: target.table,\n column,\n state: 'converted',\n declaredType,\n inspectedRows: 0,\n nonIntegral: [],\n overflow: [],\n });\n continue;\n }\n const inspected = await inspectColumn(\n db,\n target.table,\n column,\n scale,\n maxProblemRows,\n withId,\n );\n columns.push({\n table: target.table,\n column,\n state: 'pending',\n declaredType,\n ...inspected,\n });\n }\n }\n\n const base = {\n engine,\n scale,\n columns,\n pendingColumns: columns.filter((c) => c.state === 'pending').length,\n convertedColumns: columns.filter((c) => c.state === 'converted').length,\n missingColumns: columns.filter((c) => c.state === 'missing').length,\n nonIntegralRows: columns.reduce((sum, c) => sum + c.nonIntegral.length, 0),\n overflowRows: columns.reduce((sum, c) => sum + c.overflow.length, 0),\n };\n const ok = base.nonIntegralRows === 0 && base.overflowRows === 0;\n return { ...base, ok, summary: renderSummary({ ...base, ok }) };\n}\n\n/**\n * Convert floating-point major-unit money columns to integer minor units.\n *\n * Idempotent: the run is wrapped in a {@link BackfillTracker} marker, so a\n * second call is a no-op even on SQLite where the column type cannot record\n * that the conversion already happened.\n *\n * @param db - Root database handle (must support `query`).\n * @param targets - Tables and money columns owned by the calling package.\n * @param options - Marker name, scale, and whether to convert despite\n * information-losing rows.\n * @throws {MinorUnitsPreflightError} when the preflight found rows that would\n * be rounded or overflow and `force` was not set.\n */\nexport async function rescaleMoneyColumnsToMinorUnits(\n db: DatabaseInterface,\n targets: MoneyColumnTarget[],\n options: MinorUnitsRescaleOptions,\n): Promise<MinorUnitsRescaleResult> {\n const scale = options.scale ?? DEFAULT_SCALE;\n const engine = resolveEngine(db, options.engineHint);\n const tracker = new BackfillTracker({ db });\n\n if (await tracker.isApplied(options.backfillName)) {\n return {\n ran: false,\n engine,\n scale,\n preflight: await preflightMinorUnitsRescale(db, targets, options),\n rescaledColumns: [],\n statements: [],\n declaredTypeChangePending: [],\n };\n }\n\n const preflight = await preflightMinorUnitsRescale(db, targets, options);\n if (!preflight.ok && !options.force) {\n throw new MinorUnitsPreflightError(preflight);\n }\n\n const statements: string[] = [];\n const rescaledColumns: Array<{ table: string; column: string }> = [];\n const declaredTypeChangePending: Array<{ table: string; column: string }> =\n [];\n\n const altersColumnType = engine === 'postgres' || engine === 'duckdb';\n for (const report of preflight.columns) {\n if (report.state !== 'pending') continue;\n for (const sql of buildMinorUnitsStatements(\n engine,\n report.table,\n report.column,\n scale,\n )) {\n await db.query(sql);\n statements.push(sql);\n }\n if (!altersColumnType) {\n declaredTypeChangePending.push({\n table: report.table,\n column: report.column,\n });\n }\n rescaledColumns.push({ table: report.table, column: report.column });\n }\n\n await tracker.recordApplied(options.backfillName, {\n description:\n `Money columns rescaled from major units to integer minor units ` +\n `(scale ${scale}).`,\n packageName: options.packageName,\n });\n\n return {\n ran: true,\n engine,\n scale,\n preflight,\n rescaledColumns,\n statements,\n declaredTypeChangePending,\n };\n}\n"],"mappings":"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"}
@@ -4,7 +4,8 @@ import { SchemaComparer, canonicalizeDefault, generateSchemaDiff, getSQLFromDiff
4
4
  import { BackfillTableUnavailableError, BackfillTracker } from "./migrations/backfill-tracker.js";
5
5
  import { computeChecksum, normalizeSQL, shortChecksum, verifyChecksum } from "./migrations/checksum.js";
6
6
  import { MigrationGenerator, createMigrationDefinition, generateMigrationSequence, generateMigrationTimestamp } from "./migrations/generator.js";
7
+ import { MinorUnitsPreflightError, buildMinorUnitsStatements, preflightMinorUnitsRescale, rescaleMoneyColumnsToMinorUnits } from "./migrations/minor-units.js";
7
8
  import { MigrationTracker, buildConcurrentIndexPlan, extractCreatedIndexName, planPostgresStatements } from "./migrations/tracker.js";
8
9
  import { getPendingSchemaStatements, migrateSmrtSchemas } from "./migrations/orchestrate.js";
9
10
  import "./migrations/index.js";
10
- export { BackfillTableUnavailableError, BackfillTracker, MigrationGenerator, MigrationTracker, SQLITE_REBUILD_TABLE_PREFIX, SchemaComparer, buildConcurrentIndexPlan, buildSqliteRebuildStatements, canonicalizeDefault, computeChecksum, createMigrationDefinition, extractCreatedIndexName, generateMigrationSequence, generateMigrationTimestamp, generateSchemaDiff, getPendingSchemaStatements, getSQLFromDiff, hasActionableChanges, isAdvisoryOnlyChange, isInfoOnlyChange, isManualOrAdvisoryChange, isSqliteRebuildPlaceholder, migrateSmrtSchemas, normalizeSQL, parsePostgresTimeoutMs, planPostgresStatements, planSqliteTableRebuilds, rewriteSqliteCreateTable, shortChecksum, sqliteRebuildPlaceholderSql, uniqueColumnIndexName, verifyChecksum };
11
+ 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 };