1pub mod agg;
2pub mod db;
3pub mod dialect;
4pub mod executor;
5pub mod fts;
6pub mod hnsw;
7pub mod pager;
8pub mod params;
9pub mod parser;
10pub mod pragma;
11use parser::create::CreateQuery;
14use parser::insert::InsertQuery;
15use parser::select::SelectQuery;
16
17use sqlparser::ast::{AlterTableOperation, ObjectType, Statement};
18use sqlparser::parser::{Parser, ParserError};
19
20use crate::sql::dialect::SqlriteDialect;
21
22use crate::error::{Result, SQLRiteError};
23use crate::sql::db::database::Database;
24use crate::sql::db::table::Table;
25
26#[derive(Debug, PartialEq)]
27pub enum SQLCommand {
28 Insert(String),
29 Delete(String),
30 Update(String),
31 CreateTable(String),
32 Select(String),
33 Unknown(String),
34}
35
36impl SQLCommand {
37 pub fn new(command: String) -> SQLCommand {
38 let v = command.split(" ").collect::<Vec<&str>>();
39 match v[0] {
40 "insert" => SQLCommand::Insert(command),
41 "update" => SQLCommand::Update(command),
42 "delete" => SQLCommand::Delete(command),
43 "create" => SQLCommand::CreateTable(command),
44 "select" => SQLCommand::Select(command),
45 _ => SQLCommand::Unknown(command),
46 }
47 }
48}
49
50#[derive(Debug, Clone)]
69pub struct CommandOutput {
70 pub status: String,
71 pub rendered: Option<String>,
72}
73
74pub fn process_command(query: &str, db: &mut Database) -> Result<String> {
83 process_command_with_render(query, db).map(|o| o.status)
84}
85
86pub fn process_command_with_render(query: &str, db: &mut Database) -> Result<CommandOutput> {
93 if let Some(stmt) = pragma::try_parse_pragma(query)? {
98 return pragma::execute_pragma(stmt, db);
99 }
100
101 let dialect = SqlriteDialect::new();
102 let mut ast = Parser::parse_sql(&dialect, query).map_err(SQLRiteError::from)?;
103
104 if ast.len() > 1 {
105 return Err(SQLRiteError::SqlError(ParserError::ParserError(format!(
106 "Expected a single query statement, but there are {}",
107 ast.len()
108 ))));
109 }
110
111 let Some(query) = ast.pop() else {
115 return Ok(CommandOutput {
116 status: "No statement to execute.".to_string(),
117 rendered: None,
118 });
119 };
120 process_ast_with_render(query, db)
121}
122
123pub fn process_ast_with_render(query: Statement, db: &mut Database) -> Result<CommandOutput> {
128 let message: String;
129 let mut rendered: Option<String> = None;
130
131 match &query {
136 Statement::StartTransaction { .. } => {
137 db.begin_transaction()?;
138 return Ok(CommandOutput {
139 status: String::from("BEGIN"),
140 rendered: None,
141 });
142 }
143 Statement::Commit { .. } => {
144 if !db.in_transaction() {
145 return Err(SQLRiteError::General(
146 "cannot COMMIT: no transaction is open".to_string(),
147 ));
148 }
149 if let Some(path) = db.source_path.clone() {
159 if let Err(save_err) = pager::save_database(db, &path) {
160 let _ = db.rollback_transaction();
161 return Err(SQLRiteError::General(format!(
162 "COMMIT failed — transaction rolled back: {save_err}"
163 )));
164 }
165 }
166 db.commit_transaction()?;
167 return Ok(CommandOutput {
168 status: String::from("COMMIT"),
169 rendered: None,
170 });
171 }
172 Statement::Rollback { .. } => {
173 db.rollback_transaction()?;
174 return Ok(CommandOutput {
175 status: String::from("ROLLBACK"),
176 rendered: None,
177 });
178 }
179 _ => {}
180 }
181
182 let is_write_statement = matches!(
188 &query,
189 Statement::CreateTable(_)
190 | Statement::CreateIndex(_)
191 | Statement::Insert(_)
192 | Statement::Update(_)
193 | Statement::Delete(_)
194 | Statement::Drop { .. }
195 | Statement::AlterTable(_)
196 | Statement::Vacuum(_)
197 );
198 let is_vacuum = matches!(&query, Statement::Vacuum(_));
199
200 let releases_pages = match &query {
206 Statement::Drop { object_type, .. } => {
207 matches!(object_type, ObjectType::Table | ObjectType::Index)
208 }
209 Statement::AlterTable(alter) => alter
210 .operations
211 .iter()
212 .any(|op| matches!(op, AlterTableOperation::DropColumn { .. })),
213 _ => false,
214 };
215
216 if is_write_statement && db.is_read_only() {
222 return Err(SQLRiteError::General(
223 "cannot execute: database is opened read-only".to_string(),
224 ));
225 }
226
227 match query {
229 Statement::CreateTable(_) => {
230 let create_query = CreateQuery::new(&query);
231 match create_query {
232 Ok(payload) => {
233 let table_name = payload.table_name.clone();
234 if table_name == pager::MASTER_TABLE_NAME {
235 return Err(SQLRiteError::General(format!(
236 "'{}' is a reserved name used by the internal schema catalog",
237 pager::MASTER_TABLE_NAME
238 )));
239 }
240 match db.contains_table(table_name.to_string()) {
242 true => {
243 if payload.if_not_exists {
252 message = format!(
253 "CREATE TABLE Statement executed. (table '{table_name}' already exists, no-op)"
254 );
255 } else {
256 return Err(SQLRiteError::Internal(
257 "Cannot create, table already exists.".to_string(),
258 ));
259 }
260 }
261 false => {
262 let table = Table::new(payload);
263 db.tables.insert(table_name.to_string(), table);
271 message = String::from("CREATE TABLE Statement executed.");
272 }
273 }
274 }
275 Err(err) => return Err(err),
276 }
277 }
278 Statement::Insert(_) => {
279 let insert_query = InsertQuery::new(&query);
280 match insert_query {
281 Ok(payload) => {
282 let table_name = payload.table_name;
283 let columns = payload.columns;
284 let values = payload.rows;
285
286 match db.contains_table(table_name.to_string()) {
289 true => {
290 let db_table = db.get_table_mut(table_name.to_string()).unwrap();
291 match columns
293 .iter()
294 .all(|column| db_table.contains_column(column.to_string()))
295 {
296 true => {
297 for value in &values {
298 if columns.len() != value.len() {
300 return Err(SQLRiteError::Internal(format!(
301 "{} values for {} columns",
302 value.len(),
303 columns.len()
304 )));
305 }
306 db_table
307 .validate_unique_constraint(&columns, value)
308 .map_err(|err| {
309 SQLRiteError::Internal(format!(
310 "Unique key constraint violation: {err}"
311 ))
312 })?;
313 db_table.insert_row(&columns, value)?;
314 }
315 }
316 false => {
317 return Err(SQLRiteError::Internal(
318 "Cannot insert, some of the columns do not exist"
319 .to_string(),
320 ));
321 }
322 }
323 }
330 false => {
331 return Err(SQLRiteError::Internal("Table doesn't exist".to_string()));
332 }
333 }
334 }
335 Err(err) => return Err(err),
336 }
337
338 message = String::from("INSERT Statement executed.")
339 }
340 Statement::Query(_) => {
341 let select_query = SelectQuery::new(&query)?;
342 let (rendered_table, rows) = executor::execute_select(select_query, db)?;
343 rendered = Some(rendered_table);
349 message = format!(
350 "SELECT Statement executed. {rows} row{s} returned.",
351 s = if rows == 1 { "" } else { "s" }
352 );
353 }
354 Statement::Delete(_) => {
355 let rows = executor::execute_delete(&query, db)?;
356 message = format!(
357 "DELETE Statement executed. {rows} row{s} deleted.",
358 s = if rows == 1 { "" } else { "s" }
359 );
360 }
361 Statement::Update(_) => {
362 let rows = executor::execute_update(&query, db)?;
363 message = format!(
364 "UPDATE Statement executed. {rows} row{s} updated.",
365 s = if rows == 1 { "" } else { "s" }
366 );
367 }
368 Statement::CreateIndex(_) => {
369 let name = executor::execute_create_index(&query, db)?;
370 message = format!("CREATE INDEX '{name}' executed.");
371 }
372 Statement::Drop {
373 object_type,
374 if_exists,
375 names,
376 ..
377 } => match object_type {
378 ObjectType::Table => {
379 let count = executor::execute_drop_table(&names, if_exists, db)?;
380 let plural = if count == 1 { "table" } else { "tables" };
381 message = format!("DROP TABLE Statement executed. {count} {plural} dropped.");
382 }
383 ObjectType::Index => {
384 let count = executor::execute_drop_index(&names, if_exists, db)?;
385 let plural = if count == 1 { "index" } else { "indexes" };
386 message = format!("DROP INDEX Statement executed. {count} {plural} dropped.");
387 }
388 other => {
389 return Err(SQLRiteError::NotImplemented(format!(
390 "DROP {other:?} is not supported (only TABLE and INDEX)"
391 )));
392 }
393 },
394 Statement::AlterTable(alter) => {
395 message = executor::execute_alter_table(alter, db)?;
396 }
397 Statement::Vacuum(vac) => {
398 if vac.full
403 || vac.sort_only
404 || vac.delete_only
405 || vac.reindex
406 || vac.recluster
407 || vac.boost
408 || vac.table_name.is_some()
409 || vac.threshold.is_some()
410 {
411 return Err(SQLRiteError::NotImplemented(
412 "VACUUM modifiers (FULL, REINDEX, table targets, etc.) are not supported; use bare VACUUM;"
413 .to_string(),
414 ));
415 }
416 message = executor::execute_vacuum(db)?;
417 }
418 _ => {
419 return Err(SQLRiteError::NotImplemented(
420 "SQL Statement not supported yet.".to_string(),
421 ));
422 }
423 };
424
425 if is_write_statement && !is_vacuum && db.source_path.is_some() && !db.in_transaction() {
439 let path = db.source_path.clone().unwrap();
440 pager::save_database(db, &path)?;
441 }
442
443 if releases_pages && !db.in_transaction() {
455 if let (Some(threshold), Some(path)) = (db.auto_vacuum_threshold(), db.source_path.clone())
456 {
457 let should = match db.pager.as_ref() {
458 Some(p) => pager::freelist::should_auto_vacuum(p, threshold)?,
459 None => false,
460 };
461 if should {
462 pager::vacuum_database(db, &path)?;
463 }
464 }
465 }
466
467 Ok(CommandOutput {
468 status: message,
469 rendered,
470 })
471}
472
473#[cfg(test)]
474mod tests {
475 use super::*;
476 use crate::sql::db::table::Value;
477
478 fn seed_users_table() -> Database {
481 let mut db = Database::new("tempdb".to_string());
482 process_command(
483 "CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT NOT NULL, age INTEGER);",
484 &mut db,
485 )
486 .expect("create table");
487 process_command(
488 "INSERT INTO users (name, age) VALUES ('alice', 30);",
489 &mut db,
490 )
491 .expect("insert alice");
492 process_command("INSERT INTO users (name, age) VALUES ('bob', 25);", &mut db)
493 .expect("insert bob");
494 process_command(
495 "INSERT INTO users (name, age) VALUES ('carol', 40);",
496 &mut db,
497 )
498 .expect("insert carol");
499 db
500 }
501
502 #[test]
503 fn process_command_select_all_test() {
504 let mut db = seed_users_table();
505 let response = process_command("SELECT * FROM users;", &mut db).expect("select");
506 assert!(response.contains("3 rows returned"));
507 }
508
509 #[test]
510 fn process_command_select_where_test() {
511 let mut db = seed_users_table();
512 let response =
513 process_command("SELECT name FROM users WHERE age > 25;", &mut db).expect("select");
514 assert!(response.contains("2 rows returned"));
515 }
516
517 #[test]
518 fn process_command_select_eq_string_test() {
519 let mut db = seed_users_table();
520 let response =
521 process_command("SELECT name FROM users WHERE name = 'bob';", &mut db).expect("select");
522 assert!(response.contains("1 row returned"));
523 }
524
525 #[test]
526 fn process_command_select_limit_test() {
527 let mut db = seed_users_table();
528 let response = process_command("SELECT * FROM users ORDER BY age ASC LIMIT 2;", &mut db)
529 .expect("select");
530 assert!(response.contains("2 rows returned"));
531 }
532
533 #[test]
534 fn process_command_select_unknown_table_test() {
535 let mut db = Database::new("tempdb".to_string());
536 let result = process_command("SELECT * FROM nope;", &mut db);
537 assert!(result.is_err());
538 }
539
540 #[test]
541 fn process_command_select_unknown_column_test() {
542 let mut db = seed_users_table();
543 let result = process_command("SELECT height FROM users;", &mut db);
544 assert!(result.is_err());
545 }
546
547 #[test]
548 fn process_command_insert_test() {
549 let mut db = Database::new("tempdb".to_string());
551
552 let query_statement = "CREATE TABLE users (
554 id INTEGER PRIMARY KEY,
555 name TEXT
556 );";
557 let dialect = SqlriteDialect::new();
558 let mut ast = Parser::parse_sql(&dialect, query_statement).unwrap();
559 if ast.len() > 1 {
560 panic!("Expected a single query statement, but there are more then 1.")
561 }
562 let query = ast.pop().unwrap();
563 let create_query = CreateQuery::new(&query).unwrap();
564
565 db.tables.insert(
567 create_query.table_name.to_string(),
568 Table::new(create_query),
569 );
570
571 let insert_query = String::from("INSERT INTO users (name) Values ('josh');");
573 match process_command(&insert_query, &mut db) {
574 Ok(response) => assert_eq!(response, "INSERT Statement executed."),
575 Err(err) => {
576 eprintln!("Error: {}", err);
577 assert!(false)
578 }
579 };
580 }
581
582 #[test]
583 fn process_command_insert_no_pk_test() {
584 let mut db = Database::new("tempdb".to_string());
586
587 let query_statement = "CREATE TABLE users (
589 name TEXT
590 );";
591 let dialect = SqlriteDialect::new();
592 let mut ast = Parser::parse_sql(&dialect, query_statement).unwrap();
593 if ast.len() > 1 {
594 panic!("Expected a single query statement, but there are more then 1.")
595 }
596 let query = ast.pop().unwrap();
597 let create_query = CreateQuery::new(&query).unwrap();
598
599 db.tables.insert(
601 create_query.table_name.to_string(),
602 Table::new(create_query),
603 );
604
605 let insert_query = String::from("INSERT INTO users (name) Values ('josh');");
607 match process_command(&insert_query, &mut db) {
608 Ok(response) => assert_eq!(response, "INSERT Statement executed."),
609 Err(err) => {
610 eprintln!("Error: {}", err);
611 assert!(false)
612 }
613 };
614 }
615
616 #[test]
617 fn process_command_delete_where_test() {
618 let mut db = seed_users_table();
619 let response =
620 process_command("DELETE FROM users WHERE name = 'bob';", &mut db).expect("delete");
621 assert!(response.contains("1 row deleted"));
622
623 let remaining = process_command("SELECT * FROM users;", &mut db).expect("select");
624 assert!(remaining.contains("2 rows returned"));
625 }
626
627 #[test]
628 fn process_command_delete_all_test() {
629 let mut db = seed_users_table();
630 let response = process_command("DELETE FROM users;", &mut db).expect("delete");
631 assert!(response.contains("3 rows deleted"));
632 }
633
634 #[test]
635 fn process_command_update_where_test() {
636 use crate::sql::db::table::Value;
637
638 let mut db = seed_users_table();
639 let response = process_command("UPDATE users SET age = 99 WHERE name = 'bob';", &mut db)
640 .expect("update");
641 assert!(response.contains("1 row updated"));
642
643 let users = db.get_table("users".to_string()).unwrap();
645 let bob_rowid = users
646 .rowids()
647 .into_iter()
648 .find(|r| users.get_value("name", *r) == Some(Value::Text("bob".to_string())))
649 .expect("bob row must exist");
650 assert_eq!(users.get_value("age", bob_rowid), Some(Value::Integer(99)));
651 }
652
653 #[test]
654 fn process_command_update_unique_violation_test() {
655 let mut db = seed_users_table();
656 process_command(
658 "CREATE TABLE tags (id INTEGER PRIMARY KEY, label TEXT UNIQUE);",
659 &mut db,
660 )
661 .unwrap();
662 process_command("INSERT INTO tags (label) VALUES ('a');", &mut db).unwrap();
663 process_command("INSERT INTO tags (label) VALUES ('b');", &mut db).unwrap();
664
665 let result = process_command("UPDATE tags SET label = 'a' WHERE label = 'b';", &mut db);
666 assert!(result.is_err(), "expected UNIQUE violation, got {result:?}");
667 }
668
669 #[test]
670 fn process_command_insert_type_mismatch_returns_error_test() {
671 let mut db = Database::new("tempdb".to_string());
673 process_command(
674 "CREATE TABLE items (id INTEGER PRIMARY KEY, qty INTEGER);",
675 &mut db,
676 )
677 .unwrap();
678 let result = process_command("INSERT INTO items (qty) VALUES ('not a number');", &mut db);
679 assert!(result.is_err(), "expected error, got {result:?}");
680 }
681
682 #[test]
683 fn insert_omitted_integer_column_is_stored_as_null() {
684 use crate::sql::db::table::Value;
690
691 let mut db = Database::new("tempdb".to_string());
692 process_command(
693 "CREATE TABLE items (id INTEGER PRIMARY KEY, qty INTEGER);",
694 &mut db,
695 )
696 .unwrap();
697 process_command("INSERT INTO items (id) VALUES (1);", &mut db)
698 .expect("INSERT with omitted INTEGER column should succeed and store NULL");
699
700 let table = db.get_table("items".to_string()).unwrap();
701 let rowid = table.rowids().pop().expect("one row");
702 assert_eq!(table.get_value("qty", rowid), None);
705 let response = process_command("SELECT id FROM items WHERE qty IS NULL;", &mut db)
707 .expect("select IS NULL");
708 assert!(
709 response.contains("1 row returned"),
710 "qty IS NULL should match the omitted-column row, got: {response}"
711 );
712 process_command("INSERT INTO items (id, qty) VALUES (2, 7);", &mut db).unwrap();
714 let table = db.get_table("items".to_string()).unwrap();
715 let row_two = table
716 .rowids()
717 .into_iter()
718 .find(|r| table.get_value("id", *r) == Some(Value::Integer(2)))
719 .unwrap();
720 assert_eq!(table.get_value("qty", row_two), Some(Value::Integer(7)));
721 }
722
723 #[test]
724 fn insert_explicit_null_into_integer_column() {
725 let mut db = Database::new("tempdb".to_string());
726 process_command(
727 "CREATE TABLE t (id INTEGER PRIMARY KEY, n INTEGER);",
728 &mut db,
729 )
730 .unwrap();
731 process_command("INSERT INTO t (id, n) VALUES (1, NULL);", &mut db)
732 .expect("INSERT explicit NULL into INTEGER must not panic on parse::<i32>()");
733 let table = db.get_table("t".to_string()).unwrap();
734 let rowid = table.rowids().pop().unwrap();
735 assert_eq!(table.get_value("n", rowid), None);
736 }
737
738 #[test]
739 fn insert_explicit_null_into_text_column() {
740 use crate::sql::db::table::Value;
745
746 let mut db = Database::new("tempdb".to_string());
747 process_command("CREATE TABLE t (id INTEGER PRIMARY KEY, s TEXT);", &mut db).unwrap();
748 process_command("INSERT INTO t (id, s) VALUES (1, NULL);", &mut db).unwrap();
749 process_command("INSERT INTO t (id, s) VALUES (2, 'hi');", &mut db).unwrap();
750
751 let table = db.get_table("t".to_string()).unwrap();
752 let row_one = table
753 .rowids()
754 .into_iter()
755 .find(|r| table.get_value("id", *r) == Some(Value::Integer(1)))
756 .unwrap();
757 let row_two = table
758 .rowids()
759 .into_iter()
760 .find(|r| table.get_value("id", *r) == Some(Value::Integer(2)))
761 .unwrap();
762 assert_eq!(table.get_value("s", row_one), None);
763 assert_eq!(
764 table.get_value("s", row_two),
765 Some(Value::Text("hi".to_string()))
766 );
767 }
768
769 #[test]
770 fn insert_explicit_null_into_real_column() {
771 let mut db = Database::new("tempdb".to_string());
772 process_command(
773 "CREATE TABLE t (id INTEGER PRIMARY KEY, score REAL);",
774 &mut db,
775 )
776 .unwrap();
777 process_command("INSERT INTO t (id, score) VALUES (1, NULL);", &mut db)
778 .expect("INSERT explicit NULL into REAL must not panic on parse::<f32>()");
779 let table = db.get_table("t".to_string()).unwrap();
780 let rowid = table.rowids().pop().unwrap();
781 assert_eq!(table.get_value("score", rowid), None);
782 }
783
784 #[test]
785 fn insert_explicit_null_into_bool_column() {
786 let mut db = Database::new("tempdb".to_string());
787 process_command(
788 "CREATE TABLE t (id INTEGER PRIMARY KEY, flag BOOLEAN);",
789 &mut db,
790 )
791 .unwrap();
792 process_command("INSERT INTO t (id, flag) VALUES (1, NULL);", &mut db)
793 .expect("INSERT explicit NULL into BOOL must not panic on parse::<bool>()");
794 let table = db.get_table("t".to_string()).unwrap();
795 let rowid = table.rowids().pop().unwrap();
796 assert_eq!(table.get_value("flag", rowid), None);
797 }
798
799 #[test]
800 fn insert_explicit_null_into_vector_column() {
801 let mut db = Database::new("tempdb".to_string());
802 process_command(
803 "CREATE TABLE t (id INTEGER PRIMARY KEY, v VECTOR(3));",
804 &mut db,
805 )
806 .unwrap();
807 process_command("INSERT INTO t (id, v) VALUES (1, NULL);", &mut db)
808 .expect("INSERT explicit NULL into VECTOR must not panic on parse_vector_literal");
809 let table = db.get_table("t".to_string()).unwrap();
810 let rowid = table.rowids().pop().unwrap();
811 assert_eq!(table.get_value("v", rowid), None);
812 }
813
814 #[test]
815 fn insert_explicit_null_into_json_column() {
816 let mut db = Database::new("tempdb".to_string());
817 process_command(
818 "CREATE TABLE t (id INTEGER PRIMARY KEY, doc JSON);",
819 &mut db,
820 )
821 .unwrap();
822 process_command("INSERT INTO t (id, doc) VALUES (1, NULL);", &mut db)
823 .expect("INSERT explicit NULL into JSON must skip serde_json validation");
824 let table = db.get_table("t".to_string()).unwrap();
825 let rowid = table.rowids().pop().unwrap();
826 assert_eq!(table.get_value("doc", rowid), None);
827 }
828
829 #[test]
830 fn default_does_not_override_explicit_null() {
831 use crate::sql::db::table::Value;
836
837 let mut db = Database::new("tempdb".to_string());
838 process_command(
839 "CREATE TABLE t (id INTEGER PRIMARY KEY, n INTEGER DEFAULT 0);",
840 &mut db,
841 )
842 .unwrap();
843 process_command("INSERT INTO t (id, n) VALUES (1, NULL);", &mut db).unwrap();
844 process_command("INSERT INTO t (id) VALUES (2);", &mut db).unwrap();
845
846 let table = db.get_table("t".to_string()).unwrap();
847 let row_one = table
848 .rowids()
849 .into_iter()
850 .find(|r| table.get_value("id", *r) == Some(Value::Integer(1)))
851 .unwrap();
852 let row_two = table
853 .rowids()
854 .into_iter()
855 .find(|r| table.get_value("id", *r) == Some(Value::Integer(2)))
856 .unwrap();
857 assert_eq!(table.get_value("n", row_one), None);
859 assert_eq!(table.get_value("n", row_two), Some(Value::Integer(0)));
861 }
862
863 #[test]
864 fn process_command_update_arith_test() {
865 use crate::sql::db::table::Value;
866
867 let mut db = seed_users_table();
868 process_command("UPDATE users SET age = age + 1;", &mut db).expect("update +1");
869
870 let users = db.get_table("users".to_string()).unwrap();
871 let mut ages: Vec<i64> = users
872 .rowids()
873 .into_iter()
874 .filter_map(|r| match users.get_value("age", r) {
875 Some(Value::Integer(n)) => Some(n),
876 _ => None,
877 })
878 .collect();
879 ages.sort();
880 assert_eq!(ages, vec![26, 31, 41]); }
882
883 #[test]
884 fn process_command_select_arithmetic_where_test() {
885 let mut db = seed_users_table();
886 let response =
888 process_command("SELECT name FROM users WHERE age * 2 > 55;", &mut db).expect("select");
889 assert!(response.contains("2 rows returned"));
890 }
891
892 #[test]
893 fn process_command_divide_by_zero_test() {
894 let mut db = seed_users_table();
895 let result = process_command("SELECT age / 0 FROM users;", &mut db);
896 assert!(result.is_err());
898 }
899
900 #[test]
901 fn process_command_unsupported_statement_test() {
902 let mut db = Database::new("tempdb".to_string());
903 let result = process_command("CREATE VIEW v AS SELECT * FROM users;", &mut db);
907 assert!(result.is_err());
908 }
909
910 #[test]
911 fn empty_input_is_a_noop_not_a_panic() {
912 let mut db = Database::new("t".to_string());
916 for input in ["", " ", "-- just a comment", "-- comment\n-- another"] {
917 let result = process_command(input, &mut db);
918 assert!(result.is_ok(), "input {input:?} should not error");
919 let msg = result.unwrap();
920 assert!(msg.contains("No statement"), "got: {msg:?}");
921 }
922 }
923
924 #[test]
925 fn create_index_adds_explicit_index() {
926 let mut db = seed_users_table();
927 let response = process_command("CREATE INDEX users_age_idx ON users (age);", &mut db)
928 .expect("create index");
929 assert!(response.contains("users_age_idx"));
930
931 let users = db.get_table("users".to_string()).unwrap();
933 let idx = users
934 .index_by_name("users_age_idx")
935 .expect("index should exist after CREATE INDEX");
936 assert_eq!(idx.column_name, "age");
937 assert!(!idx.is_unique);
938 }
939
940 #[test]
941 fn create_unique_index_rejects_duplicate_existing_values() {
942 let mut db = seed_users_table();
943 process_command("INSERT INTO users (name, age) VALUES ('dan', 30);", &mut db).unwrap();
946 let result = process_command(
947 "CREATE UNIQUE INDEX users_age_unique ON users (age);",
948 &mut db,
949 );
950 assert!(
951 result.is_err(),
952 "expected unique-index failure, got {result:?}"
953 );
954 }
955
956 #[test]
957 fn where_eq_on_indexed_column_uses_index_probe() {
958 let mut db = Database::new("t".to_string());
962 process_command(
963 "CREATE TABLE big (id INTEGER PRIMARY KEY, tag TEXT);",
964 &mut db,
965 )
966 .unwrap();
967 process_command("CREATE INDEX big_tag_idx ON big (tag);", &mut db).unwrap();
968 for i in 1..=100 {
969 let tag = if i % 3 == 0 { "hot" } else { "cold" };
970 process_command(&format!("INSERT INTO big (tag) VALUES ('{tag}');"), &mut db).unwrap();
971 }
972 let response =
973 process_command("SELECT id FROM big WHERE tag = 'hot';", &mut db).expect("select");
974 assert!(
976 response.contains("33 rows returned"),
977 "response was {response:?}"
978 );
979 }
980
981 #[test]
982 fn where_eq_on_indexed_column_inside_parens_uses_index_probe() {
983 let mut db = seed_users_table();
984 let response = process_command("SELECT name FROM users WHERE (name = 'bob');", &mut db)
985 .expect("select");
986 assert!(response.contains("1 row returned"));
987 }
988
989 #[test]
990 fn where_eq_literal_first_side_uses_index_probe() {
991 let mut db = seed_users_table();
992 let response =
994 process_command("SELECT name FROM users WHERE 'bob' = name;", &mut db).expect("select");
995 assert!(response.contains("1 row returned"));
996 }
997
998 #[test]
999 fn non_equality_where_still_falls_back_to_full_scan() {
1000 let mut db = seed_users_table();
1003 let response =
1004 process_command("SELECT name FROM users WHERE age > 28;", &mut db).expect("select");
1005 assert!(response.contains("2 rows returned"));
1006 }
1007
1008 #[test]
1013 fn rollback_restores_pre_begin_in_memory_state() {
1014 let mut db = seed_users_table();
1017 let before = db.get_table("users".to_string()).unwrap().rowids().len();
1018 assert_eq!(before, 3);
1019
1020 process_command("BEGIN;", &mut db).expect("BEGIN");
1021 assert!(db.in_transaction());
1022 process_command("INSERT INTO users (name, age) VALUES ('dan', 50);", &mut db)
1023 .expect("INSERT inside txn");
1024 let mid = db.get_table("users".to_string()).unwrap().rowids().len();
1026 assert_eq!(mid, 4);
1027
1028 process_command("ROLLBACK;", &mut db).expect("ROLLBACK");
1029 assert!(!db.in_transaction());
1030 let after = db.get_table("users".to_string()).unwrap().rowids().len();
1031 assert_eq!(
1032 after, 3,
1033 "ROLLBACK should have restored the pre-BEGIN state"
1034 );
1035 }
1036
1037 #[test]
1038 fn commit_keeps_mutations_and_clears_txn_flag() {
1039 let mut db = seed_users_table();
1040 process_command("BEGIN;", &mut db).expect("BEGIN");
1041 process_command("INSERT INTO users (name, age) VALUES ('dan', 50);", &mut db)
1042 .expect("INSERT inside txn");
1043 process_command("COMMIT;", &mut db).expect("COMMIT");
1044 assert!(!db.in_transaction());
1045 let after = db.get_table("users".to_string()).unwrap().rowids().len();
1046 assert_eq!(after, 4);
1047 }
1048
1049 #[test]
1050 fn rollback_undoes_update_and_delete_side_by_side() {
1051 use crate::sql::db::table::Value;
1052
1053 let mut db = seed_users_table();
1054 process_command("BEGIN;", &mut db).unwrap();
1055 process_command("UPDATE users SET age = 999;", &mut db).unwrap();
1056 process_command("DELETE FROM users WHERE name = 'bob';", &mut db).unwrap();
1057 let users = db.get_table("users".to_string()).unwrap();
1059 assert_eq!(users.rowids().len(), 2);
1060 for r in users.rowids() {
1061 assert_eq!(users.get_value("age", r), Some(Value::Integer(999)));
1062 }
1063
1064 process_command("ROLLBACK;", &mut db).unwrap();
1065 let users = db.get_table("users".to_string()).unwrap();
1066 assert_eq!(users.rowids().len(), 3);
1067 for r in users.rowids() {
1069 assert_ne!(users.get_value("age", r), Some(Value::Integer(999)));
1070 }
1071 }
1072
1073 #[test]
1074 fn nested_begin_is_rejected() {
1075 let mut db = seed_users_table();
1076 process_command("BEGIN;", &mut db).unwrap();
1077 let err = process_command("BEGIN;", &mut db).unwrap_err();
1078 assert!(
1079 format!("{err}").contains("already open"),
1080 "nested BEGIN should error; got: {err}"
1081 );
1082 assert!(db.in_transaction());
1084 process_command("ROLLBACK;", &mut db).unwrap();
1085 }
1086
1087 #[test]
1088 fn orphan_commit_and_rollback_are_rejected() {
1089 let mut db = seed_users_table();
1090 let commit_err = process_command("COMMIT;", &mut db).unwrap_err();
1091 assert!(format!("{commit_err}").contains("no transaction"));
1092 let rollback_err = process_command("ROLLBACK;", &mut db).unwrap_err();
1093 assert!(format!("{rollback_err}").contains("no transaction"));
1094 }
1095
1096 #[test]
1097 fn error_inside_transaction_keeps_txn_open() {
1098 let mut db = seed_users_table();
1102 process_command("BEGIN;", &mut db).unwrap();
1103 let err = process_command("INSERT INTO nope (x) VALUES (1);", &mut db);
1104 assert!(err.is_err());
1105 assert!(db.in_transaction(), "txn should stay open after error");
1106 process_command("ROLLBACK;", &mut db).unwrap();
1107 }
1108
1109 fn seed_file_backed(name: &str, schema: &str) -> (std::path::PathBuf, Database) {
1114 use crate::sql::pager::{open_database, save_database};
1115 let mut p = std::env::temp_dir();
1116 let pid = std::process::id();
1117 let nanos = std::time::SystemTime::now()
1118 .duration_since(std::time::UNIX_EPOCH)
1119 .map(|d| d.as_nanos())
1120 .unwrap_or(0);
1121 p.push(format!("sqlrite-txn-{name}-{pid}-{nanos}.sqlrite"));
1122
1123 {
1127 let mut seed = Database::new("t".to_string());
1128 process_command(schema, &mut seed).unwrap();
1129 save_database(&mut seed, &p).unwrap();
1130 }
1131 let db = open_database(&p, "t".to_string()).unwrap();
1132 (p, db)
1133 }
1134
1135 fn cleanup_file(path: &std::path::Path) {
1136 let _ = std::fs::remove_file(path);
1137 let mut wal = path.as_os_str().to_owned();
1138 wal.push("-wal");
1139 let _ = std::fs::remove_file(std::path::PathBuf::from(wal));
1140 }
1141
1142 #[test]
1143 fn null_values_round_trip_through_disk() {
1144 use crate::sql::db::table::Value;
1150 use crate::sql::pager::open_database;
1151
1152 let (path, mut db) = seed_file_backed(
1153 "nullrt",
1154 "CREATE TABLE t (id INTEGER PRIMARY KEY, n INTEGER, s TEXT, score REAL, flag BOOLEAN);",
1155 );
1156 process_command(
1157 "INSERT INTO t (id, n, s, score, flag) VALUES (1, 10, 'hi', 1.5, true);",
1158 &mut db,
1159 )
1160 .unwrap();
1161 process_command(
1162 "INSERT INTO t (id, n, s, score, flag) VALUES (2, NULL, NULL, NULL, NULL);",
1163 &mut db,
1164 )
1165 .unwrap();
1166 process_command("INSERT INTO t (id) VALUES (3);", &mut db).unwrap();
1168
1169 drop(db); let reopened = open_database(&path, "t".to_string()).unwrap();
1172 let t = reopened.get_table("t".to_string()).unwrap();
1173 let by_id = |id: i64| {
1174 t.rowids()
1175 .into_iter()
1176 .find(|r| t.get_value("id", *r) == Some(Value::Integer(id)))
1177 .unwrap_or_else(|| panic!("row id={id} not found"))
1178 };
1179
1180 let r1 = by_id(1);
1181 assert_eq!(t.get_value("n", r1), Some(Value::Integer(10)));
1182 assert_eq!(t.get_value("s", r1), Some(Value::Text("hi".to_string())));
1183 assert_eq!(t.get_value("score", r1), Some(Value::Real(1.5)));
1184 assert_eq!(t.get_value("flag", r1), Some(Value::Bool(true)));
1185
1186 for r in [by_id(2), by_id(3)] {
1187 assert_eq!(t.get_value("n", r), None, "INTEGER NULL must round-trip");
1188 assert_eq!(t.get_value("s", r), None, "TEXT NULL must round-trip");
1189 assert_eq!(t.get_value("score", r), None, "REAL NULL must round-trip");
1190 assert_eq!(t.get_value("flag", r), None, "BOOL NULL must round-trip");
1191 }
1192
1193 drop(reopened);
1194 cleanup_file(&path);
1195 }
1196
1197 #[test]
1198 fn begin_commit_rollback_round_trip_through_disk() {
1199 use crate::sql::pager::open_database;
1203
1204 let (path, mut db) = seed_file_backed(
1205 "roundtrip",
1206 "CREATE TABLE notes (id INTEGER PRIMARY KEY, body TEXT);",
1207 );
1208
1209 process_command("BEGIN;", &mut db).unwrap();
1211 process_command("INSERT INTO notes (body) VALUES ('a');", &mut db).unwrap();
1212 process_command("INSERT INTO notes (body) VALUES ('b');", &mut db).unwrap();
1213 process_command("COMMIT;", &mut db).unwrap();
1214
1215 process_command("BEGIN;", &mut db).unwrap();
1217 process_command("INSERT INTO notes (body) VALUES ('c');", &mut db).unwrap();
1218 process_command("ROLLBACK;", &mut db).unwrap();
1219
1220 drop(db); let reopened = open_database(&path, "t".to_string()).unwrap();
1223 let notes = reopened.get_table("notes".to_string()).unwrap();
1224 assert_eq!(notes.rowids().len(), 2, "committed rows should survive");
1225
1226 drop(reopened);
1227 cleanup_file(&path);
1228 }
1229
1230 #[test]
1231 fn write_inside_transaction_does_not_autosave() {
1232 let (path, mut db) =
1236 seed_file_backed("noas", "CREATE TABLE t (id INTEGER PRIMARY KEY, x TEXT);");
1237
1238 let mut wal_path = path.as_os_str().to_owned();
1239 wal_path.push("-wal");
1240 let wal_path = std::path::PathBuf::from(wal_path);
1241 let frames_before = std::fs::metadata(&wal_path).unwrap().len();
1242
1243 process_command("BEGIN;", &mut db).unwrap();
1244 process_command("INSERT INTO t (x) VALUES ('a');", &mut db).unwrap();
1245 process_command("INSERT INTO t (x) VALUES ('b');", &mut db).unwrap();
1246
1247 let frames_mid = std::fs::metadata(&wal_path).unwrap().len();
1249 assert_eq!(
1250 frames_before, frames_mid,
1251 "WAL should not grow during an open transaction"
1252 );
1253
1254 process_command("COMMIT;", &mut db).unwrap();
1255
1256 drop(db); let fresh = crate::sql::pager::open_database(&path, "t".to_string()).unwrap();
1258 assert_eq!(
1259 fresh.get_table("t".to_string()).unwrap().rowids().len(),
1260 2,
1261 "COMMIT should have persisted both inserted rows"
1262 );
1263 drop(fresh);
1264 cleanup_file(&path);
1265 }
1266
1267 #[test]
1268 fn rollback_undoes_create_table() {
1269 let mut db = seed_users_table();
1274 assert_eq!(db.tables.len(), 1);
1275
1276 process_command("BEGIN;", &mut db).unwrap();
1277 process_command(
1278 "CREATE TABLE dropme (id INTEGER PRIMARY KEY, x TEXT);",
1279 &mut db,
1280 )
1281 .unwrap();
1282 process_command("INSERT INTO dropme (x) VALUES ('stuff');", &mut db).unwrap();
1283 assert_eq!(db.tables.len(), 2);
1284
1285 process_command("ROLLBACK;", &mut db).unwrap();
1286 assert_eq!(
1287 db.tables.len(),
1288 1,
1289 "CREATE TABLE should have been rolled back"
1290 );
1291 assert!(db.get_table("dropme".to_string()).is_err());
1292 }
1293
1294 #[test]
1295 fn rollback_restores_secondary_index_state() {
1296 let mut db = Database::new("t".to_string());
1300 process_command(
1301 "CREATE TABLE users (id INTEGER PRIMARY KEY, email TEXT UNIQUE);",
1302 &mut db,
1303 )
1304 .unwrap();
1305 process_command("INSERT INTO users (email) VALUES ('a@x');", &mut db).unwrap();
1306
1307 process_command("BEGIN;", &mut db).unwrap();
1308 process_command("INSERT INTO users (email) VALUES ('b@x');", &mut db).unwrap();
1309 process_command("ROLLBACK;", &mut db).unwrap();
1311
1312 let reinsert = process_command("INSERT INTO users (email) VALUES ('b@x');", &mut db);
1316 assert!(
1317 reinsert.is_ok(),
1318 "re-insert after rollback should succeed, got {reinsert:?}"
1319 );
1320 }
1321
1322 #[test]
1323 fn rollback_restores_last_rowid_counter() {
1324 use crate::sql::db::table::Value;
1328
1329 let mut db = seed_users_table(); let pre = db.get_table("users".to_string()).unwrap().last_rowid;
1331
1332 process_command("BEGIN;", &mut db).unwrap();
1333 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();
1336
1337 let post = db.get_table("users".to_string()).unwrap().last_rowid;
1338 assert_eq!(pre, post, "last_rowid must roll back with the snapshot");
1339
1340 process_command("INSERT INTO users (name, age) VALUES ('d', 50);", &mut db).unwrap();
1342 let users = db.get_table("users".to_string()).unwrap();
1343 let d_rowid = users
1344 .rowids()
1345 .into_iter()
1346 .find(|r| users.get_value("name", *r) == Some(Value::Text("d".into())))
1347 .expect("d row must exist");
1348 assert_eq!(d_rowid, pre + 1);
1349 }
1350
1351 #[test]
1352 fn commit_on_in_memory_db_clears_txn_without_pager_call() {
1353 let mut db = seed_users_table(); assert!(db.source_path.is_none());
1358
1359 process_command("BEGIN;", &mut db).unwrap();
1360 process_command("INSERT INTO users (name, age) VALUES ('z', 99);", &mut db).unwrap();
1361 process_command("COMMIT;", &mut db).unwrap();
1362
1363 assert!(!db.in_transaction());
1364 assert_eq!(db.get_table("users".to_string()).unwrap().rowids().len(), 4);
1365 }
1366
1367 #[test]
1368 fn failed_commit_auto_rolls_back_in_memory_state() {
1369 use crate::sql::pager::save_database;
1383
1384 let (path, mut db) = seed_file_backed(
1386 "failcommit",
1387 "CREATE TABLE notes (id INTEGER PRIMARY KEY, body TEXT);",
1388 );
1389
1390 process_command("INSERT INTO notes (body) VALUES ('before');", &mut db).unwrap();
1392
1393 process_command("BEGIN;", &mut db).unwrap();
1395 process_command("INSERT INTO notes (body) VALUES ('inflight');", &mut db).unwrap();
1396 assert_eq!(
1397 db.get_table("notes".to_string()).unwrap().rowids().len(),
1398 2,
1399 "inflight row visible mid-txn"
1400 );
1401
1402 let orig_source = db.source_path.clone();
1406 let orig_pager = db.pager.take();
1407 db.source_path = Some(std::env::temp_dir());
1408
1409 let commit_result = process_command("COMMIT;", &mut db);
1410 assert!(commit_result.is_err(), "commit must fail");
1411 let err_str = format!("{}", commit_result.unwrap_err());
1412 assert!(
1413 err_str.contains("COMMIT failed") && err_str.contains("rolled back"),
1414 "error must surface auto-rollback; got: {err_str}"
1415 );
1416
1417 assert!(
1421 !db.in_transaction(),
1422 "txn must be cleared after auto-rollback"
1423 );
1424 assert_eq!(
1425 db.get_table("notes".to_string()).unwrap().rowids().len(),
1426 1,
1427 "inflight row must be rolled back"
1428 );
1429
1430 db.source_path = orig_source;
1433 db.pager = orig_pager;
1434 process_command("INSERT INTO notes (body) VALUES ('after');", &mut db).unwrap();
1435 drop(db);
1436
1437 let reopened = crate::sql::pager::open_database(&path, "t".to_string()).unwrap();
1439 let notes = reopened.get_table("notes".to_string()).unwrap();
1440 assert_eq!(notes.rowids().len(), 2);
1441 let _ = save_database; drop(reopened);
1444 cleanup_file(&path);
1445 }
1446
1447 #[test]
1448 fn begin_on_read_only_is_rejected() {
1449 use crate::sql::pager::{open_database_read_only, save_database};
1450
1451 let path = {
1452 let mut p = std::env::temp_dir();
1453 let pid = std::process::id();
1454 let nanos = std::time::SystemTime::now()
1455 .duration_since(std::time::UNIX_EPOCH)
1456 .map(|d| d.as_nanos())
1457 .unwrap_or(0);
1458 p.push(format!("sqlrite-txn-ro-{pid}-{nanos}.sqlrite"));
1459 p
1460 };
1461 {
1462 let mut seed = Database::new("t".to_string());
1463 process_command("CREATE TABLE t (id INTEGER PRIMARY KEY);", &mut seed).unwrap();
1464 save_database(&mut seed, &path).unwrap();
1465 }
1466
1467 let mut ro = open_database_read_only(&path, "t".to_string()).unwrap();
1468 let err = process_command("BEGIN;", &mut ro).unwrap_err();
1469 assert!(
1470 format!("{err}").contains("read-only"),
1471 "BEGIN on RO db should surface read-only; got: {err}"
1472 );
1473 assert!(!ro.in_transaction());
1474
1475 let _ = std::fs::remove_file(&path);
1476 let mut wal = path.as_os_str().to_owned();
1477 wal.push("-wal");
1478 let _ = std::fs::remove_file(std::path::PathBuf::from(wal));
1479 }
1480
1481 #[test]
1482 fn read_only_database_rejects_mutations_before_touching_state() {
1483 use crate::sql::pager::open_database_read_only;
1489
1490 let mut seed = Database::new("t".to_string());
1491 process_command(
1492 "CREATE TABLE notes (id INTEGER PRIMARY KEY, body TEXT);",
1493 &mut seed,
1494 )
1495 .unwrap();
1496 process_command("INSERT INTO notes (body) VALUES ('alpha');", &mut seed).unwrap();
1497
1498 let path = {
1499 let mut p = std::env::temp_dir();
1500 let pid = std::process::id();
1501 let nanos = std::time::SystemTime::now()
1502 .duration_since(std::time::UNIX_EPOCH)
1503 .map(|d| d.as_nanos())
1504 .unwrap_or(0);
1505 p.push(format!("sqlrite-ro-reject-{pid}-{nanos}.sqlrite"));
1506 p
1507 };
1508 crate::sql::pager::save_database(&mut seed, &path).unwrap();
1509 drop(seed);
1510
1511 let mut ro = open_database_read_only(&path, "t".to_string()).unwrap();
1512 let notes_before = ro.get_table("notes".to_string()).unwrap().rowids().len();
1513
1514 for stmt in [
1515 "INSERT INTO notes (body) VALUES ('beta');",
1516 "UPDATE notes SET body = 'x';",
1517 "DELETE FROM notes;",
1518 "CREATE TABLE more (id INTEGER PRIMARY KEY);",
1519 "CREATE INDEX notes_body ON notes (body);",
1520 ] {
1521 let err = process_command(stmt, &mut ro).unwrap_err();
1522 assert!(
1523 format!("{err}").contains("read-only"),
1524 "stmt {stmt:?} should surface a read-only error; got: {err}"
1525 );
1526 }
1527
1528 let notes_after = ro.get_table("notes".to_string()).unwrap().rowids().len();
1530 assert_eq!(notes_before, notes_after);
1531 let sel = process_command("SELECT * FROM notes;", &mut ro).expect("select on RO must work");
1532 assert!(sel.contains("1 row returned"));
1533
1534 drop(ro);
1536 let _ = std::fs::remove_file(&path);
1537 let mut wal = path.as_os_str().to_owned();
1538 wal.push("-wal");
1539 let _ = std::fs::remove_file(std::path::PathBuf::from(wal));
1540 }
1541
1542 #[test]
1547 fn vector_create_table_and_insert_basic() {
1548 let mut db = Database::new("tempdb".to_string());
1549 process_command(
1550 "CREATE TABLE docs (id INTEGER PRIMARY KEY, embedding VECTOR(3));",
1551 &mut db,
1552 )
1553 .expect("create table with VECTOR(3)");
1554 process_command(
1555 "INSERT INTO docs (embedding) VALUES ([0.1, 0.2, 0.3]);",
1556 &mut db,
1557 )
1558 .expect("insert vector");
1559
1560 let sel = process_command("SELECT * FROM docs;", &mut db).expect("select");
1564 assert!(sel.contains("1 row returned"));
1565
1566 let docs = db.get_table("docs".to_string()).expect("docs table");
1567 let rowids = docs.rowids();
1568 assert_eq!(rowids.len(), 1);
1569 match docs.get_value("embedding", rowids[0]) {
1570 Some(Value::Vector(v)) => assert_eq!(v, vec![0.1f32, 0.2, 0.3]),
1571 other => panic!("expected Value::Vector(...), got {other:?}"),
1572 }
1573 }
1574
1575 #[test]
1576 fn vector_dim_mismatch_at_insert_is_clean_error() {
1577 let mut db = Database::new("tempdb".to_string());
1578 process_command(
1579 "CREATE TABLE docs (id INTEGER PRIMARY KEY, embedding VECTOR(3));",
1580 &mut db,
1581 )
1582 .expect("create table");
1583
1584 let err = process_command("INSERT INTO docs (embedding) VALUES ([0.1, 0.2]);", &mut db)
1586 .unwrap_err();
1587 let msg = format!("{err}");
1588 assert!(
1589 msg.to_lowercase().contains("dimension")
1590 && msg.contains("declared 3")
1591 && msg.contains("got 2"),
1592 "expected clear dim-mismatch error, got: {msg}"
1593 );
1594
1595 let err = process_command(
1597 "INSERT INTO docs (embedding) VALUES ([0.1, 0.2, 0.3, 0.4, 0.5]);",
1598 &mut db,
1599 )
1600 .unwrap_err();
1601 assert!(
1602 format!("{err}").contains("got 5"),
1603 "expected dim-mismatch error mentioning got 5, got: {err}"
1604 );
1605 }
1606
1607 #[test]
1608 fn vector_create_table_rejects_missing_dim() {
1609 let mut db = Database::new("tempdb".to_string());
1610 let result = process_command(
1616 "CREATE TABLE docs (id INTEGER PRIMARY KEY, embedding VECTOR);",
1617 &mut db,
1618 );
1619 assert!(
1620 result.is_err(),
1621 "expected CREATE TABLE with bare VECTOR to fail (no dim)"
1622 );
1623 }
1624
1625 #[test]
1626 fn vector_create_table_rejects_zero_dim() {
1627 let mut db = Database::new("tempdb".to_string());
1628 let err = process_command(
1629 "CREATE TABLE docs (id INTEGER PRIMARY KEY, embedding VECTOR(0));",
1630 &mut db,
1631 )
1632 .unwrap_err();
1633 let msg = format!("{err}");
1634 assert!(
1635 msg.to_lowercase().contains("vector"),
1636 "expected VECTOR-related error for VECTOR(0), got: {msg}"
1637 );
1638 }
1639
1640 #[test]
1641 fn vector_high_dim_works() {
1642 let mut db = Database::new("tempdb".to_string());
1645 process_command(
1646 "CREATE TABLE embeddings (id INTEGER PRIMARY KEY, e VECTOR(384));",
1647 &mut db,
1648 )
1649 .expect("create table VECTOR(384)");
1650
1651 let lit = format!(
1652 "[{}]",
1653 (0..384)
1654 .map(|i| format!("{}", i as f32 * 0.001))
1655 .collect::<Vec<_>>()
1656 .join(",")
1657 );
1658 let sql = format!("INSERT INTO embeddings (e) VALUES ({lit});");
1659 process_command(&sql, &mut db).expect("insert 384-dim vector");
1660
1661 let sel = process_command("SELECT id FROM embeddings;", &mut db).expect("select id");
1662 assert!(sel.contains("1 row returned"));
1663 }
1664
1665 #[test]
1666 fn vector_multiple_rows() {
1667 let mut db = Database::new("tempdb".to_string());
1670 process_command(
1671 "CREATE TABLE docs (id INTEGER PRIMARY KEY, e VECTOR(2));",
1672 &mut db,
1673 )
1674 .expect("create");
1675 for i in 0..3 {
1676 let sql = format!("INSERT INTO docs (e) VALUES ([{i}.0, {}.0]);", i + 1);
1677 process_command(&sql, &mut db).expect("insert");
1678 }
1679 let sel = process_command("SELECT * FROM docs;", &mut db).expect("select");
1680 assert!(sel.contains("3 rows returned"));
1681
1682 let docs = db.get_table("docs".to_string()).expect("docs table");
1684 let rowids = docs.rowids();
1685 assert_eq!(rowids.len(), 3);
1686 let mut vectors: Vec<Vec<f32>> = rowids
1687 .iter()
1688 .filter_map(|r| match docs.get_value("e", *r) {
1689 Some(Value::Vector(v)) => Some(v),
1690 _ => None,
1691 })
1692 .collect();
1693 vectors.sort_by(|a, b| a[0].partial_cmp(&b[0]).unwrap());
1694 assert_eq!(vectors[0], vec![0.0f32, 1.0]);
1695 assert_eq!(vectors[1], vec![1.0f32, 2.0]);
1696 assert_eq!(vectors[2], vec![2.0f32, 3.0]);
1697 }
1698
1699 fn seed_hnsw_table() -> Database {
1707 let mut db = Database::new("tempdb".to_string());
1708 process_command(
1709 "CREATE TABLE docs (id INTEGER PRIMARY KEY, e VECTOR(2));",
1710 &mut db,
1711 )
1712 .unwrap();
1713 for v in &[
1714 "[1.0, 0.0]", "[2.0, 0.0]", "[0.0, 3.0]", "[1.0, 4.0]", "[10.0, 10.0]", ] {
1720 process_command(&format!("INSERT INTO docs (e) VALUES ({v});"), &mut db).unwrap();
1721 }
1722 db
1723 }
1724
1725 #[test]
1726 fn create_index_using_hnsw_succeeds() {
1727 let mut db = seed_hnsw_table();
1728 let resp = process_command("CREATE INDEX ix_e ON docs USING hnsw (e);", &mut db).unwrap();
1729 assert!(resp.to_lowercase().contains("create index"));
1730 let table = db.get_table("docs".to_string()).unwrap();
1732 assert_eq!(table.hnsw_indexes.len(), 1);
1733 assert_eq!(table.hnsw_indexes[0].name, "ix_e");
1734 assert_eq!(table.hnsw_indexes[0].column_name, "e");
1735 assert_eq!(table.hnsw_indexes[0].index.len(), 5);
1737 }
1738
1739 #[test]
1740 fn create_index_using_hnsw_rejects_non_vector_column() {
1741 let mut db = Database::new("tempdb".to_string());
1742 process_command(
1743 "CREATE TABLE t (id INTEGER PRIMARY KEY, name TEXT);",
1744 &mut db,
1745 )
1746 .unwrap();
1747 let err =
1748 process_command("CREATE INDEX ix_name ON t USING hnsw (name);", &mut db).unwrap_err();
1749 let msg = format!("{err}");
1750 assert!(
1751 msg.to_lowercase().contains("vector"),
1752 "expected error mentioning VECTOR; got: {msg}"
1753 );
1754 }
1755
1756 #[test]
1757 fn knn_query_uses_hnsw_after_create_index() {
1758 let mut db = seed_hnsw_table();
1764 process_command("CREATE INDEX ix_e ON docs USING hnsw (e);", &mut db).unwrap();
1765
1766 let resp = process_command(
1771 "SELECT id FROM docs ORDER BY vec_distance_l2(e, [1.0, 0.0]) ASC LIMIT 3;",
1772 &mut db,
1773 )
1774 .unwrap();
1775 assert!(resp.contains("3 rows returned"), "got: {resp}");
1776 }
1777
1778 #[test]
1779 fn knn_query_works_after_subsequent_inserts() {
1780 let mut db = seed_hnsw_table();
1784 process_command("CREATE INDEX ix_e ON docs USING hnsw (e);", &mut db).unwrap();
1785 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();
1789 assert_eq!(
1790 table.hnsw_indexes[0].index.len(),
1791 7,
1792 "incremental insert should grow HNSW alongside row storage"
1793 );
1794
1795 let resp = process_command(
1798 "SELECT id FROM docs ORDER BY vec_distance_l2(e, [0.0, 0.0]) ASC LIMIT 1;",
1799 &mut db,
1800 )
1801 .unwrap();
1802 assert!(resp.contains("1 row returned"), "got: {resp}");
1803 }
1804
1805 #[test]
1811 fn delete_on_hnsw_indexed_table_succeeds_and_marks_dirty() {
1812 let mut db = seed_hnsw_table();
1813 process_command("CREATE INDEX ix_e ON docs USING hnsw (e);", &mut db).unwrap();
1814 let resp = process_command("DELETE FROM docs WHERE id = 1;", &mut db).unwrap();
1815 assert!(resp.contains("1 row"), "expected 1 row deleted: {resp}");
1816
1817 let docs = db.get_table("docs".to_string()).unwrap();
1818 let entry = docs.hnsw_indexes.iter().find(|e| e.name == "ix_e").unwrap();
1819 assert!(
1820 entry.needs_rebuild,
1821 "DELETE should have marked HNSW index dirty for rebuild on next save"
1822 );
1823 }
1824
1825 #[test]
1826 fn hnsw_delete_then_insert_rebuilds_in_same_connection() {
1827 let mut db = Database::new(String::from("test_db"));
1828 process_command(
1829 "CREATE TABLE chunks (id INTEGER PRIMARY KEY, document_id INTEGER, embedding VECTOR(4));",
1830 &mut db,
1831 )
1832 .unwrap();
1833 process_command(
1834 "CREATE INDEX idx_emb ON chunks USING hnsw (embedding);",
1835 &mut db,
1836 )
1837 .unwrap();
1838 process_command(
1839 "INSERT INTO chunks (document_id, embedding) VALUES (1, [1, 0, 0, 0]);",
1840 &mut db,
1841 )
1842 .unwrap();
1843 process_command(
1844 "INSERT INTO chunks (document_id, embedding) VALUES (1, [0, 1, 0, 0]);",
1845 &mut db,
1846 )
1847 .unwrap();
1848 process_command("DELETE FROM chunks WHERE document_id = 1;", &mut db).unwrap();
1849
1850 process_command(
1851 "INSERT INTO chunks (document_id, embedding) VALUES (2, [0, 0, 1, 0]);",
1852 &mut db,
1853 )
1854 .unwrap();
1855 let chunks = db.get_table("chunks".to_string()).unwrap();
1856 let entry = chunks
1857 .hnsw_indexes
1858 .iter()
1859 .find(|e| e.name == "idx_emb")
1860 .unwrap();
1861 assert!(
1862 !entry.needs_rebuild,
1863 "INSERT should rebuild the dirty index"
1864 );
1865 assert_eq!(entry.index.len(), 1);
1866
1867 let out = process_command_with_render(
1868 "SELECT document_id FROM chunks ORDER BY vec_distance_l2(embedding, [0, 0, 1, 0]) ASC LIMIT 1;",
1869 &mut db,
1870 )
1871 .unwrap();
1872 assert!(out.status.contains("1 row returned"), "got: {}", out.status);
1873 let rendered = out.rendered.expect("SELECT should render rows");
1874 assert!(
1875 rendered.contains("| 2 |"),
1876 "expected the post-delete inserted row: {rendered}"
1877 );
1878 }
1879
1880 #[test]
1881 fn update_on_hnsw_indexed_vector_col_succeeds_and_marks_dirty() {
1882 let mut db = seed_hnsw_table();
1883 process_command("CREATE INDEX ix_e ON docs USING hnsw (e);", &mut db).unwrap();
1884 let resp =
1885 process_command("UPDATE docs SET e = [9.0, 9.0] WHERE id = 1;", &mut db).unwrap();
1886 assert!(resp.contains("1 row"), "expected 1 row updated: {resp}");
1887
1888 let docs = db.get_table("docs".to_string()).unwrap();
1889 let entry = docs.hnsw_indexes.iter().find(|e| e.name == "ix_e").unwrap();
1890 assert!(
1891 entry.needs_rebuild,
1892 "UPDATE on the vector column should have marked HNSW index dirty"
1893 );
1894 }
1895
1896 #[test]
1897 fn duplicate_index_name_errors() {
1898 let mut db = seed_hnsw_table();
1899 process_command("CREATE INDEX ix_e ON docs USING hnsw (e);", &mut db).unwrap();
1900 let err =
1901 process_command("CREATE INDEX ix_e ON docs USING hnsw (e);", &mut db).unwrap_err();
1902 let msg = format!("{err}");
1903 assert!(
1904 msg.to_lowercase().contains("already exists"),
1905 "expected duplicate-index error; got: {msg}"
1906 );
1907 }
1908
1909 #[test]
1910 fn index_if_not_exists_is_idempotent() {
1911 let mut db = seed_hnsw_table();
1912 process_command("CREATE INDEX ix_e ON docs USING hnsw (e);", &mut db).unwrap();
1913 process_command(
1915 "CREATE INDEX IF NOT EXISTS ix_e ON docs USING hnsw (e);",
1916 &mut db,
1917 )
1918 .unwrap();
1919 let table = db.get_table("docs".to_string()).unwrap();
1920 assert_eq!(table.hnsw_indexes.len(), 1);
1921 }
1922
1923 #[test]
1928 fn create_table_if_not_exists_is_idempotent() {
1929 let mut db = Database::new("tempdb".to_string());
1930 process_command(
1931 "CREATE TABLE t (id INTEGER PRIMARY KEY, v INTEGER);",
1932 &mut db,
1933 )
1934 .unwrap();
1935 let msg = process_command(
1938 "CREATE TABLE IF NOT EXISTS t (id INTEGER PRIMARY KEY, v INTEGER);",
1939 &mut db,
1940 )
1941 .expect("CREATE TABLE IF NOT EXISTS should be a no-op on an existing table");
1942 assert!(
1943 msg.to_lowercase().contains("no-op"),
1944 "expected a no-op status; got: {msg}"
1945 );
1946 assert_eq!(db.tables.len(), 1);
1948 }
1949
1950 #[test]
1951 fn create_table_without_if_not_exists_still_errors() {
1952 let mut db = Database::new("tempdb".to_string());
1953 process_command("CREATE TABLE t (id INTEGER PRIMARY KEY);", &mut db).unwrap();
1954 let err = process_command("CREATE TABLE t (id INTEGER PRIMARY KEY);", &mut db).unwrap_err();
1955 assert!(
1956 format!("{err}").to_lowercase().contains("already exists"),
1957 "plain CREATE TABLE on an existing table must still error; got: {err}"
1958 );
1959 }
1960
1961 #[test]
1962 fn create_table_if_not_exists_on_fresh_table_creates_it() {
1963 let mut db = Database::new("tempdb".to_string());
1964 process_command(
1966 "CREATE TABLE IF NOT EXISTS t (id INTEGER PRIMARY KEY, v INTEGER);",
1967 &mut db,
1968 )
1969 .unwrap();
1970 assert!(db.contains_table("t".to_string()));
1971 }
1972
1973 #[test]
1974 fn select_from_sqlrite_master_lists_tables_and_indexes() {
1975 use crate::sql::executor::execute_select_rows;
1976 use crate::sql::parser::select::SelectQuery;
1977
1978 let mut db = Database::new("tempdb".to_string());
1979 process_command(
1980 "CREATE TABLE users (id INTEGER PRIMARY KEY, email TEXT UNIQUE);",
1981 &mut db,
1982 )
1983 .unwrap();
1984 process_command("CREATE TABLE posts (id INTEGER PRIMARY KEY);", &mut db).unwrap();
1985 process_command("CREATE INDEX ix_email ON users (email);", &mut db).unwrap();
1986
1987 let run = |sql: &str, db: &Database| -> Vec<Vec<Value>> {
1989 let dialect = SqlriteDialect::new();
1990 let mut ast = Parser::parse_sql(&dialect, sql).unwrap();
1991 let sq = SelectQuery::new(&ast.pop().unwrap()).unwrap();
1992 execute_select_rows(sq, db).unwrap().rows
1993 };
1994
1995 let names: Vec<String> = run("SELECT name FROM sqlrite_master;", &db)
1997 .into_iter()
1998 .map(|r| match &r[0] {
1999 Value::Text(s) => s.clone(),
2000 other => panic!("expected Text name, got {other:?}"),
2001 })
2002 .collect();
2003 assert!(names.contains(&"users".to_string()));
2004 assert!(names.contains(&"posts".to_string()));
2005 assert!(names.contains(&"ix_email".to_string()));
2008
2009 let table_rows = run("SELECT name FROM sqlrite_master WHERE type = 'table';", &db);
2011 assert_eq!(table_rows.len(), 2, "two user tables");
2012
2013 let index_rows = run("SELECT name FROM sqlrite_master WHERE type = 'index';", &db);
2014 assert!(
2015 !index_rows.is_empty(),
2016 "at least the explicit ix_email index"
2017 );
2018
2019 let all = run("SELECT * FROM sqlrite_master WHERE name = 'users';", &db);
2021 assert_eq!(all.len(), 1);
2022 assert_eq!(all[0].len(), 5, "type, name, sql, rootpage, last_rowid");
2023 match &all[0][2] {
2024 Value::Text(sql) => assert!(
2025 sql.to_uppercase().contains("CREATE TABLE"),
2026 "sql column should carry the CREATE TABLE text; got: {sql}"
2027 ),
2028 other => panic!("expected Text sql, got {other:?}"),
2029 }
2030 }
2031
2032 #[test]
2033 fn writes_to_sqlrite_master_are_rejected() {
2034 let mut db = Database::new("tempdb".to_string());
2035 process_command("CREATE TABLE t (id INTEGER PRIMARY KEY);", &mut db).unwrap();
2036 assert!(
2040 process_command(
2041 "INSERT INTO sqlrite_master (type, name) VALUES ('table', 'x');",
2042 &mut db
2043 )
2044 .is_err()
2045 );
2046 }
2047
2048 #[test]
2049 fn select_from_sqlrite_master_survives_save_and_reopen() {
2050 use crate::sql::executor::execute_select_rows;
2051 use crate::sql::pager::{open_database, save_database};
2052 use crate::sql::parser::select::SelectQuery;
2053
2054 let dir = std::env::temp_dir();
2055 let path = dir.join(format!("sqlr10_master_{}.sqlrite", std::process::id()));
2056 let _ = std::fs::remove_file(&path);
2057
2058 let mut db = Database::new("tempdb".to_string());
2059 process_command("CREATE TABLE alpha (id INTEGER PRIMARY KEY);", &mut db).unwrap();
2060 process_command("CREATE TABLE beta (id INTEGER PRIMARY KEY);", &mut db).unwrap();
2061 save_database(&mut db, &path).unwrap();
2062
2063 let reopened = open_database(&path, "tempdb".to_string()).unwrap();
2064 let dialect = SqlriteDialect::new();
2065 let mut ast = Parser::parse_sql(
2066 &dialect,
2067 "SELECT name FROM sqlrite_master WHERE type = 'table';",
2068 )
2069 .unwrap();
2070 let sq = SelectQuery::new(&ast.pop().unwrap()).unwrap();
2071 let names: Vec<String> = execute_select_rows(sq, &reopened)
2072 .unwrap()
2073 .rows
2074 .into_iter()
2075 .map(|r| match &r[0] {
2076 Value::Text(s) => s.clone(),
2077 other => panic!("expected Text, got {other:?}"),
2078 })
2079 .collect();
2080 assert!(names.contains(&"alpha".to_string()));
2081 assert!(names.contains(&"beta".to_string()));
2082
2083 let _ = std::fs::remove_file(&path);
2084 }
2085
2086 fn seed_fts_table() -> Database {
2093 let mut db = Database::new("tempdb".to_string());
2094 process_command(
2095 "CREATE TABLE docs (id INTEGER PRIMARY KEY, body TEXT);",
2096 &mut db,
2097 )
2098 .unwrap();
2099 for body in &[
2100 "rust embedded database", "rust web framework", "go embedded systems", "python web framework", "rust rust rust embedded power", ] {
2106 process_command(
2107 &format!("INSERT INTO docs (body) VALUES ('{body}');"),
2108 &mut db,
2109 )
2110 .unwrap();
2111 }
2112 db
2113 }
2114
2115 #[test]
2116 fn create_index_using_fts_succeeds_and_indexes_existing_rows() {
2117 let mut db = seed_fts_table();
2118 let resp =
2119 process_command("CREATE INDEX ix_body ON docs USING fts (body);", &mut db).unwrap();
2120 assert!(resp.to_lowercase().contains("create index"), "got {resp}");
2121 let table = db.get_table("docs".to_string()).unwrap();
2122 assert_eq!(table.fts_indexes.len(), 1);
2123 assert_eq!(table.fts_indexes[0].name, "ix_body");
2124 assert_eq!(table.fts_indexes[0].column_name, "body");
2125 assert_eq!(table.fts_indexes[0].index.len(), 5);
2127 }
2128
2129 #[test]
2130 fn create_index_using_fts_rejects_non_text_column() {
2131 let mut db = Database::new("tempdb".to_string());
2132 process_command(
2133 "CREATE TABLE t (id INTEGER PRIMARY KEY, n INTEGER);",
2134 &mut db,
2135 )
2136 .unwrap();
2137 let err = process_command("CREATE INDEX ix_n ON t USING fts (n);", &mut db).unwrap_err();
2138 let msg = format!("{err}");
2139 assert!(
2140 msg.to_lowercase().contains("text"),
2141 "expected error mentioning TEXT; got: {msg}"
2142 );
2143 }
2144
2145 #[test]
2146 fn fts_match_returns_expected_rows() {
2147 let mut db = seed_fts_table();
2148 process_command("CREATE INDEX ix_body ON docs USING fts (body);", &mut db).unwrap();
2149 let resp = process_command(
2151 "SELECT id FROM docs WHERE fts_match(body, 'rust');",
2152 &mut db,
2153 )
2154 .unwrap();
2155 assert!(resp.contains("3 rows returned"), "got: {resp}");
2156 }
2157
2158 #[test]
2159 fn fts_match_without_index_errors_clearly() {
2160 let mut db = seed_fts_table();
2161 let err = process_command(
2163 "SELECT id FROM docs WHERE fts_match(body, 'rust');",
2164 &mut db,
2165 )
2166 .unwrap_err();
2167 let msg = format!("{err}");
2168 assert!(
2169 msg.contains("no FTS index"),
2170 "expected no-index error; got: {msg}"
2171 );
2172 }
2173
2174 #[test]
2175 fn bm25_score_orders_descending_by_relevance() {
2176 let mut db = seed_fts_table();
2177 process_command("CREATE INDEX ix_body ON docs USING fts (body);", &mut db).unwrap();
2178 let out = process_command_with_render(
2181 "SELECT id FROM docs WHERE fts_match(body, 'rust') \
2182 ORDER BY bm25_score(body, 'rust') DESC LIMIT 1;",
2183 &mut db,
2184 )
2185 .unwrap();
2186 assert!(out.status.contains("1 row returned"), "got: {}", out.status);
2187 let rendered = out.rendered.expect("SELECT should produce rendered output");
2188 assert!(
2190 rendered.contains(" 5 "),
2191 "expected id=5 to be top-ranked; rendered:\n{rendered}"
2192 );
2193 }
2194
2195 #[test]
2196 fn bm25_score_without_index_errors_clearly() {
2197 let mut db = seed_fts_table();
2198 let err = process_command(
2199 "SELECT id FROM docs ORDER BY bm25_score(body, 'rust') DESC LIMIT 1;",
2200 &mut db,
2201 )
2202 .unwrap_err();
2203 let msg = format!("{err}");
2204 assert!(
2205 msg.contains("no FTS index"),
2206 "expected no-index error; got: {msg}"
2207 );
2208 }
2209
2210 #[test]
2211 fn fts_post_create_inserts_are_indexed_incrementally() {
2212 let mut db = seed_fts_table();
2213 process_command("CREATE INDEX ix_body ON docs USING fts (body);", &mut db).unwrap();
2214 process_command(
2215 "INSERT INTO docs (body) VALUES ('rust embedded analytics');",
2216 &mut db,
2217 )
2218 .unwrap();
2219 let table = db.get_table("docs".to_string()).unwrap();
2220 assert_eq!(table.fts_indexes[0].index.len(), 6);
2222 let resp = process_command(
2224 "SELECT id FROM docs WHERE fts_match(body, 'analytics');",
2225 &mut db,
2226 )
2227 .unwrap();
2228 assert!(resp.contains("1 row returned"), "got: {resp}");
2229 }
2230
2231 #[test]
2232 fn delete_on_fts_indexed_table_marks_dirty() {
2233 let mut db = seed_fts_table();
2234 process_command("CREATE INDEX ix_body ON docs USING fts (body);", &mut db).unwrap();
2235 let resp = process_command("DELETE FROM docs WHERE id = 1;", &mut db).unwrap();
2236 assert!(resp.contains("1 row"), "got: {resp}");
2237 let docs = db.get_table("docs".to_string()).unwrap();
2238 let entry = docs
2239 .fts_indexes
2240 .iter()
2241 .find(|e| e.name == "ix_body")
2242 .unwrap();
2243 assert!(
2244 entry.needs_rebuild,
2245 "DELETE should have flagged the FTS index dirty"
2246 );
2247 }
2248
2249 #[test]
2250 fn update_on_fts_indexed_text_col_marks_dirty() {
2251 let mut db = seed_fts_table();
2252 process_command("CREATE INDEX ix_body ON docs USING fts (body);", &mut db).unwrap();
2253 let resp = process_command(
2254 "UPDATE docs SET body = 'java spring framework' WHERE id = 1;",
2255 &mut db,
2256 )
2257 .unwrap();
2258 assert!(resp.contains("1 row"), "got: {resp}");
2259 let docs = db.get_table("docs".to_string()).unwrap();
2260 let entry = docs
2261 .fts_indexes
2262 .iter()
2263 .find(|e| e.name == "ix_body")
2264 .unwrap();
2265 assert!(
2266 entry.needs_rebuild,
2267 "UPDATE on the indexed TEXT column should have flagged dirty"
2268 );
2269 }
2270
2271 #[test]
2272 fn fts_index_name_collides_with_btree_and_hnsw_namespaces() {
2273 let mut db = seed_fts_table();
2274 process_command("CREATE INDEX ix_body ON docs USING fts (body);", &mut db).unwrap();
2275 let err = process_command("CREATE INDEX ix_body ON docs (body);", &mut db).unwrap_err();
2276 let msg = format!("{err}");
2277 assert!(
2278 msg.to_lowercase().contains("already exists"),
2279 "expected duplicate-index error; got: {msg}"
2280 );
2281 }
2282
2283 #[test]
2284 fn fts_index_rejects_unique() {
2285 let mut db = seed_fts_table();
2286 let err = process_command(
2287 "CREATE UNIQUE INDEX ix_body ON docs USING fts (body);",
2288 &mut db,
2289 )
2290 .unwrap_err();
2291 let msg = format!("{err}");
2292 assert!(
2293 msg.to_lowercase().contains("unique"),
2294 "expected UNIQUE-rejection error; got: {msg}"
2295 );
2296 }
2297
2298 #[test]
2299 fn try_fts_probe_falls_through_on_ascending() {
2300 let mut db = seed_fts_table();
2305 process_command("CREATE INDEX ix_body ON docs USING fts (body);", &mut db).unwrap();
2306 let resp = process_command(
2309 "SELECT id FROM docs WHERE fts_match(body, 'rust') \
2310 ORDER BY bm25_score(body, 'rust') ASC LIMIT 3;",
2311 &mut db,
2312 )
2313 .unwrap();
2314 assert!(resp.contains("3 rows returned"), "got: {resp}");
2315 }
2316
2317 fn assert_fts_unknown_qualifier(db: &mut Database, sql: &str, qualifier: &str) {
2325 let err = process_command(sql, db)
2326 .expect_err("a bogus qualifier in an FTS function arg must error, not be ignored");
2327 let msg = format!("{err}");
2328 assert!(
2329 msg.contains(&format!("unknown table qualifier '{qualifier}'")),
2330 "expected unknown-qualifier error for `{sql}`, got: {msg}"
2331 );
2332 }
2333
2334 #[test]
2335 fn fts_match_with_matching_table_qualifier_works() {
2336 let mut db = seed_fts_table();
2337 process_command("CREATE INDEX ix_body ON docs USING fts (body);", &mut db).unwrap();
2338 let resp = process_command(
2339 "SELECT id FROM docs WHERE fts_match(docs.body, 'rust');",
2340 &mut db,
2341 )
2342 .unwrap();
2343 assert!(resp.contains("3 rows returned"), "got: {resp}");
2344 let resp = process_command(
2346 "SELECT id FROM docs WHERE fts_match(DOCS.body, 'rust');",
2347 &mut db,
2348 )
2349 .unwrap();
2350 assert!(resp.contains("3 rows returned"), "got: {resp}");
2351 }
2352
2353 #[test]
2354 fn fts_match_with_matching_alias_qualifier_works() {
2355 let mut db = seed_fts_table();
2356 process_command("CREATE INDEX ix_body ON docs USING fts (body);", &mut db).unwrap();
2357 let resp = process_command(
2358 "SELECT id FROM docs AS d WHERE fts_match(d.body, 'rust');",
2359 &mut db,
2360 )
2361 .unwrap();
2362 assert!(resp.contains("3 rows returned"), "got: {resp}");
2363 }
2364
2365 #[test]
2366 fn fts_match_alias_shadows_table_name_as_qualifier() {
2367 let mut db = seed_fts_table();
2371 process_command("CREATE INDEX ix_body ON docs USING fts (body);", &mut db).unwrap();
2372 assert_fts_unknown_qualifier(
2373 &mut db,
2374 "SELECT id FROM docs AS d WHERE fts_match(docs.body, 'rust');",
2375 "docs",
2376 );
2377 }
2378
2379 #[test]
2380 fn fts_match_unknown_qualifier_in_where_errors() {
2381 let mut db = seed_fts_table();
2382 process_command("CREATE INDEX ix_body ON docs USING fts (body);", &mut db).unwrap();
2383 assert_fts_unknown_qualifier(
2384 &mut db,
2385 "SELECT id FROM docs WHERE fts_match(bogus.body, 'rust');",
2386 "bogus",
2387 );
2388 }
2389
2390 #[test]
2391 fn bm25_score_unknown_qualifier_in_order_by_errors() {
2392 let mut db = seed_fts_table();
2396 process_command("CREATE INDEX ix_body ON docs USING fts (body);", &mut db).unwrap();
2397 assert_fts_unknown_qualifier(
2398 &mut db,
2399 "SELECT id FROM docs ORDER BY bm25_score(bogus.body, 'rust') DESC LIMIT 1;",
2400 "bogus",
2401 );
2402 }
2403
2404 #[test]
2405 fn bm25_score_unknown_qualifier_in_where_errors() {
2406 let mut db = seed_fts_table();
2410 process_command("CREATE INDEX ix_body ON docs USING fts (body);", &mut db).unwrap();
2411 assert_fts_unknown_qualifier(
2412 &mut db,
2413 "SELECT id FROM docs WHERE bm25_score(bogus.body, 'rust') > 0.0;",
2414 "bogus",
2415 );
2416 }
2417
2418 #[test]
2419 fn bm25_score_with_matching_qualifier_still_ranks_correctly() {
2420 let mut db = seed_fts_table();
2423 process_command("CREATE INDEX ix_body ON docs USING fts (body);", &mut db).unwrap();
2424 let out = process_command_with_render(
2425 "SELECT id FROM docs WHERE fts_match(docs.body, 'rust') \
2426 ORDER BY bm25_score(docs.body, 'rust') DESC LIMIT 1;",
2427 &mut db,
2428 )
2429 .unwrap();
2430 assert!(out.status.contains("1 row returned"), "got: {}", out.status);
2431 let rendered = out.rendered.expect("SELECT should produce rendered output");
2432 assert!(
2433 rendered.contains(" 5 "),
2434 "expected id=5 to be top-ranked; rendered:\n{rendered}"
2435 );
2436 }
2437
2438 fn seed_vector_docs() -> Database {
2448 let mut db = Database::new("tempdb".to_string());
2449 process_command(
2450 "CREATE TABLE docs (id INTEGER PRIMARY KEY, e VECTOR(2));",
2451 &mut db,
2452 )
2453 .expect("create");
2454 process_command("INSERT INTO docs (e) VALUES ([1.0, 0.0]);", &mut db).expect("insert 1");
2455 process_command("INSERT INTO docs (e) VALUES ([0.0, 1.0]);", &mut db).expect("insert 2");
2456 process_command("INSERT INTO docs (e) VALUES ([1.0, 1.0]);", &mut db).expect("insert 3");
2457 db
2458 }
2459
2460 #[test]
2461 fn vec_distance_l2_in_where_filters_correctly() {
2462 let mut db = seed_vector_docs();
2468 let resp = process_command(
2469 "SELECT * FROM docs WHERE vec_distance_l2(e, [1.0, 0.0]) < 1.1;",
2470 &mut db,
2471 )
2472 .expect("select");
2473 assert!(
2474 resp.contains("2 rows returned"),
2475 "expected 2 rows, got: {resp}"
2476 );
2477 }
2478
2479 #[test]
2480 fn vec_distance_cosine_in_where() {
2481 let mut db = seed_vector_docs();
2486 let resp = process_command(
2487 "SELECT * FROM docs WHERE vec_distance_cosine(e, [1.0, 0.0]) < 0.5;",
2488 &mut db,
2489 )
2490 .expect("select");
2491 assert!(
2492 resp.contains("2 rows returned"),
2493 "expected 2 rows, got: {resp}"
2494 );
2495 }
2496
2497 #[test]
2498 fn vec_distance_dot_negated() {
2499 let mut db = seed_vector_docs();
2504 let resp = process_command(
2505 "SELECT * FROM docs WHERE vec_distance_dot(e, [1.0, 0.0]) < 0.0;",
2506 &mut db,
2507 )
2508 .expect("select");
2509 assert!(
2510 resp.contains("2 rows returned"),
2511 "expected 2 rows, got: {resp}"
2512 );
2513 }
2514
2515 #[test]
2516 fn knn_via_order_by_distance_limit() {
2517 let mut db = seed_vector_docs();
2521 let resp = process_command(
2522 "SELECT id FROM docs ORDER BY vec_distance_l2(e, [1.0, 0.0]) ASC LIMIT 2;",
2523 &mut db,
2524 )
2525 .expect("select");
2526 assert!(
2527 resp.contains("2 rows returned"),
2528 "expected 2 rows, got: {resp}"
2529 );
2530 }
2531
2532 #[test]
2533 fn distance_function_dim_mismatch_errors() {
2534 let mut db = seed_vector_docs();
2536 let err = process_command(
2537 "SELECT * FROM docs WHERE vec_distance_l2(e, [1.0, 0.0, 0.0]) < 1.0;",
2538 &mut db,
2539 )
2540 .unwrap_err();
2541 let msg = format!("{err}");
2542 assert!(
2543 msg.to_lowercase().contains("dimension")
2544 && msg.contains("lhs=2")
2545 && msg.contains("rhs=3"),
2546 "expected dim mismatch error, got: {msg}"
2547 );
2548 }
2549
2550 #[test]
2551 fn unknown_function_errors_with_name() {
2552 let mut db = seed_vector_docs();
2556 let err = process_command(
2557 "SELECT * FROM docs WHERE vec_does_not_exist(e, [1.0, 0.0]) < 1.0;",
2558 &mut db,
2559 )
2560 .unwrap_err();
2561 let msg = format!("{err}");
2562 assert!(
2563 msg.contains("vec_does_not_exist"),
2564 "expected error mentioning function name, got: {msg}"
2565 );
2566 }
2567
2568 fn seed_json_table() -> Database {
2573 let mut db = Database::new("tempdb".to_string());
2574 process_command(
2575 "CREATE TABLE docs (id INTEGER PRIMARY KEY, payload JSON);",
2576 &mut db,
2577 )
2578 .expect("create json table");
2579 db
2580 }
2581
2582 #[test]
2583 fn json_column_round_trip_primitive_values() {
2584 let mut db = seed_json_table();
2585 process_command(
2586 r#"INSERT INTO docs (payload) VALUES ('{"name": "alice", "age": 30}');"#,
2587 &mut db,
2588 )
2589 .expect("insert json");
2590 let docs = db.get_table("docs".to_string()).unwrap();
2591 let rowids = docs.rowids();
2592 assert_eq!(rowids.len(), 1);
2593 match docs.get_value("payload", rowids[0]) {
2595 Some(Value::Text(s)) => {
2596 assert!(s.contains("alice"), "expected JSON text to round-trip: {s}");
2597 }
2598 other => panic!("expected Value::Text holding JSON, got {other:?}"),
2599 }
2600 }
2601
2602 #[test]
2603 fn json_insert_rejects_invalid_json() {
2604 let mut db = seed_json_table();
2605 let err = process_command(
2606 "INSERT INTO docs (payload) VALUES ('not-valid-json{');",
2607 &mut db,
2608 )
2609 .unwrap_err();
2610 let msg = format!("{err}").to_lowercase();
2611 assert!(
2612 msg.contains("json") && msg.contains("payload"),
2613 "expected JSON validation error mentioning column, got: {msg}"
2614 );
2615 }
2616
2617 #[test]
2618 fn json_extract_object_field() {
2619 let mut db = seed_json_table();
2620 process_command(
2621 r#"INSERT INTO docs (payload) VALUES ('{"name": "alice", "age": 30}');"#,
2622 &mut db,
2623 )
2624 .unwrap();
2625 let resp = process_command(
2628 r#"SELECT id FROM docs WHERE json_extract(payload, '$.name') = 'alice';"#,
2629 &mut db,
2630 )
2631 .expect("select via json_extract");
2632 assert!(resp.contains("1 row returned"), "got: {resp}");
2633
2634 let resp = process_command(
2635 r#"SELECT id FROM docs WHERE json_extract(payload, '$.age') = 30;"#,
2636 &mut db,
2637 )
2638 .expect("select via numeric json_extract");
2639 assert!(resp.contains("1 row returned"), "got: {resp}");
2640 }
2641
2642 #[test]
2643 fn json_extract_array_index_and_nested() {
2644 let mut db = seed_json_table();
2645 process_command(
2646 r#"INSERT INTO docs (payload) VALUES ('{"tags": ["rust", "sql", "vectors"], "meta": {"author": "joao"}}');"#,
2647 &mut db,
2648 )
2649 .unwrap();
2650 let resp = process_command(
2651 r#"SELECT id FROM docs WHERE json_extract(payload, '$.tags[0]') = 'rust';"#,
2652 &mut db,
2653 )
2654 .expect("select via array index");
2655 assert!(resp.contains("1 row returned"), "got: {resp}");
2656
2657 let resp = process_command(
2658 r#"SELECT id FROM docs WHERE json_extract(payload, '$.meta.author') = 'joao';"#,
2659 &mut db,
2660 )
2661 .expect("select via nested object");
2662 assert!(resp.contains("1 row returned"), "got: {resp}");
2663 }
2664
2665 #[test]
2666 fn json_extract_missing_path_returns_null() {
2667 let mut db = seed_json_table();
2668 process_command(
2669 r#"INSERT INTO docs (payload) VALUES ('{"name": "alice"}');"#,
2670 &mut db,
2671 )
2672 .unwrap();
2673 let resp = process_command(
2676 r#"SELECT id FROM docs WHERE json_extract(payload, '$.missing') = 'something';"#,
2677 &mut db,
2678 )
2679 .expect("select with missing path");
2680 assert!(resp.contains("0 rows returned"), "got: {resp}");
2681 }
2682
2683 #[test]
2684 fn json_extract_malformed_path_errors() {
2685 let mut db = seed_json_table();
2686 process_command(
2687 r#"INSERT INTO docs (payload) VALUES ('{"a": 1}');"#,
2688 &mut db,
2689 )
2690 .unwrap();
2691 let err = process_command(
2693 r#"SELECT id FROM docs WHERE json_extract(payload, 'a.b') = 1;"#,
2694 &mut db,
2695 )
2696 .unwrap_err();
2697 assert!(format!("{err}").contains("'$'"));
2698 }
2699
2700 #[test]
2701 fn json_array_length_on_array() {
2702 let mut db = seed_json_table();
2705 process_command(
2706 r#"INSERT INTO docs (payload) VALUES ('{"tags": ["a", "b", "c"]}');"#,
2707 &mut db,
2708 )
2709 .unwrap();
2710 let resp = process_command(
2711 r#"SELECT id FROM docs WHERE json_array_length(payload, '$.tags') = 3;"#,
2712 &mut db,
2713 )
2714 .expect("select via array_length");
2715 assert!(resp.contains("1 row returned"), "got: {resp}");
2716 }
2717
2718 #[test]
2719 fn json_array_length_on_non_array_errors() {
2720 let mut db = seed_json_table();
2721 process_command(
2722 r#"INSERT INTO docs (payload) VALUES ('{"tags": "not-an-array"}');"#,
2723 &mut db,
2724 )
2725 .unwrap();
2726 let err = process_command(
2727 r#"SELECT id FROM docs WHERE json_array_length(payload, '$.tags') = 1;"#,
2728 &mut db,
2729 )
2730 .unwrap_err();
2731 let msg = format!("{err}").to_lowercase();
2732 assert!(
2733 msg.contains("non-array"),
2734 "expected non-array error, got: {msg}"
2735 );
2736 }
2737
2738 #[test]
2739 fn json_type_recognizes_each_kind() {
2740 let mut db = seed_json_table();
2741 process_command(
2742 r#"INSERT INTO docs (payload) VALUES ('{"o": {}, "a": [], "s": "x", "i": 1, "f": 1.5, "t": true, "n": null}');"#,
2743 &mut db,
2744 )
2745 .unwrap();
2746 let cases = &[
2747 ("$.o", "object"),
2748 ("$.a", "array"),
2749 ("$.s", "text"),
2750 ("$.i", "integer"),
2751 ("$.f", "real"),
2752 ("$.t", "true"),
2753 ("$.n", "null"),
2754 ];
2755 for (path, expected_type) in cases {
2756 let sql = format!(
2757 "SELECT id FROM docs WHERE json_type(payload, '{path}') = '{expected_type}';"
2758 );
2759 let resp =
2760 process_command(&sql, &mut db).unwrap_or_else(|e| panic!("path {path}: {e}"));
2761 assert!(
2762 resp.contains("1 row returned"),
2763 "path {path} expected type {expected_type}; got response: {resp}"
2764 );
2765 }
2766 }
2767
2768 #[test]
2769 fn update_on_json_column_revalidates() {
2770 let mut db = seed_json_table();
2771 process_command(
2772 r#"INSERT INTO docs (payload) VALUES ('{"a": 1}');"#,
2773 &mut db,
2774 )
2775 .unwrap();
2776 process_command(
2778 r#"UPDATE docs SET payload = '{"a": 2, "b": 3}' WHERE id = 1;"#,
2779 &mut db,
2780 )
2781 .expect("valid JSON UPDATE");
2782 let err = process_command(
2785 r#"UPDATE docs SET payload = 'not-json{' WHERE id = 1;"#,
2786 &mut db,
2787 )
2788 .unwrap_err();
2789 let msg = format!("{err}").to_lowercase();
2790 assert!(
2791 msg.contains("json") && msg.contains("payload"),
2792 "got: {msg}"
2793 );
2794 }
2795
2796 #[test]
2801 fn default_literal_int_applies_when_column_omitted() {
2802 let mut db = Database::new("t".to_string());
2803 process_command(
2804 "CREATE TABLE t (id INTEGER PRIMARY KEY, n INTEGER DEFAULT 42);",
2805 &mut db,
2806 )
2807 .unwrap();
2808 process_command("INSERT INTO t (id) VALUES (1);", &mut db).unwrap();
2809
2810 let table = db.get_table("t".to_string()).unwrap();
2811 assert_eq!(table.get_value("n", 1), Some(Value::Integer(42)));
2812 }
2813
2814 #[test]
2815 fn default_literal_text_applies_when_column_omitted() {
2816 let mut db = Database::new("t".to_string());
2817 process_command(
2818 "CREATE TABLE users (id INTEGER PRIMARY KEY, status TEXT DEFAULT 'active');",
2819 &mut db,
2820 )
2821 .unwrap();
2822 process_command("INSERT INTO users (id) VALUES (1);", &mut db).unwrap();
2823
2824 let table = db.get_table("users".to_string()).unwrap();
2825 assert_eq!(
2826 table.get_value("status", 1),
2827 Some(Value::Text("active".to_string()))
2828 );
2829 }
2830
2831 #[test]
2832 fn default_literal_real_negative_applies_when_column_omitted() {
2833 let mut db = Database::new("t".to_string());
2835 process_command(
2836 "CREATE TABLE t (id INTEGER PRIMARY KEY, score REAL DEFAULT -1.5);",
2837 &mut db,
2838 )
2839 .unwrap();
2840 process_command("INSERT INTO t (id) VALUES (1);", &mut db).unwrap();
2841
2842 let table = db.get_table("t".to_string()).unwrap();
2843 assert_eq!(table.get_value("score", 1), Some(Value::Real(-1.5)));
2844 }
2845
2846 #[test]
2847 fn default_with_type_mismatch_errors_at_create_time() {
2848 let mut db = Database::new("t".to_string());
2849 let result = process_command(
2850 "CREATE TABLE t (id INTEGER PRIMARY KEY, n INTEGER DEFAULT 'oops');",
2851 &mut db,
2852 );
2853 let err = result.expect_err("text default on INTEGER column should be rejected");
2854 let msg = format!("{err}").to_lowercase();
2855 assert!(msg.contains("default"), "got: {msg}");
2856 }
2857
2858 #[test]
2859 fn default_for_json_column_must_be_valid_json() {
2860 let mut db = Database::new("t".to_string());
2864 let err = process_command(
2865 "CREATE TABLE t (id INTEGER PRIMARY KEY, doc JSON DEFAULT 'not-json{');",
2866 &mut db,
2867 )
2868 .unwrap_err();
2869 assert!(
2870 format!("{err}").to_lowercase().contains("json"),
2871 "got: {err}"
2872 );
2873
2874 process_command(
2876 "CREATE TABLE t2 (id INTEGER PRIMARY KEY, doc JSON DEFAULT '{\"k\":1}');",
2877 &mut db,
2878 )
2879 .expect("valid JSON DEFAULT should be accepted");
2880 }
2881
2882 #[test]
2883 fn default_with_non_literal_expression_errors_at_create_time() {
2884 let mut db = Database::new("t".to_string());
2885 let result = process_command(
2888 "CREATE TABLE t (id INTEGER PRIMARY KEY, ts TEXT DEFAULT CURRENT_TIMESTAMP);",
2889 &mut db,
2890 );
2891 let err = result.expect_err("non-literal DEFAULT should be rejected");
2892 let msg = format!("{err}").to_lowercase();
2893 assert!(
2894 msg.contains("default") && msg.contains("literal"),
2895 "got: {msg}"
2896 );
2897 }
2898
2899 #[test]
2900 fn default_null_is_accepted_at_create_time() {
2901 let mut db = Database::new("t".to_string());
2905 process_command(
2906 "CREATE TABLE t (id INTEGER PRIMARY KEY, note TEXT DEFAULT NULL);",
2907 &mut db,
2908 )
2909 .expect("CREATE TABLE with DEFAULT NULL should be accepted");
2910 let table = db.get_table("t".to_string()).unwrap();
2911 let note = table
2912 .columns
2913 .iter()
2914 .find(|c| c.column_name == "note")
2915 .unwrap();
2916 assert_eq!(note.default, Some(Value::Null));
2917 }
2918
2919 #[test]
2924 fn drop_table_basic() {
2925 let mut db = seed_users_table();
2926 let response = process_command("DROP TABLE users;", &mut db).expect("drop table");
2927 assert!(response.contains("1 table dropped"));
2928 assert!(!db.contains_table("users".to_string()));
2929 }
2930
2931 #[test]
2932 fn drop_table_if_exists_noop_on_missing() {
2933 let mut db = Database::new("t".to_string());
2934 let response =
2935 process_command("DROP TABLE IF EXISTS missing;", &mut db).expect("drop if exists");
2936 assert!(response.contains("0 tables dropped"));
2937 }
2938
2939 #[test]
2940 fn drop_table_missing_errors_without_if_exists() {
2941 let mut db = Database::new("t".to_string());
2942 let err = process_command("DROP TABLE missing;", &mut db).unwrap_err();
2943 assert!(format!("{err}").contains("does not exist"), "got: {err}");
2944 }
2945
2946 #[test]
2947 fn drop_table_reserved_name_errors() {
2948 let mut db = Database::new("t".to_string());
2949 let err = process_command("DROP TABLE sqlrite_master;", &mut db).unwrap_err();
2950 assert!(format!("{err}").contains("reserved"), "got: {err}");
2951 }
2952
2953 #[test]
2954 fn drop_table_multi_target_rejected() {
2955 let mut db = seed_users_table();
2956 process_command("CREATE TABLE other (id INTEGER PRIMARY KEY);", &mut db).unwrap();
2957 let err = process_command("DROP TABLE users, other;", &mut db).unwrap_err();
2960 assert!(format!("{err}").contains("single table"), "got: {err}");
2961 }
2962
2963 #[test]
2964 fn drop_table_cascades_indexes_in_memory() {
2965 let mut db = seed_users_table();
2966 process_command("CREATE INDEX users_age_idx ON users (age);", &mut db).unwrap();
2967 let users = db.get_table("users".to_string()).unwrap();
2969 assert!(
2970 users
2971 .secondary_indexes
2972 .iter()
2973 .any(|i| i.name == "users_age_idx")
2974 );
2975
2976 process_command("DROP TABLE users;", &mut db).unwrap();
2977
2978 for table in db.tables.values() {
2980 assert!(
2981 !table
2982 .secondary_indexes
2983 .iter()
2984 .any(|i| i.name.contains("users")),
2985 "dropped table's indexes should not survive on any other table"
2986 );
2987 }
2988 }
2989
2990 #[test]
2991 fn drop_index_explicit_basic() {
2992 let mut db = seed_users_table();
2993 process_command("CREATE INDEX users_age_idx ON users (age);", &mut db).unwrap();
2994 let response = process_command("DROP INDEX users_age_idx;", &mut db).expect("drop index");
2995 assert!(response.contains("1 index dropped"));
2996
2997 let users = db.get_table("users".to_string()).unwrap();
2998 assert!(users.index_by_name("users_age_idx").is_none());
2999 }
3000
3001 #[test]
3002 fn drop_index_refuses_auto_index() {
3003 let mut db = seed_users_table();
3004 let err = process_command("DROP INDEX sqlrite_autoindex_users_id;", &mut db).unwrap_err();
3007 assert!(format!("{err}").contains("auto-created"), "got: {err}");
3008 }
3009
3010 #[test]
3011 fn drop_index_if_exists_noop_on_missing() {
3012 let mut db = Database::new("t".to_string());
3013 let response =
3014 process_command("DROP INDEX IF EXISTS nope;", &mut db).expect("drop index if exists");
3015 assert!(response.contains("0 indexes dropped"));
3016 }
3017
3018 #[test]
3019 fn drop_index_missing_errors_without_if_exists() {
3020 let mut db = Database::new("t".to_string());
3021 let err = process_command("DROP INDEX nope;", &mut db).unwrap_err();
3022 assert!(format!("{err}").contains("does not exist"), "got: {err}");
3023 }
3024
3025 #[test]
3026 fn drop_statements_rejected_on_readonly_db() {
3027 use crate::sql::pager::{open_database_read_only, save_database};
3028
3029 let mut seed = Database::new("t".to_string());
3030 process_command(
3031 "CREATE TABLE notes (id INTEGER PRIMARY KEY, body TEXT);",
3032 &mut seed,
3033 )
3034 .unwrap();
3035 process_command("CREATE INDEX notes_body ON notes (body);", &mut seed).unwrap();
3036 let path = {
3037 let mut p = std::env::temp_dir();
3038 let pid = std::process::id();
3039 let nanos = std::time::SystemTime::now()
3040 .duration_since(std::time::UNIX_EPOCH)
3041 .map(|d| d.as_nanos())
3042 .unwrap_or(0);
3043 p.push(format!("sqlrite-drop-ro-{pid}-{nanos}.sqlrite"));
3044 p
3045 };
3046 save_database(&mut seed, &path).unwrap();
3047 drop(seed);
3048
3049 let mut ro = open_database_read_only(&path, "t".to_string()).unwrap();
3050 for stmt in ["DROP TABLE notes;", "DROP INDEX notes_body;"] {
3051 let err = process_command(stmt, &mut ro).unwrap_err();
3052 assert!(
3053 format!("{err}").contains("read-only"),
3054 "{stmt:?} should surface read-only error, got: {err}"
3055 );
3056 }
3057
3058 let _ = std::fs::remove_file(&path);
3059 let mut wal = path.as_os_str().to_owned();
3060 wal.push("-wal");
3061 let _ = std::fs::remove_file(std::path::PathBuf::from(wal));
3062 }
3063
3064 #[test]
3069 fn alter_rename_table_basic() {
3070 let mut db = seed_users_table();
3071 process_command("ALTER TABLE users RENAME TO members;", &mut db).expect("rename table");
3072 assert!(!db.contains_table("users".to_string()));
3073 assert!(db.contains_table("members".to_string()));
3074 let response = process_command("SELECT * FROM members;", &mut db).expect("select");
3076 assert!(response.contains("3 rows returned"));
3077 }
3078
3079 #[test]
3080 fn alter_rename_table_renames_auto_indexes() {
3081 let mut db = Database::new("t".to_string());
3084 process_command(
3085 "CREATE TABLE accounts (id INTEGER PRIMARY KEY, email TEXT UNIQUE);",
3086 &mut db,
3087 )
3088 .unwrap();
3089 {
3090 let accounts = db.get_table("accounts".to_string()).unwrap();
3091 assert!(
3092 accounts
3093 .index_by_name("sqlrite_autoindex_accounts_id")
3094 .is_some()
3095 );
3096 assert!(
3097 accounts
3098 .index_by_name("sqlrite_autoindex_accounts_email")
3099 .is_some()
3100 );
3101 }
3102 process_command("ALTER TABLE accounts RENAME TO members;", &mut db).expect("rename");
3103 let members = db.get_table("members".to_string()).unwrap();
3104 assert!(
3105 members
3106 .index_by_name("sqlrite_autoindex_members_id")
3107 .is_some(),
3108 "PK auto-index should be renamed to match new table"
3109 );
3110 assert!(
3111 members
3112 .index_by_name("sqlrite_autoindex_members_email")
3113 .is_some()
3114 );
3115 assert!(
3117 members
3118 .index_by_name("sqlrite_autoindex_accounts_id")
3119 .is_none()
3120 );
3121 for idx in &members.secondary_indexes {
3123 assert_eq!(idx.table_name, "members");
3124 }
3125 }
3126
3127 #[test]
3128 fn alter_rename_table_to_existing_errors() {
3129 let mut db = seed_users_table();
3130 process_command("CREATE TABLE other (id INTEGER PRIMARY KEY);", &mut db).unwrap();
3131 let err = process_command("ALTER TABLE users RENAME TO other;", &mut db).unwrap_err();
3132 assert!(format!("{err}").contains("already exists"), "got: {err}");
3133 assert!(db.contains_table("users".to_string()));
3135 assert!(db.contains_table("other".to_string()));
3136 }
3137
3138 #[test]
3139 fn alter_rename_table_to_reserved_name_errors() {
3140 let mut db = seed_users_table();
3141 let err =
3142 process_command("ALTER TABLE users RENAME TO sqlrite_master;", &mut db).unwrap_err();
3143 assert!(format!("{err}").contains("reserved"), "got: {err}");
3144 }
3145
3146 #[test]
3147 fn alter_rename_column_basic() {
3148 let mut db = seed_users_table();
3149 process_command(
3150 "ALTER TABLE users RENAME COLUMN name TO full_name;",
3151 &mut db,
3152 )
3153 .expect("rename column");
3154
3155 let users = db.get_table("users".to_string()).unwrap();
3156 assert!(users.contains_column("full_name".to_string()));
3157 assert!(!users.contains_column("name".to_string()));
3158
3159 let bob_rowid = users
3162 .rowids()
3163 .into_iter()
3164 .find(|r| users.get_value("full_name", *r) == Some(Value::Text("bob".to_string())))
3165 .expect("bob row should be findable under the new column name");
3166 assert_eq!(
3167 users.get_value("full_name", bob_rowid),
3168 Some(Value::Text("bob".to_string()))
3169 );
3170 }
3171
3172 #[test]
3173 fn alter_rename_column_collision_errors() {
3174 let mut db = seed_users_table();
3175 let err =
3176 process_command("ALTER TABLE users RENAME COLUMN name TO age;", &mut db).unwrap_err();
3177 assert!(format!("{err}").contains("already exists"), "got: {err}");
3178 }
3179
3180 #[test]
3181 fn alter_rename_column_updates_indexes() {
3182 let mut db = Database::new("t".to_string());
3184 process_command(
3185 "CREATE TABLE accounts (id INTEGER PRIMARY KEY, email TEXT UNIQUE);",
3186 &mut db,
3187 )
3188 .unwrap();
3189 process_command(
3190 "ALTER TABLE accounts RENAME COLUMN email TO contact;",
3191 &mut db,
3192 )
3193 .unwrap();
3194 let accounts = db.get_table("accounts".to_string()).unwrap();
3195 assert!(
3196 accounts
3197 .index_by_name("sqlrite_autoindex_accounts_contact")
3198 .is_some()
3199 );
3200 assert!(
3201 accounts
3202 .index_by_name("sqlrite_autoindex_accounts_email")
3203 .is_none()
3204 );
3205 }
3206
3207 #[test]
3208 fn alter_add_column_basic() {
3209 let mut db = seed_users_table();
3210 process_command("ALTER TABLE users ADD COLUMN nickname TEXT;", &mut db)
3211 .expect("add column");
3212 let users = db.get_table("users".to_string()).unwrap();
3213 assert!(users.contains_column("nickname".to_string()));
3214 let any_rowid = *users.rowids().first().expect("seed has rows");
3216 assert_eq!(users.get_value("nickname", any_rowid), None);
3217
3218 process_command(
3220 "INSERT INTO users (name, age, nickname) VALUES ('dan', 22, 'd');",
3221 &mut db,
3222 )
3223 .expect("insert with new col");
3224 let users = db.get_table("users".to_string()).unwrap();
3225 let dan_rowid = users
3226 .rowids()
3227 .into_iter()
3228 .find(|r| users.get_value("name", *r) == Some(Value::Text("dan".to_string())))
3229 .unwrap();
3230 assert_eq!(
3231 users.get_value("nickname", dan_rowid),
3232 Some(Value::Text("d".to_string()))
3233 );
3234 }
3235
3236 #[test]
3237 fn alter_add_column_with_default_backfills_existing_rows() {
3238 let mut db = seed_users_table();
3239 process_command(
3240 "ALTER TABLE users ADD COLUMN status TEXT DEFAULT 'active';",
3241 &mut db,
3242 )
3243 .expect("add column with default");
3244 let users = db.get_table("users".to_string()).unwrap();
3245 for rowid in users.rowids() {
3246 assert_eq!(
3247 users.get_value("status", rowid),
3248 Some(Value::Text("active".to_string())),
3249 "rowid {rowid} should have been backfilled with the default"
3250 );
3251 }
3252 }
3253
3254 #[test]
3255 fn alter_add_column_not_null_with_default_works_on_nonempty_table() {
3256 let mut db = seed_users_table();
3257 process_command(
3258 "ALTER TABLE users ADD COLUMN score INTEGER NOT NULL DEFAULT 0;",
3259 &mut db,
3260 )
3261 .expect("NOT NULL ADD with DEFAULT should succeed even with existing rows");
3262 let users = db.get_table("users".to_string()).unwrap();
3263 for rowid in users.rowids() {
3264 assert_eq!(users.get_value("score", rowid), Some(Value::Integer(0)));
3265 }
3266 }
3267
3268 #[test]
3269 fn alter_add_column_not_null_without_default_errors_on_nonempty_table() {
3270 let mut db = seed_users_table();
3271 let err = process_command(
3272 "ALTER TABLE users ADD COLUMN score INTEGER NOT NULL;",
3273 &mut db,
3274 )
3275 .unwrap_err();
3276 let msg = format!("{err}").to_lowercase();
3277 assert!(
3278 msg.contains("not null") && msg.contains("default"),
3279 "got: {msg}"
3280 );
3281 }
3282
3283 #[test]
3284 fn alter_add_column_pk_rejected() {
3285 let mut db = seed_users_table();
3286 let err = process_command(
3287 "ALTER TABLE users ADD COLUMN extra INTEGER PRIMARY KEY;",
3288 &mut db,
3289 )
3290 .unwrap_err();
3291 assert!(
3292 format!("{err}").to_lowercase().contains("primary key"),
3293 "got: {err}"
3294 );
3295 }
3296
3297 #[test]
3298 fn alter_add_column_unique_rejected() {
3299 let mut db = seed_users_table();
3300 let err = process_command(
3301 "ALTER TABLE users ADD COLUMN extra INTEGER UNIQUE;",
3302 &mut db,
3303 )
3304 .unwrap_err();
3305 assert!(
3306 format!("{err}").to_lowercase().contains("unique"),
3307 "got: {err}"
3308 );
3309 }
3310
3311 #[test]
3312 fn alter_add_column_existing_name_errors() {
3313 let mut db = seed_users_table();
3314 let err =
3315 process_command("ALTER TABLE users ADD COLUMN age INTEGER;", &mut db).unwrap_err();
3316 assert!(format!("{err}").contains("already exists"), "got: {err}");
3317 }
3318
3319 #[test]
3325 fn alter_drop_column_basic() {
3326 let mut db = seed_users_table();
3327 process_command("ALTER TABLE users DROP COLUMN age;", &mut db).expect("drop column");
3328 let users = db.get_table("users".to_string()).unwrap();
3329 assert!(!users.contains_column("age".to_string()));
3330 assert!(users.contains_column("name".to_string()));
3332 assert_eq!(users.rowids().len(), 3);
3333 }
3334
3335 #[test]
3336 fn alter_drop_column_drops_dependent_indexes() {
3337 let mut db = seed_users_table();
3338 process_command("CREATE INDEX users_age_idx ON users (age);", &mut db).unwrap();
3339 process_command("ALTER TABLE users DROP COLUMN age;", &mut db).unwrap();
3340 let users = db.get_table("users".to_string()).unwrap();
3341 assert!(users.index_by_name("users_age_idx").is_none());
3342 }
3343
3344 #[test]
3345 fn alter_drop_column_pk_errors() {
3346 let mut db = seed_users_table();
3347 let err = process_command("ALTER TABLE users DROP COLUMN id;", &mut db).unwrap_err();
3348 assert!(
3349 format!("{err}").to_lowercase().contains("primary key"),
3350 "got: {err}"
3351 );
3352 }
3353
3354 #[test]
3355 fn alter_drop_column_only_column_errors() {
3356 let mut db = Database::new("t".to_string());
3357 process_command("CREATE TABLE solo (only_col TEXT);", &mut db).unwrap();
3358 let err = process_command("ALTER TABLE solo DROP COLUMN only_col;", &mut db).unwrap_err();
3359 assert!(
3360 format!("{err}").to_lowercase().contains("only column"),
3361 "got: {err}"
3362 );
3363 }
3364
3365 #[test]
3366 fn alter_unknown_table_errors_without_if_exists() {
3367 let mut db = Database::new("t".to_string());
3368 let err = process_command("ALTER TABLE missing RENAME TO other;", &mut db).unwrap_err();
3369 assert!(format!("{err}").contains("does not exist"), "got: {err}");
3370 }
3371
3372 #[test]
3373 fn alter_unknown_table_if_exists_noop() {
3374 let mut db = Database::new("t".to_string());
3375 let response = process_command("ALTER TABLE IF EXISTS missing RENAME TO other;", &mut db)
3376 .expect("IF EXISTS makes missing-table ALTER a no-op");
3377 assert!(response.contains("no-op"));
3378 }
3379
3380 #[test]
3381 fn drop_table_inside_transaction_rolls_back() {
3382 let mut db = seed_users_table();
3386 process_command("CREATE INDEX users_age_idx ON users (age);", &mut db).unwrap();
3387 process_command("BEGIN;", &mut db).unwrap();
3388 process_command("DROP TABLE users;", &mut db).unwrap();
3389 assert!(!db.contains_table("users".to_string()));
3390 process_command("ROLLBACK;", &mut db).unwrap();
3391 assert!(db.contains_table("users".to_string()));
3392 let users = db.get_table("users".to_string()).unwrap();
3393 assert_eq!(users.rowids().len(), 3);
3394 assert!(users.index_by_name("users_age_idx").is_some());
3395 }
3396
3397 #[test]
3398 fn alter_inside_transaction_rolls_back() {
3399 let mut db = seed_users_table();
3400 process_command("BEGIN;", &mut db).unwrap();
3401 process_command(
3402 "ALTER TABLE users ADD COLUMN status TEXT DEFAULT 'active';",
3403 &mut db,
3404 )
3405 .unwrap();
3406 assert!(
3408 db.get_table("users".to_string())
3409 .unwrap()
3410 .contains_column("status".to_string())
3411 );
3412 process_command("ROLLBACK;", &mut db).unwrap();
3413 assert!(
3415 !db.get_table("users".to_string())
3416 .unwrap()
3417 .contains_column("status".to_string())
3418 );
3419 }
3420
3421 #[test]
3422 fn alter_rejected_on_readonly_db() {
3423 use crate::sql::pager::{open_database_read_only, save_database};
3424
3425 let mut seed = Database::new("t".to_string());
3426 process_command(
3427 "CREATE TABLE notes (id INTEGER PRIMARY KEY, body TEXT);",
3428 &mut seed,
3429 )
3430 .unwrap();
3431 let path = {
3432 let mut p = std::env::temp_dir();
3433 let pid = std::process::id();
3434 let nanos = std::time::SystemTime::now()
3435 .duration_since(std::time::UNIX_EPOCH)
3436 .map(|d| d.as_nanos())
3437 .unwrap_or(0);
3438 p.push(format!("sqlrite-alter-ro-{pid}-{nanos}.sqlrite"));
3439 p
3440 };
3441 save_database(&mut seed, &path).unwrap();
3442 drop(seed);
3443
3444 let mut ro = open_database_read_only(&path, "t".to_string()).unwrap();
3445 for stmt in [
3446 "ALTER TABLE notes RENAME TO n2;",
3447 "ALTER TABLE notes RENAME COLUMN body TO b;",
3448 "ALTER TABLE notes ADD COLUMN extra TEXT;",
3449 "ALTER TABLE notes DROP COLUMN body;",
3450 ] {
3451 let err = process_command(stmt, &mut ro).unwrap_err();
3452 assert!(
3453 format!("{err}").contains("read-only"),
3454 "{stmt:?} should surface read-only error, got: {err}"
3455 );
3456 }
3457
3458 let _ = std::fs::remove_file(&path);
3459 let mut wal = path.as_os_str().to_owned();
3460 wal.push("-wal");
3461 let _ = std::fs::remove_file(std::path::PathBuf::from(wal));
3462 }
3463}