Skip to main content

akar_main/
remote.rs

1//! Remote client for the Akar embedded server (P47).
2//!
3//! The embedded server (crate `akar-server`) owns the [`Database`] instance
4//! and its exclusive file lock. Remote clients connect over TCP using a
5//! length-prefixed JSON protocol and never open the database directory
6//! themselves — the server holds every file lock on their behalf.
7//!
8//! # Usage
9//!
10//! ```no_run
11//! use akar_main::Database;
12//!
13//! // On the server side (separate process):
14//! // let db = Arc::new(Database::new("./my_db", SystemConfig::default())?);
15//! // let mut server = akar_server::Server::bind("127.0.0.1:9876", db)?;
16//! // server.start()?;
17//!
18//! // On the client side:
19//! let client = Database::connect_tcp("127.0.0.1:9876")?;
20//! let res = client.query("MATCH (n) RETURN n LIMIT 5")?;
21//! assert!(res.success);
22//! # Ok::<(), String>(())
23//! ```
24
25use akar_common::types::Value;
26use serde::{Deserialize, Serialize};
27use std::collections::HashMap;
28use std::fmt;
29use std::io::{ErrorKind, Read, Write};
30use std::net::TcpStream;
31use std::sync::Mutex;
32use std::sync::atomic::{AtomicBool, Ordering};
33use std::time::Duration;
34
35/// Default port the Akar server binds to when none is specified.
36pub const DEFAULT_PORT: u16 = 9876;
37
38/// Maximum accepted frame size (128 MiB).
39///
40/// Protects both the server and the client from unbounded allocations caused
41/// by a corrupt or hostile peer.
42pub const MAX_FRAME_SIZE: usize = 128 * 1024 * 1024;
43
44/// A request sent from a client to the server.
45#[derive(Debug, Clone, Serialize, Deserialize)]
46pub struct WireRequest {
47    /// The Cypher query to execute.
48    #[serde(default, skip_serializing_if = "String::is_empty")]
49    pub query: String,
50    /// Optional client identifier (currently informational only).
51    #[serde(default, skip_serializing_if = "Option::is_none")]
52    pub client_name: Option<String>,
53    /// Operation to perform. Defaults to `"query"` when absent.
54    ///
55    /// Supported operations:
56    /// - `"query"` — execute a Cypher query (default)
57    /// - `"ping"` — liveness check
58    /// - `"flush"` — force a CHECKPOINT to persist data
59    /// - `"stats"` — return server statistics
60    /// - `"export"` — EXPORT DATABASE to the given path (requires `path`)
61    /// - `"shutdown"` — request graceful server shutdown
62    #[serde(default, skip_serializing_if = "Option::is_none")]
63    pub op: Option<String>,
64    /// Authentication token (hex-encoded, 32 bytes). Sent on every request
65    /// when the server requires auth.
66    #[serde(default, skip_serializing_if = "Option::is_none")]
67    pub token: Option<String>,
68    /// Filesystem path for operations that require one (e.g. `export`).
69    #[serde(default, skip_serializing_if = "Option::is_none")]
70    pub path: Option<String>,
71    /// Optional query parameters as a name→value map.
72    ///
73    /// When present and non-empty, the server executes the query through the
74    /// prepared-statement pipeline with parameter substitution instead of
75    /// plain string execution.  Values are JSON primitives (number, string,
76    /// bool, null) that are converted to Akar [`Value`]s before binding.
77    #[serde(default, skip_serializing_if = "Option::is_none")]
78    pub params: Option<HashMap<String, serde_json::Value>>,
79    /// Sub-action for operation dispatch (e.g. `dream_control` action).
80    #[serde(default, skip_serializing_if = "Option::is_none")]
81    pub action: Option<String>,
82}
83
84/// The response the server returns for a query.
85///
86/// `rows` is row-major: `rows[row][col]`, with `None` for SQL NULLs.
87#[derive(Debug, Clone, Serialize, Deserialize)]
88pub struct WireResponse {
89    pub success: bool,
90    #[serde(default, skip_serializing_if = "Option::is_none")]
91    pub message: Option<String>,
92    #[serde(default, skip_serializing_if = "Option::is_none")]
93    pub error_message: Option<String>,
94    pub column_names: Vec<String>,
95    pub rows: Vec<Vec<Option<Value>>>,
96    /// Server statistics (returned by `"stats"` operation).
97    #[serde(default, skip_serializing_if = "Option::is_none")]
98    pub stats: Option<ServerStats>,
99}
100
101/// Server statistics snapshot.
102#[derive(Debug, Clone, Serialize, Deserialize)]
103pub struct ServerStats {
104    /// Number of currently connected clients.
105    pub num_clients: usize,
106    /// Total number of queries executed since the server started.
107    pub total_queries: u64,
108    /// Server uptime in seconds.
109    pub uptime_secs: u64,
110    /// Database path.
111    pub db_path: String,
112    /// Server process ID.
113    pub pid: u32,
114}
115
116impl WireResponse {
117    /// Build a success response with a human-readable message (e.g. DDL).
118    pub fn success_message(msg: String) -> Self {
119        Self {
120            success: true,
121            message: Some(msg),
122            error_message: None,
123            column_names: Vec::new(),
124            rows: Vec::new(),
125            stats: None,
126        }
127    }
128
129    /// Build an error response.
130    pub fn error(msg: String) -> Self {
131        Self {
132            success: false,
133            message: None,
134            error_message: Some(msg),
135            column_names: Vec::new(),
136            rows: Vec::new(),
137            stats: None,
138        }
139    }
140
141    /// Number of result rows.
142    pub fn num_rows(&self) -> usize {
143        self.rows.len()
144    }
145
146    /// Number of result columns.
147    pub fn num_columns(&self) -> usize {
148        self.column_names.len()
149    }
150
151    /// Read the cell at `(row, col)`, or `None` when out of range or NULL.
152    pub fn cell(&self, row: usize, col: usize) -> Option<&Value> {
153        self.rows.get(row)?.get(col)?.as_ref()
154    }
155
156    /// Collect all non-NULL values in a column.
157    pub fn column_values(&self, col: usize) -> Vec<Value> {
158        self.rows.iter().filter_map(|r| r.get(col).cloned().flatten()).collect()
159    }
160
161    /// Human-readable summary mirroring [`crate::QueryResult::result_summary`]
162    /// (shared head logic, P51.43).
163    pub fn result_summary(&self) -> String {
164        if let Some(ref stats) = self.stats {
165            return format!(
166                "Server stats: {} clients, {} queries, uptime {}s, pid {}",
167                stats.num_clients, stats.total_queries, stats.uptime_secs, stats.pid,
168            );
169        }
170        if let Some(head) = crate::query_result::result_summary_head(
171            self.message.as_deref(),
172            self.success,
173            self.error_message.as_deref(),
174            !self.rows.is_empty(),
175        ) {
176            return head;
177        }
178        format!(
179            "Returned {} rows in {} columns",
180            self.rows.len(),
181            self.column_names.len()
182        )
183    }
184}
185
186impl fmt::Display for WireResponse {
187    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
188        if let Some(ref stats) = self.stats {
189            return write!(
190                f,
191                "Server: {} clients, {} queries, uptime {}s, pid {}",
192                stats.num_clients, stats.total_queries, stats.uptime_secs, stats.pid,
193            );
194        }
195        if let Some(head) = crate::query_result::result_summary_head(
196            self.message.as_deref(),
197            self.success,
198            self.error_message.as_deref(),
199            !self.rows.is_empty(),
200        ) {
201            return write!(f, "{head}");
202        }
203        for (i, row) in self.rows.iter().enumerate() {
204            if i > 0 {
205                writeln!(f)?;
206            }
207            write!(f, "Row {}: ", i)?;
208            for (col, cell) in row.iter().enumerate() {
209                if col > 0 {
210                    write!(f, ", ")?;
211                }
212                match cell {
213                    Some(v) => write!(f, "{v:?}")?,
214                    None => write!(f, "null")?,
215                }
216            }
217        }
218        Ok(())
219    }
220}
221
222// ─────────────────────────────────────────────────────────────────────────────
223// Length-prefixed framing
224// ─────────────────────────────────────────────────────────────────────────────
225
226/// State for a partially-read frame (survives read timeouts).
227#[derive(Debug)]
228pub enum PartialFrame {
229    /// 4-byte length header partially read.
230    Header([u8; 4], usize),
231    /// Payload partially read.
232    Payload { len: usize, buf: Vec<u8>, filled: usize },
233}
234
235/// Result of draining a socket after a read timeout (P52.19).
236#[derive(Debug, Clone, Copy, PartialEq, Eq)]
237enum DrainOutcome {
238    /// A complete stale frame was read and discarded — the stream is in sync.
239    FrameConsumed,
240    /// The peer closed the connection.
241    ConnectionClosed,
242    /// No frame arrived within the grace window — the stream may be desynced.
243    NoFrameWithinGrace,
244}
245
246/// Per-read timeout used while draining stale bytes after a query timeout.
247const GRACE_TIMEOUT: Duration = Duration::from_millis(250);
248
249/// Maximum number of grace reads before giving up on reconciling the stream.
250const MAX_GRACE_READS: usize = 8; // ~2s of grace
251
252/// Read-and-discard up to one complete stale frame (resuming any partial-frame
253/// state) so the socket is re-synchronized after a query read timeout.
254///
255/// `reader` is expected to be in "short timeout" mode — each `TimedOut`/
256/// `WouldBlock` is a no-progress tick, not a failure.
257fn drain_stale_frames<R: Read>(reader: &mut R, partial: &mut Option<PartialFrame>) -> DrainOutcome {
258    for _ in 0..MAX_GRACE_READS {
259        match read_frame(reader, partial) {
260            Ok(Some(_frame)) => return DrainOutcome::FrameConsumed,
261            Ok(None) => return DrainOutcome::ConnectionClosed,
262            Err(e) if e.kind() == ErrorKind::TimedOut || e.kind() == ErrorKind::WouldBlock => continue,
263            Err(_) => return DrainOutcome::NoFrameWithinGrace,
264        }
265    }
266    DrainOutcome::NoFrameWithinGrace
267}
268
269/// Write `payload` as a single length-prefixed frame: `[u32 LE len][bytes]`.
270pub fn write_frame<W: Write>(writer: &mut W, payload: &[u8]) -> std::io::Result<()> {
271    let len = payload.len();
272    if len > MAX_FRAME_SIZE {
273        return Err(std::io::Error::new(
274            ErrorKind::InvalidData,
275            format!("Frame too large: {len} bytes"),
276        ));
277    }
278    writer.write_all(&(len as u32).to_le_bytes())?;
279    writer.write_all(payload)
280}
281
282/// Read one complete frame from `reader`.
283///
284/// Returns:
285/// - `Ok(Some(bytes))` — a complete frame;
286/// - `Ok(None)` — clean end-of-stream (peer closed before any header byte);
287/// - `Err(WouldBlock | TimedOut)` — progress was made or not, partial state is
288///   kept in `partial`, call again later;
289/// - `Err(other)` — protocol or I/O failure.
290pub fn read_frame<R: Read>(reader: &mut R, partial: &mut Option<PartialFrame>) -> std::io::Result<Option<Vec<u8>>> {
291    let mut header: ([u8; 4], usize) = match partial.take() {
292        Some(PartialFrame::Payload {
293            len,
294            mut buf,
295            mut filled,
296        }) => {
297            if len > MAX_FRAME_SIZE {
298                return Err(std::io::Error::new(ErrorKind::InvalidData, "Frame too large"));
299            }
300            loop {
301                match reader.read(&mut buf[filled..]) {
302                    Ok(0) => {
303                        return Err(std::io::Error::new(
304                            ErrorKind::UnexpectedEof,
305                            "Unexpected EOF in frame payload",
306                        ));
307                    }
308                    Ok(n) => {
309                        filled += n;
310                        if filled == len {
311                            return Ok(Some(buf));
312                        }
313                    }
314                    Err(e) if e.kind() == ErrorKind::Interrupted => continue,
315                    Err(e) => {
316                        *partial = Some(PartialFrame::Payload { len, buf, filled });
317                        return Err(e);
318                    }
319                }
320            }
321        }
322        Some(PartialFrame::Header(h, filled)) => (h, filled),
323        None => ([0u8; 4], 0),
324    };
325
326    loop {
327        match reader.read(&mut header.0[header.1..]) {
328            Ok(0) => {
329                if header.1 == 0 {
330                    return Ok(None);
331                }
332                return Err(std::io::Error::new(
333                    ErrorKind::UnexpectedEof,
334                    "Unexpected EOF in frame header",
335                ));
336            }
337            Ok(n) => {
338                header.1 += n;
339                if header.1 == 4 {
340                    break;
341                }
342            }
343            Err(e) if e.kind() == ErrorKind::Interrupted => continue,
344            Err(e) => {
345                *partial = Some(PartialFrame::Header(header.0, header.1));
346                return Err(e);
347            }
348        }
349    }
350
351    let len = u32::from_le_bytes(header.0) as usize;
352    if len > MAX_FRAME_SIZE {
353        return Err(std::io::Error::new(
354            ErrorKind::InvalidData,
355            format!("Frame too large: {len} bytes"),
356        ));
357    }
358    if len == 0 {
359        return Ok(Some(Vec::new()));
360    }
361    let mut buf = vec![0u8; len];
362    let mut filled = 0;
363    loop {
364        match reader.read(&mut buf[filled..]) {
365            Ok(0) => {
366                return Err(std::io::Error::new(
367                    ErrorKind::UnexpectedEof,
368                    "Unexpected EOF in frame payload",
369                ));
370            }
371            Ok(n) => {
372                filled += n;
373                if filled == len {
374                    return Ok(Some(buf));
375                }
376            }
377            Err(e) if e.kind() == ErrorKind::Interrupted => continue,
378            Err(e) => {
379                *partial = Some(PartialFrame::Payload { len, buf, filled });
380                return Err(e);
381            }
382        }
383    }
384}
385
386// ─────────────────────────────────────────────────────────────────────────────
387// Client
388// ─────────────────────────────────────────────────────────────────────────────
389
390/// A client connection to a remote Akar server.
391///
392/// Created via [`RemoteDatabase::connect_tcp`] (or [`crate::Database::connect_tcp`]).
393/// Executes queries over a length-prefixed JSON protocol; the server holds the
394/// database open, so the client never touches the database directory or its
395/// file locks.
396pub struct RemoteDatabase {
397    stream: TcpStream,
398    address: String,
399    partial: Mutex<Option<PartialFrame>>,
400    /// Set once a read timeout could not be reconciled (no stale frame arrived
401    /// within the drain window). The stream may still hold bytes for the
402    /// abandoned query, so further `query()` calls must refuse to run rather
403    /// than silently read a stale response as the next query's result (P52.19).
404    desynced: AtomicBool,
405    /// Optional auth token sent with every request.
406    token: Option<String>,
407}
408
409impl RemoteDatabase {
410    /// Connect to an Akar server listening at `addr` (e.g. `"127.0.0.1:9876"`).
411    pub fn connect_tcp(addr: impl Into<String>) -> Result<Self, String> {
412        let addr = addr.into();
413        let stream =
414            TcpStream::connect(&addr).map_err(|e| format!("Failed to connect to Akar server at '{addr}': {e}"))?;
415        let _ = stream.set_nodelay(true);
416        let _ = stream.set_read_timeout(Some(Duration::from_secs(30)));
417        let _ = stream.set_write_timeout(Some(Duration::from_secs(30)));
418        Ok(Self {
419            stream,
420            address: addr,
421            partial: Mutex::new(None),
422            desynced: AtomicBool::new(false),
423            token: None,
424        })
425    }
426
427    /// Connect with an authentication token. The token is sent with every
428    /// request; the server rejects connections without a valid token.
429    pub fn connect_with_token(addr: impl Into<String>, token: String) -> Result<Self, String> {
430        let mut client = Self::connect_tcp(addr)?;
431        client.token = Some(token);
432        Ok(client)
433    }
434
435    /// The address this client is connected to.
436    pub fn address(&self) -> &str {
437        &self.address
438    }
439
440    /// Set the auth token for subsequent requests.
441    pub fn set_token(&mut self, token: String) {
442        self.token = Some(token);
443    }
444
445    /// Execute a Cypher query on the remote database.
446    ///
447    /// Mirrors [`crate::Connection::query`]: returns the response on success and
448    /// an error message when the query failed (including OCC `WriteConflict`s).
449    pub fn query(&self, query_str: &str) -> Result<WireResponse, String> {
450        self.send_request(WireRequest {
451            query: query_str.to_string(),
452            client_name: None,
453            op: None,
454            token: self.token.clone(),
455            path: None,
456            params: None,
457            action: None,
458        })
459    }
460
461    /// Execute a parameterized Cypher query.
462    ///
463    /// `params` is a map of parameter names (without the `$` prefix) to JSON
464    /// values.  The server binds them via the prepared-statement pipeline.
465    pub fn query_with_params(
466        &self,
467        query_str: &str,
468        params: HashMap<String, serde_json::Value>,
469    ) -> Result<WireResponse, String> {
470        self.send_request(WireRequest {
471            query: query_str.to_string(),
472            client_name: None,
473            op: None,
474            token: self.token.clone(),
475            path: None,
476            params: Some(params),
477            action: None,
478        })
479    }
480
481    /// Send a liveness check (op: `"ping"`).
482    pub fn ping_op(&self) -> Result<WireResponse, String> {
483        self.send_request(WireRequest {
484            query: String::new(),
485            client_name: None,
486            op: Some("ping".to_string()),
487            token: self.token.clone(),
488            path: None,
489            params: None,
490            action: None,
491        })
492    }
493
494    /// Force a CHECKPOINT to persist all data to disk (op: `"flush"`).
495    pub fn flush(&self) -> Result<WireResponse, String> {
496        self.send_request(WireRequest {
497            query: String::new(),
498            client_name: None,
499            op: Some("flush".to_string()),
500            token: self.token.clone(),
501            path: None,
502            params: None,
503            action: None,
504        })
505    }
506
507    /// Request server statistics (op: `"stats"`).
508    pub fn stats(&self) -> Result<WireResponse, String> {
509        self.send_request(WireRequest {
510            query: String::new(),
511            client_name: None,
512            op: Some("stats".to_string()),
513            token: self.token.clone(),
514            path: None,
515            params: None,
516            action: None,
517        })
518    }
519
520    /// Export the database to `path` (op: `"export"`).
521    pub fn export_db(&self, path: &str) -> Result<WireResponse, String> {
522        self.send_request(WireRequest {
523            query: String::new(),
524            client_name: None,
525            op: Some("export".to_string()),
526            token: self.token.clone(),
527            path: Some(path.to_string()),
528            params: None,
529            action: None,
530        })
531    }
532
533    /// Request graceful server shutdown (op: `"shutdown"`).
534    pub fn shutdown_server(&self) -> Result<WireResponse, String> {
535        self.send_request(WireRequest {
536            query: String::new(),
537            client_name: None,
538            op: Some("shutdown".to_string()),
539            token: self.token.clone(),
540            path: None,
541            params: None,
542            action: None,
543        })
544    }
545
546    /// Send a dream_control request (op: `"dream_control"`).
547    pub fn dream_control(&self, action: &str) -> Result<WireResponse, String> {
548        self.send_request(WireRequest {
549            query: String::new(),
550            client_name: None,
551            op: Some("dream_control".to_string()),
552            token: self.token.clone(),
553            path: None,
554            params: None,
555            action: Some(action.to_string()),
556        })
557    }
558
559    /// Send a raw `WireRequest` and return the response.
560    fn send_request(&self, request: WireRequest) -> Result<WireResponse, String> {
561        if self.desynced.load(Ordering::Acquire) {
562            return Err("Connection is desynchronized after a previous read timeout; \
563                 reconnect before sending further queries"
564                .into());
565        }
566
567        let payload = serde_json::to_vec(&request).map_err(|e| format!("Failed to serialize request: {e}"))?;
568
569        // Hold the connection's partial-frame lock across the entire write+read
570        // exchange. Two threads sharing a `RemoteDatabase` would otherwise
571        // interleave: thread A writes its request, thread B writes its request,
572        // then A reads B's response (P51.9).
573        let mut partial = self.partial.lock().map_err(|e| format!("Lock poisoned: {e}"))?;
574
575        {
576            let mut writer = &self.stream;
577            write_frame(&mut writer, &payload).map_err(|e| format!("Failed to send request: {e}"))?;
578            writer.flush().map_err(|e| format!("Failed to flush request: {e}"))?;
579        }
580
581        let frame = match read_frame(&mut &self.stream, &mut partial) {
582            Ok(Some(f)) => f,
583            Ok(None) => return Err("Connection closed by server".to_string()),
584            Err(e) => {
585                // Only a timeout can leave the peer's response in the socket
586                // buffer. On any other error (EOF/protocol) there is nothing
587                // to reconcile — the connection is already broken.
588                if e.kind() != ErrorKind::TimedOut && e.kind() != ErrorKind::WouldBlock {
589                    return Err(format!("Failed to read response: {e}"));
590                }
591                // A read timeout means response A is still pending somewhere on
592                // the wire. Drain it before allowing the next query, otherwise
593                // query B would read A's stale frame as its own result (P52.19).
594                match self.drain_pending_frame(&mut partial) {
595                    DrainOutcome::FrameConsumed => {
596                        // Stale response consumed — stream is re-synchronized.
597                        return Err(format!("Failed to read response (query timed out): {e}"));
598                    }
599                    DrainOutcome::ConnectionClosed => return Err("Connection closed by server".to_string()),
600                    DrainOutcome::NoFrameWithinGrace => {
601                        self.desynced.store(true, Ordering::Release);
602                        return Err(format!(
603                            "Failed to read response: {e} (no stale frame arrived to re-synchronize; \
604                             the connection has been marked desynchronized — reconnect before continuing)"
605                        ));
606                    }
607                }
608            }
609        };
610        drop(partial);
611
612        let response: WireResponse =
613            serde_json::from_slice(&frame).map_err(|e| format!("Failed to parse response: {e}"))?;
614        if response.success {
615            Ok(response)
616        } else {
617            Err(response
618                .error_message
619                .clone()
620                .unwrap_or_else(|| "Unknown server error".to_string()))
621        }
622    }
623
624    /// After a read timeout, keep reading with a short timeout until either a
625    /// full stale frame is consumed (re-sync) or a short grace window elapses.
626    ///
627    /// The drain reuses the connection's `partial` state so a frame interrupted
628    /// mid-read by the timeout is resumed and completed.
629    fn drain_pending_frame(&self, partial: &mut Option<PartialFrame>) -> DrainOutcome {
630        let _ = self.stream.set_read_timeout(Some(GRACE_TIMEOUT));
631        let outcome = drain_stale_frames(&mut &self.stream, partial);
632        let _ = self.stream.set_read_timeout(Some(Duration::from_secs(30)));
633        outcome
634    }
635
636    /// Verify the connection is alive by round-tripping a trivial query.
637    pub fn ping(&self) -> Result<(), String> {
638        self.query("RETURN 1").map(|_| ())
639    }
640
641    /// Close the connection. The server notices the disconnect and reclaims the
642    /// session resources.
643    pub fn close(&self) {
644        let _ = self.stream.shutdown(std::net::Shutdown::Both);
645    }
646}
647
648#[cfg(test)]
649mod tests {
650    use super::*;
651
652    #[test]
653    fn test_frame_roundtrip() {
654        let payloads = [b"".to_vec(), b"hello".to_vec(), vec![0u8; 4096], b"{}".to_vec()];
655        for payload in payloads {
656            let mut buf = Vec::new();
657            write_frame(&mut buf, &payload).unwrap();
658            let mut cursor = &buf[..];
659            let mut partial = None;
660            let read = read_frame(&mut cursor, &mut partial).unwrap();
661            assert_eq!(read.as_deref(), Some(payload.as_slice()));
662        }
663    }
664
665    /// A reader that yields at most 3 bytes per `read` call, exercising the
666    /// partial-frame state machine inside `read_frame`.
667    struct ChunkedReader<'a> {
668        inner: &'a mut &'a [u8],
669        first_read: bool,
670    }
671
672    impl<'a> ChunkedReader<'a> {
673        fn new(inner: &'a mut &'a [u8]) -> Self {
674            Self {
675                inner,
676                first_read: true,
677            }
678        }
679    }
680
681    impl<'a> Read for ChunkedReader<'a> {
682        fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
683            if self.first_read {
684                self.first_read = false;
685                // Simulate a timeout before any data is available; the caller
686                // must retain its partial state and retry.
687                return Err(std::io::Error::new(ErrorKind::WouldBlock, "no data yet"));
688            }
689            if self.inner.is_empty() {
690                return Ok(0);
691            }
692            let n = 3.min(buf.len()).min(self.inner.len());
693            buf[..n].copy_from_slice(&self.inner[..n]);
694            *self.inner = &self.inner[n..];
695            Ok(n)
696        }
697    }
698
699    #[test]
700    fn test_frame_partial_reads() {
701        let mut buf = Vec::new();
702        write_frame(&mut buf, b"partial-frame-test").unwrap();
703
704        let mut cursor = &buf[..];
705        let mut partial = None;
706        let mut reader = ChunkedReader::new(&mut cursor);
707
708        // First attempt hits a simulated WouldBlock before any bytes arrive.
709        let err = read_frame(&mut reader, &mut partial).unwrap_err();
710        assert_eq!(err.kind(), ErrorKind::WouldBlock);
711        assert!(partial.is_some(), "partial state must be retained across timeouts");
712
713        // Second attempt must reassemble the frame from the retained state.
714        let result = read_frame(&mut reader, &mut partial).unwrap();
715        assert_eq!(result.as_deref(), Some(b"partial-frame-test".as_slice()));
716    }
717
718    /// A reader that simulates a query timeout: the first `read` returns
719    /// `WouldBlock` (nothing has arrived yet), then the stale response frame
720    /// becomes available and is delivered normally. Exercises the P52.19 drain.
721    #[test]
722    fn test_drain_stale_frames_consumes_stale_response() {
723        let stale_response = serde_json::to_vec(&WireResponse::success_message("slow query".into())).unwrap();
724        let mut buf = Vec::new();
725        write_frame(&mut buf, &stale_response).unwrap();
726
727        let mut cursor = &buf[..];
728        let mut partial = None;
729        let mut reader = ChunkedReader::new(&mut cursor);
730
731        // Initial read times out (no bytes yet) — this is the slow-query case.
732        let err = read_frame(&mut reader, &mut partial).unwrap_err();
733        assert_eq!(err.kind(), ErrorKind::WouldBlock);
734
735        // Draining must consume the stale frame that arrives afterwards,
736        // re-synchronizing the stream for the next query.
737        let outcome = drain_stale_frames(&mut reader, &mut partial);
738        assert_eq!(outcome, DrainOutcome::FrameConsumed);
739        // The stream is fully consumed — no residual bytes leak into the next read.
740        let mut tail = Vec::new();
741        let _ = reader.read_to_end(&mut tail);
742        assert_eq!(tail.len(), 0);
743    }
744
745    #[test]
746    fn test_drain_stale_frames_no_frame_returns_grace() {
747        // A reader that only ever times out: the stale frame never arrives, so
748        // the drain must give up after the grace window.
749        struct AlwaysBlocking;
750        impl Read for AlwaysBlocking {
751            fn read(&mut self, _buf: &mut [u8]) -> std::io::Result<usize> {
752                Err(std::io::Error::new(ErrorKind::WouldBlock, "no data"))
753            }
754        }
755
756        let mut reader = AlwaysBlocking;
757        let mut partial = None;
758        let outcome = drain_stale_frames(&mut reader, &mut partial);
759        assert_eq!(outcome, DrainOutcome::NoFrameWithinGrace);
760    }
761
762    #[test]
763    fn test_drain_stale_frames_eof_reports_closed() {
764        let mut cursor = &b""[..];
765        let mut partial = None;
766        let outcome = drain_stale_frames(&mut &mut cursor, &mut partial);
767        assert_eq!(outcome, DrainOutcome::ConnectionClosed);
768    }
769
770    #[test]
771    fn test_eof_returns_none() {
772        let mut cursor = &b""[..];
773        let mut partial = None;
774        assert!(read_frame(&mut cursor, &mut partial).unwrap().is_none());
775    }
776
777    #[test]
778    fn test_frame_too_large_rejected() {
779        let mut buf = Vec::new();
780        // u32 length bigger than MAX_FRAME_SIZE
781        buf.extend_from_slice(&(MAX_FRAME_SIZE as u32 + 1).to_le_bytes());
782        let mut cursor = &buf[..];
783        let mut partial = None;
784        assert!(read_frame(&mut cursor, &mut partial).is_err());
785    }
786
787    #[test]
788    fn test_wire_response_accessors() {
789        let resp = WireResponse {
790            success: true,
791            message: None,
792            error_message: None,
793            column_names: vec!["name".into(), "age".into()],
794            rows: vec![
795                vec![Some(Value::String("alice".into())), Some(Value::Int64(30))],
796                vec![None, Some(Value::Int64(25))],
797            ],
798            stats: None,
799        };
800        assert_eq!(resp.num_rows(), 2);
801        assert_eq!(resp.num_columns(), 2);
802        assert_eq!(resp.cell(0, 0), Some(&Value::String("alice".into())));
803        assert_eq!(resp.cell(1, 0), None);
804        assert_eq!(resp.cell(9, 9), None);
805        assert_eq!(resp.column_values(1), vec![Value::Int64(30), Value::Int64(25)]);
806        assert_eq!(resp.result_summary(), "Returned 2 rows in 2 columns");
807    }
808}