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}