Skip to content

Annotations Reference

Complete reference for all @db.* annotations available in .as files. Generic annotations are provided by @atscript/db/plugin via dbPlugin() and work with every adapter. Adapter-specific annotations (PostgreSQL, MySQL, MongoDB) require the corresponding adapter plugin.

Tables & Columns

AnnotationApplies ToArgumentsDescription
@db.tableInterfacename? (string)Mark as database table (defaults to interface name)
@db.table.renamedInterfaceoldName (string)Previous table name for schema sync migration
@db.schemaInterfacename (string)Assign to a database schema/namespace
@db.depth.limitInterfacedepth (number)Security guard on nested writes. Default 0 rejects any nested insert / replace / patch with HTTP 400. Raise N to allow deep writes up to that depth. Does not affect /meta shape.
@db.columnFieldname (string)Override the physical column name (perf note)
@db.column.renamedFieldoldName (string)Previous column name for schema sync migration
@db.column.collateFieldcollation (string)Portable collation: 'binary', 'nocase', or 'unicode'
@db.column.precisionFieldprecision (number), scale (number)Decimal precision/scale for DB storage (e.g., DECIMAL(10,2))
@db.column.dimensionFieldMark as dimension field — groupable in aggregate queries
@db.column.measureFieldMark as measure field — aggregatable (sum, avg, count, min, max). Numeric/decimal only
@db.column.filterableFieldAllow this field in client-side filter clauses when the table is in @db.table.filterable 'manual' mode (see below)
@db.column.sortableFieldAllow this field in client-side sort keys when the table is in @db.table.sortable 'manual' mode (see below)
@db.table.filterableInterfacemode? ('auto' | 'manual')Filter-gating mode. 'auto' (default) keeps all columns filterable; 'manual' requires @db.column.filterable on each filterable field
@db.table.sortableInterfacemode? ('auto' | 'manual')Sort-gating mode. 'auto' (default) keeps all columns sortable; 'manual' requires @db.column.sortable on each sortable field
@db.jsonFieldStore as a single JSON column instead of flattening
@db.ignoreFieldExclude field from the database schema entirely
atscript
@db.table 'users'
@db.schema 'auth'
interface User {
  @db.column 'full_name'
  name: string

  @db.json
  preferences: Preferences

  @db.ignore
  computedField: string

  @db.column.collate 'nocase'
  username: string

  @db.column.precision 10, 2
  price: number
}

@db.column performance

Only use @db.column when you have a genuine reason — such as mapping to a legacy schema or meeting an external naming convention you cannot change. When a table has no @db.column, nested objects, or @db.json fields, the read, filter, and patch paths take a zero-allocation fast path that skips key translation entirely. Adding even one @db.column activates per-row key remapping on every read, write, filter, and patch operation for that table. In high-throughput scenarios this overhead is measurable.

If you control the database schema, prefer naming your Atscript fields to match the desired column names directly. See Custom Column Names for more details.

Query Gate

By default, every column on a @db.table is filterable and sortable — this preserves back-compat for tables where the author hasn't thought about the query surface. When a table should expose a narrow query surface (e.g., a customer-facing reports endpoint), opt into manual mode:

atscript
@db.table 'users'
@db.table.filterable 'manual'
@db.table.sortable 'manual'
interface User {
  @meta.id
  id: number

  @db.column.filterable
  email: string

  @db.column.sortable
  createdAt: number.timestamp
}

Semantics:

  • @db.table.filterable 'manual' makes the readable controller reject (HTTP 400) any filter clause that references a field without @db.column.filterable.
  • @db.table.sortable 'manual' does the same for sort keys against @db.column.sortable.
  • @db.table.filterable 'auto' / @db.table.sortable 'auto' are documentary no-ops — they match the default behaviour when the annotation is absent, but make the author's intent explicit.
  • The two gates are independent: a table can opt into strict filtering while leaving sort open, or vice versa.
  • Without the annotation, behaviour is unchanged from previous releases — all columns are filterable/sortable.

The /meta endpoint reflects the gate: fields[<path>].filterable and .sortable mirror the permissions that the server will enforce, so clients can hide controls for restricted columns.

