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