OriginChain docs
reference · schemas

Schema reference

Every field, every option, every constraint shape. Use this page to look something up; use the tutorial if you want to learn it for the first time.

1. Top-level fields.

Field Type Required Notes
namespace string yes Letter-prefixed, snake_case. Groups related tables.
table string yes Letter-prefixed, snake_case. The table's name. Queries address it as namespace.table.
primary_key string[] yes One or more column names that uniquely identify a row. Composite keys are supported in SQL queries; the /rows/:schema/:pk shortcut is single-column only.
version int no Monotonic counter. Defaults to 1. Schema migrations require an incremented value - see migrations.

2. Columns.

One [[columns]] block per column. Three fields per block.

Field Type Notes
name string The column name. Snake_case. Used as the JSON key in row writes.
ty enum Core scalars: str, i64, u64, f64, bool, bytes. Extended types: timestamp, date, uuid, decimal, enum, text, json, list, inet, interval, point — see the table below.
required bool Default false. When true, writes that omit this column are rejected.

Extended column types

Beyond the six core scalars, OriginChain accepts purpose-built types. Each is format-validated at write time — a value of the wrong shape is rejected with a 400 type-mismatch. They store and index on the same substrate as their backing scalar, so range scans and filters stay fast.

Type Wire form Notes
timestamp integer (epoch ms) Milliseconds since 1970-01-01 UTC. Indexed and range-scanned like an integer — date ranges are fast.
date integer (epoch days) Whole days since 1970-01-01, no time component.
interval integer (ms) A duration in whole milliseconds; may be negative.
uuid string Canonical 8-4-4-4-12 hex form; validated on write.
decimal string Exact fixed-point (e.g. "19.99") — no binary-float rounding, money-safe. Optional precision / scale are enforced at write. Sent as a string, not a number.
enum string Constrained to a declared variants list; a value outside the set is rejected.
text string Unbounded UTF-8 prose. Behaves like str.
json object / array A structured JSON document, stored and returned verbatim.
list array A homogeneous array; an optional element type validates every item.
inet string An IPv4 or IPv6 address; parsed and validated on write.
point object / pair A geographic point: {"lat": …, "lng": …} or [lng, lat].

Picking the right type for common data

Your data Use Why
User ID, order ID, ULID str Travels as canonical text form. For RFC-4122 UUIDs use uuid to get format validation.
Email, name, category str Lowercase emails at write time if you want case-insensitive equality.
Price, amount, balance decimal Exact fixed-point — no float rounding. Send as a string ("19.99"). Or keep using i64 minor units (cents, paise) if you prefer integers.
Quantity, refundable count i64 Signed so refunds can be negative.
Event time, created/updated at timestamp Epoch milliseconds. Range-scans like an integer; the planner knows it's a time so date filters stay fast.
Rate, percentage f64 Float is fine for ratios where rounding doesn't matter.
Active flag, deleted flag bool Single bit.
Compressed blob, encoded payload bytes Opaque to the engine.

3. Indexes.

One [[indexes]] block per index. Without indexes, every filter does a full table scan.

[[indexes]]
name    = "by_status"
columns = ["status"]                  # single-column

[[indexes]]
name    = "by_customer_and_time"
columns = ["customer_id", "placed_ms"]  # composite, in declared order
Field Notes
name A name for the index. Used in EXPLAIN output.
columns List of column names. Composite indexes serve left-prefix matches - [customer_id, placed_ms] can serve WHERE customer_id = ? alone, but not WHERE placed_ms = ? alone.

When to add an index: any column you filter on in WHERE or use as a foreign-key target. Indexes cost write throughput - don't add them speculatively.

4. Relations (graph edges).

A relation turns a foreign-key column into a graph edge. The forward and reverse edges are written atomically when you save a row.

[[relations]]
name          = "supplied_by"         # the verb you'll walk in code
from_col      = "supplier_id"         # the FK column on THIS table
bidirectional = true                  # default; reverse edge written atomically

[relations.target]
namespace = "shop"                    # target table's namespace
table     = "suppliers"               # target table's name
pk        = "id"                      # target table's primary-key column

# Self-relations work - direction tags resolve the collision:
[[relations]]
name          = "follows"
from_col      = "followee_id"
bidirectional = true