HTTP

AnnotationApplies ToArgumentsDescription
@db.http.pathInterfacepath (string)HTTP endpoint path for this table. Used by UI for value-help on FK fields. Overwritten at runtime by the controller's computed prefix
atscript
@db.table 'authors'
@db.http.path '/authors'
interface Author {
  @meta.id
  id: number
  name: string
}

When a controller is registered without an explicit prefix, @db.http.path is used as the route. At runtime, the final computed prefix (including parent routes) is written back to @db.http.path on the type metadata, so FK references always carry the correct URL.

Normalization contract

The value carried in type.metadata["db.http.path"] has distinct semantics for writers and readers:

  • Writers (annotation at compile time): the value is an optional path hint. The controller's computed prefix takes precedence at runtime, so the annotation may be omitted or overridden by the mount point.
  • Readers (UI / client code / custom consumers): the runtime value is always (a) prefixed with a leading /, (b) inclusive of the Moost globalPrefix, and (c) the final public URL — usable verbatim with fetch() or new Client(url).

Example: an author writes @db.http.path '/authors'; a consumer reading type.metadata["db.http.path"] at runtime sees /api/db/tables/authors when the controller is mounted under globalPrefix: '/api' at /db/tables/authors.

Depth Limit (security guard)

@db.depth.limit N is a security guard on nested-write payloads — a declared ceiling on how deep a client can send nested inserts, replaces, or patches through @db.rel.from relations. Payloads deeper than N are rejected at the server boundary with HTTP 400 before any database access. Default when the annotation is absent is 0, meaning no nested writes are accepted at all; authors opt in explicitly to N >= 1 when they want the server to accept deep writes.

This annotation affects only write acceptance. It does not change /meta serialization, read/query behaviour, or wire shape (see the separate note below).

atscript
@db.table 'authors'
@db.depth.limit 2
interface Author {
  @meta.id
  id: number
  name: string

  @db.rel.from
  posts?: Post[]
}

BREAKING CHANGE

Tables without @db.depth.limit are now treated as @db.depth.limit 0: the server rejects any nested-write payload (HTTP 400) before reaching the database. Previously the server accepted arbitrary-depth nested writes via the implicit nested-writer. That implicit behaviour has been removed: tables that require nested writes must opt in explicitly with @db.depth.limit N for the appropriate N.

/meta FK ref shape is independent

/meta always ships FK fields as shallow refs ({ id, metadata }) regardless of @db.depth.limit. The target's db.http.path is carried in the ref metadata, so clients can resolve value-help URLs and fetch the target's own /meta on demand when they need deeper structure. Nav-prop trees (@db.rel.from / @db.rel.to / @db.rel.via) are fully expanded in meta regardless of this annotation, so the write-payload shape clients need is unaffected. (Prior releases shipped refDepth: 1 and coupled meta expansion to @db.depth.limit; that coupling has been removed.)

Defaults

AnnotationApplies ToArgumentsDescription
@db.defaultFieldvalue (string)Static default value
@db.default.incrementFieldstart? (number)Auto-incrementing integer (requires number type)
@db.default.uuidFieldRandom UUID string (requires string type)
@db.default.nowFieldCurrent timestamp (requires number or string type)
atscript
@db.table
interface Product {
  @meta.id
  @db.default.uuid
  id: string

  @db.default 'untitled'
  name: string

  @db.default.now
  createdAt: number
}

Indexes

AnnotationApplies ToArgumentsDescription
@db.index.plainFieldname? (string), sort? (string)Standard index, optional sort direction ('asc'/'desc')
@db.index.uniqueFieldname? (string)Unique constraint index
@db.index.fulltextFieldname? (string), weight? (number)Full-text search index with optional weight

Use the same index name on multiple fields to create a composite index.

atscript
@db.table
interface Article {
  @db.index.unique
  slug: string

  @db.index.plain 'date_idx', 'desc'
  publishedAt: number

  // Composite index across two fields
  @db.index.plain 'author_cat'
  authorId: string

