Merge 'Simulator: ALTER TABLE' from Pedro Muniz

Adds `ALTER TABLE` to the simulator. Currently, there are no properties
that generate `ALTER TABLE`. The query is only generated in
`Property::Query` or in extension queries.
Conditions to generate `ALTER TABLE`:
- In differential testing, do not generate `ALTER COLUMN` as SQLite does
not support it.
- If there is only 1 column, or all columns are present in indexes, do
not generate a `DROP COLUMN` as it would be an error in the database
- if there are no tables, obviously do not generate `ALTER TABLE`
Some fixes:
- handle NULL generation in `GTValue` and `LTValue`, as we now have to
handle nulls due to `ADD COLUMN` adding cols with NULL
- correctly compare NULLs in `binary_compare`

Closes #3650
This commit is contained in:
Jussi Saurio
2025-10-13 14:16:49 +03:00
committed by GitHub
22 changed files with 630 additions and 212 deletions

1
Cargo.lock generated
View File

@@ -3854,6 +3854,7 @@ dependencies = [
"rand_chacha 0.9.0",
"schemars 1.0.4",
"serde",
"strum",
"tracing",
"turso_core",
"turso_parser",

View File

@@ -58,11 +58,19 @@ impl InteractionPlan {
pub fn new_with(plan: Vec<Interactions>, 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<F>(&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<F>(&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
@@ -165,18 +220,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 +234,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 => {}
}
}
@@ -591,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 {
@@ -699,25 +755,26 @@ 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 {
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,
@@ -728,6 +785,7 @@ impl Display for InteractionStats {
self.begin_count,
self.commit_count,
self.rollback_count,
self.alter_table_count,
)
}
}

View File

@@ -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),
}
}
}
@@ -1352,29 +1371,24 @@ 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,
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;
@@ -1383,6 +1397,7 @@ pub(crate) 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)
@@ -1404,6 +1419,10 @@ pub(crate) 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;
@@ -1417,6 +1436,7 @@ pub(crate) fn remaining(
delete: remaining_delete,
drop: remaining_drop,
update: remaining_update,
alter_table: remaining_alter_table,
}
}
@@ -1727,7 +1747,7 @@ fn property_faulty_query<R: rand::Rng + ?Sized>(
type PropertyGenFunc<R, G> = fn(&mut R, &QueryDistribution, &G, bool) -> Property;
impl PropertyDiscriminants {
pub(super) fn gen_function<R, G>(&self) -> PropertyGenFunc<R, G>
fn gen_function<R, G>(&self) -> PropertyGenFunc<R, G>
where
R: rand::Rng + ?Sized,
G: GenerationContext,
@@ -1756,7 +1776,7 @@ impl PropertyDiscriminants {
}
}
pub fn weight(
fn weight(
&self,
env: &SimulatorEnv,
remaining: &Remaining,

View File

@@ -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,
},
};
@@ -71,7 +73,7 @@ fn random_create_index<R: rand::Rng + ?Sized>(
.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);
}
@@ -79,6 +81,14 @@ fn random_create_index<R: rand::Rng + ?Sized>(
Query::CreateIndex(create_index)
}
fn random_alter_table<R: rand::Rng + ?Sized>(
rng: &mut R,
conn_ctx: &impl GenerationContext,
) -> Query {
assert!(!conn_ctx.tables().is_empty());
Query::AlterTable(AlterTable::arbitrary(rng, conn_ctx))
}
/// Possible queries that can be generated given the table state
///
/// Does not take into account transactional statements
@@ -93,7 +103,7 @@ pub const fn possible_queries(tables: &[Table]) -> &'static [QueryDiscriminants]
type QueryGenFunc<R, G> = fn(&mut R, &G) -> Query;
impl QueryDiscriminants {
pub fn gen_function<R, G>(&self) -> QueryGenFunc<R, G>
fn gen_function<R, G>(&self) -> QueryGenFunc<R, G>
where
R: rand::Rng + ?Sized,
G: GenerationContext,
@@ -106,6 +116,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 +128,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 +139,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 => {

View File

@@ -8,11 +8,12 @@ use serde::{Deserialize, Serialize};
use sql_generation::model::{
query::{
Create, CreateIndex, Delete, Drop, Insert, Select,
alter_table::{AlterTable, AlterTableType},
select::{CompoundOperator, FromClause, ResultColumn, SelectInner},
transaction::{Begin, Commit, Rollback},
update::Update,
},
table::{JoinTable, JoinType, SimValue, Table, TableContext},
table::{Index, JoinTable, JoinType, SimValue, Table, TableContext},
};
use turso_parser::ast::Distinctness;
@@ -20,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),
@@ -29,6 +29,7 @@ pub enum Query {
Update(Update),
Drop(Drop),
CreateIndex(CreateIndex),
AlterTable(AlterTable),
Begin(Begin),
Commit(Commit),
Rollback(Rollback),
@@ -67,10 +68,15 @@ 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 {
index: Index {
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 +89,15 @@ 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 {
index: Index {
table_name: table, ..
},
})
| Query::AlterTable(AlterTable {
table_name: table, ..
}) => vec![table.clone()],
Query::Begin(..) | Query::Commit(..) | Query::Rollback(..) => vec![],
Query::Placeholder => vec![],
}
@@ -102,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(..)
)
}
}
@@ -117,6 +130,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 +151,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 +169,7 @@ bitflags! {
const UPDATE = 1 << 4;
const DROP = 1 << 5;
const CREATE_INDEX = 1 << 6;
const ALTER_TABLE = 1 << 7;
}
}
@@ -182,6 +198,7 @@ impl From<QueryDiscriminants> 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 => {
@@ -203,6 +220,7 @@ impl QueryDiscriminants {
QueryDiscriminants::Delete,
QueryDiscriminants::Drop,
QueryDiscriminants::CreateIndex,
QueryDiscriminants::AlterTable,
];
}
@@ -229,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![]
}
}
@@ -522,3 +540,46 @@ impl Shadow for Update {
Ok(vec![])
}
}
impl Shadow for AlterTable {
type Result = anyhow::Result<Vec<Vec<SimValue>>>;
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 } => {
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![])
}
}

