Skip to main content

drizzle_types/sqlite/
sql_type.rs

1//! `SQLite` column types ([`SQLiteType`]) and affinities ([`SQLiteAffinity`]).
2
3/// A `SQLite` column type as written in DDL.
4///
5/// These are the types a STRICT table accepts, plus `NUMERIC`; see
6/// [SQLite datatypes](https://sqlite.org/datatype3.html). The type decides
7/// which column attributes are allowed ([`is_valid_flag`](Self::is_valid_flag)).
8///
9/// # Examples
10///
11/// ```
12/// use drizzle_types::sqlite::SQLiteType;
13///
14/// let int_type = SQLiteType::Integer;
15/// assert_eq!(int_type.to_sql_type(), "INTEGER");
16/// assert!(int_type.is_valid_flag("autoincrement"));
17///
18/// let text_type = SQLiteType::Text;
19/// assert!(text_type.is_valid_flag("json"));
20/// assert!(!text_type.is_valid_flag("autoincrement"));
21/// ```
22#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, Hash)]
23#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
24#[cfg_attr(feature = "serde", serde(rename_all = "UPPERCASE"))]
25pub enum SQLiteType {
26    /// `SQLite` INTEGER type - stores signed integers up to 8 bytes.
27    ///
28    /// See: <https://sqlite.org/datatype3.html#integer_datatype>
29    ///
30    /// Supports: primary keys, autoincrement, enums (discriminant storage)
31    Integer,
32
33    /// `SQLite` TEXT type - stores text in UTF-8, UTF-16BE, or UTF-16LE encoding.
34    ///
35    /// See: <https://sqlite.org/datatype3.html#text_datatype>
36    ///
37    /// Supports: enums (variant name storage), JSON (`#[column(json)]`)
38    Text,
39
40    /// `SQLite` BLOB type - stores binary data exactly as input.
41    ///
42    /// See: <https://sqlite.org/datatype3.html#blob_datatype>
43    ///
44    /// Used for byte arrays and UUIDs.
45    Blob,
46
47    /// `SQLite` REAL type - stores floating point values as 8-byte IEEE floating point numbers.
48    ///
49    /// See: <https://sqlite.org/datatype3.html#real_datatype>
50    Real,
51
52    /// `SQLite` NUMERIC type - stores values as INTEGER, REAL, or TEXT depending on the value.
53    ///
54    /// See: <https://sqlite.org/datatype3.html#numeric_datatype>
55    Numeric,
56
57    /// `SQLite` ANY type - no type affinity, can store any type of data.
58    /// This holds in STRICT tables; elsewhere `SQLite` gives a column declared
59    /// `ANY` `NUMERIC` affinity. The default.
60    ///
61    /// See: <https://sqlite.org/stricttables.html>
62    #[default]
63    Any,
64}
65
66/// `SQLite` type affinity: how a column converts values it stores.
67///
68/// See <https://sqlite.org/datatype3.html#type_affinity>.
69#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
70#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
71#[cfg_attr(feature = "serde", serde(rename_all = "UPPERCASE"))]
72pub enum SQLiteAffinity {
73    /// `INTEGER` affinity.
74    Integer,
75    /// `TEXT` affinity.
76    Text,
77    /// `BLOB` affinity: values are stored as given.
78    Blob,
79    /// `REAL` affinity.
80    Real,
81    /// `NUMERIC` affinity.
82    Numeric,
83    /// No affinity (`ANY` in STRICT tables).
84    Any,
85}
86
87impl SQLiteType {
88    /// Parses a type name from a column attribute, ignoring case. Returns
89    /// `None` for unknown names.
90    ///
91    /// Also accepts `"number"` (as `NUMERIC`) and `"boolean"` (as `INTEGER`).
92    #[must_use]
93    pub const fn from_attribute_name(name: &str) -> Option<Self> {
94        if name.eq_ignore_ascii_case("integer") {
95            Some(Self::Integer)
96        } else if name.eq_ignore_ascii_case("text") {
97            Some(Self::Text)
98        } else if name.eq_ignore_ascii_case("blob") {
99            Some(Self::Blob)
100        } else if name.eq_ignore_ascii_case("real") {
101            Some(Self::Real)
102        } else if name.eq_ignore_ascii_case("number") || name.eq_ignore_ascii_case("numeric") {
103            Some(Self::Numeric)
104        } else if name.eq_ignore_ascii_case("boolean") {
105            Some(Self::Integer) // Store booleans as integers (0/1)
106        } else if name.eq_ignore_ascii_case("any") {
107            Some(Self::Any)
108        } else {
109            None
110        }
111    }
112
113    /// Returns the type as written in DDL, such as `"INTEGER"`.
114    #[must_use]
115    pub const fn to_sql_type(&self) -> &'static str {
116        match self {
117            Self::Integer => "INTEGER",
118            Self::Text => "TEXT",
119            Self::Blob => "BLOB",
120            Self::Real => "REAL",
121            Self::Numeric => "NUMERIC",
122            Self::Any => "ANY",
123        }
124    }
125
126    /// Returns this type's `SQLite` affinity.
127    #[must_use]
128    pub const fn affinity(&self) -> SQLiteAffinity {
129        match self {
130            Self::Integer => SQLiteAffinity::Integer,
131            Self::Text => SQLiteAffinity::Text,
132            Self::Blob => SQLiteAffinity::Blob,
133            Self::Real => SQLiteAffinity::Real,
134            Self::Numeric => SQLiteAffinity::Numeric,
135            Self::Any => SQLiteAffinity::Any,
136        }
137    }
138
139    /// Returns `true` if a STRICT table accepts this type: every type except `NUMERIC`.
140    #[must_use]
141    pub const fn is_strict_allowed(&self) -> bool {
142        matches!(
143            self,
144            Self::Integer | Self::Real | Self::Text | Self::Blob | Self::Any
145        )
146    }
147
148    /// Returns `true` if the column attribute flag can be used with this type.
149    ///
150    /// Valid flags per type:
151    ///
152    /// - `INTEGER`: `primary`, `primary_key`, `unique`, `autoincrement`, `enum`
153    /// - `TEXT`: `primary`, `primary_key`, `unique`, `json`, `enum`
154    /// - `BLOB`: `primary`, `primary_key`, `unique`
155    /// - `REAL`: `primary`, `primary_key`, `unique`
156    /// - `NUMERIC`: `primary`, `primary_key`, `unique`
157    /// - `ANY`: `primary`, `primary_key`, `unique`
158    #[must_use]
159    pub fn is_valid_flag(&self, flag: &str) -> bool {
160        matches!(flag, "primary" | "primary_key" | "unique")
161            || matches!(
162                (self, flag),
163                (Self::Integer, "autoincrement")
164                    | (Self::Text, "json")
165                    | (Self::Text | Self::Integer, "enum")
166            )
167    }
168}
169
170impl core::fmt::Display for SQLiteType {
171    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
172        f.write_str(self.to_sql_type())
173    }
174}
175
176#[cfg(test)]
177mod tests {
178    use super::*;
179
180    #[test]
181    fn test_from_attribute_name() {
182        assert_eq!(
183            SQLiteType::from_attribute_name("integer"),
184            Some(SQLiteType::Integer)
185        );
186        assert_eq!(
187            SQLiteType::from_attribute_name("INTEGER"),
188            Some(SQLiteType::Integer)
189        );
190        assert_eq!(
191            SQLiteType::from_attribute_name("text"),
192            Some(SQLiteType::Text)
193        );
194        assert_eq!(
195            SQLiteType::from_attribute_name("blob"),
196            Some(SQLiteType::Blob)
197        );
198        assert_eq!(
199            SQLiteType::from_attribute_name("boolean"),
200            Some(SQLiteType::Integer)
201        );
202        assert_eq!(SQLiteType::from_attribute_name("unknown"), None);
203    }
204
205    #[test]
206    fn test_to_sql_type() {
207        assert_eq!(SQLiteType::Integer.to_sql_type(), "INTEGER");
208        assert_eq!(SQLiteType::Text.to_sql_type(), "TEXT");
209        assert_eq!(SQLiteType::Blob.to_sql_type(), "BLOB");
210        assert_eq!(SQLiteType::Real.to_sql_type(), "REAL");
211        assert_eq!(SQLiteType::Numeric.to_sql_type(), "NUMERIC");
212        assert_eq!(SQLiteType::Any.to_sql_type(), "ANY");
213    }
214
215    #[test]
216    fn test_affinity_mapping() {
217        assert_eq!(SQLiteType::Integer.affinity(), SQLiteAffinity::Integer);
218        assert_eq!(SQLiteType::Text.affinity(), SQLiteAffinity::Text);
219        assert_eq!(SQLiteType::Blob.affinity(), SQLiteAffinity::Blob);
220        assert_eq!(SQLiteType::Real.affinity(), SQLiteAffinity::Real);
221        assert_eq!(SQLiteType::Numeric.affinity(), SQLiteAffinity::Numeric);
222        assert_eq!(SQLiteType::Any.affinity(), SQLiteAffinity::Any);
223    }
224
225    #[test]
226    fn test_strict_allowed_types() {
227        assert!(SQLiteType::Integer.is_strict_allowed());
228        assert!(SQLiteType::Text.is_strict_allowed());
229        assert!(SQLiteType::Blob.is_strict_allowed());
230        assert!(SQLiteType::Real.is_strict_allowed());
231        assert!(SQLiteType::Any.is_strict_allowed());
232        assert!(!SQLiteType::Numeric.is_strict_allowed());
233    }
234
235    #[test]
236    fn test_is_valid_flag() {
237        // Autoincrement only valid for INTEGER
238        assert!(SQLiteType::Integer.is_valid_flag("autoincrement"));
239        assert!(!SQLiteType::Text.is_valid_flag("autoincrement"));
240        assert!(!SQLiteType::Blob.is_valid_flag("autoincrement"));
241
242        // JSON uses TEXT storage.
243        assert!(SQLiteType::Text.is_valid_flag("json"));
244        assert!(!SQLiteType::Blob.is_valid_flag("json"));
245        assert!(!SQLiteType::Integer.is_valid_flag("json"));
246
247        // Enum valid for TEXT and INTEGER
248        assert!(SQLiteType::Text.is_valid_flag("enum"));
249        assert!(SQLiteType::Integer.is_valid_flag("enum"));
250        assert!(!SQLiteType::Blob.is_valid_flag("enum"));
251
252        // Primary/unique valid for all
253        assert!(SQLiteType::Integer.is_valid_flag("primary"));
254        assert!(SQLiteType::Text.is_valid_flag("unique"));
255        assert!(SQLiteType::Blob.is_valid_flag("primary_key"));
256    }
257}