1use crate::record::SqliteValue;
34
35#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
37pub enum StorageClass {
38 Null,
39 Integer,
40 Real,
41 Text,
42 Blob,
43}
44
45impl StorageClass {
46 pub fn of(value: &SqliteValue) -> StorageClass {
47 match value {
48 SqliteValue::Null => StorageClass::Null,
49 SqliteValue::Integer(_) => StorageClass::Integer,
50 SqliteValue::Real(_) => StorageClass::Real,
51 SqliteValue::Text(_) => StorageClass::Text,
52 SqliteValue::Blob(_) => StorageClass::Blob,
53 }
54 }
55
56 pub fn as_str(self) -> &'static str {
57 match self {
58 StorageClass::Null => "null",
59 StorageClass::Integer => "integer",
60 StorageClass::Real => "real",
61 StorageClass::Text => "text",
62 StorageClass::Blob => "blob",
63 }
64 }
65}
66
67#[derive(Debug, Clone, Copy, PartialEq, Eq)]
68pub enum Affinity {
69 Integer,
70 Text,
71 Blob,
72 Real,
73 Numeric,
74}
75
76impl Affinity {
77 pub fn of(declared_type: Option<&str>) -> Affinity {
80 let Some(declared) = declared_type else {
81 return Affinity::Blob;
82 };
83 let upper = declared.to_ascii_uppercase();
84 if upper.contains("INT") {
85 Affinity::Integer
86 } else if upper.contains("CHAR") || upper.contains("CLOB") || upper.contains("TEXT") {
87 Affinity::Text
88 } else if upper.contains("BLOB") || upper.trim().is_empty() {
89 Affinity::Blob
90 } else if upper.contains("REAL") || upper.contains("FLOA") || upper.contains("DOUB") {
91 Affinity::Real
92 } else {
93 Affinity::Numeric
94 }
95 }
96
97 pub fn as_str(self) -> &'static str {
98 match self {
99 Affinity::Integer => "integer",
100 Affinity::Text => "text",
101 Affinity::Blob => "blob",
102 Affinity::Real => "real",
103 Affinity::Numeric => "numeric",
104 }
105 }
106
107 pub fn logical_class(self, value: &SqliteValue) -> StorageClass {
113 let stored = StorageClass::of(value);
114 match (self, stored) {
115 (Affinity::Real, StorageClass::Integer) => StorageClass::Real,
116 _ => stored,
117 }
118 }
119}
120
121#[cfg(test)]
122mod tests {
123 use super::*;
124
125 #[test]
126 fn the_five_rules_run_in_order() {
127 let cases = [
128 ("INTEGER", Affinity::Integer),
129 ("INT", Affinity::Integer),
130 ("BIGINT", Affinity::Integer),
131 ("UNSIGNED BIG INT", Affinity::Integer),
132 ("INT2", Affinity::Integer),
133 ("VARCHAR(255)", Affinity::Text),
134 ("NVARCHAR(160)", Affinity::Text),
135 ("TEXT", Affinity::Text),
136 ("CLOB", Affinity::Text),
137 ("NCHAR(55)", Affinity::Text),
138 ("BLOB", Affinity::Blob),
139 ("REAL", Affinity::Real),
140 ("DOUBLE PRECISION", Affinity::Real),
141 ("FLOAT", Affinity::Real),
142 ("NUMERIC", Affinity::Numeric),
143 ("DECIMAL(10,5)", Affinity::Numeric),
144 ("BOOLEAN", Affinity::Numeric),
145 ("DATE", Affinity::Numeric),
146 ("DATETIME", Affinity::Numeric),
147 ("STRING", Affinity::Numeric),
148 ];
149 for (declared, expected) in cases {
150 assert_eq!(Affinity::of(Some(declared)), expected, "{declared}");
151 assert_eq!(
153 Affinity::of(Some(&declared.to_lowercase())),
154 expected,
155 "{declared} in lower case"
156 );
157 }
158 }
159
160 #[test]
161 fn the_rules_that_look_like_bugs() {
162 assert_eq!(Affinity::of(Some("FLOATING POINT")), Affinity::Integer);
164 assert_eq!(Affinity::of(Some("POINT")), Affinity::Integer);
165 assert_eq!(Affinity::of(Some("CHARINT")), Affinity::Integer);
167 assert_eq!(Affinity::of(Some("BLOBINT")), Affinity::Integer);
168 assert_eq!(Affinity::of(None), Affinity::Blob);
170 assert_eq!(Affinity::of(Some("")), Affinity::Blob);
171 }
172
173 #[test]
174 fn an_integer_in_a_real_column_is_a_float() {
175 let integer = SqliteValue::Integer(1);
176 assert_eq!(
177 Affinity::Real.logical_class(&integer),
178 StorageClass::Real,
179 "sqlite writes whole floats as integers in real columns"
180 );
181 for affinity in [
183 Affinity::Integer,
184 Affinity::Numeric,
185 Affinity::Text,
186 Affinity::Blob,
187 ] {
188 assert_eq!(affinity.logical_class(&integer), StorageClass::Integer);
189 }
190 }
191
192 #[test]
193 fn affinity_never_changes_the_other_classes() {
194 let values = [
195 SqliteValue::Null,
196 SqliteValue::Real(2.5),
197 SqliteValue::Text("x".into()),
198 SqliteValue::Blob(vec![1]),
199 ];
200 for affinity in [
201 Affinity::Integer,
202 Affinity::Text,
203 Affinity::Blob,
204 Affinity::Real,
205 Affinity::Numeric,
206 ] {
207 for value in &values {
208 assert_eq!(
209 affinity.logical_class(value),
210 StorageClass::of(value),
211 "{affinity:?} changed {value:?}"
212 );
213 }
214 }
215 }
216}