1---2name: atscript-db3description: Use when working with @atscript/db, @atscript/db-{sqlite,postgres,mysql,mongo,memory}, @atscript/db-sql-tools, @atscript/moost-db, @atscript/db-client, or .as models with @db.* annotations. Covers DbSpace, adapter wiring, table/view CRUD, query filters, patch/field/array ops, relations, views, schema sync, engine-specific capabilities, BaseDbAdapter subclassing, moost-db REST routes, declarative actions, @InputForm structured input, URL query syntax, browser Client, client.action(), client.getActionForm(), DB validation, optimistic concurrency control via @db.column.version + $cas + withOptimisticRetry, field-level encryption at rest (@db.encrypted, key rotation, ENC_* errors), geo search (db.geoPoint, @db.index.geo, geoSearch, $geoWithin, GET /geo), token controller binding (provideDbSpace, @db.space), model manifest (atscriptModels), planSchema, assertExposed, provideTestDbSpace. Scope is DB only. Out of scope (use the moostjs/atscript skill): .as syntax, @meta.*, @expect.*, asc, unplugin, VSCode.4---56# atscript-db78## Install910```bash11npx skills add moostjs/atscript-db # this skill (DB layer)12npx skills add moostjs/atscript # sibling — .as syntax, @meta.*, @expect.*, asc, unplugin13```1415## Packages1617```18@atscript/db core: DbSpace, AtscriptDbTable, AtscriptDbView, schema sync, relations19 ├── @atscript/db-sql-tools shared SQL builders (WHERE, SELECT, INSERT, aggregation, filter visitor)20 │ ├── @atscript/db-sqlite better-sqlite3 + FTS5 + sqlite-vec (vec0) + collation21 │ ├── @atscript/db-postgres pg + pgvector + HNSW + CITEXT + FTS22 │ └── @atscript/db-mysql mysql2 + VECTOR + FULLTEXT + utf8mb423 ├── @atscript/db-mongo mongodb (aggregation pipelines, Atlas Search, no SQL layer)24 ├── @atscript/db-memory in-memory adapter (no engine); provider-backed read-only + stored read-write25 ├── @atscript/moost-db Moost HTTP controllers: AsDbController / AsDbReadableController26 └── @atscript/db-client browser/SSR fetch client over moost-db REST27```2829```bash30pnpm add @atscript/core @atscript/typescript @atscript/db31pnpm add @atscript/db-sqlite better-sqlite3 # pick one adapter32pnpm add @atscript/db-postgres pg33pnpm add @atscript/db-mysql mysql234pnpm add @atscript/db-mongo mongodb35pnpm add @atscript/db-memory # in-memory (tests / runtime surfaces)36pnpm add @atscript/moost-db @moostjs/event-http moost # REST37pnpm add @atscript/db-client # browser/SSR client38```3940## Quick start4142```atscript43// src/todo.as44@db.table 'todos'45@db.depth.limit 046export interface Todo {47 @meta.id @db.default.increment48 id: number49 title: string50 @db.default 'false'51 completed?: boolean52 @db.default.now53 createdAt?: number.timestamp54}55```5657```ts58import { DbSpace } from "@atscript/db";59import { syncSchema } from "@atscript/db/sync";60import { SqliteAdapter, BetterSqlite3Driver } from "@atscript/db-sqlite";61import { Todo } from "./todo.as";6263const db = new DbSpace(() => new SqliteAdapter(new BetterSqlite3Driver("./app.db")));64await syncSchema(db, [Todo]); // idempotent, lock-coordinated65const todos = db.getTable(Todo);6667await todos.insertOne({ title: "ship it" }); // { insertedId: 1 }68const open = await todos.findMany({ filter: { completed: false } });69await todos.updateOne({ id: 1, completed: true }); // PK required in payload70await todos.deleteOne(1);71```7273## Invariants7475| # | Rule |76| --- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |77| 1 | **adapter parity** — application code must not branch on adapter type. Every adapter accepts the same filter shape, patch shape, and controls; per-engine features are surfaced through annotations, not API forks. |78| 2 | **`@meta.id` is the composite-key marker.** No `@meta.isKey`. Multiple `@meta.id` on different props form a composite PK. Takes no arguments. |79| 3 | **`@db.depth.limit N` gates nested writes (insert / replace / patch) along `@db.rel.from` / `@db.rel.via` chains.** Absent or `0` → server rejects nested FROM/VIA payloads with HTTP 400 and `/meta` ships shallow FK refs. Set `N ≥ 1` to opt in. `@db.rel.to` (single parent ref) is not subject to this gate. |80| 4 | **MongoDB indexes use the `atscript__` prefix.** `syncIndexes()` only manages indexes with this prefix; consumer-created indexes that start with `atscript__` are treated as managed and may be dropped on drift. |81| 5 | **Generated `*.as.d.ts` / `atscript.d.ts` files in a consuming project are produced by `asc`.** Never hand-edit. Regenerate via `npx asc` (or let `unplugin-atscript` do it at bundle time). |82| 6 | **Schema sync takes a distributed lock.** Multi-pod deployments must configure `podId`, `lockTtlMs`, `waitTimeoutMs` on the `syncSchema()` options; the control table is `__atscript_control`. |83| 7 | **Third-party `BaseDbAdapter` implementations MUST NOT import any other in-tree adapter.** Shared SQL helpers live in `@atscript/db-sql-tools`. Each adapter is independent. |84| 8 | **Navigation relations are lazy.** `@db.rel.to` / `.from` / `.via` fields are `undefined` on read unless requested via `controls.$with`. No N+1 lazy loading. |85| 9 | **`@db.column` has a per-row cost — don't use it without a hard reason.** A single use flips the whole table onto the key-translation path on every read/write/filter. Legit reasons only: (a) target name is a SQL reserved word; (b) integrating with a schema you don't own; (c) repo-wide convention forces snake_case at DB + camelCase in TS. Same spirit for `@db.table 'physical'` (no per-row cost, same intent). Details + non-reasons → [annotations.md § `@db.column` — when to use it](references/annotations.md#dbcolumn--when-to-use-it). |86| 10 | **Read responses always carry `preferredId` fields.** The `moost-db` controller silently widens `$select` to include `meta.preferredId` on every row-returning read endpoint (`/query`, `/pages`, `/one`, `/one/:id`, search/vector). Aggregate (`$groupBy`) and count (`$count`) responses are NOT widened. `transformProjection()` overrides cannot suppress preferred-id fields — hide identifiers at the network/authz layer instead. |87| 11 | **Action body is the envelope `{ ids?, input? }`.** `ids` carries the identifier(s) — JSON object (single) or array of objects (multi) for `@DbActionID*` / `@DbActionRow*`, never scalars; field set must EXACTLY match one legitimate identification (PK or any `@db.index.unique` group), strict — unknown fields rejected. `input` carries the optional `@InputForm` payload, **validated server-side** against the declared/inferred form before the handler fires (`ValidatorError` → structured 400; absent `input` validates as `{}`, so required fields 400 and the handler always receives an object). Single-field PKs send `{ "ids": { "id": "abc" } }`, never bare `"abc"`. Array or scalar root → 400. Empty body / `{}` is valid for table-level actions with no form. |88| 12 | **`disabled` requires `requiredFields`.** `@DbAction` / `@DbActions*` predicates must declare every field they read as a literal `requiredFields` tuple. The tuple type-narrows the predicate's row arg (`Pick<FlatOf<TRow>, R[number]>[]`) AND drives server-side projection widening for `@DbActionRow*` injection and the `$actions=true` augmentation. `requiredFields` is server-internal — never on the `/meta` wire. `disabled` without `requiredFields` → action dropped at discovery. |89| 13 | **Per-row action availability is server-evaluated via `?$actions=true`.** Opt-in URL control on `/query`, `/pages`, `/one`. Each returned row gets `$actions: string[]` (row/rows-level action names NOT disabled). Stripped on `$count`/`$groupBy`. `'table'`-level actions never appear. Filters through per-request `applyMetaOverlay()`. |90| 14 | **`/meta` `fields[*].filterable` / `.sortable` are adapter-gated.** `BaseDbAdapter.canFilterField(fd)` / `canSortField(fd)` defaults to `fd.storage !== 'json'`, so `@db.json` fields and array fields (both `storage: 'json'`) report `false`/`false` on SQL adapters even when annotated `@db.column.filterable`/`@db.column.sortable`. MongoAdapter overrides `canFilterField` to `true` (native dot-paths and array filters) but inherits the conservative `canSortField` default. Adapter veto wins over annotations — annotating a JSON column doesn't make the engine able to filter/sort it. In auto mode (no `@db.table.sortable 'manual'`) `sortable` additionally requires the field be **index-backed** (`TDbFieldMeta.isIndexed` = explicit `@db.index*` **OR** primary key **OR** unique — so Mongo `_id` and SQL PK/unique advertise `sortable: true` without an explicit index), while `filterable` defaults `true` for any adapter-capable field. Auto mode enforces no sort gate, so a `$sort` on a non-advertised field still succeeds — `sortable: false` is advisory, not enforced. Detail → [moost-db.md § Gate mode](references/moost-db.md). |91| 15 | **Bind controllers by model token; register spaces before `init()`.** `@TableController(Model)` resolves lazily at `app.init()` against the ambient registry — `provideDbSpace(db)` (+ named spaces via `provideDbSpace(x, "name")` matching `@db.space`) must run first. Lazy-factory form requires an explicit prefix. A subclass with its own constructor calls `super(moost)` and the base resolves from class metadata (ctor is `(app, readable?)` — readable last and optional). Instance form (`@TableController(table)`) still works but couples binding to module evaluation order. → [moost-db.md § Binding forms](references/moost-db.md#binding-forms) |92| 16 | **Sync failures are loud by default.** `syncSchema` runs with errored entries report a summary + per-entry errors even under the `NoopLogger` default (console fallback); `onError: "throw" \| "warn" \| "silent"` tunes it. Feed sync from the generated manifest (`dbPlugin({ manifest })` → `atscriptModels`) instead of a hand-maintained import array. → [schema-sync.md](references/schema-sync.md) |93| 17 | **Value-import `.as` artifacts — never `import type`.** Compiled `.as` models/forms are classes consumed at runtime (decorator args, DI by param type, `.validator()`, manifest arrays). A type-only import elides the value and `design:paramtypes` emits `Object` — form binding and DI-by-type break silently. Keep `typescript/consistent-type-imports`-style lint rules **off** in apps importing `.as` files (the create-moost preset disables it). |9495## Key imports9697```ts98// Core99import { DbSpace, AtscriptDbTable, AtscriptDbView, BaseDbAdapter, DbError } from "@atscript/db";100import { syncSchema, planSchema, SchemaSync, readStoredSnapshot } from "@atscript/db/sync";101import { dbPlugin } from "@atscript/db/plugin"; // dbPlugin({ manifest: "atscript.models.ts" }) — path is rootDir-relative102import { $inc, $dec, $mul, $replace, $insert, $upsert, $update, $remove } from "@atscript/db/ops";103// Optimistic concurrency — see references/versioning.md104import { withOptimisticRetry, CasExhaustedError } from "@atscript/db";105106// Adapters (pick one)107import {108 SqliteAdapter,109 BetterSqlite3Driver,110 createAdapter as sqliteSpace,111} from "@atscript/db-sqlite";112import { PostgresAdapter, PgDriver, createAdapter as pgSpace } from "@atscript/db-postgres";113import { MysqlAdapter, Mysql2Driver, createAdapter as mysqlSpace } from "@atscript/db-mysql";114import { MongoAdapter } from "@atscript/db-mongo";115import {116 MemoryAdapter,117 createAdapter as memorySpace,118 setMemoryProvider,119 buildMemoryPredicate,120 sortRows,121 projectRow, // shared JS-native engine — also backs moost-db value-help122} from "@atscript/db-memory";123124// HTTP125import {126 AsDbController,127 AsDbReadableController,128 TableController,129 ReadableController,130 provideDbSpace, // register the DbSpace for token-bound controllers, BEFORE app.init()131 clearDbSpaces,132 assertExposed, // dev check: @db.http.path models without a bound controller133} from "@atscript/moost-db";134import { provideTestDbSpace, resetTestDbSpaces } from "@atscript/moost-db/testing";135136// Browser client137import { Client } from "@atscript/db-client";138```139140## References — load only what's needed141142| Domain | File | When |143| -------------------- | ------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |144| First contact | [getting-started.md](references/getting-started.md) | Install, `atscript.config`, first `.as` model, `DbSpace` wiring, `syncSchema`, first CRUD call |145| `@db.*` annotations | [annotations.md](references/annotations.md) | `@db.table`, `@db.table.preferredId.uniqueIndex`, `@db.column`, `@db.default*`, indexes, `@db.rel.*`, `@db.json`, `@db.ignore`, `@db.space`, `@meta.id`, gate mode, `@db.amount.currency[.ref]`, `@db.unit[.ref]`, `db.currencyCode` primitive |146| Mongo annotations | [mongo-annotations.md](references/mongo-annotations.md) | `@db.mongo.*`: collection, capped, search.text/static/dynamic/**autocomplete** (typeahead, `edgeGram`/`nGram`/`rightEdgeGram`), query-time **fuzzy** + `$fuzzy` control, index **strategy** (`compound`/`autocomplete`/`text`), same-field multi-index variants via `$index`, patch.strategy, array.uniqueItems, primitives |147| Tables & views | [tables-and-views.md](references/tables-and-views.md) | `DbSpace.getTable/getView/get`, lifecycle, `ensureTable`, `syncIndexes`, view kinds (managed/materialized/external) |148| CRUD | [crud.md](references/crud.md) | `insertOne/Many`, `replaceOne/Many`, `updateOne/Many`, `deleteOne/Many`, `findOne/Many`, `count`, `bulkUpdate/Replace`, `DbError` |149| Queries | [queries.md](references/queries.md) | Filter operators, `$and` / `$or` / `$not`, projection (`$select`), `$sort`, `$skip` / `$limit` / `$page` / `$size`, `$count`, `$with`, `$groupBy` aggregation |150| Patch semantics | [patch.md](references/patch.md) | Field ops (`$inc/$dec/$mul`), array ops, `@db.json` handling, `@db.patch.strategy` merge vs replace, `@db.depth.limit` depth gate, Mongo `CollectionPatcher` |151| Relations | [relations.md](references/relations.md) | `@db.rel.FK/.to/.from/.via`, optional FKs, referential actions, `controls.$with`, fractional ref depth on `/meta`, nested writes |152| Schema sync | [schema-sync.md](references/schema-sync.md) | FNV-1a hash, `__atscript_control` store, distributed lock (`podId`, `lockTtlMs`, `waitTimeoutMs`), `@db.sync.method`, `safe` mode, `onError` policy (loud-by-default failures), `planSchema` dry-run, generated model manifest (`dbPlugin({ manifest })`, `atscriptModels`, `modelsBySpace`), sync hooks, `status: 'error'` entries + retry semantics (hash not persisted on error), dropping indexed/view-referenced/fulltext columns, composite index definition drift |153| SQLite specifics | [adapters-sqlite.md](references/adapters-sqlite.md) | `BetterSqlite3Driver` (with `vector: true` opt-in), FTS5, sqlite-vec / vec0 shadow tables for `@db.search.vector`, `@db.column.collate`, native FKs, in-memory `:memory:`, `$regex` → `LIKE … ESCAPE ''` (restricted subset, throws on `\d`/`[…]`/`(a\|b)`/quantifiers) |154| PostgreSQL specifics | [adapters-postgres.md](references/adapters-postgres.md) | `PgDriver`, pgvector + HNSW, CITEXT, `@db.pg.type`, `@db.pg.schema`, `@db.pg.collate`, tsvector FTS |155| MySQL specifics | [adapters-mysql.md](references/adapters-mysql.md) | `Mysql2Driver`, `@db.mysql.engine/.charset/.collate/.type/.unsigned/.onUpdate`, VECTOR, FULLTEXT, utf8mb4 default |156| MongoDB specifics | [adapters-mongo.md](references/adapters-mongo.md) | `MongoAdapter(db, client?)`, aggregation-pipeline patches, Atlas Search text + vector, `atscript__` index prefix + physical index-name helper (`mongoIndexKey`/`INDEX_PREFIX` for raw-driver `$search` interop), ObjectId primitive |157| Memory specifics | [adapters-memory.md](references/adapters-memory.md) | in-memory adapter; provider-backed runtime surfaces, in-memory tests, JS-native filter semantics |158| Custom adapters | [creating-adapters.md](references/creating-adapters.md) | `BaseDbAdapter` contract, abstract methods, overridable hooks, `supports*` flags, `@atscript/db-sql-tools` reuse |159| `moost-db` HTTP | [moost-db.md](references/moost-db.md) | `AsDbController` / `AsDbReadableController` routes, `TableController` / `ReadableController` binding forms (model token / lazy factory / instance), `provideDbSpace` + `@db.space` multi-space resolution, `super(moost)` subclass fallback, `assertExposed`, `@db.http.path` resolution, value-help endpoints |160| Actions | [actions.md](references/actions.md) | `@DbAction` / `@DbActionID*` / `@DbActionRow*` / `@InputForm` / `@DbActions*` decorators, `{ ids?, input? }` body envelope, row/rows/table actions, `processor: 'backend' \| 'navigate' \| 'custom'`, sync batch `disabled` gate (typed via `requiredFields` tuple, mandatory), `perRow()` helper, `onDisabledRows`, `@DbActionRow*` projection widening, `$actions=true` server-evaluated row availability augmentation, `inputForm` on `/meta` + `GET /meta/form/:name` form-schema endpoint (built-in server-side input validation; form inferable from the param type), preferred-id navigate substitution, `ActionDisabledError`, `/meta` `actions[]` shape |161| URL query syntax | [http-query-syntax.md](references/http-query-syntax.md) | URL filter encoding (`field=v`, `!=`, `>`, `<`, `{v1,v2}`, `~=/re/i`, ranges), `$sort`, `$select`, `$with`, `$page`, `$size`, `$search`/`$index`/`$fuzzy` (Mongo Atlas), `$vector`/`$threshold` 162163…(truncated)