Skip to main content

teaql_sql/
types.rs

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 {}