Creating tables, defining schemas, indexes, and constraints in TrailBase
TrailBase uses SQLite as its database engine, providing a powerful yet simple foundation for your application. This guide covers schema design, table creation, and optimization strategies.
CREATE TABLE articles ( id BLOB PRIMARY KEY NOT NULL CHECK(is_uuid_v7(id)) DEFAULT (uuid_v7()), author BLOB NOT NULL REFERENCES _user(id) ON DELETE CASCADE, title TEXT NOT NULL, intro TEXT NOT NULL, tag TEXT NOT NULL, body TEXT NOT NULL, image TEXT CHECK(jsonschema('std.FileUpload', image, 'image/png, image/jpeg')), created INTEGER DEFAULT (UNIXEPOCH()) NOT NULL) STRICT;
CREATE TABLE profiles ( user BLOB PRIMARY KEY NOT NULL REFERENCES _user(id) ON DELETE CASCADE, -- Require at least 3 alphanumeric characters username TEXT NOT NULL CHECK(username REGEXP '^[\w]{3,}$'), created INTEGER DEFAULT (UNIXEPOCH()) NOT NULL, updated INTEGER DEFAULT (UNIXEPOCH()) NOT NULL) STRICT;-- Ensure usernames are uniqueCREATE UNIQUE INDEX _profiles__username_index ON profiles (username);
TrailBase provides the REGEXP operator for pattern validation. Regular expressions are evaluated efficiently using Rust’s regex engine.
-- Index for foreign key lookupsCREATE INDEX _articles__author_index ON articles(author);-- Index for filteringCREATE INDEX _posts__published_index ON posts(published);-- Unique index for constraintsCREATE UNIQUE INDEX _profiles__username_index ON profiles(username);
-- Composite index for queries like: WHERE user = ? AND created > ?CREATE INDEX _todos__user_created_index ON todos(user, created);-- Unique composite constraintCREATE UNIQUE INDEX _likes__user_post_index ON likes(user, post_id);
Index Order Matters: SQLite can only use the leftmost columns of an index. An index on (user, created) helps queries filtering by user alone, but not queries filtering only by created.
CREATE TABLE comments ( id INTEGER PRIMARY KEY, post_id INTEGER NOT NULL REFERENCES posts(id) ON DELETE CASCADE, user_id BLOB NOT NULL REFERENCES _user(id) ON DELETE CASCADE, text TEXT NOT NULL) STRICT;
Cascade Actions:
ON DELETE CASCADE - Delete child records when parent is deleted
ON DELETE SET NULL - Set foreign key to NULL when parent is deleted
ON DELETE RESTRICT - Prevent parent deletion if children exist
CREATE TABLE products ( id INTEGER PRIMARY KEY, name TEXT NOT NULL, price REAL NOT NULL CHECK(price > 0), rating REAL CHECK(rating >= 0 AND rating <= 5), email TEXT CHECK(email LIKE '%_@_%._%'), status TEXT CHECK(status IN ('draft', 'published', 'archived'))) STRICT;
CREATE TABLE todos ( id INTEGER PRIMARY KEY, text TEXT NOT NULL, completed INTEGER NOT NULL DEFAULT 0, priority INTEGER DEFAULT 1, created_at INTEGER DEFAULT (UNIXEPOCH()) NOT NULL, due_date INTEGER -- NULL by default) STRICT;
CREATE TRIGGER _profiles__updated_trigger AFTER UPDATE ON profiles FOR EACH ROW BEGIN UPDATE profiles SET updated = UNIXEPOCH() WHERE user = OLD.user; END;
CREATE TRIGGER _posts__validate_dates BEFORE INSERT ON posts FOR EACH ROW BEGIN SELECT RAISE(ABORT, 'published_at must be after created_at') WHERE NEW.published_at < NEW.created_at; END;
Triggers run for every row operation and can impact performance. Use them judiciously.
Views provide read-only, computed data combining multiple tables:
-- Join articles with user profilesCREATE VIEW articles_view AS SELECT a.*, p.username FROM articles AS a LEFT JOIN profiles AS p ON p.user = a.author;
CREATE VIEW profiles_view AS SELECT p.*, -- Type cast required for JSON schema generation CAST(CASE WHEN avatar.file IS NOT NULL THEN CONCAT('/api/auth/avatar/', uuid_text(p.user)) ELSE NULL END AS TEXT) AS avatar_url, CAST(IIF(editors.user IS NULL, FALSE, TRUE) AS INTEGER) AS is_editor FROM profiles AS p LEFT JOIN _user_avatar AS avatar ON p.user = avatar.user LEFT JOIN editors ON p.user = editors.user;
Type Casts Required: TrailBase generates JSON schemas from view column types. Use explicit CAST() for computed columns so TrailBase can infer the correct type.
CREATE TABLE posts ( id INTEGER PRIMARY KEY, title TEXT NOT NULL, -- Single file upload with type constraints image TEXT CHECK(jsonschema('std.FileUpload', image, 'image/png, image/jpeg')), -- Multiple file uploads gallery TEXT CHECK(jsonschema('std.FileUploads', gallery))) STRICT;
The JSON schema validation ensures uploaded files meet your requirements. See the File Uploads guide for details.
-- Create FTS index on articlesCREATE VIRTUAL TABLE articles_fts USING fts5( title, body, content='articles', content_rowid='id');-- Triggers to keep FTS index in syncCREATE TRIGGER articles_fts_insert AFTER INSERT ON articles BEGIN INSERT INTO articles_fts(rowid, title, body) VALUES (new.id, new.title, new.body);END;CREATE TRIGGER articles_fts_update AFTER UPDATE ON articles BEGIN UPDATE articles_fts SET title = new.title, body = new.body WHERE rowid = old.id;END;CREATE TRIGGER articles_fts_delete AFTER DELETE ON articles BEGIN DELETE FROM articles_fts WHERE rowid = old.id;END;
-- Create a custom query that checks group membershipCREATE VIRTUAL TABLE _is_editor USING define(( SELECT EXISTS (SELECT * FROM editors WHERE user = $1) AS is_editor));