mirror of
https://github.com/aljazceru/turso.git
synced 2026-01-08 18:54:21 +01:00
Merge 'Clean up AST unparsing, remove ToSqlString' from Levy A.
Enables formatting `Expr::Column` by adding the context to `ToTokens` instead of creating a new unparsing implementation for each node. `ToTokens` implemented for: - [x] `UpdatePlan` - [x] `Plan` - [x] `JoinedTable` - [x] `SelectPlan` - [x] `DeletePlan` Reviewed-by: Pedro Muniz (@pedrocarlo) Closes #1949
This commit is contained in:
@@ -2,14 +2,20 @@ use core::fmt;
|
||||
use std::fmt::{Display, Formatter};
|
||||
|
||||
use turso_sqlite3_parser::{
|
||||
ast::{SortOrder, TableInternalId},
|
||||
to_sql_string::{ToSqlContext, ToSqlString},
|
||||
ast::{
|
||||
self,
|
||||
fmt::{ToTokens, TokenStream},
|
||||
SortOrder, TableInternalId,
|
||||
},
|
||||
dialect::TokenType,
|
||||
to_sql_string::ToSqlContext,
|
||||
};
|
||||
|
||||
use crate::{schema::Table, translate::plan::TableReferences};
|
||||
|
||||
use super::plan::{
|
||||
Aggregate, DeletePlan, JoinedTable, Operation, Plan, Search, SelectPlan, UpdatePlan,
|
||||
Aggregate, DeletePlan, JoinedTable, Operation, Plan, ResultSetColumn, Search, SelectPlan,
|
||||
UpdatePlan,
|
||||
};
|
||||
|
||||
impl Display for Aggregate {
|
||||
@@ -262,11 +268,16 @@ impl ToSqlContext for PlanContext<'_> {
|
||||
}
|
||||
}
|
||||
|
||||
impl ToSqlString for Plan {
|
||||
fn to_sql_string<C: ToSqlContext>(&self, context: &C) -> String {
|
||||
// Make the Plans pass their own context
|
||||
impl ToTokens for Plan {
|
||||
fn to_tokens_with_context<S: TokenStream + ?Sized, C: ToSqlContext>(
|
||||
&self,
|
||||
s: &mut S,
|
||||
context: &C,
|
||||
) -> Result<(), S::Error> {
|
||||
match self {
|
||||
Self::Select(select) => select.to_sql_string(&PlanContext(&[&select.table_references])),
|
||||
Self::Select(select) => {
|
||||
select.to_tokens_with_context(s, &PlanContext(&[&select.table_references]))?;
|
||||
}
|
||||
Self::CompoundSelect {
|
||||
left,
|
||||
right_most,
|
||||
@@ -281,186 +292,188 @@ impl ToSqlString for Plan {
|
||||
.collect::<Vec<_>>();
|
||||
let context = &PlanContext(all_refs.as_slice());
|
||||
|
||||
let mut ret = Vec::new();
|
||||
for (plan, operator) in left {
|
||||
ret.push(format!("{} {}", plan.to_sql_string(context), operator));
|
||||
plan.to_tokens_with_context(s, context)?;
|
||||
operator.to_tokens_with_context(s, context)?;
|
||||
}
|
||||
ret.push(right_most.to_sql_string(context));
|
||||
if let Some(order_by) = &order_by {
|
||||
ret.push(format!(
|
||||
"ORDER BY {}",
|
||||
order_by
|
||||
.iter()
|
||||
.map(|(expr, order)| format!(
|
||||
"{} {}",
|
||||
expr.to_sql_string(context),
|
||||
order
|
||||
))
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ")
|
||||
));
|
||||
|
||||
right_most.to_tokens_with_context(s, context)?;
|
||||
|
||||
if let Some(order_by) = order_by {
|
||||
s.append(TokenType::TK_ORDER, None)?;
|
||||
s.append(TokenType::TK_BY, None)?;
|
||||
|
||||
s.comma(
|
||||
order_by.iter().map(|(expr, order)| ast::SortedColumn {
|
||||
expr: expr.clone(),
|
||||
order: Some(*order),
|
||||
nulls: None,
|
||||
}),
|
||||
context,
|
||||
)?;
|
||||
}
|
||||
|
||||
if let Some(limit) = &limit {
|
||||
ret.push(format!("LIMIT {limit}"));
|
||||
s.append(TokenType::TK_LIMIT, None)?;
|
||||
s.append(TokenType::TK_FLOAT, Some(&limit.to_string()))?;
|
||||
}
|
||||
|
||||
if let Some(offset) = &offset {
|
||||
ret.push(format!("OFFSET {offset}"));
|
||||
s.append(TokenType::TK_OFFSET, None)?;
|
||||
s.append(TokenType::TK_FLOAT, Some(&offset.to_string()))?;
|
||||
}
|
||||
ret.join(" ")
|
||||
}
|
||||
Self::Delete(delete) => delete.to_sql_string(context),
|
||||
Self::Update(update) => update.to_sql_string(context),
|
||||
Self::Delete(delete) => delete.to_tokens_with_context(s, context)?,
|
||||
Self::Update(update) => update.to_tokens_with_context(s, context)?,
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl ToSqlString for JoinedTable {
|
||||
fn to_sql_string<C: turso_sqlite3_parser::to_sql_string::ToSqlContext>(
|
||||
impl ToTokens for JoinedTable {
|
||||
fn to_tokens_with_context<S: TokenStream + ?Sized, C: ToSqlContext>(
|
||||
&self,
|
||||
s: &mut S,
|
||||
_context: &C,
|
||||
) -> String {
|
||||
let table_or_subquery =
|
||||
match &self.table {
|
||||
Table::BTree(..) | Table::Virtual(..) => self.table.get_name().to_string(),
|
||||
Table::FromClauseSubquery(from_clause_subquery) => {
|
||||
// Could possibly merge the contexts together here
|
||||
format!(
|
||||
"({})",
|
||||
from_clause_subquery.plan.to_sql_string(&PlanContext(&[
|
||||
&from_clause_subquery.plan.table_references
|
||||
]))
|
||||
)
|
||||
) -> Result<(), S::Error> {
|
||||
match &self.table {
|
||||
Table::BTree(..) | Table::Virtual(..) => {
|
||||
let name = self.table.get_name();
|
||||
s.append(TokenType::TK_ID, Some(name))?;
|
||||
if self.identifier != name {
|
||||
s.append(TokenType::TK_AS, None)?;
|
||||
s.append(TokenType::TK_ID, Some(&self.identifier))?;
|
||||
}
|
||||
};
|
||||
// JOIN is done at a higher level
|
||||
format!(
|
||||
"{}{}",
|
||||
table_or_subquery,
|
||||
if self.identifier != table_or_subquery {
|
||||
format!(" AS {}", self.identifier)
|
||||
} else {
|
||||
"".to_string()
|
||||
}
|
||||
)
|
||||
Table::FromClauseSubquery(from_clause_subquery) => {
|
||||
s.append(TokenType::TK_LP, None)?;
|
||||
// Could possibly merge the contexts together here
|
||||
from_clause_subquery.plan.to_tokens_with_context(
|
||||
s,
|
||||
&PlanContext(&[&from_clause_subquery.plan.table_references]),
|
||||
)?;
|
||||
s.append(TokenType::TK_RP, None)?;
|
||||
|
||||
s.append(TokenType::TK_AS, None)?;
|
||||
s.append(TokenType::TK_ID, Some(&self.identifier))?;
|
||||
}
|
||||
};
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: currently cannot print the original CTE as it is optimized into a subquery
|
||||
impl ToSqlString for SelectPlan {
|
||||
fn to_sql_string<C: turso_sqlite3_parser::to_sql_string::ToSqlContext>(
|
||||
impl ToTokens for SelectPlan {
|
||||
fn to_tokens_with_context<S: TokenStream + ?Sized, C: ToSqlContext>(
|
||||
&self,
|
||||
s: &mut S,
|
||||
context: &C,
|
||||
) -> String {
|
||||
let mut ret = Vec::new();
|
||||
// VALUES SELECT statement
|
||||
) -> Result<(), S::Error> {
|
||||
if !self.values.is_empty() {
|
||||
ret.push(format!(
|
||||
"VALUES {}",
|
||||
self.values
|
||||
.iter()
|
||||
.map(|value| {
|
||||
let joined_value = value
|
||||
.iter()
|
||||
.map(|e| e.to_sql_string(context))
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ");
|
||||
format!("({joined_value})")
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ")
|
||||
));
|
||||
ast::OneSelect::Values(self.values.clone()).to_tokens_with_context(s, context)?;
|
||||
} else {
|
||||
// standard SELECT statement
|
||||
ret.push("SELECT".to_string());
|
||||
s.append(TokenType::TK_SELECT, None)?;
|
||||
if self.distinctness.is_distinct() {
|
||||
ret.push("DISTINCT".to_string());
|
||||
s.append(TokenType::TK_DISTINCT, None)?;
|
||||
}
|
||||
ret.push(
|
||||
self.result_columns
|
||||
.iter()
|
||||
.map(|cols| {
|
||||
format!(
|
||||
"{}{}",
|
||||
cols.expr.to_sql_string(context),
|
||||
cols.alias
|
||||
.as_ref()
|
||||
.map_or("".to_string(), |alias| format!(" AS {alias}"))
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.join(", "),
|
||||
);
|
||||
ret.push("FROM".to_string());
|
||||
|
||||
ret.extend(self.join_order.iter().enumerate().map(|(idx, order)| {
|
||||
let table_ref = self.joined_tables().get(order.original_idx).unwrap();
|
||||
if idx == 0 {
|
||||
table_ref.to_sql_string(context)
|
||||
} else {
|
||||
format!(
|
||||
"{}JOIN {}",
|
||||
if order.is_outer { "OUTER " } else { "" },
|
||||
table_ref.to_sql_string(context)
|
||||
)
|
||||
for (i, ResultSetColumn { expr, alias, .. }) in self.result_columns.iter().enumerate() {
|
||||
if i != 0 {
|
||||
s.append(TokenType::TK_COMMA, None)?;
|
||||
}
|
||||
|
||||
expr.to_tokens_with_context(s, context)?;
|
||||
if let Some(alias) = alias {
|
||||
s.append(TokenType::TK_AS, None)?;
|
||||
s.append(TokenType::TK_ID, Some(alias))?;
|
||||
}
|
||||
}));
|
||||
if !self.where_clause.is_empty() {
|
||||
ret.push("WHERE".to_string());
|
||||
ret.push(
|
||||
self.where_clause
|
||||
.iter()
|
||||
.map(|where_clause| where_clause.expr.to_sql_string(context))
|
||||
.collect::<Vec<_>>()
|
||||
.join(" AND "),
|
||||
);
|
||||
}
|
||||
s.append(TokenType::TK_FROM, None)?;
|
||||
|
||||
for (i, order) in self.join_order.iter().enumerate() {
|
||||
if i != 0 {
|
||||
if order.is_outer {
|
||||
s.append(TokenType::TK_ORDER, None)?;
|
||||
}
|
||||
s.append(TokenType::TK_JOIN, None)?;
|
||||
}
|
||||
|
||||
let table_ref = self.joined_tables().get(order.original_idx).unwrap();
|
||||
table_ref.to_tokens_with_context(s, context)?;
|
||||
}
|
||||
|
||||
if !self.where_clause.is_empty() {
|
||||
s.append(TokenType::TK_WHERE, None)?;
|
||||
|
||||
for (i, expr) in self
|
||||
.where_clause
|
||||
.iter()
|
||||
.map(|where_clause| where_clause.expr.clone())
|
||||
.enumerate()
|
||||
{
|
||||
if i != 0 {
|
||||
s.append(TokenType::TK_AND, None)?;
|
||||
}
|
||||
expr.to_tokens_with_context(s, context)?;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(group_by) = &self.group_by {
|
||||
// TODO: see later if group_by needs more context to parse the expressions
|
||||
// We will see this when this panics
|
||||
ret.push("GROUP BY".to_string());
|
||||
ret.push(
|
||||
group_by
|
||||
.exprs
|
||||
.iter()
|
||||
.map(|expr| expr.to_sql_string(context))
|
||||
.collect::<Vec<_>>()
|
||||
.join(", "),
|
||||
);
|
||||
s.append(TokenType::TK_GROUP, None)?;
|
||||
s.append(TokenType::TK_BY, None)?;
|
||||
|
||||
s.comma(group_by.exprs.iter(), context)?;
|
||||
|
||||
// TODO: not sure where I need to place the group_by.sort_order
|
||||
if let Some(having) = &group_by.having {
|
||||
ret.push("HAVING".to_string());
|
||||
ret.push(
|
||||
having
|
||||
.iter()
|
||||
.map(|expr| expr.to_sql_string(context))
|
||||
.collect::<Vec<_>>()
|
||||
.join(" AND "),
|
||||
);
|
||||
s.append(TokenType::TK_HAVING, None)?;
|
||||
|
||||
for (i, expr) in having.iter().enumerate() {
|
||||
if i != 0 {
|
||||
s.append(TokenType::TK_AND, None)?;
|
||||
}
|
||||
expr.to_tokens_with_context(s, context)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(order_by) = &self.order_by {
|
||||
ret.push(format!(
|
||||
"ORDER BY {}",
|
||||
order_by
|
||||
.iter()
|
||||
.map(|(expr, order)| format!("{} {}", expr.to_sql_string(context), order))
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ")
|
||||
));
|
||||
s.append(TokenType::TK_ORDER, None)?;
|
||||
s.append(TokenType::TK_BY, None)?;
|
||||
|
||||
s.comma(
|
||||
order_by.iter().map(|(expr, order)| ast::SortedColumn {
|
||||
expr: expr.clone(),
|
||||
order: Some(*order),
|
||||
nulls: None,
|
||||
}),
|
||||
context,
|
||||
)?;
|
||||
}
|
||||
|
||||
if let Some(limit) = &self.limit {
|
||||
ret.push(format!("LIMIT {limit}"));
|
||||
s.append(TokenType::TK_LIMIT, None)?;
|
||||
s.append(TokenType::TK_FLOAT, Some(&limit.to_string()))?;
|
||||
}
|
||||
|
||||
if let Some(offset) = &self.offset {
|
||||
ret.push(format!("OFFSET {offset}"));
|
||||
s.append(TokenType::TK_OFFSET, None)?;
|
||||
s.append(TokenType::TK_FLOAT, Some(&offset.to_string()))?;
|
||||
}
|
||||
ret.join(" ")
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl ToSqlString for DeletePlan {
|
||||
fn to_sql_string<C: ToSqlContext>(&self, _context: &C) -> String {
|
||||
impl ToTokens for DeletePlan {
|
||||
fn to_tokens_with_context<S: TokenStream + ?Sized, C: ToSqlContext>(
|
||||
&self,
|
||||
s: &mut S,
|
||||
_: &C,
|
||||
) -> Result<(), S::Error> {
|
||||
let table = self
|
||||
.table_references
|
||||
.joined_tables()
|
||||
@@ -468,42 +481,61 @@ impl ToSqlString for DeletePlan {
|
||||
.expect("Delete Plan should have only one table reference");
|
||||
let context = &[&self.table_references];
|
||||
let context = &PlanContext(context);
|
||||
let mut ret = Vec::new();
|
||||
|
||||
ret.push(format!("DELETE FROM {}", table.table.get_name()));
|
||||
s.append(TokenType::TK_DELETE, None)?;
|
||||
s.append(TokenType::TK_FROM, None)?;
|
||||
s.append(TokenType::TK_ID, Some(table.table.get_name()))?;
|
||||
|
||||
if !self.where_clause.is_empty() {
|
||||
ret.push("WHERE".to_string());
|
||||
ret.push(
|
||||
self.where_clause
|
||||
.iter()
|
||||
.map(|where_clause| where_clause.expr.to_sql_string(context))
|
||||
.collect::<Vec<_>>()
|
||||
.join(" AND "),
|
||||
);
|
||||
s.append(TokenType::TK_WHERE, None)?;
|
||||
|
||||
for (i, expr) in self
|
||||
.where_clause
|
||||
.iter()
|
||||
.map(|where_clause| where_clause.expr.clone())
|
||||
.enumerate()
|
||||
{
|
||||
if i != 0 {
|
||||
s.append(TokenType::TK_AND, None)?;
|
||||
}
|
||||
expr.to_tokens_with_context(s, context)?;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(order_by) = &self.order_by {
|
||||
ret.push(format!(
|
||||
"ORDER BY {}",
|
||||
order_by
|
||||
.iter()
|
||||
.map(|(expr, order)| format!("{} {}", expr.to_sql_string(context), order))
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ")
|
||||
));
|
||||
s.append(TokenType::TK_ORDER, None)?;
|
||||
s.append(TokenType::TK_BY, None)?;
|
||||
|
||||
s.comma(
|
||||
order_by.iter().map(|(expr, order)| ast::SortedColumn {
|
||||
expr: expr.clone(),
|
||||
order: Some(*order),
|
||||
nulls: None,
|
||||
}),
|
||||
context,
|
||||
)?;
|
||||
}
|
||||
|
||||
if let Some(limit) = &self.limit {
|
||||
ret.push(format!("LIMIT {limit}"));
|
||||
s.append(TokenType::TK_LIMIT, None)?;
|
||||
s.append(TokenType::TK_FLOAT, Some(&limit.to_string()))?;
|
||||
}
|
||||
|
||||
if let Some(offset) = &self.offset {
|
||||
ret.push(format!("OFFSET {offset}"));
|
||||
s.append(TokenType::TK_OFFSET, None)?;
|
||||
s.append(TokenType::TK_FLOAT, Some(&offset.to_string()))?;
|
||||
}
|
||||
ret.join(" ")
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl ToSqlString for UpdatePlan {
|
||||
fn to_sql_string<C: ToSqlContext>(&self, _context: &C) -> String {
|
||||
impl ToTokens for UpdatePlan {
|
||||
fn to_tokens_with_context<S: TokenStream + ?Sized, C: ToSqlContext>(
|
||||
&self,
|
||||
s: &mut S,
|
||||
_: &C,
|
||||
) -> Result<(), S::Error> {
|
||||
let table = self
|
||||
.table_references
|
||||
.joined_tables()
|
||||
@@ -511,60 +543,69 @@ impl ToSqlString for UpdatePlan {
|
||||
.expect("UPDATE Plan should have only one table reference");
|
||||
let context = [&self.table_references];
|
||||
let context = &PlanContext(&context);
|
||||
let mut ret = Vec::new();
|
||||
|
||||
// TODO: we don't work with conflict clauses yet
|
||||
s.append(TokenType::TK_UPDATE, None)?;
|
||||
s.append(TokenType::TK_ID, Some(table.table.get_name()))?;
|
||||
s.append(TokenType::TK_SET, None)?;
|
||||
|
||||
ret.push(format!("UPDATE {} SET", table.table.get_name()));
|
||||
s.comma(
|
||||
self.set_clauses.iter().map(|(col_idx, set_expr)| {
|
||||
let col_name = table
|
||||
.table
|
||||
.get_column_at(*col_idx)
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.name
|
||||
.as_ref()
|
||||
.unwrap();
|
||||
|
||||
// TODO: does not support column_name_list yet
|
||||
ret.push(
|
||||
self.set_clauses
|
||||
.iter()
|
||||
.map(|(col_idx, set_expr)| {
|
||||
format!(
|
||||
"{} = {}",
|
||||
table
|
||||
.table
|
||||
.get_column_at(*col_idx)
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.name
|
||||
.as_ref()
|
||||
.unwrap(),
|
||||
set_expr.to_sql_string(context)
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.join(", "),
|
||||
);
|
||||
ast::Set {
|
||||
col_names: ast::DistinctNames::single(ast::Name(col_name.clone())),
|
||||
expr: set_expr.clone(),
|
||||
}
|
||||
}),
|
||||
context,
|
||||
)?;
|
||||
|
||||
if !self.where_clause.is_empty() {
|
||||
ret.push("WHERE".to_string());
|
||||
ret.push(
|
||||
self.where_clause
|
||||
.iter()
|
||||
.map(|where_clause| where_clause.expr.to_sql_string(context))
|
||||
.collect::<Vec<_>>()
|
||||
.join(" AND "),
|
||||
);
|
||||
s.append(TokenType::TK_WHERE, None)?;
|
||||
|
||||
let mut iter = self
|
||||
.where_clause
|
||||
.iter()
|
||||
.map(|where_clause| where_clause.expr.clone());
|
||||
iter.next()
|
||||
.expect("should not be empty")
|
||||
.to_tokens_with_context(s, context)?;
|
||||
for expr in iter {
|
||||
s.append(TokenType::TK_AND, None)?;
|
||||
expr.to_tokens_with_context(s, context)?;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(order_by) = &self.order_by {
|
||||
ret.push(format!(
|
||||
"ORDER BY {}",
|
||||
order_by
|
||||
.iter()
|
||||
.map(|(expr, order)| format!("{} {}", expr.to_sql_string(context), order))
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ")
|
||||
));
|
||||
s.append(TokenType::TK_ORDER, None)?;
|
||||
s.append(TokenType::TK_BY, None)?;
|
||||
|
||||
s.comma(
|
||||
order_by.iter().map(|(expr, order)| ast::SortedColumn {
|
||||
expr: expr.clone(),
|
||||
order: Some(*order),
|
||||
nulls: None,
|
||||
}),
|
||||
context,
|
||||
)?;
|
||||
}
|
||||
|
||||
if let Some(limit) = &self.limit {
|
||||
ret.push(format!("LIMIT {limit}"));
|
||||
s.append(TokenType::TK_LIMIT, None)?;
|
||||
s.append(TokenType::TK_FLOAT, Some(&limit.to_string()))?;
|
||||
}
|
||||
if let Some(offset) = &self.offset {
|
||||
ret.push(format!("OFFSET {offset}"));
|
||||
s.append(TokenType::TK_OFFSET, None)?;
|
||||
s.append(TokenType::TK_FLOAT, Some(&offset.to_string()))?;
|
||||
}
|
||||
ret.join(" ")
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,10 +7,7 @@ use cost::Cost;
|
||||
use join::{compute_best_join_order, BestJoinOrderResult};
|
||||
use lift_common_subexpressions::lift_common_subexpressions_from_binary_or_terms;
|
||||
use order::{compute_order_target, plan_satisfies_order_target, EliminatesSortBy};
|
||||
use turso_sqlite3_parser::{
|
||||
ast::{self, Expr, SortOrder},
|
||||
to_sql_string::ToSqlString as _,
|
||||
};
|
||||
use turso_sqlite3_parser::ast::{self, fmt::ToTokens as _, Expr, SortOrder};
|
||||
|
||||
use crate::{
|
||||
parameters::PARAM_PREFIX,
|
||||
@@ -51,7 +48,11 @@ pub fn optimize_plan(plan: &mut Plan, schema: &Schema) -> Result<()> {
|
||||
}
|
||||
}
|
||||
// When debug tracing is enabled, print the optimized plan as a SQL string for debugging
|
||||
tracing::debug!(plan_sql = plan.to_sql_string(&crate::translate::display::PlanContext(&[])));
|
||||
tracing::debug!(
|
||||
plan_sql = plan
|
||||
.format_with_context(&crate::translate::display::PlanContext(&[]))
|
||||
.unwrap()
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
@@ -1,12 +1,9 @@
|
||||
use std::fmt::Display;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use turso_sqlite3_parser::{ast, to_sql_string::ToSqlString};
|
||||
use turso_sqlite3_parser::ast::{self, fmt::ToTokens};
|
||||
|
||||
use crate::model::{
|
||||
query::EmptyContext,
|
||||
table::{SimValue, Table},
|
||||
};
|
||||
use crate::model::table::{SimValue, Table};
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
pub struct Predicate(pub ast::Expr);
|
||||
@@ -138,6 +135,6 @@ pub fn expr_to_value(expr: &ast::Expr, row: &[SimValue], table: &Table) -> Optio
|
||||
|
||||
impl Display for Predicate {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{}", self.0.to_sql_string(&EmptyContext))
|
||||
self.0.to_fmt(f)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ use anyhow::Context;
|
||||
pub use ast::Distinctness;
|
||||
use itertools::Itertools;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use turso_sqlite3_parser::{ast, to_sql_string::ToSqlString};
|
||||
use turso_sqlite3_parser::ast::{self, fmt::ToTokens};
|
||||
|
||||
use crate::{
|
||||
generation::Shadow,
|
||||
@@ -383,7 +383,7 @@ impl Shadow for SelectInner {
|
||||
} else {
|
||||
return Err(anyhow::anyhow!(
|
||||
"Failed to evaluate expression in free select ({})",
|
||||
expr.0.to_sql_string(&EmptyContext {})
|
||||
expr.0.format_with_context(&EmptyContext {}).unwrap()
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -508,7 +508,7 @@ impl Select {
|
||||
}
|
||||
impl Display for Select {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{}", self.to_sql_ast().to_sql_string(&EmptyContext {}))
|
||||
self.to_sql_ast().to_fmt_with_context(f, &EmptyContext {})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -222,7 +222,7 @@ impl TokenType {
|
||||
TK_LE => Some("<="),
|
||||
TK_LT => Some("<"),
|
||||
TK_MINUS => Some("-"),
|
||||
TK_NE => Some("<>"), // or !=
|
||||
TK_NE => Some("!="), // or <>
|
||||
TK_PLUS => Some("+"),
|
||||
TK_REM => Some("%"),
|
||||
TK_RP => Some(")"),
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -14,7 +14,7 @@ use indexmap::{IndexMap, IndexSet};
|
||||
|
||||
use crate::custom_err;
|
||||
use crate::dialect::TokenType::{self, *};
|
||||
use crate::dialect::{from_token, is_identifier, Token};
|
||||
use crate::dialect::{from_token, Token};
|
||||
use crate::parser::{parse::YYCODETYPE, ParserError};
|
||||
|
||||
/// `?` or `$` Prepared statement arg placeholder(s)
|
||||
|
||||
@@ -1,428 +1,2 @@
|
||||
use std::fmt::Display;
|
||||
|
||||
use crate::ast::{self, fmt::ToTokens, Expr};
|
||||
|
||||
use super::ToSqlString;
|
||||
|
||||
impl ToSqlString for Expr {
|
||||
fn to_sql_string<C: super::ToSqlContext>(&self, context: &C) -> String {
|
||||
let mut ret = String::new();
|
||||
match self {
|
||||
Expr::Between {
|
||||
lhs,
|
||||
not,
|
||||
start,
|
||||
end,
|
||||
} => {
|
||||
ret.push_str(&lhs.to_sql_string(context));
|
||||
ret.push(' ');
|
||||
|
||||
if *not {
|
||||
ret.push_str("NOT ");
|
||||
}
|
||||
|
||||
ret.push_str("BETWEEN ");
|
||||
|
||||
ret.push_str(&start.to_sql_string(context));
|
||||
|
||||
ret.push_str(" AND ");
|
||||
|
||||
ret.push_str(&end.to_sql_string(context));
|
||||
}
|
||||
Expr::Binary(lhs, op, rhs) => {
|
||||
ret.push_str(&lhs.to_sql_string(context));
|
||||
ret.push(' ');
|
||||
ret.push_str(&op.to_string());
|
||||
ret.push(' ');
|
||||
ret.push_str(&rhs.to_sql_string(context));
|
||||
}
|
||||
Expr::Case {
|
||||
base,
|
||||
when_then_pairs,
|
||||
else_expr,
|
||||
} => {
|
||||
ret.push_str("CASE ");
|
||||
if let Some(base) = base {
|
||||
ret.push_str(&base.to_sql_string(context));
|
||||
ret.push(' ');
|
||||
}
|
||||
for (when, then) in when_then_pairs {
|
||||
ret.push_str("WHEN ");
|
||||
ret.push_str(&when.to_sql_string(context));
|
||||
ret.push_str(" THEN ");
|
||||
ret.push_str(&then.to_sql_string(context));
|
||||
}
|
||||
if let Some(else_expr) = else_expr {
|
||||
ret.push_str(" ELSE ");
|
||||
ret.push_str(&else_expr.to_sql_string(context));
|
||||
}
|
||||
ret.push_str(" END");
|
||||
}
|
||||
Expr::Cast { expr, type_name } => {
|
||||
ret.push_str("CAST");
|
||||
ret.push('(');
|
||||
ret.push_str(&expr.to_sql_string(context));
|
||||
if let Some(type_name) = type_name {
|
||||
ret.push_str(" AS ");
|
||||
ret.push_str(&type_name.to_sql_string(context));
|
||||
}
|
||||
ret.push(')');
|
||||
}
|
||||
Expr::Collate(expr, name) => {
|
||||
ret.push_str(&expr.to_sql_string(context));
|
||||
ret.push_str(" COLLATE ");
|
||||
ret.push_str(name);
|
||||
}
|
||||
Expr::DoublyQualified(name, name1, name2) => {
|
||||
ret.push_str(&name.0);
|
||||
ret.push('.');
|
||||
ret.push_str(&name1.0);
|
||||
ret.push('.');
|
||||
ret.push_str(&name2.0);
|
||||
}
|
||||
Expr::Exists(select) => {
|
||||
ret.push_str("EXISTS (");
|
||||
ret.push_str(&select.to_sql_string(context));
|
||||
ret.push(')');
|
||||
}
|
||||
Expr::FunctionCall {
|
||||
name,
|
||||
distinctness: _,
|
||||
args,
|
||||
order_by: _,
|
||||
filter_over,
|
||||
} => {
|
||||
ret.push_str(&name.0);
|
||||
// TODO: pretty sure there should be no ORDER_BY nor DISTINCT
|
||||
ret.push('(');
|
||||
if let Some(args) = args {
|
||||
let joined_args = args
|
||||
.iter()
|
||||
.map(|arg| arg.to_sql_string(context))
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ");
|
||||
ret.push_str(&joined_args);
|
||||
}
|
||||
ret.push(')');
|
||||
if let Some(filter_over) = filter_over {
|
||||
if let Some(filter) = &filter_over.filter_clause {
|
||||
ret.push_str(&format!(
|
||||
" FILTER (WHERE {})",
|
||||
filter.to_sql_string(context)
|
||||
));
|
||||
}
|
||||
if let Some(over) = &filter_over.over_clause {
|
||||
ret.push(' ');
|
||||
ret.push_str(&over.to_sql_string(context));
|
||||
}
|
||||
}
|
||||
}
|
||||
Expr::FunctionCallStar { name, filter_over } => {
|
||||
ret.push_str(&name.0);
|
||||
ret.push_str("(*)");
|
||||
if let Some(filter_over) = filter_over {
|
||||
if let Some(filter) = &filter_over.filter_clause {
|
||||
ret.push_str(&format!(
|
||||
" FILTER (WHERE {})",
|
||||
filter.to_sql_string(context)
|
||||
));
|
||||
}
|
||||
if let Some(over) = &filter_over.over_clause {
|
||||
ret.push(' ');
|
||||
ret.push_str(&over.to_sql_string(context));
|
||||
}
|
||||
}
|
||||
}
|
||||
Expr::Id(id) => {
|
||||
ret.push_str(&id.0);
|
||||
}
|
||||
Expr::Column {
|
||||
database: _, // TODO: Ignore database for now
|
||||
table,
|
||||
column,
|
||||
is_rowid_alias: _,
|
||||
} => {
|
||||
ret.push_str(context.get_table_name(*table));
|
||||
ret.push('.');
|
||||
ret.push_str(context.get_column_name(*table, *column));
|
||||
}
|
||||
Expr::RowId { database: _, table } => {
|
||||
ret.push_str(&format!("{}.rowid", context.get_table_name(*table)))
|
||||
}
|
||||
Expr::InList { lhs, not, rhs } => {
|
||||
ret.push_str(&format!(
|
||||
"{} {}IN ({})",
|
||||
lhs.to_sql_string(context),
|
||||
if *not { "NOT " } else { "" },
|
||||
if let Some(rhs) = rhs {
|
||||
rhs.iter()
|
||||
.map(|expr| expr.to_sql_string(context))
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ")
|
||||
} else {
|
||||
"".to_string()
|
||||
}
|
||||
));
|
||||
}
|
||||
Expr::InSelect { lhs, not, rhs } => {
|
||||
ret.push_str(&format!(
|
||||
"{} {}IN ({})",
|
||||
lhs.to_sql_string(context),
|
||||
if *not { "NOT " } else { "" },
|
||||
rhs.to_sql_string(context)
|
||||
));
|
||||
}
|
||||
Expr::InTable {
|
||||
lhs,
|
||||
not,
|
||||
rhs,
|
||||
args,
|
||||
} => {
|
||||
ret.push_str(&lhs.to_sql_string(context));
|
||||
ret.push(' ');
|
||||
if *not {
|
||||
ret.push_str("NOT ");
|
||||
}
|
||||
ret.push_str(&rhs.to_sql_string(context));
|
||||
|
||||
if let Some(args) = args {
|
||||
ret.push('(');
|
||||
let joined_args = args
|
||||
.iter()
|
||||
.map(|expr| expr.to_sql_string(context))
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ");
|
||||
ret.push_str(&joined_args);
|
||||
ret.push(')');
|
||||
}
|
||||
}
|
||||
Expr::IsNull(expr) => {
|
||||
ret.push_str(&expr.to_sql_string(context));
|
||||
ret.push_str(" ISNULL");
|
||||
}
|
||||
Expr::Like {
|
||||
lhs,
|
||||
not,
|
||||
op,
|
||||
rhs,
|
||||
escape,
|
||||
} => {
|
||||
ret.push_str(&lhs.to_sql_string(context));
|
||||
ret.push(' ');
|
||||
if *not {
|
||||
ret.push_str("NOT ");
|
||||
}
|
||||
ret.push_str(&op.to_string());
|
||||
ret.push(' ');
|
||||
ret.push_str(&rhs.to_sql_string(context));
|
||||
if let Some(escape) = escape {
|
||||
ret.push_str(" ESCAPE ");
|
||||
ret.push_str(&escape.to_sql_string(context));
|
||||
}
|
||||
}
|
||||
Expr::Literal(literal) => {
|
||||
ret.push_str(&literal.to_string());
|
||||
}
|
||||
Expr::Name(name) => {
|
||||
ret.push_str(&name.0);
|
||||
}
|
||||
Expr::NotNull(expr) => {
|
||||
ret.push_str(&expr.to_sql_string(context));
|
||||
ret.push_str(" NOT NULL");
|
||||
}
|
||||
Expr::Parenthesized(exprs) => {
|
||||
ret.push('(');
|
||||
let joined_args = exprs
|
||||
.iter()
|
||||
.map(|expr| expr.to_sql_string(context))
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ");
|
||||
ret.push_str(&joined_args);
|
||||
ret.push(')');
|
||||
}
|
||||
Expr::Qualified(name, name1) => {
|
||||
ret.push_str(&name.0);
|
||||
ret.push('.');
|
||||
ret.push_str(&name1.0);
|
||||
}
|
||||
Expr::Raise(resolve_type, expr) => {
|
||||
ret.push_str("RAISE(");
|
||||
ret.push_str(&resolve_type.to_string());
|
||||
if let Some(expr) = expr {
|
||||
ret.push_str(", ");
|
||||
ret.push_str(&expr.to_sql_string(context));
|
||||
}
|
||||
ret.push(')');
|
||||
}
|
||||
Expr::Subquery(select) => {
|
||||
ret.push('(');
|
||||
ret.push_str(&select.to_sql_string(context));
|
||||
ret.push(')');
|
||||
}
|
||||
Expr::Unary(unary_operator, expr) => {
|
||||
ret.push_str(&unary_operator.to_string());
|
||||
ret.push(' ');
|
||||
ret.push_str(&expr.to_sql_string(context));
|
||||
}
|
||||
Expr::Variable(variable) => {
|
||||
ret.push_str(variable);
|
||||
}
|
||||
};
|
||||
ret
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for ast::Operator {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
let value = match self {
|
||||
Self::Add => "+",
|
||||
Self::And => "AND",
|
||||
Self::ArrowRight => "->",
|
||||
Self::ArrowRightShift => "->>",
|
||||
Self::BitwiseAnd => "&",
|
||||
Self::BitwiseNot => "~",
|
||||
Self::BitwiseOr => "|",
|
||||
Self::Concat => "||",
|
||||
Self::Divide => "/",
|
||||
Self::Equals => "=",
|
||||
Self::Greater => ">",
|
||||
Self::GreaterEquals => ">=",
|
||||
Self::Is => "IS",
|
||||
Self::IsNot => "IS NOT",
|
||||
Self::LeftShift => "<<",
|
||||
Self::Less => "<",
|
||||
Self::LessEquals => "<=",
|
||||
Self::Modulus => "%",
|
||||
Self::Multiply => "*",
|
||||
Self::NotEquals => "!=",
|
||||
Self::Or => "OR",
|
||||
Self::RightShift => ">>",
|
||||
Self::Subtract => "-",
|
||||
};
|
||||
write!(f, "{value}")
|
||||
}
|
||||
}
|
||||
|
||||
impl ToSqlString for ast::Type {
|
||||
fn to_sql_string<C: super::ToSqlContext>(&self, context: &C) -> String {
|
||||
let mut ret = self.name.clone();
|
||||
if let Some(size) = &self.size {
|
||||
ret.push(' ');
|
||||
ret.push('(');
|
||||
ret.push_str(&size.to_sql_string(context));
|
||||
ret.push(')');
|
||||
}
|
||||
ret
|
||||
}
|
||||
}
|
||||
|
||||
impl ToSqlString for ast::TypeSize {
|
||||
fn to_sql_string<C: super::ToSqlContext>(&self, context: &C) -> String {
|
||||
let mut ret = String::new();
|
||||
match self {
|
||||
Self::MaxSize(e) => {
|
||||
ret.push_str(&e.to_sql_string(context));
|
||||
}
|
||||
Self::TypeSize(lhs, rhs) => {
|
||||
ret.push_str(&lhs.to_sql_string(context));
|
||||
ret.push_str(", ");
|
||||
ret.push_str(&rhs.to_sql_string(context));
|
||||
}
|
||||
};
|
||||
ret
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for ast::Distinctness {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"{}",
|
||||
match self {
|
||||
Self::All => "ALL",
|
||||
Self::Distinct => "DISTINCT",
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Can't impl Display here as it is already implemented for it
|
||||
impl ToSqlString for ast::QualifiedName {
|
||||
fn to_sql_string<C: super::ToSqlContext>(&self, _context: &C) -> String {
|
||||
let mut ret = String::new();
|
||||
if let Some(db_name) = &self.db_name {
|
||||
ret.push_str(&db_name.0);
|
||||
ret.push('.');
|
||||
}
|
||||
if let Some(alias) = &self.alias {
|
||||
ret.push_str(&alias.0);
|
||||
ret.push('.');
|
||||
}
|
||||
ret.push_str(&self.name.0);
|
||||
ret
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for ast::LikeOperator {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
self.to_fmt(f)
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for ast::Literal {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"{}",
|
||||
match self {
|
||||
Self::Blob(b) => format!("x'{b}'"),
|
||||
Self::CurrentDate => "CURRENT_DATE".to_string(),
|
||||
Self::CurrentTime => "CURRENT_TIME".to_string(),
|
||||
Self::CurrentTimestamp => "CURRENT_TIMESTAMP".to_string(),
|
||||
Self::Keyword(keyword) => keyword.clone(),
|
||||
Self::Null => "NULL".to_string(),
|
||||
Self::Numeric(num) => num.clone(),
|
||||
Self::String(s) => s.clone(),
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for ast::ResolveType {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
self.to_fmt(f)
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for ast::UnaryOperator {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"{}",
|
||||
match self {
|
||||
Self::BitwiseNot => "~",
|
||||
Self::Negative => "-",
|
||||
Self::Not => "NOT",
|
||||
Self::Positive => "+",
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl ToSqlString for ast::Over {
|
||||
fn to_sql_string<C: super::ToSqlContext>(&self, context: &C) -> String {
|
||||
let mut ret = vec!["OVER".to_string()];
|
||||
match self {
|
||||
Self::Name(name) => {
|
||||
ret.push(name.0.clone());
|
||||
}
|
||||
Self::Window(window) => {
|
||||
ret.push(window.to_sql_string(context));
|
||||
}
|
||||
}
|
||||
ret.join(" ")
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {}
|
||||
|
||||
@@ -15,18 +15,6 @@ pub trait ToSqlContext {
|
||||
fn get_column_name(&self, table_id: TableInternalId, col_idx: usize) -> &str;
|
||||
}
|
||||
|
||||
/// Trait to convert an ast to a string
|
||||
pub trait ToSqlString {
|
||||
/// Convert the given value to String
|
||||
fn to_sql_string<C: ToSqlContext>(&self, context: &C) -> String;
|
||||
}
|
||||
|
||||
impl<T: ToSqlString> ToSqlString for Box<T> {
|
||||
fn to_sql_string<C: ToSqlContext>(&self, context: &C) -> String {
|
||||
T::to_sql_string(self, context)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::ToSqlContext;
|
||||
|
||||
@@ -1,277 +1,74 @@
|
||||
use std::fmt::Display;
|
||||
|
||||
use crate::{ast, to_sql_string::ToSqlString};
|
||||
|
||||
impl ToSqlString for ast::AlterTableBody {
|
||||
fn to_sql_string<C: crate::to_sql_string::ToSqlContext>(&self, context: &C) -> String {
|
||||
match self {
|
||||
Self::AddColumn(col_def) => format!("ADD COLUMN {}", col_def.to_sql_string(context)),
|
||||
Self::DropColumn(name) => format!("DROP COLUMN {}", name.0),
|
||||
Self::RenameColumn { old, new } => format!("RENAME COLUMN {} TO {}", old.0, new.0),
|
||||
Self::RenameTo(name) => format!("RENAME TO {}", name.0),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ToSqlString for ast::ColumnDefinition {
|
||||
fn to_sql_string<C: crate::to_sql_string::ToSqlContext>(&self, context: &C) -> String {
|
||||
format!(
|
||||
"{}{}{}",
|
||||
self.col_name.0,
|
||||
if let Some(col_type) = &self.col_type {
|
||||
format!(" {}", col_type.to_sql_string(context))
|
||||
} else {
|
||||
"".to_string()
|
||||
},
|
||||
if !self.constraints.is_empty() {
|
||||
format!(
|
||||
" {}",
|
||||
self.constraints
|
||||
.iter()
|
||||
.map(|constraint| constraint.to_sql_string(context))
|
||||
.collect::<Vec<_>>()
|
||||
.join(" ")
|
||||
)
|
||||
} else {
|
||||
"".to_string()
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl ToSqlString for ast::NamedColumnConstraint {
|
||||
fn to_sql_string<C: crate::to_sql_string::ToSqlContext>(&self, context: &C) -> String {
|
||||
let mut ret = Vec::new();
|
||||
if let Some(name) = &self.name {
|
||||
ret.push(format!("CONSTRAINT {}", name.0));
|
||||
}
|
||||
ret.push(self.constraint.to_sql_string(context));
|
||||
ret.join(" ")
|
||||
}
|
||||
}
|
||||
|
||||
impl ToSqlString for ast::ColumnConstraint {
|
||||
fn to_sql_string<C: crate::to_sql_string::ToSqlContext>(&self, context: &C) -> String {
|
||||
match self {
|
||||
Self::Check(expr) => format!("CHECK ({})", expr.to_sql_string(context)),
|
||||
Self::Collate { collation_name } => format!("COLLATE {}", collation_name.0),
|
||||
Self::Default(expr) => {
|
||||
if matches!(expr, ast::Expr::Literal(..)) {
|
||||
format!("DEFAULT {}", expr.to_sql_string(context))
|
||||
} else {
|
||||
format!("DEFAULT ({})", expr.to_sql_string(context))
|
||||
}
|
||||
}
|
||||
Self::Defer(expr) => expr.to_string(),
|
||||
Self::ForeignKey {
|
||||
clause,
|
||||
deref_clause,
|
||||
} => format!(
|
||||
"{}{}",
|
||||
clause,
|
||||
if let Some(deref) = deref_clause {
|
||||
deref.to_string()
|
||||
} else {
|
||||
"".to_string()
|
||||
}
|
||||
),
|
||||
Self::Generated { expr, typ } => {
|
||||
// Don't need to add the generated part
|
||||
format!(
|
||||
"AS ({}){}",
|
||||
expr.to_sql_string(context),
|
||||
if let Some(typ) = typ {
|
||||
format!(" {}", &typ.0)
|
||||
} else {
|
||||
"".to_string()
|
||||
}
|
||||
)
|
||||
}
|
||||
Self::NotNull {
|
||||
nullable: _,
|
||||
conflict_clause,
|
||||
} => {
|
||||
// nullable should always be true here
|
||||
format!(
|
||||
"NOT NULL{}",
|
||||
conflict_clause.map_or("".to_string(), |conflict| format!(" {conflict}"))
|
||||
)
|
||||
}
|
||||
Self::PrimaryKey {
|
||||
order,
|
||||
conflict_clause,
|
||||
auto_increment,
|
||||
} => {
|
||||
format!(
|
||||
"PRIMARY KEY{}{}{}",
|
||||
order.map_or("".to_string(), |order| format!(" {order}")),
|
||||
conflict_clause.map_or("".to_string(), |conflict| format!(" {conflict}")),
|
||||
auto_increment.then_some(" AUTOINCREMENT").unwrap_or("")
|
||||
)
|
||||
}
|
||||
Self::Unique(conflict_clause) => {
|
||||
format!(
|
||||
"UNIQUE{}",
|
||||
conflict_clause.map_or("".to_string(), |conflict| format!(" {conflict}"))
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for ast::ForeignKeyClause {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
let value = format!(
|
||||
"REFERENCES {}{}{}",
|
||||
self.tbl_name.0,
|
||||
if let Some(columns) = &self.columns {
|
||||
format!(
|
||||
"({})",
|
||||
columns
|
||||
.iter()
|
||||
.map(|cols| cols.to_string())
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ")
|
||||
)
|
||||
} else {
|
||||
"".to_string()
|
||||
},
|
||||
if !self.args.is_empty() {
|
||||
format!(
|
||||
" {}",
|
||||
self.args
|
||||
.iter()
|
||||
.map(|arg| arg.to_string())
|
||||
.collect::<Vec<_>>()
|
||||
.join(" ")
|
||||
)
|
||||
} else {
|
||||
"".to_string()
|
||||
}
|
||||
);
|
||||
write!(f, "{value}")
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for ast::RefArg {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
let value = match self {
|
||||
Self::Match(name) => format!("MATCH {}", name.0),
|
||||
Self::OnDelete(act) => format!("ON DELETE {act}"),
|
||||
Self::OnUpdate(act) => format!("ON UPDATE {act}"),
|
||||
Self::OnInsert(..) => unimplemented!(
|
||||
"On Insert does not exist in SQLite: https://www.sqlite.org/lang_altertable.html"
|
||||
),
|
||||
};
|
||||
write!(f, "{value}")
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for ast::RefAct {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
let value = match self {
|
||||
Self::Cascade => "CASCADE",
|
||||
Self::NoAction => "NO ACTION",
|
||||
Self::Restrict => "RESTRICT",
|
||||
Self::SetDefault => "SET DEFAULT",
|
||||
Self::SetNull => "SET NULL",
|
||||
};
|
||||
write!(f, "{value}")
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for ast::DeferSubclause {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
let value = format!(
|
||||
"{}{}",
|
||||
if self.deferrable {
|
||||
"NOT DEFERRABLE"
|
||||
} else {
|
||||
"DEFERRABLE"
|
||||
},
|
||||
if let Some(init_deffered) = &self.init_deferred {
|
||||
match init_deffered {
|
||||
ast::InitDeferredPred::InitiallyDeferred => " INITIALLY DEFERRED",
|
||||
ast::InitDeferredPred::InitiallyImmediate => " INITIALLY IMMEDIATE",
|
||||
}
|
||||
} else {
|
||||
""
|
||||
}
|
||||
);
|
||||
write!(f, "{value}")
|
||||
}
|
||||
}
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::to_sql_string_test;
|
||||
|
||||
to_sql_string_test!(
|
||||
test_alter_table_rename,
|
||||
"ALTER TABLE t RENAME TO new_table_name;"
|
||||
"ALTER TABLE t RENAME TO new_table_name"
|
||||
);
|
||||
|
||||
to_sql_string_test!(
|
||||
test_alter_table_add_column,
|
||||
"ALTER TABLE t ADD COLUMN c INTEGER;"
|
||||
"ALTER TABLE t ADD COLUMN c INTEGER"
|
||||
);
|
||||
|
||||
to_sql_string_test!(
|
||||
test_alter_table_add_column_with_default,
|
||||
"ALTER TABLE t ADD COLUMN c TEXT DEFAULT 'value';"
|
||||
"ALTER TABLE t ADD COLUMN c TEXT DEFAULT 'value'"
|
||||
);
|
||||
|
||||
to_sql_string_test!(
|
||||
test_alter_table_add_column_not_null_default,
|
||||
"ALTER TABLE t ADD COLUMN c REAL NOT NULL DEFAULT 0.0;"
|
||||
"ALTER TABLE t ADD COLUMN c REAL NOT NULL DEFAULT 0.0"
|
||||
);
|
||||
|
||||
to_sql_string_test!(
|
||||
test_alter_table_add_column_unique,
|
||||
"ALTER TABLE t ADD COLUMN c TEXT UNIQUE",
|
||||
ignore = "ParserError = Cannot add a UNIQUE column;"
|
||||
ignore = "ParserError = Cannot add a UNIQUE column"
|
||||
);
|
||||
|
||||
to_sql_string_test!(
|
||||
test_alter_table_rename_column,
|
||||
"ALTER TABLE t RENAME COLUMN old_name TO new_name;"
|
||||
"ALTER TABLE t RENAME COLUMN old_name TO new_name"
|
||||
);
|
||||
|
||||
to_sql_string_test!(test_alter_table_drop_column, "ALTER TABLE t DROP COLUMN c;");
|
||||
to_sql_string_test!(test_alter_table_drop_column, "ALTER TABLE t DROP COLUMN c");
|
||||
|
||||
to_sql_string_test!(
|
||||
test_alter_table_add_column_check,
|
||||
"ALTER TABLE t ADD COLUMN c INTEGER CHECK (c > 0);"
|
||||
"ALTER TABLE t ADD COLUMN c INTEGER CHECK (c > 0)"
|
||||
);
|
||||
|
||||
to_sql_string_test!(
|
||||
test_alter_table_add_column_foreign_key,
|
||||
"ALTER TABLE t ADD COLUMN c INTEGER REFERENCES t2(id) ON DELETE CASCADE;"
|
||||
"ALTER TABLE t ADD COLUMN c INTEGER REFERENCES t2 (id) ON DELETE CASCADE"
|
||||
);
|
||||
|
||||
to_sql_string_test!(
|
||||
test_alter_table_add_column_collate,
|
||||
"ALTER TABLE t ADD COLUMN c TEXT COLLATE NOCASE;"
|
||||
"ALTER TABLE t ADD COLUMN c TEXT COLLATE NOCASE"
|
||||
);
|
||||
|
||||
to_sql_string_test!(
|
||||
test_alter_table_add_column_primary_key,
|
||||
"ALTER TABLE t ADD COLUMN c INTEGER PRIMARY KEY;",
|
||||
"ALTER TABLE t ADD COLUMN c INTEGER PRIMARY KEY",
|
||||
ignore = "ParserError = Cannot add a PRIMARY KEY column"
|
||||
);
|
||||
|
||||
to_sql_string_test!(
|
||||
test_alter_table_add_column_primary_key_autoincrement,
|
||||
"ALTER TABLE t ADD COLUMN c INTEGER PRIMARY KEY AUTOINCREMENT;",
|
||||
"ALTER TABLE t ADD COLUMN c INTEGER PRIMARY KEY AUTOINCREMENT",
|
||||
ignore = "ParserError = Cannot add a PRIMARY KEY column"
|
||||
);
|
||||
|
||||
to_sql_string_test!(
|
||||
test_alter_table_add_generated_column,
|
||||
"ALTER TABLE t ADD COLUMN c_generated AS (a + b) STORED;"
|
||||
"ALTER TABLE t ADD COLUMN c_generated AS (a + b) STORED"
|
||||
);
|
||||
|
||||
to_sql_string_test!(
|
||||
test_alter_table_add_column_schema,
|
||||
"ALTER TABLE schema_name.t ADD COLUMN c INTEGER;"
|
||||
"ALTER TABLE schema_name.t ADD COLUMN c INTEGER"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,240 +1,121 @@
|
||||
use std::fmt::Display;
|
||||
|
||||
use crate::{ast, to_sql_string::ToSqlString};
|
||||
|
||||
impl ToSqlString for ast::CreateTableBody {
|
||||
fn to_sql_string<C: crate::to_sql_string::ToSqlContext>(&self, context: &C) -> String {
|
||||
match self {
|
||||
Self::AsSelect(select) => format!("AS {}", select.to_sql_string(context)),
|
||||
Self::ColumnsAndConstraints {
|
||||
columns,
|
||||
constraints,
|
||||
options,
|
||||
} => {
|
||||
format!(
|
||||
"({}{}){}",
|
||||
columns
|
||||
.iter()
|
||||
.map(|(_, col)| col.to_sql_string(context))
|
||||
.collect::<Vec<_>>()
|
||||
.join(", "),
|
||||
constraints
|
||||
.as_ref()
|
||||
.map_or("".to_string(), |constraints| format!(
|
||||
", {}",
|
||||
constraints
|
||||
.iter()
|
||||
.map(|constraint| constraint.to_sql_string(context))
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ")
|
||||
)),
|
||||
options
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ToSqlString for ast::NamedTableConstraint {
|
||||
fn to_sql_string<C: crate::to_sql_string::ToSqlContext>(&self, context: &C) -> String {
|
||||
if let Some(name) = &self.name {
|
||||
format!(
|
||||
"CONSTRAINT {} {}",
|
||||
name.0,
|
||||
self.constraint.to_sql_string(context)
|
||||
)
|
||||
} else {
|
||||
self.constraint.to_sql_string(context).to_string()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ToSqlString for ast::TableConstraint {
|
||||
fn to_sql_string<C: crate::to_sql_string::ToSqlContext>(&self, context: &C) -> String {
|
||||
match self {
|
||||
Self::Check(expr) => format!("CHECK ({})", expr.to_sql_string(context)),
|
||||
Self::ForeignKey {
|
||||
columns,
|
||||
clause,
|
||||
deref_clause,
|
||||
} => format!(
|
||||
"FOREIGN KEY ({}) {}{}",
|
||||
columns
|
||||
.iter()
|
||||
.map(|col| col.to_string())
|
||||
.collect::<Vec<_>>()
|
||||
.join(", "),
|
||||
clause,
|
||||
if let Some(deref) = deref_clause {
|
||||
deref.to_string()
|
||||
} else {
|
||||
"".to_string()
|
||||
}
|
||||
),
|
||||
Self::PrimaryKey {
|
||||
columns,
|
||||
auto_increment,
|
||||
conflict_clause,
|
||||
} => format!(
|
||||
"PRIMARY KEY ({}){}{}",
|
||||
columns
|
||||
.iter()
|
||||
.map(|col| col.to_sql_string(context))
|
||||
.collect::<Vec<_>>()
|
||||
.join(", "),
|
||||
conflict_clause.map_or("".to_string(), |conflict| format!(" {conflict}")),
|
||||
auto_increment.then_some(" AUTOINCREMENT").unwrap_or("")
|
||||
),
|
||||
Self::Unique {
|
||||
columns,
|
||||
conflict_clause,
|
||||
} => format!(
|
||||
"UNIQUE ({}){}",
|
||||
columns
|
||||
.iter()
|
||||
.map(|col| col.to_sql_string(context))
|
||||
.collect::<Vec<_>>()
|
||||
.join(", "),
|
||||
conflict_clause.map_or("".to_string(), |conflict| format!(" {conflict}"))
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for ast::TableOptions {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"{}",
|
||||
if *self == Self::NONE {
|
||||
""
|
||||
} else if *self == Self::STRICT {
|
||||
" STRICT"
|
||||
} else {
|
||||
" WITHOUT ROWID"
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::to_sql_string_test;
|
||||
|
||||
to_sql_string_test!(
|
||||
test_create_table_simple,
|
||||
"CREATE TABLE t (a INTEGER, b TEXT);"
|
||||
"CREATE TABLE t (a INTEGER, b TEXT)"
|
||||
);
|
||||
|
||||
to_sql_string_test!(
|
||||
test_create_table_primary_key,
|
||||
"CREATE TABLE t (id INTEGER PRIMARY KEY, name TEXT);"
|
||||
"CREATE TABLE t (id INTEGER PRIMARY KEY, name TEXT)"
|
||||
);
|
||||
|
||||
to_sql_string_test!(
|
||||
test_create_table_multi_primary_key,
|
||||
"CREATE TABLE t (a INTEGER, b TEXT, PRIMARY KEY (a, b));"
|
||||
"CREATE TABLE t (a INTEGER, b TEXT, PRIMARY KEY (a, b))"
|
||||
);
|
||||
|
||||
to_sql_string_test!(
|
||||
test_create_table_data_types,
|
||||
"CREATE TABLE t (a INTEGER, b TEXT, c REAL, d BLOB, e NUMERIC);"
|
||||
"CREATE TABLE t (a INTEGER, b TEXT, c REAL, d BLOB, e NUMERIC)"
|
||||
);
|
||||
|
||||
to_sql_string_test!(
|
||||
test_create_table_foreign_key,
|
||||
"CREATE TABLE t2 (id INTEGER PRIMARY KEY, t_id INTEGER, FOREIGN KEY (t_id) REFERENCES t(id));"
|
||||
"CREATE TABLE t2 (id INTEGER PRIMARY KEY, t_id INTEGER, FOREIGN KEY (t_id) REFERENCES t (id))"
|
||||
);
|
||||
|
||||
to_sql_string_test!(
|
||||
test_create_table_foreign_key_cascade,
|
||||
"CREATE TABLE t2 (id INTEGER PRIMARY KEY, t_id INTEGER, FOREIGN KEY (t_id) REFERENCES t(id) ON DELETE CASCADE);"
|
||||
"CREATE TABLE t2 (id INTEGER PRIMARY KEY, t_id INTEGER, FOREIGN KEY (t_id) REFERENCES t (id) ON DELETE CASCADE)"
|
||||
);
|
||||
|
||||
to_sql_string_test!(
|
||||
test_create_table_unique,
|
||||
"CREATE TABLE t (a INTEGER UNIQUE, b TEXT);"
|
||||
"CREATE TABLE t (a INTEGER UNIQUE, b TEXT)"
|
||||
);
|
||||
|
||||
to_sql_string_test!(
|
||||
test_create_table_not_null,
|
||||
"CREATE TABLE t (a INTEGER NOT NULL, b TEXT);"
|
||||
"CREATE TABLE t (a INTEGER NOT NULL, b TEXT)"
|
||||
);
|
||||
|
||||
to_sql_string_test!(
|
||||
test_create_table_check,
|
||||
"CREATE TABLE t (a INTEGER CHECK (a > 0), b TEXT);"
|
||||
"CREATE TABLE t (a INTEGER CHECK (a > 0), b TEXT)"
|
||||
);
|
||||
|
||||
to_sql_string_test!(
|
||||
test_create_table_default,
|
||||
"CREATE TABLE t (a INTEGER DEFAULT 0, b TEXT);"
|
||||
"CREATE TABLE t (a INTEGER DEFAULT 0, b TEXT)"
|
||||
);
|
||||
|
||||
to_sql_string_test!(
|
||||
test_create_table_multiple_constraints,
|
||||
"CREATE TABLE t (a INTEGER NOT NULL UNIQUE, b TEXT DEFAULT 'default');"
|
||||
"CREATE TABLE t (a INTEGER NOT NULL UNIQUE, b TEXT DEFAULT 'default')"
|
||||
);
|
||||
|
||||
to_sql_string_test!(
|
||||
test_create_table_generated_column,
|
||||
"CREATE TABLE t (a INTEGER, b INTEGER, c INTEGER AS (a + b));"
|
||||
"CREATE TABLE t (a INTEGER, b INTEGER, c INTEGER AS (a + b))"
|
||||
);
|
||||
|
||||
to_sql_string_test!(
|
||||
test_create_table_generated_stored,
|
||||
"CREATE TABLE t (a INTEGER, b INTEGER, c INTEGER AS (a + b) STORED);"
|
||||
"CREATE TABLE t (a INTEGER, b INTEGER, c INTEGER AS (a + b) STORED)"
|
||||
);
|
||||
|
||||
to_sql_string_test!(
|
||||
test_create_table_generated_virtual,
|
||||
"CREATE TABLE t (a INTEGER, b INTEGER, c INTEGER AS (a + b) VIRTUAL);"
|
||||
"CREATE TABLE t (a INTEGER, b INTEGER, c INTEGER AS (a + b) VIRTUAL)"
|
||||
);
|
||||
|
||||
to_sql_string_test!(
|
||||
test_create_table_quoted_columns,
|
||||
"CREATE TABLE t (\"select\" INTEGER, \"from\" TEXT);"
|
||||
"CREATE TABLE t (\"select\" INTEGER, \"from\" TEXT)"
|
||||
);
|
||||
|
||||
to_sql_string_test!(
|
||||
test_create_table_quoted_table,
|
||||
"CREATE TABLE \"my table\" (a INTEGER);"
|
||||
"CREATE TABLE \"my table\" (a INTEGER)"
|
||||
);
|
||||
|
||||
to_sql_string_test!(
|
||||
test_create_table_if_not_exists,
|
||||
"CREATE TABLE IF NOT EXISTS t (a INTEGER);"
|
||||
"CREATE TABLE IF NOT EXISTS t (a INTEGER)"
|
||||
);
|
||||
|
||||
to_sql_string_test!(test_create_temp_table, "CREATE TEMP TABLE t (a INTEGER);");
|
||||
to_sql_string_test!(test_create_temp_table, "CREATE TEMP TABLE t (a INTEGER)");
|
||||
|
||||
to_sql_string_test!(
|
||||
test_create_table_without_rowid,
|
||||
"CREATE TABLE t (a INTEGER PRIMARY KEY, b TEXT) WITHOUT ROWID;"
|
||||
"CREATE TABLE t (a INTEGER PRIMARY KEY, b TEXT) WITHOUT ROWID"
|
||||
);
|
||||
|
||||
to_sql_string_test!(
|
||||
test_create_table_named_primary_key,
|
||||
"CREATE TABLE t (a INTEGER CONSTRAINT pk_a PRIMARY KEY);"
|
||||
"CREATE TABLE t (a INTEGER CONSTRAINT pk_a PRIMARY KEY)"
|
||||
);
|
||||
|
||||
to_sql_string_test!(
|
||||
test_create_table_named_unique,
|
||||
"CREATE TABLE t (a INTEGER, CONSTRAINT unique_a UNIQUE (a));"
|
||||
"CREATE TABLE t (a INTEGER, CONSTRAINT unique_a UNIQUE (a))"
|
||||
);
|
||||
|
||||
to_sql_string_test!(
|
||||
test_create_table_named_foreign_key,
|
||||
"CREATE TABLE t2 (id INTEGER, t_id INTEGER, CONSTRAINT fk_t FOREIGN KEY (t_id) REFERENCES t(id));"
|
||||
"CREATE TABLE t2 (id INTEGER, t_id INTEGER, CONSTRAINT fk_t FOREIGN KEY (t_id) REFERENCES t (id))"
|
||||
);
|
||||
|
||||
to_sql_string_test!(
|
||||
test_create_table_complex,
|
||||
"CREATE TABLE t (id INTEGER PRIMARY KEY, a INTEGER NOT NULL, b TEXT DEFAULT 'default', c INTEGER AS (a * 2), CONSTRAINT unique_a UNIQUE (a));"
|
||||
"CREATE TABLE t (id INTEGER PRIMARY KEY, a INTEGER NOT NULL, b TEXT DEFAULT 'default', c INTEGER AS (a * 2), CONSTRAINT unique_a UNIQUE (a))"
|
||||
);
|
||||
|
||||
to_sql_string_test!(
|
||||
test_create_table_multiple_foreign_keys,
|
||||
"CREATE TABLE t3 (id INTEGER PRIMARY KEY, t1_id INTEGER, t2_id INTEGER, FOREIGN KEY (t1_id) REFERENCES t1(id), FOREIGN KEY (t2_id) REFERENCES t2(id));"
|
||||
"CREATE TABLE t3 (id INTEGER PRIMARY KEY, t1_id INTEGER, t2_id INTEGER, FOREIGN KEY (t1_id) REFERENCES t1 (id), FOREIGN KEY (t2_id) REFERENCES t2 (id))"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,248 +1,3 @@
|
||||
use std::fmt::Display;
|
||||
|
||||
use crate::{
|
||||
ast::{self, fmt::ToTokens},
|
||||
to_sql_string::ToSqlString,
|
||||
};
|
||||
|
||||
impl ToSqlString for ast::CreateTrigger {
|
||||
fn to_sql_string<C: crate::to_sql_string::ToSqlContext>(&self, context: &C) -> String {
|
||||
format!(
|
||||
"CREATE{} TRIGGER {}{}{} {} ON {}{}{} BEGIN {} END;",
|
||||
if self.temporary { " TEMP" } else { "" },
|
||||
if self.if_not_exists {
|
||||
"IF NOT EXISTS "
|
||||
} else {
|
||||
""
|
||||
},
|
||||
self.trigger_name.to_sql_string(context),
|
||||
self.time.map_or("".to_string(), |time| format!(" {time}")),
|
||||
self.event,
|
||||
self.tbl_name.to_sql_string(context),
|
||||
if self.for_each_row {
|
||||
" FOR EACH ROW"
|
||||
} else {
|
||||
""
|
||||
},
|
||||
self.when_clause
|
||||
.as_ref()
|
||||
.map_or("".to_string(), |expr| format!(
|
||||
" WHEN {}",
|
||||
expr.to_sql_string(context)
|
||||
)),
|
||||
self.commands
|
||||
.iter()
|
||||
.map(|command| format!("{};", command.to_sql_string(context)))
|
||||
.collect::<Vec<_>>()
|
||||
.join(" ")
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for ast::TriggerTime {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
self.to_fmt(f)
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for ast::TriggerEvent {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"{}",
|
||||
match self {
|
||||
Self::Delete => "DELETE".to_string(),
|
||||
Self::Insert => "INSERT".to_string(),
|
||||
Self::Update => "UPDATE".to_string(),
|
||||
Self::UpdateOf(col_names) => format!(
|
||||
"UPDATE OF {}",
|
||||
col_names
|
||||
.iter()
|
||||
.map(|name| name.0.clone())
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ")
|
||||
),
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl ToSqlString for ast::TriggerCmd {
|
||||
fn to_sql_string<C: crate::to_sql_string::ToSqlContext>(&self, context: &C) -> String {
|
||||
match self {
|
||||
Self::Delete(delete) => delete.to_sql_string(context),
|
||||
Self::Insert(insert) => insert.to_sql_string(context),
|
||||
Self::Select(select) => select.to_sql_string(context),
|
||||
Self::Update(update) => update.to_sql_string(context),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ToSqlString for ast::TriggerCmdDelete {
|
||||
fn to_sql_string<C: crate::to_sql_string::ToSqlContext>(&self, context: &C) -> String {
|
||||
// https://sqlite.org/lang_createtrigger.html
|
||||
// TODO: no CTEs and returning clause present in ast for delete
|
||||
// Also for tbl_name it should be a qualified table name with indexed by clauses
|
||||
format!(
|
||||
"DELETE FROM {}{}",
|
||||
self.tbl_name.0,
|
||||
self.where_clause
|
||||
.as_ref()
|
||||
.map_or("".to_string(), |expr| format!(
|
||||
" WHERE {}",
|
||||
expr.to_sql_string(context)
|
||||
))
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl ToSqlString for ast::TriggerCmdInsert {
|
||||
fn to_sql_string<C: crate::to_sql_string::ToSqlContext>(&self, context: &C) -> String {
|
||||
// https://sqlite.org/lang_createtrigger.html
|
||||
// FOR TRIGGER SHOULD JUST USE REGULAR INSERT AST
|
||||
// TODO: no ALIAS after table name
|
||||
// TODO: no DEFAULT VALUES
|
||||
format!(
|
||||
"INSERT {}INTO {} {}{}{}{}",
|
||||
self.or_conflict
|
||||
.map_or("".to_string(), |conflict| format!("OR {conflict} ")),
|
||||
self.tbl_name.0,
|
||||
self.col_names
|
||||
.as_ref()
|
||||
.map_or("".to_string(), |col_names| format!(
|
||||
"({}) ",
|
||||
col_names
|
||||
.iter()
|
||||
.map(|name| name.0.clone())
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ")
|
||||
)),
|
||||
self.select.to_sql_string(context),
|
||||
self.upsert
|
||||
.as_ref()
|
||||
.map_or("".to_string(), |upsert| format!(
|
||||
" {}",
|
||||
upsert.to_sql_string(context)
|
||||
)),
|
||||
self.returning
|
||||
.as_ref()
|
||||
.map_or("".to_string(), |returning| format!(
|
||||
" RETURNING {}",
|
||||
returning
|
||||
.iter()
|
||||
.map(|col| col.to_sql_string(context))
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ")
|
||||
))
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl ToSqlString for ast::Upsert {
|
||||
fn to_sql_string<C: crate::to_sql_string::ToSqlContext>(&self, context: &C) -> String {
|
||||
format!(
|
||||
"ON CONFLICT{}{}{}",
|
||||
self.index.as_ref().map_or("".to_string(), |index| format!(
|
||||
"{} ",
|
||||
index.to_sql_string(context)
|
||||
)),
|
||||
self.do_clause.to_sql_string(context),
|
||||
self.next.as_ref().map_or("".to_string(), |next| format!(
|
||||
" {}",
|
||||
next.to_sql_string(context)
|
||||
))
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl ToSqlString for ast::UpsertIndex {
|
||||
fn to_sql_string<C: crate::to_sql_string::ToSqlContext>(&self, context: &C) -> String {
|
||||
format!(
|
||||
"({}){}",
|
||||
self.targets
|
||||
.iter()
|
||||
.map(|target| target.to_sql_string(context))
|
||||
.collect::<Vec<_>>()
|
||||
.join(", "),
|
||||
self.where_clause
|
||||
.as_ref()
|
||||
.map_or("".to_string(), |expr| format!(
|
||||
" WHERE {}",
|
||||
expr.to_sql_string(context)
|
||||
))
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl ToSqlString for ast::UpsertDo {
|
||||
fn to_sql_string<C: crate::to_sql_string::ToSqlContext>(&self, context: &C) -> String {
|
||||
match self {
|
||||
Self::Nothing => "DO NOTHING".to_string(),
|
||||
Self::Set { sets, where_clause } => {
|
||||
format!(
|
||||
"DO UPDATE SET {}{}",
|
||||
sets.iter()
|
||||
.map(|set| set.to_sql_string(context))
|
||||
.collect::<Vec<_>>()
|
||||
.join(", "),
|
||||
where_clause.as_ref().map_or("".to_string(), |expr| format!(
|
||||
" WHERE {}",
|
||||
expr.to_sql_string(context)
|
||||
))
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ToSqlString for ast::Set {
|
||||
fn to_sql_string<C: crate::to_sql_string::ToSqlContext>(&self, context: &C) -> String {
|
||||
if self.col_names.len() == 1 {
|
||||
format!(
|
||||
"{} = {}",
|
||||
&self.col_names[0],
|
||||
self.expr.to_sql_string(context)
|
||||
)
|
||||
} else {
|
||||
format!(
|
||||
"({}) = {}",
|
||||
self.col_names
|
||||
.iter()
|
||||
.map(|name| name.0.clone())
|
||||
.collect::<Vec<_>>()
|
||||
.join(", "),
|
||||
self.expr.to_sql_string(context)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ToSqlString for ast::TriggerCmdUpdate {
|
||||
fn to_sql_string<C: crate::to_sql_string::ToSqlContext>(&self, context: &C) -> String {
|
||||
format!(
|
||||
"UPDATE {}{} SET {}{}{}",
|
||||
self.or_conflict
|
||||
.map_or("".to_string(), |conflict| format!("OR {conflict}")),
|
||||
self.tbl_name.0, // TODO: should be a qualified table name,
|
||||
self.sets
|
||||
.iter()
|
||||
.map(|set| set.to_sql_string(context))
|
||||
.collect::<Vec<_>>()
|
||||
.join(", "),
|
||||
self.from.as_ref().map_or("".to_string(), |from| format!(
|
||||
" {}",
|
||||
from.to_sql_string(context)
|
||||
)),
|
||||
self.where_clause
|
||||
.as_ref()
|
||||
.map_or("".to_string(), |expr| format!(
|
||||
" WHERE {}",
|
||||
expr.to_sql_string(context)
|
||||
))
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::to_sql_string_test;
|
||||
@@ -255,7 +10,7 @@ mod tests {
|
||||
BEGIN
|
||||
INSERT INTO employee_log (action, employee_id, timestamp)
|
||||
VALUES ('INSERT', NEW.id, CURRENT_TIMESTAMP);
|
||||
END;"
|
||||
END"
|
||||
);
|
||||
|
||||
to_sql_string_test!(
|
||||
@@ -266,7 +21,7 @@ mod tests {
|
||||
BEGIN
|
||||
INSERT INTO employee_log (action, employee_id, old_value, new_value, timestamp)
|
||||
VALUES ('UPDATE', OLD.id, OLD.salary, NEW.salary, CURRENT_TIMESTAMP);
|
||||
END;"
|
||||
END"
|
||||
);
|
||||
|
||||
to_sql_string_test!(
|
||||
@@ -277,7 +32,7 @@ mod tests {
|
||||
BEGIN
|
||||
INSERT INTO employee_log (action, employee_id, timestamp)
|
||||
VALUES ('DELETE', OLD.id, CURRENT_TIMESTAMP);
|
||||
END;"
|
||||
END"
|
||||
);
|
||||
|
||||
to_sql_string_test!(
|
||||
@@ -287,8 +42,8 @@ mod tests {
|
||||
FOR EACH ROW
|
||||
WHEN NEW.salary < 0
|
||||
BEGIN
|
||||
SELECT RAISE(FAIL, 'Salary cannot be negative');
|
||||
END;"
|
||||
SELECT RAISE (FAIL, 'Salary cannot be negative');
|
||||
END"
|
||||
);
|
||||
|
||||
to_sql_string_test!(
|
||||
@@ -299,6 +54,6 @@ mod tests {
|
||||
BEGIN
|
||||
INSERT INTO departments (name) SELECT NEW.department WHERE NOT EXISTS (SELECT 1 FROM departments WHERE name = NEW.department);
|
||||
INSERT INTO employees (name, department_id) VALUES (NEW.name, (SELECT id FROM departments WHERE name = NEW.department));
|
||||
END;"
|
||||
END"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,84 +1,64 @@
|
||||
use crate::{ast, to_sql_string::ToSqlString};
|
||||
|
||||
impl ToSqlString for ast::CreateVirtualTable {
|
||||
fn to_sql_string<C: crate::to_sql_string::ToSqlContext>(&self, context: &C) -> String {
|
||||
format!(
|
||||
"CREATE VIRTUAL TABLE {}{} USING {}{};",
|
||||
if self.if_not_exists {
|
||||
"IF NOT EXISTS "
|
||||
} else {
|
||||
""
|
||||
},
|
||||
self.tbl_name.to_sql_string(context),
|
||||
self.module_name.0,
|
||||
self.args
|
||||
.as_ref()
|
||||
.map_or("".to_string(), |args| format!("({})", args.join(", ")))
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::to_sql_string_test;
|
||||
|
||||
to_sql_string_test!(
|
||||
test_create_virtual_table_fts5_basic,
|
||||
"CREATE VIRTUAL TABLE docs USING fts5(title, content);"
|
||||
"CREATE VIRTUAL TABLE docs USING fts5 (title, content)"
|
||||
);
|
||||
|
||||
to_sql_string_test!(
|
||||
test_create_virtual_table_fts5_tokenizer,
|
||||
"CREATE VIRTUAL TABLE docs USING fts5(title, content, tokenize = 'porter');"
|
||||
"CREATE VIRTUAL TABLE docs USING fts5 (title, content, tokenize = 'porter')"
|
||||
);
|
||||
|
||||
to_sql_string_test!(
|
||||
test_create_virtual_table_fts5_unindexed,
|
||||
"CREATE VIRTUAL TABLE docs USING fts5(title, content, metadata UNINDEXED);"
|
||||
"CREATE VIRTUAL TABLE docs USING fts5 (title, content, metadata UNINDEXED)"
|
||||
);
|
||||
|
||||
to_sql_string_test!(
|
||||
test_create_virtual_table_fts5_prefix,
|
||||
"CREATE VIRTUAL TABLE docs USING fts5(title, content, tokenize = 'unicode61', prefix = '2 4');"
|
||||
"CREATE VIRTUAL TABLE docs USING fts5 (title, content, tokenize = 'unicode61', prefix = '2 4')"
|
||||
);
|
||||
|
||||
to_sql_string_test!(
|
||||
test_create_virtual_table_fts5_contentless,
|
||||
"CREATE VIRTUAL TABLE docs USING fts5(title, content, content = '');"
|
||||
"CREATE VIRTUAL TABLE docs USING fts5 (title, content, content = '')"
|
||||
);
|
||||
|
||||
to_sql_string_test!(
|
||||
test_create_virtual_table_fts5_external_content,
|
||||
"CREATE VIRTUAL TABLE docs_fts USING fts5(title, content, content = 'documents');"
|
||||
"CREATE VIRTUAL TABLE docs_fts USING fts5 (title, content, content = 'documents')"
|
||||
);
|
||||
|
||||
to_sql_string_test!(
|
||||
test_create_virtual_table_rtree,
|
||||
"CREATE VIRTUAL TABLE geo USING rtree(id, min_x, max_x, min_y, max_y);"
|
||||
"CREATE VIRTUAL TABLE geo USING rtree (id, min_x, max_x, min_y, max_y)"
|
||||
);
|
||||
|
||||
to_sql_string_test!(
|
||||
test_create_virtual_table_rtree_aux,
|
||||
"CREATE VIRTUAL TABLE geo USING rtree(id, min_x, max_x, min_y, max_y, +name TEXT, +category INTEGER);"
|
||||
"CREATE VIRTUAL TABLE geo USING rtree (id, min_x, max_x, min_y, max_y, +name TEXT, +category INTEGER)"
|
||||
);
|
||||
|
||||
to_sql_string_test!(
|
||||
test_create_virtual_table_if_not_exists,
|
||||
"CREATE VIRTUAL TABLE IF NOT EXISTS docs USING fts5(title, content);"
|
||||
"CREATE VIRTUAL TABLE IF NOT EXISTS docs USING fts5 (title, content)"
|
||||
);
|
||||
|
||||
to_sql_string_test!(
|
||||
test_create_virtual_table_fts4,
|
||||
"CREATE VIRTUAL TABLE docs USING fts4(title, content, matchinfo = 'fts3');"
|
||||
"CREATE VIRTUAL TABLE docs USING fts4 (title, content, matchinfo = 'fts3')"
|
||||
);
|
||||
|
||||
to_sql_string_test!(
|
||||
test_create_virtual_table_fts5_detail,
|
||||
"CREATE VIRTUAL TABLE docs USING fts5(title, body TEXT, detail = 'none');"
|
||||
"CREATE VIRTUAL TABLE docs USING fts5 (title, body TEXT, detail = 'none')"
|
||||
);
|
||||
|
||||
to_sql_string_test!(
|
||||
test_create_virtual_table_schema,
|
||||
"CREATE VIRTUAL TABLE main.docs USING fts5(title, content);"
|
||||
"CREATE VIRTUAL TABLE main.docs USING fts5 (title, content)"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,133 +1,82 @@
|
||||
use crate::{ast, to_sql_string::ToSqlString};
|
||||
|
||||
impl ToSqlString for ast::Delete {
|
||||
fn to_sql_string<C: crate::to_sql_string::ToSqlContext>(&self, context: &C) -> String {
|
||||
format!(
|
||||
"{}DELETE FROM {}{}{}{}{}{};",
|
||||
self.with.as_ref().map_or("".to_string(), |with| format!(
|
||||
"{} ",
|
||||
with.to_sql_string(context)
|
||||
)),
|
||||
self.tbl_name.to_sql_string(context),
|
||||
self.indexed
|
||||
.as_ref()
|
||||
.map_or("".to_string(), |indexed| format!(" {indexed}")),
|
||||
self.where_clause
|
||||
.as_ref()
|
||||
.map_or("".to_string(), |expr| format!(
|
||||
" WHERE {}",
|
||||
expr.to_sql_string(context)
|
||||
)),
|
||||
self.returning
|
||||
.as_ref()
|
||||
.map_or("".to_string(), |returning| format!(
|
||||
" RETURNING {}",
|
||||
returning
|
||||
.iter()
|
||||
.map(|col| col.to_sql_string(context))
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ")
|
||||
)),
|
||||
self.order_by
|
||||
.as_ref()
|
||||
.map_or("".to_string(), |order_by| format!(
|
||||
" ORDER BY {}",
|
||||
order_by
|
||||
.iter()
|
||||
.map(|col| col.to_sql_string(context))
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ")
|
||||
)),
|
||||
self.limit.as_ref().map_or("".to_string(), |limit| format!(
|
||||
" {}",
|
||||
limit.to_sql_string(context)
|
||||
))
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::to_sql_string_test;
|
||||
|
||||
// Basic DELETE from a single table
|
||||
to_sql_string_test!(test_delete_all, "DELETE FROM employees;");
|
||||
to_sql_string_test!(test_delete_all, "DELETE FROM employees");
|
||||
|
||||
// DELETE with a simple WHERE clause
|
||||
to_sql_string_test!(
|
||||
test_delete_with_where,
|
||||
"DELETE FROM employees WHERE id = 1;"
|
||||
);
|
||||
to_sql_string_test!(test_delete_with_where, "DELETE FROM employees WHERE id = 1");
|
||||
|
||||
// DELETE with multiple WHERE conditions
|
||||
to_sql_string_test!(
|
||||
test_delete_with_multi_where,
|
||||
"DELETE FROM employees WHERE salary < 50000 AND department_id = 3;"
|
||||
"DELETE FROM employees WHERE salary < 50000 AND department_id = 3"
|
||||
);
|
||||
|
||||
// DELETE with IN clause
|
||||
to_sql_string_test!(
|
||||
test_delete_with_in,
|
||||
"DELETE FROM employees WHERE id IN (1, 2, 3);"
|
||||
"DELETE FROM employees WHERE id IN (1, 2, 3)"
|
||||
);
|
||||
|
||||
// DELETE with subquery in WHERE
|
||||
to_sql_string_test!(
|
||||
test_delete_with_subquery,
|
||||
"DELETE FROM employees WHERE department_id IN (SELECT id FROM departments WHERE name = 'Sales');"
|
||||
"DELETE FROM employees WHERE department_id IN (SELECT id FROM departments WHERE name = 'Sales')"
|
||||
);
|
||||
|
||||
// DELETE with EXISTS clause
|
||||
to_sql_string_test!(
|
||||
test_delete_with_exists,
|
||||
"DELETE FROM employees WHERE EXISTS (SELECT 1 FROM orders WHERE orders.employee_id = employees.id AND orders.status = 'pending');"
|
||||
"DELETE FROM employees WHERE EXISTS (SELECT 1 FROM orders WHERE orders.employee_id = employees.id AND orders.status = 'pending')"
|
||||
);
|
||||
|
||||
// DELETE with RETURNING clause
|
||||
to_sql_string_test!(
|
||||
test_delete_with_returning,
|
||||
"DELETE FROM employees WHERE salary < 30000 RETURNING id, name;"
|
||||
"DELETE FROM employees WHERE salary < 30000 RETURNING id, name"
|
||||
);
|
||||
|
||||
// DELETE with LIMIT clause
|
||||
to_sql_string_test!(
|
||||
test_delete_with_limit,
|
||||
"DELETE FROM employees WHERE salary < 40000 LIMIT 5;"
|
||||
"DELETE FROM employees WHERE salary < 40000 LIMIT 5"
|
||||
);
|
||||
|
||||
// DELETE with ORDER BY and LIMIT
|
||||
to_sql_string_test!(
|
||||
test_delete_with_order_by_limit,
|
||||
"DELETE FROM employees WHERE salary < 40000 ORDER BY id DESC LIMIT 5;"
|
||||
"DELETE FROM employees WHERE salary < 40000 ORDER BY id DESC LIMIT 5"
|
||||
);
|
||||
|
||||
// DELETE from schema-qualified table
|
||||
to_sql_string_test!(
|
||||
test_delete_schema_qualified,
|
||||
"DELETE FROM main.employees WHERE id = 1;"
|
||||
"DELETE FROM main.employees WHERE id = 1"
|
||||
);
|
||||
|
||||
// DELETE with BETWEEN clause
|
||||
to_sql_string_test!(
|
||||
test_delete_with_between,
|
||||
"DELETE FROM employees WHERE salary BETWEEN 30000 AND 50000;"
|
||||
"DELETE FROM employees WHERE salary BETWEEN 30000 AND 50000"
|
||||
);
|
||||
|
||||
// DELETE with NULL check
|
||||
to_sql_string_test!(
|
||||
test_delete_with_null,
|
||||
"DELETE FROM employees WHERE department_id IS NULL;"
|
||||
"DELETE FROM employees WHERE department_id IS NULL"
|
||||
);
|
||||
|
||||
// DELETE with LIKE clause
|
||||
to_sql_string_test!(
|
||||
test_delete_with_like,
|
||||
"DELETE FROM employees WHERE name LIKE 'J%';"
|
||||
"DELETE FROM employees WHERE name LIKE 'J%'"
|
||||
);
|
||||
|
||||
// DELETE with complex expression in WHERE
|
||||
to_sql_string_test!(
|
||||
test_delete_with_complex_expression,
|
||||
"DELETE FROM employees WHERE (salary * 1.1) > 60000 AND department_id != 1;"
|
||||
"DELETE FROM employees WHERE (salary * 1.1) > 60000 AND department_id != 1"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,57 +1,3 @@
|
||||
use crate::{ast, to_sql_string::ToSqlString};
|
||||
|
||||
impl ToSqlString for ast::Insert {
|
||||
fn to_sql_string<C: crate::to_sql_string::ToSqlContext>(&self, context: &C) -> String {
|
||||
format!(
|
||||
"{}INSERT {}INTO {} {}{}{}",
|
||||
self.with.as_ref().map_or("".to_string(), |with| format!(
|
||||
"{} ",
|
||||
with.to_sql_string(context)
|
||||
)),
|
||||
self.or_conflict
|
||||
.map_or("".to_string(), |conflict| format!("OR {conflict} ")),
|
||||
self.tbl_name.to_sql_string(context),
|
||||
self.columns
|
||||
.as_ref()
|
||||
.map_or("".to_string(), |col_names| format!(
|
||||
"({}) ",
|
||||
col_names
|
||||
.iter()
|
||||
.map(|name| name.0.clone())
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ")
|
||||
)),
|
||||
self.body.to_sql_string(context),
|
||||
self.returning
|
||||
.as_ref()
|
||||
.map_or("".to_string(), |returning| format!(
|
||||
" RETURNING {}",
|
||||
returning
|
||||
.iter()
|
||||
.map(|col| col.to_sql_string(context))
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ")
|
||||
))
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl ToSqlString for ast::InsertBody {
|
||||
fn to_sql_string<C: crate::to_sql_string::ToSqlContext>(&self, context: &C) -> String {
|
||||
match self {
|
||||
Self::DefaultValues => "DEFAULT VALUES".to_string(),
|
||||
Self::Select(select, upsert) => format!(
|
||||
"{}{}",
|
||||
select.to_sql_string(context),
|
||||
upsert.as_ref().map_or("".to_string(), |upsert| format!(
|
||||
" {}",
|
||||
upsert.to_sql_string(context)
|
||||
)),
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::to_sql_string_test;
|
||||
@@ -59,90 +5,90 @@ mod tests {
|
||||
// Basic INSERT with all columns
|
||||
to_sql_string_test!(
|
||||
test_insert_basic,
|
||||
"INSERT INTO employees (id, name, salary) VALUES (1, 'John Doe', 50000);"
|
||||
"INSERT INTO employees (id, name, salary) VALUES (1, 'John Doe', 50000)"
|
||||
);
|
||||
|
||||
// INSERT with multiple rows
|
||||
to_sql_string_test!(
|
||||
test_insert_multiple_rows,
|
||||
"INSERT INTO employees (id, name, salary) VALUES (1, 'John Doe', 50000), (2, 'Jane Smith', 60000);"
|
||||
"INSERT INTO employees (id, name, salary) VALUES (1, 'John Doe', 50000), (2, 'Jane Smith', 60000)"
|
||||
);
|
||||
|
||||
// INSERT with specific columns
|
||||
to_sql_string_test!(
|
||||
test_insert_specific_columns,
|
||||
"INSERT INTO employees (name, salary) VALUES ('Alice Brown', 55000);"
|
||||
"INSERT INTO employees (name, salary) VALUES ('Alice Brown', 55000)"
|
||||
);
|
||||
|
||||
// INSERT with DEFAULT VALUES
|
||||
to_sql_string_test!(
|
||||
test_insert_default_values,
|
||||
"INSERT INTO employees DEFAULT VALUES;"
|
||||
"INSERT INTO employees DEFAULT VALUES"
|
||||
);
|
||||
|
||||
// INSERT with SELECT subquery
|
||||
to_sql_string_test!(
|
||||
test_insert_select_subquery,
|
||||
"INSERT INTO employees (id, name, salary) SELECT id, name, salary FROM temp_employees WHERE salary > 40000;"
|
||||
"INSERT INTO employees (id, name, salary) SELECT id, name, salary FROM temp_employees WHERE salary > 40000"
|
||||
);
|
||||
|
||||
// INSERT with ON CONFLICT IGNORE
|
||||
to_sql_string_test!(
|
||||
test_insert_on_conflict_ignore,
|
||||
"INSERT INTO employees (id, name, salary) VALUES (1, 'John Doe', 50000) ON CONFLICT(id) DO NOTHING;"
|
||||
"INSERT INTO employees (id, name, salary) VALUES (1, 'John Doe', 50000) ON CONFLICT (id) DO NOTHING"
|
||||
);
|
||||
|
||||
// INSERT with ON CONFLICT REPLACE
|
||||
to_sql_string_test!(
|
||||
test_insert_on_conflict_replace,
|
||||
"INSERT INTO employees (id, name, salary) VALUES (1, 'John Doe', 50000) ON CONFLICT(id) DO UPDATE SET name = excluded.name, salary = excluded.salary;"
|
||||
"INSERT INTO employees (id, name, salary) VALUES (1, 'John Doe', 50000) ON CONFLICT (id) DO UPDATE SET name = excluded.name, salary = excluded.salary"
|
||||
);
|
||||
|
||||
// INSERT with RETURNING clause
|
||||
to_sql_string_test!(
|
||||
test_insert_with_returning,
|
||||
"INSERT INTO employees (id, name, salary) VALUES (1, 'John Doe', 50000) RETURNING id, name;"
|
||||
"INSERT INTO employees (id, name, salary) VALUES (1, 'John Doe', 50000) RETURNING id, name"
|
||||
);
|
||||
|
||||
// INSERT with NULL values
|
||||
to_sql_string_test!(
|
||||
test_insert_with_null,
|
||||
"INSERT INTO employees (id, name, salary, department_id) VALUES (1, 'John Doe', NULL, NULL);"
|
||||
"INSERT INTO employees (id, name, salary, department_id) VALUES (1, 'John Doe', NULL, NULL)"
|
||||
);
|
||||
|
||||
// INSERT with expression in VALUES
|
||||
to_sql_string_test!(
|
||||
test_insert_with_expression,
|
||||
"INSERT INTO employees (id, name, salary) VALUES (1, 'John Doe', 50000 * 1.1);"
|
||||
"INSERT INTO employees (id, name, salary) VALUES (1, 'John Doe', 50000 * 1.1)"
|
||||
);
|
||||
|
||||
// INSERT into schema-qualified table
|
||||
to_sql_string_test!(
|
||||
test_insert_schema_qualified,
|
||||
"INSERT INTO main.employees (id, name, salary) VALUES (1, 'John Doe', 50000);"
|
||||
"INSERT INTO main.employees (id, name, salary) VALUES (1, 'John Doe', 50000)"
|
||||
);
|
||||
|
||||
// INSERT with subquery and JOIN
|
||||
to_sql_string_test!(
|
||||
test_insert_subquery_join,
|
||||
"INSERT INTO employees (id, name, department_id) SELECT e.id, e.name, d.id FROM temp_employees e JOIN departments d ON e.dept_name = d.name;"
|
||||
"INSERT INTO employees (id, name, department_id) SELECT e.id, e.name, d.id FROM temp_employees e JOIN departments d ON e.dept_name = d.name"
|
||||
);
|
||||
|
||||
// INSERT with all columns from SELECT
|
||||
to_sql_string_test!(
|
||||
test_insert_all_columns_select,
|
||||
"INSERT INTO employees SELECT * FROM temp_employees;"
|
||||
"INSERT INTO employees SELECT * FROM temp_employees"
|
||||
);
|
||||
|
||||
// INSERT with ON CONFLICT and WHERE clause
|
||||
to_sql_string_test!(
|
||||
test_insert_on_conflict_where,
|
||||
"INSERT INTO employees (id, name, salary) VALUES (1, 'John Doe', 50000) ON CONFLICT(id) DO UPDATE SET salary = excluded.salary WHERE excluded.salary > employees.salary;"
|
||||
"INSERT INTO employees (id, name, salary) VALUES (1, 'John Doe', 50000) ON CONFLICT (id) DO UPDATE SET salary = excluded.salary WHERE excluded.salary > employees.salary"
|
||||
);
|
||||
|
||||
// INSERT with quoted column names (reserved words)
|
||||
to_sql_string_test!(
|
||||
test_insert_quoted_columns,
|
||||
"INSERT INTO employees (\"select\", \"from\") VALUES (1, 'data');"
|
||||
"INSERT INTO employees (\"select\", \"from\") VALUES (1, 'data')"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,3 @@
|
||||
use crate::ast;
|
||||
|
||||
use super::ToSqlString;
|
||||
|
||||
mod alter_table;
|
||||
mod create_table;
|
||||
mod create_trigger;
|
||||
@@ -11,196 +7,6 @@ mod insert;
|
||||
mod select;
|
||||
mod update;
|
||||
|
||||
impl ToSqlString for ast::Stmt {
|
||||
fn to_sql_string<C: super::ToSqlContext>(&self, context: &C) -> String {
|
||||
match self {
|
||||
Self::AlterTable(alter_table) => {
|
||||
let (name, body) = alter_table.as_ref();
|
||||
format!(
|
||||
"ALTER TABLE {} {};",
|
||||
name.to_sql_string(context),
|
||||
body.to_sql_string(context)
|
||||
)
|
||||
}
|
||||
Self::Analyze(name) => {
|
||||
if let Some(name) = name {
|
||||
format!("ANALYZE {};", name.to_sql_string(context))
|
||||
} else {
|
||||
"ANALYZE;".to_string()
|
||||
}
|
||||
}
|
||||
Self::Attach {
|
||||
expr,
|
||||
db_name,
|
||||
key: _,
|
||||
} => {
|
||||
// TODO: what is `key` in the attach syntax?
|
||||
format!(
|
||||
"ATTACH {} AS {};",
|
||||
expr.to_sql_string(context),
|
||||
db_name.to_sql_string(context)
|
||||
)
|
||||
}
|
||||
// TODO: not sure where name is applied here
|
||||
// https://www.sqlite.org/lang_transaction.html
|
||||
Self::Begin(transaction_type, _name) => {
|
||||
let t_type = transaction_type.map_or("", |t_type| match t_type {
|
||||
ast::TransactionType::Deferred => " DEFERRED",
|
||||
ast::TransactionType::Exclusive => " EXCLUSIVE",
|
||||
ast::TransactionType::Immediate => " IMMEDIATE",
|
||||
});
|
||||
format!("BEGIN{t_type};")
|
||||
}
|
||||
// END or COMMIT are equivalent here, so just defaulting to COMMIT
|
||||
// TODO: again there are no names in the docs
|
||||
Self::Commit(_name) => "COMMIT;".to_string(),
|
||||
Self::CreateIndex {
|
||||
unique,
|
||||
if_not_exists,
|
||||
idx_name,
|
||||
tbl_name,
|
||||
columns,
|
||||
where_clause,
|
||||
} => format!(
|
||||
"CREATE {}INDEX {}{} ON {} ({}){};",
|
||||
unique.then_some("UNIQUE ").unwrap_or(""),
|
||||
if_not_exists.then_some("IF NOT EXISTS ").unwrap_or(""),
|
||||
idx_name.to_sql_string(context),
|
||||
tbl_name.0,
|
||||
columns
|
||||
.iter()
|
||||
.map(|col| col.to_sql_string(context))
|
||||
.collect::<Vec<_>>()
|
||||
.join(", "),
|
||||
where_clause
|
||||
.as_ref()
|
||||
.map_or("".to_string(), |where_clause| format!(
|
||||
" WHERE {}",
|
||||
where_clause.to_sql_string(context)
|
||||
))
|
||||
),
|
||||
Self::CreateTable {
|
||||
temporary,
|
||||
if_not_exists,
|
||||
tbl_name,
|
||||
body,
|
||||
} => format!(
|
||||
"CREATE{} TABLE {}{} {};",
|
||||
temporary.then_some(" TEMP").unwrap_or(""),
|
||||
if_not_exists.then_some("IF NOT EXISTS ").unwrap_or(""),
|
||||
tbl_name.to_sql_string(context),
|
||||
body.to_sql_string(context)
|
||||
),
|
||||
Self::CreateTrigger(trigger) => trigger.to_sql_string(context),
|
||||
Self::CreateView {
|
||||
temporary,
|
||||
if_not_exists,
|
||||
view_name,
|
||||
columns,
|
||||
select,
|
||||
} => {
|
||||
format!(
|
||||
"CREATE{} VIEW {}{}{} AS {};",
|
||||
temporary.then_some(" TEMP").unwrap_or(""),
|
||||
if_not_exists.then_some("IF NOT EXISTS ").unwrap_or(""),
|
||||
view_name.to_sql_string(context),
|
||||
columns.as_ref().map_or("".to_string(), |columns| format!(
|
||||
" ({})",
|
||||
columns
|
||||
.iter()
|
||||
.map(|col| col.to_string())
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ")
|
||||
)),
|
||||
select.to_sql_string(context)
|
||||
)
|
||||
}
|
||||
Self::CreateVirtualTable(create_virtual_table) => {
|
||||
create_virtual_table.to_sql_string(context)
|
||||
}
|
||||
Self::Delete(delete) => delete.to_sql_string(context),
|
||||
Self::Detach(name) => format!("DETACH {};", name.to_sql_string(context)),
|
||||
Self::DropIndex {
|
||||
if_exists,
|
||||
idx_name,
|
||||
} => format!(
|
||||
"DROP INDEX{} {};",
|
||||
if_exists.then_some("IF EXISTS ").unwrap_or(""),
|
||||
idx_name.to_sql_string(context)
|
||||
),
|
||||
Self::DropTable {
|
||||
if_exists,
|
||||
tbl_name,
|
||||
} => format!(
|
||||
"DROP TABLE{} {};",
|
||||
if_exists.then_some("IF EXISTS ").unwrap_or(""),
|
||||
tbl_name.to_sql_string(context)
|
||||
),
|
||||
Self::DropTrigger {
|
||||
if_exists,
|
||||
trigger_name,
|
||||
} => format!(
|
||||
"DROP TRIGGER{} {};",
|
||||
if_exists.then_some("IF EXISTS ").unwrap_or(""),
|
||||
trigger_name.to_sql_string(context)
|
||||
),
|
||||
Self::DropView {
|
||||
if_exists,
|
||||
view_name,
|
||||
} => format!(
|
||||
"DROP VIEW{} {};",
|
||||
if_exists.then_some("IF EXISTS ").unwrap_or(""),
|
||||
view_name.to_sql_string(context)
|
||||
),
|
||||
Self::Insert(insert) => format!("{};", insert.to_sql_string(context)),
|
||||
Self::Pragma(name, body) => format!(
|
||||
"PRAGMA {}{};",
|
||||
name.to_sql_string(context),
|
||||
body.as_ref()
|
||||
.map_or("".to_string(), |body| match body.as_ref() {
|
||||
ast::PragmaBody::Equals(expr) =>
|
||||
format!(" = {}", expr.to_sql_string(context)),
|
||||
ast::PragmaBody::Call(expr) => format!("({})", expr.to_sql_string(context)),
|
||||
})
|
||||
),
|
||||
// TODO: missing collation name
|
||||
Self::Reindex { obj_name } => format!(
|
||||
"REINDEX{};",
|
||||
obj_name.as_ref().map_or("".to_string(), |name| format!(
|
||||
" {}",
|
||||
name.to_sql_string(context)
|
||||
))
|
||||
),
|
||||
Self::Release(name) => format!("RELEASE {};", name.0),
|
||||
Self::Rollback {
|
||||
// TODO: there is no transaction name in SQLITE
|
||||
// https://www.sqlite.org/lang_transaction.html
|
||||
tx_name: _,
|
||||
savepoint_name,
|
||||
} => format!(
|
||||
"ROLLBACK{};",
|
||||
savepoint_name
|
||||
.as_ref()
|
||||
.map_or("".to_string(), |name| format!(" TO {}", name.0))
|
||||
),
|
||||
Self::Savepoint(name) => format!("SAVEPOINT {};", name.0),
|
||||
Self::Select(select) => format!("{};", select.to_sql_string(context)),
|
||||
Self::Update(update) => format!("{};", update.to_sql_string(context)),
|
||||
Self::Vacuum(name, expr) => {
|
||||
format!(
|
||||
"VACUUM{}{};",
|
||||
name.as_ref()
|
||||
.map_or("".to_string(), |name| format!(" {}", name.0)),
|
||||
expr.as_ref().map_or("".to_string(), |expr| format!(
|
||||
" INTO {}",
|
||||
expr.to_sql_string(context)
|
||||
))
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::to_sql_string::ToSqlContext;
|
||||
@@ -211,6 +17,7 @@ mod tests {
|
||||
($test_name:ident, $input:expr) => {
|
||||
#[test]
|
||||
fn $test_name() {
|
||||
use $crate::parser::ast::fmt::ToTokens;
|
||||
let context = $crate::to_sql_string::stmt::tests::TestContext;
|
||||
let input = $input.split_whitespace().collect::<Vec<&str>>().join(" ");
|
||||
let mut parser = $crate::lexer::sql::Parser::new(input.as_bytes());
|
||||
@@ -219,7 +26,7 @@ mod tests {
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
input,
|
||||
$crate::to_sql_string::ToSqlString::to_sql_string(cmd.stmt(), &context)
|
||||
cmd.stmt().format_with_context(&context).unwrap().replace('\n', " "),
|
||||
);
|
||||
}
|
||||
};
|
||||
@@ -227,6 +34,7 @@ mod tests {
|
||||
#[test]
|
||||
$(#[$attribute])*
|
||||
fn $test_name() {
|
||||
use $crate::parser::ast::fmt::ToTokens;
|
||||
let context = $crate::to_sql_string::stmt::tests::TestContext;
|
||||
let input = $input.split_whitespace().collect::<Vec<&str>>().join(" ");
|
||||
let mut parser = $crate::lexer::sql::Parser::new(input.as_bytes());
|
||||
@@ -235,7 +43,7 @@ mod tests {
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
input,
|
||||
$crate::to_sql_string::ToSqlString::to_sql_string(cmd.stmt(), &context)
|
||||
cmd.stmt().format_with_context(&context).unwrap().replace('\n', " "),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -255,163 +63,163 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
to_sql_string_test!(test_analyze, "ANALYZE;");
|
||||
to_sql_string_test!(test_analyze, "ANALYZE");
|
||||
|
||||
to_sql_string_test!(
|
||||
test_analyze_table,
|
||||
"ANALYZE table;",
|
||||
"ANALYZE table",
|
||||
ignore = "parser can't parse table name"
|
||||
);
|
||||
|
||||
to_sql_string_test!(
|
||||
test_analyze_schema_table,
|
||||
"ANALYZE schema.table;",
|
||||
"ANALYZE schema.table",
|
||||
ignore = "parser can't parse schema.table name"
|
||||
);
|
||||
|
||||
to_sql_string_test!(test_attach, "ATTACH './test.db' AS test_db;");
|
||||
to_sql_string_test!(test_attach, "ATTACH './test.db' AS test_db");
|
||||
|
||||
to_sql_string_test!(test_transaction, "BEGIN;");
|
||||
to_sql_string_test!(test_transaction, "BEGIN");
|
||||
|
||||
to_sql_string_test!(test_transaction_deferred, "BEGIN DEFERRED;");
|
||||
to_sql_string_test!(test_transaction_deferred, "BEGIN DEFERRED");
|
||||
|
||||
to_sql_string_test!(test_transaction_immediate, "BEGIN IMMEDIATE;");
|
||||
to_sql_string_test!(test_transaction_immediate, "BEGIN IMMEDIATE");
|
||||
|
||||
to_sql_string_test!(test_transaction_exclusive, "BEGIN EXCLUSIVE;");
|
||||
to_sql_string_test!(test_transaction_exclusive, "BEGIN EXCLUSIVE");
|
||||
|
||||
to_sql_string_test!(test_commit, "COMMIT;");
|
||||
to_sql_string_test!(test_commit, "COMMIT");
|
||||
|
||||
// Test a simple index on a single column
|
||||
to_sql_string_test!(
|
||||
test_create_index_simple,
|
||||
"CREATE INDEX idx_name ON employees (last_name);"
|
||||
"CREATE INDEX idx_name ON employees (last_name)"
|
||||
);
|
||||
|
||||
// Test a unique index to enforce uniqueness on a column
|
||||
to_sql_string_test!(
|
||||
test_create_unique_index,
|
||||
"CREATE UNIQUE INDEX idx_unique_email ON users (email);"
|
||||
"CREATE UNIQUE INDEX idx_unique_email ON users (email)"
|
||||
);
|
||||
|
||||
// Test a multi-column index
|
||||
to_sql_string_test!(
|
||||
test_create_index_multi_column,
|
||||
"CREATE INDEX idx_name_salary ON employees (last_name, salary);"
|
||||
"CREATE INDEX idx_name_salary ON employees (last_name, salary)"
|
||||
);
|
||||
|
||||
// Test a partial index with a WHERE clause
|
||||
to_sql_string_test!(
|
||||
test_create_partial_index,
|
||||
"CREATE INDEX idx_active_users ON users (username) WHERE active = true;"
|
||||
"CREATE INDEX idx_active_users ON users (username) WHERE active = true"
|
||||
);
|
||||
|
||||
// Test an index on an expression
|
||||
to_sql_string_test!(
|
||||
test_create_index_on_expression,
|
||||
"CREATE INDEX idx_upper_name ON employees (UPPER(last_name));"
|
||||
"CREATE INDEX idx_upper_name ON employees (UPPER (last_name))"
|
||||
);
|
||||
|
||||
// Test an index with descending order
|
||||
to_sql_string_test!(
|
||||
test_create_index_descending,
|
||||
"CREATE INDEX idx_salary_desc ON employees (salary DESC);"
|
||||
"CREATE INDEX idx_salary_desc ON employees (salary DESC)"
|
||||
);
|
||||
|
||||
// Test an index with mixed ascending and descending orders on multiple columns
|
||||
to_sql_string_test!(
|
||||
test_create_index_mixed_order,
|
||||
"CREATE INDEX idx_name_asc_salary_desc ON employees (last_name ASC, salary DESC);"
|
||||
"CREATE INDEX idx_name_asc_salary_desc ON employees (last_name ASC, salary DESC)"
|
||||
);
|
||||
|
||||
// Test 1: View with DISTINCT keyword
|
||||
to_sql_string_test!(
|
||||
test_create_view_distinct,
|
||||
"CREATE VIEW view_distinct AS SELECT DISTINCT name FROM employees;"
|
||||
"CREATE VIEW view_distinct AS SELECT DISTINCT name FROM employees"
|
||||
);
|
||||
|
||||
// Test 2: View with LIMIT clause
|
||||
to_sql_string_test!(
|
||||
test_create_view_limit,
|
||||
"CREATE VIEW view_limit AS SELECT id, name FROM employees LIMIT 10;"
|
||||
"CREATE VIEW view_limit AS SELECT id, name FROM employees LIMIT 10"
|
||||
);
|
||||
|
||||
// Test 3: View with CASE expression
|
||||
to_sql_string_test!(
|
||||
test_create_view_case,
|
||||
"CREATE VIEW view_case AS SELECT name, CASE WHEN salary > 70000 THEN 'High' ELSE 'Low' END AS salary_level FROM employees;"
|
||||
"CREATE VIEW view_case AS SELECT name, CASE WHEN salary > 70000 THEN 'High' ELSE 'Low' END AS salary_level FROM employees"
|
||||
);
|
||||
|
||||
// Test 4: View with LEFT JOIN
|
||||
to_sql_string_test!(
|
||||
test_create_view_left_join,
|
||||
"CREATE VIEW view_left_join AS SELECT e.name, d.name AS department FROM employees e LEFT JOIN departments d ON e.department_id = d.id;"
|
||||
"CREATE VIEW view_left_join AS SELECT e.name, d.name AS department FROM employees e LEFT OUTER JOIN departments d ON e.department_id = d.id"
|
||||
);
|
||||
|
||||
// Test 5: View with HAVING clause
|
||||
to_sql_string_test!(
|
||||
test_create_view_having,
|
||||
"CREATE VIEW view_having AS SELECT department_id, AVG(salary) AS avg_salary FROM employees GROUP BY department_id HAVING AVG(salary) > 55000;"
|
||||
"CREATE VIEW view_having AS SELECT department_id, AVG (salary) AS avg_salary FROM employees GROUP BY department_id HAVING AVG (salary) > 55000"
|
||||
);
|
||||
|
||||
// Test 6: View with CTE (Common Table Expression)
|
||||
to_sql_string_test!(
|
||||
test_create_view_cte,
|
||||
"CREATE VIEW view_cte AS WITH high_earners AS (SELECT * FROM employees WHERE salary > 80000) SELECT id, name FROM high_earners;"
|
||||
"CREATE VIEW view_cte AS WITH high_earners AS (SELECT * FROM employees WHERE salary > 80000) SELECT id, name FROM high_earners"
|
||||
);
|
||||
|
||||
// Test 7: View with multiple conditions in WHERE
|
||||
to_sql_string_test!(
|
||||
test_create_view_multi_where,
|
||||
"CREATE VIEW view_multi_where AS SELECT id, name FROM employees WHERE salary > 50000 AND department_id = 3;"
|
||||
"CREATE VIEW view_multi_where AS SELECT id, name FROM employees WHERE salary > 50000 AND department_id = 3"
|
||||
);
|
||||
|
||||
// Test 8: View with NULL handling
|
||||
to_sql_string_test!(
|
||||
test_create_view_null,
|
||||
"CREATE VIEW view_null AS SELECT name, COALESCE(salary, 0) AS salary FROM employees;"
|
||||
"CREATE VIEW view_null AS SELECT name, COALESCE (salary, 0) AS salary FROM employees"
|
||||
);
|
||||
|
||||
// Test 9: View with subquery in WHERE clause
|
||||
to_sql_string_test!(
|
||||
test_create_view_subquery_where,
|
||||
"CREATE VIEW view_subquery_where AS SELECT name FROM employees WHERE department_id IN (SELECT id FROM departments WHERE name = 'Sales');"
|
||||
"CREATE VIEW view_subquery_where AS SELECT name FROM employees WHERE department_id IN (SELECT id FROM departments WHERE name = 'Sales')"
|
||||
);
|
||||
|
||||
// Test 10: View with arithmetic expression
|
||||
to_sql_string_test!(
|
||||
test_create_view_arithmetic,
|
||||
"CREATE VIEW view_arithmetic AS SELECT name, salary * 1.1 AS adjusted_salary FROM employees;"
|
||||
"CREATE VIEW view_arithmetic AS SELECT name, salary * 1.1 AS adjusted_salary FROM employees"
|
||||
);
|
||||
|
||||
to_sql_string_test!(test_detach, "DETACH 'x.db';");
|
||||
to_sql_string_test!(test_detach, "DETACH 'x.db'");
|
||||
|
||||
to_sql_string_test!(test_drop_index, "DROP INDEX schema_name.test_index;");
|
||||
to_sql_string_test!(test_drop_index, "DROP INDEX schema_name.test_index");
|
||||
|
||||
to_sql_string_test!(test_drop_table, "DROP TABLE schema_name.test_table;");
|
||||
to_sql_string_test!(test_drop_table, "DROP TABLE schema_name.test_table");
|
||||
|
||||
to_sql_string_test!(test_drop_trigger, "DROP TRIGGER schema_name.test_trigger;");
|
||||
to_sql_string_test!(test_drop_trigger, "DROP TRIGGER schema_name.test_trigger");
|
||||
|
||||
to_sql_string_test!(test_drop_view, "DROP VIEW schema_name.test_view;");
|
||||
to_sql_string_test!(test_drop_view, "DROP VIEW schema_name.test_view");
|
||||
|
||||
to_sql_string_test!(test_pragma_equals, "PRAGMA schema_name.Pragma_name = 1;");
|
||||
to_sql_string_test!(test_pragma_equals, "PRAGMA schema_name.Pragma_name = 1");
|
||||
|
||||
to_sql_string_test!(test_pragma_call, "PRAGMA schema_name.Pragma_name_2(1);");
|
||||
to_sql_string_test!(test_pragma_call, "PRAGMA schema_name.Pragma_name_2 (1)");
|
||||
|
||||
to_sql_string_test!(test_reindex, "REINDEX schema_name.test_table;");
|
||||
to_sql_string_test!(test_reindex, "REINDEX schema_name.test_table");
|
||||
|
||||
to_sql_string_test!(test_reindex_2, "REINDEX;");
|
||||
to_sql_string_test!(test_reindex_2, "REINDEX");
|
||||
|
||||
to_sql_string_test!(test_release, "RELEASE savepoint_name;");
|
||||
to_sql_string_test!(test_release, "RELEASE savepoint_name");
|
||||
|
||||
to_sql_string_test!(test_rollback, "ROLLBACK;");
|
||||
to_sql_string_test!(test_rollback, "ROLLBACK");
|
||||
|
||||
to_sql_string_test!(test_rollback_2, "ROLLBACK TO savepoint_name;");
|
||||
to_sql_string_test!(test_rollback_2, "ROLLBACK TO savepoint_name");
|
||||
|
||||
to_sql_string_test!(test_savepoint, "SAVEPOINT savepoint_name;");
|
||||
to_sql_string_test!(test_savepoint, "SAVEPOINT savepoint_name");
|
||||
|
||||
to_sql_string_test!(test_vacuum, "VACUUM;");
|
||||
to_sql_string_test!(test_vacuum, "VACUUM");
|
||||
|
||||
to_sql_string_test!(test_vacuum_2, "VACUUM schema_name;");
|
||||
to_sql_string_test!(test_vacuum_2, "VACUUM schema_name");
|
||||
|
||||
to_sql_string_test!(test_vacuum_3, "VACUUM schema_name INTO test.db;");
|
||||
to_sql_string_test!(test_vacuum_3, "VACUUM schema_name INTO test.db");
|
||||
}
|
||||
|
||||
@@ -1,658 +1,122 @@
|
||||
use std::fmt::Display;
|
||||
|
||||
use crate::{
|
||||
ast::{self, fmt::ToTokens},
|
||||
to_sql_string::{ToSqlContext, ToSqlString},
|
||||
};
|
||||
|
||||
impl ToSqlString for ast::Select {
|
||||
fn to_sql_string<C: ToSqlContext>(&self, context: &C) -> String {
|
||||
let mut ret = Vec::new();
|
||||
if let Some(with) = &self.with {
|
||||
ret.push(with.to_sql_string(context));
|
||||
}
|
||||
|
||||
ret.push(self.body.to_sql_string(context));
|
||||
|
||||
if let Some(order_by) = &self.order_by {
|
||||
// TODO: SortedColumn missing collation in ast
|
||||
let joined_cols = order_by
|
||||
.iter()
|
||||
.map(|col| col.to_sql_string(context))
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ");
|
||||
ret.push(format!("ORDER BY {joined_cols}"));
|
||||
}
|
||||
if let Some(limit) = &self.limit {
|
||||
ret.push(limit.to_sql_string(context));
|
||||
}
|
||||
ret.join(" ")
|
||||
}
|
||||
}
|
||||
|
||||
impl ToSqlString for ast::SelectBody {
|
||||
fn to_sql_string<C: ToSqlContext>(&self, context: &C) -> String {
|
||||
let mut ret = self.select.to_sql_string(context);
|
||||
|
||||
if let Some(compounds) = &self.compounds {
|
||||
ret.push(' ');
|
||||
let compound_selects = compounds
|
||||
.iter()
|
||||
.map(|compound_select| {
|
||||
let mut curr = compound_select.operator.to_string();
|
||||
curr.push(' ');
|
||||
curr.push_str(&compound_select.select.to_sql_string(context));
|
||||
curr
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.join(" ");
|
||||
ret.push_str(&compound_selects);
|
||||
}
|
||||
ret
|
||||
}
|
||||
}
|
||||
|
||||
impl ToSqlString for ast::OneSelect {
|
||||
fn to_sql_string<C: ToSqlContext>(&self, context: &C) -> String {
|
||||
match self {
|
||||
ast::OneSelect::Select(select) => select.to_sql_string(context),
|
||||
ast::OneSelect::Values(values) => {
|
||||
let joined_values = values
|
||||
.iter()
|
||||
.map(|value| {
|
||||
let joined_value = value
|
||||
.iter()
|
||||
.map(|e| e.to_sql_string(context))
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ");
|
||||
format!("({joined_value})")
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ");
|
||||
format!("VALUES {joined_values}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ToSqlString for ast::SelectInner {
|
||||
fn to_sql_string<C: ToSqlContext>(&self, context: &C) -> String {
|
||||
// dbg!(&self);
|
||||
let mut ret = Vec::with_capacity(2 + self.columns.len());
|
||||
ret.push("SELECT".to_string());
|
||||
if let Some(distinct) = self.distinctness {
|
||||
ret.push(distinct.to_string());
|
||||
}
|
||||
let joined_cols = self
|
||||
.columns
|
||||
.iter()
|
||||
.map(|col| col.to_sql_string(context))
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ");
|
||||
ret.push(joined_cols);
|
||||
|
||||
if let Some(from) = &self.from {
|
||||
ret.push(from.to_sql_string(context));
|
||||
}
|
||||
if let Some(where_expr) = &self.where_clause {
|
||||
ret.push("WHERE".to_string());
|
||||
ret.push(where_expr.to_sql_string(context));
|
||||
}
|
||||
if let Some(group_by) = &self.group_by {
|
||||
ret.push(group_by.to_sql_string(context));
|
||||
}
|
||||
if let Some(window_clause) = &self.window_clause {
|
||||
ret.push("WINDOW".to_string());
|
||||
let joined_window = window_clause
|
||||
.iter()
|
||||
.map(|window_def| window_def.to_sql_string(context))
|
||||
.collect::<Vec<_>>()
|
||||
.join(",");
|
||||
ret.push(joined_window);
|
||||
}
|
||||
|
||||
ret.join(" ")
|
||||
}
|
||||
}
|
||||
|
||||
impl ToSqlString for ast::FromClause {
|
||||
fn to_sql_string<C: ToSqlContext>(&self, context: &C) -> String {
|
||||
let mut ret = String::from("FROM");
|
||||
if let Some(select_table) = &self.select {
|
||||
ret.push(' ');
|
||||
ret.push_str(&select_table.to_sql_string(context));
|
||||
}
|
||||
if let Some(joins) = &self.joins {
|
||||
ret.push(' ');
|
||||
let joined_joins = joins
|
||||
.iter()
|
||||
.map(|join| {
|
||||
let mut curr = join.operator.to_string();
|
||||
curr.push(' ');
|
||||
curr.push_str(&join.table.to_sql_string(context));
|
||||
if let Some(join_constraint) = &join.constraint {
|
||||
curr.push(' ');
|
||||
curr.push_str(&join_constraint.to_sql_string(context));
|
||||
}
|
||||
curr
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.join(" ");
|
||||
ret.push_str(&joined_joins);
|
||||
}
|
||||
ret
|
||||
}
|
||||
}
|
||||
|
||||
impl ToSqlString for ast::SelectTable {
|
||||
fn to_sql_string<C: ToSqlContext>(&self, context: &C) -> String {
|
||||
let mut ret = String::new();
|
||||
match self {
|
||||
Self::Table(name, alias, indexed) => {
|
||||
ret.push_str(&name.to_sql_string(context));
|
||||
if let Some(alias) = alias {
|
||||
ret.push(' ');
|
||||
ret.push_str(&alias.to_string());
|
||||
}
|
||||
if let Some(indexed) = indexed {
|
||||
ret.push(' ');
|
||||
ret.push_str(&indexed.to_string());
|
||||
}
|
||||
}
|
||||
Self::TableCall(table_func, args, alias) => {
|
||||
ret.push_str(&table_func.to_sql_string(context));
|
||||
if let Some(args) = args {
|
||||
ret.push(' ');
|
||||
let joined_args = args
|
||||
.iter()
|
||||
.map(|arg| arg.to_sql_string(context))
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ");
|
||||
ret.push_str(&joined_args);
|
||||
}
|
||||
if let Some(alias) = alias {
|
||||
ret.push(' ');
|
||||
ret.push_str(&alias.to_string());
|
||||
}
|
||||
}
|
||||
Self::Select(select, alias) => {
|
||||
ret.push('(');
|
||||
ret.push_str(&select.to_sql_string(context));
|
||||
ret.push(')');
|
||||
if let Some(alias) = alias {
|
||||
ret.push(' ');
|
||||
ret.push_str(&alias.to_string());
|
||||
}
|
||||
}
|
||||
Self::Sub(from_clause, alias) => {
|
||||
ret.push('(');
|
||||
ret.push_str(&from_clause.to_sql_string(context));
|
||||
ret.push(')');
|
||||
if let Some(alias) = alias {
|
||||
ret.push(' ');
|
||||
ret.push_str(&alias.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
ret
|
||||
}
|
||||
}
|
||||
|
||||
impl ToSqlString for ast::With {
|
||||
fn to_sql_string<C: ToSqlContext>(&self, context: &C) -> String {
|
||||
format!(
|
||||
"WITH{} {}",
|
||||
if self.recursive { " RECURSIVE " } else { "" },
|
||||
self.ctes
|
||||
.iter()
|
||||
.map(|cte| cte.to_sql_string(context))
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ")
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl ToSqlString for ast::Limit {
|
||||
fn to_sql_string<C: ToSqlContext>(&self, context: &C) -> String {
|
||||
format!(
|
||||
"LIMIT {}{}",
|
||||
self.expr.to_sql_string(context),
|
||||
self.offset
|
||||
.as_ref()
|
||||
.map_or("".to_string(), |offset| format!(
|
||||
" OFFSET {}",
|
||||
offset.to_sql_string(context)
|
||||
))
|
||||
)
|
||||
// TODO: missing , + expr in ast
|
||||
}
|
||||
}
|
||||
|
||||
impl ToSqlString for ast::CommonTableExpr {
|
||||
fn to_sql_string<C: ToSqlContext>(&self, context: &C) -> String {
|
||||
let mut ret = Vec::with_capacity(self.columns.as_ref().map_or(2, |cols| cols.len()));
|
||||
ret.push(self.tbl_name.0.clone());
|
||||
if let Some(cols) = &self.columns {
|
||||
let joined_cols = cols
|
||||
.iter()
|
||||
.map(|col| col.to_string())
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ");
|
||||
|
||||
ret.push(format!("({joined_cols})"));
|
||||
}
|
||||
ret.push(format!(
|
||||
"AS {}({})",
|
||||
{
|
||||
let mut materialized = self.materialized.to_string();
|
||||
if !materialized.is_empty() {
|
||||
materialized.push(' ');
|
||||
}
|
||||
materialized
|
||||
},
|
||||
self.select.to_sql_string(context)
|
||||
));
|
||||
ret.join(" ")
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for ast::IndexedColumn {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{}", self.col_name.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl ToSqlString for ast::SortedColumn {
|
||||
fn to_sql_string<C: ToSqlContext>(&self, context: &C) -> String {
|
||||
let mut curr = self.expr.to_sql_string(context);
|
||||
if let Some(sort_order) = self.order {
|
||||
curr.push(' ');
|
||||
curr.push_str(&sort_order.to_string());
|
||||
}
|
||||
if let Some(nulls_order) = self.nulls {
|
||||
curr.push(' ');
|
||||
curr.push_str(&nulls_order.to_string());
|
||||
}
|
||||
curr
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for ast::SortOrder {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
self.to_fmt(f)
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for ast::NullsOrder {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
self.to_fmt(f)
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for ast::Materialized {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
let value = match self {
|
||||
Self::Any => "",
|
||||
Self::No => "NOT MATERIALIZED",
|
||||
Self::Yes => "MATERIALIZED",
|
||||
};
|
||||
write!(f, "{value}")
|
||||
}
|
||||
}
|
||||
|
||||
impl ToSqlString for ast::ResultColumn {
|
||||
fn to_sql_string<C: ToSqlContext>(&self, context: &C) -> String {
|
||||
let mut ret = String::new();
|
||||
match self {
|
||||
Self::Expr(expr, alias) => {
|
||||
ret.push_str(&expr.to_sql_string(context));
|
||||
if let Some(alias) = alias {
|
||||
ret.push(' ');
|
||||
ret.push_str(&alias.to_string());
|
||||
}
|
||||
}
|
||||
Self::Star => {
|
||||
ret.push('*');
|
||||
}
|
||||
Self::TableStar(name) => {
|
||||
ret.push_str(&format!("{}.*", name.0));
|
||||
}
|
||||
}
|
||||
ret
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for ast::As {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"{}",
|
||||
match self {
|
||||
Self::As(alias) => {
|
||||
format!("AS {}", alias.0)
|
||||
}
|
||||
Self::Elided(alias) => alias.0.clone(),
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for ast::Indexed {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"{}",
|
||||
match self {
|
||||
Self::NotIndexed => "NOT INDEXED".to_string(),
|
||||
Self::IndexedBy(name) => format!("INDEXED BY {}", name.0),
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for ast::JoinOperator {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"{}",
|
||||
match self {
|
||||
Self::Comma => ",".to_string(),
|
||||
Self::TypedJoin(join) => {
|
||||
let join_keyword = "JOIN";
|
||||
if let Some(join) = join {
|
||||
format!("{join} {join_keyword}")
|
||||
} else {
|
||||
join_keyword.to_string()
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for ast::JoinType {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
let value = {
|
||||
let mut modifiers = Vec::new();
|
||||
if self.contains(Self::NATURAL) {
|
||||
modifiers.push("NATURAL");
|
||||
}
|
||||
if self.contains(Self::LEFT) || self.contains(Self::RIGHT) {
|
||||
// TODO: I think the parser incorrectly asigns outer to every LEFT and RIGHT query
|
||||
if self.contains(Self::LEFT | Self::RIGHT) {
|
||||
modifiers.push("FULL");
|
||||
} else if self.contains(Self::LEFT) {
|
||||
modifiers.push("LEFT");
|
||||
} else if self.contains(Self::RIGHT) {
|
||||
modifiers.push("RIGHT");
|
||||
}
|
||||
// FIXME: ignore outer joins as I think they are parsed incorrectly in the bitflags
|
||||
// if self.contains(Self::OUTER) {
|
||||
// modifiers.push("OUTER");
|
||||
// }
|
||||
}
|
||||
|
||||
if self.contains(Self::INNER) {
|
||||
modifiers.push("INNER");
|
||||
}
|
||||
if self.contains(Self::CROSS) {
|
||||
modifiers.push("CROSS");
|
||||
}
|
||||
modifiers.join(" ")
|
||||
};
|
||||
write!(f, "{value}")
|
||||
}
|
||||
}
|
||||
|
||||
impl ToSqlString for ast::JoinConstraint {
|
||||
fn to_sql_string<C: ToSqlContext>(&self, context: &C) -> String {
|
||||
match self {
|
||||
Self::On(expr) => {
|
||||
format!("ON {}", expr.to_sql_string(context))
|
||||
}
|
||||
Self::Using(col_names) => {
|
||||
let joined_names = col_names
|
||||
.iter()
|
||||
.map(|col| col.0.clone())
|
||||
.collect::<Vec<_>>()
|
||||
.join(",");
|
||||
format!("USING ({joined_names})")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ToSqlString for ast::GroupBy {
|
||||
fn to_sql_string<C: ToSqlContext>(&self, context: &C) -> String {
|
||||
let mut ret = String::from("GROUP BY ");
|
||||
let curr = self
|
||||
.exprs
|
||||
.iter()
|
||||
.map(|expr| expr.to_sql_string(context))
|
||||
.collect::<Vec<_>>()
|
||||
.join(",");
|
||||
ret.push_str(&curr);
|
||||
if let Some(having) = &self.having {
|
||||
ret.push_str(&format!(" HAVING {}", having.to_sql_string(context)));
|
||||
}
|
||||
ret
|
||||
}
|
||||
}
|
||||
|
||||
impl ToSqlString for ast::WindowDef {
|
||||
fn to_sql_string<C: ToSqlContext>(&self, context: &C) -> String {
|
||||
format!("{} AS {}", self.name.0, self.window.to_sql_string(context))
|
||||
}
|
||||
}
|
||||
|
||||
impl ToSqlString for ast::Window {
|
||||
fn to_sql_string<C: ToSqlContext>(&self, context: &C) -> String {
|
||||
let mut ret = Vec::new();
|
||||
if let Some(name) = &self.base {
|
||||
ret.push(name.0.clone());
|
||||
}
|
||||
if let Some(partition) = &self.partition_by {
|
||||
let joined_exprs = partition
|
||||
.iter()
|
||||
.map(|e| e.to_sql_string(context))
|
||||
.collect::<Vec<_>>()
|
||||
.join(",");
|
||||
ret.push(format!("PARTITION BY {joined_exprs}"));
|
||||
}
|
||||
if let Some(order_by) = &self.order_by {
|
||||
let joined_cols = order_by
|
||||
.iter()
|
||||
.map(|col| col.to_sql_string(context))
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ");
|
||||
ret.push(format!("ORDER BY {joined_cols}"));
|
||||
}
|
||||
if let Some(frame_claue) = &self.frame_clause {
|
||||
ret.push(frame_claue.to_sql_string(context));
|
||||
}
|
||||
format!("({})", ret.join(" "))
|
||||
}
|
||||
}
|
||||
|
||||
impl ToSqlString for ast::FrameClause {
|
||||
fn to_sql_string<C: ToSqlContext>(&self, context: &C) -> String {
|
||||
let mut ret = Vec::new();
|
||||
ret.push(self.mode.to_string());
|
||||
let start_sql = self.start.to_sql_string(context);
|
||||
if let Some(end) = &self.end {
|
||||
ret.push(format!(
|
||||
"BETWEEN {} AND {}",
|
||||
start_sql,
|
||||
end.to_sql_string(context)
|
||||
));
|
||||
} else {
|
||||
ret.push(start_sql);
|
||||
}
|
||||
if let Some(exclude) = &self.exclude {
|
||||
ret.push(exclude.to_string());
|
||||
}
|
||||
|
||||
ret.join(" ")
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for ast::FrameMode {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
self.to_fmt(f)
|
||||
}
|
||||
}
|
||||
|
||||
impl ToSqlString for ast::FrameBound {
|
||||
fn to_sql_string<C: ToSqlContext>(&self, context: &C) -> String {
|
||||
match self {
|
||||
Self::CurrentRow => "CURRENT ROW".to_string(),
|
||||
Self::Following(expr) => format!("{} FOLLOWING", expr.to_sql_string(context)),
|
||||
Self::Preceding(expr) => format!("{} PRECEDING", expr.to_sql_string(context)),
|
||||
Self::UnboundedFollowing => "UNBOUNDED FOLLOWING".to_string(),
|
||||
Self::UnboundedPreceding => "UNBOUNDED PRECEDING".to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for ast::FrameExclude {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{}", {
|
||||
let clause = match self {
|
||||
Self::CurrentRow => "CURRENT ROW",
|
||||
Self::Group => "GROUP",
|
||||
Self::NoOthers => "NO OTHERS",
|
||||
Self::Ties => "TIES",
|
||||
};
|
||||
format!("EXCLUDE {clause}")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::to_sql_string_test;
|
||||
|
||||
to_sql_string_test!(test_select_basic, "SELECT 1;");
|
||||
to_sql_string_test!(test_select_basic, "SELECT 1");
|
||||
|
||||
to_sql_string_test!(test_select_table, "SELECT * FROM t;");
|
||||
to_sql_string_test!(test_select_table, "SELECT * FROM t");
|
||||
|
||||
to_sql_string_test!(test_select_table_2, "SELECT a FROM t;");
|
||||
to_sql_string_test!(test_select_table_2, "SELECT a FROM t");
|
||||
|
||||
to_sql_string_test!(test_select_multiple_columns, "SELECT a, b, c FROM t;");
|
||||
to_sql_string_test!(test_select_multiple_columns, "SELECT a, b, c FROM t");
|
||||
|
||||
to_sql_string_test!(test_select_with_alias, "SELECT a AS col1 FROM t;");
|
||||
to_sql_string_test!(test_select_with_alias, "SELECT a AS col1 FROM t");
|
||||
|
||||
to_sql_string_test!(test_select_with_table_alias, "SELECT t1.a FROM t AS t1;");
|
||||
to_sql_string_test!(test_select_with_table_alias, "SELECT t1.a FROM t AS t1");
|
||||
|
||||
to_sql_string_test!(test_select_with_where, "SELECT a FROM t WHERE b = 1;");
|
||||
to_sql_string_test!(test_select_with_where, "SELECT a FROM t WHERE b = 1");
|
||||
|
||||
to_sql_string_test!(
|
||||
test_select_with_multiple_conditions,
|
||||
"SELECT a FROM t WHERE b = 1 AND c > 2;"
|
||||
"SELECT a FROM t WHERE b = 1 AND c > 2"
|
||||
);
|
||||
|
||||
to_sql_string_test!(
|
||||
test_select_with_order_by,
|
||||
"SELECT a FROM t ORDER BY a DESC;"
|
||||
);
|
||||
to_sql_string_test!(test_select_with_order_by, "SELECT a FROM t ORDER BY a DESC");
|
||||
|
||||
to_sql_string_test!(test_select_with_limit, "SELECT a FROM t LIMIT 10;");
|
||||
to_sql_string_test!(test_select_with_limit, "SELECT a FROM t LIMIT 10");
|
||||
|
||||
to_sql_string_test!(
|
||||
test_select_with_offset,
|
||||
"SELECT a FROM t LIMIT 10 OFFSET 5;"
|
||||
);
|
||||
to_sql_string_test!(test_select_with_offset, "SELECT a FROM t LIMIT 10 OFFSET 5");
|
||||
|
||||
to_sql_string_test!(
|
||||
test_select_with_join,
|
||||
"SELECT a FROM t JOIN t2 ON t.b = t2.b;"
|
||||
"SELECT a FROM t JOIN t2 ON t.b = t2.b"
|
||||
);
|
||||
|
||||
to_sql_string_test!(
|
||||
test_select_with_group_by,
|
||||
"SELECT a, COUNT(*) FROM t GROUP BY a;"
|
||||
"SELECT a, COUNT (*) FROM t GROUP BY a"
|
||||
);
|
||||
|
||||
to_sql_string_test!(
|
||||
test_select_with_having,
|
||||
"SELECT a, COUNT(*) FROM t GROUP BY a HAVING COUNT(*) > 1;"
|
||||
"SELECT a, COUNT (*) FROM t GROUP BY a HAVING COUNT (*) > 1"
|
||||
);
|
||||
|
||||
to_sql_string_test!(test_select_with_distinct, "SELECT DISTINCT a FROM t;");
|
||||
to_sql_string_test!(test_select_with_distinct, "SELECT DISTINCT a FROM t");
|
||||
|
||||
to_sql_string_test!(test_select_with_function, "SELECT COUNT(a) FROM t;");
|
||||
to_sql_string_test!(test_select_with_function, "SELECT COUNT (a) FROM t");
|
||||
|
||||
to_sql_string_test!(
|
||||
test_select_with_subquery,
|
||||
"SELECT a FROM (SELECT b FROM t) AS sub;"
|
||||
"SELECT a FROM (SELECT b FROM t) AS sub"
|
||||
);
|
||||
|
||||
to_sql_string_test!(
|
||||
test_select_nested_subquery,
|
||||
"SELECT a FROM (SELECT b FROM (SELECT c FROM t WHERE c > 10) AS sub1 WHERE b < 20) AS sub2;"
|
||||
"SELECT a FROM (SELECT b FROM (SELECT c FROM t WHERE c > 10) AS sub1 WHERE b < 20) AS sub2"
|
||||
);
|
||||
|
||||
to_sql_string_test!(
|
||||
test_select_multiple_joins,
|
||||
"SELECT t1.a, t2.b, t3.c FROM t1 JOIN t2 ON t1.id = t2.id LEFT JOIN t3 ON t2.id = t3.id;"
|
||||
"SELECT t1.a, t2.b, t3.c FROM t1 JOIN t2 ON t1.id = t2.id LEFT OUTER JOIN t3 ON t2.id = t3.id"
|
||||
);
|
||||
|
||||
to_sql_string_test!(
|
||||
test_select_with_cte,
|
||||
"WITH cte AS (SELECT a FROM t WHERE b = 1) SELECT a FROM cte WHERE a > 10;"
|
||||
"WITH cte AS (SELECT a FROM t WHERE b = 1) SELECT a FROM cte WHERE a > 10"
|
||||
);
|
||||
|
||||
to_sql_string_test!(
|
||||
test_select_with_window_function,
|
||||
"SELECT a, ROW_NUMBER() OVER (PARTITION BY b ORDER BY c DESC) AS rn FROM t;"
|
||||
"SELECT a, ROW_NUMBER () OVER (PARTITION BY b ORDER BY c DESC) AS rn FROM t"
|
||||
);
|
||||
|
||||
to_sql_string_test!(
|
||||
test_select_with_complex_where,
|
||||
"SELECT a FROM t WHERE b IN (1, 2, 3) AND c BETWEEN 10 AND 20 OR d IS NULL;"
|
||||
"SELECT a FROM t WHERE b IN (1, 2, 3) AND c BETWEEN 10 AND 20 OR d IS NULL"
|
||||
);
|
||||
|
||||
to_sql_string_test!(
|
||||
test_select_with_case,
|
||||
"SELECT CASE WHEN a > 0 THEN 'positive' ELSE 'non-positive' END AS result FROM t;"
|
||||
"SELECT CASE WHEN a > 0 THEN 'positive' ELSE 'non-positive' END AS result FROM t"
|
||||
);
|
||||
|
||||
to_sql_string_test!(test_select_with_aggregate_and_join, "SELECT t1.a, COUNT(t2.b) FROM t1 LEFT JOIN t2 ON t1.id = t2.id GROUP BY t1.a HAVING COUNT(t2.b) > 5;");
|
||||
to_sql_string_test!(test_select_with_aggregate_and_join, "SELECT t1.a, COUNT (t2.b) FROM t1 LEFT OUTER JOIN t2 ON t1.id = t2.id GROUP BY t1.a HAVING COUNT (t2.b) > 5");
|
||||
|
||||
to_sql_string_test!(test_select_with_multiple_ctes, "WITH cte1 AS (SELECT a FROM t WHERE b = 1), cte2 AS (SELECT c FROM t2 WHERE d = 2) SELECT cte1.a, cte2.c FROM cte1 JOIN cte2 ON cte1.a = cte2.c;");
|
||||
to_sql_string_test!(test_select_with_multiple_ctes, "WITH cte1 AS (SELECT a FROM t WHERE b = 1), cte2 AS (SELECT c FROM t2 WHERE d = 2) SELECT cte1.a, cte2.c FROM cte1 JOIN cte2 ON cte1.a = cte2.c");
|
||||
|
||||
to_sql_string_test!(
|
||||
test_select_with_union,
|
||||
"SELECT a FROM t1 UNION SELECT b FROM t2;"
|
||||
"SELECT a FROM t1 UNION SELECT b FROM t2"
|
||||
);
|
||||
|
||||
to_sql_string_test!(
|
||||
test_select_with_union_all,
|
||||
"SELECT a FROM t1 UNION ALL SELECT b FROM t2;"
|
||||
"SELECT a FROM t1 UNION ALL SELECT b FROM t2"
|
||||
);
|
||||
|
||||
to_sql_string_test!(
|
||||
test_select_with_exists,
|
||||
"SELECT a FROM t WHERE EXISTS (SELECT 1 FROM t2 WHERE t2.b = t.a);"
|
||||
"SELECT a FROM t WHERE EXISTS (SELECT 1 FROM t2 WHERE t2.b = t.a)"
|
||||
);
|
||||
|
||||
to_sql_string_test!(
|
||||
test_select_with_correlated_subquery,
|
||||
"SELECT a, (SELECT COUNT(*) FROM t2 WHERE t2.b = t.a) AS count_b FROM t;"
|
||||
"SELECT a, (SELECT COUNT (*) FROM t2 WHERE t2.b = t.a) AS count_b FROM t"
|
||||
);
|
||||
|
||||
to_sql_string_test!(
|
||||
test_select_with_complex_order_by,
|
||||
"SELECT a, b FROM t ORDER BY CASE WHEN a IS NULL THEN 1 ELSE 0 END, b ASC, c DESC;"
|
||||
"SELECT a, b FROM t ORDER BY CASE WHEN a IS NULL THEN 1 ELSE 0 END, b ASC, c DESC"
|
||||
);
|
||||
|
||||
to_sql_string_test!(
|
||||
test_select_with_full_outer_join,
|
||||
"SELECT t1.a, t2.b FROM t1 FULL OUTER JOIN t2 ON t1.id = t2.id;",
|
||||
"SELECT t1.a, t2.b FROM t1 FULL OUTER JOIN t2 ON t1.id = t2.id",
|
||||
ignore = "OUTER JOIN is incorrectly parsed in parser"
|
||||
);
|
||||
|
||||
to_sql_string_test!(test_select_with_aggregate_window, "SELECT a, SUM(b) OVER (PARTITION BY c ORDER BY d ROWS BETWEEN 1 PRECEDING AND 1 FOLLOWING) AS running_sum FROM t;");
|
||||
to_sql_string_test!(test_select_with_aggregate_window, "SELECT a, SUM (b) OVER (PARTITION BY c ORDER BY d ROWS BETWEEN 1 PRECEDING AND 1 FOLLOWING) AS running_sum FROM t");
|
||||
|
||||
to_sql_string_test!(
|
||||
test_select_with_exclude,
|
||||
@@ -660,7 +124,7 @@ mod tests {
|
||||
c.name,
|
||||
o.order_id,
|
||||
o.order_amount,
|
||||
SUM(o.order_amount) OVER (PARTITION BY c.id
|
||||
SUM (o.order_amount) OVER (PARTITION BY c.id
|
||||
ORDER BY o.order_date
|
||||
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
|
||||
EXCLUDE CURRENT ROW) AS running_total_excluding_current
|
||||
@@ -669,6 +133,6 @@ JOIN orders o ON c.id = o.customer_id
|
||||
WHERE EXISTS (SELECT 1
|
||||
FROM orders o2
|
||||
WHERE o2.customer_id = c.id
|
||||
AND o2.order_amount > 1000);"
|
||||
AND o2.order_amount > 1000)"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,48 +1,3 @@
|
||||
use crate::{ast, to_sql_string::ToSqlString};
|
||||
|
||||
impl ToSqlString for ast::Update {
|
||||
fn to_sql_string<C: crate::to_sql_string::ToSqlContext>(&self, context: &C) -> String {
|
||||
format!(
|
||||
"{}UPDATE {}{}{} SET {}{}{}{}",
|
||||
self.with.as_ref().map_or("".to_string(), |with| format!(
|
||||
"{} ",
|
||||
with.to_sql_string(context)
|
||||
)),
|
||||
self.or_conflict
|
||||
.map_or("".to_string(), |conflict| format!("OR {conflict} ")),
|
||||
self.tbl_name.to_sql_string(context),
|
||||
self.indexed
|
||||
.as_ref()
|
||||
.map_or("".to_string(), |indexed| format!(" {indexed}")),
|
||||
self.sets
|
||||
.iter()
|
||||
.map(|set| set.to_sql_string(context))
|
||||
.collect::<Vec<_>>()
|
||||
.join(", "),
|
||||
self.from.as_ref().map_or("".to_string(), |from| format!(
|
||||
" {}",
|
||||
from.to_sql_string(context)
|
||||
)),
|
||||
self.where_clause
|
||||
.as_ref()
|
||||
.map_or("".to_string(), |expr| format!(
|
||||
" WHERE {}",
|
||||
expr.to_sql_string(context)
|
||||
)),
|
||||
self.returning
|
||||
.as_ref()
|
||||
.map_or("".to_string(), |returning| format!(
|
||||
" RETURNING {}",
|
||||
returning
|
||||
.iter()
|
||||
.map(|col| col.to_sql_string(context))
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ")
|
||||
))
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::to_sql_string_test;
|
||||
@@ -50,90 +5,90 @@ mod tests {
|
||||
// Basic UPDATE with a single column
|
||||
to_sql_string_test!(
|
||||
test_update_single_column,
|
||||
"UPDATE employees SET salary = 55000;"
|
||||
"UPDATE employees SET salary = 55000"
|
||||
);
|
||||
|
||||
// UPDATE with multiple columns
|
||||
to_sql_string_test!(
|
||||
test_update_multiple_columns,
|
||||
"UPDATE employees SET salary = 60000, name = 'John Smith';"
|
||||
"UPDATE employees SET salary = 60000, name = 'John Smith'"
|
||||
);
|
||||
|
||||
// UPDATE with a WHERE clause
|
||||
to_sql_string_test!(
|
||||
test_update_with_where,
|
||||
"UPDATE employees SET salary = 60000 WHERE id = 1;"
|
||||
"UPDATE employees SET salary = 60000 WHERE id = 1"
|
||||
);
|
||||
|
||||
// UPDATE with multiple WHERE conditions
|
||||
to_sql_string_test!(
|
||||
test_update_with_multi_where,
|
||||
"UPDATE employees SET salary = 65000 WHERE department_id = 3 AND salary < 50000;"
|
||||
"UPDATE employees SET salary = 65000 WHERE department_id = 3 AND salary < 50000"
|
||||
);
|
||||
|
||||
// UPDATE with a subquery in SET
|
||||
to_sql_string_test!(
|
||||
test_update_with_subquery_set,
|
||||
"UPDATE employees SET department_id = (SELECT id FROM departments WHERE name = 'Sales') WHERE id = 1;"
|
||||
"UPDATE employees SET department_id = (SELECT id FROM departments WHERE name = 'Sales') WHERE id = 1"
|
||||
);
|
||||
|
||||
// UPDATE with a subquery in WHERE
|
||||
to_sql_string_test!(
|
||||
test_update_with_subquery_where,
|
||||
"UPDATE employees SET salary = 70000 WHERE department_id IN (SELECT id FROM departments WHERE name = 'Marketing');"
|
||||
"UPDATE employees SET salary = 70000 WHERE department_id IN (SELECT id FROM departments WHERE name = 'Marketing')"
|
||||
);
|
||||
|
||||
// UPDATE with EXISTS clause
|
||||
to_sql_string_test!(
|
||||
test_update_with_exists,
|
||||
"UPDATE employees SET salary = 75000 WHERE EXISTS (SELECT 1 FROM orders WHERE orders.employee_id = employees.id AND orders.status = 'pending');"
|
||||
"UPDATE employees SET salary = 75000 WHERE EXISTS (SELECT 1 FROM orders WHERE orders.employee_id = employees.id AND orders.status = 'pending')"
|
||||
);
|
||||
|
||||
// UPDATE with FROM clause (join-like behavior)
|
||||
to_sql_string_test!(
|
||||
test_update_with_from,
|
||||
"UPDATE employees SET salary = 80000 FROM departments WHERE employees.department_id = departments.id AND departments.name = 'Engineering';"
|
||||
"UPDATE employees SET salary = 80000 FROM departments WHERE employees.department_id = departments.id AND departments.name = 'Engineering'"
|
||||
);
|
||||
|
||||
// UPDATE with RETURNING clause
|
||||
to_sql_string_test!(
|
||||
test_update_with_returning,
|
||||
"UPDATE employees SET salary = 60000 WHERE id = 1 RETURNING id, name, salary;"
|
||||
"UPDATE employees SET salary = 60000 WHERE id = 1 RETURNING id, name, salary"
|
||||
);
|
||||
|
||||
// UPDATE with expression in SET
|
||||
to_sql_string_test!(
|
||||
test_update_with_expression,
|
||||
"UPDATE employees SET salary = salary * 1.1 WHERE department_id = 2;"
|
||||
"UPDATE employees SET salary = salary * 1.1 WHERE department_id = 2"
|
||||
);
|
||||
|
||||
// UPDATE with NULL value
|
||||
to_sql_string_test!(
|
||||
test_update_with_null,
|
||||
"UPDATE employees SET department_id = NULL WHERE id = 1;"
|
||||
"UPDATE employees SET department_id = NULL WHERE id = 1"
|
||||
);
|
||||
|
||||
// UPDATE with schema-qualified table
|
||||
to_sql_string_test!(
|
||||
test_update_schema_qualified,
|
||||
"UPDATE main.employees SET salary = 65000 WHERE id = 1;"
|
||||
"UPDATE main.employees SET salary = 65000 WHERE id = 1"
|
||||
);
|
||||
|
||||
// UPDATE with CASE expression
|
||||
to_sql_string_test!(
|
||||
test_update_with_case,
|
||||
"UPDATE employees SET salary = CASE WHEN salary < 50000 THEN 55000 ELSE salary * 1.05 END WHERE department_id = 3;"
|
||||
"UPDATE employees SET salary = CASE WHEN salary < 50000 THEN 55000 ELSE salary * 1.05 END WHERE department_id = 3"
|
||||
);
|
||||
|
||||
// UPDATE with LIKE clause in WHERE
|
||||
to_sql_string_test!(
|
||||
test_update_with_like,
|
||||
"UPDATE employees SET name = 'Updated' WHERE name LIKE 'J%';"
|
||||
"UPDATE employees SET name = 'Updated' WHERE name LIKE 'J%'"
|
||||
);
|
||||
|
||||
// UPDATE with ON CONFLICT (upsert-like behavior)
|
||||
to_sql_string_test!(
|
||||
test_update_with_on_conflict,
|
||||
"INSERT INTO employees (id, name, salary) VALUES (1, 'John Doe', 50000) ON CONFLICT(id) DO UPDATE SET name = excluded.name, salary = excluded.salary;"
|
||||
"INSERT INTO employees (id, name, salary) VALUES (1, 'John Doe', 50000) ON CONFLICT (id) DO UPDATE SET name = excluded.name, salary = excluded.salary"
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user