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"