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