1pub const VERSION: i64 = 1;
12
13pub const RESHAPED_INDEXES: &[(&str, &[&str])] = &[
24 ("items_retained", &["collection", "seq"]),
28];
29
30macro_rules! statements {
33 ($($(#[$doc:meta])* $name:ident = $sql:expr;)*) => {
34 $($(#[$doc])* pub const $name: &str = $sql;)*
35
36 pub const ALL: &[(&str, &str)] = &[$((stringify!($name), $name)),*];
45 };
46}
47
48statements! {
52MIGRATION_0001 = r#"
56CREATE TABLE store_meta (
57 id INTEGER PRIMARY KEY CHECK (id = 1),
58 format TEXT NOT NULL DEFAULT 'pimdir',
59 version INTEGER NOT NULL,
60 hash_algo TEXT NOT NULL,
61 created_at TEXT NOT NULL,
62 -- Store-global monotonic counter handing out the next item `seq`; only ever
63 -- increases, so a public id is never reused across the whole store.
64 next_seq INTEGER NOT NULL DEFAULT 1
65) STRICT;
66
67-- `account` is the multi-account axis (SPEC.md §9.2): NULL in a single-account
68-- store, an opaque owner-chosen id when one store holds several. It groups; it
69-- neither keys nor partitions. No identifier is scoped by it: an identity or a
70-- body occurring in two accounts is a fact the store reports
71-- (LIST_LINK_PLACEMENTS, LIST_OBJECT_PLACEMENTS) and an interface interprets.
72CREATE TABLE collections (
73 id TEXT PRIMARY KEY,
74 account TEXT,
75 kind TEXT NOT NULL,
76 name TEXT NOT NULL,
77 parent TEXT REFERENCES collections(id) ON UPDATE CASCADE ON DELETE SET NULL,
78 color TEXT,
79 description TEXT,
80 sort_order INTEGER,
81 -- Cross-source content-conflict policy: 'manual' | 'prefer-incoming' | 'prefer-existing'.
82 conflict TEXT NOT NULL DEFAULT 'manual',
83 -- Collection generation: bumped by the owner whenever it rebuilds the
84 -- collection's handle space (a backend identity reset), so a reader can derive
85 -- epoch-dependent protocol values (an IMAP UIDVALIDITY) from the store alone
86 -- (SPEC.md §12).
87 generation INTEGER NOT NULL DEFAULT 1
88) STRICT;
89
90-- "Every collection of this account", the merged view's filter axis. Partial: a
91-- single-account store writes no account and pays for no index.
92CREATE INDEX collections_by_account ON collections(account) WHERE account IS NOT NULL;
93
94-- One row per source that syncs a collection (a server, a phone). A
95-- single-source collection has one row here.
96CREATE TABLE sources (
97 collection TEXT NOT NULL REFERENCES collections(id) ON UPDATE CASCADE ON DELETE CASCADE,
98 source TEXT NOT NULL,
99 checkpoint BLOB,
100 PRIMARY KEY (collection, source)
101) STRICT;
102
103CREATE TABLE objects (
104 hash TEXT PRIMARY KEY,
105 size INTEGER NOT NULL,
106 refcount INTEGER NOT NULL DEFAULT 0
107) STRICT;
108
109-- The shared truth of one logical item, keyed by its cross-source link id.
110-- `deleted` lingers after a source removes it, until every source has dropped
111-- it too (the cross-source delete memory). Once no source holds it, the row is
112-- RETAINED rather than deleted: a store never loses an item, purge does.
113CREATE TABLE items (
114 collection TEXT NOT NULL REFERENCES collections(id) ON UPDATE CASCADE ON DELETE CASCADE,
115 link_id TEXT NOT NULL,
116 -- The message's public id: store-global, one per link_id (shared by its
117 -- placements across mailboxes), never reused. A client shows it and resolves
118 -- it back to `link_id`.
119 seq INTEGER NOT NULL,
120 flags TEXT,
121 object_hash TEXT REFERENCES objects(hash),
122 meta TEXT,
123 -- The kind's ordering key, written beside `meta`; '' means unknown.
124 sort_key TEXT NOT NULL DEFAULT '',
125 level INTEGER NOT NULL,
126 deleted INTEGER NOT NULL DEFAULT 0,
127 -- RFC 3339 instant the last binding vanished; non-NULL means retained
128 -- (soft-deleted). One column carries both the flag and the purge clock.
129 retained_at TEXT,
130 -- The source whose removal retired the item, diagnostic only.
131 retained_by TEXT,
132 conflicted INTEGER NOT NULL DEFAULT 0,
133 conflict_object TEXT REFERENCES objects(hash),
134 PRIMARY KEY (collection, link_id)
135) STRICT;
136
137-- One source's binding of an item: its handle there, the two bases it agreed
138-- from (the one last synced with the source, which is the 3-way-merge baseline,
139-- and the shared body it last reconciled against), and whether that source's
140-- own sync is stuck on an unresolved content conflict.
141CREATE TABLE bindings (
142 collection TEXT NOT NULL,
143 link_id TEXT NOT NULL,
144 source TEXT NOT NULL,
145 -- The item's backend id on this source (IMAP UID, DAV href). Bound once: a
146 -- write resolving this binding to another handle is refused, and the one
147 -- licensed rebind is the handle-space rebuild (SPEC.md §10, §12).
148 handle TEXT NOT NULL,
149 base_flags TEXT,
150 base_object TEXT REFERENCES objects(hash),
151 base_revision TEXT,
152 -- Whether a base exists at all, which its three value columns cannot say: a
153 -- source reporting no revision, no body and markers nobody has read still
154 -- agreed, and that agreement is what tells a pending push from a settled
155 -- one. Inferring presence from the three loses exactly that shape.
156 base_present INTEGER NOT NULL DEFAULT 0,
157 -- This source and its OWN remote diverged. Distinct from
158 -- items.conflicted, which is the cross-source divergence.
159 conflicted INTEGER NOT NULL DEFAULT 0,
160 conflict_revision TEXT,
161 -- The diverging remote body at that revision, so a resolver reads the
162 -- three sides (base, local, remote) from the store and needs no
163 -- credentials. Pinned like any other reference while the binding stays
164 -- conflicted, and released when it resolves.
165 conflict_object TEXT REFERENCES objects(hash),
166 -- The shared body this source last reconciled against, the base of the
167 -- cross-source merge. base_object answers to the source's own remote and
168 -- only a sync moves it, so a body this source folded in and has not pushed
169 -- yet leaves it behind; read as the shared base it would have the source
170 -- disagree with itself. Meaningful on every binding, conflicted or not.
171 -- It names an object and pins none, hence no REFERENCES, no index and no
172 -- refcount: the value is only ever compared for equality, never read as
173 -- bytes, and a content hash compares the same after the body it named has
174 -- been swept.
175 shared_object TEXT,
176 PRIMARY KEY (collection, link_id, source),
177 FOREIGN KEY (collection, link_id) REFERENCES items(collection, link_id) ON UPDATE CASCADE ON DELETE CASCADE
178) STRICT;
179
180-- The action queue (SPEC.md §15): mutations requested by processes that are not
181-- the store owner, applied by the owner in append order.
182CREATE TABLE queue (
183 id INTEGER PRIMARY KEY AUTOINCREMENT, -- global append order
184 created_at TEXT NOT NULL, -- RFC 3339 timestamp
185 producer TEXT NOT NULL, -- enqueuing process, diagnostic only
186 collection TEXT NOT NULL REFERENCES collections(id) ON UPDATE CASCADE ON DELETE CASCADE,
187 action TEXT NOT NULL, -- 'add' | 'set-flags' | 'remove' | 'move' | 'copy' | 'update', or an owner-defined intent
188 payload TEXT NOT NULL, -- versioned JSON, shape per action (SPEC.md §15)
189 object_hash TEXT REFERENCES objects(hash), -- pins the payload's body against GC, or NULL
190 attempts INTEGER NOT NULL DEFAULT 0, -- apply attempts so far
191 error TEXT -- last failure; non-NULL means parked
192) STRICT;
193
194-- The owner drains a collection's pending actions in append order.
195CREATE INDEX queue_by_collection ON queue(collection, id);
196
197CREATE INDEX items_by_object ON items(object_hash);
198CREATE INDEX bindings_by_object ON bindings(base_object);
199-- A message's public id is shared by its placements, so it is unique per
200-- (collection, seq) — the key a client resolves.
201CREATE UNIQUE INDEX items_by_seq ON items(collection, seq);
202-- Indexes the cross-collection "does this message already have a seq?" lookup.
203CREATE INDEX items_by_link ON items(link_id);
204-- Retained (soft-deleted) items: every retained read rides this one index, and
205-- none of them touches the live rows, which are the overwhelming majority. It
206-- leads with `seq` because the trash listing pages on the public id
207-- (LIST_RETAINED_PAGE, spec §14.1), and ordering by anything this index does not
208-- lead with sorts every retained row in the collection to return one page.
209-- COUNT_RETAINED rides the collection prefix, and the store-wide purge scans the
210-- index whole, which is O(retained) because the index is partial.
211CREATE INDEX items_retained ON items(collection, seq) WHERE retained_at IS NOT NULL;
212-- Orders a collection by the kind's own sort key, with `seq` as the tiebreaker
213-- that makes a keyset page over a non-unique key well defined.
214CREATE INDEX items_by_sort ON items(collection, sort_key, seq);
215-- `seq` is the store-global public id (spec §9.1), displayed and accepted back
216-- without naming its collection; resolving one against items_by_seq means
217-- scanning that whole index, since it leads with the collection.
218CREATE INDEX items_by_seq_global ON items(seq);
219-- The sweep of unreferenced objects. Partial, so it holds only what is about to
220-- be collected and is empty at rest: without it both the list and the delete
221-- scan the whole objects table, on every write transaction.
222CREATE INDEX objects_garbage ON objects(refcount) WHERE refcount <= 0;
223-- The other three pointers at an object, so a refcount recomputation reaches
224-- every reference by index rather than by scanning items, bindings and queue
225-- once per object.
226CREATE INDEX items_by_conflict_object ON items(conflict_object);
227CREATE INDEX bindings_by_conflict_object ON bindings(conflict_object);
228CREATE INDEX queue_by_object ON queue(object_hash);
229-- The bindings waiting for a decision. Partial, so it holds only what is
230-- outstanding and is empty at rest: a run reports that count on every
231-- invocation, and a listing command asks the same question directly, both of
232-- which would otherwise scan every binding in the store.
233CREATE INDEX bindings_conflicted ON bindings(collection, link_id, source) WHERE conflicted = 1;
234-- Resolves one source handle back to the link id it is bound to, which is what
235-- a batch dropping a placement needs: a drop names a handle and the shared item
236-- is keyed by link id. Without it that resolution is a scan of every item.
237CREATE INDEX bindings_by_handle ON bindings(collection, source, handle);
238"#;
239
240
241ENSURE_COLLECTION = "\
244INSERT INTO collections(id, account, kind, name) VALUES(:collection, :account, '', :collection) \
245ON CONFLICT(id) DO NOTHING";
246
247SET_COLLECTION_KIND = "\
251INSERT INTO collections(id, account, kind, name) VALUES(:collection, :account, :kind, :collection) \
252ON CONFLICT(id) DO UPDATE SET kind = excluded.kind";
253
254SET_COLLECTION_ACCOUNT =
258 "UPDATE collections SET account = :account WHERE id = :collection";
259
260RENAME_COLLECTION = "UPDATE collections SET id = :new_id WHERE id = :collection";
270
271LOAD_ACCOUNT = "SELECT account FROM collections WHERE id = :collection";
273
274LOAD_KIND = "SELECT kind FROM collections WHERE id = :collection";
276
277SET_CONFLICT = "UPDATE collections SET conflict = :conflict WHERE id = :collection";
279
280LOAD_CONFLICT = "SELECT conflict FROM collections WHERE id = :collection";
282
283LOAD_ITEMS = "\
295SELECT link_id, flags, object_hash, meta, sort_key, level, deleted, conflicted, conflict_object \
296FROM items WHERE collection = :collection AND retained_at IS NULL";
297
298LOAD_ITEMS_BY_LINK = "\
305SELECT link_id, flags, object_hash, meta, sort_key, level, deleted, conflicted, conflict_object \
306FROM items WHERE collection = :collection AND retained_at IS NULL \
307 AND link_id IN (SELECT value FROM json_each(:links))";
308
309LIST_COLLECTIONS = "\
315SELECT id, account, kind, name, parent, color, description, sort_order, generation \
316FROM collections ORDER BY sort_order IS NULL, sort_order, id";
317
318LIST_COLLECTIONS_BY_ACCOUNT = "\
321SELECT id, account, kind, name, parent, color, description, sort_order, generation \
322FROM collections WHERE account IS :account ORDER BY sort_order IS NULL, sort_order, id";
323
324LIST_ACCOUNTS = "\
327SELECT DISTINCT account FROM collections WHERE account IS NOT NULL ORDER BY account";
328
329LIST_ITEMS_PAGE = "\
338SELECT seq, link_id, flags, object_hash, meta, sort_key, level FROM items \
339WHERE collection = :collection AND deleted = 0 AND link_id > :after \
340ORDER BY link_id LIMIT :limit";
341
342LIST_ITEMS_PAGE_ASC = "\
352SELECT seq, link_id, flags, object_hash, meta, sort_key, level FROM items \
353WHERE collection = :collection AND deleted = 0 \
354AND (sort_key, seq) > (:after_key, :after_seq) \
355ORDER BY sort_key, seq LIMIT :limit";
356
357LIST_ITEMS_PAGE_DESC = "\
367SELECT seq, link_id, flags, object_hash, meta, sort_key, level FROM items \
368WHERE collection = :collection AND deleted = 0 \
369AND (:after_key IS NULL OR (sort_key, seq) < (:after_key, :after_seq)) \
370ORDER BY sort_key DESC, seq DESC LIMIT :limit";
371
372SET_SORT_KEY = "\
377UPDATE items SET sort_key = :sort_key \
378WHERE collection = :collection AND link_id = :link_id";
379
380GET_ITEM = "\
382SELECT seq, link_id, flags, object_hash, meta, sort_key, level FROM items \
383WHERE collection = :collection AND seq = :seq AND deleted = 0";
384
385SEQ_BY_LINK =
388 "SELECT seq FROM items WHERE collection = :collection AND link_id = :link_id";
389
390COUNT_ITEMS =
392 "SELECT count(*) FROM items WHERE collection = :collection AND deleted = 0";
393
394LIST_LINK_PLACEMENTS = "\
399SELECT i.collection, c.account, i.seq, i.object_hash, i.flags, i.level \
400FROM items i JOIN collections c ON c.id = i.collection \
401WHERE i.link_id = :link_id AND i.deleted = 0 AND i.retained_at IS NULL \
402ORDER BY c.account IS NULL, c.account, i.collection";
403
404LIST_OBJECT_PLACEMENTS = "\
407SELECT i.collection, c.account, i.seq, i.link_id, i.flags, i.level \
408FROM items i JOIN collections c ON c.id = i.collection \
409WHERE i.object_hash = :hash AND i.deleted = 0 AND i.retained_at IS NULL \
410ORDER BY c.account IS NULL, c.account, i.collection";
411
412LIST_SOURCES = "SELECT DISTINCT source FROM bindings ORDER BY source";
416
417LOAD_BINDINGS = "\
420SELECT link_id, source, handle, base_flags, base_object, base_revision, base_present, \
421conflicted, conflict_revision, conflict_object, shared_object \
422FROM bindings WHERE collection = :collection";
423
424LOAD_BINDINGS_BY_LINK = "\
427SELECT link_id, source, handle, base_flags, base_object, base_revision, base_present, \
428conflicted, conflict_revision, conflict_object, shared_object \
429FROM bindings WHERE collection = :collection \
430 AND link_id IN (SELECT value FROM json_each(:links))";
431
432LIST_CONFLICTED_BINDINGS = "\
448SELECT b.collection, b.link_id, b.source, b.handle, b.conflict_revision, \
449b.base_object, i.object_hash, b.conflict_object \
450FROM bindings b \
451JOIN items i ON i.collection = b.collection AND i.link_id = b.link_id \
452JOIN collections c ON c.id = b.collection \
453WHERE b.conflicted = 1 AND c.account IS :account \
454ORDER BY b.collection, b.link_id, b.source";
455
456LIVE_ITEM_FOR_LINK = "\
463SELECT seq FROM items \
464WHERE collection = :collection AND link_id = :link_id \
465 AND deleted = 0 AND retained_at IS NULL";
466
467HANDLE_FOR_LINK = "\
470SELECT handle FROM bindings \
471WHERE collection = :collection AND link_id = :link_id AND source = :source";
472
473LINK_FOR_HANDLE = "\
479SELECT link_id FROM bindings \
480WHERE collection = :collection AND source = :source AND handle = :handle";
481
482LOAD_CHECKPOINT =
484 "SELECT checkpoint FROM sources WHERE collection = :collection AND source = :source";
485
486SEQ_FOR_LINK_ANY = "SELECT seq FROM items WHERE link_id = :link_id LIMIT 1";
489
490BUMP_NEXT_SEQ =
494 "UPDATE store_meta SET next_seq = next_seq + 1 WHERE id = 1 RETURNING next_seq - 1";
495
496INSERT_ITEM = "\
499INSERT INTO items(collection, link_id, seq, flags, object_hash, meta, sort_key, level, deleted, conflicted, conflict_object) \
500VALUES(:collection, :link_id, :seq, :flags, :object_hash, :meta, :sort_key, :level, :deleted, :conflicted, :conflict_object)";
501
502UPDATE_ITEM = "\
505UPDATE items SET flags = :flags, object_hash = :object_hash, meta = :meta, sort_key = :sort_key, \
506level = :level, deleted = :deleted, conflicted = :conflicted, conflict_object = :conflict_object \
507WHERE collection = :collection AND link_id = :link_id";
508
509RETAIN_ITEM = "\
519UPDATE items SET deleted = 1, \
520retained_at = strftime('%Y-%m-%dT%H:%M:%fZ','now'), retained_by = :source \
521WHERE collection = :collection AND link_id = :link_id";
522
523DELETE_ITEM_BINDINGS =
528 "DELETE FROM bindings WHERE collection = :collection AND link_id = :link_id";
529
530RETAINED_ITEM = "\
533SELECT seq, object_hash, conflict_object FROM items \
534WHERE collection = :collection AND link_id = :link_id AND retained_at IS NOT NULL";
535
536REVIVE_ITEM = "\
541UPDATE items SET deleted = 0, retained_at = NULL, retained_by = NULL \
542WHERE collection = :collection AND link_id = :link_id";
543
544LIST_RETAINED_PAGE = "\
553SELECT i.seq, i.link_id, i.flags, i.object_hash, i.meta, i.sort_key, i.level, \
554i.retained_at, i.retained_by, o.size \
555FROM items i LEFT JOIN objects o ON o.hash = i.object_hash \
556WHERE i.collection = :collection AND i.retained_at IS NOT NULL AND i.seq > :after \
557ORDER BY i.seq LIMIT :limit";
558
559ENSURE_INDEXES = "\
572CREATE INDEX IF NOT EXISTS items_retained ON items(collection, seq) \
573WHERE retained_at IS NOT NULL;
574CREATE INDEX IF NOT EXISTS collections_by_account ON collections(account) \
575WHERE account IS NOT NULL;
576CREATE INDEX IF NOT EXISTS items_by_sort ON items(collection, sort_key, seq);
577CREATE INDEX IF NOT EXISTS items_by_seq_global ON items(seq);
578CREATE INDEX IF NOT EXISTS objects_garbage ON objects(refcount) WHERE refcount <= 0;
579CREATE INDEX IF NOT EXISTS items_by_conflict_object ON items(conflict_object);
580CREATE INDEX IF NOT EXISTS bindings_by_conflict_object ON bindings(conflict_object);
581CREATE INDEX IF NOT EXISTS bindings_conflicted ON bindings(collection, link_id, source) \
582WHERE conflicted = 1;
583CREATE INDEX IF NOT EXISTS queue_by_object ON queue(object_hash);
584CREATE INDEX IF NOT EXISTS bindings_by_handle ON bindings(collection, source, handle);";
585
586COUNT_RETAINED =
589 "SELECT count(*) FROM items WHERE collection = :collection AND retained_at IS NOT NULL";
590
591RETAINED_BYTES = "\
595SELECT coalesce(sum(o.size), 0) FROM objects o WHERE o.hash IN \
596(SELECT object_hash FROM items WHERE retained_at IS NOT NULL AND object_hash IS NOT NULL)";
597
598PURGE_ITEM = "\
607DELETE FROM items WHERE collection = :collection AND seq = :seq AND retained_at IS NOT NULL \
608RETURNING object_hash, conflict_object";
609
610PURGE_RETAINED_BEFORE = "\
620DELETE FROM items WHERE retained_at IS NOT NULL AND retained_at < :cutoff \
621RETURNING object_hash, conflict_object";
622
623INSERT_BINDING = "\
626INSERT INTO bindings(collection, link_id, source, handle, base_flags, base_object, \
627base_revision, base_present, conflicted, conflict_revision, conflict_object, \
628shared_object) \
629VALUES(:collection, :link_id, :source, :handle, :base_flags, :base_object, \
630:base_revision, :base_present, :conflicted, :conflict_revision, :conflict_object, \
631:shared_object)";
632
633UPDATE_BINDING = "\
644UPDATE bindings SET base_flags = :base_flags, \
645base_object = :base_object, base_revision = :base_revision, base_present = :base_present, \
646conflicted = :conflicted, conflict_revision = :conflict_revision, \
647conflict_object = :conflict_object, shared_object = :shared_object \
648WHERE collection = :collection AND link_id = :link_id AND source = :source";
649
650BACKFILL_SHARED_OBJECT = "\
665UPDATE bindings SET shared_object = \
666(SELECT object_hash FROM items \
667 WHERE items.collection = bindings.collection AND items.link_id = bindings.link_id) \
668WHERE shared_object IS NULL";
669
670DELETE_BINDING = "DELETE FROM bindings WHERE collection = :collection AND link_id = :link_id AND source = :source";
672
673ADJUST_REFCOUNT =
676 "UPDATE objects SET refcount = refcount + :delta WHERE hash = :hash";
677
678RELEASE_PINS = "\
685UPDATE objects SET refcount = refcount - \
686 (SELECT count(*) FROM json_each(:hashes) WHERE value = objects.hash) \
687WHERE hash IN (SELECT value FROM json_each(:hashes))";
688
689UPSERT_CHECKPOINT = "\
692INSERT INTO sources(collection, source, checkpoint) VALUES(:collection, :source, :checkpoint) \
693ON CONFLICT(collection, source) DO UPDATE SET checkpoint = excluded.checkpoint";
694
695STORE_OBJECT = "\
699INSERT INTO objects(hash, size, refcount) VALUES(:hash, :size, 0) \
700ON CONFLICT(hash) DO UPDATE SET size = excluded.size";
701
702LOOKUP_OBJECTS = "\
713SELECT i.link_id, i.object_hash FROM items i \
714JOIN collections c ON c.id = i.collection \
715WHERE i.object_hash IS NOT NULL \
716 AND i.link_id IN (SELECT value FROM json_each(:links)) \
717 AND c.account IS :account";
718
719LIST_GARBAGE_OBJECTS = "SELECT hash FROM objects WHERE refcount <= 0";
729
730OBJECT_EXISTS = "SELECT 1 FROM objects WHERE hash = :hash";
733
734LIST_OBJECT_HASHES = "SELECT hash FROM objects";
739
740DELETE_GARBAGE_OBJECTS = "DELETE FROM objects WHERE refcount <= 0";
744
745RECOMPUTE_REFCOUNTS = "\
758UPDATE objects SET refcount = counted.n \
759FROM ( \
760 SELECT o.hash AS hash, count(r.hash) AS n FROM objects o \
761 LEFT JOIN ( \
762 SELECT object_hash AS hash FROM items WHERE object_hash IS NOT NULL \
763 UNION ALL SELECT conflict_object FROM items WHERE conflict_object IS NOT NULL \
764 UNION ALL SELECT base_object FROM bindings WHERE base_object IS NOT NULL \
765 UNION ALL SELECT conflict_object FROM bindings WHERE conflict_object IS NOT NULL \
766 UNION ALL SELECT object_hash FROM queue WHERE object_hash IS NOT NULL \
767 ) r ON r.hash = o.hash \
768 GROUP BY o.hash \
769) AS counted \
770WHERE counted.hash = objects.hash AND objects.refcount != counted.n";
771
772DELETE_DANGLING_BINDINGS = "\
776DELETE FROM bindings WHERE NOT EXISTS ( \
777 SELECT 1 FROM items i \
778 WHERE i.collection = bindings.collection AND i.link_id = bindings.link_id)";
779
780ENQUEUE_ACTION = "\
789INSERT INTO queue(created_at, producer, collection, action, payload, object_hash) \
790VALUES(:created_at, :producer, :collection, :action, :payload, :object_hash)";
791
792LIST_QUEUED_COLLECTIONS =
794 "SELECT DISTINCT collection FROM queue WHERE error IS NULL";
795
796LOAD_PENDING_ACTIONS = "\
800SELECT id, created_at, producer, action, payload, object_hash, attempts \
801FROM queue WHERE collection = :collection AND error IS NULL ORDER BY id";
802
803CLAIM_ACTION = "DELETE FROM queue WHERE id = :id RETURNING id";
812
813LOAD_ACTION_ROW = "SELECT attempts, object_hash FROM queue WHERE id = :id";
817
818CANCEL_ACTION = "DELETE FROM queue WHERE id = :id";
825
826PARK_ACTION =
829 "UPDATE queue SET attempts = :attempts, error = :error WHERE id = :id";
830
831BUMP_ATTEMPTS = "UPDATE queue SET attempts = attempts + 1 WHERE id = :id";
834
835LOAD_PARKED_ACTIONS = "\
837SELECT id, created_at, producer, collection, action, payload, attempts, error \
838FROM queue WHERE error IS NOT NULL ORDER BY id";
839
840BUMP_GENERATION = "\
843UPDATE collections SET generation = generation + 1 WHERE id = :collection \
844RETURNING generation";
845
846LOAD_GENERATION = "SELECT generation FROM collections WHERE id = :collection";
849
850OBJECT_STATS = "SELECT count(*), coalesce(sum(size), 0) FROM objects";
857
858LIVE_BYTES = "\
862SELECT coalesce(sum(size), 0) FROM objects WHERE hash IN \
863(SELECT object_hash FROM items WHERE object_hash IS NOT NULL AND retained_at IS NULL)";
864
865OBJECT_SIZE = "SELECT size FROM objects WHERE hash = :hash";
867
868COUNT_RETAINED_BEFORE = "\
872SELECT count(*), coalesce(sum(o.size), 0) FROM items i \
873LEFT JOIN objects o ON o.hash = i.object_hash \
874WHERE i.retained_at IS NOT NULL AND i.retained_at < :cutoff";
875
876REFCOUNT_DRIFT = "\
879WITH refs(hash) AS ( \
880 SELECT object_hash FROM items WHERE object_hash IS NOT NULL \
881 UNION ALL SELECT conflict_object FROM items WHERE conflict_object IS NOT NULL \
882 UNION ALL SELECT base_object FROM bindings WHERE base_object IS NOT NULL \
883 UNION ALL SELECT conflict_object FROM bindings WHERE conflict_object IS NOT NULL \
884 UNION ALL SELECT object_hash FROM queue WHERE object_hash IS NOT NULL \
885), counted(hash, n) AS (SELECT hash, count(*) FROM refs GROUP BY hash) \
886SELECT o.hash, o.refcount, coalesce(c.n, 0) FROM objects o \
887LEFT JOIN counted c ON c.hash = o.hash \
888WHERE o.refcount != coalesce(c.n, 0) ORDER BY o.hash";
889
890ITEM_BINDINGS = "\
894SELECT link_id, source, handle, base_flags, base_object, base_revision, base_present, \
895conflicted, conflict_revision, conflict_object, shared_object \
896FROM bindings WHERE collection = :collection AND link_id = :link_id \
897ORDER BY source";
898
899MINTED_KEYS = "\
910SELECT collection, count(*) FROM items \
911WHERE link_id GLOB 'dup:*' AND deleted = 0 AND retained_at IS NULL \
912GROUP BY collection ORDER BY collection";
913
914DANGLING_BINDINGS = "\
917SELECT b.collection, b.link_id, b.source FROM bindings b \
918WHERE NOT EXISTS (SELECT 1 FROM items i \
919 WHERE i.collection = b.collection AND i.link_id = b.link_id) \
920ORDER BY b.collection, b.link_id, b.source";
921
922DANGLING_ITEM_OBJECTS = "\
925SELECT collection, link_id, object_hash FROM items \
926WHERE object_hash IS NOT NULL AND object_hash NOT IN (SELECT hash FROM objects) \
927ORDER BY collection, link_id";
928
929DANGLING_QUEUE_OBJECTS = "\
932SELECT id, collection, object_hash FROM queue \
933WHERE object_hash IS NOT NULL AND object_hash NOT IN (SELECT hash FROM objects) \
934ORDER BY id";
935}
936
937#[cfg(test)]
938mod tests {
939 use super::ALL;
940
941 #[test]
942 fn no_statement_is_empty() {
943 for (name, sql) in ALL {
944 assert!(!sql.trim().is_empty(), "{name} is empty");
945 }
946 }
947}