1pub mod db;
2pub mod executor;
3pub mod fts;
4pub mod hnsw;
5pub mod pager;
6pub mod parser;
7use parser::create::CreateQuery;
10use parser::insert::InsertQuery;
11use parser::select::SelectQuery;
12
13use sqlparser::ast::{AlterTableOperation, ObjectType, Statement};
14use sqlparser::dialect::SQLiteDialect;
15use sqlparser::parser::{Parser, ParserError};
16
17use crate::error::{Result, SQLRiteError};
18use crate::sql::db::database::Database;
19use crate::sql::db::table::Table;
20
21#[derive(Debug, PartialEq)]
22pub enum SQLCommand {
23 Insert(String),
24 Delete(String),
25 Update(String),
26 CreateTable(String),
27 Select(String),
28 Unknown(String),
29}
30
31impl SQLCommand {
32 pub fn new(command: String) -> SQLCommand {
33 let v = command.split(" ").collect::<Vec<&str>>();
34 match v[0] {
35 "insert" => SQLCommand::Insert(command),
36 "update" => SQLCommand::Update(command),
37 "delete" => SQLCommand::Delete(command),
38 "create" => SQLCommand::CreateTable(command),
39 "select" => SQLCommand::Select(command),
40 _ => SQLCommand::Unknown(command),
41 }
42 }
43}
44
45#[derive(Debug, Clone)]
64pub struct CommandOutput {
65 pub status: String,
66 pub rendered: Option<String>,
67}
68
69pub fn process_command(query: &str, db: &mut Database) -> Result<String> {
78 process_command_with_render(query, db).map(|o| o.status)
79}
80
81pub fn process_command_with_render(query: &str, db: &mut Database) -> Result<CommandOutput> {
88 let dialect = SQLiteDialect {};
89 let message: String;
90 let mut rendered: Option<String> = None;
91 let mut ast = Parser::parse_sql(&dialect, query).map_err(SQLRiteError::from)?;
92
93 if ast.len() > 1 {
94 return Err(SQLRiteError::SqlError(ParserError::ParserError(format!(
95 "Expected a single query statement, but there are {}",
96 ast.len()
97 ))));
98 }
99
100 let Some(query) = ast.pop() else {
104 return Ok(CommandOutput {
105 status: "No statement to execute.".to_string(),
106 rendered: None,
107 });
108 };
109
110 match &query {
115 Statement::StartTransaction { .. } => {
116 db.begin_transaction()?;
117 return Ok(CommandOutput {
118 status: String::from("BEGIN"),
119 rendered: None,
120 });
121 }
122 Statement::Commit { .. } => {
123 if !db.in_transaction() {
124 return Err(SQLRiteError::General(
125 "cannot COMMIT: no transaction is open".to_string(),
126 ));
127 }
128 if let Some(path) = db.source_path.clone() {
138 if let Err(save_err) = pager::save_database(db, &path) {
139 let _ = db.rollback_transaction();
140 return Err(SQLRiteError::General(format!(
141 "COMMIT failed — transaction rolled back: {save_err}"
142 )));
143 }
144 }
145 db.commit_transaction()?;
146 return Ok(CommandOutput {
147 status: String::from("COMMIT"),
148 rendered: None,
149 });
150 }
151 Statement::Rollback { .. } => {
152 db.rollback_transaction()?;
153 return Ok(CommandOutput {
154 status: String::from("ROLLBACK"),
155 rendered: None,
156 });
157 }
158 _ => {}
159 }
160
161 let is_write_statement = matches!(
167 &query,
168 Statement::CreateTable(_)
169 | Statement::CreateIndex(_)
170 | Statement::Insert(_)
171 | Statement::Update(_)
172 | Statement::Delete(_)
173 | Statement::Drop { .. }
174 | Statement::AlterTable(_)
175 | Statement::Vacuum(_)
176 );
177 let is_vacuum = matches!(&query, Statement::Vacuum(_));
178
179 let releases_pages = match &query {
185 Statement::Drop { object_type, .. } => {
186 matches!(object_type, ObjectType::Table | ObjectType::Index)
187 }
188 Statement::AlterTable(alter) => alter
189 .operations
190 .iter()
191 .any(|op| matches!(op, AlterTableOperation::DropColumn { .. })),
192 _ => false,
193 };
194
195 if is_write_statement && db.is_read_only() {
201 return Err(SQLRiteError::General(
202 "cannot execute: database is opened read-only".to_string(),
203 ));
204 }
205
206 match query {
208 Statement::CreateTable(_) => {
209 let create_query = CreateQuery::new(&query);
210 match create_query {
211 Ok(payload) => {
212 let table_name = payload.table_name.clone();
213 if table_name == pager::MASTER_TABLE_NAME {
214 return Err(SQLRiteError::General(format!(
215 "'{}' is a reserved name used by the internal schema catalog",
216 pager::MASTER_TABLE_NAME
217 )));
218 }
219 match db.contains_table(table_name.to_string()) {
221 true => {
222 return Err(SQLRiteError::Internal(
223 "Cannot create, table already exists.".to_string(),
224 ));
225 }
226 false => {
227 let table = Table::new(payload);
228 db.tables.insert(table_name.to_string(), table);
236 message = String::from("CREATE TABLE Statement executed.");
237 }
238 }
239 }
240 Err(err) => return Err(err),
241 }
242 }
243 Statement::Insert(_) => {
244 let insert_query = InsertQuery::new(&query);
245 match insert_query {
246 Ok(payload) => {
247 let table_name = payload.table_name;
248 let columns = payload.columns;
249 let values = payload.rows;
250
251 match db.contains_table(table_name.to_string()) {
254 true => {
255 let db_table = db.get_table_mut(table_name.to_string()).unwrap();
256 match columns
258 .iter()
259 .all(|column| db_table.contains_column(column.to_string()))
260 {
261 true => {
262 for value in &values {
263 if columns.len() != value.len() {
265 return Err(SQLRiteError::Internal(format!(
266 "{} values for {} columns",
267 value.len(),
268 columns.len()
269 )));
270 }
271 db_table
272 .validate_unique_constraint(&columns, value)
273 .map_err(|err| {
274 SQLRiteError::Internal(format!(
275 "Unique key constraint violation: {err}"
276 ))
277 })?;
278 db_table.insert_row(&columns, value)?;
279 }
280 }
281 false => {
282 return Err(SQLRiteError::Internal(
283 "Cannot insert, some of the columns do not exist"
284 .to_string(),
285 ));
286 }
287 }
288 }
295 false => {
296 return Err(SQLRiteError::Internal("Table doesn't exist".to_string()));
297 }
298 }
299 }
300 Err(err) => return Err(err),
301 }
302
303 message = String::from("INSERT Statement executed.")
304 }
305 Statement::Query(_) => {
306 let select_query = SelectQuery::new(&query)?;
307 let (rendered_table, rows) = executor::execute_select(select_query, db)?;
308 rendered = Some(rendered_table);
314 message = format!(
315 "SELECT Statement executed. {rows} row{s} returned.",
316 s = if rows == 1 { "" } else { "s" }
317 );
318 }
319 Statement::Delete(_) => {
320 let rows = executor::execute_delete(&query, db)?;
321 message = format!(
322 "DELETE Statement executed. {rows} row{s} deleted.",
323 s = if rows == 1 { "" } else { "s" }
324 );
325 }
326 Statement::Update(_) => {
327 let rows = executor::execute_update(&query, db)?;
328 message = format!(
329 "UPDATE Statement executed. {rows} row{s} updated.",
330 s = if rows == 1 { "" } else { "s" }
331 );
332 }
333 Statement::CreateIndex(_) => {
334 let name = executor::execute_create_index(&query, db)?;
335 message = format!("CREATE INDEX '{name}' executed.");
336 }
337 Statement::Drop {
338 object_type,
339 if_exists,
340 names,
341 ..
342 } => match object_type {
343 ObjectType::Table => {
344 let count = executor::execute_drop_table(&names, if_exists, db)?;
345 let plural = if count == 1 { "table" } else { "tables" };
346 message = format!("DROP TABLE Statement executed. {count} {plural} dropped.");
347 }
348 ObjectType::Index => {
349 let count = executor::execute_drop_index(&names, if_exists, db)?;
350 let plural = if count == 1 { "index" } else { "indexes" };
351 message = format!("DROP INDEX Statement executed. {count} {plural} dropped.");
352 }
353 other => {
354 return Err(SQLRiteError::NotImplemented(format!(
355 "DROP {other:?} is not supported (only TABLE and INDEX)"
356 )));
357 }
358 },
359 Statement::AlterTable(alter) => {
360 message = executor::execute_alter_table(alter, db)?;
361 }
362 Statement::Vacuum(vac) => {
363 if vac.full
368 || vac.sort_only
369 || vac.delete_only
370 || vac.reindex
371 || vac.recluster
372 || vac.boost
373 || vac.table_name.is_some()
374 || vac.threshold.is_some()
375 {
376 return Err(SQLRiteError::NotImplemented(
377 "VACUUM modifiers (FULL, REINDEX, table targets, etc.) are not supported; use bare VACUUM;"
378 .to_string(),
379 ));
380 }
381 message = executor::execute_vacuum(db)?;
382 }
383 _ => {
384 return Err(SQLRiteError::NotImplemented(
385 "SQL Statement not supported yet.".to_string(),
386 ));
387 }
388 };
389
390 if is_write_statement && !is_vacuum && db.source_path.is_some() && !db.in_transaction() {
404 let path = db.source_path.clone().unwrap();
405 pager::save_database(db, &path)?;
406 }
407
408 if releases_pages && !db.in_transaction() {
420 if let (Some(threshold), Some(path)) = (db.auto_vacuum_threshold(), db.source_path.clone())
421 {
422 let should = match db.pager.as_ref() {
423 Some(p) => pager::freelist::should_auto_vacuum(p, threshold)?,
424 None => false,
425 };
426 if should {
427 pager::vacuum_database(db, &path)?;
428 }
429 }
430 }
431
432 Ok(CommandOutput {
433 status: message,
434 rendered,
435 })
436}
437
438#[cfg(test)]
439mod tests {
440 use super::*;
441 use crate::sql::db::table::Value;
442
443 fn seed_users_table() -> Database {
446 let mut db = Database::new("tempdb".to_string());
447 process_command(
448 "CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT NOT NULL, age INTEGER);",
449 &mut db,
450 )
451 .expect("create table");
452 process_command(
453 "INSERT INTO users (name, age) VALUES ('alice', 30);",
454 &mut db,
455 )
456 .expect("insert alice");
457 process_command("INSERT INTO users (name, age) VALUES ('bob', 25);", &mut db)
458 .expect("insert bob");
459 process_command(
460 "INSERT INTO users (name, age) VALUES ('carol', 40);",
461 &mut db,
462 )
463 .expect("insert carol");
464 db
465 }
466
467 #[test]
468 fn process_command_select_all_test() {
469 let mut db = seed_users_table();
470 let response = process_command("SELECT * FROM users;", &mut db).expect("select");
471 assert!(response.contains("3 rows returned"));
472 }
473
474 #[test]
475 fn process_command_select_where_test() {
476 let mut db = seed_users_table();
477 let response =
478 process_command("SELECT name FROM users WHERE age > 25;", &mut db).expect("select");
479 assert!(response.contains("2 rows returned"));
480 }
481
482 #[test]
483 fn process_command_select_eq_string_test() {
484 let mut db = seed_users_table();
485 let response =
486 process_command("SELECT name FROM users WHERE name = 'bob';", &mut db).expect("select");
487 assert!(response.contains("1 row returned"));
488 }
489
490 #[test]
491 fn process_command_select_limit_test() {
492 let mut db = seed_users_table();
493 let response = process_command("SELECT * FROM users ORDER BY age ASC LIMIT 2;", &mut db)
494 .expect("select");
495 assert!(response.contains("2 rows returned"));
496 }
497
498 #[test]
499 fn process_command_select_unknown_table_test() {
500 let mut db = Database::new("tempdb".to_string());
501 let result = process_command("SELECT * FROM nope;", &mut db);
502 assert!(result.is_err());
503 }
504
505 #[test]
506 fn process_command_select_unknown_column_test() {
507 let mut db = seed_users_table();
508 let result = process_command("SELECT height FROM users;", &mut db);
509 assert!(result.is_err());
510 }
511
512 #[test]
513 fn process_command_insert_test() {
514 let mut db = Database::new("tempdb".to_string());
516
517 let query_statement = "CREATE TABLE users (
519 id INTEGER PRIMARY KEY,
520 name TEXT
521 );";
522 let dialect = SQLiteDialect {};
523 let mut ast = Parser::parse_sql(&dialect, query_statement).unwrap();
524 if ast.len() > 1 {
525 panic!("Expected a single query statement, but there are more then 1.")
526 }
527 let query = ast.pop().unwrap();
528 let create_query = CreateQuery::new(&query).unwrap();
529
530 db.tables.insert(
532 create_query.table_name.to_string(),
533 Table::new(create_query),
534 );
535
536 let insert_query = String::from("INSERT INTO users (name) Values ('josh');");
538 match process_command(&insert_query, &mut db) {
539 Ok(response) => assert_eq!(response, "INSERT Statement executed."),
540 Err(err) => {
541 eprintln!("Error: {}", err);
542 assert!(false)
543 }
544 };
545 }
546
547 #[test]
548 fn process_command_insert_no_pk_test() {
549 let mut db = Database::new("tempdb".to_string());
551
552 let query_statement = "CREATE TABLE users (
554 name TEXT
555 );";
556 let dialect = SQLiteDialect {};
557 let mut ast = Parser::parse_sql(&dialect, query_statement).unwrap();
558 if ast.len() > 1 {
559 panic!("Expected a single query statement, but there are more then 1.")
560 }
561 let query = ast.pop().unwrap();
562 let create_query = CreateQuery::new(&query).unwrap();
563
564 db.tables.insert(
566 create_query.table_name.to_string(),
567 Table::new(create_query),
568 );
569
570 let insert_query = String::from("INSERT INTO users (name) Values ('josh');");
572 match process_command(&insert_query, &mut db) {
573 Ok(response) => assert_eq!(response, "INSERT Statement executed."),
574 Err(err) => {
575 eprintln!("Error: {}", err);
576 assert!(false)
577 }
578 };
579 }
580
581 #[test]
582 fn process_command_delete_where_test() {
583 let mut db = seed_users_table();
584 let response =
585 process_command("DELETE FROM users WHERE name = 'bob';", &mut db).expect("delete");
586 assert!(response.contains("1 row deleted"));
587
588 let remaining = process_command("SELECT * FROM users;", &mut db).expect("select");
589 assert!(remaining.contains("2 rows returned"));
590 }
591
592 #[test]
593 fn process_command_delete_all_test() {
594 let mut db = seed_users_table();
595 let response = process_command("DELETE FROM users;", &mut db).expect("delete");
596 assert!(response.contains("3 rows deleted"));
597 }
598
599 #[test]
600 fn process_command_update_where_test() {
601 use crate::sql::db::table::Value;
602
603 let mut db = seed_users_table();
604 let response = process_command("UPDATE users SET age = 99 WHERE name = 'bob';", &mut db)
605 .expect("update");
606 assert!(response.contains("1 row updated"));
607
608 let users = db.get_table("users".to_string()).unwrap();
610 let bob_rowid = users
611 .rowids()
612 .into_iter()
613 .find(|r| users.get_value("name", *r) == Some(Value::Text("bob".to_string())))
614 .expect("bob row must exist");
615 assert_eq!(users.get_value("age", bob_rowid), Some(Value::Integer(99)));
616 }
617
618 #[test]
619 fn process_command_update_unique_violation_test() {
620 let mut db = seed_users_table();
621 process_command(
623 "CREATE TABLE tags (id INTEGER PRIMARY KEY, label TEXT UNIQUE);",
624 &mut db,
625 )
626 .unwrap();
627 process_command("INSERT INTO tags (label) VALUES ('a');", &mut db).unwrap();
628 process_command("INSERT INTO tags (label) VALUES ('b');", &mut db).unwrap();
629
630 let result = process_command("UPDATE tags SET label = 'a' WHERE label = 'b';", &mut db);
631 assert!(result.is_err(), "expected UNIQUE violation, got {result:?}");
632 }
633
634 #[test]
635 fn process_command_insert_type_mismatch_returns_error_test() {
636 let mut db = Database::new("tempdb".to_string());
638 process_command(
639 "CREATE TABLE items (id INTEGER PRIMARY KEY, qty INTEGER);",
640 &mut db,
641 )
642 .unwrap();
643 let result = process_command("INSERT INTO items (qty) VALUES ('not a number');", &mut db);
644 assert!(result.is_err(), "expected error, got {result:?}");
645 }
646
647 #[test]
648 fn process_command_insert_missing_integer_returns_error_test() {
649 let mut db = Database::new("tempdb".to_string());
651 process_command(
652 "CREATE TABLE items (id INTEGER PRIMARY KEY, qty INTEGER);",
653 &mut db,
654 )
655 .unwrap();
656 let result = process_command("INSERT INTO items (id) VALUES (1);", &mut db);
657 assert!(result.is_err(), "expected error, got {result:?}");
658 }
659
660 #[test]
661 fn process_command_update_arith_test() {
662 use crate::sql::db::table::Value;
663
664 let mut db = seed_users_table();
665 process_command("UPDATE users SET age = age + 1;", &mut db).expect("update +1");
666
667 let users = db.get_table("users".to_string()).unwrap();
668 let mut ages: Vec<i64> = users
669 .rowids()
670 .into_iter()
671 .filter_map(|r| match users.get_value("age", r) {
672 Some(Value::Integer(n)) => Some(n),
673 _ => None,
674 })
675 .collect();
676 ages.sort();
677 assert_eq!(ages, vec![26, 31, 41]); }
679
680 #[test]
681 fn process_command_select_arithmetic_where_test() {
682 let mut db = seed_users_table();
683 let response =
685 process_command("SELECT name FROM users WHERE age * 2 > 55;", &mut db).expect("select");
686 assert!(response.contains("2 rows returned"));
687 }
688
689 #[test]
690 fn process_command_divide_by_zero_test() {
691 let mut db = seed_users_table();
692 let result = process_command("SELECT age / 0 FROM users;", &mut db);
693 assert!(result.is_err());
695 }
696
697 #[test]
698 fn process_command_unsupported_statement_test() {
699 let mut db = Database::new("tempdb".to_string());
700 let result = process_command("CREATE VIEW v AS SELECT * FROM users;", &mut db);
704 assert!(result.is_err());
705 }
706
707 #[test]
708 fn empty_input_is_a_noop_not_a_panic() {
709 let mut db = Database::new("t".to_string());
713 for input in ["", " ", "-- just a comment", "-- comment\n-- another"] {
714 let result = process_command(input, &mut db);
715 assert!(result.is_ok(), "input {input:?} should not error");
716 let msg = result.unwrap();
717 assert!(msg.contains("No statement"), "got: {msg:?}");
718 }
719 }
720
721 #[test]
722 fn create_index_adds_explicit_index() {
723 let mut db = seed_users_table();
724 let response = process_command("CREATE INDEX users_age_idx ON users (age);", &mut db)
725 .expect("create index");
726 assert!(response.contains("users_age_idx"));
727
728 let users = db.get_table("users".to_string()).unwrap();
730 let idx = users
731 .index_by_name("users_age_idx")
732 .expect("index should exist after CREATE INDEX");
733 assert_eq!(idx.column_name, "age");
734 assert!(!idx.is_unique);
735 }
736
737 #[test]
738 fn create_unique_index_rejects_duplicate_existing_values() {
739 let mut db = seed_users_table();
740 process_command("INSERT INTO users (name, age) VALUES ('dan', 30);", &mut db).unwrap();
743 let result = process_command(
744 "CREATE UNIQUE INDEX users_age_unique ON users (age);",
745 &mut db,
746 );
747 assert!(
748 result.is_err(),
749 "expected unique-index failure, got {result:?}"
750 );
751 }
752
753 #[test]
754 fn where_eq_on_indexed_column_uses_index_probe() {
755 let mut db = Database::new("t".to_string());
759 process_command(
760 "CREATE TABLE big (id INTEGER PRIMARY KEY, tag TEXT);",
761 &mut db,
762 )
763 .unwrap();
764 process_command("CREATE INDEX big_tag_idx ON big (tag);", &mut db).unwrap();
765 for i in 1..=100 {
766 let tag = if i % 3 == 0 { "hot" } else { "cold" };
767 process_command(&format!("INSERT INTO big (tag) VALUES ('{tag}');"), &mut db).unwrap();
768 }
769 let response =
770 process_command("SELECT id FROM big WHERE tag = 'hot';", &mut db).expect("select");
771 assert!(
773 response.contains("33 rows returned"),
774 "response was {response:?}"
775 );
776 }
777
778 #[test]
779 fn where_eq_on_indexed_column_inside_parens_uses_index_probe() {
780 let mut db = seed_users_table();
781 let response = process_command("SELECT name FROM users WHERE (name = 'bob');", &mut db)
782 .expect("select");
783 assert!(response.contains("1 row returned"));
784 }
785
786 #[test]
787 fn where_eq_literal_first_side_uses_index_probe() {
788 let mut db = seed_users_table();
789 let response =
791 process_command("SELECT name FROM users WHERE 'bob' = name;", &mut db).expect("select");
792 assert!(response.contains("1 row returned"));
793 }
794
795 #[test]
796 fn non_equality_where_still_falls_back_to_full_scan() {
797 let mut db = seed_users_table();
800 let response =
801 process_command("SELECT name FROM users WHERE age > 28;", &mut db).expect("select");
802 assert!(response.contains("2 rows returned"));
803 }
804
805 #[test]
810 fn rollback_restores_pre_begin_in_memory_state() {
811 let mut db = seed_users_table();
814 let before = db.get_table("users".to_string()).unwrap().rowids().len();
815 assert_eq!(before, 3);
816
817 process_command("BEGIN;", &mut db).expect("BEGIN");
818 assert!(db.in_transaction());
819 process_command("INSERT INTO users (name, age) VALUES ('dan', 50);", &mut db)
820 .expect("INSERT inside txn");
821 let mid = db.get_table("users".to_string()).unwrap().rowids().len();
823 assert_eq!(mid, 4);
824
825 process_command("ROLLBACK;", &mut db).expect("ROLLBACK");
826 assert!(!db.in_transaction());
827 let after = db.get_table("users".to_string()).unwrap().rowids().len();
828 assert_eq!(
829 after, 3,
830 "ROLLBACK should have restored the pre-BEGIN state"
831 );
832 }
833
834 #[test]
835 fn commit_keeps_mutations_and_clears_txn_flag() {
836 let mut db = seed_users_table();
837 process_command("BEGIN;", &mut db).expect("BEGIN");
838 process_command("INSERT INTO users (name, age) VALUES ('dan', 50);", &mut db)
839 .expect("INSERT inside txn");
840 process_command("COMMIT;", &mut db).expect("COMMIT");
841 assert!(!db.in_transaction());
842 let after = db.get_table("users".to_string()).unwrap().rowids().len();
843 assert_eq!(after, 4);
844 }
845
846 #[test]
847 fn rollback_undoes_update_and_delete_side_by_side() {
848 use crate::sql::db::table::Value;
849
850 let mut db = seed_users_table();
851 process_command("BEGIN;", &mut db).unwrap();
852 process_command("UPDATE users SET age = 999;", &mut db).unwrap();
853 process_command("DELETE FROM users WHERE name = 'bob';", &mut db).unwrap();
854 let users = db.get_table("users".to_string()).unwrap();
856 assert_eq!(users.rowids().len(), 2);
857 for r in users.rowids() {
858 assert_eq!(users.get_value("age", r), Some(Value::Integer(999)));
859 }
860
861 process_command("ROLLBACK;", &mut db).unwrap();
862 let users = db.get_table("users".to_string()).unwrap();
863 assert_eq!(users.rowids().len(), 3);
864 for r in users.rowids() {
866 assert_ne!(users.get_value("age", r), Some(Value::Integer(999)));
867 }
868 }
869
870 #[test]
871 fn nested_begin_is_rejected() {
872 let mut db = seed_users_table();
873 process_command("BEGIN;", &mut db).unwrap();
874 let err = process_command("BEGIN;", &mut db).unwrap_err();
875 assert!(
876 format!("{err}").contains("already open"),
877 "nested BEGIN should error; got: {err}"
878 );
879 assert!(db.in_transaction());
881 process_command("ROLLBACK;", &mut db).unwrap();
882 }
883
884 #[test]
885 fn orphan_commit_and_rollback_are_rejected() {
886 let mut db = seed_users_table();
887 let commit_err = process_command("COMMIT;", &mut db).unwrap_err();
888 assert!(format!("{commit_err}").contains("no transaction"));
889 let rollback_err = process_command("ROLLBACK;", &mut db).unwrap_err();
890 assert!(format!("{rollback_err}").contains("no transaction"));
891 }
892
893 #[test]
894 fn error_inside_transaction_keeps_txn_open() {
895 let mut db = seed_users_table();
899 process_command("BEGIN;", &mut db).unwrap();
900 let err = process_command("INSERT INTO nope (x) VALUES (1);", &mut db);
901 assert!(err.is_err());
902 assert!(db.in_transaction(), "txn should stay open after error");
903 process_command("ROLLBACK;", &mut db).unwrap();
904 }
905
906 fn seed_file_backed(name: &str, schema: &str) -> (std::path::PathBuf, Database) {
911 use crate::sql::pager::{open_database, save_database};
912 let mut p = std::env::temp_dir();
913 let pid = std::process::id();
914 let nanos = std::time::SystemTime::now()
915 .duration_since(std::time::UNIX_EPOCH)
916 .map(|d| d.as_nanos())
917 .unwrap_or(0);
918 p.push(format!("sqlrite-txn-{name}-{pid}-{nanos}.sqlrite"));
919
920 {
924 let mut seed = Database::new("t".to_string());
925 process_command(schema, &mut seed).unwrap();
926 save_database(&mut seed, &p).unwrap();
927 }
928 let db = open_database(&p, "t".to_string()).unwrap();
929 (p, db)
930 }
931
932 fn cleanup_file(path: &std::path::Path) {
933 let _ = std::fs::remove_file(path);
934 let mut wal = path.as_os_str().to_owned();
935 wal.push("-wal");
936 let _ = std::fs::remove_file(std::path::PathBuf::from(wal));
937 }
938
939 #[test]
940 fn begin_commit_rollback_round_trip_through_disk() {
941 use crate::sql::pager::open_database;
945
946 let (path, mut db) = seed_file_backed(
947 "roundtrip",
948 "CREATE TABLE notes (id INTEGER PRIMARY KEY, body TEXT);",
949 );
950
951 process_command("BEGIN;", &mut db).unwrap();
953 process_command("INSERT INTO notes (body) VALUES ('a');", &mut db).unwrap();
954 process_command("INSERT INTO notes (body) VALUES ('b');", &mut db).unwrap();
955 process_command("COMMIT;", &mut db).unwrap();
956
957 process_command("BEGIN;", &mut db).unwrap();
959 process_command("INSERT INTO notes (body) VALUES ('c');", &mut db).unwrap();
960 process_command("ROLLBACK;", &mut db).unwrap();
961
962 drop(db); let reopened = open_database(&path, "t".to_string()).unwrap();
965 let notes = reopened.get_table("notes".to_string()).unwrap();
966 assert_eq!(notes.rowids().len(), 2, "committed rows should survive");
967
968 drop(reopened);
969 cleanup_file(&path);
970 }
971
972 #[test]
973 fn write_inside_transaction_does_not_autosave() {
974 let (path, mut db) =
978 seed_file_backed("noas", "CREATE TABLE t (id INTEGER PRIMARY KEY, x TEXT);");
979
980 let mut wal_path = path.as_os_str().to_owned();
981 wal_path.push("-wal");
982 let wal_path = std::path::PathBuf::from(wal_path);
983 let frames_before = std::fs::metadata(&wal_path).unwrap().len();
984
985 process_command("BEGIN;", &mut db).unwrap();
986 process_command("INSERT INTO t (x) VALUES ('a');", &mut db).unwrap();
987 process_command("INSERT INTO t (x) VALUES ('b');", &mut db).unwrap();
988
989 let frames_mid = std::fs::metadata(&wal_path).unwrap().len();
991 assert_eq!(
992 frames_before, frames_mid,
993 "WAL should not grow during an open transaction"
994 );
995
996 process_command("COMMIT;", &mut db).unwrap();
997
998 drop(db); let fresh = crate::sql::pager::open_database(&path, "t".to_string()).unwrap();
1000 assert_eq!(
1001 fresh.get_table("t".to_string()).unwrap().rowids().len(),
1002 2,
1003 "COMMIT should have persisted both inserted rows"
1004 );
1005 drop(fresh);
1006 cleanup_file(&path);
1007 }
1008
1009 #[test]
1010 fn rollback_undoes_create_table() {
1011 let mut db = seed_users_table();
1016 assert_eq!(db.tables.len(), 1);
1017
1018 process_command("BEGIN;", &mut db).unwrap();
1019 process_command(
1020 "CREATE TABLE dropme (id INTEGER PRIMARY KEY, x TEXT);",
1021 &mut db,
1022 )
1023 .unwrap();
1024 process_command("INSERT INTO dropme (x) VALUES ('stuff');", &mut db).unwrap();
1025 assert_eq!(db.tables.len(), 2);
1026
1027 process_command("ROLLBACK;", &mut db).unwrap();
1028 assert_eq!(
1029 db.tables.len(),
1030 1,
1031 "CREATE TABLE should have been rolled back"
1032 );
1033 assert!(db.get_table("dropme".to_string()).is_err());
1034 }
1035
1036 #[test]
1037 fn rollback_restores_secondary_index_state() {
1038 let mut db = Database::new("t".to_string());
1042 process_command(
1043 "CREATE TABLE users (id INTEGER PRIMARY KEY, email TEXT UNIQUE);",
1044 &mut db,
1045 )
1046 .unwrap();
1047 process_command("INSERT INTO users (email) VALUES ('a@x');", &mut db).unwrap();
1048
1049 process_command("BEGIN;", &mut db).unwrap();
1050 process_command("INSERT INTO users (email) VALUES ('b@x');", &mut db).unwrap();
1051 process_command("ROLLBACK;", &mut db).unwrap();
1053
1054 let reinsert = process_command("INSERT INTO users (email) VALUES ('b@x');", &mut db);
1058 assert!(
1059 reinsert.is_ok(),
1060 "re-insert after rollback should succeed, got {reinsert:?}"
1061 );
1062 }
1063
1064 #[test]
1065 fn rollback_restores_last_rowid_counter() {
1066 use crate::sql::db::table::Value;
1070
1071 let mut db = seed_users_table(); let pre = db.get_table("users".to_string()).unwrap().last_rowid;
1073
1074 process_command("BEGIN;", &mut db).unwrap();
1075 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();
1078
1079 let post = db.get_table("users".to_string()).unwrap().last_rowid;
1080 assert_eq!(pre, post, "last_rowid must roll back with the snapshot");
1081
1082 process_command("INSERT INTO users (name, age) VALUES ('d', 50);", &mut db).unwrap();
1084 let users = db.get_table("users".to_string()).unwrap();
1085 let d_rowid = users
1086 .rowids()
1087 .into_iter()
1088 .find(|r| users.get_value("name", *r) == Some(Value::Text("d".into())))
1089 .expect("d row must exist");
1090 assert_eq!(d_rowid, pre + 1);
1091 }
1092
1093 #[test]
1094 fn commit_on_in_memory_db_clears_txn_without_pager_call() {
1095 let mut db = seed_users_table(); assert!(db.source_path.is_none());
1100
1101 process_command("BEGIN;", &mut db).unwrap();
1102 process_command("INSERT INTO users (name, age) VALUES ('z', 99);", &mut db).unwrap();
1103 process_command("COMMIT;", &mut db).unwrap();
1104
1105 assert!(!db.in_transaction());
1106 assert_eq!(db.get_table("users".to_string()).unwrap().rowids().len(), 4);
1107 }
1108
1109 #[test]
1110 fn failed_commit_auto_rolls_back_in_memory_state() {
1111 use crate::sql::pager::save_database;
1125
1126 let (path, mut db) = seed_file_backed(
1128 "failcommit",
1129 "CREATE TABLE notes (id INTEGER PRIMARY KEY, body TEXT);",
1130 );
1131
1132 process_command("INSERT INTO notes (body) VALUES ('before');", &mut db).unwrap();
1134
1135 process_command("BEGIN;", &mut db).unwrap();
1137 process_command("INSERT INTO notes (body) VALUES ('inflight');", &mut db).unwrap();
1138 assert_eq!(
1139 db.get_table("notes".to_string()).unwrap().rowids().len(),
1140 2,
1141 "inflight row visible mid-txn"
1142 );
1143
1144 let orig_source = db.source_path.clone();
1148 let orig_pager = db.pager.take();
1149 db.source_path = Some(std::env::temp_dir());
1150
1151 let commit_result = process_command("COMMIT;", &mut db);
1152 assert!(commit_result.is_err(), "commit must fail");
1153 let err_str = format!("{}", commit_result.unwrap_err());
1154 assert!(
1155 err_str.contains("COMMIT failed") && err_str.contains("rolled back"),
1156 "error must surface auto-rollback; got: {err_str}"
1157 );
1158
1159 assert!(
1163 !db.in_transaction(),
1164 "txn must be cleared after auto-rollback"
1165 );
1166 assert_eq!(
1167 db.get_table("notes".to_string()).unwrap().rowids().len(),
1168 1,
1169 "inflight row must be rolled back"
1170 );
1171
1172 db.source_path = orig_source;
1175 db.pager = orig_pager;
1176 process_command("INSERT INTO notes (body) VALUES ('after');", &mut db).unwrap();
1177 drop(db);
1178
1179 let reopened = crate::sql::pager::open_database(&path, "t".to_string()).unwrap();
1181 let notes = reopened.get_table("notes".to_string()).unwrap();
1182 assert_eq!(notes.rowids().len(), 2);
1183 let _ = save_database; drop(reopened);
1186 cleanup_file(&path);
1187 }
1188
1189 #[test]
1190 fn begin_on_read_only_is_rejected() {
1191 use crate::sql::pager::{open_database_read_only, save_database};
1192
1193 let path = {
1194 let mut p = std::env::temp_dir();
1195 let pid = std::process::id();
1196 let nanos = std::time::SystemTime::now()
1197 .duration_since(std::time::UNIX_EPOCH)
1198 .map(|d| d.as_nanos())
1199 .unwrap_or(0);
1200 p.push(format!("sqlrite-txn-ro-{pid}-{nanos}.sqlrite"));
1201 p
1202 };
1203 {
1204 let mut seed = Database::new("t".to_string());
1205 process_command("CREATE TABLE t (id INTEGER PRIMARY KEY);", &mut seed).unwrap();
1206 save_database(&mut seed, &path).unwrap();
1207 }
1208
1209 let mut ro = open_database_read_only(&path, "t".to_string()).unwrap();
1210 let err = process_command("BEGIN;", &mut ro).unwrap_err();
1211 assert!(
1212 format!("{err}").contains("read-only"),
1213 "BEGIN on RO db should surface read-only; got: {err}"
1214 );
1215 assert!(!ro.in_transaction());
1216
1217 let _ = std::fs::remove_file(&path);
1218 let mut wal = path.as_os_str().to_owned();
1219 wal.push("-wal");
1220 let _ = std::fs::remove_file(std::path::PathBuf::from(wal));
1221 }
1222
1223 #[test]
1224 fn read_only_database_rejects_mutations_before_touching_state() {
1225 use crate::sql::pager::open_database_read_only;
1231
1232 let mut seed = Database::new("t".to_string());
1233 process_command(
1234 "CREATE TABLE notes (id INTEGER PRIMARY KEY, body TEXT);",
1235 &mut seed,
1236 )
1237 .unwrap();
1238 process_command("INSERT INTO notes (body) VALUES ('alpha');", &mut seed).unwrap();
1239
1240 let path = {
1241 let mut p = std::env::temp_dir();
1242 let pid = std::process::id();
1243 let nanos = std::time::SystemTime::now()
1244 .duration_since(std::time::UNIX_EPOCH)
1245 .map(|d| d.as_nanos())
1246 .unwrap_or(0);
1247 p.push(format!("sqlrite-ro-reject-{pid}-{nanos}.sqlrite"));
1248 p
1249 };
1250 crate::sql::pager::save_database(&mut seed, &path).unwrap();
1251 drop(seed);
1252
1253 let mut ro = open_database_read_only(&path, "t".to_string()).unwrap();
1254 let notes_before = ro.get_table("notes".to_string()).unwrap().rowids().len();
1255
1256 for stmt in [
1257 "INSERT INTO notes (body) VALUES ('beta');",
1258 "UPDATE notes SET body = 'x';",
1259 "DELETE FROM notes;",
1260 "CREATE TABLE more (id INTEGER PRIMARY KEY);",
1261 "CREATE INDEX notes_body ON notes (body);",
1262 ] {
1263 let err = process_command(stmt, &mut ro).unwrap_err();
1264 assert!(
1265 format!("{err}").contains("read-only"),
1266 "stmt {stmt:?} should surface a read-only error; got: {err}"
1267 );
1268 }
1269
1270 let notes_after = ro.get_table("notes".to_string()).unwrap().rowids().len();
1272 assert_eq!(notes_before, notes_after);
1273 let sel = process_command("SELECT * FROM notes;", &mut ro).expect("select on RO must work");
1274 assert!(sel.contains("1 row returned"));
1275
1276 drop(ro);
1278 let _ = std::fs::remove_file(&path);
1279 let mut wal = path.as_os_str().to_owned();
1280 wal.push("-wal");
1281 let _ = std::fs::remove_file(std::path::PathBuf::from(wal));
1282 }
1283
1284 #[test]
1289 fn vector_create_table_and_insert_basic() {
1290 let mut db = Database::new("tempdb".to_string());
1291 process_command(
1292 "CREATE TABLE docs (id INTEGER PRIMARY KEY, embedding VECTOR(3));",
1293 &mut db,
1294 )
1295 .expect("create table with VECTOR(3)");
1296 process_command(
1297 "INSERT INTO docs (embedding) VALUES ([0.1, 0.2, 0.3]);",
1298 &mut db,
1299 )
1300 .expect("insert vector");
1301
1302 let sel = process_command("SELECT * FROM docs;", &mut db).expect("select");
1306 assert!(sel.contains("1 row returned"));
1307
1308 let docs = db.get_table("docs".to_string()).expect("docs table");
1309 let rowids = docs.rowids();
1310 assert_eq!(rowids.len(), 1);
1311 match docs.get_value("embedding", rowids[0]) {
1312 Some(Value::Vector(v)) => assert_eq!(v, vec![0.1f32, 0.2, 0.3]),
1313 other => panic!("expected Value::Vector(...), got {other:?}"),
1314 }
1315 }
1316
1317 #[test]
1318 fn vector_dim_mismatch_at_insert_is_clean_error() {
1319 let mut db = Database::new("tempdb".to_string());
1320 process_command(
1321 "CREATE TABLE docs (id INTEGER PRIMARY KEY, embedding VECTOR(3));",
1322 &mut db,
1323 )
1324 .expect("create table");
1325
1326 let err = process_command("INSERT INTO docs (embedding) VALUES ([0.1, 0.2]);", &mut db)
1328 .unwrap_err();
1329 let msg = format!("{err}");
1330 assert!(
1331 msg.to_lowercase().contains("dimension")
1332 && msg.contains("declared 3")
1333 && msg.contains("got 2"),
1334 "expected clear dim-mismatch error, got: {msg}"
1335 );
1336
1337 let err = process_command(
1339 "INSERT INTO docs (embedding) VALUES ([0.1, 0.2, 0.3, 0.4, 0.5]);",
1340 &mut db,
1341 )
1342 .unwrap_err();
1343 assert!(
1344 format!("{err}").contains("got 5"),
1345 "expected dim-mismatch error mentioning got 5, got: {err}"
1346 );
1347 }
1348
1349 #[test]
1350 fn vector_create_table_rejects_missing_dim() {
1351 let mut db = Database::new("tempdb".to_string());
1352 let result = process_command(
1358 "CREATE TABLE docs (id INTEGER PRIMARY KEY, embedding VECTOR);",
1359 &mut db,
1360 );
1361 assert!(
1362 result.is_err(),
1363 "expected CREATE TABLE with bare VECTOR to fail (no dim)"
1364 );
1365 }
1366
1367 #[test]
1368 fn vector_create_table_rejects_zero_dim() {
1369 let mut db = Database::new("tempdb".to_string());
1370 let err = process_command(
1371 "CREATE TABLE docs (id INTEGER PRIMARY KEY, embedding VECTOR(0));",
1372 &mut db,
1373 )
1374 .unwrap_err();
1375 let msg = format!("{err}");
1376 assert!(
1377 msg.to_lowercase().contains("vector"),
1378 "expected VECTOR-related error for VECTOR(0), got: {msg}"
1379 );
1380 }
1381
1382 #[test]
1383 fn vector_high_dim_works() {
1384 let mut db = Database::new("tempdb".to_string());
1387 process_command(
1388 "CREATE TABLE embeddings (id INTEGER PRIMARY KEY, e VECTOR(384));",
1389 &mut db,
1390 )
1391 .expect("create table VECTOR(384)");
1392
1393 let lit = format!(
1394 "[{}]",
1395 (0..384)
1396 .map(|i| format!("{}", i as f32 * 0.001))
1397 .collect::<Vec<_>>()
1398 .join(",")
1399 );
1400 let sql = format!("INSERT INTO embeddings (e) VALUES ({lit});");
1401 process_command(&sql, &mut db).expect("insert 384-dim vector");
1402
1403 let sel = process_command("SELECT id FROM embeddings;", &mut db).expect("select id");
1404 assert!(sel.contains("1 row returned"));
1405 }
1406
1407 #[test]
1408 fn vector_multiple_rows() {
1409 let mut db = Database::new("tempdb".to_string());
1412 process_command(
1413 "CREATE TABLE docs (id INTEGER PRIMARY KEY, e VECTOR(2));",
1414 &mut db,
1415 )
1416 .expect("create");
1417 for i in 0..3 {
1418 let sql = format!("INSERT INTO docs (e) VALUES ([{i}.0, {}.0]);", i + 1);
1419 process_command(&sql, &mut db).expect("insert");
1420 }
1421 let sel = process_command("SELECT * FROM docs;", &mut db).expect("select");
1422 assert!(sel.contains("3 rows returned"));
1423
1424 let docs = db.get_table("docs".to_string()).expect("docs table");
1426 let rowids = docs.rowids();
1427 assert_eq!(rowids.len(), 3);
1428 let mut vectors: Vec<Vec<f32>> = rowids
1429 .iter()
1430 .filter_map(|r| match docs.get_value("e", *r) {
1431 Some(Value::Vector(v)) => Some(v),
1432 _ => None,
1433 })
1434 .collect();
1435 vectors.sort_by(|a, b| a[0].partial_cmp(&b[0]).unwrap());
1436 assert_eq!(vectors[0], vec![0.0f32, 1.0]);
1437 assert_eq!(vectors[1], vec![1.0f32, 2.0]);
1438 assert_eq!(vectors[2], vec![2.0f32, 3.0]);
1439 }
1440
1441 fn seed_hnsw_table() -> Database {
1449 let mut db = Database::new("tempdb".to_string());
1450 process_command(
1451 "CREATE TABLE docs (id INTEGER PRIMARY KEY, e VECTOR(2));",
1452 &mut db,
1453 )
1454 .unwrap();
1455 for v in &[
1456 "[1.0, 0.0]", "[2.0, 0.0]", "[0.0, 3.0]", "[1.0, 4.0]", "[10.0, 10.0]", ] {
1462 process_command(&format!("INSERT INTO docs (e) VALUES ({v});"), &mut db).unwrap();
1463 }
1464 db
1465 }
1466
1467 #[test]
1468 fn create_index_using_hnsw_succeeds() {
1469 let mut db = seed_hnsw_table();
1470 let resp = process_command("CREATE INDEX ix_e ON docs USING hnsw (e);", &mut db).unwrap();
1471 assert!(resp.to_lowercase().contains("create index"));
1472 let table = db.get_table("docs".to_string()).unwrap();
1474 assert_eq!(table.hnsw_indexes.len(), 1);
1475 assert_eq!(table.hnsw_indexes[0].name, "ix_e");
1476 assert_eq!(table.hnsw_indexes[0].column_name, "e");
1477 assert_eq!(table.hnsw_indexes[0].index.len(), 5);
1479 }
1480
1481 #[test]
1482 fn create_index_using_hnsw_rejects_non_vector_column() {
1483 let mut db = Database::new("tempdb".to_string());
1484 process_command(
1485 "CREATE TABLE t (id INTEGER PRIMARY KEY, name TEXT);",
1486 &mut db,
1487 )
1488 .unwrap();
1489 let err =
1490 process_command("CREATE INDEX ix_name ON t USING hnsw (name);", &mut db).unwrap_err();
1491 let msg = format!("{err}");
1492 assert!(
1493 msg.to_lowercase().contains("vector"),
1494 "expected error mentioning VECTOR; got: {msg}"
1495 );
1496 }
1497
1498 #[test]
1499 fn knn_query_uses_hnsw_after_create_index() {
1500 let mut db = seed_hnsw_table();
1506 process_command("CREATE INDEX ix_e ON docs USING hnsw (e);", &mut db).unwrap();
1507
1508 let resp = process_command(
1513 "SELECT id FROM docs ORDER BY vec_distance_l2(e, [1.0, 0.0]) ASC LIMIT 3;",
1514 &mut db,
1515 )
1516 .unwrap();
1517 assert!(resp.contains("3 rows returned"), "got: {resp}");
1518 }
1519
1520 #[test]
1521 fn knn_query_works_after_subsequent_inserts() {
1522 let mut db = seed_hnsw_table();
1526 process_command("CREATE INDEX ix_e ON docs USING hnsw (e);", &mut db).unwrap();
1527 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();
1531 assert_eq!(
1532 table.hnsw_indexes[0].index.len(),
1533 7,
1534 "incremental insert should grow HNSW alongside row storage"
1535 );
1536
1537 let resp = process_command(
1540 "SELECT id FROM docs ORDER BY vec_distance_l2(e, [0.0, 0.0]) ASC LIMIT 1;",
1541 &mut db,
1542 )
1543 .unwrap();
1544 assert!(resp.contains("1 row returned"), "got: {resp}");
1545 }
1546
1547 #[test]
1553 fn delete_on_hnsw_indexed_table_succeeds_and_marks_dirty() {
1554 let mut db = seed_hnsw_table();
1555 process_command("CREATE INDEX ix_e ON docs USING hnsw (e);", &mut db).unwrap();
1556 let resp = process_command("DELETE FROM docs WHERE id = 1;", &mut db).unwrap();
1557 assert!(resp.contains("1 row"), "expected 1 row deleted: {resp}");
1558
1559 let docs = db.get_table("docs".to_string()).unwrap();
1560 let entry = docs.hnsw_indexes.iter().find(|e| e.name == "ix_e").unwrap();
1561 assert!(
1562 entry.needs_rebuild,
1563 "DELETE should have marked HNSW index dirty for rebuild on next save"
1564 );
1565 }
1566
1567 #[test]
1568 fn update_on_hnsw_indexed_vector_col_succeeds_and_marks_dirty() {
1569 let mut db = seed_hnsw_table();
1570 process_command("CREATE INDEX ix_e ON docs USING hnsw (e);", &mut db).unwrap();
1571 let resp =
1572 process_command("UPDATE docs SET e = [9.0, 9.0] WHERE id = 1;", &mut db).unwrap();
1573 assert!(resp.contains("1 row"), "expected 1 row updated: {resp}");
1574
1575 let docs = db.get_table("docs".to_string()).unwrap();
1576 let entry = docs.hnsw_indexes.iter().find(|e| e.name == "ix_e").unwrap();
1577 assert!(
1578 entry.needs_rebuild,
1579 "UPDATE on the vector column should have marked HNSW index dirty"
1580 );
1581 }
1582
1583 #[test]
1584 fn duplicate_index_name_errors() {
1585 let mut db = seed_hnsw_table();
1586 process_command("CREATE INDEX ix_e ON docs USING hnsw (e);", &mut db).unwrap();
1587 let err =
1588 process_command("CREATE INDEX ix_e ON docs USING hnsw (e);", &mut db).unwrap_err();
1589 let msg = format!("{err}");
1590 assert!(
1591 msg.to_lowercase().contains("already exists"),
1592 "expected duplicate-index error; got: {msg}"
1593 );
1594 }
1595
1596 #[test]
1597 fn index_if_not_exists_is_idempotent() {
1598 let mut db = seed_hnsw_table();
1599 process_command("CREATE INDEX ix_e ON docs USING hnsw (e);", &mut db).unwrap();
1600 process_command(
1602 "CREATE INDEX IF NOT EXISTS ix_e ON docs USING hnsw (e);",
1603 &mut db,
1604 )
1605 .unwrap();
1606 let table = db.get_table("docs".to_string()).unwrap();
1607 assert_eq!(table.hnsw_indexes.len(), 1);
1608 }
1609
1610 fn seed_fts_table() -> Database {
1617 let mut db = Database::new("tempdb".to_string());
1618 process_command(
1619 "CREATE TABLE docs (id INTEGER PRIMARY KEY, body TEXT);",
1620 &mut db,
1621 )
1622 .unwrap();
1623 for body in &[
1624 "rust embedded database", "rust web framework", "go embedded systems", "python web framework", "rust rust rust embedded power", ] {
1630 process_command(
1631 &format!("INSERT INTO docs (body) VALUES ('{body}');"),
1632 &mut db,
1633 )
1634 .unwrap();
1635 }
1636 db
1637 }
1638
1639 #[test]
1640 fn create_index_using_fts_succeeds_and_indexes_existing_rows() {
1641 let mut db = seed_fts_table();
1642 let resp =
1643 process_command("CREATE INDEX ix_body ON docs USING fts (body);", &mut db).unwrap();
1644 assert!(resp.to_lowercase().contains("create index"), "got {resp}");
1645 let table = db.get_table("docs".to_string()).unwrap();
1646 assert_eq!(table.fts_indexes.len(), 1);
1647 assert_eq!(table.fts_indexes[0].name, "ix_body");
1648 assert_eq!(table.fts_indexes[0].column_name, "body");
1649 assert_eq!(table.fts_indexes[0].index.len(), 5);
1651 }
1652
1653 #[test]
1654 fn create_index_using_fts_rejects_non_text_column() {
1655 let mut db = Database::new("tempdb".to_string());
1656 process_command(
1657 "CREATE TABLE t (id INTEGER PRIMARY KEY, n INTEGER);",
1658 &mut db,
1659 )
1660 .unwrap();
1661 let err = process_command("CREATE INDEX ix_n ON t USING fts (n);", &mut db).unwrap_err();
1662 let msg = format!("{err}");
1663 assert!(
1664 msg.to_lowercase().contains("text"),
1665 "expected error mentioning TEXT; got: {msg}"
1666 );
1667 }
1668
1669 #[test]
1670 fn fts_match_returns_expected_rows() {
1671 let mut db = seed_fts_table();
1672 process_command("CREATE INDEX ix_body ON docs USING fts (body);", &mut db).unwrap();
1673 let resp = process_command(
1675 "SELECT id FROM docs WHERE fts_match(body, 'rust');",
1676 &mut db,
1677 )
1678 .unwrap();
1679 assert!(resp.contains("3 rows returned"), "got: {resp}");
1680 }
1681
1682 #[test]
1683 fn fts_match_without_index_errors_clearly() {
1684 let mut db = seed_fts_table();
1685 let err = process_command(
1687 "SELECT id FROM docs WHERE fts_match(body, 'rust');",
1688 &mut db,
1689 )
1690 .unwrap_err();
1691 let msg = format!("{err}");
1692 assert!(
1693 msg.contains("no FTS index"),
1694 "expected no-index error; got: {msg}"
1695 );
1696 }
1697
1698 #[test]
1699 fn bm25_score_orders_descending_by_relevance() {
1700 let mut db = seed_fts_table();
1701 process_command("CREATE INDEX ix_body ON docs USING fts (body);", &mut db).unwrap();
1702 let out = process_command_with_render(
1705 "SELECT id FROM docs WHERE fts_match(body, 'rust') \
1706 ORDER BY bm25_score(body, 'rust') DESC LIMIT 1;",
1707 &mut db,
1708 )
1709 .unwrap();
1710 assert!(out.status.contains("1 row returned"), "got: {}", out.status);
1711 let rendered = out.rendered.expect("SELECT should produce rendered output");
1712 assert!(
1714 rendered.contains(" 5 "),
1715 "expected id=5 to be top-ranked; rendered:\n{rendered}"
1716 );
1717 }
1718
1719 #[test]
1720 fn bm25_score_without_index_errors_clearly() {
1721 let mut db = seed_fts_table();
1722 let err = process_command(
1723 "SELECT id FROM docs ORDER BY bm25_score(body, 'rust') DESC LIMIT 1;",
1724 &mut db,
1725 )
1726 .unwrap_err();
1727 let msg = format!("{err}");
1728 assert!(
1729 msg.contains("no FTS index"),
1730 "expected no-index error; got: {msg}"
1731 );
1732 }
1733
1734 #[test]
1735 fn fts_post_create_inserts_are_indexed_incrementally() {
1736 let mut db = seed_fts_table();
1737 process_command("CREATE INDEX ix_body ON docs USING fts (body);", &mut db).unwrap();
1738 process_command(
1739 "INSERT INTO docs (body) VALUES ('rust embedded analytics');",
1740 &mut db,
1741 )
1742 .unwrap();
1743 let table = db.get_table("docs".to_string()).unwrap();
1744 assert_eq!(table.fts_indexes[0].index.len(), 6);
1746 let resp = process_command(
1748 "SELECT id FROM docs WHERE fts_match(body, 'analytics');",
1749 &mut db,
1750 )
1751 .unwrap();
1752 assert!(resp.contains("1 row returned"), "got: {resp}");
1753 }
1754
1755 #[test]
1756 fn delete_on_fts_indexed_table_marks_dirty() {
1757 let mut db = seed_fts_table();
1758 process_command("CREATE INDEX ix_body ON docs USING fts (body);", &mut db).unwrap();
1759 let resp = process_command("DELETE FROM docs WHERE id = 1;", &mut db).unwrap();
1760 assert!(resp.contains("1 row"), "got: {resp}");
1761 let docs = db.get_table("docs".to_string()).unwrap();
1762 let entry = docs
1763 .fts_indexes
1764 .iter()
1765 .find(|e| e.name == "ix_body")
1766 .unwrap();
1767 assert!(
1768 entry.needs_rebuild,
1769 "DELETE should have flagged the FTS index dirty"
1770 );
1771 }
1772
1773 #[test]
1774 fn update_on_fts_indexed_text_col_marks_dirty() {
1775 let mut db = seed_fts_table();
1776 process_command("CREATE INDEX ix_body ON docs USING fts (body);", &mut db).unwrap();
1777 let resp = process_command(
1778 "UPDATE docs SET body = 'java spring framework' WHERE id = 1;",
1779 &mut db,
1780 )
1781 .unwrap();
1782 assert!(resp.contains("1 row"), "got: {resp}");
1783 let docs = db.get_table("docs".to_string()).unwrap();
1784 let entry = docs
1785 .fts_indexes
1786 .iter()
1787 .find(|e| e.name == "ix_body")
1788 .unwrap();
1789 assert!(
1790 entry.needs_rebuild,
1791 "UPDATE on the indexed TEXT column should have flagged dirty"
1792 );
1793 }
1794
1795 #[test]
1796 fn fts_index_name_collides_with_btree_and_hnsw_namespaces() {
1797 let mut db = seed_fts_table();
1798 process_command("CREATE INDEX ix_body ON docs USING fts (body);", &mut db).unwrap();
1799 let err = process_command("CREATE INDEX ix_body ON docs (body);", &mut db).unwrap_err();
1800 let msg = format!("{err}");
1801 assert!(
1802 msg.to_lowercase().contains("already exists"),
1803 "expected duplicate-index error; got: {msg}"
1804 );
1805 }
1806
1807 #[test]
1808 fn fts_index_rejects_unique() {
1809 let mut db = seed_fts_table();
1810 let err = process_command(
1811 "CREATE UNIQUE INDEX ix_body ON docs USING fts (body);",
1812 &mut db,
1813 )
1814 .unwrap_err();
1815 let msg = format!("{err}");
1816 assert!(
1817 msg.to_lowercase().contains("unique"),
1818 "expected UNIQUE-rejection error; got: {msg}"
1819 );
1820 }
1821
1822 #[test]
1823 fn try_fts_probe_falls_through_on_ascending() {
1824 let mut db = seed_fts_table();
1829 process_command("CREATE INDEX ix_body ON docs USING fts (body);", &mut db).unwrap();
1830 let resp = process_command(
1833 "SELECT id FROM docs WHERE fts_match(body, 'rust') \
1834 ORDER BY bm25_score(body, 'rust') ASC LIMIT 3;",
1835 &mut db,
1836 )
1837 .unwrap();
1838 assert!(resp.contains("3 rows returned"), "got: {resp}");
1839 }
1840
1841 fn seed_vector_docs() -> Database {
1851 let mut db = Database::new("tempdb".to_string());
1852 process_command(
1853 "CREATE TABLE docs (id INTEGER PRIMARY KEY, e VECTOR(2));",
1854 &mut db,
1855 )
1856 .expect("create");
1857 process_command("INSERT INTO docs (e) VALUES ([1.0, 0.0]);", &mut db).expect("insert 1");
1858 process_command("INSERT INTO docs (e) VALUES ([0.0, 1.0]);", &mut db).expect("insert 2");
1859 process_command("INSERT INTO docs (e) VALUES ([1.0, 1.0]);", &mut db).expect("insert 3");
1860 db
1861 }
1862
1863 #[test]
1864 fn vec_distance_l2_in_where_filters_correctly() {
1865 let mut db = seed_vector_docs();
1871 let resp = process_command(
1872 "SELECT * FROM docs WHERE vec_distance_l2(e, [1.0, 0.0]) < 1.1;",
1873 &mut db,
1874 )
1875 .expect("select");
1876 assert!(
1877 resp.contains("2 rows returned"),
1878 "expected 2 rows, got: {resp}"
1879 );
1880 }
1881
1882 #[test]
1883 fn vec_distance_cosine_in_where() {
1884 let mut db = seed_vector_docs();
1889 let resp = process_command(
1890 "SELECT * FROM docs WHERE vec_distance_cosine(e, [1.0, 0.0]) < 0.5;",
1891 &mut db,
1892 )
1893 .expect("select");
1894 assert!(
1895 resp.contains("2 rows returned"),
1896 "expected 2 rows, got: {resp}"
1897 );
1898 }
1899
1900 #[test]
1901 fn vec_distance_dot_negated() {
1902 let mut db = seed_vector_docs();
1907 let resp = process_command(
1908 "SELECT * FROM docs WHERE vec_distance_dot(e, [1.0, 0.0]) < 0.0;",
1909 &mut db,
1910 )
1911 .expect("select");
1912 assert!(
1913 resp.contains("2 rows returned"),
1914 "expected 2 rows, got: {resp}"
1915 );
1916 }
1917
1918 #[test]
1919 fn knn_via_order_by_distance_limit() {
1920 let mut db = seed_vector_docs();
1924 let resp = process_command(
1925 "SELECT id FROM docs ORDER BY vec_distance_l2(e, [1.0, 0.0]) ASC LIMIT 2;",
1926 &mut db,
1927 )
1928 .expect("select");
1929 assert!(
1930 resp.contains("2 rows returned"),
1931 "expected 2 rows, got: {resp}"
1932 );
1933 }
1934
1935 #[test]
1936 fn distance_function_dim_mismatch_errors() {
1937 let mut db = seed_vector_docs();
1939 let err = process_command(
1940 "SELECT * FROM docs WHERE vec_distance_l2(e, [1.0, 0.0, 0.0]) < 1.0;",
1941 &mut db,
1942 )
1943 .unwrap_err();
1944 let msg = format!("{err}");
1945 assert!(
1946 msg.to_lowercase().contains("dimension")
1947 && msg.contains("lhs=2")
1948 && msg.contains("rhs=3"),
1949 "expected dim mismatch error, got: {msg}"
1950 );
1951 }
1952
1953 #[test]
1954 fn unknown_function_errors_with_name() {
1955 let mut db = seed_vector_docs();
1959 let err = process_command(
1960 "SELECT * FROM docs WHERE vec_does_not_exist(e, [1.0, 0.0]) < 1.0;",
1961 &mut db,
1962 )
1963 .unwrap_err();
1964 let msg = format!("{err}");
1965 assert!(
1966 msg.contains("vec_does_not_exist"),
1967 "expected error mentioning function name, got: {msg}"
1968 );
1969 }
1970
1971 fn seed_json_table() -> Database {
1976 let mut db = Database::new("tempdb".to_string());
1977 process_command(
1978 "CREATE TABLE docs (id INTEGER PRIMARY KEY, payload JSON);",
1979 &mut db,
1980 )
1981 .expect("create json table");
1982 db
1983 }
1984
1985 #[test]
1986 fn json_column_round_trip_primitive_values() {
1987 let mut db = seed_json_table();
1988 process_command(
1989 r#"INSERT INTO docs (payload) VALUES ('{"name": "alice", "age": 30}');"#,
1990 &mut db,
1991 )
1992 .expect("insert json");
1993 let docs = db.get_table("docs".to_string()).unwrap();
1994 let rowids = docs.rowids();
1995 assert_eq!(rowids.len(), 1);
1996 match docs.get_value("payload", rowids[0]) {
1998 Some(Value::Text(s)) => {
1999 assert!(s.contains("alice"), "expected JSON text to round-trip: {s}");
2000 }
2001 other => panic!("expected Value::Text holding JSON, got {other:?}"),
2002 }
2003 }
2004
2005 #[test]
2006 fn json_insert_rejects_invalid_json() {
2007 let mut db = seed_json_table();
2008 let err = process_command(
2009 "INSERT INTO docs (payload) VALUES ('not-valid-json{');",
2010 &mut db,
2011 )
2012 .unwrap_err();
2013 let msg = format!("{err}").to_lowercase();
2014 assert!(
2015 msg.contains("json") && msg.contains("payload"),
2016 "expected JSON validation error mentioning column, got: {msg}"
2017 );
2018 }
2019
2020 #[test]
2021 fn json_extract_object_field() {
2022 let mut db = seed_json_table();
2023 process_command(
2024 r#"INSERT INTO docs (payload) VALUES ('{"name": "alice", "age": 30}');"#,
2025 &mut db,
2026 )
2027 .unwrap();
2028 let resp = process_command(
2031 r#"SELECT id FROM docs WHERE json_extract(payload, '$.name') = 'alice';"#,
2032 &mut db,
2033 )
2034 .expect("select via json_extract");
2035 assert!(resp.contains("1 row returned"), "got: {resp}");
2036
2037 let resp = process_command(
2038 r#"SELECT id FROM docs WHERE json_extract(payload, '$.age') = 30;"#,
2039 &mut db,
2040 )
2041 .expect("select via numeric json_extract");
2042 assert!(resp.contains("1 row returned"), "got: {resp}");
2043 }
2044
2045 #[test]
2046 fn json_extract_array_index_and_nested() {
2047 let mut db = seed_json_table();
2048 process_command(
2049 r#"INSERT INTO docs (payload) VALUES ('{"tags": ["rust", "sql", "vectors"], "meta": {"author": "joao"}}');"#,
2050 &mut db,
2051 )
2052 .unwrap();
2053 let resp = process_command(
2054 r#"SELECT id FROM docs WHERE json_extract(payload, '$.tags[0]') = 'rust';"#,
2055 &mut db,
2056 )
2057 .expect("select via array index");
2058 assert!(resp.contains("1 row returned"), "got: {resp}");
2059
2060 let resp = process_command(
2061 r#"SELECT id FROM docs WHERE json_extract(payload, '$.meta.author') = 'joao';"#,
2062 &mut db,
2063 )
2064 .expect("select via nested object");
2065 assert!(resp.contains("1 row returned"), "got: {resp}");
2066 }
2067
2068 #[test]
2069 fn json_extract_missing_path_returns_null() {
2070 let mut db = seed_json_table();
2071 process_command(
2072 r#"INSERT INTO docs (payload) VALUES ('{"name": "alice"}');"#,
2073 &mut db,
2074 )
2075 .unwrap();
2076 let resp = process_command(
2079 r#"SELECT id FROM docs WHERE json_extract(payload, '$.missing') = 'something';"#,
2080 &mut db,
2081 )
2082 .expect("select with missing path");
2083 assert!(resp.contains("0 rows returned"), "got: {resp}");
2084 }
2085
2086 #[test]
2087 fn json_extract_malformed_path_errors() {
2088 let mut db = seed_json_table();
2089 process_command(
2090 r#"INSERT INTO docs (payload) VALUES ('{"a": 1}');"#,
2091 &mut db,
2092 )
2093 .unwrap();
2094 let err = process_command(
2096 r#"SELECT id FROM docs WHERE json_extract(payload, 'a.b') = 1;"#,
2097 &mut db,
2098 )
2099 .unwrap_err();
2100 assert!(format!("{err}").contains("'$'"));
2101 }
2102
2103 #[test]
2104 fn json_array_length_on_array() {
2105 let mut db = seed_json_table();
2108 process_command(
2109 r#"INSERT INTO docs (payload) VALUES ('{"tags": ["a", "b", "c"]}');"#,
2110 &mut db,
2111 )
2112 .unwrap();
2113 let resp = process_command(
2114 r#"SELECT id FROM docs WHERE json_array_length(payload, '$.tags') = 3;"#,
2115 &mut db,
2116 )
2117 .expect("select via array_length");
2118 assert!(resp.contains("1 row returned"), "got: {resp}");
2119 }
2120
2121 #[test]
2122 fn json_array_length_on_non_array_errors() {
2123 let mut db = seed_json_table();
2124 process_command(
2125 r#"INSERT INTO docs (payload) VALUES ('{"tags": "not-an-array"}');"#,
2126 &mut db,
2127 )
2128 .unwrap();
2129 let err = process_command(
2130 r#"SELECT id FROM docs WHERE json_array_length(payload, '$.tags') = 1;"#,
2131 &mut db,
2132 )
2133 .unwrap_err();
2134 let msg = format!("{err}").to_lowercase();
2135 assert!(
2136 msg.contains("non-array"),
2137 "expected non-array error, got: {msg}"
2138 );
2139 }
2140
2141 #[test]
2142 fn json_type_recognizes_each_kind() {
2143 let mut db = seed_json_table();
2144 process_command(
2145 r#"INSERT INTO docs (payload) VALUES ('{"o": {}, "a": [], "s": "x", "i": 1, "f": 1.5, "t": true, "n": null}');"#,
2146 &mut db,
2147 )
2148 .unwrap();
2149 let cases = &[
2150 ("$.o", "object"),
2151 ("$.a", "array"),
2152 ("$.s", "text"),
2153 ("$.i", "integer"),
2154 ("$.f", "real"),
2155 ("$.t", "true"),
2156 ("$.n", "null"),
2157 ];
2158 for (path, expected_type) in cases {
2159 let sql = format!(
2160 "SELECT id FROM docs WHERE json_type(payload, '{path}') = '{expected_type}';"
2161 );
2162 let resp =
2163 process_command(&sql, &mut db).unwrap_or_else(|e| panic!("path {path}: {e}"));
2164 assert!(
2165 resp.contains("1 row returned"),
2166 "path {path} expected type {expected_type}; got response: {resp}"
2167 );
2168 }
2169 }
2170
2171 #[test]
2172 fn update_on_json_column_revalidates() {
2173 let mut db = seed_json_table();
2174 process_command(
2175 r#"INSERT INTO docs (payload) VALUES ('{"a": 1}');"#,
2176 &mut db,
2177 )
2178 .unwrap();
2179 process_command(
2181 r#"UPDATE docs SET payload = '{"a": 2, "b": 3}' WHERE id = 1;"#,
2182 &mut db,
2183 )
2184 .expect("valid JSON UPDATE");
2185 let err = process_command(
2188 r#"UPDATE docs SET payload = 'not-json{' WHERE id = 1;"#,
2189 &mut db,
2190 )
2191 .unwrap_err();
2192 let msg = format!("{err}").to_lowercase();
2193 assert!(
2194 msg.contains("json") && msg.contains("payload"),
2195 "got: {msg}"
2196 );
2197 }
2198
2199 #[test]
2204 fn default_literal_int_applies_when_column_omitted() {
2205 let mut db = Database::new("t".to_string());
2206 process_command(
2207 "CREATE TABLE t (id INTEGER PRIMARY KEY, n INTEGER DEFAULT 42);",
2208 &mut db,
2209 )
2210 .unwrap();
2211 process_command("INSERT INTO t (id) VALUES (1);", &mut db).unwrap();
2212
2213 let table = db.get_table("t".to_string()).unwrap();
2214 assert_eq!(table.get_value("n", 1), Some(Value::Integer(42)));
2215 }
2216
2217 #[test]
2218 fn default_literal_text_applies_when_column_omitted() {
2219 let mut db = Database::new("t".to_string());
2220 process_command(
2221 "CREATE TABLE users (id INTEGER PRIMARY KEY, status TEXT DEFAULT 'active');",
2222 &mut db,
2223 )
2224 .unwrap();
2225 process_command("INSERT INTO users (id) VALUES (1);", &mut db).unwrap();
2226
2227 let table = db.get_table("users".to_string()).unwrap();
2228 assert_eq!(
2229 table.get_value("status", 1),
2230 Some(Value::Text("active".to_string()))
2231 );
2232 }
2233
2234 #[test]
2235 fn default_literal_real_negative_applies_when_column_omitted() {
2236 let mut db = Database::new("t".to_string());
2238 process_command(
2239 "CREATE TABLE t (id INTEGER PRIMARY KEY, score REAL DEFAULT -1.5);",
2240 &mut db,
2241 )
2242 .unwrap();
2243 process_command("INSERT INTO t (id) VALUES (1);", &mut db).unwrap();
2244
2245 let table = db.get_table("t".to_string()).unwrap();
2246 assert_eq!(table.get_value("score", 1), Some(Value::Real(-1.5)));
2247 }
2248
2249 #[test]
2250 fn default_with_type_mismatch_errors_at_create_time() {
2251 let mut db = Database::new("t".to_string());
2252 let result = process_command(
2253 "CREATE TABLE t (id INTEGER PRIMARY KEY, n INTEGER DEFAULT 'oops');",
2254 &mut db,
2255 );
2256 let err = result.expect_err("text default on INTEGER column should be rejected");
2257 let msg = format!("{err}").to_lowercase();
2258 assert!(msg.contains("default"), "got: {msg}");
2259 }
2260
2261 #[test]
2262 fn default_for_json_column_must_be_valid_json() {
2263 let mut db = Database::new("t".to_string());
2267 let err = process_command(
2268 "CREATE TABLE t (id INTEGER PRIMARY KEY, doc JSON DEFAULT 'not-json{');",
2269 &mut db,
2270 )
2271 .unwrap_err();
2272 assert!(
2273 format!("{err}").to_lowercase().contains("json"),
2274 "got: {err}"
2275 );
2276
2277 process_command(
2279 "CREATE TABLE t2 (id INTEGER PRIMARY KEY, doc JSON DEFAULT '{\"k\":1}');",
2280 &mut db,
2281 )
2282 .expect("valid JSON DEFAULT should be accepted");
2283 }
2284
2285 #[test]
2286 fn default_with_non_literal_expression_errors_at_create_time() {
2287 let mut db = Database::new("t".to_string());
2288 let result = process_command(
2291 "CREATE TABLE t (id INTEGER PRIMARY KEY, ts TEXT DEFAULT CURRENT_TIMESTAMP);",
2292 &mut db,
2293 );
2294 let err = result.expect_err("non-literal DEFAULT should be rejected");
2295 let msg = format!("{err}").to_lowercase();
2296 assert!(
2297 msg.contains("default") && msg.contains("literal"),
2298 "got: {msg}"
2299 );
2300 }
2301
2302 #[test]
2303 fn default_null_is_accepted_at_create_time() {
2304 let mut db = Database::new("t".to_string());
2308 process_command(
2309 "CREATE TABLE t (id INTEGER PRIMARY KEY, note TEXT DEFAULT NULL);",
2310 &mut db,
2311 )
2312 .expect("CREATE TABLE with DEFAULT NULL should be accepted");
2313 let table = db.get_table("t".to_string()).unwrap();
2314 let note = table
2315 .columns
2316 .iter()
2317 .find(|c| c.column_name == "note")
2318 .unwrap();
2319 assert_eq!(note.default, Some(Value::Null));
2320 }
2321
2322 #[test]
2327 fn drop_table_basic() {
2328 let mut db = seed_users_table();
2329 let response = process_command("DROP TABLE users;", &mut db).expect("drop table");
2330 assert!(response.contains("1 table dropped"));
2331 assert!(!db.contains_table("users".to_string()));
2332 }
2333
2334 #[test]
2335 fn drop_table_if_exists_noop_on_missing() {
2336 let mut db = Database::new("t".to_string());
2337 let response =
2338 process_command("DROP TABLE IF EXISTS missing;", &mut db).expect("drop if exists");
2339 assert!(response.contains("0 tables dropped"));
2340 }
2341
2342 #[test]
2343 fn drop_table_missing_errors_without_if_exists() {
2344 let mut db = Database::new("t".to_string());
2345 let err = process_command("DROP TABLE missing;", &mut db).unwrap_err();
2346 assert!(format!("{err}").contains("does not exist"), "got: {err}");
2347 }
2348
2349 #[test]
2350 fn drop_table_reserved_name_errors() {
2351 let mut db = Database::new("t".to_string());
2352 let err = process_command("DROP TABLE sqlrite_master;", &mut db).unwrap_err();
2353 assert!(format!("{err}").contains("reserved"), "got: {err}");
2354 }
2355
2356 #[test]
2357 fn drop_table_multi_target_rejected() {
2358 let mut db = seed_users_table();
2359 process_command("CREATE TABLE other (id INTEGER PRIMARY KEY);", &mut db).unwrap();
2360 let err = process_command("DROP TABLE users, other;", &mut db).unwrap_err();
2363 assert!(format!("{err}").contains("single table"), "got: {err}");
2364 }
2365
2366 #[test]
2367 fn drop_table_cascades_indexes_in_memory() {
2368 let mut db = seed_users_table();
2369 process_command("CREATE INDEX users_age_idx ON users (age);", &mut db).unwrap();
2370 let users = db.get_table("users".to_string()).unwrap();
2372 assert!(
2373 users
2374 .secondary_indexes
2375 .iter()
2376 .any(|i| i.name == "users_age_idx")
2377 );
2378
2379 process_command("DROP TABLE users;", &mut db).unwrap();
2380
2381 for table in db.tables.values() {
2383 assert!(
2384 !table
2385 .secondary_indexes
2386 .iter()
2387 .any(|i| i.name.contains("users")),
2388 "dropped table's indexes should not survive on any other table"
2389 );
2390 }
2391 }
2392
2393 #[test]
2394 fn drop_index_explicit_basic() {
2395 let mut db = seed_users_table();
2396 process_command("CREATE INDEX users_age_idx ON users (age);", &mut db).unwrap();
2397 let response = process_command("DROP INDEX users_age_idx;", &mut db).expect("drop index");
2398 assert!(response.contains("1 index dropped"));
2399
2400 let users = db.get_table("users".to_string()).unwrap();
2401 assert!(users.index_by_name("users_age_idx").is_none());
2402 }
2403
2404 #[test]
2405 fn drop_index_refuses_auto_index() {
2406 let mut db = seed_users_table();
2407 let err = process_command("DROP INDEX sqlrite_autoindex_users_id;", &mut db).unwrap_err();
2410 assert!(format!("{err}").contains("auto-created"), "got: {err}");
2411 }
2412
2413 #[test]
2414 fn drop_index_if_exists_noop_on_missing() {
2415 let mut db = Database::new("t".to_string());
2416 let response =
2417 process_command("DROP INDEX IF EXISTS nope;", &mut db).expect("drop index if exists");
2418 assert!(response.contains("0 indexes dropped"));
2419 }
2420
2421 #[test]
2422 fn drop_index_missing_errors_without_if_exists() {
2423 let mut db = Database::new("t".to_string());
2424 let err = process_command("DROP INDEX nope;", &mut db).unwrap_err();
2425 assert!(format!("{err}").contains("does not exist"), "got: {err}");
2426 }
2427
2428 #[test]
2429 fn drop_statements_rejected_on_readonly_db() {
2430 use crate::sql::pager::{open_database_read_only, save_database};
2431
2432 let mut seed = Database::new("t".to_string());
2433 process_command(
2434 "CREATE TABLE notes (id INTEGER PRIMARY KEY, body TEXT);",
2435 &mut seed,
2436 )
2437 .unwrap();
2438 process_command("CREATE INDEX notes_body ON notes (body);", &mut seed).unwrap();
2439 let path = {
2440 let mut p = std::env::temp_dir();
2441 let pid = std::process::id();
2442 let nanos = std::time::SystemTime::now()
2443 .duration_since(std::time::UNIX_EPOCH)
2444 .map(|d| d.as_nanos())
2445 .unwrap_or(0);
2446 p.push(format!("sqlrite-drop-ro-{pid}-{nanos}.sqlrite"));
2447 p
2448 };
2449 save_database(&mut seed, &path).unwrap();
2450 drop(seed);
2451
2452 let mut ro = open_database_read_only(&path, "t".to_string()).unwrap();
2453 for stmt in ["DROP TABLE notes;", "DROP INDEX notes_body;"] {
2454 let err = process_command(stmt, &mut ro).unwrap_err();
2455 assert!(
2456 format!("{err}").contains("read-only"),
2457 "{stmt:?} should surface read-only error, got: {err}"
2458 );
2459 }
2460
2461 let _ = std::fs::remove_file(&path);
2462 let mut wal = path.as_os_str().to_owned();
2463 wal.push("-wal");
2464 let _ = std::fs::remove_file(std::path::PathBuf::from(wal));
2465 }
2466
2467 #[test]
2472 fn alter_rename_table_basic() {
2473 let mut db = seed_users_table();
2474 process_command("ALTER TABLE users RENAME TO members;", &mut db).expect("rename table");
2475 assert!(!db.contains_table("users".to_string()));
2476 assert!(db.contains_table("members".to_string()));
2477 let response = process_command("SELECT * FROM members;", &mut db).expect("select");
2479 assert!(response.contains("3 rows returned"));
2480 }
2481
2482 #[test]
2483 fn alter_rename_table_renames_auto_indexes() {
2484 let mut db = Database::new("t".to_string());
2487 process_command(
2488 "CREATE TABLE accounts (id INTEGER PRIMARY KEY, email TEXT UNIQUE);",
2489 &mut db,
2490 )
2491 .unwrap();
2492 {
2493 let accounts = db.get_table("accounts".to_string()).unwrap();
2494 assert!(
2495 accounts
2496 .index_by_name("sqlrite_autoindex_accounts_id")
2497 .is_some()
2498 );
2499 assert!(
2500 accounts
2501 .index_by_name("sqlrite_autoindex_accounts_email")
2502 .is_some()
2503 );
2504 }
2505 process_command("ALTER TABLE accounts RENAME TO members;", &mut db).expect("rename");
2506 let members = db.get_table("members".to_string()).unwrap();
2507 assert!(
2508 members
2509 .index_by_name("sqlrite_autoindex_members_id")
2510 .is_some(),
2511 "PK auto-index should be renamed to match new table"
2512 );
2513 assert!(
2514 members
2515 .index_by_name("sqlrite_autoindex_members_email")
2516 .is_some()
2517 );
2518 assert!(
2520 members
2521 .index_by_name("sqlrite_autoindex_accounts_id")
2522 .is_none()
2523 );
2524 for idx in &members.secondary_indexes {
2526 assert_eq!(idx.table_name, "members");
2527 }
2528 }
2529
2530 #[test]
2531 fn alter_rename_table_to_existing_errors() {
2532 let mut db = seed_users_table();
2533 process_command("CREATE TABLE other (id INTEGER PRIMARY KEY);", &mut db).unwrap();
2534 let err = process_command("ALTER TABLE users RENAME TO other;", &mut db).unwrap_err();
2535 assert!(format!("{err}").contains("already exists"), "got: {err}");
2536 assert!(db.contains_table("users".to_string()));
2538 assert!(db.contains_table("other".to_string()));
2539 }
2540
2541 #[test]
2542 fn alter_rename_table_to_reserved_name_errors() {
2543 let mut db = seed_users_table();
2544 let err =
2545 process_command("ALTER TABLE users RENAME TO sqlrite_master;", &mut db).unwrap_err();
2546 assert!(format!("{err}").contains("reserved"), "got: {err}");
2547 }
2548
2549 #[test]
2550 fn alter_rename_column_basic() {
2551 let mut db = seed_users_table();
2552 process_command(
2553 "ALTER TABLE users RENAME COLUMN name TO full_name;",
2554 &mut db,
2555 )
2556 .expect("rename column");
2557
2558 let users = db.get_table("users".to_string()).unwrap();
2559 assert!(users.contains_column("full_name".to_string()));
2560 assert!(!users.contains_column("name".to_string()));
2561
2562 let bob_rowid = users
2565 .rowids()
2566 .into_iter()
2567 .find(|r| users.get_value("full_name", *r) == Some(Value::Text("bob".to_string())))
2568 .expect("bob row should be findable under the new column name");
2569 assert_eq!(
2570 users.get_value("full_name", bob_rowid),
2571 Some(Value::Text("bob".to_string()))
2572 );
2573 }
2574
2575 #[test]
2576 fn alter_rename_column_collision_errors() {
2577 let mut db = seed_users_table();
2578 let err =
2579 process_command("ALTER TABLE users RENAME COLUMN name TO age;", &mut db).unwrap_err();
2580 assert!(format!("{err}").contains("already exists"), "got: {err}");
2581 }
2582
2583 #[test]
2584 fn alter_rename_column_updates_indexes() {
2585 let mut db = Database::new("t".to_string());
2587 process_command(
2588 "CREATE TABLE accounts (id INTEGER PRIMARY KEY, email TEXT UNIQUE);",
2589 &mut db,
2590 )
2591 .unwrap();
2592 process_command(
2593 "ALTER TABLE accounts RENAME COLUMN email TO contact;",
2594 &mut db,
2595 )
2596 .unwrap();
2597 let accounts = db.get_table("accounts".to_string()).unwrap();
2598 assert!(
2599 accounts
2600 .index_by_name("sqlrite_autoindex_accounts_contact")
2601 .is_some()
2602 );
2603 assert!(
2604 accounts
2605 .index_by_name("sqlrite_autoindex_accounts_email")
2606 .is_none()
2607 );
2608 }
2609
2610 #[test]
2611 fn alter_add_column_basic() {
2612 let mut db = seed_users_table();
2613 process_command("ALTER TABLE users ADD COLUMN nickname TEXT;", &mut db)
2614 .expect("add column");
2615 let users = db.get_table("users".to_string()).unwrap();
2616 assert!(users.contains_column("nickname".to_string()));
2617 let any_rowid = *users.rowids().first().expect("seed has rows");
2619 assert_eq!(users.get_value("nickname", any_rowid), None);
2620
2621 process_command(
2623 "INSERT INTO users (name, age, nickname) VALUES ('dan', 22, 'd');",
2624 &mut db,
2625 )
2626 .expect("insert with new col");
2627 let users = db.get_table("users".to_string()).unwrap();
2628 let dan_rowid = users
2629 .rowids()
2630 .into_iter()
2631 .find(|r| users.get_value("name", *r) == Some(Value::Text("dan".to_string())))
2632 .unwrap();
2633 assert_eq!(
2634 users.get_value("nickname", dan_rowid),
2635 Some(Value::Text("d".to_string()))
2636 );
2637 }
2638
2639 #[test]
2640 fn alter_add_column_with_default_backfills_existing_rows() {
2641 let mut db = seed_users_table();
2642 process_command(
2643 "ALTER TABLE users ADD COLUMN status TEXT DEFAULT 'active';",
2644 &mut db,
2645 )
2646 .expect("add column with default");
2647 let users = db.get_table("users".to_string()).unwrap();
2648 for rowid in users.rowids() {
2649 assert_eq!(
2650 users.get_value("status", rowid),
2651 Some(Value::Text("active".to_string())),
2652 "rowid {rowid} should have been backfilled with the default"
2653 );
2654 }
2655 }
2656
2657 #[test]
2658 fn alter_add_column_not_null_with_default_works_on_nonempty_table() {
2659 let mut db = seed_users_table();
2660 process_command(
2661 "ALTER TABLE users ADD COLUMN score INTEGER NOT NULL DEFAULT 0;",
2662 &mut db,
2663 )
2664 .expect("NOT NULL ADD with DEFAULT should succeed even with existing rows");
2665 let users = db.get_table("users".to_string()).unwrap();
2666 for rowid in users.rowids() {
2667 assert_eq!(users.get_value("score", rowid), Some(Value::Integer(0)));
2668 }
2669 }
2670
2671 #[test]
2672 fn alter_add_column_not_null_without_default_errors_on_nonempty_table() {
2673 let mut db = seed_users_table();
2674 let err = process_command(
2675 "ALTER TABLE users ADD COLUMN score INTEGER NOT NULL;",
2676 &mut db,
2677 )
2678 .unwrap_err();
2679 let msg = format!("{err}").to_lowercase();
2680 assert!(
2681 msg.contains("not null") && msg.contains("default"),
2682 "got: {msg}"
2683 );
2684 }
2685
2686 #[test]
2687 fn alter_add_column_pk_rejected() {
2688 let mut db = seed_users_table();
2689 let err = process_command(
2690 "ALTER TABLE users ADD COLUMN extra INTEGER PRIMARY KEY;",
2691 &mut db,
2692 )
2693 .unwrap_err();
2694 assert!(
2695 format!("{err}").to_lowercase().contains("primary key"),
2696 "got: {err}"
2697 );
2698 }
2699
2700 #[test]
2701 fn alter_add_column_unique_rejected() {
2702 let mut db = seed_users_table();
2703 let err = process_command(
2704 "ALTER TABLE users ADD COLUMN extra INTEGER UNIQUE;",
2705 &mut db,
2706 )
2707 .unwrap_err();
2708 assert!(
2709 format!("{err}").to_lowercase().contains("unique"),
2710 "got: {err}"
2711 );
2712 }
2713
2714 #[test]
2715 fn alter_add_column_existing_name_errors() {
2716 let mut db = seed_users_table();
2717 let err =
2718 process_command("ALTER TABLE users ADD COLUMN age INTEGER;", &mut db).unwrap_err();
2719 assert!(format!("{err}").contains("already exists"), "got: {err}");
2720 }
2721
2722 #[test]
2728 fn alter_drop_column_basic() {
2729 let mut db = seed_users_table();
2730 process_command("ALTER TABLE users DROP COLUMN age;", &mut db).expect("drop column");
2731 let users = db.get_table("users".to_string()).unwrap();
2732 assert!(!users.contains_column("age".to_string()));
2733 assert!(users.contains_column("name".to_string()));
2735 assert_eq!(users.rowids().len(), 3);
2736 }
2737
2738 #[test]
2739 fn alter_drop_column_drops_dependent_indexes() {
2740 let mut db = seed_users_table();
2741 process_command("CREATE INDEX users_age_idx ON users (age);", &mut db).unwrap();
2742 process_command("ALTER TABLE users DROP COLUMN age;", &mut db).unwrap();
2743 let users = db.get_table("users".to_string()).unwrap();
2744 assert!(users.index_by_name("users_age_idx").is_none());
2745 }
2746
2747 #[test]
2748 fn alter_drop_column_pk_errors() {
2749 let mut db = seed_users_table();
2750 let err = process_command("ALTER TABLE users DROP COLUMN id;", &mut db).unwrap_err();
2751 assert!(
2752 format!("{err}").to_lowercase().contains("primary key"),
2753 "got: {err}"
2754 );
2755 }
2756
2757 #[test]
2758 fn alter_drop_column_only_column_errors() {
2759 let mut db = Database::new("t".to_string());
2760 process_command("CREATE TABLE solo (only_col TEXT);", &mut db).unwrap();
2761 let err = process_command("ALTER TABLE solo DROP COLUMN only_col;", &mut db).unwrap_err();
2762 assert!(
2763 format!("{err}").to_lowercase().contains("only column"),
2764 "got: {err}"
2765 );
2766 }
2767
2768 #[test]
2769 fn alter_unknown_table_errors_without_if_exists() {
2770 let mut db = Database::new("t".to_string());
2771 let err = process_command("ALTER TABLE missing RENAME TO other;", &mut db).unwrap_err();
2772 assert!(format!("{err}").contains("does not exist"), "got: {err}");
2773 }
2774
2775 #[test]
2776 fn alter_unknown_table_if_exists_noop() {
2777 let mut db = Database::new("t".to_string());
2778 let response = process_command("ALTER TABLE IF EXISTS missing RENAME TO other;", &mut db)
2779 .expect("IF EXISTS makes missing-table ALTER a no-op");
2780 assert!(response.contains("no-op"));
2781 }
2782
2783 #[test]
2784 fn drop_table_inside_transaction_rolls_back() {
2785 let mut db = seed_users_table();
2789 process_command("CREATE INDEX users_age_idx ON users (age);", &mut db).unwrap();
2790 process_command("BEGIN;", &mut db).unwrap();
2791 process_command("DROP TABLE users;", &mut db).unwrap();
2792 assert!(!db.contains_table("users".to_string()));
2793 process_command("ROLLBACK;", &mut db).unwrap();
2794 assert!(db.contains_table("users".to_string()));
2795 let users = db.get_table("users".to_string()).unwrap();
2796 assert_eq!(users.rowids().len(), 3);
2797 assert!(users.index_by_name("users_age_idx").is_some());
2798 }
2799
2800 #[test]
2801 fn alter_inside_transaction_rolls_back() {
2802 let mut db = seed_users_table();
2803 process_command("BEGIN;", &mut db).unwrap();
2804 process_command(
2805 "ALTER TABLE users ADD COLUMN status TEXT DEFAULT 'active';",
2806 &mut db,
2807 )
2808 .unwrap();
2809 assert!(
2811 db.get_table("users".to_string())
2812 .unwrap()
2813 .contains_column("status".to_string())
2814 );
2815 process_command("ROLLBACK;", &mut db).unwrap();
2816 assert!(
2818 !db.get_table("users".to_string())
2819 .unwrap()
2820 .contains_column("status".to_string())
2821 );
2822 }
2823
2824 #[test]
2825 fn alter_rejected_on_readonly_db() {
2826 use crate::sql::pager::{open_database_read_only, save_database};
2827
2828 let mut seed = Database::new("t".to_string());
2829 process_command(
2830 "CREATE TABLE notes (id INTEGER PRIMARY KEY, body TEXT);",
2831 &mut seed,
2832 )
2833 .unwrap();
2834 let path = {
2835 let mut p = std::env::temp_dir();
2836 let pid = std::process::id();
2837 let nanos = std::time::SystemTime::now()
2838 .duration_since(std::time::UNIX_EPOCH)
2839 .map(|d| d.as_nanos())
2840 .unwrap_or(0);
2841 p.push(format!("sqlrite-alter-ro-{pid}-{nanos}.sqlrite"));
2842 p
2843 };
2844 save_database(&mut seed, &path).unwrap();
2845 drop(seed);
2846
2847 let mut ro = open_database_read_only(&path, "t".to_string()).unwrap();
2848 for stmt in [
2849 "ALTER TABLE notes RENAME TO n2;",
2850 "ALTER TABLE notes RENAME COLUMN body TO b;",
2851 "ALTER TABLE notes ADD COLUMN extra TEXT;",
2852 "ALTER TABLE notes DROP COLUMN body;",
2853 ] {
2854 let err = process_command(stmt, &mut ro).unwrap_err();
2855 assert!(
2856 format!("{err}").contains("read-only"),
2857 "{stmt:?} should surface read-only error, got: {err}"
2858 );
2859 }
2860
2861 let _ = std::fs::remove_file(&path);
2862 let mut wal = path.as_os_str().to_owned();
2863 wal.push("-wal");
2864 let _ = std::fs::remove_file(std::path::PathBuf::from(wal));
2865 }
2866}