@happyvertical/smrt-core 0.40.70 → 0.42.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 +3 -3
- package/agents/change-feed.md +1 -1
- package/agents/schema-paths.md +561 -32
- package/dist/browser.js +5 -4
- 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 +54 -13
- package/dist/change-feed.js.map +1 -1
- package/dist/change-signals.d.ts.map +1 -1
- package/dist/change-signals.js +8 -1
- package/dist/change-signals.js.map +1 -1
- package/dist/class.d.ts +36 -3
- package/dist/class.d.ts.map +1 -1
- package/dist/class.js +69 -14
- 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 +29 -0
- package/dist/collection.d.ts.map +1 -1
- package/dist/collection.js +68 -16
- 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/decorators/index.d.ts +63 -5
- package/dist/decorators/index.d.ts.map +1 -1
- package/dist/decorators/index.js +37 -5
- package/dist/decorators/index.js.map +1 -1
- package/dist/dispatch/collections/Dispatches.d.ts.map +1 -1
- package/dist/dispatch/collections/Dispatches.js +21 -6
- package/dist/dispatch/collections/Dispatches.js.map +1 -1
- package/dist/dispatch/models/Dispatch.d.ts +1 -1
- package/dist/dispatch/models/Dispatch.d.ts.map +1 -1
- package/dist/dispatch/models/Dispatch.js +3 -2
- package/dist/dispatch/models/Dispatch.js.map +1 -1
- package/dist/dispatch/models/DispatchSubscription.d.ts +1 -1
- package/dist/dispatch/models/DispatchSubscription.d.ts.map +1 -1
- package/dist/dispatch/models/DispatchSubscription.js +2 -1
- package/dist/dispatch/models/DispatchSubscription.js.map +1 -1
- package/dist/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 +31 -13
- package/dist/embeddings/storage.js.map +1 -1
- package/dist/errors.d.ts +1 -1
- package/dist/hierarchical.js +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -6
- 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/learning/memory.d.ts.map +1 -1
- package/dist/learning/memory.js +5 -4
- package/dist/learning/memory.js.map +1 -1
- package/dist/manifest/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 +9 -1
- package/dist/migrations/differ.d.ts.map +1 -1
- package/dist/migrations/differ.js +12 -7
- package/dist/migrations/differ.js.map +1 -1
- package/dist/migrations/index.d.ts +2 -0
- package/dist/migrations/index.d.ts.map +1 -1
- package/dist/migrations/index.js +3 -1
- package/dist/migrations/integer-width.d.ts +99 -0
- package/dist/migrations/integer-width.d.ts.map +1 -0
- package/dist/migrations/integer-width.js +237 -0
- package/dist/migrations/integer-width.js.map +1 -0
- package/dist/migrations/minor-units.d.ts +162 -0
- package/dist/migrations/minor-units.d.ts.map +1 -0
- package/dist/migrations/minor-units.js +383 -0
- package/dist/migrations/minor-units.js.map +1 -0
- package/dist/migrations/tracker.d.ts.map +1 -1
- package/dist/migrations/tracker.js +28 -20
- package/dist/migrations/tracker.js.map +1 -1
- package/dist/migrations.js +3 -1
- package/dist/object.d.ts +98 -5
- package/dist/object.d.ts.map +1 -1
- package/dist/object.js +138 -30
- package/dist/object.js.map +1 -1
- package/dist/query-bounds.d.ts +5 -1
- package/dist/query-bounds.d.ts.map +1 -1
- package/dist/query-bounds.js +5 -1
- package/dist/query-bounds.js.map +1 -1
- 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 +3 -2
- 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/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/duckdb-strategy.d.ts.map +1 -1
- package/dist/schema/ddl/duckdb-strategy.js +3 -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 +14 -1
- package/dist/schema/ddl/postgres-strategy.js.map +1 -1
- package/dist/schema/generator.d.ts +245 -41
- package/dist/schema/generator.d.ts.map +1 -1
- package/dist/schema/generator.js +397 -241
- 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 +2 -3
- package/dist/schema/index.d.ts.map +1 -1
- package/dist/schema/index.js +2 -3
- package/dist/schema/live-parity.d.ts +3 -1
- package/dist/schema/live-parity.d.ts.map +1 -1
- package/dist/schema/live-parity.js +23 -2
- package/dist/schema/live-parity.js.map +1 -1
- package/dist/schema/system-table-shapes.d.ts +13 -0
- package/dist/schema/system-table-shapes.d.ts.map +1 -1
- package/dist/schema/system-table-shapes.js +14 -1
- package/dist/schema/system-table-shapes.js.map +1 -1
- package/dist/schema/types.d.ts +17 -10
- package/dist/schema/types.d.ts.map +1 -1
- package/dist/schema/utils.d.ts +1 -1
- package/dist/schema/utils.d.ts.map +1 -1
- package/dist/schema/utils.js +3 -3
- package/dist/schema/utils.js.map +1 -1
- package/dist/schema.js +2 -3
- package/dist/smrt-knowledge.json +9 -9
- 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 +102 -16
- package/dist/system/schema.d.ts.map +1 -1
- package/dist/system/schema.js +85 -47
- 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.map +1 -1
- package/dist/utils/safe-integer.d.ts +19 -0
- package/dist/utils/safe-integer.d.ts.map +1 -0
- package/dist/utils/safe-integer.js +31 -0
- package/dist/utils/safe-integer.js.map +1 -0
- package/dist/utils.d.ts +2 -1
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +6 -3
- package/dist/utils.js.map +1 -1
- package/dist/vite-plugin/index.d.ts.map +1 -1
- package/dist/vite-plugin/index.js +4 -62
- package/dist/vite-plugin/index.js.map +1 -1
- package/package.json +4 -4
- package/dist/schema/override-system.d.ts +0 -43
- package/dist/schema/override-system.d.ts.map +0 -1
- package/dist/schema/override-system.js +0 -205
- package/dist/schema/override-system.js.map +0 -1
package/dist/schema/generator.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
import { classnameToTablename } from "../utils/naming.js";
|
|
1
2
|
import { formatDefaultValue, quoteIdentifier, quoteStringLiteral } from "./sql-identifiers.js";
|
|
3
|
+
import { assertIdentifierFits, enforceIdentifierLimits } from "./index-utils.js";
|
|
2
4
|
import { getDDLStrategy } from "./ddl/index.js";
|
|
3
|
-
import {
|
|
5
|
+
import { conflictIndexName, resolveConflictColumns, resolveTenantColumn, servesSlugLookup } from "./conflict-target.js";
|
|
4
6
|
import { createHash } from "node:crypto";
|
|
5
7
|
//#region src/schema/generator.ts
|
|
6
8
|
/**
|
|
@@ -8,30 +10,6 @@ import { createHash } from "node:crypto";
|
|
|
8
10
|
* Converts AST field definitions to database schema definitions
|
|
9
11
|
*/
|
|
10
12
|
var SchemaGenerator = class {
|
|
11
|
-
/**
|
|
12
|
-
* Generate schema definition from SMRT object definition
|
|
13
|
-
*/
|
|
14
|
-
generateSchema(objectDef) {
|
|
15
|
-
const tableName = this.getTableName(objectDef);
|
|
16
|
-
const columns = this.generateColumns(objectDef.fields, objectDef.decoratorConfig);
|
|
17
|
-
const indexes = this.generateIndexes(objectDef, columns);
|
|
18
|
-
const triggers = this.generateTriggers(objectDef, tableName);
|
|
19
|
-
const foreignKeys = this.extractForeignKeys(columns);
|
|
20
|
-
const dependencies = this.extractDependencies(objectDef, foreignKeys);
|
|
21
|
-
const version = this.generateVersion(objectDef);
|
|
22
|
-
this.ensureTenantIdIndex(indexes, columns, tableName);
|
|
23
|
-
return {
|
|
24
|
-
tableName,
|
|
25
|
-
columns,
|
|
26
|
-
indexes,
|
|
27
|
-
triggers,
|
|
28
|
-
foreignKeys,
|
|
29
|
-
dependencies,
|
|
30
|
-
version,
|
|
31
|
-
packageName: this.extractPackageName(objectDef.filePath),
|
|
32
|
-
baseClass: objectDef.extends
|
|
33
|
-
};
|
|
34
|
-
}
|
|
35
13
|
/**
|
|
36
14
|
* Convert field type to SQL data type
|
|
37
15
|
*/
|
|
@@ -90,55 +68,144 @@ var SchemaGenerator = class {
|
|
|
90
68
|
}
|
|
91
69
|
}
|
|
92
70
|
/**
|
|
93
|
-
*
|
|
71
|
+
* Whether an existing index already leads with `leadingColumns`, in order.
|
|
72
|
+
*
|
|
73
|
+
* Only an UNQUALIFIED index (no partial `WHERE`, no JSON-path expression)
|
|
74
|
+
* counts. A partial index such as `... WHERE _meta_type = 'Article'` cannot
|
|
75
|
+
* serve a base-class polymorphic query, which carries no subtype predicate,
|
|
76
|
+
* so it must not suppress the standalone index (#2359, review of #2384).
|
|
77
|
+
*
|
|
78
|
+
* A B-tree serves any prefix of its column list, so an index over
|
|
79
|
+
* `(tenant_id, created_at, status)` covers both `(tenant_id)` equality
|
|
80
|
+
* lookups and `(tenant_id, created_at)` ordering — hence the prefix test
|
|
81
|
+
* rather than an exact match (#2363).
|
|
94
82
|
*/
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
83
|
+
hasUnqualifiedLeadingColumns(indexes, leadingColumns) {
|
|
84
|
+
return indexes.some((index) => !index.where && !index.jsonPath && (index.columns?.length ?? 0) >= leadingColumns.length && leadingColumns.every((column, i) => index.columns[i] === column));
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Whether an existing index already serves equality lookups on `column`.
|
|
88
|
+
*
|
|
89
|
+
* Single-column shorthand for {@link hasUnqualifiedLeadingColumns}.
|
|
90
|
+
*/
|
|
91
|
+
hasUnqualifiedLeadingIndex(indexes, column) {
|
|
92
|
+
return this.hasUnqualifiedLeadingColumns(indexes, [column]);
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* The column list of the default list-ordering index for a table.
|
|
96
|
+
*
|
|
97
|
+
* Every generated list surface — REST, MCP, and the SvelteKit list route —
|
|
98
|
+
* pages with `ORDER BY created_at DESC, <pk> ASC` (`DEFAULT_LIST_ORDER_BY`,
|
|
99
|
+
* #2367). On a tenant-scoped table that page is always preceded by a
|
|
100
|
+
* `tenant_id = ?` equality filter from the tenancy interceptor, so the
|
|
101
|
+
* serving index leads with the tenant column and orders inside it.
|
|
102
|
+
*
|
|
103
|
+
* `created_at` is generator-owned on every path — a declared `createdAt`
|
|
104
|
+
* field is rewritten to the same `TIMESTAMP NOT NULL DEFAULT
|
|
105
|
+
* current_timestamp` column — so it is never a primary key and never
|
|
106
|
+
* inline-UNIQUE. That is why only `indexes` needs checking for existing
|
|
107
|
+
* coverage: no constraint-backed index can already order this column.
|
|
108
|
+
*
|
|
109
|
+
* Returns `null` when the table has no `created_at` column (no path emits
|
|
110
|
+
* such a table today, but the helper stays total).
|
|
111
|
+
*/
|
|
112
|
+
getDefaultListOrderingColumns(columns) {
|
|
113
|
+
if (!columns.created_at) return null;
|
|
114
|
+
const tenantColumn = Object.entries(columns).find(([, columnDef]) => columnDef?.referenceKind === "tenantId")?.[0];
|
|
115
|
+
return tenantColumn ? [tenantColumn, "created_at"] : ["created_at"];
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Ensure the table can serve its own default list page from an index.
|
|
119
|
+
*
|
|
120
|
+
* Generated REST/MCP/SvelteKit list routes all page with
|
|
121
|
+
* `ORDER BY created_at DESC, <pk> ASC LIMIT n` and no `created_at` index
|
|
122
|
+
* existed on any schema path (the dead AST path indexed `updated_at`
|
|
123
|
+
* instead), so every default list page was a sequential scan plus a top-N
|
|
124
|
+
* sort of the whole table — 21 ms against 0.1 ms with the right composite on
|
|
125
|
+
* the workload the assessment measured (#2363, finding A2).
|
|
126
|
+
*
|
|
127
|
+
* Emitted shape:
|
|
128
|
+
*
|
|
129
|
+
* - tenant-scoped table → `(<tenant column>, created_at)`. The tenancy
|
|
130
|
+
* interceptor adds `tenant_id = ?` to every list, so the tenant column
|
|
131
|
+
* leads and `created_at` orders within it.
|
|
132
|
+
* - otherwise → `(created_at)`.
|
|
133
|
+
*
|
|
134
|
+
* No `DESC` declaration: PostgreSQL scans a B-tree backwards just as
|
|
135
|
+
* cheaply, and `IndexDefinition` carries no per-column direction. The
|
|
136
|
+
* trailing primary-key tiebreak is left out deliberately — its direction is
|
|
137
|
+
* opposite to `created_at`'s, so no single-direction index can satisfy the
|
|
138
|
+
* whole key anyway; the leading columns turn a full sort into an index scan
|
|
139
|
+
* with an incremental sort over rows that share a timestamp.
|
|
140
|
+
*
|
|
141
|
+
* Call this AFTER {@link appendDeclaredIndexes} and BEFORE
|
|
142
|
+
* {@link ensureReferenceColumnIndexes}: a declared composite that already
|
|
143
|
+
* leads with the same columns suppresses this one, and this composite in
|
|
144
|
+
* turn suppresses the standalone tenant index that would otherwise be a
|
|
145
|
+
* redundant prefix of it.
|
|
146
|
+
*/
|
|
147
|
+
ensureDefaultListOrderingIndex(indexes, columns, tableName) {
|
|
148
|
+
const orderingColumns = this.getDefaultListOrderingColumns(columns);
|
|
149
|
+
if (!orderingColumns) return;
|
|
150
|
+
if (this.hasUnqualifiedLeadingColumns(indexes, orderingColumns)) return;
|
|
151
|
+
const name = `${tableName}_${orderingColumns.join("_")}_idx`;
|
|
152
|
+
if (indexes.some((index) => index.name === name)) return;
|
|
153
|
+
indexes.push({
|
|
154
|
+
name,
|
|
155
|
+
columns: orderingColumns
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* The `(meta_type, meta_id)` owner-lookup columns for a polymorphic
|
|
160
|
+
* association table, or `null` when the table isn't one.
|
|
161
|
+
*
|
|
162
|
+
* `SmrtPolymorphicAssociation` contributes `metaType`/`metaId`/`role` (and
|
|
163
|
+
* `sortOrder`) to any concrete subclass's manifest — the scanner's
|
|
164
|
+
* `FRAMEWORK_ABSTRACT_BASE_NAMES` wiring merges the abstract base's fields
|
|
165
|
+
* directly into the subclass, because the base carries no `@smrt()`
|
|
166
|
+
* decorator of its own to declare an index on. Detected structurally, by
|
|
167
|
+
* the same three-column test `cascade.ts`'s `isPolymorphicAssociationClass`
|
|
168
|
+
* uses on registry field maps: requiring all three keeps an unrelated class
|
|
169
|
+
* that merely happens to carry a `metaType` column from being treated as an
|
|
170
|
+
* association table.
|
|
171
|
+
*/
|
|
172
|
+
getPolymorphicAssociationOwnerColumns(columns) {
|
|
173
|
+
if (!Object.hasOwn(columns, "meta_type") || !Object.hasOwn(columns, "meta_id") || !Object.hasOwn(columns, "role")) return null;
|
|
174
|
+
return ["meta_type", "meta_id"];
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Ensure a polymorphic association table indexes its owner lookup
|
|
178
|
+
* `(meta_type, meta_id)` (#2364, epic #2382 finding A3).
|
|
179
|
+
*
|
|
180
|
+
* A concrete association's `conflictColumns` lead with its own FK (e.g.
|
|
181
|
+
* `AssetAssociation`'s `asset_id, meta_type, meta_id, role`), so
|
|
182
|
+
* `meta_type`/`meta_id` sit in the *middle* of that unique index — a
|
|
183
|
+
* lookup like `AssetAssociationCollection.byLeft(metaType, metaId)`
|
|
184
|
+
* ("what points at this target") filters columns 2-3 of a 4-column index
|
|
185
|
+
* and cannot use it as a leading prefix. `metaId` is deliberately a bare
|
|
186
|
+
* string, never a typed `@foreignKey`/`@crossPackageRef` (there is no
|
|
187
|
+
* single target table), so {@link ensureReferenceColumnIndexes} never
|
|
188
|
+
* covers it either.
|
|
189
|
+
*
|
|
190
|
+
* Call this alongside {@link ensureDefaultListOrderingIndex}, before
|
|
191
|
+
* {@link ensureReferenceColumnIndexes}.
|
|
192
|
+
*
|
|
193
|
+
* Blast radius: this applies retroactively to every
|
|
194
|
+
* `SmrtPolymorphicAssociation` subclass across every package on the next
|
|
195
|
+
* `db:migrate`, not only the ones a given change touches — roll the wave
|
|
196
|
+
* out with `smrt db:migrate --postgres-safe` (#2362), same as any other
|
|
197
|
+
* bulk index addition.
|
|
198
|
+
*/
|
|
199
|
+
ensurePolymorphicAssociationIndex(indexes, columns, tableName) {
|
|
200
|
+
const ownerColumns = this.getPolymorphicAssociationOwnerColumns(columns);
|
|
201
|
+
if (!ownerColumns) return;
|
|
202
|
+
if (this.hasUnqualifiedLeadingColumns(indexes, ownerColumns)) return;
|
|
203
|
+
const name = `${tableName}_${ownerColumns.join("_")}_idx`;
|
|
204
|
+
if (indexes.some((index) => index.name === name)) return;
|
|
205
|
+
indexes.push({
|
|
206
|
+
name,
|
|
207
|
+
columns: ownerColumns
|
|
208
|
+
});
|
|
142
209
|
}
|
|
143
210
|
/**
|
|
144
211
|
* Append `@smrt({ indexes: [...] })` declarations to a generated index set
|
|
@@ -146,10 +213,13 @@ var SchemaGenerator = class {
|
|
|
146
213
|
*
|
|
147
214
|
* Shared by every schema path — build-time AST, runtime registry CTI/STI, and
|
|
148
215
|
* manifest CTI/STI — so one declaration behaves identically however the schema
|
|
149
|
-
* was derived. Call it *
|
|
150
|
-
*
|
|
151
|
-
*
|
|
152
|
-
*
|
|
216
|
+
* was derived. Call it *first*, ahead of
|
|
217
|
+
* {@link ensureDefaultListOrderingIndex} (#2363) and
|
|
218
|
+
* {@link ensureReferenceColumnIndexes}: a declared composite leading with a
|
|
219
|
+
* reference column (e.g. `tenant_id`), or with the default ordering columns,
|
|
220
|
+
* is exactly what those helpers' leads-with check is meant to defer to, and
|
|
221
|
+
* appending afterwards would leave the table with a redundant standalone
|
|
222
|
+
* index (#2384, #2359).
|
|
153
223
|
*
|
|
154
224
|
* Nothing is dropped quietly. A column that resolves to no column on the
|
|
155
225
|
* table, a malformed entry, and a name collision with a different index are
|
|
@@ -195,6 +265,7 @@ var SchemaGenerator = class {
|
|
|
195
265
|
};
|
|
196
266
|
if (typeof spec !== "object" || spec === null || Array.isArray(spec)) throw new Error(`Declared index on "${tableName}" must be an object: got ${describe()}.`);
|
|
197
267
|
if (typeof spec.name !== "string" || spec.name.length === 0) throw new Error(`Declared index on "${tableName}" needs a non-empty "name": got ${describe()}.`);
|
|
268
|
+
assertIdentifierFits(spec.name, "Declared index", `table "${tableName}"`);
|
|
198
269
|
if (!Array.isArray(spec.columns) || spec.columns.length === 0 || !spec.columns.every((column) => typeof column === "string" && column.length > 0)) throw new Error(`Declared index "${spec.name}" on "${tableName}" needs a non-empty "columns" array of field or column names.`);
|
|
199
270
|
if (spec.unique !== void 0 && typeof spec.unique !== "boolean") throw new Error(`Declared index "${spec.name}" on "${tableName}" has a non-boolean "unique": got ${describe()}.`);
|
|
200
271
|
if (spec.where !== void 0 && (typeof spec.where !== "string" || spec.where.trim().length === 0)) throw new Error(`Declared index "${spec.name}" on "${tableName}" has an empty or non-string "where" predicate.`);
|
|
@@ -218,64 +289,189 @@ var SchemaGenerator = class {
|
|
|
218
289
|
return a.columns.length === b.columns.length && a.columns.every((column, i) => column === b.columns[i]) && (a.unique ?? false) === (b.unique ?? false) && (a.where ?? null) === (b.where ?? null);
|
|
219
290
|
}
|
|
220
291
|
/**
|
|
221
|
-
* Ensure
|
|
222
|
-
*
|
|
223
|
-
*
|
|
224
|
-
*
|
|
225
|
-
*
|
|
226
|
-
*
|
|
227
|
-
*
|
|
228
|
-
*
|
|
229
|
-
*
|
|
230
|
-
*
|
|
231
|
-
*
|
|
232
|
-
*
|
|
292
|
+
* Ensure every reference column — `@foreignKey`, `@crossPackageRef`, and the
|
|
293
|
+
* tenancy-injected `tenant_id` — has an index leading with it.
|
|
294
|
+
*
|
|
295
|
+
* These are the columns every relationship load, `include:` batch, reverse
|
|
296
|
+
* ownership lookup and tenant-scoped read filters on. Before #2359 only the
|
|
297
|
+
* registry (test) paths indexed foreign keys, the manifest (production)
|
|
298
|
+
* paths indexed none of them, and `tenant_id` was indexed nowhere (#2356):
|
|
299
|
+
* 196/231 `@foreignKey` and 91/92 `@crossPackageRef` columns shipped with no
|
|
300
|
+
* serving index. Every schema path now calls this helper so the four paths
|
|
301
|
+
* stay in step (see `schema-path-parity.test.ts`).
|
|
302
|
+
*
|
|
303
|
+
* "Leading with it" rather than "always add": a table that already has an
|
|
304
|
+
* unqualified index starting on the column (commonly the `conflictColumns`
|
|
305
|
+
* unique index, or an `indexed: true` opt-in) is already served, and a
|
|
306
|
+
* standalone duplicate would only cost writes. A *partial* index does not
|
|
307
|
+
* count — see {@link hasUnqualifiedLeadingIndex}.
|
|
308
|
+
*
|
|
309
|
+
* Reference columns are therefore always indexed; `indexed: true` on one of
|
|
310
|
+
* them is redundant and harmless.
|
|
311
|
+
*
|
|
312
|
+
* Call this LAST in every path, after every other index (conflict, opt-in,
|
|
313
|
+
* unique, the declared composites, the default list-ordering composite, and
|
|
314
|
+
* any future addition) has been appended, so the leads-with suppression sees
|
|
315
|
+
* the full set. In particular {@link ensureDefaultListOrderingIndex} runs
|
|
316
|
+
* first on a tenant-scoped table: its `(tenant_id, created_at)` composite
|
|
317
|
+
* serves the tenant equality filter too, so no standalone tenant index is
|
|
318
|
+
* added (#2363).
|
|
233
319
|
*/
|
|
234
|
-
|
|
235
|
-
const
|
|
236
|
-
|
|
237
|
-
|
|
320
|
+
ensureReferenceColumnIndexes(indexes, columns, tableName) {
|
|
321
|
+
for (const [columnName, columnDef] of Object.entries(columns)) {
|
|
322
|
+
const kind = columnDef?.referenceKind;
|
|
323
|
+
if (kind !== "foreignKey" && kind !== "crossPackageRef" && kind !== "tenantId") continue;
|
|
324
|
+
if (columnDef?.primaryKey || columnDef?.unique) continue;
|
|
325
|
+
if (this.hasUnqualifiedLeadingIndex(indexes, columnName)) continue;
|
|
326
|
+
const name = `${tableName}_${columnName}_idx`;
|
|
327
|
+
if (indexes.some((index) => index.name === name)) continue;
|
|
328
|
+
indexes.push({
|
|
329
|
+
name,
|
|
330
|
+
columns: [columnName]
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
/**
|
|
335
|
+
* Whether `conflictColumns` names exactly the primary key column(s).
|
|
336
|
+
*
|
|
337
|
+
* `ON CONFLICT (id)` binds to the primary-key constraint on every engine, so
|
|
338
|
+
* a separate unique index over the same column set is a second B-tree over
|
|
339
|
+
* the same random UUIDs with nothing to add (#2359, finding A5).
|
|
340
|
+
*/
|
|
341
|
+
conflictColumnsArePrimaryKey(conflictColumns, columns) {
|
|
342
|
+
const primaryKeyColumns = Object.entries(columns).filter(([, columnDef]) => columnDef?.primaryKey === true).map(([name]) => name);
|
|
343
|
+
return primaryKeyColumns.length > 0 && primaryKeyColumns.length === conflictColumns.length && primaryKeyColumns.every((column) => conflictColumns.includes(column));
|
|
344
|
+
}
|
|
345
|
+
/**
|
|
346
|
+
* The table's tenant column as the generated columns describe it: the
|
|
347
|
+
* column carrying `referenceKind: 'tenantId'` (set from the field's
|
|
348
|
+
* `__tenancy.isTenantIdField` marker on every path).
|
|
349
|
+
*/
|
|
350
|
+
findTenantColumn(columns) {
|
|
351
|
+
for (const [columnName, columnDef] of Object.entries(columns)) if (columnDef?.referenceKind === "tenantId") return columnName;
|
|
352
|
+
}
|
|
353
|
+
/**
|
|
354
|
+
* Resolve the conflict target the unique conflict index must cover
|
|
355
|
+
* (#2360). `config.conflictColumns` wins when the caller resolved it —
|
|
356
|
+
* registry callers pass `ObjectRegistry.getConflictColumns()`, the manifest
|
|
357
|
+
* pipeline passes the normalized `decoratorConfig` — otherwise the strategy
|
|
358
|
+
* default, led by the tenant column when `config.tenantScoped` names a
|
|
359
|
+
* tenant field that exists as a column. Both roads apply the same rule as
|
|
360
|
+
* `ObjectRegistry.getConflictColumns()`, so schema and upsert agree by
|
|
361
|
+
* construction (`schema-path-parity.test.ts`).
|
|
362
|
+
*/
|
|
363
|
+
resolveConflictTarget(strategy, columns, config) {
|
|
364
|
+
const tenantColumn = this.findTenantColumn(columns);
|
|
365
|
+
if (config?.conflictColumns && config.conflictColumns.length > 0) return {
|
|
366
|
+
conflictColumns: [...config.conflictColumns],
|
|
367
|
+
tenantColumn
|
|
368
|
+
};
|
|
369
|
+
const tenantScoped = config?.tenantScoped;
|
|
370
|
+
return {
|
|
371
|
+
conflictColumns: resolveConflictColumns({
|
|
372
|
+
strategy,
|
|
373
|
+
tenantColumn: tenantScoped ? resolveTenantColumn(typeof tenantScoped === "object" ? tenantScoped.field ?? "tenantId" : "tenantId", (fieldName) => Boolean(columns[this.toSnakeCase(fieldName)]), (fieldName) => this.toSnakeCase(fieldName)) : void 0
|
|
374
|
+
}),
|
|
375
|
+
tenantColumn
|
|
376
|
+
};
|
|
377
|
+
}
|
|
378
|
+
/**
|
|
379
|
+
* Keep `(slug, context)` lookups served when `conflictColumns` are custom.
|
|
380
|
+
*
|
|
381
|
+
* The default conflict index is `(slug, context)`, and `loadFromSlug()`,
|
|
382
|
+
* `getId()` and `getSavedId()` all filter on `slug`/`context` regardless of
|
|
383
|
+
* the configured conflict key. A class that declares custom conflict
|
|
384
|
+
* columns replaces that unique index and, before #2359, left those lookups
|
|
385
|
+
* with no index at all — 120 tables (finding A7).
|
|
386
|
+
*
|
|
387
|
+
* Emitting a plain `(slug, context)` index is the safer of the two fixes:
|
|
388
|
+
* routing the lookups through the conflict key would change which row a
|
|
389
|
+
* slug resolves to on every such class, whereas an extra non-unique index
|
|
390
|
+
* is purely additive. It is skipped when an unqualified index already
|
|
391
|
+
* serves the lookup — one leading with `slug` (e.g. custom conflict columns
|
|
392
|
+
* that still start with it), or the tenant-led default key
|
|
393
|
+
* `(tenant_id, slug, context)` of a tenant-scoped table (#2360): every slug
|
|
394
|
+
* lookup on such a table carries the tenant predicate (#2365) and is served
|
|
395
|
+
* by that prefix, so a second index would only cost writes.
|
|
396
|
+
*/
|
|
397
|
+
ensureSlugLookupIndex(indexes, columns, tableName) {
|
|
398
|
+
if (!columns.slug || !columns.context) return;
|
|
399
|
+
const tenantColumn = this.findTenantColumn(columns);
|
|
400
|
+
if (indexes.some((index) => !index.where && !index.jsonPath && servesSlugLookup(index.columns ?? [], tenantColumn))) return;
|
|
401
|
+
const name = `${tableName}_slug_context_idx`;
|
|
402
|
+
if (indexes.some((index) => index.name === name)) return;
|
|
238
403
|
indexes.push({
|
|
239
|
-
name
|
|
240
|
-
columns: [
|
|
404
|
+
name,
|
|
405
|
+
columns: ["slug", "context"]
|
|
241
406
|
});
|
|
242
407
|
}
|
|
243
408
|
/**
|
|
244
|
-
*
|
|
409
|
+
* Emit the unique conflict index of an STI table from the resolved
|
|
410
|
+
* conflict target (#2360): the STI default `(slug, context, _meta_type)`,
|
|
411
|
+
* a custom `@smrt({ conflictColumns })` declared on the STI root, or the
|
|
412
|
+
* default led by the tenant column when the root is tenant-scoped. Shared
|
|
413
|
+
* by the registry and manifest STI paths so they cannot drift.
|
|
414
|
+
*
|
|
415
|
+
* No index is emitted when the target is the primary key itself
|
|
416
|
+
* (`ON CONFLICT (id)` binds to the PK constraint), matching the CTI paths.
|
|
245
417
|
*/
|
|
246
|
-
|
|
247
|
-
const
|
|
248
|
-
|
|
249
|
-
for (const [columnName, columnDef] of Object.entries(columns)) if (columnDef.foreignKey) indexes.push({
|
|
250
|
-
name: `idx_${tableName}_${columnName}`,
|
|
251
|
-
columns: [columnName],
|
|
252
|
-
description: `Index for foreign key ${columnName}`
|
|
253
|
-
});
|
|
418
|
+
emitStiConflictIndex(indexes, columns, tableName, config) {
|
|
419
|
+
const { conflictColumns, tenantColumn } = this.resolveConflictTarget("sti", columns, config);
|
|
420
|
+
if (this.conflictColumnsArePrimaryKey(conflictColumns, columns)) return;
|
|
254
421
|
indexes.push({
|
|
255
|
-
name:
|
|
256
|
-
columns:
|
|
257
|
-
|
|
258
|
-
});
|
|
259
|
-
for (const [columnName, columnDef] of Object.entries(columns)) if (columnDef.unique && !columnDef.primaryKey) indexes.push({
|
|
260
|
-
name: `idx_${tableName}_${columnName}_unique`,
|
|
261
|
-
columns: [columnName],
|
|
262
|
-
unique: true,
|
|
263
|
-
description: `Unique index for ${columnName}`
|
|
422
|
+
name: conflictIndexName(tableName, conflictColumns, tenantColumn),
|
|
423
|
+
columns: conflictColumns,
|
|
424
|
+
unique: true
|
|
264
425
|
});
|
|
265
|
-
this.appendDeclaredIndexes(indexes, objectDef.decoratorConfig?.indexes, columns, tableName);
|
|
266
|
-
return indexes;
|
|
267
426
|
}
|
|
268
427
|
/**
|
|
269
|
-
*
|
|
428
|
+
* Emit unique indexes for `@field({ unique: true })` columns on an STI table.
|
|
429
|
+
*
|
|
430
|
+
* STI columns are the union of every class in the hierarchy, all nullable,
|
|
431
|
+
* so a column-level `UNIQUE` (what CTI renders inline) is not enough on its
|
|
432
|
+
* own: the migration differ cannot add a column constraint to an existing
|
|
433
|
+
* table, but it can add an index. Two shapes (#2359, finding A4):
|
|
434
|
+
*
|
|
435
|
+
* - declared on the STI **base** (and therefore inherited by every class in
|
|
436
|
+
* the table): one full unique index, table-wide like CTI;
|
|
437
|
+
* - declared only on a **descendant**: one partial unique index per class
|
|
438
|
+
* that carries the flag (the declaring class and, through inherited field
|
|
439
|
+
* metadata, its own descendants), `WHERE _meta_type = '<qualified>'`, so
|
|
440
|
+
* siblings that merely share the column name are not constrained.
|
|
441
|
+
* Uniqueness is then enforced per concrete class, not across the subtree
|
|
442
|
+
* — a documented limitation of the discriminator-partial shape. Engines
|
|
443
|
+
* without partial indexes (DuckDB, JSON) skip this shape entirely rather
|
|
444
|
+
* than widen it to a table-wide UNIQUE (see DuckDBStrategy.generateIndexes
|
|
445
|
+
* and SchemaComparer.compareIndexes).
|
|
446
|
+
*
|
|
447
|
+
* @param declarers - column name → classes whose field metadata carries
|
|
448
|
+
* `unique: true` for that column, in hierarchy order (base first).
|
|
270
449
|
*/
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
450
|
+
emitStiUniqueIndexes(indexes, declarers, baseClassName, tableName) {
|
|
451
|
+
for (const [columnName, classNames] of declarers.entries()) {
|
|
452
|
+
if (classNames.has(baseClassName)) {
|
|
453
|
+
indexes.push({
|
|
454
|
+
name: `${tableName}_${columnName}_unique_idx`,
|
|
455
|
+
columns: [columnName],
|
|
456
|
+
unique: true
|
|
457
|
+
});
|
|
458
|
+
continue;
|
|
459
|
+
}
|
|
460
|
+
for (const className of classNames) {
|
|
461
|
+
const simpleName = className.includes(":") ? className.slice(className.lastIndexOf(":") + 1) : className;
|
|
462
|
+
let name = `${tableName}_${columnName}_${this.toSnakeCase(simpleName)}_unique_idx`;
|
|
463
|
+
if (indexes.some((index) => index.name === name)) {
|
|
464
|
+
const digest = createHash("sha256").update(className).digest("hex").slice(0, 6);
|
|
465
|
+
name = `${tableName}_${columnName}_${this.toSnakeCase(simpleName)}_${digest}_unique_idx`;
|
|
466
|
+
}
|
|
467
|
+
indexes.push({
|
|
468
|
+
name,
|
|
469
|
+
columns: [columnName],
|
|
470
|
+
unique: true,
|
|
471
|
+
where: `_meta_type = ${quoteStringLiteral(className)}`
|
|
472
|
+
});
|
|
473
|
+
}
|
|
474
|
+
}
|
|
279
475
|
}
|
|
280
476
|
/**
|
|
281
477
|
* Extract foreign key definitions
|
|
@@ -292,45 +488,12 @@ var SchemaGenerator = class {
|
|
|
292
488
|
return foreignKeys;
|
|
293
489
|
}
|
|
294
490
|
/**
|
|
295
|
-
* Extract schema dependencies from foreign keys and inheritance
|
|
296
|
-
*/
|
|
297
|
-
extractDependencies(objectDef, foreignKeys) {
|
|
298
|
-
const dependencies = /* @__PURE__ */ new Set();
|
|
299
|
-
for (const fk of foreignKeys) dependencies.add(fk.referencesTable);
|
|
300
|
-
if (objectDef.extends && objectDef.extends !== "SmrtObject" && objectDef.extends !== "SmrtCollection") dependencies.add(this.classNameToTableName(objectDef.extends));
|
|
301
|
-
return Array.from(dependencies);
|
|
302
|
-
}
|
|
303
|
-
/**
|
|
304
|
-
* Generate version hash for schema
|
|
305
|
-
*/
|
|
306
|
-
generateVersion(objectDef) {
|
|
307
|
-
const content = JSON.stringify({
|
|
308
|
-
className: objectDef.className,
|
|
309
|
-
fields: objectDef.fields,
|
|
310
|
-
extends: objectDef.extends
|
|
311
|
-
});
|
|
312
|
-
return createHash("sha256").update(content).digest("hex").substring(0, 8);
|
|
313
|
-
}
|
|
314
|
-
/**
|
|
315
|
-
* Get table name from object definition
|
|
316
|
-
*/
|
|
317
|
-
getTableName(objectDef) {
|
|
318
|
-
return this.classNameToTableName(objectDef.className);
|
|
319
|
-
}
|
|
320
|
-
/**
|
|
321
491
|
* Convert class name to table name (camelCase to snake_case, pluralized)
|
|
322
492
|
*/
|
|
323
493
|
classNameToTableName(className) {
|
|
324
494
|
return classnameToTablename(className);
|
|
325
495
|
}
|
|
326
496
|
/**
|
|
327
|
-
* Extract package name from file path
|
|
328
|
-
*/
|
|
329
|
-
extractPackageName(filePath) {
|
|
330
|
-
const match = filePath.match(/packages\/([^/]+)/);
|
|
331
|
-
return match ? match[1] : "unknown";
|
|
332
|
-
}
|
|
333
|
-
/**
|
|
334
497
|
* Generate schema definition from ObjectRegistry fields (runtime)
|
|
335
498
|
*
|
|
336
499
|
* This method builds a SchemaDefinition from ObjectRegistry cached fields,
|
|
@@ -435,39 +598,25 @@ var SchemaGenerator = class {
|
|
|
435
598
|
this.reconcileRegistryForeignKeyColumnTypes(columns, fields, config?.registry);
|
|
436
599
|
const indexes = [];
|
|
437
600
|
if (!hasCustomPK) {
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
description: "Primary key index"
|
|
442
|
-
});
|
|
443
|
-
const conflictColumns = config?.conflictColumns || ["slug", "context"];
|
|
444
|
-
const conflictIndexName = conflictColumns.length > 2 ? `${tableName}_${conflictColumns.slice(0, 2).join("_")}_idx` : `${tableName}_${conflictColumns.join("_")}_idx`;
|
|
445
|
-
indexes.push({
|
|
446
|
-
name: conflictIndexName,
|
|
601
|
+
const { conflictColumns, tenantColumn } = this.resolveConflictTarget("cti", columns, config);
|
|
602
|
+
if (!this.conflictColumnsArePrimaryKey(conflictColumns, columns)) indexes.push({
|
|
603
|
+
name: conflictIndexName(tableName, conflictColumns, tenantColumn),
|
|
447
604
|
columns: conflictColumns,
|
|
448
605
|
unique: true,
|
|
449
606
|
description: `Unique conflict index for ${className}`
|
|
450
607
|
});
|
|
451
|
-
} else for (const [colName, colDef] of Object.entries(columns)) if (colDef.primaryKey) {
|
|
452
|
-
indexes.push({
|
|
453
|
-
name: `${tableName}_${colName}_idx`,
|
|
454
|
-
columns: [colName],
|
|
455
|
-
description: `Primary key index`
|
|
456
|
-
});
|
|
457
|
-
break;
|
|
458
608
|
}
|
|
459
|
-
|
|
460
|
-
name: `idx_${tableName}_${colName}`,
|
|
461
|
-
columns: [colName],
|
|
462
|
-
description: `Foreign key index for ${colName}`
|
|
463
|
-
});
|
|
609
|
+
this.ensureSlugLookupIndex(indexes, columns, tableName);
|
|
464
610
|
for (const colName of indexedColumns) indexes.push({
|
|
465
611
|
name: `${tableName}_${colName}_idx`,
|
|
466
612
|
columns: [colName],
|
|
467
613
|
description: `Index for ${colName}`
|
|
468
614
|
});
|
|
469
615
|
this.appendDeclaredIndexes(indexes, config?.indexes, columns, tableName);
|
|
470
|
-
this.
|
|
616
|
+
this.ensureDefaultListOrderingIndex(indexes, columns, tableName);
|
|
617
|
+
this.ensurePolymorphicAssociationIndex(indexes, columns, tableName);
|
|
618
|
+
this.ensureReferenceColumnIndexes(indexes, columns, tableName);
|
|
619
|
+
enforceIdentifierLimits(tableName, indexes);
|
|
471
620
|
return {
|
|
472
621
|
tableName,
|
|
473
622
|
columns,
|
|
@@ -487,7 +636,14 @@ var SchemaGenerator = class {
|
|
|
487
636
|
* - _meta_type: Discriminator column to identify class type
|
|
488
637
|
* - _meta_data: JSON column for flexible field storage
|
|
489
638
|
* - Union of all FK columns from descendants (all nullable)
|
|
490
|
-
* -
|
|
639
|
+
* - One plain index per reference column (FK / cross-package ref /
|
|
640
|
+
* tenant_id). Plain rather than partial-by-class: base-class polymorphic
|
|
641
|
+
* queries carry no `_meta_type` predicate, so a partial index could not
|
|
642
|
+
* serve them, while a plain index serves both those and the child
|
|
643
|
+
* collections' `_meta_type = X AND fk = ?` filters — one index per column
|
|
644
|
+
* instead of one per (column, class) (#2359).
|
|
645
|
+
* - Unique indexes for `@field({ unique: true })` columns — see
|
|
646
|
+
* {@link emitStiUniqueIndexes}.
|
|
491
647
|
*
|
|
492
648
|
* @param baseClassName - Base class name for the STI hierarchy
|
|
493
649
|
* @param tableName - Shared table name (from base class)
|
|
@@ -547,12 +703,11 @@ var SchemaGenerator = class {
|
|
|
547
703
|
let hasUpdatedAt = false;
|
|
548
704
|
const descendants = ObjectRegistry.getDescendants(baseClassName);
|
|
549
705
|
const allClassNames = [baseClassName, ...descendants];
|
|
550
|
-
const fkColumnsByClass = /* @__PURE__ */ new Map();
|
|
551
706
|
const indexedMetaFields = /* @__PURE__ */ new Set();
|
|
552
707
|
const indexedStiColumns = /* @__PURE__ */ new Set();
|
|
708
|
+
const uniqueColumnDeclarers = /* @__PURE__ */ new Map();
|
|
553
709
|
for (const className of allClassNames) {
|
|
554
710
|
const classFields = await ObjectRegistry.getAllFields(className);
|
|
555
|
-
fkColumnsByClass.set(className, /* @__PURE__ */ new Set());
|
|
556
711
|
for (const [fieldName, field] of classFields.entries()) {
|
|
557
712
|
if (field.transient || field._meta?.transient) continue;
|
|
558
713
|
if (field.type === "meta" && (field.indexed === true || field._meta?.indexed === true)) indexedMetaFields.add(fieldName);
|
|
@@ -582,6 +737,14 @@ var SchemaGenerator = class {
|
|
|
582
737
|
if (field.type === "oneToMany" || field.type === "manyToMany") continue;
|
|
583
738
|
if (field.type === "meta") continue;
|
|
584
739
|
const columnName = this.toSnakeCase(fieldName);
|
|
740
|
+
if (field._meta?.unique === true) {
|
|
741
|
+
let declarers = uniqueColumnDeclarers.get(columnName);
|
|
742
|
+
if (!declarers) {
|
|
743
|
+
declarers = /* @__PURE__ */ new Set();
|
|
744
|
+
uniqueColumnDeclarers.set(columnName, declarers);
|
|
745
|
+
}
|
|
746
|
+
declarers.add(className);
|
|
747
|
+
}
|
|
585
748
|
if (columns[columnName]) continue;
|
|
586
749
|
const columnDef = {
|
|
587
750
|
type: this.getRelationshipColumnType(field),
|
|
@@ -595,15 +758,12 @@ var SchemaGenerator = class {
|
|
|
595
758
|
if (field.type === "foreignKey") {
|
|
596
759
|
const relatedName = field.related;
|
|
597
760
|
const onDeleteAction = field._meta?.onDelete;
|
|
598
|
-
if (relatedName) {
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
};
|
|
605
|
-
fkColumnsByClass.get(className)?.add(columnName);
|
|
606
|
-
}
|
|
761
|
+
if (relatedName) columnDef.foreignKey = {
|
|
762
|
+
table: this.classNameToTableName(relatedName),
|
|
763
|
+
column: "id",
|
|
764
|
+
onDelete: onDeleteAction || "CASCADE",
|
|
765
|
+
onUpdate: "CASCADE"
|
|
766
|
+
};
|
|
607
767
|
}
|
|
608
768
|
if ((field._meta?.indexed === true || field.indexed === true) && !columnDef.foreignKey) indexedStiColumns.add(columnName);
|
|
609
769
|
columns[columnName] = columnDef;
|
|
@@ -626,32 +786,14 @@ var SchemaGenerator = class {
|
|
|
626
786
|
this.reconcileRegistryForeignKeyColumnTypes(columns, classFields, ObjectRegistry);
|
|
627
787
|
}
|
|
628
788
|
const indexes = [];
|
|
629
|
-
|
|
630
|
-
name: `${tableName}_id_idx`,
|
|
631
|
-
columns: ["id"],
|
|
632
|
-
description: "Primary key index"
|
|
633
|
-
});
|
|
634
|
-
indexes.push({
|
|
635
|
-
name: `${tableName}_slug_context_meta_type_idx`,
|
|
636
|
-
columns: [
|
|
637
|
-
"slug",
|
|
638
|
-
"context",
|
|
639
|
-
"_meta_type"
|
|
640
|
-
],
|
|
641
|
-
unique: true,
|
|
642
|
-
description: "Unique index for slug, context, and type"
|
|
643
|
-
});
|
|
789
|
+
this.emitStiConflictIndex(indexes, columns, tableName, config);
|
|
644
790
|
indexes.push({
|
|
645
791
|
name: `${tableName}_meta_type_idx`,
|
|
646
792
|
columns: ["_meta_type"],
|
|
647
793
|
description: "Index for type discriminator queries"
|
|
648
794
|
});
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
columns: [fkColumn],
|
|
652
|
-
where: `_meta_type = ${quoteStringLiteral(className)}`,
|
|
653
|
-
description: `Partial index for ${fkColumn} in ${className} rows`
|
|
654
|
-
});
|
|
795
|
+
this.emitStiUniqueIndexes(indexes, uniqueColumnDeclarers, baseClassName, tableName);
|
|
796
|
+
this.ensureSlugLookupIndex(indexes, columns, tableName);
|
|
655
797
|
for (const fieldName of indexedMetaFields) indexes.push({
|
|
656
798
|
name: `${tableName}_meta_${this.toSnakeCase(fieldName)}_idx`,
|
|
657
799
|
columns: [],
|
|
@@ -667,7 +809,10 @@ var SchemaGenerator = class {
|
|
|
667
809
|
description: `Index for ${colName}`
|
|
668
810
|
});
|
|
669
811
|
this.appendDeclaredIndexes(indexes, config?.indexes, columns, tableName);
|
|
670
|
-
this.
|
|
812
|
+
this.ensureDefaultListOrderingIndex(indexes, columns, tableName);
|
|
813
|
+
this.ensurePolymorphicAssociationIndex(indexes, columns, tableName);
|
|
814
|
+
this.ensureReferenceColumnIndexes(indexes, columns, tableName);
|
|
815
|
+
enforceIdentifierLimits(tableName, indexes);
|
|
671
816
|
return {
|
|
672
817
|
tableName,
|
|
673
818
|
columns,
|
|
@@ -732,15 +877,15 @@ var SchemaGenerator = class {
|
|
|
732
877
|
notNull: true,
|
|
733
878
|
default: "current_timestamp"
|
|
734
879
|
};
|
|
735
|
-
const
|
|
736
|
-
const
|
|
737
|
-
const
|
|
880
|
+
const baseKey = this.resolveManifestClassKey(baseClassName, manifest);
|
|
881
|
+
const descendants = this.findDescendantsInManifest(baseKey, manifest);
|
|
882
|
+
const allClassNames = [baseKey, ...descendants];
|
|
738
883
|
const indexedMetaFields = /* @__PURE__ */ new Set();
|
|
739
884
|
const indexedStiColumns = /* @__PURE__ */ new Set();
|
|
885
|
+
const uniqueColumnDeclarers = /* @__PURE__ */ new Map();
|
|
740
886
|
for (const className of allClassNames) {
|
|
741
887
|
const objDef = manifest.objects[className];
|
|
742
888
|
if (!objDef) continue;
|
|
743
|
-
fkColumnsByClass.set(className, /* @__PURE__ */ new Set());
|
|
744
889
|
for (const [fieldName, field] of Object.entries(objDef.fields)) {
|
|
745
890
|
if (field.transient || field._meta?.transient) continue;
|
|
746
891
|
const indexedField = field;
|
|
@@ -749,6 +894,14 @@ var SchemaGenerator = class {
|
|
|
749
894
|
if (field.type === "oneToMany" || field.type === "manyToMany") continue;
|
|
750
895
|
if (field.type === "meta") continue;
|
|
751
896
|
const columnName = this.toSnakeCase(fieldName);
|
|
897
|
+
if (field._meta?.unique === true) {
|
|
898
|
+
let declarers = uniqueColumnDeclarers.get(columnName);
|
|
899
|
+
if (!declarers) {
|
|
900
|
+
declarers = /* @__PURE__ */ new Set();
|
|
901
|
+
uniqueColumnDeclarers.set(columnName, declarers);
|
|
902
|
+
}
|
|
903
|
+
declarers.add(className);
|
|
904
|
+
}
|
|
752
905
|
if (columns[columnName]) continue;
|
|
753
906
|
const columnDef = {
|
|
754
907
|
type: this.getRelationshipColumnType(field),
|
|
@@ -756,29 +909,18 @@ var SchemaGenerator = class {
|
|
|
756
909
|
notNull: false
|
|
757
910
|
};
|
|
758
911
|
if (field.default !== void 0 && this.shouldEmitDefault(field, field.default)) columnDef.default = field.default;
|
|
759
|
-
if (field.type === "foreignKey") fkColumnsByClass.get(className)?.add(columnName);
|
|
760
912
|
if ((indexedField.indexed === true || field._meta?.indexed === true) && field.type !== "foreignKey") indexedStiColumns.add(columnName);
|
|
761
913
|
columns[columnName] = columnDef;
|
|
762
914
|
}
|
|
763
915
|
}
|
|
764
916
|
const indexes = [];
|
|
765
|
-
|
|
766
|
-
name: `${tableName}_id_idx`,
|
|
767
|
-
columns: ["id"]
|
|
768
|
-
});
|
|
769
|
-
indexes.push({
|
|
770
|
-
name: `${tableName}_slug_context_meta_type_idx`,
|
|
771
|
-
columns: [
|
|
772
|
-
"slug",
|
|
773
|
-
"context",
|
|
774
|
-
"_meta_type"
|
|
775
|
-
],
|
|
776
|
-
unique: true
|
|
777
|
-
});
|
|
917
|
+
this.emitStiConflictIndex(indexes, columns, tableName, config);
|
|
778
918
|
indexes.push({
|
|
779
919
|
name: `${tableName}_meta_type_idx`,
|
|
780
920
|
columns: ["_meta_type"]
|
|
781
921
|
});
|
|
922
|
+
this.emitStiUniqueIndexes(indexes, uniqueColumnDeclarers, baseKey, tableName);
|
|
923
|
+
this.ensureSlugLookupIndex(indexes, columns, tableName);
|
|
782
924
|
for (const fieldName of indexedMetaFields) indexes.push({
|
|
783
925
|
name: `${tableName}_meta_${this.toSnakeCase(fieldName)}_idx`,
|
|
784
926
|
columns: [],
|
|
@@ -792,7 +934,10 @@ var SchemaGenerator = class {
|
|
|
792
934
|
columns: [colName]
|
|
793
935
|
});
|
|
794
936
|
this.appendDeclaredIndexes(indexes, config?.indexes, columns, tableName);
|
|
795
|
-
this.
|
|
937
|
+
this.ensureDefaultListOrderingIndex(indexes, columns, tableName);
|
|
938
|
+
this.ensurePolymorphicAssociationIndex(indexes, columns, tableName);
|
|
939
|
+
this.ensureReferenceColumnIndexes(indexes, columns, tableName);
|
|
940
|
+
enforceIdentifierLimits(tableName, indexes);
|
|
796
941
|
const schemaDefinition = {
|
|
797
942
|
tableName,
|
|
798
943
|
columns: this.convertManifestColumnsToSchemaColumns(columns),
|
|
@@ -874,23 +1019,22 @@ var SchemaGenerator = class {
|
|
|
874
1019
|
columns[columnName] = columnDef;
|
|
875
1020
|
}
|
|
876
1021
|
const indexes = [];
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
});
|
|
881
|
-
const conflictColumns = config?.conflictColumns || ["slug", "context"];
|
|
882
|
-
const indexName = conflictColumns.length > 2 ? `${tableName}_${conflictColumns.slice(0, 2).join("_")}_idx` : `${tableName}_${conflictColumns.join("_")}_idx`;
|
|
883
|
-
indexes.push({
|
|
884
|
-
name: indexName,
|
|
1022
|
+
const { conflictColumns, tenantColumn } = this.resolveConflictTarget("cti", columns, config);
|
|
1023
|
+
if (!this.conflictColumnsArePrimaryKey(conflictColumns, columns)) indexes.push({
|
|
1024
|
+
name: conflictIndexName(tableName, conflictColumns, tenantColumn),
|
|
885
1025
|
columns: conflictColumns,
|
|
886
1026
|
unique: true
|
|
887
1027
|
});
|
|
1028
|
+
this.ensureSlugLookupIndex(indexes, columns, tableName);
|
|
888
1029
|
for (const colName of indexedColumns) indexes.push({
|
|
889
1030
|
name: `${tableName}_${colName}_idx`,
|
|
890
1031
|
columns: [colName]
|
|
891
1032
|
});
|
|
892
1033
|
this.appendDeclaredIndexes(indexes, config?.indexes, columns, tableName);
|
|
893
|
-
this.
|
|
1034
|
+
this.ensureDefaultListOrderingIndex(indexes, columns, tableName);
|
|
1035
|
+
this.ensurePolymorphicAssociationIndex(indexes, columns, tableName);
|
|
1036
|
+
this.ensureReferenceColumnIndexes(indexes, columns, tableName);
|
|
1037
|
+
enforceIdentifierLimits(tableName, indexes);
|
|
894
1038
|
const schemaDefinition = {
|
|
895
1039
|
tableName,
|
|
896
1040
|
columns: this.convertManifestColumnsToSchemaColumns(columns),
|
|
@@ -919,6 +1063,18 @@ var SchemaGenerator = class {
|
|
|
919
1063
|
};
|
|
920
1064
|
}
|
|
921
1065
|
/**
|
|
1066
|
+
* Resolve a class name (simple or qualified) to the key it is stored under
|
|
1067
|
+
* in `manifest.objects`. Falls back to the input when the manifest does not
|
|
1068
|
+
* carry the class (e.g. an external STI base absent from the aggregated
|
|
1069
|
+
* manifest), so callers can keep using it as an opaque label.
|
|
1070
|
+
*/
|
|
1071
|
+
resolveManifestClassKey(className, manifest) {
|
|
1072
|
+
if (manifest.objects[className]) return className;
|
|
1073
|
+
const simpleName = className.includes(":") ? className.slice(className.lastIndexOf(":") + 1) : className;
|
|
1074
|
+
for (const [key, obj] of Object.entries(manifest.objects)) if (obj.qualifiedName === className || obj.className === simpleName) return key;
|
|
1075
|
+
return className;
|
|
1076
|
+
}
|
|
1077
|
+
/**
|
|
922
1078
|
* Find all descendants of a class in the manifest
|
|
923
1079
|
*
|
|
924
1080
|
* Note: Manifest keys are now qualified names (@pkg:ClassName) but `extends` field
|