mirror of
https://github.com/aljazceru/turso.git
synced 2026-02-23 08:55:40 +01:00
copy generation code from simulator
This commit is contained in:
4
sql_generation/model/mod.rs
Normal file
4
sql_generation/model/mod.rs
Normal file
@@ -0,0 +1,4 @@
|
||||
pub mod query;
|
||||
pub mod table;
|
||||
|
||||
pub(crate) const FAULT_ERROR_MSG: &str = "Injected fault";
|
||||
45
sql_generation/model/query/create.rs
Normal file
45
sql_generation/model/query/create.rs
Normal file
@@ -0,0 +1,45 @@
|
||||
use std::fmt::Display;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{
|
||||
generation::Shadow,
|
||||
model::table::{SimValue, Table},
|
||||
runner::env::SimulatorTables,
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub(crate) struct Create {
|
||||
pub(crate) table: Table,
|
||||
}
|
||||
|
||||
impl Shadow for Create {
|
||||
type Result = anyhow::Result<Vec<Vec<SimValue>>>;
|
||||
|
||||
fn shadow(&self, tables: &mut SimulatorTables) -> Self::Result {
|
||||
if !tables.iter().any(|t| t.name == self.table.name) {
|
||||
tables.push(self.table.clone());
|
||||
Ok(vec![])
|
||||
} else {
|
||||
Err(anyhow::anyhow!(
|
||||
"Table {} already exists. CREATE TABLE statement ignored.",
|
||||
self.table.name
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for Create {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "CREATE TABLE {} (", self.table.name)?;
|
||||
|
||||
for (i, column) in self.table.columns.iter().enumerate() {
|
||||
if i != 0 {
|
||||
write!(f, ",")?;
|
||||
}
|
||||
write!(f, "{} {}", column.name, column.column_type)?;
|
||||
}
|
||||
|
||||
write!(f, ")")
|
||||
}
|
||||
}
|
||||
106
sql_generation/model/query/create_index.rs
Normal file
106
sql_generation/model/query/create_index.rs
Normal file
@@ -0,0 +1,106 @@
|
||||
use crate::{
|
||||
generation::{gen_random_text, pick, pick_n_unique, ArbitraryFrom, Shadow},
|
||||
model::table::SimValue,
|
||||
runner::env::{SimulatorEnv, SimulatorTables},
|
||||
};
|
||||
use rand::Rng;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
|
||||
pub enum SortOrder {
|
||||
Asc,
|
||||
Desc,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for SortOrder {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
SortOrder::Asc => write!(f, "ASC"),
|
||||
SortOrder::Desc => write!(f, "DESC"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
|
||||
pub(crate) struct CreateIndex {
|
||||
pub(crate) index_name: String,
|
||||
pub(crate) table_name: String,
|
||||
pub(crate) columns: Vec<(String, SortOrder)>,
|
||||
}
|
||||
|
||||
impl Shadow for CreateIndex {
|
||||
type Result = Vec<Vec<SimValue>>;
|
||||
fn shadow(&self, env: &mut SimulatorTables) -> Vec<Vec<SimValue>> {
|
||||
env.tables
|
||||
.iter_mut()
|
||||
.find(|t| t.name == self.table_name)
|
||||
.unwrap()
|
||||
.indexes
|
||||
.push(self.index_name.clone());
|
||||
vec![]
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for CreateIndex {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"CREATE INDEX {} ON {} ({})",
|
||||
self.index_name,
|
||||
self.table_name,
|
||||
self.columns
|
||||
.iter()
|
||||
.map(|(name, order)| format!("{name} {order}"))
|
||||
.collect::<Vec<String>>()
|
||||
.join(", ")
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl ArbitraryFrom<&SimulatorEnv> for CreateIndex {
|
||||
fn arbitrary_from<R: Rng>(rng: &mut R, env: &SimulatorEnv) -> Self {
|
||||
assert!(
|
||||
!env.tables.is_empty(),
|
||||
"Cannot create an index when no tables exist in the environment."
|
||||
);
|
||||
|
||||
let table = pick(&env.tables, rng);
|
||||
|
||||
if table.columns.is_empty() {
|
||||
panic!(
|
||||
"Cannot create an index on table '{}' as it has no columns.",
|
||||
table.name
|
||||
);
|
||||
}
|
||||
|
||||
let num_columns_to_pick = rng.random_range(1..=table.columns.len());
|
||||
let picked_column_indices = pick_n_unique(0..table.columns.len(), num_columns_to_pick, rng);
|
||||
|
||||
let columns = picked_column_indices
|
||||
.into_iter()
|
||||
.map(|i| {
|
||||
let column = &table.columns[i];
|
||||
(
|
||||
column.name.clone(),
|
||||
if rng.random_bool(0.5) {
|
||||
SortOrder::Asc
|
||||
} else {
|
||||
SortOrder::Desc
|
||||
},
|
||||
)
|
||||
})
|
||||
.collect::<Vec<(String, SortOrder)>>();
|
||||
|
||||
let index_name = format!(
|
||||
"idx_{}_{}",
|
||||
table.name,
|
||||
gen_random_text(rng).chars().take(8).collect::<String>()
|
||||
);
|
||||
|
||||
CreateIndex {
|
||||
index_name,
|
||||
table_name: table.name.clone(),
|
||||
columns,
|
||||
}
|
||||
}
|
||||
}
|
||||
41
sql_generation/model/query/delete.rs
Normal file
41
sql_generation/model/query/delete.rs
Normal file
@@ -0,0 +1,41 @@
|
||||
use std::fmt::Display;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{generation::Shadow, model::table::SimValue, runner::env::SimulatorTables};
|
||||
|
||||
use super::predicate::Predicate;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
pub(crate) struct Delete {
|
||||
pub(crate) table: String,
|
||||
pub(crate) predicate: Predicate,
|
||||
}
|
||||
|
||||
impl Shadow for Delete {
|
||||
type Result = anyhow::Result<Vec<Vec<SimValue>>>;
|
||||
|
||||
fn shadow(&self, tables: &mut SimulatorTables) -> Self::Result {
|
||||
let table = tables.tables.iter_mut().find(|t| t.name == self.table);
|
||||
|
||||
if let Some(table) = table {
|
||||
// If the table exists, we can delete from it
|
||||
let t2 = table.clone();
|
||||
table.rows.retain_mut(|r| !self.predicate.test(r, &t2));
|
||||
} else {
|
||||
// If the table does not exist, we return an error
|
||||
return Err(anyhow::anyhow!(
|
||||
"Table {} does not exist. DELETE statement ignored.",
|
||||
self.table
|
||||
));
|
||||
}
|
||||
|
||||
Ok(vec![])
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for Delete {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "DELETE FROM {} WHERE {}", self.table, self.predicate)
|
||||
}
|
||||
}
|
||||
34
sql_generation/model/query/drop.rs
Normal file
34
sql_generation/model/query/drop.rs
Normal file
@@ -0,0 +1,34 @@
|
||||
use std::fmt::Display;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{generation::Shadow, model::table::SimValue, runner::env::SimulatorTables};
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
pub(crate) struct Drop {
|
||||
pub(crate) table: String,
|
||||
}
|
||||
|
||||
impl Shadow for Drop {
|
||||
type Result = anyhow::Result<Vec<Vec<SimValue>>>;
|
||||
|
||||
fn shadow(&self, tables: &mut SimulatorTables) -> Self::Result {
|
||||
if !tables.iter().any(|t| t.name == self.table) {
|
||||
// If the table does not exist, we return an error
|
||||
return Err(anyhow::anyhow!(
|
||||
"Table {} does not exist. DROP statement ignored.",
|
||||
self.table
|
||||
));
|
||||
}
|
||||
|
||||
tables.tables.retain(|t| t.name != self.table);
|
||||
|
||||
Ok(vec![])
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for Drop {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "DROP TABLE {}", self.table)
|
||||
}
|
||||
}
|
||||
87
sql_generation/model/query/insert.rs
Normal file
87
sql_generation/model/query/insert.rs
Normal file
@@ -0,0 +1,87 @@
|
||||
use std::fmt::Display;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{generation::Shadow, model::table::SimValue, runner::env::SimulatorTables};
|
||||
|
||||
use super::select::Select;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
pub(crate) enum Insert {
|
||||
Values {
|
||||
table: String,
|
||||
values: Vec<Vec<SimValue>>,
|
||||
},
|
||||
Select {
|
||||
table: String,
|
||||
select: Box<Select>,
|
||||
},
|
||||
}
|
||||
|
||||
impl Shadow for Insert {
|
||||
type Result = anyhow::Result<Vec<Vec<SimValue>>>;
|
||||
|
||||
fn shadow(&self, tables: &mut SimulatorTables) -> Self::Result {
|
||||
match self {
|
||||
Insert::Values { table, values } => {
|
||||
if let Some(t) = tables.tables.iter_mut().find(|t| &t.name == table) {
|
||||
t.rows.extend(values.clone());
|
||||
} else {
|
||||
return Err(anyhow::anyhow!(
|
||||
"Table {} does not exist. INSERT statement ignored.",
|
||||
table
|
||||
));
|
||||
}
|
||||
}
|
||||
Insert::Select { table, select } => {
|
||||
let rows = select.shadow(tables)?;
|
||||
if let Some(t) = tables.tables.iter_mut().find(|t| &t.name == table) {
|
||||
t.rows.extend(rows);
|
||||
} else {
|
||||
return Err(anyhow::anyhow!(
|
||||
"Table {} does not exist. INSERT statement ignored.",
|
||||
table
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(vec![])
|
||||
}
|
||||
}
|
||||
|
||||
impl Insert {
|
||||
pub(crate) fn table(&self) -> &str {
|
||||
match self {
|
||||
Insert::Values { table, .. } | Insert::Select { table, .. } => table,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for Insert {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Insert::Values { table, values } => {
|
||||
write!(f, "INSERT INTO {table} VALUES ")?;
|
||||
for (i, row) in values.iter().enumerate() {
|
||||
if i != 0 {
|
||||
write!(f, ", ")?;
|
||||
}
|
||||
write!(f, "(")?;
|
||||
for (j, value) in row.iter().enumerate() {
|
||||
if j != 0 {
|
||||
write!(f, ", ")?;
|
||||
}
|
||||
write!(f, "{value}")?;
|
||||
}
|
||||
write!(f, ")")?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
Insert::Select { table, select } => {
|
||||
write!(f, "INSERT INTO {table} ")?;
|
||||
write!(f, "{select}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
129
sql_generation/model/query/mod.rs
Normal file
129
sql_generation/model/query/mod.rs
Normal file
@@ -0,0 +1,129 @@
|
||||
use std::{collections::HashSet, fmt::Display};
|
||||
|
||||
pub(crate) use create::Create;
|
||||
pub(crate) use create_index::CreateIndex;
|
||||
pub(crate) use delete::Delete;
|
||||
pub(crate) use drop::Drop;
|
||||
pub(crate) use insert::Insert;
|
||||
pub(crate) use select::Select;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use turso_parser::ast::fmt::ToSqlContext;
|
||||
use update::Update;
|
||||
|
||||
use crate::{
|
||||
generation::Shadow,
|
||||
model::{
|
||||
query::transaction::{Begin, Commit, Rollback},
|
||||
table::SimValue,
|
||||
},
|
||||
runner::env::SimulatorTables,
|
||||
};
|
||||
|
||||
pub mod create;
|
||||
pub mod create_index;
|
||||
pub mod delete;
|
||||
pub mod drop;
|
||||
pub mod insert;
|
||||
pub mod predicate;
|
||||
pub mod select;
|
||||
pub mod transaction;
|
||||
pub mod update;
|
||||
|
||||
// This type represents the potential queries on the database.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub(crate) enum Query {
|
||||
Create(Create),
|
||||
Select(Select),
|
||||
Insert(Insert),
|
||||
Delete(Delete),
|
||||
Update(Update),
|
||||
Drop(Drop),
|
||||
CreateIndex(CreateIndex),
|
||||
Begin(Begin),
|
||||
Commit(Commit),
|
||||
Rollback(Rollback),
|
||||
}
|
||||
|
||||
impl Query {
|
||||
pub(crate) fn dependencies(&self) -> HashSet<String> {
|
||||
match self {
|
||||
Query::Select(select) => select.dependencies(),
|
||||
Query::Create(_) => HashSet::new(),
|
||||
Query::Insert(Insert::Select { table, .. })
|
||||
| Query::Insert(Insert::Values { table, .. })
|
||||
| Query::Delete(Delete { table, .. })
|
||||
| Query::Update(Update { table, .. })
|
||||
| Query::Drop(Drop { table, .. }) => HashSet::from_iter([table.clone()]),
|
||||
Query::CreateIndex(CreateIndex { table_name, .. }) => {
|
||||
HashSet::from_iter([table_name.clone()])
|
||||
}
|
||||
Query::Begin(_) | Query::Commit(_) | Query::Rollback(_) => HashSet::new(),
|
||||
}
|
||||
}
|
||||
pub(crate) fn uses(&self) -> Vec<String> {
|
||||
match self {
|
||||
Query::Create(Create { table }) => vec![table.name.clone()],
|
||||
Query::Select(select) => select.dependencies().into_iter().collect(),
|
||||
Query::Insert(Insert::Select { table, .. })
|
||||
| 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::Begin(..) | Query::Commit(..) | Query::Rollback(..) => vec![],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Shadow for Query {
|
||||
type Result = anyhow::Result<Vec<Vec<SimValue>>>;
|
||||
|
||||
fn shadow(&self, env: &mut SimulatorTables) -> Self::Result {
|
||||
match self {
|
||||
Query::Create(create) => create.shadow(env),
|
||||
Query::Insert(insert) => insert.shadow(env),
|
||||
Query::Delete(delete) => delete.shadow(env),
|
||||
Query::Select(select) => select.shadow(env),
|
||||
Query::Update(update) => update.shadow(env),
|
||||
Query::Drop(drop) => drop.shadow(env),
|
||||
Query::CreateIndex(create_index) => Ok(create_index.shadow(env)),
|
||||
Query::Begin(begin) => Ok(begin.shadow(env)),
|
||||
Query::Commit(commit) => Ok(commit.shadow(env)),
|
||||
Query::Rollback(rollback) => Ok(rollback.shadow(env)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for Query {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::Create(create) => write!(f, "{create}"),
|
||||
Self::Select(select) => write!(f, "{select}"),
|
||||
Self::Insert(insert) => write!(f, "{insert}"),
|
||||
Self::Delete(delete) => write!(f, "{delete}"),
|
||||
Self::Update(update) => write!(f, "{update}"),
|
||||
Self::Drop(drop) => write!(f, "{drop}"),
|
||||
Self::CreateIndex(create_index) => write!(f, "{create_index}"),
|
||||
Self::Begin(begin) => write!(f, "{begin}"),
|
||||
Self::Commit(commit) => write!(f, "{commit}"),
|
||||
Self::Rollback(rollback) => write!(f, "{rollback}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Used to print sql strings that already have all the context it needs
|
||||
pub(crate) struct EmptyContext;
|
||||
|
||||
impl ToSqlContext for EmptyContext {
|
||||
fn get_column_name(
|
||||
&self,
|
||||
_table_id: turso_parser::ast::TableInternalId,
|
||||
_col_idx: usize,
|
||||
) -> String {
|
||||
unreachable!()
|
||||
}
|
||||
|
||||
fn get_table_name(&self, _id: turso_parser::ast::TableInternalId) -> &str {
|
||||
unreachable!()
|
||||
}
|
||||
}
|
||||
146
sql_generation/model/query/predicate.rs
Normal file
146
sql_generation/model/query/predicate.rs
Normal file
@@ -0,0 +1,146 @@
|
||||
use std::fmt::Display;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use turso_parser::ast::{self, fmt::ToTokens};
|
||||
|
||||
use crate::model::table::{SimValue, Table, TableContext};
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
pub struct Predicate(pub ast::Expr);
|
||||
|
||||
impl Predicate {
|
||||
pub(crate) fn true_() -> Self {
|
||||
Self(ast::Expr::Literal(ast::Literal::Keyword(
|
||||
"TRUE".to_string(),
|
||||
)))
|
||||
}
|
||||
|
||||
pub(crate) fn false_() -> Self {
|
||||
Self(ast::Expr::Literal(ast::Literal::Keyword(
|
||||
"FALSE".to_string(),
|
||||
)))
|
||||
}
|
||||
pub(crate) fn null() -> Self {
|
||||
Self(ast::Expr::Literal(ast::Literal::Null))
|
||||
}
|
||||
|
||||
pub(crate) fn not(predicate: Predicate) -> Self {
|
||||
let expr = ast::Expr::Unary(ast::UnaryOperator::Not, Box::new(predicate.0));
|
||||
Self(expr).parens()
|
||||
}
|
||||
|
||||
pub(crate) fn and(predicates: Vec<Predicate>) -> Self {
|
||||
if predicates.is_empty() {
|
||||
Self::true_()
|
||||
} else if predicates.len() == 1 {
|
||||
predicates.into_iter().next().unwrap().parens()
|
||||
} else {
|
||||
let expr = ast::Expr::Binary(
|
||||
Box::new(predicates[0].0.clone()),
|
||||
ast::Operator::And,
|
||||
Box::new(Self::and(predicates[1..].to_vec()).0),
|
||||
);
|
||||
Self(expr).parens()
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn or(predicates: Vec<Predicate>) -> Self {
|
||||
if predicates.is_empty() {
|
||||
Self::false_()
|
||||
} else if predicates.len() == 1 {
|
||||
predicates.into_iter().next().unwrap().parens()
|
||||
} else {
|
||||
let expr = ast::Expr::Binary(
|
||||
Box::new(predicates[0].0.clone()),
|
||||
ast::Operator::Or,
|
||||
Box::new(Self::or(predicates[1..].to_vec()).0),
|
||||
);
|
||||
Self(expr).parens()
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn eq(lhs: Predicate, rhs: Predicate) -> Self {
|
||||
let expr = ast::Expr::Binary(Box::new(lhs.0), ast::Operator::Equals, Box::new(rhs.0));
|
||||
Self(expr).parens()
|
||||
}
|
||||
|
||||
pub(crate) fn is(lhs: Predicate, rhs: Predicate) -> Self {
|
||||
let expr = ast::Expr::Binary(Box::new(lhs.0), ast::Operator::Is, Box::new(rhs.0));
|
||||
Self(expr).parens()
|
||||
}
|
||||
|
||||
pub(crate) fn parens(self) -> Self {
|
||||
let expr = ast::Expr::Parenthesized(vec![self.0]);
|
||||
Self(expr)
|
||||
}
|
||||
|
||||
pub(crate) fn eval(&self, row: &[SimValue], table: &Table) -> Option<SimValue> {
|
||||
expr_to_value(&self.0, row, table)
|
||||
}
|
||||
|
||||
pub(crate) fn test<T: TableContext>(&self, row: &[SimValue], table: &T) -> bool {
|
||||
let value = expr_to_value(&self.0, row, table);
|
||||
value.is_some_and(|value| value.as_bool())
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: In the future pass a Vec<Table> to support resolving a value from another table
|
||||
// This function attempts to convert an simpler easily computable expression into values
|
||||
// TODO: In the future, we can try to expand this computation if we want to support harder properties that require us
|
||||
// to already know more values before hand
|
||||
pub fn expr_to_value<T: TableContext>(
|
||||
expr: &ast::Expr,
|
||||
row: &[SimValue],
|
||||
table: &T,
|
||||
) -> Option<SimValue> {
|
||||
match expr {
|
||||
ast::Expr::DoublyQualified(_, _, ast::Name::Ident(col_name))
|
||||
| ast::Expr::DoublyQualified(_, _, ast::Name::Quoted(col_name))
|
||||
| ast::Expr::Qualified(_, ast::Name::Ident(col_name))
|
||||
| ast::Expr::Qualified(_, ast::Name::Quoted(col_name))
|
||||
| ast::Expr::Id(ast::Name::Ident(col_name)) => {
|
||||
let columns = table.columns().collect::<Vec<_>>();
|
||||
assert_eq!(row.len(), columns.len());
|
||||
columns
|
||||
.iter()
|
||||
.zip(row.iter())
|
||||
.find(|(column, _)| column.column.name == *col_name)
|
||||
.map(|(_, value)| value)
|
||||
.cloned()
|
||||
}
|
||||
ast::Expr::Literal(literal) => Some(literal.into()),
|
||||
ast::Expr::Binary(lhs, op, rhs) => {
|
||||
let lhs = expr_to_value(lhs, row, table)?;
|
||||
let rhs = expr_to_value(rhs, row, table)?;
|
||||
Some(lhs.binary_compare(&rhs, *op))
|
||||
}
|
||||
ast::Expr::Like {
|
||||
lhs,
|
||||
not,
|
||||
op,
|
||||
rhs,
|
||||
escape: _, // TODO: support escape
|
||||
} => {
|
||||
let lhs = expr_to_value(lhs, row, table)?;
|
||||
let rhs = expr_to_value(rhs, row, table)?;
|
||||
let res = lhs.like_compare(&rhs, *op);
|
||||
let value: SimValue = if *not { !res } else { res }.into();
|
||||
Some(value)
|
||||
}
|
||||
ast::Expr::Unary(op, expr) => {
|
||||
let value = expr_to_value(expr, row, table)?;
|
||||
Some(value.unary_exec(*op))
|
||||
}
|
||||
ast::Expr::Parenthesized(exprs) => {
|
||||
assert_eq!(exprs.len(), 1);
|
||||
expr_to_value(&exprs[0], row, table)
|
||||
}
|
||||
_ => unreachable!("{:?}", expr),
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for Predicate {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
self.0.to_fmt(f)
|
||||
}
|
||||
}
|
||||
496
sql_generation/model/query/select.rs
Normal file
496
sql_generation/model/query/select.rs
Normal file
@@ -0,0 +1,496 @@
|
||||
use std::{collections::HashSet, fmt::Display};
|
||||
|
||||
use anyhow::Context;
|
||||
pub use ast::Distinctness;
|
||||
use itertools::Itertools;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use turso_parser::ast::{self, fmt::ToTokens, SortOrder};
|
||||
|
||||
use crate::{
|
||||
generation::Shadow,
|
||||
model::{
|
||||
query::EmptyContext,
|
||||
table::{JoinTable, JoinType, JoinedTable, SimValue, Table, TableContext},
|
||||
},
|
||||
runner::env::SimulatorTables,
|
||||
};
|
||||
|
||||
use super::predicate::Predicate;
|
||||
|
||||
/// `SELECT` or `RETURNING` result column
|
||||
// https://sqlite.org/syntax/result-column.html
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
pub enum ResultColumn {
|
||||
/// expression
|
||||
Expr(Predicate),
|
||||
/// `*`
|
||||
Star,
|
||||
/// column name
|
||||
Column(String),
|
||||
}
|
||||
|
||||
impl Display for ResultColumn {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
ResultColumn::Expr(expr) => write!(f, "({expr})"),
|
||||
ResultColumn::Star => write!(f, "*"),
|
||||
ResultColumn::Column(name) => write!(f, "{name}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
pub(crate) struct Select {
|
||||
pub(crate) body: SelectBody,
|
||||
pub(crate) limit: Option<usize>,
|
||||
}
|
||||
|
||||
impl Select {
|
||||
pub fn simple(table: String, where_clause: Predicate) -> Self {
|
||||
Self::single(
|
||||
table,
|
||||
vec![ResultColumn::Star],
|
||||
where_clause,
|
||||
None,
|
||||
Distinctness::All,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn expr(expr: Predicate) -> Self {
|
||||
Select {
|
||||
body: SelectBody {
|
||||
select: Box::new(SelectInner {
|
||||
distinctness: Distinctness::All,
|
||||
columns: vec![ResultColumn::Expr(expr)],
|
||||
from: None,
|
||||
where_clause: Predicate::true_(),
|
||||
order_by: None,
|
||||
}),
|
||||
compounds: Vec::new(),
|
||||
},
|
||||
limit: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn single(
|
||||
table: String,
|
||||
result_columns: Vec<ResultColumn>,
|
||||
where_clause: Predicate,
|
||||
limit: Option<usize>,
|
||||
distinct: Distinctness,
|
||||
) -> Self {
|
||||
Select {
|
||||
body: SelectBody {
|
||||
select: Box::new(SelectInner {
|
||||
distinctness: distinct,
|
||||
columns: result_columns,
|
||||
from: Some(FromClause {
|
||||
table,
|
||||
joins: Vec::new(),
|
||||
}),
|
||||
where_clause,
|
||||
order_by: None,
|
||||
}),
|
||||
compounds: Vec::new(),
|
||||
},
|
||||
limit,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn compound(left: Select, right: Select, operator: CompoundOperator) -> Self {
|
||||
let mut body = left.body;
|
||||
body.compounds.push(CompoundSelect {
|
||||
operator,
|
||||
select: Box::new(right.body.select.as_ref().clone()),
|
||||
});
|
||||
Select {
|
||||
body,
|
||||
limit: left.limit.or(right.limit),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn dependencies(&self) -> HashSet<String> {
|
||||
if self.body.select.from.is_none() {
|
||||
return HashSet::new();
|
||||
}
|
||||
let from = self.body.select.from.as_ref().unwrap();
|
||||
let mut tables = HashSet::new();
|
||||
tables.insert(from.table.clone());
|
||||
|
||||
tables.extend(from.dependencies());
|
||||
|
||||
for compound in &self.body.compounds {
|
||||
tables.extend(
|
||||
compound
|
||||
.select
|
||||
.from
|
||||
.as_ref()
|
||||
.map(|f| f.dependencies())
|
||||
.unwrap_or(vec![])
|
||||
.into_iter(),
|
||||
);
|
||||
}
|
||||
|
||||
tables
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
pub struct SelectBody {
|
||||
/// first select
|
||||
pub select: Box<SelectInner>,
|
||||
/// compounds
|
||||
pub compounds: Vec<CompoundSelect>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
pub struct OrderBy {
|
||||
pub columns: Vec<(String, SortOrder)>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
pub struct SelectInner {
|
||||
/// `DISTINCT`
|
||||
pub distinctness: Distinctness,
|
||||
/// columns
|
||||
pub columns: Vec<ResultColumn>,
|
||||
/// `FROM` clause
|
||||
pub from: Option<FromClause>,
|
||||
/// `WHERE` clause
|
||||
pub where_clause: Predicate,
|
||||
/// `ORDER BY` clause
|
||||
pub order_by: Option<OrderBy>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
pub enum CompoundOperator {
|
||||
/// `UNION`
|
||||
Union,
|
||||
/// `UNION ALL`
|
||||
UnionAll,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
pub struct CompoundSelect {
|
||||
/// operator
|
||||
pub operator: CompoundOperator,
|
||||
/// select
|
||||
pub select: Box<SelectInner>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
pub struct FromClause {
|
||||
/// table
|
||||
pub table: String,
|
||||
/// `JOIN`ed tables
|
||||
pub joins: Vec<JoinedTable>,
|
||||
}
|
||||
|
||||
impl FromClause {
|
||||
fn to_sql_ast(&self) -> ast::FromClause {
|
||||
ast::FromClause {
|
||||
select: Some(Box::new(ast::SelectTable::Table(
|
||||
ast::QualifiedName::single(ast::Name::from_str(&self.table)),
|
||||
None,
|
||||
None,
|
||||
))),
|
||||
joins: if self.joins.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(
|
||||
self.joins
|
||||
.iter()
|
||||
.map(|join| ast::JoinedSelectTable {
|
||||
operator: match join.join_type {
|
||||
JoinType::Inner => {
|
||||
ast::JoinOperator::TypedJoin(Some(ast::JoinType::INNER))
|
||||
}
|
||||
JoinType::Left => {
|
||||
ast::JoinOperator::TypedJoin(Some(ast::JoinType::LEFT))
|
||||
}
|
||||
JoinType::Right => {
|
||||
ast::JoinOperator::TypedJoin(Some(ast::JoinType::RIGHT))
|
||||
}
|
||||
JoinType::Full => {
|
||||
ast::JoinOperator::TypedJoin(Some(ast::JoinType::OUTER))
|
||||
}
|
||||
JoinType::Cross => {
|
||||
ast::JoinOperator::TypedJoin(Some(ast::JoinType::CROSS))
|
||||
}
|
||||
},
|
||||
table: ast::SelectTable::Table(
|
||||
ast::QualifiedName::single(ast::Name::from_str(&join.table)),
|
||||
None,
|
||||
None,
|
||||
),
|
||||
constraint: Some(ast::JoinConstraint::On(join.on.0.clone())),
|
||||
})
|
||||
.collect(),
|
||||
)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn dependencies(&self) -> Vec<String> {
|
||||
let mut deps = vec![self.table.clone()];
|
||||
for join in &self.joins {
|
||||
deps.push(join.table.clone());
|
||||
}
|
||||
deps
|
||||
}
|
||||
}
|
||||
|
||||
impl Shadow for FromClause {
|
||||
type Result = anyhow::Result<JoinTable>;
|
||||
fn shadow(&self, env: &mut SimulatorTables) -> Self::Result {
|
||||
let tables = &mut env.tables;
|
||||
|
||||
let first_table = tables
|
||||
.iter()
|
||||
.find(|t| t.name == self.table)
|
||||
.context("Table not found")?;
|
||||
|
||||
let mut join_table = JoinTable {
|
||||
tables: vec![first_table.clone()],
|
||||
rows: Vec::new(),
|
||||
};
|
||||
|
||||
for join in &self.joins {
|
||||
let joined_table = tables
|
||||
.iter()
|
||||
.find(|t| t.name == join.table)
|
||||
.context("Joined table not found")?;
|
||||
|
||||
join_table.tables.push(joined_table.clone());
|
||||
|
||||
match join.join_type {
|
||||
JoinType::Inner => {
|
||||
// Implement inner join logic
|
||||
let join_rows = joined_table
|
||||
.rows
|
||||
.iter()
|
||||
.filter(|row| join.on.test(row, joined_table))
|
||||
.cloned()
|
||||
.collect::<Vec<_>>();
|
||||
// take a cartesian product of the rows
|
||||
let all_row_pairs = join_table
|
||||
.rows
|
||||
.clone()
|
||||
.into_iter()
|
||||
.cartesian_product(join_rows.iter());
|
||||
|
||||
for (row1, row2) in all_row_pairs {
|
||||
let row = row1.iter().chain(row2.iter()).cloned().collect::<Vec<_>>();
|
||||
|
||||
let is_in = join.on.test(&row, &join_table);
|
||||
|
||||
if is_in {
|
||||
join_table.rows.push(row);
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => todo!(),
|
||||
}
|
||||
}
|
||||
Ok(join_table)
|
||||
}
|
||||
}
|
||||
|
||||
impl Shadow for SelectInner {
|
||||
type Result = anyhow::Result<JoinTable>;
|
||||
|
||||
fn shadow(&self, env: &mut SimulatorTables) -> Self::Result {
|
||||
if let Some(from) = &self.from {
|
||||
let mut join_table = from.shadow(env)?;
|
||||
let col_count = join_table.columns().count();
|
||||
for row in &mut join_table.rows {
|
||||
assert_eq!(
|
||||
row.len(),
|
||||
col_count,
|
||||
"Row length does not match column length after join"
|
||||
);
|
||||
}
|
||||
let join_clone = join_table.clone();
|
||||
|
||||
join_table
|
||||
.rows
|
||||
.retain(|row| self.where_clause.test(row, &join_clone));
|
||||
|
||||
if self.distinctness == Distinctness::Distinct {
|
||||
join_table.rows.sort_unstable();
|
||||
join_table.rows.dedup();
|
||||
}
|
||||
|
||||
Ok(join_table)
|
||||
} else {
|
||||
assert!(self
|
||||
.columns
|
||||
.iter()
|
||||
.all(|col| matches!(col, ResultColumn::Expr(_))));
|
||||
|
||||
// If `WHERE` is false, just return an empty table
|
||||
if !self.where_clause.test(&[], &Table::anonymous(vec![])) {
|
||||
return Ok(JoinTable {
|
||||
tables: Vec::new(),
|
||||
rows: Vec::new(),
|
||||
});
|
||||
}
|
||||
|
||||
// Compute the results of the column expressions and make a row
|
||||
let mut row = Vec::new();
|
||||
for col in &self.columns {
|
||||
match col {
|
||||
ResultColumn::Expr(expr) => {
|
||||
let value = expr.eval(&[], &Table::anonymous(vec![]));
|
||||
if let Some(value) = value {
|
||||
row.push(value);
|
||||
} else {
|
||||
return Err(anyhow::anyhow!(
|
||||
"Failed to evaluate expression in free select ({})",
|
||||
expr.0.format_with_context(&EmptyContext {}).unwrap()
|
||||
));
|
||||
}
|
||||
}
|
||||
_ => unreachable!("Only expressions are allowed in free selects"),
|
||||
}
|
||||
}
|
||||
|
||||
Ok(JoinTable {
|
||||
tables: Vec::new(),
|
||||
rows: vec![row],
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Shadow for Select {
|
||||
type Result = anyhow::Result<Vec<Vec<SimValue>>>;
|
||||
|
||||
fn shadow(&self, env: &mut SimulatorTables) -> Self::Result {
|
||||
let first_result = self.body.select.shadow(env)?;
|
||||
|
||||
let mut rows = first_result.rows;
|
||||
|
||||
for compound in self.body.compounds.iter() {
|
||||
let compound_results = compound.select.shadow(env)?;
|
||||
|
||||
match compound.operator {
|
||||
CompoundOperator::Union => {
|
||||
// Union means we need to combine the results, removing duplicates
|
||||
let mut new_rows = compound_results.rows;
|
||||
new_rows.extend(rows.clone());
|
||||
new_rows.sort_unstable();
|
||||
new_rows.dedup();
|
||||
rows = new_rows;
|
||||
}
|
||||
CompoundOperator::UnionAll => {
|
||||
// Union all means we just concatenate the results
|
||||
rows.extend(compound_results.rows.into_iter());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(rows)
|
||||
}
|
||||
}
|
||||
|
||||
impl Select {
|
||||
pub fn to_sql_ast(&self) -> ast::Select {
|
||||
ast::Select {
|
||||
with: None,
|
||||
body: ast::SelectBody {
|
||||
select: Box::new(ast::OneSelect::Select(Box::new(ast::SelectInner {
|
||||
distinctness: if self.body.select.distinctness == Distinctness::Distinct {
|
||||
Some(ast::Distinctness::Distinct)
|
||||
} else {
|
||||
None
|
||||
},
|
||||
columns: self
|
||||
.body
|
||||
.select
|
||||
.columns
|
||||
.iter()
|
||||
.map(|col| match col {
|
||||
ResultColumn::Expr(expr) => {
|
||||
ast::ResultColumn::Expr(expr.0.clone(), None)
|
||||
}
|
||||
ResultColumn::Star => ast::ResultColumn::Star,
|
||||
ResultColumn::Column(name) => ast::ResultColumn::Expr(
|
||||
ast::Expr::Id(ast::Name::Ident(name.clone())),
|
||||
None,
|
||||
),
|
||||
})
|
||||
.collect(),
|
||||
from: self.body.select.from.as_ref().map(|f| f.to_sql_ast()),
|
||||
where_clause: Some(self.body.select.where_clause.0.clone()),
|
||||
group_by: None,
|
||||
window_clause: None,
|
||||
}))),
|
||||
compounds: Some(
|
||||
self.body
|
||||
.compounds
|
||||
.iter()
|
||||
.map(|compound| ast::CompoundSelect {
|
||||
operator: match compound.operator {
|
||||
CompoundOperator::Union => ast::CompoundOperator::Union,
|
||||
CompoundOperator::UnionAll => ast::CompoundOperator::UnionAll,
|
||||
},
|
||||
select: Box::new(ast::OneSelect::Select(Box::new(ast::SelectInner {
|
||||
distinctness: Some(compound.select.distinctness),
|
||||
columns: compound
|
||||
.select
|
||||
.columns
|
||||
.iter()
|
||||
.map(|col| match col {
|
||||
ResultColumn::Expr(expr) => {
|
||||
ast::ResultColumn::Expr(expr.0.clone(), None)
|
||||
}
|
||||
ResultColumn::Star => ast::ResultColumn::Star,
|
||||
ResultColumn::Column(name) => ast::ResultColumn::Expr(
|
||||
ast::Expr::Id(ast::Name::Ident(name.clone())),
|
||||
None,
|
||||
),
|
||||
})
|
||||
.collect(),
|
||||
from: compound.select.from.as_ref().map(|f| f.to_sql_ast()),
|
||||
where_clause: Some(compound.select.where_clause.0.clone()),
|
||||
group_by: None,
|
||||
window_clause: None,
|
||||
}))),
|
||||
})
|
||||
.collect(),
|
||||
),
|
||||
},
|
||||
order_by: self.body.select.order_by.as_ref().map(|o| {
|
||||
o.columns
|
||||
.iter()
|
||||
.map(|(name, order)| ast::SortedColumn {
|
||||
expr: ast::Expr::Id(ast::Name::Ident(name.clone())),
|
||||
order: match order {
|
||||
SortOrder::Asc => Some(ast::SortOrder::Asc),
|
||||
SortOrder::Desc => Some(ast::SortOrder::Desc),
|
||||
},
|
||||
nulls: None,
|
||||
})
|
||||
.collect()
|
||||
}),
|
||||
limit: self.limit.map(|l| {
|
||||
Box::new(ast::Limit {
|
||||
expr: ast::Expr::Literal(ast::Literal::Numeric(l.to_string())),
|
||||
offset: None,
|
||||
})
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
impl Display for Select {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
self.to_sql_ast().to_fmt_with_context(f, &EmptyContext {})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod select_tests {
|
||||
|
||||
#[test]
|
||||
fn test_select_display() {}
|
||||
}
|
||||
60
sql_generation/model/query/transaction.rs
Normal file
60
sql_generation/model/query/transaction.rs
Normal file
@@ -0,0 +1,60 @@
|
||||
use std::fmt::Display;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{generation::Shadow, model::table::SimValue, runner::env::SimulatorTables};
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub(crate) struct Begin {
|
||||
pub(crate) immediate: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub(crate) struct Commit;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub(crate) struct Rollback;
|
||||
|
||||
impl Shadow for Begin {
|
||||
type Result = Vec<Vec<SimValue>>;
|
||||
fn shadow(&self, tables: &mut SimulatorTables) -> Self::Result {
|
||||
tables.snapshot = Some(tables.tables.clone());
|
||||
vec![]
|
||||
}
|
||||
}
|
||||
|
||||
impl Shadow for Commit {
|
||||
type Result = Vec<Vec<SimValue>>;
|
||||
fn shadow(&self, tables: &mut SimulatorTables) -> Self::Result {
|
||||
tables.snapshot = None;
|
||||
vec![]
|
||||
}
|
||||
}
|
||||
|
||||
impl Shadow for Rollback {
|
||||
type Result = Vec<Vec<SimValue>>;
|
||||
fn shadow(&self, tables: &mut SimulatorTables) -> Self::Result {
|
||||
if let Some(tables_) = tables.snapshot.take() {
|
||||
tables.tables = tables_;
|
||||
}
|
||||
vec![]
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for Begin {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "BEGIN {}", if self.immediate { "IMMEDIATE" } else { "" })
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for Commit {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "COMMIT")
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for Rollback {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "ROLLBACK")
|
||||
}
|
||||
}
|
||||
71
sql_generation/model/query/update.rs
Normal file
71
sql_generation/model/query/update.rs
Normal file
@@ -0,0 +1,71 @@
|
||||
use std::fmt::Display;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{generation::Shadow, model::table::SimValue, runner::env::SimulatorTables};
|
||||
|
||||
use super::predicate::Predicate;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
pub(crate) struct Update {
|
||||
pub(crate) table: String,
|
||||
pub(crate) set_values: Vec<(String, SimValue)>, // Pair of value for set expressions => SET name=value
|
||||
pub(crate) predicate: Predicate,
|
||||
}
|
||||
|
||||
impl Update {
|
||||
pub fn table(&self) -> &str {
|
||||
&self.table
|
||||
}
|
||||
}
|
||||
|
||||
impl Shadow for Update {
|
||||
type Result = anyhow::Result<Vec<Vec<SimValue>>>;
|
||||
|
||||
fn shadow(&self, tables: &mut SimulatorTables) -> Self::Result {
|
||||
let table = tables.tables.iter_mut().find(|t| t.name == self.table);
|
||||
|
||||
let table = if let Some(table) = table {
|
||||
table
|
||||
} else {
|
||||
return Err(anyhow::anyhow!(
|
||||
"Table {} does not exist. UPDATE statement ignored.",
|
||||
self.table
|
||||
));
|
||||
};
|
||||
|
||||
let t2 = table.clone();
|
||||
for row in table
|
||||
.rows
|
||||
.iter_mut()
|
||||
.filter(|r| self.predicate.test(r, &t2))
|
||||
{
|
||||
for (column, set_value) in &self.set_values {
|
||||
if let Some((idx, _)) = table
|
||||
.columns
|
||||
.iter()
|
||||
.enumerate()
|
||||
.find(|(_, c)| &c.name == column)
|
||||
{
|
||||
row[idx] = set_value.clone();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(vec![])
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for Update {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "UPDATE {} SET ", self.table)?;
|
||||
for (i, (name, value)) in self.set_values.iter().enumerate() {
|
||||
if i != 0 {
|
||||
write!(f, ", ")?;
|
||||
}
|
||||
write!(f, "{name} = {value}")?;
|
||||
}
|
||||
write!(f, " WHERE {}", self.predicate)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
428
sql_generation/model/table.rs
Normal file
428
sql_generation/model/table.rs
Normal file
@@ -0,0 +1,428 @@
|
||||
use std::{fmt::Display, hash::Hash, ops::Deref};
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use turso_core::{numeric::Numeric, types};
|
||||
use turso_parser::ast;
|
||||
|
||||
use crate::model::query::predicate::Predicate;
|
||||
|
||||
pub(crate) struct Name(pub(crate) String);
|
||||
|
||||
impl Deref for Name {
|
||||
type Target = str;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct ContextColumn<'a> {
|
||||
pub table_name: &'a str,
|
||||
pub column: &'a Column,
|
||||
}
|
||||
|
||||
pub trait TableContext {
|
||||
fn columns<'a>(&'a self) -> impl Iterator<Item = ContextColumn<'a>>;
|
||||
fn rows(&self) -> &Vec<Vec<SimValue>>;
|
||||
}
|
||||
|
||||
impl TableContext for Table {
|
||||
fn columns<'a>(&'a self) -> impl Iterator<Item = ContextColumn<'a>> {
|
||||
self.columns.iter().map(|col| ContextColumn {
|
||||
column: col,
|
||||
table_name: &self.name,
|
||||
})
|
||||
}
|
||||
|
||||
fn rows(&self) -> &Vec<Vec<SimValue>> {
|
||||
&self.rows
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub(crate) struct Table {
|
||||
pub(crate) name: String,
|
||||
pub(crate) columns: Vec<Column>,
|
||||
pub(crate) rows: Vec<Vec<SimValue>>,
|
||||
pub(crate) indexes: Vec<String>,
|
||||
}
|
||||
|
||||
impl Table {
|
||||
pub fn anonymous(rows: Vec<Vec<SimValue>>) -> Self {
|
||||
Self {
|
||||
rows,
|
||||
name: "".to_string(),
|
||||
columns: vec![],
|
||||
indexes: vec![],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub(crate) struct Column {
|
||||
pub(crate) name: String,
|
||||
pub(crate) column_type: ColumnType,
|
||||
pub(crate) primary: bool,
|
||||
pub(crate) unique: bool,
|
||||
}
|
||||
|
||||
// Uniquely defined by name in this case
|
||||
impl Hash for Column {
|
||||
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
|
||||
self.name.hash(state);
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for Column {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.name == other.name
|
||||
}
|
||||
}
|
||||
|
||||
impl Eq for Column {}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub(crate) enum ColumnType {
|
||||
Integer,
|
||||
Float,
|
||||
Text,
|
||||
Blob,
|
||||
}
|
||||
|
||||
impl Display for ColumnType {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::Integer => write!(f, "INTEGER"),
|
||||
Self::Float => write!(f, "REAL"),
|
||||
Self::Text => write!(f, "TEXT"),
|
||||
Self::Blob => write!(f, "BLOB"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
pub struct JoinedTable {
|
||||
/// table name
|
||||
pub table: String,
|
||||
/// `JOIN` type
|
||||
pub join_type: JoinType,
|
||||
/// `ON` clause
|
||||
pub on: Predicate,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
pub enum JoinType {
|
||||
Inner,
|
||||
Left,
|
||||
Right,
|
||||
Full,
|
||||
Cross,
|
||||
}
|
||||
|
||||
impl TableContext for JoinTable {
|
||||
fn columns<'a>(&'a self) -> impl Iterator<Item = ContextColumn<'a>> {
|
||||
self.tables.iter().flat_map(|table| table.columns())
|
||||
}
|
||||
|
||||
fn rows(&self) -> &Vec<Vec<SimValue>> {
|
||||
&self.rows
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct JoinTable {
|
||||
pub tables: Vec<Table>,
|
||||
pub rows: Vec<Vec<SimValue>>,
|
||||
}
|
||||
|
||||
fn float_to_string<S>(float: &f64, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: serde::Serializer,
|
||||
{
|
||||
serializer.serialize_str(&format!("{float}"))
|
||||
}
|
||||
|
||||
fn string_to_float<'de, D>(deserializer: D) -> Result<f64, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
let s = String::deserialize(deserializer)?;
|
||||
s.parse().map_err(serde::de::Error::custom)
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, PartialOrd, Eq, Ord, Serialize, Deserialize)]
|
||||
pub(crate) struct SimValue(pub turso_core::Value);
|
||||
|
||||
fn to_sqlite_blob(bytes: &[u8]) -> String {
|
||||
format!(
|
||||
"X'{}'",
|
||||
bytes
|
||||
.iter()
|
||||
.fold(String::new(), |acc, b| acc + &format!("{b:02X}"))
|
||||
)
|
||||
}
|
||||
|
||||
impl Display for SimValue {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match &self.0 {
|
||||
types::Value::Null => write!(f, "NULL"),
|
||||
types::Value::Integer(i) => write!(f, "{i}"),
|
||||
types::Value::Float(fl) => write!(f, "{fl}"),
|
||||
value @ types::Value::Text(..) => write!(f, "'{value}'"),
|
||||
types::Value::Blob(b) => write!(f, "{}", to_sqlite_blob(b)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl SimValue {
|
||||
pub const FALSE: Self = SimValue(types::Value::Integer(0));
|
||||
pub const TRUE: Self = SimValue(types::Value::Integer(1));
|
||||
|
||||
pub fn as_bool(&self) -> bool {
|
||||
Numeric::from(&self.0).try_into_bool().unwrap_or_default()
|
||||
}
|
||||
|
||||
// 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
|
||||
pub fn binary_compare(&self, other: &Self, operator: ast::Operator) -> SimValue {
|
||||
match operator {
|
||||
ast::Operator::Add => self.0.exec_add(&other.0).into(),
|
||||
ast::Operator::And => self.0.exec_and(&other.0).into(),
|
||||
ast::Operator::ArrowRight => todo!(),
|
||||
ast::Operator::ArrowRightShift => todo!(),
|
||||
ast::Operator::BitwiseAnd => self.0.exec_bit_and(&other.0).into(),
|
||||
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::Divide => self.0.exec_divide(&other.0).into(),
|
||||
ast::Operator::Greater => (self > other).into(),
|
||||
ast::Operator::GreaterEquals => (self >= other).into(),
|
||||
// TODO: Test these implementations
|
||||
ast::Operator::Is => match (&self.0, &other.0) {
|
||||
(types::Value::Null, types::Value::Null) => true.into(),
|
||||
(types::Value::Null, _) => false.into(),
|
||||
(_, types::Value::Null) => false.into(),
|
||||
_ => self.binary_compare(other, ast::Operator::Equals),
|
||||
},
|
||||
ast::Operator::IsNot => self
|
||||
.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::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::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(),
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: support more operators. Copy the implementation for exec_glob
|
||||
pub fn like_compare(&self, other: &Self, operator: ast::LikeOperator) -> bool {
|
||||
match operator {
|
||||
ast::LikeOperator::Glob => todo!(),
|
||||
ast::LikeOperator::Like => {
|
||||
// TODO: support ESCAPE `expr` option in AST
|
||||
// TODO: regex cache
|
||||
types::Value::exec_like(
|
||||
None,
|
||||
other.0.to_string().as_str(),
|
||||
self.0.to_string().as_str(),
|
||||
)
|
||||
}
|
||||
ast::LikeOperator::Match => todo!(),
|
||||
ast::LikeOperator::Regexp => todo!(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn unary_exec(&self, operator: ast::UnaryOperator) -> SimValue {
|
||||
let new_value = match operator {
|
||||
ast::UnaryOperator::BitwiseNot => self.0.exec_bit_not(),
|
||||
ast::UnaryOperator::Negative => {
|
||||
SimValue(types::Value::Integer(0))
|
||||
.binary_compare(self, ast::Operator::Subtract)
|
||||
.0
|
||||
}
|
||||
ast::UnaryOperator::Not => self.0.exec_boolean_not(),
|
||||
ast::UnaryOperator::Positive => self.0.clone(),
|
||||
};
|
||||
Self(new_value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ast::Literal> for SimValue {
|
||||
fn from(value: ast::Literal) -> Self {
|
||||
Self::from(&value)
|
||||
}
|
||||
}
|
||||
|
||||
/// Converts a SQL string literal with already-escaped single quotes to a regular string by:
|
||||
/// - Removing the enclosing single quotes
|
||||
/// - Converting sequences of 2N single quotes ('''''') to N single quotes (''')
|
||||
///
|
||||
/// Assumes:
|
||||
/// - The input starts and ends with a single quote
|
||||
/// - The input contains a valid amount of single quotes inside the enclosing quotes;
|
||||
/// i.e. any ' is escaped as a double ''
|
||||
fn unescape_singlequotes(input: &str) -> String {
|
||||
assert!(
|
||||
input.starts_with('\'') && input.ends_with('\''),
|
||||
"Input string must be wrapped in single quotes"
|
||||
);
|
||||
// Skip first and last characters (the enclosing quotes)
|
||||
let inner = &input[1..input.len() - 1];
|
||||
|
||||
let mut result = String::with_capacity(inner.len());
|
||||
let mut chars = inner.chars().peekable();
|
||||
|
||||
while let Some(c) = chars.next() {
|
||||
if c == '\'' {
|
||||
// Count consecutive single quotes
|
||||
let mut quote_count = 1;
|
||||
while chars.peek() == Some(&'\'') {
|
||||
quote_count += 1;
|
||||
chars.next();
|
||||
}
|
||||
assert!(
|
||||
quote_count % 2 == 0,
|
||||
"Expected even number of quotes, got {quote_count} in string {input}"
|
||||
);
|
||||
// For every pair of quotes, output one quote
|
||||
for _ in 0..(quote_count / 2) {
|
||||
result.push('\'');
|
||||
}
|
||||
} else {
|
||||
result.push(c);
|
||||
}
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
/// Escapes a string by doubling contained single quotes and then wrapping it in single quotes.
|
||||
fn escape_singlequotes(input: &str) -> String {
|
||||
let mut result = String::with_capacity(input.len() + 2);
|
||||
result.push('\'');
|
||||
result.push_str(&input.replace("'", "''"));
|
||||
result.push('\'');
|
||||
result
|
||||
}
|
||||
|
||||
impl From<&ast::Literal> for SimValue {
|
||||
fn from(value: &ast::Literal) -> Self {
|
||||
let new_value = match value {
|
||||
ast::Literal::Null => types::Value::Null,
|
||||
ast::Literal::Numeric(number) => Numeric::from(number).into(),
|
||||
ast::Literal::String(string) => types::Value::build_text(unescape_singlequotes(string)),
|
||||
ast::Literal::Blob(blob) => types::Value::Blob(
|
||||
blob.as_bytes()
|
||||
.chunks_exact(2)
|
||||
.map(|pair| {
|
||||
// We assume that sqlite3-parser has already validated that
|
||||
// the input is valid hex string, thus unwrap is safe.
|
||||
let hex_byte = std::str::from_utf8(pair).unwrap();
|
||||
u8::from_str_radix(hex_byte, 16).unwrap()
|
||||
})
|
||||
.collect(),
|
||||
),
|
||||
ast::Literal::Keyword(keyword) => match keyword.to_uppercase().as_str() {
|
||||
"TRUE" => types::Value::Integer(1),
|
||||
"FALSE" => types::Value::Integer(0),
|
||||
"NULL" => types::Value::Null,
|
||||
_ => unimplemented!("Unsupported keyword literal: {}", keyword),
|
||||
},
|
||||
lit => unimplemented!("{:?}", lit),
|
||||
};
|
||||
Self(new_value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<SimValue> for ast::Literal {
|
||||
fn from(value: SimValue) -> Self {
|
||||
Self::from(&value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&SimValue> for ast::Literal {
|
||||
fn from(value: &SimValue) -> Self {
|
||||
match &value.0 {
|
||||
types::Value::Null => Self::Null,
|
||||
types::Value::Integer(i) => Self::Numeric(i.to_string()),
|
||||
types::Value::Float(f) => Self::Numeric(f.to_string()),
|
||||
text @ types::Value::Text(..) => Self::String(escape_singlequotes(&text.to_string())),
|
||||
types::Value::Blob(blob) => Self::Blob(hex::encode(blob)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<bool> for SimValue {
|
||||
fn from(value: bool) -> Self {
|
||||
if value {
|
||||
SimValue::TRUE
|
||||
} else {
|
||||
SimValue::FALSE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<SimValue> for turso_core::types::Value {
|
||||
fn from(value: SimValue) -> Self {
|
||||
value.0
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&SimValue> for turso_core::types::Value {
|
||||
fn from(value: &SimValue) -> Self {
|
||||
value.0.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<turso_core::types::Value> for SimValue {
|
||||
fn from(value: turso_core::types::Value) -> Self {
|
||||
Self(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&turso_core::types::Value> for SimValue {
|
||||
fn from(value: &turso_core::types::Value) -> Self {
|
||||
Self(value.clone())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::model::table::{escape_singlequotes, unescape_singlequotes};
|
||||
|
||||
#[test]
|
||||
fn test_unescape_singlequotes() {
|
||||
assert_eq!(unescape_singlequotes("'hello'"), "hello");
|
||||
assert_eq!(unescape_singlequotes("'O''Reilly'"), "O'Reilly");
|
||||
assert_eq!(
|
||||
unescape_singlequotes("'multiple''single''quotes'"),
|
||||
"multiple'single'quotes"
|
||||
);
|
||||
assert_eq!(unescape_singlequotes("'test''''test'"), "test''test");
|
||||
assert_eq!(unescape_singlequotes("'many''''''quotes'"), "many'''quotes");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_escape_singlequotes() {
|
||||
assert_eq!(escape_singlequotes("hello"), "'hello'");
|
||||
assert_eq!(escape_singlequotes("O'Reilly"), "'O''Reilly'");
|
||||
assert_eq!(
|
||||
escape_singlequotes("multiple'single'quotes"),
|
||||
"'multiple''single''quotes'"
|
||||
);
|
||||
assert_eq!(escape_singlequotes("test''test"), "'test''''test'");
|
||||
assert_eq!(escape_singlequotes("many'''quotes"), "'many''''''quotes'");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user