use crate::core::{Model, Op, SqlValue};
use crate::query::QuerySet;
use sqlx::postgres::{PgPool, PgRow};
use sqlx::FromRow;
use super::executor::Fetcher;
use super::ExecError;
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum ForeignKey<T> {
Unloaded(i64),
Loaded {
pk: i64,
value: Box<T>,
},
}
impl<T> ForeignKey<T> {
#[must_use]
pub fn unloaded(pk: i64) -> Self {
Self::Unloaded(pk)
}
#[must_use]
pub fn loaded(pk: i64, value: T) -> Self {
Self::Loaded {
pk,
value: Box::new(value),
}
}
#[must_use]
pub fn pk(&self) -> i64 {
match self {
Self::Unloaded(pk) | Self::Loaded { pk, .. } => *pk,
}
}
#[must_use]
pub fn is_loaded(&self) -> bool {
matches!(self, Self::Loaded { .. })
}
#[must_use]
pub fn value(&self) -> Option<&T> {
match self {
Self::Loaded { value, .. } => Some(value),
Self::Unloaded(_) => None,
}
}
#[must_use]
pub fn into_value(self) -> Option<T> {
match self {
Self::Loaded { value, .. } => Some(*value),
Self::Unloaded(_) => None,
}
}
}
impl<T> From<i64> for ForeignKey<T> {
fn from(pk: i64) -> Self {
Self::Unloaded(pk)
}
}
impl<T> From<ForeignKey<T>> for SqlValue {
fn from(fk: ForeignKey<T>) -> Self {
Self::I64(fk.pk())
}
}
impl<'r, T> sqlx::Decode<'r, sqlx::Postgres> for ForeignKey<T> {
fn decode(
value: <sqlx::Postgres as sqlx::Database>::ValueRef<'r>,
) -> Result<Self, sqlx::error::BoxDynError> {
Ok(Self::Unloaded(<i64 as sqlx::Decode<sqlx::Postgres>>::decode(value)?))
}
}
impl<T> sqlx::Type<sqlx::Postgres> for ForeignKey<T> {
fn type_info() -> sqlx::postgres::PgTypeInfo {
<i64 as sqlx::Type<sqlx::Postgres>>::type_info()
}
fn compatible(ty: &sqlx::postgres::PgTypeInfo) -> bool {
<i64 as sqlx::Type<sqlx::Postgres>>::compatible(ty)
}
}
impl<T> ForeignKey<T>
where
T: Model + for<'r> FromRow<'r, PgRow> + Send + Unpin,
{
pub async fn get(&mut self, pool: &PgPool) -> Result<&T, ExecError> {
if matches!(self, Self::Unloaded(_)) {
let pk = self.pk();
let pk_field = T::SCHEMA
.primary_key()
.ok_or(ExecError::MissingPrimaryKey {
table: T::SCHEMA.table,
})?;
let mut rows: Vec<T> = QuerySet::<T>::new()
.filter(pk_field.column, Op::Eq, pk)
.fetch(pool)
.await?;
let value = rows
.pop()
.ok_or(ExecError::ForeignKeyTargetMissing {
table: T::SCHEMA.table,
pk,
})?;
*self = Self::Loaded {
pk,
value: Box::new(value),
};
}
match self {
Self::Loaded { value, .. } => Ok(value),
Self::Unloaded(_) => unreachable!("just transitioned to Loaded above"),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn unloaded_constructor_and_pk_accessor() {
let fk: ForeignKey<()> = ForeignKey::unloaded(42);
assert_eq!(fk.pk(), 42);
assert!(!fk.is_loaded());
assert!(fk.value().is_none());
}
#[test]
fn loaded_constructor_caches_value() {
let fk = ForeignKey::loaded(7, "alice".to_string());
assert_eq!(fk.pk(), 7);
assert!(fk.is_loaded());
assert_eq!(fk.value(), Some(&"alice".to_string()));
}
#[test]
fn from_i64_yields_unloaded() {
let fk: ForeignKey<()> = 99_i64.into();
match fk {
ForeignKey::Unloaded(pk) => assert_eq!(pk, 99),
ForeignKey::Loaded { .. } => panic!("expected Unloaded"),
}
}
#[test]
fn into_sqlvalue_gives_i64_in_either_state() {
let unloaded: ForeignKey<()> = ForeignKey::unloaded(1);
let loaded = ForeignKey::loaded(2, ());
assert!(matches!(SqlValue::from(unloaded), SqlValue::I64(1)));
assert!(matches!(SqlValue::from(loaded), SqlValue::I64(2)));
}
#[test]
fn into_value_consumes_when_loaded() {
let loaded = ForeignKey::loaded(3, 100_u32);
assert_eq!(loaded.into_value(), Some(100));
let unloaded: ForeignKey<u32> = ForeignKey::unloaded(4);
assert_eq!(unloaded.into_value(), None);
}
}