1use teaql_core::{DataType, Value};
2
3#[derive(Debug, Clone, Copy, PartialEq, Eq)]
4pub enum DatabaseKind {
5 PostgreSql,
6 Sqlite,
7 MySql,
8}
9
10#[derive(Debug, Clone, PartialEq)]
11pub struct CompiledQuery {
12 pub sql: String,
13 pub params: Vec<Value>,
14 pub comment: Option<String>,
15}
16
17impl CompiledQuery {
18 pub fn sql_with_comment(&self) -> String {
19 match &self.comment {
20 Some(comment) if !comment.is_empty() => {
21 let escaped = comment.replace("*/", "* /");
22 format!("/* {escaped} */ {}", self.sql)
23 }
24 _ => self.sql.clone(),
25 }
26 }
27
28 pub fn debug_sql(&self, kind: DatabaseKind) -> String {
29 let sql = self.sql_with_comment();
30 match kind {
31 DatabaseKind::PostgreSql => replace_postgres_placeholders(&sql, &self.params),
32 DatabaseKind::Sqlite => {
33 replace_positional_placeholders(&sql, &self.params, DatabaseKind::Sqlite)
34 }
35 DatabaseKind::MySql => {
36 replace_positional_placeholders(&sql, &self.params, DatabaseKind::MySql)
37 }
38 }
39 }
40}
41
42fn replace_postgres_placeholders(sql: &str, params: &[Value]) -> String {
43 let mut output = String::with_capacity(sql.len());
44 let mut chars = sql.chars().peekable();
45 let mut state = SqlScanState::Sql;
46 while let Some(ch) = chars.next() {
47 match state {
48 SqlScanState::Sql => match (ch, chars.peek().copied()) {
49 ('\'', _) => {
50 output.push(ch);
51 state = SqlScanState::SingleQuote;
52 }
53 ('"', _) => {
54 output.push(ch);
55 state = SqlScanState::DoubleQuote;
56 }
57 ('-', Some('-')) => {
58 output.push_str("--");
59 chars.next();
60 state = SqlScanState::LineComment;
61 }
62 ('/', Some('*')) => {
63 output.push_str("/*");
64 chars.next();
65 state = SqlScanState::BlockComment;
66 }
67 ('$', Some(next)) if next.is_ascii_digit() => {
68 let mut index = String::new();
69 while let Some(next) = chars.peek().copied().filter(char::is_ascii_digit) {
70 index.push(next);
71 chars.next();
72 }
73 if let Ok(index) = index.parse::<usize>()
74 && let Some(value) = index.checked_sub(1).and_then(|idx| params.get(idx))
75 {
76 output.push_str(&sql_literal(value, DatabaseKind::PostgreSql));
77 } else {
78 output.push('$');
79 output.push_str(&index);
80 }
81 }
82 _ => output.push(ch),
83 },
84 SqlScanState::SingleQuote => {
85 output.push(ch);
86 if ch == '\'' {
87 if matches!(chars.peek(), Some('\'')) {
88 output.push(chars.next().expect("peeked escaped quote"));
89 } else {
90 state = SqlScanState::Sql;
91 }
92 }
93 }
94 SqlScanState::DoubleQuote => {
95 output.push(ch);
96 if ch == '"' {
97 if matches!(chars.peek(), Some('"')) {
98 output.push(chars.next().expect("peeked escaped identifier"));
99 } else {
100 state = SqlScanState::Sql;
101 }
102 }
103 }
104 SqlScanState::LineComment => {
105 output.push(ch);
106 if matches!(ch, '\r' | '\n') {
107 state = SqlScanState::Sql;
108 }
109 }
110 SqlScanState::BlockComment => {
111 output.push(ch);
112 if ch == '*' && matches!(chars.peek(), Some('/')) {
113 output.push(chars.next().expect("peeked comment end"));
114 state = SqlScanState::Sql;
115 }
116 }
117 }
118 }
119 output
120}
121
122fn replace_positional_placeholders(sql: &str, params: &[Value], kind: DatabaseKind) -> String {
123 let mut output = String::with_capacity(sql.len());
124 let mut params = params.iter();
125 let mut state = SqlScanState::Sql;
126 let mut chars = sql.chars().peekable();
127 while let Some(ch) = chars.next() {
128 match state {
129 SqlScanState::Sql => match (ch, chars.peek().copied()) {
130 ('\'', _) => {
131 output.push(ch);
132 state = SqlScanState::SingleQuote;
133 }
134 ('"', _) => {
135 output.push(ch);
136 state = SqlScanState::DoubleQuote;
137 }
138 ('-', Some('-')) => {
139 output.push(ch);
140 output.push(chars.next().expect("peeked line comment"));
141 state = SqlScanState::LineComment;
142 }
143 ('/', Some('*')) => {
144 output.push(ch);
145 output.push(chars.next().expect("peeked block comment"));
146 state = SqlScanState::BlockComment;
147 }
148 ('?', _) => match params.next() {
149 Some(value) => output.push_str(&sql_literal(value, kind)),
150 None => output.push(ch),
151 },
152 _ => output.push(ch),
153 },
154 SqlScanState::SingleQuote => {
155 output.push(ch);
156 if ch == '\'' {
157 if matches!(chars.peek(), Some('\'')) {
158 output.push(chars.next().expect("peeked escaped quote"));
159 } else {
160 state = SqlScanState::Sql;
161 }
162 }
163 }
164 SqlScanState::DoubleQuote => {
165 output.push(ch);
166 if ch == '"' {
167 if matches!(chars.peek(), Some('"')) {
168 output.push(chars.next().expect("peeked escaped identifier quote"));
169 } else {
170 state = SqlScanState::Sql;
171 }
172 }
173 }
174 SqlScanState::LineComment => {
175 output.push(ch);
176 if matches!(ch, '\r' | '\n') {
177 state = SqlScanState::Sql;
178 }
179 }
180 SqlScanState::BlockComment => {
181 output.push(ch);
182 if ch == '*' && matches!(chars.peek(), Some('/')) {
183 output.push(chars.next().expect("peeked block comment end"));
184 state = SqlScanState::Sql;
185 }
186 }
187 }
188 }
189 output
190}
191
192#[derive(Clone, Copy)]
193enum SqlScanState {
194 Sql,
195 SingleQuote,
196 DoubleQuote,
197 LineComment,
198 BlockComment,
199}
200
201fn sql_bool_literal(value: bool) -> &'static str {
202 match value {
203 true => "TRUE",
204 false => "FALSE",
205 }
206}
207
208fn sql_literal(value: &Value, kind: DatabaseKind) -> String {
209 match value {
210 Value::Null => "NULL".to_owned(),
211 Value::Bool(value) => sql_bool_literal(*value).to_owned(),
212 Value::I64(value) => value.to_string(),
213 Value::U64(value) => value.to_string(),
214 Value::F64(value) => value.to_string(),
215 Value::Decimal(value) => value.to_string(),
216 Value::Text(value) => quoted_sql_string(value),
217 Value::Json(value) => quoted_sql_string(&value.to_string()),
218 Value::Date(value) => match kind {
219 DatabaseKind::PostgreSql => format!("DATE '{}'", value),
220 DatabaseKind::MySql => format!("CAST('{}' AS DATE)", value),
221 DatabaseKind::Sqlite => quoted_sql_string(&value.to_string()),
222 },
223 Value::Timestamp(value) => match kind {
224 DatabaseKind::Sqlite => value.0.to_string(),
225 DatabaseKind::PostgreSql => format!(
226 "TIMESTAMPTZ '{}'",
227 value.to_datetime().format("%Y-%m-%d %H:%M:%S%.3fZ")
228 ),
229 DatabaseKind::MySql => format!(
230 "CAST('{}' AS DATETIME(3))",
231 value
232 .to_datetime()
233 .naive_utc()
234 .format("%Y-%m-%d %H:%M:%S%.3f")
235 ),
236 },
237 Value::Object(value) => {
238 quoted_sql_string(&Value::Object(value.clone()).to_json_value().to_string())
239 }
240 Value::List(values) => {
241 let values = values
242 .iter()
243 .map(|v| sql_literal(v, kind))
244 .collect::<Vec<_>>()
245 .join(", ");
246 match kind {
247 DatabaseKind::PostgreSql => format!("ARRAY[{values}]"),
248 _ => format!("({values})"),
249 }
250 }
251 Value::TypedNull(_) => "NULL".to_owned(),
252 }
253}
254
255fn quoted_sql_string(value: &str) -> String {
256 format!("'{}'", value.replace('\'', "''"))
257}
258
259#[derive(Debug, Clone, PartialEq, Eq)]
260pub enum SqlCompileError {
261 UnknownEntity(String),
262 UnknownField(String),
263 EmptyInList,
264 MissingIdProperty(String),
265 MissingVersionProperty(String),
266 EmptyMutation(String),
267 InvalidRecoverVersion(i64),
268 UnsupportedSchemaType(DataType),
269 InvalidFunctionArguments(String),
270 InvalidSubQueryOperator(String),
271}
272
273impl std::fmt::Display for SqlCompileError {
274 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
275 match self {
276 Self::UnknownEntity(entity) => write!(f, "unknown entity: {entity}"),
277 Self::UnknownField(field) => write!(f, "unknown field: {field}"),
278 Self::EmptyInList => write!(f, "IN requires at least one value"),
279 Self::MissingIdProperty(entity) => write!(f, "entity {entity} has no id property"),
280 Self::MissingVersionProperty(entity) => {
281 write!(f, "entity {entity} has no version property")
282 }
283 Self::EmptyMutation(kind) => write!(f, "{kind} requires at least one writable field"),
284 Self::InvalidRecoverVersion(version) => {
285 write!(f, "recover requires a negative version, got {version}")
286 }
287 Self::UnsupportedSchemaType(data_type) => {
288 write!(f, "unsupported schema type: {data_type:?}")
289 }
290 Self::InvalidFunctionArguments(message) => write!(f, "{message}"),
291 Self::InvalidSubQueryOperator(operator) => {
292 write!(f, "subquery does not support operator: {operator}")
293 }
294 }
295 }
296}
297
298impl std::error::Error for SqlCompileError {}