use crate::expr::Expr;
use crate::expr::node::ExprNode;
use crate::fts::TsQuery;
use crate::model::Model;
use crate::query::condition::Condition;
use std::marker::PhantomData;
pub struct FtsFieldRef<M: Model> {
pub(crate) column: &'static str,
pub(crate) dictionary: &'static str,
pub(crate) _m: PhantomData<fn() -> M>,
}
impl<M: Model> Copy for FtsFieldRef<M> {}
impl<M: Model> Clone for FtsFieldRef<M> {
fn clone(&self) -> Self {
*self
}
}
impl<M: Model> std::fmt::Debug for FtsFieldRef<M> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "FtsFieldRef({}, {})", self.column, self.dictionary)
}
}
impl<M: Model> FtsFieldRef<M> {
#[doc(hidden)]
pub fn __new(column: &'static str, dictionary: &'static str) -> Self {
FtsFieldRef {
column,
dictionary,
_m: PhantomData,
}
}
pub fn matches(self, query: TsQuery) -> Condition {
Condition::Expr(Expr::from_node(ExprNode::TsMatch {
column: self.column,
dictionary: self.dictionary,
query_text: query.0,
}))
}
pub fn rank(self, query: TsQuery) -> Expr<f32> {
Expr::from_node(ExprNode::TsRank {
column: self.column,
dictionary: self.dictionary,
query_text: query.0,
})
}
pub fn rank_cd(self, query: TsQuery) -> Expr<f32> {
Expr::from_node(ExprNode::TsRankCd {
column: self.column,
dictionary: self.dictionary,
query_text: query.0,
})
}
}
#[doc(hidden)]
pub mod __macro_support {
use super::FtsFieldRef;
use crate::ident::assert_plain_ident;
use crate::model::Model;
#[doc(hidden)]
#[inline]
pub fn __make_fts_ref<M: Model>(
column: &'static str,
dictionary: &'static str,
) -> FtsFieldRef<M> {
assert_plain_ident(column, "fts column");
FtsFieldRef::__new(column, dictionary)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::descriptor::ModelDescriptor;
struct FakeBook;
impl crate::model::__sealed::Sealed for FakeBook {}
#[allow(clippy::manual_async_fn)]
impl crate::model::Model for FakeBook {
type Pk = i64;
type Fields = ();
fn table_name() -> &'static str {
"book"
}
fn pk_value(&self) -> &i64 {
unreachable!()
}
fn descriptor() -> &'static ModelDescriptor {
unreachable!()
}
fn get(
_ctx: &mut crate::context::DjogiContext,
_id: i64,
) -> 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 fts_field_ref_matches_emits_at_operator() {
let fref = FtsFieldRef::<FakeBook>::__new("search", "english");
let cond = fref.matches(TsQuery::new("planet & earth"));
match cond {
Condition::Expr(expr) => match &expr.node {
ExprNode::TsMatch {
column,
dictionary,
query_text,
} => {
assert_eq!(*column, "search");
assert_eq!(*dictionary, "english");
assert_eq!(query_text, "planet & earth");
}
other => panic!("expected TsMatch, got {other:?}"),
},
other => panic!("expected Condition::Expr, got {other:?}"),
}
}
#[test]
fn fts_field_ref_rank_emits_ts_rank_node() {
let fref = FtsFieldRef::<FakeBook>::__new("search", "english");
let expr = fref.rank(TsQuery::new("planet & earth"));
match &expr.node {
ExprNode::TsRank {
column,
dictionary,
query_text,
} => {
assert_eq!(*column, "search");
assert_eq!(*dictionary, "english");
assert_eq!(query_text, "planet & earth");
}
other => panic!("expected TsRank, got {other:?}"),
}
}
#[test]
fn fts_matches_sql_emission() {
use crate::expr::sql::emit_expr;
use crate::pg::accumulator::SqlAccumulator;
use crate::query::portable::SqlEmitContext;
let fref = FtsFieldRef::<FakeBook>::__new("search", "english");
let cond = fref.matches(TsQuery::new("planet & earth"));
let Condition::Expr(expr) = cond else {
panic!("expected Condition::Expr");
};
let mut acc = SqlAccumulator::new("book");
emit_expr(&mut acc, &expr.node, SqlEmitContext::root())
.expect("FTS expression should lower to SQL");
let sql = acc.sql();
assert!(
sql.contains("search @@ to_tsquery('english',"),
"expected @@ operator in SQL, got: {sql}"
);
assert!(sql.contains("$1"), "expected bind parameter $1, got: {sql}");
}
#[test]
fn fts_rank_sql_emission() {
use crate::expr::sql::emit_expr;
use crate::pg::accumulator::SqlAccumulator;
use crate::query::portable::SqlEmitContext;
let fref = FtsFieldRef::<FakeBook>::__new("search", "english");
let rank_expr = fref.rank(TsQuery::new("planet & earth"));
let mut acc = SqlAccumulator::new("book");
emit_expr(&mut acc, &rank_expr.node, SqlEmitContext::root())
.expect("FTS expression should lower to SQL");
let sql = acc.sql();
assert!(
sql.contains("ts_rank(search, to_tsquery('english',"),
"expected ts_rank SQL, got: {sql}"
);
assert!(sql.contains("$1"), "expected bind parameter $1, got: {sql}");
}
}