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