From 2cc79471077e05a824b5dad0f896dc463184c1e5 Mon Sep 17 00:00:00 2001 From: pedrocarlo Date: Tue, 7 Oct 2025 23:34:54 -0300 Subject: [PATCH 01/17] define alter table in sql_generation --- Cargo.lock | 1 + sql_generation/Cargo.toml | 1 + sql_generation/generation/query.rs | 50 ++++++++++++++++++++- sql_generation/model/query/alter_table.rs | 54 +++++++++++++++++++++++ sql_generation/model/query/mod.rs | 1 + 5 files changed, 106 insertions(+), 1 deletion(-) create mode 100644 sql_generation/model/query/alter_table.rs diff --git a/Cargo.lock b/Cargo.lock index 2ddd72015..6d1db41bc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3854,6 +3854,7 @@ dependencies = [ "rand_chacha 0.9.0", "schemars 1.0.4", "serde", + "strum", "tracing", "turso_core", "turso_parser", diff --git a/sql_generation/Cargo.toml b/sql_generation/Cargo.toml index 5c4de8d6e..8d1084f24 100644 --- a/sql_generation/Cargo.toml +++ b/sql_generation/Cargo.toml @@ -22,6 +22,7 @@ tracing = { workspace = true } schemars = { workspace = true } garde = { workspace = true, features = ["derive", "serde"] } indexmap = { workspace = true } +strum = { workspace = true } [dev-dependencies] rand_chacha = { workspace = true } diff --git a/sql_generation/generation/query.rs b/sql_generation/generation/query.rs index f2264720e..82d6296df 100644 --- a/sql_generation/generation/query.rs +++ b/sql_generation/generation/query.rs @@ -2,6 +2,7 @@ use crate::generation::{ gen_random_text, pick_n_unique, pick_unique, Arbitrary, ArbitraryFrom, ArbitrarySized, GenerationContext, }; +use crate::model::query::alter_table::{AlterTable, AlterTableType, AlterTableTypeDiscriminants}; use crate::model::query::predicate::Predicate; use crate::model::query::select::{ CompoundOperator, CompoundSelect, Distinctness, FromClause, OrderBy, ResultColumn, SelectBody, @@ -9,9 +10,12 @@ use crate::model::query::select::{ }; use crate::model::query::update::Update; use crate::model::query::{Create, CreateIndex, Delete, Drop, Insert, Select}; -use crate::model::table::{JoinTable, JoinType, JoinedTable, SimValue, Table, TableContext}; +use crate::model::table::{ + Column, JoinTable, JoinType, JoinedTable, Name, SimValue, Table, TableContext, +}; use indexmap::IndexSet; use itertools::Itertools; +use rand::seq::IndexedRandom; use rand::Rng; use turso_parser::ast::{Expr, SortOrder}; @@ -385,3 +389,47 @@ impl Arbitrary for Update { } } } + +impl Arbitrary for AlterTable { + fn arbitrary(rng: &mut R, context: &C) -> Self { + let table = pick(context.tables(), rng); + let choices: &'static [AlterTableTypeDiscriminants] = if table.columns.len() > 1 { + &[ + AlterTableTypeDiscriminants::RenameTo, + AlterTableTypeDiscriminants::AddColumn, + // AlterTableTypeDiscriminants::AlterColumn, + AlterTableTypeDiscriminants::RenameColumn, + AlterTableTypeDiscriminants::DropColumn, + ] + } else { + &[ + AlterTableTypeDiscriminants::RenameTo, + AlterTableTypeDiscriminants::AddColumn, + // AlterTableTypeDiscriminants::AlterColumn, + AlterTableTypeDiscriminants::RenameColumn, + ] + }; + let alter_table_type = match choices.choose(rng).unwrap() { + AlterTableTypeDiscriminants::RenameTo => AlterTableType::RenameTo { + new_name: Name::arbitrary(rng, context).0, + }, + AlterTableTypeDiscriminants::AddColumn => AlterTableType::AddColumn { + column: Column::arbitrary(rng, context), + }, + AlterTableTypeDiscriminants::AlterColumn => { + todo!(); + } + AlterTableTypeDiscriminants::RenameColumn => AlterTableType::RenameColumn { + old: pick(&table.columns, rng).name.clone(), + new: Name::arbitrary(rng, context).0, + }, + AlterTableTypeDiscriminants::DropColumn => AlterTableType::DropColumn { + column_name: pick(&table.columns, rng).name.clone(), + }, + }; + Self { + table_name: table.name.clone(), + alter_table_type, + } + } +} diff --git a/sql_generation/model/query/alter_table.rs b/sql_generation/model/query/alter_table.rs new file mode 100644 index 000000000..684198b35 --- /dev/null +++ b/sql_generation/model/query/alter_table.rs @@ -0,0 +1,54 @@ +use std::fmt::Display; + +use serde::{Deserialize, Serialize}; + +use crate::model::table::Column; + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)] +pub struct AlterTable { + pub table_name: String, + pub alter_table_type: AlterTableType, +} + +// TODO: in the future maybe use parser AST's when we test almost the entire SQL spectrum +// so we can repeat less code +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash, strum::EnumDiscriminants)] +pub enum AlterTableType { + /// `RENAME TO`: new table name + RenameTo { new_name: String }, + /// `ADD COLUMN` + AddColumn { column: Column }, + /// `ALTER COLUMN` + AlterColumn { old: String, new: Column }, + /// `RENAME COLUMN` + RenameColumn { + /// old name + old: String, + /// new name + new: String, + }, + /// `DROP COLUMN` + DropColumn { column_name: String }, +} + +impl Display for AlterTable { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "ALTER TABLE {} {}", + self.table_name, self.alter_table_type + ) + } +} + +impl Display for AlterTableType { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + AlterTableType::RenameTo { new_name } => write!(f, "RENAME TO {new_name}"), + AlterTableType::AddColumn { column } => write!(f, "ADD COLUMN {column}"), + AlterTableType::AlterColumn { old, new } => write!(f, "ALTER COLUMN {old} TO {new}"), + AlterTableType::RenameColumn { old, new } => write!(f, "RENAME COLUMN {old} TO {new}"), + AlterTableType::DropColumn { column_name } => write!(f, "DROP COLUMN {column_name}"), + } + } +} diff --git a/sql_generation/model/query/mod.rs b/sql_generation/model/query/mod.rs index 98ec2bdfd..9876ffe54 100644 --- a/sql_generation/model/query/mod.rs +++ b/sql_generation/model/query/mod.rs @@ -6,6 +6,7 @@ pub use drop_index::DropIndex; pub use insert::Insert; pub use select::Select; +pub mod alter_table; pub mod create; pub mod create_index; pub mod delete; From f593080c2ac1d42fa74c914af07e7231f03d941f Mon Sep 17 00:00:00 2001 From: pedrocarlo Date: Wed, 8 Oct 2025 13:46:29 -0300 Subject: [PATCH 02/17] add `Query::AlterTable` variant --- simulator/generation/plan.rs | 37 ++++++++++++-------------------- simulator/generation/property.rs | 24 +++++++++++---------- simulator/generation/query.rs | 13 +++++++++-- simulator/model/mod.rs | 34 +++++++++++++++++++++++------ 4 files changed, 66 insertions(+), 42 deletions(-) diff --git a/simulator/generation/plan.rs b/simulator/generation/plan.rs index 337978569..d1bc9f0d3 100644 --- a/simulator/generation/plan.rs +++ b/simulator/generation/plan.rs @@ -165,18 +165,7 @@ impl InteractionPlan { } pub(crate) fn stats(&self) -> InteractionStats { - let mut stats = InteractionStats { - select_count: 0, - insert_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, - }; + let mut stats = InteractionStats::default(); fn query_stat(q: &Query, stats: &mut InteractionStats) { match q { @@ -190,6 +179,7 @@ impl InteractionPlan { Query::Begin(_) => stats.begin_count += 1, Query::Commit(_) => stats.commit_count += 1, Query::Rollback(_) => stats.rollback_count += 1, + Query::AlterTable(_) => stats.alter_table_count += 1, Query::Placeholder => {} } } @@ -699,18 +689,19 @@ impl Display for InteractionPlan { } } -#[derive(Debug, Clone, Copy)] +#[derive(Debug, Clone, Copy, Default)] pub(crate) struct InteractionStats { - pub(crate) select_count: u32, - pub(crate) insert_count: u32, - pub(crate) delete_count: u32, - pub(crate) update_count: u32, - pub(crate) create_count: u32, - pub(crate) create_index_count: u32, - pub(crate) drop_count: u32, - pub(crate) begin_count: u32, - pub(crate) commit_count: u32, - pub(crate) rollback_count: u32, + pub select_count: u32, + pub insert_count: u32, + pub delete_count: u32, + pub update_count: u32, + pub create_count: u32, + pub create_index_count: u32, + pub drop_count: u32, + pub begin_count: u32, + pub commit_count: u32, + pub rollback_count: u32, + pub alter_table_count: u32, } impl Display for InteractionStats { diff --git a/simulator/generation/property.rs b/simulator/generation/property.rs index b67026dae..a858a6516 100644 --- a/simulator/generation/property.rs +++ b/simulator/generation/property.rs @@ -1352,17 +1352,18 @@ fn assert_all_table_values( } #[derive(Debug)] -pub(crate) struct Remaining { - pub(crate) select: u32, - pub(crate) insert: u32, - pub(crate) create: u32, - pub(crate) create_index: u32, - pub(crate) delete: u32, - pub(crate) update: u32, - pub(crate) drop: u32, +pub(super) struct Remaining { + pub select: u32, + pub insert: u32, + pub create: u32, + pub create_index: u32, + pub delete: u32, + pub update: u32, + pub drop: u32, + pub alter_table: u32, } -pub(crate) fn remaining( +pub(super) fn remaining( max_interactions: u32, opts: &QueryProfile, stats: &InteractionStats, @@ -1417,6 +1418,7 @@ pub(crate) fn remaining( delete: remaining_delete, drop: remaining_drop, update: remaining_update, + alter_table: 0, // TODO: calculate remaining } } @@ -1727,7 +1729,7 @@ fn property_faulty_query( type PropertyGenFunc = fn(&mut R, &QueryDistribution, &G, bool) -> Property; impl PropertyDiscriminants { - pub(super) fn gen_function(&self) -> PropertyGenFunc + fn gen_function(&self) -> PropertyGenFunc where R: rand::Rng + ?Sized, G: GenerationContext, @@ -1756,7 +1758,7 @@ impl PropertyDiscriminants { } } - pub fn weight( + fn weight( &self, env: &SimulatorEnv, remaining: &Remaining, diff --git a/simulator/generation/query.rs b/simulator/generation/query.rs index 914b44b35..f0ab68f2a 100644 --- a/simulator/generation/query.rs +++ b/simulator/generation/query.rs @@ -79,6 +79,13 @@ fn random_create_index( Query::CreateIndex(create_index) } +fn random_alter_table( + rng: &mut R, + conn_ctx: &impl GenerationContext, +) -> Query { + todo!() +} + /// Possible queries that can be generated given the table state /// /// Does not take into account transactional statements @@ -93,7 +100,7 @@ pub const fn possible_queries(tables: &[Table]) -> &'static [QueryDiscriminants] type QueryGenFunc = fn(&mut R, &G) -> Query; impl QueryDiscriminants { - pub fn gen_function(&self) -> QueryGenFunc + fn gen_function(&self) -> QueryGenFunc where R: rand::Rng + ?Sized, G: GenerationContext, @@ -106,6 +113,7 @@ impl QueryDiscriminants { QueryDiscriminants::Update => random_update, QueryDiscriminants::Drop => random_drop, QueryDiscriminants::CreateIndex => random_create_index, + QueryDiscriminants::AlterTable => random_alter_table, QueryDiscriminants::Begin | QueryDiscriminants::Commit | QueryDiscriminants::Rollback => { @@ -117,7 +125,7 @@ impl QueryDiscriminants { } } - pub fn weight(&self, remaining: &Remaining) -> u32 { + fn weight(&self, remaining: &Remaining) -> u32 { match self { QueryDiscriminants::Create => remaining.create, // remaining.select / 3 is for the random_expr generation @@ -128,6 +136,7 @@ impl QueryDiscriminants { QueryDiscriminants::Update => remaining.update, QueryDiscriminants::Drop => remaining.drop, QueryDiscriminants::CreateIndex => remaining.create_index, + QueryDiscriminants::AlterTable => remaining.alter_table, QueryDiscriminants::Begin | QueryDiscriminants::Commit | QueryDiscriminants::Rollback => { diff --git a/simulator/model/mod.rs b/simulator/model/mod.rs index 3f8a4ec9d..24c30eb2d 100644 --- a/simulator/model/mod.rs +++ b/simulator/model/mod.rs @@ -8,6 +8,7 @@ use serde::{Deserialize, Serialize}; use sql_generation::model::{ query::{ Create, CreateIndex, Delete, Drop, Insert, Select, + alter_table::AlterTable, select::{CompoundOperator, FromClause, ResultColumn, SelectInner}, transaction::{Begin, Commit, Rollback}, update::Update, @@ -29,6 +30,7 @@ pub enum Query { Update(Update), Drop(Drop), CreateIndex(CreateIndex), + AlterTable(AlterTable), Begin(Begin), Commit(Commit), Rollback(Rollback), @@ -67,10 +69,13 @@ impl Query { | Query::Insert(Insert::Values { table, .. }) | Query::Delete(Delete { table, .. }) | Query::Update(Update { table, .. }) - | Query::Drop(Drop { table, .. }) => IndexSet::from_iter([table.clone()]), - Query::CreateIndex(CreateIndex { table_name, .. }) => { - IndexSet::from_iter([table_name.clone()]) - } + | Query::Drop(Drop { table, .. }) + | Query::CreateIndex(CreateIndex { + table_name: table, .. + }) + | Query::AlterTable(AlterTable { + table_name: table, .. + }) => IndexSet::from_iter([table.clone()]), Query::Begin(_) | Query::Commit(_) | Query::Rollback(_) => IndexSet::new(), Query::Placeholder => IndexSet::new(), } @@ -83,8 +88,13 @@ impl Query { | Query::Insert(Insert::Values { table, .. }) | Query::Delete(Delete { table, .. }) | Query::Update(Update { table, .. }) - | Query::Drop(Drop { table, .. }) => vec![table.clone()], - Query::CreateIndex(CreateIndex { table_name, .. }) => vec![table_name.clone()], + | Query::Drop(Drop { table, .. }) + | Query::CreateIndex(CreateIndex { + table_name: table, .. + }) + | Query::AlterTable(AlterTable { + table_name: table, .. + }) => vec![table.clone()], Query::Begin(..) | Query::Commit(..) | Query::Rollback(..) => vec![], Query::Placeholder => vec![], } @@ -117,6 +127,7 @@ impl Display for Query { Self::Update(update) => write!(f, "{update}"), Self::Drop(drop) => write!(f, "{drop}"), Self::CreateIndex(create_index) => write!(f, "{create_index}"), + Self::AlterTable(alter_table) => write!(f, "{alter_table}"), Self::Begin(begin) => write!(f, "{begin}"), Self::Commit(commit) => write!(f, "{commit}"), Self::Rollback(rollback) => write!(f, "{rollback}"), @@ -137,6 +148,7 @@ impl Shadow for Query { Query::Update(update) => update.shadow(env), Query::Drop(drop) => drop.shadow(env), Query::CreateIndex(create_index) => Ok(create_index.shadow(env)), + Query::AlterTable(alter_table) => alter_table.shadow(env), Query::Begin(begin) => Ok(begin.shadow(env)), Query::Commit(commit) => Ok(commit.shadow(env)), Query::Rollback(rollback) => Ok(rollback.shadow(env)), @@ -154,6 +166,7 @@ bitflags! { const UPDATE = 1 << 4; const DROP = 1 << 5; const CREATE_INDEX = 1 << 6; + const ALTER_TABLE = 1 << 7; } } @@ -182,6 +195,7 @@ impl From for QueryCapabilities { QueryDiscriminants::Update => Self::UPDATE, QueryDiscriminants::Drop => Self::DROP, QueryDiscriminants::CreateIndex => Self::CREATE_INDEX, + QueryDiscriminants::AlterTable => Self::ALTER_TABLE, QueryDiscriminants::Begin | QueryDiscriminants::Commit | QueryDiscriminants::Rollback => { @@ -522,3 +536,11 @@ impl Shadow for Update { Ok(vec![]) } } + +impl Shadow for AlterTable { + type Result = anyhow::Result>>; + + fn shadow(&self, tables: &mut ShadowTablesMut<'_>) -> Self::Result { + Ok(vec![]) + } +} From 230755eb2e9ea3859d3f4cb911898df6985790de Mon Sep 17 00:00:00 2001 From: pedrocarlo Date: Wed, 8 Oct 2025 14:09:28 -0300 Subject: [PATCH 03/17] shadow for AlterTable --- simulator/model/mod.rs | 40 ++++++++++++++++++++++++++++++++++++++-- 1 file changed, 38 insertions(+), 2 deletions(-) diff --git a/simulator/model/mod.rs b/simulator/model/mod.rs index 24c30eb2d..90c04485f 100644 --- a/simulator/model/mod.rs +++ b/simulator/model/mod.rs @@ -8,7 +8,7 @@ use serde::{Deserialize, Serialize}; use sql_generation::model::{ query::{ Create, CreateIndex, Delete, Drop, Insert, Select, - alter_table::AlterTable, + alter_table::{AlterTable, AlterTableType}, select::{CompoundOperator, FromClause, ResultColumn, SelectInner}, transaction::{Begin, Commit, Rollback}, update::Update, @@ -21,7 +21,6 @@ use crate::{generation::Shadow, runner::env::ShadowTablesMut}; // This type represents the potential queries on the database. #[derive(Debug, Clone, Serialize, Deserialize, strum::EnumDiscriminants)] -#[strum_discriminants(derive(strum::VariantArray, strum::EnumIter))] pub enum Query { Create(Create), Select(Select), @@ -541,6 +540,43 @@ impl Shadow for AlterTable { type Result = anyhow::Result>>; fn shadow(&self, tables: &mut ShadowTablesMut<'_>) -> Self::Result { + let table = tables + .iter_mut() + .find(|t| t.name == self.table_name) + .ok_or_else(|| anyhow::anyhow!("Table {} does not exist", self.table_name))?; + + match &self.alter_table_type { + AlterTableType::RenameTo { new_name } => { + table.name = new_name.clone(); + } + AlterTableType::AddColumn { column } => { + table.columns.push(column.clone()); + table.rows.iter_mut().for_each(|row| { + row.push(SimValue(turso_core::Value::Null)); + }); + } + AlterTableType::AlterColumn { old, new } => { + // TODO: have to see correct behaviour with indexes to see if we should error out + // in case there is some sort of conflict with this change + let col = table.columns.iter_mut().find(|c| c.name == *old).unwrap(); + *col = new.clone(); + } + AlterTableType::RenameColumn { old, new } => { + let col = table.columns.iter_mut().find(|c| c.name == *old).unwrap(); + col.name = new.clone(); + } + AlterTableType::DropColumn { column_name } => { + let col_idx = table + .columns + .iter() + .position(|c| c.name == *column_name) + .unwrap(); + table.columns.remove(col_idx); + table.rows.iter_mut().for_each(|row| { + row.remove(col_idx); + }); + } + }; Ok(vec![]) } } From c072058e4b9d1d276d17f61f6c60058558151d73 Mon Sep 17 00:00:00 2001 From: pedrocarlo Date: Wed, 8 Oct 2025 16:24:16 -0300 Subject: [PATCH 04/17] add Alter Table query generation in Sim --- simulator/generation/plan.rs | 3 ++- simulator/generation/property.rs | 15 +++++++-------- simulator/generation/query.rs | 7 +++++-- simulator/model/mod.rs | 1 + simulator/profiles/query.rs | 17 +++++++++++++++++ 5 files changed, 32 insertions(+), 11 deletions(-) diff --git a/simulator/generation/plan.rs b/simulator/generation/plan.rs index d1bc9f0d3..6285b62f7 100644 --- a/simulator/generation/plan.rs +++ b/simulator/generation/plan.rs @@ -708,7 +708,7 @@ 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: {}", + "Read: {}, Insert: {}, Delete: {}, Update: {}, Create: {}, CreateIndex: {}, Drop: {}, Begin: {}, Commit: {}, Rollback: {}, Alter Table: {}", self.select_count, self.insert_count, self.delete_count, @@ -719,6 +719,7 @@ impl Display for InteractionStats { self.begin_count, self.commit_count, self.rollback_count, + self.alter_table_count, ) } } diff --git a/simulator/generation/property.rs b/simulator/generation/property.rs index a858a6516..b4ac73f25 100644 --- a/simulator/generation/property.rs +++ b/simulator/generation/property.rs @@ -1369,13 +1369,7 @@ pub(super) fn remaining( stats: &InteractionStats, mvcc: bool, ) -> Remaining { - let total_weight = opts.select_weight - + opts.create_table_weight - + opts.create_index_weight - + opts.insert_weight - + opts.update_weight - + opts.delete_weight - + opts.drop_table_weight; + let total_weight = opts.total_weight(); let total_select = (max_interactions * opts.select_weight) / total_weight; let total_insert = (max_interactions * opts.insert_weight) / total_weight; @@ -1384,6 +1378,7 @@ pub(super) fn remaining( let total_delete = (max_interactions * opts.delete_weight) / total_weight; let total_update = (max_interactions * opts.update_weight) / total_weight; let total_drop = (max_interactions * opts.drop_table_weight) / total_weight; + let total_alter_table = (max_interactions * opts.alter_table_weight) / total_weight; let remaining_select = total_select .checked_sub(stats.select_count) @@ -1405,6 +1400,10 @@ pub(super) fn remaining( .unwrap_or_default(); let remaining_drop = total_drop.checked_sub(stats.drop_count).unwrap_or_default(); + let remaining_alter_table = total_alter_table + .checked_sub(stats.alter_table_count) + .unwrap_or_default(); + if mvcc { // TODO: index not supported yet for mvcc remaining_create_index = 0; @@ -1418,7 +1417,7 @@ pub(super) fn remaining( delete: remaining_delete, drop: remaining_drop, update: remaining_update, - alter_table: 0, // TODO: calculate remaining + alter_table: remaining_alter_table, } } diff --git a/simulator/generation/query.rs b/simulator/generation/query.rs index f0ab68f2a..627e5e40e 100644 --- a/simulator/generation/query.rs +++ b/simulator/generation/query.rs @@ -9,7 +9,9 @@ use rand::{ use sql_generation::{ generation::{Arbitrary, ArbitraryFrom, GenerationContext, query::SelectFree}, model::{ - query::{Create, CreateIndex, Delete, Insert, Select, update::Update}, + query::{ + Create, CreateIndex, Delete, Insert, Select, alter_table::AlterTable, update::Update, + }, table::Table, }, }; @@ -83,7 +85,8 @@ fn random_alter_table( rng: &mut R, conn_ctx: &impl GenerationContext, ) -> Query { - todo!() + assert!(!conn_ctx.tables().is_empty()); + Query::AlterTable(AlterTable::arbitrary(rng, conn_ctx)) } /// Possible queries that can be generated given the table state diff --git a/simulator/model/mod.rs b/simulator/model/mod.rs index 90c04485f..d50e386d7 100644 --- a/simulator/model/mod.rs +++ b/simulator/model/mod.rs @@ -216,6 +216,7 @@ impl QueryDiscriminants { QueryDiscriminants::Delete, QueryDiscriminants::Drop, QueryDiscriminants::CreateIndex, + QueryDiscriminants::AlterTable, ]; } diff --git a/simulator/profiles/query.rs b/simulator/profiles/query.rs index a58c983e0..ee9583596 100644 --- a/simulator/profiles/query.rs +++ b/simulator/profiles/query.rs @@ -22,6 +22,8 @@ pub struct QueryProfile { pub delete_weight: u32, #[garde(skip)] pub drop_table_weight: u32, + #[garde(skip)] + pub alter_table_weight: u32, } impl Default for QueryProfile { @@ -35,10 +37,25 @@ impl Default for QueryProfile { update_weight: 20, delete_weight: 20, drop_table_weight: 2, + alter_table_weight: 2, } } } +impl QueryProfile { + /// Attention: edit this function when another weight is added + pub fn total_weight(&self) -> u32 { + self.select_weight + + self.create_table_weight + + self.create_index_weight + + self.insert_weight + + self.update_weight + + self.delete_weight + + self.drop_table_weight + + self.alter_table_weight + } +} + #[derive(Debug, Clone, strum::VariantArray)] pub enum QueryTypes { CreateTable, From ab152890ddd0243fb2f9150e39a364e197459d82 Mon Sep 17 00:00:00 2001 From: pedrocarlo Date: Thu, 9 Oct 2025 17:33:13 -0300 Subject: [PATCH 05/17] adjust generation of `GTValue` and `LTValue` to accomodate for Null Values --- sql_generation/generation/predicate/binary.rs | 78 ++++++++----------- sql_generation/generation/predicate/mod.rs | 10 --- sql_generation/generation/value/cmp.rs | 50 +++--------- sql_generation/generation/value/mod.rs | 12 ++- sql_generation/model/table.rs | 2 +- 5 files changed, 55 insertions(+), 97 deletions(-) diff --git a/sql_generation/generation/predicate/binary.rs b/sql_generation/generation/predicate/binary.rs index e3b52d5ec..25d813c76 100644 --- a/sql_generation/generation/predicate/binary.rs +++ b/sql_generation/generation/predicate/binary.rs @@ -16,44 +16,6 @@ use crate::{ }; impl Predicate { - /// Generate an [ast::Expr::Binary] [Predicate] from a column and [SimValue] - pub fn from_column_binary( - rng: &mut R, - context: &C, - column_name: &str, - value: &SimValue, - ) -> Predicate { - let expr = one_of( - vec![ - Box::new(|_| { - Expr::Binary( - Box::new(Expr::Id(ast::Name::exact(column_name.to_string()))), - ast::Operator::Equals, - Box::new(Expr::Literal(value.into())), - ) - }), - Box::new(|rng| { - let gt_value = GTValue::arbitrary_from(rng, context, value).0; - Expr::Binary( - Box::new(Expr::Id(ast::Name::exact(column_name.to_string()))), - ast::Operator::Greater, - Box::new(Expr::Literal(gt_value.into())), - ) - }), - Box::new(|rng| { - let lt_value = LTValue::arbitrary_from(rng, context, value).0; - Expr::Binary( - Box::new(Expr::Id(ast::Name::exact(column_name.to_string()))), - ast::Operator::Less, - Box::new(Expr::Literal(lt_value.into())), - ) - }), - ], - rng, - ); - Predicate(expr) - } - /// Produces a true [ast::Expr::Binary] [Predicate] that is true for the provided row in the given table pub fn true_binary( rng: &mut R, @@ -117,7 +79,8 @@ impl Predicate { ( 1, Box::new(|rng| { - let lt_value = LTValue::arbitrary_from(rng, context, value).0; + let lt_value = + LTValue::arbitrary_from(rng, context, (value, column.column_type)).0; Some(Expr::Binary( Box::new(ast::Expr::Qualified( ast::Name::from_string(&table_name), @@ -131,7 +94,8 @@ impl Predicate { ( 1, Box::new(|rng| { - let gt_value = GTValue::arbitrary_from(rng, context, value).0; + let gt_value = + GTValue::arbitrary_from(rng, context, (value, column.column_type)).0; Some(Expr::Binary( Box::new(ast::Expr::Qualified( ast::Name::from_string(&table_name), @@ -223,7 +187,8 @@ impl Predicate { ) }), Box::new(|rng| { - let gt_value = GTValue::arbitrary_from(rng, context, value).0; + let gt_value = + GTValue::arbitrary_from(rng, context, (value, column.column_type)).0; Expr::Binary( Box::new(ast::Expr::Qualified( ast::Name::from_string(&table_name), @@ -234,7 +199,8 @@ impl Predicate { ) }), Box::new(|rng| { - let lt_value = LTValue::arbitrary_from(rng, context, value).0; + let lt_value = + LTValue::arbitrary_from(rng, context, (value, column.column_type)).0; Expr::Binary( Box::new(ast::Expr::Qualified( ast::Name::from_string(&table_name), @@ -283,7 +249,12 @@ impl SimplePredicate { ) }), Box::new(|rng| { - let lt_value = LTValue::arbitrary_from(rng, context, column_value).0; + let lt_value = LTValue::arbitrary_from( + rng, + context, + (column_value, column.column.column_type), + ) + .0; Expr::Binary( Box::new(Expr::Qualified( ast::Name::from_string(table_name), @@ -294,7 +265,12 @@ impl SimplePredicate { ) }), Box::new(|rng| { - let gt_value = GTValue::arbitrary_from(rng, context, column_value).0; + let gt_value = GTValue::arbitrary_from( + rng, + context, + (column_value, column.column.column_type), + ) + .0; Expr::Binary( Box::new(Expr::Qualified( ast::Name::from_string(table_name), @@ -341,7 +317,12 @@ impl SimplePredicate { ) }), Box::new(|rng| { - let gt_value = GTValue::arbitrary_from(rng, context, column_value).0; + let gt_value = GTValue::arbitrary_from( + rng, + context, + (column_value, column.column.column_type), + ) + .0; Expr::Binary( Box::new(ast::Expr::Qualified( ast::Name::from_string(table_name), @@ -352,7 +333,12 @@ impl SimplePredicate { ) }), Box::new(|rng| { - let lt_value = LTValue::arbitrary_from(rng, context, column_value).0; + let lt_value = LTValue::arbitrary_from( + rng, + context, + (column_value, column.column.column_type), + ) + .0; Expr::Binary( Box::new(ast::Expr::Qualified( ast::Name::from_string(table_name), diff --git a/sql_generation/generation/predicate/mod.rs b/sql_generation/generation/predicate/mod.rs index d0dd375bb..75546848a 100644 --- a/sql_generation/generation/predicate/mod.rs +++ b/sql_generation/generation/predicate/mod.rs @@ -76,16 +76,6 @@ impl ArbitraryFrom<(&T, bool)> for Predicate { } } -impl ArbitraryFrom<(&str, &SimValue)> for Predicate { - fn arbitrary_from( - rng: &mut R, - context: &C, - (column_name, value): (&str, &SimValue), - ) -> Self { - Predicate::from_column_binary(rng, context, column_name, value) - } -} - impl ArbitraryFrom<(&Table, &Vec)> for Predicate { fn arbitrary_from( rng: &mut R, diff --git a/sql_generation/generation/value/cmp.rs b/sql_generation/generation/value/cmp.rs index 567a59a5e..31c710acd 100644 --- a/sql_generation/generation/value/cmp.rs +++ b/sql_generation/generation/value/cmp.rs @@ -2,32 +2,16 @@ use turso_core::Value; use crate::{ generation::{ArbitraryFrom, GenerationContext}, - model::table::SimValue, + model::table::{ColumnType, SimValue}, }; pub struct LTValue(pub SimValue); -impl ArbitraryFrom<&Vec<&SimValue>> for LTValue { - fn arbitrary_from( - rng: &mut R, - context: &C, - values: &Vec<&SimValue>, - ) -> Self { - if values.is_empty() { - return Self(SimValue(Value::Null)); - } - - // Get value less than all values - let value = Value::exec_min(values.iter().map(|value| &value.0)); - Self::arbitrary_from(rng, context, &SimValue(value)) - } -} - -impl ArbitraryFrom<&SimValue> for LTValue { +impl ArbitraryFrom<(&SimValue, ColumnType)> for LTValue { fn arbitrary_from( rng: &mut R, _context: &C, - value: &SimValue, + (value, _col_type): (&SimValue, ColumnType), ) -> Self { let new_value = match &value.0 { Value::Integer(i) => Value::Integer(rng.random_range(i64::MIN..*i - 1)), @@ -69,7 +53,8 @@ impl ArbitraryFrom<&SimValue> for LTValue { Value::Blob(b) } } - _ => unreachable!(), + // A value with storage class NULL is considered less than any other value (including another value with storage class NULL) + Value::Null => Value::Null, }; Self(SimValue(new_value)) } @@ -77,27 +62,11 @@ impl ArbitraryFrom<&SimValue> for LTValue { pub struct GTValue(pub SimValue); -impl ArbitraryFrom<&Vec<&SimValue>> for GTValue { +impl ArbitraryFrom<(&SimValue, ColumnType)> for GTValue { fn arbitrary_from( rng: &mut R, context: &C, - values: &Vec<&SimValue>, - ) -> Self { - if values.is_empty() { - return Self(SimValue(Value::Null)); - } - // Get value greater than all values - let value = Value::exec_max(values.iter().map(|value| &value.0)); - - Self::arbitrary_from(rng, context, &SimValue(value)) - } -} - -impl ArbitraryFrom<&SimValue> for GTValue { - fn arbitrary_from( - rng: &mut R, - _context: &C, - value: &SimValue, + (value, col_type): (&SimValue, ColumnType), ) -> Self { let new_value = match &value.0 { Value::Integer(i) => Value::Integer(rng.random_range(*i..i64::MAX)), @@ -139,7 +108,10 @@ impl ArbitraryFrom<&SimValue> for GTValue { Value::Blob(b) } } - _ => unreachable!(), + Value::Null => { + // Any value is greater than NULL, except NULL + SimValue::arbitrary_from(rng, context, col_type).0 + } }; Self(SimValue(new_value)) } diff --git a/sql_generation/generation/value/mod.rs b/sql_generation/generation/value/mod.rs index e0c98ad84..5062c9e57 100644 --- a/sql_generation/generation/value/mod.rs +++ b/sql_generation/generation/value/mod.rs @@ -51,8 +51,18 @@ impl ArbitraryFrom<&ColumnType> for SimValue { ColumnType::Integer => Value::Integer(rng.random_range(i64::MIN..i64::MAX)), ColumnType::Float => Value::Float(rng.random_range(-1e10..1e10)), ColumnType::Text => Value::build_text(gen_random_text(rng)), - ColumnType::Blob => Value::Blob(gen_random_text(rng).as_bytes().to_vec()), + ColumnType::Blob => Value::Blob(gen_random_text(rng).into_bytes()), }; SimValue(value) } } + +impl ArbitraryFrom for SimValue { + fn arbitrary_from( + rng: &mut R, + context: &C, + column_type: ColumnType, + ) -> Self { + SimValue::arbitrary_from(rng, context, &column_type) + } +} diff --git a/sql_generation/model/table.rs b/sql_generation/model/table.rs index 1060b8bb8..ceb650900 100644 --- a/sql_generation/model/table.rs +++ b/sql_generation/model/table.rs @@ -98,7 +98,7 @@ impl Display for Column { } } -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Copy, Serialize, Deserialize)] pub enum ColumnType { Integer, Float, From 9c2edbb8b7c9d88836f87d02734a58194b7137d2 Mon Sep 17 00:00:00 2001 From: pedrocarlo Date: Thu, 9 Oct 2025 19:05:50 -0300 Subject: [PATCH 06/17] create separate Index struct for sql generation --- simulator/generation/query.rs | 2 +- simulator/model/mod.rs | 12 ++++++--- sql_generation/generation/query.rs | 10 +++++--- sql_generation/model/query/create_index.rs | 30 +++++++++++++++++----- sql_generation/model/table.rs | 11 ++++++-- whopper/main.rs | 18 ++++++++----- 6 files changed, 58 insertions(+), 25 deletions(-) diff --git a/simulator/generation/query.rs b/simulator/generation/query.rs index 627e5e40e..45140dfe6 100644 --- a/simulator/generation/query.rs +++ b/simulator/generation/query.rs @@ -73,7 +73,7 @@ fn random_create_index( .expect("table should exist") .indexes .iter() - .any(|i| i == &create_index.index_name) + .any(|i| i.index_name == create_index.index_name) { create_index = CreateIndex::arbitrary(rng, conn_ctx); } diff --git a/simulator/model/mod.rs b/simulator/model/mod.rs index d50e386d7..9f125a15b 100644 --- a/simulator/model/mod.rs +++ b/simulator/model/mod.rs @@ -13,7 +13,7 @@ use sql_generation::model::{ transaction::{Begin, Commit, Rollback}, update::Update, }, - table::{JoinTable, JoinType, SimValue, Table, TableContext}, + table::{Index, JoinTable, JoinType, SimValue, Table, TableContext}, }; use turso_parser::ast::Distinctness; @@ -70,7 +70,9 @@ impl Query { | Query::Update(Update { table, .. }) | Query::Drop(Drop { table, .. }) | Query::CreateIndex(CreateIndex { - table_name: table, .. + index: Index { + table_name: table, .. + }, }) | Query::AlterTable(AlterTable { table_name: table, .. @@ -89,7 +91,9 @@ impl Query { | Query::Update(Update { table, .. }) | Query::Drop(Drop { table, .. }) | Query::CreateIndex(CreateIndex { - table_name: table, .. + index: Index { + table_name: table, .. + }, }) | Query::AlterTable(AlterTable { table_name: table, .. @@ -243,7 +247,7 @@ impl Shadow for CreateIndex { .find(|t| t.name == self.table_name) .unwrap() .indexes - .push(self.index_name.clone()); + .push(self.index.clone()); vec![] } } diff --git a/sql_generation/generation/query.rs b/sql_generation/generation/query.rs index 82d6296df..ef035e0be 100644 --- a/sql_generation/generation/query.rs +++ b/sql_generation/generation/query.rs @@ -11,7 +11,7 @@ use crate::model::query::select::{ use crate::model::query::update::Update; use crate::model::query::{Create, CreateIndex, Delete, Drop, Insert, Select}; use crate::model::table::{ - Column, JoinTable, JoinType, JoinedTable, Name, SimValue, Table, TableContext, + Column, Index, JoinTable, JoinType, JoinedTable, Name, SimValue, Table, TableContext, }; use indexmap::IndexSet; use itertools::Itertools; @@ -362,9 +362,11 @@ impl Arbitrary for CreateIndex { ); CreateIndex { - index_name, - table_name: table.name.clone(), - columns, + index: Index { + index_name, + table_name: table.name.clone(), + columns, + }, } } } diff --git a/sql_generation/model/query/create_index.rs b/sql_generation/model/query/create_index.rs index db9d15a04..55548114e 100644 --- a/sql_generation/model/query/create_index.rs +++ b/sql_generation/model/query/create_index.rs @@ -1,11 +1,26 @@ +use std::ops::{Deref, DerefMut}; + use serde::{Deserialize, Serialize}; -use turso_parser::ast::SortOrder; + +use crate::model::table::Index; #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)] pub struct CreateIndex { - pub index_name: String, - pub table_name: String, - pub columns: Vec<(String, SortOrder)>, + pub index: Index, +} + +impl Deref for CreateIndex { + type Target = Index; + + fn deref(&self) -> &Self::Target { + &self.index + } +} + +impl DerefMut for CreateIndex { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.index + } } impl std::fmt::Display for CreateIndex { @@ -13,9 +28,10 @@ impl std::fmt::Display for CreateIndex { write!( f, "CREATE INDEX {} ON {} ({})", - self.index_name, - self.table_name, - self.columns + self.index.index_name, + self.index.table_name, + self.index + .columns .iter() .map(|(name, order)| format!("{name} {order}")) .collect::>() diff --git a/sql_generation/model/table.rs b/sql_generation/model/table.rs index ceb650900..e51ef8172 100644 --- a/sql_generation/model/table.rs +++ b/sql_generation/model/table.rs @@ -3,7 +3,7 @@ use std::{fmt::Display, hash::Hash, ops::Deref}; use itertools::Itertools; use serde::{Deserialize, Serialize}; use turso_core::{numeric::Numeric, types}; -use turso_parser::ast::{self, ColumnConstraint}; +use turso_parser::ast::{self, ColumnConstraint, SortOrder}; use crate::model::query::predicate::Predicate; @@ -46,7 +46,7 @@ pub struct Table { pub name: String, pub columns: Vec, pub rows: Vec>, - pub indexes: Vec, + pub indexes: Vec, } impl Table { @@ -117,6 +117,13 @@ impl Display for ColumnType { } } +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)] +pub struct Index { + pub table_name: String, + pub index_name: String, + pub columns: Vec<(String, SortOrder)>, +} + #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] pub struct JoinedTable { /// table name diff --git a/whopper/main.rs b/whopper/main.rs index 16da0fbf8..67f3f5e6b 100644 --- a/whopper/main.rs +++ b/whopper/main.rs @@ -4,11 +4,13 @@ use rand::{Rng, RngCore, SeedableRng}; use rand_chacha::ChaCha8Rng; use sql_generation::{ generation::{Arbitrary, GenerationContext, Opts}, - model::query::{ - create::Create, create_index::CreateIndex, delete::Delete, drop_index::DropIndex, - insert::Insert, select::Select, update::Update, + model::{ + query::{ + create::Create, create_index::CreateIndex, delete::Delete, drop_index::DropIndex, + insert::Insert, select::Select, update::Update, + }, + table::{Column, ColumnType, Index, Table}, }, - model::table::{Column, ColumnType, Table}, }; use std::cell::RefCell; use std::collections::HashMap; @@ -306,9 +308,11 @@ fn create_initial_indexes(rng: &mut ChaCha8Rng, tables: &[Table]) -> Vec Date: Thu, 9 Oct 2025 19:29:14 -0300 Subject: [PATCH 07/17] fix Drop Column to only be generated if no columns conflict in Indexes --- sql_generation/generation/query.rs | 105 ++++++++++++++++++++++------- 1 file changed, 80 insertions(+), 25 deletions(-) diff --git a/sql_generation/generation/query.rs b/sql_generation/generation/query.rs index ef035e0be..94aafaae9 100644 --- a/sql_generation/generation/query.rs +++ b/sql_generation/generation/query.rs @@ -1,6 +1,6 @@ use crate::generation::{ - gen_random_text, pick_n_unique, pick_unique, Arbitrary, ArbitraryFrom, ArbitrarySized, - GenerationContext, + gen_random_text, pick_index, pick_n_unique, pick_unique, Arbitrary, ArbitraryFrom, + ArbitrarySized, GenerationContext, }; use crate::model::query::alter_table::{AlterTable, AlterTableType, AlterTableTypeDiscriminants}; use crate::model::query::predicate::Predicate; @@ -392,26 +392,52 @@ impl Arbitrary for Update { } } -impl Arbitrary for AlterTable { - fn arbitrary(rng: &mut R, context: &C) -> Self { - let table = pick(context.tables(), rng); - let choices: &'static [AlterTableTypeDiscriminants] = if table.columns.len() > 1 { - &[ - AlterTableTypeDiscriminants::RenameTo, - AlterTableTypeDiscriminants::AddColumn, - // AlterTableTypeDiscriminants::AlterColumn, - AlterTableTypeDiscriminants::RenameColumn, - AlterTableTypeDiscriminants::DropColumn, - ] - } else { - &[ - AlterTableTypeDiscriminants::RenameTo, - AlterTableTypeDiscriminants::AddColumn, - // AlterTableTypeDiscriminants::AlterColumn, - AlterTableTypeDiscriminants::RenameColumn, - ] - }; - let alter_table_type = match choices.choose(rng).unwrap() { +const ALTER_TABLE_ALL: &[AlterTableTypeDiscriminants] = &[ + AlterTableTypeDiscriminants::RenameTo, + AlterTableTypeDiscriminants::AddColumn, + // AlterTableTypeDiscriminants::AlterColumn, + AlterTableTypeDiscriminants::RenameColumn, + AlterTableTypeDiscriminants::DropColumn, +]; +const ALTER_TABLE_NO_DROP: &[AlterTableTypeDiscriminants] = &[ + AlterTableTypeDiscriminants::RenameTo, + AlterTableTypeDiscriminants::AddColumn, + // AlterTableTypeDiscriminants::AlterColumn, + AlterTableTypeDiscriminants::RenameColumn, +]; + +// TODO: Unfortunately this diff strategy allocates a couple of IndexSet's +// in the future maybe change this to be more efficient. This is currently acceptable because this function +// is only called for `DropColumn` +fn get_column_diff(table: &Table) -> IndexSet<&str> { + // Columns that are referenced in INDEXES cannot be dropped + let column_cannot_drop = table + .indexes + .iter() + .flat_map(|index| index.columns.iter().map(|(col_name, _)| col_name.as_str())) + .collect::>(); + if column_cannot_drop.len() == table.columns.len() { + // Optimization: all columns are present in indexes so we do not need to but the table column set + return IndexSet::new(); + } + + let column_set: IndexSet<_, std::hash::RandomState> = + IndexSet::from_iter(table.columns.iter().map(|col| col.name.as_str())); + + let diff = column_set + .difference(&column_cannot_drop) + .copied() + .collect::>(); + diff +} + +impl ArbitraryFrom<(&Table, &[AlterTableTypeDiscriminants])> for AlterTableType { + fn arbitrary_from( + rng: &mut R, + context: &C, + (table, choices): (&Table, &[AlterTableTypeDiscriminants]), + ) -> Self { + match choices.choose(rng).unwrap() { AlterTableTypeDiscriminants::RenameTo => AlterTableType::RenameTo { new_name: Name::arbitrary(rng, context).0, }, @@ -425,10 +451,39 @@ impl Arbitrary for AlterTable { old: pick(&table.columns, rng).name.clone(), new: Name::arbitrary(rng, context).0, }, - AlterTableTypeDiscriminants::DropColumn => AlterTableType::DropColumn { - column_name: pick(&table.columns, rng).name.clone(), - }, + AlterTableTypeDiscriminants::DropColumn => { + let col_diff = get_column_diff(table); + + if col_diff.is_empty() { + // Generate a DropColumn if we can drop a column + return AlterTableType::arbitrary_from( + rng, + context, + (table, ALTER_TABLE_NO_DROP), + ); + } + + let col_idx = pick_index(col_diff.len(), rng); + let col_name = col_diff.get_index(col_idx).unwrap(); + + AlterTableType::DropColumn { + column_name: col_name.to_string(), + } + } + } + } +} + +impl Arbitrary for AlterTable { + fn arbitrary(rng: &mut R, context: &C) -> Self { + let table = pick(context.tables(), rng); + let choices: &'static [AlterTableTypeDiscriminants] = if table.columns.len() > 1 { + ALTER_TABLE_ALL + } else { + ALTER_TABLE_NO_DROP }; + + let alter_table_type = AlterTableType::arbitrary_from(rng, context, (table, choices)); Self { table_name: table.name.clone(), alter_table_type, From 703efaa724fe11c1a1c14e1470c0d59bf5b49b46 Mon Sep 17 00:00:00 2001 From: pedrocarlo Date: Thu, 9 Oct 2025 21:15:31 -0300 Subject: [PATCH 08/17] adjust Properties to skip Alter Table in certain conditions --- simulator/generation/property.rs | 41 +++++++++++++++++++++++--------- 1 file changed, 30 insertions(+), 11 deletions(-) diff --git a/simulator/generation/property.rs b/simulator/generation/property.rs index b4ac73f25..cadd42155 100644 --- a/simulator/generation/property.rs +++ b/simulator/generation/property.rs @@ -12,6 +12,7 @@ use sql_generation::{ model::{ query::{ Create, Delete, Drop, Insert, Select, + alter_table::{AlterTable, AlterTableType}, predicate::Predicate, select::{CompoundOperator, CompoundSelect, ResultColumn, SelectBody, SelectInner}, transaction::{Begin, Commit, Rollback}, @@ -283,7 +284,7 @@ impl Property { // - [x] There will be no errors in the middle interactions. (this constraint is impossible to check, so this is just best effort) // - [x] The inserted row will not be deleted. // - [x] The inserted row will not be updated. - // - [ ] The table `t` will not be renamed, dropped, or altered. (todo: add this constraint once ALTER or DROP is implemented) + // - [x] The table `t` will not be renamed, dropped, or altered. |rng: &mut R, ctx: &G, query_distr: &QueryDistribution, property: &Property| { let Property::InsertValuesSelect { insert, row_index, .. @@ -327,6 +328,10 @@ impl Property { // Cannot drop the table we are inserting None } + Query::AlterTable(AlterTable { table_name: t, .. }) if *t == table.name => { + // Cannot alter the table we are inserting + None + } _ => Some(query), } } @@ -334,7 +339,7 @@ impl Property { Property::DoubleCreateFailure { .. } => { // The interactions in the middle has the following constraints; // - [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) + // - [x] Table `t` will not be renamed or dropped. |rng: &mut R, ctx: &G, query_distr: &QueryDistribution, property: &Property| { let Property::DoubleCreateFailure { create, .. } = property else { unreachable!() @@ -358,6 +363,10 @@ impl Property { // Cannot Drop the created table None } + Query::AlterTable(AlterTable { table_name: t, .. }) if *t == table.name => { + // Cannot alter the table we created + None + } _ => Some(query), } } @@ -365,7 +374,7 @@ impl Property { Property::DeleteSelect { .. } => { // - [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) + // - [x] The table `t` will not be renamed, dropped, or altered. |rng, ctx, query_distr, property| { let Property::DeleteSelect { @@ -412,13 +421,17 @@ impl Property { // Cannot Drop the same table None } + Query::AlterTable(AlterTable { table_name: t, .. }) if *t == table.name => { + // Cannot alter the same table + None + } _ => Some(query), } } } Property::DropSelect { .. } => { // - [x] There will be no errors in the middle interactions. (this constraint is impossible to check, so this is just best effort) - // - [-] The table `t` will not be created, no table will be renamed to `t`. (todo: update this constraint once ALTER is implemented) + // - [x] The table `t` will not be created, no table will be renamed to `t`. |rng, ctx, query_distr, property: &Property| { let Property::DropSelect { table: table_name, .. @@ -428,13 +441,19 @@ impl Property { }; let query = Query::arbitrary_from(rng, ctx, query_distr); - if let Query::Create(Create { table: t }) = &query - && t.name == *table_name - { - // - The table `t` will not be created - None - } else { - Some(query) + match &query { + Query::Create(Create { table: t }) if t.name == *table_name => { + // - The table `t` will not be created + None + } + Query::AlterTable(AlterTable { + table_name: t, + alter_table_type: AlterTableType::RenameTo { new_name }, + }) if t == table_name || new_name == table_name => { + // no table will be renamed to `t` + None + } + _ => Some(query), } } } From ca8be11a568770c0b2d38225e4ffb4a16e03ad77 Mon Sep 17 00:00:00 2001 From: pedrocarlo Date: Thu, 9 Oct 2025 22:32:52 -0300 Subject: [PATCH 09/17] fix binary compare in simulator by taking into account NULL for certain compare ops --- sql_generation/model/table.rs | 40 +++++++++++++++++++++++++++++------ 1 file changed, 33 insertions(+), 7 deletions(-) diff --git a/sql_generation/model/table.rs b/sql_generation/model/table.rs index e51ef8172..dce2fdddf 100644 --- a/sql_generation/model/table.rs +++ b/sql_generation/model/table.rs @@ -186,19 +186,34 @@ impl Display for SimValue { impl SimValue { pub const FALSE: Self = SimValue(types::Value::Integer(0)); pub const TRUE: Self = SimValue(types::Value::Integer(1)); + pub const NULL: Self = SimValue(types::Value::Null); pub fn as_bool(&self) -> bool { Numeric::from(&self.0).try_into_bool().unwrap_or_default() } + #[inline] + fn is_null(&self) -> bool { + matches!(self.0, types::Value::Null) + } + + // The result of any binary operator is either a numeric value or NULL, except for the || concatenation operator, and the -> and ->> extract operators which can return values of any type. + // All operators generally evaluate to NULL when any operand is NULL, with specific exceptions as stated below. This is in accordance with the SQL92 standard. + // When paired with NULL: + // AND evaluates to 0 (false) when the other operand is false; and + // OR evaluates to 1 (true) when the other operand is true. + // The IS and IS NOT operators work like = and != except when one or both of the operands are NULL. In this case, if both operands are NULL, then the IS operator evaluates to 1 (true) and the IS NOT operator evaluates to 0 (false). If one operand is NULL and the other is not, then the IS operator evaluates to 0 (false) and the IS NOT operator is 1 (true). It is not possible for an IS or IS NOT expression to evaluate to NULL. + // The IS NOT DISTINCT FROM operator is an alternative spelling for the IS operator. Likewise, the IS DISTINCT FROM operator means the same thing as IS NOT. Standard SQL does not support the compact IS and IS NOT notation. Those compact forms are an SQLite extension. You must use the less readable IS NOT DISTINCT FROM and IS DISTINCT FROM operators in most other SQL database engines. + // TODO: support more predicates /// Returns a Result of a Binary Operation /// /// TODO: forget collations for now /// TODO: have the [ast::Operator::Equals], [ast::Operator::NotEquals], [ast::Operator::Greater], /// [ast::Operator::GreaterEquals], [ast::Operator::Less], [ast::Operator::LessEquals] function to be extracted - /// into its functions in turso_core so that it can be used here + /// into its functions in turso_core so that it can be used here. For now we just do the `not_null` check to avoid refactoring code in core pub fn binary_compare(&self, other: &Self, operator: ast::Operator) -> SimValue { + let not_null = !self.is_null() && !other.is_null(); match operator { ast::Operator::Add => self.0.exec_add(&other.0).into(), ast::Operator::And => self.0.exec_and(&other.0).into(), @@ -208,10 +223,10 @@ impl SimValue { ast::Operator::BitwiseOr => self.0.exec_bit_or(&other.0).into(), ast::Operator::BitwiseNot => todo!(), // TODO: Do not see any function usage of this operator in Core ast::Operator::Concat => self.0.exec_concat(&other.0).into(), - ast::Operator::Equals => (self == other).into(), + ast::Operator::Equals => not_null.then(|| self == other).into(), ast::Operator::Divide => self.0.exec_divide(&other.0).into(), - ast::Operator::Greater => (self > other).into(), - ast::Operator::GreaterEquals => (self >= other).into(), + ast::Operator::Greater => not_null.then(|| self > other).into(), + ast::Operator::GreaterEquals => not_null.then(|| self >= other).into(), // TODO: Test these implementations ast::Operator::Is => match (&self.0, &other.0) { (types::Value::Null, types::Value::Null) => true.into(), @@ -223,11 +238,11 @@ impl SimValue { .binary_compare(other, ast::Operator::Is) .unary_exec(ast::UnaryOperator::Not), ast::Operator::LeftShift => self.0.exec_shift_left(&other.0).into(), - ast::Operator::Less => (self < other).into(), - ast::Operator::LessEquals => (self <= other).into(), + ast::Operator::Less => not_null.then(|| self < other).into(), + ast::Operator::LessEquals => not_null.then(|| self <= other).into(), ast::Operator::Modulus => self.0.exec_remainder(&other.0).into(), ast::Operator::Multiply => self.0.exec_multiply(&other.0).into(), - ast::Operator::NotEquals => (self != other).into(), + ast::Operator::NotEquals => not_null.then(|| self != other).into(), ast::Operator::Or => self.0.exec_or(&other.0).into(), ast::Operator::RightShift => self.0.exec_shift_right(&other.0).into(), ast::Operator::Subtract => self.0.exec_subtract(&other.0).into(), @@ -372,7 +387,18 @@ impl From<&SimValue> for ast::Literal { } } +impl From> for SimValue { + #[inline] + fn from(value: Option) -> Self { + if value.is_none() { + return SimValue::NULL; + } + SimValue::from(value.unwrap()) + } +} + impl From for SimValue { + #[inline] fn from(value: bool) -> Self { if value { SimValue::TRUE From a18a4726857fde726d821f5e1792b5283310f809 Mon Sep 17 00:00:00 2001 From: pedrocarlo Date: Thu, 9 Oct 2025 23:25:22 -0300 Subject: [PATCH 10/17] add option to disable `alter column` for differential testing --- simulator/runner/env.rs | 3 +++ sql_generation/generation/opts.rs | 17 +++++++++++++- sql_generation/generation/query.rs | 36 ++++++++++++++++++++++++------ 3 files changed, 48 insertions(+), 8 deletions(-) diff --git a/simulator/runner/env.rs b/simulator/runner/env.rs index 79497c38b..56c81fd83 100644 --- a/simulator/runner/env.rs +++ b/simulator/runner/env.rs @@ -351,6 +351,9 @@ impl SimulatorEnv { profile.io.enable = false; // Disable limits due to differences in return order from turso and rusqlite opts.disable_select_limit = true; + + // There is no `ALTER COLUMN` in SQLite + profile.query.gen_opts.query.alter_table.alter_column = false; } profile.validate().unwrap(); diff --git a/sql_generation/generation/opts.rs b/sql_generation/generation/opts.rs index 190033748..e9da87207 100644 --- a/sql_generation/generation/opts.rs +++ b/sql_generation/generation/opts.rs @@ -93,6 +93,8 @@ pub struct QueryOpts { pub from_clause: FromClauseOpts, #[garde(dive)] pub insert: InsertOpts, + #[garde(dive)] + pub alter_table: AlterTableOpts, } #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, Validate)] @@ -198,6 +200,19 @@ impl Default for InsertOpts { } } +#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, Validate)] +#[serde(deny_unknown_fields)] +pub struct AlterTableOpts { + #[garde(skip)] + pub alter_column: bool, +} + +impl Default for AlterTableOpts { + fn default() -> Self { + Self { alter_column: true } + } +} + fn range_struct_min( min: T, ) -> impl FnOnce(&Range, &()) -> garde::Result { @@ -217,7 +232,7 @@ fn range_struct_min( } } -#[allow(dead_code)] +#[expect(dead_code)] fn range_struct_max( max: T, ) -> impl FnOnce(&Range, &()) -> garde::Result { diff --git a/sql_generation/generation/query.rs b/sql_generation/generation/query.rs index 94aafaae9..a21a37231 100644 --- a/sql_generation/generation/query.rs +++ b/sql_generation/generation/query.rs @@ -395,14 +395,25 @@ impl Arbitrary for Update { const ALTER_TABLE_ALL: &[AlterTableTypeDiscriminants] = &[ AlterTableTypeDiscriminants::RenameTo, AlterTableTypeDiscriminants::AddColumn, - // AlterTableTypeDiscriminants::AlterColumn, + AlterTableTypeDiscriminants::AlterColumn, AlterTableTypeDiscriminants::RenameColumn, AlterTableTypeDiscriminants::DropColumn, ]; const ALTER_TABLE_NO_DROP: &[AlterTableTypeDiscriminants] = &[ AlterTableTypeDiscriminants::RenameTo, AlterTableTypeDiscriminants::AddColumn, - // AlterTableTypeDiscriminants::AlterColumn, + AlterTableTypeDiscriminants::AlterColumn, + AlterTableTypeDiscriminants::RenameColumn, +]; +const ALTER_TABLE_NO_ALTER_COL: &[AlterTableTypeDiscriminants] = &[ + AlterTableTypeDiscriminants::RenameTo, + AlterTableTypeDiscriminants::AddColumn, + AlterTableTypeDiscriminants::RenameColumn, + AlterTableTypeDiscriminants::DropColumn, +]; +const ALTER_TABLE_NO_ALTER_COL_NO_DROP: &[AlterTableTypeDiscriminants] = &[ + AlterTableTypeDiscriminants::RenameTo, + AlterTableTypeDiscriminants::AddColumn, AlterTableTypeDiscriminants::RenameColumn, ]; @@ -459,7 +470,14 @@ impl ArbitraryFrom<(&Table, &[AlterTableTypeDiscriminants])> for AlterTableType return AlterTableType::arbitrary_from( rng, context, - (table, ALTER_TABLE_NO_DROP), + ( + table, + if context.opts().query.alter_table.alter_column { + ALTER_TABLE_NO_DROP + } else { + ALTER_TABLE_NO_ALTER_COL_NO_DROP + }, + ), ); } @@ -477,10 +495,14 @@ impl ArbitraryFrom<(&Table, &[AlterTableTypeDiscriminants])> for AlterTableType impl Arbitrary for AlterTable { fn arbitrary(rng: &mut R, context: &C) -> Self { let table = pick(context.tables(), rng); - let choices: &'static [AlterTableTypeDiscriminants] = if table.columns.len() > 1 { - ALTER_TABLE_ALL - } else { - ALTER_TABLE_NO_DROP + let choices = match ( + table.columns.len() > 1, + context.opts().query.alter_table.alter_column, + ) { + (true, true) => ALTER_TABLE_ALL, + (true, false) => ALTER_TABLE_NO_ALTER_COL, + (false, true) => ALTER_TABLE_NO_DROP, + (false, false) => ALTER_TABLE_NO_ALTER_COL_NO_DROP, }; let alter_table_type = AlterTableType::arbitrary_from(rng, context, (table, choices)); From 49e96afd39d5e43c4aec5ef66d20d44c445a4caf Mon Sep 17 00:00:00 2001 From: pedrocarlo Date: Fri, 10 Oct 2025 00:04:38 -0300 Subject: [PATCH 11/17] generate `ALTER COLUMN` --- simulator/model/mod.rs | 2 -- simulator/shrink/plan.rs | 2 ++ sql_generation/generation/query.rs | 29 ++++++++++++++++++++++++++--- 3 files changed, 28 insertions(+), 5 deletions(-) diff --git a/simulator/model/mod.rs b/simulator/model/mod.rs index 9f125a15b..a2120cd63 100644 --- a/simulator/model/mod.rs +++ b/simulator/model/mod.rs @@ -561,8 +561,6 @@ impl Shadow for AlterTable { }); } AlterTableType::AlterColumn { old, new } => { - // TODO: have to see correct behaviour with indexes to see if we should error out - // in case there is some sort of conflict with this change let col = table.columns.iter_mut().find(|c| c.name == *old).unwrap(); *col = new.clone(); } diff --git a/simulator/shrink/plan.rs b/simulator/shrink/plan.rs index 6da5d93e8..9f80f78cc 100644 --- a/simulator/shrink/plan.rs +++ b/simulator/shrink/plan.rs @@ -120,6 +120,8 @@ impl InteractionPlan { | Property::DeleteSelect { queries, .. } | Property::DropSelect { queries, .. } | Property::Queries { queries } => { + // Remove placeholder queries + queries.retain(|query| !matches!(query, Query::Placeholder)); extensional_queries.append(queries); } Property::AllTableHaveExpectedContent { tables } => { diff --git a/sql_generation/generation/query.rs b/sql_generation/generation/query.rs index a21a37231..17fe0f843 100644 --- a/sql_generation/generation/query.rs +++ b/sql_generation/generation/query.rs @@ -456,7 +456,31 @@ impl ArbitraryFrom<(&Table, &[AlterTableTypeDiscriminants])> for AlterTableType column: Column::arbitrary(rng, context), }, AlterTableTypeDiscriminants::AlterColumn => { - todo!(); + let col_diff = get_column_diff(table); + + if col_diff.is_empty() { + // Generate a DropColumn if we can drop a column + return AlterTableType::arbitrary_from( + rng, + context, + ( + table, + if choices.contains(&AlterTableTypeDiscriminants::DropColumn) { + ALTER_TABLE_NO_ALTER_COL + } else { + ALTER_TABLE_NO_ALTER_COL_NO_DROP + }, + ), + ); + } + + let col_idx = pick_index(col_diff.len(), rng); + let col_name = col_diff.get_index(col_idx).unwrap(); + + AlterTableType::AlterColumn { + old: col_name.to_string(), + new: Column::arbitrary(rng, context), + } } AlterTableTypeDiscriminants::RenameColumn => AlterTableType::RenameColumn { old: pick(&table.columns, rng).name.clone(), @@ -501,8 +525,7 @@ impl Arbitrary for AlterTable { ) { (true, true) => ALTER_TABLE_ALL, (true, false) => ALTER_TABLE_NO_ALTER_COL, - (false, true) => ALTER_TABLE_NO_DROP, - (false, false) => ALTER_TABLE_NO_ALTER_COL_NO_DROP, + (false, true) | (false, false) => ALTER_TABLE_NO_ALTER_COL_NO_DROP, }; let alter_table_type = AlterTableType::arbitrary_from(rng, context, (table, choices)); From b6c5fee300c722538b6975557d03ffa9ba1e586b Mon Sep 17 00:00:00 2001 From: pedrocarlo Date: Fri, 10 Oct 2025 11:24:25 -0300 Subject: [PATCH 12/17] do not count certain interactions in the InteractionPlan and correctly report the length when shrinking --- simulator/generation/plan.rs | 70 ++++++++++++++++++++++++++++++++++-- 1 file changed, 68 insertions(+), 2 deletions(-) diff --git a/simulator/generation/plan.rs b/simulator/generation/plan.rs index 6285b62f7..b5ff4e475 100644 --- a/simulator/generation/plan.rs +++ b/simulator/generation/plan.rs @@ -58,11 +58,19 @@ impl InteractionPlan { pub fn new_with(plan: Vec, mvcc: bool) -> Self { let len = plan .iter() - .filter(|interaction| !interaction.is_transaction()) + .filter(|interaction| !interaction.ignore()) .count(); Self { plan, mvcc, len } } + #[inline] + fn new_len(&self) -> usize { + self.plan + .iter() + .filter(|interaction| !interaction.ignore()) + .count() + } + /// Length of interactions that are not transaction statements #[inline] pub fn len(&self) -> usize { @@ -70,12 +78,59 @@ impl InteractionPlan { } pub fn push(&mut self, interactions: Interactions) { - if !interactions.is_transaction() { + if !interactions.ignore() { self.len += 1; } self.plan.push(interactions); } + pub fn remove(&mut self, index: usize) -> Interactions { + let interactions = self.plan.remove(index); + if !interactions.ignore() { + self.len -= 1; + } + interactions + } + + pub fn truncate(&mut self, len: usize) { + self.plan.truncate(len); + self.len = self.new_len(); + } + + pub fn retain_mut(&mut self, mut f: F) + where + F: FnMut(&mut Interactions) -> bool, + { + let f = |t: &mut Interactions| { + let ignore = t.ignore(); + let retain = f(t); + // removed an interaction that was not previously ignored + if !retain && !ignore { + self.len -= 1; + } + retain + }; + self.plan.retain_mut(f); + } + + #[expect(dead_code)] + pub fn retain(&mut self, mut f: F) + where + F: FnMut(&Interactions) -> bool, + { + let f = |t: &Interactions| { + let ignore = t.ignore(); + let retain = f(t); + // removed an interaction that was not previously ignored + if !retain && !ignore { + self.len -= 1; + } + retain + }; + self.plan.retain(f); + self.len = self.new_len(); + } + /// 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 @@ -581,6 +636,17 @@ impl Interactions { InteractionsType::Query(..) | InteractionsType::Fault(..) => false, } } + + /// Interactions that are not counted/ignored in the InteractionPlan. + /// Used in InteractionPlan to not count certain interactions to its length, as they are just auxiliary. This allows more + /// meaningful interactions to be generation + fn ignore(&self) -> bool { + self.is_transaction() + || matches!( + self.interactions, + InteractionsType::Property(Property::AllTableHaveExpectedContent { .. }) + ) + } } impl Deref for Interactions { From 5f651961157f80d5131417dba74188b51d5114ec Mon Sep 17 00:00:00 2001 From: pedrocarlo Date: Fri, 10 Oct 2025 12:39:34 -0300 Subject: [PATCH 13/17] fix `load_bug` --- simulator/runner/bugbase.rs | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/simulator/runner/bugbase.rs b/simulator/runner/bugbase.rs index dd0d6f432..8ebcd1bf7 100644 --- a/simulator/runner/bugbase.rs +++ b/simulator/runner/bugbase.rs @@ -293,22 +293,23 @@ impl BugBase { None => anyhow::bail!("No bugs found for seed {}", seed), Some(Bug::Unloaded { .. }) => { let plan = - std::fs::read_to_string(self.path.join(seed.to_string()).join("test.json")) + std::fs::read_to_string(self.path.join(seed.to_string()).join("plan.json")) .with_context(|| { format!( "should be able to read plan file at {}", - self.path.join(seed.to_string()).join("test.json").display() + self.path.join(seed.to_string()).join("plan.json").display() ) })?; let plan: InteractionPlan = serde_json::from_str(&plan) .with_context(|| "should be able to deserialize plan")?; - let shrunk_plan: Option = std::fs::read_to_string( - self.path.join(seed.to_string()).join("shrunk_test.json"), - ) - .with_context(|| "should be able to read shrunk plan file") - .and_then(|shrunk| serde_json::from_str(&shrunk).map_err(|e| anyhow!("{}", e))) - .ok(); + let shrunk_plan: Option = + std::fs::read_to_string(self.path.join(seed.to_string()).join("shrunk.json")) + .with_context(|| "should be able to read shrunk plan file") + .and_then(|shrunk| { + serde_json::from_str(&shrunk).map_err(|e| anyhow!("{}", e)) + }) + .ok(); let shrunk_plan: Option = shrunk_plan.and_then(|shrunk_plan| serde_json::from_str(&shrunk_plan).ok()); From d99e3f590f2c6093931d32a466da6a5e10b13de7 Mon Sep 17 00:00:00 2001 From: pedrocarlo Date: Fri, 10 Oct 2025 13:06:58 -0300 Subject: [PATCH 14/17] `ALTER TABLE` should be added to `is_ddl` --- simulator/model/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/simulator/model/mod.rs b/simulator/model/mod.rs index a2120cd63..20726cbbb 100644 --- a/simulator/model/mod.rs +++ b/simulator/model/mod.rs @@ -115,7 +115,7 @@ impl Query { pub fn is_ddl(&self) -> bool { matches!( self, - Self::Create(..) | Self::CreateIndex(..) | Self::Drop(..) + Self::Create(..) | Self::CreateIndex(..) | Self::Drop(..) | Self::AlterTable(..) ) } } From dca1137f8181ecd67ba2578435b3bfc734850a73 Mon Sep 17 00:00:00 2001 From: pedrocarlo Date: Fri, 10 Oct 2025 13:08:41 -0300 Subject: [PATCH 15/17] rusqlite stop trying to get rows when we error with `InvalidColumnIndex` --- simulator/runner/execution.rs | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/simulator/runner/execution.rs b/simulator/runner/execution.rs index 7bc9b40e4..3cc720967 100644 --- a/simulator/runner/execution.rs +++ b/simulator/runner/execution.rs @@ -283,16 +283,13 @@ fn limbo_integrity_check(conn: &Arc) -> Result<()> { Ok(()) } +#[instrument(skip(env, interaction, stack), fields(conn_index = interaction.connection_index, interaction = %interaction))] fn execute_interaction_rusqlite( env: &mut SimulatorEnv, interaction: &Interaction, stack: &mut Vec, ) -> turso_core::Result { - tracing::trace!( - "execute_interaction_rusqlite(connection_index={}, interaction={})", - interaction.connection_index, - interaction - ); + tracing::info!(""); let SimConnection::SQLiteConnection(conn) = &mut env.connections[interaction.connection_index] else { unreachable!() @@ -347,11 +344,19 @@ fn execute_query_rusqlite( match query { Query::Select(select) => { let mut stmt = connection.prepare(select.to_string().as_str())?; - let columns = stmt.column_count(); let rows = stmt.query_map([], |row| { let mut values = vec![]; - for i in 0..columns { - let value = row.get_unwrap(i); + for i in 0.. { + let value = match row.get(i) { + Ok(value) => value, + Err(err) => match err { + rusqlite::Error::InvalidColumnIndex(_) => break, + _ => { + tracing::error!(?err); + panic!("{err}") + } + }, + }; let value = match value { rusqlite::types::Value::Null => Value::Null, rusqlite::types::Value::Integer(i) => Value::Integer(i), From 773fa280631ceb3a1fed61f6776f01ffd76cfa1d Mon Sep 17 00:00:00 2001 From: pedrocarlo Date: Fri, 10 Oct 2025 15:51:31 -0300 Subject: [PATCH 16/17] workaround in sqlite for schema changes become visible to other connections --- simulator/runner/execution.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/simulator/runner/execution.rs b/simulator/runner/execution.rs index 3cc720967..1e30de520 100644 --- a/simulator/runner/execution.rs +++ b/simulator/runner/execution.rs @@ -341,6 +341,9 @@ fn execute_query_rusqlite( connection: &rusqlite::Connection, query: &Query, ) -> rusqlite::Result>> { + // https://sqlite.org/forum/forumpost/9fe5d047f0 + // Due to a bug in sqlite, we need to execute this query to clear the internal stmt cache so that schema changes become visible always to other connections + connection.query_one("SELECT * FROM pragma_user_version()", (), |_| Ok(()))?; match query { Query::Select(select) => { let mut stmt = connection.prepare(select.to_string().as_str())?; From c0f35cc17db0e4d4056b7bdc0d3c61f5878e8dad Mon Sep 17 00:00:00 2001 From: pedrocarlo Date: Sat, 11 Oct 2025 14:03:53 -0300 Subject: [PATCH 17/17] disable `ALTER COLUMN` due to incompatibility with SQLITE INTEGRITY CHECK --- sql_generation/generation/opts.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/sql_generation/generation/opts.rs b/sql_generation/generation/opts.rs index e9da87207..fcc818bbe 100644 --- a/sql_generation/generation/opts.rs +++ b/sql_generation/generation/opts.rs @@ -207,9 +207,12 @@ pub struct AlterTableOpts { pub alter_column: bool, } +#[expect(clippy::derivable_impls)] impl Default for AlterTableOpts { fn default() -> Self { - Self { alter_column: true } + Self { + alter_column: Default::default(), + } } }