View File

@@ -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,

View File

@@ -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<String> = 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<String> =
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<InteractionPlan> =
shrunk_plan.and_then(|shrunk_plan| serde_json::from_str(&shrunk_plan).ok());

View File

@@ -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();

View File

@@ -283,16 +283,13 @@ fn limbo_integrity_check(conn: &Arc<Connection>) -> 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<ResultSet>,
) -> turso_core::Result<ExecutionContinuation> {
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!()
@@ -344,14 +341,25 @@ fn execute_query_rusqlite(
connection: &rusqlite::Connection,
query: &Query,
) -> rusqlite::Result<Vec<Vec<SimValue>>> {
// 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())?;
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),

View File

@@ -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 } => {

View File

@@ -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 }

View File

@@ -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,22 @@ impl Default for InsertOpts {
}
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, Validate)]
#[serde(deny_unknown_fields)]
pub struct AlterTableOpts {
#[garde(skip)]
pub alter_column: bool,
}
#[expect(clippy::derivable_impls)]
impl Default for AlterTableOpts {
fn default() -> Self {
Self {
alter_column: Default::default(),
}
}
}
fn range_struct_min<T: PartialOrd + Display>(
min: T,
) -> impl FnOnce(&Range<T>, &()) -> garde::Result {
@@ -217,7 +235,7 @@ fn range_struct_min<T: PartialOrd + Display>(
}
}
#[allow(dead_code)]
#[expect(dead_code)]
fn range_struct_max<T: PartialOrd + Display>(
max: T,
) -> impl FnOnce(&Range<T>, &()) -> garde::Result {

View File

@@ -16,44 +16,6 @@ use crate::{
};
impl Predicate {
/// Generate an [ast::Expr::Binary] [Predicate] from a column and [SimValue]
pub fn from_column_binary<R: rand::Rng + ?Sized, C: GenerationContext>(
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<R: rand::Rng + ?Sized, C: GenerationContext>(
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),

View File

@@ -76,16 +76,6 @@ impl<T: TableContext> ArbitraryFrom<(&T, bool)> for Predicate {
}
}
impl ArbitraryFrom<(&str, &SimValue)> for Predicate {
fn arbitrary_from<R: Rng + ?Sized, C: GenerationContext>(
rng: &mut R,
context: &C,
(column_name, value): (&str, &SimValue),
) -> Self {
Predicate::from_column_binary(rng, context, column_name, value)
}
}
impl ArbitraryFrom<(&Table, &Vec<SimValue>)> for Predicate {
fn arbitrary_from<R: Rng + ?Sized, C: GenerationContext>(
rng: &mut R,

View File

@@ -1,7 +1,8 @@
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;
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, Index, 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};
@@ -358,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,
},
}
}
}
@@ -385,3 +391,147 @@ impl Arbitrary for Update {
}
}
}
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,
];
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,
];
// 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::<IndexSet<_>>();
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::<IndexSet<_, std::hash::RandomState>>();
diff
}
impl ArbitraryFrom<(&Table, &[AlterTableTypeDiscriminants])> for AlterTableType {
fn arbitrary_from<R: Rng + ?Sized, C: GenerationContext>(
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,
},
AlterTableTypeDiscriminants::AddColumn => AlterTableType::AddColumn {
column: Column::arbitrary(rng, context),
},
AlterTableTypeDiscriminants::AlterColumn => {
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(),
new: Name::arbitrary(rng, context).0,
},
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,
if context.opts().query.alter_table.alter_column {
ALTER_TABLE_NO_DROP
} 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::DropColumn {
column_name: col_name.to_string(),
}
}
}
}
}
impl Arbitrary for AlterTable {
fn arbitrary<R: Rng + ?Sized, C: GenerationContext>(rng: &mut R, context: &C) -> Self {
let table = pick(context.tables(), rng);
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) | (false, false) => ALTER_TABLE_NO_ALTER_COL_NO_DROP,
};
let alter_table_type = AlterTableType::arbitrary_from(rng, context, (table, choices));
Self {
table_name: table.name.clone(),
alter_table_type,
}
}
}

