1pub mod db;
2pub mod executor;
3pub mod fts;
4pub mod hnsw;
5pub mod pager;
6pub mod parser;
7use parser::create::CreateQuery;
10use parser::insert::InsertQuery;
11use parser::select::SelectQuery;
12
13use sqlparser::ast::{ObjectType, Statement};
14use sqlparser::dialect::SQLiteDialect;
15use sqlparser::parser::{Parser, ParserError};
16
17use crate::error::{Result, SQLRiteError};
18use crate::sql::db::database::Database;
19use crate::sql::db::table::Table;
20
21#[derive(Debug, PartialEq)]
22pub enum SQLCommand {
23 Insert(String),
24 Delete(String),
25 Update(String),
26 CreateTable(String),
27 Select(String),
28 Unknown(String),
29}
30
31impl SQLCommand {
32 pub fn new(command: String) -> SQLCommand {
33 let v = command.split(" ").collect::<Vec<&str>>();
34 match v[0] {
35 "insert" => SQLCommand::Insert(command),
36 "update" => SQLCommand::Update(command),
37 "delete" => SQLCommand::Delete(command),
38 "create" => SQLCommand::CreateTable(command),
39 "select" => SQLCommand::Select(command),
40 _ => SQLCommand::Unknown(command),
41 }
42 }
43}
44
45#[derive(Debug, Clone)]
64pub struct CommandOutput {
65 pub status: String,
66 pub rendered: Option<String>,
67}
68
69pub fn process_command(query: &str, db: &mut Database) -> Result<String> {
78 process_command_with_render(query, db).map(|o| o.status)
79}
80
81pub fn process_command_with_render(query: &str, db: &mut Database) -> Result<CommandOutput> {
88 let dialect = SQLiteDialect {};
89 let message: String;
90 let mut rendered: Option<String> = None;
91 let mut ast = Parser::parse_sql(&dialect, query).map_err(SQLRiteError::from)?;
92
93 if ast.len() > 1 {
94 return Err(SQLRiteError::SqlError(ParserError::ParserError(format!(
95 "Expected a single query statement, but there are {}",
96 ast.len()
97 ))));
98 }
99
100 let Some(query) = ast.pop() else {
104 return Ok(CommandOutput {
105 status: "No statement to execute.".to_string(),
106 rendered: None,
107 });
108 };
109
110 match &query {
115 Statement::StartTransaction { .. } => {
116 db.begin_transaction()?;
117 return Ok(CommandOutput {
118 status: String::from("BEGIN"),
119 rendered: None,
120 });
121 }
122 Statement::Commit { .. } => {
123 if !db.in_transaction() {
124 return Err(SQLRiteError::General(
125 "cannot COMMIT: no transaction is open".to_string(),
126 ));
127 }
128 if let Some(path) = db.source_path.clone() {
138 if let Err(save_err) = pager::save_database(db, &path) {
139 let _ = db.rollback_transaction();
140 return Err(SQLRiteError::General(format!(
141 "COMMIT failed — transaction rolled back: {save_err}"
142 )));
143 }
144 }
145 db.commit_transaction()?;
146 return Ok(CommandOutput {
147 status: String::from("COMMIT"),
148 rendered: None,
149 });
150 }
151 Statement::Rollback { .. } => {
152 db.rollback_transaction()?;
153 return Ok(CommandOutput {
154 status: String::from("ROLLBACK"),
155 rendered: None,
156 });
157 }
158 _ => {}
159 }
160
161 let is_write_statement = matches!(
167 &query,
168 Statement::CreateTable(_)
169 | Statement::CreateIndex(_)
170 | Statement::Insert(_)
171 | Statement::Update(_)
172 | Statement::Delete(_)
173 | Statement::Drop { .. }
174 | Statement::AlterTable(_)
175 | Statement::Vacuum(_)
176 );
177 let is_vacuum = matches!(&query, Statement::Vacuum(_));
178
179 if is_write_statement && db.is_read_only() {
185 return Err(SQLRiteError::General(
186 "cannot execute: database is opened read-only".to_string(),
187 ));
188 }
189
190 match query {
192 Statement::CreateTable(_) => {
193 let create_query = CreateQuery::new(&query);
194 match create_query {
195 Ok(payload) => {
196 let table_name = payload.table_name.clone();
197 if table_name == pager::MASTER_TABLE_NAME {
198 return Err(SQLRiteError::General(format!(
199 "'{}' is a reserved name used by the internal schema catalog",
200 pager::MASTER_TABLE_NAME
201 )));
202 }
203 match db.contains_table(table_name.to_string()) {
205 true => {
206 return Err(SQLRiteError::Internal(
207 "Cannot create, table already exists.".to_string(),
208 ));
209 }
210 false => {
211 let table = Table::new(payload);
212 db.tables.insert(table_name.to_string(), table);
220 message = String::from("CREATE TABLE Statement executed.");
221 }
222 }
223 }
224 Err(err) => return Err(err),
225 }
226 }
227 Statement::Insert(_) => {
228 let insert_query = InsertQuery::new(&query);
229 match insert_query {
230 Ok(payload) => {
231 let table_name = payload.table_name;
232 let columns = payload.columns;
233 let values = payload.rows;
234
235 match db.contains_table(table_name.to_string()) {
238 true => {
239 let db_table = db.get_table_mut(table_name.to_string()).unwrap();
240 match columns
242 .iter()
243 .all(|column| db_table.contains_column(column.to_string()))
244 {
245 true => {
246 for value in &values {
247 if columns.len() != value.len() {
249 return Err(SQLRiteError::Internal(format!(
250 "{} values for {} columns",
251 value.len(),
252 columns.len()
253 )));
254 }
255 db_table
256 .validate_unique_constraint(&columns, value)
257 .map_err(|err| {
258 SQLRiteError::Internal(format!(
259 "Unique key constraint violation: {err}"
260 ))
261 })?;
262 db_table.insert_row(&columns, value)?;
263 }
264 }
265 false => {
266 return Err(SQLRiteError::Internal(
267 "Cannot insert, some of the columns do not exist"
268 .to_string(),
269 ));
270 }
271 }
272 }
279 false => {
280 return Err(SQLRiteError::Internal("Table doesn't exist".to_string()));
281 }
282 }
283 }
284 Err(err) => return Err(err),
285 }
286
287 message = String::from("INSERT Statement executed.")
288 }
289 Statement::Query(_) => {
290 let select_query = SelectQuery::new(&query)?;
291 let (rendered_table, rows) = executor::execute_select(select_query, db)?;
292 rendered = Some(rendered_table);
298 message = format!(
299 "SELECT Statement executed. {rows} row{s} returned.",
300 s = if rows == 1 { "" } else { "s" }
301 );
302 }
303 Statement::Delete(_) => {
304 let rows = executor::execute_delete(&query, db)?;
305 message = format!(
306 "DELETE Statement executed. {rows} row{s} deleted.",
307 s = if rows == 1 { "" } else { "s" }
308 );
309 }
310 Statement::Update(_) => {
311 let rows = executor::execute_update(&query, db)?;
312 message = format!(
313 "UPDATE Statement executed. {rows} row{s} updated.",
314 s = if rows == 1 { "" } else { "s" }
315 );
316 }
317 Statement::CreateIndex(_) => {
318 let name = executor::execute_create_index(&query, db)?;
319 message = format!("CREATE INDEX '{name}' executed.");
320 }
321 Statement::Drop {
322 object_type,
323 if_exists,
324 names,
325 ..
326 } => match object_type {
327 ObjectType::Table => {
328 let count = executor::execute_drop_table(&names, if_exists, db)?;
329 let plural = if count == 1 { "table" } else { "tables" };
330 message = format!("DROP TABLE Statement executed. {count} {plural} dropped.");
331 }
332 ObjectType::Index => {
333 let count = executor::execute_drop_index(&names, if_exists, db)?;
334 let plural = if count == 1 { "index" } else { "indexes" };
335 message = format!("DROP INDEX Statement executed. {count} {plural} dropped.");
336 }
337 other => {
338 return Err(SQLRiteError::NotImplemented(format!(
339 "DROP {other:?} is not supported (only TABLE and INDEX)"
340 )));
341 }
342 },
343 Statement::AlterTable(alter) => {
344 message = executor::execute_alter_table(alter, db)?;
345 }
346 Statement::Vacuum(vac) => {
347 if vac.full
352 || vac.sort_only
353 || vac.delete_only
354 || vac.reindex
355 || vac.recluster
356 || vac.boost
357 || vac.table_name.is_some()
358 || vac.threshold.is_some()
359 {
360 return Err(SQLRiteError::NotImplemented(
361 "VACUUM modifiers (FULL, REINDEX, table targets, etc.) are not supported; use bare VACUUM;"
362 .to_string(),
363 ));
364 }
365 message = executor::execute_vacuum(db)?;
366 }
367 _ => {
368 return Err(SQLRiteError::NotImplemented(
369 "SQL Statement not supported yet.".to_string(),
370 ));
371 }
372 };
373
374 if is_write_statement && !is_vacuum && db.source_path.is_some() && !db.in_transaction() {
388 let path = db.source_path.clone().unwrap();
389 pager::save_database(db, &path)?;
390 }
391
392 Ok(CommandOutput {
393 status: message,
394 rendered,
395 })
396}
397
398#[cfg(test)]
399mod tests {
400 use super::*;
401 use crate::sql::db::table::Value;
402
403 fn seed_users_table() -> Database {
406 let mut db = Database::new("tempdb".to_string());
407 process_command(
408 "CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT NOT NULL, age INTEGER);",
409 &mut db,
410 )
411 .expect("create table");
412 process_command(
413 "INSERT INTO users (name, age) VALUES ('alice', 30);",
414 &mut db,
415 )
416 .expect("insert alice");
417 process_command("INSERT INTO users (name, age) VALUES ('bob', 25);", &mut db)
418 .expect("insert bob");
419 process_command(
420 "INSERT INTO users (name, age) VALUES ('carol', 40);",
421 &mut db,
422 )
423 .expect("insert carol");
424 db
425 }
426
427 #[test]
428 fn process_command_select_all_test() {
429 let mut db = seed_users_table();
430 let response = process_command("SELECT * FROM users;", &mut db).expect("select");
431 assert!(response.contains("3 rows returned"));
432 }
433
434 #[test]
435 fn process_command_select_where_test() {
436 let mut db = seed_users_table();
437 let response =
438 process_command("SELECT name FROM users WHERE age > 25;", &mut db).expect("select");
439 assert!(response.contains("2 rows returned"));
440 }
441
442 #[test]
443 fn process_command_select_eq_string_test() {
444 let mut db = seed_users_table();
445 let response =
446 process_command("SELECT name FROM users WHERE name = 'bob';", &mut db).expect("select");
447 assert!(response.contains("1 row returned"));
448 }
449
450 #[test]
451 fn process_command_select_limit_test() {
452 let mut db = seed_users_table();
453 let response = process_command("SELECT * FROM users ORDER BY age ASC LIMIT 2;", &mut db)
454 .expect("select");
455 assert!(response.contains("2 rows returned"));
456 }
457
458 #[test]
459 fn process_command_select_unknown_table_test() {
460 let mut db = Database::new("tempdb".to_string());
461 let result = process_command("SELECT * FROM nope;", &mut db);
462 assert!(result.is_err());
463 }
464
465 #[test]
466 fn process_command_select_unknown_column_test() {
467 let mut db = seed_users_table();
468 let result = process_command("SELECT height FROM users;", &mut db);
469 assert!(result.is_err());
470 }
471
472 #[test]
473 fn process_command_insert_test() {
474 let mut db = Database::new("tempdb".to_string());
476
477 let query_statement = "CREATE TABLE users (
479 id INTEGER PRIMARY KEY,
480 name TEXT
481 );";
482 let dialect = SQLiteDialect {};
483 let mut ast = Parser::parse_sql(&dialect, query_statement).unwrap();
484 if ast.len() > 1 {
485 panic!("Expected a single query statement, but there are more then 1.")
486 }
487 let query = ast.pop().unwrap();
488 let create_query = CreateQuery::new(&query).unwrap();
489
490 db.tables.insert(
492 create_query.table_name.to_string(),
493 Table::new(create_query),
494 );
495
496 let insert_query = String::from("INSERT INTO users (name) Values ('josh');");
498 match process_command(&insert_query, &mut db) {
499 Ok(response) => assert_eq!(response, "INSERT Statement executed."),
500 Err(err) => {
501 eprintln!("Error: {}", err);
502 assert!(false)
503 }
504 };
505 }
506
507 #[test]
508 fn process_command_insert_no_pk_test() {
509 let mut db = Database::new("tempdb".to_string());
511
512 let query_statement = "CREATE TABLE users (
514 name TEXT
515 );";
516 let dialect = SQLiteDialect {};
517 let mut ast = Parser::parse_sql(&dialect, query_statement).unwrap();
518 if ast.len() > 1 {
519 panic!("Expected a single query statement, but there are more then 1.")
520 }
521 let query = ast.pop().unwrap();
522 let create_query = CreateQuery::new(&query).unwrap();
523
524 db.tables.insert(
526 create_query.table_name.to_string(),
527 Table::new(create_query),
528 );
529
530 let insert_query = String::from("INSERT INTO users (name) Values ('josh');");
532 match process_command(&insert_query, &mut db) {
533 Ok(response) => assert_eq!(response, "INSERT Statement executed."),
534 Err(err) => {
535 eprintln!("Error: {}", err);
536 assert!(false)
537 }
538 };
539 }
540
541 #[test]
542 fn process_command_delete_where_test() {
543 let mut db = seed_users_table();
544 let response =
545 process_command("DELETE FROM users WHERE name = 'bob';", &mut db).expect("delete");
546 assert!(response.contains("1 row deleted"));
547
548 let remaining = process_command("SELECT * FROM users;", &mut db).expect("select");
549 assert!(remaining.contains("2 rows returned"));
550 }
551
552 #[test]
553 fn process_command_delete_all_test() {
554 let mut db = seed_users_table();
555 let response = process_command("DELETE FROM users;", &mut db).expect("delete");
556 assert!(response.contains("3 rows deleted"));
557 }
558
559 #[test]
560 fn process_command_update_where_test() {
561 use crate::sql::db::table::Value;
562
563 let mut db = seed_users_table();
564 let response = process_command("UPDATE users SET age = 99 WHERE name = 'bob';", &mut db)
565 .expect("update");
566 assert!(response.contains("1 row updated"));
567
568 let users = db.get_table("users".to_string()).unwrap();
570 let bob_rowid = users
571 .rowids()
572 .into_iter()
573 .find(|r| users.get_value("name", *r) == Some(Value::Text("bob".to_string())))
574 .expect("bob row must exist");
575 assert_eq!(users.get_value("age", bob_rowid), Some(Value::Integer(99)));
576 }
577
578 #[test]
579 fn process_command_update_unique_violation_test() {
580 let mut db = seed_users_table();
581 process_command(
583 "CREATE TABLE tags (id INTEGER PRIMARY KEY, label TEXT UNIQUE);",
584 &mut db,
585 )
586 .unwrap();
587 process_command("INSERT INTO tags (label) VALUES ('a');", &mut db).unwrap();
588 process_command("INSERT INTO tags (label) VALUES ('b');", &mut db).unwrap();
589
590 let result = process_command("UPDATE tags SET label = 'a' WHERE label = 'b';", &mut db);
591 assert!(result.is_err(), "expected UNIQUE violation, got {result:?}");
592 }
593
594 #[test]
595 fn process_command_insert_type_mismatch_returns_error_test() {
596 let mut db = Database::new("tempdb".to_string());
598 process_command(
599 "CREATE TABLE items (id INTEGER PRIMARY KEY, qty INTEGER);",
600 &mut db,
601 )
602 .unwrap();
603 let result = process_command("INSERT INTO items (qty) VALUES ('not a number');", &mut db);
604 assert!(result.is_err(), "expected error, got {result:?}");
605 }
606
607 #[test]
608 fn process_command_insert_missing_integer_returns_error_test() {
609 let mut db = Database::new("tempdb".to_string());
611 process_command(
612 "CREATE TABLE items (id INTEGER PRIMARY KEY, qty INTEGER);",
613 &mut db,
614 )
615 .unwrap();
616 let result = process_command("INSERT INTO items (id) VALUES (1);", &mut db);
617 assert!(result.is_err(), "expected error, got {result:?}");
618 }
619
620 #[test]
621 fn process_command_update_arith_test() {
622 use crate::sql::db::table::Value;
623
624 let mut db = seed_users_table();
625 process_command("UPDATE users SET age = age + 1;", &mut db).expect("update +1");
626
627 let users = db.get_table("users".to_string()).unwrap();
628 let mut ages: Vec<i64> = users
629 .rowids()
630 .into_iter()
631 .filter_map(|r| match users.get_value("age", r) {
632 Some(Value::Integer(n)) => Some(n),
633 _ => None,
634 })
635 .collect();
636 ages.sort();
637 assert_eq!(ages, vec![26, 31, 41]); }
639
640 #[test]
641 fn process_command_select_arithmetic_where_test() {
642 let mut db = seed_users_table();
643 let response =
645 process_command("SELECT name FROM users WHERE age * 2 > 55;", &mut db).expect("select");
646 assert!(response.contains("2 rows returned"));
647 }
648
649 #[test]
650 fn process_command_divide_by_zero_test() {
651 let mut db = seed_users_table();
652 let result = process_command("SELECT age / 0 FROM users;", &mut db);
653 assert!(result.is_err());
655 }
656
657 #[test]
658 fn process_command_unsupported_statement_test() {
659 let mut db = Database::new("tempdb".to_string());
660 let result = process_command("CREATE VIEW v AS SELECT * FROM users;", &mut db);
664 assert!(result.is_err());
665 }
666
667 #[test]
668 fn empty_input_is_a_noop_not_a_panic() {
669 let mut db = Database::new("t".to_string());
673 for input in ["", " ", "-- just a comment", "-- comment\n-- another"] {
674 let result = process_command(input, &mut db);
675 assert!(result.is_ok(), "input {input:?} should not error");
676 let msg = result.unwrap();
677 assert!(msg.contains("No statement"), "got: {msg:?}");
678 }
679 }
680
681 #[test]
682 fn create_index_adds_explicit_index() {
683 let mut db = seed_users_table();
684 let response = process_command("CREATE INDEX users_age_idx ON users (age);", &mut db)
685 .expect("create index");
686 assert!(response.contains("users_age_idx"));
687
688 let users = db.get_table("users".to_string()).unwrap();
690 let idx = users
691 .index_by_name("users_age_idx")
692 .expect("index should exist after CREATE INDEX");
693 assert_eq!(idx.column_name, "age");
694 assert!(!idx.is_unique);
695 }
696
697 #[test]
698 fn create_unique_index_rejects_duplicate_existing_values() {
699 let mut db = seed_users_table();
700 process_command("INSERT INTO users (name, age) VALUES ('dan', 30);", &mut db).unwrap();
703 let result = process_command(
704 "CREATE UNIQUE INDEX users_age_unique ON users (age);",
705 &mut db,
706 );
707 assert!(
708 result.is_err(),
709 "expected unique-index failure, got {result:?}"
710 );
711 }
712
713 #[test]
714 fn where_eq_on_indexed_column_uses_index_probe() {
715 let mut db = Database::new("t".to_string());
719 process_command(
720 "CREATE TABLE big (id INTEGER PRIMARY KEY, tag TEXT);",
721 &mut db,
722 )
723 .unwrap();
724 process_command("CREATE INDEX big_tag_idx ON big (tag);", &mut db).unwrap();
725 for i in 1..=100 {
726 let tag = if i % 3 == 0 { "hot" } else { "cold" };
727 process_command(&format!("INSERT INTO big (tag) VALUES ('{tag}');"), &mut db).unwrap();
728 }
729 let response =
730 process_command("SELECT id FROM big WHERE tag = 'hot';", &mut db).expect("select");
731 assert!(
733 response.contains("33 rows returned"),
734 "response was {response:?}"
735 );
736 }
737
738 #[test]
739 fn where_eq_on_indexed_column_inside_parens_uses_index_probe() {
740 let mut db = seed_users_table();
741 let response = process_command("SELECT name FROM users WHERE (name = 'bob');", &mut db)
742 .expect("select");
743 assert!(response.contains("1 row returned"));
744 }
745
746 #[test]
747 fn where_eq_literal_first_side_uses_index_probe() {
748 let mut db = seed_users_table();
749 let response =
751 process_command("SELECT name FROM users WHERE 'bob' = name;", &mut db).expect("select");
752 assert!(response.contains("1 row returned"));
753 }
754
755 #[test]
756 fn non_equality_where_still_falls_back_to_full_scan() {
757 let mut db = seed_users_table();
760 let response =
761 process_command("SELECT name FROM users WHERE age > 28;", &mut db).expect("select");
762 assert!(response.contains("2 rows returned"));
763 }
764
765 #[test]
770 fn rollback_restores_pre_begin_in_memory_state() {
771 let mut db = seed_users_table();
774 let before = db.get_table("users".to_string()).unwrap().rowids().len();
775 assert_eq!(before, 3);
776
777 process_command("BEGIN;", &mut db).expect("BEGIN");
778 assert!(db.in_transaction());
779 process_command("INSERT INTO users (name, age) VALUES ('dan', 50);", &mut db)
780 .expect("INSERT inside txn");
781 let mid = db.get_table("users".to_string()).unwrap().rowids().len();
783 assert_eq!(mid, 4);
784
785 process_command("ROLLBACK;", &mut db).expect("ROLLBACK");
786 assert!(!db.in_transaction());
787 let after = db.get_table("users".to_string()).unwrap().rowids().len();
788 assert_eq!(
789 after, 3,
790 "ROLLBACK should have restored the pre-BEGIN state"
791 );
792 }
793
794 #[test]
795 fn commit_keeps_mutations_and_clears_txn_flag() {
796 let mut db = seed_users_table();
797 process_command("BEGIN;", &mut db).expect("BEGIN");
798 process_command("INSERT INTO users (name, age) VALUES ('dan', 50);", &mut db)
799 .expect("INSERT inside txn");
800 process_command("COMMIT;", &mut db).expect("COMMIT");
801 assert!(!db.in_transaction());
802 let after = db.get_table("users".to_string()).unwrap().rowids().len();
803 assert_eq!(after, 4);
804 }
805
806 #[test]
807 fn rollback_undoes_update_and_delete_side_by_side() {
808 use crate::sql::db::table::Value;
809
810 let mut db = seed_users_table();
811 process_command("BEGIN;", &mut db).unwrap();
812 process_command("UPDATE users SET age = 999;", &mut db).unwrap();
813 process_command("DELETE FROM users WHERE name = 'bob';", &mut db).unwrap();
814 let users = db.get_table("users".to_string()).unwrap();
816 assert_eq!(users.rowids().len(), 2);
817 for r in users.rowids() {
818 assert_eq!(users.get_value("age", r), Some(Value::Integer(999)));
819 }
820
821 process_command("ROLLBACK;", &mut db).unwrap();
822 let users = db.get_table("users".to_string()).unwrap();
823 assert_eq!(users.rowids().len(), 3);
824 for r in users.rowids() {
826 assert_ne!(users.get_value("age", r), Some(Value::Integer(999)));
827 }
828 }
829
830 #[test]
831 fn nested_begin_is_rejected() {
832 let mut db = seed_users_table();
833 process_command("BEGIN;", &mut db).unwrap();
834 let err = process_command("BEGIN;", &mut db).unwrap_err();
835 assert!(
836 format!("{err}").contains("already open"),
837 "nested BEGIN should error; got: {err}"
838 );
839 assert!(db.in_transaction());
841 process_command("ROLLBACK;", &mut db).unwrap();
842 }
843
844 #[test]
845 fn orphan_commit_and_rollback_are_rejected() {
846 let mut db = seed_users_table();
847 let commit_err = process_command("COMMIT;", &mut db).unwrap_err();
848 assert!(format!("{commit_err}").contains("no transaction"));
849 let rollback_err = process_command("ROLLBACK;", &mut db).unwrap_err();
850 assert!(format!("{rollback_err}").contains("no transaction"));
851 }
852
853 #[test]
854 fn error_inside_transaction_keeps_txn_open() {
855 let mut db = seed_users_table();
859 process_command("BEGIN;", &mut db).unwrap();
860 let err = process_command("INSERT INTO nope (x) VALUES (1);", &mut db);
861 assert!(err.is_err());
862 assert!(db.in_transaction(), "txn should stay open after error");
863 process_command("ROLLBACK;", &mut db).unwrap();
864 }
865
866 fn seed_file_backed(name: &str, schema: &str) -> (std::path::PathBuf, Database) {
871 use crate::sql::pager::{open_database, save_database};
872 let mut p = std::env::temp_dir();
873 let pid = std::process::id();
874 let nanos = std::time::SystemTime::now()
875 .duration_since(std::time::UNIX_EPOCH)
876 .map(|d| d.as_nanos())
877 .unwrap_or(0);
878 p.push(format!("sqlrite-txn-{name}-{pid}-{nanos}.sqlrite"));
879
880 {
884 let mut seed = Database::new("t".to_string());
885 process_command(schema, &mut seed).unwrap();
886 save_database(&mut seed, &p).unwrap();
887 }
888 let db = open_database(&p, "t".to_string()).unwrap();
889 (p, db)
890 }
891
892 fn cleanup_file(path: &std::path::Path) {
893 let _ = std::fs::remove_file(path);
894 let mut wal = path.as_os_str().to_owned();
895 wal.push("-wal");
896 let _ = std::fs::remove_file(std::path::PathBuf::from(wal));
897 }
898
899 #[test]
900 fn begin_commit_rollback_round_trip_through_disk() {
901 use crate::sql::pager::open_database;
905
906 let (path, mut db) = seed_file_backed(
907 "roundtrip",
908 "CREATE TABLE notes (id INTEGER PRIMARY KEY, body TEXT);",
909 );
910
911 process_command("BEGIN;", &mut db).unwrap();
913 process_command("INSERT INTO notes (body) VALUES ('a');", &mut db).unwrap();
914 process_command("INSERT INTO notes (body) VALUES ('b');", &mut db).unwrap();
915 process_command("COMMIT;", &mut db).unwrap();
916
917 process_command("BEGIN;", &mut db).unwrap();
919 process_command("INSERT INTO notes (body) VALUES ('c');", &mut db).unwrap();
920 process_command("ROLLBACK;", &mut db).unwrap();
921
922 drop(db); let reopened = open_database(&path, "t".to_string()).unwrap();
925 let notes = reopened.get_table("notes".to_string()).unwrap();
926 assert_eq!(notes.rowids().len(), 2, "committed rows should survive");
927
928 drop(reopened);
929 cleanup_file(&path);
930 }
931
932 #[test]
933 fn write_inside_transaction_does_not_autosave() {
934 let (path, mut db) =
938 seed_file_backed("noas", "CREATE TABLE t (id INTEGER PRIMARY KEY, x TEXT);");
939
940 let mut wal_path = path.as_os_str().to_owned();
941 wal_path.push("-wal");
942 let wal_path = std::path::PathBuf::from(wal_path);
943 let frames_before = std::fs::metadata(&wal_path).unwrap().len();
944
945 process_command("BEGIN;", &mut db).unwrap();
946 process_command("INSERT INTO t (x) VALUES ('a');", &mut db).unwrap();
947 process_command("INSERT INTO t (x) VALUES ('b');", &mut db).unwrap();
948
949 let frames_mid = std::fs::metadata(&wal_path).unwrap().len();
951 assert_eq!(
952 frames_before, frames_mid,
953 "WAL should not grow during an open transaction"
954 );
955
956 process_command("COMMIT;", &mut db).unwrap();
957
958 drop(db); let fresh = crate::sql::pager::open_database(&path, "t".to_string()).unwrap();
960 assert_eq!(
961 fresh.get_table("t".to_string()).unwrap().rowids().len(),
962 2,
963 "COMMIT should have persisted both inserted rows"
964 );
965 drop(fresh);
966 cleanup_file(&path);
967 }
968
969 #[test]
970 fn rollback_undoes_create_table() {
971 let mut db = seed_users_table();
976 assert_eq!(db.tables.len(), 1);
977
978 process_command("BEGIN;", &mut db).unwrap();
979 process_command(
980 "CREATE TABLE dropme (id INTEGER PRIMARY KEY, x TEXT);",
981 &mut db,
982 )
983 .unwrap();
984 process_command("INSERT INTO dropme (x) VALUES ('stuff');", &mut db).unwrap();
985 assert_eq!(db.tables.len(), 2);
986
987 process_command("ROLLBACK;", &mut db).unwrap();
988 assert_eq!(
989 db.tables.len(),
990 1,
991 "CREATE TABLE should have been rolled back"
992 );
993 assert!(db.get_table("dropme".to_string()).is_err());
994 }
995
996 #[test]
997 fn rollback_restores_secondary_index_state() {
998 let mut db = Database::new("t".to_string());
1002 process_command(
1003 "CREATE TABLE users (id INTEGER PRIMARY KEY, email TEXT UNIQUE);",
1004 &mut db,
1005 )
1006 .unwrap();
1007 process_command("INSERT INTO users (email) VALUES ('a@x');", &mut db).unwrap();
1008
1009 process_command("BEGIN;", &mut db).unwrap();
1010 process_command("INSERT INTO users (email) VALUES ('b@x');", &mut db).unwrap();
1011 process_command("ROLLBACK;", &mut db).unwrap();
1013
1014 let reinsert = process_command("INSERT INTO users (email) VALUES ('b@x');", &mut db);
1018 assert!(
1019 reinsert.is_ok(),
1020 "re-insert after rollback should succeed, got {reinsert:?}"
1021 );
1022 }
1023
1024 #[test]
1025 fn rollback_restores_last_rowid_counter() {
1026 use crate::sql::db::table::Value;
1030
1031 let mut db = seed_users_table(); let pre = db.get_table("users".to_string()).unwrap().last_rowid;
1033
1034 process_command("BEGIN;", &mut db).unwrap();
1035 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();
1038
1039 let post = db.get_table("users".to_string()).unwrap().last_rowid;
1040 assert_eq!(pre, post, "last_rowid must roll back with the snapshot");
1041
1042 process_command("INSERT INTO users (name, age) VALUES ('d', 50);", &mut db).unwrap();
1044 let users = db.get_table("users".to_string()).unwrap();
1045 let d_rowid = users
1046 .rowids()
1047 .into_iter()
1048 .find(|r| users.get_value("name", *r) == Some(Value::Text("d".into())))
1049 .expect("d row must exist");
1050 assert_eq!(d_rowid, pre + 1);
1051 }
1052
1053 #[test]
1054 fn commit_on_in_memory_db_clears_txn_without_pager_call() {
1055 let mut db = seed_users_table(); assert!(db.source_path.is_none());
1060
1061 process_command("BEGIN;", &mut db).unwrap();
1062 process_command("INSERT INTO users (name, age) VALUES ('z', 99);", &mut db).unwrap();
1063 process_command("COMMIT;", &mut db).unwrap();
1064
1065 assert!(!db.in_transaction());
1066 assert_eq!(db.get_table("users".to_string()).unwrap().rowids().len(), 4);
1067 }
1068
1069 #[test]
1070 fn failed_commit_auto_rolls_back_in_memory_state() {
1071 use crate::sql::pager::save_database;
1085
1086 let (path, mut db) = seed_file_backed(
1088 "failcommit",
1089 "CREATE TABLE notes (id INTEGER PRIMARY KEY, body TEXT);",
1090 );
1091
1092 process_command("INSERT INTO notes (body) VALUES ('before');", &mut db).unwrap();
1094
1095 process_command("BEGIN;", &mut db).unwrap();
1097 process_command("INSERT INTO notes (body) VALUES ('inflight');", &mut db).unwrap();
1098 assert_eq!(
1099 db.get_table("notes".to_string()).unwrap().rowids().len(),
1100 2,
1101 "inflight row visible mid-txn"
1102 );
1103
1104 let orig_source = db.source_path.clone();
1108 let orig_pager = db.pager.take();
1109 db.source_path = Some(std::env::temp_dir());
1110
1111 let commit_result = process_command("COMMIT;", &mut db);
1112 assert!(commit_result.is_err(), "commit must fail");
1113 let err_str = format!("{}", commit_result.unwrap_err());
1114 assert!(
1115 err_str.contains("COMMIT failed") && err_str.contains("rolled back"),
1116 "error must surface auto-rollback; got: {err_str}"
1117 );
1118
1119 assert!(
1123 !db.in_transaction(),
1124 "txn must be cleared after auto-rollback"
1125 );
1126 assert_eq!(
1127 db.get_table("notes".to_string()).unwrap().rowids().len(),
1128 1,
1129 "inflight row must be rolled back"
1130 );
1131
1132 db.source_path = orig_source;
1135 db.pager = orig_pager;
1136 process_command("INSERT INTO notes (body) VALUES ('after');", &mut db).unwrap();
1137 drop(db);
1138
1139 let reopened = crate::sql::pager::open_database(&path, "t".to_string()).unwrap();
1141 let notes = reopened.get_table("notes".to_string()).unwrap();
1142 assert_eq!(notes.rowids().len(), 2);
1143 let _ = save_database; drop(reopened);
1146 cleanup_file(&path);
1147 }
1148
1149 #[test]
1150 fn begin_on_read_only_is_rejected() {
1151 use crate::sql::pager::{open_database_read_only, save_database};
1152
1153 let path = {
1154 let mut p = std::env::temp_dir();
1155 let pid = std::process::id();
1156 let nanos = std::time::SystemTime::now()
1157 .duration_since(std::time::UNIX_EPOCH)
1158 .map(|d| d.as_nanos())
1159 .unwrap_or(0);
1160 p.push(format!("sqlrite-txn-ro-{pid}-{nanos}.sqlrite"));
1161 p
1162 };
1163 {
1164 let mut seed = Database::new("t".to_string());
1165 process_command("CREATE TABLE t (id INTEGER PRIMARY KEY);", &mut seed).unwrap();
1166 save_database(&mut seed, &path).unwrap();
1167 }
1168
1169 let mut ro = open_database_read_only(&path, "t".to_string()).unwrap();
1170 let err = process_command("BEGIN;", &mut ro).unwrap_err();
1171 assert!(
1172 format!("{err}").contains("read-only"),
1173 "BEGIN on RO db should surface read-only; got: {err}"
1174 );
1175 assert!(!ro.in_transaction());
1176
1177 let _ = std::fs::remove_file(&path);
1178 let mut wal = path.as_os_str().to_owned();
1179 wal.push("-wal");
1180 let _ = std::fs::remove_file(std::path::PathBuf::from(wal));
1181 }
1182
1183 #[test]
1184 fn read_only_database_rejects_mutations_before_touching_state() {
1185 use crate::sql::pager::open_database_read_only;
1191
1192 let mut seed = Database::new("t".to_string());
1193 process_command(
1194 "CREATE TABLE notes (id INTEGER PRIMARY KEY, body TEXT);",
1195 &mut seed,
1196 )
1197 .unwrap();
1198 process_command("INSERT INTO notes (body) VALUES ('alpha');", &mut seed).unwrap();
1199
1200 let path = {
1201 let mut p = std::env::temp_dir();
1202 let pid = std::process::id();
1203 let nanos = std::time::SystemTime::now()
1204 .duration_since(std::time::UNIX_EPOCH)
1205 .map(|d| d.as_nanos())
1206 .unwrap_or(0);
1207 p.push(format!("sqlrite-ro-reject-{pid}-{nanos}.sqlrite"));
1208 p
1209 };
1210 crate::sql::pager::save_database(&mut seed, &path).unwrap();
1211 drop(seed);
1212
1213 let mut ro = open_database_read_only(&path, "t".to_string()).unwrap();
1214 let notes_before = ro.get_table("notes".to_string()).unwrap().rowids().len();
1215
1216 for stmt in [
1217 "INSERT INTO notes (body) VALUES ('beta');",
1218 "UPDATE notes SET body = 'x';",
1219 "DELETE FROM notes;",
1220 "CREATE TABLE more (id INTEGER PRIMARY KEY);",
1221 "CREATE INDEX notes_body ON notes (body);",
1222 ] {
1223 let err = process_command(stmt, &mut ro).unwrap_err();
1224 assert!(
1225 format!("{err}").contains("read-only"),
1226 "stmt {stmt:?} should surface a read-only error; got: {err}"
1227 );
1228 }
1229
1230 let notes_after = ro.get_table("notes".to_string()).unwrap().rowids().len();
1232 assert_eq!(notes_before, notes_after);
1233 let sel = process_command("SELECT * FROM notes;", &mut ro).expect("select on RO must work");
1234 assert!(sel.contains("1 row returned"));
1235
1236 drop(ro);
1238 let _ = std::fs::remove_file(&path);
1239 let mut wal = path.as_os_str().to_owned();
1240 wal.push("-wal");
1241 let _ = std::fs::remove_file(std::path::PathBuf::from(wal));
1242 }
1243
1244 #[test]
1249 fn vector_create_table_and_insert_basic() {
1250 let mut db = Database::new("tempdb".to_string());
1251 process_command(
1252 "CREATE TABLE docs (id INTEGER PRIMARY KEY, embedding VECTOR(3));",
1253 &mut db,
1254 )
1255 .expect("create table with VECTOR(3)");
1256 process_command(
1257 "INSERT INTO docs (embedding) VALUES ([0.1, 0.2, 0.3]);",
1258 &mut db,
1259 )
1260 .expect("insert vector");
1261
1262 let sel = process_command("SELECT * FROM docs;", &mut db).expect("select");
1266 assert!(sel.contains("1 row returned"));
1267
1268 let docs = db.get_table("docs".to_string()).expect("docs table");
1269 let rowids = docs.rowids();
1270 assert_eq!(rowids.len(), 1);
1271 match docs.get_value("embedding", rowids[0]) {
1272 Some(Value::Vector(v)) => assert_eq!(v, vec![0.1f32, 0.2, 0.3]),
1273 other => panic!("expected Value::Vector(...), got {other:?}"),
1274 }
1275 }
1276
1277 #[test]
1278 fn vector_dim_mismatch_at_insert_is_clean_error() {
1279 let mut db = Database::new("tempdb".to_string());
1280 process_command(
1281 "CREATE TABLE docs (id INTEGER PRIMARY KEY, embedding VECTOR(3));",
1282 &mut db,
1283 )
1284 .expect("create table");
1285
1286 let err = process_command("INSERT INTO docs (embedding) VALUES ([0.1, 0.2]);", &mut db)
1288 .unwrap_err();
1289 let msg = format!("{err}");
1290 assert!(
1291 msg.to_lowercase().contains("dimension")
1292 && msg.contains("declared 3")
1293 && msg.contains("got 2"),
1294 "expected clear dim-mismatch error, got: {msg}"
1295 );
1296
1297 let err = process_command(
1299 "INSERT INTO docs (embedding) VALUES ([0.1, 0.2, 0.3, 0.4, 0.5]);",
1300 &mut db,
1301 )
1302 .unwrap_err();
1303 assert!(
1304 format!("{err}").contains("got 5"),
1305 "expected dim-mismatch error mentioning got 5, got: {err}"
1306 );
1307 }
1308
1309 #[test]
1310 fn vector_create_table_rejects_missing_dim() {
1311 let mut db = Database::new("tempdb".to_string());
1312 let result = process_command(
1318 "CREATE TABLE docs (id INTEGER PRIMARY KEY, embedding VECTOR);",
1319 &mut db,
1320 );
1321 assert!(
1322 result.is_err(),
1323 "expected CREATE TABLE with bare VECTOR to fail (no dim)"
1324 );
1325 }
1326
1327 #[test]
1328 fn vector_create_table_rejects_zero_dim() {
1329 let mut db = Database::new("tempdb".to_string());
1330 let err = process_command(
1331 "CREATE TABLE docs (id INTEGER PRIMARY KEY, embedding VECTOR(0));",
1332 &mut db,
1333 )
1334 .unwrap_err();
1335 let msg = format!("{err}");
1336 assert!(
1337 msg.to_lowercase().contains("vector"),
1338 "expected VECTOR-related error for VECTOR(0), got: {msg}"
1339 );
1340 }
1341
1342 #[test]
1343 fn vector_high_dim_works() {
1344 let mut db = Database::new("tempdb".to_string());
1347 process_command(
1348 "CREATE TABLE embeddings (id INTEGER PRIMARY KEY, e VECTOR(384));",
1349 &mut db,
1350 )
1351 .expect("create table VECTOR(384)");
1352
1353 let lit = format!(
1354 "[{}]",
1355 (0..384)
1356 .map(|i| format!("{}", i as f32 * 0.001))
1357 .collect::<Vec<_>>()
1358 .join(",")
1359 );
1360 let sql = format!("INSERT INTO embeddings (e) VALUES ({lit});");
1361 process_command(&sql, &mut db).expect("insert 384-dim vector");
1362
1363 let sel = process_command("SELECT id FROM embeddings;", &mut db).expect("select id");
1364 assert!(sel.contains("1 row returned"));
1365 }
1366
1367 #[test]
1368 fn vector_multiple_rows() {
1369 let mut db = Database::new("tempdb".to_string());
1372 process_command(
1373 "CREATE TABLE docs (id INTEGER PRIMARY KEY, e VECTOR(2));",
1374 &mut db,
1375 )
1376 .expect("create");
1377 for i in 0..3 {
1378 let sql = format!("INSERT INTO docs (e) VALUES ([{i}.0, {}.0]);", i + 1);
1379 process_command(&sql, &mut db).expect("insert");
1380 }
1381 let sel = process_command("SELECT * FROM docs;", &mut db).expect("select");
1382 assert!(sel.contains("3 rows returned"));
1383
1384 let docs = db.get_table("docs".to_string()).expect("docs table");
1386 let rowids = docs.rowids();
1387 assert_eq!(rowids.len(), 3);
1388 let mut vectors: Vec<Vec<f32>> = rowids
1389 .iter()
1390 .filter_map(|r| match docs.get_value("e", *r) {
1391 Some(Value::Vector(v)) => Some(v),
1392 _ => None,
1393 })
1394 .collect();
1395 vectors.sort_by(|a, b| a[0].partial_cmp(&b[0]).unwrap());
1396 assert_eq!(vectors[0], vec![0.0f32, 1.0]);
1397 assert_eq!(vectors[1], vec![1.0f32, 2.0]);
1398 assert_eq!(vectors[2], vec![2.0f32, 3.0]);
1399 }
1400
1401 fn seed_hnsw_table() -> Database {
1409 let mut db = Database::new("tempdb".to_string());
1410 process_command(
1411 "CREATE TABLE docs (id INTEGER PRIMARY KEY, e VECTOR(2));",
1412 &mut db,
1413 )
1414 .unwrap();
1415 for v in &[
1416 "[1.0, 0.0]", "[2.0, 0.0]", "[0.0, 3.0]", "[1.0, 4.0]", "[10.0, 10.0]", ] {
1422 process_command(&format!("INSERT INTO docs (e) VALUES ({v});"), &mut db).unwrap();
1423 }
1424 db
1425 }
1426
1427 #[test]
1428 fn create_index_using_hnsw_succeeds() {
1429 let mut db = seed_hnsw_table();
1430 let resp = process_command("CREATE INDEX ix_e ON docs USING hnsw (e);", &mut db).unwrap();
1431 assert!(resp.to_lowercase().contains("create index"));
1432 let table = db.get_table("docs".to_string()).unwrap();
1434 assert_eq!(table.hnsw_indexes.len(), 1);
1435 assert_eq!(table.hnsw_indexes[0].name, "ix_e");
1436 assert_eq!(table.hnsw_indexes[0].column_name, "e");
1437 assert_eq!(table.hnsw_indexes[0].index.len(), 5);
1439 }
1440
1441 #[test]
1442 fn create_index_using_hnsw_rejects_non_vector_column() {
1443 let mut db = Database::new("tempdb".to_string());
1444 process_command(
1445 "CREATE TABLE t (id INTEGER PRIMARY KEY, name TEXT);",
1446 &mut db,
1447 )
1448 .unwrap();
1449 let err =
1450 process_command("CREATE INDEX ix_name ON t USING hnsw (name);", &mut db).unwrap_err();
1451 let msg = format!("{err}");
1452 assert!(
1453 msg.to_lowercase().contains("vector"),
1454 "expected error mentioning VECTOR; got: {msg}"
1455 );
1456 }
1457
1458 #[test]
1459 fn knn_query_uses_hnsw_after_create_index() {
1460 let mut db = seed_hnsw_table();
1466 process_command("CREATE INDEX ix_e ON docs USING hnsw (e);", &mut db).unwrap();
1467
1468 let resp = process_command(
1473 "SELECT id FROM docs ORDER BY vec_distance_l2(e, [1.0, 0.0]) ASC LIMIT 3;",
1474 &mut db,
1475 )
1476 .unwrap();
1477 assert!(resp.contains("3 rows returned"), "got: {resp}");
1478 }
1479
1480 #[test]
1481 fn knn_query_works_after_subsequent_inserts() {
1482 let mut db = seed_hnsw_table();
1486 process_command("CREATE INDEX ix_e ON docs USING hnsw (e);", &mut db).unwrap();
1487 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();
1491 assert_eq!(
1492 table.hnsw_indexes[0].index.len(),
1493 7,
1494 "incremental insert should grow HNSW alongside row storage"
1495 );
1496
1497 let resp = process_command(
1500 "SELECT id FROM docs ORDER BY vec_distance_l2(e, [0.0, 0.0]) ASC LIMIT 1;",
1501 &mut db,
1502 )
1503 .unwrap();
1504 assert!(resp.contains("1 row returned"), "got: {resp}");
1505 }
1506
1507 #[test]
1513 fn delete_on_hnsw_indexed_table_succeeds_and_marks_dirty() {
1514 let mut db = seed_hnsw_table();
1515 process_command("CREATE INDEX ix_e ON docs USING hnsw (e);", &mut db).unwrap();
1516 let resp = process_command("DELETE FROM docs WHERE id = 1;", &mut db).unwrap();
1517 assert!(resp.contains("1 row"), "expected 1 row deleted: {resp}");
1518
1519 let docs = db.get_table("docs".to_string()).unwrap();
1520 let entry = docs.hnsw_indexes.iter().find(|e| e.name == "ix_e").unwrap();
1521 assert!(
1522 entry.needs_rebuild,
1523 "DELETE should have marked HNSW index dirty for rebuild on next save"
1524 );
1525 }
1526
1527 #[test]
1528 fn update_on_hnsw_indexed_vector_col_succeeds_and_marks_dirty() {
1529 let mut db = seed_hnsw_table();
1530 process_command("CREATE INDEX ix_e ON docs USING hnsw (e);", &mut db).unwrap();
1531 let resp =
1532 process_command("UPDATE docs SET e = [9.0, 9.0] WHERE id = 1;", &mut db).unwrap();
1533 assert!(resp.contains("1 row"), "expected 1 row updated: {resp}");
1534
1535 let docs = db.get_table("docs".to_string()).unwrap();
1536 let entry = docs.hnsw_indexes.iter().find(|e| e.name == "ix_e").unwrap();
1537 assert!(
1538 entry.needs_rebuild,
1539 "UPDATE on the vector column should have marked HNSW index dirty"
1540 );
1541 }
1542
1543 #[test]
1544 fn duplicate_index_name_errors() {
1545 let mut db = seed_hnsw_table();
1546 process_command("CREATE INDEX ix_e ON docs USING hnsw (e);", &mut db).unwrap();
1547 let err =
1548 process_command("CREATE INDEX ix_e ON docs USING hnsw (e);", &mut db).unwrap_err();
1549 let msg = format!("{err}");
1550 assert!(
1551 msg.to_lowercase().contains("already exists"),
1552 "expected duplicate-index error; got: {msg}"
1553 );
1554 }
1555
1556 #[test]
1557 fn index_if_not_exists_is_idempotent() {
1558 let mut db = seed_hnsw_table();
1559 process_command("CREATE INDEX ix_e ON docs USING hnsw (e);", &mut db).unwrap();
1560 process_command(
1562 "CREATE INDEX IF NOT EXISTS ix_e ON docs USING hnsw (e);",
1563 &mut db,
1564 )
1565 .unwrap();
1566 let table = db.get_table("docs".to_string()).unwrap();
1567 assert_eq!(table.hnsw_indexes.len(), 1);
1568 }
1569
1570 fn seed_fts_table() -> Database {
1577 let mut db = Database::new("tempdb".to_string());
1578 process_command(
1579 "CREATE TABLE docs (id INTEGER PRIMARY KEY, body TEXT);",
1580 &mut db,
1581 )
1582 .unwrap();
1583 for body in &[
1584 "rust embedded database", "rust web framework", "go embedded systems", "python web framework", "rust rust rust embedded power", ] {
1590 process_command(
1591 &format!("INSERT INTO docs (body) VALUES ('{body}');"),
1592 &mut db,
1593 )
1594 .unwrap();
1595 }
1596 db
1597 }
1598
1599 #[test]
1600 fn create_index_using_fts_succeeds_and_indexes_existing_rows() {
1601 let mut db = seed_fts_table();
1602 let resp =
1603 process_command("CREATE INDEX ix_body ON docs USING fts (body);", &mut db).unwrap();
1604 assert!(resp.to_lowercase().contains("create index"), "got {resp}");
1605 let table = db.get_table("docs".to_string()).unwrap();
1606 assert_eq!(table.fts_indexes.len(), 1);
1607 assert_eq!(table.fts_indexes[0].name, "ix_body");
1608 assert_eq!(table.fts_indexes[0].column_name, "body");
1609 assert_eq!(table.fts_indexes[0].index.len(), 5);
1611 }
1612
1613 #[test]
1614 fn create_index_using_fts_rejects_non_text_column() {
1615 let mut db = Database::new("tempdb".to_string());
1616 process_command(
1617 "CREATE TABLE t (id INTEGER PRIMARY KEY, n INTEGER);",
1618 &mut db,
1619 )
1620 .unwrap();
1621 let err = process_command("CREATE INDEX ix_n ON t USING fts (n);", &mut db).unwrap_err();
1622 let msg = format!("{err}");
1623 assert!(
1624 msg.to_lowercase().contains("text"),
1625 "expected error mentioning TEXT; got: {msg}"
1626 );
1627 }
1628
1629 #[test]
1630 fn fts_match_returns_expected_rows() {
1631 let mut db = seed_fts_table();
1632 process_command("CREATE INDEX ix_body ON docs USING fts (body);", &mut db).unwrap();
1633 let resp = process_command(
1635 "SELECT id FROM docs WHERE fts_match(body, 'rust');",
1636 &mut db,
1637 )
1638 .unwrap();
1639 assert!(resp.contains("3 rows returned"), "got: {resp}");
1640 }
1641
1642 #[test]
1643 fn fts_match_without_index_errors_clearly() {
1644 let mut db = seed_fts_table();
1645 let err = process_command(
1647 "SELECT id FROM docs WHERE fts_match(body, 'rust');",
1648 &mut db,
1649 )
1650 .unwrap_err();
1651 let msg = format!("{err}");
1652 assert!(
1653 msg.contains("no FTS index"),
1654 "expected no-index error; got: {msg}"
1655 );
1656 }
1657
1658 #[test]
1659 fn bm25_score_orders_descending_by_relevance() {
1660 let mut db = seed_fts_table();
1661 process_command("CREATE INDEX ix_body ON docs USING fts (body);", &mut db).unwrap();
1662 let out = process_command_with_render(
1665 "SELECT id FROM docs WHERE fts_match(body, 'rust') \
1666 ORDER BY bm25_score(body, 'rust') DESC LIMIT 1;",
1667 &mut db,
1668 )
1669 .unwrap();
1670 assert!(out.status.contains("1 row returned"), "got: {}", out.status);
1671 let rendered = out.rendered.expect("SELECT should produce rendered output");
1672 assert!(
1674 rendered.contains(" 5 "),
1675 "expected id=5 to be top-ranked; rendered:\n{rendered}"
1676 );
1677 }
1678
1679 #[test]
1680 fn bm25_score_without_index_errors_clearly() {
1681 let mut db = seed_fts_table();
1682 let err = process_command(
1683 "SELECT id FROM docs ORDER BY bm25_score(body, 'rust') DESC LIMIT 1;",
1684 &mut db,
1685 )
1686 .unwrap_err();
1687 let msg = format!("{err}");
1688 assert!(
1689 msg.contains("no FTS index"),
1690 "expected no-index error; got: {msg}"
1691 );
1692 }
1693
1694 #[test]
1695 fn fts_post_create_inserts_are_indexed_incrementally() {
1696 let mut db = seed_fts_table();
1697 process_command("CREATE INDEX ix_body ON docs USING fts (body);", &mut db).unwrap();
1698 process_command(
1699 "INSERT INTO docs (body) VALUES ('rust embedded analytics');",
1700 &mut db,
1701 )
1702 .unwrap();
1703 let table = db.get_table("docs".to_string()).unwrap();
1704 assert_eq!(table.fts_indexes[0].index.len(), 6);
1706 let resp = process_command(
1708 "SELECT id FROM docs WHERE fts_match(body, 'analytics');",
1709 &mut db,
1710 )
1711 .unwrap();
1712 assert!(resp.contains("1 row returned"), "got: {resp}");
1713 }
1714
1715 #[test]
1716 fn delete_on_fts_indexed_table_marks_dirty() {
1717 let mut db = seed_fts_table();
1718 process_command("CREATE INDEX ix_body ON docs USING fts (body);", &mut db).unwrap();
1719 let resp = process_command("DELETE FROM docs WHERE id = 1;", &mut db).unwrap();
1720 assert!(resp.contains("1 row"), "got: {resp}");
1721 let docs = db.get_table("docs".to_string()).unwrap();
1722 let entry = docs
1723 .fts_indexes
1724 .iter()
1725 .find(|e| e.name == "ix_body")
1726 .unwrap();
1727 assert!(
1728 entry.needs_rebuild,
1729 "DELETE should have flagged the FTS index dirty"
1730 );
1731 }
1732
1733 #[test]
1734 fn update_on_fts_indexed_text_col_marks_dirty() {
1735 let mut db = seed_fts_table();
1736 process_command("CREATE INDEX ix_body ON docs USING fts (body);", &mut db).unwrap();
1737 let resp = process_command(
1738 "UPDATE docs SET body = 'java spring framework' WHERE id = 1;",
1739 &mut db,
1740 )
1741 .unwrap();
1742 assert!(resp.contains("1 row"), "got: {resp}");
1743 let docs = db.get_table("docs".to_string()).unwrap();
1744 let entry = docs
1745 .fts_indexes
1746 .iter()
1747 .find(|e| e.name == "ix_body")
1748 .unwrap();
1749 assert!(
1750 entry.needs_rebuild,
1751 "UPDATE on the indexed TEXT column should have flagged dirty"
1752 );
1753 }
1754
1755 #[test]
1756 fn fts_index_name_collides_with_btree_and_hnsw_namespaces() {
1757 let mut db = seed_fts_table();
1758 process_command("CREATE INDEX ix_body ON docs USING fts (body);", &mut db).unwrap();
1759 let err = process_command("CREATE INDEX ix_body ON docs (body);", &mut db).unwrap_err();
1760 let msg = format!("{err}");
1761 assert!(
1762 msg.to_lowercase().contains("already exists"),
1763 "expected duplicate-index error; got: {msg}"
1764 );
1765 }
1766
1767 #[test]
1768 fn fts_index_rejects_unique() {
1769 let mut db = seed_fts_table();
1770 let err = process_command(
1771 "CREATE UNIQUE INDEX ix_body ON docs USING fts (body);",
1772 &mut db,
1773 )
1774 .unwrap_err();
1775 let msg = format!("{err}");
1776 assert!(
1777 msg.to_lowercase().contains("unique"),
1778 "expected UNIQUE-rejection error; got: {msg}"
1779 );
1780 }
1781
1782 #[test]
1783 fn try_fts_probe_falls_through_on_ascending() {
1784 let mut db = seed_fts_table();
1789 process_command("CREATE INDEX ix_body ON docs USING fts (body);", &mut db).unwrap();
1790 let resp = process_command(
1793 "SELECT id FROM docs WHERE fts_match(body, 'rust') \
1794 ORDER BY bm25_score(body, 'rust') ASC LIMIT 3;",
1795 &mut db,
1796 )
1797 .unwrap();
1798 assert!(resp.contains("3 rows returned"), "got: {resp}");
1799 }
1800
1801 fn seed_vector_docs() -> Database {
1811 let mut db = Database::new("tempdb".to_string());
1812 process_command(
1813 "CREATE TABLE docs (id INTEGER PRIMARY KEY, e VECTOR(2));",
1814 &mut db,
1815 )
1816 .expect("create");
1817 process_command("INSERT INTO docs (e) VALUES ([1.0, 0.0]);", &mut db).expect("insert 1");
1818 process_command("INSERT INTO docs (e) VALUES ([0.0, 1.0]);", &mut db).expect("insert 2");
1819 process_command("INSERT INTO docs (e) VALUES ([1.0, 1.0]);", &mut db).expect("insert 3");
1820 db
1821 }
1822
1823 #[test]
1824 fn vec_distance_l2_in_where_filters_correctly() {
1825 let mut db = seed_vector_docs();
1831 let resp = process_command(
1832 "SELECT * FROM docs WHERE vec_distance_l2(e, [1.0, 0.0]) < 1.1;",
1833 &mut db,
1834 )
1835 .expect("select");
1836 assert!(
1837 resp.contains("2 rows returned"),
1838 "expected 2 rows, got: {resp}"
1839 );
1840 }
1841
1842 #[test]
1843 fn vec_distance_cosine_in_where() {
1844 let mut db = seed_vector_docs();
1849 let resp = process_command(
1850 "SELECT * FROM docs WHERE vec_distance_cosine(e, [1.0, 0.0]) < 0.5;",
1851 &mut db,
1852 )
1853 .expect("select");
1854 assert!(
1855 resp.contains("2 rows returned"),
1856 "expected 2 rows, got: {resp}"
1857 );
1858 }
1859
1860 #[test]
1861 fn vec_distance_dot_negated() {
1862 let mut db = seed_vector_docs();
1867 let resp = process_command(
1868 "SELECT * FROM docs WHERE vec_distance_dot(e, [1.0, 0.0]) < 0.0;",
1869 &mut db,
1870 )
1871 .expect("select");
1872 assert!(
1873 resp.contains("2 rows returned"),
1874 "expected 2 rows, got: {resp}"
1875 );
1876 }
1877
1878 #[test]
1879 fn knn_via_order_by_distance_limit() {
1880 let mut db = seed_vector_docs();
1884 let resp = process_command(
1885 "SELECT id FROM docs ORDER BY vec_distance_l2(e, [1.0, 0.0]) ASC LIMIT 2;",
1886 &mut db,
1887 )
1888 .expect("select");
1889 assert!(
1890 resp.contains("2 rows returned"),
1891 "expected 2 rows, got: {resp}"
1892 );
1893 }
1894
1895 #[test]
1896 fn distance_function_dim_mismatch_errors() {
1897 let mut db = seed_vector_docs();
1899 let err = process_command(
1900 "SELECT * FROM docs WHERE vec_distance_l2(e, [1.0, 0.0, 0.0]) < 1.0;",
1901 &mut db,
1902 )
1903 .unwrap_err();
1904 let msg = format!("{err}");
1905 assert!(
1906 msg.to_lowercase().contains("dimension")
1907 && msg.contains("lhs=2")
1908 && msg.contains("rhs=3"),
1909 "expected dim mismatch error, got: {msg}"
1910 );
1911 }
1912
1913 #[test]
1914 fn unknown_function_errors_with_name() {
1915 let mut db = seed_vector_docs();
1919 let err = process_command(
1920 "SELECT * FROM docs WHERE vec_does_not_exist(e, [1.0, 0.0]) < 1.0;",
1921 &mut db,
1922 )
1923 .unwrap_err();
1924 let msg = format!("{err}");
1925 assert!(
1926 msg.contains("vec_does_not_exist"),
1927 "expected error mentioning function name, got: {msg}"
1928 );
1929 }
1930
1931 fn seed_json_table() -> Database {
1936 let mut db = Database::new("tempdb".to_string());
1937 process_command(
1938 "CREATE TABLE docs (id INTEGER PRIMARY KEY, payload JSON);",
1939 &mut db,
1940 )
1941 .expect("create json table");
1942 db
1943 }
1944
1945 #[test]
1946 fn json_column_round_trip_primitive_values() {
1947 let mut db = seed_json_table();
1948 process_command(
1949 r#"INSERT INTO docs (payload) VALUES ('{"name": "alice", "age": 30}');"#,
1950 &mut db,
1951 )
1952 .expect("insert json");
1953 let docs = db.get_table("docs".to_string()).unwrap();
1954 let rowids = docs.rowids();
1955 assert_eq!(rowids.len(), 1);
1956 match docs.get_value("payload", rowids[0]) {
1958 Some(Value::Text(s)) => {
1959 assert!(s.contains("alice"), "expected JSON text to round-trip: {s}");
1960 }
1961 other => panic!("expected Value::Text holding JSON, got {other:?}"),
1962 }
1963 }
1964
1965 #[test]
1966 fn json_insert_rejects_invalid_json() {
1967 let mut db = seed_json_table();
1968 let err = process_command(
1969 "INSERT INTO docs (payload) VALUES ('not-valid-json{');",
1970 &mut db,
1971 )
1972 .unwrap_err();
1973 let msg = format!("{err}").to_lowercase();
1974 assert!(
1975 msg.contains("json") && msg.contains("payload"),
1976 "expected JSON validation error mentioning column, got: {msg}"
1977 );
1978 }
1979
1980 #[test]
1981 fn json_extract_object_field() {
1982 let mut db = seed_json_table();
1983 process_command(
1984 r#"INSERT INTO docs (payload) VALUES ('{"name": "alice", "age": 30}');"#,
1985 &mut db,
1986 )
1987 .unwrap();
1988 let resp = process_command(
1991 r#"SELECT id FROM docs WHERE json_extract(payload, '$.name') = 'alice';"#,
1992 &mut db,
1993 )
1994 .expect("select via json_extract");
1995 assert!(resp.contains("1 row returned"), "got: {resp}");
1996
1997 let resp = process_command(
1998 r#"SELECT id FROM docs WHERE json_extract(payload, '$.age') = 30;"#,
1999 &mut db,
2000 )
2001 .expect("select via numeric json_extract");
2002 assert!(resp.contains("1 row returned"), "got: {resp}");
2003 }
2004
2005 #[test]
2006 fn json_extract_array_index_and_nested() {
2007 let mut db = seed_json_table();
2008 process_command(
2009 r#"INSERT INTO docs (payload) VALUES ('{"tags": ["rust", "sql", "vectors"], "meta": {"author": "joao"}}');"#,
2010 &mut db,
2011 )
2012 .unwrap();
2013 let resp = process_command(
2014 r#"SELECT id FROM docs WHERE json_extract(payload, '$.tags[0]') = 'rust';"#,
2015 &mut db,
2016 )
2017 .expect("select via array index");
2018 assert!(resp.contains("1 row returned"), "got: {resp}");
2019
2020 let resp = process_command(
2021 r#"SELECT id FROM docs WHERE json_extract(payload, '$.meta.author') = 'joao';"#,
2022 &mut db,
2023 )
2024 .expect("select via nested object");
2025 assert!(resp.contains("1 row returned"), "got: {resp}");
2026 }
2027
2028 #[test]
2029 fn json_extract_missing_path_returns_null() {
2030 let mut db = seed_json_table();
2031 process_command(
2032 r#"INSERT INTO docs (payload) VALUES ('{"name": "alice"}');"#,
2033 &mut db,
2034 )
2035 .unwrap();
2036 let resp = process_command(
2039 r#"SELECT id FROM docs WHERE json_extract(payload, '$.missing') = 'something';"#,
2040 &mut db,
2041 )
2042 .expect("select with missing path");
2043 assert!(resp.contains("0 rows returned"), "got: {resp}");
2044 }
2045
2046 #[test]
2047 fn json_extract_malformed_path_errors() {
2048 let mut db = seed_json_table();
2049 process_command(
2050 r#"INSERT INTO docs (payload) VALUES ('{"a": 1}');"#,
2051 &mut db,
2052 )
2053 .unwrap();
2054 let err = process_command(
2056 r#"SELECT id FROM docs WHERE json_extract(payload, 'a.b') = 1;"#,
2057 &mut db,
2058 )
2059 .unwrap_err();
2060 assert!(format!("{err}").contains("'$'"));
2061 }
2062
2063 #[test]
2064 fn json_array_length_on_array() {
2065 let mut db = seed_json_table();
2068 process_command(
2069 r#"INSERT INTO docs (payload) VALUES ('{"tags": ["a", "b", "c"]}');"#,
2070 &mut db,
2071 )
2072 .unwrap();
2073 let resp = process_command(
2074 r#"SELECT id FROM docs WHERE json_array_length(payload, '$.tags') = 3;"#,
2075 &mut db,
2076 )
2077 .expect("select via array_length");
2078 assert!(resp.contains("1 row returned"), "got: {resp}");
2079 }
2080
2081 #[test]
2082 fn json_array_length_on_non_array_errors() {
2083 let mut db = seed_json_table();
2084 process_command(
2085 r#"INSERT INTO docs (payload) VALUES ('{"tags": "not-an-array"}');"#,
2086 &mut db,
2087 )
2088 .unwrap();
2089 let err = process_command(
2090 r#"SELECT id FROM docs WHERE json_array_length(payload, '$.tags') = 1;"#,
2091 &mut db,
2092 )
2093 .unwrap_err();
2094 let msg = format!("{err}").to_lowercase();
2095 assert!(
2096 msg.contains("non-array"),
2097 "expected non-array error, got: {msg}"
2098 );
2099 }
2100
2101 #[test]
2102 fn json_type_recognizes_each_kind() {
2103 let mut db = seed_json_table();
2104 process_command(
2105 r#"INSERT INTO docs (payload) VALUES ('{"o": {}, "a": [], "s": "x", "i": 1, "f": 1.5, "t": true, "n": null}');"#,
2106 &mut db,
2107 )
2108 .unwrap();
2109 let cases = &[
2110 ("$.o", "object"),
2111 ("$.a", "array"),
2112 ("$.s", "text"),
2113 ("$.i", "integer"),
2114 ("$.f", "real"),
2115 ("$.t", "true"),
2116 ("$.n", "null"),
2117 ];
2118 for (path, expected_type) in cases {
2119 let sql = format!(
2120 "SELECT id FROM docs WHERE json_type(payload, '{path}') = '{expected_type}';"
2121 );
2122 let resp =
2123 process_command(&sql, &mut db).unwrap_or_else(|e| panic!("path {path}: {e}"));
2124 assert!(
2125 resp.contains("1 row returned"),
2126 "path {path} expected type {expected_type}; got response: {resp}"
2127 );
2128 }
2129 }
2130
2131 #[test]
2132 fn update_on_json_column_revalidates() {
2133 let mut db = seed_json_table();
2134 process_command(
2135 r#"INSERT INTO docs (payload) VALUES ('{"a": 1}');"#,
2136 &mut db,
2137 )
2138 .unwrap();
2139 process_command(
2141 r#"UPDATE docs SET payload = '{"a": 2, "b": 3}' WHERE id = 1;"#,
2142 &mut db,
2143 )
2144 .expect("valid JSON UPDATE");
2145 let err = process_command(
2148 r#"UPDATE docs SET payload = 'not-json{' WHERE id = 1;"#,
2149 &mut db,
2150 )
2151 .unwrap_err();
2152 let msg = format!("{err}").to_lowercase();
2153 assert!(
2154 msg.contains("json") && msg.contains("payload"),
2155 "got: {msg}"
2156 );
2157 }
2158
2159 #[test]
2164 fn default_literal_int_applies_when_column_omitted() {
2165 let mut db = Database::new("t".to_string());
2166 process_command(
2167 "CREATE TABLE t (id INTEGER PRIMARY KEY, n INTEGER DEFAULT 42);",
2168 &mut db,
2169 )
2170 .unwrap();
2171 process_command("INSERT INTO t (id) VALUES (1);", &mut db).unwrap();
2172
2173 let table = db.get_table("t".to_string()).unwrap();
2174 assert_eq!(table.get_value("n", 1), Some(Value::Integer(42)));
2175 }
2176
2177 #[test]
2178 fn default_literal_text_applies_when_column_omitted() {
2179 let mut db = Database::new("t".to_string());
2180 process_command(
2181 "CREATE TABLE users (id INTEGER PRIMARY KEY, status TEXT DEFAULT 'active');",
2182 &mut db,
2183 )
2184 .unwrap();
2185 process_command("INSERT INTO users (id) VALUES (1);", &mut db).unwrap();
2186
2187 let table = db.get_table("users".to_string()).unwrap();
2188 assert_eq!(
2189 table.get_value("status", 1),
2190 Some(Value::Text("active".to_string()))
2191 );
2192 }
2193
2194 #[test]
2195 fn default_literal_real_negative_applies_when_column_omitted() {
2196 let mut db = Database::new("t".to_string());
2198 process_command(
2199 "CREATE TABLE t (id INTEGER PRIMARY KEY, score REAL DEFAULT -1.5);",
2200 &mut db,
2201 )
2202 .unwrap();
2203 process_command("INSERT INTO t (id) VALUES (1);", &mut db).unwrap();
2204
2205 let table = db.get_table("t".to_string()).unwrap();
2206 assert_eq!(table.get_value("score", 1), Some(Value::Real(-1.5)));
2207 }
2208
2209 #[test]
2210 fn default_with_type_mismatch_errors_at_create_time() {
2211 let mut db = Database::new("t".to_string());
2212 let result = process_command(
2213 "CREATE TABLE t (id INTEGER PRIMARY KEY, n INTEGER DEFAULT 'oops');",
2214 &mut db,
2215 );
2216 let err = result.expect_err("text default on INTEGER column should be rejected");
2217 let msg = format!("{err}").to_lowercase();
2218 assert!(msg.contains("default"), "got: {msg}");
2219 }
2220
2221 #[test]
2222 fn default_for_json_column_must_be_valid_json() {
2223 let mut db = Database::new("t".to_string());
2227 let err = process_command(
2228 "CREATE TABLE t (id INTEGER PRIMARY KEY, doc JSON DEFAULT 'not-json{');",
2229 &mut db,
2230 )
2231 .unwrap_err();
2232 assert!(
2233 format!("{err}").to_lowercase().contains("json"),
2234 "got: {err}"
2235 );
2236
2237 process_command(
2239 "CREATE TABLE t2 (id INTEGER PRIMARY KEY, doc JSON DEFAULT '{\"k\":1}');",
2240 &mut db,
2241 )
2242 .expect("valid JSON DEFAULT should be accepted");
2243 }
2244
2245 #[test]
2246 fn default_with_non_literal_expression_errors_at_create_time() {
2247 let mut db = Database::new("t".to_string());
2248 let result = process_command(
2251 "CREATE TABLE t (id INTEGER PRIMARY KEY, ts TEXT DEFAULT CURRENT_TIMESTAMP);",
2252 &mut db,
2253 );
2254 let err = result.expect_err("non-literal DEFAULT should be rejected");
2255 let msg = format!("{err}").to_lowercase();
2256 assert!(
2257 msg.contains("default") && msg.contains("literal"),
2258 "got: {msg}"
2259 );
2260 }
2261
2262 #[test]
2263 fn default_null_is_accepted_at_create_time() {
2264 let mut db = Database::new("t".to_string());
2268 process_command(
2269 "CREATE TABLE t (id INTEGER PRIMARY KEY, note TEXT DEFAULT NULL);",
2270 &mut db,
2271 )
2272 .expect("CREATE TABLE with DEFAULT NULL should be accepted");
2273 let table = db.get_table("t".to_string()).unwrap();
2274 let note = table
2275 .columns
2276 .iter()
2277 .find(|c| c.column_name == "note")
2278 .unwrap();
2279 assert_eq!(note.default, Some(Value::Null));
2280 }
2281
2282 #[test]
2287 fn drop_table_basic() {
2288 let mut db = seed_users_table();
2289 let response = process_command("DROP TABLE users;", &mut db).expect("drop table");
2290 assert!(response.contains("1 table dropped"));
2291 assert!(!db.contains_table("users".to_string()));
2292 }
2293
2294 #[test]
2295 fn drop_table_if_exists_noop_on_missing() {
2296 let mut db = Database::new("t".to_string());
2297 let response =
2298 process_command("DROP TABLE IF EXISTS missing;", &mut db).expect("drop if exists");
2299 assert!(response.contains("0 tables dropped"));
2300 }
2301
2302 #[test]
2303 fn drop_table_missing_errors_without_if_exists() {
2304 let mut db = Database::new("t".to_string());
2305 let err = process_command("DROP TABLE missing;", &mut db).unwrap_err();
2306 assert!(format!("{err}").contains("does not exist"), "got: {err}");
2307 }
2308
2309 #[test]
2310 fn drop_table_reserved_name_errors() {
2311 let mut db = Database::new("t".to_string());
2312 let err = process_command("DROP TABLE sqlrite_master;", &mut db).unwrap_err();
2313 assert!(format!("{err}").contains("reserved"), "got: {err}");
2314 }
2315
2316 #[test]
2317 fn drop_table_multi_target_rejected() {
2318 let mut db = seed_users_table();
2319 process_command("CREATE TABLE other (id INTEGER PRIMARY KEY);", &mut db).unwrap();
2320 let err = process_command("DROP TABLE users, other;", &mut db).unwrap_err();
2323 assert!(format!("{err}").contains("single table"), "got: {err}");
2324 }
2325
2326 #[test]
2327 fn drop_table_cascades_indexes_in_memory() {
2328 let mut db = seed_users_table();
2329 process_command("CREATE INDEX users_age_idx ON users (age);", &mut db).unwrap();
2330 let users = db.get_table("users".to_string()).unwrap();
2332 assert!(
2333 users
2334 .secondary_indexes
2335 .iter()
2336 .any(|i| i.name == "users_age_idx")
2337 );
2338
2339 process_command("DROP TABLE users;", &mut db).unwrap();
2340
2341 for table in db.tables.values() {
2343 assert!(
2344 !table
2345 .secondary_indexes
2346 .iter()
2347 .any(|i| i.name.contains("users")),
2348 "dropped table's indexes should not survive on any other table"
2349 );
2350 }
2351 }
2352
2353 #[test]
2354 fn drop_index_explicit_basic() {
2355 let mut db = seed_users_table();
2356 process_command("CREATE INDEX users_age_idx ON users (age);", &mut db).unwrap();
2357 let response = process_command("DROP INDEX users_age_idx;", &mut db).expect("drop index");
2358 assert!(response.contains("1 index dropped"));
2359
2360 let users = db.get_table("users".to_string()).unwrap();
2361 assert!(users.index_by_name("users_age_idx").is_none());
2362 }
2363
2364 #[test]
2365 fn drop_index_refuses_auto_index() {
2366 let mut db = seed_users_table();
2367 let err = process_command("DROP INDEX sqlrite_autoindex_users_id;", &mut db).unwrap_err();
2370 assert!(format!("{err}").contains("auto-created"), "got: {err}");
2371 }
2372
2373 #[test]
2374 fn drop_index_if_exists_noop_on_missing() {
2375 let mut db = Database::new("t".to_string());
2376 let response =
2377 process_command("DROP INDEX IF EXISTS nope;", &mut db).expect("drop index if exists");
2378 assert!(response.contains("0 indexes dropped"));
2379 }
2380
2381 #[test]
2382 fn drop_index_missing_errors_without_if_exists() {
2383 let mut db = Database::new("t".to_string());
2384 let err = process_command("DROP INDEX nope;", &mut db).unwrap_err();
2385 assert!(format!("{err}").contains("does not exist"), "got: {err}");
2386 }
2387
2388 #[test]
2389 fn drop_statements_rejected_on_readonly_db() {
2390 use crate::sql::pager::{open_database_read_only, save_database};
2391
2392 let mut seed = Database::new("t".to_string());
2393 process_command(
2394 "CREATE TABLE notes (id INTEGER PRIMARY KEY, body TEXT);",
2395 &mut seed,
2396 )
2397 .unwrap();
2398 process_command("CREATE INDEX notes_body ON notes (body);", &mut seed).unwrap();
2399 let path = {
2400 let mut p = std::env::temp_dir();
2401 let pid = std::process::id();
2402 let nanos = std::time::SystemTime::now()
2403 .duration_since(std::time::UNIX_EPOCH)
2404 .map(|d| d.as_nanos())
2405 .unwrap_or(0);
2406 p.push(format!("sqlrite-drop-ro-{pid}-{nanos}.sqlrite"));
2407 p
2408 };
2409 save_database(&mut seed, &path).unwrap();
2410 drop(seed);
2411
2412 let mut ro = open_database_read_only(&path, "t".to_string()).unwrap();
2413 for stmt in ["DROP TABLE notes;", "DROP INDEX notes_body;"] {
2414 let err = process_command(stmt, &mut ro).unwrap_err();
2415 assert!(
2416 format!("{err}").contains("read-only"),
2417 "{stmt:?} should surface read-only error, got: {err}"
2418 );
2419 }
2420
2421 let _ = std::fs::remove_file(&path);
2422 let mut wal = path.as_os_str().to_owned();
2423 wal.push("-wal");
2424 let _ = std::fs::remove_file(std::path::PathBuf::from(wal));
2425 }
2426
2427 #[test]
2432 fn alter_rename_table_basic() {
2433 let mut db = seed_users_table();
2434 process_command("ALTER TABLE users RENAME TO members;", &mut db).expect("rename table");
2435 assert!(!db.contains_table("users".to_string()));
2436 assert!(db.contains_table("members".to_string()));
2437 let response = process_command("SELECT * FROM members;", &mut db).expect("select");
2439 assert!(response.contains("3 rows returned"));
2440 }
2441
2442 #[test]
2443 fn alter_rename_table_renames_auto_indexes() {
2444 let mut db = Database::new("t".to_string());
2447 process_command(
2448 "CREATE TABLE accounts (id INTEGER PRIMARY KEY, email TEXT UNIQUE);",
2449 &mut db,
2450 )
2451 .unwrap();
2452 {
2453 let accounts = db.get_table("accounts".to_string()).unwrap();
2454 assert!(
2455 accounts
2456 .index_by_name("sqlrite_autoindex_accounts_id")
2457 .is_some()
2458 );
2459 assert!(
2460 accounts
2461 .index_by_name("sqlrite_autoindex_accounts_email")
2462 .is_some()
2463 );
2464 }
2465 process_command("ALTER TABLE accounts RENAME TO members;", &mut db).expect("rename");
2466 let members = db.get_table("members".to_string()).unwrap();
2467 assert!(
2468 members
2469 .index_by_name("sqlrite_autoindex_members_id")
2470 .is_some(),
2471 "PK auto-index should be renamed to match new table"
2472 );
2473 assert!(
2474 members
2475 .index_by_name("sqlrite_autoindex_members_email")
2476 .is_some()
2477 );
2478 assert!(
2480 members
2481 .index_by_name("sqlrite_autoindex_accounts_id")
2482 .is_none()
2483 );
2484 for idx in &members.secondary_indexes {
2486 assert_eq!(idx.table_name, "members");
2487 }
2488 }
2489
2490 #[test]
2491 fn alter_rename_table_to_existing_errors() {
2492 let mut db = seed_users_table();
2493 process_command("CREATE TABLE other (id INTEGER PRIMARY KEY);", &mut db).unwrap();
2494 let err = process_command("ALTER TABLE users RENAME TO other;", &mut db).unwrap_err();
2495 assert!(format!("{err}").contains("already exists"), "got: {err}");
2496 assert!(db.contains_table("users".to_string()));
2498 assert!(db.contains_table("other".to_string()));
2499 }
2500
2501 #[test]
2502 fn alter_rename_table_to_reserved_name_errors() {
2503 let mut db = seed_users_table();
2504 let err =
2505 process_command("ALTER TABLE users RENAME TO sqlrite_master;", &mut db).unwrap_err();
2506 assert!(format!("{err}").contains("reserved"), "got: {err}");
2507 }
2508
2509 #[test]
2510 fn alter_rename_column_basic() {
2511 let mut db = seed_users_table();
2512 process_command(
2513 "ALTER TABLE users RENAME COLUMN name TO full_name;",
2514 &mut db,
2515 )
2516 .expect("rename column");
2517
2518 let users = db.get_table("users".to_string()).unwrap();
2519 assert!(users.contains_column("full_name".to_string()));
2520 assert!(!users.contains_column("name".to_string()));
2521
2522 let bob_rowid = users
2525 .rowids()
2526 .into_iter()
2527 .find(|r| users.get_value("full_name", *r) == Some(Value::Text("bob".to_string())))
2528 .expect("bob row should be findable under the new column name");
2529 assert_eq!(
2530 users.get_value("full_name", bob_rowid),
2531 Some(Value::Text("bob".to_string()))
2532 );
2533 }
2534
2535 #[test]
2536 fn alter_rename_column_collision_errors() {
2537 let mut db = seed_users_table();
2538 let err =
2539 process_command("ALTER TABLE users RENAME COLUMN name TO age;", &mut db).unwrap_err();
2540 assert!(format!("{err}").contains("already exists"), "got: {err}");
2541 }
2542
2543 #[test]
2544 fn alter_rename_column_updates_indexes() {
2545 let mut db = Database::new("t".to_string());
2547 process_command(
2548 "CREATE TABLE accounts (id INTEGER PRIMARY KEY, email TEXT UNIQUE);",
2549 &mut db,
2550 )
2551 .unwrap();
2552 process_command(
2553 "ALTER TABLE accounts RENAME COLUMN email TO contact;",
2554 &mut db,
2555 )
2556 .unwrap();
2557 let accounts = db.get_table("accounts".to_string()).unwrap();
2558 assert!(
2559 accounts
2560 .index_by_name("sqlrite_autoindex_accounts_contact")
2561 .is_some()
2562 );
2563 assert!(
2564 accounts
2565 .index_by_name("sqlrite_autoindex_accounts_email")
2566 .is_none()
2567 );
2568 }
2569
2570 #[test]
2571 fn alter_add_column_basic() {
2572 let mut db = seed_users_table();
2573 process_command("ALTER TABLE users ADD COLUMN nickname TEXT;", &mut db)
2574 .expect("add column");
2575 let users = db.get_table("users".to_string()).unwrap();
2576 assert!(users.contains_column("nickname".to_string()));
2577 let any_rowid = *users.rowids().first().expect("seed has rows");
2579 assert_eq!(users.get_value("nickname", any_rowid), None);
2580
2581 process_command(
2583 "INSERT INTO users (name, age, nickname) VALUES ('dan', 22, 'd');",
2584 &mut db,
2585 )
2586 .expect("insert with new col");
2587 let users = db.get_table("users".to_string()).unwrap();
2588 let dan_rowid = users
2589 .rowids()
2590 .into_iter()
2591 .find(|r| users.get_value("name", *r) == Some(Value::Text("dan".to_string())))
2592 .unwrap();
2593 assert_eq!(
2594 users.get_value("nickname", dan_rowid),
2595 Some(Value::Text("d".to_string()))
2596 );
2597 }
2598
2599 #[test]
2600 fn alter_add_column_with_default_backfills_existing_rows() {
2601 let mut db = seed_users_table();
2602 process_command(
2603 "ALTER TABLE users ADD COLUMN status TEXT DEFAULT 'active';",
2604 &mut db,
2605 )
2606 .expect("add column with default");
2607 let users = db.get_table("users".to_string()).unwrap();
2608 for rowid in users.rowids() {
2609 assert_eq!(
2610 users.get_value("status", rowid),
2611 Some(Value::Text("active".to_string())),
2612 "rowid {rowid} should have been backfilled with the default"
2613 );
2614 }
2615 }
2616
2617 #[test]
2618 fn alter_add_column_not_null_with_default_works_on_nonempty_table() {
2619 let mut db = seed_users_table();
2620 process_command(
2621 "ALTER TABLE users ADD COLUMN score INTEGER NOT NULL DEFAULT 0;",
2622 &mut db,
2623 )
2624 .expect("NOT NULL ADD with DEFAULT should succeed even with existing rows");
2625 let users = db.get_table("users".to_string()).unwrap();
2626 for rowid in users.rowids() {
2627 assert_eq!(users.get_value("score", rowid), Some(Value::Integer(0)));
2628 }
2629 }
2630
2631 #[test]
2632 fn alter_add_column_not_null_without_default_errors_on_nonempty_table() {
2633 let mut db = seed_users_table();
2634 let err = process_command(
2635 "ALTER TABLE users ADD COLUMN score INTEGER NOT NULL;",
2636 &mut db,
2637 )
2638 .unwrap_err();
2639 let msg = format!("{err}").to_lowercase();
2640 assert!(
2641 msg.contains("not null") && msg.contains("default"),
2642 "got: {msg}"
2643 );
2644 }
2645
2646 #[test]
2647 fn alter_add_column_pk_rejected() {
2648 let mut db = seed_users_table();
2649 let err = process_command(
2650 "ALTER TABLE users ADD COLUMN extra INTEGER PRIMARY KEY;",
2651 &mut db,
2652 )
2653 .unwrap_err();
2654 assert!(
2655 format!("{err}").to_lowercase().contains("primary key"),
2656 "got: {err}"
2657 );
2658 }
2659
2660 #[test]
2661 fn alter_add_column_unique_rejected() {
2662 let mut db = seed_users_table();
2663 let err = process_command(
2664 "ALTER TABLE users ADD COLUMN extra INTEGER UNIQUE;",
2665 &mut db,
2666 )
2667 .unwrap_err();
2668 assert!(
2669 format!("{err}").to_lowercase().contains("unique"),
2670 "got: {err}"
2671 );
2672 }
2673
2674 #[test]
2675 fn alter_add_column_existing_name_errors() {
2676 let mut db = seed_users_table();
2677 let err =
2678 process_command("ALTER TABLE users ADD COLUMN age INTEGER;", &mut db).unwrap_err();
2679 assert!(format!("{err}").contains("already exists"), "got: {err}");
2680 }
2681
2682 #[test]
2688 fn alter_drop_column_basic() {
2689 let mut db = seed_users_table();
2690 process_command("ALTER TABLE users DROP COLUMN age;", &mut db).expect("drop column");
2691 let users = db.get_table("users".to_string()).unwrap();
2692 assert!(!users.contains_column("age".to_string()));
2693 assert!(users.contains_column("name".to_string()));
2695 assert_eq!(users.rowids().len(), 3);
2696 }
2697
2698 #[test]
2699 fn alter_drop_column_drops_dependent_indexes() {
2700 let mut db = seed_users_table();
2701 process_command("CREATE INDEX users_age_idx ON users (age);", &mut db).unwrap();
2702 process_command("ALTER TABLE users DROP COLUMN age;", &mut db).unwrap();
2703 let users = db.get_table("users".to_string()).unwrap();
2704 assert!(users.index_by_name("users_age_idx").is_none());
2705 }
2706
2707 #[test]
2708 fn alter_drop_column_pk_errors() {
2709 let mut db = seed_users_table();
2710 let err = process_command("ALTER TABLE users DROP COLUMN id;", &mut db).unwrap_err();
2711 assert!(
2712 format!("{err}").to_lowercase().contains("primary key"),
2713 "got: {err}"
2714 );
2715 }
2716
2717 #[test]
2718 fn alter_drop_column_only_column_errors() {
2719 let mut db = Database::new("t".to_string());
2720 process_command("CREATE TABLE solo (only_col TEXT);", &mut db).unwrap();
2721 let err = process_command("ALTER TABLE solo DROP COLUMN only_col;", &mut db).unwrap_err();
2722 assert!(
2723 format!("{err}").to_lowercase().contains("only column"),
2724 "got: {err}"
2725 );
2726 }
2727
2728 #[test]
2729 fn alter_unknown_table_errors_without_if_exists() {
2730 let mut db = Database::new("t".to_string());
2731 let err = process_command("ALTER TABLE missing RENAME TO other;", &mut db).unwrap_err();
2732 assert!(format!("{err}").contains("does not exist"), "got: {err}");
2733 }
2734
2735 #[test]
2736 fn alter_unknown_table_if_exists_noop() {
2737 let mut db = Database::new("t".to_string());
2738 let response = process_command("ALTER TABLE IF EXISTS missing RENAME TO other;", &mut db)
2739 .expect("IF EXISTS makes missing-table ALTER a no-op");
2740 assert!(response.contains("no-op"));
2741 }
2742
2743 #[test]
2744 fn drop_table_inside_transaction_rolls_back() {
2745 let mut db = seed_users_table();
2749 process_command("CREATE INDEX users_age_idx ON users (age);", &mut db).unwrap();
2750 process_command("BEGIN;", &mut db).unwrap();
2751 process_command("DROP TABLE users;", &mut db).unwrap();
2752 assert!(!db.contains_table("users".to_string()));
2753 process_command("ROLLBACK;", &mut db).unwrap();
2754 assert!(db.contains_table("users".to_string()));
2755 let users = db.get_table("users".to_string()).unwrap();
2756 assert_eq!(users.rowids().len(), 3);
2757 assert!(users.index_by_name("users_age_idx").is_some());
2758 }
2759
2760 #[test]
2761 fn alter_inside_transaction_rolls_back() {
2762 let mut db = seed_users_table();
2763 process_command("BEGIN;", &mut db).unwrap();
2764 process_command(
2765 "ALTER TABLE users ADD COLUMN status TEXT DEFAULT 'active';",
2766 &mut db,
2767 )
2768 .unwrap();
2769 assert!(
2771 db.get_table("users".to_string())
2772 .unwrap()
2773 .contains_column("status".to_string())
2774 );
2775 process_command("ROLLBACK;", &mut db).unwrap();
2776 assert!(
2778 !db.get_table("users".to_string())
2779 .unwrap()
2780 .contains_column("status".to_string())
2781 );
2782 }
2783
2784 #[test]
2785 fn alter_rejected_on_readonly_db() {
2786 use crate::sql::pager::{open_database_read_only, save_database};
2787
2788 let mut seed = Database::new("t".to_string());
2789 process_command(
2790 "CREATE TABLE notes (id INTEGER PRIMARY KEY, body TEXT);",
2791 &mut seed,
2792 )
2793 .unwrap();
2794 let path = {
2795 let mut p = std::env::temp_dir();
2796 let pid = std::process::id();
2797 let nanos = std::time::SystemTime::now()
2798 .duration_since(std::time::UNIX_EPOCH)
2799 .map(|d| d.as_nanos())
2800 .unwrap_or(0);
2801 p.push(format!("sqlrite-alter-ro-{pid}-{nanos}.sqlrite"));
2802 p
2803 };
2804 save_database(&mut seed, &path).unwrap();
2805 drop(seed);
2806
2807 let mut ro = open_database_read_only(&path, "t".to_string()).unwrap();
2808 for stmt in [
2809 "ALTER TABLE notes RENAME TO n2;",
2810 "ALTER TABLE notes RENAME COLUMN body TO b;",
2811 "ALTER TABLE notes ADD COLUMN extra TEXT;",
2812 "ALTER TABLE notes DROP COLUMN body;",
2813 ] {
2814 let err = process_command(stmt, &mut ro).unwrap_err();
2815 assert!(
2816 format!("{err}").contains("read-only"),
2817 "{stmt:?} should surface read-only error, got: {err}"
2818 );
2819 }
2820
2821 let _ = std::fs::remove_file(&path);
2822 let mut wal = path.as_os_str().to_owned();
2823 wal.push("-wal");
2824 let _ = std::fs::remove_file(std::path::PathBuf::from(wal));
2825 }
2826}