Skip to main content

teaql_sql/
types.rs

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 {}