@atscript/db 0.1.130 → 0.1.132

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-B7eYWS5q.d.cts → buckets-BvoYYQ2Q.d.cts} +1348 -1142
  6. package/dist/{db-readable-Bn1bV_eC.d.mts → buckets-D2XPrwSC.d.mts} +1348 -1142
  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-C2UCnGHd.d.cts → db-space-BQQEgB3d.d.mts} +2 -2
  10. package/dist/{db-space-DdIPYD0Q.d.mts → db-space-Cslq5Dux.d.cts} +2 -2
  11. package/dist/{db-view-hkwQ-fHJ.cjs → db-view-CuFAhzwF.cjs} +487 -123
  12. package/dist/{db-view-BkF2xEGd.mjs → db-view-DVGWWhA3.mjs} +439 -123
  13. package/dist/index.cjs +13 -4
  14. package/dist/index.d.cts +105 -38
  15. package/dist/index.d.mts +105 -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,6 +1,6 @@
1
1
  import { f as TFieldOps } from "./ops-AqhV7s9o.mjs";
2
2
  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";
3
- 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";
3
+ 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";
4
4
 
5
5
  //#region src/query/uniqu-select.d.ts
6
6
  /**
@@ -11,6 +11,11 @@ import { AggregateControls, AggregateExpr, AggregateExpr as AggregateExpr$1, Agg
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,539 @@ 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 (a calendar bucket's source field included),
464
+ * aggregate $field values must be measures (or '*')
465
+ * - the path guard (a bucket source must be a timestamp field) and the
466
+ * adapter's calendar-bucket units (`BUCKET_NOT_SUPPORTED`)
467
+ *
468
+ * Translates field names, delegates to adapter.aggregate(),
469
+ * then reverse-maps and applies fromStorage formatters on results.
589
470
  */
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 {
471
+ aggregate(query: AggregateQuery): Promise<Array<Record<string, unknown>>>;
472
+ /** Whether the underlying adapter supports text search. */
473
+ isSearchable(): boolean;
474
+ /** Whether the adapter can filter on a given field (proxies adapter capability). */
475
+ canFilterField(fd: TDbFieldMeta): boolean;
476
+ /** Calendar-bucket units the adapter can group by (proxies adapter capability; empty = none). */
477
+ calendarBucketUnits(): ReadonlySet<BucketUnit>;
478
+ /** Whether the adapter can sort by a given field (proxies adapter capability). */
479
+ canSortField(fd: TDbFieldMeta): boolean;
480
+ /** Returns available search indexes from the adapter. */
481
+ getSearchIndexes(): TSearchIndexInfo[];
627
482
  /**
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.
483
+ * Full-text search with query translation and result reconstruction.
632
484
  */
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;
485
+ search<Q extends Uniquery<OwnProps, NavType>>(text: string, query: Q, indexName?: string): Promise<Array<DbResponse<DataType, NavType, Q>>>;
486
+ /**
487
+ * Full-text search with count for paginated search results.
488
+ */
489
+ searchWithCount<Q extends Uniquery<OwnProps, NavType>>(text: string, query: Q, indexName?: string): Promise<{
490
+ data: Array<DbResponse<DataType, NavType, Q>>;
491
+ count: number;
652
492
  }>;
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;
493
+ /** Whether the underlying adapter supports vector similarity search. */
494
+ isVectorSearchable(): boolean;
495
+ /**
496
+ * Vector similarity search with query translation and result reconstruction.
497
+ *
498
+ * Overloads:
499
+ * - `vectorSearch(vector, query?)` — uses default vector index
500
+ * - `vectorSearch(indexName, vector, query?)` — targets a specific vector index
501
+ */
502
+ vectorSearch<Q extends Uniquery<OwnProps, NavType>>(vectorOrIndex: number[] | string, maybeVectorOrQuery?: number[] | Q, maybeQuery?: Q): Promise<Array<DbResponse<DataType, NavType, Q>>>;
503
+ /**
504
+ * Vector similarity search with count for paginated results.
505
+ *
506
+ * Overloads:
507
+ * - `vectorSearchWithCount(vector, query?)` — uses default vector index
508
+ * - `vectorSearchWithCount(indexName, vector, query?)` — targets a specific vector index
509
+ */
510
+ vectorSearchWithCount<Q extends Uniquery<OwnProps, NavType>>(vectorOrIndex: number[] | string, maybeVectorOrQuery?: number[] | Q, maybeQuery?: Q): Promise<{
511
+ data: Array<DbResponse<DataType, NavType, Q>>;
512
+ count: number;
672
513
  }>;
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;
514
+ /** Resolves overloaded vector search arguments into canonical form. */
515
+ private _resolveVectorSearchArgs;
516
+ /** Whether the underlying adapter supports geospatial search. */
517
+ isGeoSearchable(): boolean;
518
+ /**
519
+ * Distance-ranked geospatial search (mirrors {@link vectorSearch}).
520
+ * Results are sorted by distance ascending; each row carries a computed
521
+ * `$distance` field (meters from the query point). `$maxDistance` /
522
+ * `$minDistance` (meters) ride in `query.controls`; user `$sort` is rejected.
523
+ *
524
+ * Overloads:
525
+ * - `geoSearch(point, query?)` — uses the table's only geo index
526
+ * - `geoSearch(indexName, point, query?)` — targets a specific geo index
527
+ */
528
+ geoSearch<Q extends Uniquery<OwnProps, NavType>>(pointOrIndex: [number, number] | string, maybePointOrQuery?: [number, number] | Q, maybeQuery?: Q): Promise<Array<DbResponse<DataType, NavType, Q> & {
529
+ $distance: number;
530
+ }>>;
531
+ /**
532
+ * Distance-ranked geospatial search with count for paginated results.
533
+ *
534
+ * Overloads:
535
+ * - `geoSearchWithCount(point, query?)` — uses the table's only geo index
536
+ * - `geoSearchWithCount(indexName, point, query?)` — targets a specific geo index
537
+ */
538
+ geoSearchWithCount<Q extends Uniquery<OwnProps, NavType>>(pointOrIndex: [number, number] | string, maybePointOrQuery?: [number, number] | Q, maybeQuery?: Q): Promise<{
539
+ data: Array<DbResponse<DataType, NavType, Q> & {
540
+ $distance: number;
541
+ }>;
542
+ count: number;
543
+ }>;
544
+ /** Resolves overloaded geo search arguments into canonical form. */
545
+ private _resolveGeoSearchArgs;
546
+ /** Shared geoSearch validation + query translation. */
547
+ private _prepareGeoSearch;
797
548
  /**
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.
549
+ * Finds a single record by any type-compatible identifier — primary key
550
+ * or single-field unique index.
551
+ * The return type excludes nav props unless `$with` is provided in controls.
552
+ *
553
+ * ```typescript
554
+ * // Without relations — nav props stripped from result
555
+ * const user = await table.findById('123')
556
+ *
557
+ * // With relations — only requested nav props appear
558
+ * const user = await table.findById('123', { controls: { $with: [{ name: 'posts' }] } })
559
+ * ```
802
560
  */
803
- readonly rows: Row[];
804
- /** Parallel to `rows`: expected version lifted from `$cas`, or `undefined`. */
805
- readonly expectedVersions: ReadonlyArray<number | undefined>;
561
+ findById<Q extends {
562
+ controls?: UniqueryControls<OwnProps, NavType>;
563
+ } = Record<string, never>>(id: IdType, query?: Q): Promise<DbResponse<DataType, NavType, Q> | null>;
806
564
  /**
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.
565
+ * Resolve an id value (scalar or object) into a {@link FilterExpr} using the
566
+ * same identification resolution as {@link findById}. Public so callers can
567
+ * AND-combine the id-filter with a row-level read overlay before issuing
568
+ * `findOne` (avoiding the existence leak that `findById` would cause).
810
569
  */
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 {
570
+ resolveIdFilter(id: unknown): FilterExpr | null;
833
571
  /**
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.
572
+ * Resolve an id value into a filter expression.
573
+ *
574
+ * When `preferredId` differs from the PK, scalar ids resolve only against
575
+ * the preferred field (deterministic addressing). Otherwise scalars try PK
576
+ * + every single-field unique index; objects try PK + compound unique
577
+ * indexes.
837
578
  */
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;
579
+ protected _resolveIdFilter(id: unknown): FilterExpr | null;
580
+ /** Build a single-key filter from `idObj` over `fields`, or null if any field is missing/incompatible. */
581
+ private _tryCompoundFilter;
844
582
  /**
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.
583
+ * Attempts to build a single-field filter `{ field: preparedId }`.
851
584
  */
852
- guard?: TDbWriteGuard<Row>;
853
- }
854
- /** Options of `deleteOne`. */
855
- interface TDeleteOptions<Row = Record<string, unknown>> {
585
+ private _tryFieldFilter;
856
586
  /**
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.
587
+ * Public entry point for relation loading. Used by adapters for nested $with delegation.
860
588
  */
861
- guard?: TDbRemoveGuard<Row>;
589
+ loadRelations(rows: Array<Record<string, unknown>>, withRelations: WithRelation[]): Promise<void>;
590
+ /**
591
+ * Finds the FK entry that connects a `@db.rel.to` relation to its target.
592
+ * Thin wrapper — delegates to relation-loader for shared use with db-table.ts write path.
593
+ */
594
+ protected _findFKForRelation(relation: TDbRelation): {
595
+ localFields: string[];
596
+ targetFields: string[];
597
+ } | undefined;
598
+ /**
599
+ * Finds a FK on a remote table that points back to this table.
600
+ * Thin wrapper — delegates to relation-loader for shared use with db-table.ts write path.
601
+ */
602
+ protected _findRemoteFK(targetTable: {
603
+ foreignKeys: ReadonlyMap<string, TDbForeignKey>;
604
+ }, thisTableName: string, alias?: string): TDbForeignKey | undefined;
862
605
  }
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
606
  //#endregion
872
607
  //#region src/base-adapter.d.ts
608
+ /** Every calendar-bucket unit — what an adapter that renders them all returns from `calendarBucketUnits()`. */
609
+ declare const ALL_BUCKET_UNITS: ReadonlySet<BucketUnit>;
873
610
  /**
874
611
  * Abstract base class for database adapters.
875
612
  *
@@ -1022,6 +759,21 @@ declare abstract class BaseDbAdapter {
1022
759
  * to generate the value client-side or leave it for the DB.
1023
760
  */
1024
761
  nativeDefaultFns(): ReadonlySet<TDbDefaultFn>;
762
+ /**
763
+ * Calendar-bucket units (`{ $bucket, $field }` in an aggregate `$select`)
764
+ * this adapter can group by, over IANA time zones. Empty (the default) =
765
+ * calendar buckets unsupported: the core rejects them with
766
+ * `BUCKET_NOT_SUPPORTED` before dispatch, and moost-db's `/meta` advertises
767
+ * no bucketable field.
768
+ *
769
+ * A set rather than a boolean (like {@link nativeDefaultFns}) so a unit can
770
+ * be adopted adapter by adapter. An adapter that returns a unit must group
771
+ * by the bucket alias in `$groupBy` — see `controls.$select.buckets`
772
+ * (`TResolvedBucket`: physical `field`, source `fd`) — and return the
773
+ * `YYYY-MM-DD` label of the bucket's first local day (null for a null or
774
+ * out-of-range source, uniqu's `bucketLabel` semantics). Since 0.1.132.
775
+ */
776
+ calendarBucketUnits(): ReadonlySet<BucketUnit>;
1025
777
  /**
1026
778
  * Whether this adapter enforces foreign key constraints natively.
1027
779
  * When `true`, the generic layer skips application-level cascade/setNull
@@ -1042,6 +794,9 @@ declare abstract class BaseDbAdapter {
1042
794
  * Used by `AsDbReadableController.buildMetaResponse()` to gate the
1043
795
  * `filterable` flag exposed to UIs — the adapter's answer is a hard gate
1044
796
  * even when the field carries `@db.column.filterable`.
797
+ *
798
+ * Vetoes value comparison only — a sole-`$exists` entry needs just a stored
799
+ * column (`canFilterLeaf`).
1045
800
  */
1046
801
  canFilterField(fd: TDbFieldMeta): boolean;
1047
802
  /**
@@ -1195,8 +950,15 @@ declare abstract class BaseDbAdapter {
1195
950
  */
1196
951
  getSearchIndexes(): TSearchIndexInfo[];
1197
952
  /**
1198
- * Whether this adapter supports text search.
1199
- * Default: `true` when {@link getSearchIndexes} returns any entries.
953
+ * Whether this adapter can run TEXT search — `search()`, `searchWithCount()`
954
+ * and the grouped `$search` path all gate on it. Vector capability is a
955
+ * separate predicate, {@link isVectorSearchable}.
956
+ *
957
+ * Default: `true` when {@link getSearchIndexes} lists at least one non-vector
958
+ * index. Vector entries are published there for the index picker, but a
959
+ * vector index answers {@link vectorSearch} and nothing else, so counting one
960
+ * here would claim a capability the adapter does not have. An entry without
961
+ * `type` counts as text — adapters predating the field only listed text.
1200
962
  */
1201
963
  isSearchable(): boolean;
1202
964
  /**
@@ -1532,487 +1294,931 @@ declare abstract class BaseDbAdapter {
1532
1294
  formatValue?(field: TDbFieldMeta): TValueFormatterPair | ((value: unknown) => unknown) | undefined;
1533
1295
  }
1534
1296
  //#endregion
1535
- //#region src/strategies/field-mapping.d.ts
1297
+ //#region src/table/table-metadata.d.ts
1536
1298
  /**
1537
- * Strategy for mapping data between logical field shapes and physical storage.
1538
- * Two implementations: {@link DocumentFieldMapper} (nested objects, NoSQL)
1539
- * and `RelationalFieldMapper` (flattened columns, SQL).
1299
+ * Finds the nearest ancestor of `path` that belongs to `set`.
1300
+ * Used by both the build pipeline (in `_classifyFields`) and
1301
+ * runtime reconstruction on the Readable.
1540
1302
  */
1541
- declare abstract class FieldMappingStrategy {
1542
- abstract reconstructFromRead(row: Record<string, unknown>, meta: TableMetadata): Record<string, unknown>;
1543
- abstract translateQuery(query: Uniquery, meta: TableMetadata): DbQuery;
1544
- abstract translateAggregateQuery(query: AggregateQuery, meta: TableMetadata): DbQuery;
1545
- /**
1546
- * Recursively walks a filter expression, applying `@db.column` key renames
1547
- * via `columnMap` and adapter-specific value formatting via `formatFilterValue`.
1548
- *
1549
- * The relational mapper overrides this to use `leafByLogical` for deeper
1550
- * key resolution (flattened nested paths).
1551
- */
1552
- translateFilter(filter: FilterExpr, meta: TableMetadata): FilterExpr;
1553
- abstract prepareForWrite(payload: Record<string, unknown>, meta: TableMetadata, adapter: BaseDbAdapter): Record<string, unknown>;
1554
- abstract translatePatchKeys(update: Record<string, unknown>, meta: TableMetadata): Record<string, unknown>;
1555
- /**
1556
- * Reverse-maps `@db.column` renames on a row read from storage.
1557
- * Renames physical keys back to logical names in-place.
1558
- */
1559
- protected reverseColumnRenames(row: Record<string, unknown>, meta: TableMetadata): void;
1303
+ declare function findAncestorInSet(path: string, set: ReadonlySet<string>): string | undefined;
1304
+ /** Returns true if the annotated type IS the `db.geoPoint` primitive (tag-based). */
1305
+ declare function isGeoPointType(fieldType: TAtscriptAnnotatedType): boolean;
1306
+ /**
1307
+ * Returns true if the annotated type is acceptable for `@db.index.geo`:
1308
+ * the `db.geoPoint` primitive or a structurally identical `number[]`
1309
+ * (excluding `db.vector`, which is semantically an embedding).
1310
+ */
1311
+ declare function isGeoIndexableType(fieldType: TAtscriptAnnotatedType): boolean;
1312
+ /**
1313
+ * Computed metadata for a database table or view.
1314
+ *
1315
+ * Contains all field metadata, physical mapping indexes, relation definitions,
1316
+ * and constraint information derived from Atscript annotations. Built lazily
1317
+ * on first access via {@link build}, then immutable.
1318
+ *
1319
+ * This class owns the build pipeline that was previously part of
1320
+ * `AtscriptDbReadable._flatten()`. The Readable delegates all metadata
1321
+ * access to this class.
1322
+ */
1323
+ declare class TableMetadata {
1324
+ readonly nestedObjects: boolean;
1325
+ flatMap: Map<string, TAtscriptAnnotatedType>;
1326
+ fieldDescriptors: readonly TDbFieldMeta[];
1327
+ primaryKeys: string[];
1328
+ preferredId: string[];
1329
+ originalMetaIdFields: string[];
1330
+ indexes: Map<string, TDbIndex>;
1331
+ foreignKeys: Map<string, TDbForeignKey>;
1332
+ relations: Map<string, TDbRelation>;
1333
+ navFields: Set<string>;
1334
+ ignoredFields: Set<string>;
1335
+ uniqueProps: Set<string>;
1336
+ defaults: Map<string, TDbDefaultValue>;
1337
+ columnMap: Map<string, string>;
1338
+ dimensions: string[];
1339
+ measures: string[];
1340
+ /** Logical field name annotated with `@db.column.version`, if any. */
1341
+ versionField?: string;
1342
+ /** path → sibling-ref path for `@db.amount.currency.ref` / `@db.unit.ref`. */
1343
+ quantityRefByField: Map<string, string>;
1344
+ /** Logical paths annotated with `@db.encrypted` — stored as one opaque ciphertext column. */
1345
+ encryptedFields: Set<string>;
1346
+ pathToPhysical: Map<string, string>;
1347
+ physicalToPath: Map<string, string>;
1348
+ flattenedParents: Set<string>;
1349
+ jsonFields: Set<string>;
1350
+ selectExpansion: Map<string, string[]>;
1351
+ booleanFields: Set<string>;
1352
+ decimalFields: Set<string>;
1353
+ allPhysicalFields: string[];
1354
+ /** Precomputed parent path → child physical column names for fast null-setting. */
1355
+ childrenByParent: Map<string, string[]>;
1356
+ /** Precomputed parent path → optional child logical paths (replace-strategy null-fill in the patch decomposer). */
1357
+ optionalLeavesByLogicalParent: Map<string, string[]>;
1358
+ requiresMappings: boolean;
1359
+ /** True when the only mappings needed are simple `@db.column` renames (no nesting/JSON). */
1360
+ onlyColumnRenames: boolean;
1361
+ toStorageFormatters?: Map<string, (value: unknown) => unknown>;
1362
+ fromStorageFormatters?: Map<string, (value: unknown) => unknown>;
1363
+ /** Leaf field descriptors indexed by physical column name (read path). */
1364
+ leafByPhysical: Map<string, TDbFieldMeta>;
1365
+ /** Leaf field descriptors indexed by logical path (write/patch/filter paths). */
1366
+ leafByLogical: Map<string, TDbFieldMeta>;
1560
1367
  /**
1561
- * Coerces field values from storage representation to JS types
1562
- * (booleans from 0/1, decimals from number to string).
1368
+ * Non-ignored field descriptors keyed by logical path, excluding navigation
1369
+ * relations and their descendants. Unlike `leafByLogical` (relational
1370
+ * adapters only) this is built for every adapter, so the core path guard
1371
+ * (`guardPaths`) can answer "does this path have physical storage here?"
1372
+ * on nested-object adapters too.
1563
1373
  */
1564
- protected coerceFieldValues(row: Record<string, unknown>, meta: TableMetadata): Record<string, unknown>;
1374
+ descriptorByPath: Map<string, TDbFieldMeta>;
1565
1375
  /**
1566
- * Applies adapter-specific fromStorage formatting to a row read from the database.
1567
- * Converts storage representations back to JS values (e.g. Date → epoch ms).
1376
+ * Logical paths stored as a single JSON column (`storage === 'json'`,
1377
+ * non-ignored descriptors). Retained after build — unlike the build-time
1378
+ * `jsonFields` set — so the path guard can classify JSON descendants on
1379
+ * relational adapters. Empty on nested-object adapters (they keep native
1380
+ * dotted paths as descriptors).
1568
1381
  */
1569
- protected applyFromStorageFormatters(row: Record<string, unknown>, meta: TableMetadata): Record<string, unknown>;
1382
+ jsonParents: ReadonlySet<string>;
1570
1383
  /**
1571
- * Sets a value at a dot-notation path, creating intermediate objects as needed.
1384
+ * Logical paths holding a JSON value (`isJsonValueField`: JSON-stored, `json`
1385
+ * or `array` design type; non-ignored, nav-free descriptors) — a timestamp
1386
+ * beneath one is never a calendar-bucket source (`jsonValueAncestor`).
1572
1387
  */
1573
- protected setNestedValue(obj: Record<string, unknown>, dotPath: string, value: unknown): void;
1388
+ jsonValueParents: ReadonlySet<string>;
1389
+ /** Every field descriptor's `physicalName` — reserved names a bucket alias may not take. */
1390
+ physicalNames: ReadonlySet<string>;
1391
+ private _built;
1392
+ private _identifications?;
1393
+ private _collateMap;
1394
+ private _columnFromMap;
1395
+ constructor(nestedObjects: boolean);
1396
+ get isBuilt(): boolean;
1574
1397
  /**
1575
- * If all children of a flattened parent are null, collapse the parent to null.
1398
+ * Logical field path → its physical path in document storage (nested
1399
+ * objects kept inline). `@db.column` renames apply to the annotated key,
1400
+ * and a document renames the TOP-LEVEL key only — nested keys are stored
1401
+ * as-is — so a dotted path under a renamed top-level object renames its
1402
+ * first segment: `profile.bio` under `@db.column 'prof'` → `prof.bio`.
1576
1403
  */
1577
- protected reconstructNullParent(obj: Record<string, unknown>, parentPath: string, meta: TableMetadata): void;
1404
+ documentPath(path: string): string;
1578
1405
  /**
1579
- * Applies adapter-specific value formatting to a single filter value.
1580
- * Handles direct values, operator objects ({$gt: v}), and $in/$nin arrays.
1406
+ * Runs the full metadata compilation pipeline. Called once by
1407
+ * `AtscriptDbReadable._ensureBuilt()` on first metadata access.
1408
+ *
1409
+ * Pipeline steps:
1410
+ * 1. `adapter.onBeforeFlatten(type)` — adapter hook
1411
+ * 2. `flattenAnnotatedType()` — collect field tuples, detect nav fields eagerly
1412
+ * 3. Replay non-nav-descendant tuples through annotation scanning + adapter.onFieldScanned
1413
+ * 4. Classify fields and build path maps (skipped for nested-objects adapters)
1414
+ * 5. `adapter.getMetadataOverrides()` → `_applyOverrides()` (PK/unique/inject adjustments)
1415
+ * 6. Build field descriptors (TDbFieldMeta[])
1416
+ * 7. Build leaf field indexes (skipped for nested-objects adapters)
1417
+ * 8. Finalize indexes (resolve field names to physical)
1418
+ * 9. `adapter.onAfterFlatten()` — adapter hook (read-only bookkeeping)
1419
+ * 10. Build allPhysicalFields list
1581
1420
  */
1582
- protected formatFilterValue(physicalName: string, value: unknown, meta: TableMetadata): unknown;
1421
+ build(type: TAtscriptAnnotatedType<TAtscriptTypeObject>, adapter: BaseDbAdapter, logger: TGenericLogger): void;
1583
1422
  /**
1584
- * Applies adapter-specific value formatting to prepared (physical-named) data.
1423
+ * Applies adapter-provided metadata overrides atomically.
1424
+ * Processing order: injectFields → removePrimaryKeys → addPrimaryKeys → addUniqueFields.
1585
1425
  */
1586
- protected formatWriteValues(data: Record<string, unknown>, meta: TableMetadata): Record<string, unknown>;
1426
+ private _applyOverrides;
1587
1427
  /**
1588
- * Prepares primary key values and strips ignored fields.
1589
- * Shared pre-processing for both document and relational write paths.
1428
+ * Scans `@db.*` and `@meta.id` annotations on a field during flattening.
1590
1429
  */
1591
- protected prepareCommon(data: Record<string, unknown>, meta: TableMetadata, adapter: BaseDbAdapter): void;
1592
- }
1593
- /**
1594
- * Field mapper for document-oriented adapters (e.g. MongoDB).
1595
- * Nested objects are preserved as-is. Only applies column renames and
1596
- * value coercion.
1597
- */
1598
- declare class DocumentFieldMapper extends FieldMappingStrategy {
1599
- reconstructFromRead(row: Record<string, unknown>, meta: TableMetadata): Record<string, unknown>;
1600
- translateQuery(query: Uniquery, meta: TableMetadata): DbQuery;
1601
- translateAggregateQuery(query: AggregateQuery, meta: TableMetadata): DbQuery;
1602
- prepareForWrite(payload: Record<string, unknown>, meta: TableMetadata, adapter: BaseDbAdapter): Record<string, unknown>;
1603
- translatePatchKeys(update: Record<string, unknown>, meta: TableMetadata): Record<string, unknown>;
1604
- }
1605
- //#endregion
1606
- //#region src/encryption.d.ts
1607
- /**
1608
- * Configuration for field-level encryption at rest (`@db.encrypted`).
1609
- * Passed to `DbSpace` via the options bag.
1610
- */
1611
- interface TDbEncryptionOptions {
1612
- /** Key used for all new writes. */
1613
- defaultKeyId: string;
1430
+ private _scanGenericAnnotations;
1614
1431
  /**
1615
- * Key registry: keyId → 32-byte key (Buffer, or base64/hex/utf8 string).
1616
- * Decryption looks keys up by the keyId recorded in each value's envelope,
1617
- * so old keys stay in the registry for as long as data encrypted with them exists.
1432
+ * Build-time diagnostics for `@db.encrypted` (§6 of the field-encryption
1433
+ * spec). Mirrors the compile-time AnnotationSpec validation so models built
1434
+ * from pre-compiled types still fail fast.
1618
1435
  */
1619
- keys?: Record<string, string | Buffer>;
1436
+ private _validateEncryptedField;
1437
+ /** Build-time diagnostics for `@db.index.geo` (§3 of the geo-index spec). */
1438
+ private _validateGeoIndexField;
1439
+ private _addIndexField;
1620
1440
  /**
1621
- * Alternative/supplement to `keys`: async resolver (KMS, Vault, env indirection).
1622
- * Called once per keyId, result cached for the process lifetime.
1441
+ * Classifies each field as column, flattened, json, or parent-object.
1442
+ * Builds the bidirectional pathToPhysical / physicalToPath maps.
1623
1443
  */
1624
- resolveKey?: (keyId: string) => Promise<string | Buffer> | string | Buffer;
1444
+ private _classifyFields;
1445
+ /** Returns the `__`-separated parent prefix for a dot-separated path, or empty string for top-level paths. */
1446
+ private _flattenedPrefix;
1447
+ /** Nearest `@db.encrypted` ancestor of `path` (exclusive), or `undefined`. */
1625
1448
  /**
1626
- * What to do when a stored value is NOT a valid envelope (pre-existing
1627
- * plaintext rows, e.g. when @db.encrypted is added to a live column).
1628
- * - 'error' (default): fail the read with DbError("ENC_NOT_ENCRYPTED")
1629
- * - 'passthrough': return the raw value as-is (migration window mode);
1630
- * the value gets encrypted on its next write.
1449
+ * Indexes non-ignored descriptors by logical path and retains the JSON-parent
1450
+ * sets (plus every descriptor's physical name). Navigation relations and their descendants are skipped even when the
1451
+ * adapter keeps them as descriptors (nested-object adapters do) — they are
1452
+ * loaded with `$with`, never addressed as columns of this table.
1631
1453
  */
1632
- onUnencrypted?: "error" | "passthrough";
1633
- }
1634
- /** Decryption context — carried into error messages (never the key itself). */
1635
- interface TDecryptContext {
1636
- table?: string;
1637
- field?: string;
1638
- }
1639
- /**
1640
- * AES-256-GCM envelope encryption service for `@db.encrypted` fields.
1641
- *
1642
- * Owned by `DbSpace` and shared across all tables in the space. Values are
1643
- * `JSON.stringify`'d before encryption (type-exact round-trips) and stored as
1644
- * a single ASCII envelope string: `aes1$<keyId>$<iv>$<tag>$<ciphertext>`.
1645
- *
1646
- * Key material is validated eagerly at construction (`ENC_KEY_INVALID`);
1647
- * `resolveKey` lookups are cached per keyId for the process lifetime.
1648
- */
1649
- declare class DbEncryption {
1650
- readonly defaultKeyId: string;
1651
- readonly onUnencrypted: "error" | "passthrough";
1652
- private readonly _keys;
1653
- private readonly _resolveKey?;
1654
- private readonly _resolved;
1655
- constructor(options: TDbEncryptionOptions);
1656
- /** True when `value` looks like an encryption envelope produced by this service. */
1657
- isEnvelope(value: unknown): value is string;
1658
- /** Encrypts a JSON-serializable value into an envelope string using the default key. */
1659
- encrypt(value: unknown): Promise<string>;
1660
- /** Decrypts an envelope string back into its plaintext value. */
1661
- decrypt(envelope: string, ctx?: TDecryptContext): Promise<unknown>;
1662
- private _decryptFailed;
1663
- private _getKey;
1664
- }
1665
- //#endregion
1666
- //#region src/table/db-readable.d.ts
1667
- /**
1668
- * Extracts nav prop names from a query's `$with` array.
1669
- * Returns `never` when `$with` is absent → all nav props stripped from response.
1670
- */
1671
- type ExtractWith<Q> = Q extends {
1672
- controls: {
1673
- $with: Array<{
1674
- name: infer N extends string;
1675
- }>;
1676
- };
1677
- } ? N : never;
1678
- /**
1679
- * Computes the response type for a query:
1680
- * - Strips all nav props from the base DataType
1681
- * - Adds back only the nav props requested via `$with`
1682
- *
1683
- * When no `$with` is provided, result is `Omit<DataType, keyof NavType>`.
1684
- * When `$with: [{ name: 'author' }]`, result includes `author` from DataType.
1685
- * When the query type is not a literal (e.g. a variable typed as `Uniquery`),
1686
- * falls back to `DataType` (all nav props optional, as declared).
1687
- */
1688
- 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>;
1689
- /**
1690
- * Resolves the design type from an annotated type.
1691
- * Encapsulates the `kind === ''` check and fallback logic that
1692
- * otherwise trips up every adapter author.
1693
- *
1694
- * For union types (e.g., from flattened `{...} | {...}` objects):
1695
- * - If all members resolve to the same type → returns that type (strong type)
1696
- * - If members disagree → returns `'union'` (out of scope for type management)
1697
- */
1698
- declare function resolveDesignType(fieldType: TAtscriptAnnotatedType): string;
1699
- /**
1700
- * Shared read-only database abstraction driven by Atscript annotations.
1701
- *
1702
- * Contains all field metadata computation, read operations, query translation,
1703
- * relation loading, and result reconstruction. Extended by both
1704
- * {@link AtscriptDbTable} (adds write operations) and {@link AtscriptDbView}
1705
- * (adds view plan/DDL).
1706
- */
1707
- 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>> {
1708
- protected readonly _type: T;
1709
- protected readonly adapter: A;
1710
- protected readonly logger: TGenericLogger;
1711
- protected readonly _tableResolver?: TTableResolver | undefined;
1712
- /** Resolved table/collection/view name. */
1713
- readonly tableName: string;
1714
- /** Database schema/namespace from `@db.schema` (if set). */
1715
- readonly schema: string | undefined;
1716
- /** Sync method from `@db.sync.method` ('drop' | 'recreate' | undefined). */
1717
- protected readonly _syncMethod: "drop" | "recreate" | undefined;
1718
- /** Previous table/view name from `@db.table.renamed` or `@db.view.renamed`. */
1719
- readonly renamedFrom: string | undefined;
1720
- /** Computed metadata for this table/view. Built lazily on first access. */
1721
- protected readonly _meta: TableMetadata;
1722
- /** Strategy for mapping between logical field shapes and physical storage. */
1723
- protected readonly _fieldMapper: FieldMappingStrategy;
1724
- protected _writeTableResolver?: TWriteTableResolver;
1725
- /** Encryption service for `@db.encrypted` fields — set by `DbSpace` from its options. */
1726
- protected _encryption?: DbEncryption;
1727
- private _metaIdPhysical;
1728
- constructor(_type: T, adapter: A, logger?: TGenericLogger, _tableResolver?: TTableResolver | undefined);
1454
+ private _buildGuardIndexes;
1729
1455
  /**
1730
- * Sets the encryption service used for `@db.encrypted` fields.
1731
- * Called by `DbSpace` after table/view creation when the space was
1732
- * configured with an `encryption` options block.
1456
+ * Indexes `fieldDescriptors` into two lookup maps for unified
1457
+ * read/write field classification in the RelationalFieldMapper.
1733
1458
  */
1734
- setEncryption(encryption: DbEncryption | undefined): void;
1735
- /** Ensures metadata is built. Called before any metadata access. */
1736
- protected _ensureBuilt(): void;
1459
+ private _buildLeafIndexes;
1737
1460
  /**
1738
- * Built table metadata. Triggers a lazy build on first access — safe to call
1739
- * from peer tables that need this one's relations / nav fields before any
1740
- * operation has run against it directly.
1461
+ * Builds field descriptors, physical-name lookup, and value formatters.
1462
+ * Called once during build() — everything it needs
1463
+ * (flatMap, indexes, columnMap, etc.) is already populated.
1741
1464
  */
1742
- getMetadata(): TableMetadata;
1743
- protected _ensureSearchable(): void;
1744
- /** Engine-agnostic query-time guards (encrypted-field refs, $geoWithin shape). */
1745
- protected _guardQuery(query: Uniquery | undefined): void;
1746
- private _encryptedPathsCache?;
1747
- /** Pre-split `encryptedFields` paths — computed once, reused on every read/write. */
1748
- protected get _encryptedPaths(): Array<{
1749
- path: string;
1750
- segments: string[];
1751
- leaf: string;
1752
- }>;
1465
+ private _buildFieldDescriptors;
1753
1466
  /**
1754
- * Walks all but the last of `segments` down from `root`, returning the
1755
- * object holding the leaf — or `undefined` when the path is unreachable
1756
- * (a missing, non-object, or array step). With `cloneParents`, every
1757
- * traversed object is shallow-cloned and re-linked so caller-shared
1758
- * nested objects are never mutated.
1467
+ * Resolves `fkTargetField` for FK fields in field descriptors.
1759
1468
  */
1760
- protected _walkToLeafParent(root: Record<string, unknown>, segments: string[], cloneParents: boolean): Record<string, unknown> | undefined;
1469
+ private _resolveFkTargetFields;
1470
+ private _finalizeIndexes;
1761
1471
  /**
1762
- * Decrypts `@db.encrypted` fields on reconstructed rows (in place).
1763
- * Non-envelope stored values follow the configured `onUnencrypted` policy.
1472
+ * Captures legitimate row-identifier shapes from the metadata: primary key
1473
+ * (when present) followed by every unique index. Must run BEFORE
1474
+ * `_finalizeIndexes` rewrites `index.fields[i].name` from logical to
1475
+ * physical, so the field lists stay logical.
1476
+ *
1477
+ * Sources:
1478
+ * - `this.primaryKeys` — composite-aware PK (one identification covering
1479
+ * the PK columns).
1480
+ * - `this.indexes` — user-declared `@db.index.unique` (single or compound).
1481
+ * - `this.uniqueProps` — single-field uniques contributed by adapter
1482
+ * overrides (`addUniqueFields`); these aren't reflected in
1483
+ * `this.indexes` but still legitimate addressing identifications.
1484
+ * Deduped against any single-field unique index already captured.
1764
1485
  */
1765
- protected _decryptRows(rows: Array<Record<string, unknown>>): Promise<void>;
1766
- /** Whether this readable is a view (overridden in AtscriptDbView). */
1767
- get isView(): boolean;
1768
- /** Returns the underlying adapter with its concrete type preserved. */
1769
- getAdapter(): A;
1770
- /** The raw annotated type. */
1771
- get type(): TAtscriptAnnotatedType<TAtscriptTypeObject>;
1772
- /** Lazily-built flat map of all fields (dot-notation paths → annotated types). */
1773
- get flatMap(): Map<string, TAtscriptAnnotatedType>;
1774
- /** All computed indexes from `@db.index.*` annotations. */
1775
- get indexes(): Map<string, TDbIndex>;
1776
- /** Primary key field names from `@meta.id`. */
1777
- get primaryKeys(): readonly string[];
1778
- /** Preferred row identifier field names. Defaults to primary keys. */
1779
- get preferredId(): readonly string[];
1780
- /** Legitimate row-identifier shapes (primary key + every unique index). */
1781
- get identifications(): readonly TIdentification[];
1486
+ private _buildIdentifications;
1487
+ /** Legitimate row-identifier shapes — primary key first, then each unique index. */
1488
+ getIdentifications(): readonly TIdentification[];
1489
+ private _resolvePreferredId;
1490
+ }
1491
+ //#endregion
1492
+ //#region src/types.d.ts
1493
+ /** Controls with resolved projection. Used in the adapter interface. */
1494
+ interface DbControls extends Omit<UniqueryControls, "$select"> {
1495
+ $select?: UniquSelect;
1496
+ }
1497
+ /** Query object with resolved projection. Passed to adapter methods. */
1498
+ interface DbQuery {
1499
+ filter: FilterExpr;
1500
+ controls: DbControls;
1501
+ /** Pre-computed query insights (field → operators). Adapters may use this to apply query-time behaviour (e.g. collation). */
1502
+ insights?: UniqueryInsights;
1503
+ }
1504
+ /** Describes an available search index exposed by a database adapter. */
1505
+ interface TSearchIndexInfo {
1506
+ /** Index name. Empty string or 'DEFAULT' for the default index. */
1507
+ name: string;
1508
+ /** Human-readable label for UI display. */
1509
+ description?: string;
1510
+ /** Index type: text search or vector similarity search. */
1511
+ type?: "text" | "vector";
1512
+ }
1513
+ /** Relation summary in a meta response. */
1514
+ interface TRelationInfo {
1515
+ name: string;
1516
+ direction: "to" | "from" | "via";
1517
+ isArray: boolean;
1518
+ }
1519
+ /** Per-field capability flags in a meta response. */
1520
+ interface TFieldMeta {
1521
+ sortable: boolean;
1522
+ filterable: boolean;
1782
1523
  /**
1783
- * Physical column name of the single `@meta.id` field, or `null` when the
1784
- * schema has zero or multiple `@meta.id` fields. Used by adapters to return
1785
- * the user's logical ID instead of the DB-generated one on insert.
1786
- *
1787
- * @internal Adapter-facing surface; not part of the consumer API.
1524
+ * Present only when `filterable` is `false` but narrower predicates still
1525
+ * pass the gate: their operators — `$exists` on a relational adapter's JSON
1526
+ * / array column, `$geoWithin` on a geoPoint of a geo-searchable adapter.
1527
+ * Since 0.1.132.
1788
1528
  */
1789
- get metaIdPhysical(): string | null;
1529
+ filterOps?: string[];
1530
+ /** Present (true) when the field is `@db.encrypted` — stored as ciphertext at rest. */
1531
+ encrypted?: boolean;
1532
+ /** Present (true) when the field carries a `@db.index.geo` geospatial index. */
1533
+ geo?: boolean;
1790
1534
  /**
1791
- * Physical column name of the field annotated with `@db.column.version`, or
1792
- * `undefined` when the table has no version column. Used by adapters and the
1793
- * REST integration to drive optimistic concurrency control (OCC).
1535
+ * Present (true) when the field is write-only over HTTP (`@db.writeOnly` or
1536
+ * stamped by a permission overlay): settable in write payloads, never
1537
+ * present in read responses. UIs render it as a set-only input.
1794
1538
  */
1795
- get versionColumn(): string | undefined;
1796
- /** Dimension fields from `@db.column.dimension`. */
1797
- get dimensions(): readonly string[];
1798
- /** Measure fields from `@db.column.measure`. */
1799
- get measures(): readonly string[];
1800
- /** Sync method for structural changes: 'drop' (lossy), 'recreate' (lossless), or undefined (manual). */
1801
- get syncMethod(): "drop" | "recreate" | undefined;
1802
- /** Logical → physical column name mapping from `@db.column`. */
1803
- get columnMap(): ReadonlyMap<string, string>;
1804
- /** Default values from `@db.default.*`. */
1805
- get defaults(): ReadonlyMap<string, TDbDefaultValue>;
1806
- /** Fields excluded from DB via `@db.ignore`. */
1807
- get ignoredFields(): ReadonlySet<string>;
1808
- /** Navigational fields (`@db.rel.to` / `@db.rel.from`) — not stored as columns. */
1809
- get navFields(): ReadonlySet<string>;
1810
- /** Physical field names used to invert exclude-mode `$select` into a SELECT list. */
1811
- get allPhysicalFields(): readonly string[];
1812
- /** Single-field unique index properties. */
1813
- get uniqueProps(): ReadonlySet<string>;
1814
- /** Foreign key constraints from `@db.rel.FK` annotations. */
1815
- get foreignKeys(): ReadonlyMap<string, TDbForeignKey>;
1816
- /** Navigational relation metadata from `@db.rel.to` / `@db.rel.from`. */
1817
- get relations(): ReadonlyMap<string, TDbRelation>;
1818
- /** The underlying database adapter instance. */
1819
- get dbAdapter(): A;
1539
+ writeOnly?: boolean;
1820
1540
  /**
1821
- * Enables or disables verbose (debug-level) DB call logging for this table/view.
1822
- * When disabled (default), no log strings are constructed — zero overhead.
1541
+ * Present (true) when the field is index-backed (explicit `@db.index*`,
1542
+ * primary key or unique field). Advisory only — a hint for UIs that want to
1543
+ * steer users toward cheap sort keys; it never affects whether a `$sort`
1544
+ * is accepted (`sortable` does). Since 0.1.128.
1823
1545
  */
1824
- setVerbose(enabled: boolean): void;
1825
- /** Precomputed logical dot-path → physical column name map. */
1826
- get pathToPhysical(): ReadonlyMap<string, string>;
1827
- /** Precomputed physical column name → logical dot-path map (inverse). */
1828
- get physicalToPath(): ReadonlyMap<string, string>;
1829
- /** Descriptor for the primary ID field(s). */
1830
- getIdDescriptor(): TIdDescriptor;
1546
+ indexed?: boolean;
1831
1547
  /**
1832
- * Pre-computed field metadata for adapter use.
1548
+ * Present (true) exactly when a calendar bucket over this field passes the
1549
+ * gate: a physically filterable `number.timestamp` field (a dimension, when
1550
+ * the table declares dimensions) on an adapter with calendar buckets
1551
+ * (`bucketUnits`). Since 0.1.132.
1833
1552
  */
1834
- get fieldDescriptors(): readonly TDbFieldMeta[];
1553
+ bucketable?: true;
1554
+ }
1555
+ /** Built-in CRUD operation names; map 1:1 to public method names. */
1556
+ type TCrudOp = "query" | "pages" | "one" | "geo" | "insert" | "update" | "replace" | "remove";
1557
+ /**
1558
+ * CRUD permissions advertised in `/meta`. Key absent → operation is denied or
1559
+ * not exposed. Key present → operation is allowed; the `string[]` value is the
1560
+ * accepted UniQuery control whitelist for read ops (`[]` for write ops, which
1561
+ * take no controls — presence still signals "allowed").
1562
+ */
1563
+ type TCrudPermissions = Partial<Record<TCrudOp, string[]>>;
1564
+ /** Response payload for `GET /meta`. */
1565
+ interface TMetaResponse {
1566
+ searchable: boolean;
1567
+ vectorSearchable: boolean;
1568
+ /** Whether the adapter supports `geoSearch()` AND the table declares a geo index. */
1569
+ geoSearchable?: boolean;
1570
+ searchIndexes: TSearchIndexInfo[];
1571
+ primaryKeys: string[];
1572
+ preferredId: string[];
1573
+ relations: TRelationInfo[];
1574
+ fields: Record<string, TFieldMeta>;
1575
+ type: TSerializedAnnotatedType;
1576
+ actions: TDbActionInfo[];
1577
+ crud: TCrudPermissions;
1835
1578
  /**
1836
- * Resolves whether `path` references a real field — directly via `flatMap`
1837
- * or transitively through a nav relation by recursing into the target
1838
- * table. Defense-in-depth for query-path validation: `flattenAnnotatedType`
1839
- * still truncates real self-referential cycles, so paths like
1840
- * `parent.parent.name` on a self-ref schema would miss `flatMap.has` but
1841
- * remain valid field references on the target.
1579
+ * Physical column name annotated with `@db.column.version`, when the table
1580
+ * opts into optimistic concurrency control (OCC). Absent for tables without
1581
+ * the annotation, i.e. last-write-wins (default) behavior.
1582
+ */
1583
+ versionColumn?: string;
1584
+ /**
1585
+ * Calendar-bucket units the adapter can group by (`{ $bucket }` in an
1586
+ * aggregate `$select`, URL `bucket(field,unit,…)`); omitted when it has
1587
+ * none. Since 0.1.132.
1588
+ */
1589
+ bucketUnits?: BucketUnit[];
1590
+ }
1591
+ /** Where the action applies on the UI. */
1592
+ type TDbActionLevel = "table" | "row" | "rows";
1593
+ /**
1594
+ * Semantic intent the UI maps to its own visual language (color, prominence).
1595
+ *
1596
+ * Suggested visual prominence (most → least): `negative` > `warning` > `primary`
1597
+ * > `positive` > `secondary`. Use `negative` for destructive ops (delete, purge),
1598
+ * `warning` for risky-but-non-destructive ops (retry payment, force recompute,
1599
+ * reset state), `primary` for the headline action, `positive` for benign
1600
+ * confirmations (approve, publish), `secondary` for everything else.
1601
+ */
1602
+ type TDbActionIntent = "positive" | "negative" | "warning" | "primary" | "secondary";
1603
+ /** How the UI client should handle the action when invoked. */
1604
+ type TDbActionProcessor = "backend" | "navigate" | "custom";
1605
+ /**
1606
+ * Single action descriptor in the `/meta` envelope. Flat shape — `processor`
1607
+ * is a string discriminator; `value` is its sibling and is always populated.
1608
+ *
1609
+ * - `processor: 'backend'` — UI POSTs to `value` (full HTTP path).
1610
+ * - `processor: 'navigate'` — UI routes to `value` (URL template; `$1` is the row PK).
1611
+ * - `processor: 'custom'` — UI dispatches `value` as an event name (defaults to action `name`).
1612
+ */
1613
+ interface TDbActionInfo {
1614
+ name: string;
1615
+ label: string;
1616
+ level: TDbActionLevel;
1617
+ processor: TDbActionProcessor;
1618
+ value: string;
1619
+ icon?: string;
1620
+ intent?: TDbActionIntent;
1621
+ description?: string;
1622
+ order?: number;
1623
+ default?: boolean;
1624
+ /**
1625
+ * Confirmation prompt copy. String form is shown verbatim. Tuple form is
1626
+ * `[singular, plural]`: the UI picks `[0]` when the action will execute
1627
+ * against a single PK (always for `'row'`-level; for `'rows'`-level when the
1628
+ * current selection has exactly one PK) and `[1]` otherwise.
1842
1629
  *
1843
- * Cycle-safe via a visited set keyed on `<tableName>:<navField>`.
1630
+ * Placeholder substitution is UI-resolved, not server-parsed. Conventional
1631
+ * placeholders: `$1` for the single PK (singular form) and `$N` for the
1632
+ * count (plural form), e.g. `['Delete order $1?', 'Delete $N orders?']`.
1844
1633
  */
1845
- isValidFieldPath(path: string, _visited?: Set<string>): boolean;
1634
+ promptText?: string | [string, string];
1846
1635
  /**
1847
- * Creates a new validator with custom options.
1636
+ * Single-character keyboard shortcut hint. The server stores this verbatim
1637
+ * — choice of modifier prefix (Alt+, Ctrl+, bare key) and activation scope
1638
+ * (e.g. only when an actions dropdown is open) are UI/UX concerns. Conflict
1639
+ * resolution between actions sharing the same key is also up to the UI;
1640
+ * the server does no dedup.
1848
1641
  */
1849
- createValidator(opts?: Partial<TValidatorOptions>): Validator<T, DataType>;
1642
+ shortcut?: string;
1850
1643
  /**
1851
- * Finds a single record matching the query.
1852
- * The return type automatically excludes nav props unless they are
1853
- * explicitly requested via `$with`.
1644
+ * Stringified gate predicate (`fn.toString()`). Present only for `'row'`
1645
+ * and `'rows'` level actions whose decorator declared a `disabled` function.
1646
+ * The function is the batch shape `(rows: TRow[]) => boolean[]` (sync). The
1647
+ * UI evaluates against a level-specific scope to grey-out / hide the
1648
+ * button. The server has already enforced this predicate before the
1649
+ * action's handler ran — the server is authoritative; this field is purely
1650
+ * a UI hint.
1854
1651
  */
1855
- findOne<Q extends Uniquery<OwnProps, NavType>>(query: Q): Promise<DbResponse<DataType, NavType, Q> | null>;
1652
+ disabled?: string;
1856
1653
  /**
1857
- * Finds all records matching the query.
1858
- * The return type automatically excludes nav props unless they are
1859
- * explicitly requested via `$with`.
1654
+ * Name of the `.as` interface the action's `@InputForm()` parameter expects
1655
+ * (the compiled class's `.name`). Present only when the handler declares an
1656
+ * `@InputForm(FormType)` parameter. Clients fetch the serialized schema via
1657
+ * `GET /meta/form/:name` on the same controller and render a form to
1658
+ * collect the `input` field of the action's request envelope.
1860
1659
  */
1861
- findMany<Q extends Uniquery<OwnProps, NavType>>(query: Q): Promise<Array<DbResponse<DataType, NavType, Q>>>;
1660
+ inputForm?: string;
1661
+ }
1662
+ interface TDbInsertResult {
1663
+ insertedId: unknown;
1664
+ }
1665
+ interface TDbInsertManyResult {
1666
+ insertedCount: number;
1667
+ insertedIds: unknown[];
1668
+ }
1669
+ interface TDbUpdateResult {
1670
+ matchedCount: number;
1671
+ modifiedCount: number;
1672
+ }
1673
+ interface TDbDeleteResult {
1674
+ deletedCount: number;
1675
+ }
1676
+ type TDbIndexType = "plain" | "unique" | "fulltext" | "geo";
1677
+ interface TDbIndexField {
1678
+ name: string;
1679
+ sort: "asc" | "desc";
1680
+ weight?: number;
1862
1681
  /**
1863
- * Counts records matching the query.
1682
+ * Whether the indexed field is optional (declared `field?:` in the model).
1683
+ * Resolved during index finalization. Adapters use this to make a unique
1684
+ * index "present-only" so multiple value-less rows are tolerated — matching
1685
+ * SQL's `NULLS DISTINCT` default. SQL adapters get this for free and ignore
1686
+ * the flag; MongoDB needs it to emit a partial unique index.
1864
1687
  */
1865
- count(query?: Uniquery<OwnProps, NavType>): Promise<number>;
1688
+ optional?: boolean;
1689
+ /**
1690
+ * Resolved design type of the field ('string', 'number', 'boolean', …).
1691
+ * Carried alongside {@link optional} so adapters can derive a type-correct
1692
+ * present-only filter (e.g. Mongo's `partialFilterExpression`) without
1693
+ * re-resolving the field type. Undefined when the field cannot be resolved.
1694
+ */
1695
+ designType?: string;
1696
+ }
1697
+ interface TDbIndex {
1698
+ /** Unique key used for identity/diffing (e.g., "atscript__plain__email") */
1699
+ key: string;
1700
+ /** Human-readable index name. */
1701
+ name: string;
1702
+ /** Index type. */
1703
+ type: TDbIndexType;
1704
+ /** Ordered list of fields in the index. */
1705
+ fields: TDbIndexField[];
1706
+ }
1707
+ type TDbDefaultFn = "increment" | "uuid" | "now";
1708
+ type TDbCollation = "binary" | "nocase" | "unicode";
1709
+ type TDbDefaultValue = {
1710
+ kind: "value";
1711
+ value: string;
1712
+ } | {
1713
+ kind: "fn";
1714
+ fn: TDbDefaultFn;
1715
+ start?: number;
1716
+ };
1717
+ interface TIdDescriptor {
1718
+ /** Field names that form the primary key. */
1719
+ fields: string[];
1720
+ /** Whether this is a composite key (multiple fields). */
1721
+ isComposite: boolean;
1722
+ }
1723
+ /** A legitimate row-identifier shape: primary key or a unique index. */
1724
+ interface TIdentification {
1725
+ /** Logical (path) field names that form this identifier. */
1726
+ fields: readonly string[];
1727
+ /** `'primaryKey'` for the PK; the unique-index name otherwise. */
1728
+ source: string;
1729
+ }
1730
+ type TDbStorageType = "column" | "flattened" | "json";
1731
+ interface TDbFieldMeta {
1732
+ /** The dot-notation path to this field (logical name). */
1733
+ path: string;
1734
+ /** The annotated type for this field. */
1735
+ type: TAtscriptAnnotatedType;
1736
+ /** Physical column/field name (from @db.column, __-separated for flattened, or same as path). */
1737
+ physicalName: string;
1738
+ /** Resolved design type: 'string', 'number', 'boolean', 'object', 'json', etc. */
1739
+ designType: string;
1740
+ /** Whether the field is optional. */
1741
+ optional: boolean;
1742
+ /** Whether this field is part of the primary key (@meta.id). */
1743
+ isPrimaryKey: boolean;
1744
+ /** Whether this field is excluded from the DB (@db.ignore). */
1745
+ ignored: boolean;
1746
+ /** Default value from @db.default.* */
1747
+ defaultValue?: TDbDefaultValue;
1866
1748
  /**
1867
- * Finds records and total count in a single logical call.
1749
+ * How this field is stored in the database.
1750
+ * - 'column': a standard scalar column (default for primitives)
1751
+ * - 'flattened': a leaf scalar from a flattened nested object
1752
+ * - 'json': stored as a single JSON column (arrays, @db.json fields)
1868
1753
  */
1869
- findManyWithCount<Q extends Uniquery<OwnProps, NavType>>(query: Q): Promise<{
1870
- data: Array<DbResponse<DataType, NavType, Q>>;
1871
- count: number;
1872
- }>;
1754
+ storage: TDbStorageType;
1873
1755
  /**
1874
- * Executes an aggregate query with GROUP BY and aggregate functions.
1875
- *
1876
- * Validates:
1877
- * - Plain fields in $select are a subset of $groupBy
1878
- * - When dimensions/measures are defined (strict mode): $groupBy fields
1879
- * must be dimensions, aggregate $field values must be measures (or '*')
1880
- *
1881
- * Translates field names, delegates to adapter.aggregate(),
1882
- * then reverse-maps and applies fromStorage formatters on results.
1756
+ * For flattened fields: the dot-notation path (same as `path`).
1757
+ * E.g., for physicalName 'contact__email', this is 'contact.email'.
1758
+ * Undefined for non-flattened fields.
1883
1759
  */
1884
- aggregate(query: AggregateQuery): Promise<Array<Record<string, unknown>>>;
1885
- /** Whether the underlying adapter supports text search. */
1886
- isSearchable(): boolean;
1887
- /** Whether the adapter can filter on a given field (proxies adapter capability). */
1888
- canFilterField(fd: TDbFieldMeta): boolean;
1889
- /** Whether the adapter can sort by a given field (proxies adapter capability). */
1890
- canSortField(fd: TDbFieldMeta): boolean;
1891
- /** Returns available search indexes from the adapter. */
1892
- getSearchIndexes(): TSearchIndexInfo[];
1760
+ flattenedFrom?: string;
1761
+ /** Old physical column name from @db.column.renamed (for rename migration). */
1762
+ renamedFrom?: string;
1763
+ /** Collation from @db.column.collate (e.g. 'nocase', 'binary', 'unicode'). */
1764
+ collate?: TDbCollation;
1893
1765
  /**
1894
- * Full-text search with query translation and result reconstruction.
1766
+ * Whether this field is index-backed: it participates in an explicit index
1767
+ * (@db.index.plain, @db.index.unique, @db.index.fulltext) OR is a primary key
1768
+ * or unique field (which are always index-backed — Mongo `_id`, SQL PK/unique
1769
+ * constraints — even without an explicit `@db.index*`).
1895
1770
  */
1896
- search<Q extends Uniquery<OwnProps, NavType>>(text: string, query: Q, indexName?: string): Promise<Array<DbResponse<DataType, NavType, Q>>>;
1771
+ isIndexed?: boolean;
1772
+ /** Literal currency code from `@db.amount.currency 'EUR'`. */
1773
+ currencyCode?: string;
1774
+ /** Sibling field path from `@db.amount.currency.ref 'fieldName'`. */
1775
+ currencyRefField?: string;
1776
+ /** Literal unit-of-measure from `@db.unit 'kg'`. */
1777
+ unitCode?: string;
1778
+ /** Sibling field path from `@db.unit.ref 'fieldName'`. */
1779
+ unitRefField?: string;
1897
1780
  /**
1898
- * Full-text search with count for paginated search results.
1781
+ * For FK fields: the resolved field metadata of the referenced (target) PK column.
1782
+ * Adapters use this in `typeMapper` to produce matching DB types for FK columns
1783
+ * (e.g., `typeMapper(field.fkTargetField)` to inherit the target PK's DB type).
1784
+ * Undefined for non-FK fields or when the target cannot be resolved.
1899
1785
  */
1900
- searchWithCount<Q extends Uniquery<OwnProps, NavType>>(text: string, query: Q, indexName?: string): Promise<{
1901
- data: Array<DbResponse<DataType, NavType, Q>>;
1902
- count: number;
1903
- }>;
1904
- /** Whether the underlying adapter supports vector similarity search. */
1905
- isVectorSearchable(): boolean;
1786
+ fkTargetField?: TDbFieldMeta;
1906
1787
  /**
1907
- * Vector similarity search with query translation and result reconstruction.
1908
- *
1909
- * Overloads:
1910
- * - `vectorSearch(vector, query?)` — uses default vector index
1911
- * - `vectorSearch(indexName, vector, query?)` — targets a specific vector index
1788
+ * `@db.encrypted` — the value is AES-256-GCM encrypted by the core layer
1789
+ * before reaching the adapter. Adapters must map the column to an unbounded
1790
+ * text type and veto filtering/sorting (`canFilterField`/`canSortField`).
1791
+ * The descriptor's `designType` is forced to `'string'` (ciphertext envelope);
1792
+ * the declared type stays available via `type` for validation.
1912
1793
  */
1913
- vectorSearch<Q extends Uniquery<OwnProps, NavType>>(vectorOrIndex: number[] | string, maybeVectorOrQuery?: number[] | Q, maybeQuery?: Q): Promise<Array<DbResponse<DataType, NavType, Q>>>;
1794
+ encrypted?: boolean;
1914
1795
  /**
1915
- * Vector similarity search with count for paginated results.
1916
- *
1917
- * Overloads:
1918
- * - `vectorSearchWithCount(vector, query?)` — uses default vector index
1919
- * - `vectorSearchWithCount(indexName, vector, query?)` — targets a specific vector index
1796
+ * The field's declared type is the `db.geoPoint` primitive (`[lng, lat]` tuple).
1797
+ * Adapters map this to their native geo storage (e.g. MongoDB GeoJSON Point).
1920
1798
  */
1921
- vectorSearchWithCount<Q extends Uniquery<OwnProps, NavType>>(vectorOrIndex: number[] | string, maybeVectorOrQuery?: number[] | Q, maybeQuery?: Q): Promise<{
1922
- data: Array<DbResponse<DataType, NavType, Q>>;
1923
- count: number;
1799
+ isGeoPoint?: boolean;
1800
+ }
1801
+ interface TValueFormatterPair {
1802
+ /** Converts a JS value to storage representation (write + filter paths). */
1803
+ toStorage: (value: unknown) => unknown;
1804
+ /** Converts a storage value back to JS representation (read path). */
1805
+ fromStorage: (value: unknown) => unknown;
1806
+ }
1807
+ type TDbReferentialAction = "cascade" | "restrict" | "noAction" | "setNull" | "setDefault";
1808
+ interface TDbForeignKey {
1809
+ /** FK field names on this table (local columns). */
1810
+ fields: string[];
1811
+ /** Target table name (from the chain ref's type @db.table annotation). */
1812
+ targetTable: string;
1813
+ /** Target field names on the referenced table. */
1814
+ targetFields: string[];
1815
+ /** Lazy reference to the target annotated type (for on-demand table resolution). */
1816
+ targetTypeRef?: () => TAtscriptAnnotatedType;
1817
+ /** Alias grouping FK fields (if any). */
1818
+ alias?: string;
1819
+ /** Referential action on delete. */
1820
+ onDelete?: TDbReferentialAction;
1821
+ /** Referential action on update. */
1822
+ onUpdate?: TDbReferentialAction;
1823
+ }
1824
+ /** Describes an existing column in the database (from introspection). */
1825
+ interface TExistingColumn {
1826
+ name: string;
1827
+ type: string;
1828
+ notnull: boolean;
1829
+ pk: boolean;
1830
+ /** Serialized default value (e.g., "'active'", "NULL"). */
1831
+ dflt_value?: string;
1832
+ }
1833
+ /** Result of comparing desired schema against existing database columns. */
1834
+ interface TColumnDiff {
1835
+ added: TDbFieldMeta[];
1836
+ removed: TExistingColumn[];
1837
+ renamed: Array<{
1838
+ field: TDbFieldMeta;
1839
+ oldName: string;
1840
+ }>;
1841
+ typeChanged: Array<{
1842
+ field: TDbFieldMeta;
1843
+ existingType: string;
1844
+ }>;
1845
+ nullableChanged: Array<{
1846
+ field: TDbFieldMeta;
1847
+ wasNullable: boolean;
1848
+ }>;
1849
+ defaultChanged: Array<{
1850
+ field: TDbFieldMeta;
1851
+ oldDefault?: string;
1852
+ newDefault?: string;
1853
+ }>;
1854
+ conflicts: Array<{
1855
+ field: TDbFieldMeta;
1856
+ oldName: string;
1857
+ conflictsWith: string;
1924
1858
  }>;
1925
- /** Resolves overloaded vector search arguments into canonical form. */
1926
- private _resolveVectorSearchArgs;
1927
- /** Whether the underlying adapter supports geospatial search. */
1928
- isGeoSearchable(): boolean;
1929
1859
  /**
1930
- * Distance-ranked geospatial search (mirrors {@link vectorSearch}).
1931
- * Results are sorted by distance ascending; each row carries a computed
1932
- * `$distance` field (meters from the query point). `$maxDistance` /
1933
- * `$minDistance` (meters) ride in `query.controls`; user `$sort` is rejected.
1934
- *
1935
- * Overloads:
1936
- * - `geoSearch(point, query?)` — uses the table's only geo index
1937
- * - `geoSearch(indexName, point, query?)` — targets a specific geo index
1860
+ * The primary-key FIELD SET differs between the live table and the model
1861
+ * (set semantics — a composite-key reorder is not a change, consistent with
1862
+ * the schema hash). Column names are physical; a renamed PK column is
1863
+ * compared under its new name. Only reported when the table exists.
1864
+ * @since 0.1.128
1938
1865
  */
1939
- geoSearch<Q extends Uniquery<OwnProps, NavType>>(pointOrIndex: [number, number] | string, maybePointOrQuery?: [number, number] | Q, maybeQuery?: Q): Promise<Array<DbResponse<DataType, NavType, Q> & {
1940
- $distance: number;
1941
- }>>;
1866
+ primaryKeyChanged?: TPrimaryKeyChange;
1867
+ }
1868
+ /** Old and new primary-key column sets of a table whose key definition moved. */
1869
+ interface TPrimaryKeyChange {
1870
+ /** Physical PK columns currently in the database (after rename mapping). */
1871
+ from: string[];
1872
+ /** Physical PK columns the model declares. */
1873
+ to: string[];
1874
+ }
1875
+ /**
1876
+ * A live foreign-key constraint as introspected from the database
1877
+ * (outbound: declared on the table that owns it).
1878
+ */
1879
+ interface TExistingForeignKey {
1880
+ /** Local (referencing) columns, in constraint order. */
1881
+ fields: string[];
1882
+ /** Referenced table name. */
1883
+ targetTable: string;
1884
+ /** Referenced columns, in constraint order. */
1885
+ targetFields: string[];
1886
+ }
1887
+ /**
1888
+ * A live foreign key that REFERENCES a given table (inbound edge), as returned
1889
+ * by `BaseDbAdapter.getReferencingForeignKeys(tableName)`.
1890
+ */
1891
+ interface TReferencingForeignKey {
1892
+ /** The referencing (child) table. */
1893
+ table: string;
1894
+ /** Referencing columns on `table`, in constraint order. */
1895
+ fields: string[];
1896
+ /** Referenced columns on the queried table, in constraint order. */
1897
+ targetFields: string[];
1898
+ }
1899
+ /** Kind of a physical database object, as returned by `BaseDbAdapter.getObjectKind`. */
1900
+ type TDbObjectKind = "table" | "view" | "materialized";
1901
+ /** Options accepted by `BaseDbAdapter.ensureTable`. */
1902
+ interface TEnsureTableOptions {
1942
1903
  /**
1943
- * Distance-ranked geospatial search with count for paginated results.
1944
- *
1945
- * Overloads:
1946
- * - `geoSearchWithCount(point, query?)` — uses the table's only geo index
1947
- * - `geoSearchWithCount(indexName, point, query?)` — targets a specific geo index
1904
+ * Table names whose inline FOREIGN KEY constraints must be omitted from
1905
+ * CREATE TABLE — the constraints are added afterwards by `syncForeignKeys()`.
1906
+ * Schema sync passes the members of a foreign-key cycle so they can be
1907
+ * created in any order.
1948
1908
  */
1949
- geoSearchWithCount<Q extends Uniquery<OwnProps, NavType>>(pointOrIndex: [number, number] | string, maybePointOrQuery?: [number, number] | Q, maybeQuery?: Q): Promise<{
1950
- data: Array<DbResponse<DataType, NavType, Q> & {
1951
- $distance: number;
1952
- }>;
1953
- count: number;
1909
+ deferForeignKeysTo?: ReadonlySet<string>;
1910
+ }
1911
+ /** Result of applying column diff to the database. */
1912
+ interface TSyncColumnResult {
1913
+ added: string[];
1914
+ renamed: string[];
1915
+ }
1916
+ /** A single table-level option in unified key-value format. */
1917
+ interface TExistingTableOption {
1918
+ key: string;
1919
+ value: string;
1920
+ }
1921
+ /** Result of comparing desired table options against existing ones. */
1922
+ interface TTableOptionDiff {
1923
+ changed: Array<{
1924
+ key: string;
1925
+ oldValue: string;
1926
+ newValue: string; /** Whether applying this change requires dropping and recreating the table. */
1927
+ destructive: boolean;
1928
+ }>;
1929
+ }
1930
+ /**
1931
+ * Adapter-provided metadata adjustments applied atomically during the
1932
+ * build pipeline, before field descriptors are built.
1933
+ *
1934
+ * Replaces the old pattern where adapters mutated metadata via
1935
+ * back-references (`this._table.addPrimaryKey()`, etc.).
1936
+ */
1937
+ interface TMetadataOverrides {
1938
+ /** Fields to add as primary keys. */
1939
+ addPrimaryKeys?: string[];
1940
+ /** Fields to remove from primary keys. */
1941
+ removePrimaryKeys?: string[];
1942
+ /** Fields to register as having a unique constraint. */
1943
+ addUniqueFields?: string[];
1944
+ /** Synthetic fields to inject into flatMap (e.g. MongoDB's `_id`). */
1945
+ injectFields?: Array<{
1946
+ path: string;
1947
+ type: TAtscriptAnnotatedType;
1954
1948
  }>;
1955
- /** Resolves overloaded geo search arguments into canonical form. */
1956
- private _resolveGeoSearchArgs;
1957
- /** Shared geoSearch validation + query translation. */
1958
- private _prepareGeoSearch;
1959
- /**
1960
- * Finds a single record by any type-compatible identifier — primary key
1961
- * or single-field unique index.
1962
- * The return type excludes nav props unless `$with` is provided in controls.
1963
- *
1964
- * ```typescript
1965
- * // Without relations — nav props stripped from result
1966
- * const user = await table.findById('123')
1967
- *
1968
- * // With relations — only requested nav props appear
1969
- * const user = await table.findById('123', { controls: { $with: [{ name: 'posts' }] } })
1970
- * ```
1971
- */
1972
- findById<Q extends {
1973
- controls?: UniqueryControls<OwnProps, NavType>;
1974
- } = Record<string, never>>(id: IdType, query?: Q): Promise<DbResponse<DataType, NavType, Q> | null>;
1975
- /**
1976
- * Resolve an id value (scalar or object) into a {@link FilterExpr} using the
1977
- * same identification resolution as {@link findById}. Public so callers can
1978
- * AND-combine the id-filter with a row-level read overlay before issuing
1979
- * `findOne` (avoiding the existence leak that `findById` would cause).
1980
- */
1981
- resolveIdFilter(id: unknown): FilterExpr | null;
1949
+ }
1950
+ /**
1951
+ * Callback that resolves an annotated type to a queryable table instance.
1952
+ * Required for `$with` relation loading — each table needs to query related tables.
1953
+ *
1954
+ * Typically provided by the driver/registry (e.g. `DbSpace.getTable`).
1955
+ */
1956
+ type TTableResolver = (type: TAtscriptAnnotatedType) => Pick<AtscriptDbTableLike, "findMany" | "loadRelations" | "primaryKeys" | "preferredId" | "relations" | "foreignKeys" | "isValidFieldPath"> | undefined;
1957
+ /** Minimal table interface used by the table resolver. Avoids circular dependency with AtscriptDbTable. */
1958
+ interface AtscriptDbTableLike {
1959
+ findMany(query: unknown): Promise<Array<Record<string, unknown>>>;
1960
+ loadRelations(rows: Array<Record<string, unknown>>, withRelations: WithRelation[]): Promise<void>;
1961
+ primaryKeys: readonly string[];
1962
+ preferredId: readonly string[];
1963
+ relations: ReadonlyMap<string, TDbRelation>;
1964
+ foreignKeys: ReadonlyMap<string, TDbForeignKey>;
1965
+ getMetadata(): TableMetadata;
1966
+ isValidFieldPath(path: string, visited?: Set<string>): boolean;
1967
+ }
1968
+ /** Minimal writable table interface for nested creation/update. */
1969
+ interface AtscriptDbWritable {
1970
+ insertOne(payload: Record<string, unknown>, opts?: {
1971
+ maxDepth?: number;
1972
+ }): Promise<TDbInsertResult>;
1973
+ insertMany(payloads: Array<Record<string, unknown>>, opts?: {
1974
+ maxDepth?: number;
1975
+ _depth?: number;
1976
+ }): Promise<TDbInsertManyResult>;
1977
+ replaceOne(payload: Record<string, unknown>, opts?: {
1978
+ maxDepth?: number;
1979
+ }): Promise<TDbUpdateResult>;
1980
+ bulkReplace(payloads: Array<Record<string, unknown>>, opts?: {
1981
+ maxDepth?: number;
1982
+ _depth?: number;
1983
+ }): Promise<TDbUpdateResult>;
1984
+ updateOne(payload: Record<string, unknown>, opts?: {
1985
+ maxDepth?: number;
1986
+ }): Promise<TDbUpdateResult>;
1987
+ bulkUpdate(payloads: Array<Record<string, unknown>>, opts?: {
1988
+ maxDepth?: number;
1989
+ _depth?: number;
1990
+ }): Promise<TDbUpdateResult>;
1991
+ findOne(query: unknown): Promise<Record<string, unknown> | null>;
1992
+ count(query: {
1993
+ filter: Record<string, unknown>;
1994
+ }): Promise<number>;
1995
+ deleteMany(filter: unknown): Promise<TDbDeleteResult>;
1996
+ /** Pre-validate items (type + FK constraints) without inserting them. */
1997
+ preValidateItems(items: Array<Record<string, unknown>>, opts?: {
1998
+ excludeFkTargetTable?: string;
1999
+ }): Promise<void>;
2000
+ }
2001
+ /**
2002
+ * Callback that resolves an annotated type to a writable table instance.
2003
+ * Used for nested creation — inserting related records inline.
2004
+ */
2005
+ type TWriteTableResolver = (type: TAtscriptAnnotatedType) => (AtscriptDbTableLike & AtscriptDbWritable) | undefined;
2006
+ /**
2007
+ * A child table that may need cascade/setNull processing when a parent is deleted.
2008
+ * Returned by the cascade resolver.
2009
+ */
2010
+ interface TCascadeTarget {
2011
+ /** FK on the child table that references the parent being deleted. */
2012
+ fk: TDbForeignKey;
2013
+ /** Name of the child table that holds this FK. */
2014
+ childTable: string;
2015
+ /** Delete matching child records (goes through AtscriptDbTable for recursive cascade). */
2016
+ deleteMany(filter: Record<string, unknown>): Promise<TDbDeleteResult>;
2017
+ /** Update matching child records (for setNull — sets FK fields to null). */
2018
+ updateMany(filter: Record<string, unknown>, data: Record<string, unknown>): Promise<TDbUpdateResult>;
2019
+ /** Count matching child records (for restrict — check existence before delete). */
2020
+ count(filter: Record<string, unknown>): Promise<number>;
2021
+ }
2022
+ /**
2023
+ * Callback that finds all child tables with FKs pointing to a given parent table.
2024
+ * Used by AtscriptDbTable to implement application-level cascade deletes.
2025
+ */
2026
+ type TCascadeResolver = (tableName: string) => TCascadeTarget[];
2027
+ /**
2028
+ * Minimal interface for querying a target table during FK validation.
2029
+ * Only `count` is needed — we check if the referenced record exists.
2030
+ */
2031
+ interface TFkLookupTarget {
2032
+ count(filter: Record<string, unknown>): Promise<number>;
2033
+ }
2034
+ /**
2035
+ * Callback that resolves a table name to a queryable target for FK validation.
2036
+ * Returns undefined if the target table is not registered in the space.
2037
+ */
2038
+ type TFkLookupResolver = (tableName: string) => TFkLookupTarget | undefined;
2039
+ interface TDbRelation {
2040
+ /** Direction: 'to' (FK is local), 'from' (FK is remote), or 'via' (M:N junction). */
2041
+ direction: "to" | "from" | "via";
2042
+ /** The alias used for pairing (if any). */
2043
+ alias?: string;
2044
+ /** Target type's annotated type reference. */
2045
+ targetType: () => TAtscriptAnnotatedType;
2046
+ /** Whether this is an array relation (one-to-many). */
2047
+ isArray: boolean;
2048
+ /** Junction type reference for 'via' (M:N) relations. */
2049
+ viaType?: () => TAtscriptAnnotatedType;
2050
+ }
2051
+ /**
2052
+ * Write payload for insert / patch paths: every key optional, and optional
2053
+ * columns additionally accept `null` (an explicit NULL — `undefined` means
2054
+ * "absent" and is dropped before the row reaches defaults or validation).
2055
+ */
2056
+ type DbPatch<D> = { [K in keyof D]?: undefined extends D[K] ? D[K] | null : D[K] } & Record<string, unknown>;
2057
+ /**
2058
+ * Write payload for full-row replace paths: required keys stay required,
2059
+ * optional columns additionally accept `null` (explicit NULL).
2060
+ */
2061
+ type DbRow<D> = { [K in keyof D]: undefined extends D[K] ? D[K] | null : D[K] } & Record<string, unknown>;
2062
+ /** Built-in write actions a moost-db `AsDbController` endpoint performs. */
2063
+ type TDbWriteAction = "insert" | "insertMany" | "replace" | "replaceMany" | "update" | "updateMany";
2064
+ /**
2065
+ * Context handed to a write {@link TWriteOptions.guard} (since 0.1.128) — and
2066
+ * through it to `AsDbController.guardWrite()`. The table invokes the guard
2067
+ * exactly once, inside its own transaction, after `undefined`-pruning,
2068
+ * defaults and validation and before encryption / nested-relation phases.
2069
+ */
2070
+ interface TDbWriteGuardContext<Row = Record<string, unknown>> {
2071
+ /** The table method the guard runs for (`insertOne` → `insert`, `insertMany` → `insertMany`, …). */
2072
+ readonly action: TDbWriteAction;
1982
2073
  /**
1983
- * Resolve an id value into a filter expression.
1984
- *
1985
- * When `preferredId` differs from the PK, scalar ids resolve only against
1986
- * the preferred field (deterministic addressing). Otherwise scalars try PK
1987
- * + every single-field unique index; objects try PK + compound unique
1988
- * indexes.
2074
+ * insert/replace: validated rows with SDK-side defaults applied (plaintext,
2075
+ * nav data still attached); update: validated patches with the identifying
2076
+ * PK/unique fields present and `$cas` removed. Mutate in place to enrich —
2077
+ * the table re-validates the rows after the guard.
1989
2078
  */
1990
- protected _resolveIdFilter(id: unknown): FilterExpr | null;
1991
- /** Build a single-key filter from `idObj` over `fields`, or null if any field is missing/incompatible. */
1992
- private _tryCompoundFilter;
2079
+ readonly rows: Row[];
2080
+ /** Parallel to `rows`: expected version lifted from `$cas`, or `undefined`. */
2081
+ readonly expectedVersions: ReadonlyArray<number | undefined>;
1993
2082
  /**
1994
- * Attempts to build a single-field filter `{ field: preparedId }`.
2083
+ * Lazy, memoised pre-image of `rows[i]` by its identifying filter, read
2084
+ * inside the transaction. `null` when the row is missing OR when it carries
2085
+ * no identifying key yet (e.g. auto-increment inserts) — never throws.
1995
2086
  */
1996
- private _tryFieldFilter;
2087
+ current(i: number): Promise<Row | null>;
2088
+ }
2089
+ /**
2090
+ * Context handed to a delete {@link TDeleteOptions.guard} (since 0.1.128) —
2091
+ * and through it to `AsDbController.guardRemove()`. Runs inside the table's
2092
+ * transaction; an id that resolves to no filter never reaches the guard
2093
+ * (`deleteOne` answers `{ deletedCount: 0 }`).
2094
+ */
2095
+ interface TDbRemoveGuardContext<Row = Record<string, unknown>> {
2096
+ /** The id `deleteOne` was called with. */
2097
+ readonly id: unknown;
2098
+ /** `table.resolveIdFilter(id)` — never null here. */
2099
+ readonly filter: FilterExpr;
2100
+ /** Lazy, memoised pre-image of the row about to be deleted (`null` when missing). */
2101
+ current(): Promise<Row | null>;
2102
+ }
2103
+ /** A validated-stage write guard — see {@link TWriteOptions.guard}. */
2104
+ type TDbWriteGuard<Row = Record<string, unknown>> = (ctx: TDbWriteGuardContext<Row>) => void | Promise<void>;
2105
+ /** A validated-stage delete guard — see {@link TDeleteOptions.guard}. */
2106
+ type TDbRemoveGuard<Row = Record<string, unknown>> = (ctx: TDbRemoveGuardContext<Row>) => void | Promise<void>;
2107
+ /** Options of `AtscriptDbTable.touchMany` (since 0.1.129). */
2108
+ interface TTouchManyOptions {
1997
2109
  /**
1998
- * Public entry point for relation loading. Used by adapters for nested $with delegation.
2110
+ * `'all'` (default): every key must match its stored version — a stale or
2111
+ * missing row throws `DbError("CAS_MISMATCH")` and no version moves.
2112
+ * `'any'`: bump whatever matches and report the honest counts.
1999
2113
  */
2000
- loadRelations(rows: Array<Record<string, unknown>>, withRelations: WithRelation[]): Promise<void>;
2114
+ require?: "all" | "any";
2115
+ }
2116
+ /** Options of `insertOne/Many`, `replaceOne` / `bulkReplace`, `updateOne` / `bulkUpdate`. */
2117
+ interface TWriteOptions<Row = Record<string, unknown>> {
2118
+ /** Nested-relation write recursion limit (default 3). */
2119
+ maxDepth?: number;
2001
2120
  /**
2002
- * Finds the FK entry that connects a `@db.rel.to` relation to its target.
2003
- * Thin wrapper — delegates to relation-loader for shared use with db-table.ts write path.
2121
+ * Validated-stage guard (since 0.1.128): invoked exactly once inside the
2122
+ * table's transaction, after defaults + validation and before encryption
2123
+ * and nested-relation phases, with the rows the table is about to write.
2124
+ * Rows may be enriched in place — they are validated again afterwards. A
2125
+ * throw rolls the transaction back and propagates unchanged. Never runs
2126
+ * for the nested re-entries a deep write performs on related tables.
2004
2127
  */
2005
- protected _findFKForRelation(relation: TDbRelation): {
2006
- localFields: string[];
2007
- targetFields: string[];
2008
- } | undefined;
2128
+ guard?: TDbWriteGuard<Row>;
2129
+ }
2130
+ /** Options of `deleteOne`. */
2131
+ interface TDeleteOptions<Row = Record<string, unknown>> {
2009
2132
  /**
2010
- * Finds a FK on a remote table that points back to this table.
2011
- * Thin wrapper — delegates to relation-loader for shared use with db-table.ts write path.
2133
+ * Validated-stage guard (since 0.1.128): invoked inside the table's
2134
+ * transaction after the id resolved to a filter and before cascade /
2135
+ * delete. A throw rolls the transaction back and propagates unchanged.
2012
2136
  */
2013
- protected _findRemoteFK(targetTable: {
2014
- foreignKeys: ReadonlyMap<string, TDbForeignKey>;
2015
- }, thisTableName: string, alias?: string): TDbForeignKey | undefined;
2137
+ guard?: TDbRemoveGuard<Row>;
2138
+ }
2139
+ /**
2140
+ * Adds `null` to every optional property of `O`. Optional columns store SQL
2141
+ * NULL / Mongo null, and the runtime validator accepts `null` for optional
2142
+ * props — so filter shapes (`{ note: null }`, `{ note: { $ne: null } }`) and
2143
+ * row shapes must admit it at the type level too. Homomorphic: keys and
2144
+ * required properties are unchanged; applying it twice is a no-op.
2145
+ */
2146
+ type NullableOptional<O> = { [K in keyof O]: undefined extends O[K] ? O[K] | null : O[K] };
2147
+ //#endregion
2148
+ //#region src/query/buckets.d.ts
2149
+ /**
2150
+ * A calendar bucket as adapters receive it (`controls.$select.buckets`): the
2151
+ * normalized uniqu bucket (canonical zone, week start, alias) whose `field` is
2152
+ * the PHYSICAL column / document path, plus the source field's descriptor —
2153
+ * dialects read the storage kind from `fd` (since 0.1.132).
2154
+ */
2155
+ type TResolvedBucket = ResolvedBucket & {
2156
+ /** Physical column (relational) or document path (nested-object adapters). */field: string; /** Descriptor of the source field (a `number.timestamp` leaf). */
2157
+ fd: TDbFieldMeta;
2158
+ };
2159
+ /**
2160
+ * The names a bucket alias must not shadow: `TableMetadata` provides them, and
2161
+ * moost-db's HTTP gate assembles the same set from its readable and capability
2162
+ * index, so both layers resolve against the same names.
2163
+ */
2164
+ interface TBucketFieldSource {
2165
+ /** Every logical path of the table type (nested parents and navigation fields included). */
2166
+ flatMap: ReadonlyMap<string, unknown>;
2167
+ /** Every field descriptor's `physicalName` — reserved too. */
2168
+ physicalNames: ReadonlySet<string>;
2169
+ navFields: ReadonlySet<string>;
2016
2170
  }
2171
+ /**
2172
+ * The one normalizer of `$select` computed entries (since 0.1.132) — uniqu's
2173
+ * `resolveBuckets` (entry shapes, unit, time zone canonicalization, week
2174
+ * start, alias syntax and uniqueness, "grouped queries only", "must also
2175
+ * appear in $groupBy", string `$groupBy` entries) with the table's names as
2176
+ * the collision set: a bucket alias may not equal a logical path, a physical
2177
+ * column or a navigation field, so a label is never reverse-mapped as a
2178
+ * column.
2179
+ *
2180
+ * Which layer validates what:
2181
+ * - **Shapes** (this normalizer) run FIRST at every entry point — the core's
2182
+ * read path (`guardQuery`), its aggregate path (`AtscriptDbReadable.aggregate`,
2183
+ * which hands the result on to `guardAggregate` and the field mapper), and
2184
+ * moost-db's HTTP gate — so both layers answer with the same wording.
2185
+ * Everything downstream (`collectQueryPaths`, the field mappers,
2186
+ * `UniquSelect`, adapters) assumes normalized input and does not re-check.
2187
+ * - **Schema** (timestamp-typed source, JSON ancestor, encryption, physical
2188
+ * filterability) is the path guard's (`guardPath` op `bucket`), mirrored by
2189
+ * moost-db's capability index; dimensions are the aggregate rules'.
2190
+ * - **Adapter capability** (`calendarBucketUnits()`) is `guardAggregate`'s
2191
+ * (`BUCKET_NOT_SUPPORTED`); SQL builders only re-assert the inlined
2192
+ * literals (defense in depth).
2193
+ *
2194
+ * `aggregate` defaults to "`$groupBy` is non-empty".
2195
+ *
2196
+ * @throws DbError `INVALID_QUERY` carrying every issue (`path` `$select` / `$groupBy`).
2197
+ */
2198
+ declare function resolveCalendarBuckets(controls: {
2199
+ $select?: unknown;
2200
+ $groupBy?: unknown;
2201
+ } | undefined, fields: TBucketFieldSource, aggregate?: boolean): ResolvedBucket[];
2202
+ /**
2203
+ * Whether a field can be the source of a calendar bucket: a `number` /
2204
+ * `integer` leaf carrying the `timestamp` tag (`number.timestamp`,
2205
+ * `.created`, `.updated`) that is not `@db.encrypted`. The type is the
2206
+ * declaration — no annotation opts a field in. Physical filterability is the
2207
+ * caller's (the core path guard and moost-db's capability index both add it).
2208
+ */
2209
+ declare function isBucketableField(fd: TDbFieldMeta): boolean;
2210
+ /**
2211
+ * Whether a field holds a JSON value — a `@db.json` object / JSON-stored
2212
+ * column or an array. The members of `TableMetadata.jsonValueParents` (and
2213
+ * moost-db's equivalent set) — see {@link jsonValueAncestor}.
2214
+ */
2215
+ declare function isJsonValueField(fd: TDbFieldMeta): boolean;
2216
+ /**
2217
+ * The outermost ancestor of `path` in `jsonValueParents` (the paths of the
2218
+ * {@link isJsonValueField} descriptors), or `undefined`. A timestamp inside a
2219
+ * JSON value is never a bucket source — relational adapters cannot address
2220
+ * it and nested-object adapters (which can) must not diverge from them.
2221
+ */
2222
+ declare function jsonValueAncestor(path: string, jsonValueParents: ReadonlySet<string>): string | undefined;
2017
2223
  //#endregion
2018
- 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 };
2224
+ 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 };