Skip to main content

nedb_engine/
neql.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//! **neQL** — the whole language: NQL *and* PostgreSQL SQL.
6//!
7//! Not a third dialect. neQL 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 neQL a statement is written in.
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 statement in either half of neQL\n  \
82         NQL statements begin with: {}\n  \
83         SQL statements begin with: {}",
84        head,
85        NQL_HEADS.join(", "),
86        SQL_HEADS.join(", "),
87    ))
88}
89
90#[cfg(test)]
91mod tests {
92    use super::*;
93
94    #[test]
95    fn the_two_vocabularies_do_not_overlap() {
96        // The whole no-guessing argument rests on this. If a word ever appears
97        // in both lists, routing becomes a coin flip and neQL starts lying
98        // about being total.
99        for n in NQL_HEADS {
100            assert!(
101                !SQL_HEADS.contains(n),
102                "{:?} begins a statement in both dialects — routing is no longer structural",
103                n
104            );
105        }
106    }
107
108    #[test]
109    fn each_dialect_routes_to_itself() {
110        assert_eq!(route("FROM orders").unwrap(), Dialect::Nql);
111        assert_eq!(route("from orders WHERE x = 1").unwrap(), Dialect::Nql);
112        assert_eq!(route("SELECT * FROM orders").unwrap(), Dialect::Sql);
113        assert_eq!(route("  explain select 1").unwrap(), Dialect::Sql);
114        assert_eq!(route("(SELECT 1) UNION (SELECT 2)").unwrap(), Dialect::Sql);
115        assert_eq!(route("INSERT INTO o VALUES (1)").unwrap(), Dialect::Sql);
116    }
117
118    #[test]
119    fn a_word_in_neither_vocabulary_is_refused_naming_both() {
120        let e = route("GRANT ALL ON orders").unwrap_err();
121        assert!(e.contains("GRANT"), "{}", e);
122        assert!(e.contains("FROM"), "the refusal must name the NQL vocabulary: {}", e);
123        assert!(e.contains("SELECT"), "and the SQL one: {}", e);
124    }
125
126    #[test]
127    fn an_empty_statement_is_refused_rather_than_routed() {
128        assert!(route("").is_err());
129        assert!(route("   \n ").is_err());
130    }
131
132    /// The reason this module is in the engine rather than in the CLI.
133    #[test]
134    fn a_sql_statement_is_never_handed_to_the_nql_parser() {
135        // `POST /query {"nql": "SELECT ..."}` is the case that motivated
136        // this: the field is called `nql` for compatibility, and its contents
137        // are no longer required to be NQL.
138        assert_eq!(route("SELECT who FROM orders").unwrap(), Dialect::Sql);
139        assert_eq!(route("FROM orders").unwrap(), Dialect::Nql);
140    }
141}