Storage & search¶
clipboarder uses SQLite with the FTS5 extension, both bundled directly into the binary via rusqlite's bundled feature.
Schema¶
CREATE TABLE items (
id INTEGER PRIMARY KEY AUTOINCREMENT,
kind TEXT NOT NULL, -- text | url | email | code | color | image | file | pdf | music | video | repo
content TEXT NOT NULL, -- full payload (empty for images; image_path holds the file)
preview TEXT NOT NULL, -- single-line preview shown in the list
meta TEXT, -- kind-specific: host, language, color format, platform
source_app TEXT, -- localized name of the frontmost app at capture
source_app_id TEXT, -- its bundle id (e.g. "com.apple.Safari")
image_path TEXT, -- path on disk for image captures
pinned INTEGER NOT NULL DEFAULT 0,
size INTEGER NOT NULL DEFAULT 0,
content_hash TEXT NOT NULL, -- SHA-256 (32 bytes hex) for dedup
created_at INTEGER NOT NULL, -- unix ms
last_used_at INTEGER NOT NULL -- unix ms; bumped on re-copy or paste-back
);
CREATE UNIQUE INDEX idx_items_hash ON items(content_hash);
CREATE INDEX idx_items_last_used ON items(last_used_at DESC);
CREATE INDEX idx_items_kind ON items(kind, last_used_at DESC);
CREATE VIRTUAL TABLE items_fts USING fts5(
content, preview, meta,
content='items', content_rowid='id',
tokenize='unicode61 remove_diacritics 2'
);
-- Triggers keep items_fts in sync without app-level bookkeeping.
CREATE TRIGGER items_ai AFTER INSERT ON items BEGIN
INSERT INTO items_fts(rowid, content, preview, meta)
VALUES (new.id, new.content, new.preview, COALESCE(new.meta, ''));
END;
CREATE TRIGGER items_ad AFTER DELETE ON items BEGIN
INSERT INTO items_fts(items_fts, rowid, content, preview, meta)
VALUES ('delete', old.id, old.content, old.preview, COALESCE(old.meta, ''));
END;
CREATE TRIGGER items_au AFTER UPDATE ON items BEGIN
INSERT INTO items_fts(items_fts, rowid, content, preview, meta)
VALUES ('delete', old.id, old.content, old.preview, COALESCE(old.meta, ''));
INSERT INTO items_fts(rowid, content, preview, meta)
VALUES (new.id, new.content, new.preview, COALESCE(new.meta, ''));
END;
Pragmas¶
PRAGMA journal_mode = WAL; -- reader/writer concurrency
PRAGMA synchronous = NORMAL; -- WAL is safe enough at NORMAL
PRAGMA foreign_keys = ON;
WAL mode is critical: the watcher thread writes constantly, and the UI thread reads on every keystroke. WAL means they don't block each other.
Insert path¶
- Look up the row by
content_hash. If present, bumplast_used_atand return. - Otherwise INSERT new row.
- Triggers replicate the change into
items_fts.
Search path¶
Two code paths:
Empty query (just filters):
SELECT … FROM items
WHERE 1=1 [ AND kind = ? ] [ AND pinned = 1 ]
ORDER BY pinned DESC, last_used_at DESC
LIMIT ?
Non-empty query — FTS MATCH plus bm25 ranking:
SELECT … FROM items_fts f JOIN items i ON i.id = f.rowid
WHERE items_fts MATCH ?
[ AND i.kind = ? ]
[ AND i.pinned = 1 ]
ORDER BY i.pinned DESC, bm25(items_fts) ASC, i.last_used_at DESC
LIMIT ?
The match expression is built defensively — each whitespace-separated user token is alphanumeric-stripped, quoted, and suffixed with * for prefix search. That keeps user input from breaking FTS syntax.
Retention enforcement¶
Two operations, both cheap and pinned-aware:
storage::enforce_limit(max_items) -> deleted_image_paths
storage::prune_older_than(days) -> deleted_image_paths
Run at startup and on every capture. Image files referenced by deleted rows are unlinked from disk by the caller.
File-system layout¶
~/Library/Application Support/com.clipboarder.app/
├── clipboarder.sqlite # data + FTS index
├── clipboarder.sqlite-wal # WAL
├── clipboarder.sqlite-shm # shared memory file
├── settings.json
├── images/<hash16>.png # captured PNG payloads
├── app_icons/<bundle>.png # cached app icons
└── url_meta/<hash12>.json # cached OG metadata
Migrations¶
The current schema is forward-only. New columns are added via ALTER TABLE ... ADD COLUMN ... DEFAULT NULL calls wrapped in let _ = ... (so the column-already-exists error is silently ignored). When a column needs to be split or removed, we'll introduce a real migration ledger (probably as a schema_version table) at the same time.