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