Skip to main content

nedb_engine/
pgwire.rs

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