use crate::record::SqliteValue;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum StorageClass {
Null,
Integer,
Real,
Text,
Blob,
}
impl StorageClass {
pub fn of(value: &SqliteValue) -> StorageClass {
match value {
SqliteValue::Null => StorageClass::Null,
SqliteValue::Integer(_) => StorageClass::Integer,
SqliteValue::Real(_) => StorageClass::Real,
SqliteValue::Text(_) => StorageClass::Text,
SqliteValue::Blob(_) => StorageClass::Blob,
}
}
pub fn as_str(self) -> &'static str {
match self {
StorageClass::Null => "null",
StorageClass::Integer => "integer",
StorageClass::Real => "real",
StorageClass::Text => "text",
StorageClass::Blob => "blob",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Affinity {
Integer,
Text,
Blob,
Real,
Numeric,
}
impl Affinity {
pub fn of(declared_type: Option<&str>) -> Affinity {
let Some(declared) = declared_type else {
return Affinity::Blob;
};
let upper = declared.to_ascii_uppercase();
if upper.contains("INT") {
Affinity::Integer
} else if upper.contains("CHAR") || upper.contains("CLOB") || upper.contains("TEXT") {
Affinity::Text
} else if upper.contains("BLOB") || upper.trim().is_empty() {
Affinity::Blob
} else if upper.contains("REAL") || upper.contains("FLOA") || upper.contains("DOUB") {
Affinity::Real
} else {
Affinity::Numeric
}
}
pub fn as_str(self) -> &'static str {
match self {
Affinity::Integer => "integer",
Affinity::Text => "text",
Affinity::Blob => "blob",
Affinity::Real => "real",
Affinity::Numeric => "numeric",
}
}
pub fn logical_class(self, value: &SqliteValue) -> StorageClass {
let stored = StorageClass::of(value);
match (self, stored) {
(Affinity::Real, StorageClass::Integer) => StorageClass::Real,
_ => stored,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_five_rules_run_in_order() {
let cases = [
("INTEGER", Affinity::Integer),
("INT", Affinity::Integer),
("BIGINT", Affinity::Integer),
("UNSIGNED BIG INT", Affinity::Integer),
("INT2", Affinity::Integer),
("VARCHAR(255)", Affinity::Text),
("NVARCHAR(160)", Affinity::Text),
("TEXT", Affinity::Text),
("CLOB", Affinity::Text),
("NCHAR(55)", Affinity::Text),
("BLOB", Affinity::Blob),
("REAL", Affinity::Real),
("DOUBLE PRECISION", Affinity::Real),
("FLOAT", Affinity::Real),
("NUMERIC", Affinity::Numeric),
("DECIMAL(10,5)", Affinity::Numeric),
("BOOLEAN", Affinity::Numeric),
("DATE", Affinity::Numeric),
("DATETIME", Affinity::Numeric),
("STRING", Affinity::Numeric),
];
for (declared, expected) in cases {
assert_eq!(Affinity::of(Some(declared)), expected, "{declared}");
assert_eq!(
Affinity::of(Some(&declared.to_lowercase())),
expected,
"{declared} in lower case"
);
}
}
#[test]
fn the_rules_that_look_like_bugs() {
assert_eq!(Affinity::of(Some("FLOATING POINT")), Affinity::Integer);
assert_eq!(Affinity::of(Some("POINT")), Affinity::Integer);
assert_eq!(Affinity::of(Some("CHARINT")), Affinity::Integer);
assert_eq!(Affinity::of(Some("BLOBINT")), Affinity::Integer);
assert_eq!(Affinity::of(None), Affinity::Blob);
assert_eq!(Affinity::of(Some("")), Affinity::Blob);
}
#[test]
fn an_integer_in_a_real_column_is_a_float() {
let integer = SqliteValue::Integer(1);
assert_eq!(
Affinity::Real.logical_class(&integer),
StorageClass::Real,
"sqlite writes whole floats as integers in real columns"
);
for affinity in [
Affinity::Integer,
Affinity::Numeric,
Affinity::Text,
Affinity::Blob,
] {
assert_eq!(affinity.logical_class(&integer), StorageClass::Integer);
}
}
#[test]
fn affinity_never_changes_the_other_classes() {
let values = [
SqliteValue::Null,
SqliteValue::Real(2.5),
SqliteValue::Text("x".into()),
SqliteValue::Blob(vec![1]),
];
for affinity in [
Affinity::Integer,
Affinity::Text,
Affinity::Blob,
Affinity::Real,
Affinity::Numeric,
] {
for value in &values {
assert_eq!(
affinity.logical_class(value),
StorageClass::of(value),
"{affinity:?} changed {value:?}"
);
}
}
}
}