- Layer: L4
- Implementation crates:
ocql-database,ocql-schema - Status: Ratified for the on-disk format and value model; Provisional for the schema-extension rules in §6.
- Version: 0.1
L4 specifies the data model that L3 evaluates against and that L4-X extractors populate:
- The value space (§3).
- The relation model (§4).
- The on-disk format (§5).
- The
.dbschemeschema language (§6).
L4 is the most stable layer in open-codeql: a change here ripples into every extractor and every query.
| Term | Meaning |
|---|---|
| Database | An on-disk artifact produced by extraction; the input to L3. |
| Relation | A named, typed, set-valued collection of tuples. |
| Tuple | An ordered sequence of values. |
| Schema | A .dbscheme text describing relations, columns, and union types. |
| Entity | A value of an @-prefixed database type, identified by a 64-bit id. |
| InternedString | A handle into the database's per-database string table. |
Value = Int(i64)
| Float(f64) // ordered-float total order
| String(handle)
| Bool(bool)
| Entity(u64)
| Null
Reference: crates/ocql-database/src/value.rs.
- All variants are
Copy,Hash,Ord.Floatuses theordered-floattotal order so that NaN has a stable position. Stringvalues are always interned: a database with two identical string contents must have one handle for both.Nullis the unit of "no value"; relations use it sparingly. Most tables are total (no nulls); columns that admit null must be declared so in the schema (§6).
A relation is (name, columns, tuples) where:
nameis a non-empty ASCII identifier matching[a-z][a-zA-Z0-9_]*.columnsis a list of(name, ColumnType)pairs declared in the schema.tuplesis a set ofTuple. Persisted as a sorted vector for cache-friendliness.
ColumnType = QlType + DbType + RefKind
QlType = int | float | string | varchar(N) | boolean | date
DbType = the QL type (primitive or @-entity)
RefKind = ref | (none) // 'ref' means foreign-key reference
The implementation lazily builds multi-column indexes on demand from
queries (Database::lookup_each, lookup_any in
crates/ocql-database/src/database.rs). Indexes are not persisted;
they are rebuilt on load.
Index keys are SmallVec<[usize; 4]> — a column-permutation. The same
relation may have multiple live indexes simultaneously.
A database is append-only during extraction and sealed for evaluation:
- An extractor may
inserttuples into any relation it owns. - L3 inserts new relations (one per evaluated predicate) but does not mutate base relations.
- After
save_to_filethe database is immutable.
The binding format is the custom binary in
crates/ocql-database/src/serialize.rs. The contract is:
-
4-byte magic
OCQL. -
1-byte format version (currently
1). A loader must reject unknown versions. -
Little-endian throughout.
-
Header → string table → relations.
-
Each relation: name, column metadata, tuple count, tuples.
-
Each tuple:
value-count, then for each value atagbyte and the value bytes:Tag Variant Payload 0 Int i64 1 Float f64 2 String u32 handle 3 Bool u8 4 Entity u64 5 Null – -
The format is not stable across format-version bumps. A bump requires a migration tool in
scripts/.
Reference: crates/ocql-schema/src/parse.rs. The language is a
subset of CodeQL's .dbscheme syntax. The accepted productions are:
DbScheme = (Comment | Table | Union | Case)*
Table = name '(' Column (',' Column)* ')' Keyset*
Column = ('unique')? QlType name ':' DbType ('ref')?
Keyset = '#keyset' '[' name (',' name)* ']'
Union = '@' name '=' DbType ('|' DbType)+
Case = 'case' '@' name '.' name 'of' CaseArm+
CaseArm = name '=' '@' name
Block comments nest. Line comments use //.
The current parser supports:
- ✅ Tables, columns, keysets, primary-key uniqueness.
- ✅ Unions (
@container = @file | @folder). - ✅ Casetypes (
case @x.kind of …). ⚠️ Multi-line keysets and complex foreign-key declarations are partially supported; seecrates/ocql-schema/src/parse.rs.- ❌ Annotations on relations (e.g.
@@deprecated) — Provisional.
The schema is the single source of truth for what an extractor must emit. An extractor is conformant iff:
- It populates exactly the relations declared in its schema.
- Every emitted tuple satisfies the column types and
refconstraints. - Every
uniquecolumn is unique across the relation's extent. - Every
@-entity id is assigned by exactly one relation that owns it (the "key relation" for that entity type).
Violations are extractor bugs, not query bugs.
Each language extractor crate (ocql-extractor-*) ships its schema as a
const string compiled into the binary (e.g.
crates/ocql-extractor-java/src/schema.rs:11). At database creation
time, Database::from_schema parses the string and seeds the database
with empty relations.
This means the .dbscheme lives in Rust source, not on disk, which
makes schema changes a Rust-rebuild rather than a config swap. The
trade-off is intentional: it keeps the schema and the emitter logic
co-located.
A change to L4 is governed by:
| Change | Requires |
|---|---|
| Add a relation to a schema | Extractor PR; no L4 bump. |
| Add a column to a relation | Extractor PR; databases must be re-extracted. |
| Change a column's type | Format-version bump; migration tool. |
Change Value variants or tags |
Format-version bump; migration tool. |
| Change the on-disk container format | Format-version bump. |
A format-version bump is announced by incrementing the byte after the
magic and shipping a vN-to-vN+1 script under scripts/migrations/.
crates/ocql-database/tests/ (Provisional — to be created) holds:
- A round-trip test for every value variant.
- A round-trip test for a representative non-trivial schema (use
ocql-extractor-java's schema as the canonical fixture). - A version-rejection test (loader must error on
magic + 0xff).
- Persistent indexes (§4.2): is the cost of rebuilding indexes on load worth the simpler on-disk format, or do we add an optional index sidecar?
Nulldiscipline (§3): should every nullable column be marked in the schema, or do we keep the current "infer from extractor" model?- Schema annotations (§6.1): do we model
@@deprecated,@@cached,@@externalat L4 (extractor concern) or L1 (query concern)?