interface <Name> { property* }— reusable property contracts.node <Name> [implements <Iface>, ...] { property* | constraint* }edge <Name>: <FromType> -> <ToType> [@card(min..max)] { property* | constraint* }- Comments: line
//and block/* … */.
<ident>: <TypeRef> [annotation*]
The exact names _rowid, _rowaddr, _rowoffset,
_row_created_at_version, and _row_last_updated_at_version are reserved for
Lance virtual system columns and cannot be declared as interface, node, or edge
properties. Similar names such as _row_id remain valid.
| Scalar | Arrow type |
|---|---|
String |
Utf8 |
Blob |
LargeBinary |
Bool |
Boolean |
I32 / I64 |
Int32 / Int64 |
U32 / U64 |
UInt32 / UInt64 |
F32 / F64 |
Float32 / Float64 |
Date |
Date32 |
DateTime |
Date64 |
Vector(<dim>) |
FixedSizeList(Float32, dim), 1 ≤ dim ≤ i32::MAX |
[<scalar>] |
List(scalar) |
enum(v1, v2, …) |
Utf8 with sorted/dedup'd set of allowed string values |
<scalar>? |
Same as scalar but nullable: true |
| Constraint | On | Effect |
|---|---|---|
@key(p, …) |
node | Primary key; the complete ordered tuple identifies the node and implies indexes on its columns |
@unique(p, …) |
node, edge | Uniqueness across listed columns |
@index(p, …) |
node, edge | Build a scalar (BTREE) index on the columns |
@range(p, min..max) |
node | Numeric range validation (open ranges allowed) |
@check(p, "regex") |
node | Regex pattern validation |
@card(min..max?) |
edge | Edge multiplicity — default 0..*; 0..1, 1..1, 1..*, etc. |
Edge bodies only allow @unique and @index.
@<ident>or@<ident>(<literal>)on any declaration or property.- Known annotations:
@embed("source_property")on a Vector property — records which String property is the embedding source for query-timenearest($v, "string")auto-embedding. It is a catalog annotation; it does not populate the vector at ingest (supply vectors in load data, or pre-fill via the offlineomnigraph embedpipeline). An optionalmodel="…"kwarg (@embed("source_property", model="openai/text-embedding-3-large")) records the embedding model so anearest()query whose embedder uses a different model is rejected loudly;modelis the only supported kwarg. See search/embeddings.md.@description("…"),@instruction("…")on query declarations (carried through to clients).
- Custom annotations are accepted by the parser and surfaced in catalog metadata; unrecognized annotations don't fail compilation.
- Each node type compiles to a table with an
id: Utf8column plus all declared properties (blob columns are stored asLargeBinary);implementsclauses expand the interface's properties into the node. - Each edge type compiles to a table with
id: Utf8, src: Utf8, dst: Utf8plus the edge's own properties. Edge endpoint types (from/to) must exist, and edge names are matched case-insensitively.
For a keyed node, id is derived from the complete typed @key tuple. A
single-column key keeps its canonical scalar spelling. A composite key is an
unambiguous JSON array of those canonical scalar strings, ordered by stable
property identity so renaming a key property cannot change existing node IDs.
Integers use decimal spelling, booleans use true/false, Date and DateTime
use their stored epoch-day and epoch-millisecond values, finite floats use
their stored-width spelling, and both signed zeros become 0; non-finite
floats are not valid keys. Load and mutation use this same derivation. Exported
keyed rows include the physical data.id; on rebuild, a legacy scalar spelling
that is typed-equivalent is accepted and rewritten to the canonical ID, with
typed edge endpoints rewritten in the same import. New hand-authored rows may
omit data.id and let the loader derive it.
A migration plan compares the accepted schema against the desired one and reports whether the change is supported plus the ordered steps it requires:
- Add a type
- Rename a type
- Add a property
- Rename a property
- Add a constraint
- Extend an enum (pure widening: add variants to an existing
enum(...)property — same base type and nullability, every existing value retained; metadata-only at apply time, no table data touched, and the new variants are accepted immediately on every write surface. Narrowing, renaming a variant, or converting between an enum and a freeStringstill plan as unsupported,OG-MF-106. Value order is not significant — the schema IR normalizes enum values, so a reorder is not a change at all.) - Update type or property metadata (annotations)
- Unsupported change (reports the entity and reason; forces the plan to unsupported)
Applying a plan reports whether it was supported, the steps applied, and the resulting manifest version. Concurrent schema applies serialize so they can't interleave.
DropProperty and DropType steps default to Soft mode: the catalog tombstones the entry but the prior column / dataset remains time-travel-reachable via snapshot_at_version(prev) until omnigraph cleanup runs. Soft drops are reversible.
Pass --allow-data-loss (CLI schema apply) or allow_data_loss: true (SDK SchemaApplyOptions) to promote every drop in the plan to Hard mode. Hard drops run cleanup_old_versions on the affected dataset immediately after the manifest publish, making the prior column / dataset unreachable. Irreversible.
This is the direct/embedded schema-apply path — omnigraph schema apply --store … and the embedded SDK apply_schema_with_options(.., SchemaApplyOptions { allow_data_loss: true }) produce identical plans and identical effects.
Cluster-managed graphs are different. A graph served from a cluster evolves only through omnigraph cluster apply, which performs soft drops only (no allow_data_loss path), and the HTTP POST /schema/apply route is disabled (returns 409) for cluster-backed serving — see server and cluster-config. Direct schema apply against a cluster-managed storage path is likewise refused.