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 One of: str, i64, u64, f64, bool, bytes.
required bool Default false. When true, writes that omit this column are rejected.

Picking the right type for common data

Your data Use Why
User ID, order ID, ULID, UUID str Travels as canonical text form.
Email, name, category str Lowercase emails at write time if you want case-insensitive equality.
Price, amount, balance i64 Store as minor units (cents, paise). Float rounding errors compound.
Quantity, refundable count i64 Signed so refunds can be negative.
Timestamp (epoch ms or µs) u64 Pick ms or µs once per table and stay consistent.
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"