1pub mod db;
2pub mod executor;
3pub mod pager;
4pub mod parser;
5use parser::create::CreateQuery;
8use parser::insert::InsertQuery;
9use parser::select::SelectQuery;
10
11use sqlparser::ast::Statement;
12use sqlparser::dialect::SQLiteDialect;
13use sqlparser::parser::{Parser, ParserError};
14
15use crate::error::{Result, SQLRiteError};
16use crate::sql::db::database::Database;
17use crate::sql::db::table::Table;
18
19#[derive(Debug, PartialEq)]
20pub enum SQLCommand {
21 Insert(String),
22 Delete(String),
23 Update(String),
24 CreateTable(String),
25 Select(String),
26 Unknown(String),
27}
28
29impl SQLCommand {
30 pub fn new(command: String) -> SQLCommand {
31 let v = command.split(" ").collect::<Vec<&str>>();
32 match v[0] {
33 "insert" => SQLCommand::Insert(command),
34 "update" => SQLCommand::Update(command),
35 "delete" => SQLCommand::Delete(command),
36 "create" => SQLCommand::CreateTable(command),
37 "select" => SQLCommand::Select(command),
38 _ => SQLCommand::Unknown(command),
39 }
40 }
41}
42
43pub fn process_command(query: &str, db: &mut Database) -> Result<String> {
45 let dialect = SQLiteDialect {};
46 let message: String;
47 let mut ast = Parser::parse_sql(&dialect, query).map_err(SQLRiteError::from)?;
48
49 if ast.len() > 1 {
50 return Err(SQLRiteError::SqlError(ParserError::ParserError(format!(
51 "Expected a single query statement, but there are {}",
52 ast.len()
53 ))));
54 }
55
56 let Some(query) = ast.pop() else {
60 return Ok("No statement to execute.".to_string());
61 };
62
63 match &query {
68 Statement::StartTransaction { .. } => {
69 db.begin_transaction()?;
70 return Ok(String::from("BEGIN"));
71 }
72 Statement::Commit { .. } => {
73 if !db.in_transaction() {
74 return Err(SQLRiteError::General(
75 "cannot COMMIT: no transaction is open".to_string(),
76 ));
77 }
78 if let Some(path) = db.source_path.clone() {
88 if let Err(save_err) = pager::save_database(db, &path) {
89 let _ = db.rollback_transaction();
90 return Err(SQLRiteError::General(format!(
91 "COMMIT failed — transaction rolled back: {save_err}"
92 )));
93 }
94 }
95 db.commit_transaction()?;
96 return Ok(String::from("COMMIT"));
97 }
98 Statement::Rollback { .. } => {
99 db.rollback_transaction()?;
100 return Ok(String::from("ROLLBACK"));
101 }
102 _ => {}
103 }
104
105 let is_write_statement = matches!(
108 &query,
109 Statement::CreateTable(_)
110 | Statement::CreateIndex(_)
111 | Statement::Insert(_)
112 | Statement::Update(_)
113 | Statement::Delete(_)
114 );
115
116 if is_write_statement && db.is_read_only() {
122 return Err(SQLRiteError::General(
123 "cannot execute: database is opened read-only".to_string(),
124 ));
125 }
126
127 match query {
129 Statement::CreateTable(_) => {
130 let create_query = CreateQuery::new(&query);
131 match create_query {
132 Ok(payload) => {
133 let table_name = payload.table_name.clone();
134 if table_name == pager::MASTER_TABLE_NAME {
135 return Err(SQLRiteError::General(format!(
136 "'{}' is a reserved name used by the internal schema catalog",
137 pager::MASTER_TABLE_NAME
138 )));
139 }
140 match db.contains_table(table_name.to_string()) {
142 true => {
143 return Err(SQLRiteError::Internal(
144 "Cannot create, table already exists.".to_string(),
145 ));
146 }
147 false => {
148 let table = Table::new(payload);
149 let _ = table.print_table_schema();
150 db.tables.insert(table_name.to_string(), table);
151 message = String::from("CREATE TABLE Statement executed.");
156 }
157 }
158 }
159 Err(err) => return Err(err),
160 }
161 }
162 Statement::Insert(_) => {
163 let insert_query = InsertQuery::new(&query);
164 match insert_query {
165 Ok(payload) => {
166 let table_name = payload.table_name;
167 let columns = payload.columns;
168 let values = payload.rows;
169
170 match db.contains_table(table_name.to_string()) {
173 true => {
174 let db_table = db.get_table_mut(table_name.to_string()).unwrap();
175 match columns
177 .iter()
178 .all(|column| db_table.contains_column(column.to_string()))
179 {
180 true => {
181 for value in &values {
182 if columns.len() != value.len() {
184 return Err(SQLRiteError::Internal(format!(
185 "{} values for {} columns",
186 value.len(),
187 columns.len()
188 )));
189 }
190 db_table
191 .validate_unique_constraint(&columns, value)
192 .map_err(|err| {
193 SQLRiteError::Internal(format!(
194 "Unique key constraint violation: {err}"
195 ))
196 })?;
197 db_table.insert_row(&columns, value)?;
198 }
199 }
200 false => {
201 return Err(SQLRiteError::Internal(
202 "Cannot insert, some of the columns do not exist"
203 .to_string(),
204 ));
205 }
206 }
207 db_table.print_table_data();
208 }
209 false => {
210 return Err(SQLRiteError::Internal("Table doesn't exist".to_string()));
211 }
212 }
213 }
214 Err(err) => return Err(err),
215 }
216
217 message = String::from("INSERT Statement executed.")
218 }
219 Statement::Query(_) => {
220 let select_query = SelectQuery::new(&query)?;
221 let (rendered, rows) = executor::execute_select(select_query, db)?;
222 print!("{rendered}");
224 message = format!(
225 "SELECT Statement executed. {rows} row{s} returned.",
226 s = if rows == 1 { "" } else { "s" }
227 );
228 }
229 Statement::Delete(_) => {
230 let rows = executor::execute_delete(&query, db)?;
231 message = format!(
232 "DELETE Statement executed. {rows} row{s} deleted.",
233 s = if rows == 1 { "" } else { "s" }
234 );
235 }
236 Statement::Update(_) => {
237 let rows = executor::execute_update(&query, db)?;
238 message = format!(
239 "UPDATE Statement executed. {rows} row{s} updated.",
240 s = if rows == 1 { "" } else { "s" }
241 );
242 }
243 Statement::CreateIndex(_) => {
244 let name = executor::execute_create_index(&query, db)?;
245 message = format!("CREATE INDEX '{name}' executed.");
246 }
247 _ => {
248 return Err(SQLRiteError::NotImplemented(
249 "SQL Statement not supported yet.".to_string(),
250 ));
251 }
252 };
253
254 if is_write_statement && db.source_path.is_some() && !db.in_transaction() {
265 let path = db.source_path.clone().unwrap();
266 pager::save_database(db, &path)?;
267 }
268
269 Ok(message)
270}
271
272#[cfg(test)]
273mod tests {
274 use super::*;
275 use crate::sql::db::table::Value;
276
277 fn seed_users_table() -> Database {
280 let mut db = Database::new("tempdb".to_string());
281 process_command(
282 "CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT NOT NULL, age INTEGER);",
283 &mut db,
284 )
285 .expect("create table");
286 process_command(
287 "INSERT INTO users (name, age) VALUES ('alice', 30);",
288 &mut db,
289 )
290 .expect("insert alice");
291 process_command("INSERT INTO users (name, age) VALUES ('bob', 25);", &mut db)
292 .expect("insert bob");
293 process_command(
294 "INSERT INTO users (name, age) VALUES ('carol', 40);",
295 &mut db,
296 )
297 .expect("insert carol");
298 db
299 }
300
301 #[test]
302 fn process_command_select_all_test() {
303 let mut db = seed_users_table();
304 let response = process_command("SELECT * FROM users;", &mut db).expect("select");
305 assert!(response.contains("3 rows returned"));
306 }
307
308 #[test]
309 fn process_command_select_where_test() {
310 let mut db = seed_users_table();
311 let response =
312 process_command("SELECT name FROM users WHERE age > 25;", &mut db).expect("select");
313 assert!(response.contains("2 rows returned"));
314 }
315
316 #[test]
317 fn process_command_select_eq_string_test() {
318 let mut db = seed_users_table();
319 let response =
320 process_command("SELECT name FROM users WHERE name = 'bob';", &mut db).expect("select");
321 assert!(response.contains("1 row returned"));
322 }
323
324 #[test]
325 fn process_command_select_limit_test() {
326 let mut db = seed_users_table();
327 let response = process_command("SELECT * FROM users ORDER BY age ASC LIMIT 2;", &mut db)
328 .expect("select");
329 assert!(response.contains("2 rows returned"));
330 }
331
332 #[test]
333 fn process_command_select_unknown_table_test() {
334 let mut db = Database::new("tempdb".to_string());
335 let result = process_command("SELECT * FROM nope;", &mut db);
336 assert!(result.is_err());
337 }
338
339 #[test]
340 fn process_command_select_unknown_column_test() {
341 let mut db = seed_users_table();
342 let result = process_command("SELECT height FROM users;", &mut db);
343 assert!(result.is_err());
344 }
345
346 #[test]
347 fn process_command_insert_test() {
348 let mut db = Database::new("tempdb".to_string());
350
351 let query_statement = "CREATE TABLE users (
353 id INTEGER PRIMARY KEY,
354 name TEXT
355 );";
356 let dialect = SQLiteDialect {};
357 let mut ast = Parser::parse_sql(&dialect, query_statement).unwrap();
358 if ast.len() > 1 {
359 panic!("Expected a single query statement, but there are more then 1.")
360 }
361 let query = ast.pop().unwrap();
362 let create_query = CreateQuery::new(&query).unwrap();
363
364 db.tables.insert(
366 create_query.table_name.to_string(),
367 Table::new(create_query),
368 );
369
370 let insert_query = String::from("INSERT INTO users (name) Values ('josh');");
372 match process_command(&insert_query, &mut db) {
373 Ok(response) => assert_eq!(response, "INSERT Statement executed."),
374 Err(err) => {
375 eprintln!("Error: {}", err);
376 assert!(false)
377 }
378 };
379 }
380
381 #[test]
382 fn process_command_insert_no_pk_test() {
383 let mut db = Database::new("tempdb".to_string());
385
386 let query_statement = "CREATE TABLE users (
388 name TEXT
389 );";
390 let dialect = SQLiteDialect {};
391 let mut ast = Parser::parse_sql(&dialect, query_statement).unwrap();
392 if ast.len() > 1 {
393 panic!("Expected a single query statement, but there are more then 1.")
394 }
395 let query = ast.pop().unwrap();
396 let create_query = CreateQuery::new(&query).unwrap();
397
398 db.tables.insert(
400 create_query.table_name.to_string(),
401 Table::new(create_query),
402 );
403
404 let insert_query = String::from("INSERT INTO users (name) Values ('josh');");
406 match process_command(&insert_query, &mut db) {
407 Ok(response) => assert_eq!(response, "INSERT Statement executed."),
408 Err(err) => {
409 eprintln!("Error: {}", err);
410 assert!(false)
411 }
412 };
413 }
414
415 #[test]
416 fn process_command_delete_where_test() {
417 let mut db = seed_users_table();
418 let response =
419 process_command("DELETE FROM users WHERE name = 'bob';", &mut db).expect("delete");
420 assert!(response.contains("1 row deleted"));
421
422 let remaining = process_command("SELECT * FROM users;", &mut db).expect("select");
423 assert!(remaining.contains("2 rows returned"));
424 }
425
426 #[test]
427 fn process_command_delete_all_test() {
428 let mut db = seed_users_table();
429 let response = process_command("DELETE FROM users;", &mut db).expect("delete");
430 assert!(response.contains("3 rows deleted"));
431 }
432
433 #[test]
434 fn process_command_update_where_test() {
435 use crate::sql::db::table::Value;
436
437 let mut db = seed_users_table();
438 let response = process_command("UPDATE users SET age = 99 WHERE name = 'bob';", &mut db)
439 .expect("update");
440 assert!(response.contains("1 row updated"));
441
442 let users = db.get_table("users".to_string()).unwrap();
444 let bob_rowid = users
445 .rowids()
446 .into_iter()
447 .find(|r| users.get_value("name", *r) == Some(Value::Text("bob".to_string())))
448 .expect("bob row must exist");
449 assert_eq!(users.get_value("age", bob_rowid), Some(Value::Integer(99)));
450 }
451
452 #[test]
453 fn process_command_update_unique_violation_test() {
454 let mut db = seed_users_table();
455 process_command(
457 "CREATE TABLE tags (id INTEGER PRIMARY KEY, label TEXT UNIQUE);",
458 &mut db,
459 )
460 .unwrap();
461 process_command("INSERT INTO tags (label) VALUES ('a');", &mut db).unwrap();
462 process_command("INSERT INTO tags (label) VALUES ('b');", &mut db).unwrap();
463
464 let result = process_command("UPDATE tags SET label = 'a' WHERE label = 'b';", &mut db);
465 assert!(result.is_err(), "expected UNIQUE violation, got {result:?}");
466 }
467
468 #[test]
469 fn process_command_insert_type_mismatch_returns_error_test() {
470 let mut db = Database::new("tempdb".to_string());
472 process_command(
473 "CREATE TABLE items (id INTEGER PRIMARY KEY, qty INTEGER);",
474 &mut db,
475 )
476 .unwrap();
477 let result = process_command("INSERT INTO items (qty) VALUES ('not a number');", &mut db);
478 assert!(result.is_err(), "expected error, got {result:?}");
479 }
480
481 #[test]
482 fn process_command_insert_missing_integer_returns_error_test() {
483 let mut db = Database::new("tempdb".to_string());
485 process_command(
486 "CREATE TABLE items (id INTEGER PRIMARY KEY, qty INTEGER);",
487 &mut db,
488 )
489 .unwrap();
490 let result = process_command("INSERT INTO items (id) VALUES (1);", &mut db);
491 assert!(result.is_err(), "expected error, got {result:?}");
492 }
493
494 #[test]
495 fn process_command_update_arith_test() {
496 use crate::sql::db::table::Value;
497
498 let mut db = seed_users_table();
499 process_command("UPDATE users SET age = age + 1;", &mut db).expect("update +1");
500
501 let users = db.get_table("users".to_string()).unwrap();
502 let mut ages: Vec<i64> = users
503 .rowids()
504 .into_iter()
505 .filter_map(|r| match users.get_value("age", r) {
506 Some(Value::Integer(n)) => Some(n),
507 _ => None,
508 })
509 .collect();
510 ages.sort();
511 assert_eq!(ages, vec![26, 31, 41]); }
513
514 #[test]
515 fn process_command_select_arithmetic_where_test() {
516 let mut db = seed_users_table();
517 let response =
519 process_command("SELECT name FROM users WHERE age * 2 > 55;", &mut db).expect("select");
520 assert!(response.contains("2 rows returned"));
521 }
522
523 #[test]
524 fn process_command_divide_by_zero_test() {
525 let mut db = seed_users_table();
526 let result = process_command("SELECT age / 0 FROM users;", &mut db);
527 assert!(result.is_err());
529 }
530
531 #[test]
532 fn process_command_unsupported_statement_test() {
533 let mut db = Database::new("tempdb".to_string());
534 let result = process_command("DROP TABLE users;", &mut db);
536 assert!(result.is_err());
537 }
538
539 #[test]
540 fn empty_input_is_a_noop_not_a_panic() {
541 let mut db = Database::new("t".to_string());
545 for input in ["", " ", "-- just a comment", "-- comment\n-- another"] {
546 let result = process_command(input, &mut db);
547 assert!(result.is_ok(), "input {input:?} should not error");
548 let msg = result.unwrap();
549 assert!(msg.contains("No statement"), "got: {msg:?}");
550 }
551 }
552
553 #[test]
554 fn create_index_adds_explicit_index() {
555 let mut db = seed_users_table();
556 let response = process_command("CREATE INDEX users_age_idx ON users (age);", &mut db)
557 .expect("create index");
558 assert!(response.contains("users_age_idx"));
559
560 let users = db.get_table("users".to_string()).unwrap();
562 let idx = users
563 .index_by_name("users_age_idx")
564 .expect("index should exist after CREATE INDEX");
565 assert_eq!(idx.column_name, "age");
566 assert!(!idx.is_unique);
567 }
568
569 #[test]
570 fn create_unique_index_rejects_duplicate_existing_values() {
571 let mut db = seed_users_table();
572 process_command("INSERT INTO users (name, age) VALUES ('dan', 30);", &mut db).unwrap();
575 let result = process_command(
576 "CREATE UNIQUE INDEX users_age_unique ON users (age);",
577 &mut db,
578 );
579 assert!(
580 result.is_err(),
581 "expected unique-index failure, got {result:?}"
582 );
583 }
584
585 #[test]
586 fn where_eq_on_indexed_column_uses_index_probe() {
587 let mut db = Database::new("t".to_string());
591 process_command(
592 "CREATE TABLE big (id INTEGER PRIMARY KEY, tag TEXT);",
593 &mut db,
594 )
595 .unwrap();
596 process_command("CREATE INDEX big_tag_idx ON big (tag);", &mut db).unwrap();
597 for i in 1..=100 {
598 let tag = if i % 3 == 0 { "hot" } else { "cold" };
599 process_command(&format!("INSERT INTO big (tag) VALUES ('{tag}');"), &mut db).unwrap();
600 }
601 let response =
602 process_command("SELECT id FROM big WHERE tag = 'hot';", &mut db).expect("select");
603 assert!(
605 response.contains("33 rows returned"),
606 "response was {response:?}"
607 );
608 }
609
610 #[test]
611 fn where_eq_on_indexed_column_inside_parens_uses_index_probe() {
612 let mut db = seed_users_table();
613 let response = process_command("SELECT name FROM users WHERE (name = 'bob');", &mut db)
614 .expect("select");
615 assert!(response.contains("1 row returned"));
616 }
617
618 #[test]
619 fn where_eq_literal_first_side_uses_index_probe() {
620 let mut db = seed_users_table();
621 let response =
623 process_command("SELECT name FROM users WHERE 'bob' = name;", &mut db).expect("select");
624 assert!(response.contains("1 row returned"));
625 }
626
627 #[test]
628 fn non_equality_where_still_falls_back_to_full_scan() {
629 let mut db = seed_users_table();
632 let response =
633 process_command("SELECT name FROM users WHERE age > 28;", &mut db).expect("select");
634 assert!(response.contains("2 rows returned"));
635 }
636
637 #[test]
642 fn rollback_restores_pre_begin_in_memory_state() {
643 let mut db = seed_users_table();
646 let before = db.get_table("users".to_string()).unwrap().rowids().len();
647 assert_eq!(before, 3);
648
649 process_command("BEGIN;", &mut db).expect("BEGIN");
650 assert!(db.in_transaction());
651 process_command("INSERT INTO users (name, age) VALUES ('dan', 50);", &mut db)
652 .expect("INSERT inside txn");
653 let mid = db.get_table("users".to_string()).unwrap().rowids().len();
655 assert_eq!(mid, 4);
656
657 process_command("ROLLBACK;", &mut db).expect("ROLLBACK");
658 assert!(!db.in_transaction());
659 let after = db.get_table("users".to_string()).unwrap().rowids().len();
660 assert_eq!(
661 after, 3,
662 "ROLLBACK should have restored the pre-BEGIN state"
663 );
664 }
665
666 #[test]
667 fn commit_keeps_mutations_and_clears_txn_flag() {
668 let mut db = seed_users_table();
669 process_command("BEGIN;", &mut db).expect("BEGIN");
670 process_command("INSERT INTO users (name, age) VALUES ('dan', 50);", &mut db)
671 .expect("INSERT inside txn");
672 process_command("COMMIT;", &mut db).expect("COMMIT");
673 assert!(!db.in_transaction());
674 let after = db.get_table("users".to_string()).unwrap().rowids().len();
675 assert_eq!(after, 4);
676 }
677
678 #[test]
679 fn rollback_undoes_update_and_delete_side_by_side() {
680 use crate::sql::db::table::Value;
681
682 let mut db = seed_users_table();
683 process_command("BEGIN;", &mut db).unwrap();
684 process_command("UPDATE users SET age = 999;", &mut db).unwrap();
685 process_command("DELETE FROM users WHERE name = 'bob';", &mut db).unwrap();
686 let users = db.get_table("users".to_string()).unwrap();
688 assert_eq!(users.rowids().len(), 2);
689 for r in users.rowids() {
690 assert_eq!(users.get_value("age", r), Some(Value::Integer(999)));
691 }
692
693 process_command("ROLLBACK;", &mut db).unwrap();
694 let users = db.get_table("users".to_string()).unwrap();
695 assert_eq!(users.rowids().len(), 3);
696 for r in users.rowids() {
698 assert_ne!(users.get_value("age", r), Some(Value::Integer(999)));
699 }
700 }
701
702 #[test]
703 fn nested_begin_is_rejected() {
704 let mut db = seed_users_table();
705 process_command("BEGIN;", &mut db).unwrap();
706 let err = process_command("BEGIN;", &mut db).unwrap_err();
707 assert!(
708 format!("{err}").contains("already open"),
709 "nested BEGIN should error; got: {err}"
710 );
711 assert!(db.in_transaction());
713 process_command("ROLLBACK;", &mut db).unwrap();
714 }
715
716 #[test]
717 fn orphan_commit_and_rollback_are_rejected() {
718 let mut db = seed_users_table();
719 let commit_err = process_command("COMMIT;", &mut db).unwrap_err();
720 assert!(format!("{commit_err}").contains("no transaction"));
721 let rollback_err = process_command("ROLLBACK;", &mut db).unwrap_err();
722 assert!(format!("{rollback_err}").contains("no transaction"));
723 }
724
725 #[test]
726 fn error_inside_transaction_keeps_txn_open() {
727 let mut db = seed_users_table();
731 process_command("BEGIN;", &mut db).unwrap();
732 let err = process_command("INSERT INTO nope (x) VALUES (1);", &mut db);
733 assert!(err.is_err());
734 assert!(db.in_transaction(), "txn should stay open after error");
735 process_command("ROLLBACK;", &mut db).unwrap();
736 }
737
738 fn seed_file_backed(name: &str, schema: &str) -> (std::path::PathBuf, Database) {
743 use crate::sql::pager::{open_database, save_database};
744 let mut p = std::env::temp_dir();
745 let pid = std::process::id();
746 let nanos = std::time::SystemTime::now()
747 .duration_since(std::time::UNIX_EPOCH)
748 .map(|d| d.as_nanos())
749 .unwrap_or(0);
750 p.push(format!("sqlrite-txn-{name}-{pid}-{nanos}.sqlrite"));
751
752 {
756 let mut seed = Database::new("t".to_string());
757 process_command(schema, &mut seed).unwrap();
758 save_database(&mut seed, &p).unwrap();
759 }
760 let db = open_database(&p, "t".to_string()).unwrap();
761 (p, db)
762 }
763
764 fn cleanup_file(path: &std::path::Path) {
765 let _ = std::fs::remove_file(path);
766 let mut wal = path.as_os_str().to_owned();
767 wal.push("-wal");
768 let _ = std::fs::remove_file(std::path::PathBuf::from(wal));
769 }
770
771 #[test]
772 fn begin_commit_rollback_round_trip_through_disk() {
773 use crate::sql::pager::open_database;
777
778 let (path, mut db) = seed_file_backed(
779 "roundtrip",
780 "CREATE TABLE notes (id INTEGER PRIMARY KEY, body TEXT);",
781 );
782
783 process_command("BEGIN;", &mut db).unwrap();
785 process_command("INSERT INTO notes (body) VALUES ('a');", &mut db).unwrap();
786 process_command("INSERT INTO notes (body) VALUES ('b');", &mut db).unwrap();
787 process_command("COMMIT;", &mut db).unwrap();
788
789 process_command("BEGIN;", &mut db).unwrap();
791 process_command("INSERT INTO notes (body) VALUES ('c');", &mut db).unwrap();
792 process_command("ROLLBACK;", &mut db).unwrap();
793
794 drop(db); let reopened = open_database(&path, "t".to_string()).unwrap();
797 let notes = reopened.get_table("notes".to_string()).unwrap();
798 assert_eq!(notes.rowids().len(), 2, "committed rows should survive");
799
800 drop(reopened);
801 cleanup_file(&path);
802 }
803
804 #[test]
805 fn write_inside_transaction_does_not_autosave() {
806 let (path, mut db) =
810 seed_file_backed("noas", "CREATE TABLE t (id INTEGER PRIMARY KEY, x TEXT);");
811
812 let mut wal_path = path.as_os_str().to_owned();
813 wal_path.push("-wal");
814 let wal_path = std::path::PathBuf::from(wal_path);
815 let frames_before = std::fs::metadata(&wal_path).unwrap().len();
816
817 process_command("BEGIN;", &mut db).unwrap();
818 process_command("INSERT INTO t (x) VALUES ('a');", &mut db).unwrap();
819 process_command("INSERT INTO t (x) VALUES ('b');", &mut db).unwrap();
820
821 let frames_mid = std::fs::metadata(&wal_path).unwrap().len();
823 assert_eq!(
824 frames_before, frames_mid,
825 "WAL should not grow during an open transaction"
826 );
827
828 process_command("COMMIT;", &mut db).unwrap();
829
830 drop(db); let fresh = crate::sql::pager::open_database(&path, "t".to_string()).unwrap();
832 assert_eq!(
833 fresh.get_table("t".to_string()).unwrap().rowids().len(),
834 2,
835 "COMMIT should have persisted both inserted rows"
836 );
837 drop(fresh);
838 cleanup_file(&path);
839 }
840
841 #[test]
842 fn rollback_undoes_create_table() {
843 let mut db = seed_users_table();
848 assert_eq!(db.tables.len(), 1);
849
850 process_command("BEGIN;", &mut db).unwrap();
851 process_command(
852 "CREATE TABLE dropme (id INTEGER PRIMARY KEY, x TEXT);",
853 &mut db,
854 )
855 .unwrap();
856 process_command("INSERT INTO dropme (x) VALUES ('stuff');", &mut db).unwrap();
857 assert_eq!(db.tables.len(), 2);
858
859 process_command("ROLLBACK;", &mut db).unwrap();
860 assert_eq!(
861 db.tables.len(),
862 1,
863 "CREATE TABLE should have been rolled back"
864 );
865 assert!(db.get_table("dropme".to_string()).is_err());
866 }
867
868 #[test]
869 fn rollback_restores_secondary_index_state() {
870 let mut db = Database::new("t".to_string());
874 process_command(
875 "CREATE TABLE users (id INTEGER PRIMARY KEY, email TEXT UNIQUE);",
876 &mut db,
877 )
878 .unwrap();
879 process_command("INSERT INTO users (email) VALUES ('a@x');", &mut db).unwrap();
880
881 process_command("BEGIN;", &mut db).unwrap();
882 process_command("INSERT INTO users (email) VALUES ('b@x');", &mut db).unwrap();
883 process_command("ROLLBACK;", &mut db).unwrap();
885
886 let reinsert = process_command("INSERT INTO users (email) VALUES ('b@x');", &mut db);
890 assert!(
891 reinsert.is_ok(),
892 "re-insert after rollback should succeed, got {reinsert:?}"
893 );
894 }
895
896 #[test]
897 fn rollback_restores_last_rowid_counter() {
898 use crate::sql::db::table::Value;
902
903 let mut db = seed_users_table(); let pre = db.get_table("users".to_string()).unwrap().last_rowid;
905
906 process_command("BEGIN;", &mut db).unwrap();
907 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();
910
911 let post = db.get_table("users".to_string()).unwrap().last_rowid;
912 assert_eq!(pre, post, "last_rowid must roll back with the snapshot");
913
914 process_command("INSERT INTO users (name, age) VALUES ('d', 50);", &mut db).unwrap();
916 let users = db.get_table("users".to_string()).unwrap();
917 let d_rowid = users
918 .rowids()
919 .into_iter()
920 .find(|r| users.get_value("name", *r) == Some(Value::Text("d".into())))
921 .expect("d row must exist");
922 assert_eq!(d_rowid, pre + 1);
923 }
924
925 #[test]
926 fn commit_on_in_memory_db_clears_txn_without_pager_call() {
927 let mut db = seed_users_table(); assert!(db.source_path.is_none());
932
933 process_command("BEGIN;", &mut db).unwrap();
934 process_command("INSERT INTO users (name, age) VALUES ('z', 99);", &mut db).unwrap();
935 process_command("COMMIT;", &mut db).unwrap();
936
937 assert!(!db.in_transaction());
938 assert_eq!(db.get_table("users".to_string()).unwrap().rowids().len(), 4);
939 }
940
941 #[test]
942 fn failed_commit_auto_rolls_back_in_memory_state() {
943 use crate::sql::pager::save_database;
957
958 let (path, mut db) = seed_file_backed(
960 "failcommit",
961 "CREATE TABLE notes (id INTEGER PRIMARY KEY, body TEXT);",
962 );
963
964 process_command("INSERT INTO notes (body) VALUES ('before');", &mut db).unwrap();
966
967 process_command("BEGIN;", &mut db).unwrap();
969 process_command("INSERT INTO notes (body) VALUES ('inflight');", &mut db).unwrap();
970 assert_eq!(
971 db.get_table("notes".to_string()).unwrap().rowids().len(),
972 2,
973 "inflight row visible mid-txn"
974 );
975
976 let orig_source = db.source_path.clone();
980 let orig_pager = db.pager.take();
981 db.source_path = Some(std::env::temp_dir());
982
983 let commit_result = process_command("COMMIT;", &mut db);
984 assert!(commit_result.is_err(), "commit must fail");
985 let err_str = format!("{}", commit_result.unwrap_err());
986 assert!(
987 err_str.contains("COMMIT failed") && err_str.contains("rolled back"),
988 "error must surface auto-rollback; got: {err_str}"
989 );
990
991 assert!(
995 !db.in_transaction(),
996 "txn must be cleared after auto-rollback"
997 );
998 assert_eq!(
999 db.get_table("notes".to_string()).unwrap().rowids().len(),
1000 1,
1001 "inflight row must be rolled back"
1002 );
1003
1004 db.source_path = orig_source;
1007 db.pager = orig_pager;
1008 process_command("INSERT INTO notes (body) VALUES ('after');", &mut db).unwrap();
1009 drop(db);
1010
1011 let reopened = crate::sql::pager::open_database(&path, "t".to_string()).unwrap();
1013 let notes = reopened.get_table("notes".to_string()).unwrap();
1014 assert_eq!(notes.rowids().len(), 2);
1015 let _ = save_database; drop(reopened);
1018 cleanup_file(&path);
1019 }
1020
1021 #[test]
1022 fn begin_on_read_only_is_rejected() {
1023 use crate::sql::pager::{open_database_read_only, save_database};
1024
1025 let path = {
1026 let mut p = std::env::temp_dir();
1027 let pid = std::process::id();
1028 let nanos = std::time::SystemTime::now()
1029 .duration_since(std::time::UNIX_EPOCH)
1030 .map(|d| d.as_nanos())
1031 .unwrap_or(0);
1032 p.push(format!("sqlrite-txn-ro-{pid}-{nanos}.sqlrite"));
1033 p
1034 };
1035 {
1036 let mut seed = Database::new("t".to_string());
1037 process_command("CREATE TABLE t (id INTEGER PRIMARY KEY);", &mut seed).unwrap();
1038 save_database(&mut seed, &path).unwrap();
1039 }
1040
1041 let mut ro = open_database_read_only(&path, "t".to_string()).unwrap();
1042 let err = process_command("BEGIN;", &mut ro).unwrap_err();
1043 assert!(
1044 format!("{err}").contains("read-only"),
1045 "BEGIN on RO db should surface read-only; got: {err}"
1046 );
1047 assert!(!ro.in_transaction());
1048
1049 let _ = std::fs::remove_file(&path);
1050 let mut wal = path.as_os_str().to_owned();
1051 wal.push("-wal");
1052 let _ = std::fs::remove_file(std::path::PathBuf::from(wal));
1053 }
1054
1055 #[test]
1056 fn read_only_database_rejects_mutations_before_touching_state() {
1057 use crate::sql::pager::open_database_read_only;
1063
1064 let mut seed = Database::new("t".to_string());
1065 process_command(
1066 "CREATE TABLE notes (id INTEGER PRIMARY KEY, body TEXT);",
1067 &mut seed,
1068 )
1069 .unwrap();
1070 process_command("INSERT INTO notes (body) VALUES ('alpha');", &mut seed).unwrap();
1071
1072 let path = {
1073 let mut p = std::env::temp_dir();
1074 let pid = std::process::id();
1075 let nanos = std::time::SystemTime::now()
1076 .duration_since(std::time::UNIX_EPOCH)
1077 .map(|d| d.as_nanos())
1078 .unwrap_or(0);
1079 p.push(format!("sqlrite-ro-reject-{pid}-{nanos}.sqlrite"));
1080 p
1081 };
1082 crate::sql::pager::save_database(&mut seed, &path).unwrap();
1083 drop(seed);
1084
1085 let mut ro = open_database_read_only(&path, "t".to_string()).unwrap();
1086 let notes_before = ro.get_table("notes".to_string()).unwrap().rowids().len();
1087
1088 for stmt in [
1089 "INSERT INTO notes (body) VALUES ('beta');",
1090 "UPDATE notes SET body = 'x';",
1091 "DELETE FROM notes;",
1092 "CREATE TABLE more (id INTEGER PRIMARY KEY);",
1093 "CREATE INDEX notes_body ON notes (body);",
1094 ] {
1095 let err = process_command(stmt, &mut ro).unwrap_err();
1096 assert!(
1097 format!("{err}").contains("read-only"),
1098 "stmt {stmt:?} should surface a read-only error; got: {err}"
1099 );
1100 }
1101
1102 let notes_after = ro.get_table("notes".to_string()).unwrap().rowids().len();
1104 assert_eq!(notes_before, notes_after);
1105 let sel = process_command("SELECT * FROM notes;", &mut ro).expect("select on RO must work");
1106 assert!(sel.contains("1 row returned"));
1107
1108 drop(ro);
1110 let _ = std::fs::remove_file(&path);
1111 let mut wal = path.as_os_str().to_owned();
1112 wal.push("-wal");
1113 let _ = std::fs::remove_file(std::path::PathBuf::from(wal));
1114 }
1115
1116 #[test]
1121 fn vector_create_table_and_insert_basic() {
1122 let mut db = Database::new("tempdb".to_string());
1123 process_command(
1124 "CREATE TABLE docs (id INTEGER PRIMARY KEY, embedding VECTOR(3));",
1125 &mut db,
1126 )
1127 .expect("create table with VECTOR(3)");
1128 process_command(
1129 "INSERT INTO docs (embedding) VALUES ([0.1, 0.2, 0.3]);",
1130 &mut db,
1131 )
1132 .expect("insert vector");
1133
1134 let sel = process_command("SELECT * FROM docs;", &mut db).expect("select");
1138 assert!(sel.contains("1 row returned"));
1139
1140 let docs = db.get_table("docs".to_string()).expect("docs table");
1141 let rowids = docs.rowids();
1142 assert_eq!(rowids.len(), 1);
1143 match docs.get_value("embedding", rowids[0]) {
1144 Some(Value::Vector(v)) => assert_eq!(v, vec![0.1f32, 0.2, 0.3]),
1145 other => panic!("expected Value::Vector(...), got {other:?}"),
1146 }
1147 }
1148
1149 #[test]
1150 fn vector_dim_mismatch_at_insert_is_clean_error() {
1151 let mut db = Database::new("tempdb".to_string());
1152 process_command(
1153 "CREATE TABLE docs (id INTEGER PRIMARY KEY, embedding VECTOR(3));",
1154 &mut db,
1155 )
1156 .expect("create table");
1157
1158 let err = process_command("INSERT INTO docs (embedding) VALUES ([0.1, 0.2]);", &mut db)
1160 .unwrap_err();
1161 let msg = format!("{err}");
1162 assert!(
1163 msg.to_lowercase().contains("dimension")
1164 && msg.contains("declared 3")
1165 && msg.contains("got 2"),
1166 "expected clear dim-mismatch error, got: {msg}"
1167 );
1168
1169 let err = process_command(
1171 "INSERT INTO docs (embedding) VALUES ([0.1, 0.2, 0.3, 0.4, 0.5]);",
1172 &mut db,
1173 )
1174 .unwrap_err();
1175 assert!(
1176 format!("{err}").contains("got 5"),
1177 "expected dim-mismatch error mentioning got 5, got: {err}"
1178 );
1179 }
1180
1181 #[test]
1182 fn vector_create_table_rejects_missing_dim() {
1183 let mut db = Database::new("tempdb".to_string());
1184 let result = process_command(
1190 "CREATE TABLE docs (id INTEGER PRIMARY KEY, embedding VECTOR);",
1191 &mut db,
1192 );
1193 assert!(
1194 result.is_err(),
1195 "expected CREATE TABLE with bare VECTOR to fail (no dim)"
1196 );
1197 }
1198
1199 #[test]
1200 fn vector_create_table_rejects_zero_dim() {
1201 let mut db = Database::new("tempdb".to_string());
1202 let err = process_command(
1203 "CREATE TABLE docs (id INTEGER PRIMARY KEY, embedding VECTOR(0));",
1204 &mut db,
1205 )
1206 .unwrap_err();
1207 let msg = format!("{err}");
1208 assert!(
1209 msg.to_lowercase().contains("vector"),
1210 "expected VECTOR-related error for VECTOR(0), got: {msg}"
1211 );
1212 }
1213
1214 #[test]
1215 fn vector_high_dim_works() {
1216 let mut db = Database::new("tempdb".to_string());
1219 process_command(
1220 "CREATE TABLE embeddings (id INTEGER PRIMARY KEY, e VECTOR(384));",
1221 &mut db,
1222 )
1223 .expect("create table VECTOR(384)");
1224
1225 let lit = format!(
1226 "[{}]",
1227 (0..384)
1228 .map(|i| format!("{}", i as f32 * 0.001))
1229 .collect::<Vec<_>>()
1230 .join(",")
1231 );
1232 let sql = format!("INSERT INTO embeddings (e) VALUES ({lit});");
1233 process_command(&sql, &mut db).expect("insert 384-dim vector");
1234
1235 let sel = process_command("SELECT id FROM embeddings;", &mut db).expect("select id");
1236 assert!(sel.contains("1 row returned"));
1237 }
1238
1239 #[test]
1240 fn vector_multiple_rows() {
1241 let mut db = Database::new("tempdb".to_string());
1244 process_command(
1245 "CREATE TABLE docs (id INTEGER PRIMARY KEY, e VECTOR(2));",
1246 &mut db,
1247 )
1248 .expect("create");
1249 for i in 0..3 {
1250 let sql = format!("INSERT INTO docs (e) VALUES ([{i}.0, {}.0]);", i + 1);
1251 process_command(&sql, &mut db).expect("insert");
1252 }
1253 let sel = process_command("SELECT * FROM docs;", &mut db).expect("select");
1254 assert!(sel.contains("3 rows returned"));
1255
1256 let docs = db.get_table("docs".to_string()).expect("docs table");
1258 let rowids = docs.rowids();
1259 assert_eq!(rowids.len(), 3);
1260 let mut vectors: Vec<Vec<f32>> = rowids
1261 .iter()
1262 .filter_map(|r| match docs.get_value("e", *r) {
1263 Some(Value::Vector(v)) => Some(v),
1264 _ => None,
1265 })
1266 .collect();
1267 vectors.sort_by(|a, b| a[0].partial_cmp(&b[0]).unwrap());
1268 assert_eq!(vectors[0], vec![0.0f32, 1.0]);
1269 assert_eq!(vectors[1], vec![1.0f32, 2.0]);
1270 assert_eq!(vectors[2], vec![2.0f32, 3.0]);
1271 }
1272}