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 seed_vector_docs() -> Database {
2327 let mut db = Database::new("tempdb".to_string());
2328 process_command(
2329 "CREATE TABLE docs (id INTEGER PRIMARY KEY, e VECTOR(2));",
2330 &mut db,
2331 )
2332 .expect("create");
2333 process_command("INSERT INTO docs (e) VALUES ([1.0, 0.0]);", &mut db).expect("insert 1");
2334 process_command("INSERT INTO docs (e) VALUES ([0.0, 1.0]);", &mut db).expect("insert 2");
2335 process_command("INSERT INTO docs (e) VALUES ([1.0, 1.0]);", &mut db).expect("insert 3");
2336 db
2337 }
2338
2339 #[test]
2340 fn vec_distance_l2_in_where_filters_correctly() {
2341 let mut db = seed_vector_docs();
2347 let resp = process_command(
2348 "SELECT * FROM docs WHERE vec_distance_l2(e, [1.0, 0.0]) < 1.1;",
2349 &mut db,
2350 )
2351 .expect("select");
2352 assert!(
2353 resp.contains("2 rows returned"),
2354 "expected 2 rows, got: {resp}"
2355 );
2356 }
2357
2358 #[test]
2359 fn vec_distance_cosine_in_where() {
2360 let mut db = seed_vector_docs();
2365 let resp = process_command(
2366 "SELECT * FROM docs WHERE vec_distance_cosine(e, [1.0, 0.0]) < 0.5;",
2367 &mut db,
2368 )
2369 .expect("select");
2370 assert!(
2371 resp.contains("2 rows returned"),
2372 "expected 2 rows, got: {resp}"
2373 );
2374 }
2375
2376 #[test]
2377 fn vec_distance_dot_negated() {
2378 let mut db = seed_vector_docs();
2383 let resp = process_command(
2384 "SELECT * FROM docs WHERE vec_distance_dot(e, [1.0, 0.0]) < 0.0;",
2385 &mut db,
2386 )
2387 .expect("select");
2388 assert!(
2389 resp.contains("2 rows returned"),
2390 "expected 2 rows, got: {resp}"
2391 );
2392 }
2393
2394 #[test]
2395 fn knn_via_order_by_distance_limit() {
2396 let mut db = seed_vector_docs();
2400 let resp = process_command(
2401 "SELECT id FROM docs ORDER BY vec_distance_l2(e, [1.0, 0.0]) ASC LIMIT 2;",
2402 &mut db,
2403 )
2404 .expect("select");
2405 assert!(
2406 resp.contains("2 rows returned"),
2407 "expected 2 rows, got: {resp}"
2408 );
2409 }
2410
2411 #[test]
2412 fn distance_function_dim_mismatch_errors() {
2413 let mut db = seed_vector_docs();
2415 let err = process_command(
2416 "SELECT * FROM docs WHERE vec_distance_l2(e, [1.0, 0.0, 0.0]) < 1.0;",
2417 &mut db,
2418 )
2419 .unwrap_err();
2420 let msg = format!("{err}");
2421 assert!(
2422 msg.to_lowercase().contains("dimension")
2423 && msg.contains("lhs=2")
2424 && msg.contains("rhs=3"),
2425 "expected dim mismatch error, got: {msg}"
2426 );
2427 }
2428
2429 #[test]
2430 fn unknown_function_errors_with_name() {
2431 let mut db = seed_vector_docs();
2435 let err = process_command(
2436 "SELECT * FROM docs WHERE vec_does_not_exist(e, [1.0, 0.0]) < 1.0;",
2437 &mut db,
2438 )
2439 .unwrap_err();
2440 let msg = format!("{err}");
2441 assert!(
2442 msg.contains("vec_does_not_exist"),
2443 "expected error mentioning function name, got: {msg}"
2444 );
2445 }
2446
2447 fn seed_json_table() -> Database {
2452 let mut db = Database::new("tempdb".to_string());
2453 process_command(
2454 "CREATE TABLE docs (id INTEGER PRIMARY KEY, payload JSON);",
2455 &mut db,
2456 )
2457 .expect("create json table");
2458 db
2459 }
2460
2461 #[test]
2462 fn json_column_round_trip_primitive_values() {
2463 let mut db = seed_json_table();
2464 process_command(
2465 r#"INSERT INTO docs (payload) VALUES ('{"name": "alice", "age": 30}');"#,
2466 &mut db,
2467 )
2468 .expect("insert json");
2469 let docs = db.get_table("docs".to_string()).unwrap();
2470 let rowids = docs.rowids();
2471 assert_eq!(rowids.len(), 1);
2472 match docs.get_value("payload", rowids[0]) {
2474 Some(Value::Text(s)) => {
2475 assert!(s.contains("alice"), "expected JSON text to round-trip: {s}");
2476 }
2477 other => panic!("expected Value::Text holding JSON, got {other:?}"),
2478 }
2479 }
2480
2481 #[test]
2482 fn json_insert_rejects_invalid_json() {
2483 let mut db = seed_json_table();
2484 let err = process_command(
2485 "INSERT INTO docs (payload) VALUES ('not-valid-json{');",
2486 &mut db,
2487 )
2488 .unwrap_err();
2489 let msg = format!("{err}").to_lowercase();
2490 assert!(
2491 msg.contains("json") && msg.contains("payload"),
2492 "expected JSON validation error mentioning column, got: {msg}"
2493 );
2494 }
2495
2496 #[test]
2497 fn json_extract_object_field() {
2498 let mut db = seed_json_table();
2499 process_command(
2500 r#"INSERT INTO docs (payload) VALUES ('{"name": "alice", "age": 30}');"#,
2501 &mut db,
2502 )
2503 .unwrap();
2504 let resp = process_command(
2507 r#"SELECT id FROM docs WHERE json_extract(payload, '$.name') = 'alice';"#,
2508 &mut db,
2509 )
2510 .expect("select via json_extract");
2511 assert!(resp.contains("1 row returned"), "got: {resp}");
2512
2513 let resp = process_command(
2514 r#"SELECT id FROM docs WHERE json_extract(payload, '$.age') = 30;"#,
2515 &mut db,
2516 )
2517 .expect("select via numeric json_extract");
2518 assert!(resp.contains("1 row returned"), "got: {resp}");
2519 }
2520
2521 #[test]
2522 fn json_extract_array_index_and_nested() {
2523 let mut db = seed_json_table();
2524 process_command(
2525 r#"INSERT INTO docs (payload) VALUES ('{"tags": ["rust", "sql", "vectors"], "meta": {"author": "joao"}}');"#,
2526 &mut db,
2527 )
2528 .unwrap();
2529 let resp = process_command(
2530 r#"SELECT id FROM docs WHERE json_extract(payload, '$.tags[0]') = 'rust';"#,
2531 &mut db,
2532 )
2533 .expect("select via array index");
2534 assert!(resp.contains("1 row returned"), "got: {resp}");
2535
2536 let resp = process_command(
2537 r#"SELECT id FROM docs WHERE json_extract(payload, '$.meta.author') = 'joao';"#,
2538 &mut db,
2539 )
2540 .expect("select via nested object");
2541 assert!(resp.contains("1 row returned"), "got: {resp}");
2542 }
2543
2544 #[test]
2545 fn json_extract_missing_path_returns_null() {
2546 let mut db = seed_json_table();
2547 process_command(
2548 r#"INSERT INTO docs (payload) VALUES ('{"name": "alice"}');"#,
2549 &mut db,
2550 )
2551 .unwrap();
2552 let resp = process_command(
2555 r#"SELECT id FROM docs WHERE json_extract(payload, '$.missing') = 'something';"#,
2556 &mut db,
2557 )
2558 .expect("select with missing path");
2559 assert!(resp.contains("0 rows returned"), "got: {resp}");
2560 }
2561
2562 #[test]
2563 fn json_extract_malformed_path_errors() {
2564 let mut db = seed_json_table();
2565 process_command(
2566 r#"INSERT INTO docs (payload) VALUES ('{"a": 1}');"#,
2567 &mut db,
2568 )
2569 .unwrap();
2570 let err = process_command(
2572 r#"SELECT id FROM docs WHERE json_extract(payload, 'a.b') = 1;"#,
2573 &mut db,
2574 )
2575 .unwrap_err();
2576 assert!(format!("{err}").contains("'$'"));
2577 }
2578
2579 #[test]
2580 fn json_array_length_on_array() {
2581 let mut db = seed_json_table();
2584 process_command(
2585 r#"INSERT INTO docs (payload) VALUES ('{"tags": ["a", "b", "c"]}');"#,
2586 &mut db,
2587 )
2588 .unwrap();
2589 let resp = process_command(
2590 r#"SELECT id FROM docs WHERE json_array_length(payload, '$.tags') = 3;"#,
2591 &mut db,
2592 )
2593 .expect("select via array_length");
2594 assert!(resp.contains("1 row returned"), "got: {resp}");
2595 }
2596
2597 #[test]
2598 fn json_array_length_on_non_array_errors() {
2599 let mut db = seed_json_table();
2600 process_command(
2601 r#"INSERT INTO docs (payload) VALUES ('{"tags": "not-an-array"}');"#,
2602 &mut db,
2603 )
2604 .unwrap();
2605 let err = process_command(
2606 r#"SELECT id FROM docs WHERE json_array_length(payload, '$.tags') = 1;"#,
2607 &mut db,
2608 )
2609 .unwrap_err();
2610 let msg = format!("{err}").to_lowercase();
2611 assert!(
2612 msg.contains("non-array"),
2613 "expected non-array error, got: {msg}"
2614 );
2615 }
2616
2617 #[test]
2618 fn json_type_recognizes_each_kind() {
2619 let mut db = seed_json_table();
2620 process_command(
2621 r#"INSERT INTO docs (payload) VALUES ('{"o": {}, "a": [], "s": "x", "i": 1, "f": 1.5, "t": true, "n": null}');"#,
2622 &mut db,
2623 )
2624 .unwrap();
2625 let cases = &[
2626 ("$.o", "object"),
2627 ("$.a", "array"),
2628 ("$.s", "text"),
2629 ("$.i", "integer"),
2630 ("$.f", "real"),
2631 ("$.t", "true"),
2632 ("$.n", "null"),
2633 ];
2634 for (path, expected_type) in cases {
2635 let sql = format!(
2636 "SELECT id FROM docs WHERE json_type(payload, '{path}') = '{expected_type}';"
2637 );
2638 let resp =
2639 process_command(&sql, &mut db).unwrap_or_else(|e| panic!("path {path}: {e}"));
2640 assert!(
2641 resp.contains("1 row returned"),
2642 "path {path} expected type {expected_type}; got response: {resp}"
2643 );
2644 }
2645 }
2646
2647 #[test]
2648 fn update_on_json_column_revalidates() {
2649 let mut db = seed_json_table();
2650 process_command(
2651 r#"INSERT INTO docs (payload) VALUES ('{"a": 1}');"#,
2652 &mut db,
2653 )
2654 .unwrap();
2655 process_command(
2657 r#"UPDATE docs SET payload = '{"a": 2, "b": 3}' WHERE id = 1;"#,
2658 &mut db,
2659 )
2660 .expect("valid JSON UPDATE");
2661 let err = process_command(
2664 r#"UPDATE docs SET payload = 'not-json{' WHERE id = 1;"#,
2665 &mut db,
2666 )
2667 .unwrap_err();
2668 let msg = format!("{err}").to_lowercase();
2669 assert!(
2670 msg.contains("json") && msg.contains("payload"),
2671 "got: {msg}"
2672 );
2673 }
2674
2675 #[test]
2680 fn default_literal_int_applies_when_column_omitted() {
2681 let mut db = Database::new("t".to_string());
2682 process_command(
2683 "CREATE TABLE t (id INTEGER PRIMARY KEY, n INTEGER DEFAULT 42);",
2684 &mut db,
2685 )
2686 .unwrap();
2687 process_command("INSERT INTO t (id) VALUES (1);", &mut db).unwrap();
2688
2689 let table = db.get_table("t".to_string()).unwrap();
2690 assert_eq!(table.get_value("n", 1), Some(Value::Integer(42)));
2691 }
2692
2693 #[test]
2694 fn default_literal_text_applies_when_column_omitted() {
2695 let mut db = Database::new("t".to_string());
2696 process_command(
2697 "CREATE TABLE users (id INTEGER PRIMARY KEY, status TEXT DEFAULT 'active');",
2698 &mut db,
2699 )
2700 .unwrap();
2701 process_command("INSERT INTO users (id) VALUES (1);", &mut db).unwrap();
2702
2703 let table = db.get_table("users".to_string()).unwrap();
2704 assert_eq!(
2705 table.get_value("status", 1),
2706 Some(Value::Text("active".to_string()))
2707 );
2708 }
2709
2710 #[test]
2711 fn default_literal_real_negative_applies_when_column_omitted() {
2712 let mut db = Database::new("t".to_string());
2714 process_command(
2715 "CREATE TABLE t (id INTEGER PRIMARY KEY, score REAL DEFAULT -1.5);",
2716 &mut db,
2717 )
2718 .unwrap();
2719 process_command("INSERT INTO t (id) VALUES (1);", &mut db).unwrap();
2720
2721 let table = db.get_table("t".to_string()).unwrap();
2722 assert_eq!(table.get_value("score", 1), Some(Value::Real(-1.5)));
2723 }
2724
2725 #[test]
2726 fn default_with_type_mismatch_errors_at_create_time() {
2727 let mut db = Database::new("t".to_string());
2728 let result = process_command(
2729 "CREATE TABLE t (id INTEGER PRIMARY KEY, n INTEGER DEFAULT 'oops');",
2730 &mut db,
2731 );
2732 let err = result.expect_err("text default on INTEGER column should be rejected");
2733 let msg = format!("{err}").to_lowercase();
2734 assert!(msg.contains("default"), "got: {msg}");
2735 }
2736
2737 #[test]
2738 fn default_for_json_column_must_be_valid_json() {
2739 let mut db = Database::new("t".to_string());
2743 let err = process_command(
2744 "CREATE TABLE t (id INTEGER PRIMARY KEY, doc JSON DEFAULT 'not-json{');",
2745 &mut db,
2746 )
2747 .unwrap_err();
2748 assert!(
2749 format!("{err}").to_lowercase().contains("json"),
2750 "got: {err}"
2751 );
2752
2753 process_command(
2755 "CREATE TABLE t2 (id INTEGER PRIMARY KEY, doc JSON DEFAULT '{\"k\":1}');",
2756 &mut db,
2757 )
2758 .expect("valid JSON DEFAULT should be accepted");
2759 }
2760
2761 #[test]
2762 fn default_with_non_literal_expression_errors_at_create_time() {
2763 let mut db = Database::new("t".to_string());
2764 let result = process_command(
2767 "CREATE TABLE t (id INTEGER PRIMARY KEY, ts TEXT DEFAULT CURRENT_TIMESTAMP);",
2768 &mut db,
2769 );
2770 let err = result.expect_err("non-literal DEFAULT should be rejected");
2771 let msg = format!("{err}").to_lowercase();
2772 assert!(
2773 msg.contains("default") && msg.contains("literal"),
2774 "got: {msg}"
2775 );
2776 }
2777
2778 #[test]
2779 fn default_null_is_accepted_at_create_time() {
2780 let mut db = Database::new("t".to_string());
2784 process_command(
2785 "CREATE TABLE t (id INTEGER PRIMARY KEY, note TEXT DEFAULT NULL);",
2786 &mut db,
2787 )
2788 .expect("CREATE TABLE with DEFAULT NULL should be accepted");
2789 let table = db.get_table("t".to_string()).unwrap();
2790 let note = table
2791 .columns
2792 .iter()
2793 .find(|c| c.column_name == "note")
2794 .unwrap();
2795 assert_eq!(note.default, Some(Value::Null));
2796 }
2797
2798 #[test]
2803 fn drop_table_basic() {
2804 let mut db = seed_users_table();
2805 let response = process_command("DROP TABLE users;", &mut db).expect("drop table");
2806 assert!(response.contains("1 table dropped"));
2807 assert!(!db.contains_table("users".to_string()));
2808 }
2809
2810 #[test]
2811 fn drop_table_if_exists_noop_on_missing() {
2812 let mut db = Database::new("t".to_string());
2813 let response =
2814 process_command("DROP TABLE IF EXISTS missing;", &mut db).expect("drop if exists");
2815 assert!(response.contains("0 tables dropped"));
2816 }
2817
2818 #[test]
2819 fn drop_table_missing_errors_without_if_exists() {
2820 let mut db = Database::new("t".to_string());
2821 let err = process_command("DROP TABLE missing;", &mut db).unwrap_err();
2822 assert!(format!("{err}").contains("does not exist"), "got: {err}");
2823 }
2824
2825 #[test]
2826 fn drop_table_reserved_name_errors() {
2827 let mut db = Database::new("t".to_string());
2828 let err = process_command("DROP TABLE sqlrite_master;", &mut db).unwrap_err();
2829 assert!(format!("{err}").contains("reserved"), "got: {err}");
2830 }
2831
2832 #[test]
2833 fn drop_table_multi_target_rejected() {
2834 let mut db = seed_users_table();
2835 process_command("CREATE TABLE other (id INTEGER PRIMARY KEY);", &mut db).unwrap();
2836 let err = process_command("DROP TABLE users, other;", &mut db).unwrap_err();
2839 assert!(format!("{err}").contains("single table"), "got: {err}");
2840 }
2841
2842 #[test]
2843 fn drop_table_cascades_indexes_in_memory() {
2844 let mut db = seed_users_table();
2845 process_command("CREATE INDEX users_age_idx ON users (age);", &mut db).unwrap();
2846 let users = db.get_table("users".to_string()).unwrap();
2848 assert!(
2849 users
2850 .secondary_indexes
2851 .iter()
2852 .any(|i| i.name == "users_age_idx")
2853 );
2854
2855 process_command("DROP TABLE users;", &mut db).unwrap();
2856
2857 for table in db.tables.values() {
2859 assert!(
2860 !table
2861 .secondary_indexes
2862 .iter()
2863 .any(|i| i.name.contains("users")),
2864 "dropped table's indexes should not survive on any other table"
2865 );
2866 }
2867 }
2868
2869 #[test]
2870 fn drop_index_explicit_basic() {
2871 let mut db = seed_users_table();
2872 process_command("CREATE INDEX users_age_idx ON users (age);", &mut db).unwrap();
2873 let response = process_command("DROP INDEX users_age_idx;", &mut db).expect("drop index");
2874 assert!(response.contains("1 index dropped"));
2875
2876 let users = db.get_table("users".to_string()).unwrap();
2877 assert!(users.index_by_name("users_age_idx").is_none());
2878 }
2879
2880 #[test]
2881 fn drop_index_refuses_auto_index() {
2882 let mut db = seed_users_table();
2883 let err = process_command("DROP INDEX sqlrite_autoindex_users_id;", &mut db).unwrap_err();
2886 assert!(format!("{err}").contains("auto-created"), "got: {err}");
2887 }
2888
2889 #[test]
2890 fn drop_index_if_exists_noop_on_missing() {
2891 let mut db = Database::new("t".to_string());
2892 let response =
2893 process_command("DROP INDEX IF EXISTS nope;", &mut db).expect("drop index if exists");
2894 assert!(response.contains("0 indexes dropped"));
2895 }
2896
2897 #[test]
2898 fn drop_index_missing_errors_without_if_exists() {
2899 let mut db = Database::new("t".to_string());
2900 let err = process_command("DROP INDEX nope;", &mut db).unwrap_err();
2901 assert!(format!("{err}").contains("does not exist"), "got: {err}");
2902 }
2903
2904 #[test]
2905 fn drop_statements_rejected_on_readonly_db() {
2906 use crate::sql::pager::{open_database_read_only, save_database};
2907
2908 let mut seed = Database::new("t".to_string());
2909 process_command(
2910 "CREATE TABLE notes (id INTEGER PRIMARY KEY, body TEXT);",
2911 &mut seed,
2912 )
2913 .unwrap();
2914 process_command("CREATE INDEX notes_body ON notes (body);", &mut seed).unwrap();
2915 let path = {
2916 let mut p = std::env::temp_dir();
2917 let pid = std::process::id();
2918 let nanos = std::time::SystemTime::now()
2919 .duration_since(std::time::UNIX_EPOCH)
2920 .map(|d| d.as_nanos())
2921 .unwrap_or(0);
2922 p.push(format!("sqlrite-drop-ro-{pid}-{nanos}.sqlrite"));
2923 p
2924 };
2925 save_database(&mut seed, &path).unwrap();
2926 drop(seed);
2927
2928 let mut ro = open_database_read_only(&path, "t".to_string()).unwrap();
2929 for stmt in ["DROP TABLE notes;", "DROP INDEX notes_body;"] {
2930 let err = process_command(stmt, &mut ro).unwrap_err();
2931 assert!(
2932 format!("{err}").contains("read-only"),
2933 "{stmt:?} should surface read-only error, got: {err}"
2934 );
2935 }
2936
2937 let _ = std::fs::remove_file(&path);
2938 let mut wal = path.as_os_str().to_owned();
2939 wal.push("-wal");
2940 let _ = std::fs::remove_file(std::path::PathBuf::from(wal));
2941 }
2942
2943 #[test]
2948 fn alter_rename_table_basic() {
2949 let mut db = seed_users_table();
2950 process_command("ALTER TABLE users RENAME TO members;", &mut db).expect("rename table");
2951 assert!(!db.contains_table("users".to_string()));
2952 assert!(db.contains_table("members".to_string()));
2953 let response = process_command("SELECT * FROM members;", &mut db).expect("select");
2955 assert!(response.contains("3 rows returned"));
2956 }
2957
2958 #[test]
2959 fn alter_rename_table_renames_auto_indexes() {
2960 let mut db = Database::new("t".to_string());
2963 process_command(
2964 "CREATE TABLE accounts (id INTEGER PRIMARY KEY, email TEXT UNIQUE);",
2965 &mut db,
2966 )
2967 .unwrap();
2968 {
2969 let accounts = db.get_table("accounts".to_string()).unwrap();
2970 assert!(
2971 accounts
2972 .index_by_name("sqlrite_autoindex_accounts_id")
2973 .is_some()
2974 );
2975 assert!(
2976 accounts
2977 .index_by_name("sqlrite_autoindex_accounts_email")
2978 .is_some()
2979 );
2980 }
2981 process_command("ALTER TABLE accounts RENAME TO members;", &mut db).expect("rename");
2982 let members = db.get_table("members".to_string()).unwrap();
2983 assert!(
2984 members
2985 .index_by_name("sqlrite_autoindex_members_id")
2986 .is_some(),
2987 "PK auto-index should be renamed to match new table"
2988 );
2989 assert!(
2990 members
2991 .index_by_name("sqlrite_autoindex_members_email")
2992 .is_some()
2993 );
2994 assert!(
2996 members
2997 .index_by_name("sqlrite_autoindex_accounts_id")
2998 .is_none()
2999 );
3000 for idx in &members.secondary_indexes {
3002 assert_eq!(idx.table_name, "members");
3003 }
3004 }
3005
3006 #[test]
3007 fn alter_rename_table_to_existing_errors() {
3008 let mut db = seed_users_table();
3009 process_command("CREATE TABLE other (id INTEGER PRIMARY KEY);", &mut db).unwrap();
3010 let err = process_command("ALTER TABLE users RENAME TO other;", &mut db).unwrap_err();
3011 assert!(format!("{err}").contains("already exists"), "got: {err}");
3012 assert!(db.contains_table("users".to_string()));
3014 assert!(db.contains_table("other".to_string()));
3015 }
3016
3017 #[test]
3018 fn alter_rename_table_to_reserved_name_errors() {
3019 let mut db = seed_users_table();
3020 let err =
3021 process_command("ALTER TABLE users RENAME TO sqlrite_master;", &mut db).unwrap_err();
3022 assert!(format!("{err}").contains("reserved"), "got: {err}");
3023 }
3024
3025 #[test]
3026 fn alter_rename_column_basic() {
3027 let mut db = seed_users_table();
3028 process_command(
3029 "ALTER TABLE users RENAME COLUMN name TO full_name;",
3030 &mut db,
3031 )
3032 .expect("rename column");
3033
3034 let users = db.get_table("users".to_string()).unwrap();
3035 assert!(users.contains_column("full_name".to_string()));
3036 assert!(!users.contains_column("name".to_string()));
3037
3038 let bob_rowid = users
3041 .rowids()
3042 .into_iter()
3043 .find(|r| users.get_value("full_name", *r) == Some(Value::Text("bob".to_string())))
3044 .expect("bob row should be findable under the new column name");
3045 assert_eq!(
3046 users.get_value("full_name", bob_rowid),
3047 Some(Value::Text("bob".to_string()))
3048 );
3049 }
3050
3051 #[test]
3052 fn alter_rename_column_collision_errors() {
3053 let mut db = seed_users_table();
3054 let err =
3055 process_command("ALTER TABLE users RENAME COLUMN name TO age;", &mut db).unwrap_err();
3056 assert!(format!("{err}").contains("already exists"), "got: {err}");
3057 }
3058
3059 #[test]
3060 fn alter_rename_column_updates_indexes() {
3061 let mut db = Database::new("t".to_string());
3063 process_command(
3064 "CREATE TABLE accounts (id INTEGER PRIMARY KEY, email TEXT UNIQUE);",
3065 &mut db,
3066 )
3067 .unwrap();
3068 process_command(
3069 "ALTER TABLE accounts RENAME COLUMN email TO contact;",
3070 &mut db,
3071 )
3072 .unwrap();
3073 let accounts = db.get_table("accounts".to_string()).unwrap();
3074 assert!(
3075 accounts
3076 .index_by_name("sqlrite_autoindex_accounts_contact")
3077 .is_some()
3078 );
3079 assert!(
3080 accounts
3081 .index_by_name("sqlrite_autoindex_accounts_email")
3082 .is_none()
3083 );
3084 }
3085
3086 #[test]
3087 fn alter_add_column_basic() {
3088 let mut db = seed_users_table();
3089 process_command("ALTER TABLE users ADD COLUMN nickname TEXT;", &mut db)
3090 .expect("add column");
3091 let users = db.get_table("users".to_string()).unwrap();
3092 assert!(users.contains_column("nickname".to_string()));
3093 let any_rowid = *users.rowids().first().expect("seed has rows");
3095 assert_eq!(users.get_value("nickname", any_rowid), None);
3096
3097 process_command(
3099 "INSERT INTO users (name, age, nickname) VALUES ('dan', 22, 'd');",
3100 &mut db,
3101 )
3102 .expect("insert with new col");
3103 let users = db.get_table("users".to_string()).unwrap();
3104 let dan_rowid = users
3105 .rowids()
3106 .into_iter()
3107 .find(|r| users.get_value("name", *r) == Some(Value::Text("dan".to_string())))
3108 .unwrap();
3109 assert_eq!(
3110 users.get_value("nickname", dan_rowid),
3111 Some(Value::Text("d".to_string()))
3112 );
3113 }
3114
3115 #[test]
3116 fn alter_add_column_with_default_backfills_existing_rows() {
3117 let mut db = seed_users_table();
3118 process_command(
3119 "ALTER TABLE users ADD COLUMN status TEXT DEFAULT 'active';",
3120 &mut db,
3121 )
3122 .expect("add column with default");
3123 let users = db.get_table("users".to_string()).unwrap();
3124 for rowid in users.rowids() {
3125 assert_eq!(
3126 users.get_value("status", rowid),
3127 Some(Value::Text("active".to_string())),
3128 "rowid {rowid} should have been backfilled with the default"
3129 );
3130 }
3131 }
3132
3133 #[test]
3134 fn alter_add_column_not_null_with_default_works_on_nonempty_table() {
3135 let mut db = seed_users_table();
3136 process_command(
3137 "ALTER TABLE users ADD COLUMN score INTEGER NOT NULL DEFAULT 0;",
3138 &mut db,
3139 )
3140 .expect("NOT NULL ADD with DEFAULT should succeed even with existing rows");
3141 let users = db.get_table("users".to_string()).unwrap();
3142 for rowid in users.rowids() {
3143 assert_eq!(users.get_value("score", rowid), Some(Value::Integer(0)));
3144 }
3145 }
3146
3147 #[test]
3148 fn alter_add_column_not_null_without_default_errors_on_nonempty_table() {
3149 let mut db = seed_users_table();
3150 let err = process_command(
3151 "ALTER TABLE users ADD COLUMN score INTEGER NOT NULL;",
3152 &mut db,
3153 )
3154 .unwrap_err();
3155 let msg = format!("{err}").to_lowercase();
3156 assert!(
3157 msg.contains("not null") && msg.contains("default"),
3158 "got: {msg}"
3159 );
3160 }
3161
3162 #[test]
3163 fn alter_add_column_pk_rejected() {
3164 let mut db = seed_users_table();
3165 let err = process_command(
3166 "ALTER TABLE users ADD COLUMN extra INTEGER PRIMARY KEY;",
3167 &mut db,
3168 )
3169 .unwrap_err();
3170 assert!(
3171 format!("{err}").to_lowercase().contains("primary key"),
3172 "got: {err}"
3173 );
3174 }
3175
3176 #[test]
3177 fn alter_add_column_unique_rejected() {
3178 let mut db = seed_users_table();
3179 let err = process_command(
3180 "ALTER TABLE users ADD COLUMN extra INTEGER UNIQUE;",
3181 &mut db,
3182 )
3183 .unwrap_err();
3184 assert!(
3185 format!("{err}").to_lowercase().contains("unique"),
3186 "got: {err}"
3187 );
3188 }
3189
3190 #[test]
3191 fn alter_add_column_existing_name_errors() {
3192 let mut db = seed_users_table();
3193 let err =
3194 process_command("ALTER TABLE users ADD COLUMN age INTEGER;", &mut db).unwrap_err();
3195 assert!(format!("{err}").contains("already exists"), "got: {err}");
3196 }
3197
3198 #[test]
3204 fn alter_drop_column_basic() {
3205 let mut db = seed_users_table();
3206 process_command("ALTER TABLE users DROP COLUMN age;", &mut db).expect("drop column");
3207 let users = db.get_table("users".to_string()).unwrap();
3208 assert!(!users.contains_column("age".to_string()));
3209 assert!(users.contains_column("name".to_string()));
3211 assert_eq!(users.rowids().len(), 3);
3212 }
3213
3214 #[test]
3215 fn alter_drop_column_drops_dependent_indexes() {
3216 let mut db = seed_users_table();
3217 process_command("CREATE INDEX users_age_idx ON users (age);", &mut db).unwrap();
3218 process_command("ALTER TABLE users DROP COLUMN age;", &mut db).unwrap();
3219 let users = db.get_table("users".to_string()).unwrap();
3220 assert!(users.index_by_name("users_age_idx").is_none());
3221 }
3222
3223 #[test]
3224 fn alter_drop_column_pk_errors() {
3225 let mut db = seed_users_table();
3226 let err = process_command("ALTER TABLE users DROP COLUMN id;", &mut db).unwrap_err();
3227 assert!(
3228 format!("{err}").to_lowercase().contains("primary key"),
3229 "got: {err}"
3230 );
3231 }
3232
3233 #[test]
3234 fn alter_drop_column_only_column_errors() {
3235 let mut db = Database::new("t".to_string());
3236 process_command("CREATE TABLE solo (only_col TEXT);", &mut db).unwrap();
3237 let err = process_command("ALTER TABLE solo DROP COLUMN only_col;", &mut db).unwrap_err();
3238 assert!(
3239 format!("{err}").to_lowercase().contains("only column"),
3240 "got: {err}"
3241 );
3242 }
3243
3244 #[test]
3245 fn alter_unknown_table_errors_without_if_exists() {
3246 let mut db = Database::new("t".to_string());
3247 let err = process_command("ALTER TABLE missing RENAME TO other;", &mut db).unwrap_err();
3248 assert!(format!("{err}").contains("does not exist"), "got: {err}");
3249 }
3250
3251 #[test]
3252 fn alter_unknown_table_if_exists_noop() {
3253 let mut db = Database::new("t".to_string());
3254 let response = process_command("ALTER TABLE IF EXISTS missing RENAME TO other;", &mut db)
3255 .expect("IF EXISTS makes missing-table ALTER a no-op");
3256 assert!(response.contains("no-op"));
3257 }
3258
3259 #[test]
3260 fn drop_table_inside_transaction_rolls_back() {
3261 let mut db = seed_users_table();
3265 process_command("CREATE INDEX users_age_idx ON users (age);", &mut db).unwrap();
3266 process_command("BEGIN;", &mut db).unwrap();
3267 process_command("DROP TABLE users;", &mut db).unwrap();
3268 assert!(!db.contains_table("users".to_string()));
3269 process_command("ROLLBACK;", &mut db).unwrap();
3270 assert!(db.contains_table("users".to_string()));
3271 let users = db.get_table("users".to_string()).unwrap();
3272 assert_eq!(users.rowids().len(), 3);
3273 assert!(users.index_by_name("users_age_idx").is_some());
3274 }
3275
3276 #[test]
3277 fn alter_inside_transaction_rolls_back() {
3278 let mut db = seed_users_table();
3279 process_command("BEGIN;", &mut db).unwrap();
3280 process_command(
3281 "ALTER TABLE users ADD COLUMN status TEXT DEFAULT 'active';",
3282 &mut db,
3283 )
3284 .unwrap();
3285 assert!(
3287 db.get_table("users".to_string())
3288 .unwrap()
3289 .contains_column("status".to_string())
3290 );
3291 process_command("ROLLBACK;", &mut db).unwrap();
3292 assert!(
3294 !db.get_table("users".to_string())
3295 .unwrap()
3296 .contains_column("status".to_string())
3297 );
3298 }
3299
3300 #[test]
3301 fn alter_rejected_on_readonly_db() {
3302 use crate::sql::pager::{open_database_read_only, save_database};
3303
3304 let mut seed = Database::new("t".to_string());
3305 process_command(
3306 "CREATE TABLE notes (id INTEGER PRIMARY KEY, body TEXT);",
3307 &mut seed,
3308 )
3309 .unwrap();
3310 let path = {
3311 let mut p = std::env::temp_dir();
3312 let pid = std::process::id();
3313 let nanos = std::time::SystemTime::now()
3314 .duration_since(std::time::UNIX_EPOCH)
3315 .map(|d| d.as_nanos())
3316 .unwrap_or(0);
3317 p.push(format!("sqlrite-alter-ro-{pid}-{nanos}.sqlrite"));
3318 p
3319 };
3320 save_database(&mut seed, &path).unwrap();
3321 drop(seed);
3322
3323 let mut ro = open_database_read_only(&path, "t".to_string()).unwrap();
3324 for stmt in [
3325 "ALTER TABLE notes RENAME TO n2;",
3326 "ALTER TABLE notes RENAME COLUMN body TO b;",
3327 "ALTER TABLE notes ADD COLUMN extra TEXT;",
3328 "ALTER TABLE notes DROP COLUMN body;",
3329 ] {
3330 let err = process_command(stmt, &mut ro).unwrap_err();
3331 assert!(
3332 format!("{err}").contains("read-only"),
3333 "{stmt:?} should surface read-only error, got: {err}"
3334 );
3335 }
3336
3337 let _ = std::fs::remove_file(&path);
3338 let mut wal = path.as_os_str().to_owned();
3339 wal.push("-wal");
3340 let _ = std::fs::remove_file(std::path::PathBuf::from(wal));
3341 }
3342}