use crate::{RelError, Row, Value};
use std::marker::PhantomData;
use unisql_embed::Query;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum FieldKind {
Int,
Float,
Bool,
Text,
Bytes,
}
impl FieldKind {
const fn bit(self) -> u8 {
1 << (self as u8)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Field {
pub name: &'static str,
pub kind: FieldKind,
pub nullable: bool,
}
pub trait FromRow: Sized {
const FIELDS: &'static [Field];
fn from_row(row: &Row) -> Result<Self, RelError>;
}
pub trait FromValue: Sized {
const KIND: FieldKind;
const NULLABLE: bool = false;
fn from_value(v: &Value) -> Result<Self, String>;
}
fn wrong(expected: &str, got: &Value) -> String {
format!("expected {expected}, got {got:?}")
}
impl FromValue for i64 {
const KIND: FieldKind = FieldKind::Int;
fn from_value(v: &Value) -> Result<Self, String> {
match v {
Value::Int(n) => Ok(*n),
other => Err(wrong("an integer", other)),
}
}
}
impl FromValue for i32 {
const KIND: FieldKind = FieldKind::Int;
fn from_value(v: &Value) -> Result<Self, String> {
match v {
Value::Int(n) => i32::try_from(*n).map_err(|_| format!("{n} does not fit an i32")),
other => Err(wrong("an integer", other)),
}
}
}
impl FromValue for f64 {
const KIND: FieldKind = FieldKind::Float;
fn from_value(v: &Value) -> Result<Self, String> {
match v {
Value::Float(f) => Ok(*f),
other => Err(wrong("a float", other)),
}
}
}
impl FromValue for bool {
const KIND: FieldKind = FieldKind::Bool;
fn from_value(v: &Value) -> Result<Self, String> {
match v {
Value::Bool(b) => Ok(*b),
Value::Int(0) => Ok(false),
Value::Int(1) => Ok(true),
other => Err(wrong("a boolean (or 0 or 1)", other)),
}
}
}
impl FromValue for String {
const KIND: FieldKind = FieldKind::Text;
fn from_value(v: &Value) -> Result<Self, String> {
match v {
Value::Text(s) => Ok(s.clone()),
Value::Bytes(b) => {
String::from_utf8(b.clone()).map_err(|_| "bytes that are not UTF-8".to_string())
}
other => Err(wrong("text", other)),
}
}
}
impl FromValue for Vec<u8> {
const KIND: FieldKind = FieldKind::Bytes;
fn from_value(v: &Value) -> Result<Self, String> {
match v {
Value::Bytes(b) => Ok(b.clone()),
other => Err(wrong("bytes", other)),
}
}
}
impl<T: FromValue> FromValue for Option<T> {
const KIND: FieldKind = T::KIND;
const NULLABLE: bool = true;
fn from_value(v: &Value) -> Result<Self, String> {
match v {
Value::Null => Ok(None),
other => T::from_value(other).map(Some),
}
}
}
pub fn read<T: FromValue>(row: &Row, index: usize, name: &str) -> Result<T, RelError> {
let v = row.values.get(index).ok_or_else(|| {
RelError::Backend(format!(
"the row has {} columns, but field `{name}` is column {}",
row.values.len(),
index + 1
))
})?;
T::from_value(v)
.map_err(|why| RelError::Backend(format!("column {} (field `{name}`): {why}", index + 1)))
}
#[derive(Debug)]
pub struct TypedSql<T> {
pub query: Query,
marker: PhantomData<fn() -> T>,
}
impl<T> Clone for TypedSql<T> {
fn clone(&self) -> Self {
*self
}
}
impl<T> Copy for TypedSql<T> {}
impl<T> TypedSql<T> {
pub const fn new(query: Query) -> Self {
TypedSql {
query,
marker: PhantomData,
}
}
}
#[doc(hidden)]
pub mod check {
use super::{FieldKind, FromRow};
pub const NUMERIC: u8 = FieldKind::Int.bit() | FieldKind::Float.bit();
pub const TEXT: u8 = FieldKind::Text.bit();
pub const BOOLEAN: u8 = FieldKind::Bool.bit();
pub const OTHER: u8 = FieldKind::Bytes.bit() | FieldKind::Text.bit();
pub const ANY: u8 = 0xff;
const fn str_eq(a: &str, b: &str) -> bool {
let (a, b) = (a.as_bytes(), b.as_bytes());
if a.len() != b.len() {
return false;
}
let mut i = 0;
while i < a.len() {
if a[i] != b[i] {
return false;
}
i += 1;
}
true
}
pub const fn fields_are<T: FromRow>(n: usize) -> bool {
T::FIELDS.len() == n
}
pub const fn named<T: FromRow>(index: usize, name: &str) -> bool {
index < T::FIELDS.len() && str_eq(T::FIELDS[index].name, name)
}
pub const fn kind_in<T: FromRow>(index: usize, mask: u8) -> bool {
index < T::FIELDS.len() && T::FIELDS[index].kind.bit() & mask != 0
}
pub const fn null_fits<T: FromRow>(index: usize, column_nullable: bool) -> bool {
index < T::FIELDS.len() && (!column_nullable || T::FIELDS[index].nullable)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[derive(Debug)]
struct User {
id: i64,
email: String,
note: Option<String>,
}
impl FromRow for User {
const FIELDS: &'static [Field] = &[
Field {
name: "id",
kind: <i64 as FromValue>::KIND,
nullable: <i64 as FromValue>::NULLABLE,
},
Field {
name: "email",
kind: <String as FromValue>::KIND,
nullable: <String as FromValue>::NULLABLE,
},
Field {
name: "note",
kind: <Option<String> as FromValue>::KIND,
nullable: <Option<String> as FromValue>::NULLABLE,
},
];
fn from_row(row: &Row) -> Result<Self, RelError> {
Ok(User {
id: read(row, 0, "id")?,
email: read(row, 1, "email")?,
note: read(row, 2, "note")?,
})
}
}
#[test]
fn a_row_is_read_by_position_with_each_documented_loss() {
let row = Row::new(vec![Value::Int(7), Value::Text("a@x".into()), Value::Null]);
let u = User::from_row(&row).unwrap();
assert_eq!((u.id, u.email.as_str(), u.note), (7, "a@x", None));
assert_eq!(
String::from_value(&Value::Bytes(b"hi".to_vec())).unwrap(),
"hi"
);
assert!(bool::from_value(&Value::Int(1)).unwrap());
assert!(!bool::from_value(&Value::Int(0)).unwrap());
assert_eq!(Option::<i64>::from_value(&Value::Null).unwrap(), None);
assert_eq!(Option::<i64>::from_value(&Value::Int(3)).unwrap(), Some(3));
}
#[test]
fn what_cannot_be_read_says_which_column_and_why() {
let bad = Row::new(vec![
Value::Text("x".into()),
Value::Text("a".into()),
Value::Null,
]);
let e = User::from_row(&bad).unwrap_err().to_string();
assert!(
e.contains("column 1 (field `id`): expected an integer"),
"{e}"
);
let short = Row::new(vec![Value::Int(1)]);
let e = User::from_row(&short).unwrap_err().to_string();
assert!(
e.contains("the row has 1 columns, but field `email` is column 2"),
"{e}"
);
assert!(i32::from_value(&Value::Int(i64::MAX)).is_err());
assert!(bool::from_value(&Value::Int(2)).is_err());
assert!(i64::from_value(&Value::Null).is_err());
}
#[test]
fn the_const_checks_compare_name_kind_and_nullability() {
use check::*;
assert!(fields_are::<User>(3) && !fields_are::<User>(2));
assert!(named::<User>(1, "email") && !named::<User>(1, "mail") && !named::<User>(9, "x"));
assert!(kind_in::<User>(0, NUMERIC) && !kind_in::<User>(0, TEXT));
assert!(kind_in::<User>(1, TEXT) && kind_in::<User>(1, OTHER) && kind_in::<User>(1, ANY));
assert!(!null_fits::<User>(1, true) && null_fits::<User>(2, true));
assert!(null_fits::<User>(1, false) && null_fits::<User>(2, false));
const _: () = assert!(named::<User>(0, "id") && fields_are::<User>(3));
}
}