use crate::jsonb::MirJzSON;
use crate::model::Model;
use crate::query::field::{DjogiField, DjogiFieldProvenance};
use crate::query::predicate::PortablePredicate;
use sassi::predicate::{
JSahibONFieldRef, JSahibONOptionFieldRef, JSahibONPathRef, JSahibONValueRef, JTypeKind,
};
use sassi::{BasicPredicate, JOrderedScalar, JSahibON, JScalar};
use std::marker::PhantomData;
pub struct DjogiJSahibONFieldRef<M: Model> {
inner: JSahibONFieldRef<M>,
}
pub struct DjogiJSahibONOptionFieldRef<M: Model> {
inner: JSahibONOptionFieldRef<M>,
}
pub struct DjogiJSahibONPathRef<M: Model> {
inner: JSahibONPathRef<M>,
}
pub struct DjogiJSahibONValueRef<M: Model, V> {
inner: JSahibONValueRef<M, V>,
_marker: PhantomData<fn(&M, V)>,
}
impl<M: Model> Clone for DjogiJSahibONFieldRef<M> {
fn clone(&self) -> Self {
Self {
inner: self.inner.clone(),
}
}
}
impl<M: Model> Clone for DjogiJSahibONOptionFieldRef<M> {
fn clone(&self) -> Self {
Self {
inner: self.inner.clone(),
}
}
}
impl<M: Model> Clone for DjogiJSahibONPathRef<M> {
fn clone(&self) -> Self {
Self {
inner: self.inner.clone(),
}
}
}
impl<M: Model, V> Clone for DjogiJSahibONValueRef<M, V> {
fn clone(&self) -> Self {
Self {
inner: self.inner.clone(),
_marker: PhantomData,
}
}
}
macro_rules! impl_delegated_methods {
($ty:ident) => {
impl<M: Model> $ty<M> {
pub fn root(&self) -> DjogiJSahibONPathRef<M> {
DjogiJSahibONPathRef {
inner: self.inner.root(),
}
}
pub fn path(&self, dotted_plain_idents: &'static str) -> DjogiJSahibONPathRef<M> {
DjogiJSahibONPathRef {
inner: self.inner.path(dotted_plain_idents),
}
}
pub fn key(&self, key: impl Into<String>) -> DjogiJSahibONPathRef<M> {
DjogiJSahibONPathRef {
inner: self.inner.key(key),
}
}
pub fn path_segments<I, S>(&self, segments: I) -> DjogiJSahibONPathRef<M>
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
DjogiJSahibONPathRef {
inner: self.inner.path_segments(segments),
}
}
#[must_use = "predicates are lazy — dropping one silently omits the filter"]
pub fn exists(&self) -> PortablePredicate<M> {
wrap_predicate(self.inner.exists())
}
#[must_use = "predicates are lazy — dropping one silently omits the filter"]
pub fn missing(&self) -> PortablePredicate<M> {
wrap_predicate(self.inner.missing())
}
#[must_use = "predicates are lazy — dropping one silently omits the filter"]
pub fn is_json_null(&self) -> PortablePredicate<M> {
wrap_predicate(self.inner.is_json_null())
}
#[must_use = "predicates are lazy — dropping one silently omits the filter"]
pub fn is_not_json_null(&self) -> PortablePredicate<M> {
wrap_predicate(self.inner.is_not_json_null())
}
#[must_use = "predicates are lazy — dropping one silently omits the filter"]
pub fn is_type(&self, kind: JTypeKind) -> PortablePredicate<M> {
wrap_predicate(self.inner.is_type(kind))
}
#[must_use = "predicates are lazy — dropping one silently omits the filter"]
pub fn is_bool(&self) -> PortablePredicate<M> {
wrap_predicate(self.inner.is_bool())
}
#[must_use = "predicates are lazy — dropping one silently omits the filter"]
pub fn is_number(&self) -> PortablePredicate<M> {
wrap_predicate(self.inner.is_number())
}
#[must_use = "predicates are lazy — dropping one silently omits the filter"]
pub fn is_string(&self) -> PortablePredicate<M> {
wrap_predicate(self.inner.is_string())
}
#[must_use = "predicates are lazy — dropping one silently omits the filter"]
pub fn is_array(&self) -> PortablePredicate<M> {
wrap_predicate(self.inner.is_array())
}
#[must_use = "predicates are lazy — dropping one silently omits the filter"]
pub fn is_object(&self) -> PortablePredicate<M> {
wrap_predicate(self.inner.is_object())
}
#[must_use = "predicates are lazy — dropping one silently omits the filter"]
pub fn has_key(&self, key: impl Into<String>) -> PortablePredicate<M> {
wrap_predicate(self.inner.has_key(key))
}
#[must_use = "predicates are lazy — dropping one silently omits the filter"]
pub fn has_any_key<I, S>(&self, keys: I) -> PortablePredicate<M>
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
wrap_predicate(self.inner.has_any_key(keys))
}
#[must_use = "predicates are lazy — dropping one silently omits the filter"]
pub fn has_all_keys<I, S>(&self, keys: I) -> PortablePredicate<M>
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
wrap_predicate(self.inner.has_all_keys(keys))
}
pub fn value<V: JScalar>(&self) -> DjogiJSahibONValueRef<M, V> {
DjogiJSahibONValueRef {
inner: self.inner.value(),
_marker: PhantomData,
}
}
#[must_use = "predicates are lazy — dropping one silently omits the filter"]
pub fn eq_json(&self, value: JSahibON) -> PortablePredicate<M> {
wrap_predicate(self.inner.eq_json(value))
}
#[must_use = "predicates are lazy — dropping one silently omits the filter"]
pub fn neq_json(&self, value: JSahibON) -> PortablePredicate<M> {
wrap_predicate(self.inner.neq_json(value))
}
#[must_use = "predicates are lazy — dropping one silently omits the filter"]
pub fn array_contains(&self, element: JSahibON) -> PortablePredicate<M> {
wrap_predicate(self.inner.array_contains(element))
}
#[must_use = "predicates are lazy — dropping one silently omits the filter"]
pub fn array_len_eq(&self, len: usize) -> PortablePredicate<M> {
wrap_predicate(self.inner.array_len_eq(len))
}
#[must_use = "predicates are lazy — dropping one silently omits the filter"]
pub fn array_len_gt(&self, len: usize) -> PortablePredicate<M> {
wrap_predicate(self.inner.array_len_gt(len))
}
#[must_use = "predicates are lazy — dropping one silently omits the filter"]
pub fn array_len_gte(&self, len: usize) -> PortablePredicate<M> {
wrap_predicate(self.inner.array_len_gte(len))
}
#[must_use = "predicates are lazy — dropping one silently omits the filter"]
pub fn array_len_lt(&self, len: usize) -> PortablePredicate<M> {
wrap_predicate(self.inner.array_len_lt(len))
}
#[must_use = "predicates are lazy — dropping one silently omits the filter"]
pub fn array_len_lte(&self, len: usize) -> PortablePredicate<M> {
wrap_predicate(self.inner.array_len_lte(len))
}
}
};
}
impl_delegated_methods!(DjogiJSahibONFieldRef);
impl_delegated_methods!(DjogiJSahibONOptionFieldRef);
impl<M: Model> DjogiJSahibONPathRef<M> {
pub fn key(self, key: impl Into<String>) -> Self {
Self {
inner: self.inner.key(key),
}
}
pub fn path_segments<I, S>(self, segments: I) -> Self
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
Self {
inner: self.inner.path_segments(segments),
}
}
#[must_use = "predicates are lazy — dropping one silently omits the filter"]
pub fn exists(&self) -> PortablePredicate<M> {
wrap_predicate(self.inner.exists())
}
#[must_use = "predicates are lazy — dropping one silently omits the filter"]
pub fn missing(&self) -> PortablePredicate<M> {
wrap_predicate(self.inner.missing())
}
#[must_use = "predicates are lazy — dropping one silently omits the filter"]
pub fn is_json_null(&self) -> PortablePredicate<M> {
wrap_predicate(self.inner.is_json_null())
}
#[must_use = "predicates are lazy — dropping one silently omits the filter"]
pub fn is_not_json_null(&self) -> PortablePredicate<M> {
wrap_predicate(self.inner.is_not_json_null())
}
#[must_use = "predicates are lazy — dropping one silently omits the filter"]
pub fn is_type(&self, kind: JTypeKind) -> PortablePredicate<M> {
wrap_predicate(self.inner.is_type(kind))
}
#[must_use = "predicates are lazy — dropping one silently omits the filter"]
pub fn is_bool(&self) -> PortablePredicate<M> {
wrap_predicate(self.inner.is_bool())
}
#[must_use = "predicates are lazy — dropping one silently omits the filter"]
pub fn is_number(&self) -> PortablePredicate<M> {
wrap_predicate(self.inner.is_number())
}
#[must_use = "predicates are lazy — dropping one silently omits the filter"]
pub fn is_string(&self) -> PortablePredicate<M> {
wrap_predicate(self.inner.is_string())
}
#[must_use = "predicates are lazy — dropping one silently omits the filter"]
pub fn is_array(&self) -> PortablePredicate<M> {
wrap_predicate(self.inner.is_array())
}
#[must_use = "predicates are lazy — dropping one silently omits the filter"]
pub fn is_object(&self) -> PortablePredicate<M> {
wrap_predicate(self.inner.is_object())
}
#[must_use = "predicates are lazy — dropping one silently omits the filter"]
pub fn has_key(&self, key: impl Into<String>) -> PortablePredicate<M> {
wrap_predicate(self.inner.has_key(key))
}
#[must_use = "predicates are lazy — dropping one silently omits the filter"]
pub fn has_any_key<I, S>(&self, keys: I) -> PortablePredicate<M>
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
wrap_predicate(self.inner.has_any_key(keys))
}
#[must_use = "predicates are lazy — dropping one silently omits the filter"]
pub fn has_all_keys<I, S>(&self, keys: I) -> PortablePredicate<M>
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
wrap_predicate(self.inner.has_all_keys(keys))
}
pub fn value<V: JScalar>(&self) -> DjogiJSahibONValueRef<M, V> {
DjogiJSahibONValueRef {
inner: self.inner.value(),
_marker: PhantomData,
}
}
#[must_use = "predicates are lazy — dropping one silently omits the filter"]
pub fn eq_json(&self, value: JSahibON) -> PortablePredicate<M> {
wrap_predicate(self.inner.eq_json(value))
}
#[must_use = "predicates are lazy — dropping one silently omits the filter"]
pub fn neq_json(&self, value: JSahibON) -> PortablePredicate<M> {
wrap_predicate(self.inner.neq_json(value))
}
#[must_use = "predicates are lazy — dropping one silently omits the filter"]
pub fn array_contains(&self, element: JSahibON) -> PortablePredicate<M> {
wrap_predicate(self.inner.array_contains(element))
}
#[must_use = "predicates are lazy — dropping one silently omits the filter"]
pub fn array_len_eq(&self, len: usize) -> PortablePredicate<M> {
wrap_predicate(self.inner.array_len_eq(len))
}
#[must_use = "predicates are lazy — dropping one silently omits the filter"]
pub fn array_len_gt(&self, len: usize) -> PortablePredicate<M> {
wrap_predicate(self.inner.array_len_gt(len))
}
#[must_use = "predicates are lazy — dropping one silently omits the filter"]
pub fn array_len_gte(&self, len: usize) -> PortablePredicate<M> {
wrap_predicate(self.inner.array_len_gte(len))
}
#[must_use = "predicates are lazy — dropping one silently omits the filter"]
pub fn array_len_lt(&self, len: usize) -> PortablePredicate<M> {
wrap_predicate(self.inner.array_len_lt(len))
}
#[must_use = "predicates are lazy — dropping one silently omits the filter"]
pub fn array_len_lte(&self, len: usize) -> PortablePredicate<M> {
wrap_predicate(self.inner.array_len_lte(len))
}
}
impl<M: Model, V: JScalar> DjogiJSahibONValueRef<M, V> {
#[must_use = "predicates are lazy — dropping one silently omits the filter"]
pub fn eq(&self, value: V) -> PortablePredicate<M> {
wrap_predicate(self.inner.eq(value))
}
#[must_use = "predicates are lazy — dropping one silently omits the filter"]
pub fn neq(&self, value: V) -> PortablePredicate<M> {
wrap_predicate(self.inner.neq(value))
}
#[must_use = "predicates are lazy — dropping one silently omits the filter"]
pub fn in_(&self, values: Vec<V>) -> PortablePredicate<M> {
wrap_predicate(self.inner.in_(values))
}
#[must_use = "predicates are lazy — dropping one silently omits the filter"]
pub fn not_in(&self, values: Vec<V>) -> PortablePredicate<M> {
wrap_predicate(self.inner.not_in(values))
}
}
impl<M: Model, V: JOrderedScalar> DjogiJSahibONValueRef<M, V> {
#[must_use = "predicates are lazy — dropping one silently omits the filter"]
pub fn gt(&self, value: V) -> PortablePredicate<M> {
wrap_predicate(self.inner.gt(value))
}
#[must_use = "predicates are lazy — dropping one silently omits the filter"]
pub fn gte(&self, value: V) -> PortablePredicate<M> {
wrap_predicate(self.inner.gte(value))
}
#[must_use = "predicates are lazy — dropping one silently omits the filter"]
pub fn lt(&self, value: V) -> PortablePredicate<M> {
wrap_predicate(self.inner.lt(value))
}
#[must_use = "predicates are lazy — dropping one silently omits the filter"]
pub fn lte(&self, value: V) -> PortablePredicate<M> {
wrap_predicate(self.inner.lte(value))
}
#[must_use = "predicates are lazy — dropping one silently omits the filter"]
pub fn between(&self, low: V, high: V) -> PortablePredicate<M> {
wrap_predicate(self.inner.between(low, high))
}
}
fn wrap_predicate<M: Model>(bp: BasicPredicate<M>) -> PortablePredicate<M> {
PortablePredicate::from_djogi_field(bp, DjogiFieldProvenance::__mirjzson_mint())
}
impl<M: Model> DjogiField<M, MirJzSON> {
#[must_use = "the JSON builder is lazy — call a predicate method to produce a filter"]
pub fn jsahibon(self) -> DjogiJSahibONFieldRef<M> {
let column = self.__sql_field().column();
let extract_mirjzson: fn(&M) -> &MirJzSON = self.extractor;
let extract_jsahibon: fn(&M) -> &JSahibON =
unsafe { std::mem::transmute(extract_mirjzson) };
let field = sassi::Field::<M, JSahibON>::new(column, extract_jsahibon);
DjogiJSahibONFieldRef {
inner: field.jsahibon(),
}
}
}
impl<M: Model> DjogiField<M, Option<MirJzSON>> {
#[must_use = "the JSON builder is lazy — call a predicate method to produce a filter"]
pub fn jsahibon(self) -> DjogiJSahibONOptionFieldRef<M> {
let column = self.__sql_field().column();
let extract_opt_mirjzson: fn(&M) -> &Option<MirJzSON> = self.extractor;
let extract_opt_jsahibon: fn(&M) -> &Option<JSahibON> =
unsafe { std::mem::transmute(extract_opt_mirjzson) };
let field = sassi::Field::<M, Option<JSahibON>>::new(column, extract_opt_jsahibon);
DjogiJSahibONOptionFieldRef {
inner: field.jsahibon(),
}
}
}
pub struct MirJzSONFieldRef<M: Model> {
_column: &'static str,
_marker: PhantomData<fn() -> M>,
}
impl<M: Model> std::fmt::Debug for MirJzSONFieldRef<M> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("MirJzSONFieldRef")
.field("column", &self._column)
.finish_non_exhaustive()
}
}
pub struct MirJzSONOptionFieldRef<M: Model> {
_column: &'static str,
_marker: PhantomData<fn() -> M>,
}
impl<M: Model> std::fmt::Debug for MirJzSONOptionFieldRef<M> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("MirJzSONOptionFieldRef")
.field("column", &self._column)
.finish_non_exhaustive()
}
}
impl<M: Model> crate::query::field::ExplicitPgPredicateField<M, MirJzSON> {
#[must_use = "the PG predicate view is lazy — call a method to produce a filter"]
pub fn mirjzson(self) -> MirJzSONFieldRef<M> {
MirJzSONFieldRef {
_column: self.__column(),
_marker: PhantomData,
}
}
}
impl<M: Model> crate::query::field::ExplicitPgPredicateField<M, Option<MirJzSON>> {
#[must_use = "the PG predicate view is lazy — call a method to produce a filter"]
pub fn mirjzson(self) -> MirJzSONOptionFieldRef<M> {
MirJzSONOptionFieldRef {
_column: self.__column(),
_marker: PhantomData,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::DjogiError;
use crate::descriptor::ModelDescriptor;
use std::future::Future;
#[derive(Debug)]
struct Fake {
payload: MirJzSON,
maybe: Option<MirJzSON>,
}
impl crate::model::__sealed::Sealed for Fake {}
#[allow(clippy::manual_async_fn)]
impl Model for Fake {
type Pk = i64;
type Fields = ();
fn table_name() -> &'static str {
"fakes"
}
fn pk_value(&self) -> &i64 {
unimplemented!()
}
fn descriptor() -> &'static ModelDescriptor {
unimplemented!()
}
fn get(
_ctx: &mut crate::context::DjogiContext,
_id: i64,
) -> impl Future<Output = Result<Self, DjogiError>> + Send {
async { unimplemented!() }
}
fn create(
_ctx: &mut crate::context::DjogiContext,
_v: Self,
) -> impl Future<Output = Result<Self, DjogiError>> + Send {
async { unimplemented!() }
}
fn save<'ctx>(
&'ctx mut self,
_ctx: &'ctx mut crate::context::DjogiContext,
) -> impl Future<Output = Result<(), DjogiError>> + Send + 'ctx {
async { unimplemented!() }
}
fn delete(
self,
_ctx: &mut crate::context::DjogiContext,
) -> impl Future<Output = Result<(), DjogiError>> + Send {
async { unimplemented!() }
}
fn refresh_from_db<'ctx>(
&'ctx self,
_ctx: &'ctx mut crate::context::DjogiContext,
) -> impl Future<Output = Result<Self, DjogiError>> + Send + 'ctx {
async { unimplemented!() }
}
}
fn payload_field() -> DjogiField<Fake, MirJzSON> {
crate::query::field::djogi_field_macro_support::__make_djogi_field::<Fake, MirJzSON>(
"payload",
|f| &f.payload,
)
}
fn maybe_field() -> DjogiField<Fake, Option<MirJzSON>> {
crate::query::field::djogi_field_macro_support::__make_djogi_field::<Fake, Option<MirJzSON>>(
"maybe",
|f| &f.maybe,
)
}
#[test]
fn exists_stamps_djogi_provenance() {
let predicate = payload_field().jsahibon().exists();
assert!(predicate.has_field_provenance());
match predicate.into_inner() {
BasicPredicate::Field(fp) => {
assert_eq!(fp.field_name(), "payload");
assert_eq!(fp.op(), sassi::LookupOp::Json);
}
other => panic!("expected Field predicate, got {other:?}"),
}
}
#[test]
fn path_value_gte_builds_scalar_compare() {
let predicate = payload_field()
.jsahibon()
.path("engine.cylinders")
.value::<i64>()
.gte(4);
assert!(predicate.has_field_provenance());
}
#[test]
fn key_accepts_arbitrary_string() {
let predicate = payload_field()
.jsahibon()
.key("content-type")
.value::<String>()
.eq("application/json".to_string());
assert!(predicate.has_field_provenance());
}
#[test]
fn path_segments_accepts_arbitrary_segments() {
let predicate = payload_field()
.jsahibon()
.path_segments(["a.b", "0", "cafe"])
.exists();
assert!(predicate.has_field_provenance());
}
#[test]
fn option_field_builds_predicates() {
let predicate = maybe_field().jsahibon().missing();
assert!(predicate.has_field_provenance());
}
#[test]
fn u64_max_operand_round_trips() {
let predicate = payload_field()
.jsahibon()
.path("count")
.value::<u64>()
.eq(u64::MAX);
assert!(predicate.has_field_provenance());
}
use crate::pg::accumulator::SqlAccumulator;
use crate::query::SqlEmitContext;
use crate::query::portable::{JsonTrust, emit_basic_predicate};
fn emit_predicate(predicate: PortablePredicate<Fake>) -> String {
let mut acc = SqlAccumulator::new("");
let bp = predicate.into_inner();
emit_basic_predicate::<Fake>(&mut acc, &bp, SqlEmitContext::root(), JsonTrust::Trusted)
.expect("portable JSON predicate must emit SQL");
let (sql, _binds) = acc.into_parts();
sql
}
#[test]
fn sql_exists_at_root_emits_path_extraction_is_not_null() {
let predicate = payload_field().jsahibon().exists();
let sql = emit_predicate(predicate);
assert_eq!(sql, "(payload #> $1) IS NOT NULL");
}
#[test]
fn sql_missing_at_root_emits_path_extraction_is_null() {
let predicate = payload_field().jsahibon().missing();
let sql = emit_predicate(predicate);
assert_eq!(sql, "(payload #> $1) IS NULL");
}
#[test]
fn sql_is_json_null_uses_jsonb_null_literal_with_coalesce() {
let predicate = payload_field().jsahibon().is_json_null();
let sql = emit_predicate(predicate);
assert_eq!(sql, "COALESCE((payload #> $1) = 'null'::jsonb, FALSE)");
}
#[test]
fn sql_has_key_guards_object_type_and_binds_key() {
let predicate = payload_field().jsahibon().has_key("content-type");
let sql = emit_predicate(predicate);
assert_eq!(
sql,
"COALESCE(jsonb_typeof((payload #> $1)) = 'object' AND (payload #> $2) ? $3, FALSE)"
);
}
#[test]
fn sql_has_any_key_emits_key_array_bind() {
let predicate = payload_field().jsahibon().has_any_key(["a", "b"]);
let sql = emit_predicate(predicate);
assert_eq!(
sql,
"COALESCE(jsonb_typeof((payload #> $1)) = 'object' AND (payload #> $2) ?| $3, FALSE)"
);
}
#[test]
fn sql_numeric_gte_uses_safe_case_with_numeric_cast() {
let predicate = payload_field()
.jsahibon()
.path("count")
.value::<i64>()
.gte(4);
let sql = emit_predicate(predicate);
assert_eq!(
sql,
"CASE WHEN jsonb_typeof((payload #> $1)) = 'number' THEN ((payload #> $2) #>> '{}'::text[])::numeric >= $3 ELSE FALSE END"
);
}
#[test]
fn sql_array_len_eq_guards_array_type_before_length_call() {
let predicate = payload_field().jsahibon().array_len_eq(3);
let sql = emit_predicate(predicate);
assert_eq!(
sql,
"CASE WHEN jsonb_typeof((payload #> $1)) = 'array' THEN jsonb_array_length((payload #> $2)) = $3 ELSE FALSE END"
);
}
#[test]
fn sql_json_eq_wraps_in_coalesce() {
let predicate = payload_field().jsahibon().eq_json(sassi::JSahibON::I64(42));
let sql = emit_predicate(predicate);
assert_eq!(sql, "COALESCE((payload #> $1) = $2, FALSE)");
}
#[test]
fn sql_array_contains_uses_single_element_array_with_at_at_arrow() {
let predicate = payload_field()
.jsahibon()
.array_contains(sassi::JSahibON::I64(7));
let sql = emit_predicate(predicate);
assert_eq!(
sql,
"COALESCE(jsonb_typeof((payload #> $1)) = 'array' AND (payload #> $2) @> $3, FALSE)"
);
}
#[test]
fn sql_option_missing_emits_is_null() {
let predicate = maybe_field().jsahibon().missing();
let sql = emit_predicate(predicate);
assert_eq!(sql, "(maybe #> $1) IS NULL");
}
#[test]
fn sql_bind_count_matches_spec() {
fn count_binds(predicate: PortablePredicate<Fake>) -> usize {
let mut acc = SqlAccumulator::new("");
let bp = predicate.into_inner();
emit_basic_predicate::<Fake>(&mut acc, &bp, SqlEmitContext::root(), JsonTrust::Trusted)
.unwrap();
let (_sql, binds) = acc.into_parts();
binds.len()
}
assert_eq!(count_binds(payload_field().jsahibon().exists()), 1);
assert_eq!(
count_binds(payload_field().jsahibon().eq_json(sassi::JSahibON::I64(42))),
2
);
assert_eq!(count_binds(payload_field().jsahibon().has_key("x")), 3);
assert_eq!(
count_binds(
payload_field()
.jsahibon()
.path("c")
.value::<u64>()
.eq(u64::MAX),
),
3
);
}
#[test]
fn sql_is_not_json_null_uses_coalesce_with_jsonb_null() {
let predicate = payload_field().jsahibon().is_not_json_null();
let sql = emit_predicate(predicate);
assert_eq!(sql, "COALESCE((payload #> $1) <> 'null'::jsonb, FALSE)");
}
#[test]
fn sql_between_uses_safe_case_with_numeric_cast() {
let predicate = payload_field()
.jsahibon()
.path("count")
.value::<i64>()
.between(1, 10);
let sql = emit_predicate(predicate);
assert_eq!(
sql,
"CASE WHEN jsonb_typeof((payload #> $1)) = 'number' THEN \
(((payload #> $2) #>> '{}'::text[])::numeric BETWEEN $3 AND $4) \
ELSE FALSE END"
);
}
#[test]
fn sql_not_in_empty_returns_false_outside_kind_guard() {
let predicate = payload_field()
.jsahibon()
.path("count")
.value::<i64>()
.not_in(Vec::<i64>::new());
let sql = emit_predicate(predicate);
assert_eq!(
sql,
"CASE WHEN jsonb_typeof((payload #> $1)) = 'number' THEN TRUE ELSE FALSE END"
);
}
#[test]
fn sql_in_empty_returns_false_everywhere() {
let predicate = payload_field()
.jsahibon()
.path("count")
.value::<i64>()
.in_(Vec::<i64>::new());
let sql = emit_predicate(predicate);
assert_eq!(
sql,
"CASE WHEN jsonb_typeof((payload #> $1)) = 'number' THEN FALSE ELSE FALSE END"
);
}
#[test]
fn sql_has_all_keys_emits_object_guard_and_key_array() {
let predicate = payload_field().jsahibon().has_all_keys(["a", "b"]);
let sql = emit_predicate(predicate);
assert_eq!(
sql,
"COALESCE(jsonb_typeof((payload #> $1)) = 'object' AND (payload #> $2) ?& $3, FALSE)"
);
}
#[test]
fn sql_is_object_emits_jsonb_typeof_check() {
let predicate = payload_field().jsahibon().is_object();
let sql = emit_predicate(predicate);
assert_eq!(
sql,
"COALESCE(jsonb_typeof((payload #> $1)) = 'object', FALSE)"
);
}
}