Define a concrete SQLite storage model that matches the schema and query MVP. This spec fixes enough of the physical design for:
- schema application
- migration tracking
- query lowering assumptions
- runtime result shaping
- One SQLite table per object type
- Implicit
idprimary key on every object table - Scalar fields stored as direct columns
- Single relations stored as foreign key columns
- Multi relations stored in join tables
- Engine metadata stored in dedicated internal tables
- Only schema
linkfields create relation storage structures - Scalar fields never use join tables in the MVP
Recommended defaults for local development:
journal_mode = WALforeign_keys = ON
The engine should set or validate these at connection startup.
For a schema type:
type Post {
required unique slug: str
required title: str
body: str
required link author: User
}
The object table should look conceptually like:
CREATE TABLE post (
id TEXT PRIMARY KEY,
slug TEXT NOT NULL UNIQUE,
title TEXT NOT NULL,
body TEXT NULL,
author_id TEXT NOT NULL,
FOREIGN KEY (author_id) REFERENCES user(id)
);The storage layer should use deterministic physical names:
- type
User-> tableuser - scalar field
name-> columnname - single
link author-> columnauthor_id - multi
link postsonUser-> join tableuser__posts
The exact naming transformation should be centralized in one module so SQL generation and migrations cannot drift.
Recommended SQLite affinity mapping:
str->TEXTint64->INTEGERfloat64->REALbool->INTEGERuuid->TEXTdatetime->TEXT
Notes:
boolis stored as0or1uuidis stored as canonical text in the MVPdatetimeis stored as ISO-8601 text in UTC
Scalar fields declared with unique map to SQLite UNIQUE constraints.
Example:
type User {
unique nickname: str
required unique email: str
}
Maps conceptually to:
CREATE TABLE user (
id TEXT PRIMARY KEY,
nickname TEXT NULL UNIQUE,
email TEXT NOT NULL UNIQUE
);For optional unique scalar fields, Gelite uses SQLite's UNIQUE behavior:
duplicate non-null values are rejected, but multiple NULL values are allowed.
The MVP treats uniqueness as a constraint on present values.
Single relations map to a nullable or non-nullable foreign key column on the owning object's table.
Example:
type Post {
link author: User
}
Maps to:
author_id TEXT NULL REFERENCES user(id)required link author: User becomes NOT NULL.
For an MVP insert, a declared single-link assignment writes the related object
id to this <field>_id column. The temporary query-language string-literal
link shorthand is bound as a SQLite TEXT value; it is not stored as a nested
object or as a value in a join table. An optional single-link null assignment
writes SQLite NULL.
Multi relations use a dedicated join table named:
<source_table>__<field_name>
Example:
type User {
multi link posts: Post
}
Maps to:
CREATE TABLE user__posts (
source_id TEXT NOT NULL,
target_id TEXT NOT NULL,
position INTEGER NULL,
PRIMARY KEY (source_id, target_id),
FOREIGN KEY (source_id) REFERENCES user(id),
FOREIGN KEY (target_id) REFERENCES post(id)
);Notes:
positionis reserved for future stable ordering but may remain unused in the first runtime implementation.- The MVP treats multi links as unordered at the language level.
- Only
multi linkfields produce join tables. Multi-valued scalar storage is out of scope for the MVP.
Every object row has:
id TEXT PRIMARY KEY
For the current insert milestone, the query runtime generates a UUID v4 and
supplies it to SQL rendering. The renderer binds it as the id column value in
the same prepared INSERT statement as user-provided scalar and single-link
values. After successful execution, the CLI reports the generated id. The
schema language and query language do not expose user control over identity
definition in the MVP.
SQLite constraint failures, including missing required values and invalid foreign-key targets when foreign keys are enabled, are execution errors; semantic validation remains responsible for the query language's field, cardinality, and literal-type rules.
The first version should create at least these internal tables.
Tracks applied migration revisions.
CREATE TABLE _engine_schema_versions (
version_id TEXT PRIMARY KEY,
checksum TEXT NOT NULL,
applied_at TEXT NOT NULL,
schema_snapshot TEXT NOT NULL
);Stores semantic object definitions for diagnostics and diff support.
Catalog ids use SQLite INTEGER, which is a signed 64-bit value. The semantic
schema catalog uses the same signed integer range for object and field ids so
metadata planning does not need unsigned-to-signed conversion.
CREATE TABLE _engine_catalog_objects (
object_id INTEGER PRIMARY KEY,
name TEXT NOT NULL UNIQUE
);Stores semantic field definitions.
CREATE TABLE _engine_catalog_fields (
object_id INTEGER NOT NULL,
field_id INTEGER NOT NULL,
name TEXT NOT NULL,
field_kind TEXT NOT NULL,
cardinality TEXT NOT NULL,
scalar_type TEXT NULL,
target_object_id INTEGER NULL,
is_implicit INTEGER NOT NULL,
is_unique INTEGER NOT NULL,
PRIMARY KEY (object_id, field_id),
FOREIGN KEY (object_id) REFERENCES _engine_catalog_objects(object_id),
FOREIGN KEY (target_object_id) REFERENCES _engine_catalog_objects(object_id)
);Catalog field metadata uses these stored text values:
field_kind:scalarorlinkcardinality:optional,required, ormanyscalar_type:str,int64,float64,bool,uuid,datetime, orNULLfor link fields
target_object_id is NULL for scalar fields and the target
_engine_catalog_objects.object_id for link fields.
Boolean metadata is stored as integer values:
0for false1for true
These catalog tables are engine-owned metadata, not user-facing schema tables.
The migration MVP is append-only:
- Compare desired schema catalog to current catalog
- Generate one migration plan
- Apply DDL inside a transaction where SQLite allows it
- Record the migration in
_engine_schema_versions - Update catalog metadata tables
The first milestone can restrict supported schema changes to:
- create type
- add nullable scalar field
- add required scalar field only if a default/backfill strategy exists
- add unique scalar field only when existing data can satisfy the uniqueness rule
- add single relation
- add multi relation join table
Changes that may require table rebuilds can be rejected initially with a clear diagnostic.
This storage model is designed around these compiler assumptions:
- root
selectbegins from one object table - scalar fields come from direct columns
- single relations use joins on
<field>_id - multi relations may use secondary queries or grouped joins
- relation traversal is limited to declared
linkfields - backlinks or inferred inverse traversals do not exist in the MVP storage contract
The runtime is allowed to fetch nested multi relations with follow-up queries if that keeps the first implementation simpler and more predictable.
The runtime should reconstruct nested JSON-like objects using:
- object identity deduplication by
id - per-shape field selection
- merge rules for repeated joined rows
Suggested rule:
- joined scalar and single-relation selections may be handled in one SQL query
- multi-relation nested shapes may use batched follow-up queries keyed by parent ids
- filter paths may traverse declared single-link chains such as
.author.id
This keeps the initial lowering model tractable.
The MVP should create indexes for:
- every foreign key column on object tables
target_idandsource_idaccess on join tables
Optional future indexes can be introduced later by schema directives.
The MVP uses one explicit policy:
- single relations use SQLite foreign keys with
ON DELETE RESTRICT - join tables delete rows with
ON DELETE CASCADEfrom either side
Constraint failures are returned to the caller.
For:
type User {
required name: str
}
type Post {
required title: str
required link author: User
}
The core physical layout is:
CREATE TABLE user (
id TEXT PRIMARY KEY,
name TEXT NOT NULL
);
CREATE TABLE post (
id TEXT PRIMARY KEY,
title TEXT NOT NULL,
author_id TEXT NOT NULL REFERENCES user(id) ON DELETE RESTRICT
);Out of scope until the basic migration and query loop is proven:
- generated columns
- partial indexes
- full-text search
- enum storage optimizations
- online migration strategies
- schema branching