@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
@@ -28,7 +28,248 @@ function renderIndexTarget(index, engine) {
28
28
  function isJsonPathIndex(index) {
29
29
  return !!(index.jsonPath?.column && index.jsonPath.path);
30
30
  }
31
+ /**
32
+ * The exact predicate `SchemaGenerator.emitStiUniqueIndexes()` emits —
33
+ * `_meta_type = '<qualified class>'` and nothing else — with tolerance for the
34
+ * cosmetic re-rendering PostgreSQL applies when it echoes a predicate back
35
+ * (`::text` casts, wrapping parentheses). A predicate that merely STARTS with
36
+ * `_meta_type = …` but carries further conjuncts (`AND active = TRUE`) is a
37
+ * caller-declared partial unique (#2357 `@smrt({ indexes })`) and must keep
38
+ * the ordinary degrade-to-full-UNIQUE behaviour, so it does not match.
39
+ */
40
+ var STI_SUBTYPE_PREDICATE = /^\s*\(*\s*_meta_type\s*\)?\s*(?:::\w+)?\s*=\s*'(?:[^']|'')*'\s*(?:::\w+)?\s*\)*\s*$/;
41
+ /**
42
+ * True for a UNIQUE index whose predicate scopes it to one STI subtype
43
+ * (`WHERE _meta_type = '<qualified class>'`) — the shape `SchemaGenerator`
44
+ * emits for `@field({ unique: true })` declared only on an STI descendant
45
+ * (#2359).
46
+ *
47
+ * Engines without partial indexes (DuckDB, and the JSON adapter it backs)
48
+ * degrade an ordinary partial index to a full index, and a caller-declared
49
+ * partial UNIQUE (`WHERE active = TRUE`) to a full UNIQUE — a stricter but
50
+ * intended approximation. This shape is the exception: widening it would
51
+ * enforce one subtype's uniqueness across every sibling's rows in the shared
52
+ * table, so those engines skip it instead. Kept here so the DDL strategy and
53
+ * the migration differ apply the same test.
54
+ */
55
+ function isStiSubtypeUniqueIndex(index) {
56
+ return index.unique === true && typeof index.where === "string" && STI_SUBTYPE_PREDICATE.test(index.where);
57
+ }
58
+ /**
59
+ * The strictest identifier length any SMRT-supported engine imposes: 63 bytes.
60
+ *
61
+ * PostgreSQL compiles with `NAMEDATALEN = 64`, so every table, column, index,
62
+ * constraint and trigger name is silently truncated to 63 **bytes** (not
63
+ * characters — the limit is on the UTF-8 encoding). SQLite and DuckDB have no
64
+ * practical limit, which is exactly why the overflow went unnoticed: a name
65
+ * that works in every test truncates in production (#2374, finding C5).
66
+ *
67
+ * Silent truncation is not merely cosmetic. `CREATE INDEX IF NOT EXISTS` on a
68
+ * name whose first 63 bytes match an existing index is a no-op, so a second
69
+ * index that differs only past byte 63 is never created; the migration differ
70
+ * then finds it missing on every run and emits `add_index` forever. The shipped
71
+ * `content_contribution_revisions_contribution_id_revision_number_idx` (66
72
+ * bytes) is the concrete case — only the differ's signature-equivalence check
73
+ * kept it from looping.
74
+ */
75
+ var MAX_IDENTIFIER_BYTES = 63;
76
+ /**
77
+ * Trailing tokens preserved across shortening so a shortened name still reads
78
+ * as what it is (`..._idx` stays an index) and so suffix-based filters — the
79
+ * differ's `_pkey` / `_key` protection, for one — keep matching.
80
+ *
81
+ * Ordered longest-first: `_unique_idx` must win over `_idx`.
82
+ */
83
+ var PRESERVED_IDENTIFIER_SUFFIXES = [
84
+ "_unique_idx",
85
+ "_pkey",
86
+ "_idx",
87
+ "_key"
88
+ ];
89
+ /**
90
+ * Byte length of `value` when encoded as UTF-8 — what PostgreSQL counts.
91
+ *
92
+ * Hand-rolled rather than `Buffer.byteLength` / `TextEncoder`: this module is
93
+ * re-exported from `schema/utils.ts`, which exists precisely to keep Node-only
94
+ * code out of browser builds, and allocating an encoder per identifier check
95
+ * would be wasteful besides.
96
+ */
97
+ function identifierByteLength(value) {
98
+ let bytes = 0;
99
+ for (let i = 0; i < value.length; i++) {
100
+ bytes += codeUnitWidth(value, i);
101
+ if (isSurrogatePair(value, i)) i++;
102
+ }
103
+ return bytes;
104
+ }
105
+ /**
106
+ * True when `value[index]` is a high surrogate genuinely followed by a low one.
107
+ *
108
+ * The low-surrogate half matters: an unpaired high surrogate is NOT half of a
109
+ * 4-byte code point. Every UTF-8 encoder replaces it with U+FFFD, which is
110
+ * three bytes, so treating "high surrogate, something follows" as a pair
111
+ * *under*-counts a malformed name and can let it past the guard — exactly the
112
+ * silent truncation this module exists to prevent.
113
+ */
114
+ function isSurrogatePair(value, index) {
115
+ const high = value.charCodeAt(index);
116
+ if (high < 55296 || high > 56319) return false;
117
+ const low = value.charCodeAt(index + 1);
118
+ return low >= 56320 && low <= 57343;
119
+ }
120
+ /**
121
+ * UTF-8 byte width of the code point starting at `value[index]`. An unpaired
122
+ * surrogate counts as the three bytes of its U+FFFD replacement.
123
+ */
124
+ function codeUnitWidth(value, index) {
125
+ const code = value.charCodeAt(index);
126
+ if (code < 128) return 1;
127
+ if (code < 2048) return 2;
128
+ return isSurrogatePair(value, index) ? 4 : 3;
129
+ }
130
+ /**
131
+ * Longest prefix of `value` that encodes to at most `maxBytes`, never splitting
132
+ * a code point or a surrogate pair.
133
+ */
134
+ function truncateToBytes(value, maxBytes) {
135
+ let bytes = 0;
136
+ for (let i = 0; i < value.length; i++) {
137
+ const width = codeUnitWidth(value, i);
138
+ if (bytes + width > maxBytes) return value.slice(0, i);
139
+ bytes += width;
140
+ if (isSurrogatePair(value, i)) i++;
141
+ }
142
+ return value;
143
+ }
144
+ /**
145
+ * Deterministic 64-bit FNV-1a digest of `value`, rendered as lowercase hex.
146
+ *
147
+ * Deliberately not `node:crypto`: see {@link identifierByteLength} on why this
148
+ * module must stay free of Node built-ins. FNV-1a is not a cryptographic hash
149
+ * and does not need to be — nothing here is a security boundary; the digest
150
+ * only has to be stable across runtimes, processes and SMRT versions, because
151
+ * a shortened index name that changed between releases would make every
152
+ * deployment drop and recreate the index.
153
+ */
154
+ function identifierDigest(value) {
155
+ const PRIME = 1099511628211n;
156
+ const MASK = 18446744073709551615n;
157
+ let hash = 14695981039346656037n;
158
+ for (let i = 0; i < value.length; i++) {
159
+ let code = value.codePointAt(i);
160
+ if (code > 65535) i++;
161
+ else if (code >= 55296 && code <= 57343) code = 65533;
162
+ const bytes = code < 128 ? [code] : code < 2048 ? [192 | code >> 6, 128 | code & 63] : code < 65536 ? [
163
+ 224 | code >> 12,
164
+ 128 | code >> 6 & 63,
165
+ 128 | code & 63
166
+ ] : [
167
+ 240 | code >> 18,
168
+ 128 | code >> 12 & 63,
169
+ 128 | code >> 6 & 63,
170
+ 128 | code & 63
171
+ ];
172
+ for (const byte of bytes) hash = (hash ^ BigInt(byte)) * PRIME & MASK;
173
+ }
174
+ return hash.toString(16).padStart(16, "0").slice(-10);
175
+ }
176
+ /**
177
+ * Shorten a **generated** identifier to fit {@link MAX_IDENTIFIER_BYTES},
178
+ * deterministically and reversibly-by-inspection.
179
+ *
180
+ * A name that already fits is returned unchanged — the overwhelming majority,
181
+ * so existing databases see no churn. An over-long name becomes
182
+ * `<head>_<digest><suffix>`, where `suffix` is the recognisable trailing token
183
+ * (`_idx`, `_unique_idx`, …) when one is present and there is room for it, and
184
+ * `head` is the longest prefix of the remainder that leaves the digest room.
185
+ *
186
+ * The digest is taken over the **full original name**, so two names that share
187
+ * a 63-byte prefix — the case PostgreSQL would collapse into one index — get
188
+ * different digests and stay distinct.
189
+ *
190
+ * Only generator-owned names go through here. A developer's own name
191
+ * (`@smrt({ indexes: [{ name }] })`) is validated with
192
+ * {@link assertIdentifierFits} instead: silently renaming what someone wrote by
193
+ * hand would be worse than refusing it.
194
+ *
195
+ * @param name - The generated identifier
196
+ * @param maxBytes - Override the limit (tests; defaults to the PostgreSQL one)
197
+ * @returns `name` when it fits, otherwise its deterministic short form
198
+ * @throws When `maxBytes` is too small to hold even a digest — the one case
199
+ * where shortening is genuinely impossible
200
+ * @example
201
+ * // 66 bytes → 63
202
+ * shortenIdentifier('content_contribution_revisions_contribution_id_revision_number_idx')
203
+ */
204
+ function shortenIdentifier(name, maxBytes = 63) {
205
+ if (identifierByteLength(name) <= maxBytes) return name;
206
+ const digest = identifierDigest(name);
207
+ const digestCost = 1 + digest.length;
208
+ if (digestCost >= maxBytes) throw new Error(`[smrt] Cannot shorten identifier "${name}" to ${maxBytes} bytes: the disambiguating digest alone needs ${digestCost}.`);
209
+ const suffix = PRESERVED_IDENTIFIER_SUFFIXES.find((candidate) => name.endsWith(candidate) && digestCost + candidate.length < maxBytes) ?? "";
210
+ return `${truncateToBytes(name.slice(0, name.length - suffix.length), maxBytes - digestCost - suffix.length).replace(/_+$/, "")}_${digest}${suffix}`;
211
+ }
212
+ /**
213
+ * Reject a **hand-declared index name** that cannot survive PostgreSQL.
214
+ *
215
+ * Scoped deliberately narrowly — to names SMRT neither owns nor ever compares
216
+ * loosely. A declared index name is both: the generator may not rewrite what a
217
+ * developer wrote, and `SchemaComparer` matches indexes **by name** first, so a
218
+ * 70-byte declaration can never match the 63-byte index PostgreSQL actually
219
+ * stored and `db:migrate` would emit `add_index` for it on every run, forever.
220
+ *
221
+ * This does NOT apply to table or column names, and deliberately so. PostgreSQL
222
+ * truncates identifiers *consistently on every reference* — `CREATE TABLE
223
+ * "<80 bytes>"` and a later `SELECT ... FROM "<the same 80 bytes>"` both resolve
224
+ * to the same stored 63-byte name — so a single long table name round-trips
225
+ * fine end to end. `smrt-users` relies on exactly that: it ships an intentional
226
+ * 80-byte `@smrt({ tableName })` and generates unique Postgres RLS policy names
227
+ * from it. Erroring there would break a supported, tested case for no safety
228
+ * gain; the collision hazard that remains is a name the developer chose, not
229
+ * one the generator manufactured.
230
+ *
231
+ * @param name - The identifier to check
232
+ * @param kind - What it is, for the message (`Declared index`, …)
233
+ * @param context - Where it came from (`table "x"`), for the message
234
+ * @param maxBytes - Override the limit (tests)
235
+ * @throws When `name` exceeds the limit
236
+ */
237
+ function assertIdentifierFits(name, kind, context, maxBytes = 63) {
238
+ const bytes = identifierByteLength(name);
239
+ if (bytes <= maxBytes) return;
240
+ throw new Error(`[smrt] ${kind} name "${name}" on ${context} is ${bytes} bytes; PostgreSQL truncates identifiers at ${maxBytes} bytes, which silently collapses distinct names into one. Shorten it to ${maxBytes} bytes or fewer.`);
241
+ }
242
+ /**
243
+ * Apply the identifier guard to one generated schema, in place.
244
+ *
245
+ * Call this as the LAST step of every schema path, after every index pass has
246
+ * run, so it sees the final set. Shortening is safe to do here rather than at
247
+ * each `indexes.push()`: the digest is taken over the full original name, so
248
+ * two entries that were distinct before shortening stay distinct after, and the
249
+ * "does this name already exist" dedupe the passes perform upstream is
250
+ * unaffected.
251
+ *
252
+ * Only index names are touched. The table name is taken as given — see
253
+ * {@link assertIdentifierFits} on why a long table name is not an error — and
254
+ * is used solely to build the collision message. Note that an over-long table
255
+ * name still yields *in-limit, distinct* index names, because the shortening
256
+ * runs over the whole composed name.
257
+ *
258
+ * @param tableName - The table these indexes belong to, for the message
259
+ * @param indexes - The generated index list (names are shortened in place)
260
+ * @throws When shortening two index names produces a collision
261
+ */
262
+ function enforceIdentifierLimits(tableName, indexes) {
263
+ const seen = /* @__PURE__ */ new Map();
264
+ for (const index of indexes) {
265
+ const shortened = shortenIdentifier(index.name);
266
+ const previous = seen.get(shortened);
267
+ if (previous !== void 0 && previous !== index.name) throw new Error(`[smrt] Index names "${previous}" and "${index.name}" on table "${tableName}" both shorten to "${shortened}". Rename one of them.`);
268
+ seen.set(shortened, index.name);
269
+ index.name = shortened;
270
+ }
271
+ }
31
272
  //#endregion
