Skip to main content

haematite_cli/serve/
protocol.rs

1//! Versioned request vocabulary shared by the socket service and clients.
2
3use std::io;
4
5use serde::{Deserialize, Serialize};
6use serde_json::{Value, value::RawValue};
7
8use super::storage::Failure;
9
10mod equality;
11pub use equality::IndexDeclaration;
12
13#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
14#[serde(rename_all = "snake_case")]
15pub enum CountReason {
16    Withheld,
17    CountsNotReady,
18}
19
20#[derive(Debug, Serialize)]
21pub struct KeptCount {
22    pub value: Option<u64>,
23    pub reason: Option<CountReason>,
24}
25
26impl KeptCount {
27    pub(in crate::serve) fn insert(self, answer: &mut Value, member: &str) -> Result<(), Failure> {
28        let object = answer
29            .as_object_mut()
30            .ok_or_else(|| Failure::new("answer_shape", "count answer must be an object"))?;
31        object.insert(
32            member.to_owned(),
33            serde_json::to_value(self.value).map_err(Failure::storage)?,
34        );
35        if let Some(reason) = self.reason {
36            object.insert(
37                format!("{member}_reason"),
38                serde_json::to_value(reason).map_err(Failure::storage)?,
39            );
40        }
41        Ok(())
42    }
43}
44
45#[derive(Serialize)]
46pub struct RelationTotal {
47    #[serde(flatten)]
48    pub declaration: Relation,
49    pub relationships: Option<u64>,
50    #[serde(skip_serializing_if = "Option::is_none")]
51    pub relationships_reason: Option<CountReason>,
52}
53
54#[derive(Debug, Serialize, Deserialize)]
55#[serde(deny_unknown_fields)]
56pub struct CountsRebuild {
57    pub branch: String,
58    pub request_id: String,
59}
60
61/// EMB-E2 (j): the embedded field `embed.rewalk_longer` walks again.
62#[derive(Debug, Serialize, Deserialize)]
63#[serde(deny_unknown_fields)]
64pub struct EmbedRewalk {
65    pub branch: String,
66    pub collection: String,
67    pub field: String,
68}
69
70#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize)]
71#[serde(tag = "kind", rename_all = "snake_case")]
72pub enum CountBucket {
73    Documents { collection: String },
74    Relationships { relation: String },
75    Vectors { collection: String, field: String },
76}
77
78#[derive(Debug, Serialize)]
79#[serde(tag = "result", rename_all = "snake_case")]
80pub enum AuditVerdict {
81    Agree,
82    Differ { kept: Option<u64>, counted: u64 },
83}
84
85#[derive(Debug, Serialize)]
86pub struct AuditedBucket {
87    #[serde(flatten)]
88    pub bucket: CountBucket,
89    #[serde(flatten)]
90    pub verdict: AuditVerdict,
91}
92
93#[derive(Debug, Serialize)]
94#[serde(tag = "result", rename_all = "snake_case")]
95pub enum CountsAudit {
96    Compared { buckets: Vec<AuditedBucket> },
97    CannotTell { reason: CountReason },
98}
99
100#[derive(Clone, Copy, Debug, Serialize)]
101#[serde(rename_all = "snake_case")]
102pub enum MutationOutcome {
103    NotApplied,
104    Unknown,
105}
106
107#[derive(Clone, Debug, Serialize)]
108pub struct MutationContext {
109    pub outcome: MutationOutcome,
110    pub request_id: Value,
111    pub receipt_branch: Value,
112}
113
114impl MutationContext {
115    pub(super) fn from_operation(operation: &Operation) -> Option<Self> {
116        if let Operation::CountsRebuild(request) = operation {
117            return Some(Self {
118                outcome: MutationOutcome::Unknown,
119                request_id: Value::String(request.request_id.clone()),
120                receipt_branch: Value::String(request.branch.clone()),
121            });
122        }
123        let Operation::Mutate(request) = operation else {
124            return None;
125        };
126        let branch = match &request.change {
127            Change::Fork(fork) => &fork.name,
128            _ => &request.branch,
129        };
130        Some(Self {
131            outcome: MutationOutcome::Unknown,
132            request_id: Value::String(request.request_id.clone()),
133            receipt_branch: Value::String(branch.clone()),
134        })
135    }
136}
137
138/// Mutation context survives errors after the effect, including response limits.
139#[derive(Clone, Debug, Serialize)]
140pub struct ResponseFailure {
141    #[serde(flatten)]
142    failure: Failure,
143    #[serde(flatten)]
144    mutation: Option<Box<MutationContext>>,
145}
146
147impl ResponseFailure {
148    pub(super) fn new(failure: Failure, mut mutation: Option<MutationContext>) -> Self {
149        if let Some(context) = &mut mutation {
150            context.outcome = if proves_not_applied(&failure.code) {
151                MutationOutcome::NotApplied
152            } else {
153                MutationOutcome::Unknown
154            };
155        }
156        Self {
157            failure,
158            mutation: mutation.map(Box::new),
159        }
160    }
161
162    pub(super) fn after_dispatch(failure: Failure, mutation: Option<MutationContext>) -> Self {
163        Self {
164            failure,
165            mutation: mutation.map(Box::new),
166        }
167    }
168
169    pub(super) fn mutation_context(&self) -> Option<MutationContext> {
170        self.mutation.as_deref().cloned()
171    }
172
173    pub(super) const fn failure(&self) -> &Failure {
174        &self.failure
175    }
176}
177
178impl std::ops::Deref for ResponseFailure {
179    type Target = Failure;
180    fn deref(&self) -> &Self::Target {
181        self.failure()
182    }
183}
184
185impl std::fmt::Display for ResponseFailure {
186    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
187        std::fmt::Display::fmt(&self.failure, formatter)
188    }
189}
190
191impl std::error::Error for ResponseFailure {
192    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
193        Some(&self.failure)
194    }
195}
196
197/// Refusals which establish that the requested mutation was not applied.
198///
199/// Both response context and write-client exit classification use this table.
200pub const NOT_APPLIED_REFUSALS: &[&str] = &[
201    "invalid_identifier",
202    "version_conflict",
203    "main_protected",
204    "discard_target_mismatch",
205    "request_conflict",
206    "branch_exists",
207    "collection_missing",
208    "document_missing",
209    "relation_missing",
210    "relation_collection",
211    "relation_cardinality",
212    "edge_properties",
213    "vector_dimension_missing",
214    "vector_dimension",
215    "vector_empty",
216    "vector_non_finite",
217    "schema_refused",
218    "json_text_invalid",
219    "import_empty",
220    "import_duplicate",
221    "invalid_document",
222    "import_limit",
223    "import_limit_required",
224    "sequence_exhausted",
225    "invalid_operation",
226    "busy",
227    "writer_unavailable",
228    "embed_admission_wait_exceeded",
229    "invalid_params",
230    "unknown_change_kind",
231    "hello_required",
232    "access_unavailable",
233    "write_refused",
234    "counts_corrupt",
235    "count_overflow",
236    "count_underflow",
237    "counts_merge_pending",
238    "counts_discard_pending",
239    "counts_scan_refused",
240    "index_field_not_top_level",
241    "index_already_declared",
242    "index_vector_field",
243    "index_declaration_corrupt",
244    "vector_field_embedded",
245    "vector_field_supplied",
246    "embed_model_change_not_available",
247    "embed_manifest_invalid",
248    "counts_access_refused",
249    "discard_access_refused",
250    "counts_branch_missing",
251    "append_prune_scope_components_invalid",
252    "append_prune_scope_component_invalid",
253    "append_prune_suffix_invalid",
254    "append_prune_document_id_invalid",
255    "append_prune_keep_zero",
256    "append_prune_keep_limit",
257    "append_prune_key_exists",
258    "append_prune_vector_field",
259    "append_prune_limit_overflow",
260    "append_prune_key_would_be_removed",
261    "append_prune_outgoing_edge",
262    "append_prune_incoming_edge",
263    "append_prune_revision_missing",
264    "append_prune_preparation_required",
265    "route_unclassified",
266    "permission_refused",
267    "held_for_approval",
268    "pass_missing",
269    "lys_could_not_be_asked",
270    "wrong_audience",
271    "wrong_issuer",
272    "pass_expired",
273    "pass_not_yet_valid",
274    "unpublished_key",
275    "signature_refused",
276    "pass_encoding_refused",
277    "contract_refused",
278    "rights_outside_audience",
279    "rights_truncated",
280    "unknown_method",
281    "discard_pending",
282    "branch_missing",
283    "edge_key",
284];
285
286fn proves_not_applied(code: &str) -> bool {
287    NOT_APPLIED_REFUSALS.contains(&code)
288}
289
290// Classification and typed decoding must share one vocabulary.
291macro_rules! tagged_vocabulary {
292    (
293        $(#[$enum_doc:meta])*
294        $name:ident ($tag:literal, $content:literal, $known:ident) {
295            $( $(#[$doc:meta])* $variant:ident $(($parameter:ty))? => $wire:literal, )*
296        }
297    ) => {
298        $(#[$enum_doc])*
299        #[derive(Debug, Serialize, Deserialize)]
300        #[serde(tag = $tag, content = $content, deny_unknown_fields)]
301        pub enum $name {
302            $( $(#[$doc])* #[serde(rename = $wire)] $variant $(($parameter))?, )*
303        }
304
305        impl $name {
306            /// Every wire name of this vocabulary, in declaration order; the
307            /// route matrix is checked against it.
308            pub const WIRE_NAMES: &'static [&'static str] = &[$($wire),*];
309
310            fn $known(name: &str) -> bool {
311                matches!(name, $($wire)|*)
312            }
313        }
314    };
315}
316
317/// Protocol identifier negotiated before issuing requests.
318pub const PROTOCOL: &str = "haem/1";
319pub const RECEIPT_CAPABILITY: &str = "typed_receipt_fragments";
320
321/// A correlation identifier belongs to one connection, not durable history.
322#[derive(Debug, Serialize, Deserialize)]
323#[serde(deny_unknown_fields)]
324pub struct Request {
325    pub id: String,
326    pub operation: Operation,
327}
328
329tagged_vocabulary! {
330    /// Reads select a captured version; mutations have a separate stable identity.
331    Operation ("method", "params", known_method) {
332        Hello(HelloAgreement) => "hello",
333        /// Presents the connection's Lys pass; every later request on the
334        /// connection is decided from it (ACCESS-001 R2). Answers
335        /// `{holder, expires}` once the pass verifies.
336        Present(PassPresent) => "session.pass",
337        /// Runs one page of the drafts Lys approved for this database, with
338        /// the connection's pass as the connector's: each is applied exactly
339        /// once and acknowledged (ACCESS-001 R2). Answers `{acknowledged,
340        /// next}`; nothing polls or retries on its own.
341        DraftsRun(DraftsRun) => "drafts.run",
342        Branches(BranchPage) => "branches.list",
343        Collections(CollectionPage) => "collections.list",
344        Get(DocumentRead) => "documents.get",
345        List(DocumentPage) => "documents.list",
346        History(HistoryPage) => "history.list",
347        /// Answers a stream of frames, all with the request's id: the
348        /// branch's head `{branch, version}` at once, then one each time its
349        /// published version changes, ended by a refusal: `pass_expired` when
350        /// the connection's pass expires, `branch_unknown` when the branch is
351        /// gone. Pushed by the publish itself; nothing polls (serve/follow.rs).
352        Follow(HeadFollow) => "head.follow",
353        Review(ReviewPage) => "branch.review",
354        /// Answers partial owner-key counts; sum pages until `done` is true.
355        ReviewCounts(ReviewPage) => "branch.review_counts",
356        /// Answers pinned three-way owner lines without admitting a mutation.
357        MergePrepare(ReviewPage) => "branch.merge_prepare",
358        /// Answers merge endpoint findings without choosing a conflict winner.
359        MergeDangling(ReviewPage) => "branch.merge_dangling",
360        Mutate(Mutation) => "mutation.apply",
361        Receipt(ReceiptQuery) => "mutation.receipt",
362        /// Recounts visible rows atomically; a repeated request replays its receipt.
363        CountsRebuild(CountsRebuild) => "counts.rebuild",
364        /// Compares kept counts with visible rows without changing the selected version.
365        CountsAudit(Selection) => "counts.audit",
366        /// Answers `{selection, edges: [Edge], next_after}`: edges leaving `node`.
367        EdgesOut(EdgePage) => "edge.out",
368        /// Answers `{selection, edges: [Edge], next_after}`: edges arriving at `node`.
369        EdgesIn(EdgePage) => "edge.in",
370        /// Answers `{selection, edges: [Edge], next_after}`: every edge of the version.
371        GraphExport(GraphPage) => "graph.export",
372        /// Answers `{selection, vector}`; `vector` is null when none is stored.
373        VectorGet(VectorRead) => "vectors.get",
374        /// Answers `{selection, vectors: [{document_id, vector}], next_after}`.
375        VectorList(VectorPage) => "vectors.list",
376        /// Answers `{selection, hits: [{document_id, score}], scanned}`.
377        VectorSearch(VectorSearch) => "vectors.search",
378        /// Answers `{selection, hits: [{document_id, score}], compared, stale,
379        /// coverage}`: `text` embedded as a question, then the field searched.
380        VectorSearchText(VectorSearchText) => "vectors.search_text",
381        StoreStats(StoreRead) => "store.stats",
382        VectorFields(StoreRead) => "vectors.fields",
383        /// Answers `{branch, collection, field, through}`: opens one walk of
384        /// the field toward `through` for the texts it passed as longer than
385        /// the model reads (EMB-E2 (j)).
386        EmbedRewalk(EmbedRewalk) => "embed.rewalk_longer",
387        /// Answers `{selection, relations: [Relation]}` at the captured version.
388        Relations(RelationsRead) => "relations.list",
389        /// Answers `{selection, rows: [{value, origin}], next, engine, reading,
390        /// collection, records_read}`: one page of a query's rows at one captured
391        /// moment; `collection` is the one a pipeline read, else null.
392        QueryRun(QueryRequest) => "query.run",
393        QueryCheck(crate::serve::query::check::CheckRequest) => "query.check",
394        QueryWords(crate::serve::query::check::WordsRequest) => "query.words",
395        /// Answers `{from, to, rows: [{kind, origin, before, after}], next,
396        /// changes_read}`: the documents changed between two versions.
397        ChangesList(crate::serve::changes::ChangesRequest) => "changes.list",
398    }
399}
400
401/// A Lys pass for audience haematite, as Lys issued it.
402#[derive(Debug, Serialize, Deserialize)]
403#[serde(deny_unknown_fields)]
404pub struct PassPresent {
405    pub pass: String,
406}
407
408/// The approved page to run: the first, or the one after `after`.
409#[derive(Debug, Serialize, Deserialize)]
410#[serde(deny_unknown_fields)]
411pub struct DraftsRun {
412    #[serde(default)]
413    pub after: Option<String>,
414}
415
416/// A query's text and the page its answer may fill.
417#[derive(Debug, Serialize, Deserialize)]
418#[serde(deny_unknown_fields)]
419pub struct QueryRequest {
420    pub selection: Selection,
421    pub query: String,
422    /// Bound as jq variables beside the text, never pasted into it.
423    #[serde(default)]
424    pub parameters: std::collections::BTreeMap<String, Box<RawValue>>,
425    pub page: QueryPage,
426    #[serde(default)]
427    pub continuation: Option<String>,
428    /// `"origins"` leaves each row's value out; absent is `"values"`.
429    #[serde(default)]
430    pub rows: crate::serve::query::Rows,
431}
432
433/// At most `rows` rows, and at most `bytes` bytes for the whole answer.
434#[derive(Debug, Serialize, Deserialize)]
435#[serde(deny_unknown_fields)]
436pub struct QueryPage {
437    pub rows: std::num::NonZeroU64,
438    pub bytes: std::num::NonZeroU64,
439}
440
441/// Decimal strings avoid loss of nanosecond identities in browser JSON numbers.
442#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
443#[serde(deny_unknown_fields)]
444pub struct Version {
445    #[serde(with = "decimal")]
446    pub generation: u64,
447    #[serde(with = "decimal")]
448    pub sequence: u64,
449}
450
451/// An omitted version requests a fresh capture of the current branch head.
452#[derive(Debug, Serialize, Deserialize)]
453#[serde(deny_unknown_fields)]
454pub struct Selection {
455    pub branch: String,
456    pub version: Option<Version>,
457}
458
459#[derive(Debug, Serialize, Deserialize)]
460#[serde(deny_unknown_fields)]
461pub struct BranchPage {
462    pub after: Option<String>,
463    pub limit: std::num::NonZeroU32,
464}
465
466#[derive(Debug, Serialize, Deserialize)]
467#[serde(deny_unknown_fields)]
468pub struct CollectionPage {
469    pub selection: Selection,
470    pub after: Option<String>,
471    pub limit: std::num::NonZeroU32,
472}
473
474#[derive(Debug, Serialize, Deserialize)]
475#[serde(deny_unknown_fields)]
476pub struct DocumentRead {
477    pub selection: Selection,
478    pub collection: String,
479    pub document_id: String,
480}
481
482/// Continuations carry the response's captured version, never a fresh head.
483#[derive(Debug, Serialize, Deserialize)]
484#[serde(deny_unknown_fields)]
485pub struct DocumentPage {
486    pub selection: Selection,
487    pub collection: String,
488    #[serde(default, skip_serializing_if = "Option::is_none")]
489    pub prefix: Option<String>,
490    #[serde(default, skip_serializing_if = "Option::is_none")]
491    pub content: Option<String>,
492    pub after: Option<String>,
493    pub limit: std::num::NonZeroU32,
494}
495
496#[derive(Debug, Serialize, Deserialize)]
497#[serde(deny_unknown_fields)]
498pub struct HistoryPage {
499    pub branch: String,
500    pub after: Option<Version>,
501    pub limit: std::num::NonZeroU32,
502}
503
504/// The branch whose head a connection follows.
505#[derive(Debug, Serialize, Deserialize)]
506#[serde(deny_unknown_fields)]
507pub struct HeadFollow {
508    pub branch: String,
509}
510
511/// The continuation retains the review's captured versions on every page.
512#[derive(Debug, Serialize, Deserialize)]
513#[serde(deny_unknown_fields)]
514pub struct ReviewPage {
515    pub branch: String,
516    pub after: Option<String>,
517    pub limit: std::num::NonZeroU32,
518}
519
520/// Query the current named branch; the response identifies its generation.
521///
522/// A fork's receipt is queried using the destination name, so a client need
523/// not have received the new generation identifier before reconciling it.
524#[derive(Debug, Serialize, Deserialize)]
525#[serde(deny_unknown_fields)]
526pub struct ReceiptQuery {
527    pub branch: String,
528    pub request_id: String,
529}
530
531/// The service checks an existing receipt before refusing a stale expectation.
532#[derive(Debug, Serialize, Deserialize)]
533#[serde(deny_unknown_fields)]
534pub struct Mutation {
535    pub request_id: String,
536    pub branch: String,
537    pub expected: Version,
538    pub change: Change,
539}
540
541#[derive(Debug, Serialize, Deserialize)]
542#[serde(deny_unknown_fields)]
543pub struct DocumentAppendPrune {
544    pub collection: String,
545    pub scope: Vec<String>,
546    pub suffix: String,
547    pub document: Box<RawValue>,
548    pub keep: u32,
549}
550
551#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
552#[serde(deny_unknown_fields)]
553pub struct RetentionResult {
554    pub document_id: String,
555    pub removed: u32,
556    pub more: bool,
557}
558
559tagged_vocabulary! {
560    /// Collection shapes, documents, edges and vectors share the branch commit
561    /// boundary.
562    ///
563    /// Deleting a document also removes, in the same version, every
564    /// edge that leaves or arrives at it and every vector stored for it.
565    Change ("kind", "value", known_kind) {
566        Collection(CollectionWrite) => "collection.put",
567        Put(DocumentWrite) => "document.put",
568        AppendPrune(DocumentAppendPrune) => "document.append_prune",
569        Delete(DocumentDelete) => "document.delete",
570        DeleteMany(DocumentDeleteMany) => "documents.delete_many",
571        Fork(Fork) => "branch.fork",
572        Discard(BranchDiscard) => "branch.discard",
573        /// The branch `from`, at the version its review read, merged into
574        /// the mutation's branch (serve/storage/merges.rs).
575        Merge(BranchMerge) => "branch.merge",
576        EdgePut(Edge) => "edge.put",
577        EdgeDelete(EdgeDelete) => "edge.delete",
578        IndexDeclare(IndexDeclaration) => "index.declare",
579        VectorDeclare(VectorDeclaration) => "vector.declare",
580        VectorPut(VectorWrite) => "vector.put",
581        VectorPutMany(VectorImport) => "vectors.put_many",
582        VectorDelete(VectorDelete) => "vector.delete",
583        PutMany(DocumentImport) => "documents.put_many",
584        /// Every edge checked as `edge.put` checks it, and each relation's
585        /// cardinality across the batch too; all or none, in one commit.
586        EdgePutMany(EdgeImport) => "edges.put_many",
587        RelationPut(Relation) => "relation.put",
588        RelationDelete(RelationDelete) => "relation.delete",
589    }
590}
591
592pub(super) enum DecodedRequest {
593    Request(Request),
594    Refused {
595        id: String,
596        failure: ResponseFailure,
597    },
598}
599
600#[derive(Deserialize)]
601struct Correlation {
602    id: String,
603}
604
605#[derive(Deserialize)]
606struct Envelope<'a> {
607    #[serde(borrow)]
608    operation: Option<&'a RawValue>,
609}
610
611#[derive(Deserialize)]
612struct OperationProbe<'a> {
613    method: Option<String>,
614    #[serde(borrow)]
615    params: Option<&'a RawValue>,
616}
617
618#[derive(Deserialize)]
619struct MutationProbe<'a> {
620    request_id: Option<Value>,
621    branch: Option<Value>,
622    #[serde(borrow)]
623    change: Option<&'a RawValue>,
624}
625
626#[derive(Deserialize)]
627struct ChangeProbe {
628    kind: Option<String>,
629}
630
631#[derive(Deserialize)]
632struct ReceiptBranchProbe<'a> {
633    kind: Option<String>,
634    #[serde(borrow)]
635    value: Option<&'a RawValue>,
636}
637
638#[derive(Deserialize)]
639struct ForkNameProbe {
640    name: Option<Value>,
641}
642
643pub(super) fn decode_request(bytes: &[u8]) -> io::Result<DecodedRequest> {
644    if let Ok(request) = serde_json::from_slice::<Request>(bytes) {
645        validate_id(&request.id)?;
646        return Ok(DecodedRequest::Request(request));
647    }
648    let correlation: Correlation = serde_json::from_slice(bytes)
649        .map_err(|error| io::Error::new(io::ErrorKind::InvalidData, error))?;
650    validate_id(&correlation.id)?;
651    let failure = serde_json::from_slice::<Envelope<'_>>(bytes).map_or_else(
652        |_| invalid_params(),
653        |envelope| classify_refusal(envelope.operation),
654    );
655    Ok(DecodedRequest::Refused {
656        id: correlation.id,
657        failure: ResponseFailure::new(failure, malformed_mutation_context(bytes)),
658    })
659}
660
661fn malformed_mutation_context(bytes: &[u8]) -> Option<MutationContext> {
662    let envelope = serde_json::from_slice::<Envelope<'_>>(bytes).ok()?;
663    let operation = serde_json::from_str::<OperationProbe<'_>>(envelope.operation?.get()).ok()?;
664    let rebuild = operation.method.as_deref() == Some("counts.rebuild");
665    if !rebuild && operation.method.as_deref() != Some("mutation.apply") {
666        return None;
667    }
668    let mutation = operation
669        .params
670        .and_then(|params| serde_json::from_str::<MutationProbe<'_>>(params.get()).ok());
671    let (request_id, mut receipt_branch, change) = match mutation {
672        Some(mutation) => (
673            mutation.request_id.unwrap_or(Value::Null),
674            mutation.branch.unwrap_or(Value::Null),
675            mutation.change,
676        ),
677        None => (Value::Null, Value::Null, None),
678    };
679    if !rebuild
680        && let Some(change) = change
681        && let Ok(change) = serde_json::from_str::<ReceiptBranchProbe<'_>>(change.get())
682        && change.kind.as_deref() == Some("branch.fork")
683    {
684        receipt_branch = change
685            .value
686            .and_then(|value| serde_json::from_str::<ForkNameProbe>(value.get()).ok())
687            .and_then(|fork| fork.name)
688            .unwrap_or(Value::Null);
689    }
690    Some(MutationContext {
691        outcome: MutationOutcome::NotApplied,
692        request_id,
693        receipt_branch,
694    })
695}
696
697fn validate_id(id: &str) -> io::Result<()> {
698    if id.is_empty() || id.len() > 512 {
699        return Err(io::Error::new(
700            io::ErrorKind::InvalidData,
701            "invalid correlation ID",
702        ));
703    }
704    Ok(())
705}
706
707fn classify_refusal(operation: Option<&RawValue>) -> Failure {
708    let Some(operation) = operation else {
709        return invalid_params();
710    };
711    let Ok(operation) = serde_json::from_str::<OperationProbe<'_>>(operation.get()) else {
712        return invalid_params();
713    };
714    let Some(method) = operation.method else {
715        return invalid_params();
716    };
717    if !Operation::known_method(&method) {
718        return Failure::new("unknown_method", "method is not in the served vocabulary");
719    }
720    if method == "hello" {
721        return Failure::new(
722            "typed_receipt_capability_missing",
723            format!("hello must agree {RECEIPT_CAPABILITY} and {PROTOCOL}"),
724        );
725    }
726    if method == "mutation.apply"
727        && let Some(params) = operation.params
728        && let Ok(mutation) = serde_json::from_str::<MutationProbe<'_>>(params.get())
729        && let Some(change) = mutation.change
730        && let Ok(change) = serde_json::from_str::<ChangeProbe>(change.get())
731        && let Some(kind) = change.kind
732        && !Change::known_kind(&kind)
733    {
734        return Failure::new(
735            "unknown_change_kind",
736            "change kind is not in the served vocabulary",
737        );
738    }
739    invalid_params()
740}
741
742fn invalid_params() -> Failure {
743    Failure::new(
744        "invalid_params",
745        "request does not match the operation shape",
746    )
747}
748
749#[derive(Debug, Serialize, Deserialize)]
750#[serde(deny_unknown_fields)]
751pub struct BranchDiscard {
752    pub name: String,
753}
754
755#[derive(Debug, Serialize, Deserialize)]
756#[serde(deny_unknown_fields)]
757pub struct BranchMerge {
758    /// The branch merged in, which is left as it is.
759    pub from: String,
760    /// The version of `from` its review read; a merge of any other is refused.
761    pub reviewed: Version,
762}
763
764/// A declaration constrains every edge under its name at this branch version.
765#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
766#[serde(deny_unknown_fields)]
767pub struct Relation {
768    pub relation: String,
769    pub from_collection: String,
770    pub to_collection: String,
771    pub cardinality: Cardinality,
772}
773
774#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
775#[serde(rename_all = "snake_case")]
776pub enum Cardinality {
777    OneToOne,
778    OneToMany,
779    ManyToMany,
780}
781
782#[derive(Debug, Serialize, Deserialize)]
783#[serde(deny_unknown_fields)]
784pub struct RelationDelete {
785    pub relation: String,
786}
787
788#[derive(Debug, Serialize, Deserialize)]
789#[serde(deny_unknown_fields)]
790pub struct RelationsRead {
791    pub selection: Selection,
792}
793
794#[derive(Debug, Serialize, Deserialize)]
795#[serde(deny_unknown_fields)]
796pub struct CollectionWrite {
797    pub collection: String,
798    /// None explicitly removes a prior schema. JSON false remains a schema.
799    #[serde(deserialize_with = "explicit_schema")]
800    pub schema: Option<Value>,
801}
802
803#[derive(Debug, Serialize, Deserialize)]
804#[serde(deny_unknown_fields)]
805pub struct DocumentWrite {
806    pub collection: String,
807    pub document_id: String,
808    /// Keep the submitted JSON bytes; parse a separate view for validation.
809    pub document: Box<RawValue>,
810}
811
812/// Every row is validated before the import advances one branch version.
813#[derive(Debug, Serialize, Deserialize)]
814#[serde(deny_unknown_fields)]
815pub struct DocumentImport {
816    pub collection: String,
817    pub documents: Vec<ImportedDocument>,
818}
819
820#[derive(Debug, Serialize, Deserialize)]
821#[serde(deny_unknown_fields)]
822pub struct ImportedDocument {
823    pub document_id: String,
824    pub document: Box<RawValue>,
825}
826
827#[derive(Debug, Serialize, Deserialize)]
828#[serde(deny_unknown_fields)]
829pub struct StoreRead {
830    pub selection: Selection,
831}
832
833#[derive(Debug, Serialize, Deserialize)]
834#[serde(deny_unknown_fields)]
835pub struct DocumentDelete {
836    pub collection: String,
837    pub document_id: String,
838}
839
840/// Every identifier must exist in the collection before any cascade is staged.
841#[derive(Debug, Serialize, Deserialize)]
842#[serde(deny_unknown_fields)]
843pub struct DocumentDeleteMany {
844    pub collection: String,
845    pub document_ids: Vec<String>,
846}
847
848#[derive(Debug, Serialize, Deserialize)]
849#[serde(deny_unknown_fields)]
850pub struct Fork {
851    pub name: String,
852    /// Must be a retained version of the mutation's source branch.
853    pub from: Version,
854}
855
856/// One end of an edge: a document named by its collection and identifier.
857#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
858#[serde(deny_unknown_fields)]
859pub struct Node {
860    pub collection: String,
861    pub document_id: String,
862}
863
864/// An edge is identified by its relation and both ends; putting the same
865/// three again replaces its properties. Both ends must exist at the write.
866#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
867#[serde(deny_unknown_fields)]
868pub struct Edge {
869    pub relation: String,
870    pub from: Node,
871    pub to: Node,
872    /// A JSON object; any other JSON value is refused.
873    pub properties: Value,
874}
875
876/// Edges written together (REL0), counted against the same row bound as
877/// `documents.put_many`.
878#[derive(Debug, Serialize, Deserialize)]
879#[serde(deny_unknown_fields)]
880pub struct EdgeImport {
881    pub edges: Vec<Edge>,
882}
883
884#[derive(Debug, Serialize, Deserialize)]
885#[serde(deny_unknown_fields)]
886pub struct EdgeDelete {
887    pub relation: String,
888    pub from: Node,
889    pub to: Node,
890}
891
892/// Edges at one node, in key order. An omitted relation selects every
893/// relation. `after` is the previous answer's `next_after`, unchanged, and
894/// requires the captured version.
895#[derive(Debug, Serialize, Deserialize)]
896#[serde(deny_unknown_fields)]
897pub struct EdgePage {
898    pub selection: Selection,
899    pub node: Node,
900    pub relation: Option<String>,
901    pub after: Option<String>,
902    pub limit: std::num::NonZeroU32,
903}
904
905/// Every edge of the captured version, each exactly once, in key order.
906#[derive(Debug, Serialize, Deserialize)]
907#[serde(deny_unknown_fields)]
908pub struct GraphPage {
909    pub selection: Selection,
910    pub after: Option<String>,
911    pub limit: std::num::NonZeroU32,
912    /// GX1: only these relations; an empty list answers no edges. Each must
913    /// be declared.
914    #[serde(default, skip_serializing_if = "Option::is_none")]
915    pub relations: Option<Vec<String>>,
916    /// GX1: only edges with both ends in these collections.
917    #[serde(default, skip_serializing_if = "Option::is_none")]
918    pub collections: Option<Vec<String>>,
919    /// GX1: for each collection, the members whose stored JSON text each
920    /// record at an edge's end carries in `nodes`.
921    #[serde(default, skip_serializing_if = "Option::is_none")]
922    pub members: Option<std::collections::BTreeMap<String, Vec<String>>>,
923    /// GX2: only edges with both ends among these records, at most the
924    /// examination bound (`max_page × 64`) of them and their JSON within the
925    /// frame (`GX2c`). Each must exist.
926    #[serde(default, skip_serializing_if = "Option::is_none")]
927    pub records: Option<Vec<Node>>,
928    /// GR1: beside the edges, records of each chosen collection (those of
929    /// `collections`, or every collection) as nodes, whether or not any edge
930    /// names them, with the members named in `members`, never a body.
931    #[serde(default, skip_serializing_if = "Option::is_none")]
932    pub unrelated: Option<Unrelated>,
933}
934
935/// GR1: how many records with no edge asked for to draw.
936#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
937#[serde(deny_unknown_fields)]
938pub struct Unrelated {
939    /// At most this many records of each chosen collection.
940    pub per_collection: std::num::NonZeroU64,
941    /// At most this many records in the whole paged series.
942    pub most: std::num::NonZeroU64,
943}
944
945/// A field declaration shares the collection mutation and receipt boundary.
946#[derive(Debug, Serialize, Deserialize)]
947#[serde(deny_unknown_fields)]
948pub struct VectorDeclaration {
949    pub collection: String,
950    pub field: String,
951    pub declaration: super::storage::embedded::Declaration,
952}
953
954/// A vector is stored for a named field of an existing document.
955///
956/// The first
957/// write for a collection and field fixes its dimension; a later vector of
958/// another dimension, an empty vector and a non-finite member are refused.
959#[derive(Debug, Serialize, Deserialize)]
960#[serde(deny_unknown_fields)]
961pub struct VectorWrite {
962    pub collection: String,
963    pub document_id: String,
964    pub field: String,
965    pub vector: Vec<f32>,
966}
967
968/// Supplied fields must already have a recorded dimension. Every row is
969/// validated before the batch advances one branch version.
970#[derive(Debug, Serialize, Deserialize)]
971#[serde(deny_unknown_fields)]
972pub struct VectorImport {
973    pub vectors: Vec<VectorWrite>,
974}
975
976#[derive(Debug, Serialize, Deserialize)]
977#[serde(deny_unknown_fields)]
978pub struct VectorDelete {
979    pub collection: String,
980    pub document_id: String,
981    pub field: String,
982}
983
984#[derive(Debug, Serialize, Deserialize)]
985#[serde(deny_unknown_fields)]
986pub struct VectorRead {
987    pub selection: Selection,
988    pub collection: String,
989    pub document_id: String,
990    pub field: String,
991}
992
993/// Every vector of one field, by document identifier. `after` is the last
994/// document identifier of the previous page and requires the captured version.
995#[derive(Debug, Serialize, Deserialize)]
996#[serde(deny_unknown_fields)]
997pub struct VectorPage {
998    pub selection: Selection,
999    pub collection: String,
1000    pub field: String,
1001    pub after: Option<String>,
1002    pub limit: std::num::NonZeroU32,
1003}
1004
1005/// Exact search: every vector of the field is compared with `query` and the
1006/// answer's `scanned` says how many were. `k` is bounded by the page limit.
1007#[derive(Debug, Serialize, Deserialize)]
1008#[serde(deny_unknown_fields)]
1009pub struct VectorSearch {
1010    pub selection: Selection,
1011    pub collection: String,
1012    pub field: String,
1013    pub query: Vec<f32>,
1014    pub k: std::num::NonZeroU32,
1015    pub metric: Metric,
1016}
1017
1018/// Text search (EW1): `text` is embedded by the service's worker, outside any
1019/// lock, and the field's embedded vectors searched with it under the field's
1020/// declared metric. `k` is bounded by the page limit.
1021#[derive(Debug, Serialize, Deserialize)]
1022#[serde(deny_unknown_fields)]
1023pub struct VectorSearchText {
1024    pub selection: Selection,
1025    pub collection: String,
1026    pub field: String,
1027    pub text: String,
1028    pub k: std::num::NonZeroU32,
1029}
1030
1031/// Hits are ordered best first: highest score for cosine and dot, lowest
1032/// distance for l2; equal scores order by document identifier.
1033#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1034#[serde(rename_all = "lowercase")]
1035pub enum Metric {
1036    Cosine,
1037    L2,
1038    Dot,
1039}
1040
1041mod decimal {
1042    use serde::{Deserialize, Deserializer, Serializer, de::Error};
1043
1044    pub fn serialize<S: Serializer>(
1045        value: impl std::fmt::Display,
1046        serializer: S,
1047    ) -> Result<S::Ok, S::Error> {
1048        serializer.serialize_str(&value.to_string())
1049    }
1050
1051    pub fn deserialize<'de, D: Deserializer<'de>>(deserializer: D) -> Result<u64, D::Error> {
1052        let text = String::deserialize(deserializer)?;
1053        let value = text.parse::<u64>().map_err(D::Error::custom)?;
1054        if text != value.to_string() {
1055            return Err(D::Error::custom(
1056                "expected a canonical unsigned decimal string",
1057            ));
1058        }
1059        Ok(value)
1060    }
1061}
1062
1063// A custom deserializer makes absence an error while retaining explicit null.
1064fn explicit_schema<'de, D: serde::Deserializer<'de>>(
1065    deserializer: D,
1066) -> Result<Option<Value>, D::Error> {
1067    Option::<Value>::deserialize(deserializer)
1068}
1069
1070#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
1071pub enum WireOwnerKind {
1072    Database,
1073    Api,
1074    LeafLog,
1075    Observer,
1076    Branch,
1077    BranchCommit,
1078    BranchRef,
1079}
1080
1081#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
1082#[serde(deny_unknown_fields)]
1083pub struct WireOwnedCause {
1084    pub kind: WireOwnerKind,
1085    pub message: String,
1086    pub database: Option<super::receipt_wire::WireCause>,
1087}
1088
1089#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
1090#[serde(deny_unknown_fields)]
1091pub struct WireRefusal {
1092    pub code: String,
1093    pub message: String,
1094    pub path: Option<String>,
1095    pub cause: Option<WireOwnedCause>,
1096}
1097
1098#[derive(Debug, Serialize, Deserialize)]
1099#[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)]
1100pub enum WirePacket {
1101    Receipt {
1102        receipt: Box<super::receipt_wire::WireReceipt>,
1103        refusal: Option<WireRefusal>,
1104    },
1105    Refusal {
1106        refusal: WireRefusal,
1107    },
1108}
1109impl WirePacket {
1110    #[must_use]
1111    pub fn receipt(&self) -> Option<&super::receipt_wire::WireReceipt> {
1112        match self {
1113            Self::Receipt { receipt, .. } => Some(receipt.as_ref()),
1114            Self::Refusal { .. } => None,
1115        }
1116    }
1117    #[must_use]
1118    pub const fn refusal(&self) -> Option<&WireRefusal> {
1119        match self {
1120            Self::Receipt { refusal, .. } => refusal.as_ref(),
1121            Self::Refusal { refusal } => Some(refusal),
1122        }
1123    }
1124}
1125
1126#[derive(Clone, Debug, Serialize, Deserialize)]
1127#[serde(deny_unknown_fields)]
1128pub struct HelloAgreement {
1129    pub protocol: String,
1130    pub capabilities: Vec<String>,
1131    pub max_frame: std::num::NonZeroU32,
1132}
1133impl HelloAgreement {
1134    pub(crate) fn client() -> Self {
1135        Self {
1136            protocol: PROTOCOL.to_owned(),
1137            capabilities: vec![RECEIPT_CAPABILITY.to_owned()],
1138            max_frame: std::num::NonZeroU32::MAX,
1139        }
1140    }
1141    pub(crate) fn validate(
1142        &self,
1143        server_cap: std::num::NonZeroU32,
1144    ) -> Result<std::num::NonZeroU32, Failure> {
1145        if self.protocol != PROTOCOL {
1146            return Err(Failure::new(
1147                "receipt_protocol_mismatch",
1148                format!("expected {PROTOCOL}, received {}", self.protocol),
1149            ));
1150        }
1151        if !self
1152            .capabilities
1153            .iter()
1154            .any(|capability| capability == RECEIPT_CAPABILITY)
1155        {
1156            return Err(Failure::new(
1157                "typed_receipt_capability_missing",
1158                format!("hello must agree {RECEIPT_CAPABILITY}"),
1159            ));
1160        }
1161        let cap = self.max_frame.min(server_cap);
1162        let minimum = super::receipt_fragments::minimum_cap().map_err(Failure::storage)?;
1163        if u64::from(cap.get()) < u64::try_from(minimum).map_err(Failure::storage)? {
1164            return Err(Failure::new(
1165                "receipt_frame_too_small",
1166                format!("offered {}, minimum {minimum}", cap.get()),
1167            ));
1168        }
1169        Ok(cap)
1170    }
1171}
1172
1173#[cfg(test)]
1174mod receipt_agreement_tests {
1175    use super::*;
1176
1177    type TestResult = Result<(), Box<dyn std::error::Error>>;
1178
1179    #[test]
1180    fn missing_capability_and_derived_floor_are_named_before_dispatch() -> TestResult {
1181        let mut agreement = HelloAgreement::client();
1182        agreement.capabilities.clear();
1183        let cap = std::num::NonZeroU32::MAX;
1184        let Err(missing) = agreement.validate(cap) else {
1185            return Err("a hello without the receipt capability was agreed".into());
1186        };
1187        assert_eq!(missing.code, "typed_receipt_capability_missing");
1188        agreement.capabilities.push(RECEIPT_CAPABILITY.to_owned());
1189        let minimum = super::super::receipt_fragments::minimum_cap()?;
1190        let below = u32::try_from(minimum - 1)?;
1191        agreement.max_frame = std::num::NonZeroU32::new(below).ok_or("floor is one")?;
1192        let Err(error) = agreement.validate(cap) else {
1193            return Err("a frame below the receipt floor was agreed".into());
1194        };
1195        assert_eq!(error.code, "receipt_frame_too_small");
1196        assert!(error.message.contains(&minimum.to_string()));
1197        assert!(error.message.contains(&agreement.max_frame.to_string()));
1198        Ok(())
1199    }
1200}