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
//! Error types for Prolly Trees
use super::cid::Cid;
use serde::{Deserialize, Serialize};
/// A mutation to apply to the tree
///
/// Represents a single operation in a batch mutation: either an upsert (insert or update)
/// or a delete operation.
///
#[derive(Clone, Debug, PartialEq)]
pub enum Mutation {
/// Insert or update a key-value pair
Upsert { key: Vec<u8>, val: Vec<u8> },
/// Delete a key
Delete { key: Vec<u8> },
}
impl Mutation {
/// Get the key for this mutation
///
pub fn key(&self) -> &[u8] {
match self {
Mutation::Upsert { key, .. } => key,
Mutation::Delete { key } => key,
}
}
/// Check if this is a delete mutation
pub fn is_delete(&self) -> bool {
matches!(self, Mutation::Delete { .. })
}
}
/// Difference between two trees
///
/// Represents a single change between a base tree and another tree.
///
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub enum Diff {
/// Entry exists in the new tree but not in the base tree
Added { key: Vec<u8>, val: Vec<u8> },
/// Entry exists in the base tree but not in the new tree
Removed { key: Vec<u8>, val: Vec<u8> },
/// Entry exists in both trees but with different values
Changed {
key: Vec<u8>,
old: Vec<u8>,
new: Vec<u8>,
},
}
impl Diff {
/// Borrow the key affected by this diff entry.
pub fn key(&self) -> &[u8] {
match self {
Diff::Added { key, .. } | Diff::Removed { key, .. } | Diff::Changed { key, .. } => key,
}
}
}
/// Merge conflict information
///
/// Contains all the information needed to resolve a conflict during a three-way merge.
///
#[derive(Clone, Debug)]
pub struct Conflict {
/// The key where the conflict occurred
pub key: Vec<u8>,
/// The value in the base tree (None if key didn't exist in base)
pub base: Option<Vec<u8>>,
/// The value in the left tree (None if the key is absent)
pub left: Option<Vec<u8>>,
/// The value in the right tree (None if the key is absent)
pub right: Option<Vec<u8>>,
}
/// Resolution for a standard three-way merge conflict.
///
/// `Value` keeps the key with the provided value, `Delete` removes the key,
/// and `Unresolved` returns [`Error::Conflict`] to the caller.
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum Resolution {
/// Keep the key with this value.
Value(Vec<u8>),
/// Delete the key from the merged tree.
Delete,
/// Leave the conflict unresolved.
Unresolved,
}
impl Resolution {
/// Resolve the conflict to a concrete value.
pub fn value(value: impl Into<Vec<u8>>) -> Self {
Self::Value(value.into())
}
/// Resolve the conflict by deleting the key.
pub fn delete() -> Self {
Self::Delete
}
/// Leave the conflict unresolved.
pub fn unresolved() -> Self {
Self::Unresolved
}
}
/// Conflict resolution strategy
///
/// A function that takes a conflict and returns an explicit resolution.
/// If [`Resolution::Unresolved`] is returned, the merge will fail with a
/// `Conflict` error.
///
///
/// # Example
/// ```
/// use prolly::{Resolution, Resolver};
///
/// // Always prefer the left value
/// let prefer_left: Resolver = Box::new(|conflict| {
/// match &conflict.left {
/// Some(value) => Resolution::value(value.clone()),
/// None => Resolution::delete(),
/// }
/// });
///
/// // Always prefer the right value
/// let prefer_right: Resolver = Box::new(|conflict| {
/// match &conflict.right {
/// Some(value) => Resolution::value(value.clone()),
/// None => Resolution::delete(),
/// }
/// });
///
/// // Concatenate values
/// let concat: Resolver = Box::new(|conflict| {
/// match (&conflict.left, &conflict.right) {
/// (Some(left), Some(right)) => {
/// let mut result = left.clone();
/// result.extend(right);
/// Resolution::value(result)
/// }
/// _ => Resolution::unresolved(),
/// }
/// });
/// ```
pub type Resolver = Box<dyn Fn(&Conflict) -> Resolution>;
/// Ready-made standard merge resolvers.
pub mod resolver {
use super::{Conflict, Resolution};
/// Prefer the left side. If left deleted the key, delete it.
pub fn prefer_left(conflict: &Conflict) -> Resolution {
match &conflict.left {
Some(value) => Resolution::value(value.clone()),
None => Resolution::delete(),
}
}
/// Prefer the right side. If right deleted the key, delete it.
pub fn prefer_right(conflict: &Conflict) -> Resolution {
match &conflict.right {
Some(value) => Resolution::value(value.clone()),
None => Resolution::delete(),
}
}
/// Delete the key when either side deleted it; otherwise leave unresolved.
pub fn delete_wins(conflict: &Conflict) -> Resolution {
if conflict.left.is_none() || conflict.right.is_none() {
Resolution::delete()
} else {
Resolution::unresolved()
}
}
/// Keep the updated side for delete/update conflicts; otherwise leave unresolved.
pub fn update_wins(conflict: &Conflict) -> Resolution {
match (&conflict.left, &conflict.right) {
(Some(value), None) | (None, Some(value)) => Resolution::value(value.clone()),
_ => Resolution::unresolved(),
}
}
}
use super::secondary_index::IndexProjection;
use super::transaction::TransactionConflict;
use super::versioned_map::MapVersionId;
/// Prolly tree errors
#[derive(Debug)]
pub enum Error {
/// Node not found in store
NotFound(Cid),
/// Invalid node structure
InvalidNode,
/// Persisted tree format parameters are invalid.
InvalidFormat(String),
/// A node was decoded under a different persisted tree format.
FormatMismatch { expected: Cid, actual: Cid },
/// One entry cannot fit below the persisted hard byte limit.
EntryTooLarge { encoded_bytes: u64, limit: u64 },
/// Deserialization failed
Deserialize(String),
/// Serialization failed
Serialize(String),
/// Storage error
Store(Box<dyn std::error::Error + Send + Sync>),
/// Stored bytes did not hash to the CID they were stored under.
CidMismatch { expected: Cid, actual: Cid },
/// Merge conflict - occurs when both trees modify the same key differently
/// and no resolver is provided or the resolver returns `Resolution::Unresolved`
///
Conflict(Conflict),
/// Mutation buffer is full - adding a mutation would exceed the buffer size limit
BufferFull,
/// A savepoint belongs to another write-session generation.
InvalidSavepoint,
/// A patch does not describe the selected immutable base.
PatchBaseMismatch,
/// A structural patch is malformed or cannot be safely applied.
InvalidStructuralPatch(String),
/// Sorted bulk loading received keys out of order.
UnsortedInput { previous: Vec<u8>, next: Vec<u8> },
/// Canonical splice received more than one mutation for a logical key.
DuplicateCanonicalMutation { key: Vec<u8> },
/// Canonical splice manager and immutable tree use different shape settings.
CanonicalSpliceConfigMismatch,
/// A GC retention policy referenced named roots that were not present.
MissingNamedRoots { names: Vec<Vec<u8>> },
/// A portable snapshot bundle is malformed or not self-contained.
InvalidSnapshotBundle(String),
/// The configured store does not support strict atomic transactions.
UnsupportedTransactions { store: &'static str },
/// A transaction could not commit because a validated named root changed.
TransactionConflict(Box<TransactionConflict>),
/// A built-in versioned-map catalog is missing or internally inconsistent.
InvalidVersionedMap(String),
/// A runtime secondary-index definition is invalid.
InvalidIndexDefinition { reason: String },
/// Persisted active index semantics have no matching runtime extractor.
IndexRuntimeDefinitionMissing { name: Vec<u8>, generation: u64 },
/// Runtime and persisted descriptor fingerprints disagree.
IndexDefinitionMismatch {
name: Vec<u8>,
persisted: Cid,
runtime: Cid,
},
/// A managed source map must be mutated through `IndexedMap`.
IndexesRequireIndexedMap {
map_id: Vec<u8>,
active_indexes: Vec<Vec<u8>>,
},
/// The requested operation has no safe indexed implementation in v1.
IndexOperationUnsupported { operation: &'static str },
/// An application extractor rejected one source record.
IndexExtractionFailed {
name: Vec<u8>,
primary_key: Vec<u8>,
reason: String,
},
/// An extractor emission is incompatible with its projection mode.
IndexProjectionMismatch {
name: Vec<u8>,
mode: IndexProjection,
primary_key: Vec<u8>,
},
/// One source record emitted different projections for the same term.
ConflictingIndexProjection {
name: Vec<u8>,
primary_key: Vec<u8>,
term: Vec<u8>,
},
/// Repeated source movement prevented index activation.
IndexBuildConflictLimitExceeded { name: Vec<u8>, attempts: usize },
/// No exact checkpoint exists for an index at the selected source version.
IndexUnavailableAtVersion {
name: Vec<u8>,
source_version: MapVersionId,
},
/// A persisted checkpoint disagrees with the selected source or index root.
IndexCheckpointMismatch {
name: Vec<u8>,
source_version: MapVersionId,
reason: String,
},
/// A cursor belongs to a different immutable indexed snapshot.
IndexCursorVersionMismatch { expected: String, actual: String },
/// Index work exceeded a configured resource bound.
IndexResourceLimitExceeded {
resource: &'static str,
limit: usize,
actual: usize,
},
/// A current indexed-snapshot bundle is malformed or inconsistent.
InvalidIndexedSnapshotBundle { reason: String },
/// A proximity-map shape configuration is invalid.
InvalidProximityConfig { reason: String },
/// Persisted proximity bytes use a format version this build does not read.
UnsupportedProximityVersion { found: u8, required: u8 },
/// A vector is incompatible with the proximity-map configuration.
InvalidProximityVector { reason: String },
/// Cosine distance cannot prepare a vector with zero Euclidean norm.
ZeroCosineVector,
/// A proximity build or mutation contains the same logical key twice.
DuplicateProximityKey { key: Vec<u8> },
/// Proximity search options are invalid.
InvalidProximitySearch { reason: String },
/// A persisted proximity record, node, or descriptor is malformed.
InvalidProximityObject { kind: &'static str, reason: String },
/// One canonical proximity node exceeds the configured hard byte limit.
ProximityNodeTooLarge {
level: u8,
entries: usize,
encoded_bytes: usize,
limit: usize,
},
/// A typed content-graph traversal exceeded a configured resource limit.
ContentGraphResourceLimitExceeded {
resource: &'static str,
limit: usize,
actual: usize,
},
}
impl Error {
pub(crate) fn transaction_conflict(conflict: TransactionConflict) -> Self {
Self::TransactionConflict(Box::new(conflict))
}
}
impl std::fmt::Display for Error {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Error::NotFound(cid) => write!(f, "node not found: {:?}", cid),
Error::InvalidNode => write!(f, "invalid node structure"),
Error::InvalidFormat(message) => write!(f, "invalid tree format: {message}"),
Error::FormatMismatch { expected, actual } => write!(
f,
"tree format mismatch: expected {:?}, got {:?}",
expected, actual
),
Error::EntryTooLarge {
encoded_bytes,
limit,
} => write!(
f,
"entry encodes to {encoded_bytes} bytes, exceeding node limit {limit}"
),
Error::Deserialize(e) => write!(f, "deserialize error: {}", e),
Error::Serialize(e) => write!(f, "serialize error: {}", e),
Error::Store(e) => write!(f, "storage error: {}", e),
Error::CidMismatch { expected, actual } => {
write!(
f,
"content CID mismatch: expected {:?}, got {:?}",
expected, actual
)
}
Error::Conflict(c) => write!(f, "merge conflict at key: {:?}", c.key),
Error::BufferFull => write!(f, "mutation buffer is full"),
Error::InvalidSavepoint => write!(f, "invalid or stale write-session savepoint"),
Error::PatchBaseMismatch => write!(f, "patch base root or values do not match"),
Error::InvalidStructuralPatch(reason) => {
write!(f, "invalid structural patch: {reason}")
}
Error::UnsortedInput { previous, next } => write!(
f,
"sorted input keys are out of order: previous={:?} next={:?}",
previous, next
),
Error::DuplicateCanonicalMutation { key } => {
write!(f, "duplicate canonical splice mutation: {key:?}")
}
Error::CanonicalSpliceConfigMismatch => {
write!(f, "canonical splice manager/tree configuration mismatch")
}
Error::MissingNamedRoots { names } => {
write!(f, "missing named roots for retention policy: {:?}", names)
}
Error::InvalidSnapshotBundle(message) => {
write!(f, "invalid snapshot bundle: {message}")
}
Error::UnsupportedTransactions { store } => {
write!(f, "store does not support strict transactions: {store}")
}
Error::TransactionConflict(conflict) => {
write!(
f,
"transaction conflict for named root: {:?}",
conflict.name
)
}
Error::InvalidVersionedMap(message) => {
write!(f, "invalid versioned map: {message}")
}
Error::InvalidIndexDefinition { reason } => {
write!(f, "invalid secondary index definition: {reason}")
}
Error::IndexRuntimeDefinitionMissing { name, generation } => write!(
f,
"runtime secondary index definition missing: name={name:?} generation={generation}"
),
Error::IndexDefinitionMismatch {
name,
persisted,
runtime,
} => write!(
f,
"secondary index definition mismatch: name={name:?} persisted={persisted:?} runtime={runtime:?}"
),
Error::IndexesRequireIndexedMap {
map_id,
active_indexes,
} => write!(
f,
"managed map requires IndexedMap coordinator: map_id={map_id:?} active_indexes={active_indexes:?}"
),
Error::IndexOperationUnsupported { operation } => {
write!(f, "indexed map operation is unsupported in v1: {operation}")
}
Error::IndexExtractionFailed {
name,
primary_key,
reason,
} => write!(
f,
"secondary index extraction failed: name={name:?} primary_key={primary_key:?}: {reason}"
),
Error::IndexProjectionMismatch {
name,
mode,
primary_key,
} => write!(
f,
"secondary index projection mismatch: name={name:?} mode={mode:?} primary_key={primary_key:?}"
),
Error::ConflictingIndexProjection {
name,
primary_key,
term,
} => write!(
f,
"conflicting secondary index projection: name={name:?} primary_key={primary_key:?} term={term:?}"
),
Error::IndexBuildConflictLimitExceeded { name, attempts } => write!(
f,
"secondary index build conflict limit exceeded: name={name:?} attempts={attempts}"
),
Error::IndexUnavailableAtVersion {
name,
source_version,
} => write!(
f,
"secondary index unavailable at source version: name={name:?} source_version={source_version}"
),
Error::IndexCheckpointMismatch {
name,
source_version,
reason,
} => write!(
f,
"secondary index checkpoint mismatch: name={name:?} source_version={source_version}: {reason}"
),
Error::IndexCursorVersionMismatch { expected, actual } => write!(
f,
"secondary index cursor snapshot mismatch: expected={expected} actual={actual}"
),
Error::IndexResourceLimitExceeded {
resource,
limit,
actual,
} => write!(
f,
"secondary index resource limit exceeded: resource={resource} limit={limit} actual={actual}"
),
Error::InvalidIndexedSnapshotBundle { reason } => {
write!(f, "invalid indexed snapshot bundle: {reason}")
}
Error::InvalidProximityConfig { reason } => {
write!(f, "invalid proximity configuration: {reason}")
}
Error::UnsupportedProximityVersion { found, required } => write!(
f,
"unsupported proximity format version: found={found} required={required}"
),
Error::InvalidProximityVector { reason } => {
write!(f, "invalid proximity vector: {reason}")
}
Error::ZeroCosineVector => write!(f, "cosine proximity vector has zero norm"),
Error::DuplicateProximityKey { key } => {
write!(f, "duplicate proximity key: {key:?}")
}
Error::InvalidProximitySearch { reason } => {
write!(f, "invalid proximity search options: {reason}")
}
Error::InvalidProximityObject { kind, reason } => {
write!(f, "invalid proximity {kind}: {reason}")
}
Error::ProximityNodeTooLarge {
level,
entries,
encoded_bytes,
limit,
} => write!(
f,
"proximity node exceeds byte limit: level={level} entries={entries} bytes={encoded_bytes} limit={limit}"
),
Error::ContentGraphResourceLimitExceeded {
resource,
limit,
actual,
} => write!(
f,
"content graph resource limit exceeded: resource={resource} limit={limit} actual={actual}"
),
}
}
}
impl std::error::Error for Error {}