Skip to main content

sqlrite/sql/
mod.rs

1pub mod db;
2pub mod executor;
3pub mod pager;
4pub mod parser;
5// pub mod tokenizer;
6
7use parser::create::CreateQuery;
8use parser::insert::InsertQuery;
9use parser::select::SelectQuery;
10
11use sqlparser::ast::Statement;
12use sqlparser::dialect::SQLiteDialect;
13use sqlparser::parser::{Parser, ParserError};
14
15use crate::error::{Result, SQLRiteError};
16use crate::sql::db::database::Database;
17use crate::sql::db::table::Table;
18
19#[derive(Debug, PartialEq)]
20pub enum SQLCommand {
21    Insert(String),
22    Delete(String),
23    Update(String),
24    CreateTable(String),
25    Select(String),
26    Unknown(String),
27}
28
29impl SQLCommand {
30    pub fn new(command: String) -> SQLCommand {
31        let v = command.split(" ").collect::<Vec<&str>>();
32        match v[0] {
33            "insert" => SQLCommand::Insert(command),
34            "update" => SQLCommand::Update(command),
35            "delete" => SQLCommand::Delete(command),
36            "create" => SQLCommand::CreateTable(command),
37            "select" => SQLCommand::Select(command),
38            _ => SQLCommand::Unknown(command),
39        }
40    }
41}
42
43/// Performs initial parsing of SQL Statement using sqlparser-rs
44pub fn process_command(query: &str, db: &mut Database) -> Result<String> {
45    let dialect = SQLiteDialect {};
46    let message: String;
47    let mut ast = Parser::parse_sql(&dialect, query).map_err(SQLRiteError::from)?;
48
49    if ast.len() > 1 {
50        return Err(SQLRiteError::SqlError(ParserError::ParserError(format!(
51            "Expected a single query statement, but there are {}",
52            ast.len()
53        ))));
54    }
55
56    // Comment-only or whitespace-only input parses to an empty Vec<Statement>.
57    // Return a benign status rather than panicking on `pop().unwrap()`. Callers
58    // (REPL, Tauri app) treat this as a no-op with no disk write triggered.
59    let Some(query) = ast.pop() else {
60        return Ok("No statement to execute.".to_string());
61    };
62
63    // Transaction boundary statements are routed to Database-level
64    // handlers before we even inspect the rest of the AST. They don't
65    // mutate table data directly, so they short-circuit the
66    // is_write_statement / auto-save path.
67    match &query {
68        Statement::StartTransaction { .. } => {
69            db.begin_transaction()?;
70            return Ok(String::from("BEGIN"));
71        }
72        Statement::Commit { .. } => {
73            if !db.in_transaction() {
74                return Err(SQLRiteError::General(
75                    "cannot COMMIT: no transaction is open".to_string(),
76                ));
77            }
78            // Flush accumulated in-memory changes to disk. If the save
79            // fails we auto-rollback the in-memory state to the
80            // pre-BEGIN snapshot and surface a combined error. Leaving
81            // the transaction open after a failed COMMIT would be
82            // unsafe: auto-save on any subsequent non-transactional
83            // statement would silently publish partial mid-transaction
84            // work. Auto-rollback keeps the disk-plus-memory pair
85            // coherent — the user loses their in-flight work on a disk
86            // error, but that's the only safe outcome.
87            if let Some(path) = db.source_path.clone() {
88                if let Err(save_err) = pager::save_database(db, &path) {
89                    let _ = db.rollback_transaction();
90                    return Err(SQLRiteError::General(format!(
91                        "COMMIT failed — transaction rolled back: {save_err}"
92                    )));
93                }
94            }
95            db.commit_transaction()?;
96            return Ok(String::from("COMMIT"));
97        }
98        Statement::Rollback { .. } => {
99            db.rollback_transaction()?;
100            return Ok(String::from("ROLLBACK"));
101        }
102        _ => {}
103    }
104
105    // Statements that mutate state — trigger auto-save on success. Read-only
106    // SELECTs skip the save entirely to avoid pointless file writes.
107    let is_write_statement = matches!(
108        &query,
109        Statement::CreateTable(_)
110            | Statement::CreateIndex(_)
111            | Statement::Insert(_)
112            | Statement::Update(_)
113            | Statement::Delete(_)
114    );
115
116    // Early-reject mutations on a read-only database before they touch
117    // in-memory state. Phase 4e: without this, a user running INSERT
118    // on a `--readonly` REPL would see the row appear in the printed
119    // table, and then the auto-save would fail — leaving the in-memory
120    // Database visibly diverged from disk.
121    if is_write_statement && db.is_read_only() {
122        return Err(SQLRiteError::General(
123            "cannot execute: database is opened read-only".to_string(),
124        ));
125    }
126
127    // Initialy only implementing some basic SQL Statements
128    match query {
129        Statement::CreateTable(_) => {
130            let create_query = CreateQuery::new(&query);
131            match create_query {
132                Ok(payload) => {
133                    let table_name = payload.table_name.clone();
134                    if table_name == pager::MASTER_TABLE_NAME {
135                        return Err(SQLRiteError::General(format!(
136                            "'{}' is a reserved name used by the internal schema catalog",
137                            pager::MASTER_TABLE_NAME
138                        )));
139                    }
140                    // Checking if table already exists, after parsing CREATE TABLE query
141                    match db.contains_table(table_name.to_string()) {
142                        true => {
143                            return Err(SQLRiteError::Internal(
144                                "Cannot create, table already exists.".to_string(),
145                            ));
146                        }
147                        false => {
148                            let table = Table::new(payload);
149                            let _ = table.print_table_schema();
150                            db.tables.insert(table_name.to_string(), table);
151                            // Iterate over everything.
152                            // for (table_name, _) in &db.tables {
153                            //     println!("{}" , table_name);
154                            // }
155                            message = String::from("CREATE TABLE Statement executed.");
156                        }
157                    }
158                }
159                Err(err) => return Err(err),
160            }
161        }
162        Statement::Insert(_) => {
163            let insert_query = InsertQuery::new(&query);
164            match insert_query {
165                Ok(payload) => {
166                    let table_name = payload.table_name;
167                    let columns = payload.columns;
168                    let values = payload.rows;
169
170                    // println!("table_name = {:?}\n cols = {:?}\n vals = {:?}", table_name, columns, values);
171                    // Checking if Table exists in Database
172                    match db.contains_table(table_name.to_string()) {
173                        true => {
174                            let db_table = db.get_table_mut(table_name.to_string()).unwrap();
175                            // Checking if columns on INSERT query exist on Table
176                            match columns
177                                .iter()
178                                .all(|column| db_table.contains_column(column.to_string()))
179                            {
180                                true => {
181                                    for value in &values {
182                                        // Checking if number of columns in query are the same as number of values
183                                        if columns.len() != value.len() {
184                                            return Err(SQLRiteError::Internal(format!(
185                                                "{} values for {} columns",
186                                                value.len(),
187                                                columns.len()
188                                            )));
189                                        }
190                                        db_table
191                                            .validate_unique_constraint(&columns, value)
192                                            .map_err(|err| {
193                                                SQLRiteError::Internal(format!(
194                                                    "Unique key constraint violation: {err}"
195                                                ))
196                                            })?;
197                                        db_table.insert_row(&columns, value)?;
198                                    }
199                                }
200                                false => {
201                                    return Err(SQLRiteError::Internal(
202                                        "Cannot insert, some of the columns do not exist"
203                                            .to_string(),
204                                    ));
205                                }
206                            }
207                            db_table.print_table_data();
208                        }
209                        false => {
210                            return Err(SQLRiteError::Internal("Table doesn't exist".to_string()));
211                        }
212                    }
213                }
214                Err(err) => return Err(err),
215            }
216
217            message = String::from("INSERT Statement executed.")
218        }
219        Statement::Query(_) => {
220            let select_query = SelectQuery::new(&query)?;
221            let (rendered, rows) = executor::execute_select(select_query, db)?;
222            // Print the result table above the status message so the REPL shows both.
223            print!("{rendered}");
224            message = format!(
225                "SELECT Statement executed. {rows} row{s} returned.",
226                s = if rows == 1 { "" } else { "s" }
227            );
228        }
229        Statement::Delete(_) => {
230            let rows = executor::execute_delete(&query, db)?;
231            message = format!(
232                "DELETE Statement executed. {rows} row{s} deleted.",
233                s = if rows == 1 { "" } else { "s" }
234            );
235        }
236        Statement::Update(_) => {
237            let rows = executor::execute_update(&query, db)?;
238            message = format!(
239                "UPDATE Statement executed. {rows} row{s} updated.",
240                s = if rows == 1 { "" } else { "s" }
241            );
242        }
243        Statement::CreateIndex(_) => {
244            let name = executor::execute_create_index(&query, db)?;
245            message = format!("CREATE INDEX '{name}' executed.");
246        }
247        _ => {
248            return Err(SQLRiteError::NotImplemented(
249                "SQL Statement not supported yet.".to_string(),
250            ));
251        }
252    };
253
254    // Auto-save: if the database is backed by a file AND no explicit
255    // transaction is open AND the statement changed state, flush to
256    // disk before returning. Inside a `BEGIN … COMMIT` block the
257    // mutations accumulate in memory (protected by the ROLLBACK
258    // snapshot) and land on disk in one shot when COMMIT runs.
259    //
260    // A failed save surfaces as an error — the in-memory state already
261    // mutated, so the caller should know disk is out of sync. The
262    // Pager held on `db` diffs against its last-committed snapshot,
263    // so only pages whose bytes actually changed are written.
264    if is_write_statement && db.source_path.is_some() && !db.in_transaction() {
265        let path = db.source_path.clone().unwrap();
266        pager::save_database(db, &path)?;
267    }
268
269    Ok(message)
270}
271
272#[cfg(test)]
273mod tests {
274    use super::*;
275    use crate::sql::db::table::Value;
276
277    /// Builds a `users(id INTEGER PK, name TEXT, age INTEGER)` table populated
278    /// with three rows, for use in executor-level tests.
279    fn seed_users_table() -> Database {
280        let mut db = Database::new("tempdb".to_string());
281        process_command(
282            "CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT NOT NULL, age INTEGER);",
283            &mut db,
284        )
285        .expect("create table");
286        process_command(
287            "INSERT INTO users (name, age) VALUES ('alice', 30);",
288            &mut db,
289        )
290        .expect("insert alice");
291        process_command("INSERT INTO users (name, age) VALUES ('bob', 25);", &mut db)
292            .expect("insert bob");
293        process_command(
294            "INSERT INTO users (name, age) VALUES ('carol', 40);",
295            &mut db,
296        )
297        .expect("insert carol");
298        db
299    }
300
301    #[test]
302    fn process_command_select_all_test() {
303        let mut db = seed_users_table();
304        let response = process_command("SELECT * FROM users;", &mut db).expect("select");
305        assert!(response.contains("3 rows returned"));
306    }
307
308    #[test]
309    fn process_command_select_where_test() {
310        let mut db = seed_users_table();
311        let response =
312            process_command("SELECT name FROM users WHERE age > 25;", &mut db).expect("select");
313        assert!(response.contains("2 rows returned"));
314    }
315
316    #[test]
317    fn process_command_select_eq_string_test() {
318        let mut db = seed_users_table();
319        let response =
320            process_command("SELECT name FROM users WHERE name = 'bob';", &mut db).expect("select");
321        assert!(response.contains("1 row returned"));
322    }
323
324    #[test]
325    fn process_command_select_limit_test() {
326        let mut db = seed_users_table();
327        let response = process_command("SELECT * FROM users ORDER BY age ASC LIMIT 2;", &mut db)
328            .expect("select");
329        assert!(response.contains("2 rows returned"));
330    }
331
332    #[test]
333    fn process_command_select_unknown_table_test() {
334        let mut db = Database::new("tempdb".to_string());
335        let result = process_command("SELECT * FROM nope;", &mut db);
336        assert!(result.is_err());
337    }
338
339    #[test]
340    fn process_command_select_unknown_column_test() {
341        let mut db = seed_users_table();
342        let result = process_command("SELECT height FROM users;", &mut db);
343        assert!(result.is_err());
344    }
345
346    #[test]
347    fn process_command_insert_test() {
348        // Creating temporary database
349        let mut db = Database::new("tempdb".to_string());
350
351        // Creating temporary table for testing purposes
352        let query_statement = "CREATE TABLE users (
353            id INTEGER PRIMARY KEY,
354            name TEXT
355        );";
356        let dialect = SQLiteDialect {};
357        let mut ast = Parser::parse_sql(&dialect, query_statement).unwrap();
358        if ast.len() > 1 {
359            panic!("Expected a single query statement, but there are more then 1.")
360        }
361        let query = ast.pop().unwrap();
362        let create_query = CreateQuery::new(&query).unwrap();
363
364        // Inserting table into database
365        db.tables.insert(
366            create_query.table_name.to_string(),
367            Table::new(create_query),
368        );
369
370        // Inserting data into table
371        let insert_query = String::from("INSERT INTO users (name) Values ('josh');");
372        match process_command(&insert_query, &mut db) {
373            Ok(response) => assert_eq!(response, "INSERT Statement executed."),
374            Err(err) => {
375                eprintln!("Error: {}", err);
376                assert!(false)
377            }
378        };
379    }
380
381    #[test]
382    fn process_command_insert_no_pk_test() {
383        // Creating temporary database
384        let mut db = Database::new("tempdb".to_string());
385
386        // Creating temporary table for testing purposes
387        let query_statement = "CREATE TABLE users (
388            name TEXT
389        );";
390        let dialect = SQLiteDialect {};
391        let mut ast = Parser::parse_sql(&dialect, query_statement).unwrap();
392        if ast.len() > 1 {
393            panic!("Expected a single query statement, but there are more then 1.")
394        }
395        let query = ast.pop().unwrap();
396        let create_query = CreateQuery::new(&query).unwrap();
397
398        // Inserting table into database
399        db.tables.insert(
400            create_query.table_name.to_string(),
401            Table::new(create_query),
402        );
403
404        // Inserting data into table
405        let insert_query = String::from("INSERT INTO users (name) Values ('josh');");
406        match process_command(&insert_query, &mut db) {
407            Ok(response) => assert_eq!(response, "INSERT Statement executed."),
408            Err(err) => {
409                eprintln!("Error: {}", err);
410                assert!(false)
411            }
412        };
413    }
414
415    #[test]
416    fn process_command_delete_where_test() {
417        let mut db = seed_users_table();
418        let response =
419            process_command("DELETE FROM users WHERE name = 'bob';", &mut db).expect("delete");
420        assert!(response.contains("1 row deleted"));
421
422        let remaining = process_command("SELECT * FROM users;", &mut db).expect("select");
423        assert!(remaining.contains("2 rows returned"));
424    }
425
426    #[test]
427    fn process_command_delete_all_test() {
428        let mut db = seed_users_table();
429        let response = process_command("DELETE FROM users;", &mut db).expect("delete");
430        assert!(response.contains("3 rows deleted"));
431    }
432
433    #[test]
434    fn process_command_update_where_test() {
435        use crate::sql::db::table::Value;
436
437        let mut db = seed_users_table();
438        let response = process_command("UPDATE users SET age = 99 WHERE name = 'bob';", &mut db)
439            .expect("update");
440        assert!(response.contains("1 row updated"));
441
442        // Confirm the cell was actually rewritten.
443        let users = db.get_table("users".to_string()).unwrap();
444        let bob_rowid = users
445            .rowids()
446            .into_iter()
447            .find(|r| users.get_value("name", *r) == Some(Value::Text("bob".to_string())))
448            .expect("bob row must exist");
449        assert_eq!(users.get_value("age", bob_rowid), Some(Value::Integer(99)));
450    }
451
452    #[test]
453    fn process_command_update_unique_violation_test() {
454        let mut db = seed_users_table();
455        // `name` is not UNIQUE in the seed — reinforce with an explicit unique column.
456        process_command(
457            "CREATE TABLE tags (id INTEGER PRIMARY KEY, label TEXT UNIQUE);",
458            &mut db,
459        )
460        .unwrap();
461        process_command("INSERT INTO tags (label) VALUES ('a');", &mut db).unwrap();
462        process_command("INSERT INTO tags (label) VALUES ('b');", &mut db).unwrap();
463
464        let result = process_command("UPDATE tags SET label = 'a' WHERE label = 'b';", &mut db);
465        assert!(result.is_err(), "expected UNIQUE violation, got {result:?}");
466    }
467
468    #[test]
469    fn process_command_insert_type_mismatch_returns_error_test() {
470        // Previously this panicked in parse::<i32>().unwrap(); now it should return an error cleanly.
471        let mut db = Database::new("tempdb".to_string());
472        process_command(
473            "CREATE TABLE items (id INTEGER PRIMARY KEY, qty INTEGER);",
474            &mut db,
475        )
476        .unwrap();
477        let result = process_command("INSERT INTO items (qty) VALUES ('not a number');", &mut db);
478        assert!(result.is_err(), "expected error, got {result:?}");
479    }
480
481    #[test]
482    fn process_command_insert_missing_integer_returns_error_test() {
483        // Non-PK INTEGER without a value should error (not panic on "Null".parse()).
484        let mut db = Database::new("tempdb".to_string());
485        process_command(
486            "CREATE TABLE items (id INTEGER PRIMARY KEY, qty INTEGER);",
487            &mut db,
488        )
489        .unwrap();
490        let result = process_command("INSERT INTO items (id) VALUES (1);", &mut db);
491        assert!(result.is_err(), "expected error, got {result:?}");
492    }
493
494    #[test]
495    fn process_command_update_arith_test() {
496        use crate::sql::db::table::Value;
497
498        let mut db = seed_users_table();
499        process_command("UPDATE users SET age = age + 1;", &mut db).expect("update +1");
500
501        let users = db.get_table("users".to_string()).unwrap();
502        let mut ages: Vec<i64> = users
503            .rowids()
504            .into_iter()
505            .filter_map(|r| match users.get_value("age", r) {
506                Some(Value::Integer(n)) => Some(n),
507                _ => None,
508            })
509            .collect();
510        ages.sort();
511        assert_eq!(ages, vec![26, 31, 41]); // 25+1, 30+1, 40+1
512    }
513
514    #[test]
515    fn process_command_select_arithmetic_where_test() {
516        let mut db = seed_users_table();
517        // age * 2 > 55  →  only ages > 27.5  →  alice(30) + carol(40)
518        let response =
519            process_command("SELECT name FROM users WHERE age * 2 > 55;", &mut db).expect("select");
520        assert!(response.contains("2 rows returned"));
521    }
522
523    #[test]
524    fn process_command_divide_by_zero_test() {
525        let mut db = seed_users_table();
526        let result = process_command("SELECT age / 0 FROM users;", &mut db);
527        // Projection only supports bare columns, so this errors earlier; still shouldn't panic.
528        assert!(result.is_err());
529    }
530
531    #[test]
532    fn process_command_unsupported_statement_test() {
533        let mut db = Database::new("tempdb".to_string());
534        // Nothing in Phase 1 handles DROP.
535        let result = process_command("DROP TABLE users;", &mut db);
536        assert!(result.is_err());
537    }
538
539    #[test]
540    fn empty_input_is_a_noop_not_a_panic() {
541        // Regression for: desktop app pre-fills the textarea with a
542        // comment-only placeholder, and hitting Run used to panic because
543        // sqlparser produced zero statements and pop().unwrap() exploded.
544        let mut db = Database::new("t".to_string());
545        for input in ["", "   ", "-- just a comment", "-- comment\n-- another"] {
546            let result = process_command(input, &mut db);
547            assert!(result.is_ok(), "input {input:?} should not error");
548            let msg = result.unwrap();
549            assert!(msg.contains("No statement"), "got: {msg:?}");
550        }
551    }
552
553    #[test]
554    fn create_index_adds_explicit_index() {
555        let mut db = seed_users_table();
556        let response = process_command("CREATE INDEX users_age_idx ON users (age);", &mut db)
557            .expect("create index");
558        assert!(response.contains("users_age_idx"));
559
560        // The index should now be attached to the users table.
561        let users = db.get_table("users".to_string()).unwrap();
562        let idx = users
563            .index_by_name("users_age_idx")
564            .expect("index should exist after CREATE INDEX");
565        assert_eq!(idx.column_name, "age");
566        assert!(!idx.is_unique);
567    }
568
569    #[test]
570    fn create_unique_index_rejects_duplicate_existing_values() {
571        let mut db = seed_users_table();
572        // `name` is already UNIQUE (auto-indexed); insert a duplicate-age row
573        // first so CREATE UNIQUE INDEX on age catches the conflict.
574        process_command("INSERT INTO users (name, age) VALUES ('dan', 30);", &mut db).unwrap();
575        let result = process_command(
576            "CREATE UNIQUE INDEX users_age_unique ON users (age);",
577            &mut db,
578        );
579        assert!(
580            result.is_err(),
581            "expected unique-index failure, got {result:?}"
582        );
583    }
584
585    #[test]
586    fn where_eq_on_indexed_column_uses_index_probe() {
587        // Build a table big enough that a full scan would be expensive,
588        // then rely on the index-probe fast path. This test verifies
589        // correctness (right rows returned); the perf win is implicit.
590        let mut db = Database::new("t".to_string());
591        process_command(
592            "CREATE TABLE big (id INTEGER PRIMARY KEY, tag TEXT);",
593            &mut db,
594        )
595        .unwrap();
596        process_command("CREATE INDEX big_tag_idx ON big (tag);", &mut db).unwrap();
597        for i in 1..=100 {
598            let tag = if i % 3 == 0 { "hot" } else { "cold" };
599            process_command(&format!("INSERT INTO big (tag) VALUES ('{tag}');"), &mut db).unwrap();
600        }
601        let response =
602            process_command("SELECT id FROM big WHERE tag = 'hot';", &mut db).expect("select");
603        // 1..=100 has 33 multiples of 3.
604        assert!(
605            response.contains("33 rows returned"),
606            "response was {response:?}"
607        );
608    }
609
610    #[test]
611    fn where_eq_on_indexed_column_inside_parens_uses_index_probe() {
612        let mut db = seed_users_table();
613        let response = process_command("SELECT name FROM users WHERE (name = 'bob');", &mut db)
614            .expect("select");
615        assert!(response.contains("1 row returned"));
616    }
617
618    #[test]
619    fn where_eq_literal_first_side_uses_index_probe() {
620        let mut db = seed_users_table();
621        // `'bob' = name` should hit the same path as `name = 'bob'`.
622        let response =
623            process_command("SELECT name FROM users WHERE 'bob' = name;", &mut db).expect("select");
624        assert!(response.contains("1 row returned"));
625    }
626
627    #[test]
628    fn non_equality_where_still_falls_back_to_full_scan() {
629        // Sanity: range predicates bypass the optimizer and the full-scan
630        // path still returns correct results.
631        let mut db = seed_users_table();
632        let response =
633            process_command("SELECT name FROM users WHERE age > 28;", &mut db).expect("select");
634        assert!(response.contains("2 rows returned"));
635    }
636
637    // -------------------------------------------------------------------
638    // Phase 4f — Transactions (BEGIN / COMMIT / ROLLBACK)
639    // -------------------------------------------------------------------
640
641    #[test]
642    fn rollback_restores_pre_begin_in_memory_state() {
643        // In-memory DB (no pager): BEGIN, insert a row, ROLLBACK.
644        // The row must disappear from the live tables HashMap.
645        let mut db = seed_users_table();
646        let before = db.get_table("users".to_string()).unwrap().rowids().len();
647        assert_eq!(before, 3);
648
649        process_command("BEGIN;", &mut db).expect("BEGIN");
650        assert!(db.in_transaction());
651        process_command("INSERT INTO users (name, age) VALUES ('dan', 50);", &mut db)
652            .expect("INSERT inside txn");
653        // Mid-transaction read sees the new row.
654        let mid = db.get_table("users".to_string()).unwrap().rowids().len();
655        assert_eq!(mid, 4);
656
657        process_command("ROLLBACK;", &mut db).expect("ROLLBACK");
658        assert!(!db.in_transaction());
659        let after = db.get_table("users".to_string()).unwrap().rowids().len();
660        assert_eq!(
661            after, 3,
662            "ROLLBACK should have restored the pre-BEGIN state"
663        );
664    }
665
666    #[test]
667    fn commit_keeps_mutations_and_clears_txn_flag() {
668        let mut db = seed_users_table();
669        process_command("BEGIN;", &mut db).expect("BEGIN");
670        process_command("INSERT INTO users (name, age) VALUES ('dan', 50);", &mut db)
671            .expect("INSERT inside txn");
672        process_command("COMMIT;", &mut db).expect("COMMIT");
673        assert!(!db.in_transaction());
674        let after = db.get_table("users".to_string()).unwrap().rowids().len();
675        assert_eq!(after, 4);
676    }
677
678    #[test]
679    fn rollback_undoes_update_and_delete_side_by_side() {
680        use crate::sql::db::table::Value;
681
682        let mut db = seed_users_table();
683        process_command("BEGIN;", &mut db).unwrap();
684        process_command("UPDATE users SET age = 999;", &mut db).unwrap();
685        process_command("DELETE FROM users WHERE name = 'bob';", &mut db).unwrap();
686        // Mid-txn: one row gone, others have age=999.
687        let users = db.get_table("users".to_string()).unwrap();
688        assert_eq!(users.rowids().len(), 2);
689        for r in users.rowids() {
690            assert_eq!(users.get_value("age", r), Some(Value::Integer(999)));
691        }
692
693        process_command("ROLLBACK;", &mut db).unwrap();
694        let users = db.get_table("users".to_string()).unwrap();
695        assert_eq!(users.rowids().len(), 3);
696        // Original ages {30, 25, 40} — none should be 999.
697        for r in users.rowids() {
698            assert_ne!(users.get_value("age", r), Some(Value::Integer(999)));
699        }
700    }
701
702    #[test]
703    fn nested_begin_is_rejected() {
704        let mut db = seed_users_table();
705        process_command("BEGIN;", &mut db).unwrap();
706        let err = process_command("BEGIN;", &mut db).unwrap_err();
707        assert!(
708            format!("{err}").contains("already open"),
709            "nested BEGIN should error; got: {err}"
710        );
711        // Still in the original transaction; a ROLLBACK clears it.
712        assert!(db.in_transaction());
713        process_command("ROLLBACK;", &mut db).unwrap();
714    }
715
716    #[test]
717    fn orphan_commit_and_rollback_are_rejected() {
718        let mut db = seed_users_table();
719        let commit_err = process_command("COMMIT;", &mut db).unwrap_err();
720        assert!(format!("{commit_err}").contains("no transaction"));
721        let rollback_err = process_command("ROLLBACK;", &mut db).unwrap_err();
722        assert!(format!("{rollback_err}").contains("no transaction"));
723    }
724
725    #[test]
726    fn error_inside_transaction_keeps_txn_open() {
727        // A bad INSERT inside a txn doesn't commit or abort automatically —
728        // the user can still ROLLBACK. SQLite's implicit-rollback behavior
729        // isn't modeled here.
730        let mut db = seed_users_table();
731        process_command("BEGIN;", &mut db).unwrap();
732        let err = process_command("INSERT INTO nope (x) VALUES (1);", &mut db);
733        assert!(err.is_err());
734        assert!(db.in_transaction(), "txn should stay open after error");
735        process_command("ROLLBACK;", &mut db).unwrap();
736    }
737
738    /// Builds a file-backed Database at a unique temp path, with the
739    /// schema seeded and `source_path` set so subsequent process_command
740    /// calls auto-save. Returns (path, db). Drop the db before deleting
741    /// the files.
742    fn seed_file_backed(name: &str, schema: &str) -> (std::path::PathBuf, Database) {
743        use crate::sql::pager::{open_database, save_database};
744        let mut p = std::env::temp_dir();
745        let pid = std::process::id();
746        let nanos = std::time::SystemTime::now()
747            .duration_since(std::time::UNIX_EPOCH)
748            .map(|d| d.as_nanos())
749            .unwrap_or(0);
750        p.push(format!("sqlrite-txn-{name}-{pid}-{nanos}.sqlrite"));
751
752        // Seed the file, then reopen to get a source_path-attached db
753        // (save_database alone doesn't attach a fresh pager to a db
754        // whose source_path was None before the call).
755        {
756            let mut seed = Database::new("t".to_string());
757            process_command(schema, &mut seed).unwrap();
758            save_database(&mut seed, &p).unwrap();
759        }
760        let db = open_database(&p, "t".to_string()).unwrap();
761        (p, db)
762    }
763
764    fn cleanup_file(path: &std::path::Path) {
765        let _ = std::fs::remove_file(path);
766        let mut wal = path.as_os_str().to_owned();
767        wal.push("-wal");
768        let _ = std::fs::remove_file(std::path::PathBuf::from(wal));
769    }
770
771    #[test]
772    fn begin_commit_rollback_round_trip_through_disk() {
773        // File-backed DB: commit inside a transaction must actually
774        // persist. ROLLBACK inside a *later* transaction must not
775        // un-do the previously-committed changes.
776        use crate::sql::pager::open_database;
777
778        let (path, mut db) = seed_file_backed(
779            "roundtrip",
780            "CREATE TABLE notes (id INTEGER PRIMARY KEY, body TEXT);",
781        );
782
783        // Transaction 1: insert two rows, commit.
784        process_command("BEGIN;", &mut db).unwrap();
785        process_command("INSERT INTO notes (body) VALUES ('a');", &mut db).unwrap();
786        process_command("INSERT INTO notes (body) VALUES ('b');", &mut db).unwrap();
787        process_command("COMMIT;", &mut db).unwrap();
788
789        // Transaction 2: insert another, roll back.
790        process_command("BEGIN;", &mut db).unwrap();
791        process_command("INSERT INTO notes (body) VALUES ('c');", &mut db).unwrap();
792        process_command("ROLLBACK;", &mut db).unwrap();
793
794        drop(db); // release pager lock
795
796        let reopened = open_database(&path, "t".to_string()).unwrap();
797        let notes = reopened.get_table("notes".to_string()).unwrap();
798        assert_eq!(notes.rowids().len(), 2, "committed rows should survive");
799
800        drop(reopened);
801        cleanup_file(&path);
802    }
803
804    #[test]
805    fn write_inside_transaction_does_not_autosave() {
806        // File-backed DB: writes inside BEGIN/…/COMMIT must NOT hit
807        // the WAL until COMMIT. We prove it by checking the WAL file
808        // size before vs during the transaction.
809        let (path, mut db) =
810            seed_file_backed("noas", "CREATE TABLE t (id INTEGER PRIMARY KEY, x TEXT);");
811
812        let mut wal_path = path.as_os_str().to_owned();
813        wal_path.push("-wal");
814        let wal_path = std::path::PathBuf::from(wal_path);
815        let frames_before = std::fs::metadata(&wal_path).unwrap().len();
816
817        process_command("BEGIN;", &mut db).unwrap();
818        process_command("INSERT INTO t (x) VALUES ('a');", &mut db).unwrap();
819        process_command("INSERT INTO t (x) VALUES ('b');", &mut db).unwrap();
820
821        // Mid-transaction: WAL must be unchanged — no auto-save fired.
822        let frames_mid = std::fs::metadata(&wal_path).unwrap().len();
823        assert_eq!(
824            frames_before, frames_mid,
825            "WAL should not grow during an open transaction"
826        );
827
828        process_command("COMMIT;", &mut db).unwrap();
829
830        drop(db); // release pager lock
831        let fresh = crate::sql::pager::open_database(&path, "t".to_string()).unwrap();
832        assert_eq!(
833            fresh.get_table("t".to_string()).unwrap().rowids().len(),
834            2,
835            "COMMIT should have persisted both inserted rows"
836        );
837        drop(fresh);
838        cleanup_file(&path);
839    }
840
841    #[test]
842    fn rollback_undoes_create_table() {
843        // Schema DDL inside a txn: ROLLBACK must make the new table
844        // disappear. The txn snapshot captures db.tables as of BEGIN,
845        // and ROLLBACK reassigns tables from that snapshot, so a table
846        // created mid-transaction has no entry in the snapshot.
847        let mut db = seed_users_table();
848        assert_eq!(db.tables.len(), 1);
849
850        process_command("BEGIN;", &mut db).unwrap();
851        process_command(
852            "CREATE TABLE dropme (id INTEGER PRIMARY KEY, x TEXT);",
853            &mut db,
854        )
855        .unwrap();
856        process_command("INSERT INTO dropme (x) VALUES ('stuff');", &mut db).unwrap();
857        assert_eq!(db.tables.len(), 2);
858
859        process_command("ROLLBACK;", &mut db).unwrap();
860        assert_eq!(
861            db.tables.len(),
862            1,
863            "CREATE TABLE should have been rolled back"
864        );
865        assert!(db.get_table("dropme".to_string()).is_err());
866    }
867
868    #[test]
869    fn rollback_restores_secondary_index_state() {
870        // Phase 4f edge case: rolling back an INSERT on a UNIQUE-indexed
871        // column must also clean up the index, otherwise a re-insert of
872        // the same value would spuriously collide.
873        let mut db = Database::new("t".to_string());
874        process_command(
875            "CREATE TABLE users (id INTEGER PRIMARY KEY, email TEXT UNIQUE);",
876            &mut db,
877        )
878        .unwrap();
879        process_command("INSERT INTO users (email) VALUES ('a@x');", &mut db).unwrap();
880
881        process_command("BEGIN;", &mut db).unwrap();
882        process_command("INSERT INTO users (email) VALUES ('b@x');", &mut db).unwrap();
883        // Inside the txn: the index now contains both 'a@x' and 'b@x'.
884        process_command("ROLLBACK;", &mut db).unwrap();
885
886        // Re-inserting 'b@x' after rollback must succeed — if the index
887        // wasn't properly restored, it would think 'b@x' is still a
888        // collision and fail with a UNIQUE violation.
889        let reinsert = process_command("INSERT INTO users (email) VALUES ('b@x');", &mut db);
890        assert!(
891            reinsert.is_ok(),
892            "re-insert after rollback should succeed, got {reinsert:?}"
893        );
894    }
895
896    #[test]
897    fn rollback_restores_last_rowid_counter() {
898        // Rowids allocated inside a rolled-back transaction should be
899        // reusable. The snapshot restores Table::last_rowid, so the
900        // next insert picks up where the pre-BEGIN state left off.
901        use crate::sql::db::table::Value;
902
903        let mut db = seed_users_table(); // 3 rows, last_rowid = 3
904        let pre = db.get_table("users".to_string()).unwrap().last_rowid;
905
906        process_command("BEGIN;", &mut db).unwrap();
907        process_command("INSERT INTO users (name, age) VALUES ('d', 50);", &mut db).unwrap(); // would be rowid 4
908        process_command("INSERT INTO users (name, age) VALUES ('e', 60);", &mut db).unwrap(); // would be rowid 5
909        process_command("ROLLBACK;", &mut db).unwrap();
910
911        let post = db.get_table("users".to_string()).unwrap().last_rowid;
912        assert_eq!(pre, post, "last_rowid must roll back with the snapshot");
913
914        // Confirm: the next insert reuses rowid pre+1.
915        process_command("INSERT INTO users (name, age) VALUES ('d', 50);", &mut db).unwrap();
916        let users = db.get_table("users".to_string()).unwrap();
917        let d_rowid = users
918            .rowids()
919            .into_iter()
920            .find(|r| users.get_value("name", *r) == Some(Value::Text("d".into())))
921            .expect("d row must exist");
922        assert_eq!(d_rowid, pre + 1);
923    }
924
925    #[test]
926    fn commit_on_in_memory_db_clears_txn_without_pager_call() {
927        // In-memory DB (no source_path): COMMIT must still work — just
928        // no disk flush. Covers the `if let Some(path) = …` branch
929        // where the guard falls through without calling save_database.
930        let mut db = seed_users_table(); // no source_path
931        assert!(db.source_path.is_none());
932
933        process_command("BEGIN;", &mut db).unwrap();
934        process_command("INSERT INTO users (name, age) VALUES ('z', 99);", &mut db).unwrap();
935        process_command("COMMIT;", &mut db).unwrap();
936
937        assert!(!db.in_transaction());
938        assert_eq!(db.get_table("users".to_string()).unwrap().rowids().len(), 4);
939    }
940
941    #[test]
942    fn failed_commit_auto_rolls_back_in_memory_state() {
943        // Data-safety regression: on COMMIT save failure we must auto-
944        // rollback the in-memory state. Otherwise, any subsequent
945        // non-transactional statement would auto-save the partial
946        // mid-transaction work, silently publishing uncommitted
947        // changes to disk.
948        //
949        // We simulate a save failure by making the WAL sidecar path
950        // unavailable mid-transaction: after BEGIN, we take an
951        // exclusive OS lock on the WAL via a second File handle,
952        // forcing the next save to fail when it tries to append.
953        //
954        // Simpler repro: point source_path at a directory (not a file).
955        // `OpenOptions::open` will fail with EISDIR on save.
956        use crate::sql::pager::save_database;
957
958        // Seed a file-backed db.
959        let (path, mut db) = seed_file_backed(
960            "failcommit",
961            "CREATE TABLE notes (id INTEGER PRIMARY KEY, body TEXT);",
962        );
963
964        // Prime one committed row so we have a baseline.
965        process_command("INSERT INTO notes (body) VALUES ('before');", &mut db).unwrap();
966
967        // Open a new txn and add a row.
968        process_command("BEGIN;", &mut db).unwrap();
969        process_command("INSERT INTO notes (body) VALUES ('inflight');", &mut db).unwrap();
970        assert_eq!(
971            db.get_table("notes".to_string()).unwrap().rowids().len(),
972            2,
973            "inflight row visible mid-txn"
974        );
975
976        // Swap source_path to a path that will fail on open. A
977        // directory is a reliable failure mode — Pager::open on a
978        // directory errors with an I/O error.
979        let orig_source = db.source_path.clone();
980        let orig_pager = db.pager.take();
981        db.source_path = Some(std::env::temp_dir());
982
983        let commit_result = process_command("COMMIT;", &mut db);
984        assert!(commit_result.is_err(), "commit must fail");
985        let err_str = format!("{}", commit_result.unwrap_err());
986        assert!(
987            err_str.contains("COMMIT failed") && err_str.contains("rolled back"),
988            "error must surface auto-rollback; got: {err_str}"
989        );
990
991        // Auto-rollback fired: the inflight row is gone, the txn flag
992        // is cleared, and a follow-up non-txn statement won't leak
993        // stale state.
994        assert!(
995            !db.in_transaction(),
996            "txn must be cleared after auto-rollback"
997        );
998        assert_eq!(
999            db.get_table("notes".to_string()).unwrap().rowids().len(),
1000            1,
1001            "inflight row must be rolled back"
1002        );
1003
1004        // Restore the real source_path + pager and verify a clean
1005        // subsequent write goes through.
1006        db.source_path = orig_source;
1007        db.pager = orig_pager;
1008        process_command("INSERT INTO notes (body) VALUES ('after');", &mut db).unwrap();
1009        drop(db);
1010
1011        // Reopen and assert only 'before' + 'after' landed on disk.
1012        let reopened = crate::sql::pager::open_database(&path, "t".to_string()).unwrap();
1013        let notes = reopened.get_table("notes".to_string()).unwrap();
1014        assert_eq!(notes.rowids().len(), 2);
1015        // Ensure no leaked save_database partial happened.
1016        let _ = save_database; // silence unused-import lint if any
1017        drop(reopened);
1018        cleanup_file(&path);
1019    }
1020
1021    #[test]
1022    fn begin_on_read_only_is_rejected() {
1023        use crate::sql::pager::{open_database_read_only, save_database};
1024
1025        let path = {
1026            let mut p = std::env::temp_dir();
1027            let pid = std::process::id();
1028            let nanos = std::time::SystemTime::now()
1029                .duration_since(std::time::UNIX_EPOCH)
1030                .map(|d| d.as_nanos())
1031                .unwrap_or(0);
1032            p.push(format!("sqlrite-txn-ro-{pid}-{nanos}.sqlrite"));
1033            p
1034        };
1035        {
1036            let mut seed = Database::new("t".to_string());
1037            process_command("CREATE TABLE t (id INTEGER PRIMARY KEY);", &mut seed).unwrap();
1038            save_database(&mut seed, &path).unwrap();
1039        }
1040
1041        let mut ro = open_database_read_only(&path, "t".to_string()).unwrap();
1042        let err = process_command("BEGIN;", &mut ro).unwrap_err();
1043        assert!(
1044            format!("{err}").contains("read-only"),
1045            "BEGIN on RO db should surface read-only; got: {err}"
1046        );
1047        assert!(!ro.in_transaction());
1048
1049        let _ = std::fs::remove_file(&path);
1050        let mut wal = path.as_os_str().to_owned();
1051        wal.push("-wal");
1052        let _ = std::fs::remove_file(std::path::PathBuf::from(wal));
1053    }
1054
1055    #[test]
1056    fn read_only_database_rejects_mutations_before_touching_state() {
1057        // Phase 4e end-to-end: a `--readonly` caller that runs INSERT
1058        // must error *before* the row is added to the in-memory table.
1059        // Otherwise the user sees a rendered result table with the
1060        // phantom row, followed by the auto-save error — UX rot and a
1061        // state-drift risk.
1062        use crate::sql::pager::open_database_read_only;
1063
1064        let mut seed = Database::new("t".to_string());
1065        process_command(
1066            "CREATE TABLE notes (id INTEGER PRIMARY KEY, body TEXT);",
1067            &mut seed,
1068        )
1069        .unwrap();
1070        process_command("INSERT INTO notes (body) VALUES ('alpha');", &mut seed).unwrap();
1071
1072        let path = {
1073            let mut p = std::env::temp_dir();
1074            let pid = std::process::id();
1075            let nanos = std::time::SystemTime::now()
1076                .duration_since(std::time::UNIX_EPOCH)
1077                .map(|d| d.as_nanos())
1078                .unwrap_or(0);
1079            p.push(format!("sqlrite-ro-reject-{pid}-{nanos}.sqlrite"));
1080            p
1081        };
1082        crate::sql::pager::save_database(&mut seed, &path).unwrap();
1083        drop(seed);
1084
1085        let mut ro = open_database_read_only(&path, "t".to_string()).unwrap();
1086        let notes_before = ro.get_table("notes".to_string()).unwrap().rowids().len();
1087
1088        for stmt in [
1089            "INSERT INTO notes (body) VALUES ('beta');",
1090            "UPDATE notes SET body = 'x';",
1091            "DELETE FROM notes;",
1092            "CREATE TABLE more (id INTEGER PRIMARY KEY);",
1093            "CREATE INDEX notes_body ON notes (body);",
1094        ] {
1095            let err = process_command(stmt, &mut ro).unwrap_err();
1096            assert!(
1097                format!("{err}").contains("read-only"),
1098                "stmt {stmt:?} should surface a read-only error; got: {err}"
1099            );
1100        }
1101
1102        // Nothing mutated: same row count as before, and SELECTs still work.
1103        let notes_after = ro.get_table("notes".to_string()).unwrap().rowids().len();
1104        assert_eq!(notes_before, notes_after);
1105        let sel = process_command("SELECT * FROM notes;", &mut ro).expect("select on RO must work");
1106        assert!(sel.contains("1 row returned"));
1107
1108        // Cleanup.
1109        drop(ro);
1110        let _ = std::fs::remove_file(&path);
1111        let mut wal = path.as_os_str().to_owned();
1112        wal.push("-wal");
1113        let _ = std::fs::remove_file(std::path::PathBuf::from(wal));
1114    }
1115
1116    // -----------------------------------------------------------------
1117    // Phase 7a — VECTOR(N) end-to-end through process_command
1118    // -----------------------------------------------------------------
1119
1120    #[test]
1121    fn vector_create_table_and_insert_basic() {
1122        let mut db = Database::new("tempdb".to_string());
1123        process_command(
1124            "CREATE TABLE docs (id INTEGER PRIMARY KEY, embedding VECTOR(3));",
1125            &mut db,
1126        )
1127        .expect("create table with VECTOR(3)");
1128        process_command(
1129            "INSERT INTO docs (embedding) VALUES ([0.1, 0.2, 0.3]);",
1130            &mut db,
1131        )
1132        .expect("insert vector");
1133
1134        // process_command returns a status string; the rendered table
1135        // goes to stdout via print_table. Verify state by inspecting
1136        // the database directly.
1137        let sel = process_command("SELECT * FROM docs;", &mut db).expect("select");
1138        assert!(sel.contains("1 row returned"));
1139
1140        let docs = db.get_table("docs".to_string()).expect("docs table");
1141        let rowids = docs.rowids();
1142        assert_eq!(rowids.len(), 1);
1143        match docs.get_value("embedding", rowids[0]) {
1144            Some(Value::Vector(v)) => assert_eq!(v, vec![0.1f32, 0.2, 0.3]),
1145            other => panic!("expected Value::Vector(...), got {other:?}"),
1146        }
1147    }
1148
1149    #[test]
1150    fn vector_dim_mismatch_at_insert_is_clean_error() {
1151        let mut db = Database::new("tempdb".to_string());
1152        process_command(
1153            "CREATE TABLE docs (id INTEGER PRIMARY KEY, embedding VECTOR(3));",
1154            &mut db,
1155        )
1156        .expect("create table");
1157
1158        // Too few elements.
1159        let err = process_command("INSERT INTO docs (embedding) VALUES ([0.1, 0.2]);", &mut db)
1160            .unwrap_err();
1161        let msg = format!("{err}");
1162        assert!(
1163            msg.to_lowercase().contains("dimension")
1164                && msg.contains("declared 3")
1165                && msg.contains("got 2"),
1166            "expected clear dim-mismatch error, got: {msg}"
1167        );
1168
1169        // Too many elements.
1170        let err = process_command(
1171            "INSERT INTO docs (embedding) VALUES ([0.1, 0.2, 0.3, 0.4, 0.5]);",
1172            &mut db,
1173        )
1174        .unwrap_err();
1175        assert!(
1176            format!("{err}").contains("got 5"),
1177            "expected dim-mismatch error mentioning got 5, got: {err}"
1178        );
1179    }
1180
1181    #[test]
1182    fn vector_create_table_rejects_missing_dim() {
1183        let mut db = Database::new("tempdb".to_string());
1184        // `VECTOR` (no parens) currently parses as `DataType::Custom` with
1185        // empty args from sqlparser, OR may not parse as Custom at all
1186        // depending on dialect. Either way, the column shouldn't end up
1187        // as a usable Vector type. Accept any error here — the precise
1188        // message is parser-version-dependent.
1189        let result = process_command(
1190            "CREATE TABLE docs (id INTEGER PRIMARY KEY, embedding VECTOR);",
1191            &mut db,
1192        );
1193        assert!(
1194            result.is_err(),
1195            "expected CREATE TABLE with bare VECTOR to fail (no dim)"
1196        );
1197    }
1198
1199    #[test]
1200    fn vector_create_table_rejects_zero_dim() {
1201        let mut db = Database::new("tempdb".to_string());
1202        let err = process_command(
1203            "CREATE TABLE docs (id INTEGER PRIMARY KEY, embedding VECTOR(0));",
1204            &mut db,
1205        )
1206        .unwrap_err();
1207        let msg = format!("{err}");
1208        assert!(
1209            msg.to_lowercase().contains("vector"),
1210            "expected VECTOR-related error for VECTOR(0), got: {msg}"
1211        );
1212    }
1213
1214    #[test]
1215    fn vector_high_dim_works() {
1216        // 384-dim vector (OpenAI text-embedding-3-small size). Mostly a
1217        // smoke test — if cell encoding mishandles the size, this fails.
1218        let mut db = Database::new("tempdb".to_string());
1219        process_command(
1220            "CREATE TABLE embeddings (id INTEGER PRIMARY KEY, e VECTOR(384));",
1221            &mut db,
1222        )
1223        .expect("create table VECTOR(384)");
1224
1225        let lit = format!(
1226            "[{}]",
1227            (0..384)
1228                .map(|i| format!("{}", i as f32 * 0.001))
1229                .collect::<Vec<_>>()
1230                .join(",")
1231        );
1232        let sql = format!("INSERT INTO embeddings (e) VALUES ({lit});");
1233        process_command(&sql, &mut db).expect("insert 384-dim vector");
1234
1235        let sel = process_command("SELECT id FROM embeddings;", &mut db).expect("select id");
1236        assert!(sel.contains("1 row returned"));
1237    }
1238
1239    #[test]
1240    fn vector_multiple_rows() {
1241        // Three rows with different vectors — exercises the Row::Vector
1242        // BTreeMap path (not just single-row insertion).
1243        let mut db = Database::new("tempdb".to_string());
1244        process_command(
1245            "CREATE TABLE docs (id INTEGER PRIMARY KEY, e VECTOR(2));",
1246            &mut db,
1247        )
1248        .expect("create");
1249        for i in 0..3 {
1250            let sql = format!("INSERT INTO docs (e) VALUES ([{i}.0, {}.0]);", i + 1);
1251            process_command(&sql, &mut db).expect("insert");
1252        }
1253        let sel = process_command("SELECT * FROM docs;", &mut db).expect("select");
1254        assert!(sel.contains("3 rows returned"));
1255
1256        // Verify each vector round-tripped correctly via direct DB inspection.
1257        let docs = db.get_table("docs".to_string()).expect("docs table");
1258        let rowids = docs.rowids();
1259        assert_eq!(rowids.len(), 3);
1260        let mut vectors: Vec<Vec<f32>> = rowids
1261            .iter()
1262            .filter_map(|r| match docs.get_value("e", *r) {
1263                Some(Value::Vector(v)) => Some(v),
1264                _ => None,
1265            })
1266            .collect();
1267        vectors.sort_by(|a, b| a[0].partial_cmp(&b[0]).unwrap());
1268        assert_eq!(vectors[0], vec![0.0f32, 1.0]);
1269        assert_eq!(vectors[1], vec![1.0f32, 2.0]);
1270        assert_eq!(vectors[2], vec![2.0f32, 3.0]);
1271    }
1272}