Skip to main content

nedb_engine/
nesql.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//! **neSQL** — the whole language: NQL *and* PostgreSQL SQL.
6//!
7//! Not a third dialect. neSQL is the name for the pair, and this module is the
8//! one place that decides which half a statement is written in.
9//!
10//! # Why the router lives in the engine
11//!
12//! `nesql-cli` had this logic first, and it was correct there. Putting a
13//! second copy in the HTTP server would have been the same mistake this whole
14//! effort exists to undo: two implementations of one decision, drifting until
15//! `POST /query` and `nesql query` disagree about what a statement means —
16//! and disagreeing about the MEANING of a statement is worse than disagreeing
17//! about its result, because nothing looks broken.
18//!
19//! So it moved down here, where both the daemon and the CLI can reach it, and
20//! the CLI re-exports it rather than keeping its own.
21//!
22//! # Routing is structural, so nothing is guessed
23//!
24//! ```text
25//! NQL  begins with FROM
26//! SQL  begins with SELECT INSERT UPDATE DELETE EXPLAIN WITH SHOW SET
27//!                  VALUES TABLE BEGIN COMMIT ROLLBACK
28//! ```
29//!
30//! PostgreSQL has no statement form that begins with `FROM`, so the leading
31//! keyword PARTITIONS the two vocabularies rather than hinting at them. That
32//! is what makes this a decision rather than a heuristic, and it is why a
33//! first word in neither is REFUSED naming both — never handed to whichever
34//! parser seems likelier, because "seems likelier" is the guess the rule
35//! forbids.
36
37/// Which half of neSQL a statement is written in — inherited PostgreSQL, or NQL.
38#[derive(Debug, Clone, Copy, PartialEq, Eq)]
39pub enum Dialect {
40    Nql,
41    Sql,
42}
43
44impl Dialect {
45    pub fn name(self) -> &'static str {
46        match self {
47            Dialect::Nql => "nql",
48            Dialect::Sql => "sql",
49        }
50    }
51}
52
53/// Statement-initial keywords, per dialect. Disjoint by construction; the test
54/// `the_two_vocabularies_do_not_overlap` holds them that way.
55pub const NQL_HEADS: &[&str] = &["FROM"];
56pub const SQL_HEADS: &[&str] = &[
57    "SELECT", "INSERT", "UPDATE", "DELETE", "EXPLAIN", "WITH", "SHOW", "SET",
58    "VALUES", "TABLE", "BEGIN", "COMMIT", "ROLLBACK",
59];
60
61fn first_word(s: &str) -> Option<String> {
62    s.split_whitespace()
63        .next()
64        // A statement may open with a parenthesis — `(SELECT …) UNION …`.
65        .map(|w| w.trim_start_matches('(').trim_end_matches(';').to_uppercase())
66        .filter(|w| !w.is_empty())
67}
68
69/// Decide which half a statement is written in, or refuse.
70pub fn route(q: &str) -> Result<Dialect, String> {
71    let Some(head) = first_word(q) else {
72        return Err("the statement is empty".to_string());
73    };
74    if NQL_HEADS.contains(&head.as_str()) {
75        return Ok(Dialect::Nql);
76    }
77    if SQL_HEADS.contains(&head.as_str()) {
78        return Ok(Dialect::Sql);
79    }
80    Err(format!(
81        "{:?} does not begin a neSQL statement.\n\
82         neSQL is PostgreSQL's SQL plus NEDB's own clauses, so a statement starts\n\
83         in one of these two vocabularies:\n  \
84         NQL form begins with: {}\n  \
85         SQL form begins with: {}",
86        head,
87        NQL_HEADS.join(", "),
88        SQL_HEADS.join(", "),
89    ))
90}
91
92/// Run a neSQL statement, routing on the leading keyword.
93///
94/// The one place in the engine that turns "here is a statement" into rows
95/// regardless of which half it is written in. `POST /query`, `/subscribe` and
96/// the language bindings all come through here, so "NEDB speaks NQL and SQL"
97/// is one function rather than a property each caller has to remember to
98/// implement.
99///
100/// Returns the error TEXT rather than a typed error because every caller
101/// surfaces it to a client as a string, and a bespoke error enum here would be
102/// converted back to a string at each of them.
103pub fn run(db: &std::sync::Arc<crate::db::Db>, statement: &str)
104    -> Result<Vec<serde_json::Value>, String>
105{
106    match route(statement)? {
107        Dialect::Nql => crate::nql::query(db, statement)
108            .map(|(rows, _)| rows)
109            .map_err(|e| e.to_string()),
110        Dialect::Sql => crate::pgwire::execute_sql(db, statement, false)
111            .map(|done| done.rows),
112    }
113}
114
115#[cfg(test)]
116mod tests {
117    use super::*;
118
119    #[test]
120    fn the_two_vocabularies_do_not_overlap() {
121        // The whole no-guessing argument rests on this. If a word ever appears
122        // in both lists, routing becomes a coin flip and neSQL starts lying
123        // about being total.
124        for n in NQL_HEADS {
125            assert!(
126                !SQL_HEADS.contains(n),
127                "{:?} begins a statement in both dialects — routing is no longer structural",
128                n
129            );
130        }
131    }
132
133    #[test]
134    fn each_dialect_routes_to_itself() {
135        assert_eq!(route("FROM orders").unwrap(), Dialect::Nql);
136        assert_eq!(route("from orders WHERE x = 1").unwrap(), Dialect::Nql);
137        assert_eq!(route("SELECT * FROM orders").unwrap(), Dialect::Sql);
138        assert_eq!(route("  explain select 1").unwrap(), Dialect::Sql);
139        assert_eq!(route("(SELECT 1) UNION (SELECT 2)").unwrap(), Dialect::Sql);
140        assert_eq!(route("INSERT INTO o VALUES (1)").unwrap(), Dialect::Sql);
141    }
142
143    #[test]
144    fn a_word_in_neither_vocabulary_is_refused_naming_both() {
145        let e = route("GRANT ALL ON orders").unwrap_err();
146        assert!(e.contains("GRANT"), "{}", e);
147        assert!(e.contains("FROM"), "the refusal must name the NQL vocabulary: {}", e);
148        assert!(e.contains("SELECT"), "and the SQL one: {}", e);
149    }
150
151    #[test]
152    fn an_empty_statement_is_refused_rather_than_routed() {
153        assert!(route("").is_err());
154        assert!(route("   \n ").is_err());
155    }
156
157    /// The reason this module is in the engine rather than in the CLI.
158    #[test]
159    fn a_sql_statement_is_never_handed_to_the_nql_parser() {
160        // `POST /query {"nql": "SELECT ..."}` is the case that motivated
161        // this: the field is called `nql` for compatibility, and its contents
162        // are no longer required to be NQL.
163        assert_eq!(route("SELECT who FROM orders").unwrap(), Dialect::Sql);
164        assert_eq!(route("FROM orders").unwrap(), Dialect::Nql);
165    }
166}