use crate::model::Model;
use bytes::BytesMut;
use postgres_types::{FromSql, IsNull, ToSql, Type};
use std::marker::PhantomData;
pub struct ForeignKey<T: Model> {
key: T::Pk,
_target: PhantomData<fn() -> T>,
}
impl<T: Model> Clone for ForeignKey<T> {
fn clone(&self) -> Self {
Self {
key: self.key.clone(),
_target: PhantomData,
}
}
}
impl<T: Model> Copy for ForeignKey<T> where T::Pk: Copy {}
impl<T: Model> PartialEq for ForeignKey<T>
where
T::Pk: PartialEq,
{
fn eq(&self, other: &Self) -> bool {
self.key == other.key
}
}
impl<T: Model> Eq for ForeignKey<T> where T::Pk: Eq {}
impl<T: Model> std::hash::Hash for ForeignKey<T>
where
T::Pk: std::hash::Hash,
{
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.key.hash(state);
}
}
impl<T: Model> std::fmt::Debug for ForeignKey<T>
where
T::Pk: std::fmt::Debug,
{
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "ForeignKey<{}>({:?})", T::table_name(), self.key)
}
}
impl<T: Model> ForeignKey<T> {
#[inline]
pub fn new(key: T::Pk) -> Self {
Self {
key,
_target: PhantomData,
}
}
#[inline]
pub fn key(&self) -> T::Pk
where
T::Pk: Clone,
{
self.key.clone()
}
#[inline]
pub fn resolved(&self) -> Option<&T> {
None
}
pub async fn fetch(
&self,
ctx: &mut crate::context::DjogiContext,
) -> Result<T, crate::DjogiError>
where
T::Pk: Clone,
{
T::get(ctx, self.key.clone()).await
}
}
impl<T: Model> ToSql for ForeignKey<T>
where
T::Pk: ToSql,
{
fn to_sql(
&self,
ty: &Type,
out: &mut BytesMut,
) -> Result<IsNull, Box<dyn std::error::Error + Sync + Send>> {
<T::Pk as ToSql>::to_sql(&self.key, ty, out)
}
fn accepts(ty: &Type) -> bool {
<T::Pk as ToSql>::accepts(ty)
}
postgres_types::to_sql_checked!();
}
impl<'a, T: Model> FromSql<'a> for ForeignKey<T>
where
T::Pk: FromSql<'a>,
{
fn from_sql(
ty: &Type,
raw: &'a [u8],
) -> Result<Self, Box<dyn std::error::Error + Sync + Send>> {
<T::Pk as FromSql<'a>>::from_sql(ty, raw).map(ForeignKey::new)
}
fn accepts(ty: &Type) -> bool {
<T::Pk as FromSql<'a>>::accepts(ty)
}
}
impl<T: Model> serde::Serialize for ForeignKey<T>
where
T::Pk: serde::Serialize,
{
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
self.key.serialize(serializer)
}
}
impl<'de, T: Model> serde::Deserialize<'de> for ForeignKey<T>
where
T::Pk: serde::Deserialize<'de>,
{
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
<T::Pk as serde::Deserialize<'de>>::deserialize(deserializer).map(ForeignKey::new)
}
}
impl<T: Model> crate::query::field::IntoFilterValue for ForeignKey<T>
where
T::Pk: crate::query::field::IntoFilterValue + Clone,
{
fn into_filter_value(self) -> crate::query::condition::FilterValue {
self.key.into_filter_value()
}
fn jsonb_sql_cast() -> Option<crate::jsonb::JsonbSqlCast> {
<T::Pk as crate::query::field::IntoFilterValue>::jsonb_sql_cast()
}
}
impl<T: Model + 'static> crate::query::field::DjogiPortableEq for ForeignKey<T> where
T::Pk: crate::query::field::DjogiPortableEq
{
}
impl<M: Model, T: Model> crate::query::field::FieldRef<M, ForeignKey<T>> {
#[must_use = "expressions are lazy — dropping one silently omits the predicate"]
pub fn as_pk_expr(self) -> crate::expr::Expr<T::Pk> {
crate::expr::Expr::from_node(crate::expr::node::ExprNode::Field {
column: self.column(),
})
}
}
impl<M: Model, T: Model> crate::query::field::DjogiField<M, ForeignKey<T>> {
#[must_use = "expressions are lazy — dropping one silently omits the predicate"]
pub fn as_pk_expr(self) -> crate::expr::Expr<T::Pk> {
crate::expr::Expr::from_node(crate::expr::node::ExprNode::Field {
column: self.column(),
})
}
}
pub struct ForeignKeyResolved<T: Model> {
key: T::Pk,
child: Option<Box<T>>,
}
impl<T: Model + Clone> Clone for ForeignKeyResolved<T>
where
T::Pk: Clone,
{
fn clone(&self) -> Self {
Self {
key: self.key.clone(),
child: self.child.clone(),
}
}
}
impl<T: Model + std::fmt::Debug> std::fmt::Debug for ForeignKeyResolved<T>
where
T::Pk: std::fmt::Debug,
{
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ForeignKeyResolved")
.field("key", &self.key)
.field("child", &self.child)
.finish()
}
}
impl<T: Model> ForeignKeyResolved<T> {
#[allow(dead_code)]
pub(crate) fn new(key: T::Pk, child: Option<T>) -> Self {
Self {
key,
child: child.map(Box::new),
}
}
#[inline]
pub fn key(&self) -> &T::Pk {
&self.key
}
#[inline]
pub fn resolved(&self) -> Option<&T> {
self.child.as_deref()
}
#[inline]
pub fn expect_resolved(
&self,
model: &'static str,
field: &'static str,
) -> Result<&T, crate::DjogiError> {
self.child
.as_deref()
.ok_or_else(|| crate::DjogiError::relation_unloaded(model, field))
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::DjogiError;
use crate::types::HeerId;
#[derive(Debug, Clone)]
struct Dummy;
impl crate::model::__sealed::Sealed for Dummy {}
#[allow(clippy::manual_async_fn)]
impl crate::model::Model for Dummy {
type Pk = HeerId;
type Fields = ();
fn table_name() -> &'static str {
"dummies"
}
fn pk_value(&self) -> &HeerId {
unreachable!()
}
fn descriptor() -> &'static crate::descriptor::ModelDescriptor {
unreachable!()
}
fn get(
_ctx: &mut crate::context::DjogiContext,
_id: HeerId,
) -> impl std::future::Future<Output = Result<Self, DjogiError>> + Send {
async { unreachable!() }
}
fn create(
_ctx: &mut crate::context::DjogiContext,
_v: Self,
) -> impl std::future::Future<Output = Result<Self, DjogiError>> + Send {
async { unreachable!() }
}
fn save<'ctx>(
&'ctx mut self,
_ctx: &'ctx mut crate::context::DjogiContext,
) -> impl std::future::Future<Output = Result<(), DjogiError>> + Send + 'ctx {
async { unreachable!() }
}
fn delete(
self,
_ctx: &mut crate::context::DjogiContext,
) -> impl std::future::Future<Output = Result<(), DjogiError>> + Send {
async { unreachable!() }
}
fn refresh_from_db<'ctx>(
&'ctx self,
_ctx: &'ctx mut crate::context::DjogiContext,
) -> impl std::future::Future<Output = Result<Self, DjogiError>> + Send + 'ctx {
async { unreachable!() }
}
}
#[test]
fn foreign_key_stores_target_pk() {
let fk: ForeignKey<Dummy> = ForeignKey::new(HeerId::from_i64(42).unwrap());
assert_eq!(fk.key(), HeerId::from_i64(42).unwrap());
}
#[test]
fn foreign_key_resolved_always_none_on_unresolved_wrapper() {
let fk: ForeignKey<Dummy> = ForeignKey::new(HeerId::from_i64(7).unwrap());
assert!(fk.resolved().is_none());
}
#[test]
fn foreign_key_is_copy_when_pk_is_copy() {
fn takes_copy<T: Copy>(_: T) {}
let fk: ForeignKey<Dummy> = ForeignKey::new(HeerId::from_i64(1).unwrap());
takes_copy(fk);
let _second = fk;
}
#[test]
fn foreign_key_eq_compares_by_key() {
let a: ForeignKey<Dummy> = ForeignKey::new(HeerId::from_i64(99).unwrap());
let b: ForeignKey<Dummy> = ForeignKey::new(HeerId::from_i64(99).unwrap());
let c: ForeignKey<Dummy> = ForeignKey::new(HeerId::from_i64(100).unwrap());
assert_eq!(a, b);
assert_ne!(a, c);
}
#[test]
fn foreign_key_resolved_expect_resolved_err_on_missing() {
let resolved: ForeignKeyResolved<Dummy> =
ForeignKeyResolved::new(HeerId::from_i64(1).unwrap(), None);
let err = resolved.expect_resolved("Vehicle", "owner_id").unwrap_err();
let msg = format!("{err}");
assert!(
matches!(
err,
DjogiError::RelationUnloaded {
model: "Vehicle",
field: "owner_id"
}
),
"expected RelationUnloaded, got: {err:?}"
);
assert!(msg.contains("Vehicle"), "expected model name, got: {msg}");
assert!(msg.contains("owner_id"), "expected field name, got: {msg}");
}
#[test]
fn foreign_key_resolved_expect_resolved_ok_on_present() {
let resolved: ForeignKeyResolved<Dummy> =
ForeignKeyResolved::new(HeerId::from_i64(1).unwrap(), Some(Dummy));
assert!(resolved.expect_resolved("M", "f").is_ok());
assert!(resolved.resolved().is_some());
}
#[test]
fn foreign_key_field_as_pk_expr_emits_bare_column() {
use crate::expr::sql::emit_expr;
use crate::pg::accumulator::SqlAccumulator;
use crate::query::field::FieldRef;
use crate::query::portable::SqlEmitContext;
let fk_col: FieldRef<Dummy, ForeignKey<Dummy>> = FieldRef::new("ledger_id");
let expr: crate::expr::Expr<HeerId> = fk_col.as_pk_expr();
let mut acc = SqlAccumulator::new("");
emit_expr(&mut acc, &expr.node, SqlEmitContext::root()).expect("expression emission");
assert_eq!(acc.sql().trim(), "ledger_id", "got: {}", acc.sql());
}
#[test]
fn foreign_key_serializes_as_wrapped_pk() {
let fk: ForeignKey<Dummy> = ForeignKey::new(HeerId::from_i64(42).unwrap());
let json = serde_json::to_string(&fk).expect("serialize");
let pk_json = serde_json::to_string(&HeerId::from_i64(42).unwrap()).expect("pk serialize");
assert_eq!(json, pk_json, "FK must serialize identically to its PK");
}
#[test]
fn foreign_key_round_trips_through_json() {
let fk: ForeignKey<Dummy> = ForeignKey::new(HeerId::from_i64(7).unwrap());
let json = serde_json::to_string(&fk).expect("serialize");
let restored: ForeignKey<Dummy> = serde_json::from_str(&json).expect("deserialize");
assert_eq!(restored, fk);
}
}