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}