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 match kind {
30 DatabaseKind::PostgreSql => replace_postgres_placeholders(&self.sql, &self.params),
31 DatabaseKind::Sqlite => {
32 replace_positional_placeholders(&self.sql, &self.params, DatabaseKind::Sqlite)
33 }
34 DatabaseKind::MySql => {
35 replace_positional_placeholders(&self.sql, &self.params, DatabaseKind::MySql)
36 }
37 }
38 }
39}
40
41fn handle_sql_quote(
42 chars: &mut std::iter::Peekable<std::str::Chars>,
43 output: &mut String,
44 in_string: &mut bool,
45) {
46 output.push('\'');
47 match *in_string && matches!(chars.peek(), Some('\'')) {
48 true => {
49 output.push(chars.next().expect("peeked quote must exist"));
50 }
51 false => {
52 *in_string = !*in_string;
53 }
54 }
55}
56
57fn replace_postgres_placeholders(sql: &str, params: &[Value]) -> String {
58 let mut output = String::with_capacity(sql.len());
59 let mut chars = sql.chars().peekable();
60 let mut in_string = false;
61 while let Some(ch) = chars.next() {
62 if ch == '\'' {
63 handle_sql_quote(&mut chars, &mut output, &mut in_string);
64 continue;
65 }
66 if !in_string && ch == '$' && chars.peek().is_some_and(|next| next.is_ascii_digit()) {
67 let mut index = String::new();
68 while let Some(next) = chars.peek().copied().filter(char::is_ascii_digit) {
69 index.push(next);
70 chars.next();
71 }
72 if let Ok(index) = index.parse::<usize>() {
73 if let Some(value) = index.checked_sub(1).and_then(|idx| params.get(idx)) {
74 output.push_str(&sql_literal(value, DatabaseKind::PostgreSql));
75 continue;
76 }
77 }
78 output.push('$');
79 output.push_str(&index);
80 continue;
81 }
82 output.push(ch);
83 }
84 output
85}
86
87fn replace_positional_placeholders(sql: &str, params: &[Value], kind: DatabaseKind) -> String {
88 let mut output = String::with_capacity(sql.len());
89 let mut params = params.iter();
90 let mut in_string = false;
91 let mut chars = sql.chars().peekable();
92 while let Some(ch) = chars.next() {
93 if ch == '\'' {
94 handle_sql_quote(&mut chars, &mut output, &mut in_string);
95 continue;
96 }
97 if !in_string && ch == '?' {
98 match params.next() {
99 Some(value) => output.push_str(&sql_literal(value, kind)),
100 None => output.push(ch),
101 }
102 continue;
103 }
104 output.push(ch);
105 }
106 output
107}
108
109fn sql_bool_literal(value: bool) -> &'static str {
110 match value {
111 true => "TRUE",
112 false => "FALSE",
113 }
114}
115
116fn sql_literal(value: &Value, kind: DatabaseKind) -> String {
117 match value {
118 Value::Null => "NULL".to_owned(),
119 Value::Bool(value) => sql_bool_literal(*value).to_owned(),
120 Value::I64(value) => value.to_string(),
121 Value::U64(value) => value.to_string(),
122 Value::F64(value) => value.to_string(),
123 Value::Decimal(value) => value.to_string(),
124 Value::Text(value) => quoted_sql_string(value),
125 Value::Json(value) => quoted_sql_string(&value.to_string()),
126 Value::Date(value) => quoted_sql_string(&value.to_string()),
127 Value::Timestamp(value) => quoted_sql_string(&value.to_rfc3339()),
128 Value::Object(value) => {
129 quoted_sql_string(&Value::Object(value.clone()).to_json_value().to_string())
130 }
131 Value::List(values) => {
132 let values = values
133 .iter()
134 .map(|v| sql_literal(v, kind))
135 .collect::<Vec<_>>()
136 .join(", ");
137 match kind {
138 DatabaseKind::PostgreSql => format!("ARRAY[{values}]"),
139 _ => format!("({values})"),
140 }
141 }
142 }
143}
144
145fn quoted_sql_string(value: &str) -> String {
146 format!("'{}'", value.replace('\'', "''"))
147}
148
149#[derive(Debug, Clone, PartialEq, Eq)]
150pub enum SqlCompileError {
151 UnknownEntity(String),
152 UnknownField(String),
153 EmptyInList,
154 MissingIdProperty(String),
155 MissingVersionProperty(String),
156 EmptyMutation(String),
157 InvalidRecoverVersion(i64),
158 UnsupportedSchemaType(DataType),
159 InvalidFunctionArguments(String),
160 InvalidSubQueryOperator(String),
161}
162
163impl std::fmt::Display for SqlCompileError {
164 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
165 match self {
166 Self::UnknownEntity(entity) => write!(f, "unknown entity: {entity}"),
167 Self::UnknownField(field) => write!(f, "unknown field: {field}"),
168 Self::EmptyInList => write!(f, "IN requires at least one value"),
169 Self::MissingIdProperty(entity) => write!(f, "entity {entity} has no id property"),
170 Self::MissingVersionProperty(entity) => {
171 write!(f, "entity {entity} has no version property")
172 }
173 Self::EmptyMutation(kind) => write!(f, "{kind} requires at least one writable field"),
174 Self::InvalidRecoverVersion(version) => {
175 write!(f, "recover requires a negative version, got {version}")
176 }
177 Self::UnsupportedSchemaType(data_type) => {
178 write!(f, "unsupported schema type: {data_type:?}")
179 }
180 Self::InvalidFunctionArguments(message) => write!(f, "{message}"),
181 Self::InvalidSubQueryOperator(operator) => {
182 write!(f, "subquery does not support operator: {operator}")
183 }
184 }
185 }
186}
187
188impl std::error::Error for SqlCompileError {}