1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
// SPDX-License-Identifier: Apache-2.0 OR MIT
//! Metadata store trait + helper structs. SQLite impl in `sqlite` submodule.
mod sqlite;
use std::fmt;
use async_trait::async_trait;
use crate::audit::AuditOp;
use crate::content::ContentHash;
use crate::error::Result;
use crate::link::Link;
use crate::memory::{MemoryId, MemoryRef};
use crate::partition::PartitionPath;
use crate::summarizer::SummaryStyle;
use crate::summary::{Scope, SummaryId, SummarySubject};
pub use sqlite::SqliteMetadata;
/// Snapshot of `schema_meta` row.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SchemaMeta {
/// Partition scheme template.
pub partition_scheme: String,
/// Scheme version (slice 1: always 1).
pub scheme_version: i64,
/// Embedder id. `None` means caller-provided embedder mode has not yet
/// been bound (no Embedder configured on the engine and no first append
/// has happened). After the first append in caller-provided mode this
/// field carries the sentinel
/// [`crate::embedder::CALLER_PROVIDED_EMBEDDER_ID`] (`<caller-provided>`).
pub embedder_id: Option<String>,
/// Embedder dims. `None` until first vector locks the dimensionality.
pub embedder_dims: Option<i64>,
/// Created-at unix millis.
pub created_at_ms: i64,
}
/// Row pulled out of the `memory` table.
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize)]
#[non_exhaustive]
pub struct MemoryRow {
/// Memory id.
pub id: MemoryId,
/// Partition path.
pub partition_path: PartitionPath,
/// Storage key for the content blob.
pub data_path: String,
/// Content kind ("md" or "txt") — denormalised from the storage extension.
pub content_kind: String,
/// Content hash.
pub content_hash: ContentHash,
/// Body length in bytes.
pub bytes: i64,
/// Embedder id captured at append.
pub embedder_id: String,
/// Soft tombstone flag.
pub tombstoned: bool,
/// Created-at unix millis.
pub created_at_ms: i64,
/// Updated-at unix millis.
pub updated_at_ms: i64,
/// Plan 15: when the FACT became operationally true.
pub valid_from_ms: Option<i64>,
/// Plan 15: when the FACT stopped being true.
pub valid_until_ms: Option<i64>,
/// Plan 15: typed memory discriminator. NULL on legacy rows.
pub kind: Option<crate::memory::MemoryKind>,
}
/// Audit-log entry as written by the engine.
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct AuditEntry {
/// Unix millis.
pub ts_ms: i64,
/// Optional actor identifier.
pub actor: Option<String>,
/// Op tag.
pub op: AuditOp,
/// Partition path the op touched.
pub partition_path: Option<PartitionPath>,
/// Memory id the op touched.
pub memory_id: Option<MemoryId>,
/// JSON-serialisable detail.
pub detail: serde_json::Value,
}
/// Append-time payload for `MetadataStore::append_memory`.
#[derive(Debug, Clone)]
pub struct AppendMemoryRequest {
/// New memory row.
pub row: MemoryRow,
/// Explicit links to insert (bidirectional inserts handled internally).
pub explicit_links: Vec<MemoryRef>,
/// Audit entry.
pub audit: AuditEntry,
/// Plan 17 — little-endian f32 packed embedding bytes
/// (`embedder_dims * 4`). Stored in `memory.embedding_blob` so the
/// vector lives inside the same SQL transaction as the row.
pub embedding_blob: Vec<u8>,
/// Plan 18 — memory body text fed into the lexical index (`memory_fts`)
/// inside the same transaction. SQLite-backed stores also insert a
/// matching `memory_vec` row from `embedding_blob` so search hits a
/// memory the moment its commit returns.
pub content_for_index: String,
}
/// Result of `append_memory`.
#[derive(Debug, Clone)]
pub struct AppendMemoryOutcome {
/// New audit-log seq.
pub audit_seq: i64,
}
/// Plan 18 — attach-summary payload for `MetadataStore::insert_summary`.
/// Carries the index-side fields (`embedding_blob`, `content_for_index`,
/// `parent_path`) alongside the row so the SQLite impl can write
/// `summary_vec` + `summary_fts` inside the same transaction as the
/// catalog row.
#[derive(Debug, Clone)]
pub struct InsertSummaryRequest {
/// New summary row.
pub row: SummaryRow,
/// Audit entry.
pub audit: AuditEntry,
/// Optional little-endian f32 packed embedding bytes for `summary_vec`.
/// `None` skips the vector insert (caller-provided mode without an
/// embedder). Length, when present, MUST equal `embedder_dims * 4`.
pub embedding_blob: Option<Vec<u8>>,
/// Body text fed into `summary_fts`.
pub content_for_index: String,
/// `parent_path` value to bind into both index rows. The convention is
/// `<root>` for tenant subjects, the partition path for partition
/// subjects, and the memory's owning partition for memory subjects.
pub parent_path: String,
}
/// One row replayed into a leaf's delta during recovery.
#[derive(Debug, Clone)]
pub struct AuditReplayRow {
/// Audit seq.
pub seq: i64,
/// Op: `Append` or `Delete` only.
pub op: AuditOp,
/// Memory touched.
pub memory_id: MemoryId,
}
/// Metadata store trait.
#[async_trait]
pub trait MetadataStore: Send + Sync + fmt::Debug + 'static {
/// Stable identifier (e.g. `"sqlite:/path/to/db.sqlite"`, `"sqlite::memory:"`).
fn id(&self) -> String;
/// Self-declared capabilities.
fn capabilities(&self) -> crate::capabilities::MetadataCapabilities {
crate::capabilities::MetadataCapabilities::default()
}
/// Apply migrations & verify the schema is at the expected version.
async fn migrate(&self) -> Result<()>;
/// Plan 18: idempotently create the index virtual tables (`memory_vec`,
/// `summary_vec`, `memory_fts`, `summary_fts`) using the supplied embedder
/// dimension. Replaces the legacy lazy-bootstrap. Implementations whose
/// indices do not need a known dim at creation may ignore `dim`.
///
/// Default: no-op, so non-SQLite stores stay buildable without forcing a
/// vec0/fts5 surface on themselves.
async fn create_indices_if_missing(&self, _dim: usize) -> Result<()> {
Ok(())
}
/// Plan 18: borrow the underlying `sqlx::SqlitePool` if this store is
/// SQLite-backed. Returns `None` for non-SQLite implementations.
/// Used by the runtime migrator and the default `VectorIndex` /
/// `LexicalIndex` implementations to share the catalog connection.
fn sqlite_pool(&self) -> Option<&sqlx::SqlitePool> {
None
}
/// Read the `schema_meta` row, if present.
async fn read_schema_meta(&self) -> Result<Option<SchemaMeta>>;
/// Write the `schema_meta` row at first init. Returns `Error::Config` if a
/// row already exists.
async fn write_schema_meta(&self, meta: &SchemaMeta) -> Result<()>;
/// Bump `schema_meta.scheme_version`. Used by the runtime storage-layout
/// migrator after it finishes moving files. No-ops if no row exists.
async fn set_scheme_version(&self, version: i64) -> Result<()>;
/// Read a row from `migration_state` if any. Used by the runtime
/// layout migrator (Plan 9) to resume idempotently.
async fn read_migration_state(&self, name: &str) -> Result<Option<MigrationStateRow>>;
/// Insert or update a row in `migration_state`.
async fn upsert_migration_state(&self, row: MigrationStateRow) -> Result<()>;
/// Ensure the partition path (and all ancestors) exist. Idempotent.
async fn ensure_partition_chain(
&self,
path: &PartitionPath,
is_leaf: bool,
ts_ms: i64,
) -> Result<()>;
/// Insert a memory row + explicit links + audit entry in a single
/// transaction.
async fn append_memory(&self, req: AppendMemoryRequest) -> Result<AppendMemoryOutcome>;
/// Look up a single live memory.
async fn get_memory(&self, id: &MemoryId) -> Result<Option<MemoryRow>>;
/// List live memories under a partition (paginated).
async fn list_memories(
&self,
path: &PartitionPath,
limit: u32,
cursor: Option<&str>,
include_tombstoned: bool,
) -> Result<(Vec<MemoryRow>, Option<String>)>;
/// Soft-tombstone a single memory + audit entry.
async fn tombstone_memory(&self, id: &MemoryId, audit: AuditEntry) -> Result<()>;
/// Soft-tombstone every memory under a path (recursive) + audit entry.
async fn tombstone_partition(&self, path: &PartitionPath, audit: AuditEntry) -> Result<u64>;
/// Insert an explicit link (bidirectional) + audit entry.
async fn add_link(&self, link: &Link, audit: AuditEntry) -> Result<()>;
/// Remove an explicit link (bidirectional) + audit entry.
async fn remove_link(&self, src: &MemoryId, dst: &MemoryId, audit: AuditEntry) -> Result<()>;
/// Return all links for a given memory (both directions).
async fn links_of(&self, id: &MemoryId) -> Result<Vec<Link>>;
// ----- Plan 3 additions -----
/// All leaf partitions (where `is_leaf = 1`).
async fn list_leaf_partitions(&self) -> Result<Vec<PartitionPath>>;
/// Audit-log rows with `seq > since` for ops that affect indices
/// (`append`, `delete`). Restricted to one partition.
async fn audit_since(
&self,
partition: &PartitionPath,
since: i64,
) -> Result<Vec<AuditReplayRow>>;
/// Look up the data path + content kind for a memory regardless of tombstone
/// state. Used by the recovery walker to re-fetch + re-embed.
async fn lookup_for_recovery(&self, id: &MemoryId) -> Result<Option<MemoryRow>>;
/// Plan 17: every live (non-tombstoned) memory's `(id, vector)` pair under
/// `path`. Reads `embedding_blob` and decodes via `bytemuck::cast_slice`.
/// Rows whose blob is NULL or whose length disagrees with `dims * 4` are
/// surfaced as `Error::IndexCorrupt` so the index layer never silently
/// drops a vector. Used by `IndexHandle::flush` (memory layer) to rebuild
/// the usearch artefact directly from SQL — no intermediate parquet shard.
async fn list_embeddings_for_partition(
&self,
path: &PartitionPath,
dims: usize,
) -> Result<Vec<(MemoryId, Vec<f32>)>>;
/// Plan 18: read a single live memory's embedding vector from
/// `embedding_blob`. Returns `None` if the row is missing, tombstoned, or
/// has a NULL blob. `dims` is checked against `embedding_blob.len() / 4`;
/// mismatch surfaces as `Error::IndexCorrupt`. Used by `Memory::related`
/// to fetch the source memory's vector without going through the legacy
/// IndexHandle cache.
async fn get_memory_embedding(&self, id: &MemoryId, dims: usize) -> Result<Option<Vec<f32>>>;
/// Plan 18 dispatch 3 — atomic per-memory embedding swap. Updates
/// `memory.embedding_blob`, `memory.embedder_id`, `memory.updated_at`,
/// re-upserts the matching `memory_vec` row, and writes the audit
/// entry — all inside a single SQL transaction. Used by
/// [`crate::Memory::regenerate_embeddings`] so a partial run leaves
/// the catalog in a coherent state per row.
///
/// `embedding_blob` MUST be `dim * 4` little-endian bytes (the same
/// representation used by `append_memory`). The vec0 row is upserted
/// with the supplied `partition_path` and `kind` so re-targeted rows
/// land under the right scope.
///
/// Returns `Error::MemoryNotFound` if no live row exists for `id`.
async fn regenerate_memory_embedding(
&self,
id: &MemoryId,
partition_path: &PartitionPath,
kind: Option<crate::memory::MemoryKind>,
embedding_blob: &[u8],
new_embedder_id: &str,
audit: AuditEntry,
) -> Result<()>;
/// List partitions under an optional prefix. When `prefix == None`, every
/// partition row in the tenant is returned. Each `PartitionInfo` carries
/// `memory_count`: the number of *live* memories in that exact partition
/// path (not recursive). Used by tree-view callers (e.g. mind-map).
async fn list_partitions(&self, prefix: Option<&PartitionPath>) -> Result<Vec<PartitionInfo>>;
// ----- Plan 9: summaries first-class -----
/// Insert a `summary` row + write an audit entry, in one transaction.
/// Plan 18: SQLite-backed stores ALSO insert into `summary_vec` and
/// `summary_fts` inside the same transaction when the request carries
/// the index-side fields, so search hits the new summary the moment
/// the commit returns.
async fn insert_summary(&self, req: InsertSummaryRequest) -> Result<()>;
/// Look up a single summary by id (any tombstone state).
async fn get_summary(&self, id: &SummaryId) -> Result<Option<SummaryRow>>;
/// All live summaries for a subject, newest first.
async fn list_summaries_of(&self, subject: &SummarySubject) -> Result<Vec<SummaryRow>>;
/// The latest live summary for `(subject, style)`, if any.
async fn latest_summary(
&self,
subject: &SummarySubject,
style: &SummaryStyle,
) -> Result<Option<SummaryRow>>;
/// Latest live summaries for many memory subjects in one call.
///
/// Default impl loops [`Self::latest_summary`] — implementations are
/// expected to override this with a single SQL roundtrip when the
/// backend permits (the SQLite impl folds it into one statement). Used
/// by `Memory::subject_inputs` for partition-leaf summarisation: with
/// hundreds of memories per leaf the per-id loop becomes a latency
/// multiplier on every summarisation tick.
///
/// Returns a map keyed on [`MemoryId`]; missing ids are simply absent
/// from the result (no entry rather than `None`).
async fn latest_memory_summaries_batch(
&self,
memory_ids: &[crate::memory::MemoryId],
partition_paths: &[PartitionPath],
style: &SummaryStyle,
) -> Result<std::collections::BTreeMap<crate::memory::MemoryId, SummaryRow>> {
let mut out = std::collections::BTreeMap::new();
for (id, path) in memory_ids.iter().zip(partition_paths.iter()) {
let subject = SummarySubject::Memory(MemoryRef {
id: *id,
partition: path.clone(),
});
if let Some(row) = self.latest_summary(&subject, style).await? {
out.insert(*id, row);
}
}
Ok(out)
}
/// Mark `prior_id` as superseded by `new_id`. Idempotent.
async fn supersede_summary(&self, prior_id: &SummaryId, new_id: &SummaryId) -> Result<()>;
/// Soft-tombstone a summary + audit entry.
async fn delete_summary(&self, id: &SummaryId, audit: AuditEntry) -> Result<()>;
// ----- Plan 9: stale-flag plumbing -----
/// Set `summary_stale = 1` for every path in `paths`. No-op for missing paths.
async fn mark_summary_stale(&self, paths: &[PartitionPath]) -> Result<()>;
/// Clear `summary_stale` for the given path.
async fn clear_summary_stale(&self, path: &PartitionPath) -> Result<()>;
/// Set `child_index_stale = 1` for every path in `paths`.
async fn mark_child_index_stale(&self, paths: &[PartitionPath]) -> Result<()>;
// ----- Plan 9: propagation primitives -----
/// Children of `p` (one level down).
async fn children_of(&self, p: &PartitionPath) -> Result<Vec<PartitionPath>>;
/// Top-level partitions (level == 0).
async fn top_level_partitions(&self) -> Result<Vec<PartitionPath>>;
/// Whether `p` is a leaf partition.
async fn partition_is_leaf(&self, p: &PartitionPath) -> Result<bool>;
/// Subjects whose summary is currently stale, ordered deepest-first
/// (level DESC). When `scope` is `All` or `Tenant`, the tenant subject
/// is appended at the end so child rollups can run before the tenant
/// memo regenerates.
async fn subjects_needing_summary(
&self,
scope: &Scope,
style: &SummaryStyle,
) -> Result<Vec<SummarySubject>>;
// ----- Plan 10: `summary_input` denormalised -----
/// Insert a `summary_input` row. Idempotent — duplicate inserts no-op.
async fn insert_summary_input(
&self,
summary_id: &SummaryId,
input_kind: &str,
input_id: &str,
) -> Result<()>;
/// Look up which summaries cite `(input_kind, input_id)`. Used by
/// [`crate::Memory::traverse`] to walk reverse edges.
async fn summaries_citing(&self, input_kind: &str, input_id: &str) -> Result<Vec<SummaryId>>;
/// Every `summary` row regardless of subject — used by the engine-side
/// `summary_input` backfill at `Memory::open()`.
async fn list_all_summaries_for_backfill(&self) -> Result<Vec<SummaryRow>>;
// ----- Plan 10: bulk in-scope fetchers -----
/// All live links whose endpoints both fall under `scope`.
async fn links_in_subtree(&self, scope: &Scope) -> Result<Vec<crate::link::Link>>;
/// All live summaries whose subject falls under `scope`.
async fn summaries_in_subtree(&self, scope: &Scope) -> Result<Vec<SummaryRow>>;
/// All live memories whose partition falls under `scope`.
async fn memories_in_subtree(&self, scope: &Scope) -> Result<Vec<MemoryRow>>;
/// `SummaryAttach` audit rows for any partition strictly under `parent`,
/// with `seq > since`. Used by the recovery walker to replay child-summary
/// attaches into an internal-node's index.
async fn audit_attaches_for_children_since(
&self,
parent: &PartitionPath,
since: i64,
) -> Result<Vec<crate::summary::SummaryId>>;
// ----- Plan 11: typed memory attributes -----
/// Upsert one attribute on a memory row. Idempotent on `(memory_id, key)`.
/// The supplied `audit` entry is written in the same SQL transaction.
async fn set_attribute(
&self,
memory_id: &MemoryId,
key: &str,
value: &crate::attribute::AttributeValue,
audit: AuditEntry,
) -> Result<()>;
/// Read one attribute. Returns `Ok(None)` when no row exists.
async fn get_attribute(
&self,
memory_id: &MemoryId,
key: &str,
) -> Result<Option<crate::attribute::AttributeValue>>;
/// Delete one attribute. Idempotent. The supplied `audit` entry is
/// written even when the row was absent (for replay parity).
async fn clear_attribute(
&self,
memory_id: &MemoryId,
key: &str,
audit: AuditEntry,
) -> Result<()>;
/// All attributes attached to a memory, sorted by key.
async fn list_attributes(
&self,
memory_id: &MemoryId,
) -> Result<std::collections::BTreeMap<String, crate::attribute::AttributeValue>>;
/// Memory ids whose `(key, value)` matches exactly. The value's kind
/// chooses the partial index that backs the query.
async fn find_by_attribute(
&self,
key: &str,
value: &crate::attribute::AttributeValue,
) -> Result<Vec<MemoryId>>;
/// Memory ids whose attribute lies in `[min, max]`. Both ends must
/// share the same orderable kind (`Int`, `Decimal`, or `Timestamp`);
/// otherwise `Error::InvalidAttribute`.
async fn find_by_attribute_range(
&self,
key: &str,
min: &crate::attribute::AttributeValue,
max: &crate::attribute::AttributeValue,
) -> Result<Vec<MemoryId>>;
// ----- Plan 11: summary_input sub-position columns -----
/// Insert a `summary_input` row carrying optional sub-position info on
/// memory references. Idempotent — duplicate inserts no-op (PK includes
/// the range starts).
async fn insert_summary_input_with_range(
&self,
summary_id: &SummaryId,
input_kind: &str,
input_id: &str,
range: &crate::summary::content::SummaryInputRange,
) -> Result<()>;
/// Look up which summaries cite `(input_kind, input_id)` along with
/// their per-row sub-position info. Used by [`crate::Memory::references_to`].
async fn summaries_citing_with_ranges(
&self,
input_kind: &str,
input_id: &str,
) -> Result<Vec<(SummaryId, crate::summary::content::SummaryInputRange)>>;
// ----- Plan 12: snapshots + audit-seq probe -----
/// Insert a `snapshot` row + write an audit entry, in one transaction.
async fn insert_snapshot(&self, row: SnapshotRow, audit: AuditEntry) -> Result<()>;
/// All snapshot rows, newest first by `created_at`.
async fn list_snapshots(&self) -> Result<Vec<SnapshotRow>>;
/// One snapshot by id (ULID string). Returns `Ok(None)` for unknown ids.
async fn get_snapshot(&self, id: &str) -> Result<Option<SnapshotRow>>;
/// Delete a `snapshot` row + write an audit entry, in one transaction.
/// Idempotent — deleting an unknown id audits the attempt and succeeds.
/// Does **not** touch the manifest blob; `Memory::gc()` reaps orphan
/// manifests after the retention window.
async fn delete_snapshot(&self, id: &str, audit: AuditEntry) -> Result<()>;
/// Highest `audit_log.seq` currently committed. Anchors the manifest
/// at snapshot time. Returns `0` for empty stores.
async fn current_audit_seq(&self) -> Result<i64>;
// ----- Plan 12: restore primitives -----
/// Flip `memory.tombstoned` for a single row. Idempotent. Returns
/// `Error::MemoryNotFound` if no row exists. Bumps `updated_at`.
/// `Memory::restore` calls this without a per-row audit entry —
/// the restore op gets one summary audit row.
async fn set_memory_tombstone(&self, id: &MemoryId, tombstoned: bool, ts_ms: i64)
-> Result<()>;
/// Flip `summary.tombstoned` for a single row. Idempotent. Returns
/// `Error::SummaryNotFound` if no row exists.
async fn set_summary_tombstone(&self, id: &SummaryId, tombstoned: bool) -> Result<()>;
/// Insert a `(src, dst)` link (and its mirror) without writing an
/// audit entry. Idempotent. Used by `Memory::restore` to revive
/// links that were live at snapshot time. Returns `Ok(())` even
/// when an endpoint is missing — restore tolerates orphan-pair
/// inserts since the ids are already constrained by the manifest.
async fn add_link_unaudited(&self, src: &MemoryId, dst: &MemoryId, ts_ms: i64) -> Result<()>;
/// Remove a `(src, dst)` link (both directions) without writing an
/// audit entry. Idempotent.
async fn remove_link_unaudited(&self, src: &MemoryId, dst: &MemoryId) -> Result<()>;
/// Every `(memory_id, key, value)` triple in the store. Used by
/// `Memory::snapshot` to freeze the attribute set.
async fn list_all_attributes(
&self,
) -> Result<Vec<(MemoryId, String, crate::attribute::AttributeValue)>>;
/// Every memory id (live + tombstoned). Used by `Memory::restore`
/// to find rows that need un-tombstoning. Sorted ascending.
async fn list_all_memory_ids(&self) -> Result<Vec<MemoryId>>;
/// Every summary id (live + tombstoned). Used by `Memory::restore`.
async fn list_all_summary_ids(&self) -> Result<Vec<SummaryId>>;
/// Insert a `Restore` audit row. Returns the new audit seq.
async fn insert_restore_audit(&self, audit: AuditEntry) -> Result<i64>;
/// Update `memory.partition_path` and `memory.data_path` for a
/// single row. Used by `Memory::migrate_scheme` after a blob rename.
async fn relocate_memory(
&self,
id: &MemoryId,
new_partition: &PartitionPath,
new_data_path: &str,
ts_ms: i64,
) -> Result<()>;
/// Plan 15: update `memory.valid_from_ms` / `memory.valid_until_ms`
/// + write a `validity_set` audit entry, in one transaction.
/// Returns [`crate::error::Error::MemoryNotFound`] when the row is
/// absent.
async fn set_memory_validity(
&self,
id: &MemoryId,
valid_from_ms: Option<i64>,
valid_until_ms: Option<i64>,
audit: AuditEntry,
) -> Result<()>;
/// Plan 15: list live memory ids whose `kind` column matches.
/// `scope` is filtered the same way as
/// [`MetadataStore::memories_in_subtree`].
async fn find_memories_by_kind(
&self,
kind: crate::memory::MemoryKind,
scope: &Scope,
) -> Result<Vec<MemoryRow>>;
// ----- Plan 16: typed summary attributes -----
/// Upsert one attribute on a summary row + audit. Idempotent on
/// `(summary_id, key)`.
async fn set_summary_attribute(
&self,
summary_id: &SummaryId,
key: &str,
value: &crate::attribute::AttributeValue,
audit: AuditEntry,
) -> Result<()>;
/// Read one summary attribute. `Ok(None)` when no row exists.
async fn get_summary_attribute(
&self,
summary_id: &SummaryId,
key: &str,
) -> Result<Option<crate::attribute::AttributeValue>>;
/// Delete one summary attribute + audit. Idempotent — the audit row
/// is written even when the attribute was absent.
async fn clear_summary_attribute(
&self,
summary_id: &SummaryId,
key: &str,
audit: AuditEntry,
) -> Result<()>;
/// All attributes attached to a summary, sorted by key.
async fn list_summary_attributes(
&self,
summary_id: &SummaryId,
) -> Result<std::collections::BTreeMap<String, crate::attribute::AttributeValue>>;
/// Summary ids whose `(key, value)` matches exactly.
async fn find_summaries_by_attribute(
&self,
key: &str,
value: &crate::attribute::AttributeValue,
) -> Result<Vec<SummaryId>>;
// ----- Plan 16: generalized node_link -----
/// Insert a typed node→node edge + audit, in one transaction. The
/// edge is recorded once (NOT mirrored — the caller decides whether
/// to add the reverse). Idempotent on the composite primary key.
async fn add_node_link(
&self,
src: &crate::graph::NodeRef,
dst: &crate::graph::NodeRef,
kind: crate::link::LinkKind,
ts_ms: i64,
note: Option<&str>,
audit: AuditEntry,
) -> Result<()>;
/// Remove a typed node→node edge + audit. Idempotent.
async fn remove_node_link(
&self,
src: &crate::graph::NodeRef,
dst: &crate::graph::NodeRef,
kind: crate::link::LinkKind,
audit: AuditEntry,
) -> Result<()>;
/// All edges sourced at `src`, regardless of destination kind.
async fn node_links_from(&self, src: &crate::graph::NodeRef) -> Result<Vec<crate::link::Edge>>;
/// All edges targeting `dst`, regardless of source kind.
async fn node_links_to(&self, dst: &crate::graph::NodeRef) -> Result<Vec<crate::link::Edge>>;
// ----- Plan 16: atomic batch evolution -----
/// Apply a coordinated batch of catalog updates triggered by
/// `ops.trigger`. Single SQL transaction — every op succeeds or
/// every op rolls back. Returns the new audit-log seq + the
/// applied count.
async fn apply_evolution(
&self,
ops: &crate::evolve::EvolutionOps,
audit: AuditEntry,
) -> Result<crate::evolve::EvolutionApplied>;
}
/// One row of `summary` (Plan 9 — summaries first-class).
#[derive(Debug, Clone)]
pub struct SummaryRow {
/// Summary id.
pub id: SummaryId,
/// Subject kind: `"memory"`, `"partition"`, or `"tenant"`.
pub subject_kind: String,
/// Partition path (for partition / memory subjects). NULL for tenant.
pub subject_path: Option<PartitionPath>,
/// Memory id (for memory subject only).
pub subject_memory: Option<MemoryId>,
/// Persisted style tag (`"compact"` / `"detailed"` / `"custom:<n>"`).
pub style: String,
/// 1-indexed version. Latest wins.
pub version: i64,
/// Storage key for the markdown body.
pub data_path: String,
/// Body hash.
pub content_hash: ContentHash,
/// Body length in bytes.
pub bytes: i64,
/// Caller-supplied summarizer id.
pub summarizer_id: String,
/// Inputs the summarizer consumed (other subjects).
pub inputs: Vec<SummarySubject>,
/// If non-None, this summary has been superseded.
pub superseded_by: Option<SummaryId>,
/// Soft tombstone flag.
pub tombstoned: bool,
/// Created-at unix millis.
pub created_at_ms: i64,
}
/// One row of `snapshot` (Plan 12 — point-in-time snapshots).
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SnapshotRow {
/// Snapshot id (ULID string form).
pub id: String,
/// `audit_log.seq` at snapshot time.
pub seq: i64,
/// Optional caller-supplied tag.
pub tag: Option<String>,
/// Optional caller-supplied reason.
pub reason: Option<String>,
/// Storage key for the manifest JSON blob (under `metadata/`).
pub manifest_path: String,
/// Created-at unix millis.
pub created_at_ms: i64,
}
/// One row of `migration_state` (Plan 9 — runtime storage migrator).
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct MigrationStateRow {
/// Migration name (e.g. `"storage_layout_v2"`).
pub name: String,
/// Status: `"in_progress"` or `"done"`.
pub status: String,
/// Implementation-defined progress cursor.
pub cursor: Option<String>,
/// Started-at unix millis.
pub started_at_ms: i64,
/// Finished-at unix millis (set when status flips to `"done"`).
pub finished_at_ms: Option<i64>,
}
/// Snapshot of one row in the `partition` table, plus the live memory count
/// for that exact partition path. Returned by [`MetadataStore::list_partitions`]
/// and [`crate::Memory::list_partitions`].
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PartitionInfo {
/// Partition path.
pub path: PartitionPath,
/// Tree depth (root = 0).
pub level: u32,
/// `true` iff this row is a leaf (a partition that holds memories
/// directly, not just sub-partitions).
pub is_leaf: bool,
/// Created-at unix millis (when `ensure_partition_chain` first inserted
/// this row).
pub created_at_ms: i64,
/// Number of *live* (non-tombstoned) memories whose `partition_path`
/// equals `self.path` exactly. Not recursive.
pub memory_count: u64,
}