View File

@@ -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<R: rand::Rng + ?Sized, C: GenerationContext>(
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<R: rand::Rng + ?Sized, C: GenerationContext>(
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<R: rand::Rng + ?Sized, C: GenerationContext>(
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<R: rand::Rng + ?Sized, C: GenerationContext>(
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))
}

View File

@@ -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<ColumnType> for SimValue {
fn arbitrary_from<R: Rng + ?Sized, C: GenerationContext>(
rng: &mut R,
context: &C,
column_type: ColumnType,
) -> Self {
SimValue::arbitrary_from(rng, context, &column_type)
}
}

View File

@@ -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}"),
}
}
}

View File

@@ -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::<Vec<String>>()

View File

@@ -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;

View File

@@ -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<Column>,
pub rows: Vec<Vec<SimValue>>,
pub indexes: Vec<String>,
pub indexes: Vec<Index>,
}
impl Table {
@@ -98,7 +98,7 @@ impl Display for Column {
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
pub enum ColumnType {
Integer,
Float,
@@ -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
@@ -179,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(),
@@ -201,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(),
@@ -216,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(),
@@ -365,7 +387,18 @@ impl From<&SimValue> for ast::Literal {
}
}
impl From<Option<bool>> for SimValue {
#[inline]
fn from(value: Option<bool>) -> Self {
if value.is_none() {
return SimValue::NULL;
}
SimValue::from(value.unwrap())
}
}
impl From<bool> for SimValue {
#[inline]
fn from(value: bool) -> Self {
if value {
SimValue::TRUE

View File

@@ -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<CreateI
if !selected_columns.is_empty() {
let index_name = format!("idx_{}_{}", table.name, i);
let create_index = CreateIndex {
index_name,
table_name: table.name.clone(),
columns: selected_columns,
index: Index {
index_name,
table_name: table.name.clone(),
columns: selected_columns,
},
};
indexes.push(create_index);
}