1use crate::executor::{Params, Record, Value};
13use cypherlite_core::{CypherLiteError, DatabaseConfig};
14use cypherlite_storage::StorageEngine;
15use std::collections::HashMap;
16
17#[derive(Debug)]
19pub struct QueryResult {
20 pub columns: Vec<String>,
22 pub rows: Vec<Row>,
24}
25
26#[derive(Debug)]
28pub struct Row {
29 values: HashMap<String, Value>,
30 columns: Vec<String>,
31}
32
33impl Row {
34 pub fn new(values: HashMap<String, Value>, columns: Vec<String>) -> Self {
36 Self { values, columns }
37 }
38
39 pub fn get(&self, column: &str) -> Option<&Value> {
41 self.values.get(column)
42 }
43
44 pub fn get_as<T: FromValue>(&self, column: &str) -> Option<T> {
46 self.values.get(column).and_then(T::from_value)
47 }
48
49 pub fn columns(&self) -> &[String] {
51 &self.columns
52 }
53}
54
55pub trait FromValue: Sized {
57 fn from_value(value: &Value) -> Option<Self>;
59}
60
61impl FromValue for i64 {
62 fn from_value(value: &Value) -> Option<Self> {
63 match value {
64 Value::Int64(i) => Some(*i),
65 _ => None,
66 }
67 }
68}
69
70impl FromValue for f64 {
71 fn from_value(value: &Value) -> Option<Self> {
72 match value {
73 Value::Float64(f) => Some(*f),
74 _ => None,
75 }
76 }
77}
78
79impl FromValue for String {
80 fn from_value(value: &Value) -> Option<Self> {
81 match value {
82 Value::String(s) => Some(s.clone()),
83 _ => None,
84 }
85 }
86}
87
88impl FromValue for bool {
89 fn from_value(value: &Value) -> Option<Self> {
90 match value {
91 Value::Bool(b) => Some(*b),
92 _ => None,
93 }
94 }
95}
96
97pub struct CypherLite {
101 engine: StorageEngine,
102 #[cfg(feature = "plugin")]
103 scalar_functions:
104 cypherlite_core::plugin::PluginRegistry<dyn cypherlite_core::plugin::ScalarFunction>,
105 #[cfg(feature = "plugin")]
106 index_plugins:
107 cypherlite_core::plugin::PluginRegistry<dyn cypherlite_core::plugin::IndexPlugin>,
108 #[cfg(feature = "plugin")]
109 serializers: cypherlite_core::plugin::PluginRegistry<dyn cypherlite_core::plugin::Serializer>,
110 #[cfg(feature = "plugin")]
111 triggers: cypherlite_core::plugin::PluginRegistry<dyn cypherlite_core::plugin::Trigger>,
112}
113
114impl CypherLite {
115 pub fn open(config: DatabaseConfig) -> Result<Self, CypherLiteError> {
117 let engine = StorageEngine::open(config)?;
118 Ok(Self {
119 engine,
120 #[cfg(feature = "plugin")]
121 scalar_functions: cypherlite_core::plugin::PluginRegistry::new(),
122 #[cfg(feature = "plugin")]
123 index_plugins: cypherlite_core::plugin::PluginRegistry::new(),
124 #[cfg(feature = "plugin")]
125 serializers: cypherlite_core::plugin::PluginRegistry::new(),
126 #[cfg(feature = "plugin")]
127 triggers: cypherlite_core::plugin::PluginRegistry::new(),
128 })
129 }
130
131 pub fn execute(&mut self, query: &str) -> Result<QueryResult, CypherLiteError> {
133 self.execute_with_params(query, Params::new())
134 }
135
136 pub fn execute_with_params(
138 &mut self,
139 query: &str,
140 params: Params,
141 ) -> Result<QueryResult, CypherLiteError> {
142 let ast = crate::parser::parse_query(query).map_err(|e| CypherLiteError::ParseError {
144 line: e.line,
145 column: e.column,
146 message: e.message,
147 })?;
148
149 let mut analyzer = crate::semantic::SemanticAnalyzer::new(self.engine.catalog_mut());
151 analyzer
152 .analyze(&ast)
153 .map_err(|e| CypherLiteError::SemanticError(e.message))?;
154
155 let plan = crate::planner::LogicalPlanner::new(self.engine.catalog_mut())
157 .plan(&ast)
158 .map_err(|e| CypherLiteError::ExecutionError(e.message))?;
159
160 let plan = crate::planner::optimize::optimize(plan);
162
163 let mut params = params;
165 if !params.contains_key("__query_start_ms__") {
166 let now_ms = std::time::SystemTime::now()
167 .duration_since(std::time::UNIX_EPOCH)
168 .map(|d| d.as_millis() as i64)
169 .unwrap_or(0);
170 params.insert("__query_start_ms__".to_string(), Value::Int64(now_ms));
171 }
172
173 #[cfg(feature = "plugin")]
175 let scalar_fns: &dyn crate::executor::ScalarFnLookup = &self.scalar_functions;
176 #[cfg(not(feature = "plugin"))]
177 let scalar_fns: &dyn crate::executor::ScalarFnLookup = &();
178 #[cfg(feature = "plugin")]
179 let trigger_fns: &dyn crate::executor::TriggerLookup = &self.triggers;
180 #[cfg(not(feature = "plugin"))]
181 let trigger_fns: &dyn crate::executor::TriggerLookup = &();
182 let records =
183 crate::executor::execute(&plan, &mut self.engine, ¶ms, scalar_fns, trigger_fns)
184 .map_err(|e| CypherLiteError::ExecutionError(e.message))?;
185
186 let columns = extract_columns(&records);
188 let rows = records
189 .into_iter()
190 .map(|r| Row::new(r, columns.clone()))
191 .collect();
192
193 Ok(QueryResult { columns, rows })
194 }
195
196 pub fn engine(&self) -> &StorageEngine {
198 &self.engine
199 }
200
201 pub fn engine_mut(&mut self) -> &mut StorageEngine {
203 &mut self.engine
204 }
205
206 #[cfg(feature = "plugin")]
210 pub fn register_scalar_function(
211 &mut self,
212 func: Box<dyn cypherlite_core::plugin::ScalarFunction>,
213 ) -> Result<(), CypherLiteError> {
214 self.scalar_functions
215 .register(func)
216 .map_err(|e| CypherLiteError::PluginError(e.to_string()))
217 }
218
219 #[cfg(feature = "plugin")]
221 pub fn list_scalar_functions(&self) -> Vec<(&str, &str)> {
222 self.scalar_functions
223 .list()
224 .filter_map(|name| self.scalar_functions.get(name).map(|f| (name, f.version())))
225 .collect()
226 }
227
228 #[cfg(feature = "plugin")]
232 pub fn register_index_plugin(
233 &mut self,
234 plugin: Box<dyn cypherlite_core::plugin::IndexPlugin>,
235 ) -> Result<(), CypherLiteError> {
236 self.index_plugins
237 .register(plugin)
238 .map_err(|e| CypherLiteError::PluginError(e.to_string()))
239 }
240
241 #[cfg(feature = "plugin")]
243 pub fn list_index_plugins(&self) -> Vec<(&str, &str, &str)> {
244 self.index_plugins
245 .list()
246 .filter_map(|name| {
247 self.index_plugins
248 .get(name)
249 .map(|p| (name, p.version(), p.index_type()))
250 })
251 .collect()
252 }
253
254 #[cfg(feature = "plugin")]
256 pub fn get_index_plugin(
257 &self,
258 name: &str,
259 ) -> Option<&dyn cypherlite_core::plugin::IndexPlugin> {
260 self.index_plugins.get(name)
261 }
262
263 #[cfg(feature = "plugin")]
267 pub fn get_index_plugin_mut(
268 &mut self,
269 name: &str,
270 ) -> Option<&mut (dyn cypherlite_core::plugin::IndexPlugin + 'static)> {
271 self.index_plugins.get_mut(name)
272 }
273
274 #[cfg(feature = "plugin")]
278 pub fn register_serializer(
279 &mut self,
280 serializer: Box<dyn cypherlite_core::plugin::Serializer>,
281 ) -> Result<(), CypherLiteError> {
282 self.serializers
283 .register(serializer)
284 .map_err(|e| CypherLiteError::PluginError(e.to_string()))
285 }
286
287 #[cfg(feature = "plugin")]
289 pub fn list_serializers(&self) -> Vec<(&str, &str)> {
290 self.serializers
291 .list()
292 .filter_map(|name| self.serializers.get(name).map(|s| (name, s.version())))
293 .collect()
294 }
295
296 #[cfg(feature = "plugin")]
302 pub fn export_data(&mut self, format: &str, query: &str) -> Result<Vec<u8>, CypherLiteError> {
303 if !self.has_serializer_format(format) {
305 return Err(CypherLiteError::UnsupportedFormat(format.to_string()));
306 }
307
308 let result = self.execute(query)?;
310
311 let data = rows_to_property_maps(&result.rows);
313
314 let serializer = self.find_serializer_by_format(format)?;
316 serializer.export(&data)
317 }
318
319 #[cfg(feature = "plugin")]
324 pub fn import_data(
325 &self,
326 format: &str,
327 bytes: &[u8],
328 ) -> Result<Vec<HashMap<String, cypherlite_core::types::PropertyValue>>, CypherLiteError> {
329 let serializer = self.find_serializer_by_format(format)?;
330 serializer.import(bytes)
331 }
332
333 #[cfg(feature = "plugin")]
335 fn has_serializer_format(&self, format: &str) -> bool {
336 self.serializers.list().any(|name| {
337 self.serializers
338 .get(name)
339 .is_some_and(|s| s.format() == format)
340 })
341 }
342
343 #[cfg(feature = "plugin")]
345 fn find_serializer_by_format(
346 &self,
347 format: &str,
348 ) -> Result<&dyn cypherlite_core::plugin::Serializer, CypherLiteError> {
349 for name in self.serializers.list() {
350 if let Some(s) = self.serializers.get(name) {
351 if s.format() == format {
352 return Ok(s);
353 }
354 }
355 }
356 Err(CypherLiteError::UnsupportedFormat(format.to_string()))
357 }
358
359 #[cfg(feature = "plugin")]
363 pub fn register_trigger(
364 &mut self,
365 trigger: Box<dyn cypherlite_core::plugin::Trigger>,
366 ) -> Result<(), CypherLiteError> {
367 self.triggers
368 .register(trigger)
369 .map_err(|e| CypherLiteError::PluginError(e.to_string()))
370 }
371
372 #[cfg(feature = "plugin")]
374 pub fn list_triggers(&self) -> Vec<(&str, &str)> {
375 self.triggers
376 .list()
377 .filter_map(|name| self.triggers.get(name).map(|t| (name, t.version())))
378 .collect()
379 }
380
381 pub fn begin(&mut self) -> Transaction<'_> {
383 Transaction {
384 db: self,
385 committed: false,
386 }
387 }
388}
389
390#[cfg(feature = "plugin")]
393fn rows_to_property_maps(
394 rows: &[Row],
395) -> Vec<HashMap<String, cypherlite_core::types::PropertyValue>> {
396 use cypherlite_core::types::PropertyValue;
397
398 rows.iter()
399 .map(|row| {
400 row.columns()
401 .iter()
402 .filter_map(|col| {
403 row.get(col).and_then(|v| {
404 PropertyValue::try_from(v.clone())
405 .ok()
406 .map(|pv| (col.clone(), pv))
407 })
408 })
409 .collect()
410 })
411 .collect()
412}
413
414fn extract_columns(records: &[Record]) -> Vec<String> {
416 if records.is_empty() {
417 return vec![];
418 }
419 let mut cols: Vec<String> = records[0].keys().cloned().collect();
420 cols.sort(); cols
422}
423
424pub struct Transaction<'a> {
429 db: &'a mut CypherLite,
430 committed: bool,
431}
432
433impl<'a> Transaction<'a> {
434 pub fn execute(&mut self, query: &str) -> Result<QueryResult, CypherLiteError> {
436 self.db.execute(query)
437 }
438
439 pub fn execute_with_params(
441 &mut self,
442 query: &str,
443 params: Params,
444 ) -> Result<QueryResult, CypherLiteError> {
445 self.db.execute_with_params(query, params)
446 }
447
448 pub fn commit(mut self) -> Result<(), CypherLiteError> {
450 self.committed = true;
451 Ok(())
452 }
453
454 pub fn rollback(mut self) -> Result<(), CypherLiteError> {
457 self.committed = true; Ok(())
461 }
462}
463
464impl<'a> Drop for Transaction<'a> {
465 fn drop(&mut self) {
466 if !self.committed {
467 }
469 }
470}
471
472#[cfg(test)]
473mod tests {
474 use super::*;
475 use cypherlite_core::SyncMode;
476 use tempfile::tempdir;
477
478 fn test_config(dir: &std::path::Path) -> DatabaseConfig {
479 DatabaseConfig {
480 path: dir.join("test.cyl"),
481 wal_sync_mode: SyncMode::Normal,
482 ..Default::default()
483 }
484 }
485
486 #[test]
491 fn test_row_get_existing_column() {
492 let mut values = HashMap::new();
493 values.insert("name".to_string(), Value::String("Alice".into()));
494 let row = Row::new(values, vec!["name".to_string()]);
495 assert_eq!(row.get("name"), Some(&Value::String("Alice".into())));
496 }
497
498 #[test]
499 fn test_row_get_missing_column() {
500 let row = Row::new(HashMap::new(), vec![]);
501 assert_eq!(row.get("missing"), None);
502 }
503
504 #[test]
505 fn test_row_get_as_i64() {
506 let mut values = HashMap::new();
507 values.insert("age".to_string(), Value::Int64(30));
508 let row = Row::new(values, vec!["age".to_string()]);
509 assert_eq!(row.get_as::<i64>("age"), Some(30));
510 }
511
512 #[test]
513 fn test_row_get_as_f64() {
514 let mut values = HashMap::new();
515 values.insert("score".to_string(), Value::Float64(3.15));
516 let row = Row::new(values, vec!["score".to_string()]);
517 assert_eq!(row.get_as::<f64>("score"), Some(3.15));
518 }
519
520 #[test]
521 fn test_row_get_as_string() {
522 let mut values = HashMap::new();
523 values.insert("name".to_string(), Value::String("Bob".into()));
524 let row = Row::new(values, vec!["name".to_string()]);
525 assert_eq!(row.get_as::<String>("name"), Some("Bob".to_string()));
526 }
527
528 #[test]
529 fn test_row_get_as_bool() {
530 let mut values = HashMap::new();
531 values.insert("active".to_string(), Value::Bool(true));
532 let row = Row::new(values, vec!["active".to_string()]);
533 assert_eq!(row.get_as::<bool>("active"), Some(true));
534 }
535
536 #[test]
537 fn test_row_get_as_wrong_type() {
538 let mut values = HashMap::new();
539 values.insert("age".to_string(), Value::String("thirty".into()));
540 let row = Row::new(values, vec!["age".to_string()]);
541 assert_eq!(row.get_as::<i64>("age"), None);
542 }
543
544 #[test]
545 fn test_row_columns() {
546 let row = Row::new(HashMap::new(), vec!["a".to_string(), "b".to_string()]);
547 assert_eq!(row.columns(), &["a".to_string(), "b".to_string()]);
548 }
549
550 #[test]
551 fn test_query_result_empty() {
552 let result = QueryResult {
553 columns: vec![],
554 rows: vec![],
555 };
556 assert!(result.rows.is_empty());
557 assert!(result.columns.is_empty());
558 }
559
560 #[test]
561 fn test_from_value_null_returns_none() {
562 assert_eq!(i64::from_value(&Value::Null), None);
563 assert_eq!(f64::from_value(&Value::Null), None);
564 assert_eq!(String::from_value(&Value::Null), None);
565 assert_eq!(bool::from_value(&Value::Null), None);
566 }
567
568 #[test]
569 fn test_extract_columns_empty_records() {
570 let records: Vec<Record> = vec![];
571 assert!(extract_columns(&records).is_empty());
572 }
573
574 #[test]
575 fn test_extract_columns_deterministic_order() {
576 let mut r = Record::new();
577 r.insert("b".to_string(), Value::Int64(1));
578 r.insert("a".to_string(), Value::Int64(2));
579 let cols = extract_columns(&[r]);
580 assert_eq!(cols, vec!["a".to_string(), "b".to_string()]);
581 }
582
583 #[test]
588 fn test_cypherlite_open() {
589 let dir = tempdir().expect("tempdir");
590 let db = CypherLite::open(test_config(dir.path()));
591 assert!(db.is_ok());
592 }
593
594 #[test]
595 fn test_cypherlite_engine_accessors() {
596 let dir = tempdir().expect("tempdir");
597 let mut db = CypherLite::open(test_config(dir.path())).expect("open");
598 assert_eq!(db.engine().node_count(), 0);
599 assert_eq!(db.engine_mut().edge_count(), 0);
600 }
601
602 #[test]
607 fn test_transaction_commit() {
608 let dir = tempdir().expect("tempdir");
609 let mut db = CypherLite::open(test_config(dir.path())).expect("open");
610 let tx = db.begin();
611 assert!(tx.commit().is_ok());
612 }
613
614 #[test]
615 fn test_transaction_rollback() {
616 let dir = tempdir().expect("tempdir");
617 let mut db = CypherLite::open(test_config(dir.path())).expect("open");
618 let tx = db.begin();
619 assert!(tx.rollback().is_ok());
620 }
621
622 #[test]
623 fn test_transaction_auto_rollback_on_drop() {
624 let dir = tempdir().expect("tempdir");
625 let mut db = CypherLite::open(test_config(dir.path())).expect("open");
626 {
627 let _tx = db.begin();
628 }
630 }
631
632 #[test]
638 fn int_t001_create_then_match() {
639 let dir = tempdir().expect("tempdir");
640 let mut db = CypherLite::open(test_config(dir.path())).expect("open");
641
642 db.execute("CREATE (n:Person {name: 'Alice', age: 30})")
644 .expect("create");
645
646 let result = db
648 .execute("MATCH (n:Person) RETURN n.name, n.age")
649 .expect("match");
650 assert_eq!(result.rows.len(), 1);
651 assert_eq!(
652 result.rows[0].get_as::<String>("n.name"),
653 Some("Alice".to_string())
654 );
655 assert_eq!(result.rows[0].get_as::<i64>("n.age"), Some(30));
656 }
657
658 #[test]
660 fn int_t002_parameter_binding() {
661 let dir = tempdir().expect("tempdir");
662 let mut db = CypherLite::open(test_config(dir.path())).expect("open");
663
664 db.execute("CREATE (n:Person {name: 'Alice'})")
665 .expect("create");
666
667 let mut params = Params::new();
668 params.insert("name".to_string(), Value::String("Alice".into()));
669
670 let result = db
671 .execute_with_params(
672 "MATCH (n:Person) WHERE n.name = $name RETURN n.name",
673 params,
674 )
675 .expect("match with params");
676 assert_eq!(result.rows.len(), 1);
677 assert_eq!(
678 result.rows[0].get_as::<String>("n.name"),
679 Some("Alice".to_string())
680 );
681 }
682
683 #[test]
685 fn int_t003_transaction_commit() {
686 let dir = tempdir().expect("tempdir");
687 let mut db = CypherLite::open(test_config(dir.path())).expect("open");
688
689 {
690 let mut tx = db.begin();
691 tx.execute("CREATE (n:Person {name: 'Bob'})")
692 .expect("create in tx");
693 tx.commit().expect("commit");
694 }
695
696 let result = db
698 .execute("MATCH (n:Person) RETURN n.name")
699 .expect("match after commit");
700 assert_eq!(result.rows.len(), 1);
701 assert_eq!(
702 result.rows[0].get_as::<String>("n.name"),
703 Some("Bob".to_string())
704 );
705 }
706
707 #[test]
709 fn int_t004_invalid_cypher_parse_error() {
710 let dir = tempdir().expect("tempdir");
711 let mut db = CypherLite::open(test_config(dir.path())).expect("open");
712
713 let result = db.execute("INVALID QUERY @#$");
714 assert!(result.is_err());
715 let err = result.expect_err("should fail");
716 assert!(
718 matches!(err, CypherLiteError::ParseError { .. }),
719 "expected ParseError, got: {err}"
720 );
721 }
722
723 #[test]
725 fn int_t005_match_nonexistent_label_empty() {
726 let dir = tempdir().expect("tempdir");
727 let mut db = CypherLite::open(test_config(dir.path())).expect("open");
728
729 let result = db
730 .execute("MATCH (n:NonExistent) RETURN n")
731 .expect("should succeed with empty result");
732 assert!(result.rows.is_empty());
733 }
734
735 #[test]
737 fn int_t006_set_then_match() {
738 let dir = tempdir().expect("tempdir");
739 let mut db = CypherLite::open(test_config(dir.path())).expect("open");
740
741 db.execute("CREATE (n:Person {name: 'Alice', age: 25})")
742 .expect("create");
743
744 db.execute("MATCH (n:Person) SET n.age = 30").expect("set");
745
746 let result = db
747 .execute("MATCH (n:Person) RETURN n.age")
748 .expect("match after set");
749 assert_eq!(result.rows.len(), 1);
750 assert_eq!(result.rows[0].get_as::<i64>("n.age"), Some(30));
751 }
752
753 #[test]
755 fn int_t007_detach_delete() {
756 let dir = tempdir().expect("tempdir");
757 let mut db = CypherLite::open(test_config(dir.path())).expect("open");
758
759 db.execute("CREATE (a:Person {name: 'Alice'})-[:KNOWS]->(b:Person {name: 'Bob'})")
760 .expect("create");
761
762 let result = db
764 .execute("MATCH (n:Person) RETURN n.name")
765 .expect("match before delete");
766 assert_eq!(result.rows.len(), 2);
767
768 db.execute("MATCH (n:Person) DETACH DELETE n")
770 .expect("detach delete");
771
772 let result = db
774 .execute("MATCH (n:Person) RETURN n.name")
775 .expect("match after delete");
776 assert!(result.rows.is_empty());
777 }
778
779 #[test]
781 fn ac_001_match_return_three_persons() {
782 let dir = tempdir().expect("tempdir");
783 let mut db = CypherLite::open(test_config(dir.path())).expect("open");
784
785 db.execute("CREATE (n:Person {name: 'Alice'})").expect("c1");
786 db.execute("CREATE (n:Person {name: 'Bob'})").expect("c2");
787 db.execute("CREATE (n:Person {name: 'Charlie'})")
788 .expect("c3");
789
790 let result = db.execute("MATCH (n:Person) RETURN n.name").expect("match");
791 assert_eq!(result.rows.len(), 3);
792
793 let mut names: Vec<String> = result
794 .rows
795 .iter()
796 .filter_map(|r| r.get_as::<String>("n.name"))
797 .collect();
798 names.sort();
799 assert_eq!(names, vec!["Alice", "Bob", "Charlie"]);
800 }
801
802 #[test]
804 fn ac_002_create_then_match_verify() {
805 let dir = tempdir().expect("tempdir");
806 let mut db = CypherLite::open(test_config(dir.path())).expect("open");
807
808 db.execute("CREATE (a:Person {name: 'Alice'})")
809 .expect("create");
810
811 let result = db.execute("MATCH (n:Person) RETURN n.name").expect("match");
812 assert_eq!(result.rows.len(), 1);
813 assert_eq!(
814 result.rows[0].get_as::<String>("n.name"),
815 Some("Alice".to_string())
816 );
817 }
818
819 #[test]
821 fn ac_003_create_relationship_then_traverse() {
822 let dir = tempdir().expect("tempdir");
823 let mut db = CypherLite::open(test_config(dir.path())).expect("open");
824
825 db.execute("CREATE (a:Person {name: 'Alice'})-[:KNOWS]->(b:Person {name: 'Bob'})")
826 .expect("create relationship");
827
828 let result = db
829 .execute("MATCH (a:Person)-[:KNOWS]->(b:Person) RETURN b.name")
830 .expect("traverse");
831 assert_eq!(result.rows.len(), 1);
832 assert_eq!(
833 result.rows[0].get_as::<String>("b.name"),
834 Some("Bob".to_string())
835 );
836 }
837
838 #[test]
840 fn ac_004_where_filter() {
841 let dir = tempdir().expect("tempdir");
842 let mut db = CypherLite::open(test_config(dir.path())).expect("open");
843
844 db.execute("CREATE (n:Person {name: 'Alice', age: 30})")
845 .expect("c1");
846 db.execute("CREATE (n:Person {name: 'Bob', age: 25})")
847 .expect("c2");
848 db.execute("CREATE (n:Person {name: 'Charlie', age: 35})")
849 .expect("c3");
850
851 let result = db
852 .execute("MATCH (n:Person) WHERE n.age > 28 RETURN n.name")
853 .expect("filter");
854 assert_eq!(result.rows.len(), 2);
855
856 let mut names: Vec<String> = result
857 .rows
858 .iter()
859 .filter_map(|r| r.get_as::<String>("n.name"))
860 .collect();
861 names.sort();
862 assert_eq!(names, vec!["Alice", "Charlie"]);
863 }
864
865 #[test]
867 fn ac_006_syntax_error_detection() {
868 let dir = tempdir().expect("tempdir");
869 let mut db = CypherLite::open(test_config(dir.path())).expect("open");
870
871 let result = db.execute("MATCH (n:Person RETURN n");
872 assert!(result.is_err());
873 let err = result.expect_err("should fail");
874 match err {
875 CypherLiteError::ParseError {
876 line,
877 column,
878 message,
879 } => {
880 assert!(line >= 1, "line should be >= 1, got {line}");
881 assert!(column >= 1, "column should be >= 1, got {column}");
882 assert!(!message.is_empty(), "error message should not be empty");
883 }
884 other => panic!("expected ParseError, got: {other}"),
885 }
886 }
887
888 #[test]
890 fn ac_007_semantic_error() {
891 let dir = tempdir().expect("tempdir");
892 let mut db = CypherLite::open(test_config(dir.path())).expect("open");
893
894 let result = db.execute("MATCH (n:Person) RETURN m.name");
896 assert!(result.is_err());
897 let err = result.expect_err("should fail");
898 assert!(
899 matches!(err, CypherLiteError::SemanticError(_)),
900 "expected SemanticError, got: {err}"
901 );
902 }
903
904 #[test]
906 fn ac_010_null_handling() {
907 let dir = tempdir().expect("tempdir");
908 let mut db = CypherLite::open(test_config(dir.path())).expect("open");
909
910 db.execute("CREATE (n:Person {name: 'Alice', email: 'alice@example.com'})")
912 .expect("c1");
913 db.execute("CREATE (n:Person {name: 'Bob'})").expect("c2");
914
915 let result = db
917 .execute("MATCH (n:Person) RETURN n.name, n.email")
918 .expect("match");
919 assert_eq!(result.rows.len(), 2);
920
921 let mut found_null = false;
923 let mut found_email = false;
924 for row in &result.rows {
925 match row.get("n.email") {
926 Some(Value::String(s)) if !s.is_empty() => found_email = true,
927 Some(Value::Null) | None => found_null = true,
928 _ => {}
929 }
930 }
931 assert!(found_email, "should find at least one row with email");
932 assert!(found_null, "should find at least one row with null email");
933 }
934
935 #[test]
937 fn ac_010_is_not_null_filter() {
938 let dir = tempdir().expect("tempdir");
939 let mut db = CypherLite::open(test_config(dir.path())).expect("open");
940
941 db.execute("CREATE (n:Person {name: 'Alice', email: 'alice@example.com'})")
942 .expect("c1");
943 db.execute("CREATE (n:Person {name: 'Bob'})").expect("c2");
944
945 let result = db
946 .execute("MATCH (n:Person) WHERE n.email IS NOT NULL RETURN n.name")
947 .expect("filter not null");
948 assert_eq!(result.rows.len(), 1);
949 assert_eq!(
950 result.rows[0].get_as::<String>("n.name"),
951 Some("Alice".to_string())
952 );
953 }
954}