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 hnsw_delete_then_insert_rebuilds_in_same_connection() {
1813 let mut db = Database::new(String::from("test_db"));
1814 process_command(
1815 "CREATE TABLE chunks (id INTEGER PRIMARY KEY, document_id INTEGER, embedding VECTOR(4));",
1816 &mut db,
1817 )
1818 .unwrap();
1819 process_command(
1820 "CREATE INDEX idx_emb ON chunks USING hnsw (embedding);",
1821 &mut db,
1822 )
1823 .unwrap();
1824 process_command(
1825 "INSERT INTO chunks (document_id, embedding) VALUES (1, [1, 0, 0, 0]);",
1826 &mut db,
1827 )
1828 .unwrap();
1829 process_command(
1830 "INSERT INTO chunks (document_id, embedding) VALUES (1, [0, 1, 0, 0]);",
1831 &mut db,
1832 )
1833 .unwrap();
1834 process_command("DELETE FROM chunks WHERE document_id = 1;", &mut db).unwrap();
1835
1836 process_command(
1837 "INSERT INTO chunks (document_id, embedding) VALUES (2, [0, 0, 1, 0]);",
1838 &mut db,
1839 )
1840 .unwrap();
1841 let chunks = db.get_table("chunks".to_string()).unwrap();
1842 let entry = chunks
1843 .hnsw_indexes
1844 .iter()
1845 .find(|e| e.name == "idx_emb")
1846 .unwrap();
1847 assert!(
1848 !entry.needs_rebuild,
1849 "INSERT should rebuild the dirty index"
1850 );
1851 assert_eq!(entry.index.len(), 1);
1852
1853 let out = process_command_with_render(
1854 "SELECT document_id FROM chunks ORDER BY vec_distance_l2(embedding, [0, 0, 1, 0]) ASC LIMIT 1;",
1855 &mut db,
1856 )
1857 .unwrap();
1858 assert!(out.status.contains("1 row returned"), "got: {}", out.status);
1859 let rendered = out.rendered.expect("SELECT should render rows");
1860 assert!(
1861 rendered.contains("| 2 |"),
1862 "expected the post-delete inserted row: {rendered}"
1863 );
1864 }
1865
1866 #[test]
1867 fn update_on_hnsw_indexed_vector_col_succeeds_and_marks_dirty() {
1868 let mut db = seed_hnsw_table();
1869 process_command("CREATE INDEX ix_e ON docs USING hnsw (e);", &mut db).unwrap();
1870 let resp =
1871 process_command("UPDATE docs SET e = [9.0, 9.0] WHERE id = 1;", &mut db).unwrap();
1872 assert!(resp.contains("1 row"), "expected 1 row updated: {resp}");
1873
1874 let docs = db.get_table("docs".to_string()).unwrap();
1875 let entry = docs.hnsw_indexes.iter().find(|e| e.name == "ix_e").unwrap();
1876 assert!(
1877 entry.needs_rebuild,
1878 "UPDATE on the vector column should have marked HNSW index dirty"
1879 );
1880 }
1881
1882 #[test]
1883 fn duplicate_index_name_errors() {
1884 let mut db = seed_hnsw_table();
1885 process_command("CREATE INDEX ix_e ON docs USING hnsw (e);", &mut db).unwrap();
1886 let err =
1887 process_command("CREATE INDEX ix_e ON docs USING hnsw (e);", &mut db).unwrap_err();
1888 let msg = format!("{err}");
1889 assert!(
1890 msg.to_lowercase().contains("already exists"),
1891 "expected duplicate-index error; got: {msg}"
1892 );
1893 }
1894
1895 #[test]
1896 fn index_if_not_exists_is_idempotent() {
1897 let mut db = seed_hnsw_table();
1898 process_command("CREATE INDEX ix_e ON docs USING hnsw (e);", &mut db).unwrap();
1899 process_command(
1901 "CREATE INDEX IF NOT EXISTS ix_e ON docs USING hnsw (e);",
1902 &mut db,
1903 )
1904 .unwrap();
1905 let table = db.get_table("docs".to_string()).unwrap();
1906 assert_eq!(table.hnsw_indexes.len(), 1);
1907 }
1908
1909 fn seed_fts_table() -> Database {
1916 let mut db = Database::new("tempdb".to_string());
1917 process_command(
1918 "CREATE TABLE docs (id INTEGER PRIMARY KEY, body TEXT);",
1919 &mut db,
1920 )
1921 .unwrap();
1922 for body in &[
1923 "rust embedded database", "rust web framework", "go embedded systems", "python web framework", "rust rust rust embedded power", ] {
1929 process_command(
1930 &format!("INSERT INTO docs (body) VALUES ('{body}');"),
1931 &mut db,
1932 )
1933 .unwrap();
1934 }
1935 db
1936 }
1937
1938 #[test]
1939 fn create_index_using_fts_succeeds_and_indexes_existing_rows() {
1940 let mut db = seed_fts_table();
1941 let resp =
1942 process_command("CREATE INDEX ix_body ON docs USING fts (body);", &mut db).unwrap();
1943 assert!(resp.to_lowercase().contains("create index"), "got {resp}");
1944 let table = db.get_table("docs".to_string()).unwrap();
1945 assert_eq!(table.fts_indexes.len(), 1);
1946 assert_eq!(table.fts_indexes[0].name, "ix_body");
1947 assert_eq!(table.fts_indexes[0].column_name, "body");
1948 assert_eq!(table.fts_indexes[0].index.len(), 5);
1950 }
1951
1952 #[test]
1953 fn create_index_using_fts_rejects_non_text_column() {
1954 let mut db = Database::new("tempdb".to_string());
1955 process_command(
1956 "CREATE TABLE t (id INTEGER PRIMARY KEY, n INTEGER);",
1957 &mut db,
1958 )
1959 .unwrap();
1960 let err = process_command("CREATE INDEX ix_n ON t USING fts (n);", &mut db).unwrap_err();
1961 let msg = format!("{err}");
1962 assert!(
1963 msg.to_lowercase().contains("text"),
1964 "expected error mentioning TEXT; got: {msg}"
1965 );
1966 }
1967
1968 #[test]
1969 fn fts_match_returns_expected_rows() {
1970 let mut db = seed_fts_table();
1971 process_command("CREATE INDEX ix_body ON docs USING fts (body);", &mut db).unwrap();
1972 let resp = process_command(
1974 "SELECT id FROM docs WHERE fts_match(body, 'rust');",
1975 &mut db,
1976 )
1977 .unwrap();
1978 assert!(resp.contains("3 rows returned"), "got: {resp}");
1979 }
1980
1981 #[test]
1982 fn fts_match_without_index_errors_clearly() {
1983 let mut db = seed_fts_table();
1984 let err = process_command(
1986 "SELECT id FROM docs WHERE fts_match(body, 'rust');",
1987 &mut db,
1988 )
1989 .unwrap_err();
1990 let msg = format!("{err}");
1991 assert!(
1992 msg.contains("no FTS index"),
1993 "expected no-index error; got: {msg}"
1994 );
1995 }
1996
1997 #[test]
1998 fn bm25_score_orders_descending_by_relevance() {
1999 let mut db = seed_fts_table();
2000 process_command("CREATE INDEX ix_body ON docs USING fts (body);", &mut db).unwrap();
2001 let out = process_command_with_render(
2004 "SELECT id FROM docs WHERE fts_match(body, 'rust') \
2005 ORDER BY bm25_score(body, 'rust') DESC LIMIT 1;",
2006 &mut db,
2007 )
2008 .unwrap();
2009 assert!(out.status.contains("1 row returned"), "got: {}", out.status);
2010 let rendered = out.rendered.expect("SELECT should produce rendered output");
2011 assert!(
2013 rendered.contains(" 5 "),
2014 "expected id=5 to be top-ranked; rendered:\n{rendered}"
2015 );
2016 }
2017
2018 #[test]
2019 fn bm25_score_without_index_errors_clearly() {
2020 let mut db = seed_fts_table();
2021 let err = process_command(
2022 "SELECT id FROM docs ORDER BY bm25_score(body, 'rust') DESC LIMIT 1;",
2023 &mut db,
2024 )
2025 .unwrap_err();
2026 let msg = format!("{err}");
2027 assert!(
2028 msg.contains("no FTS index"),
2029 "expected no-index error; got: {msg}"
2030 );
2031 }
2032
2033 #[test]
2034 fn fts_post_create_inserts_are_indexed_incrementally() {
2035 let mut db = seed_fts_table();
2036 process_command("CREATE INDEX ix_body ON docs USING fts (body);", &mut db).unwrap();
2037 process_command(
2038 "INSERT INTO docs (body) VALUES ('rust embedded analytics');",
2039 &mut db,
2040 )
2041 .unwrap();
2042 let table = db.get_table("docs".to_string()).unwrap();
2043 assert_eq!(table.fts_indexes[0].index.len(), 6);
2045 let resp = process_command(
2047 "SELECT id FROM docs WHERE fts_match(body, 'analytics');",
2048 &mut db,
2049 )
2050 .unwrap();
2051 assert!(resp.contains("1 row returned"), "got: {resp}");
2052 }
2053
2054 #[test]
2055 fn delete_on_fts_indexed_table_marks_dirty() {
2056 let mut db = seed_fts_table();
2057 process_command("CREATE INDEX ix_body ON docs USING fts (body);", &mut db).unwrap();
2058 let resp = process_command("DELETE FROM docs WHERE id = 1;", &mut db).unwrap();
2059 assert!(resp.contains("1 row"), "got: {resp}");
2060 let docs = db.get_table("docs".to_string()).unwrap();
2061 let entry = docs
2062 .fts_indexes
2063 .iter()
2064 .find(|e| e.name == "ix_body")
2065 .unwrap();
2066 assert!(
2067 entry.needs_rebuild,
2068 "DELETE should have flagged the FTS index dirty"
2069 );
2070 }
2071
2072 #[test]
2073 fn update_on_fts_indexed_text_col_marks_dirty() {
2074 let mut db = seed_fts_table();
2075 process_command("CREATE INDEX ix_body ON docs USING fts (body);", &mut db).unwrap();
2076 let resp = process_command(
2077 "UPDATE docs SET body = 'java spring framework' WHERE id = 1;",
2078 &mut db,
2079 )
2080 .unwrap();
2081 assert!(resp.contains("1 row"), "got: {resp}");
2082 let docs = db.get_table("docs".to_string()).unwrap();
2083 let entry = docs
2084 .fts_indexes
2085 .iter()
2086 .find(|e| e.name == "ix_body")
2087 .unwrap();
2088 assert!(
2089 entry.needs_rebuild,
2090 "UPDATE on the indexed TEXT column should have flagged dirty"
2091 );
2092 }
2093
2094 #[test]
2095 fn fts_index_name_collides_with_btree_and_hnsw_namespaces() {
2096 let mut db = seed_fts_table();
2097 process_command("CREATE INDEX ix_body ON docs USING fts (body);", &mut db).unwrap();
2098 let err = process_command("CREATE INDEX ix_body ON docs (body);", &mut db).unwrap_err();
2099 let msg = format!("{err}");
2100 assert!(
2101 msg.to_lowercase().contains("already exists"),
2102 "expected duplicate-index error; got: {msg}"
2103 );
2104 }
2105
2106 #[test]
2107 fn fts_index_rejects_unique() {
2108 let mut db = seed_fts_table();
2109 let err = process_command(
2110 "CREATE UNIQUE INDEX ix_body ON docs USING fts (body);",
2111 &mut db,
2112 )
2113 .unwrap_err();
2114 let msg = format!("{err}");
2115 assert!(
2116 msg.to_lowercase().contains("unique"),
2117 "expected UNIQUE-rejection error; got: {msg}"
2118 );
2119 }
2120
2121 #[test]
2122 fn try_fts_probe_falls_through_on_ascending() {
2123 let mut db = seed_fts_table();
2128 process_command("CREATE INDEX ix_body ON docs USING fts (body);", &mut db).unwrap();
2129 let resp = process_command(
2132 "SELECT id FROM docs WHERE fts_match(body, 'rust') \
2133 ORDER BY bm25_score(body, 'rust') ASC LIMIT 3;",
2134 &mut db,
2135 )
2136 .unwrap();
2137 assert!(resp.contains("3 rows returned"), "got: {resp}");
2138 }
2139
2140 fn seed_vector_docs() -> Database {
2150 let mut db = Database::new("tempdb".to_string());
2151 process_command(
2152 "CREATE TABLE docs (id INTEGER PRIMARY KEY, e VECTOR(2));",
2153 &mut db,
2154 )
2155 .expect("create");
2156 process_command("INSERT INTO docs (e) VALUES ([1.0, 0.0]);", &mut db).expect("insert 1");
2157 process_command("INSERT INTO docs (e) VALUES ([0.0, 1.0]);", &mut db).expect("insert 2");
2158 process_command("INSERT INTO docs (e) VALUES ([1.0, 1.0]);", &mut db).expect("insert 3");
2159 db
2160 }
2161
2162 #[test]
2163 fn vec_distance_l2_in_where_filters_correctly() {
2164 let mut db = seed_vector_docs();
2170 let resp = process_command(
2171 "SELECT * FROM docs WHERE vec_distance_l2(e, [1.0, 0.0]) < 1.1;",
2172 &mut db,
2173 )
2174 .expect("select");
2175 assert!(
2176 resp.contains("2 rows returned"),
2177 "expected 2 rows, got: {resp}"
2178 );
2179 }
2180
2181 #[test]
2182 fn vec_distance_cosine_in_where() {
2183 let mut db = seed_vector_docs();
2188 let resp = process_command(
2189 "SELECT * FROM docs WHERE vec_distance_cosine(e, [1.0, 0.0]) < 0.5;",
2190 &mut db,
2191 )
2192 .expect("select");
2193 assert!(
2194 resp.contains("2 rows returned"),
2195 "expected 2 rows, got: {resp}"
2196 );
2197 }
2198
2199 #[test]
2200 fn vec_distance_dot_negated() {
2201 let mut db = seed_vector_docs();
2206 let resp = process_command(
2207 "SELECT * FROM docs WHERE vec_distance_dot(e, [1.0, 0.0]) < 0.0;",
2208 &mut db,
2209 )
2210 .expect("select");
2211 assert!(
2212 resp.contains("2 rows returned"),
2213 "expected 2 rows, got: {resp}"
2214 );
2215 }
2216
2217 #[test]
2218 fn knn_via_order_by_distance_limit() {
2219 let mut db = seed_vector_docs();
2223 let resp = process_command(
2224 "SELECT id FROM docs ORDER BY vec_distance_l2(e, [1.0, 0.0]) ASC LIMIT 2;",
2225 &mut db,
2226 )
2227 .expect("select");
2228 assert!(
2229 resp.contains("2 rows returned"),
2230 "expected 2 rows, got: {resp}"
2231 );
2232 }
2233
2234 #[test]
2235 fn distance_function_dim_mismatch_errors() {
2236 let mut db = seed_vector_docs();
2238 let err = process_command(
2239 "SELECT * FROM docs WHERE vec_distance_l2(e, [1.0, 0.0, 0.0]) < 1.0;",
2240 &mut db,
2241 )
2242 .unwrap_err();
2243 let msg = format!("{err}");
2244 assert!(
2245 msg.to_lowercase().contains("dimension")
2246 && msg.contains("lhs=2")
2247 && msg.contains("rhs=3"),
2248 "expected dim mismatch error, got: {msg}"
2249 );
2250 }
2251
2252 #[test]
2253 fn unknown_function_errors_with_name() {
2254 let mut db = seed_vector_docs();
2258 let err = process_command(
2259 "SELECT * FROM docs WHERE vec_does_not_exist(e, [1.0, 0.0]) < 1.0;",
2260 &mut db,
2261 )
2262 .unwrap_err();
2263 let msg = format!("{err}");
2264 assert!(
2265 msg.contains("vec_does_not_exist"),
2266 "expected error mentioning function name, got: {msg}"
2267 );
2268 }
2269
2270 fn seed_json_table() -> Database {
2275 let mut db = Database::new("tempdb".to_string());
2276 process_command(
2277 "CREATE TABLE docs (id INTEGER PRIMARY KEY, payload JSON);",
2278 &mut db,
2279 )
2280 .expect("create json table");
2281 db
2282 }
2283
2284 #[test]
2285 fn json_column_round_trip_primitive_values() {
2286 let mut db = seed_json_table();
2287 process_command(
2288 r#"INSERT INTO docs (payload) VALUES ('{"name": "alice", "age": 30}');"#,
2289 &mut db,
2290 )
2291 .expect("insert json");
2292 let docs = db.get_table("docs".to_string()).unwrap();
2293 let rowids = docs.rowids();
2294 assert_eq!(rowids.len(), 1);
2295 match docs.get_value("payload", rowids[0]) {
2297 Some(Value::Text(s)) => {
2298 assert!(s.contains("alice"), "expected JSON text to round-trip: {s}");
2299 }
2300 other => panic!("expected Value::Text holding JSON, got {other:?}"),
2301 }
2302 }
2303
2304 #[test]
2305 fn json_insert_rejects_invalid_json() {
2306 let mut db = seed_json_table();
2307 let err = process_command(
2308 "INSERT INTO docs (payload) VALUES ('not-valid-json{');",
2309 &mut db,
2310 )
2311 .unwrap_err();
2312 let msg = format!("{err}").to_lowercase();
2313 assert!(
2314 msg.contains("json") && msg.contains("payload"),
2315 "expected JSON validation error mentioning column, got: {msg}"
2316 );
2317 }
2318
2319 #[test]
2320 fn json_extract_object_field() {
2321 let mut db = seed_json_table();
2322 process_command(
2323 r#"INSERT INTO docs (payload) VALUES ('{"name": "alice", "age": 30}');"#,
2324 &mut db,
2325 )
2326 .unwrap();
2327 let resp = process_command(
2330 r#"SELECT id FROM docs WHERE json_extract(payload, '$.name') = 'alice';"#,
2331 &mut db,
2332 )
2333 .expect("select via json_extract");
2334 assert!(resp.contains("1 row returned"), "got: {resp}");
2335
2336 let resp = process_command(
2337 r#"SELECT id FROM docs WHERE json_extract(payload, '$.age') = 30;"#,
2338 &mut db,
2339 )
2340 .expect("select via numeric json_extract");
2341 assert!(resp.contains("1 row returned"), "got: {resp}");
2342 }
2343
2344 #[test]
2345 fn json_extract_array_index_and_nested() {
2346 let mut db = seed_json_table();
2347 process_command(
2348 r#"INSERT INTO docs (payload) VALUES ('{"tags": ["rust", "sql", "vectors"], "meta": {"author": "joao"}}');"#,
2349 &mut db,
2350 )
2351 .unwrap();
2352 let resp = process_command(
2353 r#"SELECT id FROM docs WHERE json_extract(payload, '$.tags[0]') = 'rust';"#,
2354 &mut db,
2355 )
2356 .expect("select via array index");
2357 assert!(resp.contains("1 row returned"), "got: {resp}");
2358
2359 let resp = process_command(
2360 r#"SELECT id FROM docs WHERE json_extract(payload, '$.meta.author') = 'joao';"#,
2361 &mut db,
2362 )
2363 .expect("select via nested object");
2364 assert!(resp.contains("1 row returned"), "got: {resp}");
2365 }
2366
2367 #[test]
2368 fn json_extract_missing_path_returns_null() {
2369 let mut db = seed_json_table();
2370 process_command(
2371 r#"INSERT INTO docs (payload) VALUES ('{"name": "alice"}');"#,
2372 &mut db,
2373 )
2374 .unwrap();
2375 let resp = process_command(
2378 r#"SELECT id FROM docs WHERE json_extract(payload, '$.missing') = 'something';"#,
2379 &mut db,
2380 )
2381 .expect("select with missing path");
2382 assert!(resp.contains("0 rows returned"), "got: {resp}");
2383 }
2384
2385 #[test]
2386 fn json_extract_malformed_path_errors() {
2387 let mut db = seed_json_table();
2388 process_command(
2389 r#"INSERT INTO docs (payload) VALUES ('{"a": 1}');"#,
2390 &mut db,
2391 )
2392 .unwrap();
2393 let err = process_command(
2395 r#"SELECT id FROM docs WHERE json_extract(payload, 'a.b') = 1;"#,
2396 &mut db,
2397 )
2398 .unwrap_err();
2399 assert!(format!("{err}").contains("'$'"));
2400 }
2401
2402 #[test]
2403 fn json_array_length_on_array() {
2404 let mut db = seed_json_table();
2407 process_command(
2408 r#"INSERT INTO docs (payload) VALUES ('{"tags": ["a", "b", "c"]}');"#,
2409 &mut db,
2410 )
2411 .unwrap();
2412 let resp = process_command(
2413 r#"SELECT id FROM docs WHERE json_array_length(payload, '$.tags') = 3;"#,
2414 &mut db,
2415 )
2416 .expect("select via array_length");
2417 assert!(resp.contains("1 row returned"), "got: {resp}");
2418 }
2419
2420 #[test]
2421 fn json_array_length_on_non_array_errors() {
2422 let mut db = seed_json_table();
2423 process_command(
2424 r#"INSERT INTO docs (payload) VALUES ('{"tags": "not-an-array"}');"#,
2425 &mut db,
2426 )
2427 .unwrap();
2428 let err = process_command(
2429 r#"SELECT id FROM docs WHERE json_array_length(payload, '$.tags') = 1;"#,
2430 &mut db,
2431 )
2432 .unwrap_err();
2433 let msg = format!("{err}").to_lowercase();
2434 assert!(
2435 msg.contains("non-array"),
2436 "expected non-array error, got: {msg}"
2437 );
2438 }
2439
2440 #[test]
2441 fn json_type_recognizes_each_kind() {
2442 let mut db = seed_json_table();
2443 process_command(
2444 r#"INSERT INTO docs (payload) VALUES ('{"o": {}, "a": [], "s": "x", "i": 1, "f": 1.5, "t": true, "n": null}');"#,
2445 &mut db,
2446 )
2447 .unwrap();
2448 let cases = &[
2449 ("$.o", "object"),
2450 ("$.a", "array"),
2451 ("$.s", "text"),
2452 ("$.i", "integer"),
2453 ("$.f", "real"),
2454 ("$.t", "true"),
2455 ("$.n", "null"),
2456 ];
2457 for (path, expected_type) in cases {
2458 let sql = format!(
2459 "SELECT id FROM docs WHERE json_type(payload, '{path}') = '{expected_type}';"
2460 );
2461 let resp =
2462 process_command(&sql, &mut db).unwrap_or_else(|e| panic!("path {path}: {e}"));
2463 assert!(
2464 resp.contains("1 row returned"),
2465 "path {path} expected type {expected_type}; got response: {resp}"
2466 );
2467 }
2468 }
2469
2470 #[test]
2471 fn update_on_json_column_revalidates() {
2472 let mut db = seed_json_table();
2473 process_command(
2474 r#"INSERT INTO docs (payload) VALUES ('{"a": 1}');"#,
2475 &mut db,
2476 )
2477 .unwrap();
2478 process_command(
2480 r#"UPDATE docs SET payload = '{"a": 2, "b": 3}' WHERE id = 1;"#,
2481 &mut db,
2482 )
2483 .expect("valid JSON UPDATE");
2484 let err = process_command(
2487 r#"UPDATE docs SET payload = 'not-json{' WHERE id = 1;"#,
2488 &mut db,
2489 )
2490 .unwrap_err();
2491 let msg = format!("{err}").to_lowercase();
2492 assert!(
2493 msg.contains("json") && msg.contains("payload"),
2494 "got: {msg}"
2495 );
2496 }
2497
2498 #[test]
2503 fn default_literal_int_applies_when_column_omitted() {
2504 let mut db = Database::new("t".to_string());
2505 process_command(
2506 "CREATE TABLE t (id INTEGER PRIMARY KEY, n INTEGER DEFAULT 42);",
2507 &mut db,
2508 )
2509 .unwrap();
2510 process_command("INSERT INTO t (id) VALUES (1);", &mut db).unwrap();
2511
2512 let table = db.get_table("t".to_string()).unwrap();
2513 assert_eq!(table.get_value("n", 1), Some(Value::Integer(42)));
2514 }
2515
2516 #[test]
2517 fn default_literal_text_applies_when_column_omitted() {
2518 let mut db = Database::new("t".to_string());
2519 process_command(
2520 "CREATE TABLE users (id INTEGER PRIMARY KEY, status TEXT DEFAULT 'active');",
2521 &mut db,
2522 )
2523 .unwrap();
2524 process_command("INSERT INTO users (id) VALUES (1);", &mut db).unwrap();
2525
2526 let table = db.get_table("users".to_string()).unwrap();
2527 assert_eq!(
2528 table.get_value("status", 1),
2529 Some(Value::Text("active".to_string()))
2530 );
2531 }
2532
2533 #[test]
2534 fn default_literal_real_negative_applies_when_column_omitted() {
2535 let mut db = Database::new("t".to_string());
2537 process_command(
2538 "CREATE TABLE t (id INTEGER PRIMARY KEY, score REAL DEFAULT -1.5);",
2539 &mut db,
2540 )
2541 .unwrap();
2542 process_command("INSERT INTO t (id) VALUES (1);", &mut db).unwrap();
2543
2544 let table = db.get_table("t".to_string()).unwrap();
2545 assert_eq!(table.get_value("score", 1), Some(Value::Real(-1.5)));
2546 }
2547
2548 #[test]
2549 fn default_with_type_mismatch_errors_at_create_time() {
2550 let mut db = Database::new("t".to_string());
2551 let result = process_command(
2552 "CREATE TABLE t (id INTEGER PRIMARY KEY, n INTEGER DEFAULT 'oops');",
2553 &mut db,
2554 );
2555 let err = result.expect_err("text default on INTEGER column should be rejected");
2556 let msg = format!("{err}").to_lowercase();
2557 assert!(msg.contains("default"), "got: {msg}");
2558 }
2559
2560 #[test]
2561 fn default_for_json_column_must_be_valid_json() {
2562 let mut db = Database::new("t".to_string());
2566 let err = process_command(
2567 "CREATE TABLE t (id INTEGER PRIMARY KEY, doc JSON DEFAULT 'not-json{');",
2568 &mut db,
2569 )
2570 .unwrap_err();
2571 assert!(
2572 format!("{err}").to_lowercase().contains("json"),
2573 "got: {err}"
2574 );
2575
2576 process_command(
2578 "CREATE TABLE t2 (id INTEGER PRIMARY KEY, doc JSON DEFAULT '{\"k\":1}');",
2579 &mut db,
2580 )
2581 .expect("valid JSON DEFAULT should be accepted");
2582 }
2583
2584 #[test]
2585 fn default_with_non_literal_expression_errors_at_create_time() {
2586 let mut db = Database::new("t".to_string());
2587 let result = process_command(
2590 "CREATE TABLE t (id INTEGER PRIMARY KEY, ts TEXT DEFAULT CURRENT_TIMESTAMP);",
2591 &mut db,
2592 );
2593 let err = result.expect_err("non-literal DEFAULT should be rejected");
2594 let msg = format!("{err}").to_lowercase();
2595 assert!(
2596 msg.contains("default") && msg.contains("literal"),
2597 "got: {msg}"
2598 );
2599 }
2600
2601 #[test]
2602 fn default_null_is_accepted_at_create_time() {
2603 let mut db = Database::new("t".to_string());
2607 process_command(
2608 "CREATE TABLE t (id INTEGER PRIMARY KEY, note TEXT DEFAULT NULL);",
2609 &mut db,
2610 )
2611 .expect("CREATE TABLE with DEFAULT NULL should be accepted");
2612 let table = db.get_table("t".to_string()).unwrap();
2613 let note = table
2614 .columns
2615 .iter()
2616 .find(|c| c.column_name == "note")
2617 .unwrap();
2618 assert_eq!(note.default, Some(Value::Null));
2619 }
2620
2621 #[test]
2626 fn drop_table_basic() {
2627 let mut db = seed_users_table();
2628 let response = process_command("DROP TABLE users;", &mut db).expect("drop table");
2629 assert!(response.contains("1 table dropped"));
2630 assert!(!db.contains_table("users".to_string()));
2631 }
2632
2633 #[test]
2634 fn drop_table_if_exists_noop_on_missing() {
2635 let mut db = Database::new("t".to_string());
2636 let response =
2637 process_command("DROP TABLE IF EXISTS missing;", &mut db).expect("drop if exists");
2638 assert!(response.contains("0 tables dropped"));
2639 }
2640
2641 #[test]
2642 fn drop_table_missing_errors_without_if_exists() {
2643 let mut db = Database::new("t".to_string());
2644 let err = process_command("DROP TABLE missing;", &mut db).unwrap_err();
2645 assert!(format!("{err}").contains("does not exist"), "got: {err}");
2646 }
2647
2648 #[test]
2649 fn drop_table_reserved_name_errors() {
2650 let mut db = Database::new("t".to_string());
2651 let err = process_command("DROP TABLE sqlrite_master;", &mut db).unwrap_err();
2652 assert!(format!("{err}").contains("reserved"), "got: {err}");
2653 }
2654
2655 #[test]
2656 fn drop_table_multi_target_rejected() {
2657 let mut db = seed_users_table();
2658 process_command("CREATE TABLE other (id INTEGER PRIMARY KEY);", &mut db).unwrap();
2659 let err = process_command("DROP TABLE users, other;", &mut db).unwrap_err();
2662 assert!(format!("{err}").contains("single table"), "got: {err}");
2663 }
2664
2665 #[test]
2666 fn drop_table_cascades_indexes_in_memory() {
2667 let mut db = seed_users_table();
2668 process_command("CREATE INDEX users_age_idx ON users (age);", &mut db).unwrap();
2669 let users = db.get_table("users".to_string()).unwrap();
2671 assert!(
2672 users
2673 .secondary_indexes
2674 .iter()
2675 .any(|i| i.name == "users_age_idx")
2676 );
2677
2678 process_command("DROP TABLE users;", &mut db).unwrap();
2679
2680 for table in db.tables.values() {
2682 assert!(
2683 !table
2684 .secondary_indexes
2685 .iter()
2686 .any(|i| i.name.contains("users")),
2687 "dropped table's indexes should not survive on any other table"
2688 );
2689 }
2690 }
2691
2692 #[test]
2693 fn drop_index_explicit_basic() {
2694 let mut db = seed_users_table();
2695 process_command("CREATE INDEX users_age_idx ON users (age);", &mut db).unwrap();
2696 let response = process_command("DROP INDEX users_age_idx;", &mut db).expect("drop index");
2697 assert!(response.contains("1 index dropped"));
2698
2699 let users = db.get_table("users".to_string()).unwrap();
2700 assert!(users.index_by_name("users_age_idx").is_none());
2701 }
2702
2703 #[test]
2704 fn drop_index_refuses_auto_index() {
2705 let mut db = seed_users_table();
2706 let err = process_command("DROP INDEX sqlrite_autoindex_users_id;", &mut db).unwrap_err();
2709 assert!(format!("{err}").contains("auto-created"), "got: {err}");
2710 }
2711
2712 #[test]
2713 fn drop_index_if_exists_noop_on_missing() {
2714 let mut db = Database::new("t".to_string());
2715 let response =
2716 process_command("DROP INDEX IF EXISTS nope;", &mut db).expect("drop index if exists");
2717 assert!(response.contains("0 indexes dropped"));
2718 }
2719
2720 #[test]
2721 fn drop_index_missing_errors_without_if_exists() {
2722 let mut db = Database::new("t".to_string());
2723 let err = process_command("DROP INDEX nope;", &mut db).unwrap_err();
2724 assert!(format!("{err}").contains("does not exist"), "got: {err}");
2725 }
2726
2727 #[test]
2728 fn drop_statements_rejected_on_readonly_db() {
2729 use crate::sql::pager::{open_database_read_only, save_database};
2730
2731 let mut seed = Database::new("t".to_string());
2732 process_command(
2733 "CREATE TABLE notes (id INTEGER PRIMARY KEY, body TEXT);",
2734 &mut seed,
2735 )
2736 .unwrap();
2737 process_command("CREATE INDEX notes_body ON notes (body);", &mut seed).unwrap();
2738 let path = {
2739 let mut p = std::env::temp_dir();
2740 let pid = std::process::id();
2741 let nanos = std::time::SystemTime::now()
2742 .duration_since(std::time::UNIX_EPOCH)
2743 .map(|d| d.as_nanos())
2744 .unwrap_or(0);
2745 p.push(format!("sqlrite-drop-ro-{pid}-{nanos}.sqlrite"));
2746 p
2747 };
2748 save_database(&mut seed, &path).unwrap();
2749 drop(seed);
2750
2751 let mut ro = open_database_read_only(&path, "t".to_string()).unwrap();
2752 for stmt in ["DROP TABLE notes;", "DROP INDEX notes_body;"] {
2753 let err = process_command(stmt, &mut ro).unwrap_err();
2754 assert!(
2755 format!("{err}").contains("read-only"),
2756 "{stmt:?} should surface read-only error, got: {err}"
2757 );
2758 }
2759
2760 let _ = std::fs::remove_file(&path);
2761 let mut wal = path.as_os_str().to_owned();
2762 wal.push("-wal");
2763 let _ = std::fs::remove_file(std::path::PathBuf::from(wal));
2764 }
2765
2766 #[test]
2771 fn alter_rename_table_basic() {
2772 let mut db = seed_users_table();
2773 process_command("ALTER TABLE users RENAME TO members;", &mut db).expect("rename table");
2774 assert!(!db.contains_table("users".to_string()));
2775 assert!(db.contains_table("members".to_string()));
2776 let response = process_command("SELECT * FROM members;", &mut db).expect("select");
2778 assert!(response.contains("3 rows returned"));
2779 }
2780
2781 #[test]
2782 fn alter_rename_table_renames_auto_indexes() {
2783 let mut db = Database::new("t".to_string());
2786 process_command(
2787 "CREATE TABLE accounts (id INTEGER PRIMARY KEY, email TEXT UNIQUE);",
2788 &mut db,
2789 )
2790 .unwrap();
2791 {
2792 let accounts = db.get_table("accounts".to_string()).unwrap();
2793 assert!(
2794 accounts
2795 .index_by_name("sqlrite_autoindex_accounts_id")
2796 .is_some()
2797 );
2798 assert!(
2799 accounts
2800 .index_by_name("sqlrite_autoindex_accounts_email")
2801 .is_some()
2802 );
2803 }
2804 process_command("ALTER TABLE accounts RENAME TO members;", &mut db).expect("rename");
2805 let members = db.get_table("members".to_string()).unwrap();
2806 assert!(
2807 members
2808 .index_by_name("sqlrite_autoindex_members_id")
2809 .is_some(),
2810 "PK auto-index should be renamed to match new table"
2811 );
2812 assert!(
2813 members
2814 .index_by_name("sqlrite_autoindex_members_email")
2815 .is_some()
2816 );
2817 assert!(
2819 members
2820 .index_by_name("sqlrite_autoindex_accounts_id")
2821 .is_none()
2822 );
2823 for idx in &members.secondary_indexes {
2825 assert_eq!(idx.table_name, "members");
2826 }
2827 }
2828
2829 #[test]
2830 fn alter_rename_table_to_existing_errors() {
2831 let mut db = seed_users_table();
2832 process_command("CREATE TABLE other (id INTEGER PRIMARY KEY);", &mut db).unwrap();
2833 let err = process_command("ALTER TABLE users RENAME TO other;", &mut db).unwrap_err();
2834 assert!(format!("{err}").contains("already exists"), "got: {err}");
2835 assert!(db.contains_table("users".to_string()));
2837 assert!(db.contains_table("other".to_string()));
2838 }
2839
2840 #[test]
2841 fn alter_rename_table_to_reserved_name_errors() {
2842 let mut db = seed_users_table();
2843 let err =
2844 process_command("ALTER TABLE users RENAME TO sqlrite_master;", &mut db).unwrap_err();
2845 assert!(format!("{err}").contains("reserved"), "got: {err}");
2846 }
2847
2848 #[test]
2849 fn alter_rename_column_basic() {
2850 let mut db = seed_users_table();
2851 process_command(
2852 "ALTER TABLE users RENAME COLUMN name TO full_name;",
2853 &mut db,
2854 )
2855 .expect("rename column");
2856
2857 let users = db.get_table("users".to_string()).unwrap();
2858 assert!(users.contains_column("full_name".to_string()));
2859 assert!(!users.contains_column("name".to_string()));
2860
2861 let bob_rowid = users
2864 .rowids()
2865 .into_iter()
2866 .find(|r| users.get_value("full_name", *r) == Some(Value::Text("bob".to_string())))
2867 .expect("bob row should be findable under the new column name");
2868 assert_eq!(
2869 users.get_value("full_name", bob_rowid),
2870 Some(Value::Text("bob".to_string()))
2871 );
2872 }
2873
2874 #[test]
2875 fn alter_rename_column_collision_errors() {
2876 let mut db = seed_users_table();
2877 let err =
2878 process_command("ALTER TABLE users RENAME COLUMN name TO age;", &mut db).unwrap_err();
2879 assert!(format!("{err}").contains("already exists"), "got: {err}");
2880 }
2881
2882 #[test]
2883 fn alter_rename_column_updates_indexes() {
2884 let mut db = Database::new("t".to_string());
2886 process_command(
2887 "CREATE TABLE accounts (id INTEGER PRIMARY KEY, email TEXT UNIQUE);",
2888 &mut db,
2889 )
2890 .unwrap();
2891 process_command(
2892 "ALTER TABLE accounts RENAME COLUMN email TO contact;",
2893 &mut db,
2894 )
2895 .unwrap();
2896 let accounts = db.get_table("accounts".to_string()).unwrap();
2897 assert!(
2898 accounts
2899 .index_by_name("sqlrite_autoindex_accounts_contact")
2900 .is_some()
2901 );
2902 assert!(
2903 accounts
2904 .index_by_name("sqlrite_autoindex_accounts_email")
2905 .is_none()
2906 );
2907 }
2908
2909 #[test]
2910 fn alter_add_column_basic() {
2911 let mut db = seed_users_table();
2912 process_command("ALTER TABLE users ADD COLUMN nickname TEXT;", &mut db)
2913 .expect("add column");
2914 let users = db.get_table("users".to_string()).unwrap();
2915 assert!(users.contains_column("nickname".to_string()));
2916 let any_rowid = *users.rowids().first().expect("seed has rows");
2918 assert_eq!(users.get_value("nickname", any_rowid), None);
2919
2920 process_command(
2922 "INSERT INTO users (name, age, nickname) VALUES ('dan', 22, 'd');",
2923 &mut db,
2924 )
2925 .expect("insert with new col");
2926 let users = db.get_table("users".to_string()).unwrap();
2927 let dan_rowid = users
2928 .rowids()
2929 .into_iter()
2930 .find(|r| users.get_value("name", *r) == Some(Value::Text("dan".to_string())))
2931 .unwrap();
2932 assert_eq!(
2933 users.get_value("nickname", dan_rowid),
2934 Some(Value::Text("d".to_string()))
2935 );
2936 }
2937
2938 #[test]
2939 fn alter_add_column_with_default_backfills_existing_rows() {
2940 let mut db = seed_users_table();
2941 process_command(
2942 "ALTER TABLE users ADD COLUMN status TEXT DEFAULT 'active';",
2943 &mut db,
2944 )
2945 .expect("add column with default");
2946 let users = db.get_table("users".to_string()).unwrap();
2947 for rowid in users.rowids() {
2948 assert_eq!(
2949 users.get_value("status", rowid),
2950 Some(Value::Text("active".to_string())),
2951 "rowid {rowid} should have been backfilled with the default"
2952 );
2953 }
2954 }
2955
2956 #[test]
2957 fn alter_add_column_not_null_with_default_works_on_nonempty_table() {
2958 let mut db = seed_users_table();
2959 process_command(
2960 "ALTER TABLE users ADD COLUMN score INTEGER NOT NULL DEFAULT 0;",
2961 &mut db,
2962 )
2963 .expect("NOT NULL ADD with DEFAULT should succeed even with existing rows");
2964 let users = db.get_table("users".to_string()).unwrap();
2965 for rowid in users.rowids() {
2966 assert_eq!(users.get_value("score", rowid), Some(Value::Integer(0)));
2967 }
2968 }
2969
2970 #[test]
2971 fn alter_add_column_not_null_without_default_errors_on_nonempty_table() {
2972 let mut db = seed_users_table();
2973 let err = process_command(
2974 "ALTER TABLE users ADD COLUMN score INTEGER NOT NULL;",
2975 &mut db,
2976 )
2977 .unwrap_err();
2978 let msg = format!("{err}").to_lowercase();
2979 assert!(
2980 msg.contains("not null") && msg.contains("default"),
2981 "got: {msg}"
2982 );
2983 }
2984
2985 #[test]
2986 fn alter_add_column_pk_rejected() {
2987 let mut db = seed_users_table();
2988 let err = process_command(
2989 "ALTER TABLE users ADD COLUMN extra INTEGER PRIMARY KEY;",
2990 &mut db,
2991 )
2992 .unwrap_err();
2993 assert!(
2994 format!("{err}").to_lowercase().contains("primary key"),
2995 "got: {err}"
2996 );
2997 }
2998
2999 #[test]
3000 fn alter_add_column_unique_rejected() {
3001 let mut db = seed_users_table();
3002 let err = process_command(
3003 "ALTER TABLE users ADD COLUMN extra INTEGER UNIQUE;",
3004 &mut db,
3005 )
3006 .unwrap_err();
3007 assert!(
3008 format!("{err}").to_lowercase().contains("unique"),
3009 "got: {err}"
3010 );
3011 }
3012
3013 #[test]
3014 fn alter_add_column_existing_name_errors() {
3015 let mut db = seed_users_table();
3016 let err =
3017 process_command("ALTER TABLE users ADD COLUMN age INTEGER;", &mut db).unwrap_err();
3018 assert!(format!("{err}").contains("already exists"), "got: {err}");
3019 }
3020
3021 #[test]
3027 fn alter_drop_column_basic() {
3028 let mut db = seed_users_table();
3029 process_command("ALTER TABLE users DROP COLUMN age;", &mut db).expect("drop column");
3030 let users = db.get_table("users".to_string()).unwrap();
3031 assert!(!users.contains_column("age".to_string()));
3032 assert!(users.contains_column("name".to_string()));
3034 assert_eq!(users.rowids().len(), 3);
3035 }
3036
3037 #[test]
3038 fn alter_drop_column_drops_dependent_indexes() {
3039 let mut db = seed_users_table();
3040 process_command("CREATE INDEX users_age_idx ON users (age);", &mut db).unwrap();
3041 process_command("ALTER TABLE users DROP COLUMN age;", &mut db).unwrap();
3042 let users = db.get_table("users".to_string()).unwrap();
3043 assert!(users.index_by_name("users_age_idx").is_none());
3044 }
3045
3046 #[test]
3047 fn alter_drop_column_pk_errors() {
3048 let mut db = seed_users_table();
3049 let err = process_command("ALTER TABLE users DROP COLUMN id;", &mut db).unwrap_err();
3050 assert!(
3051 format!("{err}").to_lowercase().contains("primary key"),
3052 "got: {err}"
3053 );
3054 }
3055
3056 #[test]
3057 fn alter_drop_column_only_column_errors() {
3058 let mut db = Database::new("t".to_string());
3059 process_command("CREATE TABLE solo (only_col TEXT);", &mut db).unwrap();
3060 let err = process_command("ALTER TABLE solo DROP COLUMN only_col;", &mut db).unwrap_err();
3061 assert!(
3062 format!("{err}").to_lowercase().contains("only column"),
3063 "got: {err}"
3064 );
3065 }
3066
3067 #[test]
3068 fn alter_unknown_table_errors_without_if_exists() {
3069 let mut db = Database::new("t".to_string());
3070 let err = process_command("ALTER TABLE missing RENAME TO other;", &mut db).unwrap_err();
3071 assert!(format!("{err}").contains("does not exist"), "got: {err}");
3072 }
3073
3074 #[test]
3075 fn alter_unknown_table_if_exists_noop() {
3076 let mut db = Database::new("t".to_string());
3077 let response = process_command("ALTER TABLE IF EXISTS missing RENAME TO other;", &mut db)
3078 .expect("IF EXISTS makes missing-table ALTER a no-op");
3079 assert!(response.contains("no-op"));
3080 }
3081
3082 #[test]
3083 fn drop_table_inside_transaction_rolls_back() {
3084 let mut db = seed_users_table();
3088 process_command("CREATE INDEX users_age_idx ON users (age);", &mut db).unwrap();
3089 process_command("BEGIN;", &mut db).unwrap();
3090 process_command("DROP TABLE users;", &mut db).unwrap();
3091 assert!(!db.contains_table("users".to_string()));
3092 process_command("ROLLBACK;", &mut db).unwrap();
3093 assert!(db.contains_table("users".to_string()));
3094 let users = db.get_table("users".to_string()).unwrap();
3095 assert_eq!(users.rowids().len(), 3);
3096 assert!(users.index_by_name("users_age_idx").is_some());
3097 }
3098
3099 #[test]
3100 fn alter_inside_transaction_rolls_back() {
3101 let mut db = seed_users_table();
3102 process_command("BEGIN;", &mut db).unwrap();
3103 process_command(
3104 "ALTER TABLE users ADD COLUMN status TEXT DEFAULT 'active';",
3105 &mut db,
3106 )
3107 .unwrap();
3108 assert!(
3110 db.get_table("users".to_string())
3111 .unwrap()
3112 .contains_column("status".to_string())
3113 );
3114 process_command("ROLLBACK;", &mut db).unwrap();
3115 assert!(
3117 !db.get_table("users".to_string())
3118 .unwrap()
3119 .contains_column("status".to_string())
3120 );
3121 }
3122
3123 #[test]
3124 fn alter_rejected_on_readonly_db() {
3125 use crate::sql::pager::{open_database_read_only, save_database};
3126
3127 let mut seed = Database::new("t".to_string());
3128 process_command(
3129 "CREATE TABLE notes (id INTEGER PRIMARY KEY, body TEXT);",
3130 &mut seed,
3131 )
3132 .unwrap();
3133 let path = {
3134 let mut p = std::env::temp_dir();
3135 let pid = std::process::id();
3136 let nanos = std::time::SystemTime::now()
3137 .duration_since(std::time::UNIX_EPOCH)
3138 .map(|d| d.as_nanos())
3139 .unwrap_or(0);
3140 p.push(format!("sqlrite-alter-ro-{pid}-{nanos}.sqlrite"));
3141 p
3142 };
3143 save_database(&mut seed, &path).unwrap();
3144 drop(seed);
3145
3146 let mut ro = open_database_read_only(&path, "t".to_string()).unwrap();
3147 for stmt in [
3148 "ALTER TABLE notes RENAME TO n2;",
3149 "ALTER TABLE notes RENAME COLUMN body TO b;",
3150 "ALTER TABLE notes ADD COLUMN extra TEXT;",
3151 "ALTER TABLE notes DROP COLUMN body;",
3152 ] {
3153 let err = process_command(stmt, &mut ro).unwrap_err();
3154 assert!(
3155 format!("{err}").contains("read-only"),
3156 "{stmt:?} should surface read-only error, got: {err}"
3157 );
3158 }
3159
3160 let _ = std::fs::remove_file(&path);
3161 let mut wal = path.as_os_str().to_owned();
3162 wal.push("-wal");
3163 let _ = std::fs::remove_file(std::path::PathBuf::from(wal));
3164 }
3165}