Merge 'Cleanup Simulator + Fix Column constraints in sql generation' from Pedro Muniz

- Removed a general clippy rule to allow all dead code and subsequently
removed a lot of dead code
- Fixed Column constraints in Sql Generation to accommodate all Column
constraints available to the Parser and print the constraints in other
sql queries.
- Moved Generation of simulator values to separate files
These are some of the changes I made in my Alter Table PR that I am
upstreaming here

Closes #3649
This commit is contained in:
Jussi Saurio
2025-10-09 23:37:22 +03:00
committed by GitHub
21 changed files with 324 additions and 392 deletions

View File

@@ -13,6 +13,7 @@ name = "turso_parser"
[features]
default = []
serde = ["dep:serde", "bitflags/serde"]
simulator = []
[dependencies]
bitflags = { workspace = true }

View File

@@ -1121,6 +1121,11 @@ pub struct NamedColumnConstraint {
// https://sqlite.org/syntax/column-constraint.html
#[derive(Clone, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "simulator", derive(strum::EnumDiscriminants))]
#[cfg_attr(
feature = "simulator",
strum_discriminants(derive(strum::VariantArray))
)]
pub enum ColumnConstraint {
/// `PRIMARY KEY`
PrimaryKey {

View File

@@ -63,11 +63,6 @@ impl InteractionPlan {
Self { plan, mvcc, len }
}
#[inline]
pub fn plan(&self) -> &[Interactions] {
&self.plan
}
/// Length of interactions that are not transaction statements
#[inline]
pub fn len(&self) -> usize {
@@ -629,14 +624,6 @@ impl InteractionsType {
}
impl Interactions {
pub(crate) fn name(&self) -> Option<&str> {
match &self.interactions {
InteractionsType::Property(property) => Some(property.name()),
InteractionsType::Query(_) => None,
InteractionsType::Fault(_) => None,
}
}
pub(crate) fn interactions(&self) -> Vec<Interaction> {
match &self.interactions {
InteractionsType::Property(property) => property.interactions(self.connection_index),
@@ -726,17 +713,6 @@ pub(crate) struct InteractionStats {
pub(crate) rollback_count: u32,
}
impl InteractionStats {
pub fn total_writes(&self) -> u32 {
self.insert_count
+ self.delete_count
+ self.update_count
+ self.create_count
+ self.create_index_count
+ self.drop_count
}
}
impl Display for InteractionStats {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
@@ -758,10 +734,6 @@ impl Display for InteractionStats {
type AssertionFunc = dyn Fn(&Vec<ResultSet>, &mut SimulatorEnv) -> Result<Result<(), String>>;
enum AssertionAST {
Pick(),
}
#[derive(Clone)]
pub struct Assertion {
pub func: Rc<AssertionFunc>,

View File

@@ -8,7 +8,7 @@
use rand::distr::{Distribution, weighted::WeightedIndex};
use serde::{Deserialize, Serialize};
use sql_generation::{
generation::{Arbitrary, ArbitraryFrom, GenerationContext, Opts, pick, pick_index},
generation::{Arbitrary, ArbitraryFrom, GenerationContext, pick, pick_index},
model::{
query::{
Create, Delete, Drop, Insert, Select,
@@ -17,7 +17,7 @@ use sql_generation::{
transaction::{Begin, Commit, Rollback},
update::Update,
},
table::{SimValue, Table},
table::SimValue,
},
};
use strum::IntoEnumIterator;
@@ -27,40 +27,15 @@ use turso_parser::ast::{self, Distinctness};
use crate::{
common::print_diff,
generation::{
Shadow as _, WeightedDistribution,
plan::InteractionType,
query::{QueryDistribution, possible_queries},
Shadow as _, WeightedDistribution, plan::InteractionType, query::QueryDistribution,
},
model::{Query, QueryCapabilities, QueryDiscriminants},
profiles::query::QueryProfile,
runner::env::{ShadowTablesMut, SimulatorEnv},
runner::env::SimulatorEnv,
};
use super::plan::{Assertion, Interaction, InteractionStats, ResultSet};
#[derive(Debug, Clone, Copy)]
struct PropertyGenContext<'a> {
tables: &'a Vec<sql_generation::model::table::Table>,
opts: &'a sql_generation::generation::Opts,
}
impl<'a> PropertyGenContext<'a> {
#[inline]
fn new(tables: &'a Vec<Table>, opts: &'a Opts) -> Self {
Self { tables, opts }
}
}
impl<'a> GenerationContext for PropertyGenContext<'a> {
fn tables(&self) -> &Vec<sql_generation::model::table::Table> {
self.tables
}
fn opts(&self) -> &sql_generation::generation::Opts {
self.opts
}
}
/// Properties are representations of executable specifications
/// about the database behavior.
#[derive(Debug, Clone, Serialize, Deserialize, strum::EnumDiscriminants)]
@@ -1925,11 +1900,6 @@ impl PropertyDiscriminants {
}
}
pub fn possiple_properties(tables: &[Table]) -> Vec<PropertyDiscriminants> {
let queries = possible_queries(tables);
PropertyDiscriminants::can_generate(queries)
}
pub(super) struct PropertyDistribution<'a> {
properties: Vec<PropertyDiscriminants>,
weights: WeightedIndex<u32>,
@@ -1995,49 +1965,6 @@ impl<'a> ArbitraryFrom<&PropertyDistribution<'a>> for Property {
}
}
fn generate_queries<R: rand::Rng + ?Sized, F>(
rng: &mut R,
ctx: &impl GenerationContext,
amount: usize,
init_queries: &[&Query],
func: F,
) -> Vec<Query>
where
F: Fn(&mut R, PropertyGenContext) -> Option<Query>,
{
// Create random queries respecting the constraints
let mut queries = Vec::new();
let range = 0..amount;
if !range.is_empty() {
let mut tmp_tables = ctx.tables().clone();
for query in init_queries {
tmp_shadow(&mut tmp_tables, query);
}
for _ in range {
let tmp_ctx = PropertyGenContext::new(&tmp_tables, ctx.opts());
let Some(query) = func(rng, tmp_ctx) else {
continue;
};
tmp_shadow(&mut tmp_tables, &query);
queries.push(query);
}
}
queries
}
fn tmp_shadow(tmp_tables: &mut Vec<Table>, query: &Query) {
let mut tx_tables = None;
let mut tmp_shadow_tables = ShadowTablesMut::new(tmp_tables, &mut tx_tables);
let _ = query.shadow(&mut tmp_shadow_tables);
}
fn print_row(row: &[SimValue]) -> String {
row.iter()
.map(|v| match &v.0 {

View File

@@ -1,4 +1,4 @@
#![allow(clippy::arc_with_non_send_sync, dead_code)]
#![allow(clippy::arc_with_non_send_sync)]
use anyhow::anyhow;
use clap::Parser;
use generation::plan::{InteractionPlan, InteractionPlanState};
@@ -421,6 +421,7 @@ enum SandboxedResult {
error: String,
last_execution: Execution,
},
#[expect(dead_code)]
FoundBug {
error: String,
history: ExecutionHistory,

View File

@@ -204,16 +204,6 @@ impl QueryDiscriminants {
QueryDiscriminants::Drop,
QueryDiscriminants::CreateIndex,
];
#[inline]
pub fn is_transaction(&self) -> bool {
matches!(self, Self::Begin | Self::Commit | Self::Rollback)
}
#[inline]
pub fn is_ddl(&self) -> bool {
matches!(self, Self::Create | Self::CreateIndex | Self::Drop)
}
}
impl Shadow for Create {

View File

@@ -49,6 +49,7 @@ pub(crate) struct BugRun {
}
impl Bug {
#[expect(dead_code)]
/// Check if the bug is loaded.
pub(crate) fn is_loaded(&self) -> bool {
match self {
@@ -130,6 +131,7 @@ impl BugBase {
Err(anyhow!("failed to create bug base"))
}
#[expect(dead_code)]
/// Load the bug base from one of the potential paths.
pub(crate) fn interactive_load() -> anyhow::Result<Self> {
let potential_paths = vec![
@@ -338,6 +340,7 @@ impl BugBase {
}
}
#[expect(dead_code)]
pub(crate) fn mark_successful_run(
&mut self,
seed: u64,
@@ -434,6 +437,7 @@ impl BugBase {
}
impl BugBase {
#[expect(dead_code)]
/// Get the path to the bug base directory.
pub(crate) fn path(&self) -> &PathBuf {
&self.path

View File

@@ -83,18 +83,6 @@ impl<'a, 'b> ShadowTablesMut<'a>
where
'a: 'b,
{
/// Creation of [ShadowTablesMut] outside of [SimulatorEnv] should be done sparingly and carefully.
/// Should only need to call this function if we need to do shadowing in a temporary model table
pub fn new(
commited_tables: &'a mut Vec<Table>,
transaction_tables: &'a mut Option<TransactionTables>,
) -> Self {
ShadowTablesMut {
commited_tables,
transaction_tables,
}
}
fn tables(&'a self) -> &'a Vec<Table> {
self.transaction_tables
.as_ref()
@@ -312,7 +300,6 @@ impl SimulatorEnv {
seed,
ticks: rng
.random_range(cli_opts.minimum_tests as usize..=cli_opts.maximum_tests as usize),
max_tables: rng.random_range(0..128),
disable_select_optimizer: cli_opts.disable_select_optimizer,
disable_insert_values_select: cli_opts.disable_insert_values_select,
disable_double_create_failure: cli_opts.disable_double_create_failure,
@@ -528,14 +515,6 @@ impl SimulatorEnv {
}
}
pub trait ConnectionTrait
where
Self: std::marker::Sized + Clone,
{
fn is_connected(&self) -> bool;
fn disconnect(&mut self);
}
pub(crate) enum SimConnection {
LimboConnection(Arc<turso_core::Connection>),
SQLiteConnection(rusqlite::Connection),
@@ -584,7 +563,6 @@ impl Display for SimConnection {
pub(crate) struct SimulatorOpts {
pub(crate) seed: u64,
pub(crate) ticks: usize,
pub(crate) max_tables: usize,
pub(crate) disable_select_optimizer: bool,
pub(crate) disable_insert_values_select: bool,

View File

@@ -46,6 +46,7 @@ impl ExecutionHistory {
}
pub struct ExecutionResult {
#[expect(dead_code)]
pub history: ExecutionHistory,
pub error: Option<LimboError>,
}

View File

@@ -1,4 +1,4 @@
use std::cell::{Cell, RefCell};
use std::cell::RefCell;
use std::sync::Arc;
use indexmap::IndexMap;
@@ -121,7 +121,7 @@ pub struct MemorySimIO {
timeouts: CallbackQueue,
pub files: RefCell<IndexMap<Fd, Arc<MemorySimFile>>>,
pub rng: RefCell<ChaCha8Rng>,
pub nr_run_once_faults: Cell<usize>,
#[expect(dead_code)]
pub page_size: usize,
seed: u64,
latency_probability: u8,
@@ -141,13 +141,11 @@ impl MemorySimIO {
) -> Self {
let files = RefCell::new(IndexMap::new());
let rng = RefCell::new(ChaCha8Rng::seed_from_u64(seed));
let nr_run_once_faults = Cell::new(0);
Self {
callbacks: Arc::new(Mutex::new(Vec::new())),
timeouts: Arc::new(Mutex::new(Vec::new())),
files,
rng,
nr_run_once_faults,
page_size,
seed,
latency_probability,

View File

@@ -5,7 +5,7 @@ pub mod differential;
pub mod doublecheck;
pub mod env;
pub mod execution;
#[allow(dead_code)]
#[expect(dead_code)]
pub mod file;
pub mod io;
pub mod memory;

View File

@@ -13,7 +13,7 @@ path = "lib.rs"
hex = { workspace = true }
serde = { workspace = true, features = ["derive"] }
turso_core = { workspace = true, features = ["simulator"] }
turso_parser = { workspace = true, features = ["serde"] }
turso_parser = { workspace = true, features = ["serde", "simulator"] }
rand = { workspace = true }
anarchist-readable-name-generator-lib = "0.2.0"
itertools = { workspace = true }

View File

@@ -8,6 +8,7 @@ pub mod opts;
pub mod predicate;
pub mod query;
pub mod table;
pub mod value;
pub use opts::*;

View File

@@ -6,7 +6,7 @@ use crate::{
generation::{
backtrack, one_of, pick,
predicate::{CompoundPredicate, SimplePredicate},
table::{GTValue, LTValue, LikeValue},
value::{GTValue, LTValue, LikeValue},
ArbitraryFrom, ArbitraryFromMaybe as _, GenerationContext,
},
model::{

View File

@@ -2,14 +2,9 @@ use std::sync::atomic::{AtomicU64, Ordering};
use indexmap::IndexSet;
use rand::Rng;
use turso_core::Value;
use crate::generation::{
gen_random_text, pick, readable_name_custom, Arbitrary, ArbitraryFrom, GenerationContext,
};
use crate::model::table::{Column, ColumnType, Name, SimValue, Table};
use super::ArbitraryFromMaybe;
use crate::generation::{pick, readable_name_custom, Arbitrary, GenerationContext};
use crate::model::table::{Column, ColumnType, Name, Table};
static COUNTER: AtomicU64 = AtomicU64::new(0);
@@ -56,8 +51,7 @@ impl Arbitrary for Column {
Self {
name,
column_type,
primary: false,
unique: false,
constraints: vec![], // TODO: later implement arbitrary here for ColumnConstraint
}
}
}
@@ -67,226 +61,3 @@ impl Arbitrary for ColumnType {
pick(&[Self::Integer, Self::Float, Self::Text, Self::Blob], rng).to_owned()
}
}
impl ArbitraryFrom<&Table> for Vec<SimValue> {
fn arbitrary_from<R: Rng + ?Sized, C: GenerationContext>(
rng: &mut R,
context: &C,
table: &Table,
) -> Self {
let mut row = Vec::new();
for column in table.columns.iter() {
let value = SimValue::arbitrary_from(rng, context, &column.column_type);
row.push(value);
}
row
}
}
impl ArbitraryFrom<&Vec<&SimValue>> for SimValue {
fn arbitrary_from<R: Rng + ?Sized, C: GenerationContext>(
rng: &mut R,
_context: &C,
values: &Vec<&Self>,
) -> Self {
if values.is_empty() {
return Self(Value::Null);
}
pick(values, rng).to_owned().clone()
}
}
impl ArbitraryFrom<&ColumnType> for SimValue {
fn arbitrary_from<R: Rng + ?Sized, C: GenerationContext>(
rng: &mut R,
_context: &C,
column_type: &ColumnType,
) -> Self {
let value = match column_type {
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()),
};
SimValue(value)
}
}
pub struct LTValue(pub SimValue);
impl ArbitraryFrom<&Vec<&SimValue>> for LTValue {
fn arbitrary_from<R: 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 {
fn arbitrary_from<R: Rng + ?Sized, C: GenerationContext>(
rng: &mut R,
_context: &C,
value: &SimValue,
) -> Self {
let new_value = match &value.0 {
Value::Integer(i) => Value::Integer(rng.random_range(i64::MIN..*i - 1)),
Value::Float(f) => Value::Float(f - rng.random_range(0.0..1e10)),
value @ Value::Text(..) => {
// Either shorten the string, or make at least one character smaller and mutate the rest
let mut t = value.to_string();
if rng.random_bool(0.01) {
t.pop();
Value::build_text(t)
} else {
let mut t = t.chars().map(|c| c as u32).collect::<Vec<_>>();
let index = rng.random_range(0..t.len());
t[index] -= 1;
// Mutate the rest of the string
for val in t.iter_mut().skip(index + 1) {
*val = rng.random_range('a' as u32..='z' as u32);
}
let t = t
.into_iter()
.map(|c| char::from_u32(c).unwrap_or('z'))
.collect::<String>();
Value::build_text(t)
}
}
Value::Blob(b) => {
// Either shorten the blob, or make at least one byte smaller and mutate the rest
let mut b = b.clone();
if rng.random_bool(0.01) {
b.pop();
Value::Blob(b)
} else {
let index = rng.random_range(0..b.len());
b[index] -= 1;
// Mutate the rest of the blob
for val in b.iter_mut().skip(index + 1) {
*val = rng.random_range(0..=255);
}
Value::Blob(b)
}
}
_ => unreachable!(),
};
Self(SimValue(new_value))
}
}
pub struct GTValue(pub SimValue);
impl ArbitraryFrom<&Vec<&SimValue>> for GTValue {
fn arbitrary_from<R: 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: Rng + ?Sized, C: GenerationContext>(
rng: &mut R,
_context: &C,
value: &SimValue,
) -> Self {
let new_value = match &value.0 {
Value::Integer(i) => Value::Integer(rng.random_range(*i..i64::MAX)),
Value::Float(f) => Value::Float(rng.random_range(*f..1e10)),
value @ Value::Text(..) => {
// Either lengthen the string, or make at least one character smaller and mutate the rest
let mut t = value.to_string();
if rng.random_bool(0.01) {
t.push(rng.random_range(0..=255) as u8 as char);
Value::build_text(t)
} else {
let mut t = t.chars().map(|c| c as u32).collect::<Vec<_>>();
let index = rng.random_range(0..t.len());
t[index] += 1;
// Mutate the rest of the string
for val in t.iter_mut().skip(index + 1) {
*val = rng.random_range('a' as u32..='z' as u32);
}
let t = t
.into_iter()
.map(|c| char::from_u32(c).unwrap_or('a'))
.collect::<String>();
Value::build_text(t)
}
}
Value::Blob(b) => {
// Either lengthen the blob, or make at least one byte smaller and mutate the rest
let mut b = b.clone();
if rng.random_bool(0.01) {
b.push(rng.random_range(0..=255));
Value::Blob(b)
} else {
let index = rng.random_range(0..b.len());
b[index] += 1;
// Mutate the rest of the blob
for val in b.iter_mut().skip(index + 1) {
*val = rng.random_range(0..=255);
}
Value::Blob(b)
}
}
_ => unreachable!(),
};
Self(SimValue(new_value))
}
}
pub struct LikeValue(pub SimValue);
impl ArbitraryFromMaybe<&SimValue> for LikeValue {
fn arbitrary_from_maybe<R: Rng + ?Sized, C: GenerationContext>(
rng: &mut R,
_context: &C,
value: &SimValue,
) -> Option<Self> {
match &value.0 {
value @ Value::Text(..) => {
let t = value.to_string();
let mut t = t.chars().collect::<Vec<_>>();
// Remove a number of characters, either insert `_` for each character removed, or
// insert one `%` for the whole substring
let mut i = 0;
while i < t.len() {
if rng.random_bool(0.1) {
t[i] = '_';
} else if rng.random_bool(0.05) {
t[i] = '%';
// skip a list of characters
for _ in 0..rng.random_range(0..=3.min(t.len() - i - 1)) {
t.remove(i + 1);
}
}
i += 1;
}
let index = rng.random_range(0..t.len());
t.insert(index, '%');
Some(Self(SimValue(Value::build_text(
t.into_iter().collect::<String>(),
))))
}
_ => None,
}
}
}

View File

@@ -0,0 +1,146 @@
use turso_core::Value;
use crate::{
generation::{ArbitraryFrom, GenerationContext},
model::table::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 {
fn arbitrary_from<R: rand::Rng + ?Sized, C: GenerationContext>(
rng: &mut R,
_context: &C,
value: &SimValue,
) -> Self {
let new_value = match &value.0 {
Value::Integer(i) => Value::Integer(rng.random_range(i64::MIN..*i - 1)),
Value::Float(f) => Value::Float(f - rng.random_range(0.0..1e10)),
value @ Value::Text(..) => {
// Either shorten the string, or make at least one character smaller and mutate the rest
let mut t = value.to_string();
if rng.random_bool(0.01) {
t.pop();
Value::build_text(t)
} else {
let mut t = t.chars().map(|c| c as u32).collect::<Vec<_>>();
let index = rng.random_range(0..t.len());
t[index] -= 1;
// Mutate the rest of the string
for val in t.iter_mut().skip(index + 1) {
*val = rng.random_range('a' as u32..='z' as u32);
}
let t = t
.into_iter()
.map(|c| char::from_u32(c).unwrap_or('z'))
.collect::<String>();
Value::build_text(t)
}
}
Value::Blob(b) => {
// Either shorten the blob, or make at least one byte smaller and mutate the rest
let mut b = b.clone();
if rng.random_bool(0.01) {
b.pop();
Value::Blob(b)
} else {
let index = rng.random_range(0..b.len());
b[index] -= 1;
// Mutate the rest of the blob
for val in b.iter_mut().skip(index + 1) {
*val = rng.random_range(0..=255);
}
Value::Blob(b)
}
}
_ => unreachable!(),
};
Self(SimValue(new_value))
}
}
pub struct GTValue(pub SimValue);
impl ArbitraryFrom<&Vec<&SimValue>> 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,
) -> Self {
let new_value = match &value.0 {
Value::Integer(i) => Value::Integer(rng.random_range(*i..i64::MAX)),
Value::Float(f) => Value::Float(rng.random_range(*f..1e10)),
value @ Value::Text(..) => {
// Either lengthen the string, or make at least one character smaller and mutate the rest
let mut t = value.to_string();
if rng.random_bool(0.01) {
t.push(rng.random_range(0..=255) as u8 as char);
Value::build_text(t)
} else {
let mut t = t.chars().map(|c| c as u32).collect::<Vec<_>>();
let index = rng.random_range(0..t.len());
t[index] += 1;
// Mutate the rest of the string
for val in t.iter_mut().skip(index + 1) {
*val = rng.random_range('a' as u32..='z' as u32);
}
let t = t
.into_iter()
.map(|c| char::from_u32(c).unwrap_or('a'))
.collect::<String>();
Value::build_text(t)
}
}
Value::Blob(b) => {
// Either lengthen the blob, or make at least one byte smaller and mutate the rest
let mut b = b.clone();
if rng.random_bool(0.01) {
b.push(rng.random_range(0..=255));
Value::Blob(b)
} else {
let index = rng.random_range(0..b.len());
b[index] += 1;
// Mutate the rest of the blob
for val in b.iter_mut().skip(index + 1) {
*val = rng.random_range(0..=255);
}
Value::Blob(b)
}
}
_ => unreachable!(),
};
Self(SimValue(new_value))
}
}

View File

@@ -0,0 +1,58 @@
use rand::Rng;
use turso_core::Value;
use crate::{
generation::{gen_random_text, pick, ArbitraryFrom, GenerationContext},
model::table::{ColumnType, SimValue, Table},
};
mod cmp;
mod pattern;
pub use cmp::{GTValue, LTValue};
pub use pattern::LikeValue;
impl ArbitraryFrom<&Table> for Vec<SimValue> {
fn arbitrary_from<R: Rng + ?Sized, C: GenerationContext>(
rng: &mut R,
context: &C,
table: &Table,
) -> Self {
let mut row = Vec::new();
for column in table.columns.iter() {
let value = SimValue::arbitrary_from(rng, context, &column.column_type);
row.push(value);
}
row
}
}
impl ArbitraryFrom<&Vec<&SimValue>> for SimValue {
fn arbitrary_from<R: Rng + ?Sized, C: GenerationContext>(
rng: &mut R,
_context: &C,
values: &Vec<&Self>,
) -> Self {
if values.is_empty() {
return Self(Value::Null);
}
pick(values, rng).to_owned().clone()
}
}
impl ArbitraryFrom<&ColumnType> for SimValue {
fn arbitrary_from<R: Rng + ?Sized, C: GenerationContext>(
rng: &mut R,
_context: &C,
column_type: &ColumnType,
) -> Self {
let value = match column_type {
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()),
};
SimValue(value)
}
}

View File

@@ -0,0 +1,44 @@
use turso_core::Value;
use crate::{
generation::{ArbitraryFromMaybe, GenerationContext},
model::table::SimValue,
};
pub struct LikeValue(pub SimValue);
impl ArbitraryFromMaybe<&SimValue> for LikeValue {
fn arbitrary_from_maybe<R: rand::Rng + ?Sized, C: GenerationContext>(
rng: &mut R,
_context: &C,
value: &SimValue,
) -> Option<Self> {
match &value.0 {
value @ Value::Text(..) => {
let t = value.to_string();
let mut t = t.chars().collect::<Vec<_>>();
// Remove a number of characters, either insert `_` for each character removed, or
// insert one `%` for the whole substring
let mut i = 0;
while i < t.len() {
if rng.random_bool(0.1) {
t[i] = '_';
} else if rng.random_bool(0.05) {
t[i] = '%';
// skip a list of characters
for _ in 0..rng.random_range(0..=3.min(t.len() - i - 1)) {
t.remove(i + 1);
}
}
i += 1;
}
let index = rng.random_range(0..t.len());
t.insert(index, '%');
Some(Self(SimValue(Value::build_text(
t.into_iter().collect::<String>(),
))))
}
_ => None,
}
}
}

View File

@@ -1,5 +1,6 @@
use std::fmt::Display;
use itertools::Itertools;
use serde::{Deserialize, Serialize};
use crate::model::table::Table;
@@ -13,13 +14,13 @@ impl Display for Create {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "CREATE TABLE {} (", self.table.name)?;
for (i, column) in self.table.columns.iter().enumerate() {
if i != 0 {
write!(f, ",")?;
}
write!(f, "{} {}", column.name, column.column_type)?;
}
let cols = self
.table
.columns
.iter()
.map(|column| column.to_string())
.join(", ");
write!(f, ")")
write!(f, "{cols})")
}
}

View File

@@ -1,8 +1,9 @@
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;
use turso_parser::ast::{self, ColumnConstraint};
use crate::model::query::predicate::Predicate;
@@ -63,8 +64,7 @@ impl Table {
pub struct Column {
pub name: String,
pub column_type: ColumnType,
pub primary: bool,
pub unique: bool,
pub constraints: Vec<ColumnConstraint>,
}
// Uniquely defined by name in this case
@@ -82,6 +82,22 @@ impl PartialEq for Column {
impl Eq for Column {}
impl Display for Column {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let constraints = self
.constraints
.iter()
.map(|constraint| constraint.to_string())
.join(" ");
let mut col_string = format!("{} {}", self.name, self.column_type);
if !constraints.is_empty() {
col_string.push(' ');
col_string.push_str(&constraints);
}
write!(f, "{col_string}")
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum ColumnType {
Integer,

View File

@@ -18,7 +18,7 @@ use tracing_subscriber::{EnvFilter, layer::SubscriberExt, util::SubscriberInitEx
use turso_core::{
CipherMode, Connection, Database, DatabaseOpts, EncryptionOpts, IO, OpenFlags, Statement,
};
use turso_parser::ast::SortOrder;
use turso_parser::ast::{ColumnConstraint, SortOrder};
mod io;
use crate::io::FILE_SIZE_SOFT_LIMIT;
@@ -332,12 +332,18 @@ fn create_initial_schema(rng: &mut ChaCha8Rng) -> Vec<Create> {
let num_columns = rng.random_range(2..=8);
let mut columns = Vec::new();
// TODO: there is no proper unique generation yet in whopper, so disable primary keys for now
// let primary = ColumnConstraint::PrimaryKey {
// order: None,
// conflict_clause: None,
// auto_increment: false,
// };
// Always add an id column as primary key
columns.push(Column {
name: "id".to_string(),
column_type: ColumnType::Integer,
primary: true,
unique: false,
constraints: vec![],
});
// Add random columns
@@ -348,11 +354,19 @@ fn create_initial_schema(rng: &mut ChaCha8Rng) -> Vec<Create> {
_ => ColumnType::Float,
};
// FIXME: before sql_generation did not incorporate ColumnConstraint into the sql string
// now it does and it the simulation here fails `whopper` with UNIQUE CONSTRAINT ERROR
// 20% chance of unique
let constraints = if rng.random_bool(0.0) {
vec![ColumnConstraint::Unique(None)]
} else {
Vec::new()
};
columns.push(Column {
name: format!("col_{j}"),
column_type: col_type,
primary: false,
unique: rng.random_bool(0.2), // 20% chance of unique
constraints,
});
}
@@ -366,6 +380,10 @@ fn create_initial_schema(rng: &mut ChaCha8Rng) -> Vec<Create> {
schema.push(Create { table });
}
for create in &schema {
println!("{create}");
}
schema
}