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 }
202 }
203}
204
205impl ToSqlParameterized for Qail {
206 fn to_sql_parameterized_with_dialect(&self, dialect: Dialect) -> TranspileResult {
207 let full_sql = self.to_sql_with_dialect(dialect);
210
211 let mut named_params: Vec<String> = Vec::new();
213 let mut seen_params: std::collections::HashMap<String, usize> =
214 std::collections::HashMap::new();
215 let mut result = String::with_capacity(full_sql.len());
216 let mut chars = full_sql.chars().peekable();
217 let mut param_index = 1;
218
219 while let Some(c) = chars.next() {
220 if c == ':'
221 && let Some(&next) = chars.peek()
222 {
223 if next == ':' {
224 result.push(':');
225 result.push(chars.next().unwrap());
226 continue;
227 }
228 if next.is_ascii_alphabetic() || next == '_' {
229 let mut param_name = String::new();
230 while let Some(&ch) = chars.peek() {
231 if ch.is_ascii_alphanumeric() || ch == '_' {
232 param_name.push(chars.next().unwrap());
233 } else {
234 break;
235 }
236 }
237
238 let idx = if let Some(&existing) = seen_params.get(¶m_name) {
239 existing
240 } else {
241 let idx = param_index;
242 seen_params.insert(param_name.clone(), idx);
243 named_params.push(param_name);
244 param_index += 1;
245 idx
246 };
247
248 result.push('$');
249 result.push_str(&idx.to_string());
250 continue;
251 }
252 }
253 result.push(c);
254 }
255
256 TranspileResult {
257 sql: result,
258 params: Vec::new(), named_params,
260 }
261 }
262}