1pub mod conditions;
5pub mod ddl;
6pub mod dialect;
7pub mod dml;
8pub mod sql;
9pub mod traits;
10
11pub mod nosql;
13pub use nosql::dynamo::ToDynamo;
14pub use nosql::mongo::ToMongo;
15pub use nosql::qdrant::ToQdrant;
16
17#[cfg(test)]
18mod tests;
19
20use crate::ast::*;
21pub use conditions::ConditionToSql;
22pub use dialect::Dialect;
23pub use traits::SqlGenerator;
24pub use traits::escape_identifier;
25
26#[derive(Debug, Clone, PartialEq, Default)]
28pub struct TranspileResult {
29 pub sql: String,
31 pub params: Vec<Value>,
33 pub named_params: Vec<String>,
35}
36
37impl TranspileResult {
38 pub fn new(sql: impl Into<String>, params: Vec<Value>) -> Self {
40 Self {
41 sql: sql.into(),
42 params,
43 named_params: vec![],
44 }
45 }
46
47 pub fn sql_only(sql: impl Into<String>) -> Self {
49 Self {
50 sql: sql.into(),
51 params: Vec::new(),
52 named_params: Vec::new(),
53 }
54 }
55}
56
57pub trait ToSqlParameterized {
59 fn to_sql_parameterized(&self) -> TranspileResult {
61 self.to_sql_parameterized_with_dialect(Dialect::default())
62 }
63 fn to_sql_parameterized_with_dialect(&self, dialect: Dialect) -> TranspileResult;
65}
66
67pub trait ToSql {
69 fn to_sql(&self) -> String {
71 self.to_sql_with_dialect(Dialect::default())
72 }
73 fn to_sql_with_dialect(&self, dialect: Dialect) -> String;
75}
76
77impl ToSql for Qail {
78 fn to_sql_with_dialect(&self, dialect: Dialect) -> String {
79 match self.action {
80 Action::Get => dml::select::build_select(self, dialect),
81 Action::Set => dml::update::build_update(self, dialect),
82 Action::Del => dml::delete::build_delete(self, dialect),
83 Action::Add => dml::insert::build_insert(self, dialect),
84 Action::Gen => format!("-- gen::{} (generates Rust struct, not SQL)", self.table),
85 Action::Make => ddl::build_create_table(self, dialect),
86 Action::Mod => ddl::build_alter_table(self, dialect),
87 Action::Over => dml::window::build_window(self, dialect),
88 Action::With => dml::cte::build_cte(self, dialect),
89 Action::Index => ddl::build_create_index(self, dialect),
90 Action::DropIndex => format!("DROP INDEX {}", self.table),
91 Action::Alter => ddl::build_alter_add_column(self, dialect),
92 Action::AlterDrop => ddl::build_alter_drop_column(self, dialect),
93 Action::AlterType => ddl::build_alter_column_type(self, dialect),
94 Action::TxnStart => "BEGIN TRANSACTION;".to_string(), Action::TxnCommit => "COMMIT;".to_string(),
97 Action::TxnRollback => "ROLLBACK;".to_string(),
98 Action::Put => dml::upsert::build_upsert(self, dialect),
99 Action::Drop => format!("DROP TABLE {}", self.table),
100 Action::DropCol | Action::RenameCol => ddl::build_alter_column(self, dialect),
101 Action::JsonTable => dml::json_table::build_json_table(self, dialect),
103 Action::Export => dml::select::build_select(self, dialect),
105 Action::Truncate => format!("TRUNCATE TABLE {}", self.table),
107 Action::Explain => format!("EXPLAIN {}", dml::select::build_select(self, dialect)),
109 Action::ExplainAnalyze => format!(
111 "EXPLAIN ANALYZE {}",
112 dml::select::build_select(self, dialect)
113 ),
114 Action::Lock => format!("LOCK TABLE {} IN ACCESS EXCLUSIVE MODE", self.table),
116 Action::CreateMaterializedView => {
118 if let Some(source) = &self.source_query {
119 format!(
120 "CREATE MATERIALIZED VIEW {} AS {}",
121 self.table,
122 source.to_sql_with_dialect(dialect)
123 )
124 } else {
125 format!(
126 "CREATE MATERIALIZED VIEW {} AS {}",
127 self.table,
128 dml::select::build_select(self, dialect)
129 )
130 }
131 }
132 Action::RefreshMaterializedView => format!("REFRESH MATERIALIZED VIEW {}", self.table),
134 Action::DropMaterializedView => format!("DROP MATERIALIZED VIEW {}", self.table),
136 Action::Listen => {
138 if let Some(ch) = &self.channel {
139 format!("LISTEN {}", ch)
140 } else {
141 "LISTEN".to_string()
142 }
143 }
144 Action::Notify => {
145 if let Some(ch) = &self.channel {
146 if let Some(msg) = &self.payload {
147 format!("NOTIFY {}, '{}'", ch, msg)
148 } else {
149 format!("NOTIFY {}", ch)
150 }
151 } else {
152 "NOTIFY".to_string()
153 }
154 }
155 Action::Unlisten => {
156 if let Some(ch) = &self.channel {
157 format!("UNLISTEN {}", ch)
158 } else {
159 "UNLISTEN *".to_string()
160 }
161 }
162 Action::Savepoint => {
164 if let Some(name) = &self.savepoint_name {
165 format!("SAVEPOINT {}", name)
166 } else {
167 "SAVEPOINT".to_string()
168 }
169 }
170 Action::ReleaseSavepoint => {
171 if let Some(name) = &self.savepoint_name {
172 format!("RELEASE SAVEPOINT {}", name)
173 } else {
174 "RELEASE SAVEPOINT".to_string()
175 }
176 }
177 Action::RollbackToSavepoint => {
178 if let Some(name) = &self.savepoint_name {
179 format!("ROLLBACK TO SAVEPOINT {}", name)
180 } else {
181 "ROLLBACK TO SAVEPOINT".to_string()
182 }
183 }
184 Action::CreateView => {
186 if let Some(source) = &self.source_query {
187 format!(
188 "CREATE VIEW {} AS {}",
189 self.table,
190 source.to_sql_with_dialect(dialect)
191 )
192 } else {
193 format!(
194 "CREATE VIEW {} AS {}",
195 self.table,
196 dml::select::build_select(self, dialect)
197 )
198 }
199 }
200 Action::DropView => format!("DROP VIEW IF EXISTS {}", self.table),
201 operators::Action::Search | operators::Action::Upsert | operators::Action::Scroll => {
203 format!("-- Vector operation {:?} not supported in SQL. Use qail-qdrant driver.", self.action)
204 }
205 operators::Action::CreateCollection | operators::Action::DeleteCollection => {
206 format!("-- Vector DDL {:?} not supported in SQL. Use qail-qdrant driver.", self.action)
207 }
208 operators::Action::CreateFunction => {
210 if let Some(func) = &self.function_def {
211 let lang = func.language.as_deref().unwrap_or("plpgsql");
212 format!(
213 "CREATE OR REPLACE FUNCTION {}() RETURNS {} LANGUAGE {} AS $$ {} $$",
214 func.name, func.returns, lang, func.body
215 )
216 } else {
217 "-- CreateFunction requires function_def".to_string()
218 }
219 }
220 operators::Action::DropFunction => {
221 format!("DROP FUNCTION IF EXISTS {}()", self.table)
222 }
223 operators::Action::CreateTrigger => {
224 if let Some(trig) = &self.trigger_def {
225 let timing = match trig.timing {
226 crate::ast::TriggerTiming::Before => "BEFORE",
227 crate::ast::TriggerTiming::After => "AFTER",
228 crate::ast::TriggerTiming::InsteadOf => "INSTEAD OF",
229 };
230 let events: Vec<&str> = trig.events.iter().map(|e| match e {
231 crate::ast::TriggerEvent::Insert => "INSERT",
232 crate::ast::TriggerEvent::Update => "UPDATE",
233 crate::ast::TriggerEvent::Delete => "DELETE",
234 crate::ast::TriggerEvent::Truncate => "TRUNCATE",
235 }).collect();
236 let for_each = if trig.for_each_row { "FOR EACH ROW" } else { "FOR EACH STATEMENT" };
237 format!(
239 "DROP TRIGGER IF EXISTS {} ON {};\nCREATE TRIGGER {} {} {} ON {} {} EXECUTE FUNCTION {}()",
240 trig.name, trig.table,
241 trig.name, timing, events.join(" OR "), trig.table, for_each, trig.execute_function
242 )
243 } else {
244 "-- CreateTrigger requires trigger_def".to_string()
245 }
246 }
247 operators::Action::DropTrigger => {
248 format!("DROP TRIGGER IF EXISTS {} ON {}", self.table, self.table)
249 }
250 Action::RedisGet
252 | Action::RedisSet
253 | Action::RedisDel
254 | Action::RedisIncr
255 | Action::RedisDecr
256 | Action::RedisTtl
257 | Action::RedisExpire
258 | Action::RedisExists
259 | Action::RedisMGet
260 | Action::RedisMSet
261 | Action::RedisPing => {
262 format!(
263 "-- Redis operation {:?} not supported in SQL. Use qail-redis driver.",
264 self.action
265 )
266 }
267 }
268 }
269}
270
271impl ToSqlParameterized for Qail {
272 fn to_sql_parameterized_with_dialect(&self, dialect: Dialect) -> TranspileResult {
273 let full_sql = self.to_sql_with_dialect(dialect);
276
277 let mut named_params: Vec<String> = Vec::new();
279 let mut seen_params: std::collections::HashMap<String, usize> =
280 std::collections::HashMap::new();
281 let mut result = String::with_capacity(full_sql.len());
282 let mut chars = full_sql.chars().peekable();
283 let mut param_index = 1;
284
285 while let Some(c) = chars.next() {
286 if c == ':'
287 && let Some(&next) = chars.peek()
288 {
289 if next == ':' {
290 result.push(':');
291 result.push(chars.next().unwrap());
292 continue;
293 }
294 if next.is_ascii_alphabetic() || next == '_' {
295 let mut param_name = String::new();
296 while let Some(&ch) = chars.peek() {
297 if ch.is_ascii_alphanumeric() || ch == '_' {
298 param_name.push(chars.next().unwrap());
299 } else {
300 break;
301 }
302 }
303
304 let idx = if let Some(&existing) = seen_params.get(¶m_name) {
305 existing
306 } else {
307 let idx = param_index;
308 seen_params.insert(param_name.clone(), idx);
309 named_params.push(param_name);
310 param_index += 1;
311 idx
312 };
313
314 result.push('$');
315 result.push_str(&idx.to_string());
316 continue;
317 }
318 }
319 result.push(c);
320 }
321
322 TranspileResult {
323 sql: result,
324 params: Vec::new(), named_params,
326 }
327 }
328}