pub const MIGRATION_0001: &str = r#"
CREATE TABLE store_meta (
id INTEGER PRIMARY KEY CHECK (id = 1),
format TEXT NOT NULL DEFAULT 'pimdir',
version INTEGER NOT NULL,
hash_algo TEXT NOT NULL,
created_at TEXT NOT NULL,
-- Store-global monotonic counter handing out the next item `seq`; only ever
-- increases, so a public id is never reused across the whole store.
next_seq INTEGER NOT NULL DEFAULT 1
) STRICT;
-- `account` is the multi-account axis (SPEC.md §9.2): NULL in a single-account
-- store, an opaque owner-chosen id when one store holds several. It groups; it
-- neither keys nor partitions. No identifier is scoped by it: an identity or a
-- body occurring in two accounts is a fact the store reports
-- (LIST_LINK_PLACEMENTS, LIST_OBJECT_PLACEMENTS) and an interface interprets.
CREATE TABLE collections (
id TEXT PRIMARY KEY,
account TEXT,
kind TEXT NOT NULL,
name TEXT NOT NULL,
parent TEXT REFERENCES collections(id) ON UPDATE CASCADE ON DELETE SET NULL,
color TEXT,
description TEXT,
sort_order INTEGER,
-- Cross-source content-conflict policy: 'manual' | 'prefer-incoming' | 'prefer-existing'.
conflict TEXT NOT NULL DEFAULT 'manual',
-- Collection generation: bumped by the owner whenever it rebuilds the
-- collection's handle space (a backend identity reset), so a reader can derive
-- epoch-dependent protocol values (an IMAP UIDVALIDITY) from the store alone
-- (SPEC.md §12).
generation INTEGER NOT NULL DEFAULT 1
) STRICT;
-- "Every collection of this account", the merged view's filter axis. Partial: a
-- single-account store writes no account and pays for no index.
CREATE INDEX collections_by_account ON collections(account) WHERE account IS NOT NULL;
-- One row per source that syncs a collection (a server, a phone). A
-- single-source collection has one row here.
CREATE TABLE sources (
collection TEXT NOT NULL REFERENCES collections(id) ON UPDATE CASCADE ON DELETE CASCADE,
source TEXT NOT NULL,
checkpoint BLOB,
PRIMARY KEY (collection, source)
) STRICT;
CREATE TABLE objects (
hash TEXT PRIMARY KEY,
size INTEGER NOT NULL,
refcount INTEGER NOT NULL DEFAULT 0
) STRICT;
-- The shared truth of one logical item, keyed by its cross-source link id.
-- `deleted` lingers after a source removes it, until every source has dropped
-- it too (the cross-source delete memory). Once no source holds it, the row is
-- RETAINED rather than deleted: a store never loses an item, purge does.
CREATE TABLE items (
collection TEXT NOT NULL REFERENCES collections(id) ON UPDATE CASCADE ON DELETE CASCADE,
link_id TEXT NOT NULL,
-- The message's public id: store-global, one per link_id (shared by its
-- placements across mailboxes), never reused. A client shows it and resolves
-- it back to `link_id`.
seq INTEGER NOT NULL,
flags TEXT,
object_hash TEXT REFERENCES objects(hash),
meta TEXT,
-- The kind's ordering key, written beside `meta`; '' means unknown.
sort_key TEXT NOT NULL DEFAULT '',
level INTEGER NOT NULL,
deleted INTEGER NOT NULL DEFAULT 0,
-- RFC 3339 instant the last binding vanished; non-NULL means retained
-- (soft-deleted). One column carries both the flag and the purge clock.
retained_at TEXT,
-- The source whose removal retired the item, diagnostic only.
retained_by TEXT,
conflicted INTEGER NOT NULL DEFAULT 0,
conflict_object TEXT REFERENCES objects(hash),
PRIMARY KEY (collection, link_id)
) STRICT;
-- One source's binding of an item: its handle there, the base last synced with
-- it (the 3-way-merge baseline), and whether that source's own sync is stuck on
-- an unresolved content conflict.
CREATE TABLE bindings (
collection TEXT NOT NULL,
link_id TEXT NOT NULL,
source TEXT NOT NULL,
handle TEXT NOT NULL,
base_flags TEXT,
base_object TEXT REFERENCES objects(hash),
base_revision TEXT,
-- This source and its OWN remote diverged. Distinct from
-- items.conflicted, which is the cross-source divergence.
conflicted INTEGER NOT NULL DEFAULT 0,
conflict_revision TEXT,
PRIMARY KEY (collection, link_id, source),
FOREIGN KEY (collection, link_id) REFERENCES items(collection, link_id) ON UPDATE CASCADE ON DELETE CASCADE
) STRICT;
-- The action queue (SPEC.md §15): mutations requested by processes that are not
-- the store owner, applied by the owner in append order.
CREATE TABLE queue (
id INTEGER PRIMARY KEY AUTOINCREMENT, -- global append order
created_at TEXT NOT NULL, -- RFC 3339 timestamp
producer TEXT NOT NULL, -- enqueuing process, diagnostic only
collection TEXT NOT NULL REFERENCES collections(id) ON UPDATE CASCADE ON DELETE CASCADE,
action TEXT NOT NULL, -- 'add' | 'set-flags' | 'remove' | 'move' | 'copy' | 'update', or an owner-defined intent
payload TEXT NOT NULL, -- versioned JSON, shape per action (SPEC.md §15)
object_hash TEXT REFERENCES objects(hash), -- pins the payload's body against GC, or NULL
attempts INTEGER NOT NULL DEFAULT 0, -- apply attempts so far
error TEXT -- last failure; non-NULL means parked
) STRICT;
-- The owner drains a collection's pending actions in append order.
CREATE INDEX queue_by_collection ON queue(collection, id);
CREATE INDEX items_by_object ON items(object_hash);
CREATE INDEX bindings_by_object ON bindings(base_object);
-- A message's public id is shared by its placements, so it is unique per
-- (collection, seq) — the key a client resolves.
CREATE UNIQUE INDEX items_by_seq ON items(collection, seq);
-- Indexes the cross-collection "does this message already have a seq?" lookup.
CREATE INDEX items_by_link ON items(link_id);
-- Partial: the trash view and the purge sweep scan the retained set without
-- ever touching the live rows, which are the overwhelming majority.
CREATE INDEX items_retained ON items(collection, retained_at) WHERE retained_at IS NOT NULL;
-- Orders a collection by the kind's own sort key, with `seq` as the tiebreaker
-- that makes a keyset page over a non-unique key well defined.
CREATE INDEX items_by_sort ON items(collection, sort_key, seq);
"#;
pub const VERSION: i64 = 1;
pub const ENSURE_COLLECTION: &str = "\
INSERT INTO collections(id, account, kind, name) VALUES(:collection, :account, '', :collection) \
ON CONFLICT(id) DO NOTHING";
pub const SET_COLLECTION_KIND: &str = "\
INSERT INTO collections(id, account, kind, name) VALUES(:collection, :account, :kind, :collection) \
ON CONFLICT(id) DO UPDATE SET kind = excluded.kind";
pub const SET_COLLECTION_ACCOUNT: &str =
"UPDATE collections SET account = :account WHERE id = :collection";
pub const RENAME_COLLECTION: &str = "UPDATE collections SET id = :new_id WHERE id = :collection";
pub const LOAD_ACCOUNT: &str = "SELECT account FROM collections WHERE id = :collection";
pub const LOAD_KIND: &str = "SELECT kind FROM collections WHERE id = :collection";
pub const SET_CONFLICT: &str = "UPDATE collections SET conflict = :conflict WHERE id = :collection";
pub const LOAD_CONFLICT: &str = "SELECT conflict FROM collections WHERE id = :collection";
pub const LOAD_ITEMS: &str = "\
SELECT link_id, flags, object_hash, meta, sort_key, level, deleted, conflicted, conflict_object \
FROM items WHERE collection = :collection AND retained_at IS NULL";
pub const LIST_COLLECTIONS: &str = "\
SELECT id, account, kind, name, parent, color, description, sort_order, generation \
FROM collections ORDER BY sort_order IS NULL, sort_order, id";
pub const LIST_COLLECTIONS_BY_ACCOUNT: &str = "\
SELECT id, account, kind, name, parent, color, description, sort_order, generation \
FROM collections WHERE account IS :account ORDER BY sort_order IS NULL, sort_order, id";
pub const LIST_ACCOUNTS: &str = "\
SELECT DISTINCT account FROM collections WHERE account IS NOT NULL ORDER BY account";
pub const LIST_ITEMS_PAGE: &str = "\
SELECT seq, link_id, flags, object_hash, meta, sort_key, level FROM items \
WHERE collection = :collection AND deleted = 0 AND link_id > :after \
ORDER BY link_id LIMIT :limit";
pub const LIST_ITEMS_PAGE_ASC: &str = "\
SELECT seq, link_id, flags, object_hash, meta, sort_key, level FROM items \
WHERE collection = :collection AND deleted = 0 \
AND (sort_key, seq) > (:after_key, :after_seq) \
ORDER BY sort_key, seq LIMIT :limit";
pub const LIST_ITEMS_PAGE_DESC: &str = "\
SELECT seq, link_id, flags, object_hash, meta, sort_key, level FROM items \
WHERE collection = :collection AND deleted = 0 \
AND (sort_key, seq) < (:after_key, :after_seq) \
ORDER BY sort_key DESC, seq DESC LIMIT :limit";
pub const SET_SORT_KEY: &str = "\
UPDATE items SET sort_key = :sort_key \
WHERE collection = :collection AND link_id = :link_id";
pub const GET_ITEM: &str = "\
SELECT seq, link_id, flags, object_hash, meta, sort_key, level FROM items \
WHERE collection = :collection AND seq = :seq AND deleted = 0";
pub const SEQ_BY_LINK: &str =
"SELECT seq FROM items WHERE collection = :collection AND link_id = :link_id";
pub const COUNT_ITEMS: &str =
"SELECT count(*) FROM items WHERE collection = :collection AND deleted = 0";
pub const LIST_LINK_PLACEMENTS: &str = "\
SELECT i.collection, c.account, i.seq, i.object_hash, i.flags, i.level \
FROM items i JOIN collections c ON c.id = i.collection \
WHERE i.link_id = :link_id AND i.deleted = 0 AND i.retained_at IS NULL \
ORDER BY c.account IS NULL, c.account, i.collection";
pub const LIST_OBJECT_PLACEMENTS: &str = "\
SELECT i.collection, c.account, i.seq, i.link_id, i.flags, i.level \
FROM items i JOIN collections c ON c.id = i.collection \
WHERE i.object_hash = :hash AND i.deleted = 0 AND i.retained_at IS NULL \
ORDER BY c.account IS NULL, c.account, i.collection";
pub const LIST_SOURCES: &str = "SELECT DISTINCT source FROM bindings ORDER BY source";
pub const LOAD_BINDINGS: &str = "\
SELECT link_id, source, handle, base_flags, base_object, base_revision, \
conflicted, conflict_revision \
FROM bindings WHERE collection = :collection";
pub const LOAD_CHECKPOINT: &str =
"SELECT checkpoint FROM sources WHERE collection = :collection AND source = :source";
pub const SEQ_FOR_LINK_ANY: &str = "SELECT seq FROM items WHERE link_id = :link_id LIMIT 1";
pub const BUMP_NEXT_SEQ: &str =
"UPDATE store_meta SET next_seq = next_seq + 1 WHERE id = 1 RETURNING next_seq - 1";
pub const INSERT_ITEM: &str = "\
INSERT INTO items(collection, link_id, seq, flags, object_hash, meta, sort_key, level, deleted, conflicted, conflict_object) \
VALUES(:collection, :link_id, :seq, :flags, :object_hash, :meta, :sort_key, :level, :deleted, :conflicted, :conflict_object)";
pub const UPDATE_ITEM: &str = "\
UPDATE items SET flags = :flags, object_hash = :object_hash, meta = :meta, sort_key = :sort_key, \
level = :level, deleted = :deleted, conflicted = :conflicted, conflict_object = :conflict_object \
WHERE collection = :collection AND link_id = :link_id";
pub const RETAIN_ITEM: &str = "\
UPDATE items SET deleted = 1, \
retained_at = strftime('%Y-%m-%dT%H:%M:%fZ','now'), retained_by = :source \
WHERE collection = :collection AND link_id = :link_id";
pub const DELETE_ITEM_BINDINGS: &str =
"DELETE FROM bindings WHERE collection = :collection AND link_id = :link_id";
pub const RETAINED_ITEM: &str = "\
SELECT seq, object_hash, conflict_object FROM items \
WHERE collection = :collection AND link_id = :link_id AND retained_at IS NOT NULL";
pub const REVIVE_ITEM: &str = "\
UPDATE items SET deleted = 0, retained_at = NULL, retained_by = NULL \
WHERE collection = :collection AND link_id = :link_id";
pub const LIST_RETAINED_PAGE: &str = "\
SELECT i.seq, i.link_id, i.flags, i.object_hash, i.meta, i.sort_key, i.level, \
i.retained_at, i.retained_by, o.size \
FROM items i LEFT JOIN objects o ON o.hash = i.object_hash \
WHERE i.collection = :collection AND i.retained_at IS NOT NULL AND i.seq > :after \
ORDER BY i.seq LIMIT :limit";
pub const ENSURE_RETAINED_INDEX: &str = "\
CREATE INDEX IF NOT EXISTS items_retained ON items(collection, retained_at) \
WHERE retained_at IS NOT NULL";
pub const ENSURE_ACCOUNT_INDEX: &str = "\
CREATE INDEX IF NOT EXISTS collections_by_account ON collections(account) \
WHERE account IS NOT NULL";
pub const ENSURE_SORT_INDEX: &str = "\
CREATE INDEX IF NOT EXISTS items_by_sort ON items(collection, sort_key, seq)";
pub const COUNT_RETAINED: &str =
"SELECT count(*) FROM items WHERE collection = :collection AND retained_at IS NOT NULL";
pub const RETAINED_BYTES: &str = "\
SELECT coalesce(sum(o.size), 0) FROM objects o WHERE o.hash IN \
(SELECT object_hash FROM items WHERE retained_at IS NOT NULL AND object_hash IS NOT NULL)";
pub const RETAINED_ITEM_BY_SEQ: &str = "\
SELECT object_hash, conflict_object FROM items \
WHERE collection = :collection AND seq = :seq AND retained_at IS NOT NULL";
pub const PURGE_ITEM: &str = "\
DELETE FROM items WHERE collection = :collection AND seq = :seq AND retained_at IS NOT NULL";
pub const RETAINED_BEFORE: &str = "\
SELECT collection, link_id, object_hash, conflict_object FROM items \
WHERE retained_at IS NOT NULL AND retained_at < :cutoff";
pub const PURGE_RETAINED_BEFORE: &str =
"DELETE FROM items WHERE retained_at IS NOT NULL AND retained_at < :cutoff";
pub const INSERT_BINDING: &str = "\
INSERT INTO bindings(collection, link_id, source, handle, base_flags, base_object, \
base_revision, conflicted, conflict_revision) \
VALUES(:collection, :link_id, :source, :handle, :base_flags, :base_object, \
:base_revision, :conflicted, :conflict_revision)";
pub const UPDATE_BINDING: &str = "\
UPDATE bindings SET handle = :handle, base_flags = :base_flags, \
base_object = :base_object, base_revision = :base_revision, \
conflicted = :conflicted, conflict_revision = :conflict_revision \
WHERE collection = :collection AND link_id = :link_id AND source = :source";
pub const DELETE_BINDING: &str = "DELETE FROM bindings WHERE collection = :collection AND link_id = :link_id AND source = :source";
pub const ADJUST_REFCOUNT: &str =
"UPDATE objects SET refcount = refcount + :delta WHERE hash = :hash";
pub const UPSERT_CHECKPOINT: &str = "\
INSERT INTO sources(collection, source, checkpoint) VALUES(:collection, :source, :checkpoint) \
ON CONFLICT(collection, source) DO UPDATE SET checkpoint = excluded.checkpoint";
pub const STORE_OBJECT: &str = "\
INSERT INTO objects(hash, size, refcount) VALUES(:hash, :size, 0) \
ON CONFLICT(hash) DO UPDATE SET size = excluded.size";
pub const LOOKUP_OBJECTS: &str = "\
SELECT link_id, object_hash FROM items \
WHERE object_hash IS NOT NULL \
AND link_id IN (SELECT value FROM json_each(:links))";
pub const LIST_GARBAGE_OBJECTS: &str = "SELECT hash FROM objects WHERE refcount = 0";
pub const LIST_GARBAGE_SIZED: &str = "SELECT hash, size FROM objects WHERE refcount = 0";
pub const DELETE_GARBAGE_OBJECTS: &str = "DELETE FROM objects WHERE refcount = 0";
pub const ENQUEUE_ACTION: &str = "\
INSERT INTO queue(created_at, producer, collection, action, payload, object_hash) \
VALUES(:created_at, :producer, :collection, :action, :payload, :object_hash)";
pub const LIST_QUEUED_COLLECTIONS: &str =
"SELECT DISTINCT collection FROM queue WHERE error IS NULL";
pub const LOAD_PENDING_ACTIONS: &str = "\
SELECT id, created_at, producer, action, payload, object_hash, attempts \
FROM queue WHERE collection = :collection AND error IS NULL ORDER BY id";
pub const DELETE_ACTION: &str = "DELETE FROM queue WHERE id = :id";
pub const LOAD_ACTION_ROW: &str = "SELECT attempts, object_hash FROM queue WHERE id = :id";
pub const CANCEL_ACTION: &str = "DELETE FROM queue WHERE id = :id";
pub const PARK_ACTION: &str =
"UPDATE queue SET attempts = :attempts, error = :error WHERE id = :id";
pub const BUMP_ATTEMPTS: &str = "UPDATE queue SET attempts = attempts + 1 WHERE id = :id";
pub const LOAD_PARKED_ACTIONS: &str = "\
SELECT id, created_at, producer, collection, action, payload, attempts, error \
FROM queue WHERE error IS NOT NULL ORDER BY id";
pub const BUMP_GENERATION: &str = "\
UPDATE collections SET generation = generation + 1 WHERE id = :collection \
RETURNING generation";
pub const LOAD_GENERATION: &str = "SELECT generation FROM collections WHERE id = :collection";
pub const ALL: &[(&str, &str)] = &[
("MIGRATION_0001", MIGRATION_0001),
("ENSURE_COLLECTION", ENSURE_COLLECTION),
("SET_COLLECTION_KIND", SET_COLLECTION_KIND),
("SET_COLLECTION_ACCOUNT", SET_COLLECTION_ACCOUNT),
("RENAME_COLLECTION", RENAME_COLLECTION),
("LOAD_ACCOUNT", LOAD_ACCOUNT),
("LOAD_KIND", LOAD_KIND),
("SET_CONFLICT", SET_CONFLICT),
("LOAD_CONFLICT", LOAD_CONFLICT),
("LOAD_ITEMS", LOAD_ITEMS),
("LIST_COLLECTIONS", LIST_COLLECTIONS),
("LIST_COLLECTIONS_BY_ACCOUNT", LIST_COLLECTIONS_BY_ACCOUNT),
("LIST_ACCOUNTS", LIST_ACCOUNTS),
("LIST_ITEMS_PAGE", LIST_ITEMS_PAGE),
("LIST_ITEMS_PAGE_ASC", LIST_ITEMS_PAGE_ASC),
("LIST_ITEMS_PAGE_DESC", LIST_ITEMS_PAGE_DESC),
("SET_SORT_KEY", SET_SORT_KEY),
("GET_ITEM", GET_ITEM),
("SEQ_BY_LINK", SEQ_BY_LINK),
("COUNT_ITEMS", COUNT_ITEMS),
("LIST_LINK_PLACEMENTS", LIST_LINK_PLACEMENTS),
("LIST_OBJECT_PLACEMENTS", LIST_OBJECT_PLACEMENTS),
("LIST_SOURCES", LIST_SOURCES),
("LOAD_BINDINGS", LOAD_BINDINGS),
("LOAD_CHECKPOINT", LOAD_CHECKPOINT),
("SEQ_FOR_LINK_ANY", SEQ_FOR_LINK_ANY),
("BUMP_NEXT_SEQ", BUMP_NEXT_SEQ),
("INSERT_ITEM", INSERT_ITEM),
("UPDATE_ITEM", UPDATE_ITEM),
("RETAIN_ITEM", RETAIN_ITEM),
("DELETE_ITEM_BINDINGS", DELETE_ITEM_BINDINGS),
("RETAINED_ITEM", RETAINED_ITEM),
("REVIVE_ITEM", REVIVE_ITEM),
("LIST_RETAINED_PAGE", LIST_RETAINED_PAGE),
("ENSURE_RETAINED_INDEX", ENSURE_RETAINED_INDEX),
("ENSURE_ACCOUNT_INDEX", ENSURE_ACCOUNT_INDEX),
("ENSURE_SORT_INDEX", ENSURE_SORT_INDEX),
("COUNT_RETAINED", COUNT_RETAINED),
("RETAINED_BYTES", RETAINED_BYTES),
("RETAINED_ITEM_BY_SEQ", RETAINED_ITEM_BY_SEQ),
("PURGE_ITEM", PURGE_ITEM),
("RETAINED_BEFORE", RETAINED_BEFORE),
("PURGE_RETAINED_BEFORE", PURGE_RETAINED_BEFORE),
("INSERT_BINDING", INSERT_BINDING),
("UPDATE_BINDING", UPDATE_BINDING),
("DELETE_BINDING", DELETE_BINDING),
("ADJUST_REFCOUNT", ADJUST_REFCOUNT),
("UPSERT_CHECKPOINT", UPSERT_CHECKPOINT),
("STORE_OBJECT", STORE_OBJECT),
("LOOKUP_OBJECTS", LOOKUP_OBJECTS),
("LIST_GARBAGE_OBJECTS", LIST_GARBAGE_OBJECTS),
("LIST_GARBAGE_SIZED", LIST_GARBAGE_SIZED),
("DELETE_GARBAGE_OBJECTS", DELETE_GARBAGE_OBJECTS),
("ENQUEUE_ACTION", ENQUEUE_ACTION),
("LIST_QUEUED_COLLECTIONS", LIST_QUEUED_COLLECTIONS),
("LOAD_PENDING_ACTIONS", LOAD_PENDING_ACTIONS),
("DELETE_ACTION", DELETE_ACTION),
("LOAD_ACTION_ROW", LOAD_ACTION_ROW),
("CANCEL_ACTION", CANCEL_ACTION),
("PARK_ACTION", PARK_ACTION),
("BUMP_ATTEMPTS", BUMP_ATTEMPTS),
("LOAD_PARKED_ACTIONS", LOAD_PARKED_ACTIONS),
("BUMP_GENERATION", BUMP_GENERATION),
("LOAD_GENERATION", LOAD_GENERATION),
];
#[cfg(test)]
mod tests {
use alloc::vec::Vec;
use super::ALL;
fn declared() -> Vec<&'static str> {
include_str!("sql.rs")
.lines()
.filter_map(|line| line.strip_prefix("pub const "))
.filter_map(|rest| rest.split(':').next())
.map(str::trim)
.collect()
}
#[test]
fn the_index_covers_every_statement() {
let expected: Vec<_> = declared()
.into_iter()
.filter(|name| *name != "VERSION" && *name != "ALL")
.collect();
assert!(!expected.is_empty(), "source scan found no constants");
for name in &expected {
assert!(
ALL.iter().any(|(indexed, _)| indexed == name),
"{name} is declared but missing from sql::ALL"
);
}
assert_eq!(
ALL.len(),
expected.len(),
"sql::ALL has entries the module does not declare"
);
}
#[test]
fn the_index_follows_the_declaration_order() {
let declared: Vec<_> = declared()
.into_iter()
.filter(|name| *name != "VERSION" && *name != "ALL")
.collect();
let indexed: Vec<_> = ALL.iter().map(|(name, _)| *name).collect();
assert_eq!(
indexed, declared,
"sql::ALL drifted from the declaration order"
);
}
#[test]
fn no_statement_is_empty() {
for (name, sql) in ALL {
assert!(!sql.trim().is_empty(), "{name} is empty");
}
}
}