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