Skip to main content

rango_orm_core/
query.rs

1impl SqlValue {
2    /// True for every typed NULL variant.
3    pub fn is_null(&self) -> bool {
4        matches!(
5            self,
6            SqlValue::NullBool
7                | SqlValue::NullSmallInt
8                | SqlValue::NullInt
9                | SqlValue::NullBigInt
10                | SqlValue::NullFloat
11                | SqlValue::NullDouble
12                | SqlValue::NullDecimal
13                | SqlValue::NullText
14                | SqlValue::NullBytes
15                | SqlValue::NullUuid
16                | SqlValue::NullDateTime
17                | SqlValue::NullDate
18                | SqlValue::NullTime
19                | SqlValue::NullJson
20        )
21    }
22}
23
24/// A SQL-compatible value — used to bind field values to queries
25/// without requiring sqlx as a direct dependency of rango-orm-core.
26#[derive(Debug, Clone, PartialEq)]
27pub enum SqlValue {
28    // Typed nulls — carry the column type so the DB can infer correctly.
29    NullBool,
30    NullSmallInt,
31    NullInt,
32    NullBigInt,
33    NullFloat,
34    NullDouble,
35    NullDecimal,
36    NullText,
37    NullBytes,
38    NullUuid,
39    NullDateTime,
40    NullDate,
41    NullTime,
42    NullJson,
43    // Non-null values
44    Bool(bool),
45    SmallInt(i16),
46    Int(i32),
47    BigInt(i64),
48    Float(f32),
49    Double(f64),
50    Decimal(rust_decimal::Decimal),
51    Text(String),
52    Bytes(Vec<u8>),
53    Uuid(uuid::Uuid),
54    DateTime(chrono::DateTime<chrono::Utc>),
55    Date(chrono::NaiveDate),
56    Time(chrono::NaiveTime),
57    Json(serde_json::Value),
58}
59
60/// Implemented by field types — converts to a SQL-bindable value.
61pub trait ToSqlValue {
62    fn to_sql_value(&self) -> SqlValue;
63}
64
65// ─── From<T> conversions for SqlValue ────────────────────────────────────────
66
67impl From<&str> for SqlValue {
68    fn from(s: &str) -> Self {
69        SqlValue::Text(s.to_string())
70    }
71}
72impl From<String> for SqlValue {
73    fn from(s: String) -> Self {
74        SqlValue::Text(s)
75    }
76}
77impl From<bool> for SqlValue {
78    fn from(v: bool) -> Self {
79        SqlValue::Bool(v)
80    }
81}
82impl From<i16> for SqlValue {
83    fn from(v: i16) -> Self {
84        SqlValue::SmallInt(v)
85    }
86}
87impl From<i32> for SqlValue {
88    fn from(v: i32) -> Self {
89        SqlValue::Int(v)
90    }
91}
92impl From<i64> for SqlValue {
93    fn from(v: i64) -> Self {
94        SqlValue::BigInt(v)
95    }
96}
97impl From<f32> for SqlValue {
98    fn from(v: f32) -> Self {
99        SqlValue::Float(v)
100    }
101}
102impl From<f64> for SqlValue {
103    fn from(v: f64) -> Self {
104        SqlValue::Double(v)
105    }
106}
107impl From<rust_decimal::Decimal> for SqlValue {
108    fn from(v: rust_decimal::Decimal) -> Self {
109        SqlValue::Decimal(v)
110    }
111}
112impl From<uuid::Uuid> for SqlValue {
113    fn from(v: uuid::Uuid) -> Self {
114        SqlValue::Uuid(v)
115    }
116}
117impl From<chrono::DateTime<chrono::Utc>> for SqlValue {
118    fn from(v: chrono::DateTime<chrono::Utc>) -> Self {
119        SqlValue::DateTime(v)
120    }
121}
122impl From<chrono::NaiveDate> for SqlValue {
123    fn from(v: chrono::NaiveDate) -> Self {
124        SqlValue::Date(v)
125    }
126}
127impl From<chrono::NaiveTime> for SqlValue {
128    fn from(v: chrono::NaiveTime) -> Self {
129        SqlValue::Time(v)
130    }
131}
132impl From<Vec<u8>> for SqlValue {
133    fn from(v: Vec<u8>) -> Self {
134        SqlValue::Bytes(v)
135    }
136}
137impl From<serde_json::Value> for SqlValue {
138    fn from(v: serde_json::Value) -> Self {
139        SqlValue::Json(v)
140    }
141}
142
143/// Marker trait — implemented by #[derive(Model)] to enable filter() method.
144/// The actual QueryBuilder is in rango-orm-postgres to avoid circular deps.
145pub trait Filterable: Sized {}
146
147/// Implemented by `#[derive(Model)]` — provides field names + values for INSERT/UPDATE.
148#[diagnostic::on_unimplemented(
149    message = "`{Self}` cannot be written to the database",
150    note = "add `#[derive(Model)]` (or `#[rango::model]`) to `{Self}`"
151)]
152pub trait ModelValues {
153    /// Returns (column_name, value) pairs for all non-PK fields.
154    fn field_values(&self) -> Vec<(&'static str, SqlValue)>;
155
156    /// Returns the primary key value.
157    fn pk_value(&self) -> SqlValue;
158
159    /// Returns the primary key column name.
160    fn pk_column() -> &'static str;
161
162    /// Stores a database-generated primary key (auto-increment PKs only; no-op otherwise).
163    /// Used by backends without `RETURNING` (MySQL).
164    fn set_generated_pk(&mut self, _id: i64) {}
165}
166
167/// Columns and values to INSERT for `model`. A NULL primary key (an unsaved
168/// `FieldAutoInt` / `FieldAutoBigInt`) is left out so the database generates it.
169pub fn insert_values<M: ModelValues>(model: &M) -> Vec<(&'static str, SqlValue)> {
170    let pk = model.pk_value();
171    let fields = model.field_values();
172    let mut values = Vec::with_capacity(fields.len() + 1);
173    if !pk.is_null() {
174        values.push((M::pk_column(), pk));
175    }
176    values.extend(fields);
177    values
178}
179
180/// Quote a SQL identifier (table or column name), doubling embedded `"`.
181/// Valid for PostgreSQL and SQLite.
182pub fn quote_ident(ident: &str) -> String {
183    format!("\"{}\"", ident.replace('"', "\"\""))
184}
185
186/// Quote a MySQL / MariaDB identifier with backticks, doubling embedded `` ` ``.
187pub fn quote_ident_mysql(ident: &str) -> String {
188    format!("`{}`", ident.replace('`', "``"))
189}
190
191#[cfg(test)]
192mod quote_ident_tests {
193    use super::{quote_ident, quote_ident_mysql};
194
195    #[test]
196    fn test_quote_ident_mysql_escapes_backticks() {
197        assert_eq!(quote_ident_mysql("user"), "`user`");
198        assert_eq!(
199            quote_ident_mysql("a`; DROP TABLE x; --"),
200            "`a``; DROP TABLE x; --`"
201        );
202    }
203
204    #[test]
205    fn test_quote_ident_escapes_double_quotes() {
206        assert_eq!(quote_ident("name"), "\"name\"");
207        assert_eq!(
208            quote_ident("a\"; DROP TABLE x; --"),
209            "\"a\"\"; DROP TABLE x; --\""
210        );
211    }
212}