  @db.index.plain 'author_cat'
  category: string

  @db.index.fulltext 'search', 3
  title: string

  @db.index.fulltext 'search', 1
  body: string
}
AnnotationApplies ToArgumentsDescription
@db.search.vectorFielddimensions (number), similarity? (string), indexName? (string)Vector search field
@db.search.vector.thresholdFieldvalue (number)Default minimum similarity threshold (0--1)
@db.search.filterFieldindexName (string)Pre-filter field for vector search

Similarity options: 'cosine' (default), 'euclidean', 'dotProduct'. Each adapter maps to its native vector type — see Text Search and Vector Search.

Allowed dimensions values (whitelisted at compile time): 256, 384, 512, 768, 1024, 1536, 2048, 3072, 4096, 6144, 8192, 16384.

atscript
@db.table
interface Document {
  @db.search.vector 1536, 'cosine', 'doc_vec'
  @db.search.vector.threshold 0.7
  embedding: db.vector

  @db.search.filter 'doc_vec'
  category: string
}

Relations

AnnotationApplies ToArgumentsDescription
@db.rel.FKFieldalias? (string)Foreign key (field must use chain ref). Dual role — see note below
@db.rel.toFieldalias? (string)Forward navigation (N:1, FK on this table)
@db.rel.fromFieldalias? (string)Reverse navigation (1:N, FK on other table)
@db.rel.viaFieldjunction (ref)Many-to-many navigation through a junction table
@db.rel.onDeleteFieldaction (string)Referential action on parent delete
@db.rel.onUpdateFieldaction (string)Referential action on parent update
@db.rel.filterFieldcondition (expr)Static filter condition on navigation property
atscript
@db.table
interface Task {
  @db.rel.FK
  @db.rel.onDelete 'cascade'
  projectId: Project.id

  @db.rel.to
  project: Project

  @db.rel.from
  comments: Comment[]

  @db.rel.via TaskTag
  tags: Tag[]

  @db.rel.from
  @db.rel.filter `status = 'open'`
  openSubtasks: Task[]
}

@db.rel.FK dual role

@db.rel.FK serves two purposes depending on the host interface:

  • On a @db.table interface it drives DB-relation semantics — the relation loader pairs it with @db.rel.to / @db.rel.from, it participates in @db.rel.via junction resolution, and the integrity layer validates it at write time.
  • On any other interface (value-help dictionaries, WF forms, plain interfaces) it acts purely as the value-help indicator: the client-side picker resolver reads @db.rel.FK to decide which fields render a value-help picker, and the URL for the picker comes from the target's @db.http.path.

The host-restriction rule was relaxed so the same annotation covers both cases — authors don't need a separate marker for value-help. All other validation rules still apply (the target must be a chain reference to a @meta.id or @db.index.unique field).

Referential Action Values

For @db.rel.onDelete and @db.rel.onUpdate:

ActionDescription
'cascade'Propagate delete/update to related rows
'restrict'Prevent operation if related rows exist
'noAction'Database default behavior (no action)
'setNull'Set FK to null (field must be optional)
'setDefault'Set FK to default value (needs @db.default)

Views

AnnotationApplies ToArgumentsDescription
@db.viewInterfacename? (string)Mark as database view (defaults to interface name)
@db.view.forInterfaceentry (ref)Entry/primary table for a managed view
@db.view.joinsInterfacetarget (ref), condition (expr)Explicit join clause (repeatable)
@db.view.filterInterfacecondition (expr)View WHERE clause
@db.view.havingInterfacecondition (expr)Post-aggregation HAVING clause
@db.view.materializedInterfaceMark the view as materialized
@db.view.renamedInterfaceoldName (string)Previous view name for schema sync migration
atscript
@db.view
@db.view.for Task
@db.view.joins Project, `Project.id = Task.projectId`
@db.view.filter `Task.status = 'active'`
interface ActiveTaskView {
  taskName: Task.name
  projectName: Project.name
  dueDate: Task.dueDate
}

Aggregation

