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_vector_docs() -> Database {
1284 let mut db = Database::new("tempdb".to_string());
1285 process_command(
1286 "CREATE TABLE docs (id INTEGER PRIMARY KEY, e VECTOR(2));",
1287 &mut db,
1288 )
1289 .expect("create");
1290 process_command("INSERT INTO docs (e) VALUES ([1.0, 0.0]);", &mut db).expect("insert 1");
1291 process_command("INSERT INTO docs (e) VALUES ([0.0, 1.0]);", &mut db).expect("insert 2");
1292 process_command("INSERT INTO docs (e) VALUES ([1.0, 1.0]);", &mut db).expect("insert 3");
1293 db
1294 }
1295
1296 #[test]
1297 fn vec_distance_l2_in_where_filters_correctly() {
1298 let mut db = seed_vector_docs();
1304 let resp = process_command(
1305 "SELECT * FROM docs WHERE vec_distance_l2(e, [1.0, 0.0]) < 1.1;",
1306 &mut db,
1307 )
1308 .expect("select");
1309 assert!(
1310 resp.contains("2 rows returned"),
1311 "expected 2 rows, got: {resp}"
1312 );
1313 }
1314
1315 #[test]
1316 fn vec_distance_cosine_in_where() {
1317 let mut db = seed_vector_docs();
1322 let resp = process_command(
1323 "SELECT * FROM docs WHERE vec_distance_cosine(e, [1.0, 0.0]) < 0.5;",
1324 &mut db,
1325 )
1326 .expect("select");
1327 assert!(
1328 resp.contains("2 rows returned"),
1329 "expected 2 rows, got: {resp}"
1330 );
1331 }
1332
1333 #[test]
1334 fn vec_distance_dot_negated() {
1335 let mut db = seed_vector_docs();
1340 let resp = process_command(
1341 "SELECT * FROM docs WHERE vec_distance_dot(e, [1.0, 0.0]) < 0.0;",
1342 &mut db,
1343 )
1344 .expect("select");
1345 assert!(
1346 resp.contains("2 rows returned"),
1347 "expected 2 rows, got: {resp}"
1348 );
1349 }
1350
1351 #[test]
1352 fn knn_via_order_by_distance_limit() {
1353 let mut db = seed_vector_docs();
1357 let resp = process_command(
1358 "SELECT id FROM docs ORDER BY vec_distance_l2(e, [1.0, 0.0]) ASC LIMIT 2;",
1359 &mut db,
1360 )
1361 .expect("select");
1362 assert!(
1363 resp.contains("2 rows returned"),
1364 "expected 2 rows, got: {resp}"
1365 );
1366 }
1367
1368 #[test]
1369 fn distance_function_dim_mismatch_errors() {
1370 let mut db = seed_vector_docs();
1372 let err = process_command(
1373 "SELECT * FROM docs WHERE vec_distance_l2(e, [1.0, 0.0, 0.0]) < 1.0;",
1374 &mut db,
1375 )
1376 .unwrap_err();
1377 let msg = format!("{err}");
1378 assert!(
1379 msg.to_lowercase().contains("dimension")
1380 && msg.contains("lhs=2")
1381 && msg.contains("rhs=3"),
1382 "expected dim mismatch error, got: {msg}"
1383 );
1384 }
1385
1386 #[test]
1387 fn unknown_function_errors_with_name() {
1388 let mut db = seed_vector_docs();
1392 let err = process_command(
1393 "SELECT * FROM docs WHERE vec_does_not_exist(e, [1.0, 0.0]) < 1.0;",
1394 &mut db,
1395 )
1396 .unwrap_err();
1397 let msg = format!("{err}");
1398 assert!(
1399 msg.contains("vec_does_not_exist"),
1400 "expected error mentioning function name, got: {msg}"
1401 );
1402 }
1403}