Skip to main content

nodedb_types/
protocol.rs

1//! Native binary wire protocol types.
2//!
3//! Shared between the server (`nodedb`) and the client (`nodedb-client`).
4//! All types derive `Serialize`/`Deserialize` for MessagePack encoding.
5
6use serde::{Deserialize, Serialize};
7
8use crate::value::Value;
9
10// ─── Operation Codes ────────────────────────────────────────────────
11
12/// Operation codes for the native binary protocol.
13///
14/// Encoded as a single `u8` in both the MessagePack frame and JSON frame
15/// (e.g. `{"op":3}` for `Status`). The `#[serde(try_from = "u8", into = "u8")]`
16/// attribute makes JSON encoding consistent with the numeric opcode values.
17#[repr(u8)]
18#[derive(
19    Debug,
20    Clone,
21    Copy,
22    PartialEq,
23    Eq,
24    Hash,
25    Serialize,
26    Deserialize,
27    zerompk::ToMessagePack,
28    zerompk::FromMessagePack,
29)]
30#[serde(try_from = "u8", into = "u8")]
31#[msgpack(c_enum)]
32pub enum OpCode {
33    // ── Auth & session ──────────────────────────────────────────
34    Auth = 0x01,
35    Ping = 0x02,
36    /// Report startup/readiness status. Returns the current startup phase
37    /// and whether the node is healthy. Does not require authentication.
38    Status = 0x03,
39
40    // ── Data operations (direct Data Plane dispatch) ────────────
41    PointGet = 0x10,
42    PointPut = 0x11,
43    PointDelete = 0x12,
44    VectorSearch = 0x13,
45    RangeScan = 0x14,
46    CrdtRead = 0x15,
47    CrdtApply = 0x16,
48    GraphRagFusion = 0x17,
49    AlterCollectionPolicy = 0x18,
50
51    // ── SQL & DDL ───────────────────────────────────────────────
52    Sql = 0x20,
53    Ddl = 0x21,
54    Explain = 0x22,
55    CopyFrom = 0x23,
56
57    // ── Session parameters ──────────────────────────────────────
58    Set = 0x30,
59    Show = 0x31,
60    Reset = 0x32,
61
62    // ── Transaction control ─────────────────────────────────────
63    Begin = 0x40,
64    Commit = 0x41,
65    Rollback = 0x42,
66
67    // ── Graph operations (direct Data Plane dispatch) ───────────
68    GraphHop = 0x50,
69    GraphNeighbors = 0x51,
70    GraphPath = 0x52,
71    GraphSubgraph = 0x53,
72    EdgePut = 0x54,
73    EdgeDelete = 0x55,
74    GraphAlgo = 0x56,
75    GraphMatch = 0x57,
76
77    // ── Spatial operations (direct Data Plane dispatch) ────────
78    SpatialScan = 0x19,
79
80    // ── Timeseries operations (direct Data Plane dispatch) ──────
81    TimeseriesScan = 0x1A,
82    TimeseriesIngest = 0x1B,
83
84    // ── Search operations (direct Data Plane dispatch) ──────────
85    TextSearch = 0x60,
86    HybridSearch = 0x61,
87
88    // ── Batch operations ────────────────────────────────────────
89    VectorBatchInsert = 0x70,
90    DocumentBatchInsert = 0x71,
91
92    // ── KV advanced operations ──────────────────────────────────
93    KvScan = 0x72,
94    KvExpire = 0x73,
95    KvPersist = 0x74,
96    KvGetTtl = 0x75,
97    KvBatchGet = 0x76,
98    KvBatchPut = 0x77,
99    KvFieldGet = 0x78,
100    KvFieldSet = 0x79,
101
102    // ── Document advanced operations ────────────────────────────
103    DocumentUpdate = 0x7A,
104    DocumentScan = 0x7B,
105    DocumentUpsert = 0x7C,
106    DocumentBulkUpdate = 0x7D,
107    DocumentBulkDelete = 0x7E,
108
109    // ── Vector advanced operations ──────────────────────────────
110    VectorInsert = 0x7F,
111    VectorMultiSearch = 0x80,
112    VectorDelete = 0x81,
113
114    // ── Columnar operations ─────────────────────────────────────
115    ColumnarScan = 0x82,
116    ColumnarInsert = 0x83,
117
118    // ── Query operations ────────────────────────────────────────
119    RecursiveScan = 0x84,
120
121    // ── Document DDL operations ─────────────────────────────────
122    DocumentTruncate = 0x85,
123    DocumentEstimateCount = 0x86,
124    DocumentInsertSelect = 0x87,
125    DocumentRegister = 0x88,
126    DocumentDropIndex = 0x89,
127
128    // ── KV DDL operations ───────────────────────────────────────
129    KvRegisterIndex = 0x8A,
130    KvDropIndex = 0x8B,
131    KvTruncate = 0x8C,
132
133    // ── Vector DDL operations ───────────────────────────────────
134    VectorSetParams = 0x8D,
135
136    // ── KV atomic operations ───────────────────────────────────
137    KvIncr = 0x8E,
138    KvIncrFloat = 0x8F,
139    KvCas = 0x90,
140    KvGetSet = 0x91,
141
142    // ── KV sorted index operations ─────────────────────────────
143    KvRegisterSortedIndex = 0x92,
144    KvDropSortedIndex = 0x93,
145    KvSortedIndexRank = 0x94,
146    KvSortedIndexTopK = 0x95,
147    KvSortedIndexRange = 0x96,
148    KvSortedIndexCount = 0x97,
149    KvSortedIndexScore = 0x98,
150}
151
152impl OpCode {
153    /// Returns true if this operation is a write that requires WAL append.
154    pub fn is_write(&self) -> bool {
155        matches!(
156            self,
157            OpCode::PointPut
158                | OpCode::PointDelete
159                | OpCode::CrdtApply
160                | OpCode::EdgePut
161                | OpCode::EdgeDelete
162                | OpCode::VectorBatchInsert
163                | OpCode::DocumentBatchInsert
164                | OpCode::AlterCollectionPolicy
165                | OpCode::TimeseriesIngest
166                | OpCode::KvExpire
167                | OpCode::KvPersist
168                | OpCode::KvBatchPut
169                | OpCode::KvFieldSet
170                | OpCode::DocumentUpdate
171                | OpCode::DocumentUpsert
172                | OpCode::DocumentBulkUpdate
173                | OpCode::DocumentBulkDelete
174                | OpCode::VectorInsert
175                | OpCode::VectorDelete
176                | OpCode::ColumnarInsert
177                | OpCode::DocumentTruncate
178                | OpCode::DocumentInsertSelect
179                | OpCode::DocumentRegister
180                | OpCode::DocumentDropIndex
181                | OpCode::KvRegisterIndex
182                | OpCode::KvDropIndex
183                | OpCode::KvTruncate
184                | OpCode::VectorSetParams
185                | OpCode::KvIncr
186                | OpCode::KvIncrFloat
187                | OpCode::KvCas
188                | OpCode::KvGetSet
189                | OpCode::KvRegisterSortedIndex
190                | OpCode::KvDropSortedIndex
191        )
192    }
193}
194
195impl From<OpCode> for u8 {
196    fn from(op: OpCode) -> u8 {
197        op as u8
198    }
199}
200
201impl TryFrom<u8> for OpCode {
202    type Error = String;
203
204    fn try_from(value: u8) -> Result<Self, Self::Error> {
205        match value {
206            0x01 => Ok(OpCode::Auth),
207            0x02 => Ok(OpCode::Ping),
208            0x03 => Ok(OpCode::Status),
209            0x10 => Ok(OpCode::PointGet),
210            0x11 => Ok(OpCode::PointPut),
211            0x12 => Ok(OpCode::PointDelete),
212            0x13 => Ok(OpCode::VectorSearch),
213            0x14 => Ok(OpCode::RangeScan),
214            0x15 => Ok(OpCode::CrdtRead),
215            0x16 => Ok(OpCode::CrdtApply),
216            0x17 => Ok(OpCode::GraphRagFusion),
217            0x18 => Ok(OpCode::AlterCollectionPolicy),
218            0x19 => Ok(OpCode::SpatialScan),
219            0x1A => Ok(OpCode::TimeseriesScan),
220            0x1B => Ok(OpCode::TimeseriesIngest),
221            0x20 => Ok(OpCode::Sql),
222            0x21 => Ok(OpCode::Ddl),
223            0x22 => Ok(OpCode::Explain),
224            0x23 => Ok(OpCode::CopyFrom),
225            0x30 => Ok(OpCode::Set),
226            0x31 => Ok(OpCode::Show),
227            0x32 => Ok(OpCode::Reset),
228            0x40 => Ok(OpCode::Begin),
229            0x41 => Ok(OpCode::Commit),
230            0x42 => Ok(OpCode::Rollback),
231            0x50 => Ok(OpCode::GraphHop),
232            0x51 => Ok(OpCode::GraphNeighbors),
233            0x52 => Ok(OpCode::GraphPath),
234            0x53 => Ok(OpCode::GraphSubgraph),
235            0x54 => Ok(OpCode::EdgePut),
236            0x55 => Ok(OpCode::EdgeDelete),
237            0x56 => Ok(OpCode::GraphAlgo),
238            0x57 => Ok(OpCode::GraphMatch),
239            0x60 => Ok(OpCode::TextSearch),
240            0x61 => Ok(OpCode::HybridSearch),
241            0x70 => Ok(OpCode::VectorBatchInsert),
242            0x71 => Ok(OpCode::DocumentBatchInsert),
243            0x72 => Ok(OpCode::KvScan),
244            0x73 => Ok(OpCode::KvExpire),
245            0x74 => Ok(OpCode::KvPersist),
246            0x75 => Ok(OpCode::KvGetTtl),
247            0x76 => Ok(OpCode::KvBatchGet),
248            0x77 => Ok(OpCode::KvBatchPut),
249            0x78 => Ok(OpCode::KvFieldGet),
250            0x79 => Ok(OpCode::KvFieldSet),
251            0x7A => Ok(OpCode::DocumentUpdate),
252            0x7B => Ok(OpCode::DocumentScan),
253            0x7C => Ok(OpCode::DocumentUpsert),
254            0x7D => Ok(OpCode::DocumentBulkUpdate),
255            0x7E => Ok(OpCode::DocumentBulkDelete),
256            0x7F => Ok(OpCode::VectorInsert),
257            0x80 => Ok(OpCode::VectorMultiSearch),
258            0x81 => Ok(OpCode::VectorDelete),
259            0x82 => Ok(OpCode::ColumnarScan),
260            0x83 => Ok(OpCode::ColumnarInsert),
261            0x84 => Ok(OpCode::RecursiveScan),
262            0x85 => Ok(OpCode::DocumentTruncate),
263            0x86 => Ok(OpCode::DocumentEstimateCount),
264            0x87 => Ok(OpCode::DocumentInsertSelect),
265            0x88 => Ok(OpCode::DocumentRegister),
266            0x89 => Ok(OpCode::DocumentDropIndex),
267            0x8A => Ok(OpCode::KvRegisterIndex),
268            0x8B => Ok(OpCode::KvDropIndex),
269            0x8C => Ok(OpCode::KvTruncate),
270            0x8D => Ok(OpCode::VectorSetParams),
271            0x8E => Ok(OpCode::KvIncr),
272            0x8F => Ok(OpCode::KvIncrFloat),
273            0x90 => Ok(OpCode::KvCas),
274            0x91 => Ok(OpCode::KvGetSet),
275            0x92 => Ok(OpCode::KvRegisterSortedIndex),
276            0x93 => Ok(OpCode::KvDropSortedIndex),
277            0x94 => Ok(OpCode::KvSortedIndexRank),
278            0x95 => Ok(OpCode::KvSortedIndexTopK),
279            0x96 => Ok(OpCode::KvSortedIndexRange),
280            0x97 => Ok(OpCode::KvSortedIndexCount),
281            0x98 => Ok(OpCode::KvSortedIndexScore),
282            other => Err(format!("unknown OpCode byte: 0x{other:02X}")),
283        }
284    }
285}
286
287// ─── Response Status ────────────────────────────────────────────────
288
289/// Status code in response frames.
290#[repr(u8)]
291#[derive(
292    Debug,
293    Clone,
294    Copy,
295    PartialEq,
296    Eq,
297    Serialize,
298    Deserialize,
299    zerompk::ToMessagePack,
300    zerompk::FromMessagePack,
301)]
302#[msgpack(c_enum)]
303pub enum ResponseStatus {
304    /// Request completed successfully.
305    Ok = 0,
306    /// Partial result — more chunks follow with the same `seq`.
307    Partial = 1,
308    /// Request failed — see `error` field.
309    Error = 2,
310}
311
312// ─── Auth ───────────────────────────────────────────────────────────
313
314/// Authentication method in an `Auth` request.
315#[derive(
316    Debug, Clone, Serialize, Deserialize, zerompk::ToMessagePack, zerompk::FromMessagePack,
317)]
318#[serde(tag = "method", rename_all = "snake_case")]
319pub enum AuthMethod {
320    Trust {
321        #[serde(default = "default_username")]
322        username: String,
323    },
324    Password {
325        username: String,
326        password: String,
327    },
328    ApiKey {
329        token: String,
330    },
331}
332
333fn default_username() -> String {
334    "admin".into()
335}
336
337/// Successful auth response payload.
338#[derive(
339    Debug, Clone, Serialize, Deserialize, zerompk::ToMessagePack, zerompk::FromMessagePack,
340)]
341pub struct AuthResponse {
342    pub username: String,
343    pub tenant_id: u32,
344}
345
346// ─── Request Frame ──────────────────────────────────────────────────
347
348/// A request sent from client to server over the native protocol.
349///
350/// Serialized as MessagePack. The `op` field selects the handler,
351/// `seq` correlates request to response.
352#[derive(Debug, Clone, Serialize, Deserialize)]
353pub struct NativeRequest {
354    /// Operation code.
355    pub op: OpCode,
356    /// Client-assigned sequence number for request/response correlation.
357    pub seq: u64,
358    /// Operation-specific fields (flattened into the same map).
359    #[serde(flatten)]
360    pub fields: RequestFields,
361}
362
363impl zerompk::ToMessagePack for NativeRequest {
364    fn write<W: zerompk::Write>(&self, writer: &mut W) -> zerompk::Result<()> {
365        writer.write_array_len(3)?;
366        self.op.write(writer)?;
367        writer.write_u64(self.seq)?;
368        self.fields.write(writer)
369    }
370}
371
372impl<'a> zerompk::FromMessagePack<'a> for NativeRequest {
373    fn read<R: zerompk::Read<'a>>(reader: &mut R) -> zerompk::Result<Self> {
374        let len = reader.read_array_len()?;
375        if len != 3 {
376            return Err(zerompk::Error::ArrayLengthMismatch {
377                expected: 3,
378                actual: len,
379            });
380        }
381        let op = OpCode::read(reader)?;
382        let seq = reader.read_u64()?;
383        let fields = RequestFields::read(reader)?;
384        Ok(Self { op, seq, fields })
385    }
386}
387
388/// Operation-specific request fields.
389///
390/// Each variant carries only the fields needed for that operation.
391/// Unknown fields are silently ignored during deserialization.
392#[derive(
393    Debug, Clone, Serialize, Deserialize, zerompk::ToMessagePack, zerompk::FromMessagePack,
394)]
395#[serde(untagged)]
396pub enum RequestFields {
397    /// Auth, SQL, DDL, Explain, Set, Show, Reset, Begin, Commit, Rollback,
398    /// CopyFrom, Ping — all use a subset of these text fields.
399    Text(TextFields),
400}
401
402/// Catch-all text fields used by most operations.
403///
404/// Each operation uses a subset; unused fields default to `None`/empty.
405#[derive(Debug, Clone, Default, Serialize, Deserialize)]
406pub struct TextFields {
407    // ── Auth ─────────────────────────────────────────────────
408    #[serde(skip_serializing_if = "Option::is_none")]
409    pub auth: Option<AuthMethod>,
410
411    // ── SQL/DDL/Explain/Set/Show/Reset/CopyFrom ─────────────
412    #[serde(skip_serializing_if = "Option::is_none")]
413    pub sql: Option<String>,
414    #[serde(skip_serializing_if = "Option::is_none")]
415    pub key: Option<String>,
416    #[serde(skip_serializing_if = "Option::is_none")]
417    pub value: Option<String>,
418
419    // ── Collection + document targeting ──────────────────────
420    #[serde(skip_serializing_if = "Option::is_none")]
421    pub collection: Option<String>,
422    #[serde(skip_serializing_if = "Option::is_none")]
423    pub document_id: Option<String>,
424    #[serde(skip_serializing_if = "Option::is_none")]
425    pub data: Option<Vec<u8>>,
426
427    // ── Vector search ────────────────────────────────────────
428    #[serde(skip_serializing_if = "Option::is_none")]
429    pub query_vector: Option<Vec<f32>>,
430    #[serde(skip_serializing_if = "Option::is_none")]
431    pub top_k: Option<u64>,
432
433    // ── Range scan ───────────────────────────────────────────
434    #[serde(skip_serializing_if = "Option::is_none")]
435    pub field: Option<String>,
436    #[serde(skip_serializing_if = "Option::is_none")]
437    pub limit: Option<u64>,
438
439    // ── CRDT ─────────────────────────────────────────────────
440    #[serde(skip_serializing_if = "Option::is_none")]
441    pub delta: Option<Vec<u8>>,
442    #[serde(skip_serializing_if = "Option::is_none")]
443    pub peer_id: Option<u64>,
444
445    // ── Graph RAG fusion ─────────────────────────────────────
446    #[serde(skip_serializing_if = "Option::is_none")]
447    pub vector_top_k: Option<u64>,
448    #[serde(skip_serializing_if = "Option::is_none")]
449    pub edge_label: Option<String>,
450    #[serde(skip_serializing_if = "Option::is_none")]
451    pub direction: Option<String>,
452    #[serde(skip_serializing_if = "Option::is_none")]
453    pub expansion_depth: Option<u64>,
454    #[serde(skip_serializing_if = "Option::is_none")]
455    pub final_top_k: Option<u64>,
456    #[serde(skip_serializing_if = "Option::is_none")]
457    pub vector_k: Option<f64>,
458    #[serde(skip_serializing_if = "Option::is_none")]
459    pub graph_k: Option<f64>,
460    #[serde(skip_serializing_if = "Option::is_none")]
461    pub vector_field: Option<String>,
462
463    // ── Graph ops ────────────────────────────────────────────
464    #[serde(skip_serializing_if = "Option::is_none")]
465    pub start_node: Option<String>,
466    #[serde(skip_serializing_if = "Option::is_none")]
467    pub end_node: Option<String>,
468    #[serde(skip_serializing_if = "Option::is_none")]
469    pub depth: Option<u64>,
470    #[serde(skip_serializing_if = "Option::is_none")]
471    pub from_node: Option<String>,
472    #[serde(skip_serializing_if = "Option::is_none")]
473    pub to_node: Option<String>,
474    #[serde(skip_serializing_if = "Option::is_none")]
475    pub edge_type: Option<String>,
476    #[serde(skip_serializing_if = "Option::is_none")]
477    pub properties: Option<serde_json::Value>,
478
479    // ── Text/Hybrid search ───────────────────────────────────
480    #[serde(skip_serializing_if = "Option::is_none")]
481    pub query_text: Option<String>,
482    #[serde(skip_serializing_if = "Option::is_none")]
483    pub vector_weight: Option<f64>,
484    #[serde(skip_serializing_if = "Option::is_none")]
485    pub fuzzy: Option<bool>,
486
487    // ── Vector search tuning ────────────────────────────────
488    #[serde(skip_serializing_if = "Option::is_none")]
489    pub ef_search: Option<u64>,
490    /// Named vector field (for multi-field vector collections).
491    #[serde(skip_serializing_if = "Option::is_none")]
492    pub field_name: Option<String>,
493
494    // ── Range scan bounds ───────────────────────────────────
495    #[serde(skip_serializing_if = "Option::is_none")]
496    pub lower_bound: Option<Vec<u8>>,
497    #[serde(skip_serializing_if = "Option::is_none")]
498    pub upper_bound: Option<Vec<u8>>,
499
500    // ── CRDT dedup ──────────────────────────────────────────
501    /// Monotonic mutation ID for CRDT delta deduplication.
502    #[serde(skip_serializing_if = "Option::is_none")]
503    pub mutation_id: Option<u64>,
504
505    // ── Batch operations ─────────────────────────────────────
506    #[serde(skip_serializing_if = "Option::is_none")]
507    pub vectors: Option<Vec<BatchVector>>,
508    #[serde(skip_serializing_if = "Option::is_none")]
509    pub documents: Option<Vec<BatchDocument>>,
510
511    // ── Spatial scan ───────────────────────────────────────────
512    /// Query geometry as GeoJSON bytes (for SpatialScan).
513    #[serde(skip_serializing_if = "Option::is_none")]
514    pub query_geometry: Option<Vec<u8>>,
515    /// Spatial predicate name: "dwithin", "contains", "intersects", "within".
516    #[serde(skip_serializing_if = "Option::is_none")]
517    pub spatial_predicate: Option<String>,
518    /// Distance threshold in meters (for ST_DWithin).
519    #[serde(skip_serializing_if = "Option::is_none")]
520    pub distance_meters: Option<f64>,
521
522    // ── Timeseries ───────────────────────────────────────────
523    /// ILP payload bytes (for TimeseriesIngest).
524    #[serde(skip_serializing_if = "Option::is_none")]
525    pub payload: Option<Vec<u8>>,
526    /// Ingest format (default: "ilp").
527    #[serde(skip_serializing_if = "Option::is_none")]
528    pub format: Option<String>,
529    /// Time range start (epoch ms, for TimeseriesScan).
530    #[serde(skip_serializing_if = "Option::is_none")]
531    pub time_range_start: Option<i64>,
532    /// Time range end (epoch ms, for TimeseriesScan).
533    #[serde(skip_serializing_if = "Option::is_none")]
534    pub time_range_end: Option<i64>,
535    /// Bucket interval string for time_bucket aggregation (e.g., "5m").
536    #[serde(skip_serializing_if = "Option::is_none")]
537    pub bucket_interval: Option<String>,
538
539    // ── KV advanced ─────────────────────────────────────────
540    /// TTL in milliseconds (for KvExpire, KvBatchPut).
541    #[serde(skip_serializing_if = "Option::is_none")]
542    pub ttl_ms: Option<u64>,
543    /// Cursor bytes for KvScan pagination.
544    #[serde(skip_serializing_if = "Option::is_none")]
545    pub cursor: Option<Vec<u8>>,
546    /// Glob pattern for KvScan key matching.
547    #[serde(skip_serializing_if = "Option::is_none")]
548    pub match_pattern: Option<String>,
549    /// Multiple keys for BatchGet / Delete.
550    #[serde(skip_serializing_if = "Option::is_none")]
551    pub keys: Option<Vec<Vec<u8>>>,
552    /// Key-value entries for BatchPut: [(key, value), ...].
553    #[serde(skip_serializing_if = "Option::is_none")]
554    pub entries: Option<Vec<(Vec<u8>, Vec<u8>)>>,
555    /// Field names for FieldGet.
556    #[serde(skip_serializing_if = "Option::is_none")]
557    pub fields: Option<Vec<String>>,
558
559    // ── KV atomic operations ────────────────────────────────
560    /// Integer delta for KvIncr.
561    #[serde(skip_serializing_if = "Option::is_none")]
562    pub incr_delta: Option<i64>,
563    /// Float delta for KvIncrFloat.
564    #[serde(skip_serializing_if = "Option::is_none")]
565    pub incr_float_delta: Option<f64>,
566    /// Expected value for KvCas.
567    #[serde(skip_serializing_if = "Option::is_none")]
568    pub expected: Option<Vec<u8>>,
569    /// New value for KvCas / KvGetSet.
570    #[serde(skip_serializing_if = "Option::is_none")]
571    pub new_value: Option<Vec<u8>>,
572
573    // ── KV sorted index operations ──────────────────────────
574    /// Sorted index name.
575    #[serde(skip_serializing_if = "Option::is_none")]
576    pub index_name: Option<String>,
577    /// Sort columns: [(column_name, direction), ...].
578    #[serde(skip_serializing_if = "Option::is_none")]
579    pub sort_columns: Option<Vec<(String, String)>>,
580    /// Primary key column for sorted index.
581    #[serde(skip_serializing_if = "Option::is_none")]
582    pub key_column: Option<String>,
583    /// Window type for sorted index: "none", "daily", "weekly", "monthly", "custom".
584    #[serde(skip_serializing_if = "Option::is_none")]
585    pub window_type: Option<String>,
586    /// Timestamp column for windowed sorted index.
587    #[serde(skip_serializing_if = "Option::is_none")]
588    pub window_timestamp_column: Option<String>,
589    /// Custom window start (ms since epoch).
590    #[serde(skip_serializing_if = "Option::is_none")]
591    pub window_start_ms: Option<u64>,
592    /// Custom window end (ms since epoch).
593    #[serde(skip_serializing_if = "Option::is_none")]
594    pub window_end_ms: Option<u64>,
595    /// Top-K value for SortedIndexTopK.
596    #[serde(skip_serializing_if = "Option::is_none")]
597    pub top_k_count: Option<u32>,
598    /// Score min for SortedIndexRange (encoded bytes).
599    #[serde(skip_serializing_if = "Option::is_none")]
600    pub score_min: Option<Vec<u8>>,
601    /// Score max for SortedIndexRange (encoded bytes).
602    #[serde(skip_serializing_if = "Option::is_none")]
603    pub score_max: Option<Vec<u8>>,
604
605    // ── Document advanced ───────────────────────────────────
606    /// Field-level updates: [(field_name, value_bytes), ...].
607    #[serde(skip_serializing_if = "Option::is_none")]
608    pub updates: Option<Vec<(String, Vec<u8>)>>,
609    /// Serialized filter predicates (MessagePack).
610    #[serde(skip_serializing_if = "Option::is_none")]
611    pub filters: Option<Vec<u8>>,
612
613    // ── Vector advanced ─────────────────────────────────────
614    /// Single vector embedding (for VectorInsert).
615    #[serde(skip_serializing_if = "Option::is_none")]
616    pub vector: Option<Vec<f32>>,
617    /// Vector ID for deletion.
618    #[serde(skip_serializing_if = "Option::is_none")]
619    pub vector_id: Option<u32>,
620
621    // ── Collection policy ────────────────────────────────────
622    #[serde(skip_serializing_if = "Option::is_none")]
623    pub policy: Option<serde_json::Value>,
624
625    // ── Graph algorithm/match ───────────────────────────────
626    /// Algorithm name for GraphAlgo (e.g., "pagerank", "wcc", "sssp").
627    #[serde(skip_serializing_if = "Option::is_none")]
628    pub algorithm: Option<String>,
629    /// Cypher-subset MATCH query string for GraphMatch.
630    #[serde(skip_serializing_if = "Option::is_none")]
631    pub match_query: Option<String>,
632    /// Algorithm-specific parameters (JSON object).
633    #[serde(skip_serializing_if = "Option::is_none")]
634    pub algo_params: Option<serde_json::Value>,
635
636    // ── Document DDL ────────────────────────────────────────
637    /// Index paths for DocumentRegister.
638    #[serde(skip_serializing_if = "Option::is_none")]
639    pub index_paths: Option<Vec<String>>,
640    /// Source collection for InsertSelect.
641    #[serde(skip_serializing_if = "Option::is_none")]
642    pub source_collection: Option<String>,
643
644    // ── KV DDL ──────────────────────────────────────────────
645    /// Field position in tuple for KvRegisterIndex.
646    #[serde(skip_serializing_if = "Option::is_none")]
647    pub field_position: Option<u64>,
648    /// Whether to backfill existing keys on index creation.
649    #[serde(skip_serializing_if = "Option::is_none")]
650    pub backfill: Option<bool>,
651
652    // ── Vector DDL ──────────────────────────────────────────
653    /// HNSW M parameter (max connections per layer).
654    #[serde(skip_serializing_if = "Option::is_none")]
655    pub m: Option<u64>,
656    /// HNSW ef_construction parameter.
657    #[serde(skip_serializing_if = "Option::is_none")]
658    pub ef_construction: Option<u64>,
659    /// Distance metric name ("cosine", "euclidean", "dot").
660    #[serde(skip_serializing_if = "Option::is_none")]
661    pub metric: Option<String>,
662    /// Index type ("hnsw", "ivfpq", "flat").
663    #[serde(skip_serializing_if = "Option::is_none")]
664    pub index_type: Option<String>,
665}
666
667impl zerompk::ToMessagePack for TextFields {
668    fn write<W: zerompk::Write>(&self, writer: &mut W) -> zerompk::Result<()> {
669        use crate::json_msgpack::JsonValue;
670        writer.write_array_len(81)?;
671        self.auth.write(writer)?;
672        self.sql.write(writer)?;
673        self.key.write(writer)?;
674        self.value.write(writer)?;
675        self.collection.write(writer)?;
676        self.document_id.write(writer)?;
677        self.data.write(writer)?;
678        self.query_vector.write(writer)?;
679        self.top_k.write(writer)?;
680        self.field.write(writer)?;
681        self.limit.write(writer)?;
682        self.delta.write(writer)?;
683        self.peer_id.write(writer)?;
684        self.vector_top_k.write(writer)?;
685        self.edge_label.write(writer)?;
686        self.direction.write(writer)?;
687        self.expansion_depth.write(writer)?;
688        self.final_top_k.write(writer)?;
689        self.vector_k.write(writer)?;
690        self.graph_k.write(writer)?;
691        self.vector_field.write(writer)?;
692        self.start_node.write(writer)?;
693        self.end_node.write(writer)?;
694        self.depth.write(writer)?;
695        self.from_node.write(writer)?;
696        self.to_node.write(writer)?;
697        self.edge_type.write(writer)?;
698        self.properties
699            .as_ref()
700            .map(|v| JsonValue(v.clone()))
701            .write(writer)?;
702        self.query_text.write(writer)?;
703        self.vector_weight.write(writer)?;
704        self.fuzzy.write(writer)?;
705        self.ef_search.write(writer)?;
706        self.field_name.write(writer)?;
707        self.lower_bound.write(writer)?;
708        self.upper_bound.write(writer)?;
709        self.mutation_id.write(writer)?;
710        self.vectors.write(writer)?;
711        self.documents.write(writer)?;
712        self.query_geometry.write(writer)?;
713        self.spatial_predicate.write(writer)?;
714        self.distance_meters.write(writer)?;
715        self.payload.write(writer)?;
716        self.format.write(writer)?;
717        self.time_range_start.write(writer)?;
718        self.time_range_end.write(writer)?;
719        self.bucket_interval.write(writer)?;
720        self.ttl_ms.write(writer)?;
721        self.cursor.write(writer)?;
722        self.match_pattern.write(writer)?;
723        self.keys.write(writer)?;
724        self.entries.write(writer)?;
725        self.fields.write(writer)?;
726        self.incr_delta.write(writer)?;
727        self.incr_float_delta.write(writer)?;
728        self.expected.write(writer)?;
729        self.new_value.write(writer)?;
730        self.index_name.write(writer)?;
731        self.sort_columns.write(writer)?;
732        self.key_column.write(writer)?;
733        self.window_type.write(writer)?;
734        self.window_timestamp_column.write(writer)?;
735        self.window_start_ms.write(writer)?;
736        self.window_end_ms.write(writer)?;
737        self.top_k_count.write(writer)?;
738        self.score_min.write(writer)?;
739        self.score_max.write(writer)?;
740        self.updates.write(writer)?;
741        self.filters.write(writer)?;
742        self.vector.write(writer)?;
743        self.vector_id.write(writer)?;
744        self.policy
745            .as_ref()
746            .map(|v| JsonValue(v.clone()))
747            .write(writer)?;
748        self.algorithm.write(writer)?;
749        self.match_query.write(writer)?;
750        self.algo_params
751            .as_ref()
752            .map(|v| JsonValue(v.clone()))
753            .write(writer)?;
754        self.index_paths.write(writer)?;
755        self.source_collection.write(writer)?;
756        self.field_position.write(writer)?;
757        self.backfill.write(writer)?;
758        self.m.write(writer)?;
759        self.ef_construction.write(writer)?;
760        self.metric.write(writer)?;
761        self.index_type.write(writer)
762    }
763}
764
765impl<'a> zerompk::FromMessagePack<'a> for TextFields {
766    fn read<R: zerompk::Read<'a>>(reader: &mut R) -> zerompk::Result<Self> {
767        use crate::json_msgpack::JsonValue;
768        let len = reader.read_array_len()?;
769        if len != 81 {
770            return Err(zerompk::Error::ArrayLengthMismatch {
771                expected: 81,
772                actual: len,
773            });
774        }
775        Ok(Self {
776            auth: Option::<AuthMethod>::read(reader)?,
777            sql: Option::<String>::read(reader)?,
778            key: Option::<String>::read(reader)?,
779            value: Option::<String>::read(reader)?,
780            collection: Option::<String>::read(reader)?,
781            document_id: Option::<String>::read(reader)?,
782            data: Option::<Vec<u8>>::read(reader)?,
783            query_vector: Option::<Vec<f32>>::read(reader)?,
784            top_k: Option::<u64>::read(reader)?,
785            field: Option::<String>::read(reader)?,
786            limit: Option::<u64>::read(reader)?,
787            delta: Option::<Vec<u8>>::read(reader)?,
788            peer_id: Option::<u64>::read(reader)?,
789            vector_top_k: Option::<u64>::read(reader)?,
790            edge_label: Option::<String>::read(reader)?,
791            direction: Option::<String>::read(reader)?,
792            expansion_depth: Option::<u64>::read(reader)?,
793            final_top_k: Option::<u64>::read(reader)?,
794            vector_k: Option::<f64>::read(reader)?,
795            graph_k: Option::<f64>::read(reader)?,
796            vector_field: Option::<String>::read(reader)?,
797            start_node: Option::<String>::read(reader)?,
798            end_node: Option::<String>::read(reader)?,
799            depth: Option::<u64>::read(reader)?,
800            from_node: Option::<String>::read(reader)?,
801            to_node: Option::<String>::read(reader)?,
802            edge_type: Option::<String>::read(reader)?,
803            properties: Option::<JsonValue>::read(reader)?.map(|v| v.0),
804            query_text: Option::<String>::read(reader)?,
805            vector_weight: Option::<f64>::read(reader)?,
806            fuzzy: Option::<bool>::read(reader)?,
807            ef_search: Option::<u64>::read(reader)?,
808            field_name: Option::<String>::read(reader)?,
809            lower_bound: Option::<Vec<u8>>::read(reader)?,
810            upper_bound: Option::<Vec<u8>>::read(reader)?,
811            mutation_id: Option::<u64>::read(reader)?,
812            vectors: Option::<Vec<BatchVector>>::read(reader)?,
813            documents: Option::<Vec<BatchDocument>>::read(reader)?,
814            query_geometry: Option::<Vec<u8>>::read(reader)?,
815            spatial_predicate: Option::<String>::read(reader)?,
816            distance_meters: Option::<f64>::read(reader)?,
817            payload: Option::<Vec<u8>>::read(reader)?,
818            format: Option::<String>::read(reader)?,
819            time_range_start: Option::<i64>::read(reader)?,
820            time_range_end: Option::<i64>::read(reader)?,
821            bucket_interval: Option::<String>::read(reader)?,
822            ttl_ms: Option::<u64>::read(reader)?,
823            cursor: Option::<Vec<u8>>::read(reader)?,
824            match_pattern: Option::<String>::read(reader)?,
825            keys: Option::<Vec<Vec<u8>>>::read(reader)?,
826            entries: Option::<Vec<(Vec<u8>, Vec<u8>)>>::read(reader)?,
827            fields: Option::<Vec<String>>::read(reader)?,
828            incr_delta: Option::<i64>::read(reader)?,
829            incr_float_delta: Option::<f64>::read(reader)?,
830            expected: Option::<Vec<u8>>::read(reader)?,
831            new_value: Option::<Vec<u8>>::read(reader)?,
832            index_name: Option::<String>::read(reader)?,
833            sort_columns: Option::<Vec<(String, String)>>::read(reader)?,
834            key_column: Option::<String>::read(reader)?,
835            window_type: Option::<String>::read(reader)?,
836            window_timestamp_column: Option::<String>::read(reader)?,
837            window_start_ms: Option::<u64>::read(reader)?,
838            window_end_ms: Option::<u64>::read(reader)?,
839            top_k_count: Option::<u32>::read(reader)?,
840            score_min: Option::<Vec<u8>>::read(reader)?,
841            score_max: Option::<Vec<u8>>::read(reader)?,
842            updates: Option::<Vec<(String, Vec<u8>)>>::read(reader)?,
843            filters: Option::<Vec<u8>>::read(reader)?,
844            vector: Option::<Vec<f32>>::read(reader)?,
845            vector_id: Option::<u32>::read(reader)?,
846            policy: Option::<JsonValue>::read(reader)?.map(|v| v.0),
847            algorithm: Option::<String>::read(reader)?,
848            match_query: Option::<String>::read(reader)?,
849            algo_params: Option::<JsonValue>::read(reader)?.map(|v| v.0),
850            index_paths: Option::<Vec<String>>::read(reader)?,
851            source_collection: Option::<String>::read(reader)?,
852            field_position: Option::<u64>::read(reader)?,
853            backfill: Option::<bool>::read(reader)?,
854            m: Option::<u64>::read(reader)?,
855            ef_construction: Option::<u64>::read(reader)?,
856            metric: Option::<String>::read(reader)?,
857            index_type: Option::<String>::read(reader)?,
858        })
859    }
860}
861
862/// A single vector in a batch insert.
863#[derive(Debug, Clone, Serialize, Deserialize)]
864pub struct BatchVector {
865    pub id: String,
866    pub embedding: Vec<f32>,
867    #[serde(skip_serializing_if = "Option::is_none")]
868    pub metadata: Option<serde_json::Value>,
869}
870
871impl zerompk::ToMessagePack for BatchVector {
872    fn write<W: zerompk::Write>(&self, writer: &mut W) -> zerompk::Result<()> {
873        use crate::json_msgpack::JsonValue;
874        writer.write_array_len(3)?;
875        writer.write_string(&self.id)?;
876        self.embedding.write(writer)?;
877        self.metadata
878            .as_ref()
879            .map(|v| JsonValue(v.clone()))
880            .write(writer)
881    }
882}
883
884impl<'a> zerompk::FromMessagePack<'a> for BatchVector {
885    fn read<R: zerompk::Read<'a>>(reader: &mut R) -> zerompk::Result<Self> {
886        use crate::json_msgpack::JsonValue;
887        let len = reader.read_array_len()?;
888        if len != 3 {
889            return Err(zerompk::Error::ArrayLengthMismatch {
890                expected: 3,
891                actual: len,
892            });
893        }
894        let id = reader.read_string()?.into_owned();
895        let embedding = Vec::<f32>::read(reader)?;
896        let metadata = Option::<JsonValue>::read(reader)?.map(|v| v.0);
897        Ok(Self {
898            id,
899            embedding,
900            metadata,
901        })
902    }
903}
904
905/// A single document in a batch insert.
906#[derive(Debug, Clone, Serialize, Deserialize)]
907pub struct BatchDocument {
908    pub id: String,
909    pub fields: serde_json::Value,
910}
911
912impl zerompk::ToMessagePack for BatchDocument {
913    fn write<W: zerompk::Write>(&self, writer: &mut W) -> zerompk::Result<()> {
914        use crate::json_msgpack::JsonValue;
915        writer.write_array_len(2)?;
916        writer.write_string(&self.id)?;
917        JsonValue(self.fields.clone()).write(writer)
918    }
919}
920
921impl<'a> zerompk::FromMessagePack<'a> for BatchDocument {
922    fn read<R: zerompk::Read<'a>>(reader: &mut R) -> zerompk::Result<Self> {
923        use crate::json_msgpack::JsonValue;
924        let len = reader.read_array_len()?;
925        if len != 2 {
926            return Err(zerompk::Error::ArrayLengthMismatch {
927                expected: 2,
928                actual: len,
929            });
930        }
931        let id = reader.read_string()?.into_owned();
932        let fields = JsonValue::read(reader)?.0;
933        Ok(Self { id, fields })
934    }
935}
936
937// ─── Response Frame ─────────────────────────────────────────────────
938
939/// A response sent from server to client over the native protocol.
940#[derive(
941    Debug, Clone, Serialize, Deserialize, zerompk::ToMessagePack, zerompk::FromMessagePack,
942)]
943pub struct NativeResponse {
944    /// Echoed from the request for correlation.
945    pub seq: u64,
946    /// Execution outcome.
947    pub status: ResponseStatus,
948    /// Column names (for query results).
949    #[serde(skip_serializing_if = "Option::is_none")]
950    pub columns: Option<Vec<String>>,
951    /// Row data (for query results). Each row is a Vec of Values.
952    #[serde(skip_serializing_if = "Option::is_none")]
953    pub rows: Option<Vec<Vec<Value>>>,
954    /// Number of rows affected (for writes).
955    #[serde(skip_serializing_if = "Option::is_none")]
956    pub rows_affected: Option<u64>,
957    /// WAL LSN watermark at time of computation.
958    pub watermark_lsn: u64,
959    /// Error details (if status == Error).
960    #[serde(skip_serializing_if = "Option::is_none")]
961    pub error: Option<ErrorPayload>,
962    /// Auth response (if op == Auth and status == Ok).
963    #[serde(skip_serializing_if = "Option::is_none")]
964    pub auth: Option<AuthResponse>,
965}
966
967/// Error details in a response.
968#[derive(
969    Debug, Clone, Serialize, Deserialize, zerompk::ToMessagePack, zerompk::FromMessagePack,
970)]
971pub struct ErrorPayload {
972    /// SQLSTATE-style error code (e.g., "42P01" for undefined table).
973    pub code: String,
974    /// Human-readable error message.
975    pub message: String,
976}
977
978impl NativeResponse {
979    /// Create a successful response with no data.
980    pub fn ok(seq: u64) -> Self {
981        Self {
982            seq,
983            status: ResponseStatus::Ok,
984            columns: None,
985            rows: None,
986            rows_affected: None,
987            watermark_lsn: 0,
988            error: None,
989            auth: None,
990        }
991    }
992
993    /// Create a successful response from a `QueryResult`.
994    pub fn from_query_result(seq: u64, qr: crate::result::QueryResult, lsn: u64) -> Self {
995        Self {
996            seq,
997            status: ResponseStatus::Ok,
998            columns: Some(qr.columns),
999            rows: Some(qr.rows),
1000            rows_affected: Some(qr.rows_affected),
1001            watermark_lsn: lsn,
1002            error: None,
1003            auth: None,
1004        }
1005    }
1006
1007    /// Create an error response.
1008    pub fn error(seq: u64, code: impl Into<String>, message: impl Into<String>) -> Self {
1009        Self {
1010            seq,
1011            status: ResponseStatus::Error,
1012            columns: None,
1013            rows: None,
1014            rows_affected: None,
1015            watermark_lsn: 0,
1016            error: Some(ErrorPayload {
1017                code: code.into(),
1018                message: message.into(),
1019            }),
1020            auth: None,
1021        }
1022    }
1023
1024    /// Create an auth success response.
1025    pub fn auth_ok(seq: u64, username: String, tenant_id: u32) -> Self {
1026        Self {
1027            seq,
1028            status: ResponseStatus::Ok,
1029            columns: None,
1030            rows: None,
1031            rows_affected: None,
1032            watermark_lsn: 0,
1033            error: None,
1034            auth: Some(AuthResponse {
1035                username,
1036                tenant_id,
1037            }),
1038        }
1039    }
1040
1041    /// Create a response with a single "status" column and one row.
1042    pub fn status_row(seq: u64, message: impl Into<String>) -> Self {
1043        Self {
1044            seq,
1045            status: ResponseStatus::Ok,
1046            columns: Some(vec!["status".into()]),
1047            rows: Some(vec![vec![Value::String(message.into())]]),
1048            rows_affected: Some(1),
1049            watermark_lsn: 0,
1050            error: None,
1051            auth: None,
1052        }
1053    }
1054}
1055
1056// ─── Protocol Constants ─────────────────────────────────────────────
1057
1058/// Maximum frame payload size (16 MiB).
1059pub const MAX_FRAME_SIZE: u32 = 16 * 1024 * 1024;
1060
1061/// Length of the frame header (4-byte big-endian u32 payload length).
1062pub const FRAME_HEADER_LEN: usize = 4;
1063
1064/// Default server port for the native protocol.
1065pub const DEFAULT_NATIVE_PORT: u16 = 6433;
1066
1067#[cfg(test)]
1068mod tests {
1069    use super::*;
1070
1071    #[test]
1072    fn opcode_repr() {
1073        assert_eq!(OpCode::Auth as u8, 0x01);
1074        assert_eq!(OpCode::Sql as u8, 0x20);
1075        assert_eq!(OpCode::Begin as u8, 0x40);
1076        assert_eq!(OpCode::GraphHop as u8, 0x50);
1077        assert_eq!(OpCode::TextSearch as u8, 0x60);
1078        assert_eq!(OpCode::VectorBatchInsert as u8, 0x70);
1079    }
1080
1081    #[test]
1082    fn opcode_is_write() {
1083        assert!(OpCode::PointPut.is_write());
1084        assert!(OpCode::PointDelete.is_write());
1085        assert!(OpCode::CrdtApply.is_write());
1086        assert!(OpCode::EdgePut.is_write());
1087        assert!(!OpCode::PointGet.is_write());
1088        assert!(!OpCode::Sql.is_write());
1089        assert!(!OpCode::VectorSearch.is_write());
1090        assert!(!OpCode::Ping.is_write());
1091    }
1092
1093    #[test]
1094    fn response_status_repr() {
1095        assert_eq!(ResponseStatus::Ok as u8, 0);
1096        assert_eq!(ResponseStatus::Partial as u8, 1);
1097        assert_eq!(ResponseStatus::Error as u8, 2);
1098    }
1099
1100    #[test]
1101    fn native_response_ok() {
1102        let r = NativeResponse::ok(42);
1103        assert_eq!(r.seq, 42);
1104        assert_eq!(r.status, ResponseStatus::Ok);
1105        assert!(r.error.is_none());
1106    }
1107
1108    #[test]
1109    fn native_response_error() {
1110        let r = NativeResponse::error(1, "42P01", "collection not found");
1111        assert_eq!(r.status, ResponseStatus::Error);
1112        let e = r.error.unwrap();
1113        assert_eq!(e.code, "42P01");
1114        assert_eq!(e.message, "collection not found");
1115    }
1116
1117    #[test]
1118    fn native_response_from_query_result() {
1119        let qr = crate::result::QueryResult {
1120            columns: vec!["id".into(), "name".into()],
1121            rows: vec![vec![
1122                Value::String("u1".into()),
1123                Value::String("Alice".into()),
1124            ]],
1125            rows_affected: 0,
1126        };
1127        let r = NativeResponse::from_query_result(5, qr, 100);
1128        assert_eq!(r.seq, 5);
1129        assert_eq!(r.watermark_lsn, 100);
1130        assert_eq!(r.columns.as_ref().unwrap().len(), 2);
1131        assert_eq!(r.rows.as_ref().unwrap().len(), 1);
1132    }
1133
1134    #[test]
1135    fn native_response_status_row() {
1136        let r = NativeResponse::status_row(3, "OK");
1137        assert_eq!(r.columns.as_ref().unwrap(), &["status"]);
1138        assert_eq!(r.rows.as_ref().unwrap()[0][0].as_str(), Some("OK"));
1139    }
1140
1141    #[test]
1142    fn msgpack_roundtrip_request() {
1143        let req = NativeRequest {
1144            op: OpCode::Sql,
1145            seq: 1,
1146            fields: RequestFields::Text(TextFields {
1147                sql: Some("SELECT 1".into()),
1148                ..Default::default()
1149            }),
1150        };
1151        let bytes = zerompk::to_msgpack_vec(&req).unwrap();
1152        let decoded: NativeRequest = zerompk::from_msgpack(&bytes).unwrap();
1153        assert_eq!(decoded.op, OpCode::Sql);
1154        assert_eq!(decoded.seq, 1);
1155    }
1156
1157    #[test]
1158    fn msgpack_roundtrip_response() {
1159        let resp = NativeResponse::from_query_result(
1160            7,
1161            crate::result::QueryResult {
1162                columns: vec!["x".into()],
1163                rows: vec![vec![Value::Integer(42)]],
1164                rows_affected: 0,
1165            },
1166            99,
1167        );
1168        let bytes = zerompk::to_msgpack_vec(&resp).unwrap();
1169        let decoded: NativeResponse = zerompk::from_msgpack(&bytes).unwrap();
1170        assert_eq!(decoded.seq, 7);
1171        assert_eq!(decoded.watermark_lsn, 99);
1172        assert_eq!(decoded.rows.unwrap()[0][0].as_i64(), Some(42));
1173    }
1174
1175    #[test]
1176    fn auth_method_variants() {
1177        let trust = AuthMethod::Trust {
1178            username: "admin".into(),
1179        };
1180        let bytes = zerompk::to_msgpack_vec(&trust).unwrap();
1181        let decoded: AuthMethod = zerompk::from_msgpack(&bytes).unwrap();
1182        match decoded {
1183            AuthMethod::Trust { username } => assert_eq!(username, "admin"),
1184            _ => panic!("expected Trust variant"),
1185        }
1186
1187        let pw = AuthMethod::Password {
1188            username: "user".into(),
1189            password: "secret".into(),
1190        };
1191        let bytes = zerompk::to_msgpack_vec(&pw).unwrap();
1192        let decoded: AuthMethod = zerompk::from_msgpack(&bytes).unwrap();
1193        match decoded {
1194            AuthMethod::Password { username, password } => {
1195                assert_eq!(username, "user");
1196                assert_eq!(password, "secret");
1197            }
1198            _ => panic!("expected Password variant"),
1199        }
1200    }
1201}