pub mod conditions;
pub mod ddl;
pub mod dialect;
pub mod dml;
pub mod policy;
pub mod sql;
pub mod traits;
pub mod nosql;
pub use nosql::dynamo::ToDynamo;
pub use nosql::mongo::ToMongo;
pub use nosql::qdrant::ToQdrant;
#[cfg(test)]
mod tests;
use crate::ast::*;
pub use conditions::ConditionToSql;
pub use dialect::Dialect;
pub use traits::SqlGenerator;
pub use traits::escape_identifier;
#[derive(Debug, Clone, PartialEq, Default)]
pub struct TranspileResult {
pub sql: String,
pub params: Vec<Value>,
pub named_params: Vec<String>,
}
impl TranspileResult {
pub fn new(sql: impl Into<String>, params: Vec<Value>) -> Self {
Self {
sql: sql.into(),
params,
named_params: vec![],
}
}
pub fn sql_only(sql: impl Into<String>) -> Self {
Self {
sql: sql.into(),
params: Vec::new(),
named_params: Vec::new(),
}
}
}
pub trait ToSqlParameterized {
fn to_sql_parameterized(&self) -> TranspileResult {
self.to_sql_parameterized_with_dialect(Dialect::default())
}
fn to_sql_parameterized_with_dialect(&self, dialect: Dialect) -> TranspileResult;
}
pub trait ToSql {
fn to_sql(&self) -> String {
self.to_sql_with_dialect(Dialect::default())
}
fn to_sql_with_dialect(&self, dialect: Dialect) -> String;
}
impl ToSql for Qail {
fn to_sql_with_dialect(&self, dialect: Dialect) -> String {
match self.action {
Action::Get => dml::select::build_select(self, dialect),
Action::Cnt => {
let mut count_ast = self.clone();
count_ast.action = Action::Get;
count_ast.columns = vec![Expr::Aggregate {
col: "*".to_string(),
func: AggregateFunc::Count,
distinct: false,
filter: None,
alias: None,
}];
dml::select::build_select(&count_ast, dialect)
}
Action::Set => dml::update::build_update(self, dialect),
Action::Del => dml::delete::build_delete(self, dialect),
Action::Add => dml::insert::build_insert(self, dialect),
Action::Gen => format!("-- gen::{} (generates Rust struct, not SQL)", self.table),
Action::Make => ddl::build_create_table(self, dialect),
Action::Mod => ddl::build_alter_table(self, dialect),
Action::Over => dml::window::build_window(self, dialect),
Action::With => dml::cte::build_cte(self, dialect),
Action::Index => ddl::build_create_index(self, dialect),
Action::DropIndex => format!("DROP INDEX IF EXISTS {}", self.table),
Action::Alter => ddl::build_alter_add_column(self, dialect),
Action::AlterDrop => ddl::build_alter_drop_column(self, dialect),
Action::AlterType => ddl::build_alter_column_type(self, dialect),
Action::TxnStart => "BEGIN TRANSACTION;".to_string(), Action::TxnCommit => "COMMIT;".to_string(),
Action::TxnRollback => "ROLLBACK;".to_string(),
Action::Put => dml::upsert::build_upsert(self, dialect),
Action::Drop => format!("DROP TABLE {}", self.table),
Action::DropCol | Action::RenameCol => ddl::build_alter_column(self, dialect),
Action::JsonTable => dml::json_table::build_json_table(self, dialect),
Action::Export => dml::select::build_select(self, dialect),
Action::Truncate => format!("TRUNCATE TABLE {}", self.table),
Action::Explain => format!("EXPLAIN {}", dml::select::build_select(self, dialect)),
Action::ExplainAnalyze => format!(
"EXPLAIN ANALYZE {}",
dml::select::build_select(self, dialect)
),
Action::Lock => format!("LOCK TABLE {} IN ACCESS EXCLUSIVE MODE", self.table),
Action::CreateMaterializedView => {
if let Some(source) = &self.source_query {
format!(
"CREATE MATERIALIZED VIEW {} AS {}",
self.table,
source.to_sql_with_dialect(dialect)
)
} else if let Some(query) = &self.payload {
format!("CREATE MATERIALIZED VIEW {} AS {}", self.table, query)
} else {
format!(
"CREATE MATERIALIZED VIEW {} AS {}",
self.table,
dml::select::build_select(self, dialect)
)
}
}
Action::RefreshMaterializedView => format!("REFRESH MATERIALIZED VIEW {}", self.table),
Action::DropMaterializedView => {
format!("DROP MATERIALIZED VIEW IF EXISTS {}", self.table)
}
Action::Listen => {
if let Some(ch) = &self.channel {
format!("LISTEN {}", ch)
} else {
"LISTEN".to_string()
}
}
Action::Notify => {
if let Some(ch) = &self.channel {
if let Some(msg) = &self.payload {
format!("NOTIFY {}, '{}'", ch, msg)
} else {
format!("NOTIFY {}", ch)
}
} else {
"NOTIFY".to_string()
}
}
Action::Unlisten => {
if let Some(ch) = &self.channel {
format!("UNLISTEN {}", ch)
} else {
"UNLISTEN *".to_string()
}
}
Action::Savepoint => {
if let Some(name) = &self.savepoint_name {
format!("SAVEPOINT {}", name)
} else {
"SAVEPOINT".to_string()
}
}
Action::ReleaseSavepoint => {
if let Some(name) = &self.savepoint_name {
format!("RELEASE SAVEPOINT {}", name)
} else {
"RELEASE SAVEPOINT".to_string()
}
}
Action::RollbackToSavepoint => {
if let Some(name) = &self.savepoint_name {
format!("ROLLBACK TO SAVEPOINT {}", name)
} else {
"ROLLBACK TO SAVEPOINT".to_string()
}
}
Action::CreateView => {
if let Some(source) = &self.source_query {
format!(
"CREATE VIEW {} AS {}",
self.table,
source.to_sql_with_dialect(dialect)
)
} else if let Some(query) = &self.payload {
format!("CREATE VIEW {} AS {}", self.table, query)
} else {
format!(
"CREATE VIEW {} AS {}",
self.table,
dml::select::build_select(self, dialect)
)
}
}
Action::DropView => format!("DROP VIEW IF EXISTS {}", self.table),
operators::Action::Search | operators::Action::Upsert | operators::Action::Scroll => {
format!(
"-- Vector operation {:?} not supported in SQL. Use qail-qdrant driver.",
self.action
)
}
operators::Action::CreateCollection | operators::Action::DeleteCollection => {
format!(
"-- Vector DDL {:?} not supported in SQL. Use qail-qdrant driver.",
self.action
)
}
operators::Action::CreateFunction => {
if let Some(func) = &self.function_def {
let lang = func.language.as_deref().unwrap_or("plpgsql");
let args = func.args.join(", ");
let volatility = func
.volatility
.as_deref()
.map(|v| format!(" {}", v.to_uppercase()))
.unwrap_or_default();
format!(
"CREATE OR REPLACE FUNCTION {}({}) RETURNS {} LANGUAGE {}{} AS $$ {} $$",
func.name, args, func.returns, lang, volatility, func.body
)
} else {
"-- CreateFunction requires function_def".to_string()
}
}
operators::Action::DropFunction => {
if let Some(signature) = &self.payload {
format!("DROP FUNCTION IF EXISTS {}", signature)
} else {
format!("DROP FUNCTION IF EXISTS {}()", self.table)
}
}
operators::Action::CreateTrigger => {
if let Some(trig) = &self.trigger_def {
let timing = match trig.timing {
crate::ast::TriggerTiming::Before => "BEFORE",
crate::ast::TriggerTiming::After => "AFTER",
crate::ast::TriggerTiming::InsteadOf => "INSTEAD OF",
};
let events: Vec<&str> = trig
.events
.iter()
.map(|e| match e {
crate::ast::TriggerEvent::Insert => "INSERT",
crate::ast::TriggerEvent::Update => "UPDATE",
crate::ast::TriggerEvent::Delete => "DELETE",
crate::ast::TriggerEvent::Truncate => "TRUNCATE",
})
.collect();
let for_each = if trig.for_each_row {
"FOR EACH ROW"
} else {
"FOR EACH STATEMENT"
};
format!(
"CREATE TRIGGER {} {} {} ON {} {} EXECUTE FUNCTION {}()",
trig.name,
timing,
events.join(" OR "),
trig.table,
for_each,
trig.execute_function
)
} else {
"-- CreateTrigger requires trigger_def".to_string()
}
}
operators::Action::DropTrigger => {
if let Some((table, trigger)) = self.table.rsplit_once('.') {
format!("DROP TRIGGER IF EXISTS {} ON {}", trigger, table)
} else {
format!("DROP TRIGGER IF EXISTS {}", self.table)
}
}
Action::CreateExtension => ddl::build_create_extension(self, dialect),
Action::DropExtension => ddl::build_drop_extension(self, dialect),
Action::CommentOn => ddl::build_comment_on(self, dialect),
Action::CreateSequence => ddl::build_create_sequence(self, dialect),
Action::DropSequence => ddl::build_drop_sequence(self, dialect),
Action::CreateEnum => ddl::build_create_enum(self, dialect),
Action::DropEnum => ddl::build_drop_enum(self, dialect),
Action::AlterEnumAddValue => ddl::build_alter_enum_add_value(self, dialect),
Action::AlterSetNotNull => {
if let Some(Expr::Named(col)) = self.columns.first() {
format!(
"ALTER TABLE {} ALTER COLUMN {} SET NOT NULL",
self.table, col
)
} else {
format!("ALTER TABLE {} ALTER COLUMN ... SET NOT NULL", self.table)
}
}
Action::AlterDropNotNull => {
if let Some(Expr::Named(col)) = self.columns.first() {
format!(
"ALTER TABLE {} ALTER COLUMN {} DROP NOT NULL",
self.table, col
)
} else {
format!("ALTER TABLE {} ALTER COLUMN ... DROP NOT NULL", self.table)
}
}
Action::AlterSetDefault => {
if let Some(Expr::Named(col)) = self.columns.first() {
let default_expr = self.payload.as_deref().unwrap_or("NULL");
format!(
"ALTER TABLE {} ALTER COLUMN {} SET DEFAULT {}",
self.table, col, default_expr
)
} else {
format!(
"ALTER TABLE {} ALTER COLUMN ... SET DEFAULT ...",
self.table
)
}
}
Action::AlterDropDefault => {
if let Some(Expr::Named(col)) = self.columns.first() {
format!(
"ALTER TABLE {} ALTER COLUMN {} DROP DEFAULT",
self.table, col
)
} else {
format!("ALTER TABLE {} ALTER COLUMN ... DROP DEFAULT", self.table)
}
}
Action::AlterEnableRls => {
format!("ALTER TABLE {} ENABLE ROW LEVEL SECURITY", self.table)
}
Action::AlterDisableRls => {
format!("ALTER TABLE {} DISABLE ROW LEVEL SECURITY", self.table)
}
Action::AlterForceRls => {
format!("ALTER TABLE {} FORCE ROW LEVEL SECURITY", self.table)
}
Action::AlterNoForceRls => {
format!("ALTER TABLE {} NO FORCE ROW LEVEL SECURITY", self.table)
}
Action::Call => {
format!("CALL {}", self.table)
}
Action::Do => {
let body = self.payload.as_deref().unwrap_or("");
let lang = if self.table.is_empty() {
"plpgsql"
} else {
&self.table
};
format!("DO $$ {} $$ LANGUAGE {}", body, lang)
}
Action::SessionSet => {
let value = self.payload.as_deref().unwrap_or("");
format!("SET {} = '{}'", self.table, value)
}
Action::SessionShow => {
format!("SHOW {}", self.table)
}
Action::SessionReset => {
format!("RESET {}", self.table)
}
Action::CreateDatabase => {
format!("CREATE DATABASE {}", escape_identifier(&self.table))
}
Action::DropDatabase => {
format!("DROP DATABASE IF EXISTS {}", escape_identifier(&self.table))
}
Action::Grant => {
let role = self.payload.as_deref().unwrap_or("");
let privs: Vec<String> = self
.columns
.iter()
.filter_map(|c| match c {
Expr::Named(p) => Some(p.clone()),
_ => None,
})
.collect();
format!("GRANT {} ON {} TO {}", privs.join(", "), self.table, role)
}
Action::Revoke => {
let role = self.payload.as_deref().unwrap_or("");
let privs: Vec<String> = self
.columns
.iter()
.filter_map(|c| match c {
Expr::Named(p) => Some(p.clone()),
_ => None,
})
.collect();
format!(
"REVOKE {} ON {} FROM {}",
privs.join(", "),
self.table,
role
)
}
Action::CreatePolicy => {
if let Some(policy) = &self.policy_def {
policy::create_policy_sql(policy)
} else {
"-- CreatePolicy requires policy_def".to_string()
}
}
Action::DropPolicy => {
if let Some(policy) = &self.policy_def {
policy::drop_policy_sql(&policy.name, &policy.table)
} else if let Some(policy_name) = &self.payload {
policy::drop_policy_sql(policy_name, &self.table)
} else {
"-- DropPolicy requires policy name + table".to_string()
}
}
}
}
}
impl ToSqlParameterized for Qail {
fn to_sql_parameterized_with_dialect(&self, dialect: Dialect) -> TranspileResult {
let full_sql = self.to_sql_with_dialect(dialect);
let mut named_params: Vec<String> = Vec::new();
let mut seen_params: std::collections::HashMap<String, usize> =
std::collections::HashMap::new();
let mut result = String::with_capacity(full_sql.len());
let mut chars = full_sql.chars().peekable();
let mut param_index = 1;
while let Some(c) = chars.next() {
if c == ':'
&& let Some(&next) = chars.peek()
{
if next == ':' {
result.push(':');
if let Some(double_colon) = chars.next() {
result.push(double_colon);
}
continue;
}
if next.is_ascii_alphabetic() || next == '_' {
let mut param_name = String::new();
while let Some(&ch) = chars.peek() {
if ch.is_ascii_alphanumeric() || ch == '_' {
if let Some(param_ch) = chars.next() {
param_name.push(param_ch);
} else {
break;
}
} else {
break;
}
}
let idx = if let Some(&existing) = seen_params.get(¶m_name) {
existing
} else {
let idx = param_index;
seen_params.insert(param_name.clone(), idx);
named_params.push(param_name);
param_index += 1;
idx
};
result.push('$');
result.push_str(&idx.to_string());
continue;
}
}
result.push(c);
}
TranspileResult {
sql: result,
params: Vec::new(), named_params,
}
}
}