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