Skip to main content

io_pimdir/
sql.rs

1//! The canonical pimdir SQL, inlined verbatim from the spec so the crate
2//! is self-contained. Kept in sync with `pimdir/migrations/` and
3//! `pimdir/queries/`, where the source of truth is.
4//!
5//! A store keeps one shared item per logical thing (its flags, body and
6//! summary) and one binding per source that syncs it (that source's last
7//! agreed base). A single-source store is the degenerate case of one
8//! binding per item; a two-source store keeps two.
9
10/// The current schema version.
11pub const VERSION: i64 = 1;
12
13/// Indexes an earlier draft created under the same name over different
14/// columns, as `(name, the columns it must hold now)`.
15///
16/// [`ENSURE_INDEXES`] cannot repair one: `CREATE INDEX IF NOT EXISTS`
17/// keys on the name, so it leaves the old shape in place and the store
18/// keeps planning the read the schema no longer says. Such an index is
19/// dropped on open when its columns disagree, then recreated.
20///
21/// Checked rather than dropped unconditionally, since rebuilding a large
22/// store's index on every open is the cost this exists to avoid.
23pub const RESHAPED_INDEXES: &[(&str, &[&str])] = &[
24    // NOTE: was (collection, retained_at) while `list_retained_page`
25    // still paged by `link_id`. A store keeping the old one sorts every
26    // retained row of the collection to return one page.
27    ("items_retained", &["collection", "seq"]),
28];
29
30/// Declares the module's statements and the [`ALL`] index in one
31/// expansion, so a new statement is added in one place rather than three.
32macro_rules! statements {
33    ($($(#[$doc:meta])* $name:ident = $sql:expr;)*) => {
34        $($(#[$doc])* pub const $name: &str = $sql;)*
35
36        /// Every statement in this module, paired with its constant name.
37        ///
38        /// How a consumer without the `client` feature reaches the
39        /// canonical SQL: it holds its own SQLite driver, so it needs the
40        /// statements by name rather than a Rust accessor each.
41        /// [`MIGRATION_0001`] is included, creating the database being as
42        /// much a consumer's job as querying it; [`VERSION`] is not,
43        /// being an integer.
44        pub const ALL: &[(&str, &str)] = &[$((stringify!($name), $name)),*];
45    };
46}
47
48// NOTE: the statements below are not indented into the macro invocation:
49// half of them are raw strings holding the spec's SQL verbatim, and
50// indenting would rewrite that text.
51statements! {
52/// Schema version 1 (`migrations/0001_init.sql`), the whole draft schema
53/// including the action queue and collection generations. Applied to a fresh
54/// database; the caller sets `PRAGMA user_version = 1` on success.
55MIGRATION_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
241/// Creates a collection row if it does not exist yet, leaving an existing one
242/// untouched (the kind is declared separately by `SET_COLLECTION_KIND`).
243ENSURE_COLLECTION = "\
244INSERT INTO collections(id, account, kind, name) VALUES(:collection, :account, '', :collection) \
245ON CONFLICT(id) DO NOTHING";
246
247/// Declares (or re-declares) a collection's kind, creating the row if the
248/// collection is not known yet. Updates the kind alone, so a collection never
249/// changes account as a side effect of a sync declaring its media type.
250SET_COLLECTION_KIND = "\
251INSERT INTO collections(id, account, kind, name) VALUES(:collection, :account, :kind, :collection) \
252ON CONFLICT(id) DO UPDATE SET kind = excluded.kind";
253
254/// Regroups a collection under another account, or out of one with `NULL`. Safe
255/// at any time: the account partitions no identifier (spec §9.2), so the move
256/// leaves seqs, link ids and objects alone.
257SET_COLLECTION_ACCOUNT =
258    "UPDATE collections SET account = :account WHERE id = :collection";
259
260/// Gives a collection a new id, carrying its whole contents with it: every
261/// foreign key onto `collections(id)` is `ON UPDATE CASCADE`, so the items,
262/// bindings, sources, queue rows and child collections follow in the same
263/// statement (spec §14).
264///
265/// The only safe way to change an id. Deleting and recreating the collection
266/// instead destroys the cache: the `ON DELETE CASCADE` takes every item and
267/// binding with it, so a rename silently becomes a full re-download and drops
268/// any staged local change not yet pushed.
269RENAME_COLLECTION = "UPDATE collections SET id = :new_id WHERE id = :collection";
270
271/// Reads a collection's owning account.
272LOAD_ACCOUNT = "SELECT account FROM collections WHERE id = :collection";
273
274/// Reads a collection's declared kind.
275LOAD_KIND = "SELECT kind FROM collections WHERE id = :collection";
276
277/// Stores a collection's conflict policy.
278SET_CONFLICT = "UPDATE collections SET conflict = :conflict WHERE id = :collection";
279
280/// Reads a collection's conflict policy.
281LOAD_CONFLICT = "SELECT conflict FROM collections WHERE id = :collection";
282
283/// Loads a whole collection for the sync seam: every item, tombstones
284/// included, unpaginated and unordered.
285///
286/// Retained (soft-deleted) rows are excluded, which is what makes
287/// retention safe under io-replica's contract: the merge reconciles only
288/// what `load` returns, so a hidden row is never re-derived.
289///
290/// `sort_key` rides along so the round trip preserves it: the engine
291/// carries the key on a placement, so a load that dropped it would hand
292/// every save an unknown key and erase on every sync what the last one
293/// derived (spec §9.3).
294LOAD_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
298/// The same rows, narrowed to the link ids one write batch touches (spec §14).
299///
300/// A write folds its batch into the hub and persists the difference, and
301/// that difference only names rows the batch named: reading the rest
302/// costs a full pass over the collection to compute nothing, growing
303/// with the mailbox rather than with the batch.
304LOAD_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
309// Client read surface (kind-agnostic, indexed getters over the same store the
310// sync seam writes). Distinct from `LOAD_ITEMS`: paginated, live-only, ordered.
311
312/// Lists every collection with its display metadata and generation, ordered by
313/// `sort_order` then id, the ones carrying no sort order coming last.
314LIST_COLLECTIONS = "\
315SELECT id, account, kind, name, parent, color, description, sort_order, generation \
316FROM collections ORDER BY sort_order IS NULL, sort_order, id";
317
318/// One account's collections, the filter axis of a merged view. `IS` so binding
319/// `NULL` selects the collections of a single-account store.
320LIST_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
324/// The accounts owning at least one collection. A store knows an account only
325/// through its collections (spec §9.2), so this is not a configured roster.
326LIST_ACCOUNTS = "\
327SELECT DISTINCT account FROM collections WHERE account IS NOT NULL ORDER BY account";
328
329/// A keyset page of a collection's live items in link-id order. `:after`
330/// is the exclusive lower bound on `link_id`, the empty string starting
331/// from the beginning since a `link_id` is never empty; rides the `items`
332/// primary key, with no extra index.
333///
334/// Link-id order means nothing to a reader: this is the page for a sweep
335/// that must see every item exactly once. A reader presenting a list
336/// wants one of the two ordered pages below.
337LIST_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
342/// A keyset page of a collection's live items in the kind's own
343/// ascending order (spec §9.3): A to Z for contacts, earliest first for
344/// mail and calendars.
345///
346/// The cursor is the pair `(:after_key, :after_seq)`, because a sort key
347/// is not unique: two messages share a timestamp, two contacts a name.
348/// `seq` breaks the tie and, being unique per collection, makes the page
349/// total. The empty string with seq 0 starts from the beginning, since
350/// no real key sorts before an unknown one ascending.
351LIST_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
357/// The same page descending: newest first for mail and calendars, Z to A
358/// for contacts.
359///
360/// The first page binds a NULL cursor rather than a key above every other
361/// one: a sort key is arbitrary text a writer derives, so no value is
362/// reserved and "the largest key the store can hold" is not expressible.
363/// A sentinel would hide everything sorting above it from every
364/// descending page, for good. The comparison stays a keyset one, so the
365/// index still serves it.
366LIST_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
372/// Restates one item's ordering key, for a re-projection over items
373/// already stored: a store written before its kind had a convention, one
374/// whose convention changed, or a consumer whose sync engine does not
375/// carry the key inline (spec §9.3). Not the ordinary write path.
376SET_SORT_KEY = "\
377UPDATE items SET sort_key = :sort_key \
378WHERE collection = :collection AND link_id = :link_id";
379
380/// Fetches one live item by its public id (`seq`), the client-facing key.
381GET_ITEM = "\
382SELECT seq, link_id, flags, object_hash, meta, sort_key, level FROM items \
383WHERE collection = :collection AND seq = :seq AND deleted = 0";
384
385/// Resolves an item's public id (`seq`) from its internal `link_id`, the
386/// inverse of `GET_ITEM`, for a consumer that just staged an add.
387SEQ_BY_LINK =
388    "SELECT seq FROM items WHERE collection = :collection AND link_id = :link_id";
389
390/// Counts a collection's live items (tombstones excluded).
391COUNT_ITEMS =
392    "SELECT count(*) FROM items WHERE collection = :collection AND deleted = 0";
393
394/// Every live placement of one identity, with the collection and account
395/// it sits in (spec §9.2). The store reports where a link id occurs and
396/// takes no position on whether the placements are one thing: a mail view
397/// lists them, a contact view may offer to merge them, off these rows.
398LIST_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
404/// The same on the dedup axis, by body rather than identity, so it pairs
405/// placements two servers gave different link ids.
406LIST_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
412/// The distinct source names the store has synced, across all
413/// collections, so a client discovers which source to attribute writes
414/// to.
415LIST_SOURCES = "SELECT DISTINCT source FROM bindings ORDER BY source";
416
417/// Loads every per-source binding of a collection: the stored base (handle,
418/// flags, object, revision) each sync merges against.
419LOAD_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
424/// The same rows, narrowed to the link ids one write batch touches: the binding
425/// half of [`LOAD_ITEMS_BY_LINK`].
426LOAD_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
432/// The bindings waiting for a decision, across an account's collections:
433/// what each one is, and the three bodies a resolver merges.
434///
435/// The base is the last state the two sides agreed on, the item's own
436/// `object_hash` is the local side, and `conflict_object` is the remote
437/// one at `conflict_revision`. All three come off the one row, so a
438/// resolver holding no credentials reads the whole divergence from the
439/// store.
440///
441/// Scoped to one account with `IS`, so binding `NULL` lists a
442/// single-account store whole. Rides the partial index
443/// `bindings_conflicted`, which holds only the outstanding rows: the
444/// question is asked at the end of every run, and answering it by paging
445/// each collection costs a pass over the whole store to report a number
446/// that is usually zero.
447LIST_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
456/// Whether a collection holds a live item under a link id: the collision
457/// check a queued `add` runs before staging.
458///
459/// A point read on the items primary key, because it runs once per
460/// drained action: answering it by loading the collection would make a
461/// drain of N actions cost N passes over the mailbox.
462LIVE_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
467/// One source's handle for an item, which its binding's primary key answers
468/// directly: the lookup a queued action needs to name the placement it edits.
469HANDLE_FOR_LINK = "\
470SELECT handle FROM bindings \
471WHERE collection = :collection AND link_id = :link_id AND source = :source";
472
473/// The link id one source's handle is bound to, for a batch that drops a
474/// placement: a drop names a handle, and the hub is keyed by link id.
475///
476/// Served by the `bindings_by_handle` index, so resolving it is a seek
477/// rather than a scan over every item.
478LINK_FOR_HANDLE = "\
479SELECT link_id FROM bindings \
480WHERE collection = :collection AND source = :source AND handle = :handle";
481
482/// Reads one source's sync checkpoint for a collection.
483LOAD_CHECKPOINT =
484    "SELECT checkpoint FROM sources WHERE collection = :collection AND source = :source";
485
486/// The message's existing public id, if any placement of this `link_id` already
487/// has one (in any collection), so all placements of a message share one id.
488SEQ_FOR_LINK_ANY = "SELECT seq FROM items WHERE link_id = :link_id LIMIT 1";
489
490/// Hands out, and advances, the store-global next public id via
491/// `RETURNING`. The counter only ever increases, so a `seq` is never
492/// reused. Run only when the message has no id yet.
493BUMP_NEXT_SEQ =
494    "UPDATE store_meta SET next_seq = next_seq + 1 WHERE id = 1 RETURNING next_seq - 1";
495
496/// Inserts one item row (the new-placement path; `UPDATE_ITEM` handles an
497/// existing one).
498INSERT_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
502/// Updates one existing item's columns in place (the diffed-save path; the
503/// primary key `(collection, link_id)` is unchanged).
504UPDATE_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
509// Retention (spec §11): the last binding vanishing retires the row instead of
510// deleting it, a reappearing link id revives it, and purge is the only true
511// delete.
512
513/// Retires one item: it stands exactly where a hard-deleting store would
514/// have issued its delete. The row keeps its `object_hash`, so the body
515/// keeps its reference and its blob survives the sweep. SQLite stamps the
516/// instant itself, so no clock is plumbed through the crate; a purge's
517/// cutoff is by contrast the caller's parameter.
518RETAIN_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
523/// Deletes every binding of one item, for the retire path: the row
524/// survives, but no source holds it, so no base does either. A retained
525/// row carrying no binding is the persisted form of "the removal has
526/// finished propagating" (spec §11).
527DELETE_ITEM_BINDINGS =
528    "DELETE FROM bindings WHERE collection = :collection AND link_id = :link_id";
529
530/// The retained row holding a link id, if any: its public id and the objects it
531/// pins, which revive releases and purge reclaims.
532RETAINED_ITEM = "\
533SELECT seq, object_hash, conflict_object FROM items \
534WHERE collection = :collection AND link_id = :link_id AND retained_at IS NOT NULL";
535
536/// Revives a retained row: the link id is back, from a source-side
537/// resurrection or a client `add`, so it stops being retained instead of
538/// conflicting on the primary key. The caller adopts the new content with
539/// `UPDATE_ITEM` in the same transaction, and the row keeps its `seq`.
540REVIVE_ITEM = "\
541UPDATE items SET deleted = 0, retained_at = NULL, retained_by = NULL \
542WHERE collection = :collection AND link_id = :link_id";
543
544/// A keyset page of a collection's retained items, joined to the body size the
545/// row still pins (`NULL` when unhydrated): the trash listing beside
546/// `LIST_ITEMS_PAGE`, and the only read that returns them.
547///
548/// `:after` is the exclusive lower bound on the public `seq`, 0 starting
549/// from the beginning: a real sentinel rather than an invented one, since
550/// `seq` is handed out from 1. A caller pages the trash by the same small
551/// integer it purges and restores by.
552LIST_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
559/// Every index the schema grew after version 1 was first published, as one
560/// idempotent batch: a store written by an earlier draft has the tables but not
561/// these, and an index is not something a reader can do without.
562///
563/// Run on open rather than only when a column is missing, because most of
564/// these index columns that were always there: what changed is that a
565/// statement now needs them. A store keeping the old plans would scan
566/// where the schema says it seeks.
567///
568/// `IF NOT EXISTS` keys on the name, so an index whose columns changed is
569/// not replaced by this batch and has to be dropped first (see
570/// [`RESHAPED_INDEXES`]).
571ENSURE_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
586/// Counts a collection's retained items, the counterpart of `COUNT_ITEMS`;
587/// rides the `items_retained` partial index.
588COUNT_RETAINED =
589    "SELECT count(*) FROM items WHERE collection = :collection AND retained_at IS NOT NULL";
590
591/// The store-wide size of the bodies retention is holding, each distinct
592/// object counted once. An upper bound on what a purge reclaims: an
593/// object a live item also points at keeps a reference and survives.
594RETAINED_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
598/// Purges one retained item by its public id: the only true delete. Its
599/// bindings cascade, and the body it released is unlinked by the
600/// collector once nothing else references it. Guarded on `retained_at`,
601/// so a purge can never take a live item.
602///
603/// Returns the two hashes the row pinned, so the caller settles them with
604/// [`RELEASE_PINS`] in the same transaction rather than visiting the row
605/// twice.
606PURGE_ITEM = "\
607DELETE FROM items WHERE collection = :collection AND seq = :seq AND retained_at IS NOT NULL \
608RETURNING object_hash, conflict_object";
609
610/// The time-based sweep: every item retired strictly before `:cutoff`
611/// (RFC 3339), so one retained exactly at that instant is kept.
612/// Store-wide, since how long to keep is the owner's policy. The cutoff
613/// is the caller's parameter, not the store's clock, so the boundary is
614/// deterministic even though the stamps are SQLite's.
615///
616/// Returns each purged row's two pinned hashes, on the same terms as
617/// [`PURGE_ITEM`]: this is where visiting the rows twice costs most,
618/// being the sweep that takes fifty thousand at once.
619PURGE_RETAINED_BEFORE = "\
620DELETE FROM items WHERE retained_at IS NOT NULL AND retained_at < :cutoff \
621RETURNING object_hash, conflict_object";
622
623/// Inserts one item's binding for one source (the new-binding path;
624/// `UPDATE_BINDING` handles an existing one).
625INSERT_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
633/// Updates one existing binding's columns in place (its primary key
634/// `(collection, link_id, source)` is unchanged).
635///
636/// `handle` is deliberately not among them, and cannot be. A binding
637/// pins one handle, and repointing it would destroy the evidence that a
638/// source holds an identity twice, before any later rule could act on it.
639/// A write resolving this binding to another handle is refused instead
640/// (spec §10), the second copy having a key and an item of its own (spec
641/// §9). A legitimate rebind, after a handle-space change, goes through
642/// the rebuild that drops the old spine and inserts the new one.
643UPDATE_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
650/// Gives every binding written before `shared_object` existed the item's
651/// own body as its agreement point, once the column has been added
652/// (spec §6, the `draft` allowance).
653///
654/// Left empty the column reads as "this source has never folded", which
655/// falls back to the sync base, and a binding whose push is pending sits
656/// behind the shared body by definition: the first absorb after the
657/// upgrade would then measure the cross-source axis from the base again
658/// and file the source's own next edit as a divergence. An existing
659/// store's sources agree with the body they hold, so that body is what
660/// the rows already imply.
661///
662/// Guarded on `IS NULL`, which is every row of a column just added and
663/// no row of one already backfilled, so running it twice is a no-op.
664BACKFILL_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
670/// Deletes one source's binding of an item.
671DELETE_BINDING = "DELETE FROM bindings WHERE collection = :collection AND link_id = :link_id AND source = :source";
672
673/// Adjusts one object's refcount by a signed delta; the hash's primary
674/// key makes this an indexed point update.
675ADJUST_REFCOUNT =
676    "UPDATE objects SET refcount = refcount + :delta WHERE hash = :hash";
677
678/// Releases one reference from each of the given hashes (a JSON array), the
679/// set-based form of [`ADJUST_REFCOUNT`] at `-1`.
680///
681/// A hash listed twice releases twice, which is what makes it the same
682/// operation as the loop it replaces: a retained item pins its body and
683/// its conflict body separately, and a purge releases both.
684RELEASE_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
689/// Writes one source's sync checkpoint for a collection, replacing the
690/// previous one.
691UPSERT_CHECKPOINT = "\
692INSERT INTO sources(collection, source, checkpoint) VALUES(:collection, :source, :checkpoint) \
693ON CONFLICT(collection, source) DO UPDATE SET checkpoint = excluded.checkpoint";
694
695/// Indexes an object by its content hash at refcount 0; re-storing a known
696/// hash only refreshes its size, since the count belongs to
697/// `ADJUST_REFCOUNT`.
698STORE_OBJECT = "\
699INSERT INTO objects(hash, size, refcount) VALUES(:hash, :size, 0) \
700ON CONFLICT(hash) DO UPDATE SET size = excluded.size";
701
702/// Resolves the object hash currently bound to each of the given link ids
703/// (passed as a JSON array), skipping the ones carrying no body.
704///
705/// Scoped to one account, the axis a link id is trustworthy on. Across
706/// collections it is what this read exists for: one message filed in two
707/// mailboxes is one body, downloaded once. Across accounts it is not a
708/// fact at all, two unrelated servers being free to mint the same vCard
709/// `UID` (spec §9.2), and answering with the other account's body hands
710/// one account's content to the other's sync. A single-account store
711/// writes no account, so the filter is a no-op and the dedup whole-store.
712LOOKUP_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
719/// Lists the objects nothing references any more: what the collector
720/// takes, and never a write's business, since the batch that attaches a
721/// body may not be the one that indexed it (spec §5).
722///
723/// `<= 0` rather than `= 0`, matching the partial index
724/// `objects_garbage` exactly so neither statement scans the table. Under
725/// the refcount floor (spec §7) the two select the same rows; the wider
726/// one is for a read-only reader, whose store may predate the constraint
727/// and still carry a negative count.
728LIST_GARBAGE_OBJECTS = "SELECT hash FROM objects WHERE refcount <= 0";
729
730/// Whether the index still holds a body: the collector's question about the one
731/// file in front of it, asked on the primary key (spec §5).
732OBJECT_EXISTS = "SELECT 1 FROM objects WHERE hash = :hash";
733
734/// Every hash the index holds. For the diagnosis that has to visit every
735/// row anyway, never for the collector, which asks about the file in
736/// front of it with [`OBJECT_EXISTS`] rather than holding the whole index
737/// in memory.
738LIST_OBJECT_HASHES = "SELECT hash FROM objects";
739
740/// Drops the unreferenced object rows inside the collector's transaction; their
741/// blobs are unlinked after the commit, so a crash leaves at worst an orphan
742/// blob.
743DELETE_GARBAGE_OBJECTS = "DELETE FROM objects WHERE refcount <= 0";
744
745/// Recomputes every object's refcount from the five columns that pin one (spec
746/// §7): an item's body, an item's conflict copy, a source's stored base, a
747/// binding's diverging remote body and a pending queue action's body.
748///
749/// The repair, not the write path: writes maintain the count
750/// incrementally with `ADJUST_REFCOUNT`, which is O(changes) where this
751/// is O(items+bindings+queue). The pointers are gathered into one stream
752/// and counted in a single grouped pass, so the cost is linear in them
753/// rather than in their product with the object table. The left join
754/// settles an object no pointer names any more, counting zero rather
755/// than going unvisited, and a row already holding its true count is
756/// left alone.
757RECOMPUTE_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
772/// Deletes the bindings whose item is gone, the one dangling row a repair can
773/// clear without guessing: a binding with no item is unreachable, where an item
774/// with no object row still holds the item.
775DELETE_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
780// The action queue (spec §15, `queries/queue.sql`): the write door for every
781// process that is not the store owner. A producer appends; the owner applies
782// pending actions in append order and deletes each in the same transaction as
783// its effects.
784
785/// A producer's append. Runs after `ENSURE_COLLECTION`, in one
786/// transaction with the `STORE_OBJECT` upsert when the payload references
787/// a body (spec §15.1).
788ENQUEUE_ACTION = "\
789INSERT INTO queue(created_at, producer, collection, action, payload, object_hash) \
790VALUES(:created_at, :producer, :collection, :action, :payload, :object_hash)";
791
792/// The collections with pending work, for the owner's drain loop.
793LIST_QUEUED_COLLECTIONS =
794    "SELECT DISTINCT collection FROM queue WHERE error IS NULL";
795
796/// The owner's drain: a collection's pending (non-parked) actions, in append
797/// order. A reader runs the same statement to overlay pending actions on its
798/// item projection (read-your-writes, spec §15.4).
799LOAD_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
803/// Deletes the row an owner is about to apply, and reports whether it was still
804/// there.
805///
806/// It runs first in the applying transaction, not last: the pending rows
807/// are read outside any transaction, so a second owner reading the same
808/// list would otherwise apply every action twice, and `add` and `copy`
809/// are not idempotent. Claiming the row first makes exactly-once a
810/// property of the statement rather than a convention about who drains.
811CLAIM_ACTION = "DELETE FROM queue WHERE id = :id RETURNING id";
812
813/// One queue row's spent attempts and pinned body, for a caller acting on a row
814/// by id: cancelling it, acknowledging an intent it performed out of band, or
815/// recording a failure.
816LOAD_ACTION_ROW = "SELECT attempts, object_hash FROM queue WHERE id = :id";
817
818/// One queue row removed by request rather than by application, pending
819/// or parked (spec §15.5): a queued item withdrawn, or a performed intent
820/// acknowledged by the process that carried it out. The same delete as
821/// `DELETE_ACTION`, named apart because the trigger is a request. It
822/// releases the row's `object_hash` pin, so it runs in one transaction
823/// with the refcount settle.
824CANCEL_ACTION = "DELETE FROM queue WHERE id = :id";
825
826/// A permanently failing action: recorded and skipped, visible to
827/// operators instead of blocking the collection's queue.
828PARK_ACTION =
829    "UPDATE queue SET attempts = :attempts, error = :error WHERE id = :id";
830
831/// Records a failed apply attempt without parking: the retry path, the
832/// reference `park_action` with a `NULL` error.
833BUMP_ATTEMPTS = "UPDATE queue SET attempts = attempts + 1 WHERE id = :id";
834
835/// The parked actions, for status surfaces and operator repair.
836LOAD_PARKED_ACTIONS = "\
837SELECT id, created_at, producer, collection, action, payload, attempts, error \
838FROM queue WHERE error IS NOT NULL ORDER BY id";
839
840/// The owner's handle-space reset marker (spec §12): run in the same
841/// transaction as the rebuild it records.
842BUMP_GENERATION = "\
843UPDATE collections SET generation = generation + 1 WHERE id = :collection \
844RETURNING generation";
845
846/// A collection's handle-space epoch, so a reader derives epoch-dependent
847/// protocol values (an IMAP UIDVALIDITY) from the store alone.
848LOAD_GENERATION = "SELECT generation FROM collections WHERE id = :collection";
849
850// NOTE: diagnostics (spec §7), what a consistency check asks about the
851// index rather than through it. Not canonical statements, the spec
852// stating the invariants rather than the queries that observe them, but
853// inlined so a consumer running its own driver can check what it wrote.
854
855/// How many objects are indexed and what they weigh.
856OBJECT_STATS = "SELECT count(*), coalesce(sum(size), 0) FROM objects";
857
858/// The bytes held by objects at least one live item binds. An object a
859/// live and a retained item share counts here, since purging the
860/// retained one frees nothing.
861LIVE_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
865/// One object's stored size.
866OBJECT_SIZE = "SELECT size FROM objects WHERE hash = :hash";
867
868/// What a purge with this cutoff would retire, and what its bodies weigh:
869/// the preview a confirmation prints, `PURGE_RETAINED_BEFORE` being the
870/// act.
871COUNT_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
876/// The objects whose stored refcount disagrees with the five pointer columns
877/// that justify it: the read `RECOMPUTE_REFCOUNTS` settles.
878REFCOUNT_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
890/// Every source's binding of one item: the handle it is bound to, the base
891/// the last sync agreed on, and the conflict it is stuck on. The read behind
892/// `item show`, which names one item and can afford to say everything about it.
893ITEM_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
899/// How many minted keys (spec §9, `dup:<hint>#<handle>`) each collection
900/// holds: the second copy of an identity a source hands over twice,
901/// filed as an item of its own.
902///
903/// Informational, and the only read that looks at the shape of a key at
904/// all. It counts them and nothing more: no hint and no handle is read
905/// back out of one, since a minted key is opaque and a store that
906/// resolved a prefix would make the engine's assignment reversible by
907/// accident. `GLOB` rather than `LIKE`, which is case-insensitive over
908/// ASCII and would count a hint of its own spelling.
909MINTED_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
914/// The bindings whose item is gone: the one dangling row a repair can clear,
915/// since nothing can read it (`DELETE_DANGLING_BINDINGS`).
916DANGLING_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
922/// The items whose body is not indexed. Reported, never repaired: the
923/// item is still the item.
924DANGLING_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
929/// The queue rows whose body is not indexed. Reported, never repaired:
930/// the row is still an intent somebody expressed.
931DANGLING_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}