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
461    // ── Graph ops ────────────────────────────────────────────
462    #[serde(skip_serializing_if = "Option::is_none")]
463    pub start_node: Option<String>,
464    #[serde(skip_serializing_if = "Option::is_none")]
465    pub end_node: Option<String>,
466    #[serde(skip_serializing_if = "Option::is_none")]
467    pub depth: Option<u64>,
468    #[serde(skip_serializing_if = "Option::is_none")]
469    pub from_node: Option<String>,
470    #[serde(skip_serializing_if = "Option::is_none")]
471    pub to_node: Option<String>,
472    #[serde(skip_serializing_if = "Option::is_none")]
473    pub edge_type: Option<String>,
474    #[serde(skip_serializing_if = "Option::is_none")]
475    pub properties: Option<serde_json::Value>,
476
477    // ── Text/Hybrid search ───────────────────────────────────
478    #[serde(skip_serializing_if = "Option::is_none")]
479    pub query_text: Option<String>,
480    #[serde(skip_serializing_if = "Option::is_none")]
481    pub vector_weight: Option<f64>,
482    #[serde(skip_serializing_if = "Option::is_none")]
483    pub fuzzy: Option<bool>,
484
485    // ── Vector search tuning ────────────────────────────────
486    #[serde(skip_serializing_if = "Option::is_none")]
487    pub ef_search: Option<u64>,
488    /// Named vector field (for multi-field vector collections).
489    #[serde(skip_serializing_if = "Option::is_none")]
490    pub field_name: Option<String>,
491
492    // ── Range scan bounds ───────────────────────────────────
493    #[serde(skip_serializing_if = "Option::is_none")]
494    pub lower_bound: Option<Vec<u8>>,
495    #[serde(skip_serializing_if = "Option::is_none")]
496    pub upper_bound: Option<Vec<u8>>,
497
498    // ── CRDT dedup ──────────────────────────────────────────
499    /// Monotonic mutation ID for CRDT delta deduplication.
500    #[serde(skip_serializing_if = "Option::is_none")]
501    pub mutation_id: Option<u64>,
502
503    // ── Batch operations ─────────────────────────────────────
504    #[serde(skip_serializing_if = "Option::is_none")]
505    pub vectors: Option<Vec<BatchVector>>,
506    #[serde(skip_serializing_if = "Option::is_none")]
507    pub documents: Option<Vec<BatchDocument>>,
508
509    // ── Spatial scan ───────────────────────────────────────────
510    /// Query geometry as GeoJSON bytes (for SpatialScan).
511    #[serde(skip_serializing_if = "Option::is_none")]
512    pub query_geometry: Option<Vec<u8>>,
513    /// Spatial predicate name: "dwithin", "contains", "intersects", "within".
514    #[serde(skip_serializing_if = "Option::is_none")]
515    pub spatial_predicate: Option<String>,
516    /// Distance threshold in meters (for ST_DWithin).
517    #[serde(skip_serializing_if = "Option::is_none")]
518    pub distance_meters: Option<f64>,
519
520    // ── Timeseries ───────────────────────────────────────────
521    /// ILP payload bytes (for TimeseriesIngest).
522    #[serde(skip_serializing_if = "Option::is_none")]
523    pub payload: Option<Vec<u8>>,
524    /// Ingest format (default: "ilp").
525    #[serde(skip_serializing_if = "Option::is_none")]
526    pub format: Option<String>,
527    /// Time range start (epoch ms, for TimeseriesScan).
528    #[serde(skip_serializing_if = "Option::is_none")]
529    pub time_range_start: Option<i64>,
530    /// Time range end (epoch ms, for TimeseriesScan).
531    #[serde(skip_serializing_if = "Option::is_none")]
532    pub time_range_end: Option<i64>,
533    /// Bucket interval string for time_bucket aggregation (e.g., "5m").
534    #[serde(skip_serializing_if = "Option::is_none")]
535    pub bucket_interval: Option<String>,
536
537    // ── KV advanced ─────────────────────────────────────────
538    /// TTL in milliseconds (for KvExpire, KvBatchPut).
539    #[serde(skip_serializing_if = "Option::is_none")]
540    pub ttl_ms: Option<u64>,
541    /// Cursor bytes for KvScan pagination.
542    #[serde(skip_serializing_if = "Option::is_none")]
543    pub cursor: Option<Vec<u8>>,
544    /// Glob pattern for KvScan key matching.
545    #[serde(skip_serializing_if = "Option::is_none")]
546    pub match_pattern: Option<String>,
547    /// Multiple keys for BatchGet / Delete.
548    #[serde(skip_serializing_if = "Option::is_none")]
549    pub keys: Option<Vec<Vec<u8>>>,
550    /// Key-value entries for BatchPut: [(key, value), ...].
551    #[serde(skip_serializing_if = "Option::is_none")]
552    pub entries: Option<Vec<(Vec<u8>, Vec<u8>)>>,
553    /// Field names for FieldGet.
554    #[serde(skip_serializing_if = "Option::is_none")]
555    pub fields: Option<Vec<String>>,
556
557    // ── KV atomic operations ────────────────────────────────
558    /// Integer delta for KvIncr.
559    #[serde(skip_serializing_if = "Option::is_none")]
560    pub incr_delta: Option<i64>,
561    /// Float delta for KvIncrFloat.
562    #[serde(skip_serializing_if = "Option::is_none")]
563    pub incr_float_delta: Option<f64>,
564    /// Expected value for KvCas.
565    #[serde(skip_serializing_if = "Option::is_none")]
566    pub expected: Option<Vec<u8>>,
567    /// New value for KvCas / KvGetSet.
568    #[serde(skip_serializing_if = "Option::is_none")]
569    pub new_value: Option<Vec<u8>>,
570
571    // ── KV sorted index operations ──────────────────────────
572    /// Sorted index name.
573    #[serde(skip_serializing_if = "Option::is_none")]
574    pub index_name: Option<String>,
575    /// Sort columns: [(column_name, direction), ...].
576    #[serde(skip_serializing_if = "Option::is_none")]
577    pub sort_columns: Option<Vec<(String, String)>>,
578    /// Primary key column for sorted index.
579    #[serde(skip_serializing_if = "Option::is_none")]
580    pub key_column: Option<String>,
581    /// Window type for sorted index: "none", "daily", "weekly", "monthly", "custom".
582    #[serde(skip_serializing_if = "Option::is_none")]
583    pub window_type: Option<String>,
584    /// Timestamp column for windowed sorted index.
585    #[serde(skip_serializing_if = "Option::is_none")]
586    pub window_timestamp_column: Option<String>,
587    /// Custom window start (ms since epoch).
588    #[serde(skip_serializing_if = "Option::is_none")]
589    pub window_start_ms: Option<u64>,
590    /// Custom window end (ms since epoch).
591    #[serde(skip_serializing_if = "Option::is_none")]
592    pub window_end_ms: Option<u64>,
593    /// Top-K value for SortedIndexTopK.
594    #[serde(skip_serializing_if = "Option::is_none")]
595    pub top_k_count: Option<u32>,
596    /// Score min for SortedIndexRange (encoded bytes).
597    #[serde(skip_serializing_if = "Option::is_none")]
598    pub score_min: Option<Vec<u8>>,
599    /// Score max for SortedIndexRange (encoded bytes).
600    #[serde(skip_serializing_if = "Option::is_none")]
601    pub score_max: Option<Vec<u8>>,
602
603    // ── Document advanced ───────────────────────────────────
604    /// Field-level updates: [(field_name, value_bytes), ...].
605    #[serde(skip_serializing_if = "Option::is_none")]
606    pub updates: Option<Vec<(String, Vec<u8>)>>,
607    /// Serialized filter predicates (MessagePack).
608    #[serde(skip_serializing_if = "Option::is_none")]
609    pub filters: Option<Vec<u8>>,
610
611    // ── Vector advanced ─────────────────────────────────────
612    /// Single vector embedding (for VectorInsert).
613    #[serde(skip_serializing_if = "Option::is_none")]
614    pub vector: Option<Vec<f32>>,
615    /// Vector ID for deletion.
616    #[serde(skip_serializing_if = "Option::is_none")]
617    pub vector_id: Option<u32>,
618
619    // ── Collection policy ────────────────────────────────────
620    #[serde(skip_serializing_if = "Option::is_none")]
621    pub policy: Option<serde_json::Value>,
622
623    // ── Graph algorithm/match ───────────────────────────────
624    /// Algorithm name for GraphAlgo (e.g., "pagerank", "wcc", "sssp").
625    #[serde(skip_serializing_if = "Option::is_none")]
626    pub algorithm: Option<String>,
627    /// Cypher-subset MATCH query string for GraphMatch.
628    #[serde(skip_serializing_if = "Option::is_none")]
629    pub match_query: Option<String>,
630    /// Algorithm-specific parameters (JSON object).
631    #[serde(skip_serializing_if = "Option::is_none")]
632    pub algo_params: Option<serde_json::Value>,
633
634    // ── Document DDL ────────────────────────────────────────
635    /// Index paths for DocumentRegister.
636    #[serde(skip_serializing_if = "Option::is_none")]
637    pub index_paths: Option<Vec<String>>,
638    /// Source collection for InsertSelect.
639    #[serde(skip_serializing_if = "Option::is_none")]
640    pub source_collection: Option<String>,
641
642    // ── KV DDL ──────────────────────────────────────────────
643    /// Field position in tuple for KvRegisterIndex.
644    #[serde(skip_serializing_if = "Option::is_none")]
645    pub field_position: Option<u64>,
646    /// Whether to backfill existing keys on index creation.
647    #[serde(skip_serializing_if = "Option::is_none")]
648    pub backfill: Option<bool>,
649
650    // ── Vector DDL ──────────────────────────────────────────
651    /// HNSW M parameter (max connections per layer).
652    #[serde(skip_serializing_if = "Option::is_none")]
653    pub m: Option<u64>,
654    /// HNSW ef_construction parameter.
655    #[serde(skip_serializing_if = "Option::is_none")]
656    pub ef_construction: Option<u64>,
657    /// Distance metric name ("cosine", "euclidean", "dot").
658    #[serde(skip_serializing_if = "Option::is_none")]
659    pub metric: Option<String>,
660    /// Index type ("hnsw", "ivfpq", "flat").
661    #[serde(skip_serializing_if = "Option::is_none")]
662    pub index_type: Option<String>,
663}
664
665impl zerompk::ToMessagePack for TextFields {
666    fn write<W: zerompk::Write>(&self, writer: &mut W) -> zerompk::Result<()> {
667        use crate::json_msgpack::JsonValue;
668        writer.write_array_len(81)?;
669        self.auth.write(writer)?;
670        self.sql.write(writer)?;
671        self.key.write(writer)?;
672        self.value.write(writer)?;
673        self.collection.write(writer)?;
674        self.document_id.write(writer)?;
675        self.data.write(writer)?;
676        self.query_vector.write(writer)?;
677        self.top_k.write(writer)?;
678        self.field.write(writer)?;
679        self.limit.write(writer)?;
680        self.delta.write(writer)?;
681        self.peer_id.write(writer)?;
682        self.vector_top_k.write(writer)?;
683        self.edge_label.write(writer)?;
684        self.direction.write(writer)?;
685        self.expansion_depth.write(writer)?;
686        self.final_top_k.write(writer)?;
687        self.vector_k.write(writer)?;
688        self.graph_k.write(writer)?;
689        self.start_node.write(writer)?;
690        self.end_node.write(writer)?;
691        self.depth.write(writer)?;
692        self.from_node.write(writer)?;
693        self.to_node.write(writer)?;
694        self.edge_type.write(writer)?;
695        self.properties
696            .as_ref()
697            .map(|v| JsonValue(v.clone()))
698            .write(writer)?;
699        self.query_text.write(writer)?;
700        self.vector_weight.write(writer)?;
701        self.fuzzy.write(writer)?;
702        self.ef_search.write(writer)?;
703        self.field_name.write(writer)?;
704        self.lower_bound.write(writer)?;
705        self.upper_bound.write(writer)?;
706        self.mutation_id.write(writer)?;
707        self.vectors.write(writer)?;
708        self.documents.write(writer)?;
709        self.query_geometry.write(writer)?;
710        self.spatial_predicate.write(writer)?;
711        self.distance_meters.write(writer)?;
712        self.payload.write(writer)?;
713        self.format.write(writer)?;
714        self.time_range_start.write(writer)?;
715        self.time_range_end.write(writer)?;
716        self.bucket_interval.write(writer)?;
717        self.ttl_ms.write(writer)?;
718        self.cursor.write(writer)?;
719        self.match_pattern.write(writer)?;
720        self.keys.write(writer)?;
721        self.entries.write(writer)?;
722        self.fields.write(writer)?;
723        self.incr_delta.write(writer)?;
724        self.incr_float_delta.write(writer)?;
725        self.expected.write(writer)?;
726        self.new_value.write(writer)?;
727        self.index_name.write(writer)?;
728        self.sort_columns.write(writer)?;
729        self.key_column.write(writer)?;
730        self.window_type.write(writer)?;
731        self.window_timestamp_column.write(writer)?;
732        self.window_start_ms.write(writer)?;
733        self.window_end_ms.write(writer)?;
734        self.top_k_count.write(writer)?;
735        self.score_min.write(writer)?;
736        self.score_max.write(writer)?;
737        self.updates.write(writer)?;
738        self.filters.write(writer)?;
739        self.vector.write(writer)?;
740        self.vector_id.write(writer)?;
741        self.policy
742            .as_ref()
743            .map(|v| JsonValue(v.clone()))
744            .write(writer)?;
745        self.algorithm.write(writer)?;
746        self.match_query.write(writer)?;
747        self.algo_params
748            .as_ref()
749            .map(|v| JsonValue(v.clone()))
750            .write(writer)?;
751        self.index_paths.write(writer)?;
752        self.source_collection.write(writer)?;
753        self.field_position.write(writer)?;
754        self.backfill.write(writer)?;
755        self.m.write(writer)?;
756        self.ef_construction.write(writer)?;
757        self.metric.write(writer)?;
758        self.index_type.write(writer)
759    }
760}
761
762impl<'a> zerompk::FromMessagePack<'a> for TextFields {
763    fn read<R: zerompk::Read<'a>>(reader: &mut R) -> zerompk::Result<Self> {
764        use crate::json_msgpack::JsonValue;
765        let len = reader.read_array_len()?;
766        if len != 81 {
767            return Err(zerompk::Error::ArrayLengthMismatch {
768                expected: 81,
769                actual: len,
770            });
771        }
772        Ok(Self {
773            auth: Option::<AuthMethod>::read(reader)?,
774            sql: Option::<String>::read(reader)?,
775            key: Option::<String>::read(reader)?,
776            value: Option::<String>::read(reader)?,
777            collection: Option::<String>::read(reader)?,
778            document_id: Option::<String>::read(reader)?,
779            data: Option::<Vec<u8>>::read(reader)?,
780            query_vector: Option::<Vec<f32>>::read(reader)?,
781            top_k: Option::<u64>::read(reader)?,
782            field: Option::<String>::read(reader)?,
783            limit: Option::<u64>::read(reader)?,
784            delta: Option::<Vec<u8>>::read(reader)?,
785            peer_id: Option::<u64>::read(reader)?,
786            vector_top_k: Option::<u64>::read(reader)?,
787            edge_label: Option::<String>::read(reader)?,
788            direction: Option::<String>::read(reader)?,
789            expansion_depth: Option::<u64>::read(reader)?,
790            final_top_k: Option::<u64>::read(reader)?,
791            vector_k: Option::<f64>::read(reader)?,
792            graph_k: Option::<f64>::read(reader)?,
793            start_node: Option::<String>::read(reader)?,
794            end_node: Option::<String>::read(reader)?,
795            depth: Option::<u64>::read(reader)?,
796            from_node: Option::<String>::read(reader)?,
797            to_node: Option::<String>::read(reader)?,
798            edge_type: Option::<String>::read(reader)?,
799            properties: Option::<JsonValue>::read(reader)?.map(|v| v.0),
800            query_text: Option::<String>::read(reader)?,
801            vector_weight: Option::<f64>::read(reader)?,
802            fuzzy: Option::<bool>::read(reader)?,
803            ef_search: Option::<u64>::read(reader)?,
804            field_name: Option::<String>::read(reader)?,
805            lower_bound: Option::<Vec<u8>>::read(reader)?,
806            upper_bound: Option::<Vec<u8>>::read(reader)?,
807            mutation_id: Option::<u64>::read(reader)?,
808            vectors: Option::<Vec<BatchVector>>::read(reader)?,
809            documents: Option::<Vec<BatchDocument>>::read(reader)?,
810            query_geometry: Option::<Vec<u8>>::read(reader)?,
811            spatial_predicate: Option::<String>::read(reader)?,
812            distance_meters: Option::<f64>::read(reader)?,
813            payload: Option::<Vec<u8>>::read(reader)?,
814            format: Option::<String>::read(reader)?,
815            time_range_start: Option::<i64>::read(reader)?,
816            time_range_end: Option::<i64>::read(reader)?,
817            bucket_interval: Option::<String>::read(reader)?,
818            ttl_ms: Option::<u64>::read(reader)?,
819            cursor: Option::<Vec<u8>>::read(reader)?,
820            match_pattern: Option::<String>::read(reader)?,
821            keys: Option::<Vec<Vec<u8>>>::read(reader)?,
822            entries: Option::<Vec<(Vec<u8>, Vec<u8>)>>::read(reader)?,
823            fields: Option::<Vec<String>>::read(reader)?,
824            incr_delta: Option::<i64>::read(reader)?,
825            incr_float_delta: Option::<f64>::read(reader)?,
826            expected: Option::<Vec<u8>>::read(reader)?,
827            new_value: Option::<Vec<u8>>::read(reader)?,
828            index_name: Option::<String>::read(reader)?,
829            sort_columns: Option::<Vec<(String, String)>>::read(reader)?,
830            key_column: Option::<String>::read(reader)?,
831            window_type: Option::<String>::read(reader)?,
832            window_timestamp_column: Option::<String>::read(reader)?,
833            window_start_ms: Option::<u64>::read(reader)?,
834            window_end_ms: Option::<u64>::read(reader)?,
835            top_k_count: Option::<u32>::read(reader)?,
836            score_min: Option::<Vec<u8>>::read(reader)?,
837            score_max: Option::<Vec<u8>>::read(reader)?,
838            updates: Option::<Vec<(String, Vec<u8>)>>::read(reader)?,
839            filters: Option::<Vec<u8>>::read(reader)?,
840            vector: Option::<Vec<f32>>::read(reader)?,
841            vector_id: Option::<u32>::read(reader)?,
842            policy: Option::<JsonValue>::read(reader)?.map(|v| v.0),
843            algorithm: Option::<String>::read(reader)?,
844            match_query: Option::<String>::read(reader)?,
845            algo_params: Option::<JsonValue>::read(reader)?.map(|v| v.0),
846            index_paths: Option::<Vec<String>>::read(reader)?,
847            source_collection: Option::<String>::read(reader)?,
848            field_position: Option::<u64>::read(reader)?,
849            backfill: Option::<bool>::read(reader)?,
850            m: Option::<u64>::read(reader)?,
851            ef_construction: Option::<u64>::read(reader)?,
852            metric: Option::<String>::read(reader)?,
853            index_type: Option::<String>::read(reader)?,
854        })
855    }
856}
857
858/// A single vector in a batch insert.
859#[derive(Debug, Clone, Serialize, Deserialize)]
860pub struct BatchVector {
861    pub id: String,
862    pub embedding: Vec<f32>,
863    #[serde(skip_serializing_if = "Option::is_none")]
864    pub metadata: Option<serde_json::Value>,
865}
866
867impl zerompk::ToMessagePack for BatchVector {
868    fn write<W: zerompk::Write>(&self, writer: &mut W) -> zerompk::Result<()> {
869        use crate::json_msgpack::JsonValue;
870        writer.write_array_len(3)?;
871        writer.write_string(&self.id)?;
872        self.embedding.write(writer)?;
873        self.metadata
874            .as_ref()
875            .map(|v| JsonValue(v.clone()))
876            .write(writer)
877    }
878}
879
880impl<'a> zerompk::FromMessagePack<'a> for BatchVector {
881    fn read<R: zerompk::Read<'a>>(reader: &mut R) -> zerompk::Result<Self> {
882        use crate::json_msgpack::JsonValue;
883        let len = reader.read_array_len()?;
884        if len != 3 {
885            return Err(zerompk::Error::ArrayLengthMismatch {
886                expected: 3,
887                actual: len,
888            });
889        }
890        let id = reader.read_string()?.into_owned();
891        let embedding = Vec::<f32>::read(reader)?;
892        let metadata = Option::<JsonValue>::read(reader)?.map(|v| v.0);
893        Ok(Self {
894            id,
895            embedding,
896            metadata,
897        })
898    }
899}
900
901/// A single document in a batch insert.
902#[derive(Debug, Clone, Serialize, Deserialize)]
903pub struct BatchDocument {
904    pub id: String,
905    pub fields: serde_json::Value,
906}
907
908impl zerompk::ToMessagePack for BatchDocument {
909    fn write<W: zerompk::Write>(&self, writer: &mut W) -> zerompk::Result<()> {
910        use crate::json_msgpack::JsonValue;
911        writer.write_array_len(2)?;
912        writer.write_string(&self.id)?;
913        JsonValue(self.fields.clone()).write(writer)
914    }
915}
916
917impl<'a> zerompk::FromMessagePack<'a> for BatchDocument {
918    fn read<R: zerompk::Read<'a>>(reader: &mut R) -> zerompk::Result<Self> {
919        use crate::json_msgpack::JsonValue;
920        let len = reader.read_array_len()?;
921        if len != 2 {
922            return Err(zerompk::Error::ArrayLengthMismatch {
923                expected: 2,
924                actual: len,
925            });
926        }
927        let id = reader.read_string()?.into_owned();
928        let fields = JsonValue::read(reader)?.0;
929        Ok(Self { id, fields })
930    }
931}
932
933// ─── Response Frame ─────────────────────────────────────────────────
934
935/// A response sent from server to client over the native protocol.
936#[derive(
937    Debug, Clone, Serialize, Deserialize, zerompk::ToMessagePack, zerompk::FromMessagePack,
938)]
939pub struct NativeResponse {
940    /// Echoed from the request for correlation.
941    pub seq: u64,
942    /// Execution outcome.
943    pub status: ResponseStatus,
944    /// Column names (for query results).
945    #[serde(skip_serializing_if = "Option::is_none")]
946    pub columns: Option<Vec<String>>,
947    /// Row data (for query results). Each row is a Vec of Values.
948    #[serde(skip_serializing_if = "Option::is_none")]
949    pub rows: Option<Vec<Vec<Value>>>,
950    /// Number of rows affected (for writes).
951    #[serde(skip_serializing_if = "Option::is_none")]
952    pub rows_affected: Option<u64>,
953    /// WAL LSN watermark at time of computation.
954    pub watermark_lsn: u64,
955    /// Error details (if status == Error).
956    #[serde(skip_serializing_if = "Option::is_none")]
957    pub error: Option<ErrorPayload>,
958    /// Auth response (if op == Auth and status == Ok).
959    #[serde(skip_serializing_if = "Option::is_none")]
960    pub auth: Option<AuthResponse>,
961}
962
963/// Error details in a response.
964#[derive(
965    Debug, Clone, Serialize, Deserialize, zerompk::ToMessagePack, zerompk::FromMessagePack,
966)]
967pub struct ErrorPayload {
968    /// SQLSTATE-style error code (e.g., "42P01" for undefined table).
969    pub code: String,
970    /// Human-readable error message.
971    pub message: String,
972}
973
974impl NativeResponse {
975    /// Create a successful response with no data.
976    pub fn ok(seq: u64) -> Self {
977        Self {
978            seq,
979            status: ResponseStatus::Ok,
980            columns: None,
981            rows: None,
982            rows_affected: None,
983            watermark_lsn: 0,
984            error: None,
985            auth: None,
986        }
987    }
988
989    /// Create a successful response from a `QueryResult`.
990    pub fn from_query_result(seq: u64, qr: crate::result::QueryResult, lsn: u64) -> Self {
991        Self {
992            seq,
993            status: ResponseStatus::Ok,
994            columns: Some(qr.columns),
995            rows: Some(qr.rows),
996            rows_affected: Some(qr.rows_affected),
997            watermark_lsn: lsn,
998            error: None,
999            auth: None,
1000        }
1001    }
1002
1003    /// Create an error response.
1004    pub fn error(seq: u64, code: impl Into<String>, message: impl Into<String>) -> Self {
1005        Self {
1006            seq,
1007            status: ResponseStatus::Error,
1008            columns: None,
1009            rows: None,
1010            rows_affected: None,
1011            watermark_lsn: 0,
1012            error: Some(ErrorPayload {
1013                code: code.into(),
1014                message: message.into(),
1015            }),
1016            auth: None,
1017        }
1018    }
1019
1020    /// Create an auth success response.
1021    pub fn auth_ok(seq: u64, username: String, tenant_id: u32) -> Self {
1022        Self {
1023            seq,
1024            status: ResponseStatus::Ok,
1025            columns: None,
1026            rows: None,
1027            rows_affected: None,
1028            watermark_lsn: 0,
1029            error: None,
1030            auth: Some(AuthResponse {
1031                username,
1032                tenant_id,
1033            }),
1034        }
1035    }
1036
1037    /// Create a response with a single "status" column and one row.
1038    pub fn status_row(seq: u64, message: impl Into<String>) -> Self {
1039        Self {
1040            seq,
1041            status: ResponseStatus::Ok,
1042            columns: Some(vec!["status".into()]),
1043            rows: Some(vec![vec![Value::String(message.into())]]),
1044            rows_affected: Some(1),
1045            watermark_lsn: 0,
1046            error: None,
1047            auth: None,
1048        }
1049    }
1050}
1051
1052// ─── Protocol Constants ─────────────────────────────────────────────
1053
1054/// Maximum frame payload size (16 MiB).
1055pub const MAX_FRAME_SIZE: u32 = 16 * 1024 * 1024;
1056
1057/// Length of the frame header (4-byte big-endian u32 payload length).
1058pub const FRAME_HEADER_LEN: usize = 4;
1059
1060/// Default server port for the native protocol.
1061pub const DEFAULT_NATIVE_PORT: u16 = 6433;
1062
1063#[cfg(test)]
1064mod tests {
1065    use super::*;
1066
1067    #[test]
1068    fn opcode_repr() {
1069        assert_eq!(OpCode::Auth as u8, 0x01);
1070        assert_eq!(OpCode::Sql as u8, 0x20);
1071        assert_eq!(OpCode::Begin as u8, 0x40);
1072        assert_eq!(OpCode::GraphHop as u8, 0x50);
1073        assert_eq!(OpCode::TextSearch as u8, 0x60);
1074        assert_eq!(OpCode::VectorBatchInsert as u8, 0x70);
1075    }
1076
1077    #[test]
1078    fn opcode_is_write() {
1079        assert!(OpCode::PointPut.is_write());
1080        assert!(OpCode::PointDelete.is_write());
1081        assert!(OpCode::CrdtApply.is_write());
1082        assert!(OpCode::EdgePut.is_write());
1083        assert!(!OpCode::PointGet.is_write());
1084        assert!(!OpCode::Sql.is_write());
1085        assert!(!OpCode::VectorSearch.is_write());
1086        assert!(!OpCode::Ping.is_write());
1087    }
1088
1089    #[test]
1090    fn response_status_repr() {
1091        assert_eq!(ResponseStatus::Ok as u8, 0);
1092        assert_eq!(ResponseStatus::Partial as u8, 1);
1093        assert_eq!(ResponseStatus::Error as u8, 2);
1094    }
1095
1096    #[test]
1097    fn native_response_ok() {
1098        let r = NativeResponse::ok(42);
1099        assert_eq!(r.seq, 42);
1100        assert_eq!(r.status, ResponseStatus::Ok);
1101        assert!(r.error.is_none());
1102    }
1103
1104    #[test]
1105    fn native_response_error() {
1106        let r = NativeResponse::error(1, "42P01", "collection not found");
1107        assert_eq!(r.status, ResponseStatus::Error);
1108        let e = r.error.unwrap();
1109        assert_eq!(e.code, "42P01");
1110        assert_eq!(e.message, "collection not found");
1111    }
1112
1113    #[test]
1114    fn native_response_from_query_result() {
1115        let qr = crate::result::QueryResult {
1116            columns: vec!["id".into(), "name".into()],
1117            rows: vec![vec![
1118                Value::String("u1".into()),
1119                Value::String("Alice".into()),
1120            ]],
1121            rows_affected: 0,
1122        };
1123        let r = NativeResponse::from_query_result(5, qr, 100);
1124        assert_eq!(r.seq, 5);
1125        assert_eq!(r.watermark_lsn, 100);
1126        assert_eq!(r.columns.as_ref().unwrap().len(), 2);
1127        assert_eq!(r.rows.as_ref().unwrap().len(), 1);
1128    }
1129
1130    #[test]
1131    fn native_response_status_row() {
1132        let r = NativeResponse::status_row(3, "OK");
1133        assert_eq!(r.columns.as_ref().unwrap(), &["status"]);
1134        assert_eq!(r.rows.as_ref().unwrap()[0][0].as_str(), Some("OK"));
1135    }
1136
1137    #[test]
1138    fn msgpack_roundtrip_request() {
1139        let req = NativeRequest {
1140            op: OpCode::Sql,
1141            seq: 1,
1142            fields: RequestFields::Text(TextFields {
1143                sql: Some("SELECT 1".into()),
1144                ..Default::default()
1145            }),
1146        };
1147        let bytes = zerompk::to_msgpack_vec(&req).unwrap();
1148        let decoded: NativeRequest = zerompk::from_msgpack(&bytes).unwrap();
1149        assert_eq!(decoded.op, OpCode::Sql);
1150        assert_eq!(decoded.seq, 1);
1151    }
1152
1153    #[test]
1154    fn msgpack_roundtrip_response() {
1155        let resp = NativeResponse::from_query_result(
1156            7,
1157            crate::result::QueryResult {
1158                columns: vec!["x".into()],
1159                rows: vec![vec![Value::Integer(42)]],
1160                rows_affected: 0,
1161            },
1162            99,
1163        );
1164        let bytes = zerompk::to_msgpack_vec(&resp).unwrap();
1165        let decoded: NativeResponse = zerompk::from_msgpack(&bytes).unwrap();
1166        assert_eq!(decoded.seq, 7);
1167        assert_eq!(decoded.watermark_lsn, 99);
1168        assert_eq!(decoded.rows.unwrap()[0][0].as_i64(), Some(42));
1169    }
1170
1171    #[test]
1172    fn auth_method_variants() {
1173        let trust = AuthMethod::Trust {
1174            username: "admin".into(),
1175        };
1176        let bytes = zerompk::to_msgpack_vec(&trust).unwrap();
1177        let decoded: AuthMethod = zerompk::from_msgpack(&bytes).unwrap();
1178        match decoded {
1179            AuthMethod::Trust { username } => assert_eq!(username, "admin"),
1180            _ => panic!("expected Trust variant"),
1181        }
1182
1183        let pw = AuthMethod::Password {
1184            username: "user".into(),
1185            password: "secret".into(),
1186        };
1187        let bytes = zerompk::to_msgpack_vec(&pw).unwrap();
1188        let decoded: AuthMethod = zerompk::from_msgpack(&bytes).unwrap();
1189        match decoded {
1190            AuthMethod::Password { username, password } => {
1191                assert_eq!(username, "user");
1192                assert_eq!(password, "secret");
1193            }
1194            _ => panic!("expected Password variant"),
1195        }
1196    }
1197}