Skip to content

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:

  1. POST /api/upload creates a workplate (request_uuid, aka ruuid), writes one or more {file_uuid}.{ext} files to the work dir, and returns { ruuid, ofids: [file_uuid, ...] }.
  2. WS Slice { request_uuid, scene, settings } reads each scene entry's file by file_uuid, runs the pipeline, writes {ruuid}.gcode.
  3. GET /api/download/:ruuid streams the G-code back; GET /api/file/:file_uuid streams 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

  1. The DB stores paths and metadata, never file contents. Files live in the server's work_dir; the DB stores the path and size.
  2. One requests row per workplate (request_uuid). UUIDs are generated by the upload handler; the DB enforces uniqueness.
  3. One files row 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.
  4. file_path keeps 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.
  5. Status is a finite-state machine. Transitions go forward only: AwaitingUpload → Uploading → UploadComplete → Slicing → SliceComplete, with Error reachable from any state.
  6. All Database methods are async. The underlying SeaORM DatabaseConnection is backed by sqlx-sqlite with WAL mode enabled and manages its own connection pool — no external locking needed.

Schema

sql
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:

  1. Use the SeaORM CLI tool to generate the boilerplate:
    bash
    sea-orm-cli migrate generate "add_new_table" -d src/db
    (If you don't have it installed: cargo install sea-orm-cli).
  2. Implement the up() and down() methods in the generated src/db/migrations/mYYYYMMDD_HHMMSS_add_new_table.rs file.
  3. Register the new module in src/db/migrations/mod.rs.
  4. Add it to the migrations() vec in src/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 serve process per db_path. Don't point two servers at the same SQLite file.

See also