feat: one-to-many relation helper

Allow one to specify that a field of a model is a foreign key.
It will generate a bunch of helper methods to query related entities
from one entity.
This commit is contained in:
Matthieu Bessat 2025-11-11 17:10:47 +01:00
parent 32ef1f7b33
commit 5f45671b74
25 changed files with 764 additions and 140 deletions

View file

@ -0,0 +1,20 @@
[package]
name = "sqlxgentools_misc"
description = "Various misc class to use in applications that use sqlxgentools"
publish = true
edition.workspace = true
authors.workspace = true
version.workspace = true
license.workspace = true
repository.workspace = true
[dependencies]
sqlx-core = { version = "=0.8.6" }
sqlx-sqlite = { version = "=0.8.6", features = ["offline"] }
fully_pub = "0.1"
serde = { version = "1.0", features = ["derive"] }
[lib]
[lints.clippy]
uninlined_format_args = "allow"

View file

@ -0,0 +1,92 @@
use std::error::Error;
use std::marker::PhantomData;
use fully_pub::fully_pub;
use serde::{Serialize, Serializer};
use sqlx_core::any::{Any, AnyArgumentBuffer};
use sqlx_core::database::Database;
use sqlx_core::decode::Decode;
use sqlx_core::encode::{Encode, IsNull};
use sqlx_core::error::BoxDynError;
use sqlx_core::types::Type;
use sqlx_sqlite::{Sqlite, SqliteArgumentValue};
#[fully_pub]
trait DatabaseLine {
fn id(&self) -> String;
}
/// Wrapper to mark a model field as foreign
/// You can use a generic argument inside ForeignRef to point to the target model
#[derive(Clone, Debug)]
#[fully_pub]
struct ForeignRef<T: Sized + DatabaseLine> {
pub target_type: PhantomData<T>,
pub target_id: String
}
// Implement serde Serialize for ForeignRef
impl<T: Sized + DatabaseLine> Serialize for ForeignRef<T> {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
// Serialize only the target_id as a string
serializer.serialize_str(&self.target_id)
}
}
impl<T: Sized + DatabaseLine> ForeignRef<T> {
pub fn new(entity: &T) -> ForeignRef<T> {
ForeignRef {
target_type: PhantomData,
target_id: entity.id()
}
}
}
impl<'r, DB: Database, T: Sized + DatabaseLine> Decode<'r, DB> for ForeignRef<T>
where
// we want to delegate some of the work to string decoding so let's make sure strings
// are supported by the database
&'r str: Decode<'r, DB>
{
fn decode(
value: <DB as Database>::ValueRef<'r>,
) -> Result<ForeignRef<T>, Box<dyn Error + 'static + Send + Sync>> {
let value = <&str as Decode<DB>>::decode(value)?;
let ref_val: String = value.parse()?;
Ok(ForeignRef::<T> {
target_type: PhantomData,
target_id: ref_val
})
}
}
impl<T: DatabaseLine + Sized> Encode<'_, Any> for ForeignRef<T> {
fn encode_by_ref(&self, buf: &mut AnyArgumentBuffer) -> Result<IsNull, BoxDynError> {
<String as Encode<'_, Any>>::encode_by_ref(&self.target_id.to_string(), buf)
}
}
impl<T: DatabaseLine + Sized> Type<Sqlite> for ForeignRef<T> {
fn type_info() -> <Sqlite as Database>::TypeInfo {
<String as Type<Sqlite>>::type_info()
}
}
impl<T: DatabaseLine + Sized> Encode<'_, Sqlite> for ForeignRef<T> {
fn encode_by_ref(&self, args: &mut Vec<SqliteArgumentValue<'_>>) -> Result<IsNull, BoxDynError> {
args.push(SqliteArgumentValue::Text(self.target_id.clone().into()));
Ok(IsNull::No)
}
}