Source:
src/db/README.md— this page is rendered directly from the file in the repository. Edit it there.
Db — SQLite Tracking for Workplates and the Files Inside Them
This module is the persistent ledger of "which files the browser uploaded into a workplate, which G-code we produced for that workplate, and where each one lives on disk." Two tables, one purpose.
Track sessions, never copy bytes. The filesystem is the storage; SQLite is the index.
Why it exists
The server's upload / slice / download lifecycle spans three independent HTTP and WebSocket round-trips:
POST /api/uploadcreates a workplate (request_uuid, akaruuid), writes one or more{file_uuid}.{ext}files to the work dir, and returns{ ruuid, ofids: [file_uuid, ...] }.WS Slice { request_uuid, scene, settings }reads each scene entry's file byfile_uuid, runs the pipeline, writes{ruuid}.gcode.GET /api/download/:ruuidstreams the G-code back;GET /api/file/:file_uuidstreams an uploaded model back (used by the viewer on cold reload).
In between those, the server may restart, the browser may reconnect, or the user may revisit a session days later via ListSessions. SQLite gives us a single source of truth for "does this UUID still have a result on disk, and if so, where?" — without copying the actual mesh or G-code into a database row.
The contract
- The DB stores paths and metadata, never file contents. Files live in the server's
work_dir; the DB stores the path and size. - One
requestsrow per workplate (request_uuid). UUIDs are generated by the upload handler; the DB enforces uniqueness. - One
filesrow per uploaded model (file_uuid). Files are addressed by their own UUID in the slice protocol — the workplate UUID is never also a file ID. file_pathkeeps the original extension (e.g.{file_uuid}.stl,{file_uuid}.obj,{file_uuid}.3mf). The slicer picks the loader from the on-disk extension; nothing on the wire encodes the format.- Status is a finite-state machine. Transitions go forward only:
AwaitingUpload → Uploading → UploadComplete → Slicing → SliceComplete, withErrorreachable from any state. - All
Databasemethods areasync. The underlying SeaORMDatabaseConnectionis backed bysqlx-sqlitewith WAL mode enabled and manages its own connection pool — no external locking needed.
Schema
CREATE TABLE requests (
request_uuid TEXT PRIMARY KEY,
status TEXT NOT NULL,
download_file_path TEXT, -- {ruuid}.gcode (set on slice complete)
download_file_size INTEGER,
created_at TEXT NOT NULL, -- RFC 3339
updated_at TEXT NOT NULL -- RFC 3339
);
-- One row per uploaded model belonging to a workplate.
CREATE TABLE files (
file_uuid TEXT PRIMARY KEY,
request_uuid TEXT NOT NULL,
original_filename TEXT NOT NULL, -- as sent by the browser
file_path TEXT NOT NULL, -- {file_uuid}.{ext}, ext preserved
file_size INTEGER NOT NULL,
created_at TEXT NOT NULL,
FOREIGN KEY (request_uuid) REFERENCES requests(request_uuid)
);
CREATE INDEX idx_files_request_uuid ON files(request_uuid);
CREATE INDEX idx_requests_status ON requests(status);
CREATE INDEX idx_requests_updated_at ON requests(updated_at);
CREATE INDEX idx_requests_status_updated ON requests(status, updated_at);
PRAGMA journal_mode = WAL;Indices target the two query patterns we actually run: status-based cleanup (Error, abandoned AwaitingUpload) and time-based cleanup (rows older than N hours), plus the per-workplate file lookup the slicer issues on every Slice request.
The schema is defined as a SeaORM migration in migrations/m20250101_000001_initial.rs and applied automatically by migrator.rs on every startup.
Migrations
Schema changes must be expressed as new migration files rather than editing the initial one. The workflow is:
- Use the SeaORM CLI tool to generate the boilerplate:bash(If you don't have it installed:
sea-orm-cli migrate generate "add_new_table" -d src/dbcargo install sea-orm-cli). - Implement the
up()anddown()methods in the generatedsrc/db/migrations/mYYYYMMDD_HHMMSS_add_new_table.rsfile. - Register the new module in
src/db/migrations/mod.rs. - Add it to the
migrations()vec insrc/db/migrator.rs.
SeaORM tracks applied migrations in the seaql_migrations bookkeeping table; re-running Database::open on an existing database only applies the new migration(s).
Status state machine
Error rows are kept (they show up in ListSessions) so the user can see what failed; the cleanup pass deletes them on a TTL.
Anatomy
Role in the wider system
The DB is only ever touched by the server. The CLI and the wasm build exclude it via #[cfg(not(target_arch = "wasm32"))] in crate::lib.rs.
Threading
SeaORM's DatabaseConnection manages an internal connection pool with WAL mode enabled. All Database methods are async and can be .awaited directly from any async handler — no spawn_blocking wrapper is needed.
What this module deliberately does not do
- No mesh storage. Model bytes go to disk under
{work_dir}/{file_uuid}.{ext}. The DB has the path, not the bytes. - No G-code storage. Same arrangement:
{work_dir}/{ruuid}.gcode. - No scene state. The server's per-WS-connection scene lives in memory; if the connection drops, it's gone (see
../scene/README.md). - No history of slicing parameters. Only the input/output files are tracked; rerunning a session means re-sending settings.
- No backwards compatibility with the old "request UUID is also the file ID" convention. That collapsed two distinct concepts into one primary key; both new tables (
requests,files) assume a fresh install. - No multi-process safety beyond WAL. A single
slicer-engine serveprocess perdb_path. Don't point two servers at the same SQLite file.
See also
- mod.rs —
Database,RequestSession,FileEntry,RequestStatus, queries - entities/ — SeaORM entity models for
requestsandfiles - migrations/ — versioned schema migrations
- migrator.rs —
Migrator(applies pending migrations on open) - history_tests.rs — round-trip and cleanup tests
- ../server/README.md — how the server uses this
- ../server/handlers.rs — upload / download paths
- ../server/ws_session.rs — slice-side updates