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