Skip to main content

rusticx_sql/
compiler.rs

1use rusticx_core::{
2    column::ColumnDef,
3    model::TableSchema,
4    query::{CondOp, Conjunction, Direction, QueryBuilder},
5    value::Value,
6};
7use crate::dialect::SqlDialect;
8
9/// Compiles a `QueryBuilder` or `TableSchema` into parameterized SQL.
10pub struct SqlCompiler<'d, D: SqlDialect> {
11    dialect: &'d D,
12}
13
14impl<'d, D: SqlDialect> SqlCompiler<'d, D> {
15    pub fn new(dialect: &'d D) -> Self {
16        Self { dialect }
17    }
18
19    // ── DDL ──────────────────────────────────────────────────────────────
20
21    pub fn create_table(&self, schema: &TableSchema) -> String {
22        let table = self.dialect.quote_ident(&schema.table);
23        let col_defs: Vec<String> = schema.columns.iter().map(|c| self.col_def(c)).collect();
24
25        // Primary keys
26        let pks: Vec<String> = schema
27            .columns
28            .iter()
29            .filter(|c| c.primary_key)
30            .map(|c| self.dialect.quote_ident(&c.name))
31            .collect();
32
33        let mut parts = col_defs;
34        if !pks.is_empty() {
35            parts.push(format!("PRIMARY KEY ({})", pks.join(", ")));
36        }
37
38        // Unique constraints
39        for col in schema.columns.iter().filter(|c| c.unique && !c.primary_key) {
40            parts.push(format!(
41                "UNIQUE ({})",
42                self.dialect.quote_ident(&col.name)
43            ));
44        }
45
46        let mut sql = format!(
47            "CREATE TABLE IF NOT EXISTS {table} (\n  {}\n)",
48            parts.join(",\n  ")
49        );
50
51        // Indexes
52        let mut index_stmts = vec![];
53        for idx in &schema.indexes {
54            let cols: Vec<String> = idx.columns.iter().map(|c| self.dialect.quote_ident(c)).collect();
55            let unique = if idx.unique { "UNIQUE " } else { "" };
56            index_stmts.push(format!(
57                "CREATE {unique}INDEX IF NOT EXISTS {} ON {table} ({})",
58                self.dialect.quote_ident(&idx.name),
59                cols.join(", ")
60            ));
61        }
62
63        if !index_stmts.is_empty() {
64            sql.push(';');
65            sql.push('\n');
66            sql.push_str(&index_stmts.join(";\n"));
67        }
68
69        sql
70    }
71
72    fn col_def(&self, col: &ColumnDef) -> String {
73        let name = self.dialect.quote_ident(&col.name);
74        let ty = self.dialect.sql_type(&col.col_type);
75        let mut parts = vec![format!("{name} {ty}")];
76
77        if !col.nullable {
78            parts.push("NOT NULL".into());
79        }
80        if let Some(ref d) = col.default {
81            parts.push(format!("DEFAULT {d}"));
82        }
83
84        parts.join(" ")
85    }
86
87    pub fn drop_table(&self, table: &str) -> String {
88        format!("DROP TABLE IF EXISTS {}", self.dialect.quote_ident(table))
89    }
90
91    pub fn table_exists(&self, table: &str) -> (String, Vec<Value>) {
92        // ANSI information_schema — works for both PG and MySQL
93        let sql = "SELECT COUNT(*) FROM information_schema.tables WHERE table_name = $1".to_owned();
94        (sql, vec![Value::Text(table.to_owned())])
95    }
96
97    // ── DML ──────────────────────────────────────────────────────────────
98
99    /// Compile SELECT.
100    pub fn select(&self, qb: &QueryBuilder) -> (String, Vec<Value>) {
101        let table = self.dialect.quote_ident(&qb.table);
102        let cols = if qb.columns.is_empty() {
103            "*".to_owned()
104        } else {
105            qb.columns.iter().map(|c| self.dialect.quote_ident(c)).collect::<Vec<_>>().join(", ")
106        };
107
108        let mut sql = format!("SELECT {cols} FROM {table}");
109        let mut bindings: Vec<Value> = vec![];
110        let mut idx = 1usize;
111
112        if !qb.conditions.is_empty() {
113            let (where_clause, b, next_idx) = self.where_clause(&qb.conditions, idx);
114            sql.push_str(&format!(" WHERE {where_clause}"));
115            bindings.extend(b);
116            idx = next_idx;
117        }
118
119        if !qb.order_by.is_empty() {
120            let orders: Vec<String> = qb
121                .order_by
122                .iter()
123                .map(|o| {
124                    let dir = match o.direction {
125                        Direction::Asc => "ASC",
126                        Direction::Desc => "DESC",
127                    };
128                    format!("{} {dir}", self.dialect.quote_ident(&o.column))
129                })
130                .collect();
131            sql.push_str(&format!(" ORDER BY {}", orders.join(", ")));
132        }
133
134        if let Some(limit) = qb.limit {
135            sql.push_str(&format!(" LIMIT {limit}"));
136        }
137        if let Some(offset) = qb.offset {
138            sql.push_str(&format!(" OFFSET {offset}"));
139        }
140
141        (sql, bindings)
142    }
143
144    /// Compile INSERT, returns (sql, bindings).
145    pub fn insert(&self, table: &str, row: &[(String, Value)]) -> (String, Vec<Value>) {
146        let t = self.dialect.quote_ident(table);
147        let cols: Vec<String> = row.iter().map(|(c, _)| self.dialect.quote_ident(c)).collect();
148        let mut bindings: Vec<Value> = vec![];
149        let placeholders: Vec<String> = row
150            .iter()
151            .enumerate()
152            .map(|(i, (_, v))| {
153                bindings.push(v.clone());
154                self.dialect.placeholder(i + 1)
155            })
156            .collect();
157
158        let mut sql = format!(
159            "INSERT INTO {t} ({}) VALUES ({})",
160            cols.join(", "),
161            placeholders.join(", ")
162        );
163
164        if self.dialect.supports_returning() {
165            sql.push_str(" RETURNING *");
166        }
167
168        (sql, bindings)
169    }
170
171    /// Compile UPDATE.
172    pub fn update(&self, qb: &QueryBuilder) -> (String, Vec<Value>) {
173        let table = self.dialect.quote_ident(&qb.table);
174        let mut bindings: Vec<Value> = vec![];
175        let mut idx = 1usize;
176
177        let sets: Vec<String> = qb
178            .values
179            .iter()
180            .map(|(col, val)| {
181                let ph = self.dialect.placeholder(idx);
182                idx += 1;
183                bindings.push(val.clone());
184                format!("{} = {ph}", self.dialect.quote_ident(col))
185            })
186            .collect();
187
188        let mut sql = format!("UPDATE {table} SET {}", sets.join(", "));
189
190        if !qb.conditions.is_empty() {
191            let (where_clause, b, _) = self.where_clause(&qb.conditions, idx);
192            sql.push_str(&format!(" WHERE {where_clause}"));
193            bindings.extend(b);
194        }
195
196        (sql, bindings)
197    }
198
199    /// Compile DELETE.
200    pub fn delete(&self, qb: &QueryBuilder) -> (String, Vec<Value>) {
201        let table = self.dialect.quote_ident(&qb.table);
202        let mut bindings: Vec<Value> = vec![];
203
204        let mut sql = format!("DELETE FROM {table}");
205
206        if !qb.conditions.is_empty() {
207            let (where_clause, b, _) = self.where_clause(&qb.conditions, 1);
208            sql.push_str(&format!(" WHERE {where_clause}"));
209            bindings.extend(b);
210        }
211
212        (sql, bindings)
213    }
214
215    /// Compile COUNT.
216    pub fn count(&self, qb: &QueryBuilder) -> (String, Vec<Value>) {
217        let table = self.dialect.quote_ident(&qb.table);
218        let mut sql = format!("SELECT COUNT(*) AS count FROM {table}");
219        let mut bindings: Vec<Value> = vec![];
220
221        if !qb.conditions.is_empty() {
222            let (where_clause, b, _) = self.where_clause(&qb.conditions, 1);
223            sql.push_str(&format!(" WHERE {where_clause}"));
224            bindings.extend(b);
225        }
226
227        (sql, bindings)
228    }
229
230    // ── WHERE builder ─────────────────────────────────────────────────────
231
232    fn where_clause(
233        &self,
234        conditions: &[rusticx_core::query::Condition],
235        start_idx: usize,
236    ) -> (String, Vec<Value>, usize) {
237        let mut parts: Vec<String> = vec![];
238        let mut bindings: Vec<Value> = vec![];
239        let mut idx = start_idx;
240
241        for (i, cond) in conditions.iter().enumerate() {
242            let col = self.dialect.quote_ident(&cond.column);
243            let (op_str, needs_binding) = match &cond.op {
244                CondOp::Eq => ("=", true),
245                CondOp::Ne => ("!=", true),
246                CondOp::Gt => (">", true),
247                CondOp::Gte => (">=", true),
248                CondOp::Lt => ("<", true),
249                CondOp::Lte => ("<=", true),
250                CondOp::Like => ("LIKE", true),
251                CondOp::ILike => ("ILIKE", true),
252                CondOp::In => ("IN", true),
253                CondOp::NotIn => ("NOT IN", true),
254                CondOp::IsNull => ("IS NULL", false),
255                CondOp::IsNotNull => ("IS NOT NULL", false),
256            };
257
258            let part = if needs_binding {
259                let ph = self.dialect.placeholder(idx);
260                idx += 1;
261                bindings.push(cond.value.clone());
262                format!("{col} {op_str} {ph}")
263            } else {
264                format!("{col} {op_str}")
265            };
266
267            let conj = if i == 0 {
268                String::new()
269            } else {
270                match cond.conjunction {
271                    Conjunction::And => "AND ".to_owned(),
272                    Conjunction::Or => "OR ".to_owned(),
273                }
274            };
275            parts.push(format!("{conj}{part}"));
276        }
277
278        (parts.join(" "), bindings, idx)
279    }
280}