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