sochdb_query/
soch_ql.rs

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