1pub mod agg;
2pub mod db;
3pub mod executor;
4pub mod fts;
5pub mod hnsw;
6pub mod pager;
7pub mod params;
8pub mod parser;
9use parser::create::CreateQuery;
12use parser::insert::InsertQuery;
13use parser::select::SelectQuery;
14
15use sqlparser::ast::{AlterTableOperation, ObjectType, Statement};
16use sqlparser::dialect::SQLiteDialect;
17use sqlparser::parser::{Parser, ParserError};
18
19use crate::error::{Result, SQLRiteError};
20use crate::sql::db::database::Database;
21use crate::sql::db::table::Table;
22
23#[derive(Debug, PartialEq)]
24pub enum SQLCommand {
25 Insert(String),
26 Delete(String),
27 Update(String),
28 CreateTable(String),
29 Select(String),
30 Unknown(String),
31}
32
33impl SQLCommand {
34 pub fn new(command: String) -> SQLCommand {
35 let v = command.split(" ").collect::<Vec<&str>>();
36 match v[0] {
37 "insert" => SQLCommand::Insert(command),
38 "update" => SQLCommand::Update(command),
39 "delete" => SQLCommand::Delete(command),
40 "create" => SQLCommand::CreateTable(command),
41 "select" => SQLCommand::Select(command),
42 _ => SQLCommand::Unknown(command),
43 }
44 }
45}
46
47#[derive(Debug, Clone)]
66pub struct CommandOutput {
67 pub status: String,
68 pub rendered: Option<String>,
69}
70
71pub fn process_command(query: &str, db: &mut Database) -> Result<String> {
80 process_command_with_render(query, db).map(|o| o.status)
81}
82
83pub fn process_command_with_render(query: &str, db: &mut Database) -> Result<CommandOutput> {
90 let dialect = SQLiteDialect {};
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 process_ast_with_render(query, db)
110}
111
112pub fn process_ast_with_render(query: Statement, db: &mut Database) -> Result<CommandOutput> {
117 let message: String;
118 let mut rendered: Option<String> = None;
119
120 match &query {
125 Statement::StartTransaction { .. } => {
126 db.begin_transaction()?;
127 return Ok(CommandOutput {
128 status: String::from("BEGIN"),
129 rendered: None,
130 });
131 }
132 Statement::Commit { .. } => {
133 if !db.in_transaction() {
134 return Err(SQLRiteError::General(
135 "cannot COMMIT: no transaction is open".to_string(),
136 ));
137 }
138 if let Some(path) = db.source_path.clone() {
148 if let Err(save_err) = pager::save_database(db, &path) {
149 let _ = db.rollback_transaction();
150 return Err(SQLRiteError::General(format!(
151 "COMMIT failed — transaction rolled back: {save_err}"
152 )));
153 }
154 }
155 db.commit_transaction()?;
156 return Ok(CommandOutput {
157 status: String::from("COMMIT"),
158 rendered: None,
159 });
160 }
161 Statement::Rollback { .. } => {
162 db.rollback_transaction()?;
163 return Ok(CommandOutput {
164 status: String::from("ROLLBACK"),
165 rendered: None,
166 });
167 }
168 _ => {}
169 }
170
171 let is_write_statement = matches!(
177 &query,
178 Statement::CreateTable(_)
179 | Statement::CreateIndex(_)
180 | Statement::Insert(_)
181 | Statement::Update(_)
182 | Statement::Delete(_)
183 | Statement::Drop { .. }
184 | Statement::AlterTable(_)
185 | Statement::Vacuum(_)
186 );
187 let is_vacuum = matches!(&query, Statement::Vacuum(_));
188
189 let releases_pages = match &query {
195 Statement::Drop { object_type, .. } => {
196 matches!(object_type, ObjectType::Table | ObjectType::Index)
197 }
198 Statement::AlterTable(alter) => alter
199 .operations
200 .iter()
201 .any(|op| matches!(op, AlterTableOperation::DropColumn { .. })),
202 _ => false,
203 };
204
205 if is_write_statement && db.is_read_only() {
211 return Err(SQLRiteError::General(
212 "cannot execute: database is opened read-only".to_string(),
213 ));
214 }
215
216 match query {
218 Statement::CreateTable(_) => {
219 let create_query = CreateQuery::new(&query);
220 match create_query {
221 Ok(payload) => {
222 let table_name = payload.table_name.clone();
223 if table_name == pager::MASTER_TABLE_NAME {
224 return Err(SQLRiteError::General(format!(
225 "'{}' is a reserved name used by the internal schema catalog",
226 pager::MASTER_TABLE_NAME
227 )));
228 }
229 match db.contains_table(table_name.to_string()) {
231 true => {
232 return Err(SQLRiteError::Internal(
233 "Cannot create, table already exists.".to_string(),
234 ));
235 }
236 false => {
237 let table = Table::new(payload);
238 db.tables.insert(table_name.to_string(), table);
246 message = String::from("CREATE TABLE Statement executed.");
247 }
248 }
249 }
250 Err(err) => return Err(err),
251 }
252 }
253 Statement::Insert(_) => {
254 let insert_query = InsertQuery::new(&query);
255 match insert_query {
256 Ok(payload) => {
257 let table_name = payload.table_name;
258 let columns = payload.columns;
259 let values = payload.rows;
260
261 match db.contains_table(table_name.to_string()) {
264 true => {
265 let db_table = db.get_table_mut(table_name.to_string()).unwrap();
266 match columns
268 .iter()
269 .all(|column| db_table.contains_column(column.to_string()))
270 {
271 true => {
272 for value in &values {
273 if columns.len() != value.len() {
275 return Err(SQLRiteError::Internal(format!(
276 "{} values for {} columns",
277 value.len(),
278 columns.len()
279 )));
280 }
281 db_table
282 .validate_unique_constraint(&columns, value)
283 .map_err(|err| {
284 SQLRiteError::Internal(format!(
285 "Unique key constraint violation: {err}"
286 ))
287 })?;
288 db_table.insert_row(&columns, value)?;
289 }
290 }
291 false => {
292 return Err(SQLRiteError::Internal(
293 "Cannot insert, some of the columns do not exist"
294 .to_string(),
295 ));
296 }
297 }
298 }
305 false => {
306 return Err(SQLRiteError::Internal("Table doesn't exist".to_string()));
307 }
308 }
309 }
310 Err(err) => return Err(err),
311 }
312
313 message = String::from("INSERT Statement executed.")
314 }
315 Statement::Query(_) => {
316 let select_query = SelectQuery::new(&query)?;
317 let (rendered_table, rows) = executor::execute_select(select_query, db)?;
318 rendered = Some(rendered_table);
324 message = format!(
325 "SELECT Statement executed. {rows} row{s} returned.",
326 s = if rows == 1 { "" } else { "s" }
327 );
328 }
329 Statement::Delete(_) => {
330 let rows = executor::execute_delete(&query, db)?;
331 message = format!(
332 "DELETE Statement executed. {rows} row{s} deleted.",
333 s = if rows == 1 { "" } else { "s" }
334 );
335 }
336 Statement::Update(_) => {
337 let rows = executor::execute_update(&query, db)?;
338 message = format!(
339 "UPDATE Statement executed. {rows} row{s} updated.",
340 s = if rows == 1 { "" } else { "s" }
341 );
342 }
343 Statement::CreateIndex(_) => {
344 let name = executor::execute_create_index(&query, db)?;
345 message = format!("CREATE INDEX '{name}' executed.");
346 }
347 Statement::Drop {
348 object_type,
349 if_exists,
350 names,
351 ..
352 } => match object_type {
353 ObjectType::Table => {
354 let count = executor::execute_drop_table(&names, if_exists, db)?;
355 let plural = if count == 1 { "table" } else { "tables" };
356 message = format!("DROP TABLE Statement executed. {count} {plural} dropped.");
357 }
358 ObjectType::Index => {
359 let count = executor::execute_drop_index(&names, if_exists, db)?;
360 let plural = if count == 1 { "index" } else { "indexes" };
361 message = format!("DROP INDEX Statement executed. {count} {plural} dropped.");
362 }
363 other => {
364 return Err(SQLRiteError::NotImplemented(format!(
365 "DROP {other:?} is not supported (only TABLE and INDEX)"
366 )));
367 }
368 },
369 Statement::AlterTable(alter) => {
370 message = executor::execute_alter_table(alter, db)?;
371 }
372 Statement::Vacuum(vac) => {
373 if vac.full
378 || vac.sort_only
379 || vac.delete_only
380 || vac.reindex
381 || vac.recluster
382 || vac.boost
383 || vac.table_name.is_some()
384 || vac.threshold.is_some()
385 {
386 return Err(SQLRiteError::NotImplemented(
387 "VACUUM modifiers (FULL, REINDEX, table targets, etc.) are not supported; use bare VACUUM;"
388 .to_string(),
389 ));
390 }
391 message = executor::execute_vacuum(db)?;
392 }
393 _ => {
394 return Err(SQLRiteError::NotImplemented(
395 "SQL Statement not supported yet.".to_string(),
396 ));
397 }
398 };
399
400 if is_write_statement && !is_vacuum && db.source_path.is_some() && !db.in_transaction() {
414 let path = db.source_path.clone().unwrap();
415 pager::save_database(db, &path)?;
416 }
417
418 if releases_pages && !db.in_transaction() {
430 if let (Some(threshold), Some(path)) = (db.auto_vacuum_threshold(), db.source_path.clone())
431 {
432 let should = match db.pager.as_ref() {
433 Some(p) => pager::freelist::should_auto_vacuum(p, threshold)?,
434 None => false,
435 };
436 if should {
437 pager::vacuum_database(db, &path)?;
438 }
439 }
440 }
441
442 Ok(CommandOutput {
443 status: message,
444 rendered,
445 })
446}
447
448#[cfg(test)]
449mod tests {
450 use super::*;
451 use crate::sql::db::table::Value;
452
453 fn seed_users_table() -> Database {
456 let mut db = Database::new("tempdb".to_string());
457 process_command(
458 "CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT NOT NULL, age INTEGER);",
459 &mut db,
460 )
461 .expect("create table");
462 process_command(
463 "INSERT INTO users (name, age) VALUES ('alice', 30);",
464 &mut db,
465 )
466 .expect("insert alice");
467 process_command("INSERT INTO users (name, age) VALUES ('bob', 25);", &mut db)
468 .expect("insert bob");
469 process_command(
470 "INSERT INTO users (name, age) VALUES ('carol', 40);",
471 &mut db,
472 )
473 .expect("insert carol");
474 db
475 }
476
477 #[test]
478 fn process_command_select_all_test() {
479 let mut db = seed_users_table();
480 let response = process_command("SELECT * FROM users;", &mut db).expect("select");
481 assert!(response.contains("3 rows returned"));
482 }
483
484 #[test]
485 fn process_command_select_where_test() {
486 let mut db = seed_users_table();
487 let response =
488 process_command("SELECT name FROM users WHERE age > 25;", &mut db).expect("select");
489 assert!(response.contains("2 rows returned"));
490 }
491
492 #[test]
493 fn process_command_select_eq_string_test() {
494 let mut db = seed_users_table();
495 let response =
496 process_command("SELECT name FROM users WHERE name = 'bob';", &mut db).expect("select");
497 assert!(response.contains("1 row returned"));
498 }
499
500 #[test]
501 fn process_command_select_limit_test() {
502 let mut db = seed_users_table();
503 let response = process_command("SELECT * FROM users ORDER BY age ASC LIMIT 2;", &mut db)
504 .expect("select");
505 assert!(response.contains("2 rows returned"));
506 }
507
508 #[test]
509 fn process_command_select_unknown_table_test() {
510 let mut db = Database::new("tempdb".to_string());
511 let result = process_command("SELECT * FROM nope;", &mut db);
512 assert!(result.is_err());
513 }
514
515 #[test]
516 fn process_command_select_unknown_column_test() {
517 let mut db = seed_users_table();
518 let result = process_command("SELECT height FROM users;", &mut db);
519 assert!(result.is_err());
520 }
521
522 #[test]
523 fn process_command_insert_test() {
524 let mut db = Database::new("tempdb".to_string());
526
527 let query_statement = "CREATE TABLE users (
529 id INTEGER PRIMARY KEY,
530 name TEXT
531 );";
532 let dialect = SQLiteDialect {};
533 let mut ast = Parser::parse_sql(&dialect, query_statement).unwrap();
534 if ast.len() > 1 {
535 panic!("Expected a single query statement, but there are more then 1.")
536 }
537 let query = ast.pop().unwrap();
538 let create_query = CreateQuery::new(&query).unwrap();
539
540 db.tables.insert(
542 create_query.table_name.to_string(),
543 Table::new(create_query),
544 );
545
546 let insert_query = String::from("INSERT INTO users (name) Values ('josh');");
548 match process_command(&insert_query, &mut db) {
549 Ok(response) => assert_eq!(response, "INSERT Statement executed."),
550 Err(err) => {
551 eprintln!("Error: {}", err);
552 assert!(false)
553 }
554 };
555 }
556
557 #[test]
558 fn process_command_insert_no_pk_test() {
559 let mut db = Database::new("tempdb".to_string());
561
562 let query_statement = "CREATE TABLE users (
564 name TEXT
565 );";
566 let dialect = SQLiteDialect {};
567 let mut ast = Parser::parse_sql(&dialect, query_statement).unwrap();
568 if ast.len() > 1 {
569 panic!("Expected a single query statement, but there are more then 1.")
570 }
571 let query = ast.pop().unwrap();
572 let create_query = CreateQuery::new(&query).unwrap();
573
574 db.tables.insert(
576 create_query.table_name.to_string(),
577 Table::new(create_query),
578 );
579
580 let insert_query = String::from("INSERT INTO users (name) Values ('josh');");
582 match process_command(&insert_query, &mut db) {
583 Ok(response) => assert_eq!(response, "INSERT Statement executed."),
584 Err(err) => {
585 eprintln!("Error: {}", err);
586 assert!(false)
587 }
588 };
589 }
590
591 #[test]
592 fn process_command_delete_where_test() {
593 let mut db = seed_users_table();
594 let response =
595 process_command("DELETE FROM users WHERE name = 'bob';", &mut db).expect("delete");
596 assert!(response.contains("1 row deleted"));
597
598 let remaining = process_command("SELECT * FROM users;", &mut db).expect("select");
599 assert!(remaining.contains("2 rows returned"));
600 }
601
602 #[test]
603 fn process_command_delete_all_test() {
604 let mut db = seed_users_table();
605 let response = process_command("DELETE FROM users;", &mut db).expect("delete");
606 assert!(response.contains("3 rows deleted"));
607 }
608
609 #[test]
610 fn process_command_update_where_test() {
611 use crate::sql::db::table::Value;
612
613 let mut db = seed_users_table();
614 let response = process_command("UPDATE users SET age = 99 WHERE name = 'bob';", &mut db)
615 .expect("update");
616 assert!(response.contains("1 row updated"));
617
618 let users = db.get_table("users".to_string()).unwrap();
620 let bob_rowid = users
621 .rowids()
622 .into_iter()
623 .find(|r| users.get_value("name", *r) == Some(Value::Text("bob".to_string())))
624 .expect("bob row must exist");
625 assert_eq!(users.get_value("age", bob_rowid), Some(Value::Integer(99)));
626 }
627
628 #[test]
629 fn process_command_update_unique_violation_test() {
630 let mut db = seed_users_table();
631 process_command(
633 "CREATE TABLE tags (id INTEGER PRIMARY KEY, label TEXT UNIQUE);",
634 &mut db,
635 )
636 .unwrap();
637 process_command("INSERT INTO tags (label) VALUES ('a');", &mut db).unwrap();
638 process_command("INSERT INTO tags (label) VALUES ('b');", &mut db).unwrap();
639
640 let result = process_command("UPDATE tags SET label = 'a' WHERE label = 'b';", &mut db);
641 assert!(result.is_err(), "expected UNIQUE violation, got {result:?}");
642 }
643
644 #[test]
645 fn process_command_insert_type_mismatch_returns_error_test() {
646 let mut db = Database::new("tempdb".to_string());
648 process_command(
649 "CREATE TABLE items (id INTEGER PRIMARY KEY, qty INTEGER);",
650 &mut db,
651 )
652 .unwrap();
653 let result = process_command("INSERT INTO items (qty) VALUES ('not a number');", &mut db);
654 assert!(result.is_err(), "expected error, got {result:?}");
655 }
656
657 #[test]
658 fn insert_omitted_integer_column_is_stored_as_null() {
659 use crate::sql::db::table::Value;
665
666 let mut db = Database::new("tempdb".to_string());
667 process_command(
668 "CREATE TABLE items (id INTEGER PRIMARY KEY, qty INTEGER);",
669 &mut db,
670 )
671 .unwrap();
672 process_command("INSERT INTO items (id) VALUES (1);", &mut db)
673 .expect("INSERT with omitted INTEGER column should succeed and store NULL");
674
675 let table = db.get_table("items".to_string()).unwrap();
676 let rowid = table.rowids().pop().expect("one row");
677 assert_eq!(table.get_value("qty", rowid), None);
680 let response = process_command("SELECT id FROM items WHERE qty IS NULL;", &mut db)
682 .expect("select IS NULL");
683 assert!(
684 response.contains("1 row returned"),
685 "qty IS NULL should match the omitted-column row, got: {response}"
686 );
687 process_command("INSERT INTO items (id, qty) VALUES (2, 7);", &mut db).unwrap();
689 let table = db.get_table("items".to_string()).unwrap();
690 let row_two = table
691 .rowids()
692 .into_iter()
693 .find(|r| table.get_value("id", *r) == Some(Value::Integer(2)))
694 .unwrap();
695 assert_eq!(table.get_value("qty", row_two), Some(Value::Integer(7)));
696 }
697
698 #[test]
699 fn insert_explicit_null_into_integer_column() {
700 let mut db = Database::new("tempdb".to_string());
701 process_command(
702 "CREATE TABLE t (id INTEGER PRIMARY KEY, n INTEGER);",
703 &mut db,
704 )
705 .unwrap();
706 process_command("INSERT INTO t (id, n) VALUES (1, NULL);", &mut db)
707 .expect("INSERT explicit NULL into INTEGER must not panic on parse::<i32>()");
708 let table = db.get_table("t".to_string()).unwrap();
709 let rowid = table.rowids().pop().unwrap();
710 assert_eq!(table.get_value("n", rowid), None);
711 }
712
713 #[test]
714 fn insert_explicit_null_into_text_column() {
715 use crate::sql::db::table::Value;
720
721 let mut db = Database::new("tempdb".to_string());
722 process_command("CREATE TABLE t (id INTEGER PRIMARY KEY, s TEXT);", &mut db).unwrap();
723 process_command("INSERT INTO t (id, s) VALUES (1, NULL);", &mut db).unwrap();
724 process_command("INSERT INTO t (id, s) VALUES (2, 'hi');", &mut db).unwrap();
725
726 let table = db.get_table("t".to_string()).unwrap();
727 let row_one = table
728 .rowids()
729 .into_iter()
730 .find(|r| table.get_value("id", *r) == Some(Value::Integer(1)))
731 .unwrap();
732 let row_two = table
733 .rowids()
734 .into_iter()
735 .find(|r| table.get_value("id", *r) == Some(Value::Integer(2)))
736 .unwrap();
737 assert_eq!(table.get_value("s", row_one), None);
738 assert_eq!(
739 table.get_value("s", row_two),
740 Some(Value::Text("hi".to_string()))
741 );
742 }
743
744 #[test]
745 fn insert_explicit_null_into_real_column() {
746 let mut db = Database::new("tempdb".to_string());
747 process_command(
748 "CREATE TABLE t (id INTEGER PRIMARY KEY, score REAL);",
749 &mut db,
750 )
751 .unwrap();
752 process_command("INSERT INTO t (id, score) VALUES (1, NULL);", &mut db)
753 .expect("INSERT explicit NULL into REAL must not panic on parse::<f32>()");
754 let table = db.get_table("t".to_string()).unwrap();
755 let rowid = table.rowids().pop().unwrap();
756 assert_eq!(table.get_value("score", rowid), None);
757 }
758
759 #[test]
760 fn insert_explicit_null_into_bool_column() {
761 let mut db = Database::new("tempdb".to_string());
762 process_command(
763 "CREATE TABLE t (id INTEGER PRIMARY KEY, flag BOOLEAN);",
764 &mut db,
765 )
766 .unwrap();
767 process_command("INSERT INTO t (id, flag) VALUES (1, NULL);", &mut db)
768 .expect("INSERT explicit NULL into BOOL must not panic on parse::<bool>()");
769 let table = db.get_table("t".to_string()).unwrap();
770 let rowid = table.rowids().pop().unwrap();
771 assert_eq!(table.get_value("flag", rowid), None);
772 }
773
774 #[test]
775 fn insert_explicit_null_into_vector_column() {
776 let mut db = Database::new("tempdb".to_string());
777 process_command(
778 "CREATE TABLE t (id INTEGER PRIMARY KEY, v VECTOR(3));",
779 &mut db,
780 )
781 .unwrap();
782 process_command("INSERT INTO t (id, v) VALUES (1, NULL);", &mut db)
783 .expect("INSERT explicit NULL into VECTOR must not panic on parse_vector_literal");
784 let table = db.get_table("t".to_string()).unwrap();
785 let rowid = table.rowids().pop().unwrap();
786 assert_eq!(table.get_value("v", rowid), None);
787 }
788
789 #[test]
790 fn insert_explicit_null_into_json_column() {
791 let mut db = Database::new("tempdb".to_string());
792 process_command(
793 "CREATE TABLE t (id INTEGER PRIMARY KEY, doc JSON);",
794 &mut db,
795 )
796 .unwrap();
797 process_command("INSERT INTO t (id, doc) VALUES (1, NULL);", &mut db)
798 .expect("INSERT explicit NULL into JSON must skip serde_json validation");
799 let table = db.get_table("t".to_string()).unwrap();
800 let rowid = table.rowids().pop().unwrap();
801 assert_eq!(table.get_value("doc", rowid), None);
802 }
803
804 #[test]
805 fn default_does_not_override_explicit_null() {
806 use crate::sql::db::table::Value;
811
812 let mut db = Database::new("tempdb".to_string());
813 process_command(
814 "CREATE TABLE t (id INTEGER PRIMARY KEY, n INTEGER DEFAULT 0);",
815 &mut db,
816 )
817 .unwrap();
818 process_command("INSERT INTO t (id, n) VALUES (1, NULL);", &mut db).unwrap();
819 process_command("INSERT INTO t (id) VALUES (2);", &mut db).unwrap();
820
821 let table = db.get_table("t".to_string()).unwrap();
822 let row_one = table
823 .rowids()
824 .into_iter()
825 .find(|r| table.get_value("id", *r) == Some(Value::Integer(1)))
826 .unwrap();
827 let row_two = table
828 .rowids()
829 .into_iter()
830 .find(|r| table.get_value("id", *r) == Some(Value::Integer(2)))
831 .unwrap();
832 assert_eq!(table.get_value("n", row_one), None);
834 assert_eq!(table.get_value("n", row_two), Some(Value::Integer(0)));
836 }
837
838 #[test]
839 fn process_command_update_arith_test() {
840 use crate::sql::db::table::Value;
841
842 let mut db = seed_users_table();
843 process_command("UPDATE users SET age = age + 1;", &mut db).expect("update +1");
844
845 let users = db.get_table("users".to_string()).unwrap();
846 let mut ages: Vec<i64> = users
847 .rowids()
848 .into_iter()
849 .filter_map(|r| match users.get_value("age", r) {
850 Some(Value::Integer(n)) => Some(n),
851 _ => None,
852 })
853 .collect();
854 ages.sort();
855 assert_eq!(ages, vec![26, 31, 41]); }
857
858 #[test]
859 fn process_command_select_arithmetic_where_test() {
860 let mut db = seed_users_table();
861 let response =
863 process_command("SELECT name FROM users WHERE age * 2 > 55;", &mut db).expect("select");
864 assert!(response.contains("2 rows returned"));
865 }
866
867 #[test]
868 fn process_command_divide_by_zero_test() {
869 let mut db = seed_users_table();
870 let result = process_command("SELECT age / 0 FROM users;", &mut db);
871 assert!(result.is_err());
873 }
874
875 #[test]
876 fn process_command_unsupported_statement_test() {
877 let mut db = Database::new("tempdb".to_string());
878 let result = process_command("CREATE VIEW v AS SELECT * FROM users;", &mut db);
882 assert!(result.is_err());
883 }
884
885 #[test]
886 fn empty_input_is_a_noop_not_a_panic() {
887 let mut db = Database::new("t".to_string());
891 for input in ["", " ", "-- just a comment", "-- comment\n-- another"] {
892 let result = process_command(input, &mut db);
893 assert!(result.is_ok(), "input {input:?} should not error");
894 let msg = result.unwrap();
895 assert!(msg.contains("No statement"), "got: {msg:?}");
896 }
897 }
898
899 #[test]
900 fn create_index_adds_explicit_index() {
901 let mut db = seed_users_table();
902 let response = process_command("CREATE INDEX users_age_idx ON users (age);", &mut db)
903 .expect("create index");
904 assert!(response.contains("users_age_idx"));
905
906 let users = db.get_table("users".to_string()).unwrap();
908 let idx = users
909 .index_by_name("users_age_idx")
910 .expect("index should exist after CREATE INDEX");
911 assert_eq!(idx.column_name, "age");
912 assert!(!idx.is_unique);
913 }
914
915 #[test]
916 fn create_unique_index_rejects_duplicate_existing_values() {
917 let mut db = seed_users_table();
918 process_command("INSERT INTO users (name, age) VALUES ('dan', 30);", &mut db).unwrap();
921 let result = process_command(
922 "CREATE UNIQUE INDEX users_age_unique ON users (age);",
923 &mut db,
924 );
925 assert!(
926 result.is_err(),
927 "expected unique-index failure, got {result:?}"
928 );
929 }
930
931 #[test]
932 fn where_eq_on_indexed_column_uses_index_probe() {
933 let mut db = Database::new("t".to_string());
937 process_command(
938 "CREATE TABLE big (id INTEGER PRIMARY KEY, tag TEXT);",
939 &mut db,
940 )
941 .unwrap();
942 process_command("CREATE INDEX big_tag_idx ON big (tag);", &mut db).unwrap();
943 for i in 1..=100 {
944 let tag = if i % 3 == 0 { "hot" } else { "cold" };
945 process_command(&format!("INSERT INTO big (tag) VALUES ('{tag}');"), &mut db).unwrap();
946 }
947 let response =
948 process_command("SELECT id FROM big WHERE tag = 'hot';", &mut db).expect("select");
949 assert!(
951 response.contains("33 rows returned"),
952 "response was {response:?}"
953 );
954 }
955
956 #[test]
957 fn where_eq_on_indexed_column_inside_parens_uses_index_probe() {
958 let mut db = seed_users_table();
959 let response = process_command("SELECT name FROM users WHERE (name = 'bob');", &mut db)
960 .expect("select");
961 assert!(response.contains("1 row returned"));
962 }
963
964 #[test]
965 fn where_eq_literal_first_side_uses_index_probe() {
966 let mut db = seed_users_table();
967 let response =
969 process_command("SELECT name FROM users WHERE 'bob' = name;", &mut db).expect("select");
970 assert!(response.contains("1 row returned"));
971 }
972
973 #[test]
974 fn non_equality_where_still_falls_back_to_full_scan() {
975 let mut db = seed_users_table();
978 let response =
979 process_command("SELECT name FROM users WHERE age > 28;", &mut db).expect("select");
980 assert!(response.contains("2 rows returned"));
981 }
982
983 #[test]
988 fn rollback_restores_pre_begin_in_memory_state() {
989 let mut db = seed_users_table();
992 let before = db.get_table("users".to_string()).unwrap().rowids().len();
993 assert_eq!(before, 3);
994
995 process_command("BEGIN;", &mut db).expect("BEGIN");
996 assert!(db.in_transaction());
997 process_command("INSERT INTO users (name, age) VALUES ('dan', 50);", &mut db)
998 .expect("INSERT inside txn");
999 let mid = db.get_table("users".to_string()).unwrap().rowids().len();
1001 assert_eq!(mid, 4);
1002
1003 process_command("ROLLBACK;", &mut db).expect("ROLLBACK");
1004 assert!(!db.in_transaction());
1005 let after = db.get_table("users".to_string()).unwrap().rowids().len();
1006 assert_eq!(
1007 after, 3,
1008 "ROLLBACK should have restored the pre-BEGIN state"
1009 );
1010 }
1011
1012 #[test]
1013 fn commit_keeps_mutations_and_clears_txn_flag() {
1014 let mut db = seed_users_table();
1015 process_command("BEGIN;", &mut db).expect("BEGIN");
1016 process_command("INSERT INTO users (name, age) VALUES ('dan', 50);", &mut db)
1017 .expect("INSERT inside txn");
1018 process_command("COMMIT;", &mut db).expect("COMMIT");
1019 assert!(!db.in_transaction());
1020 let after = db.get_table("users".to_string()).unwrap().rowids().len();
1021 assert_eq!(after, 4);
1022 }
1023
1024 #[test]
1025 fn rollback_undoes_update_and_delete_side_by_side() {
1026 use crate::sql::db::table::Value;
1027
1028 let mut db = seed_users_table();
1029 process_command("BEGIN;", &mut db).unwrap();
1030 process_command("UPDATE users SET age = 999;", &mut db).unwrap();
1031 process_command("DELETE FROM users WHERE name = 'bob';", &mut db).unwrap();
1032 let users = db.get_table("users".to_string()).unwrap();
1034 assert_eq!(users.rowids().len(), 2);
1035 for r in users.rowids() {
1036 assert_eq!(users.get_value("age", r), Some(Value::Integer(999)));
1037 }
1038
1039 process_command("ROLLBACK;", &mut db).unwrap();
1040 let users = db.get_table("users".to_string()).unwrap();
1041 assert_eq!(users.rowids().len(), 3);
1042 for r in users.rowids() {
1044 assert_ne!(users.get_value("age", r), Some(Value::Integer(999)));
1045 }
1046 }
1047
1048 #[test]
1049 fn nested_begin_is_rejected() {
1050 let mut db = seed_users_table();
1051 process_command("BEGIN;", &mut db).unwrap();
1052 let err = process_command("BEGIN;", &mut db).unwrap_err();
1053 assert!(
1054 format!("{err}").contains("already open"),
1055 "nested BEGIN should error; got: {err}"
1056 );
1057 assert!(db.in_transaction());
1059 process_command("ROLLBACK;", &mut db).unwrap();
1060 }
1061
1062 #[test]
1063 fn orphan_commit_and_rollback_are_rejected() {
1064 let mut db = seed_users_table();
1065 let commit_err = process_command("COMMIT;", &mut db).unwrap_err();
1066 assert!(format!("{commit_err}").contains("no transaction"));
1067 let rollback_err = process_command("ROLLBACK;", &mut db).unwrap_err();
1068 assert!(format!("{rollback_err}").contains("no transaction"));
1069 }
1070
1071 #[test]
1072 fn error_inside_transaction_keeps_txn_open() {
1073 let mut db = seed_users_table();
1077 process_command("BEGIN;", &mut db).unwrap();
1078 let err = process_command("INSERT INTO nope (x) VALUES (1);", &mut db);
1079 assert!(err.is_err());
1080 assert!(db.in_transaction(), "txn should stay open after error");
1081 process_command("ROLLBACK;", &mut db).unwrap();
1082 }
1083
1084 fn seed_file_backed(name: &str, schema: &str) -> (std::path::PathBuf, Database) {
1089 use crate::sql::pager::{open_database, save_database};
1090 let mut p = std::env::temp_dir();
1091 let pid = std::process::id();
1092 let nanos = std::time::SystemTime::now()
1093 .duration_since(std::time::UNIX_EPOCH)
1094 .map(|d| d.as_nanos())
1095 .unwrap_or(0);
1096 p.push(format!("sqlrite-txn-{name}-{pid}-{nanos}.sqlrite"));
1097
1098 {
1102 let mut seed = Database::new("t".to_string());
1103 process_command(schema, &mut seed).unwrap();
1104 save_database(&mut seed, &p).unwrap();
1105 }
1106 let db = open_database(&p, "t".to_string()).unwrap();
1107 (p, db)
1108 }
1109
1110 fn cleanup_file(path: &std::path::Path) {
1111 let _ = std::fs::remove_file(path);
1112 let mut wal = path.as_os_str().to_owned();
1113 wal.push("-wal");
1114 let _ = std::fs::remove_file(std::path::PathBuf::from(wal));
1115 }
1116
1117 #[test]
1118 fn null_values_round_trip_through_disk() {
1119 use crate::sql::db::table::Value;
1125 use crate::sql::pager::open_database;
1126
1127 let (path, mut db) = seed_file_backed(
1128 "nullrt",
1129 "CREATE TABLE t (id INTEGER PRIMARY KEY, n INTEGER, s TEXT, score REAL, flag BOOLEAN);",
1130 );
1131 process_command(
1132 "INSERT INTO t (id, n, s, score, flag) VALUES (1, 10, 'hi', 1.5, true);",
1133 &mut db,
1134 )
1135 .unwrap();
1136 process_command(
1137 "INSERT INTO t (id, n, s, score, flag) VALUES (2, NULL, NULL, NULL, NULL);",
1138 &mut db,
1139 )
1140 .unwrap();
1141 process_command("INSERT INTO t (id) VALUES (3);", &mut db).unwrap();
1143
1144 drop(db); let reopened = open_database(&path, "t".to_string()).unwrap();
1147 let t = reopened.get_table("t".to_string()).unwrap();
1148 let by_id = |id: i64| {
1149 t.rowids()
1150 .into_iter()
1151 .find(|r| t.get_value("id", *r) == Some(Value::Integer(id)))
1152 .unwrap_or_else(|| panic!("row id={id} not found"))
1153 };
1154
1155 let r1 = by_id(1);
1156 assert_eq!(t.get_value("n", r1), Some(Value::Integer(10)));
1157 assert_eq!(t.get_value("s", r1), Some(Value::Text("hi".to_string())));
1158 assert_eq!(t.get_value("score", r1), Some(Value::Real(1.5)));
1159 assert_eq!(t.get_value("flag", r1), Some(Value::Bool(true)));
1160
1161 for r in [by_id(2), by_id(3)] {
1162 assert_eq!(t.get_value("n", r), None, "INTEGER NULL must round-trip");
1163 assert_eq!(t.get_value("s", r), None, "TEXT NULL must round-trip");
1164 assert_eq!(t.get_value("score", r), None, "REAL NULL must round-trip");
1165 assert_eq!(t.get_value("flag", r), None, "BOOL NULL must round-trip");
1166 }
1167
1168 drop(reopened);
1169 cleanup_file(&path);
1170 }
1171
1172 #[test]
1173 fn begin_commit_rollback_round_trip_through_disk() {
1174 use crate::sql::pager::open_database;
1178
1179 let (path, mut db) = seed_file_backed(
1180 "roundtrip",
1181 "CREATE TABLE notes (id INTEGER PRIMARY KEY, body TEXT);",
1182 );
1183
1184 process_command("BEGIN;", &mut db).unwrap();
1186 process_command("INSERT INTO notes (body) VALUES ('a');", &mut db).unwrap();
1187 process_command("INSERT INTO notes (body) VALUES ('b');", &mut db).unwrap();
1188 process_command("COMMIT;", &mut db).unwrap();
1189
1190 process_command("BEGIN;", &mut db).unwrap();
1192 process_command("INSERT INTO notes (body) VALUES ('c');", &mut db).unwrap();
1193 process_command("ROLLBACK;", &mut db).unwrap();
1194
1195 drop(db); let reopened = open_database(&path, "t".to_string()).unwrap();
1198 let notes = reopened.get_table("notes".to_string()).unwrap();
1199 assert_eq!(notes.rowids().len(), 2, "committed rows should survive");
1200
1201 drop(reopened);
1202 cleanup_file(&path);
1203 }
1204
1205 #[test]
1206 fn write_inside_transaction_does_not_autosave() {
1207 let (path, mut db) =
1211 seed_file_backed("noas", "CREATE TABLE t (id INTEGER PRIMARY KEY, x TEXT);");
1212
1213 let mut wal_path = path.as_os_str().to_owned();
1214 wal_path.push("-wal");
1215 let wal_path = std::path::PathBuf::from(wal_path);
1216 let frames_before = std::fs::metadata(&wal_path).unwrap().len();
1217
1218 process_command("BEGIN;", &mut db).unwrap();
1219 process_command("INSERT INTO t (x) VALUES ('a');", &mut db).unwrap();
1220 process_command("INSERT INTO t (x) VALUES ('b');", &mut db).unwrap();
1221
1222 let frames_mid = std::fs::metadata(&wal_path).unwrap().len();
1224 assert_eq!(
1225 frames_before, frames_mid,
1226 "WAL should not grow during an open transaction"
1227 );
1228
1229 process_command("COMMIT;", &mut db).unwrap();
1230
1231 drop(db); let fresh = crate::sql::pager::open_database(&path, "t".to_string()).unwrap();
1233 assert_eq!(
1234 fresh.get_table("t".to_string()).unwrap().rowids().len(),
1235 2,
1236 "COMMIT should have persisted both inserted rows"
1237 );
1238 drop(fresh);
1239 cleanup_file(&path);
1240 }
1241
1242 #[test]
1243 fn rollback_undoes_create_table() {
1244 let mut db = seed_users_table();
1249 assert_eq!(db.tables.len(), 1);
1250
1251 process_command("BEGIN;", &mut db).unwrap();
1252 process_command(
1253 "CREATE TABLE dropme (id INTEGER PRIMARY KEY, x TEXT);",
1254 &mut db,
1255 )
1256 .unwrap();
1257 process_command("INSERT INTO dropme (x) VALUES ('stuff');", &mut db).unwrap();
1258 assert_eq!(db.tables.len(), 2);
1259
1260 process_command("ROLLBACK;", &mut db).unwrap();
1261 assert_eq!(
1262 db.tables.len(),
1263 1,
1264 "CREATE TABLE should have been rolled back"
1265 );
1266 assert!(db.get_table("dropme".to_string()).is_err());
1267 }
1268
1269 #[test]
1270 fn rollback_restores_secondary_index_state() {
1271 let mut db = Database::new("t".to_string());
1275 process_command(
1276 "CREATE TABLE users (id INTEGER PRIMARY KEY, email TEXT UNIQUE);",
1277 &mut db,
1278 )
1279 .unwrap();
1280 process_command("INSERT INTO users (email) VALUES ('a@x');", &mut db).unwrap();
1281
1282 process_command("BEGIN;", &mut db).unwrap();
1283 process_command("INSERT INTO users (email) VALUES ('b@x');", &mut db).unwrap();
1284 process_command("ROLLBACK;", &mut db).unwrap();
1286
1287 let reinsert = process_command("INSERT INTO users (email) VALUES ('b@x');", &mut db);
1291 assert!(
1292 reinsert.is_ok(),
1293 "re-insert after rollback should succeed, got {reinsert:?}"
1294 );
1295 }
1296
1297 #[test]
1298 fn rollback_restores_last_rowid_counter() {
1299 use crate::sql::db::table::Value;
1303
1304 let mut db = seed_users_table(); let pre = db.get_table("users".to_string()).unwrap().last_rowid;
1306
1307 process_command("BEGIN;", &mut db).unwrap();
1308 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();
1311
1312 let post = db.get_table("users".to_string()).unwrap().last_rowid;
1313 assert_eq!(pre, post, "last_rowid must roll back with the snapshot");
1314
1315 process_command("INSERT INTO users (name, age) VALUES ('d', 50);", &mut db).unwrap();
1317 let users = db.get_table("users".to_string()).unwrap();
1318 let d_rowid = users
1319 .rowids()
1320 .into_iter()
1321 .find(|r| users.get_value("name", *r) == Some(Value::Text("d".into())))
1322 .expect("d row must exist");
1323 assert_eq!(d_rowid, pre + 1);
1324 }
1325
1326 #[test]
1327 fn commit_on_in_memory_db_clears_txn_without_pager_call() {
1328 let mut db = seed_users_table(); assert!(db.source_path.is_none());
1333
1334 process_command("BEGIN;", &mut db).unwrap();
1335 process_command("INSERT INTO users (name, age) VALUES ('z', 99);", &mut db).unwrap();
1336 process_command("COMMIT;", &mut db).unwrap();
1337
1338 assert!(!db.in_transaction());
1339 assert_eq!(db.get_table("users".to_string()).unwrap().rowids().len(), 4);
1340 }
1341
1342 #[test]
1343 fn failed_commit_auto_rolls_back_in_memory_state() {
1344 use crate::sql::pager::save_database;
1358
1359 let (path, mut db) = seed_file_backed(
1361 "failcommit",
1362 "CREATE TABLE notes (id INTEGER PRIMARY KEY, body TEXT);",
1363 );
1364
1365 process_command("INSERT INTO notes (body) VALUES ('before');", &mut db).unwrap();
1367
1368 process_command("BEGIN;", &mut db).unwrap();
1370 process_command("INSERT INTO notes (body) VALUES ('inflight');", &mut db).unwrap();
1371 assert_eq!(
1372 db.get_table("notes".to_string()).unwrap().rowids().len(),
1373 2,
1374 "inflight row visible mid-txn"
1375 );
1376
1377 let orig_source = db.source_path.clone();
1381 let orig_pager = db.pager.take();
1382 db.source_path = Some(std::env::temp_dir());
1383
1384 let commit_result = process_command("COMMIT;", &mut db);
1385 assert!(commit_result.is_err(), "commit must fail");
1386 let err_str = format!("{}", commit_result.unwrap_err());
1387 assert!(
1388 err_str.contains("COMMIT failed") && err_str.contains("rolled back"),
1389 "error must surface auto-rollback; got: {err_str}"
1390 );
1391
1392 assert!(
1396 !db.in_transaction(),
1397 "txn must be cleared after auto-rollback"
1398 );
1399 assert_eq!(
1400 db.get_table("notes".to_string()).unwrap().rowids().len(),
1401 1,
1402 "inflight row must be rolled back"
1403 );
1404
1405 db.source_path = orig_source;
1408 db.pager = orig_pager;
1409 process_command("INSERT INTO notes (body) VALUES ('after');", &mut db).unwrap();
1410 drop(db);
1411
1412 let reopened = crate::sql::pager::open_database(&path, "t".to_string()).unwrap();
1414 let notes = reopened.get_table("notes".to_string()).unwrap();
1415 assert_eq!(notes.rowids().len(), 2);
1416 let _ = save_database; drop(reopened);
1419 cleanup_file(&path);
1420 }
1421
1422 #[test]
1423 fn begin_on_read_only_is_rejected() {
1424 use crate::sql::pager::{open_database_read_only, save_database};
1425
1426 let path = {
1427 let mut p = std::env::temp_dir();
1428 let pid = std::process::id();
1429 let nanos = std::time::SystemTime::now()
1430 .duration_since(std::time::UNIX_EPOCH)
1431 .map(|d| d.as_nanos())
1432 .unwrap_or(0);
1433 p.push(format!("sqlrite-txn-ro-{pid}-{nanos}.sqlrite"));
1434 p
1435 };
1436 {
1437 let mut seed = Database::new("t".to_string());
1438 process_command("CREATE TABLE t (id INTEGER PRIMARY KEY);", &mut seed).unwrap();
1439 save_database(&mut seed, &path).unwrap();
1440 }
1441
1442 let mut ro = open_database_read_only(&path, "t".to_string()).unwrap();
1443 let err = process_command("BEGIN;", &mut ro).unwrap_err();
1444 assert!(
1445 format!("{err}").contains("read-only"),
1446 "BEGIN on RO db should surface read-only; got: {err}"
1447 );
1448 assert!(!ro.in_transaction());
1449
1450 let _ = std::fs::remove_file(&path);
1451 let mut wal = path.as_os_str().to_owned();
1452 wal.push("-wal");
1453 let _ = std::fs::remove_file(std::path::PathBuf::from(wal));
1454 }
1455
1456 #[test]
1457 fn read_only_database_rejects_mutations_before_touching_state() {
1458 use crate::sql::pager::open_database_read_only;
1464
1465 let mut seed = Database::new("t".to_string());
1466 process_command(
1467 "CREATE TABLE notes (id INTEGER PRIMARY KEY, body TEXT);",
1468 &mut seed,
1469 )
1470 .unwrap();
1471 process_command("INSERT INTO notes (body) VALUES ('alpha');", &mut seed).unwrap();
1472
1473 let path = {
1474 let mut p = std::env::temp_dir();
1475 let pid = std::process::id();
1476 let nanos = std::time::SystemTime::now()
1477 .duration_since(std::time::UNIX_EPOCH)
1478 .map(|d| d.as_nanos())
1479 .unwrap_or(0);
1480 p.push(format!("sqlrite-ro-reject-{pid}-{nanos}.sqlrite"));
1481 p
1482 };
1483 crate::sql::pager::save_database(&mut seed, &path).unwrap();
1484 drop(seed);
1485
1486 let mut ro = open_database_read_only(&path, "t".to_string()).unwrap();
1487 let notes_before = ro.get_table("notes".to_string()).unwrap().rowids().len();
1488
1489 for stmt in [
1490 "INSERT INTO notes (body) VALUES ('beta');",
1491 "UPDATE notes SET body = 'x';",
1492 "DELETE FROM notes;",
1493 "CREATE TABLE more (id INTEGER PRIMARY KEY);",
1494 "CREATE INDEX notes_body ON notes (body);",
1495 ] {
1496 let err = process_command(stmt, &mut ro).unwrap_err();
1497 assert!(
1498 format!("{err}").contains("read-only"),
1499 "stmt {stmt:?} should surface a read-only error; got: {err}"
1500 );
1501 }
1502
1503 let notes_after = ro.get_table("notes".to_string()).unwrap().rowids().len();
1505 assert_eq!(notes_before, notes_after);
1506 let sel = process_command("SELECT * FROM notes;", &mut ro).expect("select on RO must work");
1507 assert!(sel.contains("1 row returned"));
1508
1509 drop(ro);
1511 let _ = std::fs::remove_file(&path);
1512 let mut wal = path.as_os_str().to_owned();
1513 wal.push("-wal");
1514 let _ = std::fs::remove_file(std::path::PathBuf::from(wal));
1515 }
1516
1517 #[test]
1522 fn vector_create_table_and_insert_basic() {
1523 let mut db = Database::new("tempdb".to_string());
1524 process_command(
1525 "CREATE TABLE docs (id INTEGER PRIMARY KEY, embedding VECTOR(3));",
1526 &mut db,
1527 )
1528 .expect("create table with VECTOR(3)");
1529 process_command(
1530 "INSERT INTO docs (embedding) VALUES ([0.1, 0.2, 0.3]);",
1531 &mut db,
1532 )
1533 .expect("insert vector");
1534
1535 let sel = process_command("SELECT * FROM docs;", &mut db).expect("select");
1539 assert!(sel.contains("1 row returned"));
1540
1541 let docs = db.get_table("docs".to_string()).expect("docs table");
1542 let rowids = docs.rowids();
1543 assert_eq!(rowids.len(), 1);
1544 match docs.get_value("embedding", rowids[0]) {
1545 Some(Value::Vector(v)) => assert_eq!(v, vec![0.1f32, 0.2, 0.3]),
1546 other => panic!("expected Value::Vector(...), got {other:?}"),
1547 }
1548 }
1549
1550 #[test]
1551 fn vector_dim_mismatch_at_insert_is_clean_error() {
1552 let mut db = Database::new("tempdb".to_string());
1553 process_command(
1554 "CREATE TABLE docs (id INTEGER PRIMARY KEY, embedding VECTOR(3));",
1555 &mut db,
1556 )
1557 .expect("create table");
1558
1559 let err = process_command("INSERT INTO docs (embedding) VALUES ([0.1, 0.2]);", &mut db)
1561 .unwrap_err();
1562 let msg = format!("{err}");
1563 assert!(
1564 msg.to_lowercase().contains("dimension")
1565 && msg.contains("declared 3")
1566 && msg.contains("got 2"),
1567 "expected clear dim-mismatch error, got: {msg}"
1568 );
1569
1570 let err = process_command(
1572 "INSERT INTO docs (embedding) VALUES ([0.1, 0.2, 0.3, 0.4, 0.5]);",
1573 &mut db,
1574 )
1575 .unwrap_err();
1576 assert!(
1577 format!("{err}").contains("got 5"),
1578 "expected dim-mismatch error mentioning got 5, got: {err}"
1579 );
1580 }
1581
1582 #[test]
1583 fn vector_create_table_rejects_missing_dim() {
1584 let mut db = Database::new("tempdb".to_string());
1585 let result = process_command(
1591 "CREATE TABLE docs (id INTEGER PRIMARY KEY, embedding VECTOR);",
1592 &mut db,
1593 );
1594 assert!(
1595 result.is_err(),
1596 "expected CREATE TABLE with bare VECTOR to fail (no dim)"
1597 );
1598 }
1599
1600 #[test]
1601 fn vector_create_table_rejects_zero_dim() {
1602 let mut db = Database::new("tempdb".to_string());
1603 let err = process_command(
1604 "CREATE TABLE docs (id INTEGER PRIMARY KEY, embedding VECTOR(0));",
1605 &mut db,
1606 )
1607 .unwrap_err();
1608 let msg = format!("{err}");
1609 assert!(
1610 msg.to_lowercase().contains("vector"),
1611 "expected VECTOR-related error for VECTOR(0), got: {msg}"
1612 );
1613 }
1614
1615 #[test]
1616 fn vector_high_dim_works() {
1617 let mut db = Database::new("tempdb".to_string());
1620 process_command(
1621 "CREATE TABLE embeddings (id INTEGER PRIMARY KEY, e VECTOR(384));",
1622 &mut db,
1623 )
1624 .expect("create table VECTOR(384)");
1625
1626 let lit = format!(
1627 "[{}]",
1628 (0..384)
1629 .map(|i| format!("{}", i as f32 * 0.001))
1630 .collect::<Vec<_>>()
1631 .join(",")
1632 );
1633 let sql = format!("INSERT INTO embeddings (e) VALUES ({lit});");
1634 process_command(&sql, &mut db).expect("insert 384-dim vector");
1635
1636 let sel = process_command("SELECT id FROM embeddings;", &mut db).expect("select id");
1637 assert!(sel.contains("1 row returned"));
1638 }
1639
1640 #[test]
1641 fn vector_multiple_rows() {
1642 let mut db = Database::new("tempdb".to_string());
1645 process_command(
1646 "CREATE TABLE docs (id INTEGER PRIMARY KEY, e VECTOR(2));",
1647 &mut db,
1648 )
1649 .expect("create");
1650 for i in 0..3 {
1651 let sql = format!("INSERT INTO docs (e) VALUES ([{i}.0, {}.0]);", i + 1);
1652 process_command(&sql, &mut db).expect("insert");
1653 }
1654 let sel = process_command("SELECT * FROM docs;", &mut db).expect("select");
1655 assert!(sel.contains("3 rows returned"));
1656
1657 let docs = db.get_table("docs".to_string()).expect("docs table");
1659 let rowids = docs.rowids();
1660 assert_eq!(rowids.len(), 3);
1661 let mut vectors: Vec<Vec<f32>> = rowids
1662 .iter()
1663 .filter_map(|r| match docs.get_value("e", *r) {
1664 Some(Value::Vector(v)) => Some(v),
1665 _ => None,
1666 })
1667 .collect();
1668 vectors.sort_by(|a, b| a[0].partial_cmp(&b[0]).unwrap());
1669 assert_eq!(vectors[0], vec![0.0f32, 1.0]);
1670 assert_eq!(vectors[1], vec![1.0f32, 2.0]);
1671 assert_eq!(vectors[2], vec![2.0f32, 3.0]);
1672 }
1673
1674 fn seed_hnsw_table() -> Database {
1682 let mut db = Database::new("tempdb".to_string());
1683 process_command(
1684 "CREATE TABLE docs (id INTEGER PRIMARY KEY, e VECTOR(2));",
1685 &mut db,
1686 )
1687 .unwrap();
1688 for v in &[
1689 "[1.0, 0.0]", "[2.0, 0.0]", "[0.0, 3.0]", "[1.0, 4.0]", "[10.0, 10.0]", ] {
1695 process_command(&format!("INSERT INTO docs (e) VALUES ({v});"), &mut db).unwrap();
1696 }
1697 db
1698 }
1699
1700 #[test]
1701 fn create_index_using_hnsw_succeeds() {
1702 let mut db = seed_hnsw_table();
1703 let resp = process_command("CREATE INDEX ix_e ON docs USING hnsw (e);", &mut db).unwrap();
1704 assert!(resp.to_lowercase().contains("create index"));
1705 let table = db.get_table("docs".to_string()).unwrap();
1707 assert_eq!(table.hnsw_indexes.len(), 1);
1708 assert_eq!(table.hnsw_indexes[0].name, "ix_e");
1709 assert_eq!(table.hnsw_indexes[0].column_name, "e");
1710 assert_eq!(table.hnsw_indexes[0].index.len(), 5);
1712 }
1713
1714 #[test]
1715 fn create_index_using_hnsw_rejects_non_vector_column() {
1716 let mut db = Database::new("tempdb".to_string());
1717 process_command(
1718 "CREATE TABLE t (id INTEGER PRIMARY KEY, name TEXT);",
1719 &mut db,
1720 )
1721 .unwrap();
1722 let err =
1723 process_command("CREATE INDEX ix_name ON t USING hnsw (name);", &mut db).unwrap_err();
1724 let msg = format!("{err}");
1725 assert!(
1726 msg.to_lowercase().contains("vector"),
1727 "expected error mentioning VECTOR; got: {msg}"
1728 );
1729 }
1730
1731 #[test]
1732 fn knn_query_uses_hnsw_after_create_index() {
1733 let mut db = seed_hnsw_table();
1739 process_command("CREATE INDEX ix_e ON docs USING hnsw (e);", &mut db).unwrap();
1740
1741 let resp = process_command(
1746 "SELECT id FROM docs ORDER BY vec_distance_l2(e, [1.0, 0.0]) ASC LIMIT 3;",
1747 &mut db,
1748 )
1749 .unwrap();
1750 assert!(resp.contains("3 rows returned"), "got: {resp}");
1751 }
1752
1753 #[test]
1754 fn knn_query_works_after_subsequent_inserts() {
1755 let mut db = seed_hnsw_table();
1759 process_command("CREATE INDEX ix_e ON docs USING hnsw (e);", &mut db).unwrap();
1760 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();
1764 assert_eq!(
1765 table.hnsw_indexes[0].index.len(),
1766 7,
1767 "incremental insert should grow HNSW alongside row storage"
1768 );
1769
1770 let resp = process_command(
1773 "SELECT id FROM docs ORDER BY vec_distance_l2(e, [0.0, 0.0]) ASC LIMIT 1;",
1774 &mut db,
1775 )
1776 .unwrap();
1777 assert!(resp.contains("1 row returned"), "got: {resp}");
1778 }
1779
1780 #[test]
1786 fn delete_on_hnsw_indexed_table_succeeds_and_marks_dirty() {
1787 let mut db = seed_hnsw_table();
1788 process_command("CREATE INDEX ix_e ON docs USING hnsw (e);", &mut db).unwrap();
1789 let resp = process_command("DELETE FROM docs WHERE id = 1;", &mut db).unwrap();
1790 assert!(resp.contains("1 row"), "expected 1 row deleted: {resp}");
1791
1792 let docs = db.get_table("docs".to_string()).unwrap();
1793 let entry = docs.hnsw_indexes.iter().find(|e| e.name == "ix_e").unwrap();
1794 assert!(
1795 entry.needs_rebuild,
1796 "DELETE should have marked HNSW index dirty for rebuild on next save"
1797 );
1798 }
1799
1800 #[test]
1801 fn update_on_hnsw_indexed_vector_col_succeeds_and_marks_dirty() {
1802 let mut db = seed_hnsw_table();
1803 process_command("CREATE INDEX ix_e ON docs USING hnsw (e);", &mut db).unwrap();
1804 let resp =
1805 process_command("UPDATE docs SET e = [9.0, 9.0] WHERE id = 1;", &mut db).unwrap();
1806 assert!(resp.contains("1 row"), "expected 1 row updated: {resp}");
1807
1808 let docs = db.get_table("docs".to_string()).unwrap();
1809 let entry = docs.hnsw_indexes.iter().find(|e| e.name == "ix_e").unwrap();
1810 assert!(
1811 entry.needs_rebuild,
1812 "UPDATE on the vector column should have marked HNSW index dirty"
1813 );
1814 }
1815
1816 #[test]
1817 fn duplicate_index_name_errors() {
1818 let mut db = seed_hnsw_table();
1819 process_command("CREATE INDEX ix_e ON docs USING hnsw (e);", &mut db).unwrap();
1820 let err =
1821 process_command("CREATE INDEX ix_e ON docs USING hnsw (e);", &mut db).unwrap_err();
1822 let msg = format!("{err}");
1823 assert!(
1824 msg.to_lowercase().contains("already exists"),
1825 "expected duplicate-index error; got: {msg}"
1826 );
1827 }
1828
1829 #[test]
1830 fn index_if_not_exists_is_idempotent() {
1831 let mut db = seed_hnsw_table();
1832 process_command("CREATE INDEX ix_e ON docs USING hnsw (e);", &mut db).unwrap();
1833 process_command(
1835 "CREATE INDEX IF NOT EXISTS ix_e ON docs USING hnsw (e);",
1836 &mut db,
1837 )
1838 .unwrap();
1839 let table = db.get_table("docs".to_string()).unwrap();
1840 assert_eq!(table.hnsw_indexes.len(), 1);
1841 }
1842
1843 fn seed_fts_table() -> Database {
1850 let mut db = Database::new("tempdb".to_string());
1851 process_command(
1852 "CREATE TABLE docs (id INTEGER PRIMARY KEY, body TEXT);",
1853 &mut db,
1854 )
1855 .unwrap();
1856 for body in &[
1857 "rust embedded database", "rust web framework", "go embedded systems", "python web framework", "rust rust rust embedded power", ] {
1863 process_command(
1864 &format!("INSERT INTO docs (body) VALUES ('{body}');"),
1865 &mut db,
1866 )
1867 .unwrap();
1868 }
1869 db
1870 }
1871
1872 #[test]
1873 fn create_index_using_fts_succeeds_and_indexes_existing_rows() {
1874 let mut db = seed_fts_table();
1875 let resp =
1876 process_command("CREATE INDEX ix_body ON docs USING fts (body);", &mut db).unwrap();
1877 assert!(resp.to_lowercase().contains("create index"), "got {resp}");
1878 let table = db.get_table("docs".to_string()).unwrap();
1879 assert_eq!(table.fts_indexes.len(), 1);
1880 assert_eq!(table.fts_indexes[0].name, "ix_body");
1881 assert_eq!(table.fts_indexes[0].column_name, "body");
1882 assert_eq!(table.fts_indexes[0].index.len(), 5);
1884 }
1885
1886 #[test]
1887 fn create_index_using_fts_rejects_non_text_column() {
1888 let mut db = Database::new("tempdb".to_string());
1889 process_command(
1890 "CREATE TABLE t (id INTEGER PRIMARY KEY, n INTEGER);",
1891 &mut db,
1892 )
1893 .unwrap();
1894 let err = process_command("CREATE INDEX ix_n ON t USING fts (n);", &mut db).unwrap_err();
1895 let msg = format!("{err}");
1896 assert!(
1897 msg.to_lowercase().contains("text"),
1898 "expected error mentioning TEXT; got: {msg}"
1899 );
1900 }
1901
1902 #[test]
1903 fn fts_match_returns_expected_rows() {
1904 let mut db = seed_fts_table();
1905 process_command("CREATE INDEX ix_body ON docs USING fts (body);", &mut db).unwrap();
1906 let resp = process_command(
1908 "SELECT id FROM docs WHERE fts_match(body, 'rust');",
1909 &mut db,
1910 )
1911 .unwrap();
1912 assert!(resp.contains("3 rows returned"), "got: {resp}");
1913 }
1914
1915 #[test]
1916 fn fts_match_without_index_errors_clearly() {
1917 let mut db = seed_fts_table();
1918 let err = process_command(
1920 "SELECT id FROM docs WHERE fts_match(body, 'rust');",
1921 &mut db,
1922 )
1923 .unwrap_err();
1924 let msg = format!("{err}");
1925 assert!(
1926 msg.contains("no FTS index"),
1927 "expected no-index error; got: {msg}"
1928 );
1929 }
1930
1931 #[test]
1932 fn bm25_score_orders_descending_by_relevance() {
1933 let mut db = seed_fts_table();
1934 process_command("CREATE INDEX ix_body ON docs USING fts (body);", &mut db).unwrap();
1935 let out = process_command_with_render(
1938 "SELECT id FROM docs WHERE fts_match(body, 'rust') \
1939 ORDER BY bm25_score(body, 'rust') DESC LIMIT 1;",
1940 &mut db,
1941 )
1942 .unwrap();
1943 assert!(out.status.contains("1 row returned"), "got: {}", out.status);
1944 let rendered = out.rendered.expect("SELECT should produce rendered output");
1945 assert!(
1947 rendered.contains(" 5 "),
1948 "expected id=5 to be top-ranked; rendered:\n{rendered}"
1949 );
1950 }
1951
1952 #[test]
1953 fn bm25_score_without_index_errors_clearly() {
1954 let mut db = seed_fts_table();
1955 let err = process_command(
1956 "SELECT id FROM docs ORDER BY bm25_score(body, 'rust') DESC LIMIT 1;",
1957 &mut db,
1958 )
1959 .unwrap_err();
1960 let msg = format!("{err}");
1961 assert!(
1962 msg.contains("no FTS index"),
1963 "expected no-index error; got: {msg}"
1964 );
1965 }
1966
1967 #[test]
1968 fn fts_post_create_inserts_are_indexed_incrementally() {
1969 let mut db = seed_fts_table();
1970 process_command("CREATE INDEX ix_body ON docs USING fts (body);", &mut db).unwrap();
1971 process_command(
1972 "INSERT INTO docs (body) VALUES ('rust embedded analytics');",
1973 &mut db,
1974 )
1975 .unwrap();
1976 let table = db.get_table("docs".to_string()).unwrap();
1977 assert_eq!(table.fts_indexes[0].index.len(), 6);
1979 let resp = process_command(
1981 "SELECT id FROM docs WHERE fts_match(body, 'analytics');",
1982 &mut db,
1983 )
1984 .unwrap();
1985 assert!(resp.contains("1 row returned"), "got: {resp}");
1986 }
1987
1988 #[test]
1989 fn delete_on_fts_indexed_table_marks_dirty() {
1990 let mut db = seed_fts_table();
1991 process_command("CREATE INDEX ix_body ON docs USING fts (body);", &mut db).unwrap();
1992 let resp = process_command("DELETE FROM docs WHERE id = 1;", &mut db).unwrap();
1993 assert!(resp.contains("1 row"), "got: {resp}");
1994 let docs = db.get_table("docs".to_string()).unwrap();
1995 let entry = docs
1996 .fts_indexes
1997 .iter()
1998 .find(|e| e.name == "ix_body")
1999 .unwrap();
2000 assert!(
2001 entry.needs_rebuild,
2002 "DELETE should have flagged the FTS index dirty"
2003 );
2004 }
2005
2006 #[test]
2007 fn update_on_fts_indexed_text_col_marks_dirty() {
2008 let mut db = seed_fts_table();
2009 process_command("CREATE INDEX ix_body ON docs USING fts (body);", &mut db).unwrap();
2010 let resp = process_command(
2011 "UPDATE docs SET body = 'java spring framework' WHERE id = 1;",
2012 &mut db,
2013 )
2014 .unwrap();
2015 assert!(resp.contains("1 row"), "got: {resp}");
2016 let docs = db.get_table("docs".to_string()).unwrap();
2017 let entry = docs
2018 .fts_indexes
2019 .iter()
2020 .find(|e| e.name == "ix_body")
2021 .unwrap();
2022 assert!(
2023 entry.needs_rebuild,
2024 "UPDATE on the indexed TEXT column should have flagged dirty"
2025 );
2026 }
2027
2028 #[test]
2029 fn fts_index_name_collides_with_btree_and_hnsw_namespaces() {
2030 let mut db = seed_fts_table();
2031 process_command("CREATE INDEX ix_body ON docs USING fts (body);", &mut db).unwrap();
2032 let err = process_command("CREATE INDEX ix_body ON docs (body);", &mut db).unwrap_err();
2033 let msg = format!("{err}");
2034 assert!(
2035 msg.to_lowercase().contains("already exists"),
2036 "expected duplicate-index error; got: {msg}"
2037 );
2038 }
2039
2040 #[test]
2041 fn fts_index_rejects_unique() {
2042 let mut db = seed_fts_table();
2043 let err = process_command(
2044 "CREATE UNIQUE INDEX ix_body ON docs USING fts (body);",
2045 &mut db,
2046 )
2047 .unwrap_err();
2048 let msg = format!("{err}");
2049 assert!(
2050 msg.to_lowercase().contains("unique"),
2051 "expected UNIQUE-rejection error; got: {msg}"
2052 );
2053 }
2054
2055 #[test]
2056 fn try_fts_probe_falls_through_on_ascending() {
2057 let mut db = seed_fts_table();
2062 process_command("CREATE INDEX ix_body ON docs USING fts (body);", &mut db).unwrap();
2063 let resp = process_command(
2066 "SELECT id FROM docs WHERE fts_match(body, 'rust') \
2067 ORDER BY bm25_score(body, 'rust') ASC LIMIT 3;",
2068 &mut db,
2069 )
2070 .unwrap();
2071 assert!(resp.contains("3 rows returned"), "got: {resp}");
2072 }
2073
2074 fn seed_vector_docs() -> Database {
2084 let mut db = Database::new("tempdb".to_string());
2085 process_command(
2086 "CREATE TABLE docs (id INTEGER PRIMARY KEY, e VECTOR(2));",
2087 &mut db,
2088 )
2089 .expect("create");
2090 process_command("INSERT INTO docs (e) VALUES ([1.0, 0.0]);", &mut db).expect("insert 1");
2091 process_command("INSERT INTO docs (e) VALUES ([0.0, 1.0]);", &mut db).expect("insert 2");
2092 process_command("INSERT INTO docs (e) VALUES ([1.0, 1.0]);", &mut db).expect("insert 3");
2093 db
2094 }
2095
2096 #[test]
2097 fn vec_distance_l2_in_where_filters_correctly() {
2098 let mut db = seed_vector_docs();
2104 let resp = process_command(
2105 "SELECT * FROM docs WHERE vec_distance_l2(e, [1.0, 0.0]) < 1.1;",
2106 &mut db,
2107 )
2108 .expect("select");
2109 assert!(
2110 resp.contains("2 rows returned"),
2111 "expected 2 rows, got: {resp}"
2112 );
2113 }
2114
2115 #[test]
2116 fn vec_distance_cosine_in_where() {
2117 let mut db = seed_vector_docs();
2122 let resp = process_command(
2123 "SELECT * FROM docs WHERE vec_distance_cosine(e, [1.0, 0.0]) < 0.5;",
2124 &mut db,
2125 )
2126 .expect("select");
2127 assert!(
2128 resp.contains("2 rows returned"),
2129 "expected 2 rows, got: {resp}"
2130 );
2131 }
2132
2133 #[test]
2134 fn vec_distance_dot_negated() {
2135 let mut db = seed_vector_docs();
2140 let resp = process_command(
2141 "SELECT * FROM docs WHERE vec_distance_dot(e, [1.0, 0.0]) < 0.0;",
2142 &mut db,
2143 )
2144 .expect("select");
2145 assert!(
2146 resp.contains("2 rows returned"),
2147 "expected 2 rows, got: {resp}"
2148 );
2149 }
2150
2151 #[test]
2152 fn knn_via_order_by_distance_limit() {
2153 let mut db = seed_vector_docs();
2157 let resp = process_command(
2158 "SELECT id FROM docs ORDER BY vec_distance_l2(e, [1.0, 0.0]) ASC LIMIT 2;",
2159 &mut db,
2160 )
2161 .expect("select");
2162 assert!(
2163 resp.contains("2 rows returned"),
2164 "expected 2 rows, got: {resp}"
2165 );
2166 }
2167
2168 #[test]
2169 fn distance_function_dim_mismatch_errors() {
2170 let mut db = seed_vector_docs();
2172 let err = process_command(
2173 "SELECT * FROM docs WHERE vec_distance_l2(e, [1.0, 0.0, 0.0]) < 1.0;",
2174 &mut db,
2175 )
2176 .unwrap_err();
2177 let msg = format!("{err}");
2178 assert!(
2179 msg.to_lowercase().contains("dimension")
2180 && msg.contains("lhs=2")
2181 && msg.contains("rhs=3"),
2182 "expected dim mismatch error, got: {msg}"
2183 );
2184 }
2185
2186 #[test]
2187 fn unknown_function_errors_with_name() {
2188 let mut db = seed_vector_docs();
2192 let err = process_command(
2193 "SELECT * FROM docs WHERE vec_does_not_exist(e, [1.0, 0.0]) < 1.0;",
2194 &mut db,
2195 )
2196 .unwrap_err();
2197 let msg = format!("{err}");
2198 assert!(
2199 msg.contains("vec_does_not_exist"),
2200 "expected error mentioning function name, got: {msg}"
2201 );
2202 }
2203
2204 fn seed_json_table() -> Database {
2209 let mut db = Database::new("tempdb".to_string());
2210 process_command(
2211 "CREATE TABLE docs (id INTEGER PRIMARY KEY, payload JSON);",
2212 &mut db,
2213 )
2214 .expect("create json table");
2215 db
2216 }
2217
2218 #[test]
2219 fn json_column_round_trip_primitive_values() {
2220 let mut db = seed_json_table();
2221 process_command(
2222 r#"INSERT INTO docs (payload) VALUES ('{"name": "alice", "age": 30}');"#,
2223 &mut db,
2224 )
2225 .expect("insert json");
2226 let docs = db.get_table("docs".to_string()).unwrap();
2227 let rowids = docs.rowids();
2228 assert_eq!(rowids.len(), 1);
2229 match docs.get_value("payload", rowids[0]) {
2231 Some(Value::Text(s)) => {
2232 assert!(s.contains("alice"), "expected JSON text to round-trip: {s}");
2233 }
2234 other => panic!("expected Value::Text holding JSON, got {other:?}"),
2235 }
2236 }
2237
2238 #[test]
2239 fn json_insert_rejects_invalid_json() {
2240 let mut db = seed_json_table();
2241 let err = process_command(
2242 "INSERT INTO docs (payload) VALUES ('not-valid-json{');",
2243 &mut db,
2244 )
2245 .unwrap_err();
2246 let msg = format!("{err}").to_lowercase();
2247 assert!(
2248 msg.contains("json") && msg.contains("payload"),
2249 "expected JSON validation error mentioning column, got: {msg}"
2250 );
2251 }
2252
2253 #[test]
2254 fn json_extract_object_field() {
2255 let mut db = seed_json_table();
2256 process_command(
2257 r#"INSERT INTO docs (payload) VALUES ('{"name": "alice", "age": 30}');"#,
2258 &mut db,
2259 )
2260 .unwrap();
2261 let resp = process_command(
2264 r#"SELECT id FROM docs WHERE json_extract(payload, '$.name') = 'alice';"#,
2265 &mut db,
2266 )
2267 .expect("select via json_extract");
2268 assert!(resp.contains("1 row returned"), "got: {resp}");
2269
2270 let resp = process_command(
2271 r#"SELECT id FROM docs WHERE json_extract(payload, '$.age') = 30;"#,
2272 &mut db,
2273 )
2274 .expect("select via numeric json_extract");
2275 assert!(resp.contains("1 row returned"), "got: {resp}");
2276 }
2277
2278 #[test]
2279 fn json_extract_array_index_and_nested() {
2280 let mut db = seed_json_table();
2281 process_command(
2282 r#"INSERT INTO docs (payload) VALUES ('{"tags": ["rust", "sql", "vectors"], "meta": {"author": "joao"}}');"#,
2283 &mut db,
2284 )
2285 .unwrap();
2286 let resp = process_command(
2287 r#"SELECT id FROM docs WHERE json_extract(payload, '$.tags[0]') = 'rust';"#,
2288 &mut db,
2289 )
2290 .expect("select via array index");
2291 assert!(resp.contains("1 row returned"), "got: {resp}");
2292
2293 let resp = process_command(
2294 r#"SELECT id FROM docs WHERE json_extract(payload, '$.meta.author') = 'joao';"#,
2295 &mut db,
2296 )
2297 .expect("select via nested object");
2298 assert!(resp.contains("1 row returned"), "got: {resp}");
2299 }
2300
2301 #[test]
2302 fn json_extract_missing_path_returns_null() {
2303 let mut db = seed_json_table();
2304 process_command(
2305 r#"INSERT INTO docs (payload) VALUES ('{"name": "alice"}');"#,
2306 &mut db,
2307 )
2308 .unwrap();
2309 let resp = process_command(
2312 r#"SELECT id FROM docs WHERE json_extract(payload, '$.missing') = 'something';"#,
2313 &mut db,
2314 )
2315 .expect("select with missing path");
2316 assert!(resp.contains("0 rows returned"), "got: {resp}");
2317 }
2318
2319 #[test]
2320 fn json_extract_malformed_path_errors() {
2321 let mut db = seed_json_table();
2322 process_command(
2323 r#"INSERT INTO docs (payload) VALUES ('{"a": 1}');"#,
2324 &mut db,
2325 )
2326 .unwrap();
2327 let err = process_command(
2329 r#"SELECT id FROM docs WHERE json_extract(payload, 'a.b') = 1;"#,
2330 &mut db,
2331 )
2332 .unwrap_err();
2333 assert!(format!("{err}").contains("'$'"));
2334 }
2335
2336 #[test]
2337 fn json_array_length_on_array() {
2338 let mut db = seed_json_table();
2341 process_command(
2342 r#"INSERT INTO docs (payload) VALUES ('{"tags": ["a", "b", "c"]}');"#,
2343 &mut db,
2344 )
2345 .unwrap();
2346 let resp = process_command(
2347 r#"SELECT id FROM docs WHERE json_array_length(payload, '$.tags') = 3;"#,
2348 &mut db,
2349 )
2350 .expect("select via array_length");
2351 assert!(resp.contains("1 row returned"), "got: {resp}");
2352 }
2353
2354 #[test]
2355 fn json_array_length_on_non_array_errors() {
2356 let mut db = seed_json_table();
2357 process_command(
2358 r#"INSERT INTO docs (payload) VALUES ('{"tags": "not-an-array"}');"#,
2359 &mut db,
2360 )
2361 .unwrap();
2362 let err = process_command(
2363 r#"SELECT id FROM docs WHERE json_array_length(payload, '$.tags') = 1;"#,
2364 &mut db,
2365 )
2366 .unwrap_err();
2367 let msg = format!("{err}").to_lowercase();
2368 assert!(
2369 msg.contains("non-array"),
2370 "expected non-array error, got: {msg}"
2371 );
2372 }
2373
2374 #[test]
2375 fn json_type_recognizes_each_kind() {
2376 let mut db = seed_json_table();
2377 process_command(
2378 r#"INSERT INTO docs (payload) VALUES ('{"o": {}, "a": [], "s": "x", "i": 1, "f": 1.5, "t": true, "n": null}');"#,
2379 &mut db,
2380 )
2381 .unwrap();
2382 let cases = &[
2383 ("$.o", "object"),
2384 ("$.a", "array"),
2385 ("$.s", "text"),
2386 ("$.i", "integer"),
2387 ("$.f", "real"),
2388 ("$.t", "true"),
2389 ("$.n", "null"),
2390 ];
2391 for (path, expected_type) in cases {
2392 let sql = format!(
2393 "SELECT id FROM docs WHERE json_type(payload, '{path}') = '{expected_type}';"
2394 );
2395 let resp =
2396 process_command(&sql, &mut db).unwrap_or_else(|e| panic!("path {path}: {e}"));
2397 assert!(
2398 resp.contains("1 row returned"),
2399 "path {path} expected type {expected_type}; got response: {resp}"
2400 );
2401 }
2402 }
2403
2404 #[test]
2405 fn update_on_json_column_revalidates() {
2406 let mut db = seed_json_table();
2407 process_command(
2408 r#"INSERT INTO docs (payload) VALUES ('{"a": 1}');"#,
2409 &mut db,
2410 )
2411 .unwrap();
2412 process_command(
2414 r#"UPDATE docs SET payload = '{"a": 2, "b": 3}' WHERE id = 1;"#,
2415 &mut db,
2416 )
2417 .expect("valid JSON UPDATE");
2418 let err = process_command(
2421 r#"UPDATE docs SET payload = 'not-json{' WHERE id = 1;"#,
2422 &mut db,
2423 )
2424 .unwrap_err();
2425 let msg = format!("{err}").to_lowercase();
2426 assert!(
2427 msg.contains("json") && msg.contains("payload"),
2428 "got: {msg}"
2429 );
2430 }
2431
2432 #[test]
2437 fn default_literal_int_applies_when_column_omitted() {
2438 let mut db = Database::new("t".to_string());
2439 process_command(
2440 "CREATE TABLE t (id INTEGER PRIMARY KEY, n INTEGER DEFAULT 42);",
2441 &mut db,
2442 )
2443 .unwrap();
2444 process_command("INSERT INTO t (id) VALUES (1);", &mut db).unwrap();
2445
2446 let table = db.get_table("t".to_string()).unwrap();
2447 assert_eq!(table.get_value("n", 1), Some(Value::Integer(42)));
2448 }
2449
2450 #[test]
2451 fn default_literal_text_applies_when_column_omitted() {
2452 let mut db = Database::new("t".to_string());
2453 process_command(
2454 "CREATE TABLE users (id INTEGER PRIMARY KEY, status TEXT DEFAULT 'active');",
2455 &mut db,
2456 )
2457 .unwrap();
2458 process_command("INSERT INTO users (id) VALUES (1);", &mut db).unwrap();
2459
2460 let table = db.get_table("users".to_string()).unwrap();
2461 assert_eq!(
2462 table.get_value("status", 1),
2463 Some(Value::Text("active".to_string()))
2464 );
2465 }
2466
2467 #[test]
2468 fn default_literal_real_negative_applies_when_column_omitted() {
2469 let mut db = Database::new("t".to_string());
2471 process_command(
2472 "CREATE TABLE t (id INTEGER PRIMARY KEY, score REAL DEFAULT -1.5);",
2473 &mut db,
2474 )
2475 .unwrap();
2476 process_command("INSERT INTO t (id) VALUES (1);", &mut db).unwrap();
2477
2478 let table = db.get_table("t".to_string()).unwrap();
2479 assert_eq!(table.get_value("score", 1), Some(Value::Real(-1.5)));
2480 }
2481
2482 #[test]
2483 fn default_with_type_mismatch_errors_at_create_time() {
2484 let mut db = Database::new("t".to_string());
2485 let result = process_command(
2486 "CREATE TABLE t (id INTEGER PRIMARY KEY, n INTEGER DEFAULT 'oops');",
2487 &mut db,
2488 );
2489 let err = result.expect_err("text default on INTEGER column should be rejected");
2490 let msg = format!("{err}").to_lowercase();
2491 assert!(msg.contains("default"), "got: {msg}");
2492 }
2493
2494 #[test]
2495 fn default_for_json_column_must_be_valid_json() {
2496 let mut db = Database::new("t".to_string());
2500 let err = process_command(
2501 "CREATE TABLE t (id INTEGER PRIMARY KEY, doc JSON DEFAULT 'not-json{');",
2502 &mut db,
2503 )
2504 .unwrap_err();
2505 assert!(
2506 format!("{err}").to_lowercase().contains("json"),
2507 "got: {err}"
2508 );
2509
2510 process_command(
2512 "CREATE TABLE t2 (id INTEGER PRIMARY KEY, doc JSON DEFAULT '{\"k\":1}');",
2513 &mut db,
2514 )
2515 .expect("valid JSON DEFAULT should be accepted");
2516 }
2517
2518 #[test]
2519 fn default_with_non_literal_expression_errors_at_create_time() {
2520 let mut db = Database::new("t".to_string());
2521 let result = process_command(
2524 "CREATE TABLE t (id INTEGER PRIMARY KEY, ts TEXT DEFAULT CURRENT_TIMESTAMP);",
2525 &mut db,
2526 );
2527 let err = result.expect_err("non-literal DEFAULT should be rejected");
2528 let msg = format!("{err}").to_lowercase();
2529 assert!(
2530 msg.contains("default") && msg.contains("literal"),
2531 "got: {msg}"
2532 );
2533 }
2534
2535 #[test]
2536 fn default_null_is_accepted_at_create_time() {
2537 let mut db = Database::new("t".to_string());
2541 process_command(
2542 "CREATE TABLE t (id INTEGER PRIMARY KEY, note TEXT DEFAULT NULL);",
2543 &mut db,
2544 )
2545 .expect("CREATE TABLE with DEFAULT NULL should be accepted");
2546 let table = db.get_table("t".to_string()).unwrap();
2547 let note = table
2548 .columns
2549 .iter()
2550 .find(|c| c.column_name == "note")
2551 .unwrap();
2552 assert_eq!(note.default, Some(Value::Null));
2553 }
2554
2555 #[test]
2560 fn drop_table_basic() {
2561 let mut db = seed_users_table();
2562 let response = process_command("DROP TABLE users;", &mut db).expect("drop table");
2563 assert!(response.contains("1 table dropped"));
2564 assert!(!db.contains_table("users".to_string()));
2565 }
2566
2567 #[test]
2568 fn drop_table_if_exists_noop_on_missing() {
2569 let mut db = Database::new("t".to_string());
2570 let response =
2571 process_command("DROP TABLE IF EXISTS missing;", &mut db).expect("drop if exists");
2572 assert!(response.contains("0 tables dropped"));
2573 }
2574
2575 #[test]
2576 fn drop_table_missing_errors_without_if_exists() {
2577 let mut db = Database::new("t".to_string());
2578 let err = process_command("DROP TABLE missing;", &mut db).unwrap_err();
2579 assert!(format!("{err}").contains("does not exist"), "got: {err}");
2580 }
2581
2582 #[test]
2583 fn drop_table_reserved_name_errors() {
2584 let mut db = Database::new("t".to_string());
2585 let err = process_command("DROP TABLE sqlrite_master;", &mut db).unwrap_err();
2586 assert!(format!("{err}").contains("reserved"), "got: {err}");
2587 }
2588
2589 #[test]
2590 fn drop_table_multi_target_rejected() {
2591 let mut db = seed_users_table();
2592 process_command("CREATE TABLE other (id INTEGER PRIMARY KEY);", &mut db).unwrap();
2593 let err = process_command("DROP TABLE users, other;", &mut db).unwrap_err();
2596 assert!(format!("{err}").contains("single table"), "got: {err}");
2597 }
2598
2599 #[test]
2600 fn drop_table_cascades_indexes_in_memory() {
2601 let mut db = seed_users_table();
2602 process_command("CREATE INDEX users_age_idx ON users (age);", &mut db).unwrap();
2603 let users = db.get_table("users".to_string()).unwrap();
2605 assert!(
2606 users
2607 .secondary_indexes
2608 .iter()
2609 .any(|i| i.name == "users_age_idx")
2610 );
2611
2612 process_command("DROP TABLE users;", &mut db).unwrap();
2613
2614 for table in db.tables.values() {
2616 assert!(
2617 !table
2618 .secondary_indexes
2619 .iter()
2620 .any(|i| i.name.contains("users")),
2621 "dropped table's indexes should not survive on any other table"
2622 );
2623 }
2624 }
2625
2626 #[test]
2627 fn drop_index_explicit_basic() {
2628 let mut db = seed_users_table();
2629 process_command("CREATE INDEX users_age_idx ON users (age);", &mut db).unwrap();
2630 let response = process_command("DROP INDEX users_age_idx;", &mut db).expect("drop index");
2631 assert!(response.contains("1 index dropped"));
2632
2633 let users = db.get_table("users".to_string()).unwrap();
2634 assert!(users.index_by_name("users_age_idx").is_none());
2635 }
2636
2637 #[test]
2638 fn drop_index_refuses_auto_index() {
2639 let mut db = seed_users_table();
2640 let err = process_command("DROP INDEX sqlrite_autoindex_users_id;", &mut db).unwrap_err();
2643 assert!(format!("{err}").contains("auto-created"), "got: {err}");
2644 }
2645
2646 #[test]
2647 fn drop_index_if_exists_noop_on_missing() {
2648 let mut db = Database::new("t".to_string());
2649 let response =
2650 process_command("DROP INDEX IF EXISTS nope;", &mut db).expect("drop index if exists");
2651 assert!(response.contains("0 indexes dropped"));
2652 }
2653
2654 #[test]
2655 fn drop_index_missing_errors_without_if_exists() {
2656 let mut db = Database::new("t".to_string());
2657 let err = process_command("DROP INDEX nope;", &mut db).unwrap_err();
2658 assert!(format!("{err}").contains("does not exist"), "got: {err}");
2659 }
2660
2661 #[test]
2662 fn drop_statements_rejected_on_readonly_db() {
2663 use crate::sql::pager::{open_database_read_only, save_database};
2664
2665 let mut seed = Database::new("t".to_string());
2666 process_command(
2667 "CREATE TABLE notes (id INTEGER PRIMARY KEY, body TEXT);",
2668 &mut seed,
2669 )
2670 .unwrap();
2671 process_command("CREATE INDEX notes_body ON notes (body);", &mut seed).unwrap();
2672 let path = {
2673 let mut p = std::env::temp_dir();
2674 let pid = std::process::id();
2675 let nanos = std::time::SystemTime::now()
2676 .duration_since(std::time::UNIX_EPOCH)
2677 .map(|d| d.as_nanos())
2678 .unwrap_or(0);
2679 p.push(format!("sqlrite-drop-ro-{pid}-{nanos}.sqlrite"));
2680 p
2681 };
2682 save_database(&mut seed, &path).unwrap();
2683 drop(seed);
2684
2685 let mut ro = open_database_read_only(&path, "t".to_string()).unwrap();
2686 for stmt in ["DROP TABLE notes;", "DROP INDEX notes_body;"] {
2687 let err = process_command(stmt, &mut ro).unwrap_err();
2688 assert!(
2689 format!("{err}").contains("read-only"),
2690 "{stmt:?} should surface read-only error, got: {err}"
2691 );
2692 }
2693
2694 let _ = std::fs::remove_file(&path);
2695 let mut wal = path.as_os_str().to_owned();
2696 wal.push("-wal");
2697 let _ = std::fs::remove_file(std::path::PathBuf::from(wal));
2698 }
2699
2700 #[test]
2705 fn alter_rename_table_basic() {
2706 let mut db = seed_users_table();
2707 process_command("ALTER TABLE users RENAME TO members;", &mut db).expect("rename table");
2708 assert!(!db.contains_table("users".to_string()));
2709 assert!(db.contains_table("members".to_string()));
2710 let response = process_command("SELECT * FROM members;", &mut db).expect("select");
2712 assert!(response.contains("3 rows returned"));
2713 }
2714
2715 #[test]
2716 fn alter_rename_table_renames_auto_indexes() {
2717 let mut db = Database::new("t".to_string());
2720 process_command(
2721 "CREATE TABLE accounts (id INTEGER PRIMARY KEY, email TEXT UNIQUE);",
2722 &mut db,
2723 )
2724 .unwrap();
2725 {
2726 let accounts = db.get_table("accounts".to_string()).unwrap();
2727 assert!(
2728 accounts
2729 .index_by_name("sqlrite_autoindex_accounts_id")
2730 .is_some()
2731 );
2732 assert!(
2733 accounts
2734 .index_by_name("sqlrite_autoindex_accounts_email")
2735 .is_some()
2736 );
2737 }
2738 process_command("ALTER TABLE accounts RENAME TO members;", &mut db).expect("rename");
2739 let members = db.get_table("members".to_string()).unwrap();
2740 assert!(
2741 members
2742 .index_by_name("sqlrite_autoindex_members_id")
2743 .is_some(),
2744 "PK auto-index should be renamed to match new table"
2745 );
2746 assert!(
2747 members
2748 .index_by_name("sqlrite_autoindex_members_email")
2749 .is_some()
2750 );
2751 assert!(
2753 members
2754 .index_by_name("sqlrite_autoindex_accounts_id")
2755 .is_none()
2756 );
2757 for idx in &members.secondary_indexes {
2759 assert_eq!(idx.table_name, "members");
2760 }
2761 }
2762
2763 #[test]
2764 fn alter_rename_table_to_existing_errors() {
2765 let mut db = seed_users_table();
2766 process_command("CREATE TABLE other (id INTEGER PRIMARY KEY);", &mut db).unwrap();
2767 let err = process_command("ALTER TABLE users RENAME TO other;", &mut db).unwrap_err();
2768 assert!(format!("{err}").contains("already exists"), "got: {err}");
2769 assert!(db.contains_table("users".to_string()));
2771 assert!(db.contains_table("other".to_string()));
2772 }
2773
2774 #[test]
2775 fn alter_rename_table_to_reserved_name_errors() {
2776 let mut db = seed_users_table();
2777 let err =
2778 process_command("ALTER TABLE users RENAME TO sqlrite_master;", &mut db).unwrap_err();
2779 assert!(format!("{err}").contains("reserved"), "got: {err}");
2780 }
2781
2782 #[test]
2783 fn alter_rename_column_basic() {
2784 let mut db = seed_users_table();
2785 process_command(
2786 "ALTER TABLE users RENAME COLUMN name TO full_name;",
2787 &mut db,
2788 )
2789 .expect("rename column");
2790
2791 let users = db.get_table("users".to_string()).unwrap();
2792 assert!(users.contains_column("full_name".to_string()));
2793 assert!(!users.contains_column("name".to_string()));
2794
2795 let bob_rowid = users
2798 .rowids()
2799 .into_iter()
2800 .find(|r| users.get_value("full_name", *r) == Some(Value::Text("bob".to_string())))
2801 .expect("bob row should be findable under the new column name");
2802 assert_eq!(
2803 users.get_value("full_name", bob_rowid),
2804 Some(Value::Text("bob".to_string()))
2805 );
2806 }
2807
2808 #[test]
2809 fn alter_rename_column_collision_errors() {
2810 let mut db = seed_users_table();
2811 let err =
2812 process_command("ALTER TABLE users RENAME COLUMN name TO age;", &mut db).unwrap_err();
2813 assert!(format!("{err}").contains("already exists"), "got: {err}");
2814 }
2815
2816 #[test]
2817 fn alter_rename_column_updates_indexes() {
2818 let mut db = Database::new("t".to_string());
2820 process_command(
2821 "CREATE TABLE accounts (id INTEGER PRIMARY KEY, email TEXT UNIQUE);",
2822 &mut db,
2823 )
2824 .unwrap();
2825 process_command(
2826 "ALTER TABLE accounts RENAME COLUMN email TO contact;",
2827 &mut db,
2828 )
2829 .unwrap();
2830 let accounts = db.get_table("accounts".to_string()).unwrap();
2831 assert!(
2832 accounts
2833 .index_by_name("sqlrite_autoindex_accounts_contact")
2834 .is_some()
2835 );
2836 assert!(
2837 accounts
2838 .index_by_name("sqlrite_autoindex_accounts_email")
2839 .is_none()
2840 );
2841 }
2842
2843 #[test]
2844 fn alter_add_column_basic() {
2845 let mut db = seed_users_table();
2846 process_command("ALTER TABLE users ADD COLUMN nickname TEXT;", &mut db)
2847 .expect("add column");
2848 let users = db.get_table("users".to_string()).unwrap();
2849 assert!(users.contains_column("nickname".to_string()));
2850 let any_rowid = *users.rowids().first().expect("seed has rows");
2852 assert_eq!(users.get_value("nickname", any_rowid), None);
2853
2854 process_command(
2856 "INSERT INTO users (name, age, nickname) VALUES ('dan', 22, 'd');",
2857 &mut db,
2858 )
2859 .expect("insert with new col");
2860 let users = db.get_table("users".to_string()).unwrap();
2861 let dan_rowid = users
2862 .rowids()
2863 .into_iter()
2864 .find(|r| users.get_value("name", *r) == Some(Value::Text("dan".to_string())))
2865 .unwrap();
2866 assert_eq!(
2867 users.get_value("nickname", dan_rowid),
2868 Some(Value::Text("d".to_string()))
2869 );
2870 }
2871
2872 #[test]
2873 fn alter_add_column_with_default_backfills_existing_rows() {
2874 let mut db = seed_users_table();
2875 process_command(
2876 "ALTER TABLE users ADD COLUMN status TEXT DEFAULT 'active';",
2877 &mut db,
2878 )
2879 .expect("add column with default");
2880 let users = db.get_table("users".to_string()).unwrap();
2881 for rowid in users.rowids() {
2882 assert_eq!(
2883 users.get_value("status", rowid),
2884 Some(Value::Text("active".to_string())),
2885 "rowid {rowid} should have been backfilled with the default"
2886 );
2887 }
2888 }
2889
2890 #[test]
2891 fn alter_add_column_not_null_with_default_works_on_nonempty_table() {
2892 let mut db = seed_users_table();
2893 process_command(
2894 "ALTER TABLE users ADD COLUMN score INTEGER NOT NULL DEFAULT 0;",
2895 &mut db,
2896 )
2897 .expect("NOT NULL ADD with DEFAULT should succeed even with existing rows");
2898 let users = db.get_table("users".to_string()).unwrap();
2899 for rowid in users.rowids() {
2900 assert_eq!(users.get_value("score", rowid), Some(Value::Integer(0)));
2901 }
2902 }
2903
2904 #[test]
2905 fn alter_add_column_not_null_without_default_errors_on_nonempty_table() {
2906 let mut db = seed_users_table();
2907 let err = process_command(
2908 "ALTER TABLE users ADD COLUMN score INTEGER NOT NULL;",
2909 &mut db,
2910 )
2911 .unwrap_err();
2912 let msg = format!("{err}").to_lowercase();
2913 assert!(
2914 msg.contains("not null") && msg.contains("default"),
2915 "got: {msg}"
2916 );
2917 }
2918
2919 #[test]
2920 fn alter_add_column_pk_rejected() {
2921 let mut db = seed_users_table();
2922 let err = process_command(
2923 "ALTER TABLE users ADD COLUMN extra INTEGER PRIMARY KEY;",
2924 &mut db,
2925 )
2926 .unwrap_err();
2927 assert!(
2928 format!("{err}").to_lowercase().contains("primary key"),
2929 "got: {err}"
2930 );
2931 }
2932
2933 #[test]
2934 fn alter_add_column_unique_rejected() {
2935 let mut db = seed_users_table();
2936 let err = process_command(
2937 "ALTER TABLE users ADD COLUMN extra INTEGER UNIQUE;",
2938 &mut db,
2939 )
2940 .unwrap_err();
2941 assert!(
2942 format!("{err}").to_lowercase().contains("unique"),
2943 "got: {err}"
2944 );
2945 }
2946
2947 #[test]
2948 fn alter_add_column_existing_name_errors() {
2949 let mut db = seed_users_table();
2950 let err =
2951 process_command("ALTER TABLE users ADD COLUMN age INTEGER;", &mut db).unwrap_err();
2952 assert!(format!("{err}").contains("already exists"), "got: {err}");
2953 }
2954
2955 #[test]
2961 fn alter_drop_column_basic() {
2962 let mut db = seed_users_table();
2963 process_command("ALTER TABLE users DROP COLUMN age;", &mut db).expect("drop column");
2964 let users = db.get_table("users".to_string()).unwrap();
2965 assert!(!users.contains_column("age".to_string()));
2966 assert!(users.contains_column("name".to_string()));
2968 assert_eq!(users.rowids().len(), 3);
2969 }
2970
2971 #[test]
2972 fn alter_drop_column_drops_dependent_indexes() {
2973 let mut db = seed_users_table();
2974 process_command("CREATE INDEX users_age_idx ON users (age);", &mut db).unwrap();
2975 process_command("ALTER TABLE users DROP COLUMN age;", &mut db).unwrap();
2976 let users = db.get_table("users".to_string()).unwrap();
2977 assert!(users.index_by_name("users_age_idx").is_none());
2978 }
2979
2980 #[test]
2981 fn alter_drop_column_pk_errors() {
2982 let mut db = seed_users_table();
2983 let err = process_command("ALTER TABLE users DROP COLUMN id;", &mut db).unwrap_err();
2984 assert!(
2985 format!("{err}").to_lowercase().contains("primary key"),
2986 "got: {err}"
2987 );
2988 }
2989
2990 #[test]
2991 fn alter_drop_column_only_column_errors() {
2992 let mut db = Database::new("t".to_string());
2993 process_command("CREATE TABLE solo (only_col TEXT);", &mut db).unwrap();
2994 let err = process_command("ALTER TABLE solo DROP COLUMN only_col;", &mut db).unwrap_err();
2995 assert!(
2996 format!("{err}").to_lowercase().contains("only column"),
2997 "got: {err}"
2998 );
2999 }
3000
3001 #[test]
3002 fn alter_unknown_table_errors_without_if_exists() {
3003 let mut db = Database::new("t".to_string());
3004 let err = process_command("ALTER TABLE missing RENAME TO other;", &mut db).unwrap_err();
3005 assert!(format!("{err}").contains("does not exist"), "got: {err}");
3006 }
3007
3008 #[test]
3009 fn alter_unknown_table_if_exists_noop() {
3010 let mut db = Database::new("t".to_string());
3011 let response = process_command("ALTER TABLE IF EXISTS missing RENAME TO other;", &mut db)
3012 .expect("IF EXISTS makes missing-table ALTER a no-op");
3013 assert!(response.contains("no-op"));
3014 }
3015
3016 #[test]
3017 fn drop_table_inside_transaction_rolls_back() {
3018 let mut db = seed_users_table();
3022 process_command("CREATE INDEX users_age_idx ON users (age);", &mut db).unwrap();
3023 process_command("BEGIN;", &mut db).unwrap();
3024 process_command("DROP TABLE users;", &mut db).unwrap();
3025 assert!(!db.contains_table("users".to_string()));
3026 process_command("ROLLBACK;", &mut db).unwrap();
3027 assert!(db.contains_table("users".to_string()));
3028 let users = db.get_table("users".to_string()).unwrap();
3029 assert_eq!(users.rowids().len(), 3);
3030 assert!(users.index_by_name("users_age_idx").is_some());
3031 }
3032
3033 #[test]
3034 fn alter_inside_transaction_rolls_back() {
3035 let mut db = seed_users_table();
3036 process_command("BEGIN;", &mut db).unwrap();
3037 process_command(
3038 "ALTER TABLE users ADD COLUMN status TEXT DEFAULT 'active';",
3039 &mut db,
3040 )
3041 .unwrap();
3042 assert!(
3044 db.get_table("users".to_string())
3045 .unwrap()
3046 .contains_column("status".to_string())
3047 );
3048 process_command("ROLLBACK;", &mut db).unwrap();
3049 assert!(
3051 !db.get_table("users".to_string())
3052 .unwrap()
3053 .contains_column("status".to_string())
3054 );
3055 }
3056
3057 #[test]
3058 fn alter_rejected_on_readonly_db() {
3059 use crate::sql::pager::{open_database_read_only, save_database};
3060
3061 let mut seed = Database::new("t".to_string());
3062 process_command(
3063 "CREATE TABLE notes (id INTEGER PRIMARY KEY, body TEXT);",
3064 &mut seed,
3065 )
3066 .unwrap();
3067 let path = {
3068 let mut p = std::env::temp_dir();
3069 let pid = std::process::id();
3070 let nanos = std::time::SystemTime::now()
3071 .duration_since(std::time::UNIX_EPOCH)
3072 .map(|d| d.as_nanos())
3073 .unwrap_or(0);
3074 p.push(format!("sqlrite-alter-ro-{pid}-{nanos}.sqlrite"));
3075 p
3076 };
3077 save_database(&mut seed, &path).unwrap();
3078 drop(seed);
3079
3080 let mut ro = open_database_read_only(&path, "t".to_string()).unwrap();
3081 for stmt in [
3082 "ALTER TABLE notes RENAME TO n2;",
3083 "ALTER TABLE notes RENAME COLUMN body TO b;",
3084 "ALTER TABLE notes ADD COLUMN extra TEXT;",
3085 "ALTER TABLE notes DROP COLUMN body;",
3086 ] {
3087 let err = process_command(stmt, &mut ro).unwrap_err();
3088 assert!(
3089 format!("{err}").contains("read-only"),
3090 "{stmt:?} should surface read-only error, got: {err}"
3091 );
3092 }
3093
3094 let _ = std::fs::remove_file(&path);
3095 let mut wal = path.as_os_str().to_owned();
3096 wal.push("-wal");
3097 let _ = std::fs::remove_file(std::path::PathBuf::from(wal));
3098 }
3099}