add Generation context trait to decouple Simulator specific code

This commit is contained in:
pedrocarlo
2025-08-25 20:11:01 -03:00
parent 642060f283
commit 0c1228b484
21 changed files with 223 additions and 2609 deletions

View File

@@ -3,6 +3,8 @@ pub mod fmt;
use strum_macros::{EnumIter, EnumString};
use crate::ast::fmt::ToTokens;
/// `?` or `$` Prepared statement arg placeholder(s)
#[derive(Default)]
pub struct ParameterInfo {
@@ -1188,7 +1190,7 @@ bitflags::bitflags! {
}
/// Sort orders
#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum SortOrder {
/// `ASC`
@@ -1197,6 +1199,12 @@ pub enum SortOrder {
Desc,
}
impl core::fmt::Display for SortOrder {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.to_fmt(f)
}
}
/// `NULLS FIRST` or `NULLS LAST`
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]

View File

@@ -5,7 +5,7 @@ use turso_parser::ast::{
use crate::{
generation::{
frequency, gen_random_text, one_of, pick, pick_index, Arbitrary, ArbitraryFrom,
ArbitrarySizedFrom,
ArbitrarySizedFrom, GenerationContext,
},
model::table::SimValue,
};
@@ -58,8 +58,8 @@ where
}
// Freestyling generation
impl ArbitrarySizedFrom<&SimulatorEnv> for Expr {
fn arbitrary_sized_from<R: rand::Rng>(rng: &mut R, t: &SimulatorEnv, size: usize) -> Self {
impl<C: GenerationContext> ArbitrarySizedFrom<&C> for Expr {
fn arbitrary_sized_from<R: rand::Rng>(rng: &mut R, t: &C, size: usize) -> Self {
frequency(
vec![
(
@@ -199,28 +199,11 @@ impl Arbitrary for Type {
}
}
struct CollateName(String);
impl Arbitrary for CollateName {
fn arbitrary<R: rand::Rng>(rng: &mut R) -> Self {
let choice = rng.random_range(0..3);
CollateName(
match choice {
0 => "BINARY",
1 => "RTRIM",
2 => "NOCASE",
_ => unreachable!(),
}
.to_string(),
)
}
}
impl ArbitraryFrom<&SimulatorEnv> for QualifiedName {
fn arbitrary_from<R: rand::Rng>(rng: &mut R, t: &SimulatorEnv) -> Self {
impl<C: GenerationContext> ArbitraryFrom<&C> for QualifiedName {
fn arbitrary_from<R: rand::Rng>(rng: &mut R, t: &C) -> Self {
// TODO: for now just generate table name
let table_idx = pick_index(t.tables.len(), rng);
let table = &t.tables[table_idx];
let table_idx = pick_index(t.tables().len(), rng);
let table = &t.tables()[table_idx];
// TODO: for now forego alias
Self {
db_name: None,
@@ -230,8 +213,8 @@ impl ArbitraryFrom<&SimulatorEnv> for QualifiedName {
}
}
impl ArbitraryFrom<&SimulatorEnv> for LikeOperator {
fn arbitrary_from<R: rand::Rng>(rng: &mut R, _t: &SimulatorEnv) -> Self {
impl<C: GenerationContext> ArbitraryFrom<&C> for LikeOperator {
fn arbitrary_from<R: rand::Rng>(rng: &mut R, _t: &C) -> Self {
let choice = rng.random_range(0..4);
match choice {
0 => LikeOperator::Glob,
@@ -244,8 +227,8 @@ impl ArbitraryFrom<&SimulatorEnv> for LikeOperator {
}
// Current implementation does not take into account the columns affinity nor if table is Strict
impl ArbitraryFrom<&SimulatorEnv> for ast::Literal {
fn arbitrary_from<R: rand::Rng>(rng: &mut R, _t: &SimulatorEnv) -> Self {
impl<C: GenerationContext> ArbitraryFrom<&C> for ast::Literal {
fn arbitrary_from<R: rand::Rng>(rng: &mut R, _t: &C) -> Self {
loop {
let choice = rng.random_range(0..5);
let lit = match choice {
@@ -282,8 +265,8 @@ impl ArbitraryFrom<&Vec<&SimValue>> for ast::Expr {
}
}
impl ArbitraryFrom<&SimulatorEnv> for UnaryOperator {
fn arbitrary_from<R: rand::Rng>(rng: &mut R, _t: &SimulatorEnv) -> Self {
impl<C: GenerationContext> ArbitraryFrom<&C> for UnaryOperator {
fn arbitrary_from<R: rand::Rng>(rng: &mut R, _t: &C) -> Self {
let choice = rng.random_range(0..4);
match choice {
0 => Self::BitwiseNot,

View File

@@ -3,13 +3,24 @@ use std::{iter::Sum, ops::SubAssign};
use anarchist_readable_name_generator_lib::readable_name_custom;
use rand::{distr::uniform::SampleUniform, Rng};
mod expr;
pub mod plan;
mod predicate;
pub mod property;
use crate::model::table::Table;
pub mod expr;
pub mod predicate;
pub mod query;
pub mod table;
pub struct Opts {
/// Indexes enabled
indexes: bool,
}
/// Trait used to provide context to generation functions
pub trait GenerationContext {
fn tables(&self) -> &Vec<Table>;
fn opts(&self) -> &Opts;
}
type ArbitraryFromFunc<'a, R, T> = Box<dyn Fn(&mut R) -> T + 'a>;
type Choice<'a, R, T> = (usize, Box<dyn Fn(&mut R) -> Option<T> + 'a>);
@@ -66,11 +77,7 @@ pub trait ArbitraryFromMaybe<T> {
/// the operations we require for the implementation.
// todo: switch to a simpler type signature that can accommodate all integer and float types, which
// should be enough for our purposes.
pub(crate) fn frequency<
T,
R: Rng,
N: Sum + PartialOrd + Copy + Default + SampleUniform + SubAssign,
>(
pub fn frequency<T, R: Rng, N: Sum + PartialOrd + Copy + Default + SampleUniform + SubAssign>(
choices: Vec<(N, ArbitraryFromFunc<R, T>)>,
rng: &mut R,
) -> T {
@@ -88,7 +95,7 @@ pub(crate) fn frequency<
}
/// one_of is a helper function for composing different generators with equal probability of occurrence.
pub(crate) fn one_of<T, R: Rng>(choices: Vec<ArbitraryFromFunc<R, T>>, rng: &mut R) -> T {
pub fn one_of<T, R: Rng>(choices: Vec<ArbitraryFromFunc<R, T>>, rng: &mut R) -> T {
let index = rng.random_range(0..choices.len());
choices[index](rng)
}
@@ -96,7 +103,7 @@ pub(crate) fn one_of<T, R: Rng>(choices: Vec<ArbitraryFromFunc<R, T>>, rng: &mut
/// backtrack is a helper function for composing different "failable" generators.
/// The function takes a list of functions that return an Option<T>, along with number of retries
/// to make before giving up.
pub(crate) fn backtrack<T, R: Rng>(mut choices: Vec<Choice<R, T>>, rng: &mut R) -> Option<T> {
pub fn backtrack<T, R: Rng>(mut choices: Vec<Choice<R, T>>, rng: &mut R) -> Option<T> {
loop {
// If there are no more choices left, we give up
let choices_ = choices
@@ -122,24 +129,20 @@ pub(crate) fn backtrack<T, R: Rng>(mut choices: Vec<Choice<R, T>>, rng: &mut R)
}
/// 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 {
pub fn pick<'a, T, R: Rng>(choices: &'a [T], rng: &mut R) -> &'a T {
let index = rng.random_range(0..choices.len());
&choices[index]
}
/// pick_index is typically used for picking an index from a slice to later refer to the element
/// at that index.
pub(crate) fn pick_index<R: Rng>(choices: usize, rng: &mut R) -> usize {
pub fn pick_index<R: Rng>(choices: usize, rng: &mut R) -> usize {
rng.random_range(0..choices)
}
/// pick_n_unique is a helper function for uniformly picking N unique elements from a range.
/// The elements themselves are usize, typically representing indices.
pub(crate) fn pick_n_unique<R: Rng>(
range: std::ops::Range<usize>,
n: usize,
rng: &mut R,
) -> Vec<usize> {
pub fn pick_n_unique<R: Rng>(range: std::ops::Range<usize>, n: usize, rng: &mut R) -> Vec<usize> {
use rand::seq::SliceRandom;
let mut items: Vec<usize> = range.collect();
items.shuffle(rng);
@@ -148,7 +151,7 @@ pub(crate) fn pick_n_unique<R: Rng>(
/// gen_random_text uses `anarchist_readable_name_generator_lib` to generate random
/// readable names for tables, columns, text values etc.
pub(crate) fn gen_random_text<T: Rng>(rng: &mut T) -> String {
pub fn gen_random_text<T: Rng>(rng: &mut T) -> String {
let big_text = rng.random_ratio(1, 1000);
if big_text {
// let max_size: u64 = 2 * 1024 * 1024 * 1024;
@@ -164,3 +167,21 @@ pub(crate) fn gen_random_text<T: Rng>(rng: &mut T) -> String {
name.replace("-", "_")
}
}
pub fn pick_unique<T: ToOwned + PartialEq>(
items: &[T],
count: usize,
rng: &mut impl rand::Rng,
) -> Vec<T::Owned>
where
<T as ToOwned>::Owned: PartialEq,
{
let mut picked: Vec<T::Owned> = Vec::new();
while picked.len() < count {
let item = pick(items, rng);
if !picked.contains(&item.to_owned()) {
picked.push(item.to_owned());
}
}
picked
}

View File

@@ -1,833 +0,0 @@
use std::{
collections::HashSet,
fmt::{Debug, Display},
path::Path,
sync::Arc,
vec,
};
use serde::{Deserialize, Serialize};
use turso_core::{Connection, Result, StepResult};
use crate::{
generation::query::SelectFree,
model::{
query::{update::Update, Create, CreateIndex, Delete, Drop, Insert, Query, Select},
table::SimValue,
},
runner::{
env::{SimConnection, SimulationType, SimulatorTables},
io::SimulatorIO,
},
SimulatorEnv,
};
use crate::generation::{frequency, Arbitrary, ArbitraryFrom};
use super::property::{remaining, Property};
pub(crate) type ResultSet = Result<Vec<Vec<SimValue>>>;
#[derive(Clone, Serialize, Deserialize)]
pub(crate) struct InteractionPlan {
pub(crate) plan: Vec<Interactions>,
}
impl InteractionPlan {
/// Compute via diff computes a a plan from a given `.plan` file without the need to parse
/// sql. This is possible because there are two versions of the plan file, one that is human
/// readable and one that is serialized as JSON. Under watch mode, the users will be able to
/// delete interactions from the human readable file, and this function uses the JSON file as
/// a baseline to detect with interactions were deleted and constructs the plan from the
/// remaining interactions.
pub(crate) fn compute_via_diff(plan_path: &Path) -> Vec<Vec<Interaction>> {
let interactions = std::fs::read_to_string(plan_path).unwrap();
let interactions = interactions.lines().collect::<Vec<_>>();
let plan: InteractionPlan = serde_json::from_str(
std::fs::read_to_string(plan_path.with_extension("json"))
.unwrap()
.as_str(),
)
.unwrap();
let mut plan = plan
.plan
.into_iter()
.map(|i| i.interactions())
.collect::<Vec<_>>();
let (mut i, mut j) = (0, 0);
while i < interactions.len() && j < plan.len() {
if interactions[i].starts_with("-- begin")
|| interactions[i].starts_with("-- end")
|| interactions[i].is_empty()
{
i += 1;
continue;
}
// interactions[i] is the i'th line in the human readable plan
// plan[j][k] is the k'th interaction in the j'th property
let mut k = 0;
while k < plan[j].len() {
if i >= interactions.len() {
let _ = plan.split_off(j + 1);
let _ = plan[j].split_off(k);
break;
}
tracing::error!("Comparing '{}' with '{}'", interactions[i], plan[j][k]);
if interactions[i].contains(plan[j][k].to_string().as_str()) {
i += 1;
k += 1;
} else {
plan[j].remove(k);
panic!("Comparing '{}' with '{}'", interactions[i], plan[j][k]);
}
}
if plan[j].is_empty() {
plan.remove(j);
} else {
j += 1;
}
}
let _ = plan.split_off(j);
plan
}
}
pub(crate) struct InteractionPlanState {
pub(crate) stack: Vec<ResultSet>,
pub(crate) interaction_pointer: usize,
pub(crate) secondary_pointer: usize,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub(crate) enum Interactions {
Property(Property),
Query(Query),
Fault(Fault),
}
impl Interactions {
pub(crate) fn name(&self) -> Option<&str> {
match self {
Interactions::Property(property) => Some(property.name()),
Interactions::Query(_) => None,
Interactions::Fault(_) => None,
}
}
pub(crate) fn interactions(&self) -> Vec<Interaction> {
match self {
Interactions::Property(property) => property.interactions(),
Interactions::Query(query) => vec![Interaction::Query(query.clone())],
Interactions::Fault(fault) => vec![Interaction::Fault(fault.clone())],
}
}
}
impl Interactions {
pub(crate) fn dependencies(&self) -> HashSet<String> {
match self {
Interactions::Property(property) => {
property
.interactions()
.iter()
.fold(HashSet::new(), |mut acc, i| match i {
Interaction::Query(q) => {
acc.extend(q.dependencies());
acc
}
_ => acc,
})
}
Interactions::Query(query) => query.dependencies(),
Interactions::Fault(_) => HashSet::new(),
}
}
pub(crate) fn uses(&self) -> Vec<String> {
match self {
Interactions::Property(property) => {
property
.interactions()
.iter()
.fold(vec![], |mut acc, i| match i {
Interaction::Query(q) => {
acc.extend(q.uses());
acc
}
_ => acc,
})
}
Interactions::Query(query) => query.uses(),
Interactions::Fault(_) => vec![],
}
}
}
impl Display for InteractionPlan {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
for interactions in &self.plan {
match interactions {
Interactions::Property(property) => {
let name = property.name();
writeln!(f, "-- begin testing '{name}'")?;
for interaction in property.interactions() {
write!(f, "\t")?;
match interaction {
Interaction::Query(query) => writeln!(f, "{query};")?,
Interaction::Assumption(assumption) => {
writeln!(f, "-- ASSUME {};", assumption.name)?
}
Interaction::Assertion(assertion) => {
writeln!(f, "-- ASSERT {};", assertion.name)?
}
Interaction::Fault(fault) => writeln!(f, "-- FAULT '{fault}';")?,
Interaction::FsyncQuery(query) => {
writeln!(f, "-- FSYNC QUERY;")?;
writeln!(f, "{query};")?;
writeln!(f, "{query};")?
}
Interaction::FaultyQuery(query) => {
writeln!(f, "{query}; -- FAULTY QUERY")?
}
}
}
writeln!(f, "-- end testing '{name}'")?;
}
Interactions::Fault(fault) => {
writeln!(f, "-- FAULT '{fault}'")?;
}
Interactions::Query(query) => {
writeln!(f, "{query};")?;
}
}
}
Ok(())
}
}
#[derive(Debug, Clone, Copy)]
pub(crate) struct InteractionStats {
pub(crate) read_count: usize,
pub(crate) write_count: usize,
pub(crate) delete_count: usize,
pub(crate) update_count: usize,
pub(crate) create_count: usize,
pub(crate) create_index_count: usize,
pub(crate) drop_count: usize,
pub(crate) begin_count: usize,
pub(crate) commit_count: usize,
pub(crate) rollback_count: usize,
}
impl Display for InteractionStats {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"Read: {}, Write: {}, Delete: {}, Update: {}, Create: {}, CreateIndex: {}, Drop: {}, Begin: {}, Commit: {}, Rollback: {}",
self.read_count,
self.write_count,
self.delete_count,
self.update_count,
self.create_count,
self.create_index_count,
self.drop_count,
self.begin_count,
self.commit_count,
self.rollback_count,
)
}
}
#[derive(Debug)]
pub(crate) enum Interaction {
Query(Query),
Assumption(Assertion),
Assertion(Assertion),
Fault(Fault),
/// Will attempt to run any random query. However, when the connection tries to sync it will
/// close all connections and reopen the database and assert that no data was lost
FsyncQuery(Query),
FaultyQuery(Query),
}
impl Display for Interaction {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Query(query) => write!(f, "{query}"),
Self::Assumption(assumption) => write!(f, "ASSUME {}", assumption.name),
Self::Assertion(assertion) => write!(f, "ASSERT {}", assertion.name),
Self::Fault(fault) => write!(f, "FAULT '{fault}'"),
Self::FsyncQuery(query) => write!(f, "{query}"),
Self::FaultyQuery(query) => write!(f, "{query}; -- FAULTY QUERY"),
}
}
}
type AssertionFunc = dyn Fn(&Vec<ResultSet>, &mut SimulatorEnv) -> Result<Result<(), String>>;
enum AssertionAST {
Pick(),
}
pub(crate) struct Assertion {
pub(crate) func: Box<AssertionFunc>,
pub(crate) name: String, // For display purposes in the plan
}
impl Debug for Assertion {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Assertion")
.field("name", &self.name)
.finish()
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub(crate) enum Fault {
Disconnect,
ReopenDatabase,
}
impl Display for Fault {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Fault::Disconnect => write!(f, "DISCONNECT"),
Fault::ReopenDatabase => write!(f, "REOPEN_DATABASE"),
}
}
}
impl InteractionPlan {
pub(crate) fn new() -> Self {
Self { plan: Vec::new() }
}
pub(crate) fn stats(&self) -> InteractionStats {
let mut stats = InteractionStats {
read_count: 0,
write_count: 0,
delete_count: 0,
update_count: 0,
create_count: 0,
create_index_count: 0,
drop_count: 0,
begin_count: 0,
commit_count: 0,
rollback_count: 0,
};
fn query_stat(q: &Query, stats: &mut InteractionStats) {
match q {
Query::Select(_) => stats.read_count += 1,
Query::Insert(_) => stats.write_count += 1,
Query::Delete(_) => stats.delete_count += 1,
Query::Create(_) => stats.create_count += 1,
Query::Drop(_) => stats.drop_count += 1,
Query::Update(_) => stats.update_count += 1,
Query::CreateIndex(_) => stats.create_index_count += 1,
Query::Begin(_) => stats.begin_count += 1,
Query::Commit(_) => stats.commit_count += 1,
Query::Rollback(_) => stats.rollback_count += 1,
}
}
for interactions in &self.plan {
match interactions {
Interactions::Property(property) => {
for interaction in &property.interactions() {
if let Interaction::Query(query) = interaction {
query_stat(query, &mut stats);
}
}
}
Interactions::Query(query) => {
query_stat(query, &mut stats);
}
Interactions::Fault(_) => {}
}
}
stats
}
}
impl ArbitraryFrom<&mut SimulatorEnv> for InteractionPlan {
fn arbitrary_from<R: rand::Rng>(rng: &mut R, env: &mut SimulatorEnv) -> Self {
let mut plan = InteractionPlan::new();
let num_interactions = env.opts.max_interactions;
// First create at least one table
let create_query = Create::arbitrary(rng);
env.tables.push(create_query.table.clone());
plan.plan
.push(Interactions::Query(Query::Create(create_query)));
while plan.plan.len() < num_interactions {
tracing::debug!(
"Generating interaction {}/{}",
plan.plan.len(),
num_interactions
);
let interactions = Interactions::arbitrary_from(rng, (env, plan.stats()));
interactions.shadow(&mut env.tables);
plan.plan.push(interactions);
}
tracing::info!("Generated plan with {} interactions", plan.plan.len());
plan
}
}
impl Interaction {
pub(crate) fn execute_query(&self, conn: &mut Arc<Connection>, _io: &SimulatorIO) -> ResultSet {
if let Self::Query(query) = self {
let query_str = query.to_string();
let rows = conn.query(&query_str);
if rows.is_err() {
let err = rows.err();
tracing::debug!(
"Error running query '{}': {:?}",
&query_str[0..query_str.len().min(4096)],
err
);
if let Some(turso_core::LimboError::ParseError(e)) = err {
panic!("Unexpected parse error: {e}");
}
return Err(err.unwrap());
}
let rows = rows?;
assert!(rows.is_some());
let mut rows = rows.unwrap();
let mut out = Vec::new();
while let Ok(row) = rows.step() {
match row {
StepResult::Row => {
let row = rows.row().unwrap();
let mut r = Vec::new();
for v in row.get_values() {
let v = v.into();
r.push(v);
}
out.push(r);
}
StepResult::IO => {
rows.run_once().unwrap();
}
StepResult::Interrupt => {}
StepResult::Done => {
break;
}
StepResult::Busy => {
return Err(turso_core::LimboError::Busy);
}
}
}
Ok(out)
} else {
unreachable!("unexpected: this function should only be called on queries")
}
}
pub(crate) fn execute_assertion(
&self,
stack: &Vec<ResultSet>,
env: &mut SimulatorEnv,
) -> Result<()> {
match self {
Self::Assertion(assertion) => {
let result = assertion.func.as_ref()(stack, env);
match result {
Ok(Ok(())) => Ok(()),
Ok(Err(message)) => Err(turso_core::LimboError::InternalError(format!(
"Assertion '{}' failed: {}",
assertion.name, message
))),
Err(err) => Err(turso_core::LimboError::InternalError(format!(
"Assertion '{}' execution error: {}",
assertion.name, err
))),
}
}
_ => {
unreachable!("unexpected: this function should only be called on assertions")
}
}
}
pub(crate) fn execute_assumption(
&self,
stack: &Vec<ResultSet>,
env: &mut SimulatorEnv,
) -> Result<()> {
match self {
Self::Assumption(assumption) => {
let result = assumption.func.as_ref()(stack, env);
match result {
Ok(Ok(())) => Ok(()),
Ok(Err(message)) => Err(turso_core::LimboError::InternalError(format!(
"Assumption '{}' failed: {}",
assumption.name, message
))),
Err(err) => Err(turso_core::LimboError::InternalError(format!(
"Assumption '{}' execution error: {}",
assumption.name, err
))),
}
}
_ => {
unreachable!("unexpected: this function should only be called on assumptions")
}
}
}
pub(crate) fn execute_fault(&self, env: &mut SimulatorEnv, conn_index: usize) -> Result<()> {
match self {
Self::Fault(fault) => {
match fault {
Fault::Disconnect => {
if env.connections[conn_index].is_connected() {
env.connections[conn_index].disconnect();
} else {
return Err(turso_core::LimboError::InternalError(
"connection already disconnected".into(),
));
}
env.connections[conn_index] = SimConnection::Disconnected;
}
Fault::ReopenDatabase => {
reopen_database(env);
}
}
Ok(())
}
_ => {
unreachable!("unexpected: this function should only be called on faults")
}
}
}
pub(crate) fn execute_fsync_query(
&self,
conn: Arc<Connection>,
env: &mut SimulatorEnv,
) -> ResultSet {
if let Self::FsyncQuery(query) = self {
let query_str = query.to_string();
let rows = conn.query(&query_str);
if rows.is_err() {
let err = rows.err();
tracing::debug!(
"Error running query '{}': {:?}",
&query_str[0..query_str.len().min(4096)],
err
);
return Err(err.unwrap());
}
let mut rows = rows.unwrap().unwrap();
let mut out = Vec::new();
while let Ok(row) = rows.step() {
match row {
StepResult::Row => {
let row = rows.row().unwrap();
let mut r = Vec::new();
for v in row.get_values() {
let v = v.into();
r.push(v);
}
out.push(r);
}
StepResult::IO => {
let syncing = {
let files = env.io.files.borrow();
// TODO: currently assuming we only have 1 file that is syncing
files
.iter()
.any(|file| file.sync_completion.borrow().is_some())
};
if syncing {
reopen_database(env);
} else {
rows.run_once().unwrap();
}
}
StepResult::Done => {
break;
}
StepResult::Busy => {
return Err(turso_core::LimboError::Busy);
}
StepResult::Interrupt => {}
}
}
Ok(out)
} else {
unreachable!("unexpected: this function should only be called on queries")
}
}
pub(crate) fn execute_faulty_query(
&self,
conn: &Arc<Connection>,
env: &mut SimulatorEnv,
) -> ResultSet {
use rand::Rng;
if let Self::FaultyQuery(query) = self {
let query_str = query.to_string();
let rows = conn.query(&query_str);
if rows.is_err() {
let err = rows.err();
tracing::debug!(
"Error running query '{}': {:?}",
&query_str[0..query_str.len().min(4096)],
err
);
if let Some(turso_core::LimboError::ParseError(e)) = err {
panic!("Unexpected parse error: {e}");
}
return Err(err.unwrap());
}
let mut rows = rows.unwrap().unwrap();
let mut out = Vec::new();
let mut current_prob = 0.05;
let mut incr = 0.001;
loop {
let syncing = {
let files = env.io.files.borrow();
files
.iter()
.any(|file| file.sync_completion.borrow().is_some())
};
let inject_fault = env.rng.gen_bool(current_prob);
// TODO: avoid for now injecting faults when syncing
if inject_fault && !syncing {
env.io.inject_fault(true);
}
match rows.step()? {
StepResult::Row => {
let row = rows.row().unwrap();
let mut r = Vec::new();
for v in row.get_values() {
let v = v.into();
r.push(v);
}
out.push(r);
}
StepResult::IO => {
rows.run_once()?;
current_prob += incr;
if current_prob > 1.0 {
current_prob = 1.0;
} else {
incr *= 1.01;
}
}
StepResult::Done => {
break;
}
StepResult::Busy => {
return Err(turso_core::LimboError::Busy);
}
StepResult::Interrupt => {}
}
}
Ok(out)
} else {
unreachable!("unexpected: this function should only be called on queries")
}
}
}
fn reopen_database(env: &mut SimulatorEnv) {
// 1. Close all connections without default checkpoint-on-close behavior
// to expose bugs related to how we handle WAL
let num_conns = env.connections.len();
env.connections.clear();
// Clear all open files
// TODO: for correct reporting of faults we should get all the recorded numbers and transfer to the new file
env.io.files.borrow_mut().clear();
// 2. Re-open database
match env.type_ {
SimulationType::Differential => {
for _ in 0..num_conns {
env.connections.push(SimConnection::SQLiteConnection(
rusqlite::Connection::open(env.get_db_path())
.expect("Failed to open SQLite connection"),
));
}
}
SimulationType::Default | SimulationType::Doublecheck => {
env.db = None;
let db = match turso_core::Database::open_file(
env.io.clone(),
env.get_db_path().to_str().expect("path should be 'to_str'"),
false,
true,
) {
Ok(db) => db,
Err(e) => {
tracing::error!(
"Failed to open database at {}: {}",
env.get_db_path().display(),
e
);
panic!("Failed to open database: {e}");
}
};
env.db = Some(db);
for _ in 0..num_conns {
env.connections.push(SimConnection::LimboConnection(
env.db.as_ref().expect("db to be Some").connect().unwrap(),
));
}
}
};
}
fn random_create<R: rand::Rng>(rng: &mut R, env: &SimulatorEnv) -> Interactions {
let mut create = Create::arbitrary(rng);
while env.tables.iter().any(|t| t.name == create.table.name) {
create = Create::arbitrary(rng);
}
Interactions::Query(Query::Create(create))
}
fn random_read<R: rand::Rng>(rng: &mut R, env: &SimulatorEnv) -> Interactions {
Interactions::Query(Query::Select(Select::arbitrary_from(rng, env)))
}
fn random_expr<R: rand::Rng>(rng: &mut R, env: &SimulatorEnv) -> Interactions {
Interactions::Query(Query::Select(SelectFree::arbitrary_from(rng, env).0))
}
fn random_write<R: rand::Rng>(rng: &mut R, env: &SimulatorEnv) -> Interactions {
Interactions::Query(Query::Insert(Insert::arbitrary_from(rng, env)))
}
fn random_delete<R: rand::Rng>(rng: &mut R, env: &SimulatorEnv) -> Interactions {
Interactions::Query(Query::Delete(Delete::arbitrary_from(rng, env)))
}
fn random_update<R: rand::Rng>(rng: &mut R, env: &SimulatorEnv) -> Interactions {
Interactions::Query(Query::Update(Update::arbitrary_from(rng, env)))
}
fn random_drop<R: rand::Rng>(rng: &mut R, env: &SimulatorEnv) -> Interactions {
Interactions::Query(Query::Drop(Drop::arbitrary_from(rng, env)))
}
fn random_create_index<R: rand::Rng>(rng: &mut R, env: &SimulatorEnv) -> Option<Interactions> {
if env.tables.is_empty() {
return None;
}
let mut create_index = CreateIndex::arbitrary_from(rng, env);
while env
.tables
.iter()
.find(|t| t.name == create_index.table_name)
.expect("table should exist")
.indexes
.iter()
.any(|i| i == &create_index.index_name)
{
create_index = CreateIndex::arbitrary_from(rng, env);
}
Some(Interactions::Query(Query::CreateIndex(create_index)))
}
fn random_fault<R: rand::Rng>(rng: &mut R, env: &SimulatorEnv) -> Interactions {
let faults = if env.opts.disable_reopen_database {
vec![Fault::Disconnect]
} else {
vec![Fault::Disconnect, Fault::ReopenDatabase]
};
let fault = faults[rng.random_range(0..faults.len())].clone();
Interactions::Fault(fault)
}
impl ArbitraryFrom<(&SimulatorEnv, InteractionStats)> for Interactions {
fn arbitrary_from<R: rand::Rng>(
rng: &mut R,
(env, stats): (&SimulatorEnv, InteractionStats),
) -> Self {
let remaining_ = remaining(env, &stats);
frequency(
vec![
(
f64::min(remaining_.read, remaining_.write) + remaining_.create,
Box::new(|rng: &mut R| {
Interactions::Property(Property::arbitrary_from(rng, (env, &stats)))
}),
),
(
remaining_.read,
Box::new(|rng: &mut R| random_read(rng, env)),
),
(
remaining_.read / 3.0,
Box::new(|rng: &mut R| random_expr(rng, env)),
),
(
remaining_.write,
Box::new(|rng: &mut R| random_write(rng, env)),
),
(
remaining_.create,
Box::new(|rng: &mut R| random_create(rng, env)),
),
(
remaining_.create_index,
Box::new(|rng: &mut R| {
if let Some(interaction) = random_create_index(rng, env) {
interaction
} else {
// if no tables exist, we can't create an index, so fallback to creating a table
random_create(rng, env)
}
}),
),
(
remaining_.delete,
Box::new(|rng: &mut R| random_delete(rng, env)),
),
(
remaining_.update,
Box::new(|rng: &mut R| random_update(rng, env)),
),
(
// remaining_.drop,
0.0,
Box::new(|rng: &mut R| random_drop(rng, env)),
),
(
remaining_
.read
.min(remaining_.write)
.min(remaining_.create)
.max(1.0),
Box::new(|rng: &mut R| random_fault(rng, env)),
),
],
rng,
)
}
}

View File

@@ -8,8 +8,8 @@ use crate::model::{
use super::{one_of, ArbitraryFrom};
mod binary;
mod unary;
pub mod binary;
pub mod unary;
#[derive(Debug)]
struct CompoundPredicate(Predicate);

View File

@@ -64,6 +64,7 @@ impl ArbitraryFromMaybe<&Vec<&SimValue>> for FalseValue {
}
}
#[allow(dead_code)]
pub struct BitNotValue(pub SimValue);
impl ArbitraryFromMaybe<(&SimValue, bool)> for BitNotValue {

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,6 @@
use crate::generation::{
gen_random_text, pick_n_unique, Arbitrary, ArbitraryFrom, ArbitrarySizedFrom,
gen_random_text, pick_n_unique, pick_unique, Arbitrary, ArbitraryFrom, ArbitrarySizedFrom,
GenerationContext,
};
use crate::model::query::predicate::Predicate;
use crate::model::query::select::{
@@ -7,15 +8,13 @@ use crate::model::query::select::{
SelectInner,
};
use crate::model::query::update::Update;
use crate::model::query::{Create, CreateIndex, Delete, Drop, Insert, Query, Select};
use crate::model::query::{Create, CreateIndex, Delete, Drop, Insert, Select};
use crate::model::table::{JoinTable, JoinType, JoinedTable, SimValue, Table, TableContext};
use crate::SimulatorEnv;
use itertools::Itertools;
use rand::Rng;
use turso_parser::ast::{Expr, SortOrder};
use super::property::Remaining;
use super::{backtrack, frequency, pick};
use super::{backtrack, pick};
impl Arbitrary for Create {
fn arbitrary<R: Rng>(rng: &mut R) -> Self {
@@ -87,14 +86,11 @@ impl ArbitraryFrom<&Vec<Table>> for FromClause {
}
}
impl ArbitraryFrom<&SimulatorEnv> for SelectInner {
fn arbitrary_from<R: Rng>(rng: &mut R, env: &SimulatorEnv) -> Self {
let from = FromClause::arbitrary_from(rng, &env.tables);
let mut tables = env.tables.clone();
// todo: this is a temporary hack because env is not separated from the tables
let join_table = from
.shadow(&mut tables)
.expect("Failed to shadow FromClause");
impl<C: GenerationContext> ArbitraryFrom<&C> for SelectInner {
fn arbitrary_from<R: Rng>(rng: &mut R, env: &C) -> Self {
let from = FromClause::arbitrary_from(rng, env.tables());
let tables = env.tables().clone();
let join_table = from.into_join_table(&tables);
let cuml_col_count = join_table.columns().count();
let order_by = 'order_by: {
@@ -137,7 +133,7 @@ impl ArbitraryFrom<&SimulatorEnv> for SelectInner {
};
SelectInner {
distinctness: if env.opts.experimental_indexes {
distinctness: if env.opts().indexes {
Distinctness::arbitrary(rng)
} else {
Distinctness::All
@@ -150,12 +146,8 @@ impl ArbitraryFrom<&SimulatorEnv> for SelectInner {
}
}
impl ArbitrarySizedFrom<&SimulatorEnv> for SelectInner {
fn arbitrary_sized_from<R: Rng>(
rng: &mut R,
env: &SimulatorEnv,
num_result_columns: usize,
) -> Self {
impl<C: GenerationContext> ArbitrarySizedFrom<&C> for SelectInner {
fn arbitrary_sized_from<R: Rng>(rng: &mut R, env: &C, num_result_columns: usize) -> Self {
let mut select_inner = SelectInner::arbitrary_from(rng, env);
let select_from = &select_inner.from.as_ref().unwrap();
let table_names = select_from
@@ -168,7 +160,7 @@ impl ArbitrarySizedFrom<&SimulatorEnv> for SelectInner {
let flat_columns_names = table_names
.iter()
.flat_map(|t| {
env.tables
env.tables()
.iter()
.find(|table| table.name == *t)
.unwrap()
@@ -208,22 +200,22 @@ impl Arbitrary for CompoundOperator {
/// SelectFree is a wrapper around Select that allows for arbitrary generation
/// of selects without requiring a specific environment, which is useful for generating
/// arbitrary expressions without referring to the tables.
pub(crate) struct SelectFree(pub(crate) Select);
pub struct SelectFree(pub Select);
impl ArbitraryFrom<&SimulatorEnv> for SelectFree {
fn arbitrary_from<R: Rng>(rng: &mut R, env: &SimulatorEnv) -> Self {
impl<C: GenerationContext> ArbitraryFrom<&C> for SelectFree {
fn arbitrary_from<R: Rng>(rng: &mut R, env: &C) -> Self {
let expr = Predicate(Expr::arbitrary_sized_from(rng, env, 8));
let select = Select::expr(expr);
Self(select)
}
}
impl ArbitraryFrom<&SimulatorEnv> for Select {
fn arbitrary_from<R: Rng>(rng: &mut R, env: &SimulatorEnv) -> Self {
impl<C: GenerationContext> ArbitraryFrom<&C> for Select {
fn arbitrary_from<R: Rng>(rng: &mut R, env: &C) -> Self {
// Generate a number of selects based on the query size
// If experimental indexes are enabled, we can have selects with compounds
// Otherwise, we just have a single select with no compounds
let num_compound_selects = if env.opts.experimental_indexes {
let num_compound_selects = if env.opts().indexes {
match rng.random_range(0..=100) {
0..=95 => 0,
96..=99 => 1,
@@ -235,7 +227,7 @@ impl ArbitraryFrom<&SimulatorEnv> for Select {
};
let min_column_count_across_tables =
env.tables.iter().map(|t| t.columns.len()).min().unwrap();
env.tables().iter().map(|t| t.columns.len()).min().unwrap();
let num_result_columns = rng.random_range(1..=min_column_count_across_tables);
@@ -269,10 +261,10 @@ impl ArbitraryFrom<&SimulatorEnv> for Select {
}
}
impl ArbitraryFrom<&SimulatorEnv> for Insert {
fn arbitrary_from<R: Rng>(rng: &mut R, env: &SimulatorEnv) -> Self {
impl<C: GenerationContext> ArbitraryFrom<&C> for Insert {
fn arbitrary_from<R: Rng>(rng: &mut R, env: &C) -> Self {
let gen_values = |rng: &mut R| {
let table = pick(&env.tables, rng);
let table = pick(env.tables(), rng);
let num_rows = rng.random_range(1..10);
let values: Vec<Vec<SimValue>> = (0..num_rows)
.map(|_| {
@@ -291,12 +283,12 @@ impl ArbitraryFrom<&SimulatorEnv> for Insert {
let _gen_select = |rng: &mut R| {
// Find a non-empty table
let select_table = env.tables.iter().find(|t| !t.rows.is_empty())?;
let select_table = env.tables().iter().find(|t| !t.rows.is_empty())?;
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::simple(select_table.name.clone(), predicate);
let table = pick(&env.tables, rng);
let table = pick(env.tables(), rng);
Some(Insert::Select {
table: table.name.clone(),
select: Box::new(select),
@@ -309,9 +301,9 @@ impl ArbitraryFrom<&SimulatorEnv> for Insert {
}
}
impl ArbitraryFrom<&SimulatorEnv> for Delete {
fn arbitrary_from<R: Rng>(rng: &mut R, env: &SimulatorEnv) -> Self {
let table = pick(&env.tables, rng);
impl<C: GenerationContext> ArbitraryFrom<&C> for Delete {
fn arbitrary_from<R: Rng>(rng: &mut R, env: &C) -> Self {
let table = pick(env.tables(), rng);
Self {
table: table.name.clone(),
predicate: Predicate::arbitrary_from(rng, table),
@@ -319,23 +311,23 @@ impl ArbitraryFrom<&SimulatorEnv> for Delete {
}
}
impl ArbitraryFrom<&SimulatorEnv> for Drop {
fn arbitrary_from<R: Rng>(rng: &mut R, env: &SimulatorEnv) -> Self {
let table = pick(&env.tables, rng);
impl<C: GenerationContext> ArbitraryFrom<&C> for Drop {
fn arbitrary_from<R: Rng>(rng: &mut R, env: &C) -> Self {
let table = pick(env.tables(), rng);
Self {
table: table.name.clone(),
}
}
}
impl ArbitraryFrom<&SimulatorEnv> for CreateIndex {
fn arbitrary_from<R: Rng>(rng: &mut R, env: &SimulatorEnv) -> Self {
impl<C: GenerationContext> ArbitraryFrom<&C> for CreateIndex {
fn arbitrary_from<R: Rng>(rng: &mut R, env: &C) -> Self {
assert!(
!env.tables.is_empty(),
!env.tables().is_empty(),
"Cannot create an index when no tables exist in the environment."
);
let table = pick(&env.tables, rng);
let table = pick(env.tables(), rng);
if table.columns.is_empty() {
panic!(
@@ -376,57 +368,9 @@ impl ArbitraryFrom<&SimulatorEnv> for CreateIndex {
}
}
impl ArbitraryFrom<(&SimulatorEnv, &Remaining)> for Query {
fn arbitrary_from<R: Rng>(rng: &mut R, (env, remaining): (&SimulatorEnv, &Remaining)) -> Self {
frequency(
vec![
(
remaining.create,
Box::new(|rng| Self::Create(Create::arbitrary(rng))),
),
(
remaining.read,
Box::new(|rng| Self::Select(Select::arbitrary_from(rng, env))),
),
(
remaining.write,
Box::new(|rng| Self::Insert(Insert::arbitrary_from(rng, env))),
),
(
remaining.update,
Box::new(|rng| Self::Update(Update::arbitrary_from(rng, env))),
),
(
f64::min(remaining.write, remaining.delete),
Box::new(|rng| Self::Delete(Delete::arbitrary_from(rng, env))),
),
],
rng,
)
}
}
fn pick_unique<T: ToOwned + PartialEq>(
items: &[T],
count: usize,
rng: &mut impl rand::Rng,
) -> Vec<T::Owned>
where
<T as ToOwned>::Owned: PartialEq,
{
let mut picked: Vec<T::Owned> = Vec::new();
while picked.len() < count {
let item = pick(items, rng);
if !picked.contains(&item.to_owned()) {
picked.push(item.to_owned());
}
}
picked
}
impl ArbitraryFrom<&SimulatorEnv> for Update {
fn arbitrary_from<R: Rng>(rng: &mut R, env: &SimulatorEnv) -> Self {
let table = pick(&env.tables, rng);
impl<C: GenerationContext> ArbitraryFrom<&C> for Update {
fn arbitrary_from<R: Rng>(rng: &mut R, env: &C) -> Self {
let table = pick(env.tables(), rng);
let num_cols = rng.random_range(1..=table.columns.len());
let columns = pick_unique(&table.columns, num_cols, rng);
let set_values: Vec<(String, SimValue)> = columns

View File

@@ -99,7 +99,7 @@ impl ArbitraryFrom<&ColumnType> for SimValue {
}
}
pub(crate) struct LTValue(pub(crate) SimValue);
pub struct LTValue(pub SimValue);
impl ArbitraryFrom<&Vec<&SimValue>> for LTValue {
fn arbitrary_from<R: Rng>(rng: &mut R, values: &Vec<&SimValue>) -> Self {
@@ -161,7 +161,7 @@ impl ArbitraryFrom<&SimValue> for LTValue {
}
}
pub(crate) struct GTValue(pub(crate) SimValue);
pub struct GTValue(pub SimValue);
impl ArbitraryFrom<&Vec<&SimValue>> for GTValue {
fn arbitrary_from<R: Rng>(rng: &mut R, values: &Vec<&SimValue>) -> Self {
@@ -223,7 +223,7 @@ impl ArbitraryFrom<&SimValue> for GTValue {
}
}
pub(crate) struct LikeValue(pub(crate) SimValue);
pub struct LikeValue(pub SimValue);
impl ArbitraryFromMaybe<&SimValue> for LikeValue {
fn arbitrary_from_maybe<R: Rng>(rng: &mut R, value: &SimValue) -> Option<Self> {

View File

@@ -1,4 +1,2 @@
pub mod query;
pub mod table;
pub(crate) const FAULT_ERROR_MSG: &str = "Injected fault";

View File

@@ -5,8 +5,8 @@ use serde::{Deserialize, Serialize};
use crate::model::table::Table;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub(crate) struct Create {
pub(crate) table: Table,
pub struct Create {
pub table: Table,
}
impl Display for Create {

View File

@@ -1,25 +1,11 @@
use serde::{Deserialize, Serialize};
use turso_parser::ast::SortOrder;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
pub enum SortOrder {
Asc,
Desc,
}
impl std::fmt::Display for SortOrder {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
SortOrder::Asc => write!(f, "ASC"),
SortOrder::Desc => write!(f, "DESC"),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
pub(crate) struct CreateIndex {
pub(crate) index_name: String,
pub(crate) table_name: String,
pub(crate) columns: Vec<(String, SortOrder)>,
pub struct CreateIndex {
pub index_name: String,
pub table_name: String,
pub columns: Vec<(String, SortOrder)>,
}
impl std::fmt::Display for CreateIndex {

View File

@@ -5,9 +5,9 @@ use serde::{Deserialize, Serialize};
use super::predicate::Predicate;
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub(crate) struct Delete {
pub(crate) table: String,
pub(crate) predicate: Predicate,
pub struct Delete {
pub table: String,
pub predicate: Predicate,
}
impl Display for Delete {

View File

@@ -3,8 +3,8 @@ use std::fmt::Display;
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub(crate) struct Drop {
pub(crate) table: String,
pub struct Drop {
pub table: String,
}
impl Display for Drop {

View File

@@ -7,7 +7,7 @@ use crate::model::table::SimValue;
use super::select::Select;
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub(crate) enum Insert {
pub enum Insert {
Values {
table: String,
values: Vec<Vec<SimValue>>,
@@ -19,7 +19,7 @@ pub(crate) enum Insert {
}
impl Insert {
pub(crate) fn table(&self) -> &str {
pub fn table(&self) -> &str {
match self {
Insert::Values { table, .. } | Insert::Select { table, .. } => table,
}

View File

@@ -1,11 +1,11 @@
use std::{collections::HashSet, fmt::Display};
pub(crate) use create::Create;
pub(crate) use create_index::CreateIndex;
pub(crate) use delete::Delete;
pub(crate) use drop::Drop;
pub(crate) use insert::Insert;
pub(crate) use select::Select;
pub use create::Create;
pub use create_index::CreateIndex;
pub use delete::Delete;
pub use drop::Drop;
pub use insert::Insert;
pub use select::Select;
use serde::{Deserialize, Serialize};
use turso_parser::ast::fmt::ToSqlContext;
use update::Update;
@@ -24,7 +24,7 @@ pub mod update;
// This type represents the potential queries on the database.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub(crate) enum Query {
pub enum Query {
Create(Create),
Select(Select),
Insert(Insert),
@@ -38,7 +38,7 @@ pub(crate) enum Query {
}
impl Query {
pub(crate) fn dependencies(&self) -> HashSet<String> {
pub fn dependencies(&self) -> HashSet<String> {
match self {
Query::Select(select) => select.dependencies(),
Query::Create(_) => HashSet::new(),
@@ -53,7 +53,7 @@ impl Query {
Query::Begin(_) | Query::Commit(_) | Query::Rollback(_) => HashSet::new(),
}
}
pub(crate) fn uses(&self) -> Vec<String> {
pub fn uses(&self) -> Vec<String> {
match self {
Query::Create(Create { table }) => vec![table.name.clone()],
Query::Select(select) => select.dependencies().into_iter().collect(),
@@ -86,7 +86,7 @@ impl Display for Query {
}
/// Used to print sql strings that already have all the context it needs
pub(crate) struct EmptyContext;
pub struct EmptyContext;
impl ToSqlContext for EmptyContext {
fn get_column_name(

View File

@@ -9,27 +9,28 @@ use crate::model::table::{SimValue, Table, TableContext};
pub struct Predicate(pub ast::Expr);
impl Predicate {
pub(crate) fn true_() -> Self {
pub fn true_() -> Self {
Self(ast::Expr::Literal(ast::Literal::Keyword(
"TRUE".to_string(),
)))
}
pub(crate) fn false_() -> Self {
pub fn false_() -> Self {
Self(ast::Expr::Literal(ast::Literal::Keyword(
"FALSE".to_string(),
)))
}
pub(crate) fn null() -> Self {
pub fn null() -> Self {
Self(ast::Expr::Literal(ast::Literal::Null))
}
pub(crate) fn not(predicate: Predicate) -> Self {
#[allow(clippy::should_implement_trait)]
pub fn not(predicate: Predicate) -> Self {
let expr = ast::Expr::Unary(ast::UnaryOperator::Not, Box::new(predicate.0));
Self(expr).parens()
}
pub(crate) fn and(predicates: Vec<Predicate>) -> Self {
pub fn and(predicates: Vec<Predicate>) -> Self {
if predicates.is_empty() {
Self::true_()
} else if predicates.len() == 1 {
@@ -44,7 +45,7 @@ impl Predicate {
}
}
pub(crate) fn or(predicates: Vec<Predicate>) -> Self {
pub fn or(predicates: Vec<Predicate>) -> Self {
if predicates.is_empty() {
Self::false_()
} else if predicates.len() == 1 {
@@ -59,26 +60,26 @@ impl Predicate {
}
}
pub(crate) fn eq(lhs: Predicate, rhs: Predicate) -> Self {
pub fn eq(lhs: Predicate, rhs: Predicate) -> Self {
let expr = ast::Expr::Binary(Box::new(lhs.0), ast::Operator::Equals, Box::new(rhs.0));
Self(expr).parens()
}
pub(crate) fn is(lhs: Predicate, rhs: Predicate) -> Self {
pub fn is(lhs: Predicate, rhs: Predicate) -> Self {
let expr = ast::Expr::Binary(Box::new(lhs.0), ast::Operator::Is, Box::new(rhs.0));
Self(expr).parens()
}
pub(crate) fn parens(self) -> Self {
pub fn parens(self) -> Self {
let expr = ast::Expr::Parenthesized(vec![self.0]);
Self(expr)
}
pub(crate) fn eval(&self, row: &[SimValue], table: &Table) -> Option<SimValue> {
pub fn eval(&self, row: &[SimValue], table: &Table) -> Option<SimValue> {
expr_to_value(&self.0, row, table)
}
pub(crate) fn test<T: TableContext>(&self, row: &[SimValue], table: &T) -> bool {
pub fn test<T: TableContext>(&self, row: &[SimValue], table: &T) -> bool {
let value = expr_to_value(&self.0, row, table);
value.is_some_and(|value| value.as_bool())
}

View File

@@ -1,12 +1,13 @@
use std::{collections::HashSet, fmt::Display};
pub use ast::Distinctness;
use itertools::Itertools;
use serde::{Deserialize, Serialize};
use turso_parser::ast::{self, fmt::ToTokens, SortOrder};
use crate::model::{
query::EmptyContext,
table::{JoinType, JoinedTable},
table::{JoinTable, JoinType, JoinedTable, Table},
};
use super::predicate::Predicate;
@@ -14,6 +15,7 @@ use super::predicate::Predicate;
/// `SELECT` or `RETURNING` result column
// https://sqlite.org/syntax/result-column.html
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[allow(clippy::large_enum_variant)]
pub enum ResultColumn {
/// expression
Expr(Predicate),
@@ -33,9 +35,9 @@ impl Display for ResultColumn {
}
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub(crate) struct Select {
pub(crate) body: SelectBody,
pub(crate) limit: Option<usize>,
pub struct Select {
pub body: SelectBody,
pub limit: Option<usize>,
}
impl Select {
@@ -102,7 +104,7 @@ impl Select {
}
}
pub(crate) fn dependencies(&self) -> HashSet<String> {
pub fn dependencies(&self) -> HashSet<String> {
if self.body.select.from.is_none() {
return HashSet::new();
}
@@ -209,13 +211,64 @@ impl FromClause {
}
}
pub(crate) fn dependencies(&self) -> Vec<String> {
pub fn dependencies(&self) -> Vec<String> {
let mut deps = vec![self.table.clone()];
for join in &self.joins {
deps.push(join.table.clone());
}
deps
}
pub fn into_join_table(&self, tables: &[Table]) -> JoinTable {
let first_table = tables
.iter()
.find(|t| t.name == self.table)
.expect("Table not found");
let mut join_table = JoinTable {
tables: vec![first_table.clone()],
rows: Vec::new(),
};
for join in &self.joins {
let joined_table = tables
.iter()
.find(|t| t.name == join.table)
.expect("Joined table not found");
join_table.tables.push(joined_table.clone());
match join.join_type {
JoinType::Inner => {
// Implement inner join logic
let join_rows = joined_table
.rows
.iter()
.filter(|row| join.on.test(row, joined_table))
.cloned()
.collect::<Vec<_>>();
// take a cartesian product of the rows
let all_row_pairs = join_table
.rows
.clone()
.into_iter()
.cartesian_product(join_rows.iter());
for (row1, row2) in all_row_pairs {
let row = row1.iter().chain(row2.iter()).cloned().collect::<Vec<_>>();
let is_in = join.on.test(&row, &join_table);
if is_in {
join_table.rows.push(row);
}
}
}
_ => todo!(),
}
}
join_table
}
}
impl Select {
@@ -302,7 +355,7 @@ impl Select {
})
.collect()
})
.unwrap_or(Vec::new()),
.unwrap_or_default(),
limit: self.limit.map(|l| ast::Limit {
expr: ast::Expr::Literal(ast::Literal::Numeric(l.to_string())).into_boxed(),
offset: None,

View File

@@ -3,15 +3,15 @@ use std::fmt::Display;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub(crate) struct Begin {
pub(crate) immediate: bool,
pub struct Begin {
pub immediate: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub(crate) struct Commit;
pub struct Commit;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub(crate) struct Rollback;
pub struct Rollback;
impl Display for Begin {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {

View File

@@ -7,10 +7,10 @@ use crate::model::table::SimValue;
use super::predicate::Predicate;
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub(crate) struct Update {
pub(crate) table: String,
pub(crate) set_values: Vec<(String, SimValue)>, // Pair of value for set expressions => SET name=value
pub(crate) predicate: Predicate,
pub struct Update {
pub table: String,
pub set_values: Vec<(String, SimValue)>, // Pair of value for set expressions => SET name=value
pub predicate: Predicate,
}
impl Update {

View File

@@ -6,7 +6,7 @@ use turso_parser::ast;
use crate::model::query::predicate::Predicate;
pub(crate) struct Name(pub(crate) String);
pub struct Name(pub String);
impl Deref for Name {
type Target = str;
@@ -41,11 +41,11 @@ impl TableContext for Table {
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub(crate) struct Table {
pub(crate) name: String,
pub(crate) columns: Vec<Column>,
pub(crate) rows: Vec<Vec<SimValue>>,
pub(crate) indexes: Vec<String>,
pub struct Table {
pub name: String,
pub columns: Vec<Column>,
pub rows: Vec<Vec<SimValue>>,
pub indexes: Vec<String>,
}
impl Table {
@@ -60,11 +60,11 @@ impl Table {
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub(crate) struct Column {
pub(crate) name: String,
pub(crate) column_type: ColumnType,
pub(crate) primary: bool,
pub(crate) unique: bool,
pub struct Column {
pub name: String,
pub column_type: ColumnType,
pub primary: bool,
pub unique: bool,
}
// Uniquely defined by name in this case
@@ -83,7 +83,7 @@ impl PartialEq for Column {
impl Eq for Column {}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub(crate) enum ColumnType {
pub enum ColumnType {
Integer,
Float,
Text,
@@ -136,23 +136,8 @@ pub struct JoinTable {
pub rows: Vec<Vec<SimValue>>,
}
fn float_to_string<S>(float: &f64, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serializer.serialize_str(&format!("{float}"))
}
fn string_to_float<'de, D>(deserializer: D) -> Result<f64, D::Error>
where
D: serde::Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
s.parse().map_err(serde::de::Error::custom)
}
#[derive(Clone, Debug, PartialEq, PartialOrd, Eq, Ord, Serialize, Deserialize)]
pub(crate) struct SimValue(pub turso_core::Value);
pub struct SimValue(pub turso_core::Value);
fn to_sqlite_blob(bytes: &[u8]) -> String {
format!(