AnnotationApplies ToArgumentsDescription
@db.agg.sumFieldfield (string)SUM of a source column (numeric/decimal only)
@db.agg.avgFieldfield (string)AVG of a source column (numeric/decimal only)
@db.agg.countFieldfield? (string)COUNT — omit argument for COUNT(*), provide field name for non-null count
@db.agg.minFieldfield (string)MIN of a source column
@db.agg.maxFieldfield (string)MAX of a source column

Use aggregation annotations on view fields together with @db.column.dimension on grouping fields.

atscript
@db.view
@db.view.for Order
@db.view.having `totalRevenue > 100`
interface CategoryStats {
  @db.column.dimension
  category: Order.category

  @db.agg.sum 'amount'
  totalRevenue: number

  @db.agg.count
  orderCount: number

  @db.agg.avg 'amount'
  avgOrderValue: number
}

Quantity Tagging (currency & unit)

Bind a numeric field to its dimension (currency code, unit of measure) so the runtime can enforce correct aggregation grouping and the readable controller can guarantee the dimension is always shipped alongside the value.

AnnotationApplies ToArgumentsDescription
@db.amount.currencyField (decimal)code (string)Hard-coded ISO-style currency code ('EUR', 'USD', 'BTC'). Validated against ^[A-Z0-9]{2,10}$. Schema-wide constant — no runtime aggregation constraint. UI reads it from /meta.
@db.amount.currency.refField (decimal)fieldName (string)Bind this amount to a sibling field that holds the per-row currency code. Sibling must exist and resolve to a string (preferably db.currencyCode).
@db.unitField (decimal|number)code (string)Hard-coded unit of measure ('kg', 'rpm', 'qps', 'requests/sec'). Free-form string — no shape validation. Schema-wide constant.
@db.unit.refField (decimal|number)fieldName (string)Bind this quantity to a sibling field holding the per-row unit. Sibling must exist and resolve to a string.

The two forms (literal vs .ref) are mutually exclusive on the same field. Money-bearing fields must be decimal — floats lose cents. Quantity fields accept both decimal (weights, lengths) and number (counts, rates).

db.currencyCode primitive

Companion type for the .ref target — a string constrained to ^[A-Z0-9]{2,10}$ so non-currency strings can't be silently used as the dimension.

atscript
@db.table 'orders'
interface Order {
  @meta.id @db.default.uuid
  id: string

  // Per-row currency: each line carries its own code.
  currency: db.currencyCode

  @db.amount.currency.ref 'currency'
  @db.column.measure
  amount: decimal
}

@db.table 'metrics'
interface Metric {
  @meta.id @db.default.uuid
  id: string

  // Single-currency table: literal form, no sibling field.
  @db.amount.currency 'EUR'
  @db.column.measure
  fee: decimal

  // Mixed-unit measurement: ref form.
  unit: string
  @db.unit.ref 'unit'
  @db.column.measure
  weight: decimal

  // Single-unit metric: literal form.
  @db.unit 'qps'
  @db.column.measure
  rate: number
}

Runtime behavior

  • Aggregation guard. When aggregate() is called against a field carrying @db.amount.currency.ref or @db.unit.ref, the referenced field MUST appear in $groupBy. Otherwise DbError("INVALID_QUERY"). Literal forms (@db.amount.currency 'EUR', @db.unit 'kg') impose no runtime constraint — the dimension is satisfied schema-wide. COUNT(*) is exempt.
  • $select auto-widening. The moost-db readable controller automatically adds the referenced sibling to $select whenever its tagged value is requested. UI can ask for $select=amount and still receive currency in the response. Literal forms are NOT widened — the constant is on the field descriptor, not row data.
  • Field descriptors. TDbFieldMeta exposes currencyCode / currencyRefField / unitCode / unitRefField so clients can format quantities correctly without inspecting raw annotations.

Schema Sync

AnnotationApplies ToArgumentsDescription
@db.sync.methodInterfacemethod (string)Sync strategy: 'drop' or 'recreate'
  • 'drop' — Drop and recreate the table on structural changes (lossy, data is deleted).
  • 'recreate' — Recreate with data preservation on structural changes.

Patch Behavior

