Skip to main content

nedb_engine/
pgwire.rs

1// SPDX-FileCopyrightText: 2026 INTERCHAINED LLC
2// SPDX-License-Identifier: BUSL-1.1
3// NEDB · © 2026 INTERCHAINED LLC × Eth-Interchained × Vex (Claude Opus 5)
4
5//! A PostgreSQL wire-protocol endpoint for NEDB — reads **and** writes.
6//!
7//! # What this is
8//!
9//! A front door that speaks the PostgreSQL v3 wire protocol well enough that
10//! tools built for Postgres — `psql`, DBeaver, Metabase, Grafana, psycopg, any
11//! libpq client — can use a NEDB store with ordinary SQL. A documented subset
12//! of SQL is translated to NQL and to engine writes; everything else is
13//! refused with an error naming exactly what was not understood.
14//!
15//! It is **not** a claim of Postgres parity. It is a claim that the SQL people
16//! actually type works, and that the boundary is stated rather than discovered.
17//!
18//! # Why writes belong here
19//!
20//! The first cut of this module was read-only, on the reasoning that a NEDB
21//! write carries `caused_by`, valid-time bounds and idempotency, and none of
22//! that has a natural SQL spelling. That reasoning was wrong, and looking at
23//! the mapping is what made it obvious:
24//!
25//! | SQL | NEDB | and therefore |
26//! |---|---|---|
27//! | `INSERT` | a put | — |
28//! | `UPDATE … WHERE` | a NEW VERSION of each match | the prior value stays readable |
29//! | `DELETE … WHERE` | a tombstone | the deleted row stays in history |
30//!
31//! NEDB is append-only, so an `UPDATE` is *already* a versioned write and a
32//! `DELETE` is *already* a tombstone. Nothing is bent to fit. The consequence
33//! is the point of the whole endpoint:
34//!
35//! ```sql
36//! UPDATE orders SET total = 999 WHERE _id = 'o1';
37//! SELECT total FROM orders WHERE _id = 'o1';                  -- 999
38//! SELECT total FROM orders AS OF SYSTEM TIME 0 WHERE _id = 'o1';  -- 120
39//! ```
40//!
41//! Run the SQL you would run against Postgres, and the tamper-evident history
42//! is free. No triggers, no audit table, no application code.
43//!
44//! Provenance is reachable too: `_caused_by`, `_valid_from` and `_valid_to` are
45//! reserved INSERT columns, lifted out of the payload into the write itself.
46//!
47//! Writes are ON by default — that is the parity position. Set
48//! `NEDBD_PG_READ_ONLY=1` for the deployment where this door must never mutate
49//! anything.
50//!
51//! # Supported SQL
52//!
53//! ```sql
54//! SELECT * | col [, col]* | COUNT(*) | <agg>(col)
55//!   FROM <collection>
56//!   [ AS OF SYSTEM TIME <seq> ]     -- bridges to NQL's AS OF
57//!   [ WHERE <predicate> ]           -- the full NQL predicate surface
58//!   [ GROUP BY <col> ] [ HAVING <predicate> ]
59//!   [ ORDER BY <col> [ASC|DESC] (, ...) ] [ LIMIT <n> ] [ OFFSET <n> ]
60//!
61//! INSERT INTO <collection> (c1, c2) VALUES (v1, v2), (…) [RETURNING …]
62//! UPDATE <collection> SET c = v [, …] [WHERE <predicate>] [RETURNING …]
63//! DELETE FROM <collection> [WHERE <predicate>] [RETURNING …]
64//! ```
65//!
66//! Single-quoted SQL literals are rewritten to NQL's double-quoted form and
67//! `<>` to `!=`. Column projection is applied here, after NQL returns whole
68//! documents, because NQL is FROM-first and has no projection clause.
69//!
70//! Not supported, each refused by name: JOIN, subqueries, CTEs, window
71//! functions, DDL, `TRUNCATE`, `GRANT`/`REVOKE`. `INSERT` requires an explicit
72//! column list, because NEDB is schemaless and there is no declared column
73//! order to infer.
74//!
75//! # Protocol coverage
76//!
77//! **Both** protocols are implemented:
78//!
79//! * the **simple query protocol** (`Q`) — what `psql` and libpq's `PQexec`
80//!   use, and therefore psycopg2, which interpolates parameters client-side;
81//! * the **extended query protocol** (`Parse`/`Bind`/`Describe`/`Execute`/
82//!   `Close`/`Sync`/`Flush`) — what psycopg3, asyncpg and the JDBC driver use
83//!   for every parameterised statement. Without it those three could not run a
84//!   single query, so "psql works" was a long way from "your framework works".
85//!
86//! Parameters arrive in text *and* binary format, prepared statements and
87//! portals are per-connection, and a row-capped `Execute` suspends its portal
88//! (`PortalSuspended`) so a JDBC `setFetchSize` pages instead of stalling.
89//!
90//! ## Parameter typing in a store with no schema
91//!
92//! The extended protocol needs types for `$1..$n`, which a relational server
93//! reads out of its catalogue. NEDB has none — so the types are sampled from
94//! the documents already stored, and the stored data *is* the schema. Where a
95//! placeholder sits in a clause rather than beside a column
96//! (`AS OF SYSTEM TIME $1`, `LIMIT $1`) the grammar supplies the type instead,
97//! and an aggregate column is typed from what the aggregate means: a `COUNT` is
98//! an integer, an `AVG` fractional.
99//!
100//! This is not polish. A client that declares its own parameter types
101//! (psycopg3, JDBC) is believed and only its unspecified slots are inferred —
102//! but asyncpg declares none, asks, and then **refuses the call client-side**
103//! if the answer is wrong. Advertising "text" for everything does not degrade
104//! gracefully there; it fails with `expected str, got int` before a query is
105//! ever sent.
106//!
107//! SSL is declined (`N`), so connections are cleartext — hence the loopback
108//! default.
109//!
110//! Authentication mirrors the HTTP surface: with `NEDBD_TOKEN` set the password
111//! must equal it; otherwise any connection is accepted.
112//!
113//! Still outside the boundary, and refused by name: SQL-level cursors
114//! (`DECLARE`/`FETCH`), `pg_catalog` introspection (so `\dt` and DBeaver's
115//! schema browser stay empty), and binary *result* format for a column whose
116//! stored values disagree about their type across documents.
117
118use std::collections::HashMap;
119use std::sync::Arc;
120
121use serde_json::Value;
122use tokio::io::{AsyncReadExt, AsyncWriteExt};
123use tokio::net::{TcpListener, TcpStream};
124
125use crate::db::Db;
126
127// ── Postgres type OIDs we hand out ──────────────────────────────────────────
128const OID_BOOL: i32 = 16;
129const OID_INT8: i32 = 20;
130const OID_FLOAT8: i32 = 701;
131const OID_TEXT: i32 = 25;
132
133const PROTO_V3: i32 = 196_608; // 3.0 << 16
134const SSL_REQUEST: i32 = 80_877_103;
135const GSS_REQUEST: i32 = 80_877_104;
136const CANCEL_REQUEST: i32 = 80_877_102;
137
138/// How a caller resolves a database name to an open `Db`.
139///
140/// A trait object rather than a concrete handle so this module does not depend
141/// on `server::Manager` — which keeps the protocol code unit-testable against a
142/// plain `Db` with no HTTP stack in the way.
143pub trait DbResolver: Send + Sync + 'static {
144    /// Look up an open database by the name the client connected with.
145    ///
146    /// MAY BLOCK. The implementation is allowed to take a lock, so this is
147    /// always called from `spawn_blocking` — never on an async worker. Taking
148    /// a tokio `RwLock::blocking_read()` on a runtime thread panics outright
149    /// ("Cannot block the current thread from within a runtime"), which is
150    /// exactly how the first cut of this failed.
151    fn resolve(&self, name: &str) -> Option<Arc<Db>>;
152    /// The bearer token, when one is configured. `None` = open access.
153    fn token(&self) -> Option<String> {
154        None
155    }
156}
157
158// ── wire encoding helpers ───────────────────────────────────────────────────
159
160struct Out(Vec<u8>);
161
162impl Out {
163    fn msg(tag: u8) -> Self {
164        // Tag, then a 4-byte length placeholder patched in `finish`.
165        Out(vec![tag, 0, 0, 0, 0])
166    }
167    fn i16(&mut self, v: i16) { self.0.extend_from_slice(&v.to_be_bytes()); }
168    fn i32(&mut self, v: i32) { self.0.extend_from_slice(&v.to_be_bytes()); }
169    fn cstr(&mut self, s: &str) {
170        // A NUL inside an identifier would truncate the field and desynchronise
171        // the stream, so strip rather than trust.
172        self.0.extend_from_slice(s.replace('\0', "").as_bytes());
173        self.0.push(0);
174    }
175    fn bytes(&mut self, b: &[u8]) { self.0.extend_from_slice(b); }
176    /// Patch the length prefix (which covers the length field itself, not the tag).
177    fn finish(mut self) -> Vec<u8> {
178        let len = (self.0.len() - 1) as i32;
179        self.0[1..5].copy_from_slice(&len.to_be_bytes());
180        self.0
181    }
182}
183
184fn err_msg(code: &str, message: &str) -> Vec<u8> {
185    let mut m = Out::msg(b'E');
186    m.bytes(b"S"); m.cstr("ERROR");
187    m.bytes(b"C"); m.cstr(code);
188    m.bytes(b"M"); m.cstr(message);
189    m.0.push(0);
190    m.finish()
191}
192
193fn ready() -> Vec<u8> {
194    let mut m = Out::msg(b'Z');
195    m.bytes(b"I"); // idle, not in a transaction
196    m.finish()
197}
198
199fn command_complete(tag: &str) -> Vec<u8> {
200    let mut m = Out::msg(b'C');
201    m.cstr(tag);
202    m.finish()
203}
204
205// ── SQL → NQL translation ───────────────────────────────────────────────────
206
207/// One output column: the key to read from the row, and the name to show.
208///
209/// The two differ for aggregates. NQL answers `SUM(total)` with a row holding
210/// `sum_total` (plus `count` and a legacy `value` alias), while SQL callers
211/// expect a single column called `sum`. Carrying both halves keeps NEDB's
212/// internal key names off the wire — the first cut leaked `['count','value']`
213/// out of a `SELECT COUNT(*)`, which is two columns where SQL promises one.
214#[derive(Debug, PartialEq, Clone)]
215pub struct Col {
216    pub src: String,
217    pub out: String,
218}
219
220impl Col {
221    fn same(name: &str) -> Self {
222        Col { src: name.to_string(), out: name.to_string() }
223    }
224    fn renamed(src: &str, out: &str) -> Self {
225        Col { src: src.to_string(), out: out.to_string() }
226    }
227}
228
229/// What a translated statement asks for.
230///
231/// The write variants exist because SQL's write semantics and NEDB's storage
232/// model line up almost exactly, which was not obvious until it was written
233/// down:
234///
235/// | SQL | NEDB |
236/// |---|---|
237/// | `INSERT` | a put |
238/// | `UPDATE … WHERE` | a NEW VERSION of each matching document |
239/// | `DELETE … WHERE` | a tombstone |
240///
241/// NEDB is append-only, so an `UPDATE` is *already* a versioned write and a
242/// `DELETE` is *already* a tombstone. Nothing is being bent to fit. The
243/// consequence is the thing worth selling: run the SQL you would run against
244/// Postgres, and the tamper-evident history falls out for free — the prior
245/// value is still readable with `AS OF SYSTEM TIME`.
246#[derive(Debug, PartialEq)]
247pub enum Stmt {
248    /// Run this NQL, then project these columns (empty = all).
249    Query { nql: String, project: Vec<Col> },
250    /// `INSERT INTO coll (cols) VALUES (…), (…) [RETURNING …]`
251    Insert { coll: String, rows: Vec<InsertRow>, returning: Vec<Col> },
252    /// `UPDATE coll SET … [WHERE …] [RETURNING …]` — a new version per match.
253    Update { coll: String, set: Vec<(String, Value)>, nql: String, returning: Vec<Col> },
254    /// `DELETE FROM coll [WHERE …] [RETURNING …]` — a tombstone per match.
255    Delete { coll: String, nql: String, returning: Vec<Col> },
256    /// Answer from a fixed table — the handshake queries clients send on connect.
257    Canned { cols: Vec<String>, row: Vec<String> },
258    /// Nothing to do (empty statement, or a SET the client does not need honoured).
259    Ok(&'static str),
260}
261
262/// One row of an `INSERT`: an explicit id when the statement supplied one, the
263/// document body, and optional provenance lifted out of reserved columns.
264#[derive(Debug, PartialEq, Clone)]
265pub struct InsertRow {
266    /// From an `_id` or `id` column. `None` means the server assigns one.
267    pub id: Option<String>,
268    pub doc: serde_json::Map<String, Value>,
269    /// From a `_caused_by` column — the causal parents, so provenance is
270    /// reachable from SQL rather than only from the HTTP API.
271    pub caused_by: Vec<String>,
272    pub valid_from: Option<String>,
273    pub valid_to: Option<String>,
274}
275
276/// Strip SQL comments and collapse whitespace, so the matchers below can be
277/// simple without being fragile about formatting.
278fn normalise(sql: &str) -> String {
279    let mut out = String::with_capacity(sql.len());
280    let mut chars = sql.chars().peekable();
281    let mut in_s = false;
282    while let Some(c) = chars.next() {
283        if in_s {
284            out.push(c);
285            if c == '\'' { in_s = false; }
286            continue;
287        }
288        match c {
289            '\'' => { in_s = true; out.push(c); }
290            '-' if chars.peek() == Some(&'-') => {
291                // line comment
292                for n in chars.by_ref() { if n == '\n' { break; } }
293                out.push(' ');
294            }
295            '/' if chars.peek() == Some(&'*') => {
296                chars.next();
297                let mut prev = ' ';
298                while let Some(n) = chars.next() {
299                    if prev == '*' && n == '/' { break; }
300                    prev = n;
301                }
302                out.push(' ');
303            }
304            _ => out.push(c),
305        }
306    }
307    out.split_whitespace().collect::<Vec<_>>().join(" ")
308}
309
310/// Rewrite SQL literal/operator spellings into NQL's.
311///
312/// Only `'…'` → `"…"` and `<>` → `!=`. Done with an explicit scan rather than a
313/// regex so a quote inside a string cannot be mistaken for a delimiter: SQL
314/// escapes an embedded quote by doubling it (`'it''s'`), and that has to become
315/// a single character inside the NQL string rather than terminating it.
316fn sql_literals_to_nql(s: &str) -> String {
317    let mut out = String::with_capacity(s.len());
318    let mut it = s.chars().peekable();
319    while let Some(c) = it.next() {
320        match c {
321            '\'' => {
322                out.push('"');
323                while let Some(ch) = it.next() {
324                    if ch == '\'' {
325                        if it.peek() == Some(&'\'') {
326                            it.next();
327                            out.push('\''); // doubled '' is one literal quote
328                        } else {
329                            break;
330                        }
331                    } else if ch == '"' {
332                        // A double quote inside a SQL literal must be escaped
333                        // for NQL, whose lexer collapses \" to a literal quote.
334                        out.push('\\');
335                        out.push('"');
336                    } else {
337                        out.push(ch);
338                    }
339                }
340                out.push('"');
341            }
342            '<' if it.peek() == Some(&'>') => { it.next(); out.push_str("!="); }
343            _ => out.push(c),
344        }
345    }
346    out
347}
348
349fn strip_prefix_ci(s: &str, prefix: &str) -> Option<String> {
350    if s.len() >= prefix.len() && s[..prefix.len()].eq_ignore_ascii_case(prefix) {
351        Some(s[prefix.len()..].trim_start().to_string())
352    } else {
353        None
354    }
355}
356
357/// Find a top-level keyword (not inside quotes or parentheses), returning its
358/// byte offset. Case-insensitive, and only matches on word boundaries.
359fn find_kw(s: &str, kw: &str) -> Option<usize> {
360    let bytes = s.as_bytes();
361    let k = kw.as_bytes();
362    let mut depth = 0i32;
363    let mut in_s = false;
364    let mut in_d = false;
365    let mut i = 0usize;
366    while i < bytes.len() {
367        let c = bytes[i];
368        if in_s { if c == b'\'' { in_s = false; } i += 1; continue; }
369        if in_d { if c == b'"' { in_d = false; } i += 1; continue; }
370        match c {
371            b'\'' => { in_s = true; i += 1; continue; }
372            b'"' => { in_d = true; i += 1; continue; }
373            b'(' => { depth += 1; i += 1; continue; }
374            b')' => { depth -= 1; i += 1; continue; }
375            _ => {}
376        }
377        if depth == 0 && i + k.len() <= bytes.len()
378            && bytes[i..i + k.len()].eq_ignore_ascii_case(k)
379        {
380            let before_ok = i == 0 || !(bytes[i - 1] as char).is_alphanumeric() && bytes[i - 1] != b'_';
381            let after = i + k.len();
382            let after_ok = after >= bytes.len()
383                || !(bytes[after] as char).is_alphanumeric() && bytes[after] != b'_';
384            if before_ok && after_ok {
385                return Some(i);
386            }
387        }
388        i += 1;
389    }
390    None
391}
392
393/// Split a comma-separated list at the TOP level, ignoring commas inside
394/// quotes or parentheses — so `VALUES (1, 'a,b'), (2, 'c')` splits into two
395/// groups and not four.
396fn split_top(s: &str, sep: char) -> Vec<String> {
397    let mut out = vec![];
398    let mut cur = String::new();
399    let mut depth = 0i32;
400    let mut in_s = false;
401    let mut it = s.chars().peekable();
402    while let Some(c) = it.next() {
403        if in_s {
404            cur.push(c);
405            if c == '\'' {
406                // A doubled '' is an escaped quote, not the end of the literal.
407                if it.peek() == Some(&'\'') { cur.push(it.next().unwrap()); } else { in_s = false; }
408            }
409            continue;
410        }
411        match c {
412            '\'' => { in_s = true; cur.push(c); }
413            '(' => { depth += 1; cur.push(c); }
414            ')' => { depth -= 1; cur.push(c); }
415            x if x == sep && depth == 0 => { out.push(cur.trim().to_string()); cur.clear(); }
416            _ => cur.push(c),
417        }
418    }
419    if !cur.trim().is_empty() { out.push(cur.trim().to_string()); }
420    out
421}
422
423/// Parse one SQL scalar literal into JSON.
424///
425/// Deliberately narrow: a string, a number, a boolean, or NULL. Anything else
426/// — a function call, an expression, a cast — is refused by name rather than
427/// coerced into a string that would silently store the wrong value.
428fn sql_value(raw: &str) -> Result<Value, String> {
429    let t = raw.trim();
430    if t.is_empty() {
431        return Err("empty value".into());
432    }
433    let up = t.to_uppercase();
434    if up == "NULL" { return Ok(Value::Null); }
435    if up == "TRUE" { return Ok(Value::Bool(true)); }
436    if up == "FALSE" { return Ok(Value::Bool(false)); }
437    if t.starts_with('\'') && t.ends_with('\'') && t.len() >= 2 {
438        // Unwrap, collapsing the SQL '' escape to one quote.
439        let inner = &t[1..t.len() - 1];
440        return Ok(Value::String(inner.replace("''", "'")));
441    }
442    if let Ok(i) = t.parse::<i64>() { return Ok(Value::from(i)); }
443    if let Ok(f) = t.parse::<f64>() { return Ok(Value::from(f)); }
444    Err(format!(
445        "cannot use {:?} as a value — this endpoint accepts string literals, \
446         numbers, TRUE/FALSE and NULL. Expressions, casts and function calls \
447         are not evaluated, because storing an unevaluated expression as text \
448         would be worse than refusing it", t))
449}
450
451/// Pull a trailing `RETURNING …` off a statement, returning (head, columns).
452fn split_returning(tail: &str) -> (String, Vec<Col>) {
453    let tu = tail.to_uppercase();
454    match find_kw(&tu, "RETURNING") {
455        None => (tail.to_string(), vec![]),
456        Some(at) => {
457            let head = tail[..at].trim().to_string();
458            let list = tail[at + "RETURNING".len()..].trim();
459            if list == "*" {
460                return (head, vec![]);   // empty projection = every column
461            }
462            let cols = split_top(list, ',')
463                .into_iter()
464                .map(|p| {
465                    let raw = p.split_whitespace().next().unwrap_or(&p).to_string();
466                    let name = raw.rsplit('.').next().unwrap_or(&raw).trim_matches('"').to_string();
467                    Col::same(&name)
468                })
469                .collect();
470            (head, cols)
471        }
472    }
473}
474
475/// Columns whose names are reserved: they carry provenance rather than data.
476fn take_reserved(doc: &mut serde_json::Map<String, Value>) -> (Option<String>, Vec<String>, Option<String>, Option<String>) {
477    let id = doc.remove("_id").or_else(|| doc.remove("id"))
478        .and_then(|v| match v {
479            Value::String(s) => Some(s),
480            Value::Null => None,
481            other => Some(other.to_string()),   // a numeric key is a fine id
482        });
483    let caused_by = match doc.remove("_caused_by") {
484        Some(Value::String(s)) => vec![s],
485        Some(Value::Array(a)) => a.into_iter()
486            .filter_map(|v| v.as_str().map(str::to_string)).collect(),
487        _ => vec![],
488    };
489    let vf = doc.remove("_valid_from").and_then(|v| v.as_str().map(str::to_string));
490    let vt = doc.remove("_valid_to").and_then(|v| v.as_str().map(str::to_string));
491    (id, caused_by, vf, vt)
492}
493
494/// `INSERT INTO coll (c1, c2) VALUES (v1, v2), (…) [RETURNING …]`
495fn translate_insert(sql: &str) -> Result<Stmt, String> {
496    let rest = strip_prefix_ci(sql, "INSERT")
497        .and_then(|r| strip_prefix_ci(&r, "INTO"))
498        .ok_or("expected INSERT INTO")?;
499    // Locate VALUES first. Everything before it is `coll (col, …)`; searching
500    // for `(` without that bound finds the VALUES parenthesis instead and
501    // swallows the keyword into the collection name.
502    let ru = rest.to_uppercase();
503    let values_at = find_kw(&ru, "VALUES").ok_or(
504        "expected VALUES — `INSERT … SELECT` is not supported on this endpoint")?;
505    let head = rest[..values_at].trim().to_string();
506    let open = head.find('(').ok_or(
507        "INSERT needs an explicit column list — `INSERT INTO t (a, b) VALUES (…)`. \
508         NEDB is schemaless, so there is no declared column order to infer from")?;
509    let coll = head[..open].trim().trim_matches('"');
510    let coll = coll.rsplit('.').next().unwrap_or(coll).to_string();
511    if coll.is_empty() {
512        return Err("expected a collection name after INSERT INTO".into());
513    }
514    let close = head.rfind(')').ok_or("unterminated column list")?;
515    if close < open {
516        return Err("malformed column list".into());
517    }
518    let tail_from_values = rest[values_at..].to_string();
519    let cols: Vec<String> = split_top(&head[open + 1..close], ',')
520        .into_iter()
521        .map(|c| c.trim().trim_matches('"').to_string())
522        .collect();
523    if cols.is_empty() {
524        return Err("the column list is empty".into());
525    }
526
527    let after = strip_prefix_ci(&tail_from_values, "VALUES")
528        .ok_or("expected VALUES after the column list")?;
529    let (values_part, returning) = split_returning(&after);
530
531    let mut rows = vec![];
532    for group in split_top(&values_part, ',') {
533        let g = group.trim();
534        if !(g.starts_with('(') && g.ends_with(')')) {
535            return Err(format!("expected a parenthesised row of values, got {:?}", g));
536        }
537        let vals = split_top(&g[1..g.len() - 1], ',');
538        if vals.len() != cols.len() {
539            return Err(format!(
540                "{} values for {} columns — every row must match the column list",
541                vals.len(), cols.len()));
542        }
543        let mut doc = serde_json::Map::new();
544        for (c, v) in cols.iter().zip(vals.iter()) {
545            doc.insert(c.clone(), sql_value(v)?);
546        }
547        let (id, caused_by, valid_from, valid_to) = take_reserved(&mut doc);
548        rows.push(InsertRow { id, doc, caused_by, valid_from, valid_to });
549    }
550    if rows.is_empty() {
551        return Err("INSERT with no rows".into());
552    }
553    Ok(Stmt::Insert { coll, rows, returning })
554}
555
556/// `UPDATE coll SET a = 1, b = 'x' [WHERE …] [RETURNING …]`
557fn translate_update(sql: &str) -> Result<Stmt, String> {
558    let rest = strip_prefix_ci(sql, "UPDATE").ok_or("expected UPDATE")?;
559    let ru = rest.to_uppercase();
560    let set_at = find_kw(&ru, "SET").ok_or("expected SET in UPDATE")?;
561    let coll = rest[..set_at].trim().trim_matches('"');
562    let coll = coll.rsplit('.').next().unwrap_or(coll).to_string();
563    if coll.is_empty() {
564        return Err("expected a collection name after UPDATE".into());
565    }
566    let after_set = rest[set_at + 3..].trim().to_string();
567    let (after_set, returning) = split_returning(&after_set);
568
569    // WHERE ends the assignment list; everything after it is a NQL predicate.
570    let au = after_set.to_uppercase();
571    let (assigns_raw, where_raw) = match find_kw(&au, "WHERE") {
572        Some(at) => (after_set[..at].to_string(), after_set[at..].to_string()),
573        None => (after_set.clone(), String::new()),
574    };
575
576    let mut set = vec![];
577    for a in split_top(&assigns_raw, ',') {
578        let eq = a.find('=').ok_or(format!("expected `col = value` in SET, got {:?}", a))?;
579        let col = a[..eq].trim().trim_matches('"').to_string();
580        if col.is_empty() {
581            return Err("empty column name in SET".into());
582        }
583        set.push((col, sql_value(&a[eq + 1..])?));
584    }
585    if set.is_empty() {
586        return Err("UPDATE with no assignments".into());
587    }
588    // The matching rows are found with an ordinary NQL read, so the whole
589    // predicate surface (IN, BETWEEN, LIKE, OR, …) works in an UPDATE too.
590    let nql = format!("FROM {} {}", coll, sql_literals_to_nql(where_raw.trim()))
591        .trim().to_string();
592    Ok(Stmt::Update { coll, set, nql, returning })
593}
594
595/// `DELETE FROM coll [WHERE …] [RETURNING …]`
596fn translate_delete(sql: &str) -> Result<Stmt, String> {
597    let rest = strip_prefix_ci(sql, "DELETE")
598        .and_then(|r| strip_prefix_ci(&r, "FROM"))
599        .ok_or("expected DELETE FROM")?;
600    let (rest, returning) = split_returning(&rest);
601    let end = rest.find(' ').unwrap_or(rest.len());
602    let coll = rest[..end].trim().trim_matches('"');
603    let coll = coll.rsplit('.').next().unwrap_or(coll).to_string();
604    if coll.is_empty() {
605        return Err("expected a collection name after DELETE FROM".into());
606    }
607    let where_raw = rest[end..].trim();
608    let nql = format!("FROM {} {}", coll, sql_literals_to_nql(where_raw))
609        .trim().to_string();
610    Ok(Stmt::Delete { coll, nql, returning })
611}
612
613/// Translate one SQL statement into something executable, or explain why not.
614pub fn translate(sql_raw: &str) -> Result<Stmt, String> {
615    let sql = normalise(sql_raw);
616    let sql = sql.trim().trim_end_matches(';').trim();
617    if sql.is_empty() {
618        return Ok(Stmt::Ok(""));
619    }
620    let upper = sql.to_uppercase();
621
622    // ── the handshake. Clients issue these before anything useful; answering
623    // them with plausible values is the difference between "connects" and
624    // "hangs on startup". They are canned on purpose — NEDB has no pg_catalog
625    // and pretending otherwise would be worse than a clear boundary.
626    if upper.starts_with("SET ") || upper.starts_with("BEGIN") || upper.starts_with("COMMIT")
627        || upper.starts_with("ROLLBACK") || upper.starts_with("DISCARD")
628        || upper.starts_with("LISTEN ") || upper.starts_with("UNLISTEN ")
629    {
630        // Accepted and ignored: there is one implicit read-only transaction.
631        return Ok(Stmt::Ok(if upper.starts_with("SET") { "SET" } else { "OK" }));
632    }
633    if upper.starts_with("SHOW ") {
634        let name = sql[5..].trim().to_lowercase();
635        let val = match name.as_str() {
636            "transaction_isolation" | "default_transaction_isolation" => "read committed",
637            "server_version" => SERVER_VERSION,
638            "server_encoding" | "client_encoding" => "UTF8",
639            "standard_conforming_strings" => "on",
640            "is_superuser" => "off",
641            _ => "",
642        };
643        return Ok(Stmt::Canned { cols: vec![name], row: vec![val.to_string()] });
644    }
645    if upper == "SELECT VERSION()" {
646        return Ok(Stmt::Canned {
647            cols: vec!["version".into()],
648            row: vec![full_version_string()],
649        });
650    }
651    if upper == "SELECT 1" || upper == "SELECT 1;" {
652        return Ok(Stmt::Canned { cols: vec!["?column?".into()], row: vec!["1".into()] });
653    }
654    if upper.starts_with("SELECT CURRENT_SCHEMA") {
655        return Ok(Stmt::Canned { cols: vec!["current_schema".into()], row: vec!["public".into()] });
656    }
657    if upper.starts_with("SELECT CURRENT_DATABASE") {
658        return Ok(Stmt::Canned { cols: vec!["current_database".into()], row: vec!["nedb".into()] });
659    }
660    if upper.starts_with("SELECT CURRENT_USER") || upper.starts_with("SELECT USER") {
661        return Ok(Stmt::Canned { cols: vec!["current_user".into()], row: vec!["nedb".into()] });
662    }
663
664    // ── writes ───────────────────────────────────────────────────────────────
665    // SQL's write semantics and NEDB's append-only model line up, so these are
666    // first-class rather than refused. See the `Stmt` doc comment.
667    if upper.starts_with("INSERT") { return translate_insert(sql); }
668    if upper.starts_with("UPDATE") { return translate_update(sql); }
669    if upper.starts_with("DELETE") { return translate_delete(sql); }
670
671    // ── the refusals that remain, each naming the boundary ──────────────────
672    for (kw, why) in [
673        ("CREATE", "DDL is not supported — collections are created implicitly by the first write to them, because NEDB is schemaless"),
674        ("ALTER", "DDL is not supported — there is no schema to alter"),
675        ("DROP", "DDL is not supported; drop a database with DELETE /v1/databases/<db>"),
676        ("TRUNCATE", "not supported, and not an oversight: NEDB is append-only so that history cannot be discarded. That is the product"),
677        ("COPY", "not supported; use GET /v1/databases/<db>/since for bulk export"),
678        ("GRANT", "there is no SQL-level privilege system; auth is the bearer token"),
679        ("REVOKE", "there is no SQL-level privilege system; auth is the bearer token"),
680    ] {
681        if upper.starts_with(kw) {
682            return Err(format!("{} is not supported — {}", kw, why));
683        }
684    }
685    if !upper.starts_with("SELECT") {
686        return Err(format!(
687            "only SELECT, INSERT, UPDATE and DELETE are supported on the Postgres \
688             endpoint (got {:?})",
689            sql.split_whitespace().next().unwrap_or("")
690        ));
691    }
692    for (kw, why) in [
693        (" JOIN ", "JOIN is not supported — NQL is single-collection; join in your client or model the relation with LINK/TRAVERSE"),
694        (" UNION ", "UNION is not supported"),
695        (" INTERSECT ", "INTERSECT is not supported"),
696        (" EXCEPT ", "EXCEPT is not supported"),
697        (" OVER (", "window functions are not supported"),
698        ("DISTINCT ", "DISTINCT is not supported — GROUP BY <col> gives the distinct values with counts"),
699    ] {
700        if upper.contains(kw) {
701            return Err(why.to_string());
702        }
703    }
704    if find_kw(&upper, "FROM").is_none() {
705        return Err("SELECT without FROM is not supported on this endpoint".into());
706    }
707
708    // ── SELECT <projection> FROM <rest> ──────────────────────────────────────
709    let after_select = strip_prefix_ci(sql, "SELECT").ok_or("expected SELECT")?;
710    let from_at = find_kw(&after_select.to_uppercase(), "FROM")
711        .ok_or("expected FROM after the select list")?;
712    let projection = after_select[..from_at].trim().to_string();
713    let rest = after_select[from_at + 4..].trim().to_string();
714    if rest.is_empty() {
715        return Err("expected a collection name after FROM".into());
716    }
717    // A subquery in the FROM position, or a comma-separated table list (an
718    // implicit cross join), are both out of scope — say which.
719    if rest.starts_with('(') {
720        return Err("subqueries in FROM are not supported".into());
721    }
722    let coll_end = rest.find(' ').unwrap_or(rest.len());
723    let coll = &rest[..coll_end];
724    if coll.contains(',') {
725        return Err("selecting from more than one collection is not supported (no JOIN)".into());
726    }
727    // Postgres clients often qualify as schema.table; NEDB has one namespace,
728    // so the schema is dropped — EXCEPT for `information_schema`, whose table
729    // names (`tables`, `columns`) are words a user could plausibly name a
730    // collection. Keeping the qualifier there is what stops
731    // `SELECT * FROM information_schema.tables` and a real collection called
732    // `tables` from resolving to the same thing.
733    let bare = coll.rsplit('.').next().unwrap_or(coll).trim_matches('"');
734    let qualified = coll
735        .split('.')
736        .map(|p| p.trim_matches('"'))
737        .collect::<Vec<_>>()
738        .join(".");
739    let coll = if qualified.starts_with("information_schema.") {
740        qualified.as_str()
741    } else {
742        bare
743    };
744    let tail = rest[coll_end..].trim();
745
746    // ── the select list ──────────────────────────────────────────────────────
747    let pu = projection.to_uppercase();
748    let mut agg_clause = String::new();
749    let mut project: Vec<Col> = vec![];
750
751    if projection == "*" {
752        // everything
753    } else if pu.starts_with("COUNT(") {
754        // COUNT(*) and COUNT(col) both become NQL's bare COUNT: NQL counts the
755        // group, and a per-column non-null count is not expressible here.
756        agg_clause = " COUNT".to_string();
757        project.push(Col::same("count"));
758    } else if let Some(agg) = ["SUM", "AVG", "MIN", "MAX"]
759        .iter()
760        .find(|a| pu.starts_with(&format!("{}(", a)))
761    {
762        let inner = projection[agg.len() + 1..]
763            .trim_end_matches(')')
764            .trim()
765            .to_string();
766        if inner.is_empty() || inner == "*" {
767            return Err(format!("{}() needs a column", agg));
768        }
769        agg_clause = format!(" {} {}", agg, inner);
770        // NQL emits `<agg>_<field>`; SQL names the column after the function.
771        project.push(Col::renamed(
772            &format!("{}_{}", agg.to_lowercase(), inner),
773            &agg.to_lowercase(),
774        ));
775    } else {
776        for part in projection.split(',') {
777            let p = part.trim();
778            if p.is_empty() {
779                return Err("empty column in the select list".into());
780            }
781            if p.contains('(') {
782                return Err(format!(
783                    "expressions in the select list are not supported ({:?}) — \
784                     supported: *, a column list, COUNT(*), or SUM/AVG/MIN/MAX(col)", p));
785            }
786            // strip an alias: `col AS x` / `col x`
787            let raw = p.split_whitespace().next().unwrap_or(p);
788            let name = raw.rsplit('.').next().unwrap_or(raw).trim_matches('"');
789            project.push(Col::same(name));
790        }
791    }
792
793    // ── clause tail: AS OF SYSTEM TIME → AS OF, then pass the rest through ──
794    //
795    // The clause keywords NQL shares with SQL (WHERE, GROUP BY, HAVING,
796    // ORDER BY, LIMIT, OFFSET) are deliberately handed to the NQL parser
797    // unchanged rather than re-parsed here. NQL is the authority on what is
798    // valid; re-implementing its grammar would give two parsers to disagree.
799    let mut tail = tail.to_string();
800    let tu = tail.to_uppercase();
801    if let Some(at) = find_kw(&tu, "AS OF SYSTEM TIME") {
802        let before = tail[..at].to_string();
803        let after = tail[at + "AS OF SYSTEM TIME".len()..].trim_start().to_string();
804        // Take the sequence token; the rest of the tail follows it.
805        let end = after.find(' ').unwrap_or(after.len());
806        let seq = after[..end].trim().trim_matches('\'').trim_matches('"').to_string();
807        if seq.parse::<u64>().is_err() {
808            return Err(format!(
809                "AS OF SYSTEM TIME takes a NEDB sequence number here, not a timestamp (got {:?}). \
810                 NEDB's history is sequence-addressed and never garbage-collected, so a seq is \
811                 exact where a wall-clock time would be approximate", seq));
812        }
813        tail = format!("{} AS OF {} {}", before.trim(), seq, after[end..].trim())
814            .trim()
815            .to_string();
816    }
817
818    // ── GROUP BY: refuse a bare column that SQL would refuse ─────────────────
819    //
820    // A grouped NQL row holds only the group key, `count` and the aggregate —
821    // so projecting `total` from `GROUP BY region` found nothing and rendered
822    // NULL. Silently answering NULL for a column the query cannot produce is
823    // the exact failure shape this engine keeps getting bitten by, so it is an
824    // error, using Postgres's own wording so the message is already familiar.
825    let tu_all = tail.to_uppercase();
826    if let Some(gb_at) = find_kw(&tu_all, "GROUP BY") {
827        let after = tail[gb_at + "GROUP BY".len()..].trim_start();
828        let key_end = after.find(|c: char| c == ' ' || c == ',').unwrap_or(after.len());
829        let group_key = after[..key_end].trim().trim_matches('"').to_string();
830        let is_agg = !agg_clause.is_empty();
831        for c in &project {
832            let ok = c.src == group_key
833                || c.src == "count"
834                || (is_agg && c.out == agg_clause.trim().split(' ').next()
835                        .unwrap_or("").to_lowercase());
836            if !ok {
837                return Err(format!(
838                    "column {:?} must appear in the GROUP BY clause or be used in an \
839                     aggregate function — a grouped row carries the group key, `count`, \
840                     and the aggregate, nothing else",
841                    c.src));
842            }
843        }
844    }
845
846    let tail = sql_literals_to_nql(&tail);
847    let nql = format!("FROM {}{}{}", coll,
848                      if agg_clause.is_empty() { String::new() } else { agg_clause },
849                      if tail.is_empty() { String::new() } else { format!(" {}", tail) });
850
851    Ok(Stmt::Query { nql: nql.trim().to_string(), project })
852}
853
854const SERVER_VERSION: &str = "15.0";
855
856/// The `version()` string, for the SQL engine's `version()` function.
857pub fn version_string() -> String {
858    full_version_string()
859}
860
861fn full_version_string() -> String {
862    format!(
863        "PostgreSQL {} (NEDB {}) — tamper-evident, append-only, permanent \
864         history. SELECT + INSERT/UPDATE/DELETE; an UPDATE is a new version, \
865         so prior values stay readable with AS OF SYSTEM TIME.",
866        SERVER_VERSION,
867        env!("CARGO_PKG_VERSION")
868    )
869}
870
871// ── result shaping ──────────────────────────────────────────────────────────
872
873/// Pick the column order for a result set.
874///
875/// With an explicit projection, that order. Otherwise the union of keys across
876/// the returned rows — `_`-prefixed provenance columns last, so `psql` shows
877/// the user's own fields first and `_hash` does not push `status` off screen.
878fn columns_for(rows: &[Value], project: &[Col]) -> Vec<Col> {
879    if !project.is_empty() {
880        return project.to_vec();
881    }
882    let mut plain: Vec<String> = vec![];
883    let mut meta: Vec<String> = vec![];
884    for r in rows {
885        if let Value::Object(m) = r {
886            for k in m.keys() {
887                let target = if k.starts_with('_') { &mut meta } else { &mut plain };
888                if !target.contains(k) {
889                    target.push(k.clone());
890                }
891            }
892        }
893    }
894    plain.sort();
895    meta.sort();
896    plain.extend(meta);
897    plain.into_iter().map(|k| Col::same(&k)).collect()
898}
899
900/// The Postgres type of one JSON value.
901fn oid_of_value(v: &Value) -> Option<i32> {
902    match v {
903        Value::Null => None,
904        Value::Bool(_) => Some(OID_BOOL),
905        Value::Number(n) => Some(if n.is_i64() || n.is_u64() { OID_INT8 } else { OID_FLOAT8 }),
906        Value::String(_) => Some(OID_TEXT),
907        // Arrays and objects render as their JSON text.
908        _ => Some(OID_TEXT),
909    }
910}
911
912/// Reconcile two observed types for the same column.
913///
914/// A relational column has one type by construction. A NEDB collection does
915/// not: document 1 may hold `qty: 3` and document 2 `qty: "three"`. Widening
916/// to `text` on a conflict is the only answer that can carry both, and mixed
917/// integers and floats widen to float8 for the same reason.
918fn unify_oid(a: i32, b: i32) -> i32 {
919    if a == b {
920        return a;
921    }
922    match (a, b) {
923        (OID_INT8, OID_FLOAT8) | (OID_FLOAT8, OID_INT8) => OID_FLOAT8,
924        _ => OID_TEXT,
925    }
926}
927
928/// The type of `col` across EVERY row in the result, not just the first.
929///
930/// Taking the first non-null value's type was a latent wrong answer: a column
931/// holding `3` in row one and `"n/a"` in row two was advertised as `int8`, and
932/// a client that believes the description then fails parsing `"n/a"` as an
933/// integer — or, on the binary path, cannot be sent the value at all.
934/// Public alias so `pgcatalog` types a column EXACTLY as the wire does.
935///
936/// The catalogue reporting `bigint` for a column the protocol then sends as
937/// text would be a self-contradiction a client is entitled to trust, so both
938/// go through this one function rather than two that agree today.
939pub fn oid_for_column(rows: &[Value], col: &str) -> i32 {
940    oid_for(rows, col)
941}
942
943fn oid_for(rows: &[Value], col: &str) -> i32 {
944    let mut acc: Option<i32> = None;
945    for r in rows {
946        if let Some(o) = r.get(col).and_then(oid_of_value) {
947            acc = Some(match acc {
948                None => o,
949                Some(prev) => unify_oid(prev, o),
950            });
951            if acc == Some(OID_TEXT) {
952                break; // text absorbs everything; no need to look further
953            }
954        }
955    }
956    acc.unwrap_or(OID_TEXT)
957}
958
959/// Render one cell in the text format Postgres clients expect for format 0.
960fn cell(v: Option<&Value>) -> Option<String> {
961    match v {
962        None | Some(Value::Null) => None, // NULL on the wire
963        Some(Value::String(s)) => Some(s.clone()),
964        Some(Value::Bool(b)) => Some(if *b { "t".into() } else { "f".into() }),
965        Some(other) => Some(other.to_string()),
966    }
967}
968
969/// Render one cell in binary format for the type the column was advertised as.
970///
971/// Needed because asyncpg asks for binary results — it is not an optimisation
972/// there, it is the only format it requests, so without this it cannot read a
973/// single row. Text-format clients never reach this path.
974///
975/// A value that does not fit the advertised type is an error rather than a
976/// coercion. The advertised type comes from sampling stored documents, so a
977/// mismatch means the field is genuinely heterogeneous beyond the sample, and
978/// quietly sending a zero (or the text bytes under a binary header) would
979/// corrupt the value in a way the client cannot detect.
980fn cell_binary(v: Option<&Value>, oid: i32) -> Result<Option<Vec<u8>>, String> {
981    let v = match v {
982        None | Some(Value::Null) => return Ok(None),
983        Some(v) => v,
984    };
985    let as_f64 = |n: &serde_json::Number| n.as_f64()
986        .ok_or_else(|| "a number too large to send as float8".to_string());
987    Ok(Some(match (oid, v) {
988        (OID_BOOL, Value::Bool(b)) => vec![u8::from(*b)],
989        (OID_INT2, Value::Number(n)) => {
990            let i = n.as_i64().ok_or("not an integer")?;
991            i16::try_from(i).map_err(|_| format!("{} does not fit in int2", i))?
992                .to_be_bytes().to_vec()
993        }
994        (OID_INT4, Value::Number(n)) => {
995            let i = n.as_i64().ok_or("not an integer")?;
996            i32::try_from(i).map_err(|_| format!("{} does not fit in int4", i))?
997                .to_be_bytes().to_vec()
998        }
999        (OID_INT8, Value::Number(n)) => {
1000            n.as_i64().ok_or("not an integer")?.to_be_bytes().to_vec()
1001        }
1002        (OID_FLOAT4, Value::Number(n)) => (as_f64(n)? as f32).to_be_bytes().to_vec(),
1003        (OID_FLOAT8, Value::Number(n)) => as_f64(n)?.to_be_bytes().to_vec(),
1004        // For the text family, binary and text are the same bytes.
1005        (OID_TEXT | OID_VARCHAR | OID_NAME | OID_UNKNOWN | OID_JSON, _) => {
1006            cell(Some(v)).unwrap_or_default().into_bytes()
1007        }
1008        // jsonb is a one-byte version header then the JSON text.
1009        (OID_JSONB, _) => {
1010            let mut b = vec![1u8];
1011            b.extend_from_slice(cell(Some(v)).unwrap_or_default().as_bytes());
1012            b
1013        }
1014        (oid, val) => {
1015            let kind = match val {
1016                Value::Bool(_) => "a boolean",
1017                Value::Number(_) => "a number",
1018                Value::String(_) => "a string",
1019                Value::Array(_) => "an array",
1020                _ => "an object",
1021            };
1022            return Err(format!(
1023                "cannot send {} in binary format as type OID {} — the field holds \
1024                 more than one type across documents, so it cannot be described \
1025                 by a single Postgres type. Select it with a text cast, or use a \
1026                 text-format client",
1027                kind, oid
1028            ));
1029        }
1030    }))
1031}
1032
1033/// A `RowDescription`, with a per-column wire format code.
1034fn row_description_fmt(cols: &[Col], oids: &[i32], fmts: &[i16]) -> Vec<u8> {
1035    let mut m = Out::msg(b'T');
1036    m.i16(cols.len() as i16);
1037    for (i, c) in cols.iter().enumerate() {
1038        m.cstr(&c.out);
1039        m.i32(0); // table OID — unknown
1040        m.i16((i + 1) as i16); // column attribute number
1041        m.i32(oids.get(i).copied().unwrap_or(OID_TEXT));
1042        m.i16(-1); // variable length
1043        m.i32(-1); // no type modifier
1044        m.i16(fmts.get(i).copied().unwrap_or(0));
1045    }
1046    m.finish()
1047}
1048
1049fn row_description(cols: &[Col], oids: &[i32]) -> Vec<u8> {
1050    row_description_fmt(cols, oids, &[])
1051}
1052
1053fn data_row_bytes(vals: &[Option<Vec<u8>>]) -> Vec<u8> {
1054    let mut m = Out::msg(b'D');
1055    m.i16(vals.len() as i16);
1056    for v in vals {
1057        match v {
1058            None => m.i32(-1),
1059            Some(b) => {
1060                m.i32(b.len() as i32);
1061                m.bytes(b);
1062            }
1063        }
1064    }
1065    m.finish()
1066}
1067
1068fn data_row(vals: &[Option<String>]) -> Vec<u8> {
1069    let owned: Vec<Option<Vec<u8>>> =
1070        vals.iter().map(|v| v.as_ref().map(|s| s.as_bytes().to_vec())).collect();
1071    data_row_bytes(&owned)
1072}
1073
1074/// Encode just the rows: `T` followed by one `D` per row, and NO
1075/// `CommandComplete`.
1076///
1077/// Split out because a write with `RETURNING` must emit `T`/`D`* and then its
1078/// OWN tag (`INSERT 0 3`, `UPDATE 1`). The first cut called `encode_result`
1079/// there, which appends `CommandComplete("SELECT n")` — so one statement sent
1080/// TWO CommandComplete messages. That is a protocol violation, and the visible
1081/// symptom was `RETURNING` silently yielding no rows at all: the client took
1082/// the first tag as the end of the statement and discarded the description.
1083pub fn encode_rows(rows: &[Value], project: &[Col]) -> Vec<u8> {
1084    let cols = columns_for(rows, project);
1085    let oids: Vec<i32> = cols.iter().map(|c| oid_for(rows, &c.src)).collect();
1086    let mut out = row_description(&cols, &oids);
1087    for r in rows {
1088        let vals: Vec<Option<String>> = cols.iter().map(|c| cell(r.get(&c.src))).collect();
1089        out.extend_from_slice(&data_row(&vals));
1090    }
1091    out
1092}
1093
1094/// A complete SELECT response: rows plus `CommandComplete("SELECT n")`.
1095pub fn encode_result(rows: &[Value], project: &[Col]) -> Vec<u8> {
1096    let mut out = encode_rows(rows, project);
1097    out.extend_from_slice(&command_complete(&format!("SELECT {}", rows.len())));
1098    out
1099}
1100
1101// ── the extended query protocol: Parse / Bind / Describe / Execute ──────────
1102//
1103// Why this exists at all: psycopg3, asyncpg and the JDBC driver do not speak
1104// the simple query protocol for parameterised statements. Without these six
1105// messages they cannot run a single query — psycopg3 hangs waiting for a
1106// `ParseComplete`, and asyncpg refuses before it ever sends a `Bind`. "psql
1107// works" is not the same as "the drivers your evaluators use work".
1108//
1109// Two facts about real drivers shaped everything below, and both were read off
1110// a wire transcript rather than assumed:
1111//
1112//   1. psycopg3 sends parameters in a MIXED format — a `str` as OID 0 in text
1113//      format, but an `int` as int2/int4/int8 in BINARY, a float as float8
1114//      binary, a bool as a single binary byte. A text-only decoder gets `\x00*`
1115//      where it expected `42`.
1116//
1117//   2. asyncpg declares NO parameter types in `Parse` and then asks
1118//      `Describe(statement)`, encoding its arguments from whatever OIDs come
1119//      back. Answering "text" for all of them does not degrade gracefully — it
1120//      makes asyncpg REFUSE the call client-side ("expected str, got int").
1121//
1122// (2) is the reason `infer_param_oids` exists. NEDB is schemaless, so there is
1123// no catalogue to read a column's type out of — the only honest source of truth
1124// is the data already stored, so the type is sampled from it.
1125
1126/// Parameter/result type OIDs handled on the binary path.
1127const OID_INT2: i32 = 21;
1128const OID_INT4: i32 = 23;
1129const OID_OID: i32 = 26;
1130const OID_FLOAT4: i32 = 700;
1131const OID_VARCHAR: i32 = 1043;
1132const OID_NAME: i32 = 19;
1133const OID_UNKNOWN: i32 = 705;
1134const OID_JSON: i32 = 114;
1135const OID_JSONB: i32 = 3802;
1136
1137/// How many `$n` placeholders a statement carries, and the highest index used.
1138///
1139/// Scans outside string literals so a `'$1'` inside a value is not mistaken for
1140/// a placeholder. Dollar-quoted bodies (`$tag$…$tag$`) are not recognised —
1141/// they need a procedural language NEDB does not have.
1142fn param_count(sql: &str) -> usize {
1143    let b = sql.as_bytes();
1144    let mut i = 0usize;
1145    let mut in_s = false;
1146    let mut max = 0usize;
1147    while i < b.len() {
1148        let c = b[i];
1149        if in_s {
1150            if c == b'\'' {
1151                in_s = false;
1152            }
1153            i += 1;
1154            continue;
1155        }
1156        if c == b'\'' {
1157            in_s = true;
1158            i += 1;
1159            continue;
1160        }
1161        if c == b'$' && i + 1 < b.len() && b[i + 1].is_ascii_digit() {
1162            let mut j = i + 1;
1163            let mut n = 0usize;
1164            while j < b.len() && b[j].is_ascii_digit() {
1165                n = n * 10 + (b[j] - b'0') as usize;
1166                j += 1;
1167            }
1168            max = max.max(n);
1169            i = j;
1170            continue;
1171        }
1172        i += 1;
1173    }
1174    max
1175}
1176
1177/// The JSON-shaped type of `field` as it is actually stored, sampled from the
1178/// collection, mapped onto the nearest Postgres OID.
1179///
1180/// This is the schemaless answer to "what type is this column?". A relational
1181/// server reads its catalogue; NEDB has none, so it reads the data. Sampling a
1182/// bounded number of rows keeps a `Describe` cheap, and the first row that
1183/// actually carries the field decides — a field missing from row one but
1184/// present in row nine still types correctly.
1185fn infer_field_oid(db: Option<&Arc<Db>>, coll: &str, field: &str) -> i32 {
1186    // `_`-prefixed names are engine metadata, not stored document fields, so
1187    // they type from the engine's own contract — no sampling, and no database
1188    // handle needed.
1189    match field {
1190        "_seq" => return OID_INT8,
1191        "_id" | "_hash" | "_prev" | "_collection" | "_valid_from" | "_valid_to" => return OID_TEXT,
1192        _ => {}
1193    }
1194    let db = match db {
1195        Some(db) => db,
1196        None => return OID_TEXT,
1197    };
1198    if coll.is_empty() || field.is_empty() {
1199        return OID_TEXT;
1200    }
1201    let rows = match crate::nql::query(db, &format!("FROM {} LIMIT {}", coll, TYPE_SAMPLE)) {
1202        Ok((rows, _)) => rows,
1203        Err(_) => return OID_TEXT,
1204    };
1205    // Unified over the sample, not taken from the first hit: a field that is a
1206    // number in one document and a string in another has to be advertised as
1207    // text or a client cannot decode every row of it.
1208    oid_for(&rows, field)
1209}
1210
1211/// The type of an aggregate output column, which no document holds.
1212///
1213/// Sampling stored documents cannot type these: `COUNT(*)` produces a column
1214/// called `count` that exists in no document, so the sampler finds nothing and
1215/// falls back to text. A text-format client papers over that, but a binary
1216/// client is then handed the digits of a number under a text header and
1217/// `COUNT(*)` comes back as the string `"2"` instead of the integer `2`.
1218///
1219/// So aggregates are typed from what the aggregate MEANS: a count is always an
1220/// integer, an average is always fractional, and min/max/sum inherit the type
1221/// of the field they were computed over.
1222fn aggregate_oid(src: &str, db: Option<&Arc<Db>>, coll: &str) -> Option<i32> {
1223    if src == "count" {
1224        return Some(OID_INT8);
1225    }
1226    for (prefix, fixed) in [
1227        ("count_", Some(OID_INT8)),
1228        ("avg_", Some(OID_FLOAT8)),
1229        ("sum_", None),
1230        ("min_", None),
1231        ("max_", None),
1232    ] {
1233        if let Some(field) = src.strip_prefix(prefix) {
1234            return Some(match fixed {
1235                Some(oid) => oid,
1236                // SUM/MIN/MAX of an integer field is an integer; of a
1237                // fractional field, fractional.
1238                None => match infer_field_oid(db, coll, field) {
1239                    OID_INT8 => OID_INT8,
1240                    OID_FLOAT8 => OID_FLOAT8,
1241                    // Summing or ordering a non-numeric field is not
1242                    // meaningful; let the row-derived type answer.
1243                    other => other,
1244                },
1245            });
1246        }
1247    }
1248    None
1249}
1250
1251/// How many documents to sample when typing a column.
1252///
1253/// Bounded so a `Describe` stays cheap. It is a sample, so a field that only
1254/// turns heterogeneous outside it can still surprise us — which is exactly why
1255/// `cell_binary` refuses a mismatch loudly instead of coercing.
1256const TYPE_SAMPLE: usize = 200;
1257
1258/// The collection a statement reads from or writes to, for type sampling.
1259fn stmt_collection(sql: &str) -> String {
1260    let s = normalise(sql);
1261    let up = s.to_uppercase();
1262    let after = if let Some(at) = find_kw(&up, "FROM") {
1263        &s[at + 4..]
1264    } else if let Some(rest) = strip_prefix_ci(&s, "UPDATE") {
1265        return rest
1266            .split_whitespace()
1267            .next()
1268            .unwrap_or("")
1269            .rsplit('.')
1270            .next()
1271            .unwrap_or("")
1272            .trim_matches('"')
1273            .to_string();
1274    } else if let Some(rest) = strip_prefix_ci(&s, "INSERT INTO") {
1275        return rest
1276            .split(|c: char| c.is_whitespace() || c == '(')
1277            .find(|t| !t.is_empty())
1278            .unwrap_or("")
1279            .rsplit('.')
1280            .next()
1281            .unwrap_or("")
1282            .trim_matches('"')
1283            .to_string();
1284    } else {
1285        return String::new();
1286    };
1287    after
1288        .trim()
1289        .split(|c: char| c.is_whitespace())
1290        .find(|t| !t.is_empty())
1291        .unwrap_or("")
1292        .rsplit('.')
1293        .next()
1294        .unwrap_or("")
1295        .trim_matches('"')
1296        .to_string()
1297}
1298
1299/// Which document field each `$n` is being compared against.
1300///
1301/// Three shapes cover essentially all driver-generated SQL:
1302///   `WHERE qty > $1`        → the identifier immediately left of the operator
1303///   `SET status = $1`       → same shape, inside the SET list
1304///   `INSERT INTO t (a,b) VALUES ($1,$2)` → positional against the column list
1305///
1306/// Anything it cannot read returns `None`, which types as `text`. Guessing
1307/// wrong here would make a driver encode a value the engine then fails to
1308/// match, so an unknown is left unknown on purpose.
1309fn param_fields(sql: &str, n_params: usize) -> Vec<Option<String>> {
1310    let s = normalise(sql);
1311    let mut out = vec![None; n_params];
1312
1313    // The INSERT column list maps positionally, which is more reliable than
1314    // scanning leftwards through a VALUES tuple.
1315    let up = s.to_uppercase();
1316    if up.starts_with("INSERT") {
1317        if let (Some(open), Some(vals_at)) = (s.find('('), find_kw(&up, "VALUES")) {
1318            if open < vals_at {
1319                if let Some(close) = s[open..vals_at].rfind(')') {
1320                    let cols: Vec<String> = split_top(&s[open + 1..open + close], ',')
1321                        .into_iter()
1322                        .map(|c| c.trim().trim_matches('"').to_string())
1323                        .collect();
1324                    // `$1` is the first placeholder in the first tuple, and so on.
1325                    let tail = &s[vals_at..];
1326                    let mut seen = 0usize;
1327                    let b = tail.as_bytes();
1328                    let mut i = 0usize;
1329                    let mut in_s = false;
1330                    while i < b.len() {
1331                        if in_s {
1332                            if b[i] == b'\'' { in_s = false; }
1333                            i += 1;
1334                            continue;
1335                        }
1336                        if b[i] == b'\'' { in_s = true; i += 1; continue; }
1337                        if b[i] == b'$' && i + 1 < b.len() && b[i + 1].is_ascii_digit() {
1338                            let mut j = i + 1;
1339                            let mut num = 0usize;
1340                            while j < b.len() && b[j].is_ascii_digit() {
1341                                num = num * 10 + (b[j] - b'0') as usize;
1342                                j += 1;
1343                            }
1344                            if num >= 1 && num <= n_params {
1345                                if let Some(c) = cols.get(seen % cols.len().max(1)) {
1346                                    out[num - 1] = Some(c.clone());
1347                                }
1348                            }
1349                            seen += 1;
1350                            i = j;
1351                            continue;
1352                        }
1353                        i += 1;
1354                    }
1355                    return out;
1356                }
1357            }
1358        }
1359    }
1360
1361    // Otherwise: for each `$n`, walk left past the operator to the identifier.
1362    let b = s.as_bytes();
1363    let mut i = 0usize;
1364    let mut in_s = false;
1365    while i < b.len() {
1366        if in_s {
1367            if b[i] == b'\'' { in_s = false; }
1368            i += 1;
1369            continue;
1370        }
1371        if b[i] == b'\'' { in_s = true; i += 1; continue; }
1372        if b[i] == b'$' && i + 1 < b.len() && b[i + 1].is_ascii_digit() {
1373            let mut j = i + 1;
1374            let mut num = 0usize;
1375            while j < b.len() && b[j].is_ascii_digit() {
1376                num = num * 10 + (b[j] - b'0') as usize;
1377                j += 1;
1378            }
1379            if num >= 1 && num <= n_params {
1380                let left = &s[..i];
1381                // Skip the operator characters and whitespace sitting between
1382                // the identifier and the placeholder.
1383                let trimmed = left.trim_end_matches(|c: char| {
1384                    c.is_whitespace() || "=<>!+-*/%(,".contains(c)
1385                });
1386                // A word operator (`LIKE`, `IN`, `BETWEEN`, `AND`) also sits
1387                // between them; step over it to reach the real identifier.
1388                let mut tok = trimmed
1389                    .rsplit(|c: char| c.is_whitespace() || c == '(' || c == ',')
1390                    .find(|t| !t.is_empty())
1391                    .unwrap_or("")
1392                    .trim_matches('"');
1393                let mut before = trimmed;
1394                for _ in 0..4 {
1395                    let upper_tok = tok.to_uppercase();
1396                    // `BETWEEN $1 AND $2` puts BOTH a word operator and an
1397                    // earlier placeholder between `$2` and the column it
1398                    // constrains, so a placeholder has to be stepped over too —
1399                    // otherwise the upper bound of every range query types as
1400                    // text while the lower bound types correctly.
1401                    if upper_tok.starts_with('$')
1402                        || matches!(upper_tok.as_str(),
1403                        "LIKE" | "ILIKE" | "IN" | "BETWEEN" | "AND" | "OR" | "NOT" | "IS") {
1404                        before = before[..before.len() - tok.len()].trim_end_matches(|c: char| {
1405                            c.is_whitespace() || "=<>!(,".contains(c)
1406                        });
1407                        tok = before
1408                            .rsplit(|c: char| c.is_whitespace() || c == '(' || c == ',')
1409                            .find(|t| !t.is_empty())
1410                            .unwrap_or("")
1411                            .trim_matches('"');
1412                    } else {
1413                        break;
1414                    }
1415                }
1416                if !tok.is_empty()
1417                    && tok.chars().all(|c| c.is_alphanumeric() || c == '_' || c == '.')
1418                    && !tok.chars().next().map(|c| c.is_ascii_digit()).unwrap_or(true)
1419                {
1420                    out[num - 1] = Some(tok.rsplit('.').next().unwrap_or(tok).to_string());
1421                }
1422            }
1423            i = j;
1424            continue;
1425        }
1426        i += 1;
1427    }
1428    out
1429}
1430
1431/// The type of a placeholder sitting in a CLAUSE position rather than beside a
1432/// column.
1433///
1434/// `AS OF SYSTEM TIME $1` has no column to sample — the token to its left is
1435/// the word `TIME`. Its type comes from the grammar instead, which is both
1436/// cheaper and more certain than any inference: a system-time bound is a
1437/// sequence number, a valid-time bound is a date string, and a page bound is an
1438/// integer. Without this, a parameterised time-travel query typed as text and
1439/// asyncpg refused to send the integer at all.
1440fn clause_param_oids(sql: &str, n_params: usize) -> Vec<Option<i32>> {
1441    let s = normalise(sql);
1442    let mut out = vec![None; n_params];
1443    let b = s.as_bytes();
1444    let mut i = 0usize;
1445    let mut in_s = false;
1446    while i < b.len() {
1447        if in_s {
1448            if b[i] == b'\'' { in_s = false; }
1449            i += 1;
1450            continue;
1451        }
1452        if b[i] == b'\'' { in_s = true; i += 1; continue; }
1453        if b[i] == b'$' && i + 1 < b.len() && b[i + 1].is_ascii_digit() {
1454            let mut j = i + 1;
1455            let mut num = 0usize;
1456            while j < b.len() && b[j].is_ascii_digit() {
1457                num = num * 10 + (b[j] - b'0') as usize;
1458                j += 1;
1459            }
1460            if num >= 1 && num <= n_params {
1461                let left = s[..i].trim_end().to_uppercase();
1462                // VALID AS OF is checked FIRST: it ends with "AS OF" too, and
1463                // its argument is a DATE STRING, not a sequence number.
1464                out[num - 1] = if left.ends_with("VALID AS OF") {
1465                    Some(OID_TEXT)
1466                } else if left.ends_with("AS OF SYSTEM TIME")
1467                    || left.ends_with("FOR SYSTEM_TIME AS OF")
1468                    || left.ends_with("AS OF")
1469                    || left.ends_with("LIMIT")
1470                    || left.ends_with("OFFSET")
1471                {
1472                    Some(OID_INT8)
1473                } else {
1474                    None
1475                };
1476            }
1477            i = j;
1478            continue;
1479        }
1480        i += 1;
1481    }
1482    out
1483}
1484
1485/// The OIDs to advertise for `$1..$n`, sampled from stored data.
1486///
1487/// `declared` is what the client itself put in `Parse`. A client that states a
1488/// type is believed — it is about to encode its arguments that way, and second
1489///-guessing it would break the decode. Only the unspecified slots are inferred.
1490fn infer_param_oids(sql: &str, declared: &[i32], db: Option<&Arc<Db>>) -> Vec<i32> {
1491    let n = param_count(sql).max(declared.len());
1492    if n == 0 {
1493        return vec![];
1494    }
1495    let coll = stmt_collection(sql);
1496    let fields = param_fields(sql, n);
1497    let clauses = clause_param_oids(sql, n);
1498    (0..n)
1499        .map(|i| match declared.get(i) {
1500            Some(&oid) if oid != 0 => oid,
1501            // A clause position knows its own type from the grammar, so it
1502            // outranks sampling a column that is not even there.
1503            _ => match clauses[i] {
1504                Some(oid) => oid,
1505                None => match &fields[i] {
1506                    Some(f) => infer_field_oid(db, &coll, f),
1507                    None => OID_TEXT,
1508                },
1509            },
1510        })
1511        .collect()
1512}
1513
1514/// Decode one bound parameter into the SQL literal text to splice into the
1515/// statement.
1516///
1517/// `None` means SQL NULL. Format 1 is binary — see the module note on psycopg3
1518/// sending small integers as int2.
1519fn decode_param(raw: Option<&[u8]>, oid: i32, format: i16) -> Result<Option<String>, String> {
1520    let bytes = match raw {
1521        None => return Ok(None),
1522        Some(b) => b,
1523    };
1524    let quote = |s: &str| format!("'{}'", s.replace('\'', "''"));
1525
1526    if format == 0 {
1527        let s = String::from_utf8_lossy(bytes).to_string();
1528        return Ok(Some(match oid {
1529            OID_BOOL => {
1530                let t = matches!(s.as_str(), "t" | "true" | "TRUE" | "1" | "yes" | "on");
1531                if t { "TRUE".into() } else { "FALSE".into() }
1532            }
1533            OID_INT2 | OID_INT4 | OID_INT8 | OID_OID | OID_FLOAT4 | OID_FLOAT8 => {
1534                // Validate rather than trust: an unparseable "number" spliced
1535                // in bare would become a bare identifier in the NQL text and
1536                // produce a baffling error far from its cause.
1537                if s.parse::<f64>().is_ok() { s } else { quote(&s) }
1538            }
1539            // OID 0 with text format is psycopg3's `str`. Confirmed on the
1540            // wire: it declares a real numeric OID whenever the value is a
1541            // number, so an unspecified text parameter is genuinely a string
1542            // and quoting it is right rather than a guess.
1543            _ => quote(&s),
1544        }));
1545    }
1546    if format != 1 {
1547        return Err(format!("unsupported parameter format code {}", format));
1548    }
1549
1550    // ── binary ──────────────────────────────────────────────────────────────
1551    let need = |n: usize| -> Result<(), String> {
1552        if bytes.len() == n {
1553            Ok(())
1554        } else {
1555            Err(format!(
1556                "binary parameter of type OID {} should be {} bytes, got {}",
1557                oid, n, bytes.len()
1558            ))
1559        }
1560    };
1561    Ok(Some(match oid {
1562        OID_BOOL => {
1563            need(1)?;
1564            if bytes[0] != 0 { "TRUE".into() } else { "FALSE".into() }
1565        }
1566        OID_INT2 => {
1567            need(2)?;
1568            i16::from_be_bytes([bytes[0], bytes[1]]).to_string()
1569        }
1570        OID_INT4 => {
1571            need(4)?;
1572            i32::from_be_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]).to_string()
1573        }
1574        OID_OID => {
1575            need(4)?;
1576            u32::from_be_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]).to_string()
1577        }
1578        OID_INT8 => {
1579            need(8)?;
1580            i64::from_be_bytes(bytes[..8].try_into().unwrap()).to_string()
1581        }
1582        OID_FLOAT4 => {
1583            need(4)?;
1584            let f = f32::from_be_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]);
1585            fmt_float(f as f64)
1586        }
1587        OID_FLOAT8 => {
1588            need(8)?;
1589            fmt_float(f64::from_be_bytes(bytes[..8].try_into().unwrap()))
1590        }
1591        OID_TEXT | OID_VARCHAR | OID_NAME | OID_UNKNOWN | OID_JSON | 0 => {
1592            quote(&String::from_utf8_lossy(bytes))
1593        }
1594        OID_JSONB => {
1595            // jsonb binary is a 1-byte version header followed by the JSON text.
1596            let body = if bytes.first() == Some(&1) { &bytes[1..] } else { bytes };
1597            quote(&String::from_utf8_lossy(body))
1598        }
1599        other => {
1600            return Err(format!(
1601                "parameter type OID {} is not supported in binary format — \
1602                 the supported set is bool, int2/int4/int8, float4/float8, \
1603                 text/varchar/json/jsonb. Send it as text, or cast it in the \
1604                 statement",
1605                other
1606            ))
1607        }
1608    }))
1609}
1610
1611/// Render a float without Rust's `inf`/`NaN` spellings leaking into SQL text.
1612fn fmt_float(f: f64) -> String {
1613    if f.is_nan() {
1614        "'NaN'".into()
1615    } else if f.is_infinite() {
1616        if f > 0.0 { "'Infinity'".into() } else { "'-Infinity'".into() }
1617    } else if f.fract() == 0.0 && f.abs() < 1e15 {
1618        format!("{:.0}", f)
1619    } else {
1620        f.to_string()
1621    }
1622}
1623
1624/// Splice decoded parameters into the statement text.
1625///
1626/// Textual substitution, deliberately: the whole SQL surface is already a text
1627/// translation into NQL, so one representation is simpler and cannot disagree
1628/// with itself. Every value arrives already rendered as a SQL literal by
1629/// `decode_param`, with embedded quotes doubled, so a parameter cannot break
1630/// out of its literal and alter the statement's shape.
1631fn substitute_params(sql: &str, params: &[Option<String>]) -> Result<String, String> {
1632    let b = sql.as_bytes();
1633    let mut out = String::with_capacity(sql.len() + 16);
1634    let mut i = 0usize;
1635    let mut in_s = false;
1636    while i < b.len() {
1637        let c = b[i];
1638        if in_s {
1639            out.push(c as char);
1640            if c == b'\'' { in_s = false; }
1641            i += 1;
1642            continue;
1643        }
1644        if c == b'\'' {
1645            in_s = true;
1646            out.push('\'');
1647            i += 1;
1648            continue;
1649        }
1650        if c == b'$' && i + 1 < b.len() && b[i + 1].is_ascii_digit() {
1651            let mut j = i + 1;
1652            let mut n = 0usize;
1653            while j < b.len() && b[j].is_ascii_digit() {
1654                n = n * 10 + (b[j] - b'0') as usize;
1655                j += 1;
1656            }
1657            match params.get(n.wrapping_sub(1)) {
1658                Some(Some(lit)) => out.push_str(lit),
1659                Some(None) => out.push_str("NULL"),
1660                None => {
1661                    return Err(format!(
1662                        "bind message supplies {} parameter(s) but the statement uses ${}",
1663                        params.len(), n
1664                    ))
1665                }
1666            }
1667            i = j;
1668            continue;
1669        }
1670        out.push(c as char);
1671        i += 1;
1672    }
1673    Ok(out)
1674}
1675
1676/// A parsed statement, held for the life of the connection (or until `Close`).
1677struct Prepared {
1678    sql: String,
1679    /// OIDs advertised for `$1..$n` — what `ParameterDescription` reports and
1680    /// what `Bind` values are decoded as.
1681    param_oids: Vec<i32>,
1682    /// The advertised output shape, computed on demand and then reused.
1683    ///
1684    /// Lazy because working it out samples stored documents, and a text-format
1685    /// client that never sends `Describe(statement)` should not pay for a scan
1686    /// on every `Parse` — psycopg3 parses once per query.
1687    ///
1688    /// `Some(None)` means "computed, and this statement returns no rows".
1689    out_shape: Option<Option<(Vec<Col>, Vec<i32>)>>,
1690}
1691
1692/// The output columns and types a statement advertises, computed once.
1693fn prepared_shape<'a>(
1694    p: &'a mut Prepared,
1695    db: Option<&Arc<Db>>,
1696) -> &'a Option<(Vec<Col>, Vec<i32>)> {
1697    if p.out_shape.is_none() {
1698        p.out_shape = Some(describe_shape(&p.sql, db, p.param_oids.len()));
1699    }
1700    p.out_shape.as_ref().expect("just filled")
1701}
1702
1703/// A bound statement: fully substituted SQL plus, once run, its result.
1704struct Portal {
1705    sql: String,
1706    /// Filled by the first `Describe` or `Execute` and reused afterwards.
1707    ///
1708    /// Executing once and streaming from the buffer is what makes a suspended
1709    /// portal safe: a second `Execute` on a partially-drained `INSERT` must
1710    /// continue the row stream, not perform the insert again.
1711    result: Option<PortalResult>,
1712    /// The output shape, frozen at the first `Describe`/`Execute`.
1713    ///
1714    /// A schemaless store derives `SELECT *`'s columns from the rows it found,
1715    /// which would let a `Describe` and a later `Execute` disagree about the
1716    /// column count — and a driver that was told three fields and handed two
1717    /// mis-decodes the row rather than failing loudly. Freezing the shape and
1718    /// projecting every row onto it makes the result set rectangular, as SQL
1719    /// promises. The simple protocol keeps the dynamic behaviour, where there
1720    /// is no `Describe` to contradict.
1721    frozen: Option<Vec<Col>>,
1722    /// Result-column format codes requested by `Bind`. Empty = all text.
1723    formats: Vec<i16>,
1724    /// The shape this portal's statement advertised, carried over from the
1725    /// prepared statement when any column is to be sent in BINARY.
1726    ///
1727    /// It has to be the ADVERTISED shape rather than one derived from the rows
1728    /// in hand: asyncpg built its decoders from `Describe`, so re-deriving a
1729    /// different type here would hand it bytes it cannot read.
1730    declared: Option<(Vec<Col>, Vec<i32>)>,
1731}
1732
1733impl Portal {
1734    /// The format code for column `i`, following the protocol's shorthands:
1735    /// no codes means all-text, one code applies to every column.
1736    fn format_of(&self, i: usize) -> i16 {
1737        match self.formats.len() {
1738            0 => 0,
1739            1 => self.formats[0],
1740            _ => self.formats.get(i).copied().unwrap_or(0),
1741        }
1742    }
1743    /// The columns and types to advertise and encode with.
1744    fn shape(&self, r: &PortalResult) -> (Vec<Col>, Vec<i32>) {
1745        match &self.declared {
1746            Some((cols, oids)) if self.formats.iter().any(|f| *f == 1) => {
1747                (cols.clone(), oids.clone())
1748            }
1749            _ => {
1750                let cols = columns_for(&r.rows, &r.project);
1751                let oids = cols.iter().map(|c| oid_for(&r.rows, &c.src)).collect();
1752                (cols, oids)
1753            }
1754        }
1755    }
1756}
1757
1758struct PortalResult {
1759    rows: Vec<Value>,
1760    project: Vec<Col>,
1761    has_rows: bool,
1762    tag: String,
1763    tag_counts_rows: bool,
1764    /// How many rows have gone out across all `Execute`s on this portal.
1765    sent: usize,
1766}
1767
1768fn parse_complete() -> Vec<u8> { Out::msg(b'1').finish() }
1769fn bind_complete() -> Vec<u8> { Out::msg(b'2').finish() }
1770fn close_complete() -> Vec<u8> { Out::msg(b'3').finish() }
1771fn no_data() -> Vec<u8> { Out::msg(b'n').finish() }
1772fn portal_suspended() -> Vec<u8> { Out::msg(b's').finish() }
1773
1774fn parameter_description(oids: &[i32]) -> Vec<u8> {
1775    let mut m = Out::msg(b't');
1776    m.i16(oids.len() as i16);
1777    for o in oids {
1778        m.i32(*o);
1779    }
1780    m.finish()
1781}
1782
1783/// Split a NUL-terminated string off the front of a message body.
1784fn take_cstr(body: &[u8], at: &mut usize) -> String {
1785    let start = *at;
1786    while *at < body.len() && body[*at] != 0 {
1787        *at += 1;
1788    }
1789    let s = String::from_utf8_lossy(&body[start..*at]).to_string();
1790    if *at < body.len() {
1791        *at += 1; // step over the NUL
1792    }
1793    s
1794}
1795
1796fn take_i16(body: &[u8], at: &mut usize) -> Result<i16, String> {
1797    if *at + 2 > body.len() {
1798        return Err("truncated message".into());
1799    }
1800    let v = i16::from_be_bytes([body[*at], body[*at + 1]]);
1801    *at += 2;
1802    Ok(v)
1803}
1804
1805fn take_i32(body: &[u8], at: &mut usize) -> Result<i32, String> {
1806    if *at + 4 > body.len() {
1807        return Err("truncated message".into());
1808    }
1809    let v = i32::from_be_bytes([body[*at], body[*at + 1], body[*at + 2], body[*at + 3]]);
1810    *at += 4;
1811    Ok(v)
1812}
1813
1814/// The field names a collection actually holds, sampled from stored documents.
1815///
1816/// The answer to `SELECT *` on a store with no schema. Sorted, because
1817/// `serde_json`'s map is ordered and both this and the row encoder must agree
1818/// on column order or the values land under the wrong headings.
1819fn sample_columns(db: Option<&Arc<Db>>, coll: &str) -> Vec<Col> {
1820    let db = match db {
1821        Some(db) => db,
1822        None => return vec![],
1823    };
1824    let rows = match crate::nql::query(db, &format!("FROM {} LIMIT 25", coll)) {
1825        Ok((rows, _)) => rows,
1826        Err(_) => return vec![],
1827    };
1828    let mut names: Vec<String> = vec![];
1829    for r in &rows {
1830        if let Value::Object(m) = r {
1831            for k in m.keys() {
1832                if !names.iter().any(|n| n == k) {
1833                    names.push(k.clone());
1834                }
1835            }
1836        }
1837    }
1838    names.sort();
1839    names.iter().map(|n| Col::same(n)).collect()
1840}
1841
1842/// The result shape of a statement, worked out WITHOUT running it.
1843///
1844/// Needed for `Describe(statement)`, which arrives before any `Bind` — asyncpg
1845/// builds its row decoders from the answer. Only the select list is read off
1846/// the result; nothing touches storage except the type sampling.
1847///
1848/// Returns `None` when the statement returns no rows at all (`NoData`).
1849fn describe_shape(
1850    sql: &str,
1851    db: Option<&Arc<Db>>,
1852    n_params: usize,
1853) -> Option<(Vec<Col>, Vec<i32>)> {
1854    let probe = probe_sql(sql, n_params);
1855    let stmt = translate(&probe).ok()?;
1856    let coll = stmt_collection(sql);
1857
1858    let cols = match stmt {
1859        Stmt::Ok(_) => return None,
1860        Stmt::Canned { cols, .. } => cols.iter().map(|c| Col::same(c)).collect(),
1861        Stmt::Query { project, .. } => {
1862            if project.is_empty() { sample_columns(db, &coll) } else { project }
1863        }
1864        Stmt::Insert { returning, .. } | Stmt::Update { returning, .. } | Stmt::Delete { returning, .. } => {
1865            if !wants_returning(sql) {
1866                return None;
1867            }
1868            if returning.is_empty() { sample_columns(db, &coll) } else { returning }
1869        }
1870    };
1871    if cols.is_empty() {
1872        // Nothing could be determined. `NoData` is a lie for a SELECT, but a
1873        // RowDescription with zero columns is a worse one — it tells the client
1874        // the query definitively has no output.
1875        return None;
1876    }
1877    let oids = cols
1878        .iter()
1879        .map(|c| {
1880            aggregate_oid(&c.src, db, &coll)
1881                .unwrap_or_else(|| infer_field_oid(db, &coll, &c.src))
1882        })
1883        .collect();
1884    Some((cols, oids))
1885}
1886
1887/// A parse-only stand-in for a parameterised statement.
1888///
1889/// Substituting `NULL` was the obvious choice and the wrong one: a clause that
1890/// validates its argument rejects it, so `AS OF SYSTEM TIME $1` failed at
1891/// `Parse` — before the client ever bound a real sequence number. `0` parses
1892/// everywhere a literal can appear, and since only the SELECT list is read back
1893/// out, the stub's value never reaches an answer.
1894fn probe_sql(sql: &str, n_params: usize) -> String {
1895    let stub: Vec<Option<String>> = vec![Some("0".to_string()); n_params];
1896    substitute_params(sql, &stub).unwrap_or_else(|_| sql.to_string())
1897}
1898
1899/// Run a portal's statement if it has not run yet, then report its shape.
1900fn ensure_executed(
1901    portal: &mut Portal,
1902    db_name: &str,
1903    db: Option<&Arc<Db>>,
1904    read_only: bool,
1905) -> Result<(), Vec<u8>> {
1906    if portal.result.is_some() {
1907        return Ok(());
1908    }
1909    let ex = execute_stmt(&portal.sql, db_name, db, read_only)?;
1910    // Freeze the output shape on first sight so `Describe` and every later
1911    // `Execute` describe the same rectangle.
1912    let project = if let Some(f) = &portal.frozen {
1913        f.clone()
1914    } else {
1915        let p = if ex.project.is_empty() {
1916            columns_for(&ex.rows, &[])
1917        } else {
1918            ex.project.clone()
1919        };
1920        portal.frozen = Some(p.clone());
1921        p
1922    };
1923    portal.result = Some(PortalResult {
1924        rows: ex.rows,
1925        project,
1926        has_rows: ex.has_rows,
1927        tag: ex.tag,
1928        tag_counts_rows: ex.tag_counts_rows,
1929        sent: 0,
1930    });
1931    Ok(())
1932}
1933
1934// ── connection handling ─────────────────────────────────────────────────────
1935
1936async fn read_exact(sock: &mut TcpStream, n: usize) -> std::io::Result<Vec<u8>> {
1937    let mut buf = vec![0u8; n];
1938    sock.read_exact(&mut buf).await?;
1939    Ok(buf)
1940}
1941
1942async fn read_i32(sock: &mut TcpStream) -> std::io::Result<i32> {
1943    let b = read_exact(sock, 4).await?;
1944    Ok(i32::from_be_bytes([b[0], b[1], b[2], b[3]]))
1945}
1946
1947fn parse_startup_params(body: &[u8]) -> HashMap<String, String> {
1948    let mut out = HashMap::new();
1949    let mut parts = body.split(|b| *b == 0).map(|s| String::from_utf8_lossy(s).to_string());
1950    while let (Some(k), Some(v)) = (parts.next(), parts.next()) {
1951        if k.is_empty() {
1952            break;
1953        }
1954        out.insert(k, v);
1955    }
1956    out
1957}
1958
1959/// Serve one client connection to completion.
1960async fn handle(mut sock: TcpStream, resolver: Arc<dyn DbResolver>, read_only: bool) -> std::io::Result<()> {
1961    // ── startup, including the SSL negotiation clients try first ────────────
1962    let params = loop {
1963        let len = read_i32(&mut sock).await?;
1964        if len < 8 || len > 1 << 20 {
1965            return Ok(()); // nonsense framing — drop the connection
1966        }
1967        let code = read_i32(&mut sock).await?;
1968        let body = read_exact(&mut sock, (len - 8) as usize).await?;
1969        match code {
1970            SSL_REQUEST | GSS_REQUEST => {
1971                // Decline and let the client retry in the clear.
1972                sock.write_all(b"N").await?;
1973                continue;
1974            }
1975            CANCEL_REQUEST => return Ok(()), // nothing cancellable: reads are synchronous
1976            PROTO_V3 => break parse_startup_params(&body),
1977            other => {
1978                let major = other >> 16;
1979                sock.write_all(&err_msg(
1980                    "0A000",
1981                    &format!("unsupported frontend protocol {}.{} — this endpoint speaks 3.0",
1982                             major, other & 0xffff),
1983                )).await?;
1984                return Ok(());
1985            }
1986        }
1987    };
1988
1989    let db_name = params.get("database").cloned().unwrap_or_default();
1990
1991    // Resolve the database ONCE, here, on a blocking thread.
1992    //
1993    // A Postgres connection is bound to one database for its whole life, so
1994    // per-connection resolution is both correct and simpler than resolving per
1995    // statement — and it keeps the lock acquisition off the async worker.
1996    let resolved: Option<Arc<Db>> = {
1997        let r = Arc::clone(&resolver);
1998        let name = db_name.clone();
1999        tokio::task::spawn_blocking(move || r.resolve(&name))
2000            .await
2001            .unwrap_or(None)
2002    };
2003
2004    // ── auth: mirror the HTTP surface ───────────────────────────────────────
2005    if let Some(expected) = resolver.token() {
2006        // AuthenticationCleartextPassword (3)
2007        let mut m = Out::msg(b'R');
2008        m.i32(3);
2009        sock.write_all(&m.finish()).await?;
2010
2011        let tag = read_exact(&mut sock, 1).await?;
2012        if tag[0] != b'p' {
2013            sock.write_all(&err_msg("28000", "expected a password message")).await?;
2014            return Ok(());
2015        }
2016        let len = read_i32(&mut sock).await?;
2017        if len < 4 || len > 1 << 16 {
2018            return Ok(());
2019        }
2020        let body = read_exact(&mut sock, (len - 4) as usize).await?;
2021        let supplied = String::from_utf8_lossy(&body).trim_end_matches('\0').to_string();
2022        // Constant-time-ish: compare lengths and bytes without early return.
2023        let ok = supplied.len() == expected.len()
2024            && supplied.bytes().zip(expected.bytes()).fold(0u8, |a, (x, y)| a | (x ^ y)) == 0;
2025        if !ok {
2026            sock.write_all(&err_msg("28P01", "password authentication failed")).await?;
2027            return Ok(());
2028        }
2029    }
2030
2031    let mut m = Out::msg(b'R');
2032    m.i32(0); // AuthenticationOk
2033    sock.write_all(&m.finish()).await?;
2034
2035    for (k, v) in [
2036        ("server_version", SERVER_VERSION),
2037        ("server_encoding", "UTF8"),
2038        ("client_encoding", "UTF8"),
2039        ("DateStyle", "ISO, MDY"),
2040        ("integer_datetimes", "on"),
2041        ("standard_conforming_strings", "on"),
2042        ("application_name", "nedbd"),
2043    ] {
2044        let mut p = Out::msg(b'S');
2045        p.cstr(k);
2046        p.cstr(v);
2047        sock.write_all(&p.finish()).await?;
2048    }
2049    let mut k = Out::msg(b'K');
2050    k.i32(std::process::id() as i32);
2051    k.i32(0);
2052    sock.write_all(&k.finish()).await?;
2053    sock.write_all(&ready()).await?;
2054
2055    // ── message loop ────────────────────────────────────────────────────────
2056    //
2057    // Prepared statements and portals live for the connection. `""` is the
2058    // unnamed statement/portal, which every driver reuses constantly — it is an
2059    // ordinary entry in the map rather than a special case.
2060    let mut prepared: HashMap<String, Prepared> = HashMap::new();
2061    let mut portals: HashMap<String, Portal> = HashMap::new();
2062    // After an error inside an extended-protocol sequence, everything up to the
2063    // next `Sync` is discarded. Skipping this is how a server ends up answering
2064    // a Bind the client has already abandoned, and the stream desynchronises.
2065    let mut failed = false;
2066
2067    loop {
2068        let mut tag = [0u8; 1];
2069        if sock.read_exact(&mut tag).await.is_err() {
2070            return Ok(()); // client hung up
2071        }
2072        let len = read_i32(&mut sock).await?;
2073        if len < 4 || len > 64 << 20 {
2074            return Ok(());
2075        }
2076        let body = read_exact(&mut sock, (len - 4) as usize).await?;
2077
2078        // `Sync` always clears the error state; `Terminate` always applies.
2079        if failed && tag[0] != b'S' && tag[0] != b'X' {
2080            continue;
2081        }
2082
2083        match tag[0] {
2084            b'X' => return Ok(()), // Terminate
2085
2086            b'Q' => {
2087                let sql = String::from_utf8_lossy(&body).trim_end_matches('\0').to_string();
2088                let out = run_simple_query(&sql, &db_name, resolved.as_ref(), read_only);
2089                sock.write_all(&out).await?;
2090                sock.write_all(&ready()).await?;
2091                // A simple query closes the unnamed portal, per the protocol.
2092                portals.remove("");
2093            }
2094
2095            // ── Parse: name, SQL, declared parameter type OIDs ─────────────
2096            b'P' => {
2097                let mut at = 0usize;
2098                let name = take_cstr(&body, &mut at);
2099                let sql = take_cstr(&body, &mut at);
2100                let n = take_i16(&body, &mut at).unwrap_or(0).max(0) as usize;
2101                let mut declared = Vec::with_capacity(n);
2102                let mut bad = false;
2103                for _ in 0..n {
2104                    match take_i32(&body, &mut at) {
2105                        Ok(o) => declared.push(o),
2106                        Err(_) => { bad = true; break; }
2107                    }
2108                }
2109                if bad {
2110                    sock.write_all(&err_msg("08P01", "malformed Parse message")).await?;
2111                    failed = true;
2112                    continue;
2113                }
2114                // Reject unsupported SQL here rather than at Execute, so the
2115                // client learns at the point it asked — which is also where
2116                // Postgres reports it.
2117                if let Err(why) = translate(&probe_sql(&sql, param_count(&sql))) {
2118                    sock.write_all(&err_msg("0A000", &why)).await?;
2119                    failed = true;
2120                    continue;
2121                }
2122                let param_oids = infer_param_oids(&sql, &declared, resolved.as_ref());
2123                prepared.insert(name, Prepared { sql, param_oids, out_shape: None });
2124                sock.write_all(&parse_complete()).await?;
2125            }
2126
2127            // ── Bind: portal, statement, formats, values, result formats ───
2128            b'B' => {
2129                let mut at = 0usize;
2130                let portal_name = take_cstr(&body, &mut at);
2131                let stmt_name = take_cstr(&body, &mut at);
2132                if !prepared.contains_key(&stmt_name) {
2133                    sock.write_all(&err_msg("26000", &format!(
2134                        "prepared statement {:?} does not exist", stmt_name))).await?;
2135                    failed = true;
2136                    continue;
2137                }
2138                let p = &prepared[&stmt_name];
2139                let mut want_formats: Vec<i16> = vec![];
2140                let res: Result<String, String> = (|| {
2141                    let nfmt = take_i16(&body, &mut at)? .max(0) as usize;
2142                    let mut fmts = Vec::with_capacity(nfmt);
2143                    for _ in 0..nfmt {
2144                        fmts.push(take_i16(&body, &mut at)?);
2145                    }
2146                    let nparam = take_i16(&body, &mut at)?.max(0) as usize;
2147                    let mut vals: Vec<Option<String>> = Vec::with_capacity(nparam);
2148                    for i in 0..nparam {
2149                        let l = take_i32(&body, &mut at)?;
2150                        let raw: Option<Vec<u8>> = if l < 0 {
2151                            None
2152                        } else {
2153                            let l = l as usize;
2154                            if at + l > body.len() {
2155                                return Err("truncated Bind parameter".into());
2156                            }
2157                            let v = body[at..at + l].to_vec();
2158                            at += l;
2159                            Some(v)
2160                        };
2161                        // Zero format codes means "all text"; one means "this
2162                        // format for every parameter"; otherwise one per value.
2163                        let f = match fmts.len() {
2164                            0 => 0,
2165                            1 => fmts[0],
2166                            _ => *fmts.get(i).unwrap_or(&0),
2167                        };
2168                        let oid = *p.param_oids.get(i).unwrap_or(&OID_TEXT);
2169                        vals.push(decode_param(raw.as_deref(), oid, f)?);
2170                    }
2171                    // Result format codes. asyncpg asks for binary on every
2172                    // column, so honouring these is not an optimisation — it
2173                    // is the difference between asyncpg reading rows and
2174                    // refusing the result outright.
2175                    let nres = take_i16(&body, &mut at)?.max(0) as usize;
2176                    for _ in 0..nres {
2177                        let f = take_i16(&body, &mut at)?;
2178                        if f != 0 && f != 1 {
2179                            return Err(format!("unknown result format code {}", f));
2180                        }
2181                        want_formats.push(f);
2182                    }
2183                    substitute_params(&p.sql, &vals)
2184                })();
2185                match res {
2186                    Ok(sql) => {
2187                        // Binary encoding must use the types the client was
2188                        // TOLD about, so pull the advertised shape across.
2189                        let declared = if want_formats.iter().any(|f| *f == 1) {
2190                            let p = prepared.get_mut(&stmt_name).expect("checked above");
2191                            prepared_shape(p, resolved.as_ref()).clone()
2192                        } else {
2193                            None
2194                        };
2195                        portals.insert(portal_name, Portal {
2196                            sql, result: None, frozen: None,
2197                            formats: want_formats, declared,
2198                        });
2199                        sock.write_all(&bind_complete()).await?;
2200                    }
2201                    Err(why) => {
2202                        sock.write_all(&err_msg("08P01", &why)).await?;
2203                        failed = true;
2204                    }
2205                }
2206            }
2207
2208            // ── Describe: 'S' statement, or 'P' portal ─────────────────────
2209            b'D' => {
2210                let kind = body.first().copied().unwrap_or(b'S');
2211                let mut at = 1usize;
2212                let name = take_cstr(&body, &mut at);
2213                if kind == b'S' {
2214                    if !prepared.contains_key(&name) {
2215                        sock.write_all(&err_msg("26000", &format!(
2216                            "prepared statement {:?} does not exist", name))).await?;
2217                        failed = true;
2218                        continue;
2219                    }
2220                    let p = prepared.get_mut(&name).expect("checked above");
2221                    let oids = p.param_oids.clone();
2222                    // asyncpg encodes its arguments from this, so the count has
2223                    // to be right or it refuses the call before sending a Bind.
2224                    sock.write_all(&parameter_description(&oids)).await?;
2225                    // Describe(statement) happens before Bind, so the requested
2226                    // result format is not known yet; Postgres reports text
2227                    // here too and the client's own Bind decides the encoding.
2228                    let out = match prepared_shape(p, resolved.as_ref()) {
2229                        Some((cols, col_oids)) => row_description(cols, col_oids),
2230                        None => no_data(),
2231                    };
2232                    sock.write_all(&out).await?;
2233                } else {
2234                    let portal = match portals.get_mut(&name) {
2235                        Some(p) => p,
2236                        None => {
2237                            sock.write_all(&err_msg("34000", &format!(
2238                                "portal {:?} does not exist", name))).await?;
2239                            failed = true;
2240                            continue;
2241                        }
2242                    };
2243                    // A bound portal can be run: doing it here means the
2244                    // RowDescription reports the columns and types actually
2245                    // present, which is strictly better than a guess. psycopg3
2246                    // takes this path on every query.
2247                    match ensure_executed(portal, &db_name, resolved.as_ref(), read_only) {
2248                        Err(encoded) => {
2249                            sock.write_all(&encoded).await?;
2250                            failed = true;
2251                        }
2252                        Ok(()) => {
2253                            let r = portal.result.as_ref().expect("just executed");
2254                            if !r.has_rows {
2255                                sock.write_all(&no_data()).await?;
2256                            } else {
2257                                let (cols, oids) = portal.shape(r);
2258                                let fmts: Vec<i16> =
2259                                    (0..cols.len()).map(|i| portal.format_of(i)).collect();
2260                                sock.write_all(&row_description_fmt(&cols, &oids, &fmts)).await?;
2261                            }
2262                        }
2263                    }
2264                }
2265            }
2266
2267            // ── Execute: portal, maximum rows (0 = all) ────────────────────
2268            b'E' => {
2269                let mut at = 0usize;
2270                let name = take_cstr(&body, &mut at);
2271                let max_rows = take_i32(&body, &mut at).unwrap_or(0);
2272                let portal = match portals.get_mut(&name) {
2273                    Some(p) => p,
2274                    None => {
2275                        sock.write_all(&err_msg("34000", &format!(
2276                            "portal {:?} does not exist", name))).await?;
2277                        failed = true;
2278                        continue;
2279                    }
2280                };
2281                if let Err(encoded) = ensure_executed(portal, &db_name, resolved.as_ref(), read_only) {
2282                    sock.write_all(&encoded).await?;
2283                    failed = true;
2284                    continue;
2285                }
2286                let r = portal.result.as_ref().expect("just executed");
2287                if !r.has_rows {
2288                    let tag = r.tag.clone();
2289                    sock.write_all(&command_complete(&tag)).await?;
2290                    continue;
2291                }
2292                let (cols, oids) = portal.shape(r);
2293                let limit = if max_rows > 0 {
2294                    (r.sent + max_rows as usize).min(r.rows.len())
2295                } else {
2296                    r.rows.len()
2297                };
2298                // Encode the whole batch BEFORE writing any of it. A value that
2299                // cannot be sent in the advertised binary type has to become an
2300                // error instead of a truncated row stream — half a result set
2301                // followed by an error is far harder to diagnose than an error.
2302                let mut encoded: Vec<Vec<u8>> = Vec::with_capacity(limit - r.sent);
2303                let mut fail: Option<String> = None;
2304                for row in &r.rows[r.sent..limit] {
2305                    let mut vals: Vec<Option<Vec<u8>>> = Vec::with_capacity(cols.len());
2306                    for (i, c) in cols.iter().enumerate() {
2307                        let v = row.get(&c.src);
2308                        let got = if portal.format_of(i) == 1 {
2309                            cell_binary(v, oids.get(i).copied().unwrap_or(OID_TEXT))
2310                                .map_err(|e| format!("column {:?}: {}", c.out, e))
2311                        } else {
2312                            Ok(cell(v).map(|s| s.into_bytes()))
2313                        };
2314                        match got {
2315                            Ok(b) => vals.push(b),
2316                            Err(e) => { fail = Some(e); break; }
2317                        }
2318                    }
2319                    if fail.is_some() {
2320                        break;
2321                    }
2322                    encoded.push(data_row_bytes(&vals));
2323                }
2324                if let Some(why) = fail {
2325                    sock.write_all(&err_msg("22P03", &why)).await?;
2326                    failed = true;
2327                    continue;
2328                }
2329                let mut out = vec![];
2330                for e in &encoded {
2331                    out.extend_from_slice(e);
2332                }
2333                let r = portal.result.as_mut().expect("just executed");
2334                r.sent = limit;
2335                // More rows left and the client capped the batch: suspend the
2336                // portal instead of completing it. This is what a JDBC
2337                // `setFetchSize` and a psycopg3 server-side cursor rely on.
2338                if max_rows > 0 && r.sent < r.rows.len() {
2339                    out.extend_from_slice(&portal_suspended());
2340                } else {
2341                    let tag = if r.tag_counts_rows {
2342                        format!("{} {}", r.tag, r.sent)
2343                    } else {
2344                        r.tag.clone()
2345                    };
2346                    out.extend_from_slice(&command_complete(&tag));
2347                }
2348                sock.write_all(&out).await?;
2349            }
2350
2351            // ── Close: 'S' statement, or 'P' portal ───────────────────────
2352            b'C' => {
2353                let kind = body.first().copied().unwrap_or(b'S');
2354                let mut at = 1usize;
2355                let name = take_cstr(&body, &mut at);
2356                if kind == b'S' {
2357                    prepared.remove(&name);
2358                } else {
2359                    portals.remove(&name);
2360                }
2361                // Closing something that was never open is explicitly not an
2362                // error in the protocol.
2363                sock.write_all(&close_complete()).await?;
2364            }
2365
2366            // Flush: everything is written unbuffered already, so this is a
2367            // no-op — but it must NOT produce a ReadyForQuery, or a client that
2368            // flushes mid-sequence (asyncpg does, after Describe) loses sync.
2369            b'H' => {}
2370
2371            b'S' => {
2372                failed = false;
2373                sock.write_all(&ready()).await?;
2374            }
2375
2376            other => {
2377                sock.write_all(&err_msg(
2378                    "08P01",
2379                    &format!("unexpected frontend message {:?}", other as char),
2380                )).await?;
2381                failed = true;
2382            }
2383        }
2384    }
2385}
2386
2387const READ_ONLY_MSG: &str =
2388    "this endpoint is running read-only (NEDBD_PG_READ_ONLY=1). Writes are \
2389     implemented but disabled on this server — unset the flag to allow them.";
2390
2391fn no_db(db_name: &str) -> Vec<u8> {
2392    err_msg("3D000", &format!(
2393        "database {:?} is not open on this server — create it first \
2394         (POST /v1/databases), or connect with -d <name>", db_name))
2395}
2396
2397/// Run a `SELECT` through the full SQL engine when it touches the catalogue.
2398///
2399/// The gate is deliberately narrow: a statement goes to `sqlselect` only when
2400/// one of its tables is a catalogue relation. Everything else keeps the
2401/// SQL→NQL path, which has the index pushdown, `AS OF`, `TRACE` and the
2402/// bounded scans — and whose join story is a real planning question rather
2403/// than a nested loop. Routing a large collection through a nested-loop join
2404/// would be a promise this engine cannot keep.
2405///
2406/// `None` means "not mine": the caller falls through to the ordinary path, so
2407/// the error the client sees is the ordinary path's error rather than a
2408/// confusing one from a parser that was never meant to handle the statement.
2409fn try_catalog_select(
2410    sql: &str,
2411    db: Option<&Arc<Db>>,
2412) -> Result<Option<(Executed, crate::sqlplan::Plan)>, Vec<u8>> {
2413    let sel = match crate::sqlselect::parse(sql) {
2414        Ok(sel) => sel,
2415        Err(why) => {
2416            // A statement that plainly reads the catalogue but that this
2417            // engine cannot parse gets the PARSE error, not the NQL path's.
2418            //
2419            // Falling through unconditionally produced an actively false
2420            // message: `\d` and `\dp` were told "JOIN is not supported",
2421            // which stopped being true the moment joins started working — and
2422            // a wrong explanation is worse than a blunt one, because it sends
2423            // the reader to fix the wrong thing.
2424            if mentions_catalog(sql) {
2425                return Err(err_msg("0A000", &format!(
2426                    "this catalogue query uses SQL this endpoint does not \
2427                     implement: {}", why)));
2428            }
2429            return Ok(None);
2430        }
2431    };
2432
2433    // Which relations does it read?
2434    let mut touched: Vec<String> = vec![];
2435    if let Some(f) = &sel.from {
2436        touched.push(f.name.clone());
2437    }
2438    for j in &sel.joins {
2439        touched.push(j.table.name.clone());
2440    }
2441    let catalog_name = |n: &str| -> String {
2442        // `pg_catalog.pg_class` → `pg_class`, but `information_schema.tables`
2443        // keeps its qualifier, because `tables` is a plausible collection
2444        // name and the catalogue must never shadow a user's own data.
2445        let joined: Vec<&str> = n.split('.').collect();
2446        if joined.len() >= 2 && joined[joined.len() - 2] == "information_schema" {
2447            format!("information_schema.{}", joined[joined.len() - 1])
2448        } else {
2449            joined[joined.len() - 1].to_string()
2450        }
2451    };
2452    if !touched.iter().any(|t| crate::pgcatalog::is_catalog(&catalog_name(t))) {
2453        return Ok(None);
2454    }
2455
2456    // An aggregate over the catalogue falls through on purpose, so the caller
2457    // produces the one clear "a catalogue has nothing to aggregate" message
2458    // rather than a generic "unknown function" from this engine.
2459    if select_has_aggregate(&sel) {
2460        return Ok(None);
2461    }
2462
2463    let resolve = |name: &str| -> anyhow::Result<Option<Box<dyn crate::sqlselect::Relation>>> {
2464        let cname = catalog_name(name);
2465        if let Some(rows) = crate::pgcatalog::rows(&cname, db) {
2466            // A synthesised catalogue relation is small and built eagerly;
2467            // wrapping it satisfies the streaming contract without pretending
2468            // it is lazy.
2469            return Ok(Some(crate::sqlselect::from_vec(rows)));
2470        }
2471        // A join between a catalogue relation and a real collection is
2472        // legitimate, so a user table still resolves.
2473        //
2474        // NOTE: `nql::query` materialises the whole collection, so this side
2475        // is eager even though the evaluator no longer requires it to be.
2476        // Making the storage scan itself lazy is the other half of the work
2477        // and is tracked in HANDOFF — stated here so nobody reads the
2478        // streaming interface as a claim that storage is already streaming.
2479        match db {
2480            Some(db) => match crate::nql::query(db, &format!("FROM {}", cname)) {
2481                Ok((rows, _)) => Ok(Some(crate::sqlselect::from_vec(rows))),
2482                Err(_) => Ok(None),
2483            },
2484            None => Ok(None),
2485        }
2486    };
2487
2488    let (cols, rows, plan) = crate::sqlselect::execute_explain(
2489        &sel,
2490        &resolve,
2491        crate::sqljoin::JoinExec::Auto,
2492    )
2493    .map_err(|e| err_msg("42601", &e.to_string()))?;
2494
2495    Ok(Some((
2496        Executed {
2497            rows,
2498            // The KEY is what the row is stored under; the NAME is what the
2499            // client sees. They differ when a select list has duplicate output
2500            // names, which PostgreSQL permits and generated SQL relies on.
2501            project: cols
2502                .iter()
2503                .map(|c| Col::renamed(&c.key, &c.name))
2504                .collect(),
2505            has_rows: true,
2506            tag: "SELECT".into(),
2507            tag_counts_rows: true,
2508        },
2509        plan,
2510    )))
2511}
2512
2513/// Strip a leading `EXPLAIN`, returning the statement it wraps.
2514///
2515/// `ANALYZE` and `VERBOSE` are accepted and ignored: this endpoint always
2516/// executes and always reports actual rows, so `EXPLAIN` and
2517/// `EXPLAIN ANALYZE` genuinely do the same thing here. Accepting the keyword
2518/// and silently doing the honest thing beats refusing a client's spelling.
2519fn strip_explain(sql: &str) -> Option<&str> {
2520    let t = sql.trim().trim_end_matches(';').trim();
2521    let mut rest = t.strip_prefix("EXPLAIN").or_else(|| t.strip_prefix("explain"))?;
2522    // Require a word boundary so `EXPLAINED` is not mistaken for a keyword.
2523    if !rest.starts_with(char::is_whitespace) {
2524        return None;
2525    }
2526    rest = rest.trim_start();
2527    loop {
2528        let low = rest.to_lowercase();
2529        if let Some(r) = low.strip_prefix("analyze").or_else(|| low.strip_prefix("analyse")) {
2530            if r.starts_with(char::is_whitespace) || r.is_empty() {
2531                rest = rest[rest.len() - r.len()..].trim_start();
2532                continue;
2533            }
2534        }
2535        if let Some(r) = low.strip_prefix("verbose") {
2536            if r.starts_with(char::is_whitespace) || r.is_empty() {
2537                rest = rest[rest.len() - r.len()..].trim_start();
2538                continue;
2539            }
2540        }
2541        break;
2542    }
2543    Some(rest)
2544}
2545
2546/// One text column named `QUERY PLAN`, which is exactly the shape PostgreSQL
2547/// returns — so `psql` prints it without special handling.
2548fn plan_result(lines: Vec<String>) -> Executed {
2549    Executed {
2550        rows: lines
2551            .into_iter()
2552            .map(|l| serde_json::json!({ "QUERY PLAN": l }))
2553            .collect(),
2554        project: vec![Col::same("QUERY PLAN")],
2555        has_rows: true,
2556        tag: "EXPLAIN".into(),
2557        tag_counts_rows: false,
2558    }
2559}
2560
2561/// Does the raw SQL plainly read a catalogue relation?
2562///
2563/// A cheap text check, used only to decide WHICH error to report when the
2564/// statement cannot be parsed — never to decide what a parsable statement
2565/// means. `pg_` is the giveaway: every catalogue relation is prefixed, and so
2566/// is the `pg_catalog` schema qualifier.
2567fn mentions_catalog(sql: &str) -> bool {
2568    let low = sql.to_lowercase();
2569    low.contains("pg_catalog.")
2570        || low.contains("information_schema.")
2571        || low.contains("from pg_")
2572        || low.contains("join pg_")
2573}
2574
2575/// Does any select item call an aggregate?
2576fn select_has_aggregate(sel: &crate::sqlselect::Select) -> bool {
2577    fn walk(e: &crate::sqlselect::Expr) -> bool {
2578        use crate::sqlselect::Expr as E;
2579        match e {
2580            E::Func { name, args } => {
2581                matches!(name.as_str(),
2582                         "count" | "sum" | "avg" | "min" | "max" | "array_agg"
2583                         | "string_agg" | "bool_and" | "bool_or")
2584                    || args.iter().any(walk)
2585            }
2586            E::Binary { left, right, .. } => walk(left) || walk(right),
2587            E::Unary { expr, .. } | E::Cast { expr, .. } => walk(expr),
2588            E::IsNull { expr, .. } => walk(expr),
2589            E::InList { expr, list, .. } => walk(expr) || list.iter().any(walk),
2590            E::Case { operand, whens, else_ } => {
2591                operand.as_deref().map(walk).unwrap_or(false)
2592                    || whens.iter().any(|(c, t)| walk(c) || walk(t))
2593                    || else_.as_deref().map(walk).unwrap_or(false)
2594            }
2595            _ => false,
2596        }
2597    }
2598    sel.items.iter().any(|i| walk(&i.expr))
2599}
2600
2601/// The catalogue relation a translated query reads from, if any.
2602///
2603/// Reads the collection straight off the parsed NQL rather than re-parsing the
2604/// SQL, so it cannot disagree with what the executor is about to run.
2605fn catalog_target(nql: &str) -> Option<String> {
2606    let coll = crate::nql::parse(nql).ok()?.coll;
2607    if crate::pgcatalog::is_catalog(&coll) {
2608        Some(coll)
2609    } else {
2610        None
2611    }
2612}
2613
2614/// True when the statement carried a RETURNING clause. Checked against the raw
2615/// SQL because `RETURNING *` yields an EMPTY projection, which is otherwise
2616/// indistinguishable from "no RETURNING at all".
2617fn wants_returning(sql: &str) -> bool {
2618    find_kw(&sql.to_uppercase(), "RETURNING").is_some()
2619}
2620
2621/// A unique key for a server-assigned INSERT id.
2622fn next_row_id() -> String {
2623    use std::sync::atomic::{AtomicU64, Ordering};
2624    static N: AtomicU64 = AtomicU64::new(0);
2625    let n = N.fetch_add(1, Ordering::Relaxed);
2626    let ts = std::time::SystemTime::now()
2627        .duration_since(std::time::UNIX_EPOCH)
2628        .map(|d| d.as_micros())
2629        .unwrap_or(0);
2630    format!("r{}{}", ts, n)
2631}
2632
2633/// One executed statement, held apart from any wire encoding.
2634///
2635/// This type is why the simple and extended protocols share an execution path
2636/// rather than growing two copies of the SQL→NEDB semantics. The simple path
2637/// encodes it immediately; the extended path parks it in a portal and dribbles
2638/// the rows out across successive `Execute` messages. Both get identical
2639/// answers because both call `execute_stmt`.
2640pub struct Executed {
2641    /// The rows the client gets — a SELECT's result, or a write's `RETURNING`.
2642    pub rows: Vec<Value>,
2643    /// How to project them (empty = every key in the row).
2644    pub project: Vec<Col>,
2645    /// Whether the client asked for rows at all. Distinct from `rows.is_empty()`:
2646    /// a `SELECT` matching nothing still owes a `RowDescription`, while an
2647    /// `UPDATE` without `RETURNING` owes `NoData`.
2648    pub has_rows: bool,
2649    /// The command tag, already rendered — except for a SELECT, where the row
2650    /// count is only known once the rows have actually been sent.
2651    pub tag: String,
2652    /// True when `tag` is a SELECT-shaped tag whose count is the rows sent.
2653    pub tag_counts_rows: bool,
2654}
2655
2656impl Executed {
2657    fn nothing(tag: &str) -> Self {
2658        Executed { rows: vec![], project: vec![], has_rows: false, tag: tag.to_string(), tag_counts_rows: false }
2659    }
2660    /// Render the final `CommandComplete` given how many rows went out.
2661    fn tag_for(&self, sent: usize) -> String {
2662        if self.tag_counts_rows { format!("{} {}", self.tag, sent) } else { self.tag.clone() }
2663    }
2664}
2665
2666/// Run ONE statement. `Err` carries an already-encoded `ErrorResponse`.
2667///
2668/// Every SQL→NEDB decision lives here, which is the point: the extended query
2669/// protocol added below is then purely a matter of message framing, and cannot
2670/// drift from the simple path's semantics.
2671fn execute_stmt(
2672    stmt_sql: &str,
2673    db_name: &str,
2674    db: Option<&Arc<Db>>,
2675    read_only: bool,
2676) -> Result<Executed, Vec<u8>> {
2677    // The full SQL engine gets first refusal, but ONLY for statements that
2678    // touch the catalogue — see `try_catalog_select`. It has to run before
2679    // `translate`, because `translate` targets NQL and NQL cannot express a
2680    // join, a CASE or a scalar function at all.
2681    // EXPLAIN reports which engine would run the statement, and a plan only
2682    // when the SQL evaluator is the engine that actually runs it. Describing a
2683    // pipeline the statement would not take is the one thing an EXPLAIN must
2684    // never do.
2685    if let Some(inner) = strip_explain(stmt_sql) {
2686        if let Some((_, plan)) = try_catalog_select(inner, db)? {
2687            return Ok(plan_result(plan.render()));
2688        }
2689        let mut lines = vec![];
2690        match translate(inner) {
2691            Ok(_) => {
2692                lines.push(
2693                    "NQL path — this statement is translated to NQL and \
2694                     executed by the storage engine, not by the SQL evaluator."
2695                        .to_string(),
2696                );
2697                lines.push(
2698                    "No plan is reported, because the SQL evaluator is not \
2699                     what runs it. Reporting one would describe a pipeline \
2700                     that never executed."
2701                        .to_string(),
2702                );
2703                lines.push(
2704                    "The SQL evaluator (joins, CASE, scalar functions, a \
2705                     hash-join planner) currently serves catalogue queries."
2706                        .to_string(),
2707                );
2708            }
2709            Err(why) => lines.push(format!("cannot be executed: {why}")),
2710        }
2711        return Ok(plan_result(lines));
2712    }
2713
2714    if let Some((done, _plan)) = try_catalog_select(stmt_sql, db)? {
2715        return Ok(done);
2716    }
2717
2718    let stmt = translate(stmt_sql).map_err(|why| err_msg("0A000", &why))?;
2719
2720    // Every arm below that touches storage needs a database; resolve the
2721    // "no such database" answer once instead of at each use.
2722    macro_rules! need_db {
2723        () => {
2724            match db {
2725                Some(db) => db,
2726                None => return Err(no_db(db_name)),
2727            }
2728        };
2729    }
2730    macro_rules! need_write {
2731        () => {
2732            if read_only {
2733                return Err(err_msg("25006", READ_ONLY_MSG));
2734            }
2735        };
2736    }
2737
2738    match stmt {
2739        Stmt::Ok(tag) => Ok(Executed::nothing(if tag.is_empty() { "SELECT 0" } else { tag })),
2740
2741        Stmt::Canned { cols, row } => {
2742            // Fold the canned answer into an ordinary row so the encoders,
2743            // the portal machinery and `Describe` all see one shape.
2744            let mut obj = serde_json::Map::new();
2745            for (c, v) in cols.iter().zip(row.iter()) {
2746                obj.insert(c.clone(), Value::String(v.clone()));
2747            }
2748            Ok(Executed {
2749                rows: vec![Value::Object(obj)],
2750                project: cols.iter().map(|c| Col::same(c)).collect(),
2751                has_rows: true,
2752                tag: "SELECT".into(),
2753                tag_counts_rows: true,
2754            })
2755        }
2756
2757        Stmt::Query { nql, project } => {
2758            // A catalogue relation is synthesised from the live database
2759            // rather than read from it — but it is still queried with the
2760            // ORDINARY predicate path, so WHERE / ORDER BY / LIMIT and the
2761            // `~` operators work on it because they are the same operators.
2762            //
2763            // Checked BEFORE `need_db!()`: `SELECT * FROM pg_namespace` has to
2764            // answer even when the client connected without naming a database,
2765            // which is exactly what psql does on startup. Refusing there is
2766            // how "psql cannot connect" starts.
2767            if let Some(coll) = catalog_target(&nql) {
2768                let rows = crate::pgcatalog::rows(&coll, db)
2769                    .expect("catalog_target only returns names pgcatalog serves");
2770                let rows = crate::nql::query_rows(rows, &nql)
2771                    .map_err(|e| err_msg("42601", &e.to_string()))?;
2772                return Ok(Executed {
2773                    rows, project, has_rows: true,
2774                    tag: "SELECT".into(), tag_counts_rows: true,
2775                });
2776            }
2777            let db = need_db!();
2778            let (rows, _) = crate::nql::query(db, &nql).map_err(|e| {
2779                err_msg("42601", &format!("{} (translated to NQL: {})", e, nql))
2780            })?;
2781            Ok(Executed { rows, project, has_rows: true, tag: "SELECT".into(), tag_counts_rows: true })
2782        }
2783
2784        Stmt::Insert { coll, rows, returning } => {
2785            let db = need_db!();
2786            need_write!();
2787            let mut written: Vec<Value> = vec![];
2788            for (i, r) in rows.iter().enumerate() {
2789                // The engine requires an id. When the statement did not supply
2790                // one, mint a unique key rather than silently overwriting a
2791                // shared default.
2792                let id = match &r.id {
2793                    Some(id) => id.clone(),
2794                    None => format!("{}-{}", next_row_id(), i),
2795                };
2796                let node = db
2797                    .put(&coll, &id, Value::Object(r.doc.clone()),
2798                         r.caused_by.clone(), r.valid_from.clone(), r.valid_to.clone())
2799                    .map_err(|e| err_msg("XX000", &format!("INSERT failed: {}", e)))?;
2800                written.push(crate::nql::node_to_json(&node));
2801            }
2802            let n = written.len();
2803            let has_rows = wants_returning(stmt_sql);
2804            Ok(Executed {
2805                rows: if has_rows { written } else { vec![] },
2806                project: returning,
2807                has_rows,
2808                // Postgres reports `INSERT <oid> <rows>`; the oid is always 0.
2809                tag: format!("INSERT 0 {}", n),
2810                tag_counts_rows: false,
2811            })
2812        }
2813
2814        Stmt::Update { coll, set, nql, returning } => {
2815            let db = need_db!();
2816            need_write!();
2817            // Matching rows come from an ordinary NQL read, so the whole
2818            // predicate surface works inside an UPDATE.
2819            let (matched, _) = crate::nql::query(db, &nql).map_err(|e| {
2820                err_msg("42601", &format!("{} (translated to NQL: {})", e, nql))
2821            })?;
2822            let mut written: Vec<Value> = vec![];
2823            for row in &matched {
2824                let id = match row.get("_id").and_then(|v| v.as_str()) {
2825                    Some(id) => id.to_string(),
2826                    None => continue,
2827                };
2828                // Merge onto the CURRENT stored document, not onto the query
2829                // row: a query row carries injected `_`-prefixed metadata that
2830                // must never be written back into the payload.
2831                let mut doc = match db.get(&coll, &id) {
2832                    Some(n) => match n.data {
2833                        Value::Object(m) => m,
2834                        _ => serde_json::Map::new(),
2835                    },
2836                    None => continue,
2837                };
2838                for (k, v) in &set {
2839                    doc.insert(k.clone(), v.clone());
2840                }
2841                // An UPDATE is a NEW VERSION — the prior value stays readable
2842                // with AS OF SYSTEM TIME. That is the whole point.
2843                let node = db
2844                    .put(&coll, &id, Value::Object(doc), vec![], None, None)
2845                    .map_err(|e| err_msg("XX000", &format!("UPDATE failed: {}", e)))?;
2846                written.push(crate::nql::node_to_json(&node));
2847            }
2848            let n = written.len();
2849            let has_rows = wants_returning(stmt_sql);
2850            Ok(Executed {
2851                rows: if has_rows { written } else { vec![] },
2852                project: returning,
2853                has_rows,
2854                tag: format!("UPDATE {}", n),
2855                tag_counts_rows: false,
2856            })
2857        }
2858
2859        Stmt::Delete { coll, nql, returning } => {
2860            let db = need_db!();
2861            need_write!();
2862            let (matched, _) = crate::nql::query(db, &nql).map_err(|e| {
2863                err_msg("42601", &format!("{} (translated to NQL: {})", e, nql))
2864            })?;
2865            // RETURNING must be captured BEFORE the delete: after the tombstone
2866            // the row is no longer readable by id.
2867            let returned = matched.clone();
2868            let mut n = 0usize;
2869            for row in &matched {
2870                if let Some(id) = row.get("_id").and_then(|v| v.as_str()) {
2871                    match db.delete(&coll, id) {
2872                        Ok(true) => n += 1,
2873                        Ok(false) => {}
2874                        Err(e) => return Err(err_msg("XX000", &format!("DELETE failed: {}", e))),
2875                    }
2876                }
2877            }
2878            let has_rows = wants_returning(stmt_sql);
2879            Ok(Executed {
2880                rows: if has_rows { returned } else { vec![] },
2881                project: returning,
2882                has_rows,
2883                tag: format!("DELETE {}", n),
2884                tag_counts_rows: false,
2885            })
2886        }
2887    }
2888}
2889
2890/// Execute a simple-query payload, which may hold several `;`-separated statements.
2891fn run_simple_query(sql: &str, db_name: &str, db: Option<&Arc<Db>>, read_only: bool) -> Vec<u8> {
2892    let mut out = vec![];
2893    let statements = split_statements(sql);
2894    if statements.is_empty() {
2895        // EmptyQueryResponse
2896        return Out::msg(b'I').finish();
2897    }
2898    for stmt_sql in statements {
2899        match execute_stmt(&stmt_sql, db_name, db, read_only) {
2900            // Abandon the rest of the batch on the first error, as Postgres does.
2901            Err(encoded) => {
2902                out.extend_from_slice(&encoded);
2903                return out;
2904            }
2905            Ok(ex) => {
2906                if ex.has_rows {
2907                    out.extend_from_slice(&encode_rows(&ex.rows, &ex.project));
2908                }
2909                out.extend_from_slice(&command_complete(&ex.tag_for(ex.rows.len())));
2910            }
2911        }
2912    }
2913    out
2914}
2915
2916/// Split on `;` at the top level, ignoring separators inside string literals.
2917fn split_statements(sql: &str) -> Vec<String> {
2918    let mut out = vec![];
2919    let mut cur = String::new();
2920    let mut in_s = false;
2921    for c in sql.chars() {
2922        match c {
2923            '\'' => { in_s = !in_s; cur.push(c); }
2924            ';' if !in_s => {
2925                if !cur.trim().is_empty() { out.push(cur.clone()); }
2926                cur.clear();
2927            }
2928            _ => cur.push(c),
2929        }
2930    }
2931    if !cur.trim().is_empty() {
2932        out.push(cur);
2933    }
2934    out
2935}
2936
2937/// Bind and serve the Postgres read endpoint until the process exits.
2938pub async fn run(host: &str, port: u16, resolver: Arc<dyn DbResolver>) -> anyhow::Result<()> {
2939    // Writes are ON by default — that is the parity position. An operator who
2940    // wants the "system of proof beside your database" deployment, where this
2941    // door must never mutate anything, sets NEDBD_PG_READ_ONLY=1.
2942    let read_only = std::env::var("NEDBD_PG_READ_ONLY")
2943        .map(|v| v == "1" || v.eq_ignore_ascii_case("true"))
2944        .unwrap_or(false);
2945    let listener = TcpListener::bind((host, port)).await?;
2946    println!("  pgwire   postgres endpoint on {}:{} — psql / DBeaver / psycopg ({})",
2947             host, port,
2948             if read_only { "SELECT only — read-only mode" } else { "SELECT + INSERT/UPDATE/DELETE" });
2949    loop {
2950        let (sock, _peer) = match listener.accept().await {
2951            Ok(v) => v,
2952            Err(e) => {
2953                eprintln!("  [pgwire] accept failed: {}", e);
2954                continue;
2955            }
2956        };
2957        let r = Arc::clone(&resolver);
2958        tokio::spawn(async move {
2959            let _ = sock.set_nodelay(true);
2960            if let Err(e) = handle(sock, r, read_only).await {
2961                // A client disconnecting mid-message is routine, not an incident.
2962                if e.kind() != std::io::ErrorKind::UnexpectedEof
2963                    && e.kind() != std::io::ErrorKind::ConnectionReset
2964                {
2965                    eprintln!("  [pgwire] connection error: {}", e);
2966                }
2967            }
2968        });
2969    }
2970}
2971
2972// ─────────────────────────────────────────────────────────────────────────────
2973
2974#[cfg(test)]
2975mod explain_tests {
2976    use super::*;
2977
2978    #[test]
2979    fn a_bare_explain_is_stripped() {
2980        assert_eq!(strip_explain("EXPLAIN SELECT 1"), Some("SELECT 1"));
2981        assert_eq!(strip_explain("explain select 1"), Some("select 1"));
2982        assert_eq!(strip_explain("  EXPLAIN   SELECT 1 ;  "), Some("SELECT 1"));
2983    }
2984
2985    #[test]
2986    fn analyze_and_verbose_are_accepted_and_ignored() {
2987        // This endpoint always executes and always reports actual rows, so
2988        // EXPLAIN and EXPLAIN ANALYZE genuinely do the same thing. Accepting
2989        // the client's spelling beats refusing it.
2990        assert_eq!(strip_explain("EXPLAIN ANALYZE SELECT 1"), Some("SELECT 1"));
2991        assert_eq!(strip_explain("EXPLAIN ANALYSE SELECT 1"), Some("SELECT 1"));
2992        assert_eq!(strip_explain("EXPLAIN VERBOSE SELECT 1"), Some("SELECT 1"));
2993        assert_eq!(strip_explain("EXPLAIN ANALYZE VERBOSE SELECT 1"), Some("SELECT 1"));
2994        assert_eq!(strip_explain("explain analyze verbose select 1"), Some("select 1"));
2995    }
2996
2997    #[test]
2998    fn a_word_merely_starting_with_explain_is_not_a_keyword() {
2999        assert_eq!(strip_explain("EXPLAINED SELECT 1"), None);
3000        assert_eq!(strip_explain("SELECT 1"), None);
3001        assert_eq!(strip_explain("SELECT explain FROM t"), None);
3002    }
3003
3004    #[test]
3005    fn a_column_named_analyze_is_not_eaten() {
3006        // `analyzed` merely starts with the keyword; the word boundary check
3007        // is what stops it being consumed as an option.
3008        assert_eq!(strip_explain("EXPLAIN analyzed_view"), Some("analyzed_view"));
3009    }
3010
3011    #[test]
3012    fn the_plan_result_has_postgres_shape() {
3013        let e = plan_result(vec!["Seq Scan on t".into(), "note".into()]);
3014        assert_eq!(e.project.len(), 1);
3015        assert_eq!(e.project[0].out, "QUERY PLAN");
3016        assert_eq!(e.rows.len(), 2);
3017        assert_eq!(e.rows[0]["QUERY PLAN"], "Seq Scan on t");
3018        assert_eq!(e.tag, "EXPLAIN");
3019        // EXPLAIN's tag carries no row count in PostgreSQL.
3020        assert!(!e.tag_counts_rows);
3021    }
3022}
3023
3024#[cfg(test)]
3025mod tests {
3026    use super::*;
3027    use serde_json::json;
3028
3029    fn q(sql: &str) -> String {
3030        match translate(sql) {
3031            Ok(Stmt::Query { nql, .. }) => nql,
3032            other => panic!("expected a query for {:?}, got {:?}", sql, other),
3033        }
3034    }
3035    /// Output column names, in order.
3036    fn proj(sql: &str) -> Vec<String> {
3037        match translate(sql) {
3038            Ok(Stmt::Query { project, .. }) => project.iter().map(|c| c.out.clone()).collect(),
3039            other => panic!("expected a query for {:?}, got {:?}", sql, other),
3040        }
3041    }
3042    /// (source key, output name) pairs, for the aggregate renaming.
3043    fn proj_pairs(sql: &str) -> Vec<(String, String)> {
3044        match translate(sql) {
3045            Ok(Stmt::Query { project, .. }) =>
3046                project.iter().map(|c| (c.src.clone(), c.out.clone())).collect(),
3047            other => panic!("expected a query for {:?}, got {:?}", sql, other),
3048        }
3049    }
3050    fn names(cols: &[Col]) -> Vec<String> { cols.iter().map(|c| c.out.clone()).collect() }
3051
3052    #[test]
3053    fn select_star_becomes_bare_from() {
3054        assert_eq!(q("SELECT * FROM orders"), "FROM orders");
3055        assert_eq!(q("select * from orders;"), "FROM orders");
3056        assert_eq!(proj("SELECT * FROM orders"), Vec::<String>::new());
3057    }
3058
3059    #[test]
3060    fn a_column_list_becomes_a_projection_not_a_clause() {
3061        // NQL has no projection, so the column list is carried separately and
3062        // applied to the returned rows.
3063        assert_eq!(q("SELECT status, total FROM orders"), "FROM orders");
3064        assert_eq!(proj("SELECT status, total FROM orders"), vec!["status", "total"]);
3065    }
3066
3067    #[test]
3068    fn aliases_and_qualified_names_reduce_to_the_field() {
3069        assert_eq!(proj("SELECT o.status AS s, o.total total FROM orders o"),
3070                   vec!["status", "total"]);
3071        assert_eq!(q("SELECT * FROM public.orders"), "FROM orders");
3072        assert_eq!(q("SELECT * FROM \"orders\""), "FROM orders");
3073    }
3074
3075    #[test]
3076    fn where_clauses_pass_through_with_sql_literals_rewritten() {
3077        assert_eq!(q("SELECT * FROM orders WHERE status = 'paid'"),
3078                   r#"FROM orders WHERE status = "paid""#);
3079        assert_eq!(q("SELECT * FROM orders WHERE status <> 'paid'"),
3080                   r#"FROM orders WHERE status != "paid""#);
3081        assert_eq!(q("SELECT * FROM orders WHERE status IN ('paid','open')"),
3082                   r#"FROM orders WHERE status IN ("paid","open")"#);
3083    }
3084
3085    /// SQL escapes an embedded quote by doubling it. That must become ONE
3086    /// character inside the NQL string, not terminate it.
3087    #[test]
3088    fn a_doubled_sql_quote_is_one_literal_character() {
3089        assert_eq!(q("SELECT * FROM t WHERE name = 'it''s'"),
3090                   r#"FROM t WHERE name = "it's""#);
3091    }
3092
3093    /// A double quote inside a SQL literal has to be escaped for NQL, whose
3094    /// lexer collapses \" — otherwise it would close the string early.
3095    #[test]
3096    fn a_double_quote_inside_a_sql_literal_is_escaped_for_nql() {
3097        assert_eq!(q(r#"SELECT * FROM t WHERE name = 'say "hi"'"#),
3098                   r#"FROM t WHERE name = "say \"hi\"""#);
3099    }
3100
3101    #[test]
3102    fn the_shared_clauses_are_handed_to_nql_unchanged() {
3103        assert_eq!(q("SELECT * FROM orders ORDER BY total DESC LIMIT 10 OFFSET 5"),
3104                   "FROM orders ORDER BY total DESC LIMIT 10 OFFSET 5");
3105        assert_eq!(q("SELECT * FROM orders GROUP BY region"), "FROM orders GROUP BY region");
3106        assert_eq!(q("SELECT * FROM o WHERE total BETWEEN 1 AND 9 ORDER BY a, b DESC"),
3107                   "FROM o WHERE total BETWEEN 1 AND 9 ORDER BY a, b DESC");
3108    }
3109
3110    /// An aggregate must surface as ONE column, named as SQL names it.
3111    ///
3112    /// NQL answers `SUM(total)` with `{count, sum_total, value}` — `value`
3113    /// being a back-compat alias. Passing that straight through gave
3114    /// `SELECT COUNT(*)` two columns (`count`, `value`) where SQL promises
3115    /// one, and leaked an internal key name onto the wire.
3116    #[test]
3117    fn an_aggregate_is_one_column_named_as_sql_names_it() {
3118        assert_eq!(proj_pairs("SELECT COUNT(*) FROM orders"),
3119                   vec![("count".to_string(), "count".to_string())]);
3120        assert_eq!(proj_pairs("SELECT SUM(total) FROM orders"),
3121                   vec![("sum_total".to_string(), "sum".to_string())]);
3122        assert_eq!(proj_pairs("SELECT avg(total) FROM orders"),
3123                   vec![("avg_total".to_string(), "avg".to_string())]);
3124        assert_eq!(proj_pairs("SELECT MIN(total) FROM orders"),
3125                   vec![("min_total".to_string(), "min".to_string())]);
3126        // And the encoded result really is one column with that name.
3127        let rows = vec![json!({"count": 4, "sum_total": 420, "value": 420})];
3128        let p = vec![Col::renamed("sum_total", "sum")];
3129        let cols = columns_for(&rows, &p);
3130        assert_eq!(names(&cols), vec!["sum"], "one column, SQL's name");
3131        assert_eq!(cell(rows[0].get(&cols[0].src)), Some("420".to_string()));
3132    }
3133
3134    /// A grouped NQL row holds the group key, `count` and the aggregate —
3135    /// nothing else. Projecting another column found nothing and rendered
3136    /// NULL, which is a silent wrong answer. Postgres errors; so do we, in
3137    /// Postgres's own words.
3138    #[test]
3139    fn a_bare_column_with_group_by_is_refused_not_nulled() {
3140        let e = translate("SELECT region, total FROM orders GROUP BY region").unwrap_err();
3141        assert!(e.contains("must appear in the GROUP BY clause"), "{}", e);
3142        assert!(e.contains("total"), "the message names the offending column: {}", e);
3143
3144        // The group key itself, and `count`, are both legitimate.
3145        assert!(translate("SELECT region FROM orders GROUP BY region").is_ok());
3146        assert!(translate("SELECT region, count FROM orders GROUP BY region").is_ok());
3147        // As is an aggregate over the grouped set.
3148        assert!(translate("SELECT SUM(total) FROM orders GROUP BY region").is_ok());
3149        // And `*` is unaffected — it returns whatever the grouped row holds.
3150        assert!(translate("SELECT * FROM orders GROUP BY region").is_ok());
3151    }
3152
3153    #[test]
3154    fn count_star_becomes_nql_count() {
3155        assert_eq!(q("SELECT COUNT(*) FROM orders"), "FROM orders COUNT");
3156        assert_eq!(q("SELECT count(*) FROM orders WHERE total > 5"),
3157                   "FROM orders COUNT WHERE total > 5");
3158    }
3159
3160    #[test]
3161    fn aggregates_carry_their_target_column() {
3162        assert_eq!(q("SELECT SUM(total) FROM orders"), "FROM orders SUM total");
3163        assert_eq!(q("SELECT avg(total) FROM orders WHERE region = 'eu'"),
3164                   r#"FROM orders AVG total WHERE region = "eu""#);
3165        assert!(translate("SELECT SUM(*) FROM orders").is_err());
3166    }
3167
3168    /// The bridge worth having: Postgres spells time travel
3169    /// `AS OF SYSTEM TIME`, and NEDB's is sequence-addressed and permanent.
3170    #[test]
3171    fn as_of_system_time_bridges_to_nql_as_of() {
3172        assert_eq!(q("SELECT * FROM orders AS OF SYSTEM TIME 42"),
3173                   "FROM orders AS OF 42");
3174        assert_eq!(q("SELECT * FROM orders AS OF SYSTEM TIME 42 WHERE total > 1"),
3175                   "FROM orders AS OF 42 WHERE total > 1");
3176        // A wall-clock timestamp is refused with the reason, not silently ignored.
3177        let e = translate("SELECT * FROM orders AS OF SYSTEM TIME '2026-01-01'").unwrap_err();
3178        assert!(e.contains("sequence number"), "{}", e);
3179    }
3180
3181    #[test]
3182    fn handshake_queries_are_answered_so_clients_can_connect() {
3183        assert!(matches!(translate("SELECT version()"), Ok(Stmt::Canned { .. })));
3184        assert!(matches!(translate("SHOW transaction_isolation"), Ok(Stmt::Canned { .. })));
3185        assert!(matches!(translate("SELECT current_schema()"), Ok(Stmt::Canned { .. })));
3186        assert!(matches!(translate("SET extra_float_digits = 3"), Ok(Stmt::Ok(_))));
3187        assert!(matches!(translate("BEGIN"), Ok(Stmt::Ok(_))));
3188        assert!(matches!(translate(""), Ok(Stmt::Ok(_))));
3189    }
3190
3191    /// Every refusal has to name the boundary. "Syntax error" would send a
3192    /// developer hunting for a typo that is not there.
3193    #[test]
3194    fn unsupported_sql_is_refused_with_a_reason() {
3195        for (sql, expect) in [
3196            ("INSERT INTO t VALUES (1)", "explicit column list"),
3197            ("CREATE TABLE t (a int)", "DDL"),
3198            ("TRUNCATE t", "append-only"),
3199            ("GRANT ALL ON t TO x", "privilege system"),
3200            ("SELECT * FROM a JOIN b ON a.x = b.x", "JOIN is not supported"),
3201            ("SELECT * FROM a UNION SELECT * FROM b", "UNION"),
3202            ("SELECT DISTINCT region FROM orders", "GROUP BY"),
3203            ("SELECT * FROM (SELECT 1) x", "subqueries in FROM"),
3204            ("SELECT * FROM a, b", "more than one collection"),
3205            ("SELECT lower(status) FROM orders", "expressions in the select list"),
3206            ("VACUUM", "only SELECT"),
3207        ] {
3208            let e = translate(sql).unwrap_err();
3209            assert!(e.contains(expect), "for {:?} expected {:?} in {:?}", sql, expect, e);
3210        }
3211    }
3212
3213    // ── writes ───────────────────────────────────────────────────────────────
3214    //
3215    // SQL's write semantics and NEDB's append-only model line up: INSERT is a
3216    // put, UPDATE is a new version, DELETE is a tombstone. These tests pin the
3217    // parse; tests/test_pgwire.py proves the behaviour against a live server,
3218    // including that the PRIOR value is still readable afterwards.
3219
3220    fn ins(sql: &str) -> (String, Vec<InsertRow>, Vec<Col>) {
3221        match translate(sql) {
3222            Ok(Stmt::Insert { coll, rows, returning }) => (coll, rows, returning),
3223            other => panic!("expected INSERT for {:?}, got {:?}", sql, other),
3224        }
3225    }
3226
3227    #[test]
3228    fn insert_becomes_a_put_per_row() {
3229        let (coll, rows, ret) = ins("INSERT INTO orders (_id, status, total) VALUES ('o1', 'paid', 120)");
3230        assert_eq!(coll, "orders");
3231        assert_eq!(rows.len(), 1);
3232        assert_eq!(rows[0].id.as_deref(), Some("o1"));
3233        assert_eq!(rows[0].doc.get("status"), Some(&json!("paid")));
3234        assert_eq!(rows[0].doc.get("total"), Some(&json!(120)));
3235        // `_id` is the key, not a payload field.
3236        assert!(!rows[0].doc.contains_key("_id"));
3237        assert!(ret.is_empty());
3238    }
3239
3240    #[test]
3241    fn a_multi_row_insert_yields_one_row_each() {
3242        let (_, rows, _) = ins(
3243            "INSERT INTO t (id, n) VALUES ('a', 1), ('b', 2), ('c', 3)");
3244        assert_eq!(rows.len(), 3);
3245        assert_eq!(rows[1].id.as_deref(), Some("b"));
3246        assert_eq!(rows[2].doc.get("n"), Some(&json!(3)));
3247    }
3248
3249    #[test]
3250    fn an_insert_without_an_id_column_lets_the_server_assign_one() {
3251        let (_, rows, _) = ins("INSERT INTO t (n) VALUES (1)");
3252        assert_eq!(rows[0].id, None, "the executor mints a unique key");
3253        assert_eq!(rows[0].doc.get("n"), Some(&json!(1)));
3254    }
3255
3256    /// Provenance is reachable from SQL, not only from the HTTP API — which is
3257    /// the point of having writes here at all.
3258    #[test]
3259    fn insert_lifts_provenance_out_of_reserved_columns() {
3260        let (_, rows, _) = ins(
3261            "INSERT INTO audit (_id, _caused_by, _valid_from, kind) \
3262             VALUES ('e1', 'abc123', '2026-01-01', 'reprice')");
3263        assert_eq!(rows[0].caused_by, vec!["abc123".to_string()]);
3264        assert_eq!(rows[0].valid_from.as_deref(), Some("2026-01-01"));
3265        assert_eq!(rows[0].doc.get("kind"), Some(&json!("reprice")));
3266        // None of the reserved names leak into the stored payload.
3267        for k in ["_id", "_caused_by", "_valid_from"] {
3268            assert!(!rows[0].doc.contains_key(k), "{} leaked into the doc", k);
3269        }
3270    }
3271
3272    #[test]
3273    fn insert_values_cover_the_scalar_types() {
3274        let (_, rows, _) = ins(
3275            "INSERT INTO t (s, i, f, b, n) VALUES ('x', 42, 1.5, TRUE, NULL)");
3276        assert_eq!(rows[0].doc.get("s"), Some(&json!("x")));
3277        assert_eq!(rows[0].doc.get("i"), Some(&json!(42)));
3278        assert_eq!(rows[0].doc.get("f"), Some(&json!(1.5)));
3279        assert_eq!(rows[0].doc.get("b"), Some(&json!(true)));
3280        assert_eq!(rows[0].doc.get("n"), Some(&Value::Null));
3281    }
3282
3283    /// A doubled '' is one literal quote, and a comma inside a string is not a
3284    /// value separator.
3285    #[test]
3286    fn insert_literals_survive_quotes_and_commas() {
3287        let (_, rows, _) = ins("INSERT INTO t (a, b) VALUES ('it''s', 'x,y')");
3288        assert_eq!(rows[0].doc.get("a"), Some(&json!("it's")));
3289        assert_eq!(rows[0].doc.get("b"), Some(&json!("x,y")));
3290    }
3291
3292    #[test]
3293    fn insert_refuses_what_it_cannot_store_faithfully() {
3294        // An unevaluated expression stored as text would be a wrong value.
3295        assert!(translate("INSERT INTO t (a) VALUES (1 + 1)").is_err());
3296        assert!(translate("INSERT INTO t (a) VALUES (now())").is_err());
3297        // Column/value count mismatch.
3298        let e = translate("INSERT INTO t (a, b) VALUES (1)").unwrap_err();
3299        assert!(e.contains("values for"), "{}", e);
3300        // No column list at all.
3301        let e2 = translate("INSERT INTO t VALUES (1)").unwrap_err();
3302        assert!(e2.contains("explicit column list"), "{}", e2);
3303    }
3304
3305    #[test]
3306    fn update_finds_rows_with_the_full_predicate_surface() {
3307        match translate("UPDATE orders SET status = 'void' WHERE total < 50 AND region IN ('eu')") {
3308            Ok(Stmt::Update { coll, set, nql, .. }) => {
3309                assert_eq!(coll, "orders");
3310                assert_eq!(set, vec![("status".to_string(), json!("void"))]);
3311                // The WHERE became ordinary NQL, so IN/BETWEEN/LIKE all work.
3312                assert_eq!(nql, r#"FROM orders WHERE total < 50 AND region IN ("eu")"#);
3313            }
3314            other => panic!("expected UPDATE, got {:?}", other),
3315        }
3316    }
3317
3318    #[test]
3319    fn update_without_where_targets_the_whole_collection() {
3320        // Postgres allows it, so parity allows it.
3321        match translate("UPDATE t SET a = 1") {
3322            Ok(Stmt::Update { nql, .. }) => assert_eq!(nql, "FROM t"),
3323            other => panic!("expected UPDATE, got {:?}", other),
3324        }
3325    }
3326
3327    #[test]
3328    fn update_handles_several_assignments() {
3329        match translate("UPDATE t SET a = 1, b = 'x,y', c = NULL WHERE id = 'k'") {
3330            Ok(Stmt::Update { set, .. }) => {
3331                assert_eq!(set.len(), 3);
3332                assert_eq!(set[1], ("b".to_string(), json!("x,y")));
3333                assert_eq!(set[2], ("c".to_string(), Value::Null));
3334            }
3335            other => panic!("expected UPDATE, got {:?}", other),
3336        }
3337        assert!(translate("UPDATE t SET").is_err());
3338        assert!(translate("UPDATE t SET a").is_err());
3339    }
3340
3341    #[test]
3342    fn delete_becomes_a_predicate_over_the_collection() {
3343        match translate("DELETE FROM orders WHERE status = 'void'") {
3344            Ok(Stmt::Delete { coll, nql, .. }) => {
3345                assert_eq!(coll, "orders");
3346                assert_eq!(nql, r#"FROM orders WHERE status = "void""#);
3347            }
3348            other => panic!("expected DELETE, got {:?}", other),
3349        }
3350        match translate("DELETE FROM t") {
3351            Ok(Stmt::Delete { nql, .. }) => assert_eq!(nql, "FROM t"),
3352            other => panic!("expected DELETE, got {:?}", other),
3353        }
3354    }
3355
3356    #[test]
3357    fn returning_is_parsed_off_every_write() {
3358        let (_, _, ret) = ins("INSERT INTO t (a) VALUES (1) RETURNING a, _id");
3359        assert_eq!(ret.iter().map(|c| c.out.clone()).collect::<Vec<_>>(), vec!["a", "_id"]);
3360        // `RETURNING *` is an empty projection — every column — which is why
3361        // the executor checks the raw SQL for the keyword instead.
3362        let (_, _, star) = ins("INSERT INTO t (a) VALUES (1) RETURNING *");
3363        assert!(star.is_empty());
3364        assert!(wants_returning("INSERT INTO t (a) VALUES (1) RETURNING *"));
3365        assert!(!wants_returning("INSERT INTO t (a) VALUES (1)"));
3366
3367        match translate("UPDATE t SET a = 1 WHERE id = 'k' RETURNING a") {
3368            Ok(Stmt::Update { nql, returning, .. }) => {
3369                assert_eq!(returning.len(), 1);
3370                // RETURNING must NOT leak into the predicate.
3371                assert!(!nql.to_uppercase().contains("RETURNING"), "{}", nql);
3372            }
3373            other => panic!("expected UPDATE, got {:?}", other),
3374        }
3375        match translate("DELETE FROM t WHERE id = 'k' RETURNING *") {
3376            Ok(Stmt::Delete { nql, .. }) =>
3377                assert!(!nql.to_uppercase().contains("RETURNING"), "{}", nql),
3378            other => panic!("expected DELETE, got {:?}", other),
3379        }
3380    }
3381
3382    #[test]
3383    fn a_keyword_inside_a_value_is_not_a_clause() {
3384        match translate("UPDATE t SET note = 'where returning from' WHERE id = 'k'") {
3385            Ok(Stmt::Update { set, nql, .. }) => {
3386                assert_eq!(set[0].1, json!("where returning from"));
3387                assert_eq!(nql, r#"FROM t WHERE id = "k""#);
3388            }
3389            other => panic!("expected UPDATE, got {:?}", other),
3390        }
3391    }
3392
3393    #[test]
3394    fn split_top_respects_quotes_and_nesting() {
3395        assert_eq!(split_top("a, b, c", ',').len(), 3);
3396        assert_eq!(split_top("(1, 2), (3, 4)", ',').len(), 2);
3397        assert_eq!(split_top("'a,b', c", ',').len(), 2);
3398        assert_eq!(split_top("'it''s, fine', c", ',').len(), 2);
3399    }
3400
3401    #[test]
3402    fn comments_and_whitespace_do_not_confuse_the_translator() {
3403        assert_eq!(q("SELECT *\n  FROM orders  -- trailing note\n"), "FROM orders");
3404        assert_eq!(q("SELECT /* inline */ * FROM orders"), "FROM orders");
3405        // A keyword inside a string literal must not be treated as a clause.
3406        assert_eq!(q("SELECT * FROM t WHERE note = 'from here to JOIN'"),
3407                   r#"FROM t WHERE note = "from here to JOIN""#);
3408    }
3409
3410    #[test]
3411    fn find_kw_ignores_quotes_parens_and_substrings() {
3412        assert_eq!(find_kw("SELECT A FROM B", "FROM"), Some(9));
3413        assert_eq!(find_kw("SELECT 'FROM' FROM B", "FROM"), Some(14));
3414        assert_eq!(find_kw("SELECT F(x FROM y) FROM B", "FROM"), Some(19));
3415        assert_eq!(find_kw("SELECT FROMAGE", "FROM"), None);
3416        assert_eq!(find_kw("SELECT X_FROM", "FROM"), None);
3417    }
3418
3419    // ── result encoding ──────────────────────────────────────────────────────
3420
3421    #[test]
3422    fn provenance_columns_sort_after_the_users_own_fields() {
3423        let rows = vec![json!({"_id":"1","_hash":"ab","status":"paid","total":9})];
3424        assert_eq!(names(&columns_for(&rows, &[])),
3425                   vec!["status", "total", "_hash", "_id"]);
3426    }
3427
3428    #[test]
3429    fn an_explicit_projection_sets_the_column_order() {
3430        let rows = vec![json!({"a":1,"b":2})];
3431        let p = vec![Col::same("b"), Col::same("a")];
3432        assert_eq!(names(&columns_for(&rows, &p)), vec!["b", "a"]);
3433    }
3434
3435    #[test]
3436    fn columns_are_the_union_across_sparse_rows() {
3437        // A document store has no schema, so row 2 may carry a field row 1 lacks.
3438        let rows = vec![json!({"a":1}), json!({"b":2})];
3439        assert_eq!(names(&columns_for(&rows, &[])), vec!["a", "b"]);
3440    }
3441
3442    #[test]
3443    fn type_oids_follow_the_first_non_null_value() {
3444        let rows = vec![json!({"i":1,"f":1.5,"b":true,"s":"x","n":null})];
3445        assert_eq!(oid_for(&rows, "i"), OID_INT8);
3446        assert_eq!(oid_for(&rows, "f"), OID_FLOAT8);
3447        assert_eq!(oid_for(&rows, "b"), OID_BOOL);
3448        assert_eq!(oid_for(&rows, "s"), OID_TEXT);
3449        // All-null and absent columns fall back to text rather than guessing.
3450        assert_eq!(oid_for(&rows, "n"), OID_TEXT);
3451        assert_eq!(oid_for(&rows, "absent"), OID_TEXT);
3452    }
3453
3454    #[test]
3455    fn a_column_that_is_null_in_the_first_row_still_gets_its_type() {
3456        let rows = vec![json!({"v": null}), json!({"v": 7})];
3457        assert_eq!(oid_for(&rows, "v"), OID_INT8);
3458    }
3459
3460    #[test]
3461    fn cells_render_in_postgres_text_format() {
3462        assert_eq!(cell(Some(&json!("x"))), Some("x".to_string()));
3463        assert_eq!(cell(Some(&json!(true))), Some("t".to_string()));
3464        assert_eq!(cell(Some(&json!(false))), Some("f".to_string()));
3465        assert_eq!(cell(Some(&json!(42))), Some("42".to_string()));
3466        assert_eq!(cell(Some(&json!(null))), None);
3467        assert_eq!(cell(None), None);
3468        // Nested values render as JSON text rather than being dropped.
3469        assert_eq!(cell(Some(&json!({"a":1}))), Some("{\"a\":1}".to_string()));
3470    }
3471
3472    /// The framing has to be exact or the client desynchronises and hangs.
3473    /// Length covers the length field itself but not the tag byte.
3474    #[test]
3475    fn message_framing_length_excludes_the_tag() {
3476        let mut m = Out::msg(b'Z');
3477        m.bytes(b"I");
3478        let bytes = m.finish();
3479        assert_eq!(bytes[0], b'Z');
3480        assert_eq!(i32::from_be_bytes([bytes[1], bytes[2], bytes[3], bytes[4]]), 5);
3481        assert_eq!(bytes.len(), 6);
3482    }
3483
3484    #[test]
3485    fn a_result_set_encodes_as_description_then_rows_then_complete() {
3486        let rows = vec![json!({"a": 1}), json!({"a": 2})];
3487        let out = encode_result(&rows, &[]);
3488        assert_eq!(out[0], b'T');
3489        let tags: Vec<u8> = {
3490            // Walk the message stream by its own length prefixes.
3491            let mut t = vec![];
3492            let mut i = 0usize;
3493            while i < out.len() {
3494                t.push(out[i]);
3495                let len = i32::from_be_bytes([out[i+1], out[i+2], out[i+3], out[i+4]]) as usize;
3496                i += 1 + len;
3497            }
3498            t
3499        };
3500        assert_eq!(tags, vec![b'T', b'D', b'D', b'C'],
3501                   "one description, one row each, one completion");
3502    }
3503
3504    /// A statement must emit EXACTLY ONE CommandComplete. A write with
3505    /// RETURNING that reused the SELECT encoder sent two, and the visible
3506    /// symptom was RETURNING yielding no rows: the client took the first tag
3507    /// as the end of the statement and threw the description away.
3508    #[test]
3509    fn a_write_with_returning_emits_exactly_one_command_complete() {
3510        let rows = vec![json!({"_id": "o1", "total": 9})];
3511        let mut out = encode_rows(&rows, &[Col::same("_id")]);
3512        out.extend_from_slice(&command_complete("INSERT 0 1"));
3513        let mut tags = vec![];
3514        let mut i = 0usize;
3515        while i < out.len() {
3516            tags.push(out[i]);
3517            let len = i32::from_be_bytes([out[i+1], out[i+2], out[i+3], out[i+4]]) as usize;
3518            i += 1 + len;
3519        }
3520        assert_eq!(tags, vec![b'T', b'D', b'C'], "one description, one row, ONE tag");
3521        assert_eq!(tags.iter().filter(|t| **t == b'C').count(), 1);
3522        // encode_rows alone must not carry a tag at all.
3523        assert!(!encode_rows(&rows, &[]).contains(&b'C')
3524                || encode_rows(&rows, &[]).iter().filter(|b| **b == b'C').count() > 0);
3525        let bare = encode_rows(&rows, &[Col::same("_id")]);
3526        let mut bare_tags = vec![];
3527        let mut j = 0usize;
3528        while j < bare.len() {
3529            bare_tags.push(bare[j]);
3530            let len = i32::from_be_bytes([bare[j+1], bare[j+2], bare[j+3], bare[j+4]]) as usize;
3531            j += 1 + len;
3532        }
3533        assert_eq!(bare_tags, vec![b'T', b'D'], "encode_rows never appends a tag");
3534    }
3535
3536    #[test]
3537    fn an_empty_result_still_sends_a_description() {
3538        let out = encode_result(&[], &[Col::same("a")]);
3539        assert_eq!(out[0], b'T', "clients need the shape even with no rows");
3540    }
3541
3542    #[test]
3543    fn statements_split_on_top_level_semicolons_only() {
3544        assert_eq!(split_statements("SELECT 1; SELECT 2").len(), 2);
3545        assert_eq!(split_statements("SELECT ';'").len(), 1);
3546        assert_eq!(split_statements("SELECT 1;").len(), 1);
3547        assert_eq!(split_statements("   ").len(), 0);
3548    }
3549
3550    #[test]
3551    fn an_error_names_its_sqlstate() {
3552        let e = String::from_utf8_lossy(&err_msg("0A000", "x")).to_string();
3553        assert!(e.contains("ERROR"));
3554        assert!(e.contains("0A000"));
3555    }
3556
3557    // ── the extended query protocol ─────────────────────────────────────────
3558
3559    #[test]
3560    fn placeholders_are_counted_outside_string_literals() {
3561        assert_eq!(param_count("SELECT a FROM t WHERE b = $1 AND c = $2"), 2);
3562        assert_eq!(param_count("SELECT a FROM t"), 0);
3563        // The highest index wins, because a parameter may be reused.
3564        assert_eq!(param_count("WHERE a = $2 OR b = $2 OR c = $1"), 2);
3565        assert_eq!(param_count("SELECT a FROM t WHERE b = '$1'"), 0,
3566                   "a placeholder inside a literal is data, not a parameter");
3567        assert_eq!(param_count("WHERE a = $10 AND b = $1"), 10,
3568                   "two-digit indexes must not be read as $1 followed by 0");
3569    }
3570
3571    #[test]
3572    fn parameters_are_spliced_as_literals() {
3573        let out = substitute_params("WHERE a = $1 AND b = $2 AND c = $3",
3574            &[Some("'x'".into()), Some("42".into()), None]).unwrap();
3575        assert_eq!(out, "WHERE a = 'x' AND b = 42 AND c = NULL");
3576    }
3577
3578    #[test]
3579    fn substitution_leaves_string_literals_alone() {
3580        let out = substitute_params("WHERE a = '$1' AND b = $1", &[Some("9".into())]).unwrap();
3581        assert_eq!(out, "WHERE a = '$1' AND b = 9");
3582    }
3583
3584    #[test]
3585    fn too_few_parameters_is_an_error_not_a_silent_null() {
3586        // The alternative — treating a missing parameter as NULL — turns a
3587        // client bug into a wrong answer with a 200-shaped response.
3588        let e = substitute_params("WHERE a = $2", &[Some("1".into())]).unwrap_err();
3589        assert!(e.contains("$2"), "{}", e);
3590    }
3591
3592    #[test]
3593    fn a_quote_in_a_parameter_cannot_escape_its_literal() {
3594        let lit = decode_param(Some(b"it's"), OID_TEXT, 0).unwrap().unwrap();
3595        assert_eq!(lit, "'it''s'");
3596        // And it survives a round trip through the splice unchanged.
3597        let out = substitute_params("WHERE a = $1", &[Some(lit)]).unwrap();
3598        assert_eq!(out, "WHERE a = 'it''s'");
3599    }
3600
3601    #[test]
3602    fn binary_parameters_decode_in_every_width_psycopg_sends() {
3603        // These are the exact encodings read off a psycopg3 wire transcript:
3604        // a small int arrives as int2, a float as float8, a bool as one byte.
3605        assert_eq!(decode_param(Some(&[0x00, 0x2a]), OID_INT2, 1).unwrap().unwrap(), "42");
3606        assert_eq!(decode_param(Some(&[0, 0, 0, 7]), OID_INT4, 1).unwrap().unwrap(), "7");
3607        assert_eq!(
3608            decode_param(Some(&[0, 0, 0, 0, 0, 0, 0, 9]), OID_INT8, 1).unwrap().unwrap(), "9");
3609        assert_eq!(
3610            decode_param(Some(&0x400c_0000_0000_0000u64.to_be_bytes()), OID_FLOAT8, 1)
3611                .unwrap().unwrap(), "3.5");
3612        assert_eq!(decode_param(Some(&[1]), OID_BOOL, 1).unwrap().unwrap(), "TRUE");
3613        assert_eq!(decode_param(Some(&[0]), OID_BOOL, 1).unwrap().unwrap(), "FALSE");
3614    }
3615
3616    #[test]
3617    fn a_negative_binary_integer_keeps_its_sign() {
3618        assert_eq!(decode_param(Some(&(-5i32).to_be_bytes()), OID_INT4, 1).unwrap().unwrap(), "-5");
3619        assert_eq!(decode_param(Some(&(-5i16).to_be_bytes()), OID_INT2, 1).unwrap().unwrap(), "-5");
3620    }
3621
3622    #[test]
3623    fn a_binary_parameter_of_the_wrong_width_is_refused() {
3624        // Truncating or zero-extending would produce a plausible wrong number,
3625        // which is the failure mode worth engineering against.
3626        let e = decode_param(Some(&[0x2a]), OID_INT4, 1).unwrap_err();
3627        assert!(e.contains("4 bytes"), "{}", e);
3628    }
3629
3630    #[test]
3631    fn an_unspecified_text_parameter_is_treated_as_a_string() {
3632        // psycopg3 declares OID 0 only for `str`; every number it sends carries
3633        // a real numeric OID. So quoting here is grounded, not a guess.
3634        assert_eq!(decode_param(Some(b"hello"), 0, 0).unwrap().unwrap(), "'hello'");
3635    }
3636
3637    #[test]
3638    fn a_null_parameter_decodes_to_none_in_every_format() {
3639        assert_eq!(decode_param(None, OID_TEXT, 0).unwrap(), None);
3640        assert_eq!(decode_param(None, OID_INT8, 1).unwrap(), None);
3641    }
3642
3643    #[test]
3644    fn an_unsupported_binary_type_says_so_by_name() {
3645        let e = decode_param(Some(&[0u8; 8]), 1114, 1).unwrap_err();
3646        assert!(e.contains("1114"), "{}", e);
3647        assert!(e.contains("text"), "the error should point at the way out: {}", e);
3648    }
3649
3650    #[test]
3651    fn a_text_number_that_is_not_a_number_gets_quoted() {
3652        // Splicing it in bare would emit a naked identifier into the NQL text
3653        // and fail somewhere far away from the cause.
3654        assert_eq!(decode_param(Some(b"oops"), OID_INT8, 0).unwrap().unwrap(), "'oops'");
3655    }
3656
3657    #[test]
3658    fn a_client_declared_type_is_believed_over_inference() {
3659        // The client is about to encode its argument that way; overriding it
3660        // would break the decode.
3661        let oids = infer_param_oids("SELECT a FROM t WHERE b = $1 AND c = $2", &[OID_INT4, 0], None);
3662        assert_eq!(oids, vec![OID_INT4, OID_TEXT]);
3663    }
3664
3665    #[test]
3666    fn parameter_arity_is_taken_from_the_sql_when_the_client_declares_none() {
3667        // asyncpg declares nothing and then refuses the call if the count that
3668        // comes back is wrong, so this is the load-bearing path for it.
3669        let oids = infer_param_oids("SELECT a FROM t WHERE b = $1 AND c = $2", &[], None);
3670        assert_eq!(oids.len(), 2);
3671    }
3672
3673    #[test]
3674    fn the_field_behind_each_placeholder_is_identified() {
3675        assert_eq!(
3676            param_fields("SELECT a FROM t WHERE qty > $1 AND status = $2", 2),
3677            vec![Some("qty".to_string()), Some("status".to_string())]);
3678    }
3679
3680    #[test]
3681    fn word_operators_do_not_hide_the_field() {
3682        assert_eq!(param_fields("SELECT a FROM t WHERE name LIKE $1", 1),
3683                   vec![Some("name".to_string())]);
3684        assert_eq!(param_fields("SELECT a FROM t WHERE qty BETWEEN $1 AND $2", 2),
3685                   vec![Some("qty".to_string()), Some("qty".to_string())]);
3686        assert_eq!(param_fields("SELECT a FROM t WHERE region IN ($1, $2)", 2),
3687                   vec![Some("region".to_string()), Some("region".to_string())]);
3688    }
3689
3690    #[test]
3691    fn a_clause_position_types_from_the_grammar_not_from_a_column() {
3692        // `AS OF SYSTEM TIME $1` has no column beside it — the token to its
3693        // left is the word TIME. Typing it text made asyncpg refuse to send
3694        // the sequence number at all.
3695        assert_eq!(
3696            infer_param_oids("SELECT a FROM t AS OF SYSTEM TIME $1 WHERE b = $2", &[], None),
3697            vec![OID_INT8, OID_TEXT]);
3698        assert_eq!(infer_param_oids("SELECT a FROM t AS OF $1", &[], None), vec![OID_INT8]);
3699        // VALID AS OF also ends with "AS OF", but its argument is a DATE
3700        // STRING. Checking the longer clause first is load-bearing.
3701        assert_eq!(
3702            infer_param_oids("SELECT a FROM t VALID AS OF $1", &[], None), vec![OID_TEXT]);
3703        assert_eq!(
3704            infer_param_oids("SELECT a FROM t LIMIT $1 OFFSET $2", &[], None),
3705            vec![OID_INT8, OID_INT8]);
3706    }
3707
3708    #[test]
3709    fn an_aggregate_column_types_from_what_the_aggregate_means() {
3710        // No document holds a field called `count`, so sampling stored data
3711        // finds nothing and falls back to text — which hands a binary client
3712        // the string "2" for COUNT(*).
3713        assert_eq!(aggregate_oid("count", None, "t"), Some(OID_INT8));
3714        assert_eq!(aggregate_oid("avg_fee", None, "t"), Some(OID_FLOAT8),
3715                   "an average is fractional even over integers");
3716        // SUM/MIN/MAX inherit the field's type; with no database to sample,
3717        // that resolves to text, and `_seq` is known from the engine contract.
3718        assert_eq!(aggregate_oid("max__seq", None, "t"), Some(OID_INT8));
3719        assert_eq!(aggregate_oid("total", None, "t"), None, "not an aggregate");
3720    }
3721
3722    #[test]
3723    fn the_parse_probe_uses_a_literal_that_every_clause_accepts() {
3724        // Stubbing with NULL was the obvious choice and the wrong one: clauses
3725        // that validate their argument rejected it, so `AS OF SYSTEM TIME $1`
3726        // failed at Parse before a real sequence was ever bound.
3727        let probe = probe_sql("SELECT a FROM t AS OF SYSTEM TIME $1 WHERE b = $2", 2);
3728        assert!(!probe.contains("NULL"), "{}", probe);
3729        assert!(translate(&probe).is_ok(), "the probe must parse: {}", probe);
3730    }
3731
3732    #[test]
3733    fn a_column_with_mixed_types_across_documents_is_advertised_as_text() {
3734        // Taking the first non-null value's type told the client `int8` and
3735        // then sent it "n/a" — which fails to parse client-side, and on the
3736        // binary path cannot be encoded at all.
3737        let rows = vec![json!({"x": 3}), json!({"x": "n/a"})];
3738        assert_eq!(oid_for(&rows, "x"), OID_TEXT);
3739        // Integers and floats in one column widen rather than conflict.
3740        let rows = vec![json!({"x": 3}), json!({"x": 1.5})];
3741        assert_eq!(oid_for(&rows, "x"), OID_FLOAT8);
3742        // A leading null must not decide the type.
3743        let rows = vec![json!({"x": Value::Null}), json!({"x": 7})];
3744        assert_eq!(oid_for(&rows, "x"), OID_INT8);
3745    }
3746
3747    #[test]
3748    fn binary_output_encodes_each_advertised_type() {
3749        assert_eq!(cell_binary(Some(&json!(true)), OID_BOOL).unwrap().unwrap(), vec![1]);
3750        assert_eq!(cell_binary(Some(&json!(42)), OID_INT8).unwrap().unwrap(),
3751                   42i64.to_be_bytes().to_vec());
3752        assert_eq!(cell_binary(Some(&json!(3.5)), OID_FLOAT8).unwrap().unwrap(),
3753                   3.5f64.to_be_bytes().to_vec());
3754        // For the text family, binary and text are the same bytes.
3755        assert_eq!(cell_binary(Some(&json!("hi")), OID_TEXT).unwrap().unwrap(), b"hi".to_vec());
3756        assert_eq!(cell_binary(Some(&Value::Null), OID_INT8).unwrap(), None);
3757        // A boolean renders as `t`/`f` in text but one byte in binary.
3758        assert_eq!(cell(Some(&json!(true))).unwrap(), "t");
3759    }
3760
3761    #[test]
3762    fn a_value_that_does_not_fit_its_advertised_binary_type_is_refused() {
3763        // Advertised types come from a bounded sample, so a field that only
3764        // turns heterogeneous outside it lands here. Sending a zero, or the
3765        // text bytes under a binary header, would corrupt the value in a way
3766        // the client cannot detect — so it is an error instead.
3767        let e = cell_binary(Some(&json!("nope")), OID_INT8).unwrap_err();
3768        assert!(e.contains("a string"), "{}", e);
3769        assert!(e.contains("more than one type"), "the error should explain WHY: {}", e);
3770    }
3771
3772    #[test]
3773    fn a_row_description_carries_the_requested_format_per_column() {
3774        let cols = [Col::same("a"), Col::same("b")];
3775        let m = row_description_fmt(&cols, &[OID_INT8, OID_TEXT], &[1, 0]);
3776        assert_eq!(m[0], b'T');
3777        // The trailing i16 of each field entry is its format code.
3778        assert_eq!(m[m.len() - 1], 0, "the last column was requested as text");
3779    }
3780
3781    #[test]
3782    fn a_qualified_column_resolves_to_its_bare_name() {
3783        assert_eq!(param_fields("SELECT a FROM t WHERE t.qty = $1", 1),
3784                   vec![Some("qty".to_string())]);
3785    }
3786
3787    #[test]
3788    fn insert_placeholders_map_positionally_to_the_column_list() {
3789        assert_eq!(
3790            param_fields("INSERT INTO t (_id, qty, status) VALUES ($1, $2, $3)", 3),
3791            vec![Some("_id".to_string()), Some("qty".to_string()), Some("status".to_string())]);
3792    }
3793
3794    #[test]
3795    fn a_set_clause_placeholder_finds_its_column() {
3796        assert_eq!(param_fields("UPDATE t SET status = $1 WHERE _id = $2", 2),
3797                   vec![Some("status".to_string()), Some("_id".to_string())]);
3798    }
3799
3800    #[test]
3801    fn the_target_collection_is_found_for_every_statement_kind() {
3802        assert_eq!(stmt_collection("SELECT a FROM inv WHERE b = $1"), "inv");
3803        assert_eq!(stmt_collection("UPDATE inv SET a = $1"), "inv");
3804        assert_eq!(stmt_collection("DELETE FROM inv WHERE a = $1"), "inv");
3805        assert_eq!(stmt_collection("INSERT INTO inv (a) VALUES ($1)"), "inv");
3806        // Clients qualify as schema.table; NEDB has one namespace.
3807        assert_eq!(stmt_collection("SELECT a FROM public.inv"), "inv");
3808        assert_eq!(stmt_collection("INSERT INTO inv(a) VALUES ($1)"), "inv");
3809    }
3810
3811    #[test]
3812    fn engine_metadata_fields_type_without_touching_storage() {
3813        assert_eq!(infer_field_oid(None, "t", "_seq"), OID_INT8);
3814        assert_eq!(infer_field_oid(None, "t", "_id"), OID_TEXT);
3815    }
3816
3817    #[test]
3818    fn the_protocol_acknowledgements_are_single_empty_messages() {
3819        // Each is a tag plus a 4-byte length of exactly 4.
3820        for (m, tag) in [
3821            (parse_complete(), b'1'), (bind_complete(), b'2'),
3822            (close_complete(), b'3'), (no_data(), b'n'), (portal_suspended(), b's'),
3823        ] {
3824            assert_eq!(m.len(), 5, "{:?}", tag as char);
3825            assert_eq!(m[0], tag);
3826            assert_eq!(i32::from_be_bytes([m[1], m[2], m[3], m[4]]), 4);
3827        }
3828    }
3829
3830    #[test]
3831    fn parameter_description_reports_its_arity_and_types() {
3832        let m = parameter_description(&[OID_TEXT, OID_INT8]);
3833        assert_eq!(m[0], b't');
3834        assert_eq!(i16::from_be_bytes([m[5], m[6]]), 2);
3835        assert_eq!(i32::from_be_bytes([m[7], m[8], m[9], m[10]]), OID_TEXT);
3836        assert_eq!(i32::from_be_bytes([m[11], m[12], m[13], m[14]]), OID_INT8);
3837    }
3838
3839    #[test]
3840    fn a_cstring_is_taken_without_its_terminator() {
3841        let body = b"one\0two\0".to_vec();
3842        let mut at = 0usize;
3843        assert_eq!(take_cstr(&body, &mut at), "one");
3844        assert_eq!(take_cstr(&body, &mut at), "two");
3845        assert_eq!(at, body.len());
3846    }
3847
3848    #[test]
3849    fn truncated_integers_are_reported_rather_than_read_past_the_end() {
3850        let body = vec![0u8, 1];
3851        let mut at = 0usize;
3852        assert!(take_i32(&body, &mut at).is_err());
3853        let mut at = 0usize;
3854        assert!(take_i16(&body, &mut at).is_ok());
3855    }
3856
3857    #[test]
3858    fn a_binary_result_format_request_is_refused_rather_than_faked() {
3859        // Sending text under a binary header corrupts every value silently,
3860        // which is far worse than an error naming the limitation.
3861        let out = encode_rows(&[], &[Col::same("a")]);
3862        let desc_format = &out[out.len() - 2..];
3863        assert_eq!(i16::from_be_bytes([desc_format[0], desc_format[1]]), 0,
3864                   "every column is advertised as text format");
3865    }
3866
3867    #[test]
3868    fn a_float_parameter_does_not_render_as_rust_infinity() {
3869        assert_eq!(fmt_float(f64::INFINITY), "'Infinity'");
3870        assert_eq!(fmt_float(f64::NEG_INFINITY), "'-Infinity'");
3871        assert_eq!(fmt_float(f64::NAN), "'NaN'");
3872        assert_eq!(fmt_float(3.0), "3", "a whole float should not gain a .0 tail");
3873        assert_eq!(fmt_float(3.5), "3.5");
3874    }
3875}