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