AnnotationApplies ToArgumentsDescription
@db.patch.strategyFieldstrategy (string)'replace' (default) or 'merge'

Controls how nested objects are handled during PATCH/update operations. With 'replace', the entire nested object is overwritten. With 'merge', individual sub-fields are deep-merged.

PostgreSQL-Specific

These annotations require the @atscript/db-postgres plugin. See PostgreSQL adapter.

AnnotationApplies ToArgumentsDescription
@db.pg.typeFieldtype (string)Override native PG column type (e.g., CITEXT, INET, MACADDR)
@db.pg.schemaInterfaceschema (string)PostgreSQL schema (default: public)
@db.pg.collateInterface / Fieldcollation (string)Native PG collation (overrides portable @db.column.collate)
atscript
use '@atscript/db-postgres'

@db.table 'users'
@db.pg.schema 'auth'
interface User {
  @meta.id
  @db.default.uuid
  id: string

  @db.pg.type 'CITEXT'
  email: string

  @db.pg.collate 'tr-x-icu'
  name: string
}

MySQL-Specific

These annotations require the @atscript/db-mysql plugin. See MySQL adapter.

AnnotationApplies ToArgumentsDescription
@db.mysql.engineInterfaceengine (string)Storage engine (default: InnoDB)
@db.mysql.charsetInterface / Fieldcharset (string)Character set (default: utf8mb4)
@db.mysql.collateInterface / Fieldcollation (string)Native MySQL collation (overrides portable @db.column.collate)
@db.mysql.unsignedFieldUNSIGNED modifier for integer columns
@db.mysql.typeFieldtype (string)Override native MySQL column type (e.g., MEDIUMTEXT, TINYTEXT)
@db.mysql.onUpdateFieldexpression (string)ON UPDATE expression (e.g., CURRENT_TIMESTAMP)
atscript
use '@atscript/db-mysql'

@db.table 'events'
@db.mysql.engine 'InnoDB'
@db.mysql.charset 'utf8mb4'
interface Event {
  @meta.id
  @db.default.increment
  id: number

  @db.mysql.type 'MEDIUMTEXT'
  description: string

  @db.mysql.unsigned
  viewCount: number

  @db.default.now
  @db.mysql.onUpdate 'CURRENT_TIMESTAMP'
  updatedAt: number
}

MongoDB-Specific

These annotations require the @atscript/db-mongo plugin. See MongoDB adapter.

AnnotationApplies ToArgumentsDescription
@db.mongo.collectionInterfaceMark as MongoDB collection (auto-injects _id)
@db.mongo.cappedInterfacesize (number), max? (number)Capped collection with max byte size and optional doc limit
@db.mongo.search.dynamicInterfaceanalyzer? (string), fuzzy? (number)Dynamic Atlas Search index
@db.mongo.search.staticInterfaceanalyzer? (string), fuzzy? (number), indexName? (string)Named static Atlas Search index
@db.mongo.search.textFieldanalyzer? (string), indexName? (string)Include field in a search index
atscript
use '@atscript/db-mongo'

@db.table 'products'
@db.mongo.collection
@db.mongo.search.static 'lucene.english', 1, 'main_search'
interface Product {
  @meta.id
  _id: mongo.objectId

  @db.mongo.search.text 'lucene.english', 'main_search'
  name: string

  @db.search.vector 1536, 'cosine', 'vec_idx'
  embedding: number[]

  @db.search.filter 'vec_idx'
  category: string
}

Generic search annotations

@db.search.vector and @db.search.filter are generic annotations (not MongoDB-specific) and work across all adapters that support vector search. See the Search section above.

These are not @db.* annotations but are commonly used alongside the database layer.

AnnotationApplies ToArgumentsDescription
@meta.idFieldMark as primary key field (multiple fields form a composite key)
@expect.array.keyFieldArray element key field for patch matching
@expect.array.uniqueItemsFieldEnforce unique items in an array
atscript
@db.table
interface OrderLine {
  // Composite primary key
  @meta.id
  orderId: Order.id

  @meta.id
  productId: Product.id

  quantity: number
}

Released under the MIT License.