Skip to main content

powdb_server/
protocol.rs

1use powdb_storage::pj1::pj1_validate;
2use powdb_storage::types::{TypeId, Value};
3use tokio::io::{AsyncReadExt, AsyncWriteExt};
4use zeroize::Zeroizing;
5
6const MSG_CONNECT: u8 = 0x01;
7const MSG_CONNECT_OK: u8 = 0x02;
8const MSG_QUERY: u8 = 0x03;
9/// Query carrying positional `$N` parameters (Task 4). Pure protocol
10/// addition: old clients never send it, and old servers reject it with the
11/// existing "unknown message type" error — no existing frame changes shape.
12const MSG_QUERY_PARAMS: u8 = 0x04;
13/// SQL query frame. Plain Query remains PowQL for backward compatibility.
14const MSG_QUERY_SQL: u8 = 0x05;
15/// Private sync control frames. These are append-only protocol extensions:
16/// legacy query/result frames keep their original tags and shape.
17const MSG_SYNC_STATUS: u8 = 0x20;
18const MSG_SYNC_PULL: u8 = 0x21;
19const MSG_SYNC_ACK: u8 = 0x22;
20const MSG_SYNC_STATUS_RESULT: u8 = 0x23;
21const MSG_SYNC_PULL_RESULT: u8 = 0x24;
22const MSG_SYNC_ACK_RESULT: u8 = 0x25;
23const MSG_RESULT_ROWS: u8 = 0x07;
24const MSG_RESULT_SCALAR: u8 = 0x08;
25const MSG_RESULT_OK: u8 = 0x09;
26const MSG_ERROR: u8 = 0x0A;
27const MSG_RESULT_MSG: u8 = 0x0B;
28const MSG_DISCONNECT: u8 = 0x10;
29const MSG_PING: u8 = 0x11;
30const MSG_PONG: u8 = 0x12;
31/// Native typed PowQL request. The response is `MSG_RESULT_ROWS_NATIVE` or
32/// `MSG_RESULT_SCALAR_NATIVE`; legacy result frames remain byte-identical.
33pub const MSG_QUERY_NATIVE: u8 = 0x13;
34/// Native typed PowQL request with positional `$N` parameters.
35pub const MSG_QUERY_PARAMS_NATIVE: u8 = 0x14;
36/// Native typed SQL request.
37pub const MSG_QUERY_SQL_NATIVE: u8 = 0x15;
38/// Native typed row result.
39pub const MSG_RESULT_ROWS_NATIVE: u8 = 0x16;
40/// Native typed scalar result.
41pub const MSG_RESULT_SCALAR_NATIVE: u8 = 0x17;
42
43/// Maximum payload size accepted from the wire (64 MB).
44const MAX_PAYLOAD_SIZE: usize = 64 * 1024 * 1024;
45
46/// Maximum payload size for pre-auth CONNECT messages (4 KB).
47/// Only a database name and password are needed before authentication.
48const MAX_CONNECT_PAYLOAD_SIZE: usize = 4096;
49
50/// Maximum number of columns allowed in a result set.
51const MAX_COLUMNS: usize = 4096;
52
53/// Maximum number of rows allowed in a single result message.
54const MAX_ROWS: usize = 10_000_000;
55
56/// Maximum number of bound parameters in a single QueryWithParams message.
57const MAX_PARAMS: usize = 4096;
58
59/// Maximum retained units accepted in one sync pull frame.
60const MAX_SYNC_UNITS: usize = 4096;
61
62const STRING_LEN_PREFIX: usize = 4; // decode_string reads a 4-byte length prefix
63
64/// A positional parameter value carried by [`Message::QueryWithParams`].
65///
66/// Wire encoding per param: a 1-byte tag followed by the body —
67///   `0` null (no body), `1` int (8B LE i64), `2` float (8B LE f64),
68///   `3` bool (1B), `4` str (length-prefixed UTF-8).
69#[derive(Debug, Clone, PartialEq)]
70pub enum WireParam {
71    Null,
72    Int(i64),
73    Float(f64),
74    Bool(bool),
75    Str(String),
76}
77
78#[derive(Debug, Clone, Copy, PartialEq, Eq)]
79pub enum WireSyncRepairAction {
80    None,
81    Pull,
82    AwaitArchive,
83    Rebootstrap,
84}
85
86#[derive(Debug, Clone, PartialEq, Eq)]
87pub struct WireSyncStatus {
88    pub replica_id: String,
89    pub active: bool,
90    pub last_applied_lsn: Option<u64>,
91    pub remote_lsn: u64,
92    pub servable_lsn: Option<u64>,
93    pub unarchived_lsn: Option<u64>,
94    pub lag_lsn: Option<u64>,
95    pub lag_bytes: Option<u64>,
96    pub lag_ms: Option<u64>,
97    pub stale: bool,
98    pub repair_action: WireSyncRepairAction,
99    pub last_sync_error: Option<String>,
100}
101
102#[derive(Debug, Clone, PartialEq, Eq)]
103pub struct WireRetainedUnit {
104    pub tx_id: u64,
105    pub record_type: u8,
106    pub lsn: u64,
107    pub data: Vec<u8>,
108}
109
110impl WireRetainedUnit {
111    /// Encoded byte length of this retained unit inside a sync pull result
112    /// payload. Keep this next to `encode_retained_unit` so metrics and
113    /// max-byte enforcement evolve with the wire shape.
114    pub fn encoded_len(&self) -> Result<u64, String> {
115        let data_len = u64::try_from(self.data.len())
116            .map_err(|_| "sync retained unit payload too large".to_string())?;
117        8u64.checked_add(1)
118            .and_then(|n| n.checked_add(8))
119            .and_then(|n| n.checked_add(4))
120            .and_then(|n| n.checked_add(data_len))
121            .ok_or_else(|| "sync retained unit encoded length overflow".to_string())
122    }
123}
124
125#[derive(Debug, Clone)]
126pub enum Message {
127    Connect {
128        db_name: String,
129        /// Client-supplied candidate password. Wrapped in `Zeroizing` so the
130        /// raw bytes from the wire are wiped from memory once the `Message`
131        /// (and thus this field) is dropped after the constant-time compare —
132        /// the candidate never lingers in a plain `String`.
133        password: Option<Zeroizing<String>>,
134        /// Optional user name for multi-user authentication. Appended after the
135        /// password on the wire so the format is a pure, backward-compatible
136        /// extension: old clients that omit it decode as `None`.
137        username: Option<String>,
138    },
139    ConnectOk {
140        version: String,
141    },
142    Query {
143        query: String,
144    },
145    /// A SQL query string.
146    QuerySql {
147        query: String,
148    },
149    /// A query string with positional `$N` parameters bound at the server.
150    QueryWithParams {
151        query: String,
152        params: Vec<WireParam>,
153    },
154    /// PowQL request whose result preserves storage value types.
155    QueryNative {
156        query: String,
157    },
158    /// Parameterized PowQL request whose result preserves storage value types.
159    QueryWithParamsNative {
160        query: String,
161        params: Vec<WireParam>,
162    },
163    /// SQL request whose result preserves storage value types.
164    QuerySqlNative {
165        query: String,
166    },
167    /// Request primary-side status for one embedded replica cursor.
168    SyncStatus {
169        replica_id: String,
170    },
171    /// Pull a bounded retained-unit chunk after the server-side replica cursor.
172    SyncPull {
173        replica_id: String,
174        since_lsn: u64,
175        max_units: u32,
176        max_bytes: u64,
177        database_id: [u8; 16],
178        primary_generation: u64,
179        wal_format_version: u16,
180        catalog_version: u16,
181        segment_format_version: u16,
182    },
183    /// Acknowledge that the replica applied retained history through `applied_lsn`.
184    SyncAck {
185        replica_id: String,
186        applied_lsn: u64,
187        remote_lsn: u64,
188    },
189    SyncStatusResult {
190        status: WireSyncStatus,
191    },
192    SyncPullResult {
193        status: WireSyncStatus,
194        units: Vec<WireRetainedUnit>,
195        has_more: bool,
196    },
197    SyncAckResult {
198        previous_applied_lsn: u64,
199        applied_lsn: u64,
200        remote_lsn: u64,
201        advanced: bool,
202        status: WireSyncStatus,
203    },
204    ResultRows {
205        columns: Vec<String>,
206        rows: Vec<Vec<String>>,
207    },
208    ResultScalar {
209        value: String,
210    },
211    ResultRowsNative {
212        columns: Vec<String>,
213        rows: Vec<Vec<Value>>,
214    },
215    ResultScalarNative {
216        value: Value,
217    },
218    ResultOk {
219        affected: u64,
220    },
221    /// A descriptive status message (e.g. "type User created", "index dropped").
222    ResultMessage {
223        message: String,
224    },
225    Error {
226        message: String,
227    },
228    Disconnect,
229    Ping,
230    Pong,
231}
232
233impl Message {
234    /// Encode message into wire format: `[type(1)][flags(1)][len(4)][payload]`
235    pub fn encode(&self) -> Vec<u8> {
236        let (msg_type, payload) = match self {
237            Message::Connect {
238                db_name,
239                password,
240                username,
241            } => {
242                let mut buf = encode_string(db_name);
243                // Password is encoded as a length-prefixed string. Empty (len=0) means None.
244                match password {
245                    Some(p) => buf.extend_from_slice(&encode_string(p)),
246                    None => buf.extend_from_slice(&0u32.to_le_bytes()),
247                }
248                // Username is appended after the password (append-only extension).
249                // Length-prefixed string; empty (len=0) means None.
250                match username {
251                    Some(u) => buf.extend_from_slice(&encode_string(u)),
252                    None => buf.extend_from_slice(&0u32.to_le_bytes()),
253                }
254                (MSG_CONNECT, buf)
255            }
256            Message::ConnectOk { version } => (MSG_CONNECT_OK, encode_string(version)),
257            Message::Query { query } => (MSG_QUERY, encode_string(query)),
258            Message::QuerySql { query } => (MSG_QUERY_SQL, encode_string(query)),
259            Message::QueryWithParams { query, params } => {
260                (MSG_QUERY_PARAMS, encode_query_with_params(query, params))
261            }
262            Message::QueryNative { query } => (MSG_QUERY_NATIVE, encode_string(query)),
263            Message::QueryWithParamsNative { query, params } => (
264                MSG_QUERY_PARAMS_NATIVE,
265                encode_query_with_params(query, params),
266            ),
267            Message::QuerySqlNative { query } => (MSG_QUERY_SQL_NATIVE, encode_string(query)),
268            Message::SyncStatus { replica_id } => (MSG_SYNC_STATUS, encode_string(replica_id)),
269            Message::SyncPull {
270                replica_id,
271                since_lsn,
272                max_units,
273                max_bytes,
274                database_id,
275                primary_generation,
276                wal_format_version,
277                catalog_version,
278                segment_format_version,
279            } => {
280                let mut buf = encode_string(replica_id);
281                buf.extend_from_slice(&since_lsn.to_le_bytes());
282                buf.extend_from_slice(&max_units.to_le_bytes());
283                buf.extend_from_slice(&max_bytes.to_le_bytes());
284                buf.extend_from_slice(database_id);
285                buf.extend_from_slice(&primary_generation.to_le_bytes());
286                buf.extend_from_slice(&wal_format_version.to_le_bytes());
287                buf.extend_from_slice(&catalog_version.to_le_bytes());
288                buf.extend_from_slice(&segment_format_version.to_le_bytes());
289                (MSG_SYNC_PULL, buf)
290            }
291            Message::SyncAck {
292                replica_id,
293                applied_lsn,
294                remote_lsn,
295            } => {
296                let mut buf = encode_string(replica_id);
297                buf.extend_from_slice(&applied_lsn.to_le_bytes());
298                buf.extend_from_slice(&remote_lsn.to_le_bytes());
299                (MSG_SYNC_ACK, buf)
300            }
301            Message::SyncStatusResult { status } => {
302                (MSG_SYNC_STATUS_RESULT, encode_sync_status(status))
303            }
304            Message::SyncPullResult {
305                status,
306                units,
307                has_more,
308            } => {
309                let mut buf = encode_sync_status(status);
310                buf.extend_from_slice(&(units.len() as u32).to_le_bytes());
311                for unit in units {
312                    encode_retained_unit(&mut buf, unit);
313                }
314                buf.push(u8::from(*has_more));
315                (MSG_SYNC_PULL_RESULT, buf)
316            }
317            Message::SyncAckResult {
318                previous_applied_lsn,
319                applied_lsn,
320                remote_lsn,
321                advanced,
322                status,
323            } => {
324                let mut buf = Vec::new();
325                buf.extend_from_slice(&previous_applied_lsn.to_le_bytes());
326                buf.extend_from_slice(&applied_lsn.to_le_bytes());
327                buf.extend_from_slice(&remote_lsn.to_le_bytes());
328                buf.push(u8::from(*advanced));
329                buf.extend_from_slice(&encode_sync_status(status));
330                (MSG_SYNC_ACK_RESULT, buf)
331            }
332            Message::ResultRows { columns, rows } => {
333                let mut buf = Vec::new();
334                buf.extend_from_slice(&(columns.len() as u16).to_le_bytes());
335                for col in columns {
336                    buf.extend_from_slice(&encode_string(col));
337                }
338                buf.extend_from_slice(&(rows.len() as u32).to_le_bytes());
339                for row in rows {
340                    for val in row {
341                        buf.extend_from_slice(&encode_string(val));
342                    }
343                }
344                (MSG_RESULT_ROWS, buf)
345            }
346            Message::ResultScalar { value } => (MSG_RESULT_SCALAR, encode_string(value)),
347            Message::ResultRowsNative { columns, rows } => {
348                let mut buf = Vec::new();
349                buf.extend_from_slice(&(columns.len() as u16).to_le_bytes());
350                for column in columns {
351                    buf.extend_from_slice(&encode_string(column));
352                }
353                buf.extend_from_slice(&(rows.len() as u32).to_le_bytes());
354                for row in rows {
355                    for value in row {
356                        encode_typed_value(&mut buf, value);
357                    }
358                }
359                (MSG_RESULT_ROWS_NATIVE, buf)
360            }
361            Message::ResultScalarNative { value } => {
362                let mut buf = Vec::new();
363                encode_typed_value(&mut buf, value);
364                (MSG_RESULT_SCALAR_NATIVE, buf)
365            }
366            Message::ResultOk { affected } => (MSG_RESULT_OK, affected.to_le_bytes().to_vec()),
367            Message::ResultMessage { message } => (MSG_RESULT_MSG, encode_string(message)),
368            Message::Error { message } => (MSG_ERROR, encode_string(message)),
369            Message::Disconnect => (MSG_DISCONNECT, Vec::new()),
370            Message::Ping => (MSG_PING, Vec::new()),
371            Message::Pong => (MSG_PONG, Vec::new()),
372        };
373
374        let mut frame = Vec::with_capacity(6 + payload.len());
375        frame.push(msg_type);
376        frame.push(0); // flags
377        frame.extend_from_slice(&(payload.len() as u32).to_le_bytes());
378        frame.extend_from_slice(&payload);
379        frame
380    }
381
382    /// Decode message from wire format.
383    pub fn decode(data: &[u8]) -> Result<Message, String> {
384        if data.len() < 6 {
385            return Err("frame too short".into());
386        }
387        let msg_type = data[0];
388        let _flags = data[1];
389        let len_bytes: [u8; 4] = data[2..6]
390            .try_into()
391            .map_err(|_| "invalid header length field".to_string())?;
392        let payload_len = u32::from_le_bytes(len_bytes) as usize;
393        if 6 + payload_len > data.len() {
394            return Err("payload length exceeds frame".into());
395        }
396        let payload = &data[6..6 + payload_len];
397
398        match msg_type {
399            MSG_CONNECT => {
400                let mut pos = 0;
401                let db_name = decode_string(payload, &mut pos)?;
402                // Password is optional. If there are no more bytes, treat as None
403                // (backwards compatible with old clients that don't send a password).
404                let password = if pos < payload.len() {
405                    // Wrap the candidate immediately so the only owned copy of
406                    // the secret lives inside `Zeroizing` and is wiped on drop.
407                    let p = Zeroizing::new(decode_string(payload, &mut pos)?);
408                    if p.is_empty() {
409                        None
410                    } else {
411                        Some(p)
412                    }
413                } else {
414                    None
415                };
416                // Username is optional and appended after the password. Old
417                // clients omit it entirely → no more bytes → None.
418                let username = if pos < payload.len() {
419                    let u = decode_string(payload, &mut pos)?;
420                    if u.is_empty() {
421                        None
422                    } else {
423                        Some(u)
424                    }
425                } else {
426                    None
427                };
428                Ok(Message::Connect {
429                    db_name,
430                    password,
431                    username,
432                })
433            }
434            MSG_CONNECT_OK => {
435                let version = decode_string(payload, &mut 0)?;
436                Ok(Message::ConnectOk { version })
437            }
438            MSG_QUERY => {
439                let query = decode_string(payload, &mut 0)?;
440                Ok(Message::Query { query })
441            }
442            MSG_QUERY_SQL => {
443                let query = decode_string(payload, &mut 0)?;
444                Ok(Message::QuerySql { query })
445            }
446            MSG_QUERY_PARAMS => {
447                let mut pos = 0;
448                let query = decode_string(payload, &mut pos)?;
449                if pos + 2 > payload.len() {
450                    return Err("truncated param count".into());
451                }
452                let count_bytes: [u8; 2] = payload[pos..pos + 2]
453                    .try_into()
454                    .map_err(|_| "invalid param count bytes".to_string())?;
455                let count = u16::from_le_bytes(count_bytes) as usize;
456                pos += 2;
457                if count > MAX_PARAMS {
458                    return Err("too many parameters".into());
459                }
460                let mut params = Vec::with_capacity(count.min(payload.len() - pos));
461                for _ in 0..count {
462                    if pos >= payload.len() {
463                        return Err("truncated param tag".into());
464                    }
465                    let tag = payload[pos];
466                    pos += 1;
467                    let p = match tag {
468                        0 => WireParam::Null,
469                        1 => {
470                            if pos + 8 > payload.len() {
471                                return Err("truncated int param".into());
472                            }
473                            let b: [u8; 8] = payload[pos..pos + 8]
474                                .try_into()
475                                .map_err(|_| "invalid int param bytes".to_string())?;
476                            pos += 8;
477                            WireParam::Int(i64::from_le_bytes(b))
478                        }
479                        2 => {
480                            if pos + 8 > payload.len() {
481                                return Err("truncated float param".into());
482                            }
483                            let b: [u8; 8] = payload[pos..pos + 8]
484                                .try_into()
485                                .map_err(|_| "invalid float param bytes".to_string())?;
486                            pos += 8;
487                            WireParam::Float(f64::from_le_bytes(b))
488                        }
489                        3 => {
490                            if pos + 1 > payload.len() {
491                                return Err("truncated bool param".into());
492                            }
493                            let v = payload[pos] != 0;
494                            pos += 1;
495                            WireParam::Bool(v)
496                        }
497                        4 => WireParam::Str(decode_string(payload, &mut pos)?),
498                        other => return Err(format!("unknown param tag: {other}")),
499                    };
500                    params.push(p);
501                }
502                Ok(Message::QueryWithParams { query, params })
503            }
504            MSG_QUERY_NATIVE => {
505                let query = decode_exact_string(payload, "native PowQL query")?;
506                Ok(Message::QueryNative { query })
507            }
508            MSG_QUERY_PARAMS_NATIVE => {
509                let (query, params) = decode_query_with_params_exact(payload)?;
510                Ok(Message::QueryWithParamsNative { query, params })
511            }
512            MSG_QUERY_SQL_NATIVE => {
513                let query = decode_exact_string(payload, "native SQL query")?;
514                Ok(Message::QuerySqlNative { query })
515            }
516            MSG_SYNC_STATUS => {
517                let replica_id = decode_string(payload, &mut 0)?;
518                Ok(Message::SyncStatus { replica_id })
519            }
520            MSG_SYNC_PULL => {
521                let mut pos = 0;
522                let replica_id = decode_string(payload, &mut pos)?;
523                let since_lsn = decode_u64(payload, &mut pos, "sync pull since LSN")?;
524                let max_units = decode_u32(payload, &mut pos, "sync pull max units")?;
525                let max_bytes = decode_u64(payload, &mut pos, "sync pull max bytes")?;
526                let database_id = decode_16_bytes(payload, &mut pos, "sync database id")?;
527                let primary_generation = decode_u64(payload, &mut pos, "sync primary generation")?;
528                let wal_format_version = decode_u16(payload, &mut pos, "sync WAL format version")?;
529                let catalog_version = decode_u16(payload, &mut pos, "sync catalog version")?;
530                let segment_format_version =
531                    decode_u16(payload, &mut pos, "sync segment format version")?;
532                Ok(Message::SyncPull {
533                    replica_id,
534                    since_lsn,
535                    max_units,
536                    max_bytes,
537                    database_id,
538                    primary_generation,
539                    wal_format_version,
540                    catalog_version,
541                    segment_format_version,
542                })
543            }
544            MSG_SYNC_ACK => {
545                let mut pos = 0;
546                let replica_id = decode_string(payload, &mut pos)?;
547                let applied_lsn = decode_u64(payload, &mut pos, "sync ack applied LSN")?;
548                let remote_lsn = decode_u64(payload, &mut pos, "sync ack remote LSN")?;
549                Ok(Message::SyncAck {
550                    replica_id,
551                    applied_lsn,
552                    remote_lsn,
553                })
554            }
555            MSG_SYNC_STATUS_RESULT => {
556                let mut pos = 0;
557                let status = decode_sync_status(payload, &mut pos)?;
558                Ok(Message::SyncStatusResult { status })
559            }
560            MSG_SYNC_PULL_RESULT => {
561                let mut pos = 0;
562                let status = decode_sync_status(payload, &mut pos)?;
563                let count = decode_u32(payload, &mut pos, "sync retained unit count")? as usize;
564                if count > MAX_SYNC_UNITS {
565                    return Err("too many retained units".into());
566                }
567                let mut units = Vec::with_capacity(count.min(payload.len().saturating_sub(pos)));
568                for _ in 0..count {
569                    units.push(decode_retained_unit(payload, &mut pos)?);
570                }
571                let has_more = decode_bool(payload, &mut pos, "sync has_more")?;
572                Ok(Message::SyncPullResult {
573                    status,
574                    units,
575                    has_more,
576                })
577            }
578            MSG_SYNC_ACK_RESULT => {
579                let mut pos = 0;
580                let previous_applied_lsn = decode_u64(payload, &mut pos, "previous applied LSN")?;
581                let applied_lsn = decode_u64(payload, &mut pos, "applied LSN")?;
582                let remote_lsn = decode_u64(payload, &mut pos, "remote LSN")?;
583                let advanced = decode_bool(payload, &mut pos, "sync ack advanced")?;
584                let status = decode_sync_status(payload, &mut pos)?;
585                Ok(Message::SyncAckResult {
586                    previous_applied_lsn,
587                    applied_lsn,
588                    remote_lsn,
589                    advanced,
590                    status,
591                })
592            }
593            MSG_RESULT_ROWS => {
594                let mut pos = 0;
595                if pos + 2 > payload.len() {
596                    return Err("truncated column count".into());
597                }
598                let col_bytes: [u8; 2] = payload[pos..pos + 2]
599                    .try_into()
600                    .map_err(|_| "invalid column count bytes".to_string())?;
601                let col_count = u16::from_le_bytes(col_bytes) as usize;
602                pos += 2;
603                if col_count > MAX_COLUMNS {
604                    return Err("too many columns".into());
605                }
606                let mut columns =
607                    Vec::with_capacity(col_count.min((payload.len() - pos) / STRING_LEN_PREFIX));
608                for _ in 0..col_count {
609                    columns.push(decode_string(payload, &mut pos)?);
610                }
611                if pos + 4 > payload.len() {
612                    return Err("truncated row count".into());
613                }
614                let row_bytes: [u8; 4] = payload[pos..pos + 4]
615                    .try_into()
616                    .map_err(|_| "invalid row count bytes".to_string())?;
617                let row_count = u32::from_le_bytes(row_bytes) as usize;
618                pos += 4;
619                if row_count > MAX_ROWS {
620                    return Err("too many rows".into());
621                }
622                // Never preallocate (or iterate) proportional to an untrusted count: each row
623                // carries `col_count` length-prefixed strings of >= STRING_LEN_PREFIX bytes, so
624                // the remaining payload bounds how many rows can follow. A zero-column row
625                // consumes no bytes (vacuous bound), so a tiny frame could otherwise declare
626                // millions of rows and force a huge allocation (reachable pre-auth). Reject it.
627                let max_rows = match col_count.checked_mul(STRING_LEN_PREFIX) {
628                    Some(0) | None => 0,
629                    Some(per_row) => (payload.len() - pos) / per_row,
630                };
631                if row_count > max_rows {
632                    return Err("row count exceeds payload size".into());
633                }
634                let mut rows = Vec::with_capacity(row_count);
635                for _ in 0..row_count {
636                    let mut row = Vec::with_capacity(col_count);
637                    for _ in 0..col_count {
638                        row.push(decode_string(payload, &mut pos)?);
639                    }
640                    rows.push(row);
641                }
642                Ok(Message::ResultRows { columns, rows })
643            }
644            MSG_RESULT_SCALAR => {
645                let value = decode_string(payload, &mut 0)?;
646                Ok(Message::ResultScalar { value })
647            }
648            MSG_RESULT_ROWS_NATIVE => decode_native_rows(payload),
649            MSG_RESULT_SCALAR_NATIVE => {
650                let mut pos = 0;
651                let value = decode_typed_value(payload, &mut pos)?;
652                require_payload_end(payload, pos, "native scalar")?;
653                Ok(Message::ResultScalarNative { value })
654            }
655            MSG_RESULT_OK => {
656                if payload.len() < 8 {
657                    return Err("truncated result ok payload".into());
658                }
659                let aff_bytes: [u8; 8] = payload[0..8]
660                    .try_into()
661                    .map_err(|_| "invalid affected count bytes".to_string())?;
662                let affected = u64::from_le_bytes(aff_bytes);
663                Ok(Message::ResultOk { affected })
664            }
665            MSG_RESULT_MSG => {
666                let message = decode_string(payload, &mut 0)?;
667                Ok(Message::ResultMessage { message })
668            }
669            MSG_ERROR => {
670                let message = decode_string(payload, &mut 0)?;
671                Ok(Message::Error { message })
672            }
673            MSG_DISCONNECT => Ok(Message::Disconnect),
674            MSG_PING => Ok(Message::Ping),
675            MSG_PONG => Ok(Message::Pong),
676            _ => Err(format!("unknown message type: {msg_type:#x}")),
677        }
678    }
679
680    /// Write this message to an async writer.
681    pub async fn write_to<W: AsyncWriteExt + Unpin>(&self, writer: &mut W) -> std::io::Result<()> {
682        let bytes = self.encode();
683        writer.write_all(&bytes).await
684    }
685
686    /// Read a message from an async reader.
687    pub async fn read_from<R: AsyncReadExt + Unpin>(
688        reader: &mut R,
689    ) -> std::io::Result<Option<Message>> {
690        Self::read_from_with_limit(reader, MAX_PAYLOAD_SIZE).await
691    }
692
693    /// Read a pre-auth message with a smaller payload limit (4 KB).
694    /// Use this before authentication is complete to prevent oversized
695    /// CONNECT payloads from consuming server memory.
696    pub async fn read_from_preauth<R: AsyncReadExt + Unpin>(
697        reader: &mut R,
698    ) -> std::io::Result<Option<Message>> {
699        Self::read_from_with_limit(reader, MAX_CONNECT_PAYLOAD_SIZE).await
700    }
701
702    /// Read a message from an async reader with a configurable payload limit.
703    async fn read_from_with_limit<R: AsyncReadExt + Unpin>(
704        reader: &mut R,
705        max_payload: usize,
706    ) -> std::io::Result<Option<Message>> {
707        let mut header = [0u8; 6];
708        match reader.read_exact(&mut header).await {
709            Ok(_) => {}
710            Err(e) if e.kind() == std::io::ErrorKind::UnexpectedEof => return Ok(None),
711            Err(e) => return Err(e),
712        }
713        let len_bytes: [u8; 4] = header[2..6].try_into().map_err(|_| {
714            std::io::Error::new(
715                std::io::ErrorKind::InvalidData,
716                "invalid header length field",
717            )
718        })?;
719        let payload_len = u32::from_le_bytes(len_bytes) as usize;
720        if payload_len > max_payload {
721            return Err(std::io::Error::new(
722                std::io::ErrorKind::InvalidData,
723                format!("payload too large: {payload_len} bytes (max {max_payload})"),
724            ));
725        }
726        let mut payload = vec![0u8; payload_len];
727        if payload_len > 0 {
728            reader.read_exact(&mut payload).await?;
729        }
730
731        let mut full = Vec::with_capacity(6 + payload_len);
732        full.extend_from_slice(&header);
733        full.extend_from_slice(&payload);
734
735        Message::decode(&full)
736            .map(Some)
737            .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))
738    }
739}
740
741fn encode_query_with_params(query: &str, params: &[WireParam]) -> Vec<u8> {
742    let mut buf = encode_string(query);
743    buf.extend_from_slice(&(params.len() as u16).to_le_bytes());
744    for param in params {
745        match param {
746            WireParam::Null => buf.push(0),
747            WireParam::Int(value) => {
748                buf.push(1);
749                buf.extend_from_slice(&value.to_le_bytes());
750            }
751            WireParam::Float(value) => {
752                buf.push(2);
753                buf.extend_from_slice(&value.to_le_bytes());
754            }
755            WireParam::Bool(value) => {
756                buf.push(3);
757                buf.push(u8::from(*value));
758            }
759            WireParam::Str(value) => {
760                buf.push(4);
761                buf.extend_from_slice(&encode_string(value));
762            }
763        }
764    }
765    buf
766}
767
768fn decode_query_with_params_exact(payload: &[u8]) -> Result<(String, Vec<WireParam>), String> {
769    let mut pos = 0;
770    let query = decode_string_strict(payload, &mut pos, "native query")?;
771    let count = decode_u16(payload, &mut pos, "native param count")? as usize;
772    if count > MAX_PARAMS {
773        return Err("too many parameters".into());
774    }
775    // Every parameter consumes at least its one-byte tag.
776    if count > payload.len().saturating_sub(pos) {
777        return Err("parameter count exceeds payload size".into());
778    }
779    let mut params = Vec::with_capacity(count);
780    for _ in 0..count {
781        if pos >= payload.len() {
782            return Err("truncated param tag".into());
783        }
784        let tag = payload[pos];
785        pos += 1;
786        params.push(match tag {
787            0 => WireParam::Null,
788            1 => {
789                let bytes = take_exact(payload, &mut pos, 8, "int param")?;
790                WireParam::Int(i64::from_le_bytes(bytes.try_into().expect("8 bytes")))
791            }
792            2 => {
793                let bytes = take_exact(payload, &mut pos, 8, "float param")?;
794                WireParam::Float(f64::from_le_bytes(bytes.try_into().expect("8 bytes")))
795            }
796            3 => WireParam::Bool(decode_bool(payload, &mut pos, "bool param")?),
797            4 => WireParam::Str(decode_string_strict(payload, &mut pos, "string param")?),
798            other => return Err(format!("unknown param tag: {other}")),
799        });
800    }
801    require_payload_end(payload, pos, "native parameterized query")?;
802    Ok((query, params))
803}
804
805fn encode_typed_value(out: &mut Vec<u8>, value: &Value) {
806    out.push(value.type_id() as u8);
807    let body_len = match value {
808        Value::Empty => 0,
809        Value::Int(_) | Value::Float(_) | Value::DateTime(_) => 8,
810        Value::Bool(_) => 1,
811        Value::Str(value) => value.len(),
812        Value::Uuid(_) => 16,
813        Value::Bytes(value) => value.len(),
814        Value::Json(value) => value.len(),
815    };
816    out.extend_from_slice(&(body_len as u32).to_le_bytes());
817    match value {
818        Value::Empty => {}
819        Value::Int(value) | Value::DateTime(value) => out.extend_from_slice(&value.to_le_bytes()),
820        Value::Float(value) => out.extend_from_slice(&value.to_le_bytes()),
821        Value::Bool(value) => out.push(u8::from(*value)),
822        Value::Str(value) => out.extend_from_slice(value.as_bytes()),
823        Value::Uuid(value) => out.extend_from_slice(value),
824        Value::Bytes(value) => out.extend_from_slice(value),
825        Value::Json(value) => out.extend_from_slice(value),
826    }
827}
828
829fn decode_typed_value(data: &[u8], pos: &mut usize) -> Result<Value, String> {
830    if *pos >= data.len() {
831        return Err("truncated typed value tag".into());
832    }
833    let raw_type = data[*pos];
834    *pos += 1;
835    let type_id =
836        TypeId::from_u8(raw_type).ok_or_else(|| format!("unknown typed value tag: {raw_type}"))?;
837    let body_len = decode_u32(data, pos, "typed value body length")? as usize;
838    let body = take_exact(data, pos, body_len, "typed value body")?;
839
840    let require_len = |expected: usize| {
841        if body_len == expected {
842            Ok(())
843        } else {
844            Err(format!(
845                "invalid {type_id:?} typed value length: expected {expected}, got {body_len}"
846            ))
847        }
848    };
849    match type_id {
850        TypeId::Empty => {
851            require_len(0)?;
852            Ok(Value::Empty)
853        }
854        TypeId::Int => {
855            require_len(8)?;
856            Ok(Value::Int(i64::from_le_bytes(
857                body.try_into().expect("validated 8-byte int"),
858            )))
859        }
860        TypeId::Float => {
861            require_len(8)?;
862            Ok(Value::Float(f64::from_le_bytes(
863                body.try_into().expect("validated 8-byte float"),
864            )))
865        }
866        TypeId::Bool => {
867            require_len(1)?;
868            match body[0] {
869                0 => Ok(Value::Bool(false)),
870                1 => Ok(Value::Bool(true)),
871                other => Err(format!("invalid typed boolean: {other}")),
872            }
873        }
874        TypeId::Str => Ok(Value::Str(
875            std::str::from_utf8(body)
876                .map_err(|error| format!("invalid UTF-8 in typed string: {error}"))?
877                .to_owned(),
878        )),
879        TypeId::DateTime => {
880            require_len(8)?;
881            Ok(Value::DateTime(i64::from_le_bytes(
882                body.try_into().expect("validated 8-byte datetime"),
883            )))
884        }
885        TypeId::Uuid => {
886            require_len(16)?;
887            Ok(Value::Uuid(
888                body.try_into().expect("validated 16-byte UUID"),
889            ))
890        }
891        TypeId::Bytes => Ok(Value::Bytes(body.to_vec())),
892        TypeId::Json => {
893            pj1_validate(body).map_err(|error| format!("invalid typed PJ1 JSON: {error}"))?;
894            Ok(Value::Json(body.into()))
895        }
896    }
897}
898
899fn decode_native_rows(payload: &[u8]) -> Result<Message, String> {
900    let mut pos = 0;
901    let col_count = decode_u16(payload, &mut pos, "native column count")? as usize;
902    if col_count > MAX_COLUMNS {
903        return Err("too many columns".into());
904    }
905    let mut columns = Vec::with_capacity(col_count);
906    for _ in 0..col_count {
907        columns.push(decode_string_strict(
908            payload,
909            &mut pos,
910            "native column name",
911        )?);
912    }
913    let row_count = decode_u32(payload, &mut pos, "native row count")? as usize;
914    if row_count > MAX_ROWS {
915        return Err("too many rows".into());
916    }
917    // Every cell has at least a one-byte type and four-byte body length.
918    let minimum_row_len = col_count
919        .checked_mul(5)
920        .ok_or_else(|| "native row width overflow".to_string())?;
921    if minimum_row_len == 0 {
922        if row_count != 0 {
923            return Err("nonzero native row count with zero columns".into());
924        }
925    } else if row_count > payload.len().saturating_sub(pos) / minimum_row_len {
926        return Err("native row count exceeds payload size".into());
927    }
928
929    let mut rows = Vec::with_capacity(row_count);
930    for _ in 0..row_count {
931        let mut row = Vec::with_capacity(col_count);
932        for _ in 0..col_count {
933            row.push(decode_typed_value(payload, &mut pos)?);
934        }
935        rows.push(row);
936    }
937    require_payload_end(payload, pos, "native rows")?;
938    Ok(Message::ResultRowsNative { columns, rows })
939}
940
941fn take_exact<'a>(
942    data: &'a [u8],
943    pos: &mut usize,
944    len: usize,
945    label: &str,
946) -> Result<&'a [u8], String> {
947    let end = pos
948        .checked_add(len)
949        .ok_or_else(|| format!("{label} length overflow"))?;
950    if end > data.len() {
951        return Err(format!("truncated {label}"));
952    }
953    let bytes = &data[*pos..end];
954    *pos = end;
955    Ok(bytes)
956}
957
958fn require_payload_end(payload: &[u8], pos: usize, label: &str) -> Result<(), String> {
959    if pos == payload.len() {
960        Ok(())
961    } else {
962        Err(format!("trailing bytes in {label} payload"))
963    }
964}
965
966fn decode_exact_string(payload: &[u8], label: &str) -> Result<String, String> {
967    let mut pos = 0;
968    let value = decode_string_strict(payload, &mut pos, label)?;
969    require_payload_end(payload, pos, label)?;
970    Ok(value)
971}
972
973fn decode_string_strict(data: &[u8], pos: &mut usize, label: &str) -> Result<String, String> {
974    let len = decode_u32(data, pos, label)? as usize;
975    let bytes = take_exact(data, pos, len, label)?;
976    std::str::from_utf8(bytes)
977        .map(str::to_owned)
978        .map_err(|error| format!("invalid UTF-8 in {label}: {error}"))
979}
980
981fn encode_string(s: &str) -> Vec<u8> {
982    let mut buf = Vec::with_capacity(4 + s.len());
983    buf.extend_from_slice(&(s.len() as u32).to_le_bytes());
984    buf.extend_from_slice(s.as_bytes());
985    buf
986}
987
988fn decode_string(data: &[u8], pos: &mut usize) -> Result<String, String> {
989    if *pos + 4 > data.len() {
990        return Err("truncated string length".into());
991    }
992    let len_bytes: [u8; 4] = data[*pos..*pos + 4]
993        .try_into()
994        .map_err(|_| "invalid string length bytes".to_string())?;
995    let len = u32::from_le_bytes(len_bytes) as usize;
996    *pos += 4;
997    if *pos + len > data.len() {
998        return Err("truncated string data".into());
999    }
1000    let s = String::from_utf8_lossy(&data[*pos..*pos + len]).into_owned();
1001    *pos += len;
1002    Ok(s)
1003}
1004
1005fn encode_option_u64(out: &mut Vec<u8>, value: Option<u64>) {
1006    match value {
1007        Some(value) => {
1008            out.push(1);
1009            out.extend_from_slice(&value.to_le_bytes());
1010        }
1011        None => out.push(0),
1012    }
1013}
1014
1015fn decode_option_u64(data: &[u8], pos: &mut usize, label: &str) -> Result<Option<u64>, String> {
1016    let present = decode_bool(data, pos, label)?;
1017    if present {
1018        Ok(Some(decode_u64(data, pos, label)?))
1019    } else {
1020        Ok(None)
1021    }
1022}
1023
1024fn encode_option_string(out: &mut Vec<u8>, value: Option<&String>) {
1025    match value {
1026        Some(value) => {
1027            out.push(1);
1028            out.extend_from_slice(&encode_string(value));
1029        }
1030        None => out.push(0),
1031    }
1032}
1033
1034fn decode_option_string(
1035    data: &[u8],
1036    pos: &mut usize,
1037    label: &str,
1038) -> Result<Option<String>, String> {
1039    let present = decode_bool(data, pos, label)?;
1040    if present {
1041        Ok(Some(decode_string(data, pos)?))
1042    } else {
1043        Ok(None)
1044    }
1045}
1046
1047fn encode_sync_status(status: &WireSyncStatus) -> Vec<u8> {
1048    let mut out = Vec::new();
1049    out.extend_from_slice(&encode_string(&status.replica_id));
1050    out.push(u8::from(status.active));
1051    encode_option_u64(&mut out, status.last_applied_lsn);
1052    out.extend_from_slice(&status.remote_lsn.to_le_bytes());
1053    encode_option_u64(&mut out, status.servable_lsn);
1054    encode_option_u64(&mut out, status.unarchived_lsn);
1055    encode_option_u64(&mut out, status.lag_lsn);
1056    encode_option_u64(&mut out, status.lag_bytes);
1057    encode_option_u64(&mut out, status.lag_ms);
1058    out.push(u8::from(status.stale));
1059    out.push(match status.repair_action {
1060        WireSyncRepairAction::None => 0,
1061        WireSyncRepairAction::Pull => 1,
1062        WireSyncRepairAction::AwaitArchive => 2,
1063        WireSyncRepairAction::Rebootstrap => 3,
1064    });
1065    encode_option_string(&mut out, status.last_sync_error.as_ref());
1066    out
1067}
1068
1069fn decode_sync_status(data: &[u8], pos: &mut usize) -> Result<WireSyncStatus, String> {
1070    let replica_id = decode_string(data, pos)?;
1071    let active = decode_bool(data, pos, "sync status active")?;
1072    let last_applied_lsn = decode_option_u64(data, pos, "sync status last applied LSN")?;
1073    let remote_lsn = decode_u64(data, pos, "sync status remote LSN")?;
1074    let servable_lsn = decode_option_u64(data, pos, "sync status servable LSN")?;
1075    let unarchived_lsn = decode_option_u64(data, pos, "sync status unarchived LSN")?;
1076    let lag_lsn = decode_option_u64(data, pos, "sync status lag LSN")?;
1077    let lag_bytes = decode_option_u64(data, pos, "sync status lag bytes")?;
1078    let lag_ms = decode_option_u64(data, pos, "sync status lag milliseconds")?;
1079    let stale = decode_bool(data, pos, "sync status stale")?;
1080    if *pos >= data.len() {
1081        return Err("truncated sync repair action".into());
1082    }
1083    let repair_action = match data[*pos] {
1084        0 => WireSyncRepairAction::None,
1085        1 => WireSyncRepairAction::Pull,
1086        2 => WireSyncRepairAction::AwaitArchive,
1087        3 => WireSyncRepairAction::Rebootstrap,
1088        other => return Err(format!("unknown sync repair action: {other}")),
1089    };
1090    *pos += 1;
1091    let last_sync_error = decode_option_string(data, pos, "sync status last error")?;
1092    Ok(WireSyncStatus {
1093        replica_id,
1094        active,
1095        last_applied_lsn,
1096        remote_lsn,
1097        servable_lsn,
1098        unarchived_lsn,
1099        lag_lsn,
1100        lag_bytes,
1101        lag_ms,
1102        stale,
1103        repair_action,
1104        last_sync_error,
1105    })
1106}
1107
1108fn encode_retained_unit(out: &mut Vec<u8>, unit: &WireRetainedUnit) {
1109    out.extend_from_slice(&unit.tx_id.to_le_bytes());
1110    out.push(unit.record_type);
1111    out.extend_from_slice(&unit.lsn.to_le_bytes());
1112    out.extend_from_slice(&(unit.data.len() as u32).to_le_bytes());
1113    out.extend_from_slice(&unit.data);
1114}
1115
1116fn decode_retained_unit(data: &[u8], pos: &mut usize) -> Result<WireRetainedUnit, String> {
1117    let tx_id = decode_u64(data, pos, "sync retained unit tx id")?;
1118    if *pos >= data.len() {
1119        return Err("truncated sync retained unit record type".into());
1120    }
1121    let record_type = data[*pos];
1122    *pos += 1;
1123    let lsn = decode_u64(data, pos, "sync retained unit LSN")?;
1124    let data = decode_bytes(data, pos, "sync retained unit payload")?;
1125    Ok(WireRetainedUnit {
1126        tx_id,
1127        record_type,
1128        lsn,
1129        data,
1130    })
1131}
1132
1133fn decode_bool(data: &[u8], pos: &mut usize, label: &str) -> Result<bool, String> {
1134    if *pos >= data.len() {
1135        return Err(format!("truncated {label}"));
1136    }
1137    let raw = data[*pos];
1138    *pos += 1;
1139    match raw {
1140        0 => Ok(false),
1141        1 => Ok(true),
1142        other => Err(format!("invalid boolean for {label}: {other}")),
1143    }
1144}
1145
1146fn decode_u16(data: &[u8], pos: &mut usize, label: &str) -> Result<u16, String> {
1147    if *pos + 2 > data.len() {
1148        return Err(format!("truncated {label}"));
1149    }
1150    let bytes: [u8; 2] = data[*pos..*pos + 2]
1151        .try_into()
1152        .map_err(|_| format!("invalid {label} bytes"))?;
1153    *pos += 2;
1154    Ok(u16::from_le_bytes(bytes))
1155}
1156
1157fn decode_u32(data: &[u8], pos: &mut usize, label: &str) -> Result<u32, String> {
1158    if *pos + 4 > data.len() {
1159        return Err(format!("truncated {label}"));
1160    }
1161    let bytes: [u8; 4] = data[*pos..*pos + 4]
1162        .try_into()
1163        .map_err(|_| format!("invalid {label} bytes"))?;
1164    *pos += 4;
1165    Ok(u32::from_le_bytes(bytes))
1166}
1167
1168fn decode_u64(data: &[u8], pos: &mut usize, label: &str) -> Result<u64, String> {
1169    if *pos + 8 > data.len() {
1170        return Err(format!("truncated {label}"));
1171    }
1172    let bytes: [u8; 8] = data[*pos..*pos + 8]
1173        .try_into()
1174        .map_err(|_| format!("invalid {label} bytes"))?;
1175    *pos += 8;
1176    Ok(u64::from_le_bytes(bytes))
1177}
1178
1179fn decode_16_bytes(data: &[u8], pos: &mut usize, label: &str) -> Result<[u8; 16], String> {
1180    if *pos + 16 > data.len() {
1181        return Err(format!("truncated {label}"));
1182    }
1183    let mut out = [0u8; 16];
1184    out.copy_from_slice(&data[*pos..*pos + 16]);
1185    *pos += 16;
1186    Ok(out)
1187}
1188
1189fn decode_bytes(data: &[u8], pos: &mut usize, label: &str) -> Result<Vec<u8>, String> {
1190    let len = decode_u32(data, pos, label)? as usize;
1191    if *pos + len > data.len() {
1192        return Err(format!("truncated {label}"));
1193    }
1194    let out = data[*pos..*pos + len].to_vec();
1195    *pos += len;
1196    Ok(out)
1197}
1198
1199#[cfg(test)]
1200mod tests {
1201    use super::*;
1202
1203    #[test]
1204    fn test_encode_decode_query() {
1205        let msg = Message::Query {
1206            query: "User filter .age > 30".into(),
1207        };
1208        let bytes = msg.encode();
1209        let decoded = Message::decode(&bytes).unwrap();
1210        match decoded {
1211            Message::Query { query } => assert_eq!(query, "User filter .age > 30"),
1212            _ => panic!("expected Query"),
1213        }
1214    }
1215
1216    #[test]
1217    fn test_encode_decode_connect_with_username() {
1218        let msg = Message::Connect {
1219            db_name: "mydb".into(),
1220            password: Some(Zeroizing::new("secret".into())),
1221            username: Some("alice".into()),
1222        };
1223        let bytes = msg.encode();
1224        match Message::decode(&bytes).unwrap() {
1225            Message::Connect {
1226                db_name,
1227                password,
1228                username,
1229            } => {
1230                assert_eq!(db_name, "mydb");
1231                assert_eq!(password.as_deref().map(|s| s.as_str()), Some("secret"));
1232                assert_eq!(username.as_deref(), Some("alice"));
1233            }
1234            other => panic!("expected Connect, got {other:?}"),
1235        }
1236    }
1237
1238    #[test]
1239    fn test_encode_decode_connect_without_username() {
1240        // New-format Connect that explicitly carries no username.
1241        let msg = Message::Connect {
1242            db_name: "mydb".into(),
1243            password: Some(Zeroizing::new("secret".into())),
1244            username: None,
1245        };
1246        let bytes = msg.encode();
1247        match Message::decode(&bytes).unwrap() {
1248            Message::Connect {
1249                db_name,
1250                password,
1251                username,
1252            } => {
1253                assert_eq!(db_name, "mydb");
1254                assert_eq!(password.as_deref().map(|s| s.as_str()), Some("secret"));
1255                assert_eq!(username, None);
1256            }
1257            other => panic!("expected Connect, got {other:?}"),
1258        }
1259    }
1260
1261    #[test]
1262    fn test_decode_old_client_connect_db_and_password_only() {
1263        // Simulate an OLD client frame: db_name + password, with NO username
1264        // bytes at all. Must decode with username: None (backward compat).
1265        let mut payload = encode_string("mydb");
1266        payload.extend_from_slice(&encode_string("pw"));
1267        let mut frame = vec![MSG_CONNECT, 0];
1268        frame.extend_from_slice(&(payload.len() as u32).to_le_bytes());
1269        frame.extend_from_slice(&payload);
1270        match Message::decode(&frame).unwrap() {
1271            Message::Connect {
1272                db_name,
1273                password,
1274                username,
1275            } => {
1276                assert_eq!(db_name, "mydb");
1277                assert_eq!(password.as_deref().map(|s| s.as_str()), Some("pw"));
1278                assert_eq!(username, None, "old-client frame must yield username=None");
1279            }
1280            other => panic!("expected Connect, got {other:?}"),
1281        }
1282    }
1283
1284    #[test]
1285    fn test_decode_old_client_connect_db_only() {
1286        // Oldest client: db_name only, no password and no username bytes.
1287        let payload = encode_string("mydb");
1288        let mut frame = vec![MSG_CONNECT, 0];
1289        frame.extend_from_slice(&(payload.len() as u32).to_le_bytes());
1290        frame.extend_from_slice(&payload);
1291        match Message::decode(&frame).unwrap() {
1292            Message::Connect {
1293                db_name,
1294                password,
1295                username,
1296            } => {
1297                assert_eq!(db_name, "mydb");
1298                assert_eq!(password, None);
1299                assert_eq!(username, None);
1300            }
1301            other => panic!("expected Connect, got {other:?}"),
1302        }
1303    }
1304
1305    #[test]
1306    fn test_encode_decode_result_rows() {
1307        let msg = Message::ResultRows {
1308            columns: vec!["name".into(), "age".into()],
1309            rows: vec![
1310                vec!["Alice".into(), "30".into()],
1311                vec!["Bob".into(), "25".into()],
1312            ],
1313        };
1314        let bytes = msg.encode();
1315        let decoded = Message::decode(&bytes).unwrap();
1316        match decoded {
1317            Message::ResultRows { columns, rows } => {
1318                assert_eq!(columns, vec!["name", "age"]);
1319                assert_eq!(rows.len(), 2);
1320            }
1321            _ => panic!("expected ResultRows"),
1322        }
1323    }
1324
1325    #[test]
1326    fn legacy_rows_frame_is_byte_identical() {
1327        let encoded = Message::ResultRows {
1328            columns: vec!["x".into()],
1329            rows: vec![vec!["y".into()]],
1330        }
1331        .encode();
1332        assert_eq!(
1333            encoded,
1334            vec![
1335                0x07, 0x00, 0x10, 0x00, 0x00, 0x00, // frame header
1336                0x01, 0x00, // one column
1337                0x01, 0x00, 0x00, 0x00, b'x', // column name
1338                0x01, 0x00, 0x00, 0x00, // one row
1339                0x01, 0x00, 0x00, 0x00, b'y', // one legacy string cell
1340            ]
1341        );
1342    }
1343
1344    #[test]
1345    fn native_request_tags_and_params_round_trip() {
1346        let cases = [
1347            (
1348                MSG_QUERY_NATIVE,
1349                Message::QueryNative {
1350                    query: "T { .x }".into(),
1351                },
1352            ),
1353            (
1354                MSG_QUERY_SQL_NATIVE,
1355                Message::QuerySqlNative {
1356                    query: "SELECT x FROM T".into(),
1357                },
1358            ),
1359        ];
1360        for (tag, message) in cases {
1361            let encoded = message.encode();
1362            assert_eq!(encoded[0], tag);
1363            match Message::decode(&encoded).expect("native request round trip") {
1364                Message::QueryNative { query } => assert_eq!(query, "T { .x }"),
1365                Message::QuerySqlNative { query } => assert_eq!(query, "SELECT x FROM T"),
1366                other => panic!("unexpected native request: {other:?}"),
1367            }
1368        }
1369
1370        let encoded = Message::QueryWithParamsNative {
1371            query: "T filter .x = $1".into(),
1372            params: vec![WireParam::Int(7), WireParam::Bool(false)],
1373        }
1374        .encode();
1375        assert_eq!(encoded[0], MSG_QUERY_PARAMS_NATIVE);
1376        match Message::decode(&encoded).expect("native params round trip") {
1377            Message::QueryWithParamsNative { query, params } => {
1378                assert_eq!(query, "T filter .x = $1");
1379                assert_eq!(params, vec![WireParam::Int(7), WireParam::Bool(false)]);
1380            }
1381            other => panic!("unexpected native parameterized request: {other:?}"),
1382        }
1383    }
1384
1385    fn every_native_value() -> Vec<Value> {
1386        vec![
1387            Value::Empty,
1388            Value::Int(-9_007_199_254_740_993),
1389            Value::Float(2.5),
1390            Value::Bool(true),
1391            Value::Str("héllo".into()),
1392            Value::DateTime(1_723_650_123_456_789),
1393            Value::Uuid([
1394                0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xaa, 0xbb, 0xcc, 0xdd,
1395                0xee, 0xff,
1396            ]),
1397            Value::Bytes(vec![0x00, 0x7f, 0x80, 0xff]),
1398            Value::Json(
1399                powdb_storage::pj1::parse_json_text("9007199254740993")
1400                    .expect("PJ1 fixture")
1401                    .into_boxed_slice(),
1402            ),
1403        ]
1404    }
1405
1406    #[test]
1407    fn native_rows_and_scalar_round_trip_every_type() {
1408        let values = every_native_value();
1409        let columns = (0..values.len()).map(|index| format!("c{index}")).collect();
1410        let encoded = Message::ResultRowsNative {
1411            columns,
1412            rows: vec![values.clone()],
1413        }
1414        .encode();
1415        assert_eq!(encoded[0], MSG_RESULT_ROWS_NATIVE);
1416        match Message::decode(&encoded).expect("native rows round trip") {
1417            Message::ResultRowsNative { columns, rows } => {
1418                assert_eq!(columns.len(), values.len());
1419                assert_eq!(rows, vec![values.clone()]);
1420            }
1421            other => panic!("unexpected native rows: {other:?}"),
1422        }
1423
1424        for value in values {
1425            let encoded = Message::ResultScalarNative {
1426                value: value.clone(),
1427            }
1428            .encode();
1429            assert_eq!(encoded[0], MSG_RESULT_SCALAR_NATIVE);
1430            match Message::decode(&encoded).expect("native scalar round trip") {
1431                Message::ResultScalarNative { value: decoded } => assert_eq!(decoded, value),
1432                other => panic!("unexpected native scalar: {other:?}"),
1433            }
1434        }
1435    }
1436
1437    #[test]
1438    fn native_mixed_row_matches_cross_client_golden() {
1439        let encoded = Message::ResultRowsNative {
1440            columns: ["e", "i", "f", "b", "s", "d", "u", "x", "j"]
1441                .into_iter()
1442                .map(str::to_owned)
1443                .collect(),
1444            rows: vec![every_native_value()],
1445        }
1446        .encode();
1447        let hex = encoded
1448            .iter()
1449            .map(|byte| format!("{byte:02x}"))
1450            .collect::<String>();
1451        assert_eq!(
1452            hex,
1453            "16009c000000090001000000650100000069010000006601000000620100000073010000006401000000750100000078010000006a0100000000000000000108000000ffffffffffffdfff02080000000000000000000440030100000001040600000068c3a96c6c6f050800000015615391a61f0600061000000000112233445566778899aabbccddeeff0704000000007f80ff0809000000030100000000002000"
1454        );
1455    }
1456
1457    fn frame(tag: u8, payload: &[u8]) -> Vec<u8> {
1458        let mut frame = vec![tag, 0];
1459        frame.extend_from_slice(&(payload.len() as u32).to_le_bytes());
1460        frame.extend_from_slice(payload);
1461        frame
1462    }
1463
1464    fn typed_cell(tag: u8, body: &[u8]) -> Vec<u8> {
1465        let mut payload = vec![tag];
1466        payload.extend_from_slice(&(body.len() as u32).to_le_bytes());
1467        payload.extend_from_slice(body);
1468        payload
1469    }
1470
1471    #[test]
1472    fn native_scalar_rejects_malformed_cells() {
1473        let malformed = [
1474            typed_cell(0xff, &[]),
1475            typed_cell(TypeId::Int as u8, &[0; 7]),
1476            typed_cell(TypeId::Bool as u8, &[2]),
1477            typed_cell(TypeId::Str as u8, &[0xff]),
1478            typed_cell(TypeId::Json as u8, &[0xff]),
1479            typed_cell(TypeId::Json as u8, &[0, 0]),
1480        ];
1481        for payload in malformed {
1482            assert!(Message::decode(&frame(MSG_RESULT_SCALAR_NATIVE, &payload)).is_err());
1483        }
1484
1485        let mut trailing = typed_cell(TypeId::Empty as u8, &[]);
1486        trailing.push(0);
1487        assert!(Message::decode(&frame(MSG_RESULT_SCALAR_NATIVE, &trailing)).is_err());
1488    }
1489
1490    #[test]
1491    fn native_rows_rejects_bad_counts_and_trailing_data() {
1492        let mut payload = Vec::new();
1493        payload.extend_from_slice(&1u16.to_le_bytes());
1494        payload.extend_from_slice(&encode_string("x"));
1495        payload.extend_from_slice(&2u32.to_le_bytes());
1496        payload.extend_from_slice(&typed_cell(TypeId::Empty as u8, &[]));
1497        assert!(Message::decode(&frame(MSG_RESULT_ROWS_NATIVE, &payload)).is_err());
1498
1499        payload[7..11].copy_from_slice(&1u32.to_le_bytes());
1500        payload.extend_from_slice(&[0xaa]);
1501        assert!(Message::decode(&frame(MSG_RESULT_ROWS_NATIVE, &payload)).is_err());
1502    }
1503
1504    #[test]
1505    fn test_encode_decode_result_message() {
1506        let msg = Message::ResultMessage {
1507            message: "type User created".into(),
1508        };
1509        let bytes = msg.encode();
1510        let decoded = Message::decode(&bytes).unwrap();
1511        match decoded {
1512            Message::ResultMessage { message } => assert_eq!(message, "type User created"),
1513            _ => panic!("expected ResultMessage"),
1514        }
1515    }
1516
1517    #[test]
1518    fn test_encode_decode_error() {
1519        let msg = Message::Error {
1520            message: "table not found".into(),
1521        };
1522        let bytes = msg.encode();
1523        let decoded = Message::decode(&bytes).unwrap();
1524        match decoded {
1525            Message::Error { message } => assert_eq!(message, "table not found"),
1526            _ => panic!("expected Error"),
1527        }
1528    }
1529
1530    #[test]
1531    fn test_encode_decode_query_sql() {
1532        let msg = Message::QuerySql {
1533            query: "SELECT * FROM User".into(),
1534        };
1535        let decoded = Message::decode(&msg.encode()).unwrap();
1536        match decoded {
1537            Message::QuerySql { query } => assert_eq!(query, "SELECT * FROM User"),
1538            other => panic!("expected QuerySql, got {other:?}"),
1539        }
1540    }
1541
1542    #[test]
1543    fn test_encode_decode_query_with_params() {
1544        let msg = Message::QueryWithParams {
1545            query: "insert User { name := $1, age := $2, ok := $3, note := $4 }".into(),
1546            params: vec![
1547                WireParam::Str(r#"a"b\c; drop User"#.into()),
1548                WireParam::Int(-7),
1549                WireParam::Bool(true),
1550                WireParam::Null,
1551            ],
1552        };
1553        let bytes = msg.encode();
1554        // The new frame must use the dedicated 0x04 tag.
1555        assert_eq!(bytes[0], 0x04);
1556        match Message::decode(&bytes).unwrap() {
1557            Message::QueryWithParams { query, params } => {
1558                assert!(query.contains("$1"));
1559                assert_eq!(params.len(), 4);
1560                assert!(matches!(&params[0], WireParam::Str(s) if s == r#"a"b\c; drop User"#));
1561                assert!(matches!(&params[1], WireParam::Int(-7)));
1562                assert!(matches!(&params[2], WireParam::Bool(true)));
1563                assert!(matches!(&params[3], WireParam::Null));
1564            }
1565            other => panic!("expected QueryWithParams, got {other:?}"),
1566        }
1567    }
1568
1569    #[test]
1570    fn test_query_with_params_float_round_trip() {
1571        let msg = Message::QueryWithParams {
1572            query: "T filter .f = $1".into(),
1573            params: vec![WireParam::Float(2.5)],
1574        };
1575        match Message::decode(&msg.encode()).unwrap() {
1576            Message::QueryWithParams { params, .. } => {
1577                assert!(matches!(&params[0], WireParam::Float(f) if (*f - 2.5).abs() < 1e-12));
1578            }
1579            other => panic!("expected QueryWithParams, got {other:?}"),
1580        }
1581    }
1582
1583    fn sample_sync_status() -> WireSyncStatus {
1584        WireSyncStatus {
1585            replica_id: "replica-a".into(),
1586            active: true,
1587            last_applied_lsn: Some(7),
1588            remote_lsn: 10,
1589            servable_lsn: Some(10),
1590            unarchived_lsn: Some(0),
1591            lag_lsn: Some(3),
1592            lag_bytes: Some(2048),
1593            lag_ms: Some(5000),
1594            stale: true,
1595            repair_action: WireSyncRepairAction::Pull,
1596            last_sync_error: None,
1597        }
1598    }
1599
1600    #[test]
1601    fn test_encode_decode_sync_requests() {
1602        let database_id = *b"sync-protocol!!!";
1603        let pull = Message::SyncPull {
1604            replica_id: "replica-a".into(),
1605            since_lsn: 7,
1606            max_units: 128,
1607            max_bytes: 4096,
1608            database_id,
1609            primary_generation: 9,
1610            wal_format_version: 1,
1611            catalog_version: 2,
1612            segment_format_version: 1,
1613        };
1614        let bytes = pull.encode();
1615        assert_eq!(bytes[0], MSG_SYNC_PULL);
1616        match Message::decode(&bytes).unwrap() {
1617            Message::SyncPull {
1618                replica_id,
1619                since_lsn,
1620                max_units,
1621                max_bytes,
1622                database_id: decoded_database_id,
1623                primary_generation,
1624                wal_format_version,
1625                catalog_version,
1626                segment_format_version,
1627            } => {
1628                assert_eq!(replica_id, "replica-a");
1629                assert_eq!(since_lsn, 7);
1630                assert_eq!(max_units, 128);
1631                assert_eq!(max_bytes, 4096);
1632                assert_eq!(decoded_database_id, database_id);
1633                assert_eq!(primary_generation, 9);
1634                assert_eq!(wal_format_version, 1);
1635                assert_eq!(catalog_version, 2);
1636                assert_eq!(segment_format_version, 1);
1637            }
1638            other => panic!("expected SyncPull, got {other:?}"),
1639        }
1640
1641        let ack = Message::SyncAck {
1642            replica_id: "replica-a".into(),
1643            applied_lsn: 10,
1644            remote_lsn: 10,
1645        };
1646        match Message::decode(&ack.encode()).unwrap() {
1647            Message::SyncAck {
1648                replica_id,
1649                applied_lsn,
1650                remote_lsn,
1651            } => {
1652                assert_eq!(replica_id, "replica-a");
1653                assert_eq!(applied_lsn, 10);
1654                assert_eq!(remote_lsn, 10);
1655            }
1656            other => panic!("expected SyncAck, got {other:?}"),
1657        }
1658    }
1659
1660    #[test]
1661    fn test_encode_decode_sync_results() {
1662        let status = sample_sync_status();
1663        match Message::decode(
1664            &Message::SyncStatusResult {
1665                status: status.clone(),
1666            }
1667            .encode(),
1668        )
1669        .unwrap()
1670        {
1671            Message::SyncStatusResult { status: decoded } => assert_eq!(decoded, status),
1672            other => panic!("expected SyncStatusResult, got {other:?}"),
1673        }
1674
1675        let await_archive_status = WireSyncStatus {
1676            servable_lsn: Some(7),
1677            unarchived_lsn: Some(3),
1678            repair_action: WireSyncRepairAction::AwaitArchive,
1679            last_sync_error: Some("primary WAL is not yet archived".into()),
1680            ..status.clone()
1681        };
1682        match Message::decode(
1683            &Message::SyncStatusResult {
1684                status: await_archive_status.clone(),
1685            }
1686            .encode(),
1687        )
1688        .unwrap()
1689        {
1690            Message::SyncStatusResult { status: decoded } => {
1691                assert_eq!(decoded, await_archive_status)
1692            }
1693            other => panic!("expected AwaitArchive SyncStatusResult, got {other:?}"),
1694        }
1695
1696        let units = vec![
1697            WireRetainedUnit {
1698                tx_id: 1,
1699                record_type: 4,
1700                lsn: 8,
1701                data: vec![1, 2, 3],
1702            },
1703            WireRetainedUnit {
1704                tx_id: 1,
1705                record_type: 4,
1706                lsn: 9,
1707                data: vec![4, 5],
1708            },
1709        ];
1710        let empty_pull_len = Message::SyncPullResult {
1711            status: status.clone(),
1712            units: Vec::new(),
1713            has_more: true,
1714        }
1715        .encode()
1716        .len();
1717        let populated_pull_len = Message::SyncPullResult {
1718            status: status.clone(),
1719            units: units.clone(),
1720            has_more: true,
1721        }
1722        .encode()
1723        .len();
1724        let expected_unit_len: u64 = units
1725            .iter()
1726            .map(WireRetainedUnit::encoded_len)
1727            .collect::<Result<Vec<_>, _>>()
1728            .unwrap()
1729            .into_iter()
1730            .sum();
1731        assert_eq!(
1732            u64::try_from(populated_pull_len - empty_pull_len).unwrap(),
1733            expected_unit_len,
1734            "retained-unit encoded length must track SyncPullResult wire shape"
1735        );
1736
1737        match Message::decode(
1738            &Message::SyncPullResult {
1739                status: status.clone(),
1740                units: units.clone(),
1741                has_more: true,
1742            }
1743            .encode(),
1744        )
1745        .unwrap()
1746        {
1747            Message::SyncPullResult {
1748                status: decoded_status,
1749                units: decoded_units,
1750                has_more,
1751            } => {
1752                assert_eq!(decoded_status, status);
1753                assert_eq!(decoded_units, units);
1754                assert!(has_more);
1755            }
1756            other => panic!("expected SyncPullResult, got {other:?}"),
1757        }
1758
1759        match Message::decode(
1760            &Message::SyncAckResult {
1761                previous_applied_lsn: 7,
1762                applied_lsn: 10,
1763                remote_lsn: 10,
1764                advanced: true,
1765                status: WireSyncStatus {
1766                    stale: false,
1767                    repair_action: WireSyncRepairAction::None,
1768                    lag_lsn: Some(0),
1769                    lag_bytes: Some(0),
1770                    lag_ms: Some(0),
1771                    ..status
1772                },
1773            }
1774            .encode(),
1775        )
1776        .unwrap()
1777        {
1778            Message::SyncAckResult {
1779                previous_applied_lsn,
1780                applied_lsn,
1781                remote_lsn,
1782                advanced,
1783                status,
1784            } => {
1785                assert_eq!(previous_applied_lsn, 7);
1786                assert_eq!(applied_lsn, 10);
1787                assert_eq!(remote_lsn, 10);
1788                assert!(advanced);
1789                assert!(!status.stale);
1790            }
1791            other => panic!("expected SyncAckResult, got {other:?}"),
1792        }
1793    }
1794
1795    #[test]
1796    fn test_decode_garbage_never_panics() {
1797        // Feed a wide range of malformed/truncated byte sequences to the
1798        // wire-protocol decode path. Every one must return Err, never panic:
1799        // a malformed client message must not crash the server.
1800        let cases: Vec<Vec<u8>> = vec![
1801            vec![],                                   // empty
1802            vec![0x03],                               // 1 byte, shorter than header
1803            vec![0x03, 0x00, 0x00, 0x00, 0x00],       // 5 bytes, header truncated by one
1804            vec![0xFF, 0x00, 0x00, 0x00, 0x00, 0x00], // unknown message type
1805            // QUERY with payload_len far exceeding the frame.
1806            vec![0x03, 0x00, 0xFF, 0xFF, 0xFF, 0xFF],
1807            // SYNC_PULL with only a replica id and no fixed fields.
1808            {
1809                let mut payload = encode_string("replica-a");
1810                let mut frame = vec![MSG_SYNC_PULL, 0];
1811                frame.extend_from_slice(&(payload.len() as u32).to_le_bytes());
1812                frame.append(&mut payload);
1813                frame
1814            },
1815            // SYNC_PULL_RESULT with an amplified retained-unit count.
1816            {
1817                let mut payload = encode_sync_status(&sample_sync_status());
1818                payload.extend_from_slice(&((MAX_SYNC_UNITS as u32) + 1).to_le_bytes());
1819                let mut frame = vec![MSG_SYNC_PULL_RESULT, 0];
1820                frame.extend_from_slice(&(payload.len() as u32).to_le_bytes());
1821                frame.extend_from_slice(&payload);
1822                frame
1823            },
1824            // CONNECT claiming a string len of 0xFFFFFFFF but no data.
1825            vec![0x01, 0x00, 0x04, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF],
1826            // RESULT_ROWS claiming a huge column count with no data.
1827            vec![0x07, 0x00, 0x02, 0x00, 0x00, 0x00, 0xFF, 0xFF],
1828            // RESULT_OK with a truncated 8-byte affected field.
1829            vec![0x09, 0x00, 0x03, 0x00, 0x00, 0x00, 0x01, 0x02, 0x03],
1830            // QUERY_PARAMS (0x04) claiming a query string len with no data.
1831            vec![0x04, 0x00, 0x04, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF],
1832            // QUERY_PARAMS: empty query string, claims 1 param, no param bytes.
1833            vec![
1834                0x04, 0x00, 0x06, 0x00, 0x00, 0x00, // header, payload_len=6
1835                0x00, 0x00, 0x00, 0x00, // query string len = 0
1836                0x01, 0x00, // param count = 1, then nothing
1837            ],
1838            // QUERY_PARAMS: 1 int param with a truncated i64 body.
1839            vec![
1840                0x04, 0x00, 0x0B, 0x00, 0x00, 0x00, // header, payload_len=11
1841                0x00, 0x00, 0x00, 0x00, // query len = 0
1842                0x01, 0x00, // param count = 1
1843                0x01, // tag = int, then only 3 of 8 bytes
1844                0x01, 0x02, 0x03,
1845            ],
1846            // QUERY_PARAMS: 1 str param with a truncated string body.
1847            vec![
1848                0x04, 0x00, 0x0F, 0x00, 0x00, 0x00, // header, payload_len=15
1849                0x00, 0x00, 0x00, 0x00, // query len = 0
1850                0x01, 0x00, // param count = 1
1851                0x04, // tag = str
1852                0xFF, 0xFF, 0xFF, 0xFF, // str len huge, no data
1853            ],
1854            // QUERY_PARAMS: unknown param tag byte.
1855            vec![
1856                0x04, 0x00, 0x0B, 0x00, 0x00, 0x00, // header, payload_len=11
1857                0x00, 0x00, 0x00, 0x00, // query len = 0
1858                0x01, 0x00, // param count = 1
1859                0x63, // bogus tag
1860            ],
1861        ];
1862        for bytes in cases {
1863            let result = Message::decode(&bytes);
1864            assert!(
1865                result.is_err(),
1866                "expected Err for malformed input {bytes:?}, got {result:?}"
1867            );
1868        }
1869    }
1870
1871    #[test]
1872    fn test_decode_arbitrary_prefixes_never_panic() {
1873        // Exhaustively walk every truncation of a well-formed frame plus a
1874        // few byte mutations. None may panic.
1875        let valid = Message::ResultRows {
1876            columns: vec!["a".into(), "b".into()],
1877            rows: vec![vec!["1".into(), "2".into()]],
1878        }
1879        .encode();
1880        for end in 0..valid.len() {
1881            // Every prefix that is not the full frame must be rejected, not panic.
1882            let _ = Message::decode(&valid[..end]);
1883        }
1884        // Mutate each byte to a high value and confirm no panic.
1885        for i in 0..valid.len() {
1886            let mut m = valid.clone();
1887            m[i] = 0xFF;
1888            let _ = Message::decode(&m);
1889        }
1890    }
1891
1892    #[test]
1893    fn test_decode_result_rows_rejects_amplified_row_count() {
1894        // A tiny frame that declares col_count=0 and row_count=10_000_000.
1895        // With zero columns each row consumes no bytes, so the old decoder
1896        // would allocate/iterate 10M empty rows from ~12 bytes (reachable
1897        // pre-auth). The amplification guard must reject it.
1898        let mut payload = Vec::new();
1899        payload.extend_from_slice(&0u16.to_le_bytes()); // col_count = 0
1900        payload.extend_from_slice(&10_000_000u32.to_le_bytes()); // row_count = 10M
1901        let mut frame = vec![MSG_RESULT_ROWS, 0];
1902        frame.extend_from_slice(&(payload.len() as u32).to_le_bytes());
1903        frame.extend_from_slice(&payload);
1904        assert!(
1905            Message::decode(&frame).is_err(),
1906            "amplified row count must be rejected"
1907        );
1908
1909        // A normal small ResultRows must still round-trip unchanged.
1910        let msg = Message::ResultRows {
1911            columns: vec!["a".into()],
1912            rows: vec![vec!["x".into()]],
1913        };
1914        match Message::decode(&msg.encode()).unwrap() {
1915            Message::ResultRows { columns, rows } => {
1916                assert_eq!(columns, vec!["a"]);
1917                assert_eq!(rows, vec![vec!["x".to_string()]]);
1918            }
1919            other => panic!("expected ResultRows, got {other:?}"),
1920        }
1921    }
1922
1923    #[test]
1924    fn test_frame_length() {
1925        let msg = Message::Query {
1926            query: "User".into(),
1927        };
1928        let bytes = msg.encode();
1929        assert!(bytes.len() >= 6);
1930        let payload_len = u32::from_le_bytes(bytes[2..6].try_into().unwrap()) as usize;
1931        assert_eq!(bytes.len(), 6 + payload_len);
1932    }
1933}