Skip to main content

sochdb_query/
soch_ql.rs

1// SPDX-License-Identifier: AGPL-3.0-or-later
2// SochDB - LLM-Optimized Embedded Database
3// Copyright (C) 2026 Sushanth Reddy Vanagala (https://github.com/sushanthpy)
4//
5// This program is free software: you can redistribute it and/or modify
6// it under the terms of the GNU Affero General Public License as published by
7// the Free Software Foundation, either version 3 of the License, or
8// (at your option) any later version.
9//
10// This program is distributed in the hope that it will be useful,
11// but WITHOUT ANY WARRANTY; without even the implied warranty of
12// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13// GNU Affero General Public License for more details.
14//
15// You should have received a copy of the GNU Affero General Public License
16// along with this program. If not, see <https://www.gnu.org/licenses/>.
17
18//! TOON Query Language (SOCH-QL)
19//!
20//! SQL-like query language for TOON-native data.
21//!
22//! ## Query Syntax
23//!
24//! ```text
25//! -- Schema Definition (DDL)
26//! CREATE TABLE users {
27//!   id: u64 PRIMARY KEY,
28//!   name: text NOT NULL,
29//!   email: text UNIQUE,
30//!   score: f64 DEFAULT 0.0
31//! }
32//!
33//! -- Data Manipulation (DML) - TOON in, TOON out
34//! INSERT users:
35//! id: 1
36//! name: Alice
37//! email: alice@example.com
38//!
39//! -- Queries return TOON
40//! SELECT id,name FROM users WHERE score > 80
41//! → users[2]{id,name}:
42//!   1,Alice
43//!   3,Charlie
44//! ```
45
46/// A parsed SOCH-QL query
47#[derive(Debug, Clone)]
48pub enum SochQuery {
49    /// SELECT query
50    Select(SelectQuery),
51    /// INSERT query  
52    Insert(InsertQuery),
53    /// CREATE TABLE query
54    CreateTable(CreateTableQuery),
55    /// DROP TABLE query
56    DropTable { table: String },
57}
58
59/// SELECT query
60#[derive(Debug, Clone)]
61pub struct SelectQuery {
62    /// Columns to select (* means all)
63    pub columns: Vec<String>,
64    /// Table to query
65    pub table: String,
66    /// WHERE clause conditions
67    pub where_clause: Option<WhereClause>,
68    /// ORDER BY clause
69    pub order_by: Option<OrderBy>,
70    /// LIMIT clause
71    pub limit: Option<usize>,
72    /// OFFSET clause
73    pub offset: Option<usize>,
74}
75
76/// INSERT query
77#[derive(Debug, Clone)]
78pub struct InsertQuery {
79    /// Target table
80    pub table: String,
81    /// Columns to insert
82    pub columns: Vec<String>,
83    /// Rows of values
84    pub rows: Vec<Vec<SochValue>>,
85}
86
87/// CREATE TABLE query
88#[derive(Debug, Clone)]
89pub struct CreateTableQuery {
90    /// Table name
91    pub table: String,
92    /// Column definitions
93    pub columns: Vec<ColumnDef>,
94    /// Primary key column
95    pub primary_key: Option<String>,
96}
97
98/// Column definition for CREATE TABLE
99#[derive(Debug, Clone)]
100pub struct ColumnDef {
101    /// Column name
102    pub name: String,
103    /// Column type
104    pub col_type: ColumnType,
105    /// NOT NULL constraint
106    pub not_null: bool,
107    /// UNIQUE constraint
108    pub unique: bool,
109    /// Default value
110    pub default: Option<SochValue>,
111}
112
113/// Column type
114#[derive(Debug, Clone, Copy, PartialEq, Eq)]
115pub enum ColumnType {
116    Bool,
117    Int64,
118    UInt64,
119    Float64,
120    Text,
121    Binary,
122    Timestamp,
123}
124
125/// A value in TOON format
126#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
127pub enum SochValue {
128    Null,
129    Bool(bool),
130    Int(i64),
131    UInt(u64),
132    Float(f64),
133    Text(String),
134    Binary(Vec<u8>),
135    Array(Vec<SochValue>),
136}
137
138impl SochValue {
139    /// Format as TOON string
140    pub fn to_soch_string(&self) -> String {
141        match self {
142            SochValue::Null => "null".to_string(),
143            SochValue::Bool(b) => b.to_string(),
144            SochValue::Int(i) => i.to_string(),
145            SochValue::UInt(u) => u.to_string(),
146            SochValue::Float(f) => format!("{:.2}", f),
147            SochValue::Text(s) => s.clone(),
148            SochValue::Binary(b) => {
149                let hex_str: String = b.iter().map(|byte| format!("{:02x}", byte)).collect();
150                format!("0x{}", hex_str)
151            }
152            SochValue::Array(arr) => {
153                let items: Vec<String> = arr.iter().map(|v| v.to_soch_string()).collect();
154                format!("[{}]", items.join(","))
155            }
156        }
157    }
158}
159
160/// WHERE clause
161#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
162pub struct WhereClause {
163    pub conditions: Vec<Condition>,
164    pub operator: LogicalOp,
165}
166
167/// Logical operator
168#[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize)]
169pub enum LogicalOp {
170    And,
171    Or,
172}
173
174/// A condition in WHERE clause
175#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
176pub struct Condition {
177    pub column: String,
178    pub operator: ComparisonOp,
179    pub value: SochValue,
180}
181
182/// Comparison operator
183#[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize)]
184pub enum ComparisonOp {
185    Eq,
186    Ne,
187    Lt,
188    Le,
189    Gt,
190    Ge,
191    Like,
192    In,
193    /// Vector similarity search: `column SIMILAR TO 'query text'`
194    /// The value should be the query text to embed and search for.
195    SimilarTo,
196}
197
198/// ORDER BY clause
199#[derive(Debug, Clone)]
200pub struct OrderBy {
201    pub column: String,
202    pub direction: SortDirection,
203}
204
205/// Sort direction
206#[derive(Debug, Clone, Copy, PartialEq, Eq)]
207pub enum SortDirection {
208    Asc,
209    Desc,
210}
211
212/// Query result in TOON format
213#[derive(Debug, Clone)]
214pub struct SochResult {
215    /// Table name
216    pub table: String,
217    /// Column names
218    pub columns: Vec<String>,
219    /// Rows of values
220    pub rows: Vec<Vec<SochValue>>,
221}
222
223impl SochResult {
224    /// Format as TOON string
225    ///
226    /// Example output:
227    /// ```text
228    /// users[2]{id,name}:
229    /// 1,Alice
230    /// 2,Bob
231    /// ```
232    pub fn to_soch_string(&self) -> String {
233        let mut result = String::new();
234
235        // Header: table[row_count]{columns}:
236        result.push_str(&format!(
237            "{}[{}]{{{}}}:\n",
238            self.table,
239            self.rows.len(),
240            self.columns.join(",")
241        ));
242
243        // Data rows
244        for row in &self.rows {
245            let values: Vec<String> = row.iter().map(|v| v.to_soch_string()).collect();
246            result.push_str(&values.join(","));
247            result.push('\n');
248        }
249
250        result
251    }
252
253    /// Number of rows
254    pub fn row_count(&self) -> usize {
255        self.rows.len()
256    }
257
258    /// Number of columns
259    pub fn column_count(&self) -> usize {
260        self.columns.len()
261    }
262
263    /// Get a value by row and column index
264    pub fn get(&self, row: usize, col: usize) -> Option<&SochValue> {
265        self.rows.get(row)?.get(col)
266    }
267}
268
269/// Simple SOCH-QL parser
270pub struct SochQlParser;
271
272impl SochQlParser {
273    /// Parse a SOCH-QL query string
274    pub fn parse(query: &str) -> Result<SochQuery, ParseError> {
275        let query = query.trim();
276
277        if query.to_uppercase().starts_with("SELECT") {
278            Self::parse_select(query)
279        } else if query.to_uppercase().starts_with("INSERT") {
280            Self::parse_insert(query)
281        } else if query.to_uppercase().starts_with("CREATE TABLE") {
282            Self::parse_create_table(query)
283        } else if query.to_uppercase().starts_with("DROP TABLE") {
284            Self::parse_drop_table(query)
285        } else {
286            Err(ParseError::UnknownStatement)
287        }
288    }
289
290    fn parse_select(query: &str) -> Result<SochQuery, ParseError> {
291        // Simple SELECT parser
292        // SELECT col1, col2 FROM table WHERE condition ORDER BY col LIMIT n
293        let query_upper = query.to_uppercase();
294
295        // Extract columns
296        let from_idx = query_upper.find("FROM").ok_or(ParseError::MissingFrom)?;
297        let columns_str = &query[6..from_idx].trim();
298        let columns: Vec<String> = if columns_str == &"*" {
299            vec!["*".to_string()]
300        } else {
301            columns_str
302                .split(',')
303                .map(|s| s.trim().to_string())
304                .collect()
305        };
306
307        // Extract table name
308        let after_from = &query[from_idx + 4..].trim();
309        let table_end = after_from
310            .find(|c: char| c.is_whitespace())
311            .unwrap_or(after_from.len());
312        let table = after_from[..table_end].to_string();
313
314        // Parse WHERE clause
315        let where_clause = Self::parse_where_clause(&query_upper, query)?;
316
317        let order_by = None;
318        let limit = None;
319        let offset = None;
320
321        Ok(SochQuery::Select(SelectQuery {
322            columns,
323            table,
324            where_clause,
325            order_by,
326            limit,
327            offset,
328        }))
329    }
330
331    /// Parse WHERE clause from query
332    fn parse_where_clause(
333        query_upper: &str,
334        original: &str,
335    ) -> Result<Option<WhereClause>, ParseError> {
336        let where_idx = match query_upper.find("WHERE") {
337            Some(idx) => idx,
338            None => return Ok(None),
339        };
340
341        // Find the end of WHERE clause (ORDER BY, LIMIT, or end of string)
342        let after_where = &original[where_idx + 5..].trim();
343        let clause_end = after_where
344            .to_uppercase()
345            .find("ORDER BY")
346            .or_else(|| after_where.to_uppercase().find("LIMIT"))
347            .unwrap_or(after_where.len());
348
349        let condition_str = after_where[..clause_end].trim();
350
351        // Parse condition: column op value
352        // Supported operators: =, !=, <, <=, >, >=, LIKE, IN
353        let (column, operator, value) = Self::parse_condition(condition_str)?;
354
355        Ok(Some(WhereClause {
356            conditions: vec![Condition {
357                column,
358                operator,
359                value,
360            }],
361            operator: LogicalOp::And, // Default to AND for single condition
362        }))
363    }
364
365    /// Parse a single condition: field op value
366    fn parse_condition(condition: &str) -> Result<(String, ComparisonOp, SochValue), ParseError> {
367        let condition_upper = condition.to_uppercase();
368
369        // Check for IN operator first (contains space)
370        if let Some(in_idx) = condition_upper.find(" IN ") {
371            let field = condition[..in_idx].trim().to_string();
372            let values_str = condition[in_idx + 4..].trim();
373            // Parse (val1, val2, val3)
374            let values = Self::parse_in_values(values_str)?;
375            return Ok((field, ComparisonOp::In, values));
376        }
377
378        // Check for LIKE operator
379        if let Some(like_idx) = condition_upper.find(" LIKE ") {
380            let field = condition[..like_idx].trim().to_string();
381            let pattern = condition[like_idx + 6..].trim();
382            let value = Self::parse_value(pattern)?;
383            return Ok((field, ComparisonOp::Like, value));
384        }
385
386        // Check comparison operators (in order of length)
387        let operators = [
388            ("!=", ComparisonOp::Ne),
389            ("<=", ComparisonOp::Le),
390            (">=", ComparisonOp::Ge),
391            ("<>", ComparisonOp::Ne),
392            ("=", ComparisonOp::Eq),
393            ("<", ComparisonOp::Lt),
394            (">", ComparisonOp::Gt),
395        ];
396
397        for (op_str, op) in operators {
398            if let Some(op_idx) = condition.find(op_str) {
399                let field = condition[..op_idx].trim().to_string();
400                let value_str = condition[op_idx + op_str.len()..].trim();
401                let value = Self::parse_value(value_str)?;
402                return Ok((field, op, value));
403            }
404        }
405
406        Err(ParseError::InvalidSyntax)
407    }
408
409    /// Parse IN clause values: (val1, val2, val3)
410    fn parse_in_values(values_str: &str) -> Result<SochValue, ParseError> {
411        let trimmed = values_str.trim();
412        if !trimmed.starts_with('(') || !trimmed.ends_with(')') {
413            return Err(ParseError::InvalidSyntax);
414        }
415
416        let inner = &trimmed[1..trimmed.len() - 1];
417        let values: Result<Vec<SochValue>, ParseError> = inner
418            .split(',')
419            .map(|v| Self::parse_value(v.trim()))
420            .collect();
421
422        // Return as an array SochValue
423        Ok(SochValue::Array(values?))
424    }
425
426    /// Parse a single value
427    fn parse_value(value_str: &str) -> Result<SochValue, ParseError> {
428        let trimmed = value_str.trim();
429
430        // String literal
431        if (trimmed.starts_with('\'') && trimmed.ends_with('\''))
432            || (trimmed.starts_with('"') && trimmed.ends_with('"'))
433        {
434            let inner = &trimmed[1..trimmed.len() - 1];
435            return Ok(SochValue::Text(inner.to_string()));
436        }
437
438        // Boolean
439        if trimmed.eq_ignore_ascii_case("true") {
440            return Ok(SochValue::Bool(true));
441        }
442        if trimmed.eq_ignore_ascii_case("false") {
443            return Ok(SochValue::Bool(false));
444        }
445
446        // NULL
447        if trimmed.eq_ignore_ascii_case("null") {
448            return Ok(SochValue::Null);
449        }
450
451        // Float (contains decimal point)
452        if trimmed.contains('.')
453            && let Ok(f) = trimmed.parse::<f64>()
454        {
455            return Ok(SochValue::Float(f));
456        }
457
458        // Integer
459        if let Ok(i) = trimmed.parse::<i64>() {
460            return Ok(SochValue::Int(i));
461        }
462
463        // Unsigned integer (if positive and within range)
464        if let Ok(u) = trimmed.parse::<u64>() {
465            return Ok(SochValue::UInt(u));
466        }
467
468        // If nothing else, treat as unquoted string (column name or identifier)
469        Ok(SochValue::Text(trimmed.to_string()))
470    }
471
472    fn parse_insert(query: &str) -> Result<SochQuery, ParseError> {
473        // Simple INSERT parser
474        // INSERT table: col1: val1 col2: val2
475        // or INSERT table[n]{cols}: val1,val2 val3,val4
476        let after_insert = query[6..].trim();
477
478        // Find table name
479        let table_end = after_insert
480            .find([':', '['])
481            .ok_or(ParseError::InvalidSyntax)?;
482        let table = after_insert[..table_end].trim().to_string();
483
484        // Simplified: just return structure
485        Ok(SochQuery::Insert(InsertQuery {
486            table,
487            columns: Vec::new(),
488            rows: Vec::new(),
489        }))
490    }
491
492    fn parse_create_table(query: &str) -> Result<SochQuery, ParseError> {
493        // CREATE TABLE name { ... }
494        let after_create = &query[12..].trim();
495        let brace_idx = after_create.find('{').ok_or(ParseError::InvalidSyntax)?;
496        let table = after_create[..brace_idx].trim().to_string();
497
498        Ok(SochQuery::CreateTable(CreateTableQuery {
499            table,
500            columns: Vec::new(),
501            primary_key: None,
502        }))
503    }
504
505    fn parse_drop_table(query: &str) -> Result<SochQuery, ParseError> {
506        let table = query[10..].trim().to_string();
507        Ok(SochQuery::DropTable { table })
508    }
509}
510
511/// Parse error
512#[derive(Debug, Clone)]
513pub enum ParseError {
514    UnknownStatement,
515    MissingFrom,
516    InvalidSyntax,
517    InvalidValue(String),
518}
519
520impl std::fmt::Display for ParseError {
521    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
522        match self {
523            ParseError::UnknownStatement => write!(f, "Unknown statement"),
524            ParseError::MissingFrom => write!(f, "Missing FROM clause"),
525            ParseError::InvalidSyntax => write!(f, "Invalid syntax"),
526            ParseError::InvalidValue(msg) => write!(f, "Invalid value: {}", msg),
527        }
528    }
529}
530
531impl std::error::Error for ParseError {}
532
533#[cfg(test)]
534mod tests {
535    use super::*;
536
537    #[test]
538    fn test_parse_select() {
539        let query = "SELECT id, name FROM users";
540        let result = SochQlParser::parse(query).unwrap();
541
542        match result {
543            SochQuery::Select(select) => {
544                assert_eq!(select.table, "users");
545                assert_eq!(select.columns, vec!["id", "name"]);
546            }
547            _ => panic!("Expected SELECT query"),
548        }
549    }
550
551    #[test]
552    fn test_parse_select_star() {
553        let query = "SELECT * FROM users";
554        let result = SochQlParser::parse(query).unwrap();
555
556        match result {
557            SochQuery::Select(select) => {
558                assert_eq!(select.table, "users");
559                assert_eq!(select.columns, vec!["*"]);
560            }
561            _ => panic!("Expected SELECT query"),
562        }
563    }
564
565    #[test]
566    fn test_parse_create_table() {
567        let query = "CREATE TABLE users { id: u64, name: text }";
568        let result = SochQlParser::parse(query).unwrap();
569
570        match result {
571            SochQuery::CreateTable(ct) => {
572                assert_eq!(ct.table, "users");
573            }
574            _ => panic!("Expected CREATE TABLE query"),
575        }
576    }
577
578    #[test]
579    fn test_parse_drop_table() {
580        let query = "DROP TABLE users";
581        let result = SochQlParser::parse(query).unwrap();
582
583        match result {
584            SochQuery::DropTable { table } => {
585                assert_eq!(table, "users");
586            }
587            _ => panic!("Expected DROP TABLE query"),
588        }
589    }
590
591    #[test]
592    fn test_soch_result_format() {
593        let result = SochResult {
594            table: "users".to_string(),
595            columns: vec!["id".to_string(), "name".to_string()],
596            rows: vec![
597                vec![SochValue::UInt(1), SochValue::Text("Alice".to_string())],
598                vec![SochValue::UInt(2), SochValue::Text("Bob".to_string())],
599            ],
600        };
601
602        let formatted = result.to_soch_string();
603        assert!(formatted.contains("users[2]{id,name}:"));
604        assert!(formatted.contains("1,Alice"));
605        assert!(formatted.contains("2,Bob"));
606    }
607
608    #[test]
609    #[allow(clippy::approx_constant)]
610    fn test_soch_value_format() {
611        assert_eq!(SochValue::Null.to_soch_string(), "null");
612        assert_eq!(SochValue::Bool(true).to_soch_string(), "true");
613        assert_eq!(SochValue::Int(-42).to_soch_string(), "-42");
614        assert_eq!(SochValue::UInt(100).to_soch_string(), "100");
615        assert_eq!(SochValue::Float(3.14).to_soch_string(), "3.14");
616        assert_eq!(
617            SochValue::Text("hello".to_string()).to_soch_string(),
618            "hello"
619        );
620    }
621}