use crate::model::Model;
use crate::query::condition::{Condition, FilterValue};
use std::marker::PhantomData;
pub(crate) fn is_plain_ident(s: &str) -> bool {
let bytes = s.as_bytes();
if bytes.is_empty() || bytes.len() > 63 {
return false;
}
let first = bytes[0];
if !(first.is_ascii_alphabetic() || first == b'_') {
return false;
}
bytes
.iter()
.all(|&b| b.is_ascii_alphanumeric() || b == b'_')
}
fn validate_dotted_path(dotted: &'static str) {
if dotted.is_empty() {
panic!("Djogi: JsonbPathRef path must be non-empty (got empty string)");
}
for segment in dotted.split('.') {
if !is_plain_ident(segment) {
panic!(
"Djogi: JsonbPathRef path segment {segment:?} is not a valid plain identifier. \
Each segment must be non-empty, begin with an ASCII letter or underscore, \
contain only ASCII alphanumerics or underscores, and be at most 63 bytes long."
);
}
}
}
pub struct JsonbPathRef<M, V> {
column: &'static str,
path: &'static str,
_phantom: PhantomData<fn() -> (M, V)>,
}
impl<M, V> Copy for JsonbPathRef<M, V> {}
impl<M, V> Clone for JsonbPathRef<M, V> {
fn clone(&self) -> Self {
*self
}
}
impl<M, V> std::fmt::Debug for JsonbPathRef<M, V> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "JsonbPathRef({}.{})", self.column, self.path)
}
}
impl<M, V> JsonbPathRef<M, V> {
pub(crate) fn new(column: &'static str, path: &'static str) -> Self {
validate_dotted_path(path);
JsonbPathRef {
column,
path,
_phantom: PhantomData,
}
}
#[doc(hidden)]
pub fn __from_macro(column: &'static str, path: &'static str) -> Self {
validate_dotted_path(path);
JsonbPathRef {
column,
path,
_phantom: PhantomData,
}
}
#[doc(hidden)]
pub fn column(self) -> &'static str {
self.column
}
#[doc(hidden)]
pub fn path_str(self) -> &'static str {
self.path
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum JsonbSqlCast {
Int2,
Int4,
Int8,
Float4,
Float8,
Boolean,
Timestamptz,
Date,
Uuid,
Numeric,
Interval,
#[cfg(feature = "network")]
Inet,
#[cfg(feature = "network")]
Cidr,
#[cfg(feature = "network")]
Macaddr,
}
impl JsonbSqlCast {
pub(crate) fn suffix(self) -> &'static str {
match self {
JsonbSqlCast::Int2 => "::int2",
JsonbSqlCast::Int4 => "::int4",
JsonbSqlCast::Int8 => "::int8",
JsonbSqlCast::Float4 => "::float4",
JsonbSqlCast::Float8 => "::float8",
JsonbSqlCast::Boolean => "::boolean",
JsonbSqlCast::Timestamptz => "::timestamptz",
JsonbSqlCast::Date => "::date",
JsonbSqlCast::Uuid => "::uuid",
JsonbSqlCast::Numeric => "::numeric",
JsonbSqlCast::Interval => "::interval",
#[cfg(feature = "network")]
JsonbSqlCast::Inet => "::inet",
#[cfg(feature = "network")]
JsonbSqlCast::Cidr => "::cidr",
#[cfg(feature = "network")]
JsonbSqlCast::Macaddr => "::macaddr",
}
}
}
pub(crate) fn jsonb_sql_cast_for_type(type_name: &str) -> Option<JsonbSqlCast> {
match type_name {
"i16" => Some(JsonbSqlCast::Int2),
"i32" => Some(JsonbSqlCast::Int4),
"i64" => Some(JsonbSqlCast::Int8),
"i8" => Some(JsonbSqlCast::Int2),
"u8" => Some(JsonbSqlCast::Int2),
"u16" => Some(JsonbSqlCast::Int4),
"u32" => Some(JsonbSqlCast::Int8),
"u64" => Some(JsonbSqlCast::Numeric),
"f32" => Some(JsonbSqlCast::Float4),
"f64" => Some(JsonbSqlCast::Float8),
"bool" => Some(JsonbSqlCast::Boolean),
"time::offset_date_time::OffsetDateTime" | "time::OffsetDateTime" | "OffsetDateTime" => {
Some(JsonbSqlCast::Timestamptz)
}
"time::date::Date" | "time::Date" | "Date" => Some(JsonbSqlCast::Date),
"uuid::Uuid" | "Uuid" => Some(JsonbSqlCast::Uuid),
"heeranjid::heer::HeerId" | "djogi::types::HeerId" | "heeranjid::HeerId" => {
Some(JsonbSqlCast::Int8)
}
"heeranjid::heer_desc::HeerIdDesc"
| "djogi::types::HeerIdDesc"
| "heeranjid::HeerIdDesc" => Some(JsonbSqlCast::Int8),
"heeranjid::ranj::RanjId" | "djogi::types::RanjId" | "heeranjid::RanjId" => {
Some(JsonbSqlCast::Uuid)
}
"heeranjid::ranj_desc::RanjIdDesc"
| "djogi::types::RanjIdDesc"
| "heeranjid::RanjIdDesc" => Some(JsonbSqlCast::Uuid),
"rust_decimal::decimal::Decimal" | "rust_decimal::Decimal" | "Decimal" => {
Some(JsonbSqlCast::Numeric)
}
"djogi::pg_types::Interval" | "djogi::types::Interval" | "Interval" => {
Some(JsonbSqlCast::Interval)
}
#[cfg(feature = "network")]
"core::net::ip_addr::IpAddr" | "std::net::IpAddr" | "core::net::IpAddr" | "IpAddr" => {
Some(JsonbSqlCast::Inet)
}
#[cfg(feature = "network")]
"djogi::pg_types::CidrAddr" | "djogi::types::CidrAddr" | "CidrAddr" => {
Some(JsonbSqlCast::Cidr)
}
#[cfg(feature = "network")]
"djogi::pg_types::MacAddr" | "djogi::types::MacAddr" | "MacAddr" => {
Some(JsonbSqlCast::Macaddr)
}
"alloc::string::String" | "String" | "&str" | "str" => None,
_ => None,
}
}
#[cfg(test)]
pub(crate) fn sql_cast_for_type(type_name: &str) -> Option<&'static str> {
jsonb_sql_cast_for_type(type_name).map(JsonbSqlCast::suffix)
}
#[cfg(test)]
pub(crate) fn build_path_sql(
column: &'static str,
path: &'static str,
cast: Option<&'static str>,
) -> String {
let segments: Vec<&str> = path.split('.').collect();
let mut s = String::new();
s.push('(');
s.push_str(column);
for (i, seg) in segments.iter().enumerate() {
if i == segments.len() - 1 {
s.push_str("->>'");
s.push_str(seg);
s.push('\'');
} else {
s.push_str("->'");
s.push_str(seg);
s.push('\'');
}
}
s.push(')');
if let Some(c) = cast {
s.push_str(c);
}
s
}
#[derive(Debug, Clone)]
pub struct JsonbPathLeaf {
pub(crate) column: &'static str,
pub(crate) path: &'static str,
pub(crate) cast: Option<&'static str>,
pub(crate) op: crate::query::condition::LookupOp,
pub(crate) value: FilterValue,
}
use crate::query::field::IntoFilterValue;
pub trait JsonbPathComparable {}
macro_rules! impl_jsonb_path_comparable {
($($t:ty),* $(,)?) => {$(
impl JsonbPathComparable for $t {}
)*};
}
impl_jsonb_path_comparable!(
i8,
i16,
i32,
i64,
u8,
u16,
u32,
u64,
f32,
f64,
bool,
String,
time::OffsetDateTime,
time::Date,
uuid::Uuid,
rust_decimal::Decimal,
crate::HeerId,
crate::RanjId,
crate::HeerIdDesc,
crate::RanjIdDesc,
);
impl JsonbPathComparable for &'static str {}
#[cfg(feature = "network")]
impl_jsonb_path_comparable!(std::net::IpAddr, crate::CidrAddr, crate::MacAddr);
impl<M: Model, V: IntoFilterValue + JsonbPathComparable + 'static> JsonbPathRef<M, V> {
fn cast_for_v() -> Option<&'static str> {
V::jsonb_sql_cast().map(JsonbSqlCast::suffix)
}
fn leaf_condition(
self,
op: crate::query::condition::LookupOp,
value: FilterValue,
) -> Condition {
Condition::JsonbPath(JsonbPathLeaf {
column: self.column,
path: self.path,
cast: Self::cast_for_v(),
op,
value,
})
}
#[must_use = "conditions are lazy — dropping one silently omits the filter"]
pub fn eq(self, value: V) -> Condition {
use crate::query::condition::LookupOp;
self.leaf_condition(LookupOp::Eq, value.into_filter_value())
}
#[must_use = "conditions are lazy — dropping one silently omits the filter"]
pub fn neq(self, value: V) -> Condition {
use crate::query::condition::LookupOp;
self.leaf_condition(LookupOp::Neq, value.into_filter_value())
}
#[must_use = "conditions are lazy — dropping one silently omits the filter"]
pub fn gt(self, value: V) -> Condition {
use crate::query::condition::LookupOp;
self.leaf_condition(LookupOp::Gt, value.into_filter_value())
}
#[must_use = "conditions are lazy — dropping one silently omits the filter"]
pub fn gte(self, value: V) -> Condition {
use crate::query::condition::LookupOp;
self.leaf_condition(LookupOp::Gte, value.into_filter_value())
}
#[must_use = "conditions are lazy — dropping one silently omits the filter"]
pub fn lt(self, value: V) -> Condition {
use crate::query::condition::LookupOp;
self.leaf_condition(LookupOp::Lt, value.into_filter_value())
}
#[must_use = "conditions are lazy — dropping one silently omits the filter"]
pub fn lte(self, value: V) -> Condition {
use crate::query::condition::LookupOp;
self.leaf_condition(LookupOp::Lte, value.into_filter_value())
}
#[must_use = "conditions are lazy — dropping one silently omits the filter"]
pub fn is_null(self) -> Condition {
use crate::query::condition::LookupOp;
self.leaf_condition(LookupOp::IsNull, FilterValue::Null)
}
#[must_use = "conditions are lazy — dropping one silently omits the filter"]
pub fn is_not_null(self) -> Condition {
use crate::query::condition::LookupOp;
self.leaf_condition(LookupOp::IsNotNull, FilterValue::Null)
}
#[must_use = "conditions are lazy — dropping one silently omits the filter"]
pub fn in_list<I: IntoIterator<Item = V>>(self, values: I) -> Condition {
use crate::query::condition::LookupOp;
let list = FilterValue::List(
values
.into_iter()
.map(IntoFilterValue::into_filter_value)
.collect(),
);
self.leaf_condition(LookupOp::In, list)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn plain_ident_accepts_simple_name() {
assert!(is_plain_ident("engine"));
}
#[test]
fn plain_ident_accepts_leading_underscore() {
assert!(is_plain_ident("_private"));
}
#[test]
fn plain_ident_accepts_internal_underscore() {
assert!(is_plain_ident("my_field"));
}
#[test]
fn plain_ident_accepts_mixed_alphanumeric() {
assert!(is_plain_ident("field123"));
}
#[test]
fn plain_ident_rejects_empty() {
assert!(!is_plain_ident(""));
}
#[test]
fn plain_ident_rejects_starts_with_digit() {
assert!(!is_plain_ident("1abc"));
}
#[test]
fn plain_ident_rejects_non_ascii() {
assert!(!is_plain_ident("café"));
}
#[test]
fn plain_ident_rejects_too_long() {
let s = "a".repeat(64);
assert!(!is_plain_ident(&s));
}
#[test]
fn plain_ident_accepts_exactly_63_bytes() {
let s = "a".repeat(63);
assert!(is_plain_ident(&s));
}
#[test]
fn plain_ident_rejects_hyphen() {
assert!(!is_plain_ident("my-field"));
}
#[test]
fn build_path_single_segment_with_cast() {
let sql = build_path_sql("col", "key", Some("::int"));
assert_eq!(sql, "(col->>'key')::int");
}
#[test]
fn build_path_two_segments_with_cast() {
let sql = build_path_sql("specs", "engine.cylinders", Some("::int"));
assert_eq!(sql, "(specs->'engine'->>'cylinders')::int");
}
#[test]
fn build_path_three_segments_no_cast() {
let sql = build_path_sql("data", "a.b.c", None);
assert_eq!(sql, "(data->'a'->'b'->>'c')");
}
#[test]
fn sql_cast_for_i16() {
assert_eq!(sql_cast_for_type("i16"), Some("::int2"));
}
#[test]
fn sql_cast_for_i32() {
assert_eq!(sql_cast_for_type("i32"), Some("::int4"));
}
#[test]
fn sql_cast_for_i64() {
assert_eq!(sql_cast_for_type("i64"), Some("::int8"));
}
#[test]
fn sql_cast_for_i8() {
assert_eq!(sql_cast_for_type("i8"), Some("::int2"));
}
#[test]
fn sql_cast_for_u8() {
assert_eq!(sql_cast_for_type("u8"), Some("::int2"));
}
#[test]
fn sql_cast_for_u16() {
assert_eq!(sql_cast_for_type("u16"), Some("::int4"));
}
#[test]
fn sql_cast_for_u32() {
assert_eq!(sql_cast_for_type("u32"), Some("::int8"));
}
#[test]
fn sql_cast_for_f32() {
assert_eq!(sql_cast_for_type("f32"), Some("::float4"));
}
#[test]
fn sql_cast_for_f64() {
assert_eq!(sql_cast_for_type("f64"), Some("::float8"));
}
#[test]
fn sql_cast_for_bool() {
assert_eq!(sql_cast_for_type("bool"), Some("::boolean"));
}
#[test]
fn sql_cast_for_offset_datetime() {
assert_eq!(
sql_cast_for_type("time::OffsetDateTime"),
Some("::timestamptz")
);
assert_eq!(sql_cast_for_type("OffsetDateTime"), Some("::timestamptz"));
}
#[test]
fn sql_cast_for_date() {
assert_eq!(sql_cast_for_type("time::Date"), Some("::date"));
assert_eq!(sql_cast_for_type("Date"), Some("::date"));
}
#[test]
fn sql_cast_for_primitive_date_time_is_none_intentionally() {
assert_eq!(
sql_cast_for_type("time::primitive_date_time::PrimitiveDateTime"),
None
);
assert_eq!(sql_cast_for_type("time::PrimitiveDateTime"), None);
assert_eq!(sql_cast_for_type("PrimitiveDateTime"), None);
}
#[test]
fn sql_cast_for_uuid() {
assert_eq!(sql_cast_for_type("uuid::Uuid"), Some("::uuid"));
assert_eq!(sql_cast_for_type("Uuid"), Some("::uuid"));
}
#[test]
fn sql_cast_for_heer_id() {
assert_eq!(sql_cast_for_type("djogi::types::HeerId"), Some("::int8"));
assert_eq!(sql_cast_for_type("heeranjid::HeerId"), Some("::int8"));
}
#[test]
fn sql_cast_for_ranj_id() {
assert_eq!(sql_cast_for_type("djogi::types::RanjId"), Some("::uuid"));
assert_eq!(sql_cast_for_type("heeranjid::RanjId"), Some("::uuid"));
}
#[test]
fn sql_cast_for_decimal() {
assert_eq!(
sql_cast_for_type("rust_decimal::Decimal"),
Some("::numeric")
);
assert_eq!(sql_cast_for_type("Decimal"), Some("::numeric"));
}
#[test]
fn sql_cast_for_string_is_none() {
assert_eq!(sql_cast_for_type("alloc::string::String"), None);
assert_eq!(sql_cast_for_type("String"), None);
}
#[test]
fn sql_cast_for_str_ref_is_none() {
assert_eq!(sql_cast_for_type("&str"), None);
assert_eq!(sql_cast_for_type("str"), None);
}
#[test]
fn sql_cast_for_unknown_is_none() {
assert_eq!(sql_cast_for_type("some::unknown::Type"), None);
}
#[test]
fn sql_cast_uses_actual_type_name_for_offset_datetime() {
let name = std::any::type_name::<::time::OffsetDateTime>();
assert_eq!(
sql_cast_for_type(name),
Some("::timestamptz"),
"type_name<OffsetDateTime>() = {name:?} did not map to ::timestamptz"
);
}
#[test]
fn sql_cast_uses_actual_type_name_for_date() {
let name = std::any::type_name::<::time::Date>();
assert_eq!(
sql_cast_for_type(name),
Some("::date"),
"type_name<Date>() = {name:?} did not map to ::date"
);
}
#[test]
fn sql_cast_uses_actual_type_name_for_uuid() {
let name = std::any::type_name::<::uuid::Uuid>();
assert_eq!(
sql_cast_for_type(name),
Some("::uuid"),
"type_name<Uuid>() = {name:?} did not map to ::uuid"
);
}
#[test]
fn sql_cast_uses_actual_type_name_for_heer_id() {
let name = std::any::type_name::<::heeranjid::HeerId>();
assert_eq!(
sql_cast_for_type(name),
Some("::int8"),
"type_name<HeerId>() = {name:?} did not map to ::int8"
);
}
#[test]
fn sql_cast_uses_actual_type_name_for_decimal() {
let name = std::any::type_name::<::rust_decimal::Decimal>();
assert_eq!(
sql_cast_for_type(name),
Some("::numeric"),
"type_name<Decimal>() = {name:?} did not map to ::numeric"
);
}
#[test]
fn sql_cast_uses_actual_type_name_for_string_returns_none() {
let name = std::any::type_name::<String>();
assert_eq!(
sql_cast_for_type(name),
None,
"type_name<String>() = {name:?} should require no cast"
);
}
#[test]
fn sql_cast_for_interval() {
assert_eq!(
sql_cast_for_type("djogi::pg_types::Interval"),
Some("::interval")
);
assert_eq!(
sql_cast_for_type("djogi::types::Interval"),
Some("::interval")
);
assert_eq!(sql_cast_for_type("Interval"), Some("::interval"));
}
#[test]
fn sql_cast_uses_actual_type_name_for_interval() {
let name = std::any::type_name::<crate::Interval>();
assert_eq!(
sql_cast_for_type(name),
Some("::interval"),
"type_name<Interval>() = {name:?} did not map to ::interval"
);
}
#[cfg(feature = "network")]
#[test]
fn sql_cast_for_inet_aliases() {
assert_eq!(
sql_cast_for_type("core::net::ip_addr::IpAddr"),
Some("::inet")
);
assert_eq!(sql_cast_for_type("std::net::IpAddr"), Some("::inet"));
assert_eq!(sql_cast_for_type("core::net::IpAddr"), Some("::inet"));
assert_eq!(sql_cast_for_type("IpAddr"), Some("::inet"));
}
#[cfg(feature = "network")]
#[test]
fn sql_cast_uses_actual_type_name_for_ip_addr() {
let name = std::any::type_name::<std::net::IpAddr>();
assert_eq!(
sql_cast_for_type(name),
Some("::inet"),
"type_name<IpAddr>() = {name:?} did not map to ::inet"
);
}
#[cfg(feature = "network")]
#[test]
fn sql_cast_for_cidr_addr_aliases() {
assert_eq!(
sql_cast_for_type("djogi::pg_types::CidrAddr"),
Some("::cidr")
);
assert_eq!(sql_cast_for_type("djogi::types::CidrAddr"), Some("::cidr"));
assert_eq!(sql_cast_for_type("CidrAddr"), Some("::cidr"));
}
#[cfg(feature = "network")]
#[test]
fn sql_cast_uses_actual_type_name_for_cidr_addr() {
let name = std::any::type_name::<crate::CidrAddr>();
assert_eq!(
sql_cast_for_type(name),
Some("::cidr"),
"type_name<CidrAddr>() = {name:?} did not map to ::cidr"
);
}
#[cfg(feature = "network")]
#[test]
fn sql_cast_for_mac_addr_aliases() {
assert_eq!(
sql_cast_for_type("djogi::pg_types::MacAddr"),
Some("::macaddr")
);
assert_eq!(
sql_cast_for_type("djogi::types::MacAddr"),
Some("::macaddr")
);
assert_eq!(sql_cast_for_type("MacAddr"), Some("::macaddr"));
}
#[cfg(feature = "network")]
#[test]
fn sql_cast_uses_actual_type_name_for_mac_addr() {
let name = std::any::type_name::<crate::MacAddr>();
assert_eq!(
sql_cast_for_type(name),
Some("::macaddr"),
"type_name<MacAddr>() = {name:?} did not map to ::macaddr"
);
}
#[test]
fn sql_cast_uses_actual_type_name_for_heer_id_desc() {
let name = std::any::type_name::<crate::HeerIdDesc>();
assert_eq!(
sql_cast_for_type(name),
Some("::int8"),
"type_name<HeerIdDesc>() = {name:?} did not map to ::int8"
);
}
#[test]
fn sql_cast_uses_actual_type_name_for_ranj_id_desc() {
let name = std::any::type_name::<crate::RanjIdDesc>();
assert_eq!(
sql_cast_for_type(name),
Some("::uuid"),
"type_name<RanjIdDesc>() = {name:?} did not map to ::uuid"
);
}
#[test]
fn sql_cast_for_u64_numeric() {
assert_eq!(sql_cast_for_type("u64"), Some("::numeric"));
}
#[test]
fn jsonb_sql_cast_for_u64_numeric() {
assert_eq!(
jsonb_sql_cast_for_type("u64"),
Some(JsonbSqlCast::Numeric),
"u64 must map to JsonbSqlCast::Numeric"
);
}
#[test]
fn jsonb_sql_cast_uses_actual_type_name_for_u64() {
let name = std::any::type_name::<u64>();
assert_eq!(
sql_cast_for_type(name),
Some("::numeric"),
"type_name<u64>() = {name:?} did not map to ::numeric"
);
assert_eq!(
jsonb_sql_cast_for_type(name),
Some(JsonbSqlCast::Numeric),
"type_name<u64>() = {name:?} did not map to JsonbSqlCast::Numeric"
);
}
#[test]
fn jsonb_sql_cast_suffix_round_trips_integer_variants() {
assert_eq!(JsonbSqlCast::Int2.suffix(), "::int2");
assert_eq!(JsonbSqlCast::Int4.suffix(), "::int4");
assert_eq!(JsonbSqlCast::Int8.suffix(), "::int8");
}
#[test]
fn jsonb_sql_cast_suffix_round_trips_float_variants() {
assert_eq!(JsonbSqlCast::Float4.suffix(), "::float4");
assert_eq!(JsonbSqlCast::Float8.suffix(), "::float8");
}
#[test]
fn jsonb_sql_cast_suffix_round_trips_misc_variants() {
assert_eq!(JsonbSqlCast::Boolean.suffix(), "::boolean");
assert_eq!(JsonbSqlCast::Timestamptz.suffix(), "::timestamptz");
assert_eq!(JsonbSqlCast::Date.suffix(), "::date");
assert_eq!(JsonbSqlCast::Uuid.suffix(), "::uuid");
assert_eq!(JsonbSqlCast::Numeric.suffix(), "::numeric");
assert_eq!(JsonbSqlCast::Interval.suffix(), "::interval");
}
#[test]
fn jsonb_sql_cast_for_known_built_ins() {
assert_eq!(jsonb_sql_cast_for_type("i16"), Some(JsonbSqlCast::Int2));
assert_eq!(jsonb_sql_cast_for_type("i32"), Some(JsonbSqlCast::Int4));
assert_eq!(jsonb_sql_cast_for_type("i64"), Some(JsonbSqlCast::Int8));
assert_eq!(jsonb_sql_cast_for_type("f32"), Some(JsonbSqlCast::Float4));
assert_eq!(jsonb_sql_cast_for_type("f64"), Some(JsonbSqlCast::Float8));
assert_eq!(jsonb_sql_cast_for_type("bool"), Some(JsonbSqlCast::Boolean));
}
#[test]
fn jsonb_sql_cast_for_string_is_none() {
assert_eq!(jsonb_sql_cast_for_type("String"), None);
assert_eq!(jsonb_sql_cast_for_type("alloc::string::String"), None);
assert_eq!(jsonb_sql_cast_for_type("&str"), None);
assert_eq!(jsonb_sql_cast_for_type("str"), None);
}
#[test]
fn jsonb_sql_cast_for_unknown_is_none() {
assert_eq!(jsonb_sql_cast_for_type("some::unknown::Type"), None);
}
#[cfg(feature = "network")]
#[test]
fn jsonb_sql_cast_suffix_round_trips_network_variants() {
assert_eq!(JsonbSqlCast::Inet.suffix(), "::inet");
assert_eq!(JsonbSqlCast::Cidr.suffix(), "::cidr");
assert_eq!(JsonbSqlCast::Macaddr.suffix(), "::macaddr");
}
use crate::query::field::IntoFilterValue;
#[test]
fn into_filter_value_jsonb_sql_cast_resolves_built_in_primitives() {
assert_eq!(
<i32 as IntoFilterValue>::jsonb_sql_cast(),
Some(JsonbSqlCast::Int4)
);
assert_eq!(
<i64 as IntoFilterValue>::jsonb_sql_cast(),
Some(JsonbSqlCast::Int8)
);
assert_eq!(
<u64 as IntoFilterValue>::jsonb_sql_cast(),
Some(JsonbSqlCast::Numeric)
);
assert_eq!(
<bool as IntoFilterValue>::jsonb_sql_cast(),
Some(JsonbSqlCast::Boolean)
);
assert_eq!(<String as IntoFilterValue>::jsonb_sql_cast(), None);
}
#[derive(Debug, Clone, Copy)]
struct LocalI64Id(i64);
impl IntoFilterValue for LocalI64Id {
fn into_filter_value(self) -> crate::query::condition::FilterValue {
<i64 as IntoFilterValue>::into_filter_value(self.0)
}
fn jsonb_sql_cast() -> Option<JsonbSqlCast> {
<i64 as IntoFilterValue>::jsonb_sql_cast()
}
}
impl JsonbPathComparable for LocalI64Id {}
#[test]
fn local_newtype_wrapper_delegates_jsonb_sql_cast_to_inner() {
assert_eq!(
<LocalI64Id as IntoFilterValue>::jsonb_sql_cast(),
Some(JsonbSqlCast::Int8),
"wrapper must delegate cast metadata to its inner SQL value type"
);
}
struct StubModel;
impl crate::model::__sealed::Sealed for StubModel {}
#[allow(clippy::manual_async_fn)]
impl crate::model::Model for StubModel {
type Pk = crate::HeerId;
type Fields = ();
fn table_name() -> &'static str {
"stub"
}
fn pk_value(&self) -> &crate::HeerId {
unreachable!()
}
fn descriptor() -> &'static crate::descriptor::ModelDescriptor {
unreachable!()
}
fn get(
_ctx: &mut crate::context::DjogiContext,
_id: crate::HeerId,
) -> impl std::future::Future<Output = Result<Self, crate::DjogiError>> + Send {
async { unreachable!() }
}
fn create(
_ctx: &mut crate::context::DjogiContext,
_v: Self,
) -> impl std::future::Future<Output = Result<Self, crate::DjogiError>> + Send {
async { unreachable!() }
}
fn save<'ctx>(
&'ctx mut self,
_ctx: &'ctx mut crate::context::DjogiContext,
) -> impl std::future::Future<Output = Result<(), crate::DjogiError>> + Send + 'ctx
{
async { unreachable!() }
}
fn delete(
self,
_ctx: &mut crate::context::DjogiContext,
) -> impl std::future::Future<Output = Result<(), crate::DjogiError>> + Send {
async { unreachable!() }
}
fn refresh_from_db<'ctx>(
&'ctx self,
_ctx: &'ctx mut crate::context::DjogiContext,
) -> impl std::future::Future<Output = Result<Self, crate::DjogiError>> + Send + 'ctx
{
async { unreachable!() }
}
}
#[test]
fn jsonb_path_ref_builds_int8_cast_for_local_wrapper() {
use crate::query::condition::Condition;
let path: JsonbPathRef<StubModel, LocalI64Id> = JsonbPathRef::new("specs", "rank");
let cond = path.gt(LocalI64Id(9));
let leaf = match cond {
Condition::JsonbPath(l) => l,
other => panic!("expected JsonbPath leaf, got {other:?}"),
};
assert_eq!(
leaf.cast,
Some("::int8"),
"JsonbPathRef<_, LocalI64Id> must carry ::int8 cast via delegation"
);
let lhs = build_path_sql(leaf.column, leaf.path, leaf.cast);
assert_eq!(lhs, "(specs->>'rank')::int8");
}
#[test]
fn jsonb_path_ref_builds_numeric_cast_for_u64() {
use crate::query::condition::Condition;
let path: JsonbPathRef<StubModel, u64> = JsonbPathRef::new("meta", "view_count");
let cond = path.gt(9u64);
let leaf = match cond {
Condition::JsonbPath(l) => l,
other => panic!("expected JsonbPath leaf, got {other:?}"),
};
assert_eq!(
leaf.cast,
Some("::numeric"),
"u64 JSONB path comparisons must cast to ::numeric"
);
let lhs = build_path_sql(leaf.column, leaf.path, leaf.cast);
assert_eq!(lhs, "(meta->>'view_count')::numeric");
}
#[test]
fn jsonb_path_ref_builds_timestamptz_cast_for_offset_date_time() {
use crate::query::condition::Condition;
let path: JsonbPathRef<StubModel, time::OffsetDateTime> =
JsonbPathRef::new("meta", "seen_at");
let probe = time::Date::from_calendar_date(2021, time::Month::January, 2)
.unwrap()
.with_time(time::Time::from_hms(3, 4, 5).unwrap())
.assume_utc();
let cond = path.gt(probe);
let leaf = match cond {
Condition::JsonbPath(l) => l,
other => panic!("expected JsonbPath leaf, got {other:?}"),
};
assert_eq!(leaf.cast, Some("::timestamptz"));
let lhs = build_path_sql(leaf.column, leaf.path, leaf.cast);
assert_eq!(lhs, "(meta->>'seen_at')::timestamptz");
}
}