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!(
164 &query,
165 Statement::CreateTable(_)
166 | Statement::CreateIndex(_)
167 | Statement::Insert(_)
168 | Statement::Update(_)
169 | Statement::Delete(_)
170 | Statement::Drop { .. }
171 | Statement::AlterTable(_)
172 );
173
174 if is_write_statement && db.is_read_only() {
180 return Err(SQLRiteError::General(
181 "cannot execute: database is opened read-only".to_string(),
182 ));
183 }
184
185 match query {
187 Statement::CreateTable(_) => {
188 let create_query = CreateQuery::new(&query);
189 match create_query {
190 Ok(payload) => {
191 let table_name = payload.table_name.clone();
192 if table_name == pager::MASTER_TABLE_NAME {
193 return Err(SQLRiteError::General(format!(
194 "'{}' is a reserved name used by the internal schema catalog",
195 pager::MASTER_TABLE_NAME
196 )));
197 }
198 match db.contains_table(table_name.to_string()) {
200 true => {
201 return Err(SQLRiteError::Internal(
202 "Cannot create, table already exists.".to_string(),
203 ));
204 }
205 false => {
206 let table = Table::new(payload);
207 db.tables.insert(table_name.to_string(), table);
215 message = String::from("CREATE TABLE Statement executed.");
216 }
217 }
218 }
219 Err(err) => return Err(err),
220 }
221 }
222 Statement::Insert(_) => {
223 let insert_query = InsertQuery::new(&query);
224 match insert_query {
225 Ok(payload) => {
226 let table_name = payload.table_name;
227 let columns = payload.columns;
228 let values = payload.rows;
229
230 match db.contains_table(table_name.to_string()) {
233 true => {
234 let db_table = db.get_table_mut(table_name.to_string()).unwrap();
235 match columns
237 .iter()
238 .all(|column| db_table.contains_column(column.to_string()))
239 {
240 true => {
241 for value in &values {
242 if columns.len() != value.len() {
244 return Err(SQLRiteError::Internal(format!(
245 "{} values for {} columns",
246 value.len(),
247 columns.len()
248 )));
249 }
250 db_table
251 .validate_unique_constraint(&columns, value)
252 .map_err(|err| {
253 SQLRiteError::Internal(format!(
254 "Unique key constraint violation: {err}"
255 ))
256 })?;
257 db_table.insert_row(&columns, value)?;
258 }
259 }
260 false => {
261 return Err(SQLRiteError::Internal(
262 "Cannot insert, some of the columns do not exist"
263 .to_string(),
264 ));
265 }
266 }
267 }
274 false => {
275 return Err(SQLRiteError::Internal("Table doesn't exist".to_string()));
276 }
277 }
278 }
279 Err(err) => return Err(err),
280 }
281
282 message = String::from("INSERT Statement executed.")
283 }
284 Statement::Query(_) => {
285 let select_query = SelectQuery::new(&query)?;
286 let (rendered_table, rows) = executor::execute_select(select_query, db)?;
287 rendered = Some(rendered_table);
293 message = format!(
294 "SELECT Statement executed. {rows} row{s} returned.",
295 s = if rows == 1 { "" } else { "s" }
296 );
297 }
298 Statement::Delete(_) => {
299 let rows = executor::execute_delete(&query, db)?;
300 message = format!(
301 "DELETE Statement executed. {rows} row{s} deleted.",
302 s = if rows == 1 { "" } else { "s" }
303 );
304 }
305 Statement::Update(_) => {
306 let rows = executor::execute_update(&query, db)?;
307 message = format!(
308 "UPDATE Statement executed. {rows} row{s} updated.",
309 s = if rows == 1 { "" } else { "s" }
310 );
311 }
312 Statement::CreateIndex(_) => {
313 let name = executor::execute_create_index(&query, db)?;
314 message = format!("CREATE INDEX '{name}' executed.");
315 }
316 Statement::Drop {
317 object_type,
318 if_exists,
319 names,
320 ..
321 } => match object_type {
322 ObjectType::Table => {
323 let count = executor::execute_drop_table(&names, if_exists, db)?;
324 let plural = if count == 1 { "table" } else { "tables" };
325 message = format!("DROP TABLE Statement executed. {count} {plural} dropped.");
326 }
327 ObjectType::Index => {
328 let count = executor::execute_drop_index(&names, if_exists, db)?;
329 let plural = if count == 1 { "index" } else { "indexes" };
330 message = format!("DROP INDEX Statement executed. {count} {plural} dropped.");
331 }
332 other => {
333 return Err(SQLRiteError::NotImplemented(format!(
334 "DROP {other:?} is not supported (only TABLE and INDEX)"
335 )));
336 }
337 },
338 Statement::AlterTable(alter) => {
339 message = executor::execute_alter_table(alter, db)?;
340 }
341 _ => {
342 return Err(SQLRiteError::NotImplemented(
343 "SQL Statement not supported yet.".to_string(),
344 ));
345 }
346 };
347
348 if is_write_statement && db.source_path.is_some() && !db.in_transaction() {
359 let path = db.source_path.clone().unwrap();
360 pager::save_database(db, &path)?;
361 }
362
363 Ok(CommandOutput {
364 status: message,
365 rendered,
366 })
367}
368
369#[cfg(test)]
370mod tests {
371 use super::*;
372 use crate::sql::db::table::Value;
373
374 fn seed_users_table() -> Database {
377 let mut db = Database::new("tempdb".to_string());
378 process_command(
379 "CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT NOT NULL, age INTEGER);",
380 &mut db,
381 )
382 .expect("create table");
383 process_command(
384 "INSERT INTO users (name, age) VALUES ('alice', 30);",
385 &mut db,
386 )
387 .expect("insert alice");
388 process_command("INSERT INTO users (name, age) VALUES ('bob', 25);", &mut db)
389 .expect("insert bob");
390 process_command(
391 "INSERT INTO users (name, age) VALUES ('carol', 40);",
392 &mut db,
393 )
394 .expect("insert carol");
395 db
396 }
397
398 #[test]
399 fn process_command_select_all_test() {
400 let mut db = seed_users_table();
401 let response = process_command("SELECT * FROM users;", &mut db).expect("select");
402 assert!(response.contains("3 rows returned"));
403 }
404
405 #[test]
406 fn process_command_select_where_test() {
407 let mut db = seed_users_table();
408 let response =
409 process_command("SELECT name FROM users WHERE age > 25;", &mut db).expect("select");
410 assert!(response.contains("2 rows returned"));
411 }
412
413 #[test]
414 fn process_command_select_eq_string_test() {
415 let mut db = seed_users_table();
416 let response =
417 process_command("SELECT name FROM users WHERE name = 'bob';", &mut db).expect("select");
418 assert!(response.contains("1 row returned"));
419 }
420
421 #[test]
422 fn process_command_select_limit_test() {
423 let mut db = seed_users_table();
424 let response = process_command("SELECT * FROM users ORDER BY age ASC LIMIT 2;", &mut db)
425 .expect("select");
426 assert!(response.contains("2 rows returned"));
427 }
428
429 #[test]
430 fn process_command_select_unknown_table_test() {
431 let mut db = Database::new("tempdb".to_string());
432 let result = process_command("SELECT * FROM nope;", &mut db);
433 assert!(result.is_err());
434 }
435
436 #[test]
437 fn process_command_select_unknown_column_test() {
438 let mut db = seed_users_table();
439 let result = process_command("SELECT height FROM users;", &mut db);
440 assert!(result.is_err());
441 }
442
443 #[test]
444 fn process_command_insert_test() {
445 let mut db = Database::new("tempdb".to_string());
447
448 let query_statement = "CREATE TABLE users (
450 id INTEGER PRIMARY KEY,
451 name TEXT
452 );";
453 let dialect = SQLiteDialect {};
454 let mut ast = Parser::parse_sql(&dialect, query_statement).unwrap();
455 if ast.len() > 1 {
456 panic!("Expected a single query statement, but there are more then 1.")
457 }
458 let query = ast.pop().unwrap();
459 let create_query = CreateQuery::new(&query).unwrap();
460
461 db.tables.insert(
463 create_query.table_name.to_string(),
464 Table::new(create_query),
465 );
466
467 let insert_query = String::from("INSERT INTO users (name) Values ('josh');");
469 match process_command(&insert_query, &mut db) {
470 Ok(response) => assert_eq!(response, "INSERT Statement executed."),
471 Err(err) => {
472 eprintln!("Error: {}", err);
473 assert!(false)
474 }
475 };
476 }
477
478 #[test]
479 fn process_command_insert_no_pk_test() {
480 let mut db = Database::new("tempdb".to_string());
482
483 let query_statement = "CREATE TABLE users (
485 name TEXT
486 );";
487 let dialect = SQLiteDialect {};
488 let mut ast = Parser::parse_sql(&dialect, query_statement).unwrap();
489 if ast.len() > 1 {
490 panic!("Expected a single query statement, but there are more then 1.")
491 }
492 let query = ast.pop().unwrap();
493 let create_query = CreateQuery::new(&query).unwrap();
494
495 db.tables.insert(
497 create_query.table_name.to_string(),
498 Table::new(create_query),
499 );
500
501 let insert_query = String::from("INSERT INTO users (name) Values ('josh');");
503 match process_command(&insert_query, &mut db) {
504 Ok(response) => assert_eq!(response, "INSERT Statement executed."),
505 Err(err) => {
506 eprintln!("Error: {}", err);
507 assert!(false)
508 }
509 };
510 }
511
512 #[test]
513 fn process_command_delete_where_test() {
514 let mut db = seed_users_table();
515 let response =
516 process_command("DELETE FROM users WHERE name = 'bob';", &mut db).expect("delete");
517 assert!(response.contains("1 row deleted"));
518
519 let remaining = process_command("SELECT * FROM users;", &mut db).expect("select");
520 assert!(remaining.contains("2 rows returned"));
521 }
522
523 #[test]
524 fn process_command_delete_all_test() {
525 let mut db = seed_users_table();
526 let response = process_command("DELETE FROM users;", &mut db).expect("delete");
527 assert!(response.contains("3 rows deleted"));
528 }
529
530 #[test]
531 fn process_command_update_where_test() {
532 use crate::sql::db::table::Value;
533
534 let mut db = seed_users_table();
535 let response = process_command("UPDATE users SET age = 99 WHERE name = 'bob';", &mut db)
536 .expect("update");
537 assert!(response.contains("1 row updated"));
538
539 let users = db.get_table("users".to_string()).unwrap();
541 let bob_rowid = users
542 .rowids()
543 .into_iter()
544 .find(|r| users.get_value("name", *r) == Some(Value::Text("bob".to_string())))
545 .expect("bob row must exist");
546 assert_eq!(users.get_value("age", bob_rowid), Some(Value::Integer(99)));
547 }
548
549 #[test]
550 fn process_command_update_unique_violation_test() {
551 let mut db = seed_users_table();
552 process_command(
554 "CREATE TABLE tags (id INTEGER PRIMARY KEY, label TEXT UNIQUE);",
555 &mut db,
556 )
557 .unwrap();
558 process_command("INSERT INTO tags (label) VALUES ('a');", &mut db).unwrap();
559 process_command("INSERT INTO tags (label) VALUES ('b');", &mut db).unwrap();
560
561 let result = process_command("UPDATE tags SET label = 'a' WHERE label = 'b';", &mut db);
562 assert!(result.is_err(), "expected UNIQUE violation, got {result:?}");
563 }
564
565 #[test]
566 fn process_command_insert_type_mismatch_returns_error_test() {
567 let mut db = Database::new("tempdb".to_string());
569 process_command(
570 "CREATE TABLE items (id INTEGER PRIMARY KEY, qty INTEGER);",
571 &mut db,
572 )
573 .unwrap();
574 let result = process_command("INSERT INTO items (qty) VALUES ('not a number');", &mut db);
575 assert!(result.is_err(), "expected error, got {result:?}");
576 }
577
578 #[test]
579 fn process_command_insert_missing_integer_returns_error_test() {
580 let mut db = Database::new("tempdb".to_string());
582 process_command(
583 "CREATE TABLE items (id INTEGER PRIMARY KEY, qty INTEGER);",
584 &mut db,
585 )
586 .unwrap();
587 let result = process_command("INSERT INTO items (id) VALUES (1);", &mut db);
588 assert!(result.is_err(), "expected error, got {result:?}");
589 }
590
591 #[test]
592 fn process_command_update_arith_test() {
593 use crate::sql::db::table::Value;
594
595 let mut db = seed_users_table();
596 process_command("UPDATE users SET age = age + 1;", &mut db).expect("update +1");
597
598 let users = db.get_table("users".to_string()).unwrap();
599 let mut ages: Vec<i64> = users
600 .rowids()
601 .into_iter()
602 .filter_map(|r| match users.get_value("age", r) {
603 Some(Value::Integer(n)) => Some(n),
604 _ => None,
605 })
606 .collect();
607 ages.sort();
608 assert_eq!(ages, vec![26, 31, 41]); }
610
611 #[test]
612 fn process_command_select_arithmetic_where_test() {
613 let mut db = seed_users_table();
614 let response =
616 process_command("SELECT name FROM users WHERE age * 2 > 55;", &mut db).expect("select");
617 assert!(response.contains("2 rows returned"));
618 }
619
620 #[test]
621 fn process_command_divide_by_zero_test() {
622 let mut db = seed_users_table();
623 let result = process_command("SELECT age / 0 FROM users;", &mut db);
624 assert!(result.is_err());
626 }
627
628 #[test]
629 fn process_command_unsupported_statement_test() {
630 let mut db = Database::new("tempdb".to_string());
631 let result = process_command("CREATE VIEW v AS SELECT * FROM users;", &mut db);
635 assert!(result.is_err());
636 }
637
638 #[test]
639 fn empty_input_is_a_noop_not_a_panic() {
640 let mut db = Database::new("t".to_string());
644 for input in ["", " ", "-- just a comment", "-- comment\n-- another"] {
645 let result = process_command(input, &mut db);
646 assert!(result.is_ok(), "input {input:?} should not error");
647 let msg = result.unwrap();
648 assert!(msg.contains("No statement"), "got: {msg:?}");
649 }
650 }
651
652 #[test]
653 fn create_index_adds_explicit_index() {
654 let mut db = seed_users_table();
655 let response = process_command("CREATE INDEX users_age_idx ON users (age);", &mut db)
656 .expect("create index");
657 assert!(response.contains("users_age_idx"));
658
659 let users = db.get_table("users".to_string()).unwrap();
661 let idx = users
662 .index_by_name("users_age_idx")
663 .expect("index should exist after CREATE INDEX");
664 assert_eq!(idx.column_name, "age");
665 assert!(!idx.is_unique);
666 }
667
668 #[test]
669 fn create_unique_index_rejects_duplicate_existing_values() {
670 let mut db = seed_users_table();
671 process_command("INSERT INTO users (name, age) VALUES ('dan', 30);", &mut db).unwrap();
674 let result = process_command(
675 "CREATE UNIQUE INDEX users_age_unique ON users (age);",
676 &mut db,
677 );
678 assert!(
679 result.is_err(),
680 "expected unique-index failure, got {result:?}"
681 );
682 }
683
684 #[test]
685 fn where_eq_on_indexed_column_uses_index_probe() {
686 let mut db = Database::new("t".to_string());
690 process_command(
691 "CREATE TABLE big (id INTEGER PRIMARY KEY, tag TEXT);",
692 &mut db,
693 )
694 .unwrap();
695 process_command("CREATE INDEX big_tag_idx ON big (tag);", &mut db).unwrap();
696 for i in 1..=100 {
697 let tag = if i % 3 == 0 { "hot" } else { "cold" };
698 process_command(&format!("INSERT INTO big (tag) VALUES ('{tag}');"), &mut db).unwrap();
699 }
700 let response =
701 process_command("SELECT id FROM big WHERE tag = 'hot';", &mut db).expect("select");
702 assert!(
704 response.contains("33 rows returned"),
705 "response was {response:?}"
706 );
707 }
708
709 #[test]
710 fn where_eq_on_indexed_column_inside_parens_uses_index_probe() {
711 let mut db = seed_users_table();
712 let response = process_command("SELECT name FROM users WHERE (name = 'bob');", &mut db)
713 .expect("select");
714 assert!(response.contains("1 row returned"));
715 }
716
717 #[test]
718 fn where_eq_literal_first_side_uses_index_probe() {
719 let mut db = seed_users_table();
720 let response =
722 process_command("SELECT name FROM users WHERE 'bob' = name;", &mut db).expect("select");
723 assert!(response.contains("1 row returned"));
724 }
725
726 #[test]
727 fn non_equality_where_still_falls_back_to_full_scan() {
728 let mut db = seed_users_table();
731 let response =
732 process_command("SELECT name FROM users WHERE age > 28;", &mut db).expect("select");
733 assert!(response.contains("2 rows returned"));
734 }
735
736 #[test]
741 fn rollback_restores_pre_begin_in_memory_state() {
742 let mut db = seed_users_table();
745 let before = db.get_table("users".to_string()).unwrap().rowids().len();
746 assert_eq!(before, 3);
747
748 process_command("BEGIN;", &mut db).expect("BEGIN");
749 assert!(db.in_transaction());
750 process_command("INSERT INTO users (name, age) VALUES ('dan', 50);", &mut db)
751 .expect("INSERT inside txn");
752 let mid = db.get_table("users".to_string()).unwrap().rowids().len();
754 assert_eq!(mid, 4);
755
756 process_command("ROLLBACK;", &mut db).expect("ROLLBACK");
757 assert!(!db.in_transaction());
758 let after = db.get_table("users".to_string()).unwrap().rowids().len();
759 assert_eq!(
760 after, 3,
761 "ROLLBACK should have restored the pre-BEGIN state"
762 );
763 }
764
765 #[test]
766 fn commit_keeps_mutations_and_clears_txn_flag() {
767 let mut db = seed_users_table();
768 process_command("BEGIN;", &mut db).expect("BEGIN");
769 process_command("INSERT INTO users (name, age) VALUES ('dan', 50);", &mut db)
770 .expect("INSERT inside txn");
771 process_command("COMMIT;", &mut db).expect("COMMIT");
772 assert!(!db.in_transaction());
773 let after = db.get_table("users".to_string()).unwrap().rowids().len();
774 assert_eq!(after, 4);
775 }
776
777 #[test]
778 fn rollback_undoes_update_and_delete_side_by_side() {
779 use crate::sql::db::table::Value;
780
781 let mut db = seed_users_table();
782 process_command("BEGIN;", &mut db).unwrap();
783 process_command("UPDATE users SET age = 999;", &mut db).unwrap();
784 process_command("DELETE FROM users WHERE name = 'bob';", &mut db).unwrap();
785 let users = db.get_table("users".to_string()).unwrap();
787 assert_eq!(users.rowids().len(), 2);
788 for r in users.rowids() {
789 assert_eq!(users.get_value("age", r), Some(Value::Integer(999)));
790 }
791
792 process_command("ROLLBACK;", &mut db).unwrap();
793 let users = db.get_table("users".to_string()).unwrap();
794 assert_eq!(users.rowids().len(), 3);
795 for r in users.rowids() {
797 assert_ne!(users.get_value("age", r), Some(Value::Integer(999)));
798 }
799 }
800
801 #[test]
802 fn nested_begin_is_rejected() {
803 let mut db = seed_users_table();
804 process_command("BEGIN;", &mut db).unwrap();
805 let err = process_command("BEGIN;", &mut db).unwrap_err();
806 assert!(
807 format!("{err}").contains("already open"),
808 "nested BEGIN should error; got: {err}"
809 );
810 assert!(db.in_transaction());
812 process_command("ROLLBACK;", &mut db).unwrap();
813 }
814
815 #[test]
816 fn orphan_commit_and_rollback_are_rejected() {
817 let mut db = seed_users_table();
818 let commit_err = process_command("COMMIT;", &mut db).unwrap_err();
819 assert!(format!("{commit_err}").contains("no transaction"));
820 let rollback_err = process_command("ROLLBACK;", &mut db).unwrap_err();
821 assert!(format!("{rollback_err}").contains("no transaction"));
822 }
823
824 #[test]
825 fn error_inside_transaction_keeps_txn_open() {
826 let mut db = seed_users_table();
830 process_command("BEGIN;", &mut db).unwrap();
831 let err = process_command("INSERT INTO nope (x) VALUES (1);", &mut db);
832 assert!(err.is_err());
833 assert!(db.in_transaction(), "txn should stay open after error");
834 process_command("ROLLBACK;", &mut db).unwrap();
835 }
836
837 fn seed_file_backed(name: &str, schema: &str) -> (std::path::PathBuf, Database) {
842 use crate::sql::pager::{open_database, save_database};
843 let mut p = std::env::temp_dir();
844 let pid = std::process::id();
845 let nanos = std::time::SystemTime::now()
846 .duration_since(std::time::UNIX_EPOCH)
847 .map(|d| d.as_nanos())
848 .unwrap_or(0);
849 p.push(format!("sqlrite-txn-{name}-{pid}-{nanos}.sqlrite"));
850
851 {
855 let mut seed = Database::new("t".to_string());
856 process_command(schema, &mut seed).unwrap();
857 save_database(&mut seed, &p).unwrap();
858 }
859 let db = open_database(&p, "t".to_string()).unwrap();
860 (p, db)
861 }
862
863 fn cleanup_file(path: &std::path::Path) {
864 let _ = std::fs::remove_file(path);
865 let mut wal = path.as_os_str().to_owned();
866 wal.push("-wal");
867 let _ = std::fs::remove_file(std::path::PathBuf::from(wal));
868 }
869
870 #[test]
871 fn begin_commit_rollback_round_trip_through_disk() {
872 use crate::sql::pager::open_database;
876
877 let (path, mut db) = seed_file_backed(
878 "roundtrip",
879 "CREATE TABLE notes (id INTEGER PRIMARY KEY, body TEXT);",
880 );
881
882 process_command("BEGIN;", &mut db).unwrap();
884 process_command("INSERT INTO notes (body) VALUES ('a');", &mut db).unwrap();
885 process_command("INSERT INTO notes (body) VALUES ('b');", &mut db).unwrap();
886 process_command("COMMIT;", &mut db).unwrap();
887
888 process_command("BEGIN;", &mut db).unwrap();
890 process_command("INSERT INTO notes (body) VALUES ('c');", &mut db).unwrap();
891 process_command("ROLLBACK;", &mut db).unwrap();
892
893 drop(db); let reopened = open_database(&path, "t".to_string()).unwrap();
896 let notes = reopened.get_table("notes".to_string()).unwrap();
897 assert_eq!(notes.rowids().len(), 2, "committed rows should survive");
898
899 drop(reopened);
900 cleanup_file(&path);
901 }
902
903 #[test]
904 fn write_inside_transaction_does_not_autosave() {
905 let (path, mut db) =
909 seed_file_backed("noas", "CREATE TABLE t (id INTEGER PRIMARY KEY, x TEXT);");
910
911 let mut wal_path = path.as_os_str().to_owned();
912 wal_path.push("-wal");
913 let wal_path = std::path::PathBuf::from(wal_path);
914 let frames_before = std::fs::metadata(&wal_path).unwrap().len();
915
916 process_command("BEGIN;", &mut db).unwrap();
917 process_command("INSERT INTO t (x) VALUES ('a');", &mut db).unwrap();
918 process_command("INSERT INTO t (x) VALUES ('b');", &mut db).unwrap();
919
920 let frames_mid = std::fs::metadata(&wal_path).unwrap().len();
922 assert_eq!(
923 frames_before, frames_mid,
924 "WAL should not grow during an open transaction"
925 );
926
927 process_command("COMMIT;", &mut db).unwrap();
928
929 drop(db); let fresh = crate::sql::pager::open_database(&path, "t".to_string()).unwrap();
931 assert_eq!(
932 fresh.get_table("t".to_string()).unwrap().rowids().len(),
933 2,
934 "COMMIT should have persisted both inserted rows"
935 );
936 drop(fresh);
937 cleanup_file(&path);
938 }
939
940 #[test]
941 fn rollback_undoes_create_table() {
942 let mut db = seed_users_table();
947 assert_eq!(db.tables.len(), 1);
948
949 process_command("BEGIN;", &mut db).unwrap();
950 process_command(
951 "CREATE TABLE dropme (id INTEGER PRIMARY KEY, x TEXT);",
952 &mut db,
953 )
954 .unwrap();
955 process_command("INSERT INTO dropme (x) VALUES ('stuff');", &mut db).unwrap();
956 assert_eq!(db.tables.len(), 2);
957
958 process_command("ROLLBACK;", &mut db).unwrap();
959 assert_eq!(
960 db.tables.len(),
961 1,
962 "CREATE TABLE should have been rolled back"
963 );
964 assert!(db.get_table("dropme".to_string()).is_err());
965 }
966
967 #[test]
968 fn rollback_restores_secondary_index_state() {
969 let mut db = Database::new("t".to_string());
973 process_command(
974 "CREATE TABLE users (id INTEGER PRIMARY KEY, email TEXT UNIQUE);",
975 &mut db,
976 )
977 .unwrap();
978 process_command("INSERT INTO users (email) VALUES ('a@x');", &mut db).unwrap();
979
980 process_command("BEGIN;", &mut db).unwrap();
981 process_command("INSERT INTO users (email) VALUES ('b@x');", &mut db).unwrap();
982 process_command("ROLLBACK;", &mut db).unwrap();
984
985 let reinsert = process_command("INSERT INTO users (email) VALUES ('b@x');", &mut db);
989 assert!(
990 reinsert.is_ok(),
991 "re-insert after rollback should succeed, got {reinsert:?}"
992 );
993 }
994
995 #[test]
996 fn rollback_restores_last_rowid_counter() {
997 use crate::sql::db::table::Value;
1001
1002 let mut db = seed_users_table(); let pre = db.get_table("users".to_string()).unwrap().last_rowid;
1004
1005 process_command("BEGIN;", &mut db).unwrap();
1006 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();
1009
1010 let post = db.get_table("users".to_string()).unwrap().last_rowid;
1011 assert_eq!(pre, post, "last_rowid must roll back with the snapshot");
1012
1013 process_command("INSERT INTO users (name, age) VALUES ('d', 50);", &mut db).unwrap();
1015 let users = db.get_table("users".to_string()).unwrap();
1016 let d_rowid = users
1017 .rowids()
1018 .into_iter()
1019 .find(|r| users.get_value("name", *r) == Some(Value::Text("d".into())))
1020 .expect("d row must exist");
1021 assert_eq!(d_rowid, pre + 1);
1022 }
1023
1024 #[test]
1025 fn commit_on_in_memory_db_clears_txn_without_pager_call() {
1026 let mut db = seed_users_table(); assert!(db.source_path.is_none());
1031
1032 process_command("BEGIN;", &mut db).unwrap();
1033 process_command("INSERT INTO users (name, age) VALUES ('z', 99);", &mut db).unwrap();
1034 process_command("COMMIT;", &mut db).unwrap();
1035
1036 assert!(!db.in_transaction());
1037 assert_eq!(db.get_table("users".to_string()).unwrap().rowids().len(), 4);
1038 }
1039
1040 #[test]
1041 fn failed_commit_auto_rolls_back_in_memory_state() {
1042 use crate::sql::pager::save_database;
1056
1057 let (path, mut db) = seed_file_backed(
1059 "failcommit",
1060 "CREATE TABLE notes (id INTEGER PRIMARY KEY, body TEXT);",
1061 );
1062
1063 process_command("INSERT INTO notes (body) VALUES ('before');", &mut db).unwrap();
1065
1066 process_command("BEGIN;", &mut db).unwrap();
1068 process_command("INSERT INTO notes (body) VALUES ('inflight');", &mut db).unwrap();
1069 assert_eq!(
1070 db.get_table("notes".to_string()).unwrap().rowids().len(),
1071 2,
1072 "inflight row visible mid-txn"
1073 );
1074
1075 let orig_source = db.source_path.clone();
1079 let orig_pager = db.pager.take();
1080 db.source_path = Some(std::env::temp_dir());
1081
1082 let commit_result = process_command("COMMIT;", &mut db);
1083 assert!(commit_result.is_err(), "commit must fail");
1084 let err_str = format!("{}", commit_result.unwrap_err());
1085 assert!(
1086 err_str.contains("COMMIT failed") && err_str.contains("rolled back"),
1087 "error must surface auto-rollback; got: {err_str}"
1088 );
1089
1090 assert!(
1094 !db.in_transaction(),
1095 "txn must be cleared after auto-rollback"
1096 );
1097 assert_eq!(
1098 db.get_table("notes".to_string()).unwrap().rowids().len(),
1099 1,
1100 "inflight row must be rolled back"
1101 );
1102
1103 db.source_path = orig_source;
1106 db.pager = orig_pager;
1107 process_command("INSERT INTO notes (body) VALUES ('after');", &mut db).unwrap();
1108 drop(db);
1109
1110 let reopened = crate::sql::pager::open_database(&path, "t".to_string()).unwrap();
1112 let notes = reopened.get_table("notes".to_string()).unwrap();
1113 assert_eq!(notes.rowids().len(), 2);
1114 let _ = save_database; drop(reopened);
1117 cleanup_file(&path);
1118 }
1119
1120 #[test]
1121 fn begin_on_read_only_is_rejected() {
1122 use crate::sql::pager::{open_database_read_only, save_database};
1123
1124 let path = {
1125 let mut p = std::env::temp_dir();
1126 let pid = std::process::id();
1127 let nanos = std::time::SystemTime::now()
1128 .duration_since(std::time::UNIX_EPOCH)
1129 .map(|d| d.as_nanos())
1130 .unwrap_or(0);
1131 p.push(format!("sqlrite-txn-ro-{pid}-{nanos}.sqlrite"));
1132 p
1133 };
1134 {
1135 let mut seed = Database::new("t".to_string());
1136 process_command("CREATE TABLE t (id INTEGER PRIMARY KEY);", &mut seed).unwrap();
1137 save_database(&mut seed, &path).unwrap();
1138 }
1139
1140 let mut ro = open_database_read_only(&path, "t".to_string()).unwrap();
1141 let err = process_command("BEGIN;", &mut ro).unwrap_err();
1142 assert!(
1143 format!("{err}").contains("read-only"),
1144 "BEGIN on RO db should surface read-only; got: {err}"
1145 );
1146 assert!(!ro.in_transaction());
1147
1148 let _ = std::fs::remove_file(&path);
1149 let mut wal = path.as_os_str().to_owned();
1150 wal.push("-wal");
1151 let _ = std::fs::remove_file(std::path::PathBuf::from(wal));
1152 }
1153
1154 #[test]
1155 fn read_only_database_rejects_mutations_before_touching_state() {
1156 use crate::sql::pager::open_database_read_only;
1162
1163 let mut seed = Database::new("t".to_string());
1164 process_command(
1165 "CREATE TABLE notes (id INTEGER PRIMARY KEY, body TEXT);",
1166 &mut seed,
1167 )
1168 .unwrap();
1169 process_command("INSERT INTO notes (body) VALUES ('alpha');", &mut seed).unwrap();
1170
1171 let path = {
1172 let mut p = std::env::temp_dir();
1173 let pid = std::process::id();
1174 let nanos = std::time::SystemTime::now()
1175 .duration_since(std::time::UNIX_EPOCH)
1176 .map(|d| d.as_nanos())
1177 .unwrap_or(0);
1178 p.push(format!("sqlrite-ro-reject-{pid}-{nanos}.sqlrite"));
1179 p
1180 };
1181 crate::sql::pager::save_database(&mut seed, &path).unwrap();
1182 drop(seed);
1183
1184 let mut ro = open_database_read_only(&path, "t".to_string()).unwrap();
1185 let notes_before = ro.get_table("notes".to_string()).unwrap().rowids().len();
1186
1187 for stmt in [
1188 "INSERT INTO notes (body) VALUES ('beta');",
1189 "UPDATE notes SET body = 'x';",
1190 "DELETE FROM notes;",
1191 "CREATE TABLE more (id INTEGER PRIMARY KEY);",
1192 "CREATE INDEX notes_body ON notes (body);",
1193 ] {
1194 let err = process_command(stmt, &mut ro).unwrap_err();
1195 assert!(
1196 format!("{err}").contains("read-only"),
1197 "stmt {stmt:?} should surface a read-only error; got: {err}"
1198 );
1199 }
1200
1201 let notes_after = ro.get_table("notes".to_string()).unwrap().rowids().len();
1203 assert_eq!(notes_before, notes_after);
1204 let sel = process_command("SELECT * FROM notes;", &mut ro).expect("select on RO must work");
1205 assert!(sel.contains("1 row returned"));
1206
1207 drop(ro);
1209 let _ = std::fs::remove_file(&path);
1210 let mut wal = path.as_os_str().to_owned();
1211 wal.push("-wal");
1212 let _ = std::fs::remove_file(std::path::PathBuf::from(wal));
1213 }
1214
1215 #[test]
1220 fn vector_create_table_and_insert_basic() {
1221 let mut db = Database::new("tempdb".to_string());
1222 process_command(
1223 "CREATE TABLE docs (id INTEGER PRIMARY KEY, embedding VECTOR(3));",
1224 &mut db,
1225 )
1226 .expect("create table with VECTOR(3)");
1227 process_command(
1228 "INSERT INTO docs (embedding) VALUES ([0.1, 0.2, 0.3]);",
1229 &mut db,
1230 )
1231 .expect("insert vector");
1232
1233 let sel = process_command("SELECT * FROM docs;", &mut db).expect("select");
1237 assert!(sel.contains("1 row returned"));
1238
1239 let docs = db.get_table("docs".to_string()).expect("docs table");
1240 let rowids = docs.rowids();
1241 assert_eq!(rowids.len(), 1);
1242 match docs.get_value("embedding", rowids[0]) {
1243 Some(Value::Vector(v)) => assert_eq!(v, vec![0.1f32, 0.2, 0.3]),
1244 other => panic!("expected Value::Vector(...), got {other:?}"),
1245 }
1246 }
1247
1248 #[test]
1249 fn vector_dim_mismatch_at_insert_is_clean_error() {
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");
1256
1257 let err = process_command("INSERT INTO docs (embedding) VALUES ([0.1, 0.2]);", &mut db)
1259 .unwrap_err();
1260 let msg = format!("{err}");
1261 assert!(
1262 msg.to_lowercase().contains("dimension")
1263 && msg.contains("declared 3")
1264 && msg.contains("got 2"),
1265 "expected clear dim-mismatch error, got: {msg}"
1266 );
1267
1268 let err = process_command(
1270 "INSERT INTO docs (embedding) VALUES ([0.1, 0.2, 0.3, 0.4, 0.5]);",
1271 &mut db,
1272 )
1273 .unwrap_err();
1274 assert!(
1275 format!("{err}").contains("got 5"),
1276 "expected dim-mismatch error mentioning got 5, got: {err}"
1277 );
1278 }
1279
1280 #[test]
1281 fn vector_create_table_rejects_missing_dim() {
1282 let mut db = Database::new("tempdb".to_string());
1283 let result = process_command(
1289 "CREATE TABLE docs (id INTEGER PRIMARY KEY, embedding VECTOR);",
1290 &mut db,
1291 );
1292 assert!(
1293 result.is_err(),
1294 "expected CREATE TABLE with bare VECTOR to fail (no dim)"
1295 );
1296 }
1297
1298 #[test]
1299 fn vector_create_table_rejects_zero_dim() {
1300 let mut db = Database::new("tempdb".to_string());
1301 let err = process_command(
1302 "CREATE TABLE docs (id INTEGER PRIMARY KEY, embedding VECTOR(0));",
1303 &mut db,
1304 )
1305 .unwrap_err();
1306 let msg = format!("{err}");
1307 assert!(
1308 msg.to_lowercase().contains("vector"),
1309 "expected VECTOR-related error for VECTOR(0), got: {msg}"
1310 );
1311 }
1312
1313 #[test]
1314 fn vector_high_dim_works() {
1315 let mut db = Database::new("tempdb".to_string());
1318 process_command(
1319 "CREATE TABLE embeddings (id INTEGER PRIMARY KEY, e VECTOR(384));",
1320 &mut db,
1321 )
1322 .expect("create table VECTOR(384)");
1323
1324 let lit = format!(
1325 "[{}]",
1326 (0..384)
1327 .map(|i| format!("{}", i as f32 * 0.001))
1328 .collect::<Vec<_>>()
1329 .join(",")
1330 );
1331 let sql = format!("INSERT INTO embeddings (e) VALUES ({lit});");
1332 process_command(&sql, &mut db).expect("insert 384-dim vector");
1333
1334 let sel = process_command("SELECT id FROM embeddings;", &mut db).expect("select id");
1335 assert!(sel.contains("1 row returned"));
1336 }
1337
1338 #[test]
1339 fn vector_multiple_rows() {
1340 let mut db = Database::new("tempdb".to_string());
1343 process_command(
1344 "CREATE TABLE docs (id INTEGER PRIMARY KEY, e VECTOR(2));",
1345 &mut db,
1346 )
1347 .expect("create");
1348 for i in 0..3 {
1349 let sql = format!("INSERT INTO docs (e) VALUES ([{i}.0, {}.0]);", i + 1);
1350 process_command(&sql, &mut db).expect("insert");
1351 }
1352 let sel = process_command("SELECT * FROM docs;", &mut db).expect("select");
1353 assert!(sel.contains("3 rows returned"));
1354
1355 let docs = db.get_table("docs".to_string()).expect("docs table");
1357 let rowids = docs.rowids();
1358 assert_eq!(rowids.len(), 3);
1359 let mut vectors: Vec<Vec<f32>> = rowids
1360 .iter()
1361 .filter_map(|r| match docs.get_value("e", *r) {
1362 Some(Value::Vector(v)) => Some(v),
1363 _ => None,
1364 })
1365 .collect();
1366 vectors.sort_by(|a, b| a[0].partial_cmp(&b[0]).unwrap());
1367 assert_eq!(vectors[0], vec![0.0f32, 1.0]);
1368 assert_eq!(vectors[1], vec![1.0f32, 2.0]);
1369 assert_eq!(vectors[2], vec![2.0f32, 3.0]);
1370 }
1371
1372 fn seed_hnsw_table() -> Database {
1380 let mut db = Database::new("tempdb".to_string());
1381 process_command(
1382 "CREATE TABLE docs (id INTEGER PRIMARY KEY, e VECTOR(2));",
1383 &mut db,
1384 )
1385 .unwrap();
1386 for v in &[
1387 "[1.0, 0.0]", "[2.0, 0.0]", "[0.0, 3.0]", "[1.0, 4.0]", "[10.0, 10.0]", ] {
1393 process_command(&format!("INSERT INTO docs (e) VALUES ({v});"), &mut db).unwrap();
1394 }
1395 db
1396 }
1397
1398 #[test]
1399 fn create_index_using_hnsw_succeeds() {
1400 let mut db = seed_hnsw_table();
1401 let resp = process_command("CREATE INDEX ix_e ON docs USING hnsw (e);", &mut db).unwrap();
1402 assert!(resp.to_lowercase().contains("create index"));
1403 let table = db.get_table("docs".to_string()).unwrap();
1405 assert_eq!(table.hnsw_indexes.len(), 1);
1406 assert_eq!(table.hnsw_indexes[0].name, "ix_e");
1407 assert_eq!(table.hnsw_indexes[0].column_name, "e");
1408 assert_eq!(table.hnsw_indexes[0].index.len(), 5);
1410 }
1411
1412 #[test]
1413 fn create_index_using_hnsw_rejects_non_vector_column() {
1414 let mut db = Database::new("tempdb".to_string());
1415 process_command(
1416 "CREATE TABLE t (id INTEGER PRIMARY KEY, name TEXT);",
1417 &mut db,
1418 )
1419 .unwrap();
1420 let err =
1421 process_command("CREATE INDEX ix_name ON t USING hnsw (name);", &mut db).unwrap_err();
1422 let msg = format!("{err}");
1423 assert!(
1424 msg.to_lowercase().contains("vector"),
1425 "expected error mentioning VECTOR; got: {msg}"
1426 );
1427 }
1428
1429 #[test]
1430 fn knn_query_uses_hnsw_after_create_index() {
1431 let mut db = seed_hnsw_table();
1437 process_command("CREATE INDEX ix_e ON docs USING hnsw (e);", &mut db).unwrap();
1438
1439 let resp = process_command(
1444 "SELECT id FROM docs ORDER BY vec_distance_l2(e, [1.0, 0.0]) ASC LIMIT 3;",
1445 &mut db,
1446 )
1447 .unwrap();
1448 assert!(resp.contains("3 rows returned"), "got: {resp}");
1449 }
1450
1451 #[test]
1452 fn knn_query_works_after_subsequent_inserts() {
1453 let mut db = seed_hnsw_table();
1457 process_command("CREATE INDEX ix_e ON docs USING hnsw (e);", &mut db).unwrap();
1458 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();
1462 assert_eq!(
1463 table.hnsw_indexes[0].index.len(),
1464 7,
1465 "incremental insert should grow HNSW alongside row storage"
1466 );
1467
1468 let resp = process_command(
1471 "SELECT id FROM docs ORDER BY vec_distance_l2(e, [0.0, 0.0]) ASC LIMIT 1;",
1472 &mut db,
1473 )
1474 .unwrap();
1475 assert!(resp.contains("1 row returned"), "got: {resp}");
1476 }
1477
1478 #[test]
1484 fn delete_on_hnsw_indexed_table_succeeds_and_marks_dirty() {
1485 let mut db = seed_hnsw_table();
1486 process_command("CREATE INDEX ix_e ON docs USING hnsw (e);", &mut db).unwrap();
1487 let resp = process_command("DELETE FROM docs WHERE id = 1;", &mut db).unwrap();
1488 assert!(resp.contains("1 row"), "expected 1 row deleted: {resp}");
1489
1490 let docs = db.get_table("docs".to_string()).unwrap();
1491 let entry = docs.hnsw_indexes.iter().find(|e| e.name == "ix_e").unwrap();
1492 assert!(
1493 entry.needs_rebuild,
1494 "DELETE should have marked HNSW index dirty for rebuild on next save"
1495 );
1496 }
1497
1498 #[test]
1499 fn update_on_hnsw_indexed_vector_col_succeeds_and_marks_dirty() {
1500 let mut db = seed_hnsw_table();
1501 process_command("CREATE INDEX ix_e ON docs USING hnsw (e);", &mut db).unwrap();
1502 let resp =
1503 process_command("UPDATE docs SET e = [9.0, 9.0] WHERE id = 1;", &mut db).unwrap();
1504 assert!(resp.contains("1 row"), "expected 1 row updated: {resp}");
1505
1506 let docs = db.get_table("docs".to_string()).unwrap();
1507 let entry = docs.hnsw_indexes.iter().find(|e| e.name == "ix_e").unwrap();
1508 assert!(
1509 entry.needs_rebuild,
1510 "UPDATE on the vector column should have marked HNSW index dirty"
1511 );
1512 }
1513
1514 #[test]
1515 fn duplicate_index_name_errors() {
1516 let mut db = seed_hnsw_table();
1517 process_command("CREATE INDEX ix_e ON docs USING hnsw (e);", &mut db).unwrap();
1518 let err =
1519 process_command("CREATE INDEX ix_e ON docs USING hnsw (e);", &mut db).unwrap_err();
1520 let msg = format!("{err}");
1521 assert!(
1522 msg.to_lowercase().contains("already exists"),
1523 "expected duplicate-index error; got: {msg}"
1524 );
1525 }
1526
1527 #[test]
1528 fn index_if_not_exists_is_idempotent() {
1529 let mut db = seed_hnsw_table();
1530 process_command("CREATE INDEX ix_e ON docs USING hnsw (e);", &mut db).unwrap();
1531 process_command(
1533 "CREATE INDEX IF NOT EXISTS ix_e ON docs USING hnsw (e);",
1534 &mut db,
1535 )
1536 .unwrap();
1537 let table = db.get_table("docs".to_string()).unwrap();
1538 assert_eq!(table.hnsw_indexes.len(), 1);
1539 }
1540
1541 fn seed_fts_table() -> Database {
1548 let mut db = Database::new("tempdb".to_string());
1549 process_command(
1550 "CREATE TABLE docs (id INTEGER PRIMARY KEY, body TEXT);",
1551 &mut db,
1552 )
1553 .unwrap();
1554 for body in &[
1555 "rust embedded database", "rust web framework", "go embedded systems", "python web framework", "rust rust rust embedded power", ] {
1561 process_command(
1562 &format!("INSERT INTO docs (body) VALUES ('{body}');"),
1563 &mut db,
1564 )
1565 .unwrap();
1566 }
1567 db
1568 }
1569
1570 #[test]
1571 fn create_index_using_fts_succeeds_and_indexes_existing_rows() {
1572 let mut db = seed_fts_table();
1573 let resp =
1574 process_command("CREATE INDEX ix_body ON docs USING fts (body);", &mut db).unwrap();
1575 assert!(resp.to_lowercase().contains("create index"), "got {resp}");
1576 let table = db.get_table("docs".to_string()).unwrap();
1577 assert_eq!(table.fts_indexes.len(), 1);
1578 assert_eq!(table.fts_indexes[0].name, "ix_body");
1579 assert_eq!(table.fts_indexes[0].column_name, "body");
1580 assert_eq!(table.fts_indexes[0].index.len(), 5);
1582 }
1583
1584 #[test]
1585 fn create_index_using_fts_rejects_non_text_column() {
1586 let mut db = Database::new("tempdb".to_string());
1587 process_command(
1588 "CREATE TABLE t (id INTEGER PRIMARY KEY, n INTEGER);",
1589 &mut db,
1590 )
1591 .unwrap();
1592 let err = process_command("CREATE INDEX ix_n ON t USING fts (n);", &mut db).unwrap_err();
1593 let msg = format!("{err}");
1594 assert!(
1595 msg.to_lowercase().contains("text"),
1596 "expected error mentioning TEXT; got: {msg}"
1597 );
1598 }
1599
1600 #[test]
1601 fn fts_match_returns_expected_rows() {
1602 let mut db = seed_fts_table();
1603 process_command("CREATE INDEX ix_body ON docs USING fts (body);", &mut db).unwrap();
1604 let resp = process_command(
1606 "SELECT id FROM docs WHERE fts_match(body, 'rust');",
1607 &mut db,
1608 )
1609 .unwrap();
1610 assert!(resp.contains("3 rows returned"), "got: {resp}");
1611 }
1612
1613 #[test]
1614 fn fts_match_without_index_errors_clearly() {
1615 let mut db = seed_fts_table();
1616 let err = process_command(
1618 "SELECT id FROM docs WHERE fts_match(body, 'rust');",
1619 &mut db,
1620 )
1621 .unwrap_err();
1622 let msg = format!("{err}");
1623 assert!(
1624 msg.contains("no FTS index"),
1625 "expected no-index error; got: {msg}"
1626 );
1627 }
1628
1629 #[test]
1630 fn bm25_score_orders_descending_by_relevance() {
1631 let mut db = seed_fts_table();
1632 process_command("CREATE INDEX ix_body ON docs USING fts (body);", &mut db).unwrap();
1633 let out = process_command_with_render(
1636 "SELECT id FROM docs WHERE fts_match(body, 'rust') \
1637 ORDER BY bm25_score(body, 'rust') DESC LIMIT 1;",
1638 &mut db,
1639 )
1640 .unwrap();
1641 assert!(out.status.contains("1 row returned"), "got: {}", out.status);
1642 let rendered = out.rendered.expect("SELECT should produce rendered output");
1643 assert!(
1645 rendered.contains(" 5 "),
1646 "expected id=5 to be top-ranked; rendered:\n{rendered}"
1647 );
1648 }
1649
1650 #[test]
1651 fn bm25_score_without_index_errors_clearly() {
1652 let mut db = seed_fts_table();
1653 let err = process_command(
1654 "SELECT id FROM docs ORDER BY bm25_score(body, 'rust') DESC LIMIT 1;",
1655 &mut db,
1656 )
1657 .unwrap_err();
1658 let msg = format!("{err}");
1659 assert!(
1660 msg.contains("no FTS index"),
1661 "expected no-index error; got: {msg}"
1662 );
1663 }
1664
1665 #[test]
1666 fn fts_post_create_inserts_are_indexed_incrementally() {
1667 let mut db = seed_fts_table();
1668 process_command("CREATE INDEX ix_body ON docs USING fts (body);", &mut db).unwrap();
1669 process_command(
1670 "INSERT INTO docs (body) VALUES ('rust embedded analytics');",
1671 &mut db,
1672 )
1673 .unwrap();
1674 let table = db.get_table("docs".to_string()).unwrap();
1675 assert_eq!(table.fts_indexes[0].index.len(), 6);
1677 let resp = process_command(
1679 "SELECT id FROM docs WHERE fts_match(body, 'analytics');",
1680 &mut db,
1681 )
1682 .unwrap();
1683 assert!(resp.contains("1 row returned"), "got: {resp}");
1684 }
1685
1686 #[test]
1687 fn delete_on_fts_indexed_table_marks_dirty() {
1688 let mut db = seed_fts_table();
1689 process_command("CREATE INDEX ix_body ON docs USING fts (body);", &mut db).unwrap();
1690 let resp = process_command("DELETE FROM docs WHERE id = 1;", &mut db).unwrap();
1691 assert!(resp.contains("1 row"), "got: {resp}");
1692 let docs = db.get_table("docs".to_string()).unwrap();
1693 let entry = docs
1694 .fts_indexes
1695 .iter()
1696 .find(|e| e.name == "ix_body")
1697 .unwrap();
1698 assert!(
1699 entry.needs_rebuild,
1700 "DELETE should have flagged the FTS index dirty"
1701 );
1702 }
1703
1704 #[test]
1705 fn update_on_fts_indexed_text_col_marks_dirty() {
1706 let mut db = seed_fts_table();
1707 process_command("CREATE INDEX ix_body ON docs USING fts (body);", &mut db).unwrap();
1708 let resp = process_command(
1709 "UPDATE docs SET body = 'java spring framework' WHERE id = 1;",
1710 &mut db,
1711 )
1712 .unwrap();
1713 assert!(resp.contains("1 row"), "got: {resp}");
1714 let docs = db.get_table("docs".to_string()).unwrap();
1715 let entry = docs
1716 .fts_indexes
1717 .iter()
1718 .find(|e| e.name == "ix_body")
1719 .unwrap();
1720 assert!(
1721 entry.needs_rebuild,
1722 "UPDATE on the indexed TEXT column should have flagged dirty"
1723 );
1724 }
1725
1726 #[test]
1727 fn fts_index_name_collides_with_btree_and_hnsw_namespaces() {
1728 let mut db = seed_fts_table();
1729 process_command("CREATE INDEX ix_body ON docs USING fts (body);", &mut db).unwrap();
1730 let err = process_command("CREATE INDEX ix_body ON docs (body);", &mut db).unwrap_err();
1731 let msg = format!("{err}");
1732 assert!(
1733 msg.to_lowercase().contains("already exists"),
1734 "expected duplicate-index error; got: {msg}"
1735 );
1736 }
1737
1738 #[test]
1739 fn fts_index_rejects_unique() {
1740 let mut db = seed_fts_table();
1741 let err = process_command(
1742 "CREATE UNIQUE INDEX ix_body ON docs USING fts (body);",
1743 &mut db,
1744 )
1745 .unwrap_err();
1746 let msg = format!("{err}");
1747 assert!(
1748 msg.to_lowercase().contains("unique"),
1749 "expected UNIQUE-rejection error; got: {msg}"
1750 );
1751 }
1752
1753 #[test]
1754 fn try_fts_probe_falls_through_on_ascending() {
1755 let mut db = seed_fts_table();
1760 process_command("CREATE INDEX ix_body ON docs USING fts (body);", &mut db).unwrap();
1761 let resp = process_command(
1764 "SELECT id FROM docs WHERE fts_match(body, 'rust') \
1765 ORDER BY bm25_score(body, 'rust') ASC LIMIT 3;",
1766 &mut db,
1767 )
1768 .unwrap();
1769 assert!(resp.contains("3 rows returned"), "got: {resp}");
1770 }
1771
1772 fn seed_vector_docs() -> Database {
1782 let mut db = Database::new("tempdb".to_string());
1783 process_command(
1784 "CREATE TABLE docs (id INTEGER PRIMARY KEY, e VECTOR(2));",
1785 &mut db,
1786 )
1787 .expect("create");
1788 process_command("INSERT INTO docs (e) VALUES ([1.0, 0.0]);", &mut db).expect("insert 1");
1789 process_command("INSERT INTO docs (e) VALUES ([0.0, 1.0]);", &mut db).expect("insert 2");
1790 process_command("INSERT INTO docs (e) VALUES ([1.0, 1.0]);", &mut db).expect("insert 3");
1791 db
1792 }
1793
1794 #[test]
1795 fn vec_distance_l2_in_where_filters_correctly() {
1796 let mut db = seed_vector_docs();
1802 let resp = process_command(
1803 "SELECT * FROM docs WHERE vec_distance_l2(e, [1.0, 0.0]) < 1.1;",
1804 &mut db,
1805 )
1806 .expect("select");
1807 assert!(
1808 resp.contains("2 rows returned"),
1809 "expected 2 rows, got: {resp}"
1810 );
1811 }
1812
1813 #[test]
1814 fn vec_distance_cosine_in_where() {
1815 let mut db = seed_vector_docs();
1820 let resp = process_command(
1821 "SELECT * FROM docs WHERE vec_distance_cosine(e, [1.0, 0.0]) < 0.5;",
1822 &mut db,
1823 )
1824 .expect("select");
1825 assert!(
1826 resp.contains("2 rows returned"),
1827 "expected 2 rows, got: {resp}"
1828 );
1829 }
1830
1831 #[test]
1832 fn vec_distance_dot_negated() {
1833 let mut db = seed_vector_docs();
1838 let resp = process_command(
1839 "SELECT * FROM docs WHERE vec_distance_dot(e, [1.0, 0.0]) < 0.0;",
1840 &mut db,
1841 )
1842 .expect("select");
1843 assert!(
1844 resp.contains("2 rows returned"),
1845 "expected 2 rows, got: {resp}"
1846 );
1847 }
1848
1849 #[test]
1850 fn knn_via_order_by_distance_limit() {
1851 let mut db = seed_vector_docs();
1855 let resp = process_command(
1856 "SELECT id FROM docs ORDER BY vec_distance_l2(e, [1.0, 0.0]) ASC LIMIT 2;",
1857 &mut db,
1858 )
1859 .expect("select");
1860 assert!(
1861 resp.contains("2 rows returned"),
1862 "expected 2 rows, got: {resp}"
1863 );
1864 }
1865
1866 #[test]
1867 fn distance_function_dim_mismatch_errors() {
1868 let mut db = seed_vector_docs();
1870 let err = process_command(
1871 "SELECT * FROM docs WHERE vec_distance_l2(e, [1.0, 0.0, 0.0]) < 1.0;",
1872 &mut db,
1873 )
1874 .unwrap_err();
1875 let msg = format!("{err}");
1876 assert!(
1877 msg.to_lowercase().contains("dimension")
1878 && msg.contains("lhs=2")
1879 && msg.contains("rhs=3"),
1880 "expected dim mismatch error, got: {msg}"
1881 );
1882 }
1883
1884 #[test]
1885 fn unknown_function_errors_with_name() {
1886 let mut db = seed_vector_docs();
1890 let err = process_command(
1891 "SELECT * FROM docs WHERE vec_does_not_exist(e, [1.0, 0.0]) < 1.0;",
1892 &mut db,
1893 )
1894 .unwrap_err();
1895 let msg = format!("{err}");
1896 assert!(
1897 msg.contains("vec_does_not_exist"),
1898 "expected error mentioning function name, got: {msg}"
1899 );
1900 }
1901
1902 fn seed_json_table() -> Database {
1907 let mut db = Database::new("tempdb".to_string());
1908 process_command(
1909 "CREATE TABLE docs (id INTEGER PRIMARY KEY, payload JSON);",
1910 &mut db,
1911 )
1912 .expect("create json table");
1913 db
1914 }
1915
1916 #[test]
1917 fn json_column_round_trip_primitive_values() {
1918 let mut db = seed_json_table();
1919 process_command(
1920 r#"INSERT INTO docs (payload) VALUES ('{"name": "alice", "age": 30}');"#,
1921 &mut db,
1922 )
1923 .expect("insert json");
1924 let docs = db.get_table("docs".to_string()).unwrap();
1925 let rowids = docs.rowids();
1926 assert_eq!(rowids.len(), 1);
1927 match docs.get_value("payload", rowids[0]) {
1929 Some(Value::Text(s)) => {
1930 assert!(s.contains("alice"), "expected JSON text to round-trip: {s}");
1931 }
1932 other => panic!("expected Value::Text holding JSON, got {other:?}"),
1933 }
1934 }
1935
1936 #[test]
1937 fn json_insert_rejects_invalid_json() {
1938 let mut db = seed_json_table();
1939 let err = process_command(
1940 "INSERT INTO docs (payload) VALUES ('not-valid-json{');",
1941 &mut db,
1942 )
1943 .unwrap_err();
1944 let msg = format!("{err}").to_lowercase();
1945 assert!(
1946 msg.contains("json") && msg.contains("payload"),
1947 "expected JSON validation error mentioning column, got: {msg}"
1948 );
1949 }
1950
1951 #[test]
1952 fn json_extract_object_field() {
1953 let mut db = seed_json_table();
1954 process_command(
1955 r#"INSERT INTO docs (payload) VALUES ('{"name": "alice", "age": 30}');"#,
1956 &mut db,
1957 )
1958 .unwrap();
1959 let resp = process_command(
1962 r#"SELECT id FROM docs WHERE json_extract(payload, '$.name') = 'alice';"#,
1963 &mut db,
1964 )
1965 .expect("select via json_extract");
1966 assert!(resp.contains("1 row returned"), "got: {resp}");
1967
1968 let resp = process_command(
1969 r#"SELECT id FROM docs WHERE json_extract(payload, '$.age') = 30;"#,
1970 &mut db,
1971 )
1972 .expect("select via numeric json_extract");
1973 assert!(resp.contains("1 row returned"), "got: {resp}");
1974 }
1975
1976 #[test]
1977 fn json_extract_array_index_and_nested() {
1978 let mut db = seed_json_table();
1979 process_command(
1980 r#"INSERT INTO docs (payload) VALUES ('{"tags": ["rust", "sql", "vectors"], "meta": {"author": "joao"}}');"#,
1981 &mut db,
1982 )
1983 .unwrap();
1984 let resp = process_command(
1985 r#"SELECT id FROM docs WHERE json_extract(payload, '$.tags[0]') = 'rust';"#,
1986 &mut db,
1987 )
1988 .expect("select via array index");
1989 assert!(resp.contains("1 row returned"), "got: {resp}");
1990
1991 let resp = process_command(
1992 r#"SELECT id FROM docs WHERE json_extract(payload, '$.meta.author') = 'joao';"#,
1993 &mut db,
1994 )
1995 .expect("select via nested object");
1996 assert!(resp.contains("1 row returned"), "got: {resp}");
1997 }
1998
1999 #[test]
2000 fn json_extract_missing_path_returns_null() {
2001 let mut db = seed_json_table();
2002 process_command(
2003 r#"INSERT INTO docs (payload) VALUES ('{"name": "alice"}');"#,
2004 &mut db,
2005 )
2006 .unwrap();
2007 let resp = process_command(
2010 r#"SELECT id FROM docs WHERE json_extract(payload, '$.missing') = 'something';"#,
2011 &mut db,
2012 )
2013 .expect("select with missing path");
2014 assert!(resp.contains("0 rows returned"), "got: {resp}");
2015 }
2016
2017 #[test]
2018 fn json_extract_malformed_path_errors() {
2019 let mut db = seed_json_table();
2020 process_command(
2021 r#"INSERT INTO docs (payload) VALUES ('{"a": 1}');"#,
2022 &mut db,
2023 )
2024 .unwrap();
2025 let err = process_command(
2027 r#"SELECT id FROM docs WHERE json_extract(payload, 'a.b') = 1;"#,
2028 &mut db,
2029 )
2030 .unwrap_err();
2031 assert!(format!("{err}").contains("'$'"));
2032 }
2033
2034 #[test]
2035 fn json_array_length_on_array() {
2036 let mut db = seed_json_table();
2039 process_command(
2040 r#"INSERT INTO docs (payload) VALUES ('{"tags": ["a", "b", "c"]}');"#,
2041 &mut db,
2042 )
2043 .unwrap();
2044 let resp = process_command(
2045 r#"SELECT id FROM docs WHERE json_array_length(payload, '$.tags') = 3;"#,
2046 &mut db,
2047 )
2048 .expect("select via array_length");
2049 assert!(resp.contains("1 row returned"), "got: {resp}");
2050 }
2051
2052 #[test]
2053 fn json_array_length_on_non_array_errors() {
2054 let mut db = seed_json_table();
2055 process_command(
2056 r#"INSERT INTO docs (payload) VALUES ('{"tags": "not-an-array"}');"#,
2057 &mut db,
2058 )
2059 .unwrap();
2060 let err = process_command(
2061 r#"SELECT id FROM docs WHERE json_array_length(payload, '$.tags') = 1;"#,
2062 &mut db,
2063 )
2064 .unwrap_err();
2065 let msg = format!("{err}").to_lowercase();
2066 assert!(
2067 msg.contains("non-array"),
2068 "expected non-array error, got: {msg}"
2069 );
2070 }
2071
2072 #[test]
2073 fn json_type_recognizes_each_kind() {
2074 let mut db = seed_json_table();
2075 process_command(
2076 r#"INSERT INTO docs (payload) VALUES ('{"o": {}, "a": [], "s": "x", "i": 1, "f": 1.5, "t": true, "n": null}');"#,
2077 &mut db,
2078 )
2079 .unwrap();
2080 let cases = &[
2081 ("$.o", "object"),
2082 ("$.a", "array"),
2083 ("$.s", "text"),
2084 ("$.i", "integer"),
2085 ("$.f", "real"),
2086 ("$.t", "true"),
2087 ("$.n", "null"),
2088 ];
2089 for (path, expected_type) in cases {
2090 let sql = format!(
2091 "SELECT id FROM docs WHERE json_type(payload, '{path}') = '{expected_type}';"
2092 );
2093 let resp =
2094 process_command(&sql, &mut db).unwrap_or_else(|e| panic!("path {path}: {e}"));
2095 assert!(
2096 resp.contains("1 row returned"),
2097 "path {path} expected type {expected_type}; got response: {resp}"
2098 );
2099 }
2100 }
2101
2102 #[test]
2103 fn update_on_json_column_revalidates() {
2104 let mut db = seed_json_table();
2105 process_command(
2106 r#"INSERT INTO docs (payload) VALUES ('{"a": 1}');"#,
2107 &mut db,
2108 )
2109 .unwrap();
2110 process_command(
2112 r#"UPDATE docs SET payload = '{"a": 2, "b": 3}' WHERE id = 1;"#,
2113 &mut db,
2114 )
2115 .expect("valid JSON UPDATE");
2116 let err = process_command(
2119 r#"UPDATE docs SET payload = 'not-json{' WHERE id = 1;"#,
2120 &mut db,
2121 )
2122 .unwrap_err();
2123 let msg = format!("{err}").to_lowercase();
2124 assert!(
2125 msg.contains("json") && msg.contains("payload"),
2126 "got: {msg}"
2127 );
2128 }
2129
2130 #[test]
2135 fn default_literal_int_applies_when_column_omitted() {
2136 let mut db = Database::new("t".to_string());
2137 process_command(
2138 "CREATE TABLE t (id INTEGER PRIMARY KEY, n INTEGER DEFAULT 42);",
2139 &mut db,
2140 )
2141 .unwrap();
2142 process_command("INSERT INTO t (id) VALUES (1);", &mut db).unwrap();
2143
2144 let table = db.get_table("t".to_string()).unwrap();
2145 assert_eq!(table.get_value("n", 1), Some(Value::Integer(42)));
2146 }
2147
2148 #[test]
2149 fn default_literal_text_applies_when_column_omitted() {
2150 let mut db = Database::new("t".to_string());
2151 process_command(
2152 "CREATE TABLE users (id INTEGER PRIMARY KEY, status TEXT DEFAULT 'active');",
2153 &mut db,
2154 )
2155 .unwrap();
2156 process_command("INSERT INTO users (id) VALUES (1);", &mut db).unwrap();
2157
2158 let table = db.get_table("users".to_string()).unwrap();
2159 assert_eq!(
2160 table.get_value("status", 1),
2161 Some(Value::Text("active".to_string()))
2162 );
2163 }
2164
2165 #[test]
2166 fn default_literal_real_negative_applies_when_column_omitted() {
2167 let mut db = Database::new("t".to_string());
2169 process_command(
2170 "CREATE TABLE t (id INTEGER PRIMARY KEY, score REAL DEFAULT -1.5);",
2171 &mut db,
2172 )
2173 .unwrap();
2174 process_command("INSERT INTO t (id) VALUES (1);", &mut db).unwrap();
2175
2176 let table = db.get_table("t".to_string()).unwrap();
2177 assert_eq!(table.get_value("score", 1), Some(Value::Real(-1.5)));
2178 }
2179
2180 #[test]
2181 fn default_with_type_mismatch_errors_at_create_time() {
2182 let mut db = Database::new("t".to_string());
2183 let result = process_command(
2184 "CREATE TABLE t (id INTEGER PRIMARY KEY, n INTEGER DEFAULT 'oops');",
2185 &mut db,
2186 );
2187 let err = result.expect_err("text default on INTEGER column should be rejected");
2188 let msg = format!("{err}").to_lowercase();
2189 assert!(msg.contains("default"), "got: {msg}");
2190 }
2191
2192 #[test]
2193 fn default_for_json_column_must_be_valid_json() {
2194 let mut db = Database::new("t".to_string());
2198 let err = process_command(
2199 "CREATE TABLE t (id INTEGER PRIMARY KEY, doc JSON DEFAULT 'not-json{');",
2200 &mut db,
2201 )
2202 .unwrap_err();
2203 assert!(
2204 format!("{err}").to_lowercase().contains("json"),
2205 "got: {err}"
2206 );
2207
2208 process_command(
2210 "CREATE TABLE t2 (id INTEGER PRIMARY KEY, doc JSON DEFAULT '{\"k\":1}');",
2211 &mut db,
2212 )
2213 .expect("valid JSON DEFAULT should be accepted");
2214 }
2215
2216 #[test]
2217 fn default_with_non_literal_expression_errors_at_create_time() {
2218 let mut db = Database::new("t".to_string());
2219 let result = process_command(
2222 "CREATE TABLE t (id INTEGER PRIMARY KEY, ts TEXT DEFAULT CURRENT_TIMESTAMP);",
2223 &mut db,
2224 );
2225 let err = result.expect_err("non-literal DEFAULT should be rejected");
2226 let msg = format!("{err}").to_lowercase();
2227 assert!(
2228 msg.contains("default") && msg.contains("literal"),
2229 "got: {msg}"
2230 );
2231 }
2232
2233 #[test]
2234 fn default_null_is_accepted_at_create_time() {
2235 let mut db = Database::new("t".to_string());
2239 process_command(
2240 "CREATE TABLE t (id INTEGER PRIMARY KEY, note TEXT DEFAULT NULL);",
2241 &mut db,
2242 )
2243 .expect("CREATE TABLE with DEFAULT NULL should be accepted");
2244 let table = db.get_table("t".to_string()).unwrap();
2245 let note = table
2246 .columns
2247 .iter()
2248 .find(|c| c.column_name == "note")
2249 .unwrap();
2250 assert_eq!(note.default, Some(Value::Null));
2251 }
2252
2253 #[test]
2258 fn drop_table_basic() {
2259 let mut db = seed_users_table();
2260 let response = process_command("DROP TABLE users;", &mut db).expect("drop table");
2261 assert!(response.contains("1 table dropped"));
2262 assert!(!db.contains_table("users".to_string()));
2263 }
2264
2265 #[test]
2266 fn drop_table_if_exists_noop_on_missing() {
2267 let mut db = Database::new("t".to_string());
2268 let response =
2269 process_command("DROP TABLE IF EXISTS missing;", &mut db).expect("drop if exists");
2270 assert!(response.contains("0 tables dropped"));
2271 }
2272
2273 #[test]
2274 fn drop_table_missing_errors_without_if_exists() {
2275 let mut db = Database::new("t".to_string());
2276 let err = process_command("DROP TABLE missing;", &mut db).unwrap_err();
2277 assert!(format!("{err}").contains("does not exist"), "got: {err}");
2278 }
2279
2280 #[test]
2281 fn drop_table_reserved_name_errors() {
2282 let mut db = Database::new("t".to_string());
2283 let err = process_command("DROP TABLE sqlrite_master;", &mut db).unwrap_err();
2284 assert!(format!("{err}").contains("reserved"), "got: {err}");
2285 }
2286
2287 #[test]
2288 fn drop_table_multi_target_rejected() {
2289 let mut db = seed_users_table();
2290 process_command("CREATE TABLE other (id INTEGER PRIMARY KEY);", &mut db).unwrap();
2291 let err = process_command("DROP TABLE users, other;", &mut db).unwrap_err();
2294 assert!(format!("{err}").contains("single table"), "got: {err}");
2295 }
2296
2297 #[test]
2298 fn drop_table_cascades_indexes_in_memory() {
2299 let mut db = seed_users_table();
2300 process_command("CREATE INDEX users_age_idx ON users (age);", &mut db).unwrap();
2301 let users = db.get_table("users".to_string()).unwrap();
2303 assert!(
2304 users
2305 .secondary_indexes
2306 .iter()
2307 .any(|i| i.name == "users_age_idx")
2308 );
2309
2310 process_command("DROP TABLE users;", &mut db).unwrap();
2311
2312 for table in db.tables.values() {
2314 assert!(
2315 !table
2316 .secondary_indexes
2317 .iter()
2318 .any(|i| i.name.contains("users")),
2319 "dropped table's indexes should not survive on any other table"
2320 );
2321 }
2322 }
2323
2324 #[test]
2325 fn drop_index_explicit_basic() {
2326 let mut db = seed_users_table();
2327 process_command("CREATE INDEX users_age_idx ON users (age);", &mut db).unwrap();
2328 let response = process_command("DROP INDEX users_age_idx;", &mut db).expect("drop index");
2329 assert!(response.contains("1 index dropped"));
2330
2331 let users = db.get_table("users".to_string()).unwrap();
2332 assert!(users.index_by_name("users_age_idx").is_none());
2333 }
2334
2335 #[test]
2336 fn drop_index_refuses_auto_index() {
2337 let mut db = seed_users_table();
2338 let err = process_command("DROP INDEX sqlrite_autoindex_users_id;", &mut db).unwrap_err();
2341 assert!(format!("{err}").contains("auto-created"), "got: {err}");
2342 }
2343
2344 #[test]
2345 fn drop_index_if_exists_noop_on_missing() {
2346 let mut db = Database::new("t".to_string());
2347 let response =
2348 process_command("DROP INDEX IF EXISTS nope;", &mut db).expect("drop index if exists");
2349 assert!(response.contains("0 indexes dropped"));
2350 }
2351
2352 #[test]
2353 fn drop_index_missing_errors_without_if_exists() {
2354 let mut db = Database::new("t".to_string());
2355 let err = process_command("DROP INDEX nope;", &mut db).unwrap_err();
2356 assert!(format!("{err}").contains("does not exist"), "got: {err}");
2357 }
2358
2359 #[test]
2360 fn drop_statements_rejected_on_readonly_db() {
2361 use crate::sql::pager::{open_database_read_only, save_database};
2362
2363 let mut seed = Database::new("t".to_string());
2364 process_command(
2365 "CREATE TABLE notes (id INTEGER PRIMARY KEY, body TEXT);",
2366 &mut seed,
2367 )
2368 .unwrap();
2369 process_command("CREATE INDEX notes_body ON notes (body);", &mut seed).unwrap();
2370 let path = {
2371 let mut p = std::env::temp_dir();
2372 let pid = std::process::id();
2373 let nanos = std::time::SystemTime::now()
2374 .duration_since(std::time::UNIX_EPOCH)
2375 .map(|d| d.as_nanos())
2376 .unwrap_or(0);
2377 p.push(format!("sqlrite-drop-ro-{pid}-{nanos}.sqlrite"));
2378 p
2379 };
2380 save_database(&mut seed, &path).unwrap();
2381 drop(seed);
2382
2383 let mut ro = open_database_read_only(&path, "t".to_string()).unwrap();
2384 for stmt in ["DROP TABLE notes;", "DROP INDEX notes_body;"] {
2385 let err = process_command(stmt, &mut ro).unwrap_err();
2386 assert!(
2387 format!("{err}").contains("read-only"),
2388 "{stmt:?} should surface read-only error, got: {err}"
2389 );
2390 }
2391
2392 let _ = std::fs::remove_file(&path);
2393 let mut wal = path.as_os_str().to_owned();
2394 wal.push("-wal");
2395 let _ = std::fs::remove_file(std::path::PathBuf::from(wal));
2396 }
2397
2398 #[test]
2403 fn alter_rename_table_basic() {
2404 let mut db = seed_users_table();
2405 process_command("ALTER TABLE users RENAME TO members;", &mut db).expect("rename table");
2406 assert!(!db.contains_table("users".to_string()));
2407 assert!(db.contains_table("members".to_string()));
2408 let response = process_command("SELECT * FROM members;", &mut db).expect("select");
2410 assert!(response.contains("3 rows returned"));
2411 }
2412
2413 #[test]
2414 fn alter_rename_table_renames_auto_indexes() {
2415 let mut db = Database::new("t".to_string());
2418 process_command(
2419 "CREATE TABLE accounts (id INTEGER PRIMARY KEY, email TEXT UNIQUE);",
2420 &mut db,
2421 )
2422 .unwrap();
2423 {
2424 let accounts = db.get_table("accounts".to_string()).unwrap();
2425 assert!(
2426 accounts
2427 .index_by_name("sqlrite_autoindex_accounts_id")
2428 .is_some()
2429 );
2430 assert!(
2431 accounts
2432 .index_by_name("sqlrite_autoindex_accounts_email")
2433 .is_some()
2434 );
2435 }
2436 process_command("ALTER TABLE accounts RENAME TO members;", &mut db).expect("rename");
2437 let members = db.get_table("members".to_string()).unwrap();
2438 assert!(
2439 members
2440 .index_by_name("sqlrite_autoindex_members_id")
2441 .is_some(),
2442 "PK auto-index should be renamed to match new table"
2443 );
2444 assert!(
2445 members
2446 .index_by_name("sqlrite_autoindex_members_email")
2447 .is_some()
2448 );
2449 assert!(
2451 members
2452 .index_by_name("sqlrite_autoindex_accounts_id")
2453 .is_none()
2454 );
2455 for idx in &members.secondary_indexes {
2457 assert_eq!(idx.table_name, "members");
2458 }
2459 }
2460
2461 #[test]
2462 fn alter_rename_table_to_existing_errors() {
2463 let mut db = seed_users_table();
2464 process_command("CREATE TABLE other (id INTEGER PRIMARY KEY);", &mut db).unwrap();
2465 let err = process_command("ALTER TABLE users RENAME TO other;", &mut db).unwrap_err();
2466 assert!(format!("{err}").contains("already exists"), "got: {err}");
2467 assert!(db.contains_table("users".to_string()));
2469 assert!(db.contains_table("other".to_string()));
2470 }
2471
2472 #[test]
2473 fn alter_rename_table_to_reserved_name_errors() {
2474 let mut db = seed_users_table();
2475 let err =
2476 process_command("ALTER TABLE users RENAME TO sqlrite_master;", &mut db).unwrap_err();
2477 assert!(format!("{err}").contains("reserved"), "got: {err}");
2478 }
2479
2480 #[test]
2481 fn alter_rename_column_basic() {
2482 let mut db = seed_users_table();
2483 process_command(
2484 "ALTER TABLE users RENAME COLUMN name TO full_name;",
2485 &mut db,
2486 )
2487 .expect("rename column");
2488
2489 let users = db.get_table("users".to_string()).unwrap();
2490 assert!(users.contains_column("full_name".to_string()));
2491 assert!(!users.contains_column("name".to_string()));
2492
2493 let bob_rowid = users
2496 .rowids()
2497 .into_iter()
2498 .find(|r| users.get_value("full_name", *r) == Some(Value::Text("bob".to_string())))
2499 .expect("bob row should be findable under the new column name");
2500 assert_eq!(
2501 users.get_value("full_name", bob_rowid),
2502 Some(Value::Text("bob".to_string()))
2503 );
2504 }
2505
2506 #[test]
2507 fn alter_rename_column_collision_errors() {
2508 let mut db = seed_users_table();
2509 let err =
2510 process_command("ALTER TABLE users RENAME COLUMN name TO age;", &mut db).unwrap_err();
2511 assert!(format!("{err}").contains("already exists"), "got: {err}");
2512 }
2513
2514 #[test]
2515 fn alter_rename_column_updates_indexes() {
2516 let mut db = Database::new("t".to_string());
2518 process_command(
2519 "CREATE TABLE accounts (id INTEGER PRIMARY KEY, email TEXT UNIQUE);",
2520 &mut db,
2521 )
2522 .unwrap();
2523 process_command(
2524 "ALTER TABLE accounts RENAME COLUMN email TO contact;",
2525 &mut db,
2526 )
2527 .unwrap();
2528 let accounts = db.get_table("accounts".to_string()).unwrap();
2529 assert!(
2530 accounts
2531 .index_by_name("sqlrite_autoindex_accounts_contact")
2532 .is_some()
2533 );
2534 assert!(
2535 accounts
2536 .index_by_name("sqlrite_autoindex_accounts_email")
2537 .is_none()
2538 );
2539 }
2540
2541 #[test]
2542 fn alter_add_column_basic() {
2543 let mut db = seed_users_table();
2544 process_command("ALTER TABLE users ADD COLUMN nickname TEXT;", &mut db)
2545 .expect("add column");
2546 let users = db.get_table("users".to_string()).unwrap();
2547 assert!(users.contains_column("nickname".to_string()));
2548 let any_rowid = *users.rowids().first().expect("seed has rows");
2550 assert_eq!(users.get_value("nickname", any_rowid), None);
2551
2552 process_command(
2554 "INSERT INTO users (name, age, nickname) VALUES ('dan', 22, 'd');",
2555 &mut db,
2556 )
2557 .expect("insert with new col");
2558 let users = db.get_table("users".to_string()).unwrap();
2559 let dan_rowid = users
2560 .rowids()
2561 .into_iter()
2562 .find(|r| users.get_value("name", *r) == Some(Value::Text("dan".to_string())))
2563 .unwrap();
2564 assert_eq!(
2565 users.get_value("nickname", dan_rowid),
2566 Some(Value::Text("d".to_string()))
2567 );
2568 }
2569
2570 #[test]
2571 fn alter_add_column_with_default_backfills_existing_rows() {
2572 let mut db = seed_users_table();
2573 process_command(
2574 "ALTER TABLE users ADD COLUMN status TEXT DEFAULT 'active';",
2575 &mut db,
2576 )
2577 .expect("add column with default");
2578 let users = db.get_table("users".to_string()).unwrap();
2579 for rowid in users.rowids() {
2580 assert_eq!(
2581 users.get_value("status", rowid),
2582 Some(Value::Text("active".to_string())),
2583 "rowid {rowid} should have been backfilled with the default"
2584 );
2585 }
2586 }
2587
2588 #[test]
2589 fn alter_add_column_not_null_with_default_works_on_nonempty_table() {
2590 let mut db = seed_users_table();
2591 process_command(
2592 "ALTER TABLE users ADD COLUMN score INTEGER NOT NULL DEFAULT 0;",
2593 &mut db,
2594 )
2595 .expect("NOT NULL ADD with DEFAULT should succeed even with existing rows");
2596 let users = db.get_table("users".to_string()).unwrap();
2597 for rowid in users.rowids() {
2598 assert_eq!(users.get_value("score", rowid), Some(Value::Integer(0)));
2599 }
2600 }
2601
2602 #[test]
2603 fn alter_add_column_not_null_without_default_errors_on_nonempty_table() {
2604 let mut db = seed_users_table();
2605 let err = process_command(
2606 "ALTER TABLE users ADD COLUMN score INTEGER NOT NULL;",
2607 &mut db,
2608 )
2609 .unwrap_err();
2610 let msg = format!("{err}").to_lowercase();
2611 assert!(
2612 msg.contains("not null") && msg.contains("default"),
2613 "got: {msg}"
2614 );
2615 }
2616
2617 #[test]
2618 fn alter_add_column_pk_rejected() {
2619 let mut db = seed_users_table();
2620 let err = process_command(
2621 "ALTER TABLE users ADD COLUMN extra INTEGER PRIMARY KEY;",
2622 &mut db,
2623 )
2624 .unwrap_err();
2625 assert!(
2626 format!("{err}").to_lowercase().contains("primary key"),
2627 "got: {err}"
2628 );
2629 }
2630
2631 #[test]
2632 fn alter_add_column_unique_rejected() {
2633 let mut db = seed_users_table();
2634 let err = process_command(
2635 "ALTER TABLE users ADD COLUMN extra INTEGER UNIQUE;",
2636 &mut db,
2637 )
2638 .unwrap_err();
2639 assert!(
2640 format!("{err}").to_lowercase().contains("unique"),
2641 "got: {err}"
2642 );
2643 }
2644
2645 #[test]
2646 fn alter_add_column_existing_name_errors() {
2647 let mut db = seed_users_table();
2648 let err =
2649 process_command("ALTER TABLE users ADD COLUMN age INTEGER;", &mut db).unwrap_err();
2650 assert!(format!("{err}").contains("already exists"), "got: {err}");
2651 }
2652
2653 #[test]
2659 fn alter_drop_column_basic() {
2660 let mut db = seed_users_table();
2661 process_command("ALTER TABLE users DROP COLUMN age;", &mut db).expect("drop column");
2662 let users = db.get_table("users".to_string()).unwrap();
2663 assert!(!users.contains_column("age".to_string()));
2664 assert!(users.contains_column("name".to_string()));
2666 assert_eq!(users.rowids().len(), 3);
2667 }
2668
2669 #[test]
2670 fn alter_drop_column_drops_dependent_indexes() {
2671 let mut db = seed_users_table();
2672 process_command("CREATE INDEX users_age_idx ON users (age);", &mut db).unwrap();
2673 process_command("ALTER TABLE users DROP COLUMN age;", &mut db).unwrap();
2674 let users = db.get_table("users".to_string()).unwrap();
2675 assert!(users.index_by_name("users_age_idx").is_none());
2676 }
2677
2678 #[test]
2679 fn alter_drop_column_pk_errors() {
2680 let mut db = seed_users_table();
2681 let err = process_command("ALTER TABLE users DROP COLUMN id;", &mut db).unwrap_err();
2682 assert!(
2683 format!("{err}").to_lowercase().contains("primary key"),
2684 "got: {err}"
2685 );
2686 }
2687
2688 #[test]
2689 fn alter_drop_column_only_column_errors() {
2690 let mut db = Database::new("t".to_string());
2691 process_command("CREATE TABLE solo (only_col TEXT);", &mut db).unwrap();
2692 let err = process_command("ALTER TABLE solo DROP COLUMN only_col;", &mut db).unwrap_err();
2693 assert!(
2694 format!("{err}").to_lowercase().contains("only column"),
2695 "got: {err}"
2696 );
2697 }
2698
2699 #[test]
2700 fn alter_unknown_table_errors_without_if_exists() {
2701 let mut db = Database::new("t".to_string());
2702 let err = process_command("ALTER TABLE missing RENAME TO other;", &mut db).unwrap_err();
2703 assert!(format!("{err}").contains("does not exist"), "got: {err}");
2704 }
2705
2706 #[test]
2707 fn alter_unknown_table_if_exists_noop() {
2708 let mut db = Database::new("t".to_string());
2709 let response = process_command("ALTER TABLE IF EXISTS missing RENAME TO other;", &mut db)
2710 .expect("IF EXISTS makes missing-table ALTER a no-op");
2711 assert!(response.contains("no-op"));
2712 }
2713
2714 #[test]
2715 fn drop_table_inside_transaction_rolls_back() {
2716 let mut db = seed_users_table();
2720 process_command("CREATE INDEX users_age_idx ON users (age);", &mut db).unwrap();
2721 process_command("BEGIN;", &mut db).unwrap();
2722 process_command("DROP TABLE users;", &mut db).unwrap();
2723 assert!(!db.contains_table("users".to_string()));
2724 process_command("ROLLBACK;", &mut db).unwrap();
2725 assert!(db.contains_table("users".to_string()));
2726 let users = db.get_table("users".to_string()).unwrap();
2727 assert_eq!(users.rowids().len(), 3);
2728 assert!(users.index_by_name("users_age_idx").is_some());
2729 }
2730
2731 #[test]
2732 fn alter_inside_transaction_rolls_back() {
2733 let mut db = seed_users_table();
2734 process_command("BEGIN;", &mut db).unwrap();
2735 process_command(
2736 "ALTER TABLE users ADD COLUMN status TEXT DEFAULT 'active';",
2737 &mut db,
2738 )
2739 .unwrap();
2740 assert!(
2742 db.get_table("users".to_string())
2743 .unwrap()
2744 .contains_column("status".to_string())
2745 );
2746 process_command("ROLLBACK;", &mut db).unwrap();
2747 assert!(
2749 !db.get_table("users".to_string())
2750 .unwrap()
2751 .contains_column("status".to_string())
2752 );
2753 }
2754
2755 #[test]
2756 fn alter_rejected_on_readonly_db() {
2757 use crate::sql::pager::{open_database_read_only, save_database};
2758
2759 let mut seed = Database::new("t".to_string());
2760 process_command(
2761 "CREATE TABLE notes (id INTEGER PRIMARY KEY, body TEXT);",
2762 &mut seed,
2763 )
2764 .unwrap();
2765 let path = {
2766 let mut p = std::env::temp_dir();
2767 let pid = std::process::id();
2768 let nanos = std::time::SystemTime::now()
2769 .duration_since(std::time::UNIX_EPOCH)
2770 .map(|d| d.as_nanos())
2771 .unwrap_or(0);
2772 p.push(format!("sqlrite-alter-ro-{pid}-{nanos}.sqlrite"));
2773 p
2774 };
2775 save_database(&mut seed, &path).unwrap();
2776 drop(seed);
2777
2778 let mut ro = open_database_read_only(&path, "t".to_string()).unwrap();
2779 for stmt in [
2780 "ALTER TABLE notes RENAME TO n2;",
2781 "ALTER TABLE notes RENAME COLUMN body TO b;",
2782 "ALTER TABLE notes ADD COLUMN extra TEXT;",
2783 "ALTER TABLE notes DROP COLUMN body;",
2784 ] {
2785 let err = process_command(stmt, &mut ro).unwrap_err();
2786 assert!(
2787 format!("{err}").contains("read-only"),
2788 "{stmt:?} should surface read-only error, got: {err}"
2789 );
2790 }
2791
2792 let _ = std::fs::remove_file(&path);
2793 let mut wal = path.as_os_str().to_owned();
2794 wal.push("-wal");
2795 let _ = std::fs::remove_file(std::path::PathBuf::from(wal));
2796 }
2797}