@happyvertical/smrt-core 0.40.69 → 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.
- package/AGENTS.md +29 -4
- package/README.md +20 -1
- package/agents/change-feed.md +1 -1
- package/agents/query-bounds.md +45 -0
- package/agents/schema-paths.md +786 -0
- package/dist/browser.d.ts +1 -0
- package/dist/browser.d.ts.map +1 -1
- package/dist/browser.js +5 -3
- package/dist/cascade.d.ts +120 -0
- package/dist/cascade.d.ts.map +1 -0
- package/dist/cascade.js +430 -0
- package/dist/cascade.js.map +1 -0
- package/dist/change-feed.d.ts +34 -2
- package/dist/change-feed.d.ts.map +1 -1
- package/dist/change-feed.js +52 -11
- package/dist/change-feed.js.map +1 -1
- package/dist/class.d.ts +36 -3
- package/dist/class.d.ts.map +1 -1
- package/dist/class.js +87 -9
- package/dist/class.js.map +1 -1
- package/dist/collection-cache.js +0 -0
- package/dist/collection-cache.js.map +1 -1
- package/dist/collection.d.ts +130 -2
- package/dist/collection.d.ts.map +1 -1
- package/dist/collection.js +290 -57
- package/dist/collection.js.map +1 -1
- package/dist/config.d.ts +10 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js.map +1 -1
- package/dist/database.d.ts +8 -0
- package/dist/database.d.ts.map +1 -1
- package/dist/database.js +16 -8
- package/dist/database.js.map +1 -1
- package/dist/db-errors.d.ts +105 -0
- package/dist/db-errors.d.ts.map +1 -0
- package/dist/db-errors.js +382 -0
- package/dist/db-errors.js.map +1 -0
- package/dist/decorators/index.d.ts +80 -6
- package/dist/decorators/index.d.ts.map +1 -1
- package/dist/decorators/index.js +102 -12
- package/dist/decorators/index.js.map +1 -1
- package/dist/dispatch/bus.d.ts.map +1 -1
- package/dist/dispatch/bus.js +4 -3
- package/dist/dispatch/bus.js.map +1 -1
- package/dist/dispatch/collections/Dispatches.d.ts.map +1 -1
- package/dist/dispatch/collections/Dispatches.js +19 -4
- package/dist/dispatch/collections/Dispatches.js.map +1 -1
- package/dist/dispatch/types.d.ts +5 -0
- package/dist/dispatch/types.d.ts.map +1 -1
- package/dist/embedded-write-queue.d.ts +46 -0
- package/dist/embedded-write-queue.d.ts.map +1 -0
- package/dist/embedded-write-queue.js +66 -0
- package/dist/embedded-write-queue.js.map +1 -0
- package/dist/embeddings/storage.d.ts +7 -0
- package/dist/embeddings/storage.d.ts.map +1 -1
- package/dist/embeddings/storage.js +29 -12
- package/dist/embeddings/storage.js.map +1 -1
- package/dist/errors.d.ts +31 -3
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +34 -2
- package/dist/errors.js.map +1 -1
- package/dist/generators/changes-route.d.ts.map +1 -1
- package/dist/generators/changes-route.js +6 -3
- package/dist/generators/changes-route.js.map +1 -1
- package/dist/generators/mcp-runtime-template.d.ts +8 -0
- package/dist/generators/mcp-runtime-template.d.ts.map +1 -1
- package/dist/generators/mcp-runtime-template.js +38 -4
- package/dist/generators/mcp-runtime-template.js.map +1 -1
- package/dist/generators/mcp.d.ts +16 -0
- package/dist/generators/mcp.d.ts.map +1 -1
- package/dist/generators/mcp.js +41 -3
- package/dist/generators/mcp.js.map +1 -1
- package/dist/generators/rest.d.ts +22 -0
- package/dist/generators/rest.d.ts.map +1 -1
- package/dist/generators/rest.js +34 -3
- package/dist/generators/rest.js.map +1 -1
- package/dist/hierarchical.js +1 -1
- package/dist/index.d.ts +7 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +12 -5
- package/dist/interceptors.d.ts +21 -0
- package/dist/interceptors.d.ts.map +1 -1
- package/dist/interceptors.js +27 -1
- package/dist/interceptors.js.map +1 -1
- package/dist/manifest/generator.d.ts.map +1 -1
- package/dist/manifest/generator.js +4 -7
- package/dist/manifest/generator.js.map +1 -1
- package/dist/manifest/static-manifest.js +10 -10
- package/dist/manifest/static-manifest.js.map +1 -1
- package/dist/manifest/store.js +1 -1
- package/dist/manifest/store.js.map +1 -1
- package/dist/manifest.json +19 -19
- package/dist/migrations/differ.d.ts +211 -9
- package/dist/migrations/differ.d.ts.map +1 -1
- package/dist/migrations/differ.js +613 -50
- package/dist/migrations/differ.js.map +1 -1
- package/dist/migrations/generator.d.ts +31 -4
- package/dist/migrations/generator.d.ts.map +1 -1
- package/dist/migrations/generator.js +49 -5
- package/dist/migrations/generator.js.map +1 -1
- package/dist/migrations/index.d.ts +4 -2
- package/dist/migrations/index.d.ts.map +1 -1
- package/dist/migrations/index.js +6 -3
- package/dist/migrations/minor-units.d.ts +162 -0
- package/dist/migrations/minor-units.d.ts.map +1 -0
- package/dist/migrations/minor-units.js +381 -0
- package/dist/migrations/minor-units.js.map +1 -0
- package/dist/migrations/orchestrate.js +35 -6
- package/dist/migrations/orchestrate.js.map +1 -1
- package/dist/migrations/sqlite-rebuild.d.ts +142 -0
- package/dist/migrations/sqlite-rebuild.d.ts.map +1 -0
- package/dist/migrations/sqlite-rebuild.js +514 -0
- package/dist/migrations/sqlite-rebuild.js.map +1 -0
- package/dist/migrations/tracker.d.ts +114 -1
- package/dist/migrations/tracker.d.ts.map +1 -1
- package/dist/migrations/tracker.js +331 -16
- package/dist/migrations/tracker.js.map +1 -1
- package/dist/migrations/types.d.ts +19 -4
- package/dist/migrations/types.d.ts.map +1 -1
- package/dist/migrations.js +6 -3
- package/dist/object.d.ts +142 -10
- package/dist/object.d.ts.map +1 -1
- package/dist/object.js +196 -41
- package/dist/object.js.map +1 -1
- package/dist/postgres-timeouts.d.ts +240 -0
- package/dist/postgres-timeouts.d.ts.map +1 -0
- package/dist/postgres-timeouts.js +204 -0
- package/dist/postgres-timeouts.js.map +1 -0
- package/dist/query-bounds.d.ts +101 -0
- package/dist/query-bounds.d.ts.map +1 -0
- package/dist/query-bounds.js +177 -0
- package/dist/query-bounds.js.map +1 -0
- package/dist/registry/class-registration.d.ts.map +1 -1
- package/dist/registry/class-registration.js +3 -1
- package/dist/registry/class-registration.js.map +1 -1
- package/dist/registry/manifest-field-merge.d.ts +12 -0
- package/dist/registry/manifest-field-merge.d.ts.map +1 -1
- package/dist/registry/manifest-field-merge.js +14 -2
- package/dist/registry/manifest-field-merge.js.map +1 -1
- package/dist/registry/schema-builder.d.ts +22 -1
- package/dist/registry/schema-builder.d.ts.map +1 -1
- package/dist/registry/schema-builder.js +205 -165
- package/dist/registry/schema-builder.js.map +1 -1
- package/dist/registry/types.d.ts +35 -3
- package/dist/registry/types.d.ts.map +1 -1
- package/dist/registry.d.ts +41 -46
- package/dist/registry.d.ts.map +1 -1
- package/dist/registry.js +61 -83
- package/dist/registry.js.map +1 -1
- package/dist/scanner/manifest-generator.d.ts +45 -0
- package/dist/scanner/manifest-generator.d.ts.map +1 -1
- package/dist/scanner/manifest-generator.js +92 -28
- package/dist/scanner/manifest-generator.js.map +1 -1
- package/dist/scanner/types.d.ts +5 -0
- package/dist/scanner/types.d.ts.map +1 -1
- package/dist/scanner/types.js.map +1 -1
- package/dist/schema/conflict-target.d.ts +104 -0
- package/dist/schema/conflict-target.d.ts.map +1 -0
- package/dist/schema/conflict-target.js +129 -0
- package/dist/schema/conflict-target.js.map +1 -0
- package/dist/schema/ddl/base-strategy.d.ts.map +1 -1
- package/dist/schema/ddl/base-strategy.js +2 -2
- package/dist/schema/ddl/base-strategy.js.map +1 -1
- package/dist/schema/ddl/duckdb-strategy.d.ts.map +1 -1
- package/dist/schema/ddl/duckdb-strategy.js +2 -1
- package/dist/schema/ddl/duckdb-strategy.js.map +1 -1
- package/dist/schema/ddl/postgres-strategy.d.ts.map +1 -1
- package/dist/schema/ddl/postgres-strategy.js +12 -1
- package/dist/schema/ddl/postgres-strategy.js.map +1 -1
- package/dist/schema/generator.d.ts +307 -6
- package/dist/schema/generator.d.ts.map +1 -1
- package/dist/schema/generator.js +510 -87
- package/dist/schema/generator.js.map +1 -1
- package/dist/schema/index-utils.d.ts +120 -0
- package/dist/schema/index-utils.d.ts.map +1 -1
- package/dist/schema/index-utils.js +242 -1
- package/dist/schema/index-utils.js.map +1 -1
- package/dist/schema/index.d.ts +3 -0
- package/dist/schema/index.d.ts.map +1 -1
- package/dist/schema/index.js +4 -1
- package/dist/schema/live-parity.d.ts +90 -0
- package/dist/schema/live-parity.d.ts.map +1 -0
- package/dist/schema/live-parity.js +602 -0
- package/dist/schema/live-parity.js.map +1 -0
- package/dist/schema/manifest-schema.d.ts +121 -0
- package/dist/schema/manifest-schema.d.ts.map +1 -0
- package/dist/schema/manifest-schema.js +267 -0
- package/dist/schema/manifest-schema.js.map +1 -0
- package/dist/schema/schema-aggregator.d.ts +24 -10
- package/dist/schema/schema-aggregator.d.ts.map +1 -1
- package/dist/schema/schema-aggregator.js +35 -90
- package/dist/schema/schema-aggregator.js.map +1 -1
- package/dist/schema/system-table-shapes.d.ts +65 -0
- package/dist/schema/system-table-shapes.d.ts.map +1 -0
- package/dist/schema/system-table-shapes.js +187 -0
- package/dist/schema/system-table-shapes.js.map +1 -0
- package/dist/schema/types.d.ts +103 -4
- package/dist/schema/types.d.ts.map +1 -1
- package/dist/schema/utils.d.ts +2 -1
- package/dist/schema/utils.d.ts.map +1 -1
- package/dist/schema/utils.js +5 -3
- package/dist/schema/utils.js.map +1 -1
- package/dist/schema.js +4 -1
- package/dist/smrt-knowledge.json +20 -8
- package/dist/sync/apply.d.ts.map +1 -1
- package/dist/sync/apply.js +9 -16
- package/dist/sync/apply.js.map +1 -1
- package/dist/system/compatibility.d.ts +42 -0
- package/dist/system/compatibility.d.ts.map +1 -1
- package/dist/system/compatibility.js +182 -9
- package/dist/system/compatibility.js.map +1 -1
- package/dist/system/index.d.ts +1 -0
- package/dist/system/index.d.ts.map +1 -1
- package/dist/system/index.js +3 -2
- package/dist/system/retention.d.ts +237 -0
- package/dist/system/retention.d.ts.map +1 -0
- package/dist/system/retention.js +497 -0
- package/dist/system/retention.js.map +1 -0
- package/dist/system/schema.d.ts +100 -15
- package/dist/system/schema.d.ts.map +1 -1
- package/dist/system/schema.js +81 -45
- package/dist/system/schema.js.map +1 -1
- package/dist/system/types.d.ts +0 -2
- package/dist/system/types.d.ts.map +1 -1
- package/dist/testing/database.d.ts.map +1 -1
- package/dist/testing/database.js +1 -0
- package/dist/testing/database.js.map +1 -1
- package/dist/vite-plugin/index.d.ts.map +1 -1
- package/dist/vite-plugin/index.js +4 -9
- package/dist/vite-plugin/index.js.map +1 -1
- package/dist/vite-plugin/sveltekit-generator.d.ts.map +1 -1
- package/dist/vite-plugin/sveltekit-generator.js +71 -5
- package/dist/vite-plugin/sveltekit-generator.js.map +1 -1
- package/dist/vite-plugin/web-collections.d.ts.map +1 -1
- package/dist/vite-plugin/web-collections.js +6 -4
- package/dist/vite-plugin/web-collections.js.map +1 -1
- package/package.json +5 -5
|
@@ -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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"}
|
package/dist/schema/index.d.ts
CHANGED
|
@@ -4,10 +4,13 @@
|
|
|
4
4
|
export { SchemaCodeGenerator } from './code-generator.js';
|
|
5
5
|
export * from './ddl/index.js';
|
|
6
6
|
export { SchemaGenerator } from './generator.js';
|
|
7
|
+
export { type ConflictTargetInput, checkLiveSchemaParity, type ExpectedTableOrigin, type LiveParityFinding, type LiveParityFindingKind, type LiveParitySeverity, LiveSchemaParityError, type LiveSchemaParityOptions, type LiveSchemaParityReport, normalizeSqlType, parseIndexDefColumns, } from './live-parity.js';
|
|
8
|
+
export { type CollectedManifestTable, collectManifestTables, type ManifestColumnLike, type ManifestIndexLike, type ManifestSchemaLike, manifestColumnsToDefinitions, manifestIndexesToDefinitions, manifestSchemaToDefinition, mergeSchemaDefinitionInto, renderCollectedManifestTable, } from './manifest-schema.js';
|
|
7
9
|
export { SchemaOverrideSystem } from './override-system.js';
|
|
8
10
|
export type { AggregatedTable, AggregateOptions, AggregationResult, } from './schema-aggregator.js';
|
|
9
11
|
export { SchemaAggregator } from './schema-aggregator.js';
|
|
10
12
|
export { createSchemaManager, SchemaManager } from './schema-manager.js';
|
|
13
|
+
export { getSystemTableShapes, parseTableShapes, SYSTEM_TABLE_NAMES, type SystemTableColumnShape, type SystemTableIndexShape, type SystemTableShape, } from './system-table-shapes.js';
|
|
11
14
|
export type { ColumnDefinition, ForeignKeyDefinition, IndexDefinition, SchemaDefinition, SchemaManifest, SchemaMigration, SchemaOverride, TriggerDefinition, } from './types.js';
|
|
12
15
|
export * from './types.js';
|
|
13
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;
|
|
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"}
|
package/dist/schema/index.js
CHANGED
|
@@ -4,9 +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";
|
|
10
|
+
import { collectManifestTables, manifestColumnsToDefinitions, manifestIndexesToDefinitions, manifestSchemaToDefinition, mergeSchemaDefinitionInto, renderCollectedManifestTable } from "./manifest-schema.js";
|
|
11
|
+
import { LiveSchemaParityError, checkLiveSchemaParity, normalizeSqlType, parseIndexDefColumns } from "./live-parity.js";
|
|
9
12
|
import { SchemaCodeGenerator } from "./code-generator.js";
|
|
10
13
|
import { SchemaOverrideSystem } from "./override-system.js";
|
|
11
14
|
import { SchemaAggregator } from "./schema-aggregator.js";
|
|
12
|
-
export { BaseDDLStrategy, DuckDBStrategy, JsonDuckDBStrategy, PostgresStrategy, SQLiteStrategy, SchemaAggregator, SchemaCodeGenerator, SchemaGenerator, SchemaManager, SchemaOverrideSystem, createSchemaManager, detectEngine, generateDDLForEngine, generateMultiEngineDDL, getAllStrategies, getDDLStrategy };
|
|
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 };
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { DatabaseInterface } from '@happyvertical/sql';
|
|
2
|
+
import { DatabaseEngine } from './ddl/types.js';
|
|
3
|
+
import { SchemaDefinition } from './types.js';
|
|
4
|
+
/** How badly a finding breaks the deployment. */
|
|
5
|
+
export type LiveParitySeverity = 'error' | 'warning' | 'info';
|
|
6
|
+
/** The class of drift a finding describes. */
|
|
7
|
+
export type LiveParityFindingKind = 'missing_table' | 'extra_table' | 'missing_column' | 'extra_column' | 'column_type_drift' | 'column_nullability_drift' | 'missing_index' | 'extra_index' | 'unindexed_reference' | 'conflict_target_unindexed' | 'conflict_target_not_unique' | 'unique_constraint_missing' | 'invalid_index';
|
|
8
|
+
/** One difference between the live database and the expected shape. */
|
|
9
|
+
export interface LiveParityFinding {
|
|
10
|
+
kind: LiveParityFindingKind;
|
|
11
|
+
severity: LiveParitySeverity;
|
|
12
|
+
/** Table the finding is about. */
|
|
13
|
+
table: string;
|
|
14
|
+
/** Column or index the finding is about, when it is narrower than a table. */
|
|
15
|
+
target?: string;
|
|
16
|
+
/** Whether the table is an application table or a `_smrt_*` system table. */
|
|
17
|
+
origin: ExpectedTableOrigin;
|
|
18
|
+
message: string;
|
|
19
|
+
recommendation: string;
|
|
20
|
+
details?: Record<string, unknown>;
|
|
21
|
+
}
|
|
22
|
+
/** Aggregate result of one parity run. */
|
|
23
|
+
export interface LiveSchemaParityReport {
|
|
24
|
+
engine: DatabaseEngine;
|
|
25
|
+
/** Expected tables that exist in the live database and were compared. */
|
|
26
|
+
tablesChecked: number;
|
|
27
|
+
/** Expected tables that are absent from the live database. */
|
|
28
|
+
tablesMissing: number;
|
|
29
|
+
/** Whether `_smrt_*` system tables participated. */
|
|
30
|
+
systemTablesIncluded: boolean;
|
|
31
|
+
/**
|
|
32
|
+
* `full` when index metadata (uniqueness, partial predicates, PostgreSQL
|
|
33
|
+
* validity) could be read for this engine; `unavailable` when it could not,
|
|
34
|
+
* in which case every index-class check is skipped rather than guessed.
|
|
35
|
+
*/
|
|
36
|
+
indexIntrospection: 'full' | 'unavailable';
|
|
37
|
+
findings: LiveParityFinding[];
|
|
38
|
+
counts: Record<LiveParitySeverity, number>;
|
|
39
|
+
/** True when no `error`-severity finding was produced. */
|
|
40
|
+
ok: boolean;
|
|
41
|
+
}
|
|
42
|
+
/** A declared upsert conflict target for one table. */
|
|
43
|
+
export interface ConflictTargetInput {
|
|
44
|
+
columns: string[];
|
|
45
|
+
/** Where the target came from, e.g. a class name — used in messages. */
|
|
46
|
+
source?: string;
|
|
47
|
+
}
|
|
48
|
+
export interface LiveSchemaParityOptions {
|
|
49
|
+
db: DatabaseInterface;
|
|
50
|
+
/** Expected application table shapes, keyed by table name. */
|
|
51
|
+
schemas?: Record<string, SchemaDefinition>;
|
|
52
|
+
/** Declared upsert conflict targets, keyed by table name. */
|
|
53
|
+
conflictTargets?: Record<string, ConflictTargetInput[]>;
|
|
54
|
+
/** Include `_smrt_*` system tables (default true). */
|
|
55
|
+
includeSystemTables?: boolean;
|
|
56
|
+
/** Explicit engine hint for adapters whose URL is empty or ambiguous. */
|
|
57
|
+
engineHint?: string;
|
|
58
|
+
/** Report live tables no expected shape covers (default true). */
|
|
59
|
+
reportExtraTables?: boolean;
|
|
60
|
+
}
|
|
61
|
+
/** Whether an expected table is an application table or a system table. */
|
|
62
|
+
export type ExpectedTableOrigin = 'application' | 'system';
|
|
63
|
+
/**
|
|
64
|
+
* Thrown when the live database cannot be introspected. Callers fail closed:
|
|
65
|
+
* an unreadable database is never reported as "in sync".
|
|
66
|
+
*/
|
|
67
|
+
export declare class LiveSchemaParityError extends Error {
|
|
68
|
+
constructor(message: string, options?: {
|
|
69
|
+
cause?: unknown;
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Compare a live database against the expected shape and return every
|
|
74
|
+
* difference found.
|
|
75
|
+
*
|
|
76
|
+
* @throws {LiveSchemaParityError} when the database cannot be reached or the
|
|
77
|
+
* adapter cannot describe a table. Connection problems must fail closed.
|
|
78
|
+
*/
|
|
79
|
+
export declare function checkLiveSchemaParity(options: LiveSchemaParityOptions): Promise<LiveSchemaParityReport>;
|
|
80
|
+
/** Fold engine-specific type spellings into comparable buckets. */
|
|
81
|
+
export declare function normalizeSqlType(type: string): string;
|
|
82
|
+
/**
|
|
83
|
+
* Extract the indexed column list from a `CREATE INDEX` definition.
|
|
84
|
+
*
|
|
85
|
+
* Expression components (`lower(name)`, `(meta ->> 'x')`) are kept verbatim so
|
|
86
|
+
* they never collide with a plain column of the same name; ordering is
|
|
87
|
+
* preserved because leading-column coverage is what the policy checks.
|
|
88
|
+
*/
|
|
89
|
+
export declare function parseIndexDefColumns(indexDef: string): string[];
|
|
90
|
+
//# sourceMappingURL=live-parity.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"live-parity.d.ts","sourceRoot":"","sources":["../../src/schema/live-parity.ts"],"names":[],"mappings":"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"}
|