Skip to main content

nedb_engine/
pgwire.rs

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