[relations.target]
namespace = "social"
table     = "users"
pk        = "id"
Field Notes
name The edge verb - used as the rel= param when walking the edge with the graph endpoint.
from_col The column on this table that holds the target's primary key.
bidirectional Default true. When true, the reverse edge is also indexed - "who points at this row?" becomes a one-call lookup.
[relations.target] namespace Target table's namespace.
[relations.target] table Target table's name.
[relations.target] pk Target table's primary-key column.

Relations alone do not enforce that the target row exists. For that, also declare a foreign key (next section).

5. Foreign keys.

Reject writes that point at non-existent rows. The integrity check is a single key fetch, so it's fast.

[[foreign_keys]]
name          = "fk_customer"         # optional - defaults to "<from_col>_fk"
from_col      = "customer_id"         # column on THIS table
target_schema = "shop.customers"      # "namespace.table" of the target
target_col    = "id"                  # PK or unique-indexed column on target
on_delete     = "no_action"           # no_action | restrict | set_null
Field Notes
name Optional. Defaults to "<from_col>_fk". Used in error messages.
from_col Column on this table.
target_schema The target schema as "namespace.table".
target_col Must be the target's primary key, or a unique-indexed column.
on_delete no_action (default), restrict, or set_null. cascade and set_default are not yet supported.

6. CHECK constraints.

Reject writes whose values violate a rule. Checks run on every write - keep them simple.

[[check_constraints]]
name       = "qty_positive"
expression = "qty > 0"

[[check_constraints]]
name       = "country_or_continent_set"
expression = "country IS NOT NULL OR continent IS NOT NULL"

[[check_constraints]]
name       = "status_in_set"
expression = "status IN ('pending', 'paid', 'cancelled')"

Expression grammar

Operator Example
= != < <= > >= qty > 0
AND OR NOT price > 0 AND price < 1000000
IS NULL / IS NOT NULL description IS NOT NULL
IN (...) status IN ('pending', 'paid')

Three-valued logic applies: NULL on any operand yields NULL, which fails the CHECK. If a column can be NULL and you want the CHECK to ignore null values, use col IS NULL OR col > 0.

7. JSON extractions.

Pull a value out of a nested JSON row into a queryable column at write time. Useful when your row body is already-structured JSON and you don't want to flatten it manually.

# Pull a derived column out of a nested JSON row at write time.
# The dotted path walks into the JSON; missing fields emit zero values.

[[extractions]]
name = "customer_country"
path = "customer.address.country"
ty   = "str"

[[extractions]]
name = "order_total_cents"
path = "totals.grand_cents"
ty   = "i64"
Field Notes
name The derived column's name. Queryable with SQL after extraction.
path Dotted JSON path walked at write time. Missing fields emit the type's zero value.
ty One of the six column types.

Extractions are not the same as vector or full-text indexes. There is no [[extractions.fts]] or [[extractions.vector]] block - those live on separate runtime endpoints.

8. Full example.

Every block from this page in one schema.

# A schema using every block at once.
namespace   = "shop"
table       = "orders"
primary_key = ["id"]

[[columns]]
name = "id"
ty   = "str"
required = true

[[columns]]
name = "customer_id"
ty   = "str"
required = true

[[columns]]
name = "product_id"
ty   = "str"
required = true

[[columns]]
name = "qty"
ty   = "i64"

[[columns]]
name = "total_cents"
ty   = "i64"

[[columns]]
name = "placed_ms"
ty   = "u64"

[[indexes]]
name    = "by_customer"
columns = ["customer_id"]

[[indexes]]
name    = "by_product_and_time"
columns = ["product_id", "placed_ms"]

[[relations]]
name          = "bought_product"
from_col      = "product_id"
bidirectional = true

[relations.target]
namespace = "shop"
table     = "products"
pk        = "id"

[[relations]]
name          = "by_customer"
from_col      = "customer_id"
bidirectional = true

[relations.target]
namespace = "shop"
table     = "customers"
pk        = "id"

[[foreign_keys]]
name          = "fk_customer"
from_col      = "customer_id"
target_schema = "shop.customers"
target_col    = "id"
on_delete     = "restrict"

[[check_constraints]]
name       = "qty_positive"
expression = "qty > 0"

[[check_constraints]]
name       = "total_positive"
expression = "total_cents > 0"