1pub mod db;
2pub mod executor;
3pub mod hnsw;
4pub mod pager;
5pub mod parser;
6use parser::create::CreateQuery;
9use parser::insert::InsertQuery;
10use parser::select::SelectQuery;
11
12use sqlparser::ast::Statement;
13use sqlparser::dialect::SQLiteDialect;
14use sqlparser::parser::{Parser, ParserError};
15
16use crate::error::{Result, SQLRiteError};
17use crate::sql::db::database::Database;
18use crate::sql::db::table::Table;
19
20#[derive(Debug, PartialEq)]
21pub enum SQLCommand {
22 Insert(String),
23 Delete(String),
24 Update(String),
25 CreateTable(String),
26 Select(String),
27 Unknown(String),
28}
29
30impl SQLCommand {
31 pub fn new(command: String) -> SQLCommand {
32 let v = command.split(" ").collect::<Vec<&str>>();
33 match v[0] {
34 "insert" => SQLCommand::Insert(command),
35 "update" => SQLCommand::Update(command),
36 "delete" => SQLCommand::Delete(command),
37 "create" => SQLCommand::CreateTable(command),
38 "select" => SQLCommand::Select(command),
39 _ => SQLCommand::Unknown(command),
40 }
41 }
42}
43
44pub fn process_command(query: &str, db: &mut Database) -> Result<String> {
46 let dialect = SQLiteDialect {};
47 let message: String;
48 let mut ast = Parser::parse_sql(&dialect, query).map_err(SQLRiteError::from)?;
49
50 if ast.len() > 1 {
51 return Err(SQLRiteError::SqlError(ParserError::ParserError(format!(
52 "Expected a single query statement, but there are {}",
53 ast.len()
54 ))));
55 }
56
57 let Some(query) = ast.pop() else {
61 return Ok("No statement to execute.".to_string());
62 };
63
64 match &query {
69 Statement::StartTransaction { .. } => {
70 db.begin_transaction()?;
71 return Ok(String::from("BEGIN"));
72 }
73 Statement::Commit { .. } => {
74 if !db.in_transaction() {
75 return Err(SQLRiteError::General(
76 "cannot COMMIT: no transaction is open".to_string(),
77 ));
78 }
79 if let Some(path) = db.source_path.clone() {
89 if let Err(save_err) = pager::save_database(db, &path) {
90 let _ = db.rollback_transaction();
91 return Err(SQLRiteError::General(format!(
92 "COMMIT failed — transaction rolled back: {save_err}"
93 )));
94 }
95 }
96 db.commit_transaction()?;
97 return Ok(String::from("COMMIT"));
98 }
99 Statement::Rollback { .. } => {
100 db.rollback_transaction()?;
101 return Ok(String::from("ROLLBACK"));
102 }
103 _ => {}
104 }
105
106 let is_write_statement = matches!(
109 &query,
110 Statement::CreateTable(_)
111 | Statement::CreateIndex(_)
112 | Statement::Insert(_)
113 | Statement::Update(_)
114 | Statement::Delete(_)
115 );
116
117 if is_write_statement && db.is_read_only() {
123 return Err(SQLRiteError::General(
124 "cannot execute: database is opened read-only".to_string(),
125 ));
126 }
127
128 match query {
130 Statement::CreateTable(_) => {
131 let create_query = CreateQuery::new(&query);
132 match create_query {
133 Ok(payload) => {
134 let table_name = payload.table_name.clone();
135 if table_name == pager::MASTER_TABLE_NAME {
136 return Err(SQLRiteError::General(format!(
137 "'{}' is a reserved name used by the internal schema catalog",
138 pager::MASTER_TABLE_NAME
139 )));
140 }
141 match db.contains_table(table_name.to_string()) {
143 true => {
144 return Err(SQLRiteError::Internal(
145 "Cannot create, table already exists.".to_string(),
146 ));
147 }
148 false => {
149 let table = Table::new(payload);
150 let _ = table.print_table_schema();
151 db.tables.insert(table_name.to_string(), table);
152 message = String::from("CREATE TABLE Statement executed.");
157 }
158 }
159 }
160 Err(err) => return Err(err),
161 }
162 }
163 Statement::Insert(_) => {
164 let insert_query = InsertQuery::new(&query);
165 match insert_query {
166 Ok(payload) => {
167 let table_name = payload.table_name;
168 let columns = payload.columns;
169 let values = payload.rows;
170
171 match db.contains_table(table_name.to_string()) {
174 true => {
175 let db_table = db.get_table_mut(table_name.to_string()).unwrap();
176 match columns
178 .iter()
179 .all(|column| db_table.contains_column(column.to_string()))
180 {
181 true => {
182 for value in &values {
183 if columns.len() != value.len() {
185 return Err(SQLRiteError::Internal(format!(
186 "{} values for {} columns",
187 value.len(),
188 columns.len()
189 )));
190 }
191 db_table
192 .validate_unique_constraint(&columns, value)
193 .map_err(|err| {
194 SQLRiteError::Internal(format!(
195 "Unique key constraint violation: {err}"
196 ))
197 })?;
198 db_table.insert_row(&columns, value)?;
199 }
200 }
201 false => {
202 return Err(SQLRiteError::Internal(
203 "Cannot insert, some of the columns do not exist"
204 .to_string(),
205 ));
206 }
207 }
208 db_table.print_table_data();
209 }
210 false => {
211 return Err(SQLRiteError::Internal("Table doesn't exist".to_string()));
212 }
213 }
214 }
215 Err(err) => return Err(err),
216 }
217
218 message = String::from("INSERT Statement executed.")
219 }
220 Statement::Query(_) => {
221 let select_query = SelectQuery::new(&query)?;
222 let (rendered, rows) = executor::execute_select(select_query, db)?;
223 print!("{rendered}");
225 message = format!(
226 "SELECT Statement executed. {rows} row{s} returned.",
227 s = if rows == 1 { "" } else { "s" }
228 );
229 }
230 Statement::Delete(_) => {
231 let rows = executor::execute_delete(&query, db)?;
232 message = format!(
233 "DELETE Statement executed. {rows} row{s} deleted.",
234 s = if rows == 1 { "" } else { "s" }
235 );
236 }
237 Statement::Update(_) => {
238 let rows = executor::execute_update(&query, db)?;
239 message = format!(
240 "UPDATE Statement executed. {rows} row{s} updated.",
241 s = if rows == 1 { "" } else { "s" }
242 );
243 }
244 Statement::CreateIndex(_) => {
245 let name = executor::execute_create_index(&query, db)?;
246 message = format!("CREATE INDEX '{name}' executed.");
247 }
248 _ => {
249 return Err(SQLRiteError::NotImplemented(
250 "SQL Statement not supported yet.".to_string(),
251 ));
252 }
253 };
254
255 if is_write_statement && db.source_path.is_some() && !db.in_transaction() {
266 let path = db.source_path.clone().unwrap();
267 pager::save_database(db, &path)?;
268 }
269
270 Ok(message)
271}
272
273#[cfg(test)]
274mod tests {
275 use super::*;
276 use crate::sql::db::table::Value;
277
278 fn seed_users_table() -> Database {
281 let mut db = Database::new("tempdb".to_string());
282 process_command(
283 "CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT NOT NULL, age INTEGER);",
284 &mut db,
285 )
286 .expect("create table");
287 process_command(
288 "INSERT INTO users (name, age) VALUES ('alice', 30);",
289 &mut db,
290 )
291 .expect("insert alice");
292 process_command("INSERT INTO users (name, age) VALUES ('bob', 25);", &mut db)
293 .expect("insert bob");
294 process_command(
295 "INSERT INTO users (name, age) VALUES ('carol', 40);",
296 &mut db,
297 )
298 .expect("insert carol");
299 db
300 }
301
302 #[test]
303 fn process_command_select_all_test() {
304 let mut db = seed_users_table();
305 let response = process_command("SELECT * FROM users;", &mut db).expect("select");
306 assert!(response.contains("3 rows returned"));
307 }
308
309 #[test]
310 fn process_command_select_where_test() {
311 let mut db = seed_users_table();
312 let response =
313 process_command("SELECT name FROM users WHERE age > 25;", &mut db).expect("select");
314 assert!(response.contains("2 rows returned"));
315 }
316
317 #[test]
318 fn process_command_select_eq_string_test() {
319 let mut db = seed_users_table();
320 let response =
321 process_command("SELECT name FROM users WHERE name = 'bob';", &mut db).expect("select");
322 assert!(response.contains("1 row returned"));
323 }
324
325 #[test]
326 fn process_command_select_limit_test() {
327 let mut db = seed_users_table();
328 let response = process_command("SELECT * FROM users ORDER BY age ASC LIMIT 2;", &mut db)
329 .expect("select");
330 assert!(response.contains("2 rows returned"));
331 }
332
333 #[test]
334 fn process_command_select_unknown_table_test() {
335 let mut db = Database::new("tempdb".to_string());
336 let result = process_command("SELECT * FROM nope;", &mut db);
337 assert!(result.is_err());
338 }
339
340 #[test]
341 fn process_command_select_unknown_column_test() {
342 let mut db = seed_users_table();
343 let result = process_command("SELECT height FROM users;", &mut db);
344 assert!(result.is_err());
345 }
346
347 #[test]
348 fn process_command_insert_test() {
349 let mut db = Database::new("tempdb".to_string());
351
352 let query_statement = "CREATE TABLE users (
354 id INTEGER PRIMARY KEY,
355 name TEXT
356 );";
357 let dialect = SQLiteDialect {};
358 let mut ast = Parser::parse_sql(&dialect, query_statement).unwrap();
359 if ast.len() > 1 {
360 panic!("Expected a single query statement, but there are more then 1.")
361 }
362 let query = ast.pop().unwrap();
363 let create_query = CreateQuery::new(&query).unwrap();
364
365 db.tables.insert(
367 create_query.table_name.to_string(),
368 Table::new(create_query),
369 );
370
371 let insert_query = String::from("INSERT INTO users (name) Values ('josh');");
373 match process_command(&insert_query, &mut db) {
374 Ok(response) => assert_eq!(response, "INSERT Statement executed."),
375 Err(err) => {
376 eprintln!("Error: {}", err);
377 assert!(false)
378 }
379 };
380 }
381
382 #[test]
383 fn process_command_insert_no_pk_test() {
384 let mut db = Database::new("tempdb".to_string());
386
387 let query_statement = "CREATE TABLE users (
389 name TEXT
390 );";
391 let dialect = SQLiteDialect {};
392 let mut ast = Parser::parse_sql(&dialect, query_statement).unwrap();
393 if ast.len() > 1 {
394 panic!("Expected a single query statement, but there are more then 1.")
395 }
396 let query = ast.pop().unwrap();
397 let create_query = CreateQuery::new(&query).unwrap();
398
399 db.tables.insert(
401 create_query.table_name.to_string(),
402 Table::new(create_query),
403 );
404
405 let insert_query = String::from("INSERT INTO users (name) Values ('josh');");
407 match process_command(&insert_query, &mut db) {
408 Ok(response) => assert_eq!(response, "INSERT Statement executed."),
409 Err(err) => {
410 eprintln!("Error: {}", err);
411 assert!(false)
412 }
413 };
414 }
415
416 #[test]
417 fn process_command_delete_where_test() {
418 let mut db = seed_users_table();
419 let response =
420 process_command("DELETE FROM users WHERE name = 'bob';", &mut db).expect("delete");
421 assert!(response.contains("1 row deleted"));
422
423 let remaining = process_command("SELECT * FROM users;", &mut db).expect("select");
424 assert!(remaining.contains("2 rows returned"));
425 }
426
427 #[test]
428 fn process_command_delete_all_test() {
429 let mut db = seed_users_table();
430 let response = process_command("DELETE FROM users;", &mut db).expect("delete");
431 assert!(response.contains("3 rows deleted"));
432 }
433
434 #[test]
435 fn process_command_update_where_test() {
436 use crate::sql::db::table::Value;
437
438 let mut db = seed_users_table();
439 let response = process_command("UPDATE users SET age = 99 WHERE name = 'bob';", &mut db)
440 .expect("update");
441 assert!(response.contains("1 row updated"));
442
443 let users = db.get_table("users".to_string()).unwrap();
445 let bob_rowid = users
446 .rowids()
447 .into_iter()
448 .find(|r| users.get_value("name", *r) == Some(Value::Text("bob".to_string())))
449 .expect("bob row must exist");
450 assert_eq!(users.get_value("age", bob_rowid), Some(Value::Integer(99)));
451 }
452
453 #[test]
454 fn process_command_update_unique_violation_test() {
455 let mut db = seed_users_table();
456 process_command(
458 "CREATE TABLE tags (id INTEGER PRIMARY KEY, label TEXT UNIQUE);",
459 &mut db,
460 )
461 .unwrap();
462 process_command("INSERT INTO tags (label) VALUES ('a');", &mut db).unwrap();
463 process_command("INSERT INTO tags (label) VALUES ('b');", &mut db).unwrap();
464
465 let result = process_command("UPDATE tags SET label = 'a' WHERE label = 'b';", &mut db);
466 assert!(result.is_err(), "expected UNIQUE violation, got {result:?}");
467 }
468
469 #[test]
470 fn process_command_insert_type_mismatch_returns_error_test() {
471 let mut db = Database::new("tempdb".to_string());
473 process_command(
474 "CREATE TABLE items (id INTEGER PRIMARY KEY, qty INTEGER);",
475 &mut db,
476 )
477 .unwrap();
478 let result = process_command("INSERT INTO items (qty) VALUES ('not a number');", &mut db);
479 assert!(result.is_err(), "expected error, got {result:?}");
480 }
481
482 #[test]
483 fn process_command_insert_missing_integer_returns_error_test() {
484 let mut db = Database::new("tempdb".to_string());
486 process_command(
487 "CREATE TABLE items (id INTEGER PRIMARY KEY, qty INTEGER);",
488 &mut db,
489 )
490 .unwrap();
491 let result = process_command("INSERT INTO items (id) VALUES (1);", &mut db);
492 assert!(result.is_err(), "expected error, got {result:?}");
493 }
494
495 #[test]
496 fn process_command_update_arith_test() {
497 use crate::sql::db::table::Value;
498
499 let mut db = seed_users_table();
500 process_command("UPDATE users SET age = age + 1;", &mut db).expect("update +1");
501
502 let users = db.get_table("users".to_string()).unwrap();
503 let mut ages: Vec<i64> = users
504 .rowids()
505 .into_iter()
506 .filter_map(|r| match users.get_value("age", r) {
507 Some(Value::Integer(n)) => Some(n),
508 _ => None,
509 })
510 .collect();
511 ages.sort();
512 assert_eq!(ages, vec![26, 31, 41]); }
514
515 #[test]
516 fn process_command_select_arithmetic_where_test() {
517 let mut db = seed_users_table();
518 let response =
520 process_command("SELECT name FROM users WHERE age * 2 > 55;", &mut db).expect("select");
521 assert!(response.contains("2 rows returned"));
522 }
523
524 #[test]
525 fn process_command_divide_by_zero_test() {
526 let mut db = seed_users_table();
527 let result = process_command("SELECT age / 0 FROM users;", &mut db);
528 assert!(result.is_err());
530 }
531
532 #[test]
533 fn process_command_unsupported_statement_test() {
534 let mut db = Database::new("tempdb".to_string());
535 let result = process_command("DROP TABLE users;", &mut db);
537 assert!(result.is_err());
538 }
539
540 #[test]
541 fn empty_input_is_a_noop_not_a_panic() {
542 let mut db = Database::new("t".to_string());
546 for input in ["", " ", "-- just a comment", "-- comment\n-- another"] {
547 let result = process_command(input, &mut db);
548 assert!(result.is_ok(), "input {input:?} should not error");
549 let msg = result.unwrap();
550 assert!(msg.contains("No statement"), "got: {msg:?}");
551 }
552 }
553
554 #[test]
555 fn create_index_adds_explicit_index() {
556 let mut db = seed_users_table();
557 let response = process_command("CREATE INDEX users_age_idx ON users (age);", &mut db)
558 .expect("create index");
559 assert!(response.contains("users_age_idx"));
560
561 let users = db.get_table("users".to_string()).unwrap();
563 let idx = users
564 .index_by_name("users_age_idx")
565 .expect("index should exist after CREATE INDEX");
566 assert_eq!(idx.column_name, "age");
567 assert!(!idx.is_unique);
568 }
569
570 #[test]
571 fn create_unique_index_rejects_duplicate_existing_values() {
572 let mut db = seed_users_table();
573 process_command("INSERT INTO users (name, age) VALUES ('dan', 30);", &mut db).unwrap();
576 let result = process_command(
577 "CREATE UNIQUE INDEX users_age_unique ON users (age);",
578 &mut db,
579 );
580 assert!(
581 result.is_err(),
582 "expected unique-index failure, got {result:?}"
583 );
584 }
585
586 #[test]
587 fn where_eq_on_indexed_column_uses_index_probe() {
588 let mut db = Database::new("t".to_string());
592 process_command(
593 "CREATE TABLE big (id INTEGER PRIMARY KEY, tag TEXT);",
594 &mut db,
595 )
596 .unwrap();
597 process_command("CREATE INDEX big_tag_idx ON big (tag);", &mut db).unwrap();
598 for i in 1..=100 {
599 let tag = if i % 3 == 0 { "hot" } else { "cold" };
600 process_command(&format!("INSERT INTO big (tag) VALUES ('{tag}');"), &mut db).unwrap();
601 }
602 let response =
603 process_command("SELECT id FROM big WHERE tag = 'hot';", &mut db).expect("select");
604 assert!(
606 response.contains("33 rows returned"),
607 "response was {response:?}"
608 );
609 }
610
611 #[test]
612 fn where_eq_on_indexed_column_inside_parens_uses_index_probe() {
613 let mut db = seed_users_table();
614 let response = process_command("SELECT name FROM users WHERE (name = 'bob');", &mut db)
615 .expect("select");
616 assert!(response.contains("1 row returned"));
617 }
618
619 #[test]
620 fn where_eq_literal_first_side_uses_index_probe() {
621 let mut db = seed_users_table();
622 let response =
624 process_command("SELECT name FROM users WHERE 'bob' = name;", &mut db).expect("select");
625 assert!(response.contains("1 row returned"));
626 }
627
628 #[test]
629 fn non_equality_where_still_falls_back_to_full_scan() {
630 let mut db = seed_users_table();
633 let response =
634 process_command("SELECT name FROM users WHERE age > 28;", &mut db).expect("select");
635 assert!(response.contains("2 rows returned"));
636 }
637
638 #[test]
643 fn rollback_restores_pre_begin_in_memory_state() {
644 let mut db = seed_users_table();
647 let before = db.get_table("users".to_string()).unwrap().rowids().len();
648 assert_eq!(before, 3);
649
650 process_command("BEGIN;", &mut db).expect("BEGIN");
651 assert!(db.in_transaction());
652 process_command("INSERT INTO users (name, age) VALUES ('dan', 50);", &mut db)
653 .expect("INSERT inside txn");
654 let mid = db.get_table("users".to_string()).unwrap().rowids().len();
656 assert_eq!(mid, 4);
657
658 process_command("ROLLBACK;", &mut db).expect("ROLLBACK");
659 assert!(!db.in_transaction());
660 let after = db.get_table("users".to_string()).unwrap().rowids().len();
661 assert_eq!(
662 after, 3,
663 "ROLLBACK should have restored the pre-BEGIN state"
664 );
665 }
666
667 #[test]
668 fn commit_keeps_mutations_and_clears_txn_flag() {
669 let mut db = seed_users_table();
670 process_command("BEGIN;", &mut db).expect("BEGIN");
671 process_command("INSERT INTO users (name, age) VALUES ('dan', 50);", &mut db)
672 .expect("INSERT inside txn");
673 process_command("COMMIT;", &mut db).expect("COMMIT");
674 assert!(!db.in_transaction());
675 let after = db.get_table("users".to_string()).unwrap().rowids().len();
676 assert_eq!(after, 4);
677 }
678
679 #[test]
680 fn rollback_undoes_update_and_delete_side_by_side() {
681 use crate::sql::db::table::Value;
682
683 let mut db = seed_users_table();
684 process_command("BEGIN;", &mut db).unwrap();
685 process_command("UPDATE users SET age = 999;", &mut db).unwrap();
686 process_command("DELETE FROM users WHERE name = 'bob';", &mut db).unwrap();
687 let users = db.get_table("users".to_string()).unwrap();
689 assert_eq!(users.rowids().len(), 2);
690 for r in users.rowids() {
691 assert_eq!(users.get_value("age", r), Some(Value::Integer(999)));
692 }
693
694 process_command("ROLLBACK;", &mut db).unwrap();
695 let users = db.get_table("users".to_string()).unwrap();
696 assert_eq!(users.rowids().len(), 3);
697 for r in users.rowids() {
699 assert_ne!(users.get_value("age", r), Some(Value::Integer(999)));
700 }
701 }
702
703 #[test]
704 fn nested_begin_is_rejected() {
705 let mut db = seed_users_table();
706 process_command("BEGIN;", &mut db).unwrap();
707 let err = process_command("BEGIN;", &mut db).unwrap_err();
708 assert!(
709 format!("{err}").contains("already open"),
710 "nested BEGIN should error; got: {err}"
711 );
712 assert!(db.in_transaction());
714 process_command("ROLLBACK;", &mut db).unwrap();
715 }
716
717 #[test]
718 fn orphan_commit_and_rollback_are_rejected() {
719 let mut db = seed_users_table();
720 let commit_err = process_command("COMMIT;", &mut db).unwrap_err();
721 assert!(format!("{commit_err}").contains("no transaction"));
722 let rollback_err = process_command("ROLLBACK;", &mut db).unwrap_err();
723 assert!(format!("{rollback_err}").contains("no transaction"));
724 }
725
726 #[test]
727 fn error_inside_transaction_keeps_txn_open() {
728 let mut db = seed_users_table();
732 process_command("BEGIN;", &mut db).unwrap();
733 let err = process_command("INSERT INTO nope (x) VALUES (1);", &mut db);
734 assert!(err.is_err());
735 assert!(db.in_transaction(), "txn should stay open after error");
736 process_command("ROLLBACK;", &mut db).unwrap();
737 }
738
739 fn seed_file_backed(name: &str, schema: &str) -> (std::path::PathBuf, Database) {
744 use crate::sql::pager::{open_database, save_database};
745 let mut p = std::env::temp_dir();
746 let pid = std::process::id();
747 let nanos = std::time::SystemTime::now()
748 .duration_since(std::time::UNIX_EPOCH)
749 .map(|d| d.as_nanos())
750 .unwrap_or(0);
751 p.push(format!("sqlrite-txn-{name}-{pid}-{nanos}.sqlrite"));
752
753 {
757 let mut seed = Database::new("t".to_string());
758 process_command(schema, &mut seed).unwrap();
759 save_database(&mut seed, &p).unwrap();
760 }
761 let db = open_database(&p, "t".to_string()).unwrap();
762 (p, db)
763 }
764
765 fn cleanup_file(path: &std::path::Path) {
766 let _ = std::fs::remove_file(path);
767 let mut wal = path.as_os_str().to_owned();
768 wal.push("-wal");
769 let _ = std::fs::remove_file(std::path::PathBuf::from(wal));
770 }
771
772 #[test]
773 fn begin_commit_rollback_round_trip_through_disk() {
774 use crate::sql::pager::open_database;
778
779 let (path, mut db) = seed_file_backed(
780 "roundtrip",
781 "CREATE TABLE notes (id INTEGER PRIMARY KEY, body TEXT);",
782 );
783
784 process_command("BEGIN;", &mut db).unwrap();
786 process_command("INSERT INTO notes (body) VALUES ('a');", &mut db).unwrap();
787 process_command("INSERT INTO notes (body) VALUES ('b');", &mut db).unwrap();
788 process_command("COMMIT;", &mut db).unwrap();
789
790 process_command("BEGIN;", &mut db).unwrap();
792 process_command("INSERT INTO notes (body) VALUES ('c');", &mut db).unwrap();
793 process_command("ROLLBACK;", &mut db).unwrap();
794
795 drop(db); let reopened = open_database(&path, "t".to_string()).unwrap();
798 let notes = reopened.get_table("notes".to_string()).unwrap();
799 assert_eq!(notes.rowids().len(), 2, "committed rows should survive");
800
801 drop(reopened);
802 cleanup_file(&path);
803 }
804
805 #[test]
806 fn write_inside_transaction_does_not_autosave() {
807 let (path, mut db) =
811 seed_file_backed("noas", "CREATE TABLE t (id INTEGER PRIMARY KEY, x TEXT);");
812
813 let mut wal_path = path.as_os_str().to_owned();
814 wal_path.push("-wal");
815 let wal_path = std::path::PathBuf::from(wal_path);
816 let frames_before = std::fs::metadata(&wal_path).unwrap().len();
817
818 process_command("BEGIN;", &mut db).unwrap();
819 process_command("INSERT INTO t (x) VALUES ('a');", &mut db).unwrap();
820 process_command("INSERT INTO t (x) VALUES ('b');", &mut db).unwrap();
821
822 let frames_mid = std::fs::metadata(&wal_path).unwrap().len();
824 assert_eq!(
825 frames_before, frames_mid,
826 "WAL should not grow during an open transaction"
827 );
828
829 process_command("COMMIT;", &mut db).unwrap();
830
831 drop(db); let fresh = crate::sql::pager::open_database(&path, "t".to_string()).unwrap();
833 assert_eq!(
834 fresh.get_table("t".to_string()).unwrap().rowids().len(),
835 2,
836 "COMMIT should have persisted both inserted rows"
837 );
838 drop(fresh);
839 cleanup_file(&path);
840 }
841
842 #[test]
843 fn rollback_undoes_create_table() {
844 let mut db = seed_users_table();
849 assert_eq!(db.tables.len(), 1);
850
851 process_command("BEGIN;", &mut db).unwrap();
852 process_command(
853 "CREATE TABLE dropme (id INTEGER PRIMARY KEY, x TEXT);",
854 &mut db,
855 )
856 .unwrap();
857 process_command("INSERT INTO dropme (x) VALUES ('stuff');", &mut db).unwrap();
858 assert_eq!(db.tables.len(), 2);
859
860 process_command("ROLLBACK;", &mut db).unwrap();
861 assert_eq!(
862 db.tables.len(),
863 1,
864 "CREATE TABLE should have been rolled back"
865 );
866 assert!(db.get_table("dropme".to_string()).is_err());
867 }
868
869 #[test]
870 fn rollback_restores_secondary_index_state() {
871 let mut db = Database::new("t".to_string());
875 process_command(
876 "CREATE TABLE users (id INTEGER PRIMARY KEY, email TEXT UNIQUE);",
877 &mut db,
878 )
879 .unwrap();
880 process_command("INSERT INTO users (email) VALUES ('a@x');", &mut db).unwrap();
881
882 process_command("BEGIN;", &mut db).unwrap();
883 process_command("INSERT INTO users (email) VALUES ('b@x');", &mut db).unwrap();
884 process_command("ROLLBACK;", &mut db).unwrap();
886
887 let reinsert = process_command("INSERT INTO users (email) VALUES ('b@x');", &mut db);
891 assert!(
892 reinsert.is_ok(),
893 "re-insert after rollback should succeed, got {reinsert:?}"
894 );
895 }
896
897 #[test]
898 fn rollback_restores_last_rowid_counter() {
899 use crate::sql::db::table::Value;
903
904 let mut db = seed_users_table(); let pre = db.get_table("users".to_string()).unwrap().last_rowid;
906
907 process_command("BEGIN;", &mut db).unwrap();
908 process_command("INSERT INTO users (name, age) VALUES ('d', 50);", &mut db).unwrap(); process_command("INSERT INTO users (name, age) VALUES ('e', 60);", &mut db).unwrap(); process_command("ROLLBACK;", &mut db).unwrap();
911
912 let post = db.get_table("users".to_string()).unwrap().last_rowid;
913 assert_eq!(pre, post, "last_rowid must roll back with the snapshot");
914
915 process_command("INSERT INTO users (name, age) VALUES ('d', 50);", &mut db).unwrap();
917 let users = db.get_table("users".to_string()).unwrap();
918 let d_rowid = users
919 .rowids()
920 .into_iter()
921 .find(|r| users.get_value("name", *r) == Some(Value::Text("d".into())))
922 .expect("d row must exist");
923 assert_eq!(d_rowid, pre + 1);
924 }
925
926 #[test]
927 fn commit_on_in_memory_db_clears_txn_without_pager_call() {
928 let mut db = seed_users_table(); assert!(db.source_path.is_none());
933
934 process_command("BEGIN;", &mut db).unwrap();
935 process_command("INSERT INTO users (name, age) VALUES ('z', 99);", &mut db).unwrap();
936 process_command("COMMIT;", &mut db).unwrap();
937
938 assert!(!db.in_transaction());
939 assert_eq!(db.get_table("users".to_string()).unwrap().rowids().len(), 4);
940 }
941
942 #[test]
943 fn failed_commit_auto_rolls_back_in_memory_state() {
944 use crate::sql::pager::save_database;
958
959 let (path, mut db) = seed_file_backed(
961 "failcommit",
962 "CREATE TABLE notes (id INTEGER PRIMARY KEY, body TEXT);",
963 );
964
965 process_command("INSERT INTO notes (body) VALUES ('before');", &mut db).unwrap();
967
968 process_command("BEGIN;", &mut db).unwrap();
970 process_command("INSERT INTO notes (body) VALUES ('inflight');", &mut db).unwrap();
971 assert_eq!(
972 db.get_table("notes".to_string()).unwrap().rowids().len(),
973 2,
974 "inflight row visible mid-txn"
975 );
976
977 let orig_source = db.source_path.clone();
981 let orig_pager = db.pager.take();
982 db.source_path = Some(std::env::temp_dir());
983
984 let commit_result = process_command("COMMIT;", &mut db);
985 assert!(commit_result.is_err(), "commit must fail");
986 let err_str = format!("{}", commit_result.unwrap_err());
987 assert!(
988 err_str.contains("COMMIT failed") && err_str.contains("rolled back"),
989 "error must surface auto-rollback; got: {err_str}"
990 );
991
992 assert!(
996 !db.in_transaction(),
997 "txn must be cleared after auto-rollback"
998 );
999 assert_eq!(
1000 db.get_table("notes".to_string()).unwrap().rowids().len(),
1001 1,
1002 "inflight row must be rolled back"
1003 );
1004
1005 db.source_path = orig_source;
1008 db.pager = orig_pager;
1009 process_command("INSERT INTO notes (body) VALUES ('after');", &mut db).unwrap();
1010 drop(db);
1011
1012 let reopened = crate::sql::pager::open_database(&path, "t".to_string()).unwrap();
1014 let notes = reopened.get_table("notes".to_string()).unwrap();
1015 assert_eq!(notes.rowids().len(), 2);
1016 let _ = save_database; drop(reopened);
1019 cleanup_file(&path);
1020 }
1021
1022 #[test]
1023 fn begin_on_read_only_is_rejected() {
1024 use crate::sql::pager::{open_database_read_only, save_database};
1025
1026 let path = {
1027 let mut p = std::env::temp_dir();
1028 let pid = std::process::id();
1029 let nanos = std::time::SystemTime::now()
1030 .duration_since(std::time::UNIX_EPOCH)
1031 .map(|d| d.as_nanos())
1032 .unwrap_or(0);
1033 p.push(format!("sqlrite-txn-ro-{pid}-{nanos}.sqlrite"));
1034 p
1035 };
1036 {
1037 let mut seed = Database::new("t".to_string());
1038 process_command("CREATE TABLE t (id INTEGER PRIMARY KEY);", &mut seed).unwrap();
1039 save_database(&mut seed, &path).unwrap();
1040 }
1041
1042 let mut ro = open_database_read_only(&path, "t".to_string()).unwrap();
1043 let err = process_command("BEGIN;", &mut ro).unwrap_err();
1044 assert!(
1045 format!("{err}").contains("read-only"),
1046 "BEGIN on RO db should surface read-only; got: {err}"
1047 );
1048 assert!(!ro.in_transaction());
1049
1050 let _ = std::fs::remove_file(&path);
1051 let mut wal = path.as_os_str().to_owned();
1052 wal.push("-wal");
1053 let _ = std::fs::remove_file(std::path::PathBuf::from(wal));
1054 }
1055
1056 #[test]
1057 fn read_only_database_rejects_mutations_before_touching_state() {
1058 use crate::sql::pager::open_database_read_only;
1064
1065 let mut seed = Database::new("t".to_string());
1066 process_command(
1067 "CREATE TABLE notes (id INTEGER PRIMARY KEY, body TEXT);",
1068 &mut seed,
1069 )
1070 .unwrap();
1071 process_command("INSERT INTO notes (body) VALUES ('alpha');", &mut seed).unwrap();
1072
1073 let path = {
1074 let mut p = std::env::temp_dir();
1075 let pid = std::process::id();
1076 let nanos = std::time::SystemTime::now()
1077 .duration_since(std::time::UNIX_EPOCH)
1078 .map(|d| d.as_nanos())
1079 .unwrap_or(0);
1080 p.push(format!("sqlrite-ro-reject-{pid}-{nanos}.sqlrite"));
1081 p
1082 };
1083 crate::sql::pager::save_database(&mut seed, &path).unwrap();
1084 drop(seed);
1085
1086 let mut ro = open_database_read_only(&path, "t".to_string()).unwrap();
1087 let notes_before = ro.get_table("notes".to_string()).unwrap().rowids().len();
1088
1089 for stmt in [
1090 "INSERT INTO notes (body) VALUES ('beta');",
1091 "UPDATE notes SET body = 'x';",
1092 "DELETE FROM notes;",
1093 "CREATE TABLE more (id INTEGER PRIMARY KEY);",
1094 "CREATE INDEX notes_body ON notes (body);",
1095 ] {
1096 let err = process_command(stmt, &mut ro).unwrap_err();
1097 assert!(
1098 format!("{err}").contains("read-only"),
1099 "stmt {stmt:?} should surface a read-only error; got: {err}"
1100 );
1101 }
1102
1103 let notes_after = ro.get_table("notes".to_string()).unwrap().rowids().len();
1105 assert_eq!(notes_before, notes_after);
1106 let sel = process_command("SELECT * FROM notes;", &mut ro).expect("select on RO must work");
1107 assert!(sel.contains("1 row returned"));
1108
1109 drop(ro);
1111 let _ = std::fs::remove_file(&path);
1112 let mut wal = path.as_os_str().to_owned();
1113 wal.push("-wal");
1114 let _ = std::fs::remove_file(std::path::PathBuf::from(wal));
1115 }
1116
1117 #[test]
1122 fn vector_create_table_and_insert_basic() {
1123 let mut db = Database::new("tempdb".to_string());
1124 process_command(
1125 "CREATE TABLE docs (id INTEGER PRIMARY KEY, embedding VECTOR(3));",
1126 &mut db,
1127 )
1128 .expect("create table with VECTOR(3)");
1129 process_command(
1130 "INSERT INTO docs (embedding) VALUES ([0.1, 0.2, 0.3]);",
1131 &mut db,
1132 )
1133 .expect("insert vector");
1134
1135 let sel = process_command("SELECT * FROM docs;", &mut db).expect("select");
1139 assert!(sel.contains("1 row returned"));
1140
1141 let docs = db.get_table("docs".to_string()).expect("docs table");
1142 let rowids = docs.rowids();
1143 assert_eq!(rowids.len(), 1);
1144 match docs.get_value("embedding", rowids[0]) {
1145 Some(Value::Vector(v)) => assert_eq!(v, vec![0.1f32, 0.2, 0.3]),
1146 other => panic!("expected Value::Vector(...), got {other:?}"),
1147 }
1148 }
1149
1150 #[test]
1151 fn vector_dim_mismatch_at_insert_is_clean_error() {
1152 let mut db = Database::new("tempdb".to_string());
1153 process_command(
1154 "CREATE TABLE docs (id INTEGER PRIMARY KEY, embedding VECTOR(3));",
1155 &mut db,
1156 )
1157 .expect("create table");
1158
1159 let err = process_command("INSERT INTO docs (embedding) VALUES ([0.1, 0.2]);", &mut db)
1161 .unwrap_err();
1162 let msg = format!("{err}");
1163 assert!(
1164 msg.to_lowercase().contains("dimension")
1165 && msg.contains("declared 3")
1166 && msg.contains("got 2"),
1167 "expected clear dim-mismatch error, got: {msg}"
1168 );
1169
1170 let err = process_command(
1172 "INSERT INTO docs (embedding) VALUES ([0.1, 0.2, 0.3, 0.4, 0.5]);",
1173 &mut db,
1174 )
1175 .unwrap_err();
1176 assert!(
1177 format!("{err}").contains("got 5"),
1178 "expected dim-mismatch error mentioning got 5, got: {err}"
1179 );
1180 }
1181
1182 #[test]
1183 fn vector_create_table_rejects_missing_dim() {
1184 let mut db = Database::new("tempdb".to_string());
1185 let result = process_command(
1191 "CREATE TABLE docs (id INTEGER PRIMARY KEY, embedding VECTOR);",
1192 &mut db,
1193 );
1194 assert!(
1195 result.is_err(),
1196 "expected CREATE TABLE with bare VECTOR to fail (no dim)"
1197 );
1198 }
1199
1200 #[test]
1201 fn vector_create_table_rejects_zero_dim() {
1202 let mut db = Database::new("tempdb".to_string());
1203 let err = process_command(
1204 "CREATE TABLE docs (id INTEGER PRIMARY KEY, embedding VECTOR(0));",
1205 &mut db,
1206 )
1207 .unwrap_err();
1208 let msg = format!("{err}");
1209 assert!(
1210 msg.to_lowercase().contains("vector"),
1211 "expected VECTOR-related error for VECTOR(0), got: {msg}"
1212 );
1213 }
1214
1215 #[test]
1216 fn vector_high_dim_works() {
1217 let mut db = Database::new("tempdb".to_string());
1220 process_command(
1221 "CREATE TABLE embeddings (id INTEGER PRIMARY KEY, e VECTOR(384));",
1222 &mut db,
1223 )
1224 .expect("create table VECTOR(384)");
1225
1226 let lit = format!(
1227 "[{}]",
1228 (0..384)
1229 .map(|i| format!("{}", i as f32 * 0.001))
1230 .collect::<Vec<_>>()
1231 .join(",")
1232 );
1233 let sql = format!("INSERT INTO embeddings (e) VALUES ({lit});");
1234 process_command(&sql, &mut db).expect("insert 384-dim vector");
1235
1236 let sel = process_command("SELECT id FROM embeddings;", &mut db).expect("select id");
1237 assert!(sel.contains("1 row returned"));
1238 }
1239
1240 #[test]
1241 fn vector_multiple_rows() {
1242 let mut db = Database::new("tempdb".to_string());
1245 process_command(
1246 "CREATE TABLE docs (id INTEGER PRIMARY KEY, e VECTOR(2));",
1247 &mut db,
1248 )
1249 .expect("create");
1250 for i in 0..3 {
1251 let sql = format!("INSERT INTO docs (e) VALUES ([{i}.0, {}.0]);", i + 1);
1252 process_command(&sql, &mut db).expect("insert");
1253 }
1254 let sel = process_command("SELECT * FROM docs;", &mut db).expect("select");
1255 assert!(sel.contains("3 rows returned"));
1256
1257 let docs = db.get_table("docs".to_string()).expect("docs table");
1259 let rowids = docs.rowids();
1260 assert_eq!(rowids.len(), 3);
1261 let mut vectors: Vec<Vec<f32>> = rowids
1262 .iter()
1263 .filter_map(|r| match docs.get_value("e", *r) {
1264 Some(Value::Vector(v)) => Some(v),
1265 _ => None,
1266 })
1267 .collect();
1268 vectors.sort_by(|a, b| a[0].partial_cmp(&b[0]).unwrap());
1269 assert_eq!(vectors[0], vec![0.0f32, 1.0]);
1270 assert_eq!(vectors[1], vec![1.0f32, 2.0]);
1271 assert_eq!(vectors[2], vec![2.0f32, 3.0]);
1272 }
1273
1274 fn seed_hnsw_table() -> Database {
1282 let mut db = Database::new("tempdb".to_string());
1283 process_command(
1284 "CREATE TABLE docs (id INTEGER PRIMARY KEY, e VECTOR(2));",
1285 &mut db,
1286 )
1287 .unwrap();
1288 for v in &[
1289 "[1.0, 0.0]", "[2.0, 0.0]", "[0.0, 3.0]", "[1.0, 4.0]", "[10.0, 10.0]", ] {
1295 process_command(&format!("INSERT INTO docs (e) VALUES ({v});"), &mut db).unwrap();
1296 }
1297 db
1298 }
1299
1300 #[test]
1301 fn create_index_using_hnsw_succeeds() {
1302 let mut db = seed_hnsw_table();
1303 let resp = process_command("CREATE INDEX ix_e ON docs USING hnsw (e);", &mut db).unwrap();
1304 assert!(resp.to_lowercase().contains("create index"));
1305 let table = db.get_table("docs".to_string()).unwrap();
1307 assert_eq!(table.hnsw_indexes.len(), 1);
1308 assert_eq!(table.hnsw_indexes[0].name, "ix_e");
1309 assert_eq!(table.hnsw_indexes[0].column_name, "e");
1310 assert_eq!(table.hnsw_indexes[0].index.len(), 5);
1312 }
1313
1314 #[test]
1315 fn create_index_using_hnsw_rejects_non_vector_column() {
1316 let mut db = Database::new("tempdb".to_string());
1317 process_command(
1318 "CREATE TABLE t (id INTEGER PRIMARY KEY, name TEXT);",
1319 &mut db,
1320 )
1321 .unwrap();
1322 let err =
1323 process_command("CREATE INDEX ix_name ON t USING hnsw (name);", &mut db).unwrap_err();
1324 let msg = format!("{err}");
1325 assert!(
1326 msg.to_lowercase().contains("vector"),
1327 "expected error mentioning VECTOR; got: {msg}"
1328 );
1329 }
1330
1331 #[test]
1332 fn knn_query_uses_hnsw_after_create_index() {
1333 let mut db = seed_hnsw_table();
1339 process_command("CREATE INDEX ix_e ON docs USING hnsw (e);", &mut db).unwrap();
1340
1341 let resp = process_command(
1346 "SELECT id FROM docs ORDER BY vec_distance_l2(e, [1.0, 0.0]) ASC LIMIT 3;",
1347 &mut db,
1348 )
1349 .unwrap();
1350 assert!(resp.contains("3 rows returned"), "got: {resp}");
1351 }
1352
1353 #[test]
1354 fn knn_query_works_after_subsequent_inserts() {
1355 let mut db = seed_hnsw_table();
1359 process_command("CREATE INDEX ix_e ON docs USING hnsw (e);", &mut db).unwrap();
1360 process_command("INSERT INTO docs (e) VALUES ([0.5, 0.0]);", &mut db).unwrap(); process_command("INSERT INTO docs (e) VALUES ([0.1, 0.1]);", &mut db).unwrap(); let table = db.get_table("docs".to_string()).unwrap();
1364 assert_eq!(
1365 table.hnsw_indexes[0].index.len(),
1366 7,
1367 "incremental insert should grow HNSW alongside row storage"
1368 );
1369
1370 let resp = process_command(
1373 "SELECT id FROM docs ORDER BY vec_distance_l2(e, [0.0, 0.0]) ASC LIMIT 1;",
1374 &mut db,
1375 )
1376 .unwrap();
1377 assert!(resp.contains("1 row returned"), "got: {resp}");
1378 }
1379
1380 #[test]
1386 fn delete_on_hnsw_indexed_table_succeeds_and_marks_dirty() {
1387 let mut db = seed_hnsw_table();
1388 process_command("CREATE INDEX ix_e ON docs USING hnsw (e);", &mut db).unwrap();
1389 let resp = process_command("DELETE FROM docs WHERE id = 1;", &mut db).unwrap();
1390 assert!(resp.contains("1 row"), "expected 1 row deleted: {resp}");
1391
1392 let docs = db.get_table("docs".to_string()).unwrap();
1393 let entry = docs.hnsw_indexes.iter().find(|e| e.name == "ix_e").unwrap();
1394 assert!(
1395 entry.needs_rebuild,
1396 "DELETE should have marked HNSW index dirty for rebuild on next save"
1397 );
1398 }
1399
1400 #[test]
1401 fn update_on_hnsw_indexed_vector_col_succeeds_and_marks_dirty() {
1402 let mut db = seed_hnsw_table();
1403 process_command("CREATE INDEX ix_e ON docs USING hnsw (e);", &mut db).unwrap();
1404 let resp =
1405 process_command("UPDATE docs SET e = [9.0, 9.0] WHERE id = 1;", &mut db).unwrap();
1406 assert!(resp.contains("1 row"), "expected 1 row updated: {resp}");
1407
1408 let docs = db.get_table("docs".to_string()).unwrap();
1409 let entry = docs.hnsw_indexes.iter().find(|e| e.name == "ix_e").unwrap();
1410 assert!(
1411 entry.needs_rebuild,
1412 "UPDATE on the vector column should have marked HNSW index dirty"
1413 );
1414 }
1415
1416 #[test]
1417 fn duplicate_index_name_errors() {
1418 let mut db = seed_hnsw_table();
1419 process_command("CREATE INDEX ix_e ON docs USING hnsw (e);", &mut db).unwrap();
1420 let err =
1421 process_command("CREATE INDEX ix_e ON docs USING hnsw (e);", &mut db).unwrap_err();
1422 let msg = format!("{err}");
1423 assert!(
1424 msg.to_lowercase().contains("already exists"),
1425 "expected duplicate-index error; got: {msg}"
1426 );
1427 }
1428
1429 #[test]
1430 fn index_if_not_exists_is_idempotent() {
1431 let mut db = seed_hnsw_table();
1432 process_command("CREATE INDEX ix_e ON docs USING hnsw (e);", &mut db).unwrap();
1433 process_command(
1435 "CREATE INDEX IF NOT EXISTS ix_e ON docs USING hnsw (e);",
1436 &mut db,
1437 )
1438 .unwrap();
1439 let table = db.get_table("docs".to_string()).unwrap();
1440 assert_eq!(table.hnsw_indexes.len(), 1);
1441 }
1442
1443 fn seed_vector_docs() -> Database {
1453 let mut db = Database::new("tempdb".to_string());
1454 process_command(
1455 "CREATE TABLE docs (id INTEGER PRIMARY KEY, e VECTOR(2));",
1456 &mut db,
1457 )
1458 .expect("create");
1459 process_command("INSERT INTO docs (e) VALUES ([1.0, 0.0]);", &mut db).expect("insert 1");
1460 process_command("INSERT INTO docs (e) VALUES ([0.0, 1.0]);", &mut db).expect("insert 2");
1461 process_command("INSERT INTO docs (e) VALUES ([1.0, 1.0]);", &mut db).expect("insert 3");
1462 db
1463 }
1464
1465 #[test]
1466 fn vec_distance_l2_in_where_filters_correctly() {
1467 let mut db = seed_vector_docs();
1473 let resp = process_command(
1474 "SELECT * FROM docs WHERE vec_distance_l2(e, [1.0, 0.0]) < 1.1;",
1475 &mut db,
1476 )
1477 .expect("select");
1478 assert!(
1479 resp.contains("2 rows returned"),
1480 "expected 2 rows, got: {resp}"
1481 );
1482 }
1483
1484 #[test]
1485 fn vec_distance_cosine_in_where() {
1486 let mut db = seed_vector_docs();
1491 let resp = process_command(
1492 "SELECT * FROM docs WHERE vec_distance_cosine(e, [1.0, 0.0]) < 0.5;",
1493 &mut db,
1494 )
1495 .expect("select");
1496 assert!(
1497 resp.contains("2 rows returned"),
1498 "expected 2 rows, got: {resp}"
1499 );
1500 }
1501
1502 #[test]
1503 fn vec_distance_dot_negated() {
1504 let mut db = seed_vector_docs();
1509 let resp = process_command(
1510 "SELECT * FROM docs WHERE vec_distance_dot(e, [1.0, 0.0]) < 0.0;",
1511 &mut db,
1512 )
1513 .expect("select");
1514 assert!(
1515 resp.contains("2 rows returned"),
1516 "expected 2 rows, got: {resp}"
1517 );
1518 }
1519
1520 #[test]
1521 fn knn_via_order_by_distance_limit() {
1522 let mut db = seed_vector_docs();
1526 let resp = process_command(
1527 "SELECT id FROM docs ORDER BY vec_distance_l2(e, [1.0, 0.0]) ASC LIMIT 2;",
1528 &mut db,
1529 )
1530 .expect("select");
1531 assert!(
1532 resp.contains("2 rows returned"),
1533 "expected 2 rows, got: {resp}"
1534 );
1535 }
1536
1537 #[test]
1538 fn distance_function_dim_mismatch_errors() {
1539 let mut db = seed_vector_docs();
1541 let err = process_command(
1542 "SELECT * FROM docs WHERE vec_distance_l2(e, [1.0, 0.0, 0.0]) < 1.0;",
1543 &mut db,
1544 )
1545 .unwrap_err();
1546 let msg = format!("{err}");
1547 assert!(
1548 msg.to_lowercase().contains("dimension")
1549 && msg.contains("lhs=2")
1550 && msg.contains("rhs=3"),
1551 "expected dim mismatch error, got: {msg}"
1552 );
1553 }
1554
1555 #[test]
1556 fn unknown_function_errors_with_name() {
1557 let mut db = seed_vector_docs();
1561 let err = process_command(
1562 "SELECT * FROM docs WHERE vec_does_not_exist(e, [1.0, 0.0]) < 1.0;",
1563 &mut db,
1564 )
1565 .unwrap_err();
1566 let msg = format!("{err}");
1567 assert!(
1568 msg.contains("vec_does_not_exist"),
1569 "expected error mentioning function name, got: {msg}"
1570 );
1571 }
1572
1573 fn seed_json_table() -> Database {
1578 let mut db = Database::new("tempdb".to_string());
1579 process_command(
1580 "CREATE TABLE docs (id INTEGER PRIMARY KEY, payload JSON);",
1581 &mut db,
1582 )
1583 .expect("create json table");
1584 db
1585 }
1586
1587 #[test]
1588 fn json_column_round_trip_primitive_values() {
1589 let mut db = seed_json_table();
1590 process_command(
1591 r#"INSERT INTO docs (payload) VALUES ('{"name": "alice", "age": 30}');"#,
1592 &mut db,
1593 )
1594 .expect("insert json");
1595 let docs = db.get_table("docs".to_string()).unwrap();
1596 let rowids = docs.rowids();
1597 assert_eq!(rowids.len(), 1);
1598 match docs.get_value("payload", rowids[0]) {
1600 Some(Value::Text(s)) => {
1601 assert!(s.contains("alice"), "expected JSON text to round-trip: {s}");
1602 }
1603 other => panic!("expected Value::Text holding JSON, got {other:?}"),
1604 }
1605 }
1606
1607 #[test]
1608 fn json_insert_rejects_invalid_json() {
1609 let mut db = seed_json_table();
1610 let err = process_command(
1611 "INSERT INTO docs (payload) VALUES ('not-valid-json{');",
1612 &mut db,
1613 )
1614 .unwrap_err();
1615 let msg = format!("{err}").to_lowercase();
1616 assert!(
1617 msg.contains("json") && msg.contains("payload"),
1618 "expected JSON validation error mentioning column, got: {msg}"
1619 );
1620 }
1621
1622 #[test]
1623 fn json_extract_object_field() {
1624 let mut db = seed_json_table();
1625 process_command(
1626 r#"INSERT INTO docs (payload) VALUES ('{"name": "alice", "age": 30}');"#,
1627 &mut db,
1628 )
1629 .unwrap();
1630 let resp = process_command(
1633 r#"SELECT id FROM docs WHERE json_extract(payload, '$.name') = 'alice';"#,
1634 &mut db,
1635 )
1636 .expect("select via json_extract");
1637 assert!(resp.contains("1 row returned"), "got: {resp}");
1638
1639 let resp = process_command(
1640 r#"SELECT id FROM docs WHERE json_extract(payload, '$.age') = 30;"#,
1641 &mut db,
1642 )
1643 .expect("select via numeric json_extract");
1644 assert!(resp.contains("1 row returned"), "got: {resp}");
1645 }
1646
1647 #[test]
1648 fn json_extract_array_index_and_nested() {
1649 let mut db = seed_json_table();
1650 process_command(
1651 r#"INSERT INTO docs (payload) VALUES ('{"tags": ["rust", "sql", "vectors"], "meta": {"author": "joao"}}');"#,
1652 &mut db,
1653 )
1654 .unwrap();
1655 let resp = process_command(
1656 r#"SELECT id FROM docs WHERE json_extract(payload, '$.tags[0]') = 'rust';"#,
1657 &mut db,
1658 )
1659 .expect("select via array index");
1660 assert!(resp.contains("1 row returned"), "got: {resp}");
1661
1662 let resp = process_command(
1663 r#"SELECT id FROM docs WHERE json_extract(payload, '$.meta.author') = 'joao';"#,
1664 &mut db,
1665 )
1666 .expect("select via nested object");
1667 assert!(resp.contains("1 row returned"), "got: {resp}");
1668 }
1669
1670 #[test]
1671 fn json_extract_missing_path_returns_null() {
1672 let mut db = seed_json_table();
1673 process_command(
1674 r#"INSERT INTO docs (payload) VALUES ('{"name": "alice"}');"#,
1675 &mut db,
1676 )
1677 .unwrap();
1678 let resp = process_command(
1681 r#"SELECT id FROM docs WHERE json_extract(payload, '$.missing') = 'something';"#,
1682 &mut db,
1683 )
1684 .expect("select with missing path");
1685 assert!(resp.contains("0 rows returned"), "got: {resp}");
1686 }
1687
1688 #[test]
1689 fn json_extract_malformed_path_errors() {
1690 let mut db = seed_json_table();
1691 process_command(
1692 r#"INSERT INTO docs (payload) VALUES ('{"a": 1}');"#,
1693 &mut db,
1694 )
1695 .unwrap();
1696 let err = process_command(
1698 r#"SELECT id FROM docs WHERE json_extract(payload, 'a.b') = 1;"#,
1699 &mut db,
1700 )
1701 .unwrap_err();
1702 assert!(format!("{err}").contains("'$'"));
1703 }
1704
1705 #[test]
1706 fn json_array_length_on_array() {
1707 let mut db = seed_json_table();
1710 process_command(
1711 r#"INSERT INTO docs (payload) VALUES ('{"tags": ["a", "b", "c"]}');"#,
1712 &mut db,
1713 )
1714 .unwrap();
1715 let resp = process_command(
1716 r#"SELECT id FROM docs WHERE json_array_length(payload, '$.tags') = 3;"#,
1717 &mut db,
1718 )
1719 .expect("select via array_length");
1720 assert!(resp.contains("1 row returned"), "got: {resp}");
1721 }
1722
1723 #[test]
1724 fn json_array_length_on_non_array_errors() {
1725 let mut db = seed_json_table();
1726 process_command(
1727 r#"INSERT INTO docs (payload) VALUES ('{"tags": "not-an-array"}');"#,
1728 &mut db,
1729 )
1730 .unwrap();
1731 let err = process_command(
1732 r#"SELECT id FROM docs WHERE json_array_length(payload, '$.tags') = 1;"#,
1733 &mut db,
1734 )
1735 .unwrap_err();
1736 let msg = format!("{err}").to_lowercase();
1737 assert!(
1738 msg.contains("non-array"),
1739 "expected non-array error, got: {msg}"
1740 );
1741 }
1742
1743 #[test]
1744 fn json_type_recognizes_each_kind() {
1745 let mut db = seed_json_table();
1746 process_command(
1747 r#"INSERT INTO docs (payload) VALUES ('{"o": {}, "a": [], "s": "x", "i": 1, "f": 1.5, "t": true, "n": null}');"#,
1748 &mut db,
1749 )
1750 .unwrap();
1751 let cases = &[
1752 ("$.o", "object"),
1753 ("$.a", "array"),
1754 ("$.s", "text"),
1755 ("$.i", "integer"),
1756 ("$.f", "real"),
1757 ("$.t", "true"),
1758 ("$.n", "null"),
1759 ];
1760 for (path, expected_type) in cases {
1761 let sql = format!(
1762 "SELECT id FROM docs WHERE json_type(payload, '{path}') = '{expected_type}';"
1763 );
1764 let resp =
1765 process_command(&sql, &mut db).unwrap_or_else(|e| panic!("path {path}: {e}"));
1766 assert!(
1767 resp.contains("1 row returned"),
1768 "path {path} expected type {expected_type}; got response: {resp}"
1769 );
1770 }
1771 }
1772
1773 #[test]
1774 fn update_on_json_column_revalidates() {
1775 let mut db = seed_json_table();
1776 process_command(
1777 r#"INSERT INTO docs (payload) VALUES ('{"a": 1}');"#,
1778 &mut db,
1779 )
1780 .unwrap();
1781 process_command(
1783 r#"UPDATE docs SET payload = '{"a": 2, "b": 3}' WHERE id = 1;"#,
1784 &mut db,
1785 )
1786 .expect("valid JSON UPDATE");
1787 let err = process_command(
1790 r#"UPDATE docs SET payload = 'not-json{' WHERE id = 1;"#,
1791 &mut db,
1792 )
1793 .unwrap_err();
1794 let msg = format!("{err}").to_lowercase();
1795 assert!(
1796 msg.contains("json") && msg.contains("payload"),
1797 "got: {msg}"
1798 );
1799 }
1800}