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 && let Some(value) = index.checked_sub(1).and_then(|idx| params.get(idx))
74 {
75 output.push_str(&sql_literal(value, DatabaseKind::PostgreSql));
76 continue;
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 Value::TypedNull(_) => "NULL".to_owned(),
143 }
144}
145
146fn quoted_sql_string(value: &str) -> String {
147 format!("'{}'", value.replace('\'', "''"))
148}
149
150#[derive(Debug, Clone, PartialEq, Eq)]
151pub enum SqlCompileError {
152 UnknownEntity(String),
153 UnknownField(String),
154 EmptyInList,
155 MissingIdProperty(String),
156 MissingVersionProperty(String),
157 EmptyMutation(String),
158 InvalidRecoverVersion(i64),
159 UnsupportedSchemaType(DataType),
160 InvalidFunctionArguments(String),
161 InvalidSubQueryOperator(String),
162}
163
164impl std::fmt::Display for SqlCompileError {
165 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
166 match self {
167 Self::UnknownEntity(entity) => write!(f, "unknown entity: {entity}"),
168 Self::UnknownField(field) => write!(f, "unknown field: {field}"),
169 Self::EmptyInList => write!(f, "IN requires at least one value"),
170 Self::MissingIdProperty(entity) => write!(f, "entity {entity} has no id property"),
171 Self::MissingVersionProperty(entity) => {
172 write!(f, "entity {entity} has no version property")
173 }
174 Self::EmptyMutation(kind) => write!(f, "{kind} requires at least one writable field"),
175 Self::InvalidRecoverVersion(version) => {
176 write!(f, "recover requires a negative version, got {version}")
177 }
178 Self::UnsupportedSchemaType(data_type) => {
179 write!(f, "unsupported schema type: {data_type:?}")
180 }
181 Self::InvalidFunctionArguments(message) => write!(f, "{message}"),
182 Self::InvalidSubQueryOperator(operator) => {
183 write!(f, "subquery does not support operator: {operator}")
184 }
185 }
186 }
187}
188
189impl std::error::Error for SqlCompileError {}