32
- export { isJsonPathIndex, renderIndexTarget };
273
+ export { MAX_IDENTIFIER_BYTES, assertIdentifierFits, enforceIdentifierLimits, identifierByteLength, isJsonPathIndex, isStiSubtypeUniqueIndex, renderIndexTarget, shortenIdentifier };
33
274
 
34
275
  //# sourceMappingURL=index-utils.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index-utils.js","names":[],"sources":["../../src/schema/index-utils.ts"],"sourcesContent":["/**\n * Index-rendering helpers shared across DDL strategies, schema aggregator,\n * and the migration generator/differ.\n *\n * IMPORTANT: this module must remain a pure utility with **type-only\n * imports** to avoid introducing a cycle with `registry.ts` /\n * `collection.ts`. Don't add runtime imports that pull SmrtClass /\n * SmrtCollection / ObjectRegistry through here.\n */\n\nimport type { DatabaseEngine } from './ddl/types.js';\nimport {\n isSafeIdentifier,\n isSafeIdentifierPath,\n quoteIdentifier,\n quoteStringLiteral,\n} from './sql-identifiers.js';\nimport type { IndexDefinition } from './types.js';\n\n/**\n * Render the SQL target of a CREATE INDEX statement.\n *\n * For ordinary indexes this is a comma-separated, double-quoted column list.\n * For JSON-path indexes (introduced by `@meta({ indexed: true })`) it is the\n * dialect-specific expression that points into a JSONB column.\n *\n * @param index - The index definition (may carry `jsonPath`)\n * @param engine - The target database dialect\n * @returns The contents of the trailing `(...)` of the CREATE INDEX statement\n */\nexport function renderIndexTarget(\n index: Pick<IndexDefinition, 'columns' | 'jsonPath'>,\n engine: DatabaseEngine,\n): string {\n if (index.jsonPath?.column && index.jsonPath.path) {\n const col = index.jsonPath.column;\n const path = index.jsonPath.path;\n // The column is interpolated as an identifier and the path as a SQL string\n // literal. Validate both against an identifier allowlist (these are\n // developer-controlled `@meta` field names) so a malformed name can't\n // smuggle structure into the expression even after escaping.\n if (!isSafeIdentifier(col)) {\n throw new Error(\n `[index-utils] Unsafe JSON-path index column \"${col}\": must be a simple identifier`,\n );\n }\n if (!isSafeIdentifierPath(path)) {\n throw new Error(\n `[index-utils] Unsafe JSON-path index path \"${path}\": must be a simple (dotted) identifier`,\n );\n }\n if (engine === 'sqlite') {\n // SQLite's json_extract is a function call — no extra wrapping needed.\n return `json_extract(${quoteIdentifier(col)}, ${quoteStringLiteral(\n `$.${path}`,\n )})`;\n }\n // Postgres / DuckDB JSON path access via `->>`. PostgreSQL requires\n // operator expressions in index elements to be parenthesized — the\n // outer `()` in `CREATE INDEX ... ON tbl (...)` is the column list, so\n // the expression itself needs its own parens. Returning the wrapped\n // form here keeps the rule local to this helper and works on DuckDB\n // (extra parens are harmless).\n return `(${quoteIdentifier(col)}->>${quoteStringLiteral(path)})`;\n }\n return (index.columns ?? []).map((c) => quoteIdentifier(c)).join(', ');\n}\n\n/**\n * True if the index has a valid jsonPath target.\n */\nexport function isJsonPathIndex(\n index: Pick<IndexDefinition, 'jsonPath'>,\n): boolean {\n return !!(index.jsonPath?.column && index.jsonPath.path);\n}\n"],"mappings":";;;;;;;;;;;;;AA8BA,SAAgB,kBACd,OACA,QACQ;CACR,IAAI,MAAM,UAAU,UAAU,MAAM,SAAS,MAAM;EACjD,MAAM,MAAM,MAAM,SAAS;EAC3B,MAAM,OAAO,MAAM,SAAS;EAK5B,IAAI,CAAC,iBAAiB,GAAG,GACvB,MAAM,IAAI,MACR,gDAAgD,IAAI,+BACtD;EAEF,IAAI,CAAC,qBAAqB,IAAI,GAC5B,MAAM,IAAI,MACR,8CAA8C,KAAK,wCACrD;EAEF,IAAI,WAAW,UAEb,OAAO,gBAAgB,gBAAgB,GAAG,EAAE,IAAI,mBAC9C,KAAK,MACP,EAAE;EAQJ,OAAO,IAAI,gBAAgB,GAAG,EAAE,KAAK,mBAAmB,IAAI,EAAE;CAChE;CACA,QAAQ,MAAM,WAAW,CAAC,EAAA,CAAG,KAAK,MAAM,gBAAgB,CAAC,CAAC,CAAC,CAAC,KAAK,IAAI;AACvE;;;;AAKA,SAAgB,gBACd,OACS;CACT,OAAO,CAAC,EAAE,MAAM,UAAU,UAAU,MAAM,SAAS;AACrD"}
1
+ {"version":3,"file":"index-utils.js","names":[],"sources":["../../src/schema/index-utils.ts"],"sourcesContent":["/**\n * Index-rendering helpers shared across DDL strategies, schema aggregator,\n * and the migration generator/differ.\n *\n * IMPORTANT: this module must remain a pure utility with **type-only\n * imports** to avoid introducing a cycle with `registry.ts` /\n * `collection.ts`. Don't add runtime imports that pull SmrtClass /\n * SmrtCollection / ObjectRegistry through here.\n */\n\nimport type { DatabaseEngine } from './ddl/types.js';\nimport {\n isSafeIdentifier,\n isSafeIdentifierPath,\n quoteIdentifier,\n quoteStringLiteral,\n} from './sql-identifiers.js';\nimport type { IndexDefinition } from './types.js';\n\n/**\n * Render the SQL target of a CREATE INDEX statement.\n *\n * For ordinary indexes this is a comma-separated, double-quoted column list.\n * For JSON-path indexes (introduced by `@meta({ indexed: true })`) it is the\n * dialect-specific expression that points into a JSONB column.\n *\n * @param index - The index definition (may carry `jsonPath`)\n * @param engine - The target database dialect\n * @returns The contents of the trailing `(...)` of the CREATE INDEX statement\n */\nexport function renderIndexTarget(\n index: Pick<IndexDefinition, 'columns' | 'jsonPath'>,\n engine: DatabaseEngine,\n): string {\n if (index.jsonPath?.column && index.jsonPath.path) {\n const col = index.jsonPath.column;\n const path = index.jsonPath.path;\n // The column is interpolated as an identifier and the path as a SQL string\n // literal. Validate both against an identifier allowlist (these are\n // developer-controlled `@meta` field names) so a malformed name can't\n // smuggle structure into the expression even after escaping.\n if (!isSafeIdentifier(col)) {\n throw new Error(\n `[index-utils] Unsafe JSON-path index column \"${col}\": must be a simple identifier`,\n );\n }\n if (!isSafeIdentifierPath(path)) {\n throw new Error(\n `[index-utils] Unsafe JSON-path index path \"${path}\": must be a simple (dotted) identifier`,\n );\n }\n if (engine === 'sqlite') {\n // SQLite's json_extract is a function call — no extra wrapping needed.\n return `json_extract(${quoteIdentifier(col)}, ${quoteStringLiteral(\n `$.${path}`,\n )})`;\n }\n // Postgres / DuckDB JSON path access via `->>`. PostgreSQL requires\n // operator expressions in index elements to be parenthesized — the\n // outer `()` in `CREATE INDEX ... ON tbl (...)` is the column list, so\n // the expression itself needs its own parens. Returning the wrapped\n // form here keeps the rule local to this helper and works on DuckDB\n // (extra parens are harmless).\n return `(${quoteIdentifier(col)}->>${quoteStringLiteral(path)})`;\n }\n return (index.columns ?? []).map((c) => quoteIdentifier(c)).join(', ');\n}\n\n/**\n * True if the index has a valid jsonPath target.\n */\nexport function isJsonPathIndex(\n index: Pick<IndexDefinition, 'jsonPath'>,\n): boolean {\n return !!(index.jsonPath?.column && index.jsonPath.path);\n}\n\n/**\n * The exact predicate `SchemaGenerator.emitStiUniqueIndexes()` emits —\n * `_meta_type = '<qualified class>'` and nothing else — with tolerance for the\n * cosmetic re-rendering PostgreSQL applies when it echoes a predicate back\n * (`::text` casts, wrapping parentheses). A predicate that merely STARTS with\n * `_meta_type = …` but carries further conjuncts (`AND active = TRUE`) is a\n * caller-declared partial unique (#2357 `@smrt({ indexes })`) and must keep\n * the ordinary degrade-to-full-UNIQUE behaviour, so it does not match.\n */\nconst STI_SUBTYPE_PREDICATE =\n /^\\s*\\(*\\s*_meta_type\\s*\\)?\\s*(?:::\\w+)?\\s*=\\s*'(?:[^']|'')*'\\s*(?:::\\w+)?\\s*\\)*\\s*$/;\n\n/**\n * True for a UNIQUE index whose predicate scopes it to one STI subtype\n * (`WHERE _meta_type = '<qualified class>'`) — the shape `SchemaGenerator`\n * emits for `@field({ unique: true })` declared only on an STI descendant\n * (#2359).\n *\n * Engines without partial indexes (DuckDB, and the JSON adapter it backs)\n * degrade an ordinary partial index to a full index, and a caller-declared\n * partial UNIQUE (`WHERE active = TRUE`) to a full UNIQUE — a stricter but\n * intended approximation. This shape is the exception: widening it would\n * enforce one subtype's uniqueness across every sibling's rows in the shared\n * table, so those engines skip it instead. Kept here so the DDL strategy and\n * the migration differ apply the same test.\n */\nexport function isStiSubtypeUniqueIndex(\n index: Pick<IndexDefinition, 'unique' | 'where'>,\n): boolean {\n return (\n index.unique === true &&\n typeof index.where === 'string' &&\n STI_SUBTYPE_PREDICATE.test(index.where)\n );\n}\n\n/**\n * The strictest identifier length any SMRT-supported engine imposes: 63 bytes.\n *\n * PostgreSQL compiles with `NAMEDATALEN = 64`, so every table, column, index,\n * constraint and trigger name is silently truncated to 63 **bytes** (not\n * characters — the limit is on the UTF-8 encoding). SQLite and DuckDB have no\n * practical limit, which is exactly why the overflow went unnoticed: a name\n * that works in every test truncates in production (#2374, finding C5).\n *\n * Silent truncation is not merely cosmetic. `CREATE INDEX IF NOT EXISTS` on a\n * name whose first 63 bytes match an existing index is a no-op, so a second\n * index that differs only past byte 63 is never created; the migration differ\n * then finds it missing on every run and emits `add_index` forever. The shipped\n * `content_contribution_revisions_contribution_id_revision_number_idx` (66\n * bytes) is the concrete case — only the differ's signature-equivalence check\n * kept it from looping.\n */\nexport const MAX_IDENTIFIER_BYTES = 63;\n\n/**\n * Hex digits of the disambiguating digest appended to a shortened identifier.\n *\n * 40 bits. The digest exists to keep two names that share a long prefix apart,\n * so its only job is collision resistance across the handful of over-length\n * identifiers a schema produces; {@link enforceIdentifierLimits} additionally\n * fails loudly if two names on one table ever do collide, so a residual\n * collision can never reproduce the silent-no-op bug this guard fixes.\n */\nconst IDENTIFIER_DIGEST_HEX = 10;\n\n/**\n * Trailing tokens preserved across shortening so a shortened name still reads\n * as what it is (`..._idx` stays an index) and so suffix-based filters — the\n * differ's `_pkey` / `_key` protection, for one — keep matching.\n *\n * Ordered longest-first: `_unique_idx` must win over `_idx`.\n */\nconst PRESERVED_IDENTIFIER_SUFFIXES = [\n '_unique_idx',\n '_pkey',\n '_idx',\n '_key',\n] as const;\n\n/**\n * Byte length of `value` when encoded as UTF-8 — what PostgreSQL counts.\n *\n * Hand-rolled rather than `Buffer.byteLength` / `TextEncoder`: this module is\n * re-exported from `schema/utils.ts`, which exists precisely to keep Node-only\n * code out of browser builds, and allocating an encoder per identifier check\n * would be wasteful besides.\n */\nexport function identifierByteLength(value: string): number {\n let bytes = 0;\n for (let i = 0; i < value.length; i++) {\n bytes += codeUnitWidth(value, i);\n if (isSurrogatePair(value, i)) i++;\n }\n return bytes;\n}\n\n/**\n * True when `value[index]` is a high surrogate genuinely followed by a low one.\n *\n * The low-surrogate half matters: an unpaired high surrogate is NOT half of a\n * 4-byte code point. Every UTF-8 encoder replaces it with U+FFFD, which is\n * three bytes, so treating \"high surrogate, something follows\" as a pair\n * *under*-counts a malformed name and can let it past the guard — exactly the\n * silent truncation this module exists to prevent.\n */\nfunction isSurrogatePair(value: string, index: number): boolean {\n const high = value.charCodeAt(index);\n if (high < 0xd800 || high > 0xdbff) return false;\n const low = value.charCodeAt(index + 1);\n return low >= 0xdc00 && low <= 0xdfff;\n}\n\n/**\n * UTF-8 byte width of the code point starting at `value[index]`. An unpaired\n * surrogate counts as the three bytes of its U+FFFD replacement.\n */\nfunction codeUnitWidth(value: string, index: number): number {\n const code = value.charCodeAt(index);\n if (code < 0x80) return 1;\n if (code < 0x800) return 2;\n return isSurrogatePair(value, index) ? 4 : 3;\n}\n\n/**\n * Longest prefix of `value` that encodes to at most `maxBytes`, never splitting\n * a code point or a surrogate pair.\n */\nfunction truncateToBytes(value: string, maxBytes: number): string {\n let bytes = 0;\n for (let i = 0; i < value.length; i++) {\n const width = codeUnitWidth(value, i);\n if (bytes + width > maxBytes) return value.slice(0, i);\n bytes += width;\n if (isSurrogatePair(value, i)) i++;\n }\n return value;\n}\n\n/**\n * Deterministic 64-bit FNV-1a digest of `value`, rendered as lowercase hex.\n *\n * Deliberately not `node:crypto`: see {@link identifierByteLength} on why this\n * module must stay free of Node built-ins. FNV-1a is not a cryptographic hash\n * and does not need to be — nothing here is a security boundary; the digest\n * only has to be stable across runtimes, processes and SMRT versions, because\n * a shortened index name that changed between releases would make every\n * deployment drop and recreate the index.\n */\nfunction identifierDigest(value: string): string {\n const PRIME = 0x100000001b3n;\n const MASK = 0xffffffffffffffffn;\n let hash = 0xcbf29ce484222325n;\n // Hash the UTF-8 bytes, not the UTF-16 code units, so the digest is taken\n // over exactly what PostgreSQL receives — and over exactly what\n // `identifierByteLength` counted. An unpaired surrogate is therefore hashed\n // as its U+FFFD replacement, which is what every real encoder puts on the\n // wire; emitting the surrogate's own three bytes instead would hash bytes the\n // server never sees.\n for (let i = 0; i < value.length; i++) {\n let code = value.codePointAt(i) as number;\n if (code > 0xffff) {\n i++;\n } else if (code >= 0xd800 && code <= 0xdfff) {\n code = 0xfffd;\n }\n const bytes =\n code < 0x80\n ? [code]\n : code < 0x800\n ? [0xc0 | (code >> 6), 0x80 | (code & 0x3f)]\n : code < 0x10000\n ? [\n 0xe0 | (code >> 12),\n 0x80 | ((code >> 6) & 0x3f),\n 0x80 | (code & 0x3f),\n ]\n : [\n 0xf0 | (code >> 18),\n 0x80 | ((code >> 12) & 0x3f),\n 0x80 | ((code >> 6) & 0x3f),\n 0x80 | (code & 0x3f),\n ];\n for (const byte of bytes) {\n hash = ((hash ^ BigInt(byte)) * PRIME) & MASK;\n }\n }\n return hash.toString(16).padStart(16, '0').slice(-IDENTIFIER_DIGEST_HEX);\n}\n\n/**\n * Shorten a **generated** identifier to fit {@link MAX_IDENTIFIER_BYTES},\n * deterministically and reversibly-by-inspection.\n *\n * A name that already fits is returned unchanged — the overwhelming majority,\n * so existing databases see no churn. An over-long name becomes\n * `<head>_<digest><suffix>`, where `suffix` is the recognisable trailing token\n * (`_idx`, `_unique_idx`, …) when one is present and there is room for it, and\n * `head` is the longest prefix of the remainder that leaves the digest room.\n *\n * The digest is taken over the **full original name**, so two names that share\n * a 63-byte prefix — the case PostgreSQL would collapse into one index — get\n * different digests and stay distinct.\n *\n * Only generator-owned names go through here. A developer's own name\n * (`@smrt({ indexes: [{ name }] })`) is validated with\n * {@link assertIdentifierFits} instead: silently renaming what someone wrote by\n * hand would be worse than refusing it.\n *\n * @param name - The generated identifier\n * @param maxBytes - Override the limit (tests; defaults to the PostgreSQL one)\n * @returns `name` when it fits, otherwise its deterministic short form\n * @throws When `maxBytes` is too small to hold even a digest — the one case\n * where shortening is genuinely impossible\n * @example\n * // 66 bytes → 63\n * shortenIdentifier('content_contribution_revisions_contribution_id_revision_number_idx')\n */\nexport function shortenIdentifier(\n name: string,\n maxBytes: number = MAX_IDENTIFIER_BYTES,\n): string {\n if (identifierByteLength(name) <= maxBytes) return name;\n\n const digest = identifierDigest(name);\n // `_` + digest is the irreducible part of the short form.\n const digestCost = 1 + digest.length;\n if (digestCost >= maxBytes) {\n throw new Error(\n `[smrt] Cannot shorten identifier \"${name}\" to ${maxBytes} bytes: the ` +\n `disambiguating digest alone needs ${digestCost}.`,\n );\n }\n\n const suffix =\n PRESERVED_IDENTIFIER_SUFFIXES.find(\n (candidate) =>\n name.endsWith(candidate) &&\n // Keep at least one byte of head, or the name becomes just a digest.\n digestCost + candidate.length < maxBytes,\n ) ?? '';\n\n // The trailing `_` is trimmed because the digest brings its own separator.\n const head = truncateToBytes(\n name.slice(0, name.length - suffix.length),\n maxBytes - digestCost - suffix.length,\n ).replace(/_+$/, '');\n\n return `${head}_${digest}${suffix}`;\n}\n\n/**\n * Reject a **hand-declared index name** that cannot survive PostgreSQL.\n *\n * Scoped deliberately narrowly — to names SMRT neither owns nor ever compares\n * loosely. A declared index name is both: the generator may not rewrite what a\n * developer wrote, and `SchemaComparer` matches indexes **by name** first, so a\n * 70-byte declaration can never match the 63-byte index PostgreSQL actually\n * stored and `db:migrate` would emit `add_index` for it on every run, forever.\n *\n * This does NOT apply to table or column names, and deliberately so. PostgreSQL\n * truncates identifiers *consistently on every reference* — `CREATE TABLE\n * \"<80 bytes>\"` and a later `SELECT ... FROM \"<the same 80 bytes>\"` both resolve\n * to the same stored 63-byte name — so a single long table name round-trips\n * fine end to end. `smrt-users` relies on exactly that: it ships an intentional\n * 80-byte `@smrt({ tableName })` and generates unique Postgres RLS policy names\n * from it. Erroring there would break a supported, tested case for no safety\n * gain; the collision hazard that remains is a name the developer chose, not\n * one the generator manufactured.\n *\n * @param name - The identifier to check\n * @param kind - What it is, for the message (`Declared index`, …)\n * @param context - Where it came from (`table \"x\"`), for the message\n * @param maxBytes - Override the limit (tests)\n * @throws When `name` exceeds the limit\n */\nexport function assertIdentifierFits(\n name: string,\n kind: string,\n context: string,\n maxBytes: number = MAX_IDENTIFIER_BYTES,\n): void {\n const bytes = identifierByteLength(name);\n if (bytes <= maxBytes) return;\n throw new Error(\n `[smrt] ${kind} name \"${name}\" on ${context} is ${bytes} bytes; ` +\n `PostgreSQL truncates identifiers at ${maxBytes} bytes, which silently ` +\n `collapses distinct names into one. Shorten it to ${maxBytes} bytes or fewer.`,\n );\n}\n\n/**\n * Apply the identifier guard to one generated schema, in place.\n *\n * Call this as the LAST step of every schema path, after every index pass has\n * run, so it sees the final set. Shortening is safe to do here rather than at\n * each `indexes.push()`: the digest is taken over the full original name, so\n * two entries that were distinct before shortening stay distinct after, and the\n * \"does this name already exist\" dedupe the passes perform upstream is\n * unaffected.\n *\n * Only index names are touched. The table name is taken as given — see\n * {@link assertIdentifierFits} on why a long table name is not an error — and\n * is used solely to build the collision message. Note that an over-long table\n * name still yields *in-limit, distinct* index names, because the shortening\n * runs over the whole composed name.\n *\n * @param tableName - The table these indexes belong to, for the message\n * @param indexes - The generated index list (names are shortened in place)\n * @throws When shortening two index names produces a collision\n */\nexport function enforceIdentifierLimits(\n tableName: string,\n indexes: Array<{ name: string }>,\n): void {\n const seen = new Map<string, string>();\n for (const index of indexes) {\n const shortened = shortenIdentifier(index.name);\n const previous = seen.get(shortened);\n if (previous !== undefined && previous !== index.name) {\n throw new Error(\n `[smrt] Index names \"${previous}\" and \"${index.name}\" on table ` +\n `\"${tableName}\" both shorten to \"${shortened}\". Rename one of them.`,\n );\n }\n seen.set(shortened, index.name);\n index.name = shortened;\n }\n}\n"],"mappings":"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"}
@@ -10,7 +10,7 @@ export { SchemaOverrideSystem } from './override-system.js';
10
10
  export type { AggregatedTable, AggregateOptions, AggregationResult, } from './schema-aggregator.js';
11
11
  export { SchemaAggregator } from './schema-aggregator.js';
12
12
  export { createSchemaManager, SchemaManager } from './schema-manager.js';
13
- export { getSystemTableShapes, parseTableShapes, type SystemTableColumnShape, type SystemTableIndexShape, type SystemTableShape, } from './system-table-shapes.js';
13
+ export { getSystemTableShapes, parseTableShapes, SYSTEM_TABLE_NAMES, type SystemTableColumnShape, type SystemTableIndexShape, type SystemTableShape, } from './system-table-shapes.js';
14
14
  export type { ColumnDefinition, ForeignKeyDefinition, IndexDefinition, SchemaDefinition, SchemaManifest, SchemaMigration, SchemaOverride, TriggerDefinition, } from './types.js';
15
15
  export * from './types.js';
16
16
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/schema/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,mBAAmB,EAAE,MAAM,qBAAqB,CAAC;AAE1D,cAAc,gBAAgB,CAAC;AAC/B,OAAO,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAC;AAEjD,OAAO,EACL,KAAK,mBAAmB,EACxB,qBAAqB,EACrB,KAAK,mBAAmB,EACxB,KAAK,iBAAiB,EACtB,KAAK,qBAAqB,EAC1B,KAAK,kBAAkB,EACvB,qBAAqB,EACrB,KAAK,uBAAuB,EAC5B,KAAK,sBAAsB,EAC3B,gBAAgB,EAChB,oBAAoB,GACrB,MAAM,kBAAkB,CAAC;AAG1B,OAAO,EACL,KAAK,sBAAsB,EAC3B,qBAAqB,EACrB,KAAK,kBAAkB,EACvB,KAAK,iBAAiB,EACtB,KAAK,kBAAkB,EACvB,4BAA4B,EAC5B,4BAA4B,EAC5B,0BAA0B,EAC1B,yBAAyB,EACzB,4BAA4B,GAC7B,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EAAE,oBAAoB,EAAE,MAAM,sBAAsB,CAAC;AAC5D,YAAY,EACV,eAAe,EACf,gBAAgB,EAChB,iBAAiB,GAClB,MAAM,wBAAwB,CAAC;AAChC,OAAO,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAC1D,OAAO,EAAE,mBAAmB,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACzE,OAAO,EACL,oBAAoB,EACpB,gBAAgB,EAChB,KAAK,sBAAsB,EAC3B,KAAK,qBAAqB,EAC1B,KAAK,gBAAgB,GACtB,MAAM,0BAA0B,CAAC;AAGlC,YAAY,EACV,gBAAgB,EAChB,oBAAoB,EACpB,eAAe,EACf,gBAAgB,EAChB,cAAc,EACd,eAAe,EACf,cAAc,EACd,iBAAiB,GAClB,MAAM,YAAY,CAAC;AACpB,cAAc,YAAY,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/schema/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,mBAAmB,EAAE,MAAM,qBAAqB,CAAC;AAE1D,cAAc,gBAAgB,CAAC;AAC/B,OAAO,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAC;AAEjD,OAAO,EACL,KAAK,mBAAmB,EACxB,qBAAqB,EACrB,KAAK,mBAAmB,EACxB,KAAK,iBAAiB,EACtB,KAAK,qBAAqB,EAC1B,KAAK,kBAAkB,EACvB,qBAAqB,EACrB,KAAK,uBAAuB,EAC5B,KAAK,sBAAsB,EAC3B,gBAAgB,EAChB,oBAAoB,GACrB,MAAM,kBAAkB,CAAC;AAG1B,OAAO,EACL,KAAK,sBAAsB,EAC3B,qBAAqB,EACrB,KAAK,kBAAkB,EACvB,KAAK,iBAAiB,EACtB,KAAK,kBAAkB,EACvB,4BAA4B,EAC5B,4BAA4B,EAC5B,0BAA0B,EAC1B,yBAAyB,EACzB,4BAA4B,GAC7B,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EAAE,oBAAoB,EAAE,MAAM,sBAAsB,CAAC;AAC5D,YAAY,EACV,eAAe,EACf,gBAAgB,EAChB,iBAAiB,GAClB,MAAM,wBAAwB,CAAC;AAChC,OAAO,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAC1D,OAAO,EAAE,mBAAmB,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACzE,OAAO,EACL,oBAAoB,EACpB,gBAAgB,EAChB,kBAAkB,EAClB,KAAK,sBAAsB,EAC3B,KAAK,qBAAqB,EAC1B,KAAK,gBAAgB,GACtB,MAAM,0BAA0B,CAAC;AAGlC,YAAY,EACV,gBAAgB,EAChB,oBAAoB,EACpB,eAAe,EACf,gBAAgB,EAChB,cAAc,EACd,eAAe,EACf,cAAc,EACd,iBAAiB,GAClB,MAAM,YAAY,CAAC;AACpB,cAAc,YAAY,CAAC"}
@@ -4,12 +4,12 @@ import { JsonDuckDBStrategy } from "./ddl/json-duckdb-strategy.js";
4
4
  import { PostgresStrategy } from "./ddl/postgres-strategy.js";
5
5
  import { SQLiteStrategy } from "./ddl/sqlite-strategy.js";
6
6
  import { detectEngine, generateDDLForEngine, generateMultiEngineDDL, getAllStrategies, getDDLStrategy } from "./ddl/index.js";
7
+ import { SYSTEM_TABLE_NAMES, getSystemTableShapes, parseTableShapes } from "./system-table-shapes.js";
7
8
  import { SchemaGenerator } from "./generator.js";
8
9
  import { SchemaManager, createSchemaManager } from "./schema-manager.js";
9
10
  import { collectManifestTables, manifestColumnsToDefinitions, manifestIndexesToDefinitions, manifestSchemaToDefinition, mergeSchemaDefinitionInto, renderCollectedManifestTable } from "./manifest-schema.js";
10
- import { getSystemTableShapes, parseTableShapes } from "./system-table-shapes.js";
11
11
  import { LiveSchemaParityError, checkLiveSchemaParity, normalizeSqlType, parseIndexDefColumns } from "./live-parity.js";
12
12
  import { SchemaCodeGenerator } from "./code-generator.js";
13
13
  import { SchemaOverrideSystem } from "./override-system.js";
14
14
  import { SchemaAggregator } from "./schema-aggregator.js";
15
- export { BaseDDLStrategy, DuckDBStrategy, JsonDuckDBStrategy, LiveSchemaParityError, PostgresStrategy, SQLiteStrategy, SchemaAggregator, SchemaCodeGenerator, SchemaGenerator, SchemaManager, SchemaOverrideSystem, checkLiveSchemaParity, collectManifestTables, createSchemaManager, detectEngine, generateDDLForEngine, generateMultiEngineDDL, getAllStrategies, getDDLStrategy, getSystemTableShapes, manifestColumnsToDefinitions, manifestIndexesToDefinitions, manifestSchemaToDefinition, mergeSchemaDefinitionInto, normalizeSqlType, parseIndexDefColumns, parseTableShapes, renderCollectedManifestTable };
15
+ export { BaseDDLStrategy, DuckDBStrategy, JsonDuckDBStrategy, LiveSchemaParityError, PostgresStrategy, SQLiteStrategy, SYSTEM_TABLE_NAMES, SchemaAggregator, SchemaCodeGenerator, SchemaGenerator, SchemaManager, SchemaOverrideSystem, checkLiveSchemaParity, collectManifestTables, createSchemaManager, detectEngine, generateDDLForEngine, generateMultiEngineDDL, getAllStrategies, getDDLStrategy, getSystemTableShapes, manifestColumnsToDefinitions, manifestIndexesToDefinitions, manifestSchemaToDefinition, mergeSchemaDefinitionInto, normalizeSqlType, parseIndexDefColumns, parseTableShapes, renderCollectedManifestTable };
@@ -1 +1 @@
1
- {"version":3,"file":"live-parity.d.ts","sourceRoot":"","sources":["../../src/schema/live-parity.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"live-parity.d.ts","sourceRoot":"","sources":["../../src/schema/live-parity.ts"],"names":[],"mappings":"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"}
@@ -1,4 +1,5 @@
1
1
  import { detectEngine, getDDLStrategy } from "./ddl/index.js";
2
+ import { RETIRED_SYSTEM_TABLES } from "../system/schema.js";
2
3
  import { getSystemTableShapes } from "./system-table-shapes.js";
3
4
  //#region src/schema/live-parity.ts
4
5
  /**
@@ -59,13 +60,14 @@ async function checkLiveSchemaParity(options) {
59
60
  if (expectedNames.has(liveName)) continue;
60
61
  if (isInternalTableName(liveName)) continue;
61
62
  if (!includeSystemTables && liveName.startsWith("_smrt_")) continue;
63
+ const retired = RETIRED_SYSTEM_TABLES.includes(liveName);
62
64
  findings.push({
63
65
  kind: "extra_table",
64
66
  severity: "info",
65
67
  table: liveName,
66
68
  origin: liveName.startsWith("_smrt_") ? "system" : "application",
67
- message: `Live table \`${liveName}\` is not covered by any declared schema.`,
68
- recommendation: "Confirm the table belongs to another application sharing this database, or remove it once its owning model is gone."
69
+ message: retired ? `Live table \`${liveName}\` is a retired SMRT system table; nothing reads or writes it.` : `Live table \`${liveName}\` is not covered by any declared schema.`,
70
+ recommendation: retired ? `Drop it once you have confirmed it is unused: DROP TABLE IF EXISTS ${liveName};` : "Confirm the table belongs to another application sharing this database, or remove it once its owning model is gone."
69
71
  });
70
72
  }
71
73
  }