@atscript/db 0.1.135 → 0.1.137

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/dist/{agg-DKuf_v2L.d.cts → agg-CV7y8nC6.d.cts} +13 -3
  2. package/dist/{agg-BtlGeRfj.d.mts → agg-D5DHsAby.d.mts} +13 -3
  3. package/dist/agg.cjs +35 -3
  4. package/dist/agg.d.cts +2 -2
  5. package/dist/agg.d.mts +2 -2
  6. package/dist/agg.mjs +34 -1
  7. package/dist/aggregate-fns-CGBv3E8S.cjs +87 -0
  8. package/dist/aggregate-fns-CfsveE1w.mjs +58 -0
  9. package/dist/{buckets-GruoVxH5.d.mts → buckets-BFG2RYRW.d.mts} +662 -31
  10. package/dist/{buckets-D6PBXKRJ.d.cts → buckets-C-27xmtq.d.cts} +662 -31
  11. package/dist/column-diff-BmqvgBWw.d.cts +24 -0
  12. package/dist/{db-view-Doh8vPg3.mjs → column-diff-BwOA5101.mjs} +795 -125
  13. package/dist/{db-view-Dm7MpUGB.cjs → column-diff-CgxgFKzx.cjs} +873 -125
  14. package/dist/column-diff-DPkbZIVE.d.mts +24 -0
  15. package/dist/index.cjs +79 -55
  16. package/dist/index.d.cts +12 -10
  17. package/dist/index.d.mts +12 -10
  18. package/dist/index.mjs +30 -11
  19. package/dist/{nested-writer-wk1EFUNY.cjs → nested-writer-BZNCuqI6.cjs} +16 -2
  20. package/dist/{nested-writer-CqL24ojl.mjs → nested-writer-FWD5oOYh.mjs} +11 -3
  21. package/dist/plugin.cjs +217 -77
  22. package/dist/plugin.mjs +217 -78
  23. package/dist/rel.cjs +2 -2
  24. package/dist/rel.d.cts +2 -20
  25. package/dist/rel.d.mts +2 -20
  26. package/dist/rel.mjs +2 -2
  27. package/dist/relation-helpers-D3Zu0Mta.d.mts +30 -0
  28. package/dist/relation-helpers-DxrvS6ar.d.cts +30 -0
  29. package/dist/{relation-loader-BD4xANQJ.cjs → relation-loader-6ZB_5KFq.cjs} +1 -1
  30. package/dist/{relation-loader-B68R1LET.mjs → relation-loader-CTFaZpVa.mjs} +1 -1
  31. package/dist/shared.cjs +5 -1
  32. package/dist/shared.d.cts +16 -3
  33. package/dist/shared.d.mts +16 -3
  34. package/dist/shared.mjs +2 -2
  35. package/dist/sync.cjs +38 -321
  36. package/dist/sync.d.cts +29 -17
  37. package/dist/sync.d.mts +29 -17
  38. package/dist/sync.mjs +6 -289
  39. package/dist/{validation-utils-MWOP1Ts4.mjs → validation-utils-B4h-GW4d.mjs} +29 -7
  40. package/dist/{validation-utils-B7SXPkm7.cjs → validation-utils-Dg0hW6dn.cjs} +52 -6
  41. package/dist/{validator-CewfnGZj.d.mts → validator-Drb2N-YL.d.cts} +1 -1
  42. package/dist/{validator-CewfnGZj.d.cts → validator-Drb2N-YL.d.mts} +1 -1
  43. package/dist/validator.d.cts +1 -1
  44. package/dist/validator.d.mts +1 -1
  45. package/package.json +1 -1
  46. package/dist/agg-CvXDGnKi.mjs +0 -61
  47. package/dist/agg-EcIbAFnJ.cjs +0 -78
  48. package/dist/db-space-9CH5neN7.d.mts +0 -439
  49. package/dist/db-space-BdtBNTeH.d.cts +0 -439
package/dist/sync.d.mts CHANGED
@@ -1,5 +1,5 @@
1
- import { E as TColumnDiff, F as TDbDefaultValue, Ht as TGenericLogger, L as TDbFieldMeta, Pt as AtscriptDbReadable, R as TDbForeignKey, W as TDbObjectKind, Y as TDbStorageType, it as TExistingTableOption, mt as TReferencingForeignKey, nt as TExistingColumn, pt as TPrimaryKeyChange, rt as TExistingForeignKey, tt as TEnsureTableOptions, vt as TTableOptionDiff } from "./buckets-GruoVxH5.mjs";
2
- import { c as AtscriptQueryFieldRef$1, i as AtscriptDbView, l as AtscriptQueryNode$1, t as DbSpace } from "./db-space-9CH5neN7.mjs";
1
+ import { $t as AtscriptDbReadable, Ft as DbSpace, G as TDbObjectKind, I as TDbDefaultValue, R as TDbFieldMeta, Rt as AtscriptDbView, Ut as AtscriptQueryFieldRef$1, Wt as AtscriptQueryNode$1, X as TDbStorageType, at as TExistingTableOption, ht as TReferencingForeignKey, it as TExistingForeignKey, mt as TPrimaryKeyChange, nt as TEnsureTableOptions, rt as TExistingColumn, sn as TGenericLogger, yt as TTableOptionDiff, z as TDbForeignKey } from "./buckets-BFG2RYRW.mjs";
2
+ import { t as computeColumnDiff } from "./column-diff-DPkbZIVE.mjs";
3
3
  import { TAtscriptAnnotatedType } from "@atscript/typescript/utils";
4
4
 
5
5
  //#region src/schema/sync-entry.d.ts
@@ -269,9 +269,27 @@ interface TViewJoinSnapshot {
269
269
  targetTable: string;
270
270
  /** Canonical JSON of the join condition (see {@link canonicalizeQueryNode}). */
271
271
  condition: string;
272
- /** Reserved for optional joins; absent today so it does not perturb the hash. */
272
+ /** Emitted only for `"left"` — an inner join (the default) carries no key. @since 0.1.136 */
273
273
  kind?: "inner" | "left";
274
274
  }
275
+ /**
276
+ * One column of a managed view as stored in its snapshot — the PHYSICAL
277
+ * source it reads, so a source rename (`@db.column`, flattening, a moved
278
+ * JSON leaf) or an aggregate change recreates the view.
279
+ * @since 0.1.136
280
+ */
281
+ interface TViewColumnSnapshot {
282
+ column: string;
283
+ sourceTable: string;
284
+ sourceColumn: string;
285
+ /** JSON array of the path segments inside a JSON source column. */
286
+ jsonPath?: string;
287
+ jsonType?: string;
288
+ aggFn?: string;
289
+ aggField?: string;
290
+ /** Canonical JSON of a conditional aggregate's predicate. */
291
+ aggFilter?: string;
292
+ }
275
293
  interface TViewSnapshot {
276
294
  tableName: string;
277
295
  viewType: "V" | "M" | "E";
@@ -281,10 +299,17 @@ interface TViewSnapshot {
281
299
  * join-less view (`[]`) serializes byte-identically to older snapshots.
282
300
  */
283
301
  joinTables?: TViewJoinSnapshot[];
302
+ /** @since 0.1.136 — view columns and their physical sources, sorted by `column`. */
303
+ columns?: TViewColumnSnapshot[];
284
304
  filterHash?: string;
285
305
  /** @since 0.1.128 — hash of the canonical `@db.view.having` predicate. */
286
306
  havingHash?: string;
287
307
  materialized?: boolean;
308
+ /**
309
+ * @since 0.1.137 — the adapter's `viewRenderRevision()`, present only when
310
+ * the adapter defines one (managed views only).
311
+ */
312
+ renderRevision?: string;
288
313
  fields: TFieldSnapshot[];
289
314
  }
290
315
  /**
@@ -534,19 +559,6 @@ declare class SchemaSync {
534
559
  private buildExecutorDeps;
535
560
  }
536
561
  //#endregion
537
- //#region src/schema/column-diff.d.ts
538
- /**
539
- * Computes the difference between desired schema fields and existing database columns.
540
- *
541
- * @param desired - Field descriptors from the Atscript type (after flattening).
542
- * @param existing - Columns currently in the database (from introspection).
543
- * @param typeMapper - Optional function to map field metadata to DB-native type strings.
544
- * Receives the full field meta (design type, annotations, PK status, etc.)
545
- * so adapters can produce context-aware types (e.g., `VARCHAR(255)` from maxLength).
546
- * Required for type change detection.
547
- */
548
- declare function computeColumnDiff(desired: readonly TDbFieldMeta[], existing: TExistingColumn[], typeMapper?: (field: TDbFieldMeta) => string): TColumnDiff;
549
- //#endregion
550
562
  //#region src/schema/table-option-diff.d.ts
551
563
  /**
552
564
  * Computes the difference between desired and existing table options.
@@ -649,4 +661,4 @@ declare function syncSchema(space: DbSpace, types: readonly TAtscriptAnnotatedTy
649
661
  */
650
662
  declare function planSchema(space: DbSpace, types: readonly TAtscriptAnnotatedType[], opts?: Pick<TSyncOptions, "force" | "safe">): Promise<TSyncPlan>;
651
663
  //#endregion
652
- export { SchemaSync, SyncEntry, type TDbObjectKind, type TDependencyEdge, type TEnsureTableOptions, type TExistingForeignKey, type TFieldSnapshot, type TForeignKeyDiff, type TForeignKeySnapshot, type TPrimaryKeyChange, type TReferencingForeignKey, type TSyncColors, type TSyncEntryInit, type TSyncEntryStatus, type TSyncOptions, type TSyncPlan, type TSyncResult, type TSyncSkippedWork, type TTableSnapshot, type TViewJoinSnapshot, type TViewSnapshot, canonicalizeQueryNode, computeColumnDiff, computeForeignKeyDiff, computeSchemaHash, computeTableHash, computeTableOptionDiff, computeTableSnapshot, computeViewSnapshot, fkKey, hasForeignKeyChanges, planSchema, readStoredSnapshot, snapshotToExistingColumns, snapshotToExistingTableOptions, syncSchema, topoOrder };
664
+ export { SchemaSync, SyncEntry, type TDbObjectKind, type TDependencyEdge, type TEnsureTableOptions, type TExistingForeignKey, type TFieldSnapshot, type TForeignKeyDiff, type TForeignKeySnapshot, type TPrimaryKeyChange, type TReferencingForeignKey, type TSyncColors, type TSyncEntryInit, type TSyncEntryStatus, type TSyncOptions, type TSyncPlan, type TSyncResult, type TSyncSkippedWork, type TTableSnapshot, type TViewColumnSnapshot, type TViewJoinSnapshot, type TViewSnapshot, canonicalizeQueryNode, computeColumnDiff, computeForeignKeyDiff, computeSchemaHash, computeTableHash, computeTableOptionDiff, computeTableSnapshot, computeViewSnapshot, fkKey, hasForeignKeyChanges, planSchema, readStoredSnapshot, snapshotToExistingColumns, snapshotToExistingTableOptions, syncSchema, topoOrder };
package/dist/sync.mjs CHANGED
@@ -1,291 +1,4 @@
1
- import { U as NoopLogger } from "./db-view-Doh8vPg3.mjs";
2
- //#region src/schema/schema-hash.ts
3
- /** Extracts sorted field snapshots from a readable's field descriptors. */
4
- function extractFieldSnapshots(fields, typeMapper) {
5
- return fields.filter((f) => !f.ignored).map((f) => {
6
- const snap = {
7
- physicalName: f.physicalName,
8
- designType: f.designType,
9
- optional: f.optional,
10
- isPrimaryKey: f.isPrimaryKey,
11
- storage: f.storage
12
- };
13
- if (f.defaultValue) snap.defaultValue = f.defaultValue;
14
- if (typeMapper) snap.mappedType = typeMapper(f);
15
- if (f.encrypted) snap.encrypted = true;
16
- return snap;
17
- }).toSorted((a, b) => a.physicalName.localeCompare(b.physicalName));
18
- }
19
- /**
20
- * Extracts a canonical, serializable snapshot from a readable's metadata.
21
- * Sorted deterministically so the hash is stable across runs.
22
- *
23
- * @param readable - The table/view readable.
24
- * @param typeMapper - Optional adapter-specific type mapper. When provided,
25
- * each field's mapped type (e.g., "VARCHAR(255)") is stored in the snapshot
26
- * for precise type change detection.
27
- */
28
- function computeTableSnapshot(readable, typeMapper, tableOptions) {
29
- const fields = extractFieldSnapshots(readable.fieldDescriptors, typeMapper);
30
- const indexes = [...readable.indexes.values()].map((idx) => ({
31
- key: idx.key,
32
- type: idx.type,
33
- fields: idx.fields.map((f) => ({
34
- name: f.name,
35
- sort: f.sort
36
- }))
37
- })).toSorted((a, b) => a.key.localeCompare(b.key));
38
- const foreignKeys = [...readable.foreignKeys.values()].map((fk) => ({
39
- fields: [...fk.fields].toSorted(),
40
- targetTable: fk.targetTable,
41
- targetFields: [...fk.targetFields].toSorted(),
42
- onDelete: fk.onDelete,
43
- onUpdate: fk.onUpdate
44
- })).toSorted((a, b) => a.fields.join(",").localeCompare(b.fields.join(",")));
45
- const snapshot = {
46
- tableName: readable.tableName,
47
- fields,
48
- indexes,
49
- foreignKeys
50
- };
51
- if (tableOptions?.length) snapshot.tableOptions = [...tableOptions].toSorted((a, b) => a.key.localeCompare(b.key));
52
- return snapshot;
53
- }
54
- /**
55
- * Extracts a canonical, serializable snapshot from a view's metadata.
56
- * Captures view plan (entry table, joins, filter, materialization) for
57
- * detecting view definition changes.
58
- */
59
- function computeViewSnapshot(view) {
60
- const fields = extractFieldSnapshots(view.fieldDescriptors);
61
- if (view.isExternal) return {
62
- tableName: view.tableName,
63
- viewType: "E",
64
- fields
65
- };
66
- const plan = view.viewPlan;
67
- const qualify = (ref) => view.resolveFieldRef(ref, (n) => n);
68
- const result = {
69
- tableName: view.tableName,
70
- viewType: plan.materialized ? "M" : "V",
71
- entryTable: plan.entryTable,
72
- joinTables: plan.joins.map((j) => ({
73
- targetTable: j.targetTable,
74
- condition: JSON.stringify(canonicalizeQueryNode(j.condition, qualify))
75
- })),
76
- materialized: plan.materialized || void 0,
77
- fields
78
- };
79
- if (plan.filter) result.filterHash = fnv1a(JSON.stringify(canonicalizeQueryNode(plan.filter, qualify)));
80
- if (plan.having) result.havingHash = fnv1a(JSON.stringify(canonicalizeQueryNode(plan.having, qualify)));
81
- return result;
82
- }
83
- /**
84
- * Converts a view predicate (join condition, `@db.view.filter`,
85
- * `@db.view.having`) into a serializable structure whose JSON is a stable
86
- * function of its MEANING: field refs become `qualify(ref)` — the view's
87
- * `resolveFieldRef(ref, (n) => n)`, i.e. `"<table>.<field>"`, so a predicate
88
- * retargeted to another table with the same field name changes — operators
89
- * and literal values are kept as-is, `$and`/`$or` keep declaration order, and
90
- * no function references survive. Two identical models produce byte-identical
91
- * JSON.
92
- * @since 0.1.128
93
- */
94
- function canonicalizeQueryNode(node, qualify) {
95
- if ("$and" in node) return { and: node.$and.map((n) => canonicalizeQueryNode(n, qualify)) };
96
- if ("$or" in node) return { or: node.$or.map((n) => canonicalizeQueryNode(n, qualify)) };
97
- if ("$not" in node) return { not: canonicalizeQueryNode(node.$not, qualify) };
98
- const comp = node;
99
- const out = {
100
- l: qualify(comp.left),
101
- op: comp.op
102
- };
103
- if (comp.right !== void 0) out.r = comp.right !== null && typeof comp.right === "object" && "field" in comp.right ? { f: qualify(comp.right) } : comp.right;
104
- return out;
105
- }
106
- /**
107
- * Computes a deterministic hash string from multiple table snapshots.
108
- * Uses FNV-1a for speed — not cryptographic, just needs stability + collision resistance.
109
- */
110
- function computeSchemaHash(snapshots) {
111
- const sorted = [...snapshots].toSorted((a, b) => a.tableName.localeCompare(b.tableName));
112
- return fnv1a(JSON.stringify(sorted));
113
- }
114
- /**
115
- * Computes a hash for a single table/view snapshot.
116
- * Used for per-table change detection via stored snapshots.
117
- */
118
- function computeTableHash(snapshot) {
119
- return fnv1a(JSON.stringify(snapshot));
120
- }
121
- /**
122
- * Converts stored snapshot fields to `TExistingColumn[]` format
123
- * for use with `computeColumnDiff`. Used by adapters that lack
124
- * native column introspection (e.g., MongoDB).
125
- *
126
- * The `type` field uses `mappedType` when available (adapter-specific),
127
- * falling back to `designType`.
128
- */
129
- function snapshotToExistingColumns(snapshot) {
130
- return snapshot.fields.map((f) => ({
131
- name: f.physicalName,
132
- type: f.mappedType ?? f.designType,
133
- notnull: !f.optional,
134
- pk: f.isPrimaryKey,
135
- dflt_value: serializeDefaultValue(f.defaultValue)
136
- }));
137
- }
138
- /**
139
- * Extracts table options from a stored snapshot for diff comparison.
140
- * Used as fallback when an adapter lacks native table option introspection.
141
- */
142
- function snapshotToExistingTableOptions(snapshot) {
143
- return snapshot.tableOptions ?? [];
144
- }
145
- /** Serializes a TDbDefaultValue to a comparable string. */
146
- function serializeDefaultValue(dv) {
147
- if (!dv) return;
148
- if (dv.kind === "value") return dv.value;
149
- return `fn:${dv.fn}`;
150
- }
151
- /** FNV-1a 32-bit hash → hex string */
152
- function fnv1a(str) {
153
- let hash = 2166136261;
154
- for (let i = 0; i < str.length; i++) {
155
- hash ^= str.codePointAt(i);
156
- hash = Math.imul(hash, 16777619);
157
- }
158
- return Math.trunc(hash).toString(16).padStart(8, "0");
159
- }
160
- //#endregion
161
- //#region src/schema/fk-diff.ts
162
- /** Canonical key for an FK: sorted local field names, comma-joined. */
163
- function fkKey(fields) {
164
- return [...fields].toSorted().join(",");
165
- }
166
- /**
167
- * Compares desired FK constraints against stored snapshot to detect
168
- * additions, removals, and property changes (target table, target fields,
169
- * onDelete, onUpdate).
170
- */
171
- function computeForeignKeyDiff(desired, existingSnapshot) {
172
- const added = [];
173
- const removed = [];
174
- const changed = [];
175
- const existingByKey = /* @__PURE__ */ new Map();
176
- for (const fk of existingSnapshot) existingByKey.set(fkKey(fk.fields), fk);
177
- const desiredKeys = /* @__PURE__ */ new Set();
178
- for (const fk of desired.values()) {
179
- const key = fkKey(fk.fields);
180
- desiredKeys.add(key);
181
- const existing = existingByKey.get(key);
182
- if (!existing) added.push(fk);
183
- else if (fkPropertiesDiffer(fk, existing)) changed.push({
184
- desired: fk,
185
- existing
186
- });
187
- }
188
- for (const [key, fk] of existingByKey) if (!desiredKeys.has(key)) removed.push(fk);
189
- return {
190
- added,
191
- removed,
192
- changed
193
- };
194
- }
195
- /** Whether the FK diff contains any changes. */
196
- function hasForeignKeyChanges(diff) {
197
- return diff.added.length > 0 || diff.removed.length > 0 || diff.changed.length > 0;
198
- }
199
- function fkPropertiesDiffer(desired, existing) {
200
- if (desired.targetTable !== existing.targetTable) return true;
201
- if (fkKey(desired.targetFields) !== fkKey(existing.targetFields)) return true;
202
- if ((desired.onDelete ?? void 0) !== (existing.onDelete ?? void 0)) return true;
203
- if ((desired.onUpdate ?? void 0) !== (existing.onUpdate ?? void 0)) return true;
204
- return false;
205
- }
206
- //#endregion
207
- //#region src/schema/column-diff.ts
208
- /**
209
- * Computes the difference between desired schema fields and existing database columns.
210
- *
211
- * @param desired - Field descriptors from the Atscript type (after flattening).
212
- * @param existing - Columns currently in the database (from introspection).
213
- * @param typeMapper - Optional function to map field metadata to DB-native type strings.
214
- * Receives the full field meta (design type, annotations, PK status, etc.)
215
- * so adapters can produce context-aware types (e.g., `VARCHAR(255)` from maxLength).
216
- * Required for type change detection.
217
- */
218
- function computeColumnDiff(desired, existing, typeMapper) {
219
- const existingByName = new Map(existing.map((c) => [c.name, c]));
220
- const desiredByName = /* @__PURE__ */ new Map();
221
- const renamedOldNames = /* @__PURE__ */ new Set();
222
- const added = [];
223
- const renamed = [];
224
- const typeChanged = [];
225
- const nullableChanged = [];
226
- const defaultChanged = [];
227
- const conflicts = [];
228
- for (const field of desired) {
229
- if (field.ignored) continue;
230
- desiredByName.set(field.physicalName, field);
231
- const existingCol = existingByName.get(field.physicalName);
232
- if (existingCol) if (field.renamedFrom && existingByName.has(field.renamedFrom)) {
233
- conflicts.push({
234
- field,
235
- oldName: field.renamedFrom,
236
- conflictsWith: field.physicalName
237
- });
238
- renamedOldNames.add(field.renamedFrom);
239
- } else {
240
- if (typeMapper) {
241
- if (typeMapper(field).toUpperCase() !== existingCol.type.toUpperCase()) typeChanged.push({
242
- field,
243
- existingType: existingCol.type
244
- });
245
- }
246
- if (!field.isPrimaryKey && !existingCol.pk) {
247
- const desiredNotNull = !field.optional;
248
- if (existingCol.notnull !== desiredNotNull) nullableChanged.push({
249
- field,
250
- wasNullable: !existingCol.notnull
251
- });
252
- }
253
- const desiredDefault = serializeDefaultValue(field.defaultValue);
254
- if (existingCol.dflt_value !== void 0 && existingCol.dflt_value !== desiredDefault) defaultChanged.push({
255
- field,
256
- oldDefault: existingCol.dflt_value,
257
- newDefault: desiredDefault
258
- });
259
- }
260
- else if (field.renamedFrom && existingByName.has(field.renamedFrom)) {
261
- renamed.push({
262
- field,
263
- oldName: field.renamedFrom
264
- });
265
- renamedOldNames.add(field.renamedFrom);
266
- } else added.push(field);
267
- }
268
- const diff = {
269
- added,
270
- removed: existing.filter((c) => !desiredByName.has(c.name) && !renamedOldNames.has(c.name)),
271
- renamed,
272
- typeChanged,
273
- nullableChanged,
274
- defaultChanged,
275
- conflicts
276
- };
277
- if (existing.length > 0) {
278
- const newNameByOld = new Map(renamed.map((r) => [r.oldName, r.field.physicalName]));
279
- const from = existing.filter((c) => c.pk).map((c) => newNameByOld.get(c.name) ?? c.name);
280
- const to = desired.filter((f) => !f.ignored && f.isPrimaryKey).map((f) => f.physicalName);
281
- if (fkKey(from) !== fkKey(to)) diff.primaryKeyChanged = {
282
- from,
283
- to
284
- };
285
- }
286
- return diff;
287
- }
288
- //#endregion
1
+ import { a as hasForeignKeyChanges, c as computeTableHash, d as snapshotToExistingColumns, f as snapshotToExistingTableOptions, i as fkKey, l as computeTableSnapshot, nt as NoopLogger, o as canonicalizeQueryNode, r as computeForeignKeyDiff, s as computeSchemaHash, t as computeColumnDiff, u as computeViewSnapshot } from "./column-diff-BwOA5101.mjs";
289
2
  //#region src/schema/table-option-diff.ts
290
3
  /**
291
4
  * Computes the difference between desired and existing table options.
@@ -1746,7 +1459,11 @@ var SchemaSync = class {
1746
1459
  if (d.externalTargets.get(fk.targetTable) === false) addRefusal(refusals, t.name, `FK ${t.name}.${fk.fields.join(",")} references "${fk.targetTable}" which is neither in the sync inventory nor present in the database`);
1747
1460
  }
1748
1461
  }
1749
- for (const view of d.views) if (d.viewObjectKinds.get(view.tableName) === "table") addRefusal(refusals, view.tableName, `A physical table "${view.tableName}" exists where managed view "${view.tableName}" is declared — drop or rename it`);
1462
+ for (const view of d.views) {
1463
+ if (d.viewObjectKinds.get(view.tableName) === "table") addRefusal(refusals, view.tableName, `A physical table "${view.tableName}" exists where managed view "${view.tableName}" is declared — drop or rename it`);
1464
+ const fns = view.dbAdapter.aggregateFns();
1465
+ for (const col of view.getViewColumnMappings()) if (col.aggFn && !fns.has(col.aggFn)) addRefusal(refusals, view.tableName, `View "${view.tableName}" field "${col.viewPath}": aggregate "${col.aggFn}" is not supported by this adapter (aggregateFns())`);
1466
+ }
1750
1467
  if (!safe) for (const r of d.removed) {
1751
1468
  if (r.isView) continue;
1752
1469
  const referencers = [];
@@ -11,14 +11,17 @@ function validateExclusiveWith(token, selfName, others) {
11
11
  });
12
12
  return errors;
13
13
  }
14
+ /** Primitive base type (`string`, `number`, `decimal`, …) of a resolved definition, if it is a primitive. */
15
+ function primitiveBaseType(def) {
16
+ if (!def || !isPrimitive(def)) return void 0;
17
+ const ct = def.config.type;
18
+ return typeof ct === "object" ? ct.kind === "final" ? ct.value : ct.kind : ct;
19
+ }
14
20
  /** Resolves a field/prop's primitive base type, or `undefined` if it isn't a ref-to-primitive. */
15
21
  function getPrimitiveBaseType(node, doc) {
16
22
  const def = node.getDefinition();
17
23
  if (!def || !isRef(def)) return void 0;
18
- const unwound = doc.unwindType(def.id, def.chain);
19
- if (!unwound || !isPrimitive(unwound.def)) return void 0;
20
- const ct = unwound.def.config.type;
21
- return typeof ct === "object" ? ct.kind === "final" ? ct.value : ct.kind : ct;
24
+ return primitiveBaseType(doc.unwindType(def.id, def.chain)?.def);
22
25
  }
23
26
  /** Asserts `args[0]` names a sibling property whose primitive base type is `string`. */
24
27
  function validateSiblingStringField(token, args, doc, selfName) {
@@ -253,6 +256,24 @@ const viewAnnotationNames = [
253
256
  "db.view.filter",
254
257
  "db.view.materialized"
255
258
  ];
259
+ /** The `@db.view.joins` annotations of a view interface, in declaration order. */
260
+ function viewJoins(owner) {
261
+ return owner.annotations?.filter((a) => a.name === "db.view.joins") ?? [];
262
+ }
263
+ /** The target type names of `@db.view.joins` annotations. */
264
+ function joinTargets(joins) {
265
+ return joins.map((a) => a.args[0]?.text).filter((t) => !!t);
266
+ }
267
+ /**
268
+ * The type names a view predicate (`@db.view.filter`, a conditional
269
+ * `@db.agg.*`) may reference: the `@db.view.for` entry table, then every
270
+ * `@db.view.joins` target.
271
+ */
272
+ function viewScopeTypes(owner) {
273
+ const entry = getAnnotationAlias(owner, "db.view.for");
274
+ const targets = joinTargets(viewJoins(owner));
275
+ return entry ? [entry, ...targets] : targets;
276
+ }
256
277
  /**
257
278
  * Check if a node has any @db.view.* annotation.
258
279
  */
@@ -266,8 +287,9 @@ function hasAnyViewAnnotation(node) {
266
287
  * @param allowedTypes - Type names allowed as qualified refs
267
288
  * @param unqualifiedTarget - Type name for resolving unqualified refs, or null to disallow them
268
289
  * @param doc - The document for type lookups
290
+ * @param scopeHint - Replaces the default "expected 'A' or 'B'" tail of the out-of-scope message
269
291
  */
270
- function validateQueryScope(queryToken, allowedTypes, unqualifiedTarget, doc) {
292
+ function validateQueryScope(queryToken, allowedTypes, unqualifiedTarget, doc, scopeHint) {
271
293
  const errors = [];
272
294
  const queryNode = queryToken.queryNode;
273
295
  if (!queryNode) return errors;
@@ -275,7 +297,7 @@ function validateQueryScope(queryToken, allowedTypes, unqualifiedTarget, doc) {
275
297
  if (ref.typeRef) {
276
298
  const typeName = ref.typeRef.text;
277
299
  if (!allowedTypes.includes(typeName)) errors.push({
278
- message: `Query references '${typeName}' which is not in scope — expected ${allowedTypes.map((t) => `'${t}'`).join(" or ")}`,
300
+ message: `Query references '${typeName}' which is not in scope — ${scopeHint ?? `expected ${allowedTypes.map((t) => `'${t}'`).join(" or ")}`}`,
279
301
  severity: 1,
280
302
  range: ref.typeRef.range
281
303
  });
@@ -310,4 +332,4 @@ function validateQueryScope(queryToken, allowedTypes, unqualifiedTarget, doc) {
310
332
  return errors;
311
333
  }
312
334
  //#endregion
313
- export { getAnnotationAlias as a, getParentStruct as c, validateExclusiveWith as d, validateFieldBaseType as f, validateRefArgument as i, getParentTypeName as l, hasAnyViewAnnotation as n, getDbTableOwner as o, validateSiblingStringField as p, validateQueryScope as r, getNavTargetTypeName as s, findFKFieldsPointingTo as t, refActionAnnotation as u };
335
+ export { validateSiblingStringField as _, validateRefArgument as a, getAnnotationAlias as c, getParentStruct as d, getParentTypeName as f, validateFieldBaseType as g, validateExclusiveWith as h, validateQueryScope as i, getDbTableOwner as l, refActionAnnotation as m, hasAnyViewAnnotation as n, viewJoins as o, primitiveBaseType as p, joinTargets as r, viewScopeTypes as s, findFKFieldsPointingTo as t, getNavTargetTypeName as u };
@@ -11,14 +11,17 @@ function validateExclusiveWith(token, selfName, others) {
11
11
  });
12
12
  return errors;
13
13
  }
14
+ /** Primitive base type (`string`, `number`, `decimal`, …) of a resolved definition, if it is a primitive. */
15
+ function primitiveBaseType(def) {
16
+ if (!def || !(0, _atscript_core.isPrimitive)(def)) return void 0;
17
+ const ct = def.config.type;
18
+ return typeof ct === "object" ? ct.kind === "final" ? ct.value : ct.kind : ct;
19
+ }
14
20
  /** Resolves a field/prop's primitive base type, or `undefined` if it isn't a ref-to-primitive. */
15
21
  function getPrimitiveBaseType(node, doc) {
16
22
  const def = node.getDefinition();
17
23
  if (!def || !(0, _atscript_core.isRef)(def)) return void 0;
18
- const unwound = doc.unwindType(def.id, def.chain);
19
- if (!unwound || !(0, _atscript_core.isPrimitive)(unwound.def)) return void 0;
20
- const ct = unwound.def.config.type;
21
- return typeof ct === "object" ? ct.kind === "final" ? ct.value : ct.kind : ct;
24
+ return primitiveBaseType(doc.unwindType(def.id, def.chain)?.def);
22
25
  }
23
26
  /** Asserts `args[0]` names a sibling property whose primitive base type is `string`. */
24
27
  function validateSiblingStringField(token, args, doc, selfName) {
@@ -253,6 +256,24 @@ const viewAnnotationNames = [
253
256
  "db.view.filter",
254
257
  "db.view.materialized"
255
258
  ];
259
+ /** The `@db.view.joins` annotations of a view interface, in declaration order. */
260
+ function viewJoins(owner) {
261
+ return owner.annotations?.filter((a) => a.name === "db.view.joins") ?? [];
262
+ }
263
+ /** The target type names of `@db.view.joins` annotations. */
264
+ function joinTargets(joins) {
265
+ return joins.map((a) => a.args[0]?.text).filter((t) => !!t);
266
+ }
267
+ /**
268
+ * The type names a view predicate (`@db.view.filter`, a conditional
269
+ * `@db.agg.*`) may reference: the `@db.view.for` entry table, then every
270
+ * `@db.view.joins` target.
271
+ */
272
+ function viewScopeTypes(owner) {
273
+ const entry = getAnnotationAlias(owner, "db.view.for");
274
+ const targets = joinTargets(viewJoins(owner));
275
+ return entry ? [entry, ...targets] : targets;
276
+ }
256
277
  /**
257
278
  * Check if a node has any @db.view.* annotation.
258
279
  */
@@ -266,8 +287,9 @@ function hasAnyViewAnnotation(node) {
266
287
  * @param allowedTypes - Type names allowed as qualified refs
267
288
  * @param unqualifiedTarget - Type name for resolving unqualified refs, or null to disallow them
268
289
  * @param doc - The document for type lookups
290
+ * @param scopeHint - Replaces the default "expected 'A' or 'B'" tail of the out-of-scope message
269
291
  */
270
- function validateQueryScope(queryToken, allowedTypes, unqualifiedTarget, doc) {
292
+ function validateQueryScope(queryToken, allowedTypes, unqualifiedTarget, doc, scopeHint) {
271
293
  const errors = [];
272
294
  const queryNode = queryToken.queryNode;
273
295
  if (!queryNode) return errors;
@@ -275,7 +297,7 @@ function validateQueryScope(queryToken, allowedTypes, unqualifiedTarget, doc) {
275
297
  if (ref.typeRef) {
276
298
  const typeName = ref.typeRef.text;
277
299
  if (!allowedTypes.includes(typeName)) errors.push({
278
- message: `Query references '${typeName}' which is not in scope — expected ${allowedTypes.map((t) => `'${t}'`).join(" or ")}`,
300
+ message: `Query references '${typeName}' which is not in scope — ${scopeHint ?? `expected ${allowedTypes.map((t) => `'${t}'`).join(" or ")}`}`,
279
301
  severity: 1,
280
302
  range: ref.typeRef.range
281
303
  });
@@ -352,6 +374,18 @@ Object.defineProperty(exports, "hasAnyViewAnnotation", {
352
374
  return hasAnyViewAnnotation;
353
375
  }
354
376
  });
377
+ Object.defineProperty(exports, "joinTargets", {
378
+ enumerable: true,
379
+ get: function() {
380
+ return joinTargets;
381
+ }
382
+ });
383
+ Object.defineProperty(exports, "primitiveBaseType", {
384
+ enumerable: true,
385
+ get: function() {
386
+ return primitiveBaseType;
387
+ }
388
+ });
355
389
  Object.defineProperty(exports, "refActionAnnotation", {
356
390
  enumerable: true,
357
391
  get: function() {
@@ -388,3 +422,15 @@ Object.defineProperty(exports, "validateSiblingStringField", {
388
422
  return validateSiblingStringField;
389
423
  }
390
424
  });
425
+ Object.defineProperty(exports, "viewJoins", {
426
+ enumerable: true,
427
+ get: function() {
428
+ return viewJoins;
429
+ }
430
+ });
431
+ Object.defineProperty(exports, "viewScopeTypes", {
432
+ enumerable: true,
433
+ get: function() {
434
+ return viewScopeTypes;
435
+ }
436
+ });
@@ -1,7 +1,7 @@
1
1
  import { TAtscriptAnnotatedType, TAtscriptTypeArray, TAtscriptTypeObject, TValidatorPlugin, Validator } from "@atscript/typescript/utils";
2
2
 
3
3
  //#region src/db-error.d.ts
4
- type DbErrorCode = "CONFLICT" | "FK_VIOLATION" | "NOT_FOUND" | "CASCADE_CYCLE" | "INVALID_QUERY" | "DEPTH_EXCEEDED" | "VERSION_COLUMN_WRITE" | "CAS_EXHAUSTED" | "CAS_MISMATCH" | "ENC_CONFIG_MISSING" | "ENC_KEY_INVALID" | "ENC_NOT_ENCRYPTED" | "ENC_DECRYPT_FAILED" | "ENC_FIELD_FILTER" | "ENC_FIELD_SORT" | "ENC_FIELD_AGG" | "ENC_FIELD_PATCH_OP" | "GEO_INDEX_MISSING" | "GEO_NOT_SUPPORTED" | "FILTER_TYPE_MISMATCH" /** The adapter's `calendarBucketUnits()` lacks the requested unit (moost-db: 400). */ | "BUCKET_NOT_SUPPORTED" /** The engine cannot resolve the bucket's time zone — a store-configuration condition (moost-db: 501). */ | "BUCKET_TZ_UNAVAILABLE" | "TX_WAIT_TIMEOUT";
4
+ type DbErrorCode = "CONFLICT" | "FK_VIOLATION" | "NOT_FOUND" | "CASCADE_CYCLE" | "INVALID_QUERY" | "DEPTH_EXCEEDED" | "VERSION_COLUMN_WRITE" | "CAS_EXHAUSTED" | "CAS_MISMATCH" | "ENC_CONFIG_MISSING" | "ENC_KEY_INVALID" | "ENC_NOT_ENCRYPTED" | "ENC_DECRYPT_FAILED" | "ENC_FIELD_FILTER" | "ENC_FIELD_SORT" | "ENC_FIELD_AGG" | "ENC_FIELD_PATCH_OP" | "GEO_INDEX_MISSING" | "GEO_NOT_SUPPORTED" | "FILTER_TYPE_MISMATCH" /** The adapter's `calendarBucketUnits()` lacks the requested unit (moost-db: 400). */ | "BUCKET_NOT_SUPPORTED" /** The engine cannot resolve the bucket's time zone — a store-configuration condition (moost-db: 501). */ | "BUCKET_TZ_UNAVAILABLE" /** A known aggregate function the adapter's `aggregateFns()` lacks (moost-db: 400). @since 0.1.136 */ | "AGG_FN_NOT_SUPPORTED" | "TX_WAIT_TIMEOUT";
5
5
  declare class DbError extends Error {
6
6
  readonly code: DbErrorCode;
7
7
  readonly errors: Array<{
@@ -1,7 +1,7 @@
1
1
  import { TAtscriptAnnotatedType, TAtscriptTypeArray, TAtscriptTypeObject, TValidatorPlugin, Validator } from "@atscript/typescript/utils";
2
2
 
3
3
  //#region src/db-error.d.ts
4
- type DbErrorCode = "CONFLICT" | "FK_VIOLATION" | "NOT_FOUND" | "CASCADE_CYCLE" | "INVALID_QUERY" | "DEPTH_EXCEEDED" | "VERSION_COLUMN_WRITE" | "CAS_EXHAUSTED" | "CAS_MISMATCH" | "ENC_CONFIG_MISSING" | "ENC_KEY_INVALID" | "ENC_NOT_ENCRYPTED" | "ENC_DECRYPT_FAILED" | "ENC_FIELD_FILTER" | "ENC_FIELD_SORT" | "ENC_FIELD_AGG" | "ENC_FIELD_PATCH_OP" | "GEO_INDEX_MISSING" | "GEO_NOT_SUPPORTED" | "FILTER_TYPE_MISMATCH" /** The adapter's `calendarBucketUnits()` lacks the requested unit (moost-db: 400). */ | "BUCKET_NOT_SUPPORTED" /** The engine cannot resolve the bucket's time zone — a store-configuration condition (moost-db: 501). */ | "BUCKET_TZ_UNAVAILABLE" | "TX_WAIT_TIMEOUT";
4
+ type DbErrorCode = "CONFLICT" | "FK_VIOLATION" | "NOT_FOUND" | "CASCADE_CYCLE" | "INVALID_QUERY" | "DEPTH_EXCEEDED" | "VERSION_COLUMN_WRITE" | "CAS_EXHAUSTED" | "CAS_MISMATCH" | "ENC_CONFIG_MISSING" | "ENC_KEY_INVALID" | "ENC_NOT_ENCRYPTED" | "ENC_DECRYPT_FAILED" | "ENC_FIELD_FILTER" | "ENC_FIELD_SORT" | "ENC_FIELD_AGG" | "ENC_FIELD_PATCH_OP" | "GEO_INDEX_MISSING" | "GEO_NOT_SUPPORTED" | "FILTER_TYPE_MISMATCH" /** The adapter's `calendarBucketUnits()` lacks the requested unit (moost-db: 400). */ | "BUCKET_NOT_SUPPORTED" /** The engine cannot resolve the bucket's time zone — a store-configuration condition (moost-db: 501). */ | "BUCKET_TZ_UNAVAILABLE" /** A known aggregate function the adapter's `aggregateFns()` lacks (moost-db: 400). @since 0.1.136 */ | "AGG_FN_NOT_SUPPORTED" | "TX_WAIT_TIMEOUT";
5
5
  declare class DbError extends Error {
6
6
  readonly code: DbErrorCode;
7
7
  readonly errors: Array<{
@@ -1,4 +1,4 @@
1
1
  import { a as $mul, c as $update, d as TDbFieldOp, g as separateCas, h as reconcileCas, i as $insert, l as $upsert, m as isDbFieldOp, n as $dec, o as $remove, r as $inc, s as $replace, t as $cas, u as TDbCas } from "./ops-AqhV7s9o.cjs";
2
- import { a as buildValidationContext, c as isNavRelation, d as TArrayPatch, f as TDbPatch, g as DbError, i as buildPatchPartial, l as isEmptyObject, n as ValidatorMode, o as dbPlugin, p as getKeyProps, r as buildDbValidator, s as forceNavNonOptional, t as ValidationContext, u as isPlainObject } from "./validator-CewfnGZj.cjs";
2
+ import { a as buildValidationContext, c as isNavRelation, d as TArrayPatch, f as TDbPatch, g as DbError, i as buildPatchPartial, l as isEmptyObject, n as ValidatorMode, o as dbPlugin, p as getKeyProps, r as buildDbValidator, s as forceNavNonOptional, t as ValidationContext, u as isPlainObject } from "./validator-Drb2N-YL.cjs";
3
3
  import { n as createDbValidatorPlugin, t as DbValidationContext } from "./db-validator-plugin-BWy60OvG.cjs";
4
4
  export { $cas, $dec, $inc, $insert, $mul, $remove, $replace, $update, $upsert, DbError, type DbValidationContext, type TArrayPatch, type TDbCas, type TDbFieldOp, type TDbPatch, ValidationContext, ValidatorMode, buildDbValidator, buildPatchPartial, buildValidationContext, createDbValidatorPlugin, dbPlugin, forceNavNonOptional, getKeyProps, isDbFieldOp, isEmptyObject, isNavRelation, isPlainObject, reconcileCas, separateCas };
@@ -1,4 +1,4 @@
1
1
  import { a as $mul, c as $update, d as TDbFieldOp, g as separateCas, h as reconcileCas, i as $insert, l as $upsert, m as isDbFieldOp, n as $dec, o as $remove, r as $inc, s as $replace, t as $cas, u as TDbCas } from "./ops-AqhV7s9o.mjs";
2
- import { a as buildValidationContext, c as isNavRelation, d as TArrayPatch, f as TDbPatch, g as DbError, i as buildPatchPartial, l as isEmptyObject, n as ValidatorMode, o as dbPlugin, p as getKeyProps, r as buildDbValidator, s as forceNavNonOptional, t as ValidationContext, u as isPlainObject } from "./validator-CewfnGZj.mjs";
2
+ import { a as buildValidationContext, c as isNavRelation, d as TArrayPatch, f as TDbPatch, g as DbError, i as buildPatchPartial, l as isEmptyObject, n as ValidatorMode, o as dbPlugin, p as getKeyProps, r as buildDbValidator, s as forceNavNonOptional, t as ValidationContext, u as isPlainObject } from "./validator-Drb2N-YL.mjs";
3
3
  import { n as createDbValidatorPlugin, t as DbValidationContext } from "./db-validator-plugin-BWy60OvG.mjs";
4
4
  export { $cas, $dec, $inc, $insert, $mul, $remove, $replace, $update, $upsert, DbError, type DbValidationContext, type TArrayPatch, type TDbCas, type TDbFieldOp, type TDbPatch, ValidationContext, ValidatorMode, buildDbValidator, buildPatchPartial, buildValidationContext, createDbValidatorPlugin, dbPlugin, forceNavNonOptional, getKeyProps, isDbFieldOp, isEmptyObject, isNavRelation, isPlainObject, reconcileCas, separateCas };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atscript/db",
3
- "version": "0.1.135",
3
+ "version": "0.1.137",
4
4
  "description": "Database adapter utilities for atscript.",
5
5
  "keywords": [
6
6
  "atscript",