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