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 insert_omitted_integer_column_is_stored_as_null() {
649 use crate::sql::db::table::Value;
655
656 let mut db = Database::new("tempdb".to_string());
657 process_command(
658 "CREATE TABLE items (id INTEGER PRIMARY KEY, qty INTEGER);",
659 &mut db,
660 )
661 .unwrap();
662 process_command("INSERT INTO items (id) VALUES (1);", &mut db)
663 .expect("INSERT with omitted INTEGER column should succeed and store NULL");
664
665 let table = db.get_table("items".to_string()).unwrap();
666 let rowid = table.rowids().pop().expect("one row");
667 assert_eq!(table.get_value("qty", rowid), None);
670 let response = process_command("SELECT id FROM items WHERE qty IS NULL;", &mut db)
672 .expect("select IS NULL");
673 assert!(
674 response.contains("1 row returned"),
675 "qty IS NULL should match the omitted-column row, got: {response}"
676 );
677 process_command("INSERT INTO items (id, qty) VALUES (2, 7);", &mut db).unwrap();
679 let table = db.get_table("items".to_string()).unwrap();
680 let row_two = table
681 .rowids()
682 .into_iter()
683 .find(|r| table.get_value("id", *r) == Some(Value::Integer(2)))
684 .unwrap();
685 assert_eq!(table.get_value("qty", row_two), Some(Value::Integer(7)));
686 }
687
688 #[test]
689 fn insert_explicit_null_into_integer_column() {
690 let mut db = Database::new("tempdb".to_string());
691 process_command(
692 "CREATE TABLE t (id INTEGER PRIMARY KEY, n INTEGER);",
693 &mut db,
694 )
695 .unwrap();
696 process_command("INSERT INTO t (id, n) VALUES (1, NULL);", &mut db)
697 .expect("INSERT explicit NULL into INTEGER must not panic on parse::<i32>()");
698 let table = db.get_table("t".to_string()).unwrap();
699 let rowid = table.rowids().pop().unwrap();
700 assert_eq!(table.get_value("n", rowid), None);
701 }
702
703 #[test]
704 fn insert_explicit_null_into_text_column() {
705 use crate::sql::db::table::Value;
710
711 let mut db = Database::new("tempdb".to_string());
712 process_command("CREATE TABLE t (id INTEGER PRIMARY KEY, s TEXT);", &mut db).unwrap();
713 process_command("INSERT INTO t (id, s) VALUES (1, NULL);", &mut db).unwrap();
714 process_command("INSERT INTO t (id, s) VALUES (2, 'hi');", &mut db).unwrap();
715
716 let table = db.get_table("t".to_string()).unwrap();
717 let row_one = table
718 .rowids()
719 .into_iter()
720 .find(|r| table.get_value("id", *r) == Some(Value::Integer(1)))
721 .unwrap();
722 let row_two = table
723 .rowids()
724 .into_iter()
725 .find(|r| table.get_value("id", *r) == Some(Value::Integer(2)))
726 .unwrap();
727 assert_eq!(table.get_value("s", row_one), None);
728 assert_eq!(
729 table.get_value("s", row_two),
730 Some(Value::Text("hi".to_string()))
731 );
732 }
733
734 #[test]
735 fn insert_explicit_null_into_real_column() {
736 let mut db = Database::new("tempdb".to_string());
737 process_command(
738 "CREATE TABLE t (id INTEGER PRIMARY KEY, score REAL);",
739 &mut db,
740 )
741 .unwrap();
742 process_command("INSERT INTO t (id, score) VALUES (1, NULL);", &mut db)
743 .expect("INSERT explicit NULL into REAL must not panic on parse::<f32>()");
744 let table = db.get_table("t".to_string()).unwrap();
745 let rowid = table.rowids().pop().unwrap();
746 assert_eq!(table.get_value("score", rowid), None);
747 }
748
749 #[test]
750 fn insert_explicit_null_into_bool_column() {
751 let mut db = Database::new("tempdb".to_string());
752 process_command(
753 "CREATE TABLE t (id INTEGER PRIMARY KEY, flag BOOLEAN);",
754 &mut db,
755 )
756 .unwrap();
757 process_command("INSERT INTO t (id, flag) VALUES (1, NULL);", &mut db)
758 .expect("INSERT explicit NULL into BOOL must not panic on parse::<bool>()");
759 let table = db.get_table("t".to_string()).unwrap();
760 let rowid = table.rowids().pop().unwrap();
761 assert_eq!(table.get_value("flag", rowid), None);
762 }
763
764 #[test]
765 fn insert_explicit_null_into_vector_column() {
766 let mut db = Database::new("tempdb".to_string());
767 process_command(
768 "CREATE TABLE t (id INTEGER PRIMARY KEY, v VECTOR(3));",
769 &mut db,
770 )
771 .unwrap();
772 process_command("INSERT INTO t (id, v) VALUES (1, NULL);", &mut db)
773 .expect("INSERT explicit NULL into VECTOR must not panic on parse_vector_literal");
774 let table = db.get_table("t".to_string()).unwrap();
775 let rowid = table.rowids().pop().unwrap();
776 assert_eq!(table.get_value("v", rowid), None);
777 }
778
779 #[test]
780 fn insert_explicit_null_into_json_column() {
781 let mut db = Database::new("tempdb".to_string());
782 process_command(
783 "CREATE TABLE t (id INTEGER PRIMARY KEY, doc JSON);",
784 &mut db,
785 )
786 .unwrap();
787 process_command("INSERT INTO t (id, doc) VALUES (1, NULL);", &mut db)
788 .expect("INSERT explicit NULL into JSON must skip serde_json validation");
789 let table = db.get_table("t".to_string()).unwrap();
790 let rowid = table.rowids().pop().unwrap();
791 assert_eq!(table.get_value("doc", rowid), None);
792 }
793
794 #[test]
795 fn default_does_not_override_explicit_null() {
796 use crate::sql::db::table::Value;
801
802 let mut db = Database::new("tempdb".to_string());
803 process_command(
804 "CREATE TABLE t (id INTEGER PRIMARY KEY, n INTEGER DEFAULT 0);",
805 &mut db,
806 )
807 .unwrap();
808 process_command("INSERT INTO t (id, n) VALUES (1, NULL);", &mut db).unwrap();
809 process_command("INSERT INTO t (id) VALUES (2);", &mut db).unwrap();
810
811 let table = db.get_table("t".to_string()).unwrap();
812 let row_one = table
813 .rowids()
814 .into_iter()
815 .find(|r| table.get_value("id", *r) == Some(Value::Integer(1)))
816 .unwrap();
817 let row_two = table
818 .rowids()
819 .into_iter()
820 .find(|r| table.get_value("id", *r) == Some(Value::Integer(2)))
821 .unwrap();
822 assert_eq!(table.get_value("n", row_one), None);
824 assert_eq!(table.get_value("n", row_two), Some(Value::Integer(0)));
826 }
827
828 #[test]
829 fn process_command_update_arith_test() {
830 use crate::sql::db::table::Value;
831
832 let mut db = seed_users_table();
833 process_command("UPDATE users SET age = age + 1;", &mut db).expect("update +1");
834
835 let users = db.get_table("users".to_string()).unwrap();
836 let mut ages: Vec<i64> = users
837 .rowids()
838 .into_iter()
839 .filter_map(|r| match users.get_value("age", r) {
840 Some(Value::Integer(n)) => Some(n),
841 _ => None,
842 })
843 .collect();
844 ages.sort();
845 assert_eq!(ages, vec![26, 31, 41]); }
847
848 #[test]
849 fn process_command_select_arithmetic_where_test() {
850 let mut db = seed_users_table();
851 let response =
853 process_command("SELECT name FROM users WHERE age * 2 > 55;", &mut db).expect("select");
854 assert!(response.contains("2 rows returned"));
855 }
856
857 #[test]
858 fn process_command_divide_by_zero_test() {
859 let mut db = seed_users_table();
860 let result = process_command("SELECT age / 0 FROM users;", &mut db);
861 assert!(result.is_err());
863 }
864
865 #[test]
866 fn process_command_unsupported_statement_test() {
867 let mut db = Database::new("tempdb".to_string());
868 let result = process_command("CREATE VIEW v AS SELECT * FROM users;", &mut db);
872 assert!(result.is_err());
873 }
874
875 #[test]
876 fn empty_input_is_a_noop_not_a_panic() {
877 let mut db = Database::new("t".to_string());
881 for input in ["", " ", "-- just a comment", "-- comment\n-- another"] {
882 let result = process_command(input, &mut db);
883 assert!(result.is_ok(), "input {input:?} should not error");
884 let msg = result.unwrap();
885 assert!(msg.contains("No statement"), "got: {msg:?}");
886 }
887 }
888
889 #[test]
890 fn create_index_adds_explicit_index() {
891 let mut db = seed_users_table();
892 let response = process_command("CREATE INDEX users_age_idx ON users (age);", &mut db)
893 .expect("create index");
894 assert!(response.contains("users_age_idx"));
895
896 let users = db.get_table("users".to_string()).unwrap();
898 let idx = users
899 .index_by_name("users_age_idx")
900 .expect("index should exist after CREATE INDEX");
901 assert_eq!(idx.column_name, "age");
902 assert!(!idx.is_unique);
903 }
904
905 #[test]
906 fn create_unique_index_rejects_duplicate_existing_values() {
907 let mut db = seed_users_table();
908 process_command("INSERT INTO users (name, age) VALUES ('dan', 30);", &mut db).unwrap();
911 let result = process_command(
912 "CREATE UNIQUE INDEX users_age_unique ON users (age);",
913 &mut db,
914 );
915 assert!(
916 result.is_err(),
917 "expected unique-index failure, got {result:?}"
918 );
919 }
920
921 #[test]
922 fn where_eq_on_indexed_column_uses_index_probe() {
923 let mut db = Database::new("t".to_string());
927 process_command(
928 "CREATE TABLE big (id INTEGER PRIMARY KEY, tag TEXT);",
929 &mut db,
930 )
931 .unwrap();
932 process_command("CREATE INDEX big_tag_idx ON big (tag);", &mut db).unwrap();
933 for i in 1..=100 {
934 let tag = if i % 3 == 0 { "hot" } else { "cold" };
935 process_command(&format!("INSERT INTO big (tag) VALUES ('{tag}');"), &mut db).unwrap();
936 }
937 let response =
938 process_command("SELECT id FROM big WHERE tag = 'hot';", &mut db).expect("select");
939 assert!(
941 response.contains("33 rows returned"),
942 "response was {response:?}"
943 );
944 }
945
946 #[test]
947 fn where_eq_on_indexed_column_inside_parens_uses_index_probe() {
948 let mut db = seed_users_table();
949 let response = process_command("SELECT name FROM users WHERE (name = 'bob');", &mut db)
950 .expect("select");
951 assert!(response.contains("1 row returned"));
952 }
953
954 #[test]
955 fn where_eq_literal_first_side_uses_index_probe() {
956 let mut db = seed_users_table();
957 let response =
959 process_command("SELECT name FROM users WHERE 'bob' = name;", &mut db).expect("select");
960 assert!(response.contains("1 row returned"));
961 }
962
963 #[test]
964 fn non_equality_where_still_falls_back_to_full_scan() {
965 let mut db = seed_users_table();
968 let response =
969 process_command("SELECT name FROM users WHERE age > 28;", &mut db).expect("select");
970 assert!(response.contains("2 rows returned"));
971 }
972
973 #[test]
978 fn rollback_restores_pre_begin_in_memory_state() {
979 let mut db = seed_users_table();
982 let before = db.get_table("users".to_string()).unwrap().rowids().len();
983 assert_eq!(before, 3);
984
985 process_command("BEGIN;", &mut db).expect("BEGIN");
986 assert!(db.in_transaction());
987 process_command("INSERT INTO users (name, age) VALUES ('dan', 50);", &mut db)
988 .expect("INSERT inside txn");
989 let mid = db.get_table("users".to_string()).unwrap().rowids().len();
991 assert_eq!(mid, 4);
992
993 process_command("ROLLBACK;", &mut db).expect("ROLLBACK");
994 assert!(!db.in_transaction());
995 let after = db.get_table("users".to_string()).unwrap().rowids().len();
996 assert_eq!(
997 after, 3,
998 "ROLLBACK should have restored the pre-BEGIN state"
999 );
1000 }
1001
1002 #[test]
1003 fn commit_keeps_mutations_and_clears_txn_flag() {
1004 let mut db = seed_users_table();
1005 process_command("BEGIN;", &mut db).expect("BEGIN");
1006 process_command("INSERT INTO users (name, age) VALUES ('dan', 50);", &mut db)
1007 .expect("INSERT inside txn");
1008 process_command("COMMIT;", &mut db).expect("COMMIT");
1009 assert!(!db.in_transaction());
1010 let after = db.get_table("users".to_string()).unwrap().rowids().len();
1011 assert_eq!(after, 4);
1012 }
1013
1014 #[test]
1015 fn rollback_undoes_update_and_delete_side_by_side() {
1016 use crate::sql::db::table::Value;
1017
1018 let mut db = seed_users_table();
1019 process_command("BEGIN;", &mut db).unwrap();
1020 process_command("UPDATE users SET age = 999;", &mut db).unwrap();
1021 process_command("DELETE FROM users WHERE name = 'bob';", &mut db).unwrap();
1022 let users = db.get_table("users".to_string()).unwrap();
1024 assert_eq!(users.rowids().len(), 2);
1025 for r in users.rowids() {
1026 assert_eq!(users.get_value("age", r), Some(Value::Integer(999)));
1027 }
1028
1029 process_command("ROLLBACK;", &mut db).unwrap();
1030 let users = db.get_table("users".to_string()).unwrap();
1031 assert_eq!(users.rowids().len(), 3);
1032 for r in users.rowids() {
1034 assert_ne!(users.get_value("age", r), Some(Value::Integer(999)));
1035 }
1036 }
1037
1038 #[test]
1039 fn nested_begin_is_rejected() {
1040 let mut db = seed_users_table();
1041 process_command("BEGIN;", &mut db).unwrap();
1042 let err = process_command("BEGIN;", &mut db).unwrap_err();
1043 assert!(
1044 format!("{err}").contains("already open"),
1045 "nested BEGIN should error; got: {err}"
1046 );
1047 assert!(db.in_transaction());
1049 process_command("ROLLBACK;", &mut db).unwrap();
1050 }
1051
1052 #[test]
1053 fn orphan_commit_and_rollback_are_rejected() {
1054 let mut db = seed_users_table();
1055 let commit_err = process_command("COMMIT;", &mut db).unwrap_err();
1056 assert!(format!("{commit_err}").contains("no transaction"));
1057 let rollback_err = process_command("ROLLBACK;", &mut db).unwrap_err();
1058 assert!(format!("{rollback_err}").contains("no transaction"));
1059 }
1060
1061 #[test]
1062 fn error_inside_transaction_keeps_txn_open() {
1063 let mut db = seed_users_table();
1067 process_command("BEGIN;", &mut db).unwrap();
1068 let err = process_command("INSERT INTO nope (x) VALUES (1);", &mut db);
1069 assert!(err.is_err());
1070 assert!(db.in_transaction(), "txn should stay open after error");
1071 process_command("ROLLBACK;", &mut db).unwrap();
1072 }
1073
1074 fn seed_file_backed(name: &str, schema: &str) -> (std::path::PathBuf, Database) {
1079 use crate::sql::pager::{open_database, save_database};
1080 let mut p = std::env::temp_dir();
1081 let pid = std::process::id();
1082 let nanos = std::time::SystemTime::now()
1083 .duration_since(std::time::UNIX_EPOCH)
1084 .map(|d| d.as_nanos())
1085 .unwrap_or(0);
1086 p.push(format!("sqlrite-txn-{name}-{pid}-{nanos}.sqlrite"));
1087
1088 {
1092 let mut seed = Database::new("t".to_string());
1093 process_command(schema, &mut seed).unwrap();
1094 save_database(&mut seed, &p).unwrap();
1095 }
1096 let db = open_database(&p, "t".to_string()).unwrap();
1097 (p, db)
1098 }
1099
1100 fn cleanup_file(path: &std::path::Path) {
1101 let _ = std::fs::remove_file(path);
1102 let mut wal = path.as_os_str().to_owned();
1103 wal.push("-wal");
1104 let _ = std::fs::remove_file(std::path::PathBuf::from(wal));
1105 }
1106
1107 #[test]
1108 fn null_values_round_trip_through_disk() {
1109 use crate::sql::db::table::Value;
1115 use crate::sql::pager::open_database;
1116
1117 let (path, mut db) = seed_file_backed(
1118 "nullrt",
1119 "CREATE TABLE t (id INTEGER PRIMARY KEY, n INTEGER, s TEXT, score REAL, flag BOOLEAN);",
1120 );
1121 process_command(
1122 "INSERT INTO t (id, n, s, score, flag) VALUES (1, 10, 'hi', 1.5, true);",
1123 &mut db,
1124 )
1125 .unwrap();
1126 process_command(
1127 "INSERT INTO t (id, n, s, score, flag) VALUES (2, NULL, NULL, NULL, NULL);",
1128 &mut db,
1129 )
1130 .unwrap();
1131 process_command("INSERT INTO t (id) VALUES (3);", &mut db).unwrap();
1133
1134 drop(db); let reopened = open_database(&path, "t".to_string()).unwrap();
1137 let t = reopened.get_table("t".to_string()).unwrap();
1138 let by_id = |id: i64| {
1139 t.rowids()
1140 .into_iter()
1141 .find(|r| t.get_value("id", *r) == Some(Value::Integer(id)))
1142 .unwrap_or_else(|| panic!("row id={id} not found"))
1143 };
1144
1145 let r1 = by_id(1);
1146 assert_eq!(t.get_value("n", r1), Some(Value::Integer(10)));
1147 assert_eq!(t.get_value("s", r1), Some(Value::Text("hi".to_string())));
1148 assert_eq!(t.get_value("score", r1), Some(Value::Real(1.5)));
1149 assert_eq!(t.get_value("flag", r1), Some(Value::Bool(true)));
1150
1151 for r in [by_id(2), by_id(3)] {
1152 assert_eq!(t.get_value("n", r), None, "INTEGER NULL must round-trip");
1153 assert_eq!(t.get_value("s", r), None, "TEXT NULL must round-trip");
1154 assert_eq!(t.get_value("score", r), None, "REAL NULL must round-trip");
1155 assert_eq!(t.get_value("flag", r), None, "BOOL NULL must round-trip");
1156 }
1157
1158 drop(reopened);
1159 cleanup_file(&path);
1160 }
1161
1162 #[test]
1163 fn begin_commit_rollback_round_trip_through_disk() {
1164 use crate::sql::pager::open_database;
1168
1169 let (path, mut db) = seed_file_backed(
1170 "roundtrip",
1171 "CREATE TABLE notes (id INTEGER PRIMARY KEY, body TEXT);",
1172 );
1173
1174 process_command("BEGIN;", &mut db).unwrap();
1176 process_command("INSERT INTO notes (body) VALUES ('a');", &mut db).unwrap();
1177 process_command("INSERT INTO notes (body) VALUES ('b');", &mut db).unwrap();
1178 process_command("COMMIT;", &mut db).unwrap();
1179
1180 process_command("BEGIN;", &mut db).unwrap();
1182 process_command("INSERT INTO notes (body) VALUES ('c');", &mut db).unwrap();
1183 process_command("ROLLBACK;", &mut db).unwrap();
1184
1185 drop(db); let reopened = open_database(&path, "t".to_string()).unwrap();
1188 let notes = reopened.get_table("notes".to_string()).unwrap();
1189 assert_eq!(notes.rowids().len(), 2, "committed rows should survive");
1190
1191 drop(reopened);
1192 cleanup_file(&path);
1193 }
1194
1195 #[test]
1196 fn write_inside_transaction_does_not_autosave() {
1197 let (path, mut db) =
1201 seed_file_backed("noas", "CREATE TABLE t (id INTEGER PRIMARY KEY, x TEXT);");
1202
1203 let mut wal_path = path.as_os_str().to_owned();
1204 wal_path.push("-wal");
1205 let wal_path = std::path::PathBuf::from(wal_path);
1206 let frames_before = std::fs::metadata(&wal_path).unwrap().len();
1207
1208 process_command("BEGIN;", &mut db).unwrap();
1209 process_command("INSERT INTO t (x) VALUES ('a');", &mut db).unwrap();
1210 process_command("INSERT INTO t (x) VALUES ('b');", &mut db).unwrap();
1211
1212 let frames_mid = std::fs::metadata(&wal_path).unwrap().len();
1214 assert_eq!(
1215 frames_before, frames_mid,
1216 "WAL should not grow during an open transaction"
1217 );
1218
1219 process_command("COMMIT;", &mut db).unwrap();
1220
1221 drop(db); let fresh = crate::sql::pager::open_database(&path, "t".to_string()).unwrap();
1223 assert_eq!(
1224 fresh.get_table("t".to_string()).unwrap().rowids().len(),
1225 2,
1226 "COMMIT should have persisted both inserted rows"
1227 );
1228 drop(fresh);
1229 cleanup_file(&path);
1230 }
1231
1232 #[test]
1233 fn rollback_undoes_create_table() {
1234 let mut db = seed_users_table();
1239 assert_eq!(db.tables.len(), 1);
1240
1241 process_command("BEGIN;", &mut db).unwrap();
1242 process_command(
1243 "CREATE TABLE dropme (id INTEGER PRIMARY KEY, x TEXT);",
1244 &mut db,
1245 )
1246 .unwrap();
1247 process_command("INSERT INTO dropme (x) VALUES ('stuff');", &mut db).unwrap();
1248 assert_eq!(db.tables.len(), 2);
1249
1250 process_command("ROLLBACK;", &mut db).unwrap();
1251 assert_eq!(
1252 db.tables.len(),
1253 1,
1254 "CREATE TABLE should have been rolled back"
1255 );
1256 assert!(db.get_table("dropme".to_string()).is_err());
1257 }
1258
1259 #[test]
1260 fn rollback_restores_secondary_index_state() {
1261 let mut db = Database::new("t".to_string());
1265 process_command(
1266 "CREATE TABLE users (id INTEGER PRIMARY KEY, email TEXT UNIQUE);",
1267 &mut db,
1268 )
1269 .unwrap();
1270 process_command("INSERT INTO users (email) VALUES ('a@x');", &mut db).unwrap();
1271
1272 process_command("BEGIN;", &mut db).unwrap();
1273 process_command("INSERT INTO users (email) VALUES ('b@x');", &mut db).unwrap();
1274 process_command("ROLLBACK;", &mut db).unwrap();
1276
1277 let reinsert = process_command("INSERT INTO users (email) VALUES ('b@x');", &mut db);
1281 assert!(
1282 reinsert.is_ok(),
1283 "re-insert after rollback should succeed, got {reinsert:?}"
1284 );
1285 }
1286
1287 #[test]
1288 fn rollback_restores_last_rowid_counter() {
1289 use crate::sql::db::table::Value;
1293
1294 let mut db = seed_users_table(); let pre = db.get_table("users".to_string()).unwrap().last_rowid;
1296
1297 process_command("BEGIN;", &mut db).unwrap();
1298 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();
1301
1302 let post = db.get_table("users".to_string()).unwrap().last_rowid;
1303 assert_eq!(pre, post, "last_rowid must roll back with the snapshot");
1304
1305 process_command("INSERT INTO users (name, age) VALUES ('d', 50);", &mut db).unwrap();
1307 let users = db.get_table("users".to_string()).unwrap();
1308 let d_rowid = users
1309 .rowids()
1310 .into_iter()
1311 .find(|r| users.get_value("name", *r) == Some(Value::Text("d".into())))
1312 .expect("d row must exist");
1313 assert_eq!(d_rowid, pre + 1);
1314 }
1315
1316 #[test]
1317 fn commit_on_in_memory_db_clears_txn_without_pager_call() {
1318 let mut db = seed_users_table(); assert!(db.source_path.is_none());
1323
1324 process_command("BEGIN;", &mut db).unwrap();
1325 process_command("INSERT INTO users (name, age) VALUES ('z', 99);", &mut db).unwrap();
1326 process_command("COMMIT;", &mut db).unwrap();
1327
1328 assert!(!db.in_transaction());
1329 assert_eq!(db.get_table("users".to_string()).unwrap().rowids().len(), 4);
1330 }
1331
1332 #[test]
1333 fn failed_commit_auto_rolls_back_in_memory_state() {
1334 use crate::sql::pager::save_database;
1348
1349 let (path, mut db) = seed_file_backed(
1351 "failcommit",
1352 "CREATE TABLE notes (id INTEGER PRIMARY KEY, body TEXT);",
1353 );
1354
1355 process_command("INSERT INTO notes (body) VALUES ('before');", &mut db).unwrap();
1357
1358 process_command("BEGIN;", &mut db).unwrap();
1360 process_command("INSERT INTO notes (body) VALUES ('inflight');", &mut db).unwrap();
1361 assert_eq!(
1362 db.get_table("notes".to_string()).unwrap().rowids().len(),
1363 2,
1364 "inflight row visible mid-txn"
1365 );
1366
1367 let orig_source = db.source_path.clone();
1371 let orig_pager = db.pager.take();
1372 db.source_path = Some(std::env::temp_dir());
1373
1374 let commit_result = process_command("COMMIT;", &mut db);
1375 assert!(commit_result.is_err(), "commit must fail");
1376 let err_str = format!("{}", commit_result.unwrap_err());
1377 assert!(
1378 err_str.contains("COMMIT failed") && err_str.contains("rolled back"),
1379 "error must surface auto-rollback; got: {err_str}"
1380 );
1381
1382 assert!(
1386 !db.in_transaction(),
1387 "txn must be cleared after auto-rollback"
1388 );
1389 assert_eq!(
1390 db.get_table("notes".to_string()).unwrap().rowids().len(),
1391 1,
1392 "inflight row must be rolled back"
1393 );
1394
1395 db.source_path = orig_source;
1398 db.pager = orig_pager;
1399 process_command("INSERT INTO notes (body) VALUES ('after');", &mut db).unwrap();
1400 drop(db);
1401
1402 let reopened = crate::sql::pager::open_database(&path, "t".to_string()).unwrap();
1404 let notes = reopened.get_table("notes".to_string()).unwrap();
1405 assert_eq!(notes.rowids().len(), 2);
1406 let _ = save_database; drop(reopened);
1409 cleanup_file(&path);
1410 }
1411
1412 #[test]
1413 fn begin_on_read_only_is_rejected() {
1414 use crate::sql::pager::{open_database_read_only, save_database};
1415
1416 let path = {
1417 let mut p = std::env::temp_dir();
1418 let pid = std::process::id();
1419 let nanos = std::time::SystemTime::now()
1420 .duration_since(std::time::UNIX_EPOCH)
1421 .map(|d| d.as_nanos())
1422 .unwrap_or(0);
1423 p.push(format!("sqlrite-txn-ro-{pid}-{nanos}.sqlrite"));
1424 p
1425 };
1426 {
1427 let mut seed = Database::new("t".to_string());
1428 process_command("CREATE TABLE t (id INTEGER PRIMARY KEY);", &mut seed).unwrap();
1429 save_database(&mut seed, &path).unwrap();
1430 }
1431
1432 let mut ro = open_database_read_only(&path, "t".to_string()).unwrap();
1433 let err = process_command("BEGIN;", &mut ro).unwrap_err();
1434 assert!(
1435 format!("{err}").contains("read-only"),
1436 "BEGIN on RO db should surface read-only; got: {err}"
1437 );
1438 assert!(!ro.in_transaction());
1439
1440 let _ = std::fs::remove_file(&path);
1441 let mut wal = path.as_os_str().to_owned();
1442 wal.push("-wal");
1443 let _ = std::fs::remove_file(std::path::PathBuf::from(wal));
1444 }
1445
1446 #[test]
1447 fn read_only_database_rejects_mutations_before_touching_state() {
1448 use crate::sql::pager::open_database_read_only;
1454
1455 let mut seed = Database::new("t".to_string());
1456 process_command(
1457 "CREATE TABLE notes (id INTEGER PRIMARY KEY, body TEXT);",
1458 &mut seed,
1459 )
1460 .unwrap();
1461 process_command("INSERT INTO notes (body) VALUES ('alpha');", &mut seed).unwrap();
1462
1463 let path = {
1464 let mut p = std::env::temp_dir();
1465 let pid = std::process::id();
1466 let nanos = std::time::SystemTime::now()
1467 .duration_since(std::time::UNIX_EPOCH)
1468 .map(|d| d.as_nanos())
1469 .unwrap_or(0);
1470 p.push(format!("sqlrite-ro-reject-{pid}-{nanos}.sqlrite"));
1471 p
1472 };
1473 crate::sql::pager::save_database(&mut seed, &path).unwrap();
1474 drop(seed);
1475
1476 let mut ro = open_database_read_only(&path, "t".to_string()).unwrap();
1477 let notes_before = ro.get_table("notes".to_string()).unwrap().rowids().len();
1478
1479 for stmt in [
1480 "INSERT INTO notes (body) VALUES ('beta');",
1481 "UPDATE notes SET body = 'x';",
1482 "DELETE FROM notes;",
1483 "CREATE TABLE more (id INTEGER PRIMARY KEY);",
1484 "CREATE INDEX notes_body ON notes (body);",
1485 ] {
1486 let err = process_command(stmt, &mut ro).unwrap_err();
1487 assert!(
1488 format!("{err}").contains("read-only"),
1489 "stmt {stmt:?} should surface a read-only error; got: {err}"
1490 );
1491 }
1492
1493 let notes_after = ro.get_table("notes".to_string()).unwrap().rowids().len();
1495 assert_eq!(notes_before, notes_after);
1496 let sel = process_command("SELECT * FROM notes;", &mut ro).expect("select on RO must work");
1497 assert!(sel.contains("1 row returned"));
1498
1499 drop(ro);
1501 let _ = std::fs::remove_file(&path);
1502 let mut wal = path.as_os_str().to_owned();
1503 wal.push("-wal");
1504 let _ = std::fs::remove_file(std::path::PathBuf::from(wal));
1505 }
1506
1507 #[test]
1512 fn vector_create_table_and_insert_basic() {
1513 let mut db = Database::new("tempdb".to_string());
1514 process_command(
1515 "CREATE TABLE docs (id INTEGER PRIMARY KEY, embedding VECTOR(3));",
1516 &mut db,
1517 )
1518 .expect("create table with VECTOR(3)");
1519 process_command(
1520 "INSERT INTO docs (embedding) VALUES ([0.1, 0.2, 0.3]);",
1521 &mut db,
1522 )
1523 .expect("insert vector");
1524
1525 let sel = process_command("SELECT * FROM docs;", &mut db).expect("select");
1529 assert!(sel.contains("1 row returned"));
1530
1531 let docs = db.get_table("docs".to_string()).expect("docs table");
1532 let rowids = docs.rowids();
1533 assert_eq!(rowids.len(), 1);
1534 match docs.get_value("embedding", rowids[0]) {
1535 Some(Value::Vector(v)) => assert_eq!(v, vec![0.1f32, 0.2, 0.3]),
1536 other => panic!("expected Value::Vector(...), got {other:?}"),
1537 }
1538 }
1539
1540 #[test]
1541 fn vector_dim_mismatch_at_insert_is_clean_error() {
1542 let mut db = Database::new("tempdb".to_string());
1543 process_command(
1544 "CREATE TABLE docs (id INTEGER PRIMARY KEY, embedding VECTOR(3));",
1545 &mut db,
1546 )
1547 .expect("create table");
1548
1549 let err = process_command("INSERT INTO docs (embedding) VALUES ([0.1, 0.2]);", &mut db)
1551 .unwrap_err();
1552 let msg = format!("{err}");
1553 assert!(
1554 msg.to_lowercase().contains("dimension")
1555 && msg.contains("declared 3")
1556 && msg.contains("got 2"),
1557 "expected clear dim-mismatch error, got: {msg}"
1558 );
1559
1560 let err = process_command(
1562 "INSERT INTO docs (embedding) VALUES ([0.1, 0.2, 0.3, 0.4, 0.5]);",
1563 &mut db,
1564 )
1565 .unwrap_err();
1566 assert!(
1567 format!("{err}").contains("got 5"),
1568 "expected dim-mismatch error mentioning got 5, got: {err}"
1569 );
1570 }
1571
1572 #[test]
1573 fn vector_create_table_rejects_missing_dim() {
1574 let mut db = Database::new("tempdb".to_string());
1575 let result = process_command(
1581 "CREATE TABLE docs (id INTEGER PRIMARY KEY, embedding VECTOR);",
1582 &mut db,
1583 );
1584 assert!(
1585 result.is_err(),
1586 "expected CREATE TABLE with bare VECTOR to fail (no dim)"
1587 );
1588 }
1589
1590 #[test]
1591 fn vector_create_table_rejects_zero_dim() {
1592 let mut db = Database::new("tempdb".to_string());
1593 let err = process_command(
1594 "CREATE TABLE docs (id INTEGER PRIMARY KEY, embedding VECTOR(0));",
1595 &mut db,
1596 )
1597 .unwrap_err();
1598 let msg = format!("{err}");
1599 assert!(
1600 msg.to_lowercase().contains("vector"),
1601 "expected VECTOR-related error for VECTOR(0), got: {msg}"
1602 );
1603 }
1604
1605 #[test]
1606 fn vector_high_dim_works() {
1607 let mut db = Database::new("tempdb".to_string());
1610 process_command(
1611 "CREATE TABLE embeddings (id INTEGER PRIMARY KEY, e VECTOR(384));",
1612 &mut db,
1613 )
1614 .expect("create table VECTOR(384)");
1615
1616 let lit = format!(
1617 "[{}]",
1618 (0..384)
1619 .map(|i| format!("{}", i as f32 * 0.001))
1620 .collect::<Vec<_>>()
1621 .join(",")
1622 );
1623 let sql = format!("INSERT INTO embeddings (e) VALUES ({lit});");
1624 process_command(&sql, &mut db).expect("insert 384-dim vector");
1625
1626 let sel = process_command("SELECT id FROM embeddings;", &mut db).expect("select id");
1627 assert!(sel.contains("1 row returned"));
1628 }
1629
1630 #[test]
1631 fn vector_multiple_rows() {
1632 let mut db = Database::new("tempdb".to_string());
1635 process_command(
1636 "CREATE TABLE docs (id INTEGER PRIMARY KEY, e VECTOR(2));",
1637 &mut db,
1638 )
1639 .expect("create");
1640 for i in 0..3 {
1641 let sql = format!("INSERT INTO docs (e) VALUES ([{i}.0, {}.0]);", i + 1);
1642 process_command(&sql, &mut db).expect("insert");
1643 }
1644 let sel = process_command("SELECT * FROM docs;", &mut db).expect("select");
1645 assert!(sel.contains("3 rows returned"));
1646
1647 let docs = db.get_table("docs".to_string()).expect("docs table");
1649 let rowids = docs.rowids();
1650 assert_eq!(rowids.len(), 3);
1651 let mut vectors: Vec<Vec<f32>> = rowids
1652 .iter()
1653 .filter_map(|r| match docs.get_value("e", *r) {
1654 Some(Value::Vector(v)) => Some(v),
1655 _ => None,
1656 })
1657 .collect();
1658 vectors.sort_by(|a, b| a[0].partial_cmp(&b[0]).unwrap());
1659 assert_eq!(vectors[0], vec![0.0f32, 1.0]);
1660 assert_eq!(vectors[1], vec![1.0f32, 2.0]);
1661 assert_eq!(vectors[2], vec![2.0f32, 3.0]);
1662 }
1663
1664 fn seed_hnsw_table() -> Database {
1672 let mut db = Database::new("tempdb".to_string());
1673 process_command(
1674 "CREATE TABLE docs (id INTEGER PRIMARY KEY, e VECTOR(2));",
1675 &mut db,
1676 )
1677 .unwrap();
1678 for v in &[
1679 "[1.0, 0.0]", "[2.0, 0.0]", "[0.0, 3.0]", "[1.0, 4.0]", "[10.0, 10.0]", ] {
1685 process_command(&format!("INSERT INTO docs (e) VALUES ({v});"), &mut db).unwrap();
1686 }
1687 db
1688 }
1689
1690 #[test]
1691 fn create_index_using_hnsw_succeeds() {
1692 let mut db = seed_hnsw_table();
1693 let resp = process_command("CREATE INDEX ix_e ON docs USING hnsw (e);", &mut db).unwrap();
1694 assert!(resp.to_lowercase().contains("create index"));
1695 let table = db.get_table("docs".to_string()).unwrap();
1697 assert_eq!(table.hnsw_indexes.len(), 1);
1698 assert_eq!(table.hnsw_indexes[0].name, "ix_e");
1699 assert_eq!(table.hnsw_indexes[0].column_name, "e");
1700 assert_eq!(table.hnsw_indexes[0].index.len(), 5);
1702 }
1703
1704 #[test]
1705 fn create_index_using_hnsw_rejects_non_vector_column() {
1706 let mut db = Database::new("tempdb".to_string());
1707 process_command(
1708 "CREATE TABLE t (id INTEGER PRIMARY KEY, name TEXT);",
1709 &mut db,
1710 )
1711 .unwrap();
1712 let err =
1713 process_command("CREATE INDEX ix_name ON t USING hnsw (name);", &mut db).unwrap_err();
1714 let msg = format!("{err}");
1715 assert!(
1716 msg.to_lowercase().contains("vector"),
1717 "expected error mentioning VECTOR; got: {msg}"
1718 );
1719 }
1720
1721 #[test]
1722 fn knn_query_uses_hnsw_after_create_index() {
1723 let mut db = seed_hnsw_table();
1729 process_command("CREATE INDEX ix_e ON docs USING hnsw (e);", &mut db).unwrap();
1730
1731 let resp = process_command(
1736 "SELECT id FROM docs ORDER BY vec_distance_l2(e, [1.0, 0.0]) ASC LIMIT 3;",
1737 &mut db,
1738 )
1739 .unwrap();
1740 assert!(resp.contains("3 rows returned"), "got: {resp}");
1741 }
1742
1743 #[test]
1744 fn knn_query_works_after_subsequent_inserts() {
1745 let mut db = seed_hnsw_table();
1749 process_command("CREATE INDEX ix_e ON docs USING hnsw (e);", &mut db).unwrap();
1750 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();
1754 assert_eq!(
1755 table.hnsw_indexes[0].index.len(),
1756 7,
1757 "incremental insert should grow HNSW alongside row storage"
1758 );
1759
1760 let resp = process_command(
1763 "SELECT id FROM docs ORDER BY vec_distance_l2(e, [0.0, 0.0]) ASC LIMIT 1;",
1764 &mut db,
1765 )
1766 .unwrap();
1767 assert!(resp.contains("1 row returned"), "got: {resp}");
1768 }
1769
1770 #[test]
1776 fn delete_on_hnsw_indexed_table_succeeds_and_marks_dirty() {
1777 let mut db = seed_hnsw_table();
1778 process_command("CREATE INDEX ix_e ON docs USING hnsw (e);", &mut db).unwrap();
1779 let resp = process_command("DELETE FROM docs WHERE id = 1;", &mut db).unwrap();
1780 assert!(resp.contains("1 row"), "expected 1 row deleted: {resp}");
1781
1782 let docs = db.get_table("docs".to_string()).unwrap();
1783 let entry = docs.hnsw_indexes.iter().find(|e| e.name == "ix_e").unwrap();
1784 assert!(
1785 entry.needs_rebuild,
1786 "DELETE should have marked HNSW index dirty for rebuild on next save"
1787 );
1788 }
1789
1790 #[test]
1791 fn update_on_hnsw_indexed_vector_col_succeeds_and_marks_dirty() {
1792 let mut db = seed_hnsw_table();
1793 process_command("CREATE INDEX ix_e ON docs USING hnsw (e);", &mut db).unwrap();
1794 let resp =
1795 process_command("UPDATE docs SET e = [9.0, 9.0] WHERE id = 1;", &mut db).unwrap();
1796 assert!(resp.contains("1 row"), "expected 1 row updated: {resp}");
1797
1798 let docs = db.get_table("docs".to_string()).unwrap();
1799 let entry = docs.hnsw_indexes.iter().find(|e| e.name == "ix_e").unwrap();
1800 assert!(
1801 entry.needs_rebuild,
1802 "UPDATE on the vector column should have marked HNSW index dirty"
1803 );
1804 }
1805
1806 #[test]
1807 fn duplicate_index_name_errors() {
1808 let mut db = seed_hnsw_table();
1809 process_command("CREATE INDEX ix_e ON docs USING hnsw (e);", &mut db).unwrap();
1810 let err =
1811 process_command("CREATE INDEX ix_e ON docs USING hnsw (e);", &mut db).unwrap_err();
1812 let msg = format!("{err}");
1813 assert!(
1814 msg.to_lowercase().contains("already exists"),
1815 "expected duplicate-index error; got: {msg}"
1816 );
1817 }
1818
1819 #[test]
1820 fn index_if_not_exists_is_idempotent() {
1821 let mut db = seed_hnsw_table();
1822 process_command("CREATE INDEX ix_e ON docs USING hnsw (e);", &mut db).unwrap();
1823 process_command(
1825 "CREATE INDEX IF NOT EXISTS ix_e ON docs USING hnsw (e);",
1826 &mut db,
1827 )
1828 .unwrap();
1829 let table = db.get_table("docs".to_string()).unwrap();
1830 assert_eq!(table.hnsw_indexes.len(), 1);
1831 }
1832
1833 fn seed_fts_table() -> Database {
1840 let mut db = Database::new("tempdb".to_string());
1841 process_command(
1842 "CREATE TABLE docs (id INTEGER PRIMARY KEY, body TEXT);",
1843 &mut db,
1844 )
1845 .unwrap();
1846 for body in &[
1847 "rust embedded database", "rust web framework", "go embedded systems", "python web framework", "rust rust rust embedded power", ] {
1853 process_command(
1854 &format!("INSERT INTO docs (body) VALUES ('{body}');"),
1855 &mut db,
1856 )
1857 .unwrap();
1858 }
1859 db
1860 }
1861
1862 #[test]
1863 fn create_index_using_fts_succeeds_and_indexes_existing_rows() {
1864 let mut db = seed_fts_table();
1865 let resp =
1866 process_command("CREATE INDEX ix_body ON docs USING fts (body);", &mut db).unwrap();
1867 assert!(resp.to_lowercase().contains("create index"), "got {resp}");
1868 let table = db.get_table("docs".to_string()).unwrap();
1869 assert_eq!(table.fts_indexes.len(), 1);
1870 assert_eq!(table.fts_indexes[0].name, "ix_body");
1871 assert_eq!(table.fts_indexes[0].column_name, "body");
1872 assert_eq!(table.fts_indexes[0].index.len(), 5);
1874 }
1875
1876 #[test]
1877 fn create_index_using_fts_rejects_non_text_column() {
1878 let mut db = Database::new("tempdb".to_string());
1879 process_command(
1880 "CREATE TABLE t (id INTEGER PRIMARY KEY, n INTEGER);",
1881 &mut db,
1882 )
1883 .unwrap();
1884 let err = process_command("CREATE INDEX ix_n ON t USING fts (n);", &mut db).unwrap_err();
1885 let msg = format!("{err}");
1886 assert!(
1887 msg.to_lowercase().contains("text"),
1888 "expected error mentioning TEXT; got: {msg}"
1889 );
1890 }
1891
1892 #[test]
1893 fn fts_match_returns_expected_rows() {
1894 let mut db = seed_fts_table();
1895 process_command("CREATE INDEX ix_body ON docs USING fts (body);", &mut db).unwrap();
1896 let resp = process_command(
1898 "SELECT id FROM docs WHERE fts_match(body, 'rust');",
1899 &mut db,
1900 )
1901 .unwrap();
1902 assert!(resp.contains("3 rows returned"), "got: {resp}");
1903 }
1904
1905 #[test]
1906 fn fts_match_without_index_errors_clearly() {
1907 let mut db = seed_fts_table();
1908 let err = process_command(
1910 "SELECT id FROM docs WHERE fts_match(body, 'rust');",
1911 &mut db,
1912 )
1913 .unwrap_err();
1914 let msg = format!("{err}");
1915 assert!(
1916 msg.contains("no FTS index"),
1917 "expected no-index error; got: {msg}"
1918 );
1919 }
1920
1921 #[test]
1922 fn bm25_score_orders_descending_by_relevance() {
1923 let mut db = seed_fts_table();
1924 process_command("CREATE INDEX ix_body ON docs USING fts (body);", &mut db).unwrap();
1925 let out = process_command_with_render(
1928 "SELECT id FROM docs WHERE fts_match(body, 'rust') \
1929 ORDER BY bm25_score(body, 'rust') DESC LIMIT 1;",
1930 &mut db,
1931 )
1932 .unwrap();
1933 assert!(out.status.contains("1 row returned"), "got: {}", out.status);
1934 let rendered = out.rendered.expect("SELECT should produce rendered output");
1935 assert!(
1937 rendered.contains(" 5 "),
1938 "expected id=5 to be top-ranked; rendered:\n{rendered}"
1939 );
1940 }
1941
1942 #[test]
1943 fn bm25_score_without_index_errors_clearly() {
1944 let mut db = seed_fts_table();
1945 let err = process_command(
1946 "SELECT id FROM docs ORDER BY bm25_score(body, 'rust') DESC LIMIT 1;",
1947 &mut db,
1948 )
1949 .unwrap_err();
1950 let msg = format!("{err}");
1951 assert!(
1952 msg.contains("no FTS index"),
1953 "expected no-index error; got: {msg}"
1954 );
1955 }
1956
1957 #[test]
1958 fn fts_post_create_inserts_are_indexed_incrementally() {
1959 let mut db = seed_fts_table();
1960 process_command("CREATE INDEX ix_body ON docs USING fts (body);", &mut db).unwrap();
1961 process_command(
1962 "INSERT INTO docs (body) VALUES ('rust embedded analytics');",
1963 &mut db,
1964 )
1965 .unwrap();
1966 let table = db.get_table("docs".to_string()).unwrap();
1967 assert_eq!(table.fts_indexes[0].index.len(), 6);
1969 let resp = process_command(
1971 "SELECT id FROM docs WHERE fts_match(body, 'analytics');",
1972 &mut db,
1973 )
1974 .unwrap();
1975 assert!(resp.contains("1 row returned"), "got: {resp}");
1976 }
1977
1978 #[test]
1979 fn delete_on_fts_indexed_table_marks_dirty() {
1980 let mut db = seed_fts_table();
1981 process_command("CREATE INDEX ix_body ON docs USING fts (body);", &mut db).unwrap();
1982 let resp = process_command("DELETE FROM docs WHERE id = 1;", &mut db).unwrap();
1983 assert!(resp.contains("1 row"), "got: {resp}");
1984 let docs = db.get_table("docs".to_string()).unwrap();
1985 let entry = docs
1986 .fts_indexes
1987 .iter()
1988 .find(|e| e.name == "ix_body")
1989 .unwrap();
1990 assert!(
1991 entry.needs_rebuild,
1992 "DELETE should have flagged the FTS index dirty"
1993 );
1994 }
1995
1996 #[test]
1997 fn update_on_fts_indexed_text_col_marks_dirty() {
1998 let mut db = seed_fts_table();
1999 process_command("CREATE INDEX ix_body ON docs USING fts (body);", &mut db).unwrap();
2000 let resp = process_command(
2001 "UPDATE docs SET body = 'java spring framework' WHERE id = 1;",
2002 &mut db,
2003 )
2004 .unwrap();
2005 assert!(resp.contains("1 row"), "got: {resp}");
2006 let docs = db.get_table("docs".to_string()).unwrap();
2007 let entry = docs
2008 .fts_indexes
2009 .iter()
2010 .find(|e| e.name == "ix_body")
2011 .unwrap();
2012 assert!(
2013 entry.needs_rebuild,
2014 "UPDATE on the indexed TEXT column should have flagged dirty"
2015 );
2016 }
2017
2018 #[test]
2019 fn fts_index_name_collides_with_btree_and_hnsw_namespaces() {
2020 let mut db = seed_fts_table();
2021 process_command("CREATE INDEX ix_body ON docs USING fts (body);", &mut db).unwrap();
2022 let err = process_command("CREATE INDEX ix_body ON docs (body);", &mut db).unwrap_err();
2023 let msg = format!("{err}");
2024 assert!(
2025 msg.to_lowercase().contains("already exists"),
2026 "expected duplicate-index error; got: {msg}"
2027 );
2028 }
2029
2030 #[test]
2031 fn fts_index_rejects_unique() {
2032 let mut db = seed_fts_table();
2033 let err = process_command(
2034 "CREATE UNIQUE INDEX ix_body ON docs USING fts (body);",
2035 &mut db,
2036 )
2037 .unwrap_err();
2038 let msg = format!("{err}");
2039 assert!(
2040 msg.to_lowercase().contains("unique"),
2041 "expected UNIQUE-rejection error; got: {msg}"
2042 );
2043 }
2044
2045 #[test]
2046 fn try_fts_probe_falls_through_on_ascending() {
2047 let mut db = seed_fts_table();
2052 process_command("CREATE INDEX ix_body ON docs USING fts (body);", &mut db).unwrap();
2053 let resp = process_command(
2056 "SELECT id FROM docs WHERE fts_match(body, 'rust') \
2057 ORDER BY bm25_score(body, 'rust') ASC LIMIT 3;",
2058 &mut db,
2059 )
2060 .unwrap();
2061 assert!(resp.contains("3 rows returned"), "got: {resp}");
2062 }
2063
2064 fn seed_vector_docs() -> Database {
2074 let mut db = Database::new("tempdb".to_string());
2075 process_command(
2076 "CREATE TABLE docs (id INTEGER PRIMARY KEY, e VECTOR(2));",
2077 &mut db,
2078 )
2079 .expect("create");
2080 process_command("INSERT INTO docs (e) VALUES ([1.0, 0.0]);", &mut db).expect("insert 1");
2081 process_command("INSERT INTO docs (e) VALUES ([0.0, 1.0]);", &mut db).expect("insert 2");
2082 process_command("INSERT INTO docs (e) VALUES ([1.0, 1.0]);", &mut db).expect("insert 3");
2083 db
2084 }
2085
2086 #[test]
2087 fn vec_distance_l2_in_where_filters_correctly() {
2088 let mut db = seed_vector_docs();
2094 let resp = process_command(
2095 "SELECT * FROM docs WHERE vec_distance_l2(e, [1.0, 0.0]) < 1.1;",
2096 &mut db,
2097 )
2098 .expect("select");
2099 assert!(
2100 resp.contains("2 rows returned"),
2101 "expected 2 rows, got: {resp}"
2102 );
2103 }
2104
2105 #[test]
2106 fn vec_distance_cosine_in_where() {
2107 let mut db = seed_vector_docs();
2112 let resp = process_command(
2113 "SELECT * FROM docs WHERE vec_distance_cosine(e, [1.0, 0.0]) < 0.5;",
2114 &mut db,
2115 )
2116 .expect("select");
2117 assert!(
2118 resp.contains("2 rows returned"),
2119 "expected 2 rows, got: {resp}"
2120 );
2121 }
2122
2123 #[test]
2124 fn vec_distance_dot_negated() {
2125 let mut db = seed_vector_docs();
2130 let resp = process_command(
2131 "SELECT * FROM docs WHERE vec_distance_dot(e, [1.0, 0.0]) < 0.0;",
2132 &mut db,
2133 )
2134 .expect("select");
2135 assert!(
2136 resp.contains("2 rows returned"),
2137 "expected 2 rows, got: {resp}"
2138 );
2139 }
2140
2141 #[test]
2142 fn knn_via_order_by_distance_limit() {
2143 let mut db = seed_vector_docs();
2147 let resp = process_command(
2148 "SELECT id FROM docs ORDER BY vec_distance_l2(e, [1.0, 0.0]) ASC LIMIT 2;",
2149 &mut db,
2150 )
2151 .expect("select");
2152 assert!(
2153 resp.contains("2 rows returned"),
2154 "expected 2 rows, got: {resp}"
2155 );
2156 }
2157
2158 #[test]
2159 fn distance_function_dim_mismatch_errors() {
2160 let mut db = seed_vector_docs();
2162 let err = process_command(
2163 "SELECT * FROM docs WHERE vec_distance_l2(e, [1.0, 0.0, 0.0]) < 1.0;",
2164 &mut db,
2165 )
2166 .unwrap_err();
2167 let msg = format!("{err}");
2168 assert!(
2169 msg.to_lowercase().contains("dimension")
2170 && msg.contains("lhs=2")
2171 && msg.contains("rhs=3"),
2172 "expected dim mismatch error, got: {msg}"
2173 );
2174 }
2175
2176 #[test]
2177 fn unknown_function_errors_with_name() {
2178 let mut db = seed_vector_docs();
2182 let err = process_command(
2183 "SELECT * FROM docs WHERE vec_does_not_exist(e, [1.0, 0.0]) < 1.0;",
2184 &mut db,
2185 )
2186 .unwrap_err();
2187 let msg = format!("{err}");
2188 assert!(
2189 msg.contains("vec_does_not_exist"),
2190 "expected error mentioning function name, got: {msg}"
2191 );
2192 }
2193
2194 fn seed_json_table() -> Database {
2199 let mut db = Database::new("tempdb".to_string());
2200 process_command(
2201 "CREATE TABLE docs (id INTEGER PRIMARY KEY, payload JSON);",
2202 &mut db,
2203 )
2204 .expect("create json table");
2205 db
2206 }
2207
2208 #[test]
2209 fn json_column_round_trip_primitive_values() {
2210 let mut db = seed_json_table();
2211 process_command(
2212 r#"INSERT INTO docs (payload) VALUES ('{"name": "alice", "age": 30}');"#,
2213 &mut db,
2214 )
2215 .expect("insert json");
2216 let docs = db.get_table("docs".to_string()).unwrap();
2217 let rowids = docs.rowids();
2218 assert_eq!(rowids.len(), 1);
2219 match docs.get_value("payload", rowids[0]) {
2221 Some(Value::Text(s)) => {
2222 assert!(s.contains("alice"), "expected JSON text to round-trip: {s}");
2223 }
2224 other => panic!("expected Value::Text holding JSON, got {other:?}"),
2225 }
2226 }
2227
2228 #[test]
2229 fn json_insert_rejects_invalid_json() {
2230 let mut db = seed_json_table();
2231 let err = process_command(
2232 "INSERT INTO docs (payload) VALUES ('not-valid-json{');",
2233 &mut db,
2234 )
2235 .unwrap_err();
2236 let msg = format!("{err}").to_lowercase();
2237 assert!(
2238 msg.contains("json") && msg.contains("payload"),
2239 "expected JSON validation error mentioning column, got: {msg}"
2240 );
2241 }
2242
2243 #[test]
2244 fn json_extract_object_field() {
2245 let mut db = seed_json_table();
2246 process_command(
2247 r#"INSERT INTO docs (payload) VALUES ('{"name": "alice", "age": 30}');"#,
2248 &mut db,
2249 )
2250 .unwrap();
2251 let resp = process_command(
2254 r#"SELECT id FROM docs WHERE json_extract(payload, '$.name') = 'alice';"#,
2255 &mut db,
2256 )
2257 .expect("select via json_extract");
2258 assert!(resp.contains("1 row returned"), "got: {resp}");
2259
2260 let resp = process_command(
2261 r#"SELECT id FROM docs WHERE json_extract(payload, '$.age') = 30;"#,
2262 &mut db,
2263 )
2264 .expect("select via numeric json_extract");
2265 assert!(resp.contains("1 row returned"), "got: {resp}");
2266 }
2267
2268 #[test]
2269 fn json_extract_array_index_and_nested() {
2270 let mut db = seed_json_table();
2271 process_command(
2272 r#"INSERT INTO docs (payload) VALUES ('{"tags": ["rust", "sql", "vectors"], "meta": {"author": "joao"}}');"#,
2273 &mut db,
2274 )
2275 .unwrap();
2276 let resp = process_command(
2277 r#"SELECT id FROM docs WHERE json_extract(payload, '$.tags[0]') = 'rust';"#,
2278 &mut db,
2279 )
2280 .expect("select via array index");
2281 assert!(resp.contains("1 row returned"), "got: {resp}");
2282
2283 let resp = process_command(
2284 r#"SELECT id FROM docs WHERE json_extract(payload, '$.meta.author') = 'joao';"#,
2285 &mut db,
2286 )
2287 .expect("select via nested object");
2288 assert!(resp.contains("1 row returned"), "got: {resp}");
2289 }
2290
2291 #[test]
2292 fn json_extract_missing_path_returns_null() {
2293 let mut db = seed_json_table();
2294 process_command(
2295 r#"INSERT INTO docs (payload) VALUES ('{"name": "alice"}');"#,
2296 &mut db,
2297 )
2298 .unwrap();
2299 let resp = process_command(
2302 r#"SELECT id FROM docs WHERE json_extract(payload, '$.missing') = 'something';"#,
2303 &mut db,
2304 )
2305 .expect("select with missing path");
2306 assert!(resp.contains("0 rows returned"), "got: {resp}");
2307 }
2308
2309 #[test]
2310 fn json_extract_malformed_path_errors() {
2311 let mut db = seed_json_table();
2312 process_command(
2313 r#"INSERT INTO docs (payload) VALUES ('{"a": 1}');"#,
2314 &mut db,
2315 )
2316 .unwrap();
2317 let err = process_command(
2319 r#"SELECT id FROM docs WHERE json_extract(payload, 'a.b') = 1;"#,
2320 &mut db,
2321 )
2322 .unwrap_err();
2323 assert!(format!("{err}").contains("'$'"));
2324 }
2325
2326 #[test]
2327 fn json_array_length_on_array() {
2328 let mut db = seed_json_table();
2331 process_command(
2332 r#"INSERT INTO docs (payload) VALUES ('{"tags": ["a", "b", "c"]}');"#,
2333 &mut db,
2334 )
2335 .unwrap();
2336 let resp = process_command(
2337 r#"SELECT id FROM docs WHERE json_array_length(payload, '$.tags') = 3;"#,
2338 &mut db,
2339 )
2340 .expect("select via array_length");
2341 assert!(resp.contains("1 row returned"), "got: {resp}");
2342 }
2343
2344 #[test]
2345 fn json_array_length_on_non_array_errors() {
2346 let mut db = seed_json_table();
2347 process_command(
2348 r#"INSERT INTO docs (payload) VALUES ('{"tags": "not-an-array"}');"#,
2349 &mut db,
2350 )
2351 .unwrap();
2352 let err = process_command(
2353 r#"SELECT id FROM docs WHERE json_array_length(payload, '$.tags') = 1;"#,
2354 &mut db,
2355 )
2356 .unwrap_err();
2357 let msg = format!("{err}").to_lowercase();
2358 assert!(
2359 msg.contains("non-array"),
2360 "expected non-array error, got: {msg}"
2361 );
2362 }
2363
2364 #[test]
2365 fn json_type_recognizes_each_kind() {
2366 let mut db = seed_json_table();
2367 process_command(
2368 r#"INSERT INTO docs (payload) VALUES ('{"o": {}, "a": [], "s": "x", "i": 1, "f": 1.5, "t": true, "n": null}');"#,
2369 &mut db,
2370 )
2371 .unwrap();
2372 let cases = &[
2373 ("$.o", "object"),
2374 ("$.a", "array"),
2375 ("$.s", "text"),
2376 ("$.i", "integer"),
2377 ("$.f", "real"),
2378 ("$.t", "true"),
2379 ("$.n", "null"),
2380 ];
2381 for (path, expected_type) in cases {
2382 let sql = format!(
2383 "SELECT id FROM docs WHERE json_type(payload, '{path}') = '{expected_type}';"
2384 );
2385 let resp =
2386 process_command(&sql, &mut db).unwrap_or_else(|e| panic!("path {path}: {e}"));
2387 assert!(
2388 resp.contains("1 row returned"),
2389 "path {path} expected type {expected_type}; got response: {resp}"
2390 );
2391 }
2392 }
2393
2394 #[test]
2395 fn update_on_json_column_revalidates() {
2396 let mut db = seed_json_table();
2397 process_command(
2398 r#"INSERT INTO docs (payload) VALUES ('{"a": 1}');"#,
2399 &mut db,
2400 )
2401 .unwrap();
2402 process_command(
2404 r#"UPDATE docs SET payload = '{"a": 2, "b": 3}' WHERE id = 1;"#,
2405 &mut db,
2406 )
2407 .expect("valid JSON UPDATE");
2408 let err = process_command(
2411 r#"UPDATE docs SET payload = 'not-json{' WHERE id = 1;"#,
2412 &mut db,
2413 )
2414 .unwrap_err();
2415 let msg = format!("{err}").to_lowercase();
2416 assert!(
2417 msg.contains("json") && msg.contains("payload"),
2418 "got: {msg}"
2419 );
2420 }
2421
2422 #[test]
2427 fn default_literal_int_applies_when_column_omitted() {
2428 let mut db = Database::new("t".to_string());
2429 process_command(
2430 "CREATE TABLE t (id INTEGER PRIMARY KEY, n INTEGER DEFAULT 42);",
2431 &mut db,
2432 )
2433 .unwrap();
2434 process_command("INSERT INTO t (id) VALUES (1);", &mut db).unwrap();
2435
2436 let table = db.get_table("t".to_string()).unwrap();
2437 assert_eq!(table.get_value("n", 1), Some(Value::Integer(42)));
2438 }
2439
2440 #[test]
2441 fn default_literal_text_applies_when_column_omitted() {
2442 let mut db = Database::new("t".to_string());
2443 process_command(
2444 "CREATE TABLE users (id INTEGER PRIMARY KEY, status TEXT DEFAULT 'active');",
2445 &mut db,
2446 )
2447 .unwrap();
2448 process_command("INSERT INTO users (id) VALUES (1);", &mut db).unwrap();
2449
2450 let table = db.get_table("users".to_string()).unwrap();
2451 assert_eq!(
2452 table.get_value("status", 1),
2453 Some(Value::Text("active".to_string()))
2454 );
2455 }
2456
2457 #[test]
2458 fn default_literal_real_negative_applies_when_column_omitted() {
2459 let mut db = Database::new("t".to_string());
2461 process_command(
2462 "CREATE TABLE t (id INTEGER PRIMARY KEY, score REAL DEFAULT -1.5);",
2463 &mut db,
2464 )
2465 .unwrap();
2466 process_command("INSERT INTO t (id) VALUES (1);", &mut db).unwrap();
2467
2468 let table = db.get_table("t".to_string()).unwrap();
2469 assert_eq!(table.get_value("score", 1), Some(Value::Real(-1.5)));
2470 }
2471
2472 #[test]
2473 fn default_with_type_mismatch_errors_at_create_time() {
2474 let mut db = Database::new("t".to_string());
2475 let result = process_command(
2476 "CREATE TABLE t (id INTEGER PRIMARY KEY, n INTEGER DEFAULT 'oops');",
2477 &mut db,
2478 );
2479 let err = result.expect_err("text default on INTEGER column should be rejected");
2480 let msg = format!("{err}").to_lowercase();
2481 assert!(msg.contains("default"), "got: {msg}");
2482 }
2483
2484 #[test]
2485 fn default_for_json_column_must_be_valid_json() {
2486 let mut db = Database::new("t".to_string());
2490 let err = process_command(
2491 "CREATE TABLE t (id INTEGER PRIMARY KEY, doc JSON DEFAULT 'not-json{');",
2492 &mut db,
2493 )
2494 .unwrap_err();
2495 assert!(
2496 format!("{err}").to_lowercase().contains("json"),
2497 "got: {err}"
2498 );
2499
2500 process_command(
2502 "CREATE TABLE t2 (id INTEGER PRIMARY KEY, doc JSON DEFAULT '{\"k\":1}');",
2503 &mut db,
2504 )
2505 .expect("valid JSON DEFAULT should be accepted");
2506 }
2507
2508 #[test]
2509 fn default_with_non_literal_expression_errors_at_create_time() {
2510 let mut db = Database::new("t".to_string());
2511 let result = process_command(
2514 "CREATE TABLE t (id INTEGER PRIMARY KEY, ts TEXT DEFAULT CURRENT_TIMESTAMP);",
2515 &mut db,
2516 );
2517 let err = result.expect_err("non-literal DEFAULT should be rejected");
2518 let msg = format!("{err}").to_lowercase();
2519 assert!(
2520 msg.contains("default") && msg.contains("literal"),
2521 "got: {msg}"
2522 );
2523 }
2524
2525 #[test]
2526 fn default_null_is_accepted_at_create_time() {
2527 let mut db = Database::new("t".to_string());
2531 process_command(
2532 "CREATE TABLE t (id INTEGER PRIMARY KEY, note TEXT DEFAULT NULL);",
2533 &mut db,
2534 )
2535 .expect("CREATE TABLE with DEFAULT NULL should be accepted");
2536 let table = db.get_table("t".to_string()).unwrap();
2537 let note = table
2538 .columns
2539 .iter()
2540 .find(|c| c.column_name == "note")
2541 .unwrap();
2542 assert_eq!(note.default, Some(Value::Null));
2543 }
2544
2545 #[test]
2550 fn drop_table_basic() {
2551 let mut db = seed_users_table();
2552 let response = process_command("DROP TABLE users;", &mut db).expect("drop table");
2553 assert!(response.contains("1 table dropped"));
2554 assert!(!db.contains_table("users".to_string()));
2555 }
2556
2557 #[test]
2558 fn drop_table_if_exists_noop_on_missing() {
2559 let mut db = Database::new("t".to_string());
2560 let response =
2561 process_command("DROP TABLE IF EXISTS missing;", &mut db).expect("drop if exists");
2562 assert!(response.contains("0 tables dropped"));
2563 }
2564
2565 #[test]
2566 fn drop_table_missing_errors_without_if_exists() {
2567 let mut db = Database::new("t".to_string());
2568 let err = process_command("DROP TABLE missing;", &mut db).unwrap_err();
2569 assert!(format!("{err}").contains("does not exist"), "got: {err}");
2570 }
2571
2572 #[test]
2573 fn drop_table_reserved_name_errors() {
2574 let mut db = Database::new("t".to_string());
2575 let err = process_command("DROP TABLE sqlrite_master;", &mut db).unwrap_err();
2576 assert!(format!("{err}").contains("reserved"), "got: {err}");
2577 }
2578
2579 #[test]
2580 fn drop_table_multi_target_rejected() {
2581 let mut db = seed_users_table();
2582 process_command("CREATE TABLE other (id INTEGER PRIMARY KEY);", &mut db).unwrap();
2583 let err = process_command("DROP TABLE users, other;", &mut db).unwrap_err();
2586 assert!(format!("{err}").contains("single table"), "got: {err}");
2587 }
2588
2589 #[test]
2590 fn drop_table_cascades_indexes_in_memory() {
2591 let mut db = seed_users_table();
2592 process_command("CREATE INDEX users_age_idx ON users (age);", &mut db).unwrap();
2593 let users = db.get_table("users".to_string()).unwrap();
2595 assert!(
2596 users
2597 .secondary_indexes
2598 .iter()
2599 .any(|i| i.name == "users_age_idx")
2600 );
2601
2602 process_command("DROP TABLE users;", &mut db).unwrap();
2603
2604 for table in db.tables.values() {
2606 assert!(
2607 !table
2608 .secondary_indexes
2609 .iter()
2610 .any(|i| i.name.contains("users")),
2611 "dropped table's indexes should not survive on any other table"
2612 );
2613 }
2614 }
2615
2616 #[test]
2617 fn drop_index_explicit_basic() {
2618 let mut db = seed_users_table();
2619 process_command("CREATE INDEX users_age_idx ON users (age);", &mut db).unwrap();
2620 let response = process_command("DROP INDEX users_age_idx;", &mut db).expect("drop index");
2621 assert!(response.contains("1 index dropped"));
2622
2623 let users = db.get_table("users".to_string()).unwrap();
2624 assert!(users.index_by_name("users_age_idx").is_none());
2625 }
2626
2627 #[test]
2628 fn drop_index_refuses_auto_index() {
2629 let mut db = seed_users_table();
2630 let err = process_command("DROP INDEX sqlrite_autoindex_users_id;", &mut db).unwrap_err();
2633 assert!(format!("{err}").contains("auto-created"), "got: {err}");
2634 }
2635
2636 #[test]
2637 fn drop_index_if_exists_noop_on_missing() {
2638 let mut db = Database::new("t".to_string());
2639 let response =
2640 process_command("DROP INDEX IF EXISTS nope;", &mut db).expect("drop index if exists");
2641 assert!(response.contains("0 indexes dropped"));
2642 }
2643
2644 #[test]
2645 fn drop_index_missing_errors_without_if_exists() {
2646 let mut db = Database::new("t".to_string());
2647 let err = process_command("DROP INDEX nope;", &mut db).unwrap_err();
2648 assert!(format!("{err}").contains("does not exist"), "got: {err}");
2649 }
2650
2651 #[test]
2652 fn drop_statements_rejected_on_readonly_db() {
2653 use crate::sql::pager::{open_database_read_only, save_database};
2654
2655 let mut seed = Database::new("t".to_string());
2656 process_command(
2657 "CREATE TABLE notes (id INTEGER PRIMARY KEY, body TEXT);",
2658 &mut seed,
2659 )
2660 .unwrap();
2661 process_command("CREATE INDEX notes_body ON notes (body);", &mut seed).unwrap();
2662 let path = {
2663 let mut p = std::env::temp_dir();
2664 let pid = std::process::id();
2665 let nanos = std::time::SystemTime::now()
2666 .duration_since(std::time::UNIX_EPOCH)
2667 .map(|d| d.as_nanos())
2668 .unwrap_or(0);
2669 p.push(format!("sqlrite-drop-ro-{pid}-{nanos}.sqlrite"));
2670 p
2671 };
2672 save_database(&mut seed, &path).unwrap();
2673 drop(seed);
2674
2675 let mut ro = open_database_read_only(&path, "t".to_string()).unwrap();
2676 for stmt in ["DROP TABLE notes;", "DROP INDEX notes_body;"] {
2677 let err = process_command(stmt, &mut ro).unwrap_err();
2678 assert!(
2679 format!("{err}").contains("read-only"),
2680 "{stmt:?} should surface read-only error, got: {err}"
2681 );
2682 }
2683
2684 let _ = std::fs::remove_file(&path);
2685 let mut wal = path.as_os_str().to_owned();
2686 wal.push("-wal");
2687 let _ = std::fs::remove_file(std::path::PathBuf::from(wal));
2688 }
2689
2690 #[test]
2695 fn alter_rename_table_basic() {
2696 let mut db = seed_users_table();
2697 process_command("ALTER TABLE users RENAME TO members;", &mut db).expect("rename table");
2698 assert!(!db.contains_table("users".to_string()));
2699 assert!(db.contains_table("members".to_string()));
2700 let response = process_command("SELECT * FROM members;", &mut db).expect("select");
2702 assert!(response.contains("3 rows returned"));
2703 }
2704
2705 #[test]
2706 fn alter_rename_table_renames_auto_indexes() {
2707 let mut db = Database::new("t".to_string());
2710 process_command(
2711 "CREATE TABLE accounts (id INTEGER PRIMARY KEY, email TEXT UNIQUE);",
2712 &mut db,
2713 )
2714 .unwrap();
2715 {
2716 let accounts = db.get_table("accounts".to_string()).unwrap();
2717 assert!(
2718 accounts
2719 .index_by_name("sqlrite_autoindex_accounts_id")
2720 .is_some()
2721 );
2722 assert!(
2723 accounts
2724 .index_by_name("sqlrite_autoindex_accounts_email")
2725 .is_some()
2726 );
2727 }
2728 process_command("ALTER TABLE accounts RENAME TO members;", &mut db).expect("rename");
2729 let members = db.get_table("members".to_string()).unwrap();
2730 assert!(
2731 members
2732 .index_by_name("sqlrite_autoindex_members_id")
2733 .is_some(),
2734 "PK auto-index should be renamed to match new table"
2735 );
2736 assert!(
2737 members
2738 .index_by_name("sqlrite_autoindex_members_email")
2739 .is_some()
2740 );
2741 assert!(
2743 members
2744 .index_by_name("sqlrite_autoindex_accounts_id")
2745 .is_none()
2746 );
2747 for idx in &members.secondary_indexes {
2749 assert_eq!(idx.table_name, "members");
2750 }
2751 }
2752
2753 #[test]
2754 fn alter_rename_table_to_existing_errors() {
2755 let mut db = seed_users_table();
2756 process_command("CREATE TABLE other (id INTEGER PRIMARY KEY);", &mut db).unwrap();
2757 let err = process_command("ALTER TABLE users RENAME TO other;", &mut db).unwrap_err();
2758 assert!(format!("{err}").contains("already exists"), "got: {err}");
2759 assert!(db.contains_table("users".to_string()));
2761 assert!(db.contains_table("other".to_string()));
2762 }
2763
2764 #[test]
2765 fn alter_rename_table_to_reserved_name_errors() {
2766 let mut db = seed_users_table();
2767 let err =
2768 process_command("ALTER TABLE users RENAME TO sqlrite_master;", &mut db).unwrap_err();
2769 assert!(format!("{err}").contains("reserved"), "got: {err}");
2770 }
2771
2772 #[test]
2773 fn alter_rename_column_basic() {
2774 let mut db = seed_users_table();
2775 process_command(
2776 "ALTER TABLE users RENAME COLUMN name TO full_name;",
2777 &mut db,
2778 )
2779 .expect("rename column");
2780
2781 let users = db.get_table("users".to_string()).unwrap();
2782 assert!(users.contains_column("full_name".to_string()));
2783 assert!(!users.contains_column("name".to_string()));
2784
2785 let bob_rowid = users
2788 .rowids()
2789 .into_iter()
2790 .find(|r| users.get_value("full_name", *r) == Some(Value::Text("bob".to_string())))
2791 .expect("bob row should be findable under the new column name");
2792 assert_eq!(
2793 users.get_value("full_name", bob_rowid),
2794 Some(Value::Text("bob".to_string()))
2795 );
2796 }
2797
2798 #[test]
2799 fn alter_rename_column_collision_errors() {
2800 let mut db = seed_users_table();
2801 let err =
2802 process_command("ALTER TABLE users RENAME COLUMN name TO age;", &mut db).unwrap_err();
2803 assert!(format!("{err}").contains("already exists"), "got: {err}");
2804 }
2805
2806 #[test]
2807 fn alter_rename_column_updates_indexes() {
2808 let mut db = Database::new("t".to_string());
2810 process_command(
2811 "CREATE TABLE accounts (id INTEGER PRIMARY KEY, email TEXT UNIQUE);",
2812 &mut db,
2813 )
2814 .unwrap();
2815 process_command(
2816 "ALTER TABLE accounts RENAME COLUMN email TO contact;",
2817 &mut db,
2818 )
2819 .unwrap();
2820 let accounts = db.get_table("accounts".to_string()).unwrap();
2821 assert!(
2822 accounts
2823 .index_by_name("sqlrite_autoindex_accounts_contact")
2824 .is_some()
2825 );
2826 assert!(
2827 accounts
2828 .index_by_name("sqlrite_autoindex_accounts_email")
2829 .is_none()
2830 );
2831 }
2832
2833 #[test]
2834 fn alter_add_column_basic() {
2835 let mut db = seed_users_table();
2836 process_command("ALTER TABLE users ADD COLUMN nickname TEXT;", &mut db)
2837 .expect("add column");
2838 let users = db.get_table("users".to_string()).unwrap();
2839 assert!(users.contains_column("nickname".to_string()));
2840 let any_rowid = *users.rowids().first().expect("seed has rows");
2842 assert_eq!(users.get_value("nickname", any_rowid), None);
2843
2844 process_command(
2846 "INSERT INTO users (name, age, nickname) VALUES ('dan', 22, 'd');",
2847 &mut db,
2848 )
2849 .expect("insert with new col");
2850 let users = db.get_table("users".to_string()).unwrap();
2851 let dan_rowid = users
2852 .rowids()
2853 .into_iter()
2854 .find(|r| users.get_value("name", *r) == Some(Value::Text("dan".to_string())))
2855 .unwrap();
2856 assert_eq!(
2857 users.get_value("nickname", dan_rowid),
2858 Some(Value::Text("d".to_string()))
2859 );
2860 }
2861
2862 #[test]
2863 fn alter_add_column_with_default_backfills_existing_rows() {
2864 let mut db = seed_users_table();
2865 process_command(
2866 "ALTER TABLE users ADD COLUMN status TEXT DEFAULT 'active';",
2867 &mut db,
2868 )
2869 .expect("add column with default");
2870 let users = db.get_table("users".to_string()).unwrap();
2871 for rowid in users.rowids() {
2872 assert_eq!(
2873 users.get_value("status", rowid),
2874 Some(Value::Text("active".to_string())),
2875 "rowid {rowid} should have been backfilled with the default"
2876 );
2877 }
2878 }
2879
2880 #[test]
2881 fn alter_add_column_not_null_with_default_works_on_nonempty_table() {
2882 let mut db = seed_users_table();
2883 process_command(
2884 "ALTER TABLE users ADD COLUMN score INTEGER NOT NULL DEFAULT 0;",
2885 &mut db,
2886 )
2887 .expect("NOT NULL ADD with DEFAULT should succeed even with existing rows");
2888 let users = db.get_table("users".to_string()).unwrap();
2889 for rowid in users.rowids() {
2890 assert_eq!(users.get_value("score", rowid), Some(Value::Integer(0)));
2891 }
2892 }
2893
2894 #[test]
2895 fn alter_add_column_not_null_without_default_errors_on_nonempty_table() {
2896 let mut db = seed_users_table();
2897 let err = process_command(
2898 "ALTER TABLE users ADD COLUMN score INTEGER NOT NULL;",
2899 &mut db,
2900 )
2901 .unwrap_err();
2902 let msg = format!("{err}").to_lowercase();
2903 assert!(
2904 msg.contains("not null") && msg.contains("default"),
2905 "got: {msg}"
2906 );
2907 }
2908
2909 #[test]
2910 fn alter_add_column_pk_rejected() {
2911 let mut db = seed_users_table();
2912 let err = process_command(
2913 "ALTER TABLE users ADD COLUMN extra INTEGER PRIMARY KEY;",
2914 &mut db,
2915 )
2916 .unwrap_err();
2917 assert!(
2918 format!("{err}").to_lowercase().contains("primary key"),
2919 "got: {err}"
2920 );
2921 }
2922
2923 #[test]
2924 fn alter_add_column_unique_rejected() {
2925 let mut db = seed_users_table();
2926 let err = process_command(
2927 "ALTER TABLE users ADD COLUMN extra INTEGER UNIQUE;",
2928 &mut db,
2929 )
2930 .unwrap_err();
2931 assert!(
2932 format!("{err}").to_lowercase().contains("unique"),
2933 "got: {err}"
2934 );
2935 }
2936
2937 #[test]
2938 fn alter_add_column_existing_name_errors() {
2939 let mut db = seed_users_table();
2940 let err =
2941 process_command("ALTER TABLE users ADD COLUMN age INTEGER;", &mut db).unwrap_err();
2942 assert!(format!("{err}").contains("already exists"), "got: {err}");
2943 }
2944
2945 #[test]
2951 fn alter_drop_column_basic() {
2952 let mut db = seed_users_table();
2953 process_command("ALTER TABLE users DROP COLUMN age;", &mut db).expect("drop column");
2954 let users = db.get_table("users".to_string()).unwrap();
2955 assert!(!users.contains_column("age".to_string()));
2956 assert!(users.contains_column("name".to_string()));
2958 assert_eq!(users.rowids().len(), 3);
2959 }
2960
2961 #[test]
2962 fn alter_drop_column_drops_dependent_indexes() {
2963 let mut db = seed_users_table();
2964 process_command("CREATE INDEX users_age_idx ON users (age);", &mut db).unwrap();
2965 process_command("ALTER TABLE users DROP COLUMN age;", &mut db).unwrap();
2966 let users = db.get_table("users".to_string()).unwrap();
2967 assert!(users.index_by_name("users_age_idx").is_none());
2968 }
2969
2970 #[test]
2971 fn alter_drop_column_pk_errors() {
2972 let mut db = seed_users_table();
2973 let err = process_command("ALTER TABLE users DROP COLUMN id;", &mut db).unwrap_err();
2974 assert!(
2975 format!("{err}").to_lowercase().contains("primary key"),
2976 "got: {err}"
2977 );
2978 }
2979
2980 #[test]
2981 fn alter_drop_column_only_column_errors() {
2982 let mut db = Database::new("t".to_string());
2983 process_command("CREATE TABLE solo (only_col TEXT);", &mut db).unwrap();
2984 let err = process_command("ALTER TABLE solo DROP COLUMN only_col;", &mut db).unwrap_err();
2985 assert!(
2986 format!("{err}").to_lowercase().contains("only column"),
2987 "got: {err}"
2988 );
2989 }
2990
2991 #[test]
2992 fn alter_unknown_table_errors_without_if_exists() {
2993 let mut db = Database::new("t".to_string());
2994 let err = process_command("ALTER TABLE missing RENAME TO other;", &mut db).unwrap_err();
2995 assert!(format!("{err}").contains("does not exist"), "got: {err}");
2996 }
2997
2998 #[test]
2999 fn alter_unknown_table_if_exists_noop() {
3000 let mut db = Database::new("t".to_string());
3001 let response = process_command("ALTER TABLE IF EXISTS missing RENAME TO other;", &mut db)
3002 .expect("IF EXISTS makes missing-table ALTER a no-op");
3003 assert!(response.contains("no-op"));
3004 }
3005
3006 #[test]
3007 fn drop_table_inside_transaction_rolls_back() {
3008 let mut db = seed_users_table();
3012 process_command("CREATE INDEX users_age_idx ON users (age);", &mut db).unwrap();
3013 process_command("BEGIN;", &mut db).unwrap();
3014 process_command("DROP TABLE users;", &mut db).unwrap();
3015 assert!(!db.contains_table("users".to_string()));
3016 process_command("ROLLBACK;", &mut db).unwrap();
3017 assert!(db.contains_table("users".to_string()));
3018 let users = db.get_table("users".to_string()).unwrap();
3019 assert_eq!(users.rowids().len(), 3);
3020 assert!(users.index_by_name("users_age_idx").is_some());
3021 }
3022
3023 #[test]
3024 fn alter_inside_transaction_rolls_back() {
3025 let mut db = seed_users_table();
3026 process_command("BEGIN;", &mut db).unwrap();
3027 process_command(
3028 "ALTER TABLE users ADD COLUMN status TEXT DEFAULT 'active';",
3029 &mut db,
3030 )
3031 .unwrap();
3032 assert!(
3034 db.get_table("users".to_string())
3035 .unwrap()
3036 .contains_column("status".to_string())
3037 );
3038 process_command("ROLLBACK;", &mut db).unwrap();
3039 assert!(
3041 !db.get_table("users".to_string())
3042 .unwrap()
3043 .contains_column("status".to_string())
3044 );
3045 }
3046
3047 #[test]
3048 fn alter_rejected_on_readonly_db() {
3049 use crate::sql::pager::{open_database_read_only, save_database};
3050
3051 let mut seed = Database::new("t".to_string());
3052 process_command(
3053 "CREATE TABLE notes (id INTEGER PRIMARY KEY, body TEXT);",
3054 &mut seed,
3055 )
3056 .unwrap();
3057 let path = {
3058 let mut p = std::env::temp_dir();
3059 let pid = std::process::id();
3060 let nanos = std::time::SystemTime::now()
3061 .duration_since(std::time::UNIX_EPOCH)
3062 .map(|d| d.as_nanos())
3063 .unwrap_or(0);
3064 p.push(format!("sqlrite-alter-ro-{pid}-{nanos}.sqlrite"));
3065 p
3066 };
3067 save_database(&mut seed, &path).unwrap();
3068 drop(seed);
3069
3070 let mut ro = open_database_read_only(&path, "t".to_string()).unwrap();
3071 for stmt in [
3072 "ALTER TABLE notes RENAME TO n2;",
3073 "ALTER TABLE notes RENAME COLUMN body TO b;",
3074 "ALTER TABLE notes ADD COLUMN extra TEXT;",
3075 "ALTER TABLE notes DROP COLUMN body;",
3076 ] {
3077 let err = process_command(stmt, &mut ro).unwrap_err();
3078 assert!(
3079 format!("{err}").contains("read-only"),
3080 "{stmt:?} should surface read-only error, got: {err}"
3081 );
3082 }
3083
3084 let _ = std::fs::remove_file(&path);
3085 let mut wal = path.as_os_str().to_owned();
3086 wal.push("-wal");
3087 let _ = std::fs::remove_file(std::path::PathBuf::from(wal));
3088 }
3089}