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 trait ToValue {
fn to_value(&self) -> Value;
}
impl ToValue for Value {
fn to_value(&self) -> Value {
self.clone()
}
}
impl ToValue for i64 {
fn to_value(&self) -> Value {
Value::Int(*self)
}
}
impl ToValue for i32 {
fn to_value(&self) -> Value {
Value::Int(i64::from(*self))
}
}
impl ToValue for f64 {
fn to_value(&self) -> Value {
Value::Float(*self)
}
}
impl ToValue for bool {
fn to_value(&self) -> Value {
Value::Bool(*self)
}
}
impl ToValue for str {
fn to_value(&self) -> Value {
Value::Text(self.to_string())
}
}
impl ToValue for String {
fn to_value(&self) -> Value {
Value::Text(self.clone())
}
}
impl ToValue for [u8] {
fn to_value(&self) -> Value {
Value::Bytes(self.to_vec())
}
}
impl ToValue for Vec<u8> {
fn to_value(&self) -> Value {
Value::Bytes(self.clone())
}
}
impl<T: ToValue + ?Sized> ToValue for &T {
fn to_value(&self) -> Value {
(**self).to_value()
}
}
impl<T: ToValue> ToValue for Option<T> {
fn to_value(&self) -> Value {
self.as_ref().map_or(Value::Null, ToValue::to_value)
}
}
#[cfg(any(feature = "uuid", feature = "chrono", feature = "json"))]
fn text_of<'a>(v: &'a Value, what: &str) -> Result<&'a str, String> {
match v {
Value::Text(s) => Ok(s),
other => Err(wrong(what, other)),
}
}
#[cfg(feature = "uuid")]
impl FromValue for uuid::Uuid {
const KIND: FieldKind = FieldKind::Text;
fn from_value(v: &Value) -> Result<Self, String> {
let s = text_of(v, "a uuid as text")?;
uuid::Uuid::parse_str(s).map_err(|e| format!("{s:?} is not a uuid: {e}"))
}
}
#[cfg(feature = "uuid")]
impl ToValue for uuid::Uuid {
fn to_value(&self) -> Value {
Value::Text(self.hyphenated().to_string())
}
}
#[cfg(feature = "chrono")]
mod chrono_values {
use super::{FieldKind, FromValue, ToValue, Value, text_of};
use chrono::{DateTime, NaiveDate, NaiveDateTime, NaiveTime, SecondsFormat, Utc};
impl FromValue for DateTime<Utc> {
const KIND: FieldKind = FieldKind::Text;
fn from_value(v: &Value) -> Result<Self, String> {
let s = text_of(v, "a timestamp as text")?.replacen(' ', "T", 1);
if let Ok(t) = DateTime::parse_from_rfc3339(&s) {
return Ok(t.with_timezone(&Utc));
}
NaiveDateTime::parse_from_str(&s, "%Y-%m-%dT%H:%M:%S%.f")
.map(|n| n.and_utc())
.map_err(|e| format!("{s:?} is not a timestamp: {e}"))
}
}
impl ToValue for DateTime<Utc> {
fn to_value(&self) -> Value {
Value::Text(self.to_rfc3339_opts(SecondsFormat::AutoSi, true))
}
}
impl FromValue for NaiveDateTime {
const KIND: FieldKind = FieldKind::Text;
fn from_value(v: &Value) -> Result<Self, String> {
let s = text_of(v, "a timestamp as text")?.replacen(' ', "T", 1);
NaiveDateTime::parse_from_str(&s, "%Y-%m-%dT%H:%M:%S%.f")
.map_err(|e| format!("{s:?} is not a timestamp: {e}"))
}
}
impl ToValue for NaiveDateTime {
fn to_value(&self) -> Value {
Value::Text(self.format("%Y-%m-%dT%H:%M:%S%.f").to_string())
}
}
impl FromValue for NaiveDate {
const KIND: FieldKind = FieldKind::Text;
fn from_value(v: &Value) -> Result<Self, String> {
let s = text_of(v, "a date as text")?;
NaiveDate::parse_from_str(s, "%Y-%m-%d")
.map_err(|e| format!("{s:?} is not a date: {e}"))
}
}
impl ToValue for NaiveDate {
fn to_value(&self) -> Value {
Value::Text(self.format("%Y-%m-%d").to_string())
}
}
impl FromValue for NaiveTime {
const KIND: FieldKind = FieldKind::Text;
fn from_value(v: &Value) -> Result<Self, String> {
let s = text_of(v, "a time as text")?;
NaiveTime::parse_from_str(s, "%H:%M:%S%.f")
.map_err(|e| format!("{s:?} is not a time: {e}"))
}
}
impl ToValue for NaiveTime {
fn to_value(&self) -> Value {
Value::Text(self.format("%H:%M:%S%.f").to_string())
}
}
}
#[cfg(feature = "json")]
impl FromValue for serde_json::Value {
const KIND: FieldKind = FieldKind::Text;
fn from_value(v: &Value) -> Result<Self, String> {
let s = text_of(v, "json as text")?;
serde_json::from_str(s).map_err(|e| format!("not json: {e}"))
}
}
#[cfg(feature = "json")]
impl ToValue for serde_json::Value {
fn to_value(&self) -> Value {
Value::Text(self.to_string())
}
}
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));
}
#[test]
fn parameters_convert_and_an_option_none_is_null() {
assert_eq!(7i64.to_value(), Value::Int(7));
assert_eq!(7i32.to_value(), Value::Int(7));
assert_eq!("a".to_value(), Value::Text("a".into()));
assert_eq!("a".to_string().to_value(), Value::Text("a".into()));
assert_eq!(vec![1u8, 2].to_value(), Value::Bytes(vec![1, 2]));
assert_eq!(Some(true).to_value(), Value::Bool(true));
assert_eq!(Option::<i64>::None.to_value(), Value::Null);
}
#[cfg(feature = "uuid")]
#[test]
fn a_uuid_is_read_from_and_sent_as_canonical_text() {
let text = "a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11";
let id = uuid::Uuid::from_value(&Value::Text(text.into())).unwrap();
assert_eq!(id.to_value(), Value::Text(text.into()));
assert_eq!(
Option::<uuid::Uuid>::from_value(&Value::Null).unwrap(),
None
);
assert!(uuid::Uuid::from_value(&Value::Int(1)).is_err());
assert!(uuid::Uuid::from_value(&Value::Text("nope".into())).is_err());
}
#[cfg(feature = "chrono")]
#[test]
fn timestamps_dates_and_times_round_trip_through_their_text() {
use chrono::{DateTime, NaiveDate, NaiveDateTime, NaiveTime, Utc};
for text in ["2026-10-17T12:34:56Z", "2026-10-17T12:34:56.789Z"] {
let t = DateTime::<Utc>::from_value(&Value::Text(text.into())).unwrap();
assert_eq!(t.to_value(), Value::Text(text.into()));
}
let a =
DateTime::<Utc>::from_value(&Value::Text("2026-10-17T14:34:56+02:00".into())).unwrap();
let b = DateTime::<Utc>::from_value(&Value::Text("2026-10-17 12:34:56".into())).unwrap();
assert_eq!(a, b);
let n = NaiveDateTime::from_value(&Value::Text("2026-10-17T12:34:56.500".into())).unwrap();
assert_eq!(n.to_value(), Value::Text("2026-10-17T12:34:56.500".into()));
let d = NaiveDate::from_value(&Value::Text("2026-10-17".into())).unwrap();
assert_eq!(d.to_value(), Value::Text("2026-10-17".into()));
let t = NaiveTime::from_value(&Value::Text("12:34:56.789".into())).unwrap();
assert_eq!(t.to_value(), Value::Text("12:34:56.789".into()));
assert!(DateTime::<Utc>::from_value(&Value::Text("yesterday".into())).is_err());
assert!(NaiveDate::from_value(&Value::Int(1)).is_err());
assert!(NaiveDate::from_value(&Value::Text("2026-02-30".into())).is_err());
}
#[cfg(feature = "json")]
#[test]
fn json_is_read_from_and_sent_as_text() {
let v = serde_json::Value::from_value(&Value::Text(r#"{"a":[1,2]}"#.into())).unwrap();
assert_eq!(v["a"][1], 2);
assert_eq!(v.to_value(), Value::Text(r#"{"a":[1,2]}"#.into()));
assert!(serde_json::Value::from_value(&Value::Text("{".into())).is_err());
}
}