@atscript/db 0.1.131 → 0.1.133

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 (39) hide show
  1. package/dist/agg.cjs +19 -5
  2. package/dist/agg.d.cts +3 -4
  3. package/dist/agg.d.mts +3 -4
  4. package/dist/agg.mjs +2 -5
  5. package/dist/{db-readable-D8aa-wfP.d.mts → buckets-BENuu_tK.d.mts} +1346 -1141
  6. package/dist/{db-readable-Cmrg9aLU.d.cts → buckets-CTWy7Y1k.d.cts} +1346 -1141
  7. package/dist/{db-error-COrO58t5.mjs → db-error-D5uilS_A.mjs} +13 -1
  8. package/dist/{db-error-C4JuLcvb.cjs → db-error-DTkkeu5b.cjs} +18 -0
  9. package/dist/{db-space-B4MjNjY4.d.mts → db-space-Bv3Ar_Xd.d.mts} +1 -1
  10. package/dist/{db-space-rQFgz3-k.d.cts → db-space-DROLifb-.d.cts} +1 -1
  11. package/dist/{db-view-DUDnyQ3b.cjs → db-view-B5-bQ5AV.cjs} +552 -120
  12. package/dist/{db-view-BYH4HJ-U.mjs → db-view-caPATLmX.mjs} +486 -120
  13. package/dist/index.cjs +16 -4
  14. package/dist/index.d.cts +163 -38
  15. package/dist/index.d.mts +163 -38
  16. package/dist/index.mjs +5 -5
  17. package/dist/{nested-writer-CkDo-ZfH.mjs → nested-writer-CqL24ojl.mjs} +1 -1
  18. package/dist/{nested-writer-DxPhmWFz.cjs → nested-writer-wk1EFUNY.cjs} +1 -1
  19. package/dist/ops.cjs +1 -1
  20. package/dist/ops.mjs +1 -1
  21. package/dist/rel.cjs +2 -2
  22. package/dist/rel.d.cts +1 -1
  23. package/dist/rel.d.mts +1 -1
  24. package/dist/rel.mjs +2 -2
  25. package/dist/{relation-loader-CUGcxJ18.mjs → relation-loader-B68R1LET.mjs} +1 -1
  26. package/dist/{relation-loader-C8GOpNYJ.cjs → relation-loader-BD4xANQJ.cjs} +1 -1
  27. package/dist/sync.cjs +1 -1
  28. package/dist/sync.d.cts +2 -2
  29. package/dist/sync.d.mts +2 -2
  30. package/dist/sync.mjs +1 -1
  31. package/dist/{validator-lkCJKuoo.cjs → validator-BtZbcLN2.cjs} +1 -1
  32. package/dist/{validator-wBARmD68.d.cts → validator-CewfnGZj.d.cts} +9 -2
  33. package/dist/{validator-wBARmD68.d.mts → validator-CewfnGZj.d.mts} +9 -2
  34. package/dist/{validator-CeD_fqyW.mjs → validator-Ch7UIQl9.mjs} +1 -1
  35. package/dist/validator.cjs +2 -2
  36. package/dist/validator.d.cts +1 -1
  37. package/dist/validator.d.mts +1 -1
  38. package/dist/validator.mjs +2 -2
  39. package/package.json +3 -3
@@ -1,5 +1,5 @@
1
1
  import { f as TFieldOps } from "./ops-AqhV7s9o.cjs";
2
- import { AggregateControls, AggregateExpr, AggregateExpr as AggregateExpr$1, AggregateFn, AggregateQuery, AggregateQuery as AggregateQuery$1, AggregateResult, FieldOpsFor, FilterExpr, FilterExpr as FilterExpr$1, TypedWithRelation, Uniquery, Uniquery as Uniquery$1, UniqueryControls, UniqueryControls as UniqueryControls$1, UniqueryInsights, WithRelation, WithRelation as WithRelation$1 } from "@uniqu/core";
2
+ import { AggregateControls, AggregateExpr, AggregateExpr as AggregateExpr$1, AggregateFn, AggregateQuery, AggregateQuery as AggregateQuery$1, AggregateResult, BucketUnit, FieldOpsFor, FilterExpr, FilterExpr as FilterExpr$1, ResolvedBucket, TypedWithRelation, Uniquery, Uniquery as Uniquery$1, UniqueryControls, UniqueryControls as UniqueryControls$1, UniqueryInsights, WithRelation, WithRelation as WithRelation$1 } from "@uniqu/core";
3
3
  import { FlatOf, FlatOf as FlatOf$1, NavPropsOf, NavPropsOf as NavPropsOf$1, OwnPropsOf, OwnPropsOf as OwnPropsOf$1, PrimaryKeyOf, PrimaryKeyOf as PrimaryKeyOf$1, TAtscriptAnnotatedType, TAtscriptDataType, TAtscriptTypeObject, TMetadataMap, TSerializedAnnotatedType, TValidatorOptions, TValidatorPlugin, Validator } from "@atscript/typescript/utils";
4
4
 
5
5
  //#region src/query/uniqu-select.d.ts
@@ -11,6 +11,11 @@ import { FlatOf, FlatOf as FlatOf$1, NavPropsOf, NavPropsOf as NavPropsOf$1, Own
11
11
  * `controls.$select` is `UniquSelect | undefined`.
12
12
  *
13
13
  * For exclusion → inclusion inversion, pass `allFields` (physical field names).
14
+ *
15
+ * An array `$select` holds plain field names and computed entries —
16
+ * aggregates (`{ $fn, $field }`, {@link aggregates}) and calendar buckets
17
+ * (`{ $bucket, $field }`, {@link buckets}). Entries arrive normalized
18
+ * (`resolveCalendarBuckets` rejects any other shape before translation).
14
19
  */
15
20
  declare class UniquSelect {
16
21
  private static readonly UNRESOLVED;
@@ -19,12 +24,25 @@ declare class UniquSelect {
19
24
  private _array;
20
25
  private _projection;
21
26
  private _aggregates;
22
- constructor(raw: UniqueryControls["$select"], allFields?: string[]);
23
- /** Type guard: checks if a value is an AggregateExpr ({$fn, $field}). */
24
- private static _isAggregateExpr;
27
+ /**
28
+ * The calendar buckets of an aggregate `$select`, normalized (canonical
29
+ * zone, week start, alias) with the PHYSICAL source `field` and its
30
+ * descriptor `fd`. `undefined` when there are none. A `$groupBy` key equal
31
+ * to a bucket's `alias` groups by that bucket (aliases never collide with
32
+ * columns). Since 0.1.132.
33
+ */
34
+ readonly buckets: readonly TResolvedBucket[] | undefined;
35
+ /**
36
+ * @param raw - the `$select` value (field paths already physical).
37
+ * @param allFields - physical field names, for exclusion-form inversion.
38
+ * @param buckets - the resolved calendar buckets of the raw `$select`'s
39
+ * `{ $bucket }` entries (the field mappers supply them — physical `field`,
40
+ * source `fd`).
41
+ */
42
+ constructor(raw: UniqueryControls["$select"], allFields?: string[], buckets?: readonly TResolvedBucket[]);
25
43
  /**
26
44
  * Resolved inclusion array of plain field names (strings only).
27
- * AggregateExpr objects are filtered out.
45
+ * Computed entries (aggregates, calendar buckets) are filtered out.
28
46
  * For exclusion form, inverts using `allFields` from constructor.
29
47
  */
30
48
  get asArray(): string[] | undefined;
@@ -42,6 +60,8 @@ declare class UniquSelect {
42
60
  get aggregates(): AggregateExpr[] | undefined;
43
61
  /** Whether the $select contains any AggregateExpr entries. */
44
62
  get hasAggregates(): boolean;
63
+ /** The calendar bucket whose alias is `key`, if any — how adapters resolve a `$groupBy` / `$having` key. */
64
+ bucketByAlias(key: string): TResolvedBucket | undefined;
45
65
  }
46
66
  //#endregion
47
67
  //#region src/logger.d.ts
@@ -54,822 +74,540 @@ interface TGenericLogger {
54
74
  }
55
75
  declare const NoopLogger: TGenericLogger;
56
76
  //#endregion
57
- //#region src/table/table-metadata.d.ts
58
- /**
59
- * Finds the nearest ancestor of `path` that belongs to `set`.
60
- * Used by both the build pipeline (in `_classifyFields`) and
61
- * runtime reconstruction on the Readable.
62
- */
63
- declare function findAncestorInSet(path: string, set: ReadonlySet<string>): string | undefined;
64
- /** Returns true if the annotated type IS the `db.geoPoint` primitive (tag-based). */
65
- declare function isGeoPointType(fieldType: TAtscriptAnnotatedType): boolean;
66
- /**
67
- * Returns true if the annotated type is acceptable for `@db.index.geo`:
68
- * the `db.geoPoint` primitive or a structurally identical `number[]`
69
- * (excluding `db.vector`, which is semantically an embedding).
70
- */
71
- declare function isGeoIndexableType(fieldType: TAtscriptAnnotatedType): boolean;
77
+ //#region src/strategies/field-mapping.d.ts
72
78
  /**
73
- * Computed metadata for a database table or view.
74
- *
75
- * Contains all field metadata, physical mapping indexes, relation definitions,
76
- * and constraint information derived from Atscript annotations. Built lazily
77
- * on first access via {@link build}, then immutable.
78
- *
79
- * This class owns the build pipeline that was previously part of
80
- * `AtscriptDbReadable._flatten()`. The Readable delegates all metadata
81
- * access to this class.
79
+ * Strategy for mapping data between logical field shapes and physical storage.
80
+ * Two implementations: {@link DocumentFieldMapper} (nested objects, NoSQL)
81
+ * and `RelationalFieldMapper` (flattened columns, SQL).
82
82
  */
83
- declare class TableMetadata {
84
- readonly nestedObjects: boolean;
85
- flatMap: Map<string, TAtscriptAnnotatedType>;
86
- fieldDescriptors: readonly TDbFieldMeta[];
87
- primaryKeys: string[];
88
- preferredId: string[];
89
- originalMetaIdFields: string[];
90
- indexes: Map<string, TDbIndex>;
91
- foreignKeys: Map<string, TDbForeignKey>;
92
- relations: Map<string, TDbRelation>;
93
- navFields: Set<string>;
94
- ignoredFields: Set<string>;
95
- uniqueProps: Set<string>;
96
- defaults: Map<string, TDbDefaultValue>;
97
- columnMap: Map<string, string>;
98
- dimensions: string[];
99
- measures: string[];
100
- /** Logical field name annotated with `@db.column.version`, if any. */
101
- versionField?: string;
102
- /** path → sibling-ref path for `@db.amount.currency.ref` / `@db.unit.ref`. */
103
- quantityRefByField: Map<string, string>;
104
- /** Logical paths annotated with `@db.encrypted` — stored as one opaque ciphertext column. */
105
- encryptedFields: Set<string>;
106
- pathToPhysical: Map<string, string>;
107
- physicalToPath: Map<string, string>;
108
- flattenedParents: Set<string>;
109
- jsonFields: Set<string>;
110
- selectExpansion: Map<string, string[]>;
111
- booleanFields: Set<string>;
112
- decimalFields: Set<string>;
113
- allPhysicalFields: string[];
114
- /** Precomputed parent path → child physical column names for fast null-setting. */
115
- childrenByParent: Map<string, string[]>;
116
- /** Precomputed parent path → optional child logical paths (replace-strategy null-fill in the patch decomposer). */
117
- optionalLeavesByLogicalParent: Map<string, string[]>;
118
- requiresMappings: boolean;
119
- /** True when the only mappings needed are simple `@db.column` renames (no nesting/JSON). */
120
- onlyColumnRenames: boolean;
121
- toStorageFormatters?: Map<string, (value: unknown) => unknown>;
122
- fromStorageFormatters?: Map<string, (value: unknown) => unknown>;
123
- /** Leaf field descriptors indexed by physical column name (read path). */
124
- leafByPhysical: Map<string, TDbFieldMeta>;
125
- /** Leaf field descriptors indexed by logical path (write/patch/filter paths). */
126
- leafByLogical: Map<string, TDbFieldMeta>;
83
+ declare abstract class FieldMappingStrategy {
84
+ abstract reconstructFromRead(row: Record<string, unknown>, meta: TableMetadata): Record<string, unknown>;
85
+ abstract translateQuery(query: Uniquery, meta: TableMetadata): DbQuery;
127
86
  /**
128
- * Non-ignored field descriptors keyed by logical path, excluding navigation
129
- * relations and their descendants. Unlike `leafByLogical` (relational
130
- * adapters only) this is built for every adapter, so the core path guard
131
- * (`guardPaths`) can answer "does this path have physical storage here?"
132
- * on nested-object adapters too.
87
+ * The physical path of a logical field path — a `__`-joined column
88
+ * (relational) or a document path with `@db.column` renames applied.
133
89
  */
134
- descriptorByPath: Map<string, TDbFieldMeta>;
90
+ protected abstract physicalPath(logical: string, meta: TableMetadata): string;
135
91
  /**
136
- * Logical paths stored as a single JSON column (`storage === 'json'`,
137
- * non-ignored descriptors). Retained after build — unlike the build-time
138
- * `jsonFields` set — so the path guard can classify JSON descendants on
139
- * relational adapters. Empty on nested-object adapters (they keep native
140
- * dotted paths as descriptors).
92
+ * Whether {@link physicalPath} can differ from the logical path for this
93
+ * table; `false` lets the path translations hand their input back as-is.
141
94
  */
142
- jsonParents: ReadonlySet<string>;
143
- private _built;
144
- private _identifications?;
145
- private _collateMap;
146
- private _columnFromMap;
147
- constructor(nestedObjects: boolean);
148
- get isBuilt(): boolean;
95
+ protected renamesPaths(_meta: TableMetadata): boolean;
149
96
  /**
150
- * Runs the full metadata compilation pipeline. Called once by
151
- * `AtscriptDbReadable._ensureBuilt()` on first metadata access.
97
+ * Translates a grouped query to physical names: the filter and `$having`
98
+ * through {@link translateFilter}, and every field path in `$groupBy`,
99
+ * `$select` (plain and computed `$field`s) and `$sort` through
100
+ * {@link physicalPath}. Computed aliases pass through (a bucket alias never
101
+ * equals a field name).
152
102
  *
153
- * Pipeline steps:
154
- * 1. `adapter.onBeforeFlatten(type)` — adapter hook
155
- * 2. `flattenAnnotatedType()` — collect field tuples, detect nav fields eagerly
156
- * 3. Replay non-nav-descendant tuples through annotation scanning + adapter.onFieldScanned
157
- * 4. Classify fields and build path maps (skipped for nested-objects adapters)
158
- * 5. `adapter.getMetadataOverrides()` → `_applyOverrides()` (PK/unique/inject adjustments)
159
- * 6. Build field descriptors (TDbFieldMeta[])
160
- * 7. Build leaf field indexes (skipped for nested-objects adapters)
161
- * 8. Finalize indexes (resolve field names to physical)
162
- * 9. `adapter.onAfterFlatten()` — adapter hook (read-only bookkeeping)
163
- * 10. Build allPhysicalFields list
103
+ * `buckets` are the query's calendar buckets as the core's normalizer
104
+ * resolved them (`resolveCalendarBuckets` — `AtscriptDbReadable.aggregate`
105
+ * runs it before the guards); they reach adapters with `field` made
106
+ * physical and the source descriptor as `fd`.
164
107
  */
165
- build(type: TAtscriptAnnotatedType<TAtscriptTypeObject>, adapter: BaseDbAdapter, logger: TGenericLogger): void;
108
+ translateAggregateQuery(query: AggregateQuery, meta: TableMetadata, buckets: readonly ResolvedBucket[]): DbQuery;
109
+ /** Output aliases of the computed `$select` entries (aggregates and calendar buckets). */
110
+ private computedAliasSet;
166
111
  /**
167
- * Applies adapter-provided metadata overrides atomically.
168
- * Processing order: injectFields → removePrimaryKeys → addPrimaryKeys → addUniqueFields.
112
+ * `$select` with its field paths made physical: array-form names and
113
+ * computed `$field`s (`'*'` kept), or the keys of the object
114
+ * (inclusion / exclusion) form.
169
115
  */
170
- private _applyOverrides;
116
+ protected physicalSelect(select: NonNullable<UniqueryControls["$select"]>, meta: TableMetadata): NonNullable<UniqueryControls["$select"]>;
117
+ /** `$sort` with physical keys; computed `aliases` (grouped queries) pass through. */
118
+ protected physicalSort(sort: NonNullable<DbControls["$sort"]>, meta: TableMetadata, aliases?: ReadonlySet<string>): DbControls["$sort"];
171
119
  /**
172
- * Scans `@db.*` and `@meta.id` annotations on a field during flattening.
120
+ * Recursively walks a filter expression, applying `@db.column` key renames
121
+ * (document paths — {@link TableMetadata.documentPath}) and adapter-specific
122
+ * value formatting via `formatFilterValue`.
123
+ *
124
+ * The relational mapper overrides this to use `leafByLogical` for deeper
125
+ * key resolution (flattened nested paths).
173
126
  */
174
- private _scanGenericAnnotations;
127
+ translateFilter(filter: FilterExpr, meta: TableMetadata): FilterExpr;
128
+ abstract prepareForWrite(payload: Record<string, unknown>, meta: TableMetadata, adapter: BaseDbAdapter): Record<string, unknown>;
129
+ abstract translatePatchKeys(update: Record<string, unknown>, meta: TableMetadata): Record<string, unknown>;
175
130
  /**
176
- * Build-time diagnostics for `@db.encrypted` (§6 of the field-encryption
177
- * spec). Mirrors the compile-time AnnotationSpec validation so models built
178
- * from pre-compiled types still fail fast.
131
+ * Reverse-maps `@db.column` renames on a row read from storage.
132
+ * Renames physical keys back to logical names in-place.
179
133
  */
180
- private _validateEncryptedField;
181
- /** Build-time diagnostics for `@db.index.geo` (§3 of the geo-index spec). */
182
- private _validateGeoIndexField;
183
- private _addIndexField;
134
+ protected reverseColumnRenames(row: Record<string, unknown>, meta: TableMetadata): void;
184
135
  /**
185
- * Classifies each field as column, flattened, json, or parent-object.
186
- * Builds the bidirectional pathToPhysical / physicalToPath maps.
136
+ * Coerces field values from storage representation to JS types
137
+ * (booleans from 0/1, decimals from number to string).
187
138
  */
188
- private _classifyFields;
189
- /** Returns the `__`-separated parent prefix for a dot-separated path, or empty string for top-level paths. */
190
- private _flattenedPrefix;
191
- /** Nearest `@db.encrypted` ancestor of `path` (exclusive), or `undefined`. */
139
+ protected coerceFieldValues(row: Record<string, unknown>, meta: TableMetadata): Record<string, unknown>;
192
140
  /**
193
- * Indexes non-ignored descriptors by logical path and retains the JSON-parent
194
- * set. Navigation relations and their descendants are skipped even when the
195
- * adapter keeps them as descriptors (nested-object adapters do) — they are
196
- * loaded with `$with`, never addressed as columns of this table.
141
+ * Applies adapter-specific fromStorage formatting to a row read from the database.
142
+ * Converts storage representations back to JS values (e.g. Date → epoch ms).
197
143
  */
198
- private _buildGuardIndexes;
144
+ protected applyFromStorageFormatters(row: Record<string, unknown>, meta: TableMetadata): Record<string, unknown>;
199
145
  /**
200
- * Indexes `fieldDescriptors` into two lookup maps for unified
201
- * read/write field classification in the RelationalFieldMapper.
146
+ * Sets a value at a dot-notation path, creating intermediate objects as needed.
202
147
  */
203
- private _buildLeafIndexes;
148
+ protected setNestedValue(obj: Record<string, unknown>, dotPath: string, value: unknown): void;
204
149
  /**
205
- * Builds field descriptors, physical-name lookup, and value formatters.
206
- * Called once during build() — everything it needs
207
- * (flatMap, indexes, columnMap, etc.) is already populated.
150
+ * If all children of a flattened parent are null, collapse the parent to null.
208
151
  */
209
- private _buildFieldDescriptors;
152
+ protected reconstructNullParent(obj: Record<string, unknown>, parentPath: string, meta: TableMetadata): void;
210
153
  /**
211
- * Resolves `fkTargetField` for FK fields in field descriptors.
154
+ * Applies adapter-specific value formatting to a single filter value.
155
+ * Handles direct values, operator objects ({$gt: v}), and $in/$nin arrays.
212
156
  */
213
- private _resolveFkTargetFields;
214
- private _finalizeIndexes;
157
+ protected formatFilterValue(physicalName: string, value: unknown, meta: TableMetadata): unknown;
215
158
  /**
216
- * Captures legitimate row-identifier shapes from the metadata: primary key
217
- * (when present) followed by every unique index. Must run BEFORE
218
- * `_finalizeIndexes` rewrites `index.fields[i].name` from logical to
219
- * physical, so the field lists stay logical.
220
- *
221
- * Sources:
222
- * - `this.primaryKeys` — composite-aware PK (one identification covering
223
- * the PK columns).
224
- * - `this.indexes` — user-declared `@db.index.unique` (single or compound).
225
- * - `this.uniqueProps` — single-field uniques contributed by adapter
226
- * overrides (`addUniqueFields`); these aren't reflected in
227
- * `this.indexes` but still legitimate addressing identifications.
228
- * Deduped against any single-field unique index already captured.
159
+ * Applies adapter-specific value formatting to prepared (physical-named) data.
229
160
  */
230
- private _buildIdentifications;
231
- /** Legitimate row-identifier shapes — primary key first, then each unique index. */
232
- getIdentifications(): readonly TIdentification[];
233
- private _resolvePreferredId;
234
- }
235
- //#endregion
236
- //#region src/types.d.ts
237
- /** Controls with resolved projection. Used in the adapter interface. */
238
- interface DbControls extends Omit<UniqueryControls, "$select"> {
239
- $select?: UniquSelect;
240
- }
241
- /** Query object with resolved projection. Passed to adapter methods. */
242
- interface DbQuery {
243
- filter: FilterExpr;
244
- controls: DbControls;
245
- /** Pre-computed query insights (field → operators). Adapters may use this to apply query-time behaviour (e.g. collation). */
246
- insights?: UniqueryInsights;
247
- }
248
- /** Describes an available search index exposed by a database adapter. */
249
- interface TSearchIndexInfo {
250
- /** Index name. Empty string or 'DEFAULT' for the default index. */
251
- name: string;
252
- /** Human-readable label for UI display. */
253
- description?: string;
254
- /** Index type: text search or vector similarity search. */
255
- type?: "text" | "vector";
256
- }
257
- /** Relation summary in a meta response. */
258
- interface TRelationInfo {
259
- name: string;
260
- direction: "to" | "from" | "via";
261
- isArray: boolean;
262
- }
263
- /** Per-field capability flags in a meta response. */
264
- interface TFieldMeta {
265
- sortable: boolean;
266
- filterable: boolean;
267
- /** Present (true) when the field is `@db.encrypted` — stored as ciphertext at rest. */
268
- encrypted?: boolean;
269
- /** Present (true) when the field carries a `@db.index.geo` geospatial index. */
270
- geo?: boolean;
161
+ protected formatWriteValues(data: Record<string, unknown>, meta: TableMetadata): Record<string, unknown>;
271
162
  /**
272
- * Present (true) when the field is write-only over HTTP (`@db.writeOnly` or
273
- * stamped by a permission overlay): settable in write payloads, never
274
- * present in read responses. UIs render it as a set-only input.
163
+ * Prepares primary key values and strips ignored fields.
164
+ * Shared pre-processing for both document and relational write paths.
275
165
  */
276
- writeOnly?: boolean;
166
+ protected prepareCommon(data: Record<string, unknown>, meta: TableMetadata, adapter: BaseDbAdapter): void;
167
+ }
168
+ /**
169
+ * Field mapper for document-oriented adapters (e.g. MongoDB).
170
+ * Nested objects are preserved as-is. Only applies column renames and
171
+ * value coercion.
172
+ */
173
+ declare class DocumentFieldMapper extends FieldMappingStrategy {
174
+ reconstructFromRead(row: Record<string, unknown>, meta: TableMetadata): Record<string, unknown>;
277
175
  /**
278
- * Present (true) when the field is index-backed (explicit `@db.index*`,
279
- * primary key or unique field). Advisory only — a hint for UIs that want to
280
- * steer users toward cheap sort keys; it never affects whether a `$sort`
281
- * is accepted (`sortable` does). Since 0.1.128.
176
+ * Every field-path position goes through `@db.column` renames
177
+ * ({@link TableMetadata.documentPath}): filter keys, `$select` fields
178
+ * (array, inclusion and exclusion forms) and `$sort` keys.
282
179
  */
283
- indexed?: boolean;
180
+ translateQuery(query: Uniquery, meta: TableMetadata): DbQuery;
181
+ /** A document path with `@db.column` renames ({@link TableMetadata.documentPath}). */
182
+ protected physicalPath(logical: string, meta: TableMetadata): string;
183
+ protected renamesPaths(meta: TableMetadata): boolean;
184
+ prepareForWrite(payload: Record<string, unknown>, meta: TableMetadata, adapter: BaseDbAdapter): Record<string, unknown>;
185
+ translatePatchKeys(update: Record<string, unknown>, meta: TableMetadata): Record<string, unknown>;
284
186
  }
285
- /** Built-in CRUD operation names; map 1:1 to public method names. */
286
- type TCrudOp = "query" | "pages" | "one" | "geo" | "insert" | "update" | "replace" | "remove";
187
+ //#endregion
188
+ //#region src/encryption.d.ts
287
189
  /**
288
- * CRUD permissions advertised in `/meta`. Key absent → operation is denied or
289
- * not exposed. Key present → operation is allowed; the `string[]` value is the
290
- * accepted UniQuery control whitelist for read ops (`[]` for write ops, which
291
- * take no controls — presence still signals "allowed").
190
+ * Configuration for field-level encryption at rest (`@db.encrypted`).
191
+ * Passed to `DbSpace` via the options bag.
292
192
  */
293
- type TCrudPermissions = Partial<Record<TCrudOp, string[]>>;
294
- /** Response payload for `GET /meta`. */
295
- interface TMetaResponse {
296
- searchable: boolean;
297
- vectorSearchable: boolean;
298
- /** Whether the adapter supports `geoSearch()` AND the table declares a geo index. */
299
- geoSearchable?: boolean;
300
- searchIndexes: TSearchIndexInfo[];
301
- primaryKeys: string[];
302
- preferredId: string[];
303
- relations: TRelationInfo[];
304
- fields: Record<string, TFieldMeta>;
305
- type: TSerializedAnnotatedType;
306
- actions: TDbActionInfo[];
307
- crud: TCrudPermissions;
193
+ interface TDbEncryptionOptions {
194
+ /** Key used for all new writes. */
195
+ defaultKeyId: string;
308
196
  /**
309
- * Physical column name annotated with `@db.column.version`, when the table
310
- * opts into optimistic concurrency control (OCC). Absent for tables without
311
- * the annotation, i.e. last-write-wins (default) behavior.
197
+ * Key registry: keyId → 32-byte key (Buffer, or base64/hex/utf8 string).
198
+ * Decryption looks keys up by the keyId recorded in each value's envelope,
199
+ * so old keys stay in the registry for as long as data encrypted with them exists.
312
200
  */
313
- versionColumn?: string;
201
+ keys?: Record<string, string | Buffer>;
202
+ /**
203
+ * Alternative/supplement to `keys`: async resolver (KMS, Vault, env indirection).
204
+ * Called once per keyId, result cached for the process lifetime.
205
+ */
206
+ resolveKey?: (keyId: string) => Promise<string | Buffer> | string | Buffer;
207
+ /**
208
+ * What to do when a stored value is NOT a valid envelope (pre-existing
209
+ * plaintext rows, e.g. when @db.encrypted is added to a live column).
210
+ * - 'error' (default): fail the read with DbError("ENC_NOT_ENCRYPTED")
211
+ * - 'passthrough': return the raw value as-is (migration window mode);
212
+ * the value gets encrypted on its next write.
213
+ */
214
+ onUnencrypted?: "error" | "passthrough";
215
+ }
216
+ /** Decryption context — carried into error messages (never the key itself). */
217
+ interface TDecryptContext {
218
+ table?: string;
219
+ field?: string;
314
220
  }
315
- /** Where the action applies on the UI. */
316
- type TDbActionLevel = "table" | "row" | "rows";
317
221
  /**
318
- * Semantic intent the UI maps to its own visual language (color, prominence).
222
+ * AES-256-GCM envelope encryption service for `@db.encrypted` fields.
319
223
  *
320
- * Suggested visual prominence (most → least): `negative` > `warning` > `primary`
321
- * > `positive` > `secondary`. Use `negative` for destructive ops (delete, purge),
322
- * `warning` for risky-but-non-destructive ops (retry payment, force recompute,
323
- * reset state), `primary` for the headline action, `positive` for benign
324
- * confirmations (approve, publish), `secondary` for everything else.
224
+ * Owned by `DbSpace` and shared across all tables in the space. Values are
225
+ * `JSON.stringify`'d before encryption (type-exact round-trips) and stored as
226
+ * a single ASCII envelope string: `aes1$<keyId>$<iv>$<tag>$<ciphertext>`.
227
+ *
228
+ * Key material is validated eagerly at construction (`ENC_KEY_INVALID`);
229
+ * `resolveKey` lookups are cached per keyId for the process lifetime.
325
230
  */
326
- type TDbActionIntent = "positive" | "negative" | "warning" | "primary" | "secondary";
327
- /** How the UI client should handle the action when invoked. */
328
- type TDbActionProcessor = "backend" | "navigate" | "custom";
231
+ declare class DbEncryption {
232
+ readonly defaultKeyId: string;
233
+ readonly onUnencrypted: "error" | "passthrough";
234
+ private readonly _keys;
235
+ private readonly _resolveKey?;
236
+ private readonly _resolved;
237
+ constructor(options: TDbEncryptionOptions);
238
+ /** True when `value` looks like an encryption envelope produced by this service. */
239
+ isEnvelope(value: unknown): value is string;
240
+ /** Encrypts a JSON-serializable value into an envelope string using the default key. */
241
+ encrypt(value: unknown): Promise<string>;
242
+ /** Decrypts an envelope string back into its plaintext value. */
243
+ decrypt(envelope: string, ctx?: TDecryptContext): Promise<unknown>;
244
+ private _decryptFailed;
245
+ private _getKey;
246
+ }
247
+ //#endregion
248
+ //#region src/table/db-readable.d.ts
329
249
  /**
330
- * Single action descriptor in the `/meta` envelope. Flat shape — `processor`
331
- * is a string discriminator; `value` is its sibling and is always populated.
250
+ * Extracts nav prop names from a query's `$with` array.
251
+ * Returns `never` when `$with` is absent → all nav props stripped from response.
252
+ */
253
+ type ExtractWith<Q> = Q extends {
254
+ controls: {
255
+ $with: Array<{
256
+ name: infer N extends string;
257
+ }>;
258
+ };
259
+ } ? N : never;
260
+ /**
261
+ * Computes the response type for a query:
262
+ * - Strips all nav props from the base DataType
263
+ * - Adds back only the nav props requested via `$with`
332
264
  *
333
- * - `processor: 'backend'` — UI POSTs to `value` (full HTTP path).
334
- * - `processor: 'navigate'` — UI routes to `value` (URL template; `$1` is the row PK).
335
- * - `processor: 'custom'` — UI dispatches `value` as an event name (defaults to action `name`).
265
+ * When no `$with` is provided, result is `Omit<DataType, keyof NavType>`.
266
+ * When `$with: [{ name: 'author' }]`, result includes `author` from DataType.
267
+ * When the query type is not a literal (e.g. a variable typed as `Uniquery`),
268
+ * falls back to `DataType` (all nav props optional, as declared).
336
269
  */
337
- interface TDbActionInfo {
338
- name: string;
339
- label: string;
340
- level: TDbActionLevel;
341
- processor: TDbActionProcessor;
342
- value: string;
343
- icon?: string;
344
- intent?: TDbActionIntent;
345
- description?: string;
346
- order?: number;
347
- default?: boolean;
270
+ type DbResponse<Data, Nav, Q> = [keyof Nav] extends [never] ? Data : string extends keyof Nav ? Data : Omit<Data, keyof Nav & string> & Pick<Data, ExtractWith<Q> & keyof Data & string>;
271
+ /**
272
+ * Resolves the design type from an annotated type.
273
+ * Encapsulates the `kind === ''` check and fallback logic that
274
+ * otherwise trips up every adapter author.
275
+ *
276
+ * For union types (e.g., from flattened `{...} | {...}` objects):
277
+ * - If all members resolve to the same type → returns that type (strong type)
278
+ * - If members disagree → returns `'union'` (out of scope for type management)
279
+ */
280
+ declare function resolveDesignType(fieldType: TAtscriptAnnotatedType): string;
281
+ /**
282
+ * Shared read-only database abstraction driven by Atscript annotations.
283
+ *
284
+ * Contains all field metadata computation, read operations, query translation,
285
+ * relation loading, and result reconstruction. Extended by both
286
+ * {@link AtscriptDbTable} (adds write operations) and {@link AtscriptDbView}
287
+ * (adds view plan/DDL).
288
+ */
289
+ declare class AtscriptDbReadable<T extends TAtscriptAnnotatedType = TAtscriptAnnotatedType, DataType = TAtscriptDataType<T>, _FlatType = NullableOptional<FlatOf<T>>, A extends BaseDbAdapter = BaseDbAdapter, IdType = PrimaryKeyOf<T>, OwnProps = NullableOptional<OwnPropsOf<T>>, NavType extends Record<string, unknown> = NavPropsOf<T>> {
290
+ protected readonly _type: T;
291
+ protected readonly adapter: A;
292
+ protected readonly logger: TGenericLogger;
293
+ protected readonly _tableResolver?: TTableResolver | undefined;
294
+ /** Resolved table/collection/view name. */
295
+ readonly tableName: string;
296
+ /** Database schema/namespace from `@db.schema` (if set). */
297
+ readonly schema: string | undefined;
298
+ /** Sync method from `@db.sync.method` ('drop' | 'recreate' | undefined). */
299
+ protected readonly _syncMethod: "drop" | "recreate" | undefined;
300
+ /** Previous table/view name from `@db.table.renamed` or `@db.view.renamed`. */
301
+ readonly renamedFrom: string | undefined;
302
+ /** Computed metadata for this table/view. Built lazily on first access. */
303
+ protected readonly _meta: TableMetadata;
304
+ /** Strategy for mapping between logical field shapes and physical storage. */
305
+ protected readonly _fieldMapper: FieldMappingStrategy;
306
+ protected _writeTableResolver?: TWriteTableResolver;
307
+ /** Encryption service for `@db.encrypted` fields — set by `DbSpace` from its options. */
308
+ protected _encryption?: DbEncryption;
309
+ private _metaIdPhysical;
310
+ constructor(_type: T, adapter: A, logger?: TGenericLogger, _tableResolver?: TTableResolver | undefined);
348
311
  /**
349
- * Confirmation prompt copy. String form is shown verbatim. Tuple form is
350
- * `[singular, plural]`: the UI picks `[0]` when the action will execute
351
- * against a single PK (always for `'row'`-level; for `'rows'`-level when the
352
- * current selection has exactly one PK) and `[1]` otherwise.
353
- *
354
- * Placeholder substitution is UI-resolved, not server-parsed. Conventional
355
- * placeholders: `$1` for the single PK (singular form) and `$N` for the
356
- * count (plural form), e.g. `['Delete order $1?', 'Delete $N orders?']`.
312
+ * Sets the encryption service used for `@db.encrypted` fields.
313
+ * Called by `DbSpace` after table/view creation when the space was
314
+ * configured with an `encryption` options block.
357
315
  */
358
- promptText?: string | [string, string];
316
+ setEncryption(encryption: DbEncryption | undefined): void;
317
+ /** Ensures metadata is built. Called before any metadata access. */
318
+ protected _ensureBuilt(): void;
359
319
  /**
360
- * Single-character keyboard shortcut hint. The server stores this verbatim
361
- * — choice of modifier prefix (Alt+, Ctrl+, bare key) and activation scope
362
- * (e.g. only when an actions dropdown is open) are UI/UX concerns. Conflict
363
- * resolution between actions sharing the same key is also up to the UI;
364
- * the server does no dedup.
320
+ * Built table metadata. Triggers a lazy build on first access — safe to call
321
+ * from peer tables that need this one's relations / nav fields before any
322
+ * operation has run against it directly.
365
323
  */
366
- shortcut?: string;
324
+ getMetadata(): TableMetadata;
325
+ protected _ensureSearchable(): void;
326
+ /** Engine-agnostic query-time guards (encrypted-field refs, $geoWithin shape). */
327
+ protected _guardQuery(query: Uniquery | undefined): void;
328
+ private _encryptedPathsCache?;
329
+ /** Pre-split `encryptedFields` paths — computed once, reused on every read/write. */
330
+ protected get _encryptedPaths(): Array<{
331
+ path: string;
332
+ segments: string[];
333
+ leaf: string;
334
+ }>;
367
335
  /**
368
- * Stringified gate predicate (`fn.toString()`). Present only for `'row'`
369
- * and `'rows'` level actions whose decorator declared a `disabled` function.
370
- * The function is the batch shape `(rows: TRow[]) => boolean[]` (sync). The
371
- * UI evaluates against a level-specific scope to grey-out / hide the
372
- * button. The server has already enforced this predicate before the
373
- * action's handler ran — the server is authoritative; this field is purely
374
- * a UI hint.
336
+ * Walks all but the last of `segments` down from `root`, returning the
337
+ * object holding the leaf — or `undefined` when the path is unreachable
338
+ * (a missing, non-object, or array step). With `cloneParents`, every
339
+ * traversed object is shallow-cloned and re-linked so caller-shared
340
+ * nested objects are never mutated.
375
341
  */
376
- disabled?: string;
342
+ protected _walkToLeafParent(root: Record<string, unknown>, segments: string[], cloneParents: boolean): Record<string, unknown> | undefined;
377
343
  /**
378
- * Name of the `.as` interface the action's `@InputForm()` parameter expects
379
- * (the compiled class's `.name`). Present only when the handler declares an
380
- * `@InputForm(FormType)` parameter. Clients fetch the serialized schema via
381
- * `GET /meta/form/:name` on the same controller and render a form to
382
- * collect the `input` field of the action's request envelope.
344
+ * Decrypts `@db.encrypted` fields on reconstructed rows (in place).
345
+ * Non-envelope stored values follow the configured `onUnencrypted` policy.
383
346
  */
384
- inputForm?: string;
385
- }
386
- interface TDbInsertResult {
387
- insertedId: unknown;
388
- }
389
- interface TDbInsertManyResult {
390
- insertedCount: number;
391
- insertedIds: unknown[];
392
- }
393
- interface TDbUpdateResult {
394
- matchedCount: number;
395
- modifiedCount: number;
396
- }
397
- interface TDbDeleteResult {
398
- deletedCount: number;
399
- }
400
- type TDbIndexType = "plain" | "unique" | "fulltext" | "geo";
401
- interface TDbIndexField {
402
- name: string;
403
- sort: "asc" | "desc";
404
- weight?: number;
347
+ protected _decryptRows(rows: Array<Record<string, unknown>>): Promise<void>;
348
+ /** Whether this readable is a view (overridden in AtscriptDbView). */
349
+ get isView(): boolean;
350
+ /** Returns the underlying adapter with its concrete type preserved. */
351
+ getAdapter(): A;
352
+ /** The raw annotated type. */
353
+ get type(): TAtscriptAnnotatedType<TAtscriptTypeObject>;
354
+ /** Lazily-built flat map of all fields (dot-notation paths → annotated types). */
355
+ get flatMap(): Map<string, TAtscriptAnnotatedType>;
356
+ /** All computed indexes from `@db.index.*` annotations. */
357
+ get indexes(): Map<string, TDbIndex>;
358
+ /** Primary key field names from `@meta.id`. */
359
+ get primaryKeys(): readonly string[];
360
+ /** Preferred row identifier field names. Defaults to primary keys. */
361
+ get preferredId(): readonly string[];
362
+ /** Legitimate row-identifier shapes (primary key + every unique index). */
363
+ get identifications(): readonly TIdentification[];
405
364
  /**
406
- * Whether the indexed field is optional (declared `field?:` in the model).
407
- * Resolved during index finalization. Adapters use this to make a unique
408
- * index "present-only" so multiple value-less rows are tolerated — matching
409
- * SQL's `NULLS DISTINCT` default. SQL adapters get this for free and ignore
410
- * the flag; MongoDB needs it to emit a partial unique index.
411
- */
412
- optional?: boolean;
365
+ * Physical column name of the single `@meta.id` field, or `null` when the
366
+ * schema has zero or multiple `@meta.id` fields. Used by adapters to return
367
+ * the user's logical ID instead of the DB-generated one on insert.
368
+ *
369
+ * @internal Adapter-facing surface; not part of the consumer API.
370
+ */
371
+ get metaIdPhysical(): string | null;
413
372
  /**
414
- * Resolved design type of the field ('string', 'number', 'boolean', …).
415
- * Carried alongside {@link optional} so adapters can derive a type-correct
416
- * present-only filter (e.g. Mongo's `partialFilterExpression`) without
417
- * re-resolving the field type. Undefined when the field cannot be resolved.
373
+ * Physical column name of the field annotated with `@db.column.version`, or
374
+ * `undefined` when the table has no version column. Used by adapters and the
375
+ * REST integration to drive optimistic concurrency control (OCC).
418
376
  */
419
- designType?: string;
420
- }
421
- interface TDbIndex {
422
- /** Unique key used for identity/diffing (e.g., "atscript__plain__email") */
423
- key: string;
424
- /** Human-readable index name. */
425
- name: string;
426
- /** Index type. */
427
- type: TDbIndexType;
428
- /** Ordered list of fields in the index. */
429
- fields: TDbIndexField[];
430
- }
431
- type TDbDefaultFn = "increment" | "uuid" | "now";
432
- type TDbCollation = "binary" | "nocase" | "unicode";
433
- type TDbDefaultValue = {
434
- kind: "value";
435
- value: string;
436
- } | {
437
- kind: "fn";
438
- fn: TDbDefaultFn;
439
- start?: number;
440
- };
441
- interface TIdDescriptor {
442
- /** Field names that form the primary key. */
443
- fields: string[];
444
- /** Whether this is a composite key (multiple fields). */
445
- isComposite: boolean;
446
- }
447
- /** A legitimate row-identifier shape: primary key or a unique index. */
448
- interface TIdentification {
449
- /** Logical (path) field names that form this identifier. */
450
- fields: readonly string[];
451
- /** `'primaryKey'` for the PK; the unique-index name otherwise. */
452
- source: string;
453
- }
454
- type TDbStorageType = "column" | "flattened" | "json";
455
- interface TDbFieldMeta {
456
- /** The dot-notation path to this field (logical name). */
457
- path: string;
458
- /** The annotated type for this field. */
459
- type: TAtscriptAnnotatedType;
460
- /** Physical column/field name (from @db.column, __-separated for flattened, or same as path). */
461
- physicalName: string;
462
- /** Resolved design type: 'string', 'number', 'boolean', 'object', 'json', etc. */
463
- designType: string;
464
- /** Whether the field is optional. */
465
- optional: boolean;
466
- /** Whether this field is part of the primary key (@meta.id). */
467
- isPrimaryKey: boolean;
468
- /** Whether this field is excluded from the DB (@db.ignore). */
469
- ignored: boolean;
470
- /** Default value from @db.default.* */
471
- defaultValue?: TDbDefaultValue;
377
+ get versionColumn(): string | undefined;
378
+ /** Dimension fields from `@db.column.dimension`. */
379
+ get dimensions(): readonly string[];
380
+ /** Measure fields from `@db.column.measure`. */
381
+ get measures(): readonly string[];
382
+ /** Sync method for structural changes: 'drop' (lossy), 'recreate' (lossless), or undefined (manual). */
383
+ get syncMethod(): "drop" | "recreate" | undefined;
384
+ /** Logical → physical column name mapping from `@db.column`. */
385
+ get columnMap(): ReadonlyMap<string, string>;
386
+ /** Default values from `@db.default.*`. */
387
+ get defaults(): ReadonlyMap<string, TDbDefaultValue>;
388
+ /** Fields excluded from DB via `@db.ignore`. */
389
+ get ignoredFields(): ReadonlySet<string>;
390
+ /** Navigational fields (`@db.rel.to` / `@db.rel.from`) — not stored as columns. */
391
+ get navFields(): ReadonlySet<string>;
392
+ /** Physical field names used to invert exclude-mode `$select` into a SELECT list. */
393
+ get allPhysicalFields(): readonly string[];
394
+ /** Single-field unique index properties. */
395
+ get uniqueProps(): ReadonlySet<string>;
396
+ /** Foreign key constraints from `@db.rel.FK` annotations. */
397
+ get foreignKeys(): ReadonlyMap<string, TDbForeignKey>;
398
+ /** Navigational relation metadata from `@db.rel.to` / `@db.rel.from`. */
399
+ get relations(): ReadonlyMap<string, TDbRelation>;
400
+ /** The underlying database adapter instance. */
401
+ get dbAdapter(): A;
472
402
  /**
473
- * How this field is stored in the database.
474
- * - 'column': a standard scalar column (default for primitives)
475
- * - 'flattened': a leaf scalar from a flattened nested object
476
- * - 'json': stored as a single JSON column (arrays, @db.json fields)
403
+ * Enables or disables verbose (debug-level) DB call logging for this table/view.
404
+ * When disabled (default), no log strings are constructed — zero overhead.
477
405
  */
478
- storage: TDbStorageType;
406
+ setVerbose(enabled: boolean): void;
407
+ /** Precomputed logical dot-path → physical column name map. */
408
+ get pathToPhysical(): ReadonlyMap<string, string>;
409
+ /** Precomputed physical column name → logical dot-path map (inverse). */
410
+ get physicalToPath(): ReadonlyMap<string, string>;
411
+ /** Descriptor for the primary ID field(s). */
412
+ getIdDescriptor(): TIdDescriptor;
479
413
  /**
480
- * For flattened fields: the dot-notation path (same as `path`).
481
- * E.g., for physicalName 'contact__email', this is 'contact.email'.
482
- * Undefined for non-flattened fields.
414
+ * Pre-computed field metadata for adapter use.
483
415
  */
484
- flattenedFrom?: string;
485
- /** Old physical column name from @db.column.renamed (for rename migration). */
486
- renamedFrom?: string;
487
- /** Collation from @db.column.collate (e.g. 'nocase', 'binary', 'unicode'). */
488
- collate?: TDbCollation;
416
+ get fieldDescriptors(): readonly TDbFieldMeta[];
489
417
  /**
490
- * Whether this field is index-backed: it participates in an explicit index
491
- * (@db.index.plain, @db.index.unique, @db.index.fulltext) OR is a primary key
492
- * or unique field (which are always index-backed — Mongo `_id`, SQL PK/unique
493
- * constraints — even without an explicit `@db.index*`).
418
+ * Resolves whether `path` references a real field — directly via `flatMap`
419
+ * or transitively through a nav relation by recursing into the target
420
+ * table. Defense-in-depth for query-path validation: `flattenAnnotatedType`
421
+ * still truncates real self-referential cycles, so paths like
422
+ * `parent.parent.name` on a self-ref schema would miss `flatMap.has` but
423
+ * remain valid field references on the target.
424
+ *
425
+ * Cycle-safe via a visited set keyed on `<tableName>:<navField>`.
494
426
  */
495
- isIndexed?: boolean;
496
- /** Literal currency code from `@db.amount.currency 'EUR'`. */
497
- currencyCode?: string;
498
- /** Sibling field path from `@db.amount.currency.ref 'fieldName'`. */
499
- currencyRefField?: string;
500
- /** Literal unit-of-measure from `@db.unit 'kg'`. */
501
- unitCode?: string;
502
- /** Sibling field path from `@db.unit.ref 'fieldName'`. */
503
- unitRefField?: string;
427
+ isValidFieldPath(path: string, _visited?: Set<string>): boolean;
504
428
  /**
505
- * For FK fields: the resolved field metadata of the referenced (target) PK column.
506
- * Adapters use this in `typeMapper` to produce matching DB types for FK columns
507
- * (e.g., `typeMapper(field.fkTargetField)` to inherit the target PK's DB type).
508
- * Undefined for non-FK fields or when the target cannot be resolved.
429
+ * Creates a new validator with custom options.
509
430
  */
510
- fkTargetField?: TDbFieldMeta;
431
+ createValidator(opts?: Partial<TValidatorOptions>): Validator<T, DataType>;
511
432
  /**
512
- * `@db.encrypted` — the value is AES-256-GCM encrypted by the core layer
513
- * before reaching the adapter. Adapters must map the column to an unbounded
514
- * text type and veto filtering/sorting (`canFilterField`/`canSortField`).
515
- * The descriptor's `designType` is forced to `'string'` (ciphertext envelope);
516
- * the declared type stays available via `type` for validation.
433
+ * Finds a single record matching the query.
434
+ * The return type automatically excludes nav props unless they are
435
+ * explicitly requested via `$with`.
517
436
  */
518
- encrypted?: boolean;
437
+ findOne<Q extends Uniquery<OwnProps, NavType>>(query: Q): Promise<DbResponse<DataType, NavType, Q> | null>;
519
438
  /**
520
- * The field's declared type is the `db.geoPoint` primitive (`[lng, lat]` tuple).
521
- * Adapters map this to their native geo storage (e.g. MongoDB GeoJSON Point).
439
+ * Finds all records matching the query.
440
+ * The return type automatically excludes nav props unless they are
441
+ * explicitly requested via `$with`.
522
442
  */
523
- isGeoPoint?: boolean;
524
- }
525
- interface TValueFormatterPair {
526
- /** Converts a JS value to storage representation (write + filter paths). */
527
- toStorage: (value: unknown) => unknown;
528
- /** Converts a storage value back to JS representation (read path). */
529
- fromStorage: (value: unknown) => unknown;
530
- }
531
- type TDbReferentialAction = "cascade" | "restrict" | "noAction" | "setNull" | "setDefault";
532
- interface TDbForeignKey {
533
- /** FK field names on this table (local columns). */
534
- fields: string[];
535
- /** Target table name (from the chain ref's type @db.table annotation). */
536
- targetTable: string;
537
- /** Target field names on the referenced table. */
538
- targetFields: string[];
539
- /** Lazy reference to the target annotated type (for on-demand table resolution). */
540
- targetTypeRef?: () => TAtscriptAnnotatedType;
541
- /** Alias grouping FK fields (if any). */
542
- alias?: string;
543
- /** Referential action on delete. */
544
- onDelete?: TDbReferentialAction;
545
- /** Referential action on update. */
546
- onUpdate?: TDbReferentialAction;
547
- }
548
- /** Describes an existing column in the database (from introspection). */
549
- interface TExistingColumn {
550
- name: string;
551
- type: string;
552
- notnull: boolean;
553
- pk: boolean;
554
- /** Serialized default value (e.g., "'active'", "NULL"). */
555
- dflt_value?: string;
556
- }
557
- /** Result of comparing desired schema against existing database columns. */
558
- interface TColumnDiff {
559
- added: TDbFieldMeta[];
560
- removed: TExistingColumn[];
561
- renamed: Array<{
562
- field: TDbFieldMeta;
563
- oldName: string;
564
- }>;
565
- typeChanged: Array<{
566
- field: TDbFieldMeta;
567
- existingType: string;
568
- }>;
569
- nullableChanged: Array<{
570
- field: TDbFieldMeta;
571
- wasNullable: boolean;
572
- }>;
573
- defaultChanged: Array<{
574
- field: TDbFieldMeta;
575
- oldDefault?: string;
576
- newDefault?: string;
577
- }>;
578
- conflicts: Array<{
579
- field: TDbFieldMeta;
580
- oldName: string;
581
- conflictsWith: string;
443
+ findMany<Q extends Uniquery<OwnProps, NavType>>(query: Q): Promise<Array<DbResponse<DataType, NavType, Q>>>;
444
+ /**
445
+ * Counts records matching the query.
446
+ */
447
+ count(query?: Uniquery<OwnProps, NavType>): Promise<number>;
448
+ /**
449
+ * Finds records and total count in a single logical call.
450
+ */
451
+ findManyWithCount<Q extends Uniquery<OwnProps, NavType>>(query: Q): Promise<{
452
+ data: Array<DbResponse<DataType, NavType, Q>>;
453
+ count: number;
582
454
  }>;
583
455
  /**
584
- * The primary-key FIELD SET differs between the live table and the model
585
- * (set semantics — a composite-key reorder is not a change, consistent with
586
- * the schema hash). Column names are physical; a renamed PK column is
587
- * compared under its new name. Only reported when the table exists.
588
- * @since 0.1.128
456
+ * Executes an aggregate query with GROUP BY and aggregate functions.
457
+ *
458
+ * Validates:
459
+ * - `$select` computed entries and calendar buckets (the shared normalizer,
460
+ * `resolveCalendarBuckets`: shapes, unit, zone, alias, grouping)
461
+ * - Plain fields in $select are a subset of $groupBy
462
+ * - When dimensions/measures are defined (strict mode): $groupBy fields
463
+ * must be dimensions, aggregate $field values must be measures (or '*')
464
+ * - the path guard (a bucket source must pass `bucketSourceVerdict` —
465
+ * timestamp type, no JSON ancestor, a dimension in strict mode, an
466
+ * adapter with calendar buckets) and the adapter's calendar-bucket units
467
+ * (`BUCKET_NOT_SUPPORTED`)
468
+ *
469
+ * Translates field names, delegates to adapter.aggregate(),
470
+ * then reverse-maps and applies fromStorage formatters on results.
589
471
  */
590
- primaryKeyChanged?: TPrimaryKeyChange;
591
- }
592
- /** Old and new primary-key column sets of a table whose key definition moved. */
593
- interface TPrimaryKeyChange {
594
- /** Physical PK columns currently in the database (after rename mapping). */
595
- from: string[];
596
- /** Physical PK columns the model declares. */
597
- to: string[];
598
- }
599
- /**
600
- * A live foreign-key constraint as introspected from the database
601
- * (outbound: declared on the table that owns it).
602
- */
603
- interface TExistingForeignKey {
604
- /** Local (referencing) columns, in constraint order. */
605
- fields: string[];
606
- /** Referenced table name. */
607
- targetTable: string;
608
- /** Referenced columns, in constraint order. */
609
- targetFields: string[];
610
- }
611
- /**
612
- * A live foreign key that REFERENCES a given table (inbound edge), as returned
613
- * by `BaseDbAdapter.getReferencingForeignKeys(tableName)`.
614
- */
615
- interface TReferencingForeignKey {
616
- /** The referencing (child) table. */
617
- table: string;
618
- /** Referencing columns on `table`, in constraint order. */
619
- fields: string[];
620
- /** Referenced columns on the queried table, in constraint order. */
621
- targetFields: string[];
622
- }
623
- /** Kind of a physical database object, as returned by `BaseDbAdapter.getObjectKind`. */
624
- type TDbObjectKind = "table" | "view" | "materialized";
625
- /** Options accepted by `BaseDbAdapter.ensureTable`. */
626
- interface TEnsureTableOptions {
472
+ aggregate(query: AggregateQuery): Promise<Array<Record<string, unknown>>>;
473
+ /** Whether the underlying adapter supports text search. */
474
+ isSearchable(): boolean;
475
+ /** Whether the adapter can filter on a given field (proxies adapter capability). */
476
+ canFilterField(fd: TDbFieldMeta): boolean;
477
+ /** Calendar-bucket units the adapter can group by (proxies adapter capability; empty = none). */
478
+ calendarBucketUnits(): ReadonlySet<BucketUnit>;
479
+ /** Whether the adapter can sort by a given field (proxies adapter capability). */
480
+ canSortField(fd: TDbFieldMeta): boolean;
481
+ /** Returns available search indexes from the adapter. */
482
+ getSearchIndexes(): TSearchIndexInfo[];
627
483
  /**
628
- * Table names whose inline FOREIGN KEY constraints must be omitted from
629
- * CREATE TABLE — the constraints are added afterwards by `syncForeignKeys()`.
630
- * Schema sync passes the members of a foreign-key cycle so they can be
631
- * created in any order.
484
+ * Full-text search with query translation and result reconstruction.
632
485
  */
633
- deferForeignKeysTo?: ReadonlySet<string>;
634
- }
635
- /** Result of applying column diff to the database. */
636
- interface TSyncColumnResult {
637
- added: string[];
638
- renamed: string[];
639
- }
640
- /** A single table-level option in unified key-value format. */
641
- interface TExistingTableOption {
642
- key: string;
643
- value: string;
644
- }
645
- /** Result of comparing desired table options against existing ones. */
646
- interface TTableOptionDiff {
647
- changed: Array<{
648
- key: string;
649
- oldValue: string;
650
- newValue: string; /** Whether applying this change requires dropping and recreating the table. */
651
- destructive: boolean;
486
+ search<Q extends Uniquery<OwnProps, NavType>>(text: string, query: Q, indexName?: string): Promise<Array<DbResponse<DataType, NavType, Q>>>;
487
+ /**
488
+ * Full-text search with count for paginated search results.
489
+ */
490
+ searchWithCount<Q extends Uniquery<OwnProps, NavType>>(text: string, query: Q, indexName?: string): Promise<{
491
+ data: Array<DbResponse<DataType, NavType, Q>>;
492
+ count: number;
652
493
  }>;
653
- }
654
- /**
655
- * Adapter-provided metadata adjustments applied atomically during the
656
- * build pipeline, before field descriptors are built.
657
- *
658
- * Replaces the old pattern where adapters mutated metadata via
659
- * back-references (`this._table.addPrimaryKey()`, etc.).
660
- */
661
- interface TMetadataOverrides {
662
- /** Fields to add as primary keys. */
663
- addPrimaryKeys?: string[];
664
- /** Fields to remove from primary keys. */
665
- removePrimaryKeys?: string[];
666
- /** Fields to register as having a unique constraint. */
667
- addUniqueFields?: string[];
668
- /** Synthetic fields to inject into flatMap (e.g. MongoDB's `_id`). */
669
- injectFields?: Array<{
670
- path: string;
671
- type: TAtscriptAnnotatedType;
494
+ /** Whether the underlying adapter supports vector similarity search. */
495
+ isVectorSearchable(): boolean;
496
+ /**
497
+ * Vector similarity search with query translation and result reconstruction.
498
+ *
499
+ * Overloads:
500
+ * - `vectorSearch(vector, query?)` — uses default vector index
501
+ * - `vectorSearch(indexName, vector, query?)` — targets a specific vector index
502
+ */
503
+ vectorSearch<Q extends Uniquery<OwnProps, NavType>>(vectorOrIndex: number[] | string, maybeVectorOrQuery?: number[] | Q, maybeQuery?: Q): Promise<Array<DbResponse<DataType, NavType, Q>>>;
504
+ /**
505
+ * Vector similarity search with count for paginated results.
506
+ *
507
+ * Overloads:
508
+ * - `vectorSearchWithCount(vector, query?)` — uses default vector index
509
+ * - `vectorSearchWithCount(indexName, vector, query?)` — targets a specific vector index
510
+ */
511
+ vectorSearchWithCount<Q extends Uniquery<OwnProps, NavType>>(vectorOrIndex: number[] | string, maybeVectorOrQuery?: number[] | Q, maybeQuery?: Q): Promise<{
512
+ data: Array<DbResponse<DataType, NavType, Q>>;
513
+ count: number;
672
514
  }>;
673
- }
674
- /**
675
- * Callback that resolves an annotated type to a queryable table instance.
676
- * Required for `$with` relation loading — each table needs to query related tables.
677
- *
678
- * Typically provided by the driver/registry (e.g. `DbSpace.getTable`).
679
- */
680
- type TTableResolver = (type: TAtscriptAnnotatedType) => Pick<AtscriptDbTableLike, "findMany" | "loadRelations" | "primaryKeys" | "preferredId" | "relations" | "foreignKeys" | "isValidFieldPath"> | undefined;
681
- /** Minimal table interface used by the table resolver. Avoids circular dependency with AtscriptDbTable. */
682
- interface AtscriptDbTableLike {
683
- findMany(query: unknown): Promise<Array<Record<string, unknown>>>;
684
- loadRelations(rows: Array<Record<string, unknown>>, withRelations: WithRelation[]): Promise<void>;
685
- primaryKeys: readonly string[];
686
- preferredId: readonly string[];
687
- relations: ReadonlyMap<string, TDbRelation>;
688
- foreignKeys: ReadonlyMap<string, TDbForeignKey>;
689
- getMetadata(): TableMetadata;
690
- isValidFieldPath(path: string, visited?: Set<string>): boolean;
691
- }
692
- /** Minimal writable table interface for nested creation/update. */
693
- interface AtscriptDbWritable {
694
- insertOne(payload: Record<string, unknown>, opts?: {
695
- maxDepth?: number;
696
- }): Promise<TDbInsertResult>;
697
- insertMany(payloads: Array<Record<string, unknown>>, opts?: {
698
- maxDepth?: number;
699
- _depth?: number;
700
- }): Promise<TDbInsertManyResult>;
701
- replaceOne(payload: Record<string, unknown>, opts?: {
702
- maxDepth?: number;
703
- }): Promise<TDbUpdateResult>;
704
- bulkReplace(payloads: Array<Record<string, unknown>>, opts?: {
705
- maxDepth?: number;
706
- _depth?: number;
707
- }): Promise<TDbUpdateResult>;
708
- updateOne(payload: Record<string, unknown>, opts?: {
709
- maxDepth?: number;
710
- }): Promise<TDbUpdateResult>;
711
- bulkUpdate(payloads: Array<Record<string, unknown>>, opts?: {
712
- maxDepth?: number;
713
- _depth?: number;
714
- }): Promise<TDbUpdateResult>;
715
- findOne(query: unknown): Promise<Record<string, unknown> | null>;
716
- count(query: {
717
- filter: Record<string, unknown>;
718
- }): Promise<number>;
719
- deleteMany(filter: unknown): Promise<TDbDeleteResult>;
720
- /** Pre-validate items (type + FK constraints) without inserting them. */
721
- preValidateItems(items: Array<Record<string, unknown>>, opts?: {
722
- excludeFkTargetTable?: string;
723
- }): Promise<void>;
724
- }
725
- /**
726
- * Callback that resolves an annotated type to a writable table instance.
727
- * Used for nested creation — inserting related records inline.
728
- */
729
- type TWriteTableResolver = (type: TAtscriptAnnotatedType) => (AtscriptDbTableLike & AtscriptDbWritable) | undefined;
730
- /**
731
- * A child table that may need cascade/setNull processing when a parent is deleted.
732
- * Returned by the cascade resolver.
733
- */
734
- interface TCascadeTarget {
735
- /** FK on the child table that references the parent being deleted. */
736
- fk: TDbForeignKey;
737
- /** Name of the child table that holds this FK. */
738
- childTable: string;
739
- /** Delete matching child records (goes through AtscriptDbTable for recursive cascade). */
740
- deleteMany(filter: Record<string, unknown>): Promise<TDbDeleteResult>;
741
- /** Update matching child records (for setNull — sets FK fields to null). */
742
- updateMany(filter: Record<string, unknown>, data: Record<string, unknown>): Promise<TDbUpdateResult>;
743
- /** Count matching child records (for restrict — check existence before delete). */
744
- count(filter: Record<string, unknown>): Promise<number>;
745
- }
746
- /**
747
- * Callback that finds all child tables with FKs pointing to a given parent table.
748
- * Used by AtscriptDbTable to implement application-level cascade deletes.
749
- */
750
- type TCascadeResolver = (tableName: string) => TCascadeTarget[];
751
- /**
752
- * Minimal interface for querying a target table during FK validation.
753
- * Only `count` is needed — we check if the referenced record exists.
754
- */
755
- interface TFkLookupTarget {
756
- count(filter: Record<string, unknown>): Promise<number>;
757
- }
758
- /**
759
- * Callback that resolves a table name to a queryable target for FK validation.
760
- * Returns undefined if the target table is not registered in the space.
761
- */
762
- type TFkLookupResolver = (tableName: string) => TFkLookupTarget | undefined;
763
- interface TDbRelation {
764
- /** Direction: 'to' (FK is local), 'from' (FK is remote), or 'via' (M:N junction). */
765
- direction: "to" | "from" | "via";
766
- /** The alias used for pairing (if any). */
767
- alias?: string;
768
- /** Target type's annotated type reference. */
769
- targetType: () => TAtscriptAnnotatedType;
770
- /** Whether this is an array relation (one-to-many). */
771
- isArray: boolean;
772
- /** Junction type reference for 'via' (M:N) relations. */
773
- viaType?: () => TAtscriptAnnotatedType;
774
- }
775
- /**
776
- * Write payload for insert / patch paths: every key optional, and optional
777
- * columns additionally accept `null` (an explicit NULL — `undefined` means
778
- * "absent" and is dropped before the row reaches defaults or validation).
779
- */
780
- type DbPatch<D> = { [K in keyof D]?: undefined extends D[K] ? D[K] | null : D[K] } & Record<string, unknown>;
781
- /**
782
- * Write payload for full-row replace paths: required keys stay required,
783
- * optional columns additionally accept `null` (explicit NULL).
784
- */
785
- type DbRow<D> = { [K in keyof D]: undefined extends D[K] ? D[K] | null : D[K] } & Record<string, unknown>;
786
- /** Built-in write actions a moost-db `AsDbController` endpoint performs. */
787
- type TDbWriteAction = "insert" | "insertMany" | "replace" | "replaceMany" | "update" | "updateMany";
788
- /**
789
- * Context handed to a write {@link TWriteOptions.guard} (since 0.1.128) — and
790
- * through it to `AsDbController.guardWrite()`. The table invokes the guard
791
- * exactly once, inside its own transaction, after `undefined`-pruning,
792
- * defaults and validation and before encryption / nested-relation phases.
793
- */
794
- interface TDbWriteGuardContext<Row = Record<string, unknown>> {
795
- /** The table method the guard runs for (`insertOne` → `insert`, `insertMany` → `insertMany`, …). */
796
- readonly action: TDbWriteAction;
515
+ /** Resolves overloaded vector search arguments into canonical form. */
516
+ private _resolveVectorSearchArgs;
517
+ /** Whether the underlying adapter supports geospatial search. */
518
+ isGeoSearchable(): boolean;
519
+ /**
520
+ * Distance-ranked geospatial search (mirrors {@link vectorSearch}).
521
+ * Results are sorted by distance ascending; each row carries a computed
522
+ * `$distance` field (meters from the query point). `$maxDistance` /
523
+ * `$minDistance` (meters) ride in `query.controls`; user `$sort` is rejected.
524
+ *
525
+ * Overloads:
526
+ * - `geoSearch(point, query?)` — uses the table's only geo index
527
+ * - `geoSearch(indexName, point, query?)` — targets a specific geo index
528
+ */
529
+ geoSearch<Q extends Uniquery<OwnProps, NavType>>(pointOrIndex: [number, number] | string, maybePointOrQuery?: [number, number] | Q, maybeQuery?: Q): Promise<Array<DbResponse<DataType, NavType, Q> & {
530
+ $distance: number;
531
+ }>>;
532
+ /**
533
+ * Distance-ranked geospatial search with count for paginated results.
534
+ *
535
+ * Overloads:
536
+ * - `geoSearchWithCount(point, query?)` — uses the table's only geo index
537
+ * - `geoSearchWithCount(indexName, point, query?)` — targets a specific geo index
538
+ */
539
+ geoSearchWithCount<Q extends Uniquery<OwnProps, NavType>>(pointOrIndex: [number, number] | string, maybePointOrQuery?: [number, number] | Q, maybeQuery?: Q): Promise<{
540
+ data: Array<DbResponse<DataType, NavType, Q> & {
541
+ $distance: number;
542
+ }>;
543
+ count: number;
544
+ }>;
545
+ /** Resolves overloaded geo search arguments into canonical form. */
546
+ private _resolveGeoSearchArgs;
547
+ /** Shared geoSearch validation + query translation. */
548
+ private _prepareGeoSearch;
549
+ /**
550
+ * Finds a single record by any type-compatible identifier — primary key
551
+ * or single-field unique index.
552
+ * The return type excludes nav props unless `$with` is provided in controls.
553
+ *
554
+ * ```typescript
555
+ * // Without relations — nav props stripped from result
556
+ * const user = await table.findById('123')
557
+ *
558
+ * // With relations — only requested nav props appear
559
+ * const user = await table.findById('123', { controls: { $with: [{ name: 'posts' }] } })
560
+ * ```
561
+ */
562
+ findById<Q extends {
563
+ controls?: UniqueryControls<OwnProps, NavType>;
564
+ } = Record<string, never>>(id: IdType, query?: Q): Promise<DbResponse<DataType, NavType, Q> | null>;
797
565
  /**
798
- * insert/replace: validated rows with SDK-side defaults applied (plaintext,
799
- * nav data still attached); update: validated patches with the identifying
800
- * PK/unique fields present and `$cas` removed. Mutate in place to enrich —
801
- * the table re-validates the rows after the guard.
566
+ * Resolve an id value (scalar or object) into a {@link FilterExpr} using the
567
+ * same identification resolution as {@link findById}. Public so callers can
568
+ * AND-combine the id-filter with a row-level read overlay before issuing
569
+ * `findOne` (avoiding the existence leak that `findById` would cause).
802
570
  */
803
- readonly rows: Row[];
804
- /** Parallel to `rows`: expected version lifted from `$cas`, or `undefined`. */
805
- readonly expectedVersions: ReadonlyArray<number | undefined>;
571
+ resolveIdFilter(id: unknown): FilterExpr | null;
806
572
  /**
807
- * Lazy, memoised pre-image of `rows[i]` by its identifying filter, read
808
- * inside the transaction. `null` when the row is missing OR when it carries
809
- * no identifying key yet (e.g. auto-increment inserts) — never throws.
573
+ * Resolve an id value into a filter expression.
574
+ *
575
+ * When `preferredId` differs from the PK, scalar ids resolve only against
576
+ * the preferred field (deterministic addressing). Otherwise scalars try PK
577
+ * + every single-field unique index; objects try PK + compound unique
578
+ * indexes.
810
579
  */
811
- current(i: number): Promise<Row | null>;
812
- }
813
- /**
814
- * Context handed to a delete {@link TDeleteOptions.guard} (since 0.1.128) —
815
- * and through it to `AsDbController.guardRemove()`. Runs inside the table's
816
- * transaction; an id that resolves to no filter never reaches the guard
817
- * (`deleteOne` answers `{ deletedCount: 0 }`).
818
- */
819
- interface TDbRemoveGuardContext<Row = Record<string, unknown>> {
820
- /** The id `deleteOne` was called with. */
821
- readonly id: unknown;
822
- /** `table.resolveIdFilter(id)` — never null here. */
823
- readonly filter: FilterExpr;
824
- /** Lazy, memoised pre-image of the row about to be deleted (`null` when missing). */
825
- current(): Promise<Row | null>;
826
- }
827
- /** A validated-stage write guard — see {@link TWriteOptions.guard}. */
828
- type TDbWriteGuard<Row = Record<string, unknown>> = (ctx: TDbWriteGuardContext<Row>) => void | Promise<void>;
829
- /** A validated-stage delete guard — see {@link TDeleteOptions.guard}. */
830
- type TDbRemoveGuard<Row = Record<string, unknown>> = (ctx: TDbRemoveGuardContext<Row>) => void | Promise<void>;
831
- /** Options of `AtscriptDbTable.touchMany` (since 0.1.129). */
832
- interface TTouchManyOptions {
580
+ protected _resolveIdFilter(id: unknown): FilterExpr | null;
581
+ /** Build a single-key filter from `idObj` over `fields`, or null if any field is missing/incompatible. */
582
+ private _tryCompoundFilter;
833
583
  /**
834
- * `'all'` (default): every key must match its stored version — a stale or
835
- * missing row throws `DbError("CAS_MISMATCH")` and no version moves.
836
- * `'any'`: bump whatever matches and report the honest counts.
584
+ * Attempts to build a single-field filter `{ field: preparedId }`.
837
585
  */
838
- require?: "all" | "any";
839
- }
840
- /** Options of `insertOne/Many`, `replaceOne` / `bulkReplace`, `updateOne` / `bulkUpdate`. */
841
- interface TWriteOptions<Row = Record<string, unknown>> {
842
- /** Nested-relation write recursion limit (default 3). */
843
- maxDepth?: number;
586
+ private _tryFieldFilter;
844
587
  /**
845
- * Validated-stage guard (since 0.1.128): invoked exactly once inside the
846
- * table's transaction, after defaults + validation and before encryption
847
- * and nested-relation phases, with the rows the table is about to write.
848
- * Rows may be enriched in place — they are validated again afterwards. A
849
- * throw rolls the transaction back and propagates unchanged. Never runs
850
- * for the nested re-entries a deep write performs on related tables.
588
+ * Public entry point for relation loading. Used by adapters for nested $with delegation.
851
589
  */
852
- guard?: TDbWriteGuard<Row>;
853
- }
854
- /** Options of `deleteOne`. */
855
- interface TDeleteOptions<Row = Record<string, unknown>> {
590
+ loadRelations(rows: Array<Record<string, unknown>>, withRelations: WithRelation[]): Promise<void>;
856
591
  /**
857
- * Validated-stage guard (since 0.1.128): invoked inside the table's
858
- * transaction after the id resolved to a filter and before cascade /
859
- * delete. A throw rolls the transaction back and propagates unchanged.
592
+ * Finds the FK entry that connects a `@db.rel.to` relation to its target.
593
+ * Thin wrapper — delegates to relation-loader for shared use with db-table.ts write path.
860
594
  */
861
- guard?: TDbRemoveGuard<Row>;
595
+ protected _findFKForRelation(relation: TDbRelation): {
596
+ localFields: string[];
597
+ targetFields: string[];
598
+ } | undefined;
599
+ /**
600
+ * Finds a FK on a remote table that points back to this table.
601
+ * Thin wrapper — delegates to relation-loader for shared use with db-table.ts write path.
602
+ */
603
+ protected _findRemoteFK(targetTable: {
604
+ foreignKeys: ReadonlyMap<string, TDbForeignKey>;
605
+ }, thisTableName: string, alias?: string): TDbForeignKey | undefined;
862
606
  }
863
- /**
864
- * Adds `null` to every optional property of `O`. Optional columns store SQL
865
- * NULL / Mongo null, and the runtime validator accepts `null` for optional
866
- * props — so filter shapes (`{ note: null }`, `{ note: { $ne: null } }`) and
867
- * row shapes must admit it at the type level too. Homomorphic: keys and
868
- * required properties are unchanged; applying it twice is a no-op.
869
- */
870
- type NullableOptional<O> = { [K in keyof O]: undefined extends O[K] ? O[K] | null : O[K] };
871
607
  //#endregion
872
608
  //#region src/base-adapter.d.ts
609
+ /** Every calendar-bucket unit — what an adapter that renders them all returns from `calendarBucketUnits()`. */
610
+ declare const ALL_BUCKET_UNITS: ReadonlySet<BucketUnit>;
873
611
  /**
874
612
  * Abstract base class for database adapters.
875
613
  *
@@ -1022,6 +760,21 @@ declare abstract class BaseDbAdapter {
1022
760
  * to generate the value client-side or leave it for the DB.
1023
761
  */
1024
762
  nativeDefaultFns(): ReadonlySet<TDbDefaultFn>;
763
+ /**
764
+ * Calendar-bucket units (`{ $bucket, $field }` in an aggregate `$select`)
765
+ * this adapter can group by, over IANA time zones. Empty (the default) =
766
+ * calendar buckets unsupported: the core rejects them with
767
+ * `BUCKET_NOT_SUPPORTED` before dispatch, and moost-db's `/meta` advertises
768
+ * no bucketable field.
769
+ *
770
+ * A set rather than a boolean (like {@link nativeDefaultFns}) so a unit can
771
+ * be adopted adapter by adapter. An adapter that returns a unit must group
772
+ * by the bucket alias in `$groupBy` — see `controls.$select.buckets`
773
+ * (`TResolvedBucket`: physical `field`, source `fd`) — and return the
774
+ * `YYYY-MM-DD` label of the bucket's first local day (null for a null or
775
+ * out-of-range source, uniqu's `bucketLabel` semantics). Since 0.1.132.
776
+ */
777
+ calendarBucketUnits(): ReadonlySet<BucketUnit>;
1025
778
  /**
1026
779
  * Whether this adapter enforces foreign key constraints natively.
1027
780
  * When `true`, the generic layer skips application-level cascade/setNull
@@ -1042,6 +795,9 @@ declare abstract class BaseDbAdapter {
1042
795
  * Used by `AsDbReadableController.buildMetaResponse()` to gate the
1043
796
  * `filterable` flag exposed to UIs — the adapter's answer is a hard gate
1044
797
  * even when the field carries `@db.column.filterable`.
798
+ *
799
+ * Vetoes value comparison only — a sole-`$exists` entry needs just a stored
800
+ * column (`canFilterLeaf`).
1045
801
  */
1046
802
  canFilterField(fd: TDbFieldMeta): boolean;
1047
803
  /**
@@ -1539,487 +1295,936 @@ declare abstract class BaseDbAdapter {
1539
1295
  formatValue?(field: TDbFieldMeta): TValueFormatterPair | ((value: unknown) => unknown) | undefined;
1540
1296
  }
1541
1297
  //#endregion
1542
- //#region src/strategies/field-mapping.d.ts
1298
+ //#region src/table/table-metadata.d.ts
1543
1299
  /**
1544
- * Strategy for mapping data between logical field shapes and physical storage.
1545
- * Two implementations: {@link DocumentFieldMapper} (nested objects, NoSQL)
1546
- * and `RelationalFieldMapper` (flattened columns, SQL).
1300
+ * Finds the nearest ancestor of `path` that belongs to `set`.
1301
+ * Used by both the build pipeline (in `_classifyFields`) and
1302
+ * runtime reconstruction on the Readable.
1547
1303
  */
1548
- declare abstract class FieldMappingStrategy {
1549
- abstract reconstructFromRead(row: Record<string, unknown>, meta: TableMetadata): Record<string, unknown>;
1550
- abstract translateQuery(query: Uniquery, meta: TableMetadata): DbQuery;
1551
- abstract translateAggregateQuery(query: AggregateQuery, meta: TableMetadata): DbQuery;
1552
- /**
1553
- * Recursively walks a filter expression, applying `@db.column` key renames
1554
- * via `columnMap` and adapter-specific value formatting via `formatFilterValue`.
1555
- *
1556
- * The relational mapper overrides this to use `leafByLogical` for deeper
1557
- * key resolution (flattened nested paths).
1558
- */
1559
- translateFilter(filter: FilterExpr, meta: TableMetadata): FilterExpr;
1560
- abstract prepareForWrite(payload: Record<string, unknown>, meta: TableMetadata, adapter: BaseDbAdapter): Record<string, unknown>;
1561
- abstract translatePatchKeys(update: Record<string, unknown>, meta: TableMetadata): Record<string, unknown>;
1562
- /**
1563
- * Reverse-maps `@db.column` renames on a row read from storage.
1564
- * Renames physical keys back to logical names in-place.
1565
- */
1566
- protected reverseColumnRenames(row: Record<string, unknown>, meta: TableMetadata): void;
1304
+ declare function findAncestorInSet(path: string, set: ReadonlySet<string>): string | undefined;
1305
+ /** Returns true if the annotated type IS the `db.geoPoint` primitive (tag-based). */
1306
+ declare function isGeoPointType(fieldType: TAtscriptAnnotatedType): boolean;
1307
+ /**
1308
+ * Returns true if the annotated type is acceptable for `@db.index.geo`:
1309
+ * the `db.geoPoint` primitive or a structurally identical `number[]`
1310
+ * (excluding `db.vector`, which is semantically an embedding).
1311
+ */
1312
+ declare function isGeoIndexableType(fieldType: TAtscriptAnnotatedType): boolean;
1313
+ /**
1314
+ * Computed metadata for a database table or view.
1315
+ *
1316
+ * Contains all field metadata, physical mapping indexes, relation definitions,
1317
+ * and constraint information derived from Atscript annotations. Built lazily
1318
+ * on first access via {@link build}, then immutable.
1319
+ *
1320
+ * This class owns the build pipeline that was previously part of
1321
+ * `AtscriptDbReadable._flatten()`. The Readable delegates all metadata
1322
+ * access to this class.
1323
+ */
1324
+ declare class TableMetadata {
1325
+ readonly nestedObjects: boolean;
1326
+ flatMap: Map<string, TAtscriptAnnotatedType>;
1327
+ fieldDescriptors: readonly TDbFieldMeta[];
1328
+ primaryKeys: string[];
1329
+ preferredId: string[];
1330
+ originalMetaIdFields: string[];
1331
+ indexes: Map<string, TDbIndex>;
1332
+ foreignKeys: Map<string, TDbForeignKey>;
1333
+ relations: Map<string, TDbRelation>;
1334
+ navFields: Set<string>;
1335
+ ignoredFields: Set<string>;
1336
+ uniqueProps: Set<string>;
1337
+ defaults: Map<string, TDbDefaultValue>;
1338
+ columnMap: Map<string, string>;
1339
+ dimensions: string[];
1340
+ measures: string[];
1341
+ /** Logical field name annotated with `@db.column.version`, if any. */
1342
+ versionField?: string;
1343
+ /** path → sibling-ref path for `@db.amount.currency.ref` / `@db.unit.ref`. */
1344
+ quantityRefByField: Map<string, string>;
1345
+ /** Logical paths annotated with `@db.encrypted` — stored as one opaque ciphertext column. */
1346
+ encryptedFields: Set<string>;
1347
+ pathToPhysical: Map<string, string>;
1348
+ physicalToPath: Map<string, string>;
1349
+ flattenedParents: Set<string>;
1350
+ jsonFields: Set<string>;
1351
+ selectExpansion: Map<string, string[]>;
1352
+ booleanFields: Set<string>;
1353
+ decimalFields: Set<string>;
1354
+ allPhysicalFields: string[];
1355
+ /** Precomputed parent path → child physical column names for fast null-setting. */
1356
+ childrenByParent: Map<string, string[]>;
1357
+ /** Precomputed parent path → optional child logical paths (replace-strategy null-fill in the patch decomposer). */
1358
+ optionalLeavesByLogicalParent: Map<string, string[]>;
1359
+ requiresMappings: boolean;
1360
+ /** True when the only mappings needed are simple `@db.column` renames (no nesting/JSON). */
1361
+ onlyColumnRenames: boolean;
1362
+ toStorageFormatters?: Map<string, (value: unknown) => unknown>;
1363
+ fromStorageFormatters?: Map<string, (value: unknown) => unknown>;
1364
+ /** Leaf field descriptors indexed by physical column name (read path). */
1365
+ leafByPhysical: Map<string, TDbFieldMeta>;
1366
+ /** Leaf field descriptors indexed by logical path (write/patch/filter paths). */
1367
+ leafByLogical: Map<string, TDbFieldMeta>;
1567
1368
  /**
1568
- * Coerces field values from storage representation to JS types
1569
- * (booleans from 0/1, decimals from number to string).
1369
+ * Non-ignored field descriptors keyed by logical path, excluding navigation
1370
+ * relations and their descendants. Unlike `leafByLogical` (relational
1371
+ * adapters only) this is built for every adapter, so the core path guard
1372
+ * (`guardPaths`) can answer "does this path have physical storage here?"
1373
+ * on nested-object adapters too.
1570
1374
  */
1571
- protected coerceFieldValues(row: Record<string, unknown>, meta: TableMetadata): Record<string, unknown>;
1375
+ descriptorByPath: Map<string, TDbFieldMeta>;
1572
1376
  /**
1573
- * Applies adapter-specific fromStorage formatting to a row read from the database.
1574
- * Converts storage representations back to JS values (e.g. Date → epoch ms).
1377
+ * Logical paths stored as a single JSON column (`storage === 'json'`,
1378
+ * non-ignored descriptors). Retained after build — unlike the build-time
1379
+ * `jsonFields` set — so the path guard can classify JSON descendants on
1380
+ * relational adapters. Empty on nested-object adapters (they keep native
1381
+ * dotted paths as descriptors).
1575
1382
  */
1576
- protected applyFromStorageFormatters(row: Record<string, unknown>, meta: TableMetadata): Record<string, unknown>;
1383
+ jsonParents: ReadonlySet<string>;
1577
1384
  /**
1578
- * Sets a value at a dot-notation path, creating intermediate objects as needed.
1385
+ * Logical paths holding a JSON value (`isJsonValueField`: JSON-stored, `json`
1386
+ * or `array` design type; non-ignored, nav-free descriptors) — a timestamp
1387
+ * beneath one is never a calendar-bucket source (`jsonValueAncestor`).
1579
1388
  */
1580
- protected setNestedValue(obj: Record<string, unknown>, dotPath: string, value: unknown): void;
1389
+ jsonValueParents: ReadonlySet<string>;
1390
+ /** Every field descriptor's `physicalName` — reserved names a bucket alias may not take. */
1391
+ physicalNames: ReadonlySet<string>;
1392
+ private _built;
1393
+ private _identifications?;
1394
+ private _collateMap;
1395
+ private _columnFromMap;
1396
+ constructor(nestedObjects: boolean);
1397
+ get isBuilt(): boolean;
1581
1398
  /**
1582
- * If all children of a flattened parent are null, collapse the parent to null.
1399
+ * Logical field path → its physical path in document storage (nested
1400
+ * objects kept inline). `@db.column` renames apply to the annotated key,
1401
+ * and a document renames the TOP-LEVEL key only — nested keys are stored
1402
+ * as-is — so a dotted path under a renamed top-level object renames its
1403
+ * first segment: `profile.bio` under `@db.column 'prof'` → `prof.bio`.
1583
1404
  */
1584
- protected reconstructNullParent(obj: Record<string, unknown>, parentPath: string, meta: TableMetadata): void;
1405
+ documentPath(path: string): string;
1585
1406
  /**
1586
- * Applies adapter-specific value formatting to a single filter value.
1587
- * Handles direct values, operator objects ({$gt: v}), and $in/$nin arrays.
1407
+ * Runs the full metadata compilation pipeline. Called once by
1408
+ * `AtscriptDbReadable._ensureBuilt()` on first metadata access.
1409
+ *
1410
+ * Pipeline steps:
1411
+ * 1. `adapter.onBeforeFlatten(type)` — adapter hook
1412
+ * 2. `flattenAnnotatedType()` — collect field tuples, detect nav fields eagerly
1413
+ * 3. Replay non-nav-descendant tuples through annotation scanning + adapter.onFieldScanned
1414
+ * 4. Classify fields and build path maps (skipped for nested-objects adapters)
1415
+ * 5. `adapter.getMetadataOverrides()` → `_applyOverrides()` (PK/unique/inject adjustments)
1416
+ * 6. Build field descriptors (TDbFieldMeta[])
1417
+ * 7. Build leaf field indexes (skipped for nested-objects adapters)
1418
+ * 8. Finalize indexes (resolve field names to physical)
1419
+ * 9. `adapter.onAfterFlatten()` — adapter hook (read-only bookkeeping)
1420
+ * 10. Build allPhysicalFields list
1588
1421
  */
1589
- protected formatFilterValue(physicalName: string, value: unknown, meta: TableMetadata): unknown;
1422
+ build(type: TAtscriptAnnotatedType<TAtscriptTypeObject>, adapter: BaseDbAdapter, logger: TGenericLogger): void;
1590
1423
  /**
1591
- * Applies adapter-specific value formatting to prepared (physical-named) data.
1424
+ * Applies adapter-provided metadata overrides atomically.
1425
+ * Processing order: injectFields → removePrimaryKeys → addPrimaryKeys → addUniqueFields.
1592
1426
  */
1593
- protected formatWriteValues(data: Record<string, unknown>, meta: TableMetadata): Record<string, unknown>;
1594
- /**
1595
- * Prepares primary key values and strips ignored fields.
1596
- * Shared pre-processing for both document and relational write paths.
1427
+ private _applyOverrides;
1428
+ /**
1429
+ * Scans `@db.*` and `@meta.id` annotations on a field during flattening.
1597
1430
  */
1598
- protected prepareCommon(data: Record<string, unknown>, meta: TableMetadata, adapter: BaseDbAdapter): void;
1599
- }
1600
- /**
1601
- * Field mapper for document-oriented adapters (e.g. MongoDB).
1602
- * Nested objects are preserved as-is. Only applies column renames and
1603
- * value coercion.
1604
- */
1605
- declare class DocumentFieldMapper extends FieldMappingStrategy {
1606
- reconstructFromRead(row: Record<string, unknown>, meta: TableMetadata): Record<string, unknown>;
1607
- translateQuery(query: Uniquery, meta: TableMetadata): DbQuery;
1608
- translateAggregateQuery(query: AggregateQuery, meta: TableMetadata): DbQuery;
1609
- prepareForWrite(payload: Record<string, unknown>, meta: TableMetadata, adapter: BaseDbAdapter): Record<string, unknown>;
1610
- translatePatchKeys(update: Record<string, unknown>, meta: TableMetadata): Record<string, unknown>;
1611
- }
1612
- //#endregion
1613
- //#region src/encryption.d.ts
1614
- /**
1615
- * Configuration for field-level encryption at rest (`@db.encrypted`).
1616
- * Passed to `DbSpace` via the options bag.
1617
- */
1618
- interface TDbEncryptionOptions {
1619
- /** Key used for all new writes. */
1620
- defaultKeyId: string;
1431
+ private _scanGenericAnnotations;
1621
1432
  /**
1622
- * Key registry: keyId → 32-byte key (Buffer, or base64/hex/utf8 string).
1623
- * Decryption looks keys up by the keyId recorded in each value's envelope,
1624
- * so old keys stay in the registry for as long as data encrypted with them exists.
1433
+ * Build-time diagnostics for `@db.encrypted` (§6 of the field-encryption
1434
+ * spec). Mirrors the compile-time AnnotationSpec validation so models built
1435
+ * from pre-compiled types still fail fast.
1625
1436
  */
1626
- keys?: Record<string, string | Buffer>;
1437
+ private _validateEncryptedField;
1438
+ /** Build-time diagnostics for `@db.index.geo` (§3 of the geo-index spec). */
1439
+ private _validateGeoIndexField;
1440
+ private _addIndexField;
1627
1441
  /**
1628
- * Alternative/supplement to `keys`: async resolver (KMS, Vault, env indirection).
1629
- * Called once per keyId, result cached for the process lifetime.
1442
+ * Classifies each field as column, flattened, json, or parent-object.
1443
+ * Builds the bidirectional pathToPhysical / physicalToPath maps.
1630
1444
  */
1631
- resolveKey?: (keyId: string) => Promise<string | Buffer> | string | Buffer;
1445
+ private _classifyFields;
1446
+ /** Returns the `__`-separated parent prefix for a dot-separated path, or empty string for top-level paths. */
1447
+ private _flattenedPrefix;
1448
+ /** Nearest `@db.encrypted` ancestor of `path` (exclusive), or `undefined`. */
1632
1449
  /**
1633
- * What to do when a stored value is NOT a valid envelope (pre-existing
1634
- * plaintext rows, e.g. when @db.encrypted is added to a live column).
1635
- * - 'error' (default): fail the read with DbError("ENC_NOT_ENCRYPTED")
1636
- * - 'passthrough': return the raw value as-is (migration window mode);
1637
- * the value gets encrypted on its next write.
1450
+ * Indexes non-ignored descriptors by logical path and retains the JSON-parent
1451
+ * sets (plus every descriptor's physical name). Navigation relations and their descendants are skipped even when the
1452
+ * adapter keeps them as descriptors (nested-object adapters do) — they are
1453
+ * loaded with `$with`, never addressed as columns of this table.
1638
1454
  */
1639
- onUnencrypted?: "error" | "passthrough";
1640
- }
1641
- /** Decryption context — carried into error messages (never the key itself). */
1642
- interface TDecryptContext {
1643
- table?: string;
1644
- field?: string;
1645
- }
1646
- /**
1647
- * AES-256-GCM envelope encryption service for `@db.encrypted` fields.
1648
- *
1649
- * Owned by `DbSpace` and shared across all tables in the space. Values are
1650
- * `JSON.stringify`'d before encryption (type-exact round-trips) and stored as
1651
- * a single ASCII envelope string: `aes1$<keyId>$<iv>$<tag>$<ciphertext>`.
1652
- *
1653
- * Key material is validated eagerly at construction (`ENC_KEY_INVALID`);
1654
- * `resolveKey` lookups are cached per keyId for the process lifetime.
1655
- */
1656
- declare class DbEncryption {
1657
- readonly defaultKeyId: string;
1658
- readonly onUnencrypted: "error" | "passthrough";
1659
- private readonly _keys;
1660
- private readonly _resolveKey?;
1661
- private readonly _resolved;
1662
- constructor(options: TDbEncryptionOptions);
1663
- /** True when `value` looks like an encryption envelope produced by this service. */
1664
- isEnvelope(value: unknown): value is string;
1665
- /** Encrypts a JSON-serializable value into an envelope string using the default key. */
1666
- encrypt(value: unknown): Promise<string>;
1667
- /** Decrypts an envelope string back into its plaintext value. */
1668
- decrypt(envelope: string, ctx?: TDecryptContext): Promise<unknown>;
1669
- private _decryptFailed;
1670
- private _getKey;
1671
- }
1672
- //#endregion
1673
- //#region src/table/db-readable.d.ts
1674
- /**
1675
- * Extracts nav prop names from a query's `$with` array.
1676
- * Returns `never` when `$with` is absent → all nav props stripped from response.
1677
- */
1678
- type ExtractWith<Q> = Q extends {
1679
- controls: {
1680
- $with: Array<{
1681
- name: infer N extends string;
1682
- }>;
1683
- };
1684
- } ? N : never;
1685
- /**
1686
- * Computes the response type for a query:
1687
- * - Strips all nav props from the base DataType
1688
- * - Adds back only the nav props requested via `$with`
1689
- *
1690
- * When no `$with` is provided, result is `Omit<DataType, keyof NavType>`.
1691
- * When `$with: [{ name: 'author' }]`, result includes `author` from DataType.
1692
- * When the query type is not a literal (e.g. a variable typed as `Uniquery`),
1693
- * falls back to `DataType` (all nav props optional, as declared).
1694
- */
1695
- type DbResponse<Data, Nav, Q> = [keyof Nav] extends [never] ? Data : string extends keyof Nav ? Data : Omit<Data, keyof Nav & string> & Pick<Data, ExtractWith<Q> & keyof Data & string>;
1696
- /**
1697
- * Resolves the design type from an annotated type.
1698
- * Encapsulates the `kind === ''` check and fallback logic that
1699
- * otherwise trips up every adapter author.
1700
- *
1701
- * For union types (e.g., from flattened `{...} | {...}` objects):
1702
- * - If all members resolve to the same type → returns that type (strong type)
1703
- * - If members disagree → returns `'union'` (out of scope for type management)
1704
- */
1705
- declare function resolveDesignType(fieldType: TAtscriptAnnotatedType): string;
1706
- /**
1707
- * Shared read-only database abstraction driven by Atscript annotations.
1708
- *
1709
- * Contains all field metadata computation, read operations, query translation,
1710
- * relation loading, and result reconstruction. Extended by both
1711
- * {@link AtscriptDbTable} (adds write operations) and {@link AtscriptDbView}
1712
- * (adds view plan/DDL).
1713
- */
1714
- declare class AtscriptDbReadable<T extends TAtscriptAnnotatedType = TAtscriptAnnotatedType, DataType = TAtscriptDataType<T>, _FlatType = NullableOptional<FlatOf<T>>, A extends BaseDbAdapter = BaseDbAdapter, IdType = PrimaryKeyOf<T>, OwnProps = NullableOptional<OwnPropsOf<T>>, NavType extends Record<string, unknown> = NavPropsOf<T>> {
1715
- protected readonly _type: T;
1716
- protected readonly adapter: A;
1717
- protected readonly logger: TGenericLogger;
1718
- protected readonly _tableResolver?: TTableResolver | undefined;
1719
- /** Resolved table/collection/view name. */
1720
- readonly tableName: string;
1721
- /** Database schema/namespace from `@db.schema` (if set). */
1722
- readonly schema: string | undefined;
1723
- /** Sync method from `@db.sync.method` ('drop' | 'recreate' | undefined). */
1724
- protected readonly _syncMethod: "drop" | "recreate" | undefined;
1725
- /** Previous table/view name from `@db.table.renamed` or `@db.view.renamed`. */
1726
- readonly renamedFrom: string | undefined;
1727
- /** Computed metadata for this table/view. Built lazily on first access. */
1728
- protected readonly _meta: TableMetadata;
1729
- /** Strategy for mapping between logical field shapes and physical storage. */
1730
- protected readonly _fieldMapper: FieldMappingStrategy;
1731
- protected _writeTableResolver?: TWriteTableResolver;
1732
- /** Encryption service for `@db.encrypted` fields — set by `DbSpace` from its options. */
1733
- protected _encryption?: DbEncryption;
1734
- private _metaIdPhysical;
1735
- constructor(_type: T, adapter: A, logger?: TGenericLogger, _tableResolver?: TTableResolver | undefined);
1455
+ private _buildGuardIndexes;
1736
1456
  /**
1737
- * Sets the encryption service used for `@db.encrypted` fields.
1738
- * Called by `DbSpace` after table/view creation when the space was
1739
- * configured with an `encryption` options block.
1457
+ * Indexes `fieldDescriptors` into two lookup maps for unified
1458
+ * read/write field classification in the RelationalFieldMapper.
1740
1459
  */
1741
- setEncryption(encryption: DbEncryption | undefined): void;
1742
- /** Ensures metadata is built. Called before any metadata access. */
1743
- protected _ensureBuilt(): void;
1460
+ private _buildLeafIndexes;
1744
1461
  /**
1745
- * Built table metadata. Triggers a lazy build on first access — safe to call
1746
- * from peer tables that need this one's relations / nav fields before any
1747
- * operation has run against it directly.
1462
+ * Builds field descriptors, physical-name lookup, and value formatters.
1463
+ * Called once during build() — everything it needs
1464
+ * (flatMap, indexes, columnMap, etc.) is already populated.
1748
1465
  */
1749
- getMetadata(): TableMetadata;
1750
- protected _ensureSearchable(): void;
1751
- /** Engine-agnostic query-time guards (encrypted-field refs, $geoWithin shape). */
1752
- protected _guardQuery(query: Uniquery | undefined): void;
1753
- private _encryptedPathsCache?;
1754
- /** Pre-split `encryptedFields` paths — computed once, reused on every read/write. */
1755
- protected get _encryptedPaths(): Array<{
1756
- path: string;
1757
- segments: string[];
1758
- leaf: string;
1759
- }>;
1466
+ private _buildFieldDescriptors;
1760
1467
  /**
1761
- * Walks all but the last of `segments` down from `root`, returning the
1762
- * object holding the leaf — or `undefined` when the path is unreachable
1763
- * (a missing, non-object, or array step). With `cloneParents`, every
1764
- * traversed object is shallow-cloned and re-linked so caller-shared
1765
- * nested objects are never mutated.
1468
+ * Resolves `fkTargetField` for FK fields in field descriptors.
1766
1469
  */
1767
- protected _walkToLeafParent(root: Record<string, unknown>, segments: string[], cloneParents: boolean): Record<string, unknown> | undefined;
1470
+ private _resolveFkTargetFields;
1471
+ private _finalizeIndexes;
1768
1472
  /**
1769
- * Decrypts `@db.encrypted` fields on reconstructed rows (in place).
1770
- * Non-envelope stored values follow the configured `onUnencrypted` policy.
1473
+ * Captures legitimate row-identifier shapes from the metadata: primary key
1474
+ * (when present) followed by every unique index. Must run BEFORE
1475
+ * `_finalizeIndexes` rewrites `index.fields[i].name` from logical to
1476
+ * physical, so the field lists stay logical.
1477
+ *
1478
+ * Sources:
1479
+ * - `this.primaryKeys` — composite-aware PK (one identification covering
1480
+ * the PK columns).
1481
+ * - `this.indexes` — user-declared `@db.index.unique` (single or compound).
1482
+ * - `this.uniqueProps` — single-field uniques contributed by adapter
1483
+ * overrides (`addUniqueFields`); these aren't reflected in
1484
+ * `this.indexes` but still legitimate addressing identifications.
1485
+ * Deduped against any single-field unique index already captured.
1771
1486
  */
1772
- protected _decryptRows(rows: Array<Record<string, unknown>>): Promise<void>;
1773
- /** Whether this readable is a view (overridden in AtscriptDbView). */
1774
- get isView(): boolean;
1775
- /** Returns the underlying adapter with its concrete type preserved. */
1776
- getAdapter(): A;
1777
- /** The raw annotated type. */
1778
- get type(): TAtscriptAnnotatedType<TAtscriptTypeObject>;
1779
- /** Lazily-built flat map of all fields (dot-notation paths → annotated types). */
1780
- get flatMap(): Map<string, TAtscriptAnnotatedType>;
1781
- /** All computed indexes from `@db.index.*` annotations. */
1782
- get indexes(): Map<string, TDbIndex>;
1783
- /** Primary key field names from `@meta.id`. */
1784
- get primaryKeys(): readonly string[];
1785
- /** Preferred row identifier field names. Defaults to primary keys. */
1786
- get preferredId(): readonly string[];
1787
- /** Legitimate row-identifier shapes (primary key + every unique index). */
1788
- get identifications(): readonly TIdentification[];
1487
+ private _buildIdentifications;
1488
+ /** Legitimate row-identifier shapes — primary key first, then each unique index. */
1489
+ getIdentifications(): readonly TIdentification[];
1490
+ private _resolvePreferredId;
1491
+ }
1492
+ //#endregion
1493
+ //#region src/types.d.ts
1494
+ /** Controls with resolved projection. Used in the adapter interface. */
1495
+ interface DbControls extends Omit<UniqueryControls, "$select"> {
1496
+ $select?: UniquSelect;
1497
+ }
1498
+ /** Query object with resolved projection. Passed to adapter methods. */
1499
+ interface DbQuery {
1500
+ filter: FilterExpr;
1501
+ controls: DbControls;
1502
+ /** Pre-computed query insights (field → operators). Adapters may use this to apply query-time behaviour (e.g. collation). */
1503
+ insights?: UniqueryInsights;
1504
+ }
1505
+ /** Describes an available search index exposed by a database adapter. */
1506
+ interface TSearchIndexInfo {
1507
+ /** Index name. Empty string or 'DEFAULT' for the default index. */
1508
+ name: string;
1509
+ /** Human-readable label for UI display. */
1510
+ description?: string;
1511
+ /** Index type: text search or vector similarity search. */
1512
+ type?: "text" | "vector";
1513
+ }
1514
+ /** Relation summary in a meta response. */
1515
+ interface TRelationInfo {
1516
+ name: string;
1517
+ direction: "to" | "from" | "via";
1518
+ isArray: boolean;
1519
+ }
1520
+ /** Per-field capability flags in a meta response. */
1521
+ interface TFieldMeta {
1522
+ sortable: boolean;
1523
+ filterable: boolean;
1789
1524
  /**
1790
- * Physical column name of the single `@meta.id` field, or `null` when the
1791
- * schema has zero or multiple `@meta.id` fields. Used by adapters to return
1792
- * the user's logical ID instead of the DB-generated one on insert.
1793
- *
1794
- * @internal Adapter-facing surface; not part of the consumer API.
1525
+ * Present only when `filterable` is `false` but narrower predicates still
1526
+ * pass the gate: their operators — `$exists` on a relational adapter's JSON
1527
+ * / array column, `$geoWithin` on a geoPoint of a geo-searchable adapter.
1528
+ * Since 0.1.132.
1795
1529
  */
1796
- get metaIdPhysical(): string | null;
1530
+ filterOps?: string[];
1531
+ /** Present (true) when the field is `@db.encrypted` — stored as ciphertext at rest. */
1532
+ encrypted?: boolean;
1533
+ /** Present (true) when the field carries a `@db.index.geo` geospatial index. */
1534
+ geo?: boolean;
1797
1535
  /**
1798
- * Physical column name of the field annotated with `@db.column.version`, or
1799
- * `undefined` when the table has no version column. Used by adapters and the
1800
- * REST integration to drive optimistic concurrency control (OCC).
1536
+ * Present (true) when the field is write-only over HTTP (`@db.writeOnly` or
1537
+ * stamped by a permission overlay): settable in write payloads, never
1538
+ * present in read responses. UIs render it as a set-only input.
1801
1539
  */
1802
- get versionColumn(): string | undefined;
1803
- /** Dimension fields from `@db.column.dimension`. */
1804
- get dimensions(): readonly string[];
1805
- /** Measure fields from `@db.column.measure`. */
1806
- get measures(): readonly string[];
1807
- /** Sync method for structural changes: 'drop' (lossy), 'recreate' (lossless), or undefined (manual). */
1808
- get syncMethod(): "drop" | "recreate" | undefined;
1809
- /** Logical → physical column name mapping from `@db.column`. */
1810
- get columnMap(): ReadonlyMap<string, string>;
1811
- /** Default values from `@db.default.*`. */
1812
- get defaults(): ReadonlyMap<string, TDbDefaultValue>;
1813
- /** Fields excluded from DB via `@db.ignore`. */
1814
- get ignoredFields(): ReadonlySet<string>;
1815
- /** Navigational fields (`@db.rel.to` / `@db.rel.from`) — not stored as columns. */
1816
- get navFields(): ReadonlySet<string>;
1817
- /** Physical field names used to invert exclude-mode `$select` into a SELECT list. */
1818
- get allPhysicalFields(): readonly string[];
1819
- /** Single-field unique index properties. */
1820
- get uniqueProps(): ReadonlySet<string>;
1821
- /** Foreign key constraints from `@db.rel.FK` annotations. */
1822
- get foreignKeys(): ReadonlyMap<string, TDbForeignKey>;
1823
- /** Navigational relation metadata from `@db.rel.to` / `@db.rel.from`. */
1824
- get relations(): ReadonlyMap<string, TDbRelation>;
1825
- /** The underlying database adapter instance. */
1826
- get dbAdapter(): A;
1540
+ writeOnly?: boolean;
1827
1541
  /**
1828
- * Enables or disables verbose (debug-level) DB call logging for this table/view.
1829
- * When disabled (default), no log strings are constructed — zero overhead.
1542
+ * Present (true) when the field is index-backed (explicit `@db.index*`,
1543
+ * primary key or unique field). Advisory only — a hint for UIs that want to
1544
+ * steer users toward cheap sort keys; it never affects whether a `$sort`
1545
+ * is accepted (`sortable` does). Since 0.1.128.
1830
1546
  */
1831
- setVerbose(enabled: boolean): void;
1832
- /** Precomputed logical dot-path → physical column name map. */
1833
- get pathToPhysical(): ReadonlyMap<string, string>;
1834
- /** Precomputed physical column name → logical dot-path map (inverse). */
1835
- get physicalToPath(): ReadonlyMap<string, string>;
1836
- /** Descriptor for the primary ID field(s). */
1837
- getIdDescriptor(): TIdDescriptor;
1547
+ indexed?: boolean;
1838
1548
  /**
1839
- * Pre-computed field metadata for adapter use.
1549
+ * Present (true) exactly when a calendar bucket over this field passes the
1550
+ * gate: a physically filterable `number.timestamp` field (a dimension, when
1551
+ * the table declares dimensions) on an adapter with calendar buckets
1552
+ * (`bucketUnits`). Since 0.1.132.
1840
1553
  */
1841
- get fieldDescriptors(): readonly TDbFieldMeta[];
1554
+ bucketable?: true;
1555
+ }
1556
+ /** Built-in CRUD operation names; map 1:1 to public method names. */
1557
+ type TCrudOp = "query" | "pages" | "one" | "geo" | "insert" | "update" | "replace" | "remove";
1558
+ /**
1559
+ * CRUD permissions advertised in `/meta`. Key absent → operation is denied or
1560
+ * not exposed. Key present → operation is allowed; the `string[]` value is the
1561
+ * accepted UniQuery control whitelist for read ops (`[]` for write ops, which
1562
+ * take no controls — presence still signals "allowed").
1563
+ */
1564
+ type TCrudPermissions = Partial<Record<TCrudOp, string[]>>;
1565
+ /** Response payload for `GET /meta`. */
1566
+ interface TMetaResponse {
1567
+ searchable: boolean;
1568
+ vectorSearchable: boolean;
1569
+ /** Whether the adapter supports `geoSearch()` AND the table declares a geo index. */
1570
+ geoSearchable?: boolean;
1571
+ searchIndexes: TSearchIndexInfo[];
1572
+ primaryKeys: string[];
1573
+ preferredId: string[];
1574
+ relations: TRelationInfo[];
1575
+ fields: Record<string, TFieldMeta>;
1576
+ type: TSerializedAnnotatedType;
1577
+ actions: TDbActionInfo[];
1578
+ crud: TCrudPermissions;
1842
1579
  /**
1843
- * Resolves whether `path` references a real field — directly via `flatMap`
1844
- * or transitively through a nav relation by recursing into the target
1845
- * table. Defense-in-depth for query-path validation: `flattenAnnotatedType`
1846
- * still truncates real self-referential cycles, so paths like
1847
- * `parent.parent.name` on a self-ref schema would miss `flatMap.has` but
1848
- * remain valid field references on the target.
1580
+ * Physical column name annotated with `@db.column.version`, when the table
1581
+ * opts into optimistic concurrency control (OCC). Absent for tables without
1582
+ * the annotation, i.e. last-write-wins (default) behavior.
1583
+ */
1584
+ versionColumn?: string;
1585
+ /**
1586
+ * Calendar-bucket units the adapter can group by (`{ $bucket }` in an
1587
+ * aggregate `$select`, URL `bucket(field,unit,…)`); omitted when it has
1588
+ * none. Since 0.1.132.
1589
+ */
1590
+ bucketUnits?: BucketUnit[];
1591
+ }
1592
+ /** Where the action applies on the UI. */
1593
+ type TDbActionLevel = "table" | "row" | "rows";
1594
+ /**
1595
+ * Semantic intent the UI maps to its own visual language (color, prominence).
1596
+ *
1597
+ * Suggested visual prominence (most → least): `negative` > `warning` > `primary`
1598
+ * > `positive` > `secondary`. Use `negative` for destructive ops (delete, purge),
1599
+ * `warning` for risky-but-non-destructive ops (retry payment, force recompute,
1600
+ * reset state), `primary` for the headline action, `positive` for benign
1601
+ * confirmations (approve, publish), `secondary` for everything else.
1602
+ */
1603
+ type TDbActionIntent = "positive" | "negative" | "warning" | "primary" | "secondary";
1604
+ /** How the UI client should handle the action when invoked. */
1605
+ type TDbActionProcessor = "backend" | "navigate" | "custom";
1606
+ /**
1607
+ * Single action descriptor in the `/meta` envelope. Flat shape — `processor`
1608
+ * is a string discriminator; `value` is its sibling and is always populated.
1609
+ *
1610
+ * - `processor: 'backend'` — UI POSTs to `value` (full HTTP path).
1611
+ * - `processor: 'navigate'` — UI routes to `value` (URL template; `$1` is the row PK).
1612
+ * - `processor: 'custom'` — UI dispatches `value` as an event name (defaults to action `name`).
1613
+ */
1614
+ interface TDbActionInfo {
1615
+ name: string;
1616
+ label: string;
1617
+ level: TDbActionLevel;
1618
+ processor: TDbActionProcessor;
1619
+ value: string;
1620
+ icon?: string;
1621
+ intent?: TDbActionIntent;
1622
+ description?: string;
1623
+ order?: number;
1624
+ default?: boolean;
1625
+ /**
1626
+ * Confirmation prompt copy. String form is shown verbatim. Tuple form is
1627
+ * `[singular, plural]`: the UI picks `[0]` when the action will execute
1628
+ * against a single PK (always for `'row'`-level; for `'rows'`-level when the
1629
+ * current selection has exactly one PK) and `[1]` otherwise.
1849
1630
  *
1850
- * Cycle-safe via a visited set keyed on `<tableName>:<navField>`.
1631
+ * Placeholder substitution is UI-resolved, not server-parsed. Conventional
1632
+ * placeholders: `$1` for the single PK (singular form) and `$N` for the
1633
+ * count (plural form), e.g. `['Delete order $1?', 'Delete $N orders?']`.
1851
1634
  */
1852
- isValidFieldPath(path: string, _visited?: Set<string>): boolean;
1635
+ promptText?: string | [string, string];
1853
1636
  /**
1854
- * Creates a new validator with custom options.
1637
+ * Single-character keyboard shortcut hint. The server stores this verbatim
1638
+ * — choice of modifier prefix (Alt+, Ctrl+, bare key) and activation scope
1639
+ * (e.g. only when an actions dropdown is open) are UI/UX concerns. Conflict
1640
+ * resolution between actions sharing the same key is also up to the UI;
1641
+ * the server does no dedup.
1855
1642
  */
1856
- createValidator(opts?: Partial<TValidatorOptions>): Validator<T, DataType>;
1643
+ shortcut?: string;
1857
1644
  /**
1858
- * Finds a single record matching the query.
1859
- * The return type automatically excludes nav props unless they are
1860
- * explicitly requested via `$with`.
1645
+ * Stringified gate predicate (`fn.toString()`). Present only for `'row'`
1646
+ * and `'rows'` level actions whose decorator declared a `disabled` function.
1647
+ * The function is the batch shape `(rows: TRow[]) => boolean[]` (sync). The
1648
+ * UI evaluates against a level-specific scope to grey-out / hide the
1649
+ * button. The server has already enforced this predicate before the
1650
+ * action's handler ran — the server is authoritative; this field is purely
1651
+ * a UI hint.
1861
1652
  */
1862
- findOne<Q extends Uniquery<OwnProps, NavType>>(query: Q): Promise<DbResponse<DataType, NavType, Q> | null>;
1653
+ disabled?: string;
1863
1654
  /**
1864
- * Finds all records matching the query.
1865
- * The return type automatically excludes nav props unless they are
1866
- * explicitly requested via `$with`.
1655
+ * Name of the `.as` interface the action's `@InputForm()` parameter expects
1656
+ * (the compiled class's `.name`). Present only when the handler declares an
1657
+ * `@InputForm(FormType)` parameter. Clients fetch the serialized schema via
1658
+ * `GET /meta/form/:name` on the same controller and render a form to
1659
+ * collect the `input` field of the action's request envelope.
1867
1660
  */
1868
- findMany<Q extends Uniquery<OwnProps, NavType>>(query: Q): Promise<Array<DbResponse<DataType, NavType, Q>>>;
1661
+ inputForm?: string;
1662
+ }
1663
+ interface TDbInsertResult {
1664
+ insertedId: unknown;
1665
+ }
1666
+ interface TDbInsertManyResult {
1667
+ insertedCount: number;
1668
+ insertedIds: unknown[];
1669
+ }
1670
+ interface TDbUpdateResult {
1671
+ matchedCount: number;
1672
+ modifiedCount: number;
1673
+ }
1674
+ interface TDbDeleteResult {
1675
+ deletedCount: number;
1676
+ }
1677
+ type TDbIndexType = "plain" | "unique" | "fulltext" | "geo";
1678
+ interface TDbIndexField {
1679
+ name: string;
1680
+ sort: "asc" | "desc";
1681
+ weight?: number;
1869
1682
  /**
1870
- * Counts records matching the query.
1683
+ * Whether the indexed field is optional (declared `field?:` in the model).
1684
+ * Resolved during index finalization. Adapters use this to make a unique
1685
+ * index "present-only" so multiple value-less rows are tolerated — matching
1686
+ * SQL's `NULLS DISTINCT` default. SQL adapters get this for free and ignore
1687
+ * the flag; MongoDB needs it to emit a partial unique index.
1871
1688
  */
1872
- count(query?: Uniquery<OwnProps, NavType>): Promise<number>;
1689
+ optional?: boolean;
1690
+ /**
1691
+ * Resolved design type of the field ('string', 'number', 'boolean', …).
1692
+ * Carried alongside {@link optional} so adapters can derive a type-correct
1693
+ * present-only filter (e.g. Mongo's `partialFilterExpression`) without
1694
+ * re-resolving the field type. Undefined when the field cannot be resolved.
1695
+ */
1696
+ designType?: string;
1697
+ }
1698
+ interface TDbIndex {
1699
+ /** Unique key used for identity/diffing (e.g., "atscript__plain__email") */
1700
+ key: string;
1701
+ /** Human-readable index name. */
1702
+ name: string;
1703
+ /** Index type. */
1704
+ type: TDbIndexType;
1705
+ /** Ordered list of fields in the index. */
1706
+ fields: TDbIndexField[];
1707
+ }
1708
+ type TDbDefaultFn = "increment" | "uuid" | "now";
1709
+ type TDbCollation = "binary" | "nocase" | "unicode";
1710
+ type TDbDefaultValue = {
1711
+ kind: "value";
1712
+ value: string;
1713
+ } | {
1714
+ kind: "fn";
1715
+ fn: TDbDefaultFn;
1716
+ start?: number;
1717
+ };
1718
+ interface TIdDescriptor {
1719
+ /** Field names that form the primary key. */
1720
+ fields: string[];
1721
+ /** Whether this is a composite key (multiple fields). */
1722
+ isComposite: boolean;
1723
+ }
1724
+ /** A legitimate row-identifier shape: primary key or a unique index. */
1725
+ interface TIdentification {
1726
+ /** Logical (path) field names that form this identifier. */
1727
+ fields: readonly string[];
1728
+ /** `'primaryKey'` for the PK; the unique-index name otherwise. */
1729
+ source: string;
1730
+ }
1731
+ type TDbStorageType = "column" | "flattened" | "json";
1732
+ interface TDbFieldMeta {
1733
+ /** The dot-notation path to this field (logical name). */
1734
+ path: string;
1735
+ /** The annotated type for this field. */
1736
+ type: TAtscriptAnnotatedType;
1737
+ /** Physical column/field name (from @db.column, __-separated for flattened, or same as path). */
1738
+ physicalName: string;
1739
+ /** Resolved design type: 'string', 'number', 'boolean', 'object', 'json', etc. */
1740
+ designType: string;
1741
+ /** Whether the field is optional. */
1742
+ optional: boolean;
1743
+ /** Whether this field is part of the primary key (@meta.id). */
1744
+ isPrimaryKey: boolean;
1745
+ /** Whether this field is excluded from the DB (@db.ignore). */
1746
+ ignored: boolean;
1747
+ /** Default value from @db.default.* */
1748
+ defaultValue?: TDbDefaultValue;
1873
1749
  /**
1874
- * Finds records and total count in a single logical call.
1750
+ * How this field is stored in the database.
1751
+ * - 'column': a standard scalar column (default for primitives)
1752
+ * - 'flattened': a leaf scalar from a flattened nested object
1753
+ * - 'json': stored as a single JSON column (arrays, @db.json fields)
1875
1754
  */
1876
- findManyWithCount<Q extends Uniquery<OwnProps, NavType>>(query: Q): Promise<{
1877
- data: Array<DbResponse<DataType, NavType, Q>>;
1878
- count: number;
1879
- }>;
1755
+ storage: TDbStorageType;
1880
1756
  /**
1881
- * Executes an aggregate query with GROUP BY and aggregate functions.
1882
- *
1883
- * Validates:
1884
- * - Plain fields in $select are a subset of $groupBy
1885
- * - When dimensions/measures are defined (strict mode): $groupBy fields
1886
- * must be dimensions, aggregate $field values must be measures (or '*')
1887
- *
1888
- * Translates field names, delegates to adapter.aggregate(),
1889
- * then reverse-maps and applies fromStorage formatters on results.
1757
+ * For flattened fields: the dot-notation path (same as `path`).
1758
+ * E.g., for physicalName 'contact__email', this is 'contact.email'.
1759
+ * Undefined for non-flattened fields.
1890
1760
  */
1891
- aggregate(query: AggregateQuery): Promise<Array<Record<string, unknown>>>;
1892
- /** Whether the underlying adapter supports text search. */
1893
- isSearchable(): boolean;
1894
- /** Whether the adapter can filter on a given field (proxies adapter capability). */
1895
- canFilterField(fd: TDbFieldMeta): boolean;
1896
- /** Whether the adapter can sort by a given field (proxies adapter capability). */
1897
- canSortField(fd: TDbFieldMeta): boolean;
1898
- /** Returns available search indexes from the adapter. */
1899
- getSearchIndexes(): TSearchIndexInfo[];
1761
+ flattenedFrom?: string;
1762
+ /** Old physical column name from @db.column.renamed (for rename migration). */
1763
+ renamedFrom?: string;
1764
+ /** Collation from @db.column.collate (e.g. 'nocase', 'binary', 'unicode'). */
1765
+ collate?: TDbCollation;
1900
1766
  /**
1901
- * Full-text search with query translation and result reconstruction.
1767
+ * Whether this field is index-backed: it participates in an explicit index
1768
+ * (@db.index.plain, @db.index.unique, @db.index.fulltext) OR is a primary key
1769
+ * or unique field (which are always index-backed — Mongo `_id`, SQL PK/unique
1770
+ * constraints — even without an explicit `@db.index*`).
1902
1771
  */
1903
- search<Q extends Uniquery<OwnProps, NavType>>(text: string, query: Q, indexName?: string): Promise<Array<DbResponse<DataType, NavType, Q>>>;
1772
+ isIndexed?: boolean;
1773
+ /** Literal currency code from `@db.amount.currency 'EUR'`. */
1774
+ currencyCode?: string;
1775
+ /** Sibling field path from `@db.amount.currency.ref 'fieldName'`. */
1776
+ currencyRefField?: string;
1777
+ /** Literal unit-of-measure from `@db.unit 'kg'`. */
1778
+ unitCode?: string;
1779
+ /** Sibling field path from `@db.unit.ref 'fieldName'`. */
1780
+ unitRefField?: string;
1904
1781
  /**
1905
- * Full-text search with count for paginated search results.
1782
+ * For FK fields: the resolved field metadata of the referenced (target) PK column.
1783
+ * Adapters use this in `typeMapper` to produce matching DB types for FK columns
1784
+ * (e.g., `typeMapper(field.fkTargetField)` to inherit the target PK's DB type).
1785
+ * Undefined for non-FK fields or when the target cannot be resolved.
1906
1786
  */
1907
- searchWithCount<Q extends Uniquery<OwnProps, NavType>>(text: string, query: Q, indexName?: string): Promise<{
1908
- data: Array<DbResponse<DataType, NavType, Q>>;
1909
- count: number;
1910
- }>;
1911
- /** Whether the underlying adapter supports vector similarity search. */
1912
- isVectorSearchable(): boolean;
1787
+ fkTargetField?: TDbFieldMeta;
1913
1788
  /**
1914
- * Vector similarity search with query translation and result reconstruction.
1915
- *
1916
- * Overloads:
1917
- * - `vectorSearch(vector, query?)` — uses default vector index
1918
- * - `vectorSearch(indexName, vector, query?)` — targets a specific vector index
1789
+ * `@db.encrypted` — the value is AES-256-GCM encrypted by the core layer
1790
+ * before reaching the adapter. Adapters must map the column to an unbounded
1791
+ * text type and veto filtering/sorting (`canFilterField`/`canSortField`).
1792
+ * The descriptor's `designType` is forced to `'string'` (ciphertext envelope);
1793
+ * the declared type stays available via `type` for validation.
1919
1794
  */
1920
- vectorSearch<Q extends Uniquery<OwnProps, NavType>>(vectorOrIndex: number[] | string, maybeVectorOrQuery?: number[] | Q, maybeQuery?: Q): Promise<Array<DbResponse<DataType, NavType, Q>>>;
1795
+ encrypted?: boolean;
1921
1796
  /**
1922
- * Vector similarity search with count for paginated results.
1923
- *
1924
- * Overloads:
1925
- * - `vectorSearchWithCount(vector, query?)` — uses default vector index
1926
- * - `vectorSearchWithCount(indexName, vector, query?)` — targets a specific vector index
1797
+ * The field's declared type is the `db.geoPoint` primitive (`[lng, lat]` tuple).
1798
+ * Adapters map this to their native geo storage (e.g. MongoDB GeoJSON Point).
1927
1799
  */
1928
- vectorSearchWithCount<Q extends Uniquery<OwnProps, NavType>>(vectorOrIndex: number[] | string, maybeVectorOrQuery?: number[] | Q, maybeQuery?: Q): Promise<{
1929
- data: Array<DbResponse<DataType, NavType, Q>>;
1930
- count: number;
1800
+ isGeoPoint?: boolean;
1801
+ }
1802
+ interface TValueFormatterPair {
1803
+ /** Converts a JS value to storage representation (write + filter paths). */
1804
+ toStorage: (value: unknown) => unknown;
1805
+ /** Converts a storage value back to JS representation (read path). */
1806
+ fromStorage: (value: unknown) => unknown;
1807
+ }
1808
+ type TDbReferentialAction = "cascade" | "restrict" | "noAction" | "setNull" | "setDefault";
1809
+ interface TDbForeignKey {
1810
+ /** FK field names on this table (local columns). */
1811
+ fields: string[];
1812
+ /** Target table name (from the chain ref's type @db.table annotation). */
1813
+ targetTable: string;
1814
+ /** Target field names on the referenced table. */
1815
+ targetFields: string[];
1816
+ /** Lazy reference to the target annotated type (for on-demand table resolution). */
1817
+ targetTypeRef?: () => TAtscriptAnnotatedType;
1818
+ /** Alias grouping FK fields (if any). */
1819
+ alias?: string;
1820
+ /** Referential action on delete. */
1821
+ onDelete?: TDbReferentialAction;
1822
+ /** Referential action on update. */
1823
+ onUpdate?: TDbReferentialAction;
1824
+ }
1825
+ /** Describes an existing column in the database (from introspection). */
1826
+ interface TExistingColumn {
1827
+ name: string;
1828
+ type: string;
1829
+ notnull: boolean;
1830
+ pk: boolean;
1831
+ /** Serialized default value (e.g., "'active'", "NULL"). */
1832
+ dflt_value?: string;
1833
+ }
1834
+ /** Result of comparing desired schema against existing database columns. */
1835
+ interface TColumnDiff {
1836
+ added: TDbFieldMeta[];
1837
+ removed: TExistingColumn[];
1838
+ renamed: Array<{
1839
+ field: TDbFieldMeta;
1840
+ oldName: string;
1931
1841
  }>;
1932
- /** Resolves overloaded vector search arguments into canonical form. */
1933
- private _resolveVectorSearchArgs;
1934
- /** Whether the underlying adapter supports geospatial search. */
1935
- isGeoSearchable(): boolean;
1936
- /**
1937
- * Distance-ranked geospatial search (mirrors {@link vectorSearch}).
1938
- * Results are sorted by distance ascending; each row carries a computed
1939
- * `$distance` field (meters from the query point). `$maxDistance` /
1940
- * `$minDistance` (meters) ride in `query.controls`; user `$sort` is rejected.
1941
- *
1942
- * Overloads:
1943
- * - `geoSearch(point, query?)` — uses the table's only geo index
1944
- * - `geoSearch(indexName, point, query?)` — targets a specific geo index
1945
- */
1946
- geoSearch<Q extends Uniquery<OwnProps, NavType>>(pointOrIndex: [number, number] | string, maybePointOrQuery?: [number, number] | Q, maybeQuery?: Q): Promise<Array<DbResponse<DataType, NavType, Q> & {
1947
- $distance: number;
1948
- }>>;
1949
- /**
1950
- * Distance-ranked geospatial search with count for paginated results.
1951
- *
1952
- * Overloads:
1953
- * - `geoSearchWithCount(point, query?)` — uses the table's only geo index
1954
- * - `geoSearchWithCount(indexName, point, query?)` — targets a specific geo index
1955
- */
1956
- geoSearchWithCount<Q extends Uniquery<OwnProps, NavType>>(pointOrIndex: [number, number] | string, maybePointOrQuery?: [number, number] | Q, maybeQuery?: Q): Promise<{
1957
- data: Array<DbResponse<DataType, NavType, Q> & {
1958
- $distance: number;
1959
- }>;
1960
- count: number;
1842
+ typeChanged: Array<{
1843
+ field: TDbFieldMeta;
1844
+ existingType: string;
1845
+ }>;
1846
+ nullableChanged: Array<{
1847
+ field: TDbFieldMeta;
1848
+ wasNullable: boolean;
1849
+ }>;
1850
+ defaultChanged: Array<{
1851
+ field: TDbFieldMeta;
1852
+ oldDefault?: string;
1853
+ newDefault?: string;
1854
+ }>;
1855
+ conflicts: Array<{
1856
+ field: TDbFieldMeta;
1857
+ oldName: string;
1858
+ conflictsWith: string;
1961
1859
  }>;
1962
- /** Resolves overloaded geo search arguments into canonical form. */
1963
- private _resolveGeoSearchArgs;
1964
- /** Shared geoSearch validation + query translation. */
1965
- private _prepareGeoSearch;
1966
1860
  /**
1967
- * Finds a single record by any type-compatible identifier — primary key
1968
- * or single-field unique index.
1969
- * The return type excludes nav props unless `$with` is provided in controls.
1970
- *
1971
- * ```typescript
1972
- * // Without relations — nav props stripped from result
1973
- * const user = await table.findById('123')
1974
- *
1975
- * // With relations — only requested nav props appear
1976
- * const user = await table.findById('123', { controls: { $with: [{ name: 'posts' }] } })
1977
- * ```
1861
+ * The primary-key FIELD SET differs between the live table and the model
1862
+ * (set semantics — a composite-key reorder is not a change, consistent with
1863
+ * the schema hash). Column names are physical; a renamed PK column is
1864
+ * compared under its new name. Only reported when the table exists.
1865
+ * @since 0.1.128
1978
1866
  */
1979
- findById<Q extends {
1980
- controls?: UniqueryControls<OwnProps, NavType>;
1981
- } = Record<string, never>>(id: IdType, query?: Q): Promise<DbResponse<DataType, NavType, Q> | null>;
1867
+ primaryKeyChanged?: TPrimaryKeyChange;
1868
+ }
1869
+ /** Old and new primary-key column sets of a table whose key definition moved. */
1870
+ interface TPrimaryKeyChange {
1871
+ /** Physical PK columns currently in the database (after rename mapping). */
1872
+ from: string[];
1873
+ /** Physical PK columns the model declares. */
1874
+ to: string[];
1875
+ }
1876
+ /**
1877
+ * A live foreign-key constraint as introspected from the database
1878
+ * (outbound: declared on the table that owns it).
1879
+ */
1880
+ interface TExistingForeignKey {
1881
+ /** Local (referencing) columns, in constraint order. */
1882
+ fields: string[];
1883
+ /** Referenced table name. */
1884
+ targetTable: string;
1885
+ /** Referenced columns, in constraint order. */
1886
+ targetFields: string[];
1887
+ }
1888
+ /**
1889
+ * A live foreign key that REFERENCES a given table (inbound edge), as returned
1890
+ * by `BaseDbAdapter.getReferencingForeignKeys(tableName)`.
1891
+ */
1892
+ interface TReferencingForeignKey {
1893
+ /** The referencing (child) table. */
1894
+ table: string;
1895
+ /** Referencing columns on `table`, in constraint order. */
1896
+ fields: string[];
1897
+ /** Referenced columns on the queried table, in constraint order. */
1898
+ targetFields: string[];
1899
+ }
1900
+ /** Kind of a physical database object, as returned by `BaseDbAdapter.getObjectKind`. */
1901
+ type TDbObjectKind = "table" | "view" | "materialized";
1902
+ /** Options accepted by `BaseDbAdapter.ensureTable`. */
1903
+ interface TEnsureTableOptions {
1982
1904
  /**
1983
- * Resolve an id value (scalar or object) into a {@link FilterExpr} using the
1984
- * same identification resolution as {@link findById}. Public so callers can
1985
- * AND-combine the id-filter with a row-level read overlay before issuing
1986
- * `findOne` (avoiding the existence leak that `findById` would cause).
1905
+ * Table names whose inline FOREIGN KEY constraints must be omitted from
1906
+ * CREATE TABLE — the constraints are added afterwards by `syncForeignKeys()`.
1907
+ * Schema sync passes the members of a foreign-key cycle so they can be
1908
+ * created in any order.
1987
1909
  */
1988
- resolveIdFilter(id: unknown): FilterExpr | null;
1910
+ deferForeignKeysTo?: ReadonlySet<string>;
1911
+ }
1912
+ /** Result of applying column diff to the database. */
1913
+ interface TSyncColumnResult {
1914
+ added: string[];
1915
+ renamed: string[];
1916
+ }
1917
+ /** A single table-level option in unified key-value format. */
1918
+ interface TExistingTableOption {
1919
+ key: string;
1920
+ value: string;
1921
+ }
1922
+ /** Result of comparing desired table options against existing ones. */
1923
+ interface TTableOptionDiff {
1924
+ changed: Array<{
1925
+ key: string;
1926
+ oldValue: string;
1927
+ newValue: string; /** Whether applying this change requires dropping and recreating the table. */
1928
+ destructive: boolean;
1929
+ }>;
1930
+ }
1931
+ /**
1932
+ * Adapter-provided metadata adjustments applied atomically during the
1933
+ * build pipeline, before field descriptors are built.
1934
+ *
1935
+ * Replaces the old pattern where adapters mutated metadata via
1936
+ * back-references (`this._table.addPrimaryKey()`, etc.).
1937
+ */
1938
+ interface TMetadataOverrides {
1939
+ /** Fields to add as primary keys. */
1940
+ addPrimaryKeys?: string[];
1941
+ /** Fields to remove from primary keys. */
1942
+ removePrimaryKeys?: string[];
1943
+ /** Fields to register as having a unique constraint. */
1944
+ addUniqueFields?: string[];
1945
+ /** Synthetic fields to inject into flatMap (e.g. MongoDB's `_id`). */
1946
+ injectFields?: Array<{
1947
+ path: string;
1948
+ type: TAtscriptAnnotatedType;
1949
+ }>;
1950
+ }
1951
+ /**
1952
+ * Callback that resolves an annotated type to a queryable table instance.
1953
+ * Required for `$with` relation loading — each table needs to query related tables.
1954
+ *
1955
+ * Typically provided by the driver/registry (e.g. `DbSpace.getTable`).
1956
+ */
1957
+ type TTableResolver = (type: TAtscriptAnnotatedType) => Pick<AtscriptDbTableLike, "findMany" | "loadRelations" | "primaryKeys" | "preferredId" | "relations" | "foreignKeys" | "isValidFieldPath"> | undefined;
1958
+ /** Minimal table interface used by the table resolver. Avoids circular dependency with AtscriptDbTable. */
1959
+ interface AtscriptDbTableLike {
1960
+ findMany(query: unknown): Promise<Array<Record<string, unknown>>>;
1961
+ loadRelations(rows: Array<Record<string, unknown>>, withRelations: WithRelation[]): Promise<void>;
1962
+ primaryKeys: readonly string[];
1963
+ preferredId: readonly string[];
1964
+ relations: ReadonlyMap<string, TDbRelation>;
1965
+ foreignKeys: ReadonlyMap<string, TDbForeignKey>;
1966
+ getMetadata(): TableMetadata;
1967
+ isValidFieldPath(path: string, visited?: Set<string>): boolean;
1968
+ }
1969
+ /** Minimal writable table interface for nested creation/update. */
1970
+ interface AtscriptDbWritable {
1971
+ insertOne(payload: Record<string, unknown>, opts?: {
1972
+ maxDepth?: number;
1973
+ }): Promise<TDbInsertResult>;
1974
+ insertMany(payloads: Array<Record<string, unknown>>, opts?: {
1975
+ maxDepth?: number;
1976
+ _depth?: number;
1977
+ }): Promise<TDbInsertManyResult>;
1978
+ replaceOne(payload: Record<string, unknown>, opts?: {
1979
+ maxDepth?: number;
1980
+ }): Promise<TDbUpdateResult>;
1981
+ bulkReplace(payloads: Array<Record<string, unknown>>, opts?: {
1982
+ maxDepth?: number;
1983
+ _depth?: number;
1984
+ }): Promise<TDbUpdateResult>;
1985
+ updateOne(payload: Record<string, unknown>, opts?: {
1986
+ maxDepth?: number;
1987
+ }): Promise<TDbUpdateResult>;
1988
+ bulkUpdate(payloads: Array<Record<string, unknown>>, opts?: {
1989
+ maxDepth?: number;
1990
+ _depth?: number;
1991
+ }): Promise<TDbUpdateResult>;
1992
+ findOne(query: unknown): Promise<Record<string, unknown> | null>;
1993
+ count(query: {
1994
+ filter: Record<string, unknown>;
1995
+ }): Promise<number>;
1996
+ deleteMany(filter: unknown): Promise<TDbDeleteResult>;
1997
+ /** Pre-validate items (type + FK constraints) without inserting them. */
1998
+ preValidateItems(items: Array<Record<string, unknown>>, opts?: {
1999
+ excludeFkTargetTable?: string;
2000
+ }): Promise<void>;
2001
+ }
2002
+ /**
2003
+ * Callback that resolves an annotated type to a writable table instance.
2004
+ * Used for nested creation — inserting related records inline.
2005
+ */
2006
+ type TWriteTableResolver = (type: TAtscriptAnnotatedType) => (AtscriptDbTableLike & AtscriptDbWritable) | undefined;
2007
+ /**
2008
+ * A child table that may need cascade/setNull processing when a parent is deleted.
2009
+ * Returned by the cascade resolver.
2010
+ */
2011
+ interface TCascadeTarget {
2012
+ /** FK on the child table that references the parent being deleted. */
2013
+ fk: TDbForeignKey;
2014
+ /** Name of the child table that holds this FK. */
2015
+ childTable: string;
2016
+ /** Delete matching child records (goes through AtscriptDbTable for recursive cascade). */
2017
+ deleteMany(filter: Record<string, unknown>): Promise<TDbDeleteResult>;
2018
+ /** Update matching child records (for setNull — sets FK fields to null). */
2019
+ updateMany(filter: Record<string, unknown>, data: Record<string, unknown>): Promise<TDbUpdateResult>;
2020
+ /** Count matching child records (for restrict — check existence before delete). */
2021
+ count(filter: Record<string, unknown>): Promise<number>;
2022
+ }
2023
+ /**
2024
+ * Callback that finds all child tables with FKs pointing to a given parent table.
2025
+ * Used by AtscriptDbTable to implement application-level cascade deletes.
2026
+ */
2027
+ type TCascadeResolver = (tableName: string) => TCascadeTarget[];
2028
+ /**
2029
+ * Minimal interface for querying a target table during FK validation.
2030
+ * Only `count` is needed — we check if the referenced record exists.
2031
+ */
2032
+ interface TFkLookupTarget {
2033
+ count(filter: Record<string, unknown>): Promise<number>;
2034
+ }
2035
+ /**
2036
+ * Callback that resolves a table name to a queryable target for FK validation.
2037
+ * Returns undefined if the target table is not registered in the space.
2038
+ */
2039
+ type TFkLookupResolver = (tableName: string) => TFkLookupTarget | undefined;
2040
+ interface TDbRelation {
2041
+ /** Direction: 'to' (FK is local), 'from' (FK is remote), or 'via' (M:N junction). */
2042
+ direction: "to" | "from" | "via";
2043
+ /** The alias used for pairing (if any). */
2044
+ alias?: string;
2045
+ /** Target type's annotated type reference. */
2046
+ targetType: () => TAtscriptAnnotatedType;
2047
+ /** Whether this is an array relation (one-to-many). */
2048
+ isArray: boolean;
2049
+ /** Junction type reference for 'via' (M:N) relations. */
2050
+ viaType?: () => TAtscriptAnnotatedType;
2051
+ }
2052
+ /**
2053
+ * Write payload for insert / patch paths: every key optional, and optional
2054
+ * columns additionally accept `null` (an explicit NULL — `undefined` means
2055
+ * "absent" and is dropped before the row reaches defaults or validation).
2056
+ */
2057
+ type DbPatch<D> = { [K in keyof D]?: undefined extends D[K] ? D[K] | null : D[K] } & Record<string, unknown>;
2058
+ /**
2059
+ * Write payload for full-row replace paths: required keys stay required,
2060
+ * optional columns additionally accept `null` (explicit NULL).
2061
+ */
2062
+ type DbRow<D> = { [K in keyof D]: undefined extends D[K] ? D[K] | null : D[K] } & Record<string, unknown>;
2063
+ /** Built-in write actions a moost-db `AsDbController` endpoint performs. */
2064
+ type TDbWriteAction = "insert" | "insertMany" | "replace" | "replaceMany" | "update" | "updateMany";
2065
+ /**
2066
+ * Context handed to a write {@link TWriteOptions.guard} (since 0.1.128) — and
2067
+ * through it to `AsDbController.guardWrite()`. The table invokes the guard
2068
+ * exactly once, inside its own transaction, after `undefined`-pruning,
2069
+ * defaults and validation and before encryption / nested-relation phases.
2070
+ */
2071
+ interface TDbWriteGuardContext<Row = Record<string, unknown>> {
2072
+ /** The table method the guard runs for (`insertOne` → `insert`, `insertMany` → `insertMany`, …). */
2073
+ readonly action: TDbWriteAction;
1989
2074
  /**
1990
- * Resolve an id value into a filter expression.
1991
- *
1992
- * When `preferredId` differs from the PK, scalar ids resolve only against
1993
- * the preferred field (deterministic addressing). Otherwise scalars try PK
1994
- * + every single-field unique index; objects try PK + compound unique
1995
- * indexes.
2075
+ * insert/replace: validated rows with SDK-side defaults applied (plaintext,
2076
+ * nav data still attached); update: validated patches with the identifying
2077
+ * PK/unique fields present and `$cas` removed. Mutate in place to enrich —
2078
+ * the table re-validates the rows after the guard.
1996
2079
  */
1997
- protected _resolveIdFilter(id: unknown): FilterExpr | null;
1998
- /** Build a single-key filter from `idObj` over `fields`, or null if any field is missing/incompatible. */
1999
- private _tryCompoundFilter;
2080
+ readonly rows: Row[];
2081
+ /** Parallel to `rows`: expected version lifted from `$cas`, or `undefined`. */
2082
+ readonly expectedVersions: ReadonlyArray<number | undefined>;
2000
2083
  /**
2001
- * Attempts to build a single-field filter `{ field: preparedId }`.
2084
+ * Lazy, memoised pre-image of `rows[i]` by its identifying filter, read
2085
+ * inside the transaction. `null` when the row is missing OR when it carries
2086
+ * no identifying key yet (e.g. auto-increment inserts) — never throws.
2002
2087
  */
2003
- private _tryFieldFilter;
2088
+ current(i: number): Promise<Row | null>;
2089
+ }
2090
+ /**
2091
+ * Context handed to a delete {@link TDeleteOptions.guard} (since 0.1.128) —
2092
+ * and through it to `AsDbController.guardRemove()`. Runs inside the table's
2093
+ * transaction; an id that resolves to no filter never reaches the guard
2094
+ * (`deleteOne` answers `{ deletedCount: 0 }`).
2095
+ */
2096
+ interface TDbRemoveGuardContext<Row = Record<string, unknown>> {
2097
+ /** The id `deleteOne` was called with. */
2098
+ readonly id: unknown;
2099
+ /** `table.resolveIdFilter(id)` — never null here. */
2100
+ readonly filter: FilterExpr;
2101
+ /** Lazy, memoised pre-image of the row about to be deleted (`null` when missing). */
2102
+ current(): Promise<Row | null>;
2103
+ }
2104
+ /** A validated-stage write guard — see {@link TWriteOptions.guard}. */
2105
+ type TDbWriteGuard<Row = Record<string, unknown>> = (ctx: TDbWriteGuardContext<Row>) => void | Promise<void>;
2106
+ /** A validated-stage delete guard — see {@link TDeleteOptions.guard}. */
2107
+ type TDbRemoveGuard<Row = Record<string, unknown>> = (ctx: TDbRemoveGuardContext<Row>) => void | Promise<void>;
2108
+ /** Options of `AtscriptDbTable.touchMany` (since 0.1.129). */
2109
+ interface TTouchManyOptions {
2004
2110
  /**
2005
- * Public entry point for relation loading. Used by adapters for nested $with delegation.
2111
+ * `'all'` (default): every key must match its stored version — a stale or
2112
+ * missing row throws `DbError("CAS_MISMATCH")` and no version moves.
2113
+ * `'any'`: bump whatever matches and report the honest counts.
2006
2114
  */
2007
- loadRelations(rows: Array<Record<string, unknown>>, withRelations: WithRelation[]): Promise<void>;
2115
+ require?: "all" | "any";
2116
+ }
2117
+ /** Options of `insertOne/Many`, `replaceOne` / `bulkReplace`, `updateOne` / `bulkUpdate`. */
2118
+ interface TWriteOptions<Row = Record<string, unknown>> {
2119
+ /** Nested-relation write recursion limit (default 3). */
2120
+ maxDepth?: number;
2008
2121
  /**
2009
- * Finds the FK entry that connects a `@db.rel.to` relation to its target.
2010
- * Thin wrapper — delegates to relation-loader for shared use with db-table.ts write path.
2122
+ * Validated-stage guard (since 0.1.128): invoked exactly once inside the
2123
+ * table's transaction, after defaults + validation and before encryption
2124
+ * and nested-relation phases, with the rows the table is about to write.
2125
+ * Rows may be enriched in place — they are validated again afterwards. A
2126
+ * throw rolls the transaction back and propagates unchanged. Never runs
2127
+ * for the nested re-entries a deep write performs on related tables.
2011
2128
  */
2012
- protected _findFKForRelation(relation: TDbRelation): {
2013
- localFields: string[];
2014
- targetFields: string[];
2015
- } | undefined;
2129
+ guard?: TDbWriteGuard<Row>;
2130
+ }
2131
+ /** Options of `deleteOne`. */
2132
+ interface TDeleteOptions<Row = Record<string, unknown>> {
2016
2133
  /**
2017
- * Finds a FK on a remote table that points back to this table.
2018
- * Thin wrapper — delegates to relation-loader for shared use with db-table.ts write path.
2134
+ * Validated-stage guard (since 0.1.128): invoked inside the table's
2135
+ * transaction after the id resolved to a filter and before cascade /
2136
+ * delete. A throw rolls the transaction back and propagates unchanged.
2019
2137
  */
2020
- protected _findRemoteFK(targetTable: {
2021
- foreignKeys: ReadonlyMap<string, TDbForeignKey>;
2022
- }, thisTableName: string, alias?: string): TDbForeignKey | undefined;
2138
+ guard?: TDbRemoveGuard<Row>;
2139
+ }
2140
+ /**
2141
+ * Adds `null` to every optional property of `O`. Optional columns store SQL
2142
+ * NULL / Mongo null, and the runtime validator accepts `null` for optional
2143
+ * props — so filter shapes (`{ note: null }`, `{ note: { $ne: null } }`) and
2144
+ * row shapes must admit it at the type level too. Homomorphic: keys and
2145
+ * required properties are unchanged; applying it twice is a no-op.
2146
+ */
2147
+ type NullableOptional<O> = { [K in keyof O]: undefined extends O[K] ? O[K] | null : O[K] };
2148
+ //#endregion
2149
+ //#region src/query/buckets.d.ts
2150
+ /**
2151
+ * A calendar bucket as adapters receive it (`controls.$select.buckets`): the
2152
+ * normalized uniqu bucket (canonical zone, week start, alias) whose `field` is
2153
+ * the PHYSICAL column / document path, plus the source field's descriptor —
2154
+ * dialects read the storage kind from `fd` (since 0.1.132).
2155
+ */
2156
+ type TResolvedBucket = ResolvedBucket & {
2157
+ /** Physical column (relational) or document path (nested-object adapters). */field: string; /** Descriptor of the source field (a `number.timestamp` leaf). */
2158
+ fd: TDbFieldMeta;
2159
+ };
2160
+ /**
2161
+ * The names a bucket alias must not shadow: `TableMetadata` provides them, and
2162
+ * moost-db's HTTP gate assembles the same set from its readable and capability
2163
+ * index, so both layers resolve against the same names.
2164
+ */
2165
+ interface TBucketFieldSource {
2166
+ /** Every logical path of the table type (nested parents and navigation fields included). */
2167
+ flatMap: ReadonlyMap<string, unknown>;
2168
+ /** Every field descriptor's `physicalName` — reserved too. */
2169
+ physicalNames: ReadonlySet<string>;
2170
+ navFields: ReadonlySet<string>;
2023
2171
  }
2172
+ /**
2173
+ * The one normalizer of `$select` computed entries (since 0.1.132) — uniqu's
2174
+ * `resolveBuckets` (entry shapes, unit, time zone canonicalization, week
2175
+ * start, alias syntax and uniqueness, "grouped queries only", "must also
2176
+ * appear in $groupBy", string `$groupBy` entries) with the table's names as
2177
+ * the collision set: a bucket alias may not equal a logical path, a physical
2178
+ * column or a navigation field, so a label is never reverse-mapped as a
2179
+ * column.
2180
+ *
2181
+ * Which layer validates what:
2182
+ * - **Shapes** (this normalizer) run FIRST at every entry point — the core's
2183
+ * read path (`guardQuery`), its aggregate path (`AtscriptDbReadable.aggregate`,
2184
+ * which hands the result on to `guardAggregate` and the field mapper), and
2185
+ * moost-db's HTTP gate — so both layers answer with the same wording.
2186
+ * Everything downstream (`collectQueryPaths`, the field mappers,
2187
+ * `UniquSelect`, adapters) assumes normalized input and does not re-check.
2188
+ * - **The source field** (encryption, JSON ancestor, timestamp type,
2189
+ * physical filterability, strict-mode dimension, an adapter with calendar
2190
+ * buckets) is `bucketSourceVerdict` — ONE function, called by the path
2191
+ * guard (`guardPath` op `bucket`) and by moost-db's capability index, so
2192
+ * `/meta.fields[P].bucketable` and the core cannot disagree.
2193
+ * - **The unit** (`calendarBucketUnits()` lacks it) is `guardAggregate`'s
2194
+ * (`BUCKET_NOT_SUPPORTED`); SQL builders only re-assert the inlined
2195
+ * literals (defense in depth).
2196
+ *
2197
+ * `aggregate` defaults to "`$groupBy` is non-empty".
2198
+ *
2199
+ * @throws DbError `INVALID_QUERY` carrying every issue (`path` `$select` / `$groupBy`).
2200
+ */
2201
+ declare function resolveCalendarBuckets(controls: {
2202
+ $select?: unknown;
2203
+ $groupBy?: unknown;
2204
+ } | undefined, fields: TBucketFieldSource, aggregate?: boolean): ResolvedBucket[];
2205
+ /**
2206
+ * Whether a field's TYPE allows it to be the source of a calendar bucket: a
2207
+ * `number` / `integer` leaf carrying the `timestamp` tag
2208
+ * (`number.timestamp`, `.created`, `.updated`) that is not
2209
+ * `@db.encrypted`. The type is the declaration — no annotation opts a field
2210
+ * in.
2211
+ *
2212
+ * @deprecated since 0.1.133 — the type rule alone; use `bucketSourceVerdict`,
2213
+ * the full bucket-source rule the core and moost-db apply.
2214
+ */
2215
+ declare function isBucketableField(fd: TDbFieldMeta): boolean;
2216
+ /**
2217
+ * Whether a field holds a JSON value — a `@db.json` object / JSON-stored
2218
+ * column or an array. The members of `TableMetadata.jsonValueParents` (and
2219
+ * moost-db's equivalent set) — see {@link jsonValueAncestor}.
2220
+ */
2221
+ declare function isJsonValueField(fd: TDbFieldMeta): boolean;
2222
+ /**
2223
+ * The outermost ancestor of `path` in `jsonValueParents` (the paths of the
2224
+ * {@link isJsonValueField} descriptors), or `undefined`. A timestamp inside a
2225
+ * JSON value is never a bucket source — relational adapters cannot address
2226
+ * it and nested-object adapters (which can) must not diverge from them.
2227
+ */
2228
+ declare function jsonValueAncestor(path: string, jsonValueParents: ReadonlySet<string>): string | undefined;
2024
2229
  //#endregion
2025
- export { TDbWriteAction as $, TCrudPermissions as A, findAncestorInSet as At, TDbForeignKey as B, NullableOptional as C, TWriteOptions as Ct, TCascadeTarget as D, UniqueryControls$1 as Dt, TCascadeResolver as E, Uniquery$1 as Et, TDbCollation as F, UniquSelect as Ft, TDbInsertResult as G, TDbIndexField as H, TDbDefaultFn as I, TDbRelation as J, TDbObjectKind as K, TDbDefaultValue as L, TDbActionIntent as M, isGeoPointType as Mt, TDbActionLevel as N, NoopLogger as Nt, TColumnDiff as O, WithRelation$1 as Ot, TDbActionProcessor as P, TGenericLogger as Pt, TDbUpdateResult as Q, TDbDeleteResult as R, NavPropsOf$1 as S, TValueFormatterPair as St, PrimaryKeyOf$1 as T, TypedWithRelation as Tt, TDbIndexType as U, TDbIndex as V, TDbInsertManyResult as W, TDbRemoveGuardContext as X, TDbRemoveGuard as Y, TDbStorageType as Z, DbQuery as _, TSearchIndexInfo as _t, TDbEncryptionOptions as a, TExistingForeignKey as at, FilterExpr$1 as b, TTableResolver as bt, BaseDbAdapter as c, TFkLookupResolver as ct, AggregateFn as d, TIdentification as dt, TDbWriteGuard as et, AggregateQuery$1 as f, TMetaResponse as ft, DbPatch as g, TRelationInfo as gt, DbControls as h, TReferencingForeignKey as ht, DbEncryption as i, TExistingColumn as it, TDbActionInfo as j, isGeoIndexableType as jt, TCrudOp as k, TableMetadata as kt, AggregateControls as l, TFkLookupTarget as lt, AtscriptDbWritable as m, TPrimaryKeyChange as mt, DbResponse as n, TDeleteOptions as nt, DocumentFieldMapper as o, TExistingTableOption as ot, AggregateResult as p, TMetadataOverrides as pt, TDbReferentialAction as q, resolveDesignType as r, TEnsureTableOptions as rt, FieldMappingStrategy as s, TFieldMeta as st, AtscriptDbReadable as t, TDbWriteGuardContext as tt, AggregateExpr$1 as u, TIdDescriptor as ut, DbRow as v, TSyncColumnResult as vt, OwnPropsOf$1 as w, TWriteTableResolver as wt, FlatOf$1 as x, TTouchManyOptions as xt, FieldOpsFor as y, TTableOptionDiff as yt, TDbFieldMeta as z };
2230
+ export { TDbWriteGuardContext as $, TDbActionIntent as A, isGeoPointType as At, TDbIndexField as B, NoopLogger as Bt, PrimaryKeyOf$1 as C, TypedWithRelation as Ct, TCrudOp as D, TableMetadata as Dt, TColumnDiff as E, WithRelation$1 as Et, TDbDefaultValue as F, resolveDesignType as Ft, TDbReferentialAction as G, TDbInsertManyResult as H, UniquSelect as Ht, TDbDeleteResult as I, DbEncryption as It, TDbRemoveGuardContext as J, TDbRelation as K, TDbFieldMeta as L, TDbEncryptionOptions as Lt, TDbActionProcessor as M, BaseDbAdapter as Mt, TDbCollation as N, AtscriptDbReadable as Nt, TCrudPermissions as O, findAncestorInSet as Ot, TDbDefaultFn as P, DbResponse as Pt, TDbWriteGuard as Q, TDbForeignKey as R, DocumentFieldMapper as Rt, OwnPropsOf$1 as S, TWriteTableResolver as St, TCascadeTarget as T, UniqueryControls$1 as Tt, TDbInsertResult as U, TDbIndexType as V, TGenericLogger as Vt, TDbObjectKind as W, TDbUpdateResult as X, TDbStorageType as Y, TDbWriteAction as Z, FieldOpsFor as _, TTableOptionDiff as _t, jsonValueAncestor as a, TFieldMeta as at, NavPropsOf$1 as b, TValueFormatterPair as bt, AggregateExpr$1 as c, TIdDescriptor as ct, AggregateResult as d, TMetadataOverrides as dt, TDeleteOptions as et, AtscriptDbWritable as f, TPrimaryKeyChange as ft, DbRow as g, TSyncColumnResult as gt, DbQuery as h, TSearchIndexInfo as ht, isJsonValueField as i, TExistingTableOption as it, TDbActionLevel as j, ALL_BUCKET_UNITS as jt, TDbActionInfo as k, isGeoIndexableType as kt, AggregateFn as l, TIdentification as lt, DbPatch as m, TRelationInfo as mt, TResolvedBucket as n, TExistingColumn as nt, resolveCalendarBuckets as o, TFkLookupResolver as ot, DbControls as p, TReferencingForeignKey as pt, TDbRemoveGuard as q, isBucketableField as r, TExistingForeignKey as rt, AggregateControls as s, TFkLookupTarget as st, TBucketFieldSource as t, TEnsureTableOptions as tt, AggregateQuery$1 as u, TMetaResponse as ut, FilterExpr$1 as v, TTableResolver as vt, TCascadeResolver as w, Uniquery$1 as wt, NullableOptional as x, TWriteOptions as xt, FlatOf$1 as y, TTouchManyOptions as yt, TDbIndex as z, FieldMappingStrategy as zt };