diff --git a/COMPAT.md b/COMPAT.md index 1c2b9b227..772d53a43 100644 --- a/COMPAT.md +++ b/COMPAT.md @@ -380,7 +380,7 @@ Modifiers: | json_type(json,path) | Yes | | | json_valid(json) | Yes | | | json_valid(json,flags) | | | -| json_quote(value) | | | +| json_quote(value) | Yes | | | json_group_array(value) | | | | jsonb_group_array(value) | | | | json_group_object(label,value) | | | diff --git a/Cargo.lock b/Cargo.lock index d26da84a6..819311184 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1394,15 +1394,6 @@ version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674" -[[package]] -name = "java-limbo" -version = "0.0.14" -dependencies = [ - "jni", - "limbo_core", - "thiserror 2.0.11", -] - [[package]] name = "jni" version = "0.21.1" @@ -1590,6 +1581,15 @@ dependencies = [ "limbo_core", ] +[[package]] +name = "limbo-java" +version = "0.0.14" +dependencies = [ + "jni", + "limbo_core", + "thiserror 2.0.11", +] + [[package]] name = "limbo-wasm" version = "0.0.14" @@ -1735,6 +1735,8 @@ dependencies = [ "notify", "rand 0.8.5", "rand_chacha 0.3.1", + "regex", + "regex-syntax", "serde", "serde_json", "tempfile", diff --git a/Makefile b/Makefile index e1fdf6d29..0a2e1082d 100644 --- a/Makefile +++ b/Makefile @@ -89,3 +89,7 @@ test-time: test-sqlite3: limbo-c LIBS="$(SQLITE_LIB)" HEADERS="$(SQLITE_LIB_HEADERS)" make -C sqlite3/tests test .PHONY: test-sqlite3 + +test-json: + SQLITE_EXEC=$(SQLITE_EXEC) ./testing/json.test +.PHONY: test-json diff --git a/README.md b/README.md index 6e150716c..480933581 100644 --- a/README.md +++ b/README.md @@ -142,6 +142,11 @@ for rows.Next() { } ``` +### ☕️ Java (wip) + +We integrated Limbo into JDBC. For detailed instructions on how to use Limbo with java, please refer to +the [README.md under bindings/java](bindings/java/README.md). + ## Contributing We'd love to have you contribute to Limbo! Please check out the [contribution guide] to get started. diff --git a/bindings/java/Cargo.toml b/bindings/java/Cargo.toml index e3b7660c5..9b78b1597 100644 --- a/bindings/java/Cargo.toml +++ b/bindings/java/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "java-limbo" +name = "limbo-java" version.workspace = true authors.workspace = true edition.workspace = true diff --git a/bindings/java/src/test/java/org/github/tursodatabase/jdbc4/JDBC4ResultSetTest.java b/bindings/java/src/test/java/org/github/tursodatabase/jdbc4/JDBC4ResultSetTest.java index ec79ce1eb..de1b5e46c 100644 --- a/bindings/java/src/test/java/org/github/tursodatabase/jdbc4/JDBC4ResultSetTest.java +++ b/bindings/java/src/test/java/org/github/tursodatabase/jdbc4/JDBC4ResultSetTest.java @@ -16,7 +16,6 @@ import java.util.Properties; import java.util.stream.Stream; import org.github.tursodatabase.TestUtils; import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.MethodSource; @@ -241,7 +240,6 @@ class JDBC4ResultSetTest { } @Test - @Disabled("limbo has a bug which sees -9223372036854775808 as double") void test_getLong() throws Exception { stmt.executeUpdate("CREATE TABLE test_long (long_col BIGINT);"); stmt.executeUpdate("INSERT INTO test_long (long_col) VALUES (1234567890);"); @@ -253,6 +251,7 @@ class JDBC4ResultSetTest { ResultSet resultSet = stmt.executeQuery("SELECT * FROM test_long"); // Test typical long value + assertTrue(resultSet.next()); assertEquals(1234567890L, resultSet.getLong(1)); // Test maximum long value diff --git a/cli/app.rs b/cli/app.rs index 7d812d7fe..7fa7b70f0 100644 --- a/cli/app.rs +++ b/cli/app.rs @@ -2,7 +2,8 @@ use crate::{ import::{ImportFile, IMPORT_HELP}, opcodes_dictionary::OPCODE_DESCRIPTIONS, }; -use cli_table::{Cell, Table}; +use cli_table::format::{Border, HorizontalLine, Separator, VerticalLine}; +use cli_table::{Cell, Style, Table}; use limbo_core::{Database, LimboError, Statement, StepResult, Value}; use clap::{Parser, ValueEnum}; @@ -670,6 +671,16 @@ impl Limbo { return Ok(()); } let mut table_rows: Vec> = vec![]; + if rows.num_columns() > 0 { + let columns = (0..rows.num_columns()) + .map(|i| { + rows.get_column_name(i) + .map(|name| name.cell().bold(true)) + .unwrap_or_else(|| " ".cell()) + }) + .collect::>(); + table_rows.push(columns); + } loop { match rows.step() { Ok(StepResult::Row) => { @@ -707,11 +718,7 @@ impl Limbo { } } } - if let Ok(table) = table_rows.table().display() { - let _ = self.write_fmt(format_args!("{}", table)); - } else { - let _ = self.writeln("Error displaying table."); - } + self.print_table(table_rows); } }, Ok(None) => {} @@ -727,6 +734,40 @@ impl Limbo { Ok(()) } + fn print_table(&mut self, table_rows: Vec>) { + if table_rows.is_empty() { + return; + } + + let horizontal_line = HorizontalLine::new('┌', '┐', '┬', '─'); + let horizontal_line_mid = HorizontalLine::new('├', '┤', '┼', '─'); + let horizontal_line_bottom = HorizontalLine::new('└', '┘', '┴', '─'); + let vertical_line = VerticalLine::new('│'); + + let border = Border::builder() + .top(horizontal_line) + .bottom(horizontal_line_bottom) + .left(vertical_line.clone()) + .right(vertical_line.clone()) + .build(); + + let separator = Separator::builder() + .column(Some(vertical_line)) + .row(Some(horizontal_line_mid)) + .build(); + + if let Ok(table) = table_rows + .table() + .border(border) + .separator(separator) + .display() + { + let _ = self.write_fmt(format_args!("{}", table)); + } else { + let _ = self.writeln("Error displaying table."); + } + } + fn display_schema(&mut self, table: Option<&str>) -> anyhow::Result<()> { let sql = match table { Some(table_name) => format!( diff --git a/core/function.rs b/core/function.rs index 83c755796..fa10d9787 100644 --- a/core/function.rs +++ b/core/function.rs @@ -84,6 +84,7 @@ pub enum JsonFunc { JsonRemove, JsonPretty, JsonSet, + JsonQuote, } #[cfg(feature = "json")] @@ -107,6 +108,7 @@ impl Display for JsonFunc { Self::JsonRemove => "json_remove".to_string(), Self::JsonPretty => "json_pretty".to_string(), Self::JsonSet => "json_set".to_string(), + Self::JsonQuote => "json_quote".to_string(), } ) } @@ -568,6 +570,8 @@ impl Func { "json_pretty" => Ok(Self::Json(JsonFunc::JsonPretty)), #[cfg(feature = "json")] "json_set" => Ok(Self::Json(JsonFunc::JsonSet)), + #[cfg(feature = "json")] + "json_quote" => Ok(Self::Json(JsonFunc::JsonQuote)), "unixepoch" => Ok(Self::Scalar(ScalarFunc::UnixEpoch)), "julianday" => Ok(Self::Scalar(ScalarFunc::JulianDay)), "hex" => Ok(Self::Scalar(ScalarFunc::Hex)), diff --git a/core/io/io_uring.rs b/core/io/io_uring.rs index 1598debfa..eef5e523d 100644 --- a/core/io/io_uring.rs +++ b/core/io/io_uring.rs @@ -4,7 +4,6 @@ use log::{debug, trace}; use rustix::fs::{self, FlockOperation, OFlags}; use rustix::io_uring::iovec; use std::cell::RefCell; -use std::collections::HashMap; use std::fmt; use std::io::ErrorKind; use std::os::fd::AsFd; @@ -40,7 +39,7 @@ pub struct UringIO { struct WrappedIOUring { ring: io_uring::IoUring, pending_ops: usize, - pub pending: HashMap>, + pub pending: [Option>; MAX_IOVECS as usize + 1], key: u64, } @@ -63,7 +62,7 @@ impl UringIO { ring: WrappedIOUring { ring, pending_ops: 0, - pending: HashMap::new(), + pending: [const { None }; MAX_IOVECS as usize + 1], key: 0, }, iovecs: [iovec { @@ -92,7 +91,7 @@ impl InnerUringIO { impl WrappedIOUring { fn submit_entry(&mut self, entry: &io_uring::squeue::Entry, c: Rc) { trace!("submit_entry({:?})", entry); - self.pending.insert(entry.get_user_data(), c); + self.pending[entry.get_user_data() as usize] = Some(c); unsafe { self.ring .submission() @@ -124,6 +123,11 @@ impl WrappedIOUring { fn get_key(&mut self) -> u64 { self.key += 1; + if self.key == MAX_IOVECS as u64 { + let key = self.key; + self.key = 0; + return key; + } self.key } } @@ -175,10 +179,11 @@ impl IO for UringIO { ))); } { - let c = ring.pending.get(&cqe.user_data()).unwrap().clone(); - c.complete(cqe.result()); + if let Some(c) = ring.pending[cqe.user_data() as usize].as_ref() { + c.complete(cqe.result()); + } } - ring.pending.remove(&cqe.user_data()); + ring.pending[cqe.user_data() as usize] = None; } Ok(()) } diff --git a/core/json/mod.rs b/core/json/mod.rs index 31f12ba14..bca1eb5de 100644 --- a/core/json/mod.rs +++ b/core/json/mod.rs @@ -674,6 +674,43 @@ pub fn is_json_valid(json_value: &OwnedValue) -> crate::Result { } } +pub fn json_quote(value: &OwnedValue) -> crate::Result { + match value { + OwnedValue::Text(ref t) => { + // If X is a JSON value returned by another JSON function, + // then this function is a no-op + if t.subtype == TextSubtype::Json { + // Should just return the json value with no quotes + return Ok(value.to_owned()); + } + + let mut escaped_value = String::with_capacity(t.value.len() + 4); + escaped_value.push('"'); + + for c in t.as_str().chars() { + match c { + '"' | '\\' | '\n' | '\r' | '\t' | '\u{0008}' | '\u{000c}' => { + escaped_value.push('\\'); + escaped_value.push(c); + } + c => escaped_value.push(c), + } + } + escaped_value.push('"'); + + Ok(OwnedValue::Text(Text::new(Rc::new(escaped_value)))) + } + // Numbers are unquoted in json + OwnedValue::Integer(ref int) => Ok(OwnedValue::Integer(int.to_owned())), + OwnedValue::Float(ref float) => Ok(OwnedValue::Float(float.to_owned())), + OwnedValue::Blob(_) => crate::bail_constraint_error!("JSON cannot hold BLOB values"), + OwnedValue::Null => Ok(OwnedValue::Text(Text::new(Rc::new("null".to_string())))), + _ => { + unreachable!() + } + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/core/mvcc/database/mod.rs b/core/mvcc/database/mod.rs index 6c6e49053..4ae85f195 100644 --- a/core/mvcc/database/mod.rs +++ b/core/mvcc/database/mod.rs @@ -58,7 +58,9 @@ impl LogRecord { /// versions switch to tracking timestamps. #[derive(Clone, Debug, PartialEq, PartialOrd)] enum TxTimestampOrID { + /// A committed transaction's timestamp. Timestamp(u64), + /// The ID of a non-committed transaction. TxID(TxID), } @@ -229,55 +231,6 @@ impl MvStore u64 { - match ts_or_id { - TxTimestampOrID::Timestamp(ts) => *ts, - TxTimestampOrID::TxID(tx_id) => { - self.txs - .get(tx_id) - .unwrap() - .value() - .read() - .unwrap() - .begin_ts - } - } - } - - /// Inserts a new row version into the database, while making sure that - /// the row version is inserted in the correct order. - fn insert_version(&self, id: RowID, row_version: RowVersion) { - let versions = self.rows.get_or_insert_with(id, || RwLock::new(Vec::new())); - let mut versions = versions.value().write().unwrap(); - self.insert_version_raw(&mut versions, row_version) - } - - /// Inserts a new row version into the internal data structure for versions, - /// while making sure that the row version is inserted in the correct order. - fn insert_version_raw(&self, versions: &mut Vec>, row_version: RowVersion) { - // NOTICE: this is an insert a'la insertion sort, with pessimistic linear complexity. - // However, we expect the number of versions to be nearly sorted, so we deem it worthy - // to search linearly for the insertion point instead of paying the price of using - // another data structure, e.g. a BTreeSet. If it proves to be too quadratic empirically, - // we can either switch to a tree-like structure, or at least use partition_point() - // which performs a binary search for the insertion point. - let position = versions - .iter() - .rposition(|v| { - self.get_begin_timestamp(&v.begin) < self.get_begin_timestamp(&row_version.begin) - }) - .map(|p| p + 1) - .unwrap_or(0); - if versions.len() - position > 3 { - tracing::debug!( - "Inserting a row version {} positions from the end", - versions.len() - position - ); - } - versions.insert(position, row_version); - } - /// Inserts a new row into the database. /// /// This function inserts a new `row` into the database within the context @@ -365,6 +318,10 @@ impl MvStore MvStore MvStore = tx.write_set.iter().map(|v| *v.value()).collect(); drop(tx); // Postprocessing: inserting row versions and logging the transaction to persistent storage. @@ -568,7 +523,9 @@ impl MvStore MvStore MvStore u64 { + match ts_or_id { + TxTimestampOrID::Timestamp(ts) => *ts, + TxTimestampOrID::TxID(tx_id) => { + self.txs + .get(tx_id) + .unwrap() + .value() + .read() + .unwrap() + .begin_ts + } + } + } + + /// Inserts a new row version into the database, while making sure that + /// the row version is inserted in the correct order. + fn insert_version(&self, id: RowID, row_version: RowVersion) { + let versions = self.rows.get_or_insert_with(id, || RwLock::new(Vec::new())); + let mut versions = versions.value().write().unwrap(); + self.insert_version_raw(&mut versions, row_version) + } + + /// Inserts a new row version into the internal data structure for versions, + /// while making sure that the row version is inserted in the correct order. + fn insert_version_raw(&self, versions: &mut Vec>, row_version: RowVersion) { + // NOTICE: this is an insert a'la insertion sort, with pessimistic linear complexity. + // However, we expect the number of versions to be nearly sorted, so we deem it worthy + // to search linearly for the insertion point instead of paying the price of using + // another data structure, e.g. a BTreeSet. If it proves to be too quadratic empirically, + // we can either switch to a tree-like structure, or at least use partition_point() + // which performs a binary search for the insertion point. + let position = versions + .iter() + .rposition(|v| { + self.get_begin_timestamp(&v.begin) < self.get_begin_timestamp(&row_version.begin) + }) + .map(|p| p + 1) + .unwrap_or(0); + if versions.len() - position > 3 { + tracing::debug!( + "Inserting a row version {} positions from the end", + versions.len() - position + ); + } + versions.insert(position, row_version); + } } -/// A write-write conflict happens when transaction T_m attempts to update a -/// row version that is currently being updated by an active transaction T_n. +/// A write-write conflict happens when transaction T_current attempts to update a +/// row version that is: +/// a) currently being updated by an active transaction T_previous, or +/// b) was updated by an ended transaction T_previous that committed AFTER T_current started +/// but BEFORE T_previous commits. +/// +/// "Suppose transaction T wants to update a version V. V is updatable +/// only if it is the latest version, that is, it has an end timestamp equal +/// to infinity or its End field contains the ID of a transaction TE and +/// TE’s state is Aborted" +/// Ref: https://www.cs.cmu.edu/~15721-f24/papers/Hekaton.pdf , page 301, +/// 2.6. Updating a Version. pub(crate) fn is_write_write_conflict( txs: &SkipMap>, tx: &Transaction, @@ -731,12 +749,16 @@ pub(crate) fn is_write_write_conflict( Some(TxTimestampOrID::TxID(rv_end)) => { let te = txs.get(&rv_end).unwrap(); let te = te.value().read().unwrap(); - match te.state.load() { - TransactionState::Active | TransactionState::Preparing => tx.tx_id != te.tx_id, - _ => false, + if te.tx_id == tx.tx_id { + return false; } + te.state.load() != TransactionState::Aborted } - Some(TxTimestampOrID::Timestamp(_)) => false, + // A non-"infinity" end timestamp (here modeled by Some(ts)) functions as a write lock + // on the row, so it can never be updated by another transaction. + // Ref: https://www.cs.cmu.edu/~15721-f24/papers/Hekaton.pdf , page 301, + // 2.6. Updating a Version. + Some(TxTimestampOrID::Timestamp(_)) => true, None => false, } } diff --git a/core/mvcc/database/tests.rs b/core/mvcc/database/tests.rs index 8cb1c3027..b317a15d2 100644 --- a/core/mvcc/database/tests.rs +++ b/core/mvcc/database/tests.rs @@ -382,7 +382,7 @@ fn test_fuzzy_read() { table_id: 1, row_id: 1, }, - data: "Hello".to_string(), + data: "First".to_string(), }; db.insert(tx1, tx1_row.clone()).unwrap(); let row = db @@ -419,7 +419,7 @@ fn test_fuzzy_read() { table_id: 1, row_id: 1, }, - data: "World".to_string(), + data: "Second".to_string(), }; db.update(tx3, tx3_row).unwrap(); db.commit_tx(tx3).unwrap(); @@ -436,6 +436,18 @@ fn test_fuzzy_read() { .unwrap() .unwrap(); assert_eq!(tx1_row, row); + + // T2 tries to update the row, but fails because T3 has already committed an update to the row, + // so T2 trying to write would violate snapshot isolation if it succeeded. + let tx2_newrow = Row { + id: RowID { + table_id: 1, + row_id: 1, + }, + data: "Third".to_string(), + }; + let update_result = db.update(tx2, tx2_newrow); + assert_eq!(Err(DatabaseError::WriteWriteConflict), update_result); } #[test] diff --git a/core/translate/expr.rs b/core/translate/expr.rs index c23cb053a..8ddb580df 100644 --- a/core/translate/expr.rs +++ b/core/translate/expr.rs @@ -975,7 +975,7 @@ pub fn translate_expr( translate_function( program, - &args, + args, referenced_tables, resolver, target_register, @@ -1017,6 +1017,17 @@ pub fn translate_expr( }); Ok(target_register) } + JsonFunc::JsonQuote => { + let args = expect_arguments_exact!(args, 1, j); + translate_function( + program, + args, + referenced_tables, + resolver, + target_register, + func_ctx, + ) + } JsonFunc::JsonPretty => { let args = expect_arguments_max!(args, 2, j); diff --git a/core/vdbe/mod.rs b/core/vdbe/mod.rs index 6403d2565..3ccfdc1e3 100644 --- a/core/vdbe/mod.rs +++ b/core/vdbe/mod.rs @@ -49,7 +49,7 @@ use crate::{ function::JsonFunc, json::get_json, json::is_json_valid, json::json_array, json::json_array_length, json::json_arrow_extract, json::json_arrow_shift_extract, json::json_error_position, json::json_extract, json::json_object, json::json_patch, - json::json_remove, json::json_set, json::json_type, + json::json_quote, json::json_remove, json::json_set, json::json_type, }; use crate::{resolve_ext_path, Connection, Result, TransactionState, DATABASE_VERSION}; use insn::{ @@ -1973,6 +1973,14 @@ impl Program { Err(e) => return Err(e), } } + JsonFunc::JsonQuote => { + let json_value = &state.registers[*start_reg]; + + match json_quote(json_value) { + Ok(result) => state.registers[*dest] = result, + Err(e) => return Err(e), + } + } }, crate::function::Func::Scalar(scalar_func) => match scalar_func { ScalarFunc::Cast => { diff --git a/simulator/Cargo.toml b/simulator/Cargo.toml index 43956e5e2..0c2a355a1 100644 --- a/simulator/Cargo.toml +++ b/simulator/Cargo.toml @@ -21,6 +21,8 @@ rand_chacha = "0.3.1" log = "0.4.20" tempfile = "3.0.7" env_logger = "0.10.1" +regex = "1.11.1" +regex-syntax = { version = "0.8.5", default-features = false, features = ["unicode"] } anarchist-readable-name-generator-lib = "0.1.2" clap = { version = "4.5", features = ["derive"] } serde = { version = "1.0", features = ["derive"] } diff --git a/simulator/generation/mod.rs b/simulator/generation/mod.rs index 04b5ee03d..ac7defd54 100644 --- a/simulator/generation/mod.rs +++ b/simulator/generation/mod.rs @@ -25,6 +25,13 @@ pub trait ArbitraryFrom { fn arbitrary_from(rng: &mut R, t: T) -> Self; } +/// ArbitraryFromMaybe trait for fallibally generating random values from a given value +pub trait ArbitraryFromMaybe { + fn arbitrary_from_maybe(rng: &mut R, t: T) -> Option + where + Self: Sized; +} + /// Frequency is a helper function for composing different generators with different frequency /// of occurences. /// The type signature for the `N` parameter is a bit complex, but it @@ -60,6 +67,36 @@ pub(crate) fn one_of<'a, T, R: Rng>(choices: Vec T + 'a>>, choices[index](rng) } +/// backtrack is a helper function for composing different "failable" generators. +/// The function takes a list of functions that return an Option, along with number of retries +/// to make before giving up. +pub(crate) fn backtrack<'a, T, R: Rng>( + mut choices: Vec<(u32, Box Option + 'a>)>, + rng: &mut R, +) -> T { + loop { + // If there are no more choices left, we give up + let choices_ = choices + .iter() + .enumerate() + .filter(|(_, (retries, _))| *retries > 0) + .collect::>(); + if choices_.is_empty() { + panic!("backtrack: no more choices left"); + } + // Run a one_of on the remaining choices + let (choice_index, choice) = pick(&choices_, rng); + let choice_index = *choice_index; + // If the choice returns None, we decrement the number of retries and try again + let result = choice.1(rng); + if let Some(result) = result { + return result; + } else { + choices[choice_index].0 -= 1; + } + } +} + /// pick is a helper function for uniformly picking a random element from a slice pub(crate) fn pick<'a, T, R: Rng>(choices: &'a [T], rng: &mut R) -> &'a T { let index = rng.gen_range(0..choices.len()); diff --git a/simulator/generation/plan.rs b/simulator/generation/plan.rs index b0c61488d..d06cdfff3 100644 --- a/simulator/generation/plan.rs +++ b/simulator/generation/plan.rs @@ -5,7 +5,7 @@ use serde::{Deserialize, Serialize}; use crate::{ model::{ - query::{Create, Insert, Query, Select}, + query::{Create, Delete, Distinctness, Insert, Query, Select}, table::Value, }, runner::env::SimConnection, @@ -14,10 +14,7 @@ use crate::{ use crate::generation::{frequency, Arbitrary, ArbitraryFrom}; -use super::{ - pick, - property::{remaining, Property}, -}; +use super::property::{remaining, Property}; pub(crate) type ResultSet = Result>>; @@ -261,7 +258,7 @@ impl Interactions { match self { Interactions::Property(property) => { match property { - Property::InsertSelect { + Property::InsertValuesSelect { insert, row_index: _, queries, @@ -282,6 +279,32 @@ impl Interactions { query.shadow(env); } } + Property::SelectLimit { select } => { + select.shadow(env); + } + Property::DeleteSelect { + table, + predicate, + queries, + } => { + let delete = Query::Delete(Delete { + table: table.clone(), + predicate: predicate.clone(), + }); + + let select = Query::Select(Select { + table: table.clone(), + predicate: predicate.clone(), + distinct: Distinctness::All, + limit: None, + }); + + delete.shadow(env); + for query in queries { + query.shadow(env); + } + select.shadow(env); + } } for interaction in property.interactions() { match interaction { @@ -292,14 +315,26 @@ impl Interactions { } } Query::Insert(insert) => { + let values = match &insert { + Insert::Values { values, .. } => values.clone(), + Insert::Select { select, .. } => select.shadow(env), + }; let table = env .tables .iter_mut() - .find(|t| t.name == insert.table) + .find(|t| t.name == insert.table()) .unwrap(); - table.rows.extend(insert.values.clone()); + table.rows.extend(values); + } + Query::Delete(delete) => { + let table = env + .tables + .iter_mut() + .find(|t| t.name == delete.table) + .unwrap(); + let t2 = &table.clone(); + table.rows.retain_mut(|r| delete.predicate.test(r, t2)); } - Query::Delete(_) => todo!(), Query::Select(_) => {} }, Interaction::Assertion(_) => {} @@ -308,7 +343,9 @@ impl Interactions { } } } - Interactions::Query(query) => query.shadow(env), + Interactions::Query(query) => { + query.shadow(env); + } Interactions::Fault(_) => {} } } @@ -389,12 +426,10 @@ impl ArbitraryFrom<&mut SimulatorEnv> for InteractionPlan { } impl Interaction { - pub(crate) fn shadow(&self, env: &mut SimulatorEnv) { + pub(crate) fn shadow(&self, env: &mut SimulatorEnv) -> Vec> { match self { Self::Query(query) => query.shadow(env), - Self::Assumption(_) => {} - Self::Assertion(_) => {} - Self::Fault(_) => {} + Self::Assumption(_) | Self::Assertion(_) | Self::Fault(_) => vec![], } } pub(crate) fn execute_query(&self, conn: &mut Rc) -> ResultSet { @@ -547,12 +582,11 @@ fn create_table(rng: &mut R, _env: &SimulatorEnv) -> Interactions } fn random_read(rng: &mut R, env: &SimulatorEnv) -> Interactions { - Interactions::Query(Query::Select(Select::arbitrary_from(rng, &env.tables))) + Interactions::Query(Query::Select(Select::arbitrary_from(rng, env))) } fn random_write(rng: &mut R, env: &SimulatorEnv) -> Interactions { - let table = pick(&env.tables, rng); - let insert_query = Query::Insert(Insert::arbitrary_from(rng, table)); + let insert_query = Query::Insert(Insert::arbitrary_from(rng, env)); Interactions::Query(insert_query) } diff --git a/simulator/generation/property.rs b/simulator/generation/property.rs index bfa1e1ed5..fe454e9fd 100644 --- a/simulator/generation/property.rs +++ b/simulator/generation/property.rs @@ -3,7 +3,7 @@ use serde::{Deserialize, Serialize}; use crate::{ model::{ - query::{Create, Delete, Insert, Predicate, Query, Select}, + query::{Create, Delete, Distinctness, Insert, Predicate, Query, Select}, table::Value, }, runner::env::SimulatorEnv, @@ -34,7 +34,7 @@ pub(crate) enum Property { /// - The inserted row will not be deleted. /// - The inserted row will not be updated. /// - The table `t` will not be renamed, dropped, or altered. - InsertSelect { + InsertValuesSelect { /// The insert query insert: Insert, /// Selected row index @@ -62,13 +62,47 @@ pub(crate) enum Property { /// Additional interactions in the middle of the property queries: Vec, }, + /// Select Limit is a property in which the select query + /// has a limit clause that is respected by the query. + /// The execution of the property is as follows + /// SELECT * FROM WHERE LIMIT + /// This property is a single-interaction property. + /// The interaction has the following constraints; + /// - The select query will respect the limit clause. + SelectLimit { + /// The select query + select: Select, + }, + /// Delete-Select is a property in which the deleted row + /// must not be in the resulting rows of a select query that has a + /// where clause that matches the deleted row. In practice, `p1` of + /// the delete query will be used as the predicate for the select query, + /// hence the select should return NO ROWS. + /// The execution of the property is as follows + /// DELETE FROM WHERE + /// I_0 + /// I_1 + /// ... + /// I_n + /// SELECT * FROM WHERE + /// The interactions in the middle has the following constraints; + /// - There will be no errors in the middle interactions. + /// - A row that holds for the predicate will not be inserted. + /// - The table `t` will not be renamed, dropped, or altered. + DeleteSelect { + table: String, + predicate: Predicate, + queries: Vec, + }, } impl Property { pub(crate) fn name(&self) -> String { match self { - Property::InsertSelect { .. } => "Insert-Select".to_string(), + Property::InsertValuesSelect { .. } => "Insert-Values-Select".to_string(), Property::DoubleCreateFailure { .. } => "Double-Create-Failure".to_string(), + Property::SelectLimit { .. } => "Select-Limit".to_string(), + Property::DeleteSelect { .. } => "Delete-Select".to_string(), } } /// interactions construct a list of interactions, which is an executable representation of the property. @@ -76,26 +110,33 @@ impl Property { /// and `interaction` cannot be serialized directly. pub(crate) fn interactions(&self) -> Vec { match self { - Property::InsertSelect { + Property::InsertValuesSelect { insert, row_index, queries, select, } => { + let (table, values) = if let Insert::Values { table, values } = insert { + (table, values) + } else { + unreachable!( + "insert query should be Insert::Values for Insert-Values-Select property" + ) + }; // Check that the insert query has at least 1 value assert!( - !insert.values.is_empty(), + !values.is_empty(), "insert query should have at least 1 value" ); // Pick a random row within the insert values - let row = insert.values[*row_index].clone(); + let row = values[*row_index].clone(); // Assume that the table exists let assumption = Interaction::Assumption(Assertion { - message: format!("table {} exists", insert.table), + message: format!("table {} exists", insert.table()), func: Box::new({ - let table_name = insert.table.clone(); + let table_name = table.clone(); move |_: &Vec, env: &SimulatorEnv| { Ok(env.tables.iter().any(|t| t.name == table_name)) } @@ -106,7 +147,7 @@ impl Property { message: format!( "row [{:?}] not found in table {}", row.iter().map(|v| v.to_string()).collect::>(), - insert.table, + insert.table(), ), func: Box::new(move |stack: &Vec, _: &SimulatorEnv| { let rows = stack.last().unwrap(); @@ -162,12 +203,97 @@ impl Property { interactions.push(cq2); interactions.push(assertion); + interactions + } + Property::SelectLimit { select } => { + let table_name = select.table.clone(); + + let assumption = Interaction::Assumption(Assertion { + message: format!("table {} exists", table_name), + func: Box::new({ + let table_name = table_name.clone(); + move |_: &Vec, env: &SimulatorEnv| { + Ok(env.tables.iter().any(|t| t.name == table_name)) + } + }), + }); + + let limit = select + .limit + .expect("Property::SelectLimit without a LIMIT clause"); + + let assertion = Interaction::Assertion(Assertion { + message: "select query should respect the limit clause".to_string(), + func: Box::new(move |stack: &Vec, _: &SimulatorEnv| { + let last = stack.last().unwrap(); + match last { + Ok(rows) => Ok(limit >= rows.len()), + Err(_) => Ok(true), + } + }), + }); + + vec![ + assumption, + Interaction::Query(Query::Select(select.clone())), + assertion, + ] + } + Property::DeleteSelect { + table, + predicate, + queries, + } => { + let assumption = Interaction::Assumption(Assertion { + message: format!("table {} exists", table), + func: Box::new({ + let table = table.clone(); + move |_: &Vec, env: &SimulatorEnv| { + Ok(env.tables.iter().any(|t| t.name == table)) + } + }), + }); + + let assertion = Interaction::Assertion(Assertion { + message: format!( + "select '{}' should return no values for table '{}'", + predicate, table, + ), + func: Box::new(move |stack: &Vec, _: &SimulatorEnv| { + let rows = stack.last().unwrap(); + match rows { + Ok(rows) => Ok(rows.is_empty()), + Err(err) => Err(LimboError::InternalError(err.to_string())), + } + }), + }); + + let delete = Interaction::Query(Query::Delete(Delete { + table: table.clone(), + predicate: predicate.clone(), + })); + + let select = Interaction::Query(Query::Select(Select { + table: table.clone(), + predicate: predicate.clone(), + limit: None, + distinct: Distinctness::All, + })); + + let mut interactions = Vec::new(); + interactions.push(assumption); + interactions.push(delete); + interactions.extend(queries.clone().into_iter().map(Interaction::Query)); + interactions.push(select); + interactions.push(assertion); + interactions } } } } +#[derive(Debug)] pub(crate) struct Remaining { pub(crate) read: f64, pub(crate) write: f64, @@ -192,7 +318,7 @@ pub(crate) fn remaining(env: &SimulatorEnv, stats: &InteractionStats) -> Remaini } } -fn property_insert_select( +fn property_insert_values_select( rng: &mut R, env: &SimulatorEnv, remaining: &Remaining, @@ -209,7 +335,7 @@ fn property_insert_select( let row = rows[row_index].clone(); // Insert the rows - let insert_query = Insert { + let insert_query = Insert::Values { table: table.name.clone(), values: rows, }; @@ -221,7 +347,7 @@ fn property_insert_select( // - [ ] The inserted row will not be updated. (todo: add this constraint once UPDATE is implemented) // - [ ] The table `t` will not be renamed, dropped, or altered. (todo: add this constraint once ALTER or DROP is implemented) for _ in 0..rng.gen_range(0..3) { - let query = Query::arbitrary_from(rng, (table, remaining)); + let query = Query::arbitrary_from(rng, (env, remaining)); match &query { Query::Delete(Delete { table: t, @@ -248,9 +374,11 @@ fn property_insert_select( let select_query = Select { table: table.name.clone(), predicate: Predicate::arbitrary_from(rng, (table, &row)), + limit: None, + distinct: Distinctness::All, }; - Property::InsertSelect { + Property::InsertValuesSelect { insert: insert_query, row_index, queries, @@ -258,6 +386,19 @@ fn property_insert_select( } } +fn property_select_limit(rng: &mut R, env: &SimulatorEnv) -> Property { + // Get a random table + let table = pick(&env.tables, rng); + // Select the table + let select = Select { + table: table.name.clone(), + predicate: Predicate::arbitrary_from(rng, table), + limit: Some(rng.gen_range(1..=5)), + distinct: Distinctness::All, + }; + Property::SelectLimit { select } +} + fn property_double_create_failure( rng: &mut R, env: &SimulatorEnv, @@ -276,7 +417,7 @@ fn property_double_create_failure( // - [x] There will be no errors in the middle interactions.(best effort) // - [ ] Table `t` will not be renamed or dropped.(todo: add this constraint once ALTER or DROP is implemented) for _ in 0..rng.gen_range(0..3) { - let query = Query::arbitrary_from(rng, (table, remaining)); + let query = Query::arbitrary_from(rng, (env, remaining)); match &query { Query::Create(Create { table: t }) => { // There will be no errors in the middle interactions. @@ -296,6 +437,48 @@ fn property_double_create_failure( } } +fn property_delete_select( + rng: &mut R, + env: &SimulatorEnv, + remaining: &Remaining, +) -> Property { + // Get a random table + let table = pick(&env.tables, rng); + // Generate a random predicate + let predicate = Predicate::arbitrary_from(rng, table); + + // Create random queries respecting the constraints + let mut queries = Vec::new(); + // - [x] There will be no errors in the middle interactions. (this constraint is impossible to check, so this is just best effort) + // - [x] A row that holds for the predicate will not be inserted. + // - [ ] The table `t` will not be renamed, dropped, or altered. (todo: add this constraint once ALTER or DROP is implemented) + for _ in 0..rng.gen_range(0..3) { + let query = Query::arbitrary_from(rng, (env, remaining)); + match &query { + Query::Insert(Insert::Values { table: t, values }) => { + // A row that holds for the predicate will not be inserted. + if t == &table.name && values.iter().any(|v| predicate.test(v, table)) { + continue; + } + } + Query::Create(Create { table: t }) => { + // There will be no errors in the middle interactions. + // - Creating the same table is an error + if t.name == table.name { + continue; + } + } + _ => (), + } + queries.push(query); + } + + Property::DeleteSelect { + table: table.name.clone(), + predicate, + queries, + } +} impl ArbitraryFrom<(&SimulatorEnv, &InteractionStats)> for Property { fn arbitrary_from( rng: &mut R, @@ -306,12 +489,20 @@ impl ArbitraryFrom<(&SimulatorEnv, &InteractionStats)> for Property { vec![ ( f64::min(remaining_.read, remaining_.write), - Box::new(|rng: &mut R| property_insert_select(rng, env, &remaining_)), + Box::new(|rng: &mut R| property_insert_values_select(rng, env, &remaining_)), ), ( remaining_.create / 2.0, Box::new(|rng: &mut R| property_double_create_failure(rng, env, &remaining_)), ), + ( + remaining_.read, + Box::new(|rng: &mut R| property_select_limit(rng, env)), + ), + ( + f64::min(remaining_.read, remaining_.write), + Box::new(|rng: &mut R| property_delete_select(rng, env, &remaining_)), + ), ], rng, ) diff --git a/simulator/generation/query.rs b/simulator/generation/query.rs index 8b93fa993..7624367a5 100644 --- a/simulator/generation/query.rs +++ b/simulator/generation/query.rs @@ -1,13 +1,15 @@ use crate::generation::table::{GTValue, LTValue}; use crate::generation::{one_of, Arbitrary, ArbitraryFrom}; -use crate::model::query::{Create, Delete, Insert, Predicate, Query, Select}; +use crate::model::query::{Create, Delete, Distinctness, Insert, Predicate, Query, Select}; use crate::model::table::{Table, Value}; +use crate::SimulatorEnv; use rand::seq::SliceRandom as _; use rand::Rng; use super::property::Remaining; -use super::{frequency, pick}; +use super::table::LikeValue; +use super::{backtrack, frequency, pick, ArbitraryFromMaybe}; impl Arbitrary for Create { fn arbitrary(rng: &mut R) -> Self { @@ -17,79 +19,85 @@ impl Arbitrary for Create { } } -impl ArbitraryFrom<&Vec> for Select { - fn arbitrary_from(rng: &mut R, tables: &Vec
) -> Self { - let table = pick(tables, rng); +impl ArbitraryFrom<&SimulatorEnv> for Select { + fn arbitrary_from(rng: &mut R, env: &SimulatorEnv) -> Self { + let table = pick(&env.tables, rng); Self { table: table.name.clone(), predicate: Predicate::arbitrary_from(rng, table), + limit: Some(rng.gen_range(0..=1000)), + distinct: Distinctness::All, } } } -impl ArbitraryFrom<&Vec<&Table>> for Select { - fn arbitrary_from(rng: &mut R, tables: &Vec<&Table>) -> Self { - let table = pick(tables, rng); - Self { - table: table.name.clone(), - predicate: Predicate::arbitrary_from(rng, *table), - } - } -} - -impl ArbitraryFrom<&Table> for Insert { - fn arbitrary_from(rng: &mut R, table: &Table) -> Self { - let num_rows = rng.gen_range(1..10); - let values: Vec> = (0..num_rows) - .map(|_| { - table - .columns - .iter() - .map(|c| Value::arbitrary_from(rng, &c.column_type)) - .collect() +impl ArbitraryFrom<&SimulatorEnv> for Insert { + fn arbitrary_from(rng: &mut R, env: &SimulatorEnv) -> Self { + let gen_values = |rng: &mut R| { + let table = pick(&env.tables, rng); + let num_rows = rng.gen_range(1..10); + let values: Vec> = (0..num_rows) + .map(|_| { + table + .columns + .iter() + .map(|c| Value::arbitrary_from(rng, &c.column_type)) + .collect() + }) + .collect(); + Some(Insert::Values { + table: table.name.clone(), + values, }) - .collect(); - Self { - table: table.name.clone(), - values, - } - } -} + }; -impl ArbitraryFrom<&Table> for Delete { - fn arbitrary_from(rng: &mut R, table: &Table) -> Self { - Self { - table: table.name.clone(), - predicate: Predicate::arbitrary_from(rng, table), - } - } -} + let _gen_select = |rng: &mut R| { + // Find a non-empty table + let table = env.tables.iter().find(|t| !t.rows.is_empty()); + if table.is_none() { + return None; + } -impl ArbitraryFrom<&Table> for Query { - fn arbitrary_from(rng: &mut R, table: &Table) -> Self { - frequency( + let select_table = table.unwrap(); + let row = pick(&select_table.rows, rng); + let predicate = Predicate::arbitrary_from(rng, (select_table, row)); + // Pick another table to insert into + let select = Select { + table: select_table.name.clone(), + predicate, + limit: None, + distinct: Distinctness::All, + }; + let table = pick(&env.tables, rng); + Some(Insert::Select { + table: table.name.clone(), + select: Box::new(select), + }) + }; + + backtrack( vec![ - (1, Box::new(|rng| Self::Create(Create::arbitrary(rng)))), - ( - 100, - Box::new(|rng| Self::Select(Select::arbitrary_from(rng, &vec![table]))), - ), - ( - 100, - Box::new(|rng| Self::Insert(Insert::arbitrary_from(rng, table))), - ), - ( - 0, - Box::new(|rng| Self::Delete(Delete::arbitrary_from(rng, table))), - ), + (1, Box::new(|rng| gen_values(rng))), + // todo: test and enable this once `INSERT INTO
SELECT * FROM
` is supported + // (1, Box::new(|rng| gen_select(rng))), ], rng, ) } } -impl ArbitraryFrom<(&Table, &Remaining)> for Query { - fn arbitrary_from(rng: &mut R, (table, remaining): (&Table, &Remaining)) -> Self { +impl ArbitraryFrom<&SimulatorEnv> for Delete { + fn arbitrary_from(rng: &mut R, env: &SimulatorEnv) -> Self { + let table = pick(&env.tables, rng); + Self { + table: table.name.clone(), + predicate: Predicate::arbitrary_from(rng, table), + } + } +} + +impl ArbitraryFrom<(&SimulatorEnv, &Remaining)> for Query { + fn arbitrary_from(rng: &mut R, (env, remaining): (&SimulatorEnv, &Remaining)) -> Self { frequency( vec![ ( @@ -98,15 +106,15 @@ impl ArbitraryFrom<(&Table, &Remaining)> for Query { ), ( remaining.read, - Box::new(|rng| Self::Select(Select::arbitrary_from(rng, &vec![table]))), + Box::new(|rng| Self::Select(Select::arbitrary_from(rng, env))), ), ( remaining.write, - Box::new(|rng| Self::Insert(Insert::arbitrary_from(rng, table))), + Box::new(|rng| Self::Insert(Insert::arbitrary_from(rng, env))), ), ( - 0.0, - Box::new(|rng| Self::Delete(Delete::arbitrary_from(rng, table))), + remaining.write, + Box::new(|rng| Self::Delete(Delete::arbitrary_from(rng, env))), ), ], rng, @@ -280,24 +288,48 @@ fn produce_true_predicate(rng: &mut R, (t, row): (&Table, &Vec)) let column_index = rng.gen_range(0..t.columns.len()); let column = &t.columns[column_index]; let value = &row[column_index]; - one_of( + backtrack( vec![ - Box::new(|_| Predicate::Eq(column.name.clone(), value.clone())), - Box::new(|rng| { - let v = loop { + ( + 1, + Box::new(|_| Some(Predicate::Eq(column.name.clone(), value.clone()))), + ), + ( + 1, + Box::new(|rng| { let v = Value::arbitrary_from(rng, &column.column_type); - if &v != value { - break v; + if &v == value { + None + } else { + Some(Predicate::Neq(column.name.clone(), v)) } - }; - Predicate::Neq(column.name.clone(), v) - }), - Box::new(|rng| { - Predicate::Gt(column.name.clone(), LTValue::arbitrary_from(rng, value).0) - }), - Box::new(|rng| { - Predicate::Lt(column.name.clone(), GTValue::arbitrary_from(rng, value).0) - }), + }), + ), + ( + 1, + Box::new(|rng| { + Some(Predicate::Gt( + column.name.clone(), + LTValue::arbitrary_from(rng, value).0, + )) + }), + ), + ( + 1, + Box::new(|rng| { + Some(Predicate::Lt( + column.name.clone(), + GTValue::arbitrary_from(rng, value).0, + )) + }), + ), + ( + 1, + Box::new(|rng| { + LikeValue::arbitrary_from_maybe(rng, value) + .map(|like| Predicate::Like(column.name.clone(), like.0)) + }), + ), ], rng, ) diff --git a/simulator/generation/table.rs b/simulator/generation/table.rs index 258cd1483..415b4b03a 100644 --- a/simulator/generation/table.rs +++ b/simulator/generation/table.rs @@ -3,6 +3,8 @@ use rand::Rng; use crate::generation::{gen_random_text, pick, readable_name_custom, Arbitrary, ArbitraryFrom}; use crate::model::table::{Column, ColumnType, Name, Table, Value}; +use super::ArbitraryFromMaybe; + impl Arbitrary for Name { fn arbitrary(rng: &mut R) -> Self { let name = readable_name_custom("_", rng); @@ -194,3 +196,34 @@ impl ArbitraryFrom<&Value> for GTValue { } } } + +pub(crate) struct LikeValue(pub(crate) String); + +impl ArbitraryFromMaybe<&Value> for LikeValue { + fn arbitrary_from_maybe(rng: &mut R, value: &Value) -> Option { + match value { + Value::Text(t) => { + let mut t = t.chars().collect::>(); + // Remove a number of characters, either insert `_` for each character removed, or + // insert one `%` for the whole substring + let mut i = 0; + while i < t.len() { + if rng.gen_bool(0.1) { + t[i] = '_'; + } else if rng.gen_bool(0.05) { + t[i] = '%'; + // skip a list of characters + for _ in 0..rng.gen_range(0..=3.min(t.len() - i - 1)) { + t.remove(i + 1); + } + } + i += 1; + } + let index = rng.gen_range(0..t.len()); + t.insert(index, '%'); + Some(Self(t.into_iter().collect())) + } + _ => None, + } + } +} diff --git a/simulator/model/query.rs b/simulator/model/query.rs index f03bbde6f..9d3b3d404 100644 --- a/simulator/model/query.rs +++ b/simulator/model/query.rs @@ -1,5 +1,6 @@ use std::fmt::Display; +use regex::{Regex, RegexBuilder}; use serde::{Deserialize, Serialize}; use crate::{ @@ -9,12 +10,48 @@ use crate::{ #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] pub(crate) enum Predicate { - And(Vec), // p1 AND p2 AND p3... AND pn - Or(Vec), // p1 OR p2 OR p3... OR pn - Eq(String, Value), // column = Value - Neq(String, Value), // column != Value - Gt(String, Value), // column > Value - Lt(String, Value), // column < Value + And(Vec), // p1 AND p2 AND p3... AND pn + Or(Vec), // p1 OR p2 OR p3... OR pn + Eq(String, Value), // column = Value + Neq(String, Value), // column != Value + Gt(String, Value), // column > Value + Lt(String, Value), // column < Value + Like(String, String), // column LIKE Value +} + +/// This function is a duplication of the exec_like function in core/vdbe/mod.rs at commit 9b9d5f9b4c9920e066ef1237c80878f4c3968524 +/// Any updates to the original function should be reflected here, otherwise the test will be incorrect. +fn construct_like_regex(pattern: &str) -> Regex { + let mut regex_pattern = String::with_capacity(pattern.len() * 2); + + regex_pattern.push('^'); + + for c in pattern.chars() { + match c { + '\\' => regex_pattern.push_str("\\\\"), + '%' => regex_pattern.push_str(".*"), + '_' => regex_pattern.push('.'), + ch => { + if regex_syntax::is_meta_character(c) { + regex_pattern.push('\\'); + } + regex_pattern.push(ch); + } + } + } + + regex_pattern.push('$'); + + RegexBuilder::new(®ex_pattern) + .case_insensitive(true) + .dot_matches_new_line(true) + .build() + .unwrap() +} + +fn exec_like(pattern: &str, text: &str) -> bool { + let re = construct_like_regex(pattern); + re.is_match(text) } impl Predicate { @@ -43,6 +80,9 @@ impl Predicate { Predicate::Neq(column, value) => get_value(column) != Some(value), Predicate::Gt(column, value) => get_value(column).map(|v| v > value).unwrap_or(false), Predicate::Lt(column, value) => get_value(column).map(|v| v < value).unwrap_or(false), + Predicate::Like(column, value) => get_value(column) + .map(|v| exec_like(v.to_string().as_str(), value.as_str())) + .unwrap_or(false), } } } @@ -83,6 +123,7 @@ impl Display for Predicate { Self::Neq(name, value) => write!(f, "{} != {}", name, value), Self::Gt(name, value) => write!(f, "{} > {}", name, value), Self::Lt(name, value) => write!(f, "{} < {}", name, value), + Self::Like(name, value) => write!(f, "{} LIKE '{}'", name, value), } } } @@ -101,7 +142,8 @@ impl Query { match self { Query::Create(_) => vec![], Query::Select(Select { table, .. }) - | Query::Insert(Insert { table, .. }) + | Query::Insert(Insert::Select { table, .. }) + | Query::Insert(Insert::Values { table, .. }) | Query::Delete(Delete { table, .. }) => vec![table.clone()], } } @@ -109,12 +151,13 @@ impl Query { match self { Query::Create(Create { table }) => vec![table.name.clone()], Query::Select(Select { table, .. }) - | Query::Insert(Insert { table, .. }) + | Query::Insert(Insert::Select { table, .. }) + | Query::Insert(Insert::Values { table, .. }) | Query::Delete(Delete { table, .. }) => vec![table.clone()], } } - pub(crate) fn shadow(&self, env: &mut SimulatorEnv) { + pub(crate) fn shadow(&self, env: &mut SimulatorEnv) -> Vec> { match self { Query::Create(create) => create.shadow(env), Query::Insert(insert) => insert.shadow(env), @@ -129,33 +172,110 @@ pub(crate) struct Create { } impl Create { - pub(crate) fn shadow(&self, env: &mut SimulatorEnv) { + pub(crate) fn shadow(&self, env: &mut SimulatorEnv) -> Vec> { if !env.tables.iter().any(|t| t.name == self.table.name) { env.tables.push(self.table.clone()); } + + vec![] } } +impl Display for Create { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "CREATE TABLE {} (", self.table.name)?; + + for (i, column) in self.table.columns.iter().enumerate() { + if i != 0 { + write!(f, ",")?; + } + write!(f, "{} {}", column.name, column.column_type)?; + } + + write!(f, ")") + } +} + +/// `SELECT` distinctness +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub enum Distinctness { + /// `DISTINCT` + Distinct, + /// `ALL` + All, +} + #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] pub(crate) struct Select { pub(crate) table: String, pub(crate) predicate: Predicate, + pub(crate) distinct: Distinctness, + pub(crate) limit: Option, } impl Select { - pub(crate) fn shadow(&self, _env: &mut SimulatorEnv) {} + pub(crate) fn shadow(&self, env: &mut SimulatorEnv) -> Vec> { + let table = env.tables.iter().find(|t| t.name == self.table.as_str()); + if let Some(table) = table { + table + .rows + .iter() + .filter(|row| self.predicate.test(row, table)) + .cloned() + .collect() + } else { + vec![] + } + } +} + +impl Display for Select { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "SELECT * FROM {} WHERE {}{}", + self.table, + self.predicate, + self.limit + .map_or("".to_string(), |l| format!(" LIMIT {}", l)) + ) + } } #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] -pub(crate) struct Insert { - pub(crate) table: String, - pub(crate) values: Vec>, +pub(crate) enum Insert { + Values { + table: String, + values: Vec>, + }, + Select { + table: String, + select: Box