Skip to content

Schema

Schemas map Zig structs to database tables at compile time with zero runtime overhead. The Schema.define function analyzes your struct fields and generates column lists, INSERT placeholders, and CREATE TABLE SQL for both SQLite and PostgreSQL.

Every schema is a regular Zig struct with a pub const Meta declaration produced by Schema.define:

const zzz_db = @import("zzz_db");
pub const User = struct {
id: i64,
name: []const u8,
email: []const u8,
inserted_at: i64 = 0,
updated_at: i64 = 0,
pub const Meta = zzz_db.Schema.define(@This(), .{
.table = "users",
.primary_key = "id",
.timestamps = true,
});
};

The Meta type is generated entirely at compile time and contains all the metadata the repository and query builder need.

The second argument to Schema.define is a SchemaOpts struct:

OptionTypeDefaultDescription
table[:0]const u8RequiredThe database table name
primary_key[]const u8"id"Name of the primary key field
timestampsbooltrueWhether inserted_at/updated_at are timestamps
associations[]const AssociationDef&.{}Association definitions (has_many, belongs_to, etc)
virtual_fields[]const []const u8&.{}Fields excluded from INSERT (computed/virtual)

zzz_db automatically maps Zig types to SQL column types at compile time:

Zig typeSQLite typePostgreSQL type
i16, u16, i32, u32INTEGERINTEGER
i64, u64BIGINTBIGINT
f32, f64REALDOUBLE PRECISION
boolBOOLEANBOOLEAN
[]const u8TEXTTEXT
?[]const u8TEXTTEXT
Other pointer typesBLOBBYTEA

Optional types (?T) are mapped to the same SQL type as T and are treated as nullable columns.

The field named by primary_key (default "id") is treated specially:

  • It is excluded from INSERT column lists (auto-generated by the database)
  • For integer PKs, SQLite uses INTEGER PRIMARY KEY AUTOINCREMENT and PostgreSQL uses BIGSERIAL PRIMARY KEY
  • Non-integer PKs use a standard PRIMARY KEY constraint
// Integer primary key (auto-increment)
pub const Post = struct {
id: i64,
title: []const u8,
pub const Meta = zzz_db.Schema.define(@This(), .{
.table = "posts",
.primary_key = "id",
.timestamps = false,
});
};
// Custom primary key name
pub const Country = struct {
code: []const u8,
name: []const u8,
pub const Meta = zzz_db.Schema.define(@This(), .{
.table = "countries",
.primary_key = "code",
.timestamps = false,
});
};

When timestamps is set to true, fields named inserted_at and updated_at are marked as timestamp fields in the schema metadata. You must declare these fields on your struct:

pub const Article = struct {
id: i64,
title: []const u8,
body: []const u8,
inserted_at: i64 = 0,
updated_at: i64 = 0,
pub const Meta = zzz_db.Schema.define(@This(), .{
.table = "articles",
.timestamps = true,
});
};

Timestamp values are stored as i64 (Unix epoch). Default values of 0 ensure they are populated by your application logic on insert and update.

Virtual fields are included in SELECT queries but excluded from INSERT statements. This is useful for computed columns, aggregates, or fields populated by database triggers:

pub const Item = struct {
id: i64,
name: []const u8,
full_name: []const u8 = "",
pub const Meta = zzz_db.Schema.define(@This(), .{
.table = "items",
.primary_key = "id",
.timestamps = false,
.virtual_fields = &.{"full_name"},
});
};

In the example above, full_name appears in SELECT column lists but not in INSERT statements:

// Meta.columns -> "id, name, full_name"
// Meta.insert_columns -> "name"

The Meta type produced by Schema.define exposes the following comptime constants:

ConstantTypeDescription
Table[:0]const u8Table name
PrimaryKey[]const u8Primary key field name
TimestampsboolWhether timestamps are enabled
StructtypeThe original struct type
field_countusizeTotal number of fields
field_info[N]FieldInfoPer-field metadata (name, sql_type, nullable, is_pk, is_timestamp)
field_names[N][]const u8Array of field name strings
columns[]const u8Comma-separated column list for SELECT
insert_columns[]const u8Comma-separated columns for INSERT (excludes PK and virtual)
insert_placeholders[]const u8SQLite placeholders (?, ?, ?)
insert_placeholders_pg[]const u8PostgreSQL placeholders ($1, $2, $3)
create_table[:0]const u8CREATE TABLE IF NOT EXISTS SQL (SQLite)
create_table_pg[:0]const u8CREATE TABLE IF NOT EXISTS SQL (PostgreSQL)
associations[]const AssociationDefAssociation metadata
virtual_fields[]const []const u8Virtual field names

The createTable function dispatches by dialect:

const sql = User.Meta.createTable(.sqlite); // SQLite DDL
const sql_pg = User.Meta.createTable(.postgres); // PostgreSQL DDL

Given this schema:

pub const User = struct {
id: i64,
name: []const u8,
email: []const u8,
inserted_at: i64 = 0,
updated_at: i64 = 0,
pub const Meta = zzz_db.Schema.define(@This(), .{
.table = "users",
.primary_key = "id",
.timestamps = true,
});
};
-- User.Meta.create_table
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT,
email TEXT,
inserted_at BIGINT,
updated_at BIGINT
)
-- User.Meta.columns
-- "id, name, email, inserted_at, updated_at"
-- User.Meta.insert_columns
-- "name, email, inserted_at, updated_at"
-- User.Meta.insert_placeholders
-- "?, ?, ?, ?"

Schemas can declare associations for use with repo.preload():

pub const User = struct {
id: i64,
name: []const u8,
email: []const u8,
inserted_at: i64 = 0,
updated_at: i64 = 0,
pub const Meta = zzz_db.Schema.define(@This(), .{
.table = "users",
.primary_key = "id",
.timestamps = true,
.associations = &.{
.{
.name = "posts",
.assoc_type = .has_many,
.related_table = "posts",
.foreign_key = "user_id",
},
.{
.name = "profile",
.assoc_type = .has_one,
.related_table = "profiles",
.foreign_key = "user_id",
},
},
});
};

Available association types: has_many, has_one, belongs_to, many_to_many.

For many_to_many, additionally specify join_table, join_fk, and join_assoc_fk.

zzz_db does not enforce naming conventions — you explicitly set the table name in SchemaOpts. Common conventions:

  • Use lowercase plural names for tables: users, posts, order_items
  • Use snake_case for column names, matching Zig field names
  • Use inserted_at and updated_at for timestamp fields (required by the timestamps feature)

Use Schema.meta(T) to retrieve the Meta type from any schema struct:

const M = zzz_db.Schema.meta(User);
// M.Table == "users"
// M.PrimaryKey == "id"
// M.field_count == 5

This is useful in generic functions that operate on any schema type.