diff --git a/.gitignore b/.gitignore index 4df730fd8..ab8060936 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ Cargo.lock .direnv/ .vscode/ +.idea/ diff --git a/db/scripts/postgres/ltree.sql b/db/scripts/postgres/ltree.sql new file mode 100644 index 000000000..9ddaa7dee --- /dev/null +++ b/db/scripts/postgres/ltree.sql @@ -0,0 +1 @@ +CREATE EXTENSION IF NOT EXISTS "ltree"; diff --git a/docker-compose.yml b/docker-compose.yml index dcb9fc07d..3a01fd363 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -12,6 +12,8 @@ services: networks: - databases tmpfs: /pgtmpfs12 + volumes: + - ./db/scripts/postgres:/docker-entrypoint-initdb.d mysql57: image: mysql:5.7 diff --git a/src/ast.rs b/src/ast.rs index 03f9bc234..7ed0a0404 100644 --- a/src/ast.rs +++ b/src/ast.rs @@ -31,6 +31,8 @@ mod values; pub use column::{Column, DefaultValue, TypeDataLength, TypeFamily}; pub use compare::{Comparable, Compare, JsonCompare, JsonType}; +#[cfg(feature = "postgresql")] +pub use compare::{LtreeCompare, LtreeQuery}; pub use conditions::ConditionTree; pub use conjunctive::Conjunctive; pub use cte::{CommonTableExpression, IntoCommonTableExpression}; diff --git a/src/ast/compare.rs b/src/ast/compare.rs index ae758b5af..4ac49eb14 100644 --- a/src/ast/compare.rs +++ b/src/ast/compare.rs @@ -53,6 +53,8 @@ pub enum Compare<'a> { /// (NOT `left` @@ to_tsquery(`value`)) #[cfg(feature = "postgresql")] NotMatches(Box>, Cow<'a, str>), + #[cfg(feature = "postgresql")] + LtreeCompare(LtreeCompare<'a>), } #[derive(Debug, Clone, PartialEq)] @@ -72,6 +74,44 @@ pub enum JsonType { Null, } +#[derive(Debug, Clone, PartialEq)] +#[cfg(feature = "postgresql")] +pub enum LtreeQuery<'a> { + String(Cow<'a, str>), + Array(Vec>), +} + +#[cfg(feature = "postgresql")] +impl<'a> LtreeQuery<'a> { + pub fn string(string: S) -> LtreeQuery<'a> + where + S: Into>, + { + LtreeQuery::String(string.into()) + } + + pub fn array(array: A) -> LtreeQuery<'a> + where + V: Into>, + A: Into>, + { + LtreeQuery::Array(array.into().into_iter().map(|v| v.into()).collect()) + } +} + +#[derive(Debug, Clone, PartialEq)] +#[cfg(feature = "postgresql")] +pub enum LtreeCompare<'a> { + IsAncestor(Box>, LtreeQuery<'a>), + IsNotAncestor(Box>, LtreeQuery<'a>), + IsDescendant(Box>, LtreeQuery<'a>), + IsNotDescendant(Box>, LtreeQuery<'a>), + Matches(Box>, LtreeQuery<'a>), + DoesNotMatch(Box>, LtreeQuery<'a>), + MatchesFullText(Box>, LtreeQuery<'a>), + DoesNotMatchFullText(Box>, LtreeQuery<'a>), +} + impl<'a> Compare<'a> { /// Finds a possible `(a,y) IN (SELECT x,z FROM B)`, takes the select out and /// converts the comparison into `a IN (SELECT x FROM cte_n where z = y)`. @@ -873,6 +913,185 @@ pub trait Comparable<'a> { where T: Into>, V: Into>; + + /// Determines whether a given ltree is the ancestor of one or more lqueries + /// + /// ```rust + /// # use quaint::{ast::*, visitor::{Visitor, Postgres}}; + /// # fn main() -> Result<(), quaint::error::Error> { + /// let query = Select::from_table("paths").so_that("path".ltree_is_ancestor(LtreeQuery::string("a.b.c"))); + /// let (sql, params) = Postgres::build(query)?; + /// + /// assert_eq!("SELECT \"paths\".* FROM \"paths\" WHERE \"path\" @> $1", sql); + /// + /// assert_eq!(vec![ + /// Value::from("a.b.c") + /// ], params); + /// + /// # Ok(()) + /// # } + /// ``` + #[cfg(feature = "postgresql")] + fn ltree_is_ancestor(self, ltree: T) -> Compare<'a> + where + T: Into>; + + /// Determines whether a given ltree is not the ancestor of one or more lqueries + /// + /// ```rust + /// # use quaint::{ast::*, visitor::{Visitor, Postgres}}; + /// # fn main() -> Result<(), quaint::error::Error> { + /// let query = Select::from_table("paths").so_that("path".ltree_is_not_ancestor(LtreeQuery::array(vec!["a.b.c", "d.e.f"]))); + /// let (sql, params) = Postgres::build(query)?; + /// + /// assert_eq!("SELECT \"paths\".* FROM \"paths\" WHERE (NOT \"path\" @> ARRAY[$1,$2]::ltree[])", sql); + /// + /// assert_eq!(vec![ + /// Value::from("a.b.c"), + /// Value::from("d.e.f") + /// ], params); + /// + /// # Ok(()) + /// # } + /// ``` + #[cfg(feature = "postgresql")] + fn ltree_is_not_ancestor(self, ltree: T) -> Compare<'a> + where + T: Into>; + + /// Determines whether a given ltree is the descendent of one or more lqueries + /// + /// ```rust + /// # use quaint::{ast::*, visitor::{Visitor, Postgres}}; + /// # fn main() -> Result<(), quaint::error::Error> { + /// let query = Select::from_table("paths").so_that("path".ltree_is_descendant(LtreeQuery::string("a.b.c"))); + /// let (sql, params) = Postgres::build(query)?; + /// + /// assert_eq!("SELECT \"paths\".* FROM \"paths\" WHERE \"path\" <@ $1", sql); + /// + /// assert_eq!(vec![ + /// Value::from("a.b.c") + /// ], params); + /// + /// # Ok(()) + /// # } + /// ``` + #[cfg(feature = "postgresql")] + fn ltree_is_descendant(self, ltree: T) -> Compare<'a> + where + T: Into>; + + /// Determines whether a given ltree is not the descendent of one or more lqueries + /// + /// ```rust + /// # use quaint::{ast::*, visitor::{Visitor, Postgres}}; + /// # fn main() -> Result<(), quaint::error::Error> { + /// let query = Select::from_table("paths").so_that("path".ltree_is_not_descendant(LtreeQuery::array(vec!["a.b.c", "d.e.f"]))); + /// let (sql, params) = Postgres::build(query)?; + /// + /// assert_eq!("SELECT \"paths\".* FROM \"paths\" WHERE (NOT \"path\" <@ ARRAY[$1,$2]::ltree[])", sql); + /// + /// assert_eq!(vec![ + /// Value::from("a.b.c"), + /// Value::from("d.e.f") + /// ], params); + /// + /// # Ok(()) + /// # } + /// ``` + #[cfg(feature = "postgresql")] + fn ltree_is_not_descendant(self, ltree: T) -> Compare<'a> + where + T: Into>; + + /// Determines whether a given ltree matches one or more lqueries + /// + /// ```rust + /// # use quaint::{ast::*, visitor::{Visitor, Postgres}}; + /// # fn main() -> Result<(), quaint::error::Error> { + /// let query = Select::from_table("paths").so_that("path".ltree_match(LtreeQuery::string("a.b.c"))); + /// let (sql, params) = Postgres::build(query)?; + /// + /// assert_eq!("SELECT \"paths\".* FROM \"paths\" WHERE \"path\" ~ $1", sql); + /// + /// assert_eq!(vec![ + /// Value::from("a.b.c") + /// ], params); + /// + /// # Ok(()) + /// # } + /// ``` + #[cfg(feature = "postgresql")] + fn ltree_match(self, lquery: T) -> Compare<'a> + where + T: Into>; + + /// Determines whether a given ltree does not match one or more lqueries + /// + /// ```rust + /// # use quaint::{ast::*, visitor::{Visitor, Postgres}}; + /// # fn main() -> Result<(), quaint::error::Error> { + /// let query = Select::from_table("paths").so_that("path".ltree_does_not_match(LtreeQuery::array(vec!["a.b.c", "d.e.f"]))); + /// let (sql, params) = Postgres::build(query)?; + /// + /// assert_eq!("SELECT \"paths\".* FROM \"paths\" WHERE (NOT \"path\" ? ARRAY[$1,$2]::lquery[])", sql); + /// + /// assert_eq!(vec![ + /// Value::from("a.b.c"), + /// Value::from("d.e.f") + /// ], params); + /// + /// # Ok(()) + /// # } + /// ``` + #[cfg(feature = "postgresql")] + fn ltree_does_not_match(self, lquery: T) -> Compare<'a> + where + T: Into>; + + /// Determines whether a given ltree matches fulltext one or more ltxtqueries + /// + /// ```rust + /// # use quaint::{ast::*, visitor::{Visitor, Postgres}}; + /// # fn main() -> Result<(), quaint::error::Error> { + /// let query = Select::from_table("paths").so_that("path".ltree_match_fulltext(LtreeQuery::string("a.b.c"))); + /// let (sql, params) = Postgres::build(query)?; + /// + /// assert_eq!("SELECT \"paths\".* FROM \"paths\" WHERE \"path\" @ $1", sql); + /// + /// assert_eq!(vec![ + /// Value::from("a.b.c") + /// ], params); + /// + /// # Ok(()) + /// # } + /// ``` + #[cfg(feature = "postgresql")] + fn ltree_match_fulltext(self, ltxtquery: T) -> Compare<'a> + where + T: Into>; + + /// Determines whether a given ltree does not match fulltext one or more ltxtqueries + /// + /// ```rust + /// # use quaint::{ast::*, visitor::{Visitor, Postgres}}; + /// # fn main() -> Result<(), quaint::error::Error> { + /// let query = Select::from_table("paths").so_that("path".ltree_does_not_match_fulltext(LtreeQuery::string("a.b.c"))); + /// let (sql, params) = Postgres::build(query)?; + /// + /// assert_eq!("SELECT \"paths\".* FROM \"paths\" WHERE (NOT \"path\" @ $1)", sql); + /// + /// assert_eq!(vec![ + /// Value::from("a.b.c") + /// ], params); + /// + /// # Ok(()) + /// # } + /// ``` + #[cfg(feature = "postgresql")] + fn ltree_does_not_match_fulltext(self, ltxtquery: T) -> Compare<'a> + where + T: Into>; } impl<'a, U> Comparable<'a> for U @@ -1150,4 +1369,92 @@ where val.not_matches(query) } + + #[cfg(feature = "postgresql")] + fn ltree_is_ancestor(self, ltree: T) -> Compare<'a> + where + T: Into>, + { + let col: Column<'a> = self.into(); + let val: Expression<'a> = col.into(); + + val.ltree_is_ancestor(ltree) + } + + #[cfg(feature = "postgresql")] + fn ltree_is_not_ancestor(self, ltree: T) -> Compare<'a> + where + T: Into>, + { + let col: Column<'a> = self.into(); + let val: Expression<'a> = col.into(); + + val.ltree_is_not_ancestor(ltree) + } + + #[cfg(feature = "postgresql")] + fn ltree_is_descendant(self, ltree: T) -> Compare<'a> + where + T: Into>, + { + let col: Column<'a> = self.into(); + let val: Expression<'a> = col.into(); + + val.ltree_is_descendant(ltree) + } + + #[cfg(feature = "postgresql")] + fn ltree_is_not_descendant(self, ltree: T) -> Compare<'a> + where + T: Into>, + { + let col: Column<'a> = self.into(); + let val: Expression<'a> = col.into(); + + val.ltree_is_not_descendant(ltree) + } + + #[cfg(feature = "postgresql")] + fn ltree_match(self, lquery: T) -> Compare<'a> + where + T: Into>, + { + let col: Column<'a> = self.into(); + let val: Expression<'a> = col.into(); + + val.ltree_match(lquery) + } + + #[cfg(feature = "postgresql")] + fn ltree_does_not_match(self, lquery: T) -> Compare<'a> + where + T: Into>, + { + let col: Column<'a> = self.into(); + let val: Expression<'a> = col.into(); + + val.ltree_does_not_match(lquery) + } + + #[cfg(feature = "postgresql")] + fn ltree_match_fulltext(self, ltxtquery: T) -> Compare<'a> + where + T: Into>, + { + let col: Column<'a> = self.into(); + let val: Expression<'a> = col.into(); + + val.ltree_match_fulltext(ltxtquery) + } + + #[cfg(feature = "postgresql")] + fn ltree_does_not_match_fulltext(self, ltxtquery: T) -> Compare<'a> + where + T: Into>, + { + let col: Column<'a> = self.into(); + let val: Expression<'a> = col.into(); + + val.ltree_does_not_match_fulltext(ltxtquery) + } } diff --git a/src/ast/expression.rs b/src/ast/expression.rs index 643b5cac8..fad9afabd 100644 --- a/src/ast/expression.rs +++ b/src/ast/expression.rs @@ -1,5 +1,7 @@ #[cfg(all(feature = "json", any(feature = "postgresql", feature = "mysql")))] use super::compare::{JsonCompare, JsonType}; +#[cfg(feature = "postgresql")] +use crate::ast::compare::LtreeCompare; use crate::ast::*; use query::SelectQuery; use std::borrow::Cow; @@ -524,4 +526,68 @@ impl<'a> Comparable<'a> for Expression<'a> { { Compare::NotMatches(Box::new(self), query.into()) } + + #[cfg(feature = "postgresql")] + fn ltree_is_ancestor(self, ltree: T) -> Compare<'a> + where + T: Into>, + { + Compare::LtreeCompare(LtreeCompare::IsAncestor(Box::new(self), ltree.into())) + } + + #[cfg(feature = "postgresql")] + fn ltree_is_not_ancestor(self, ltree: T) -> Compare<'a> + where + T: Into>, + { + Compare::LtreeCompare(LtreeCompare::IsNotAncestor(Box::new(self), ltree.into())) + } + + #[cfg(feature = "postgresql")] + fn ltree_is_descendant(self, ltree: T) -> Compare<'a> + where + T: Into>, + { + Compare::LtreeCompare(LtreeCompare::IsDescendant(Box::new(self), ltree.into())) + } + + #[cfg(feature = "postgresql")] + fn ltree_is_not_descendant(self, ltree: T) -> Compare<'a> + where + T: Into>, + { + Compare::LtreeCompare(LtreeCompare::IsNotDescendant(Box::new(self), ltree.into())) + } + + #[cfg(feature = "postgresql")] + fn ltree_match(self, lquery: T) -> Compare<'a> + where + T: Into>, + { + Compare::LtreeCompare(LtreeCompare::Matches(Box::new(self), lquery.into())) + } + + #[cfg(feature = "postgresql")] + fn ltree_does_not_match(self, lquery: T) -> Compare<'a> + where + T: Into>, + { + Compare::LtreeCompare(LtreeCompare::DoesNotMatch(Box::new(self), lquery.into())) + } + + #[cfg(feature = "postgresql")] + fn ltree_match_fulltext(self, ltxtquery: T) -> Compare<'a> + where + T: Into>, + { + Compare::LtreeCompare(LtreeCompare::MatchesFullText(Box::new(self), ltxtquery.into())) + } + + #[cfg(feature = "postgresql")] + fn ltree_does_not_match_fulltext(self, ltxtquery: T) -> Compare<'a> + where + T: Into>, + { + Compare::LtreeCompare(LtreeCompare::DoesNotMatchFullText(Box::new(self), ltxtquery.into())) + } } diff --git a/src/ast/row.rs b/src/ast/row.rs index 1d36226ef..0dd25a5da 100644 --- a/src/ast/row.rs +++ b/src/ast/row.rs @@ -1,5 +1,7 @@ #[cfg(all(feature = "json", any(feature = "postgresql", feature = "mysql")))] use super::compare::JsonType; +#[cfg(feature = "postgresql")] +use crate::ast::LtreeQuery; use crate::ast::{Comparable, Compare, Expression}; use std::borrow::Cow; @@ -404,4 +406,84 @@ impl<'a> Comparable<'a> for Row<'a> { value.not_matches(query) } + + #[cfg(feature = "postgresql")] + fn ltree_is_ancestor(self, ltree: T) -> Compare<'a> + where + T: Into>, + { + let val: Expression<'a> = self.into(); + + val.ltree_is_ancestor(ltree) + } + + #[cfg(feature = "postgresql")] + fn ltree_is_not_ancestor(self, ltree: T) -> Compare<'a> + where + T: Into>, + { + let val: Expression<'a> = self.into(); + + val.ltree_is_not_ancestor(ltree) + } + + #[cfg(feature = "postgresql")] + fn ltree_is_descendant(self, ltree: T) -> Compare<'a> + where + T: Into>, + { + let val: Expression<'a> = self.into(); + + val.ltree_is_descendant(ltree) + } + + #[cfg(feature = "postgresql")] + fn ltree_is_not_descendant(self, ltree: T) -> Compare<'a> + where + T: Into>, + { + let val: Expression<'a> = self.into(); + + val.ltree_is_not_descendant(ltree) + } + + #[cfg(feature = "postgresql")] + fn ltree_match(self, lquery: T) -> Compare<'a> + where + T: Into>, + { + let val: Expression<'a> = self.into(); + + val.ltree_match(lquery) + } + + #[cfg(feature = "postgresql")] + fn ltree_does_not_match(self, lquery: T) -> Compare<'a> + where + T: Into>, + { + let val: Expression<'a> = self.into(); + + val.ltree_does_not_match(lquery) + } + + #[cfg(feature = "postgresql")] + fn ltree_match_fulltext(self, ltxtquery: T) -> Compare<'a> + where + T: Into>, + { + let val: Expression<'a> = self.into(); + + val.ltree_match_fulltext(ltxtquery) + } + + #[cfg(feature = "postgresql")] + fn ltree_does_not_match_fulltext(self, ltxtquery: T) -> Compare<'a> + where + T: Into>, + { + let val: Expression<'a> = self.into(); + + val.ltree_does_not_match_fulltext(ltxtquery) + } } diff --git a/src/error.rs b/src/error.rs index 0d86e681e..39f3022f6 100644 --- a/src/error.rs +++ b/src/error.rs @@ -248,6 +248,9 @@ pub enum ErrorKind { #[error("Cannot find a FULLTEXT index to use for the search")] MissingFullTextSearchIndex, + + #[error("{} is not currently supported.", _0)] + Unsupported(String), } impl ErrorKind { diff --git a/src/tests/query.rs b/src/tests/query.rs index 2ad4f882a..901e0780f 100644 --- a/src/tests/query.rs +++ b/src/tests/query.rs @@ -2897,3 +2897,535 @@ async fn generate_native_uuid(api: &mut dyn TestApi) -> crate::Result<()> { Ok(()) } + +#[cfg(feature = "postgresql")] +#[test_each_connector(tags("postgresql"))] +async fn ltree_select_is_ancestor(api: &mut dyn TestApi) -> crate::Result<()> { + let table = api.create_table("path ltree").await?; + + let insert = Insert::multi_into(&table, vec!["path"]) + .values(vec!["A"]) + .values(vec!["B"]) + .values(vec!["C"]) + .values(vec!["A.B"]) + .values(vec!["A.B.C"]) + .values(vec!["B.C.D"]) + .values(vec!["C.D.E"]); + + api.conn().insert(insert.into()).await?; + + let select = Select::from_table(&table).so_that(col!("path").ltree_is_ancestor(LtreeQuery::string("A.B.C"))); + let rows = api.conn().select(select).await?; + + assert_eq!(rows.len(), 3); + + for (i, row) in rows.into_iter().enumerate() { + match i { + 0 => assert_eq!(Value::text("A"), row["path"]), + 1 => assert_eq!(Value::text("A.B"), row["path"]), + _ => assert_eq!(Value::text("A.B.C"), row["path"]), + } + } + + Ok(()) +} + +#[cfg(feature = "postgresql")] +#[test_each_connector(tags("postgresql"))] +async fn ltree_select_is_ancestor_any(api: &mut dyn TestApi) -> crate::Result<()> { + let table = api.create_table("path ltree").await?; + + let insert = Insert::multi_into(&table, vec!["path"]) + .values(vec!["A"]) + .values(vec!["B"]) + .values(vec!["C"]) + .values(vec!["A.B"]) + .values(vec!["A.B.C"]) + .values(vec!["B.C.D"]) + .values(vec!["C.D.E"]); + + api.conn().insert(insert.into()).await?; + + let select = + Select::from_table(&table).so_that(col!("path").ltree_is_ancestor(LtreeQuery::array(vec!["A.B", "B.C.D"]))); + let rows = api.conn().select(select).await?; + + assert_eq!(rows.len(), 4); + + for (i, row) in rows.into_iter().enumerate() { + match i { + 0 => assert_eq!(Value::text("A"), row["path"]), + 1 => assert_eq!(Value::text("B"), row["path"]), + 2 => assert_eq!(Value::text("A.B"), row["path"]), + _ => assert_eq!(Value::text("B.C.D"), row["path"]), + } + } + + Ok(()) +} + +#[cfg(feature = "postgresql")] +#[test_each_connector(tags("postgresql"))] +async fn ltree_select_is_not_ancestor(api: &mut dyn TestApi) -> crate::Result<()> { + let table = api.create_table("path ltree").await?; + + let insert = Insert::multi_into(&table, vec!["path"]) + .values(vec!["A"]) + .values(vec!["B"]) + .values(vec!["C"]) + .values(vec!["A.B"]) + .values(vec!["A.B.C"]) + .values(vec!["B.C.D"]) + .values(vec!["C.D.E"]); + + api.conn().insert(insert.into()).await?; + + let select = Select::from_table(&table).so_that(col!("path").ltree_is_not_ancestor(LtreeQuery::string("A.B"))); + let rows = api.conn().select(select).await?; + + assert_eq!(rows.len(), 5); + + for (i, row) in rows.into_iter().enumerate() { + match i { + 0 => assert_eq!(Value::text("B"), row["path"]), + 1 => assert_eq!(Value::text("C"), row["path"]), + 2 => assert_eq!(Value::text("A.B.C"), row["path"]), + 3 => assert_eq!(Value::text("B.C.D"), row["path"]), + _ => assert_eq!(Value::text("C.D.E"), row["path"]), + } + } + + Ok(()) +} + +#[cfg(feature = "postgresql")] +#[test_each_connector(tags("postgresql"))] +async fn ltree_select_is_not_ancestor_any(api: &mut dyn TestApi) -> crate::Result<()> { + let table = api.create_table("path ltree").await?; + + let insert = Insert::multi_into(&table, vec!["path"]) + .values(vec!["A"]) + .values(vec!["B"]) + .values(vec!["C"]) + .values(vec!["A.B"]) + .values(vec!["A.B.C"]) + .values(vec!["B.C.D"]) + .values(vec!["C.D.E"]); + + api.conn().insert(insert.into()).await?; + + let select = Select::from_table(&table) + .so_that(col!("path").ltree_is_not_ancestor(LtreeQuery::array(vec!["A.B.C", "B.C.D"]))); + let rows = api.conn().select(select).await?; + + assert_eq!(rows.len(), 2); + + for (i, row) in rows.into_iter().enumerate() { + match i { + 0 => assert_eq!(Value::text("C"), row["path"]), + _ => assert_eq!(Value::text("C.D.E"), row["path"]), + } + } + + Ok(()) +} + +#[cfg(feature = "postgresql")] +#[test_each_connector(tags("postgresql"))] +async fn ltree_select_is_descendant(api: &mut dyn TestApi) -> crate::Result<()> { + let table = api.create_table("path ltree").await?; + + let insert = Insert::multi_into(&table, vec!["path"]) + .values(vec!["A"]) + .values(vec!["B"]) + .values(vec!["C"]) + .values(vec!["A.B"]) + .values(vec!["A.B.C"]) + .values(vec!["B.C.D"]) + .values(vec!["C.D.E"]); + + api.conn().insert(insert.into()).await?; + + let select = Select::from_table(&table).so_that(col!("path").ltree_is_descendant(LtreeQuery::string("A.B"))); + let rows = api.conn().select(select).await?; + + assert_eq!(rows.len(), 2); + + for (i, row) in rows.into_iter().enumerate() { + match i { + 0 => assert_eq!(Value::text("A.B"), row["path"]), + _ => assert_eq!(Value::text("A.B.C"), row["path"]), + } + } + + Ok(()) +} + +#[cfg(feature = "postgresql")] +#[test_each_connector(tags("postgresql"))] +async fn ltree_select_is_descendant_any(api: &mut dyn TestApi) -> crate::Result<()> { + let table = api.create_table("path ltree").await?; + + let insert = Insert::multi_into(&table, vec!["path"]) + .values(vec!["A"]) + .values(vec!["B"]) + .values(vec!["C"]) + .values(vec!["A.B"]) + .values(vec!["A.B.C"]) + .values(vec!["B.C.D"]) + .values(vec!["C.D.E"]); + + api.conn().insert(insert.into()).await?; + + let select = + Select::from_table(&table).so_that(col!("path").ltree_is_descendant(LtreeQuery::array(vec!["A.B", "B"]))); + let rows = api.conn().select(select).await?; + + assert_eq!(rows.len(), 4); + + for (i, row) in rows.into_iter().enumerate() { + match i { + 0 => assert_eq!(Value::text("B"), row["path"]), + 1 => assert_eq!(Value::text("A.B"), row["path"]), + 2 => assert_eq!(Value::text("A.B.C"), row["path"]), + _ => assert_eq!(Value::text("B.C.D"), row["path"]), + } + } + + Ok(()) +} + +#[cfg(feature = "postgresql")] +#[test_each_connector(tags("postgresql"))] +async fn ltree_select_is_not_descendant(api: &mut dyn TestApi) -> crate::Result<()> { + let table = api.create_table("path ltree").await?; + + let insert = Insert::multi_into(&table, vec!["path"]) + .values(vec!["A"]) + .values(vec!["B"]) + .values(vec!["C"]) + .values(vec!["A.B"]) + .values(vec!["A.B.C"]) + .values(vec!["B.C.D"]) + .values(vec!["C.D.E"]); + + api.conn().insert(insert.into()).await?; + + let select = Select::from_table(&table).so_that(col!("path").ltree_is_not_descendant(LtreeQuery::string("A.B"))); + let rows = api.conn().select(select).await?; + + assert_eq!(rows.len(), 5); + + for (i, row) in rows.into_iter().enumerate() { + match i { + 0 => assert_eq!(Value::text("A"), row["path"]), + 1 => assert_eq!(Value::text("B"), row["path"]), + 2 => assert_eq!(Value::text("C"), row["path"]), + 3 => assert_eq!(Value::text("B.C.D"), row["path"]), + _ => assert_eq!(Value::text("C.D.E"), row["path"]), + } + } + + Ok(()) +} + +#[cfg(feature = "postgresql")] +#[test_each_connector(tags("postgresql"))] +async fn ltree_select_is_not_descendant_any(api: &mut dyn TestApi) -> crate::Result<()> { + let table = api.create_table("path ltree").await?; + + let insert = Insert::multi_into(&table, vec!["path"]) + .values(vec!["A"]) + .values(vec!["B"]) + .values(vec!["C"]) + .values(vec!["A.B"]) + .values(vec!["A.B.C"]) + .values(vec!["B.C.D"]) + .values(vec!["C.D.E"]); + + api.conn().insert(insert.into()).await?; + + let select = + Select::from_table(&table).so_that(col!("path").ltree_is_not_descendant(LtreeQuery::array(vec!["A.B", "C"]))); + let rows = api.conn().select(select).await?; + + assert_eq!(rows.len(), 3); + + for (i, row) in rows.into_iter().enumerate() { + match i { + 0 => assert_eq!(Value::text("A"), row["path"]), + 1 => assert_eq!(Value::text("B"), row["path"]), + _ => assert_eq!(Value::text("B.C.D"), row["path"]), + } + } + + Ok(()) +} + +#[cfg(feature = "postgresql")] +#[test_each_connector(tags("postgresql"))] +async fn ltree_select_match(api: &mut dyn TestApi) -> crate::Result<()> { + let table = api.create_table("path ltree").await?; + + let insert = Insert::multi_into(&table, vec!["path"]) + .values(vec!["foo.foo.foo"]) + .values(vec!["foo.foo.baz"]) + .values(vec!["foo.foo.bar"]) + .values(vec!["foo.baz.baz"]) + .values(vec!["foo.baz.bar"]) + .values(vec!["foo.baz.foo"]) + .values(vec!["foo.bar.bar"]) + .values(vec!["foo.bar.baz"]) + .values(vec!["foo.bar.foo"]); + + api.conn().insert(insert.into()).await?; + + let select = Select::from_table(&table).so_that(col!("path").ltree_match(LtreeQuery::string("foo.baz|bar.!foo"))); + let rows = api.conn().select(select).await?; + + assert_eq!(rows.len(), 4); + + for (i, row) in rows.into_iter().enumerate() { + match i { + 0 => assert_eq!(Value::text("foo.baz.baz"), row["path"]), + 1 => assert_eq!(Value::text("foo.baz.bar"), row["path"]), + 2 => assert_eq!(Value::text("foo.bar.bar"), row["path"]), + _ => assert_eq!(Value::text("foo.bar.baz"), row["path"]), + } + } + + Ok(()) +} + +#[cfg(feature = "postgresql")] +#[test_each_connector(tags("postgresql"))] +async fn ltree_select_match_any(api: &mut dyn TestApi) -> crate::Result<()> { + let table = api.create_table("path ltree").await?; + + let insert = Insert::multi_into(&table, vec!["path"]) + .values(vec!["foo.foo.foo"]) + .values(vec!["foo.foo.baz"]) + .values(vec!["foo.foo.bar"]) + .values(vec!["foo.baz.baz"]) + .values(vec!["foo.baz.bar"]) + .values(vec!["foo.baz.foo"]) + .values(vec!["foo.bar.bar"]) + .values(vec!["foo.bar.baz"]) + .values(vec!["foo.bar.foo"]); + + api.conn().insert(insert.into()).await?; + + let select = Select::from_table(&table) + .so_that(col!("path").ltree_match(LtreeQuery::array(vec!["foo.baz|bar.!foo", "foo.*.foo"]))); + let rows = api.conn().select(select).await?; + + assert_eq!(rows.len(), 7); + + for (i, row) in rows.into_iter().enumerate() { + match i { + 0 => assert_eq!(Value::text("foo.foo.foo"), row["path"]), + 1 => assert_eq!(Value::text("foo.baz.baz"), row["path"]), + 2 => assert_eq!(Value::text("foo.baz.bar"), row["path"]), + 3 => assert_eq!(Value::text("foo.baz.foo"), row["path"]), + 4 => assert_eq!(Value::text("foo.bar.bar"), row["path"]), + 5 => assert_eq!(Value::text("foo.bar.baz"), row["path"]), + _ => assert_eq!(Value::text("foo.bar.foo"), row["path"]), + } + } + + Ok(()) +} + +#[cfg(feature = "postgresql")] +#[test_each_connector(tags("postgresql"))] +async fn ltree_select_not_match(api: &mut dyn TestApi) -> crate::Result<()> { + let table = api.create_table("path ltree").await?; + + let insert = Insert::multi_into(&table, vec!["path"]) + .values(vec!["foo.foo.foo"]) + .values(vec!["foo.foo.baz"]) + .values(vec!["foo.foo.bar"]) + .values(vec!["foo.baz.baz"]) + .values(vec!["foo.baz.bar"]) + .values(vec!["foo.baz.foo"]) + .values(vec!["foo.bar.bar"]) + .values(vec!["foo.bar.baz"]) + .values(vec!["foo.bar.foo"]); + + api.conn().insert(insert.into()).await?; + + let select = + Select::from_table(&table).so_that(col!("path").ltree_does_not_match(LtreeQuery::string("foo.*.baz|bar"))); + let rows = api.conn().select(select).await?; + + assert_eq!(rows.len(), 3); + + for (i, row) in rows.into_iter().enumerate() { + match i { + 0 => assert_eq!(Value::text("foo.foo.foo"), row["path"]), + 1 => assert_eq!(Value::text("foo.baz.foo"), row["path"]), + _ => assert_eq!(Value::text("foo.bar.foo"), row["path"]), + } + } + + Ok(()) +} + +#[cfg(feature = "postgresql")] +#[test_each_connector(tags("postgresql"))] +async fn ltree_select_not_match_any(api: &mut dyn TestApi) -> crate::Result<()> { + let table = api.create_table("path ltree").await?; + + let insert = Insert::multi_into(&table, vec!["path"]) + .values(vec!["foo.foo.foo"]) + .values(vec!["foo.foo.baz"]) + .values(vec!["foo.foo.bar"]) + .values(vec!["foo.baz.baz"]) + .values(vec!["foo.baz.bar"]) + .values(vec!["foo.baz.foo"]) + .values(vec!["foo.bar.bar"]) + .values(vec!["foo.bar.baz"]) + .values(vec!["foo.bar.foo"]); + + api.conn().insert(insert.into()).await?; + + let select = Select::from_table(&table) + .so_that(col!("path").ltree_does_not_match(LtreeQuery::array(vec!["foo.*.baz|bar", "foo.b*.*"]))); + let rows = api.conn().select(select).await?; + + assert_eq!(rows.len(), 1); + + for (i, row) in rows.into_iter().enumerate() { + match i { + _ => assert_eq!(Value::text("foo.foo.foo"), row["path"]), + } + } + + Ok(()) +} + +#[cfg(feature = "postgresql")] +#[test_each_connector(tags("postgresql"))] +async fn ltree_select_match_fulltext(api: &mut dyn TestApi) -> crate::Result<()> { + let table = api.create_table("path ltree").await?; + + let insert = Insert::multi_into(&table, vec!["path"]) + .values(vec!["foo.foo.foo"]) + .values(vec!["foo.foo.baz"]) + .values(vec!["foo.foo.bar"]) + .values(vec!["foo.baz.baz"]) + .values(vec!["foo.baz.bar"]) + .values(vec!["foo.baz.foo"]) + .values(vec!["foo.bar.bar"]) + .values(vec!["foo.bar.baz"]) + .values(vec!["foo.bar.foo"]); + + api.conn().insert(insert.into()).await?; + + let select = Select::from_table(&table).so_that(col!("path").ltree_match_fulltext(LtreeQuery::string("b* & !bar"))); + let rows = api.conn().select(select).await?; + + assert_eq!(rows.len(), 3); + + for (i, row) in rows.into_iter().enumerate() { + match i { + 0 => assert_eq!(Value::text("foo.foo.baz"), row["path"]), + 1 => assert_eq!(Value::text("foo.baz.baz"), row["path"]), + _ => assert_eq!(Value::text("foo.baz.foo"), row["path"]), + } + } + + Ok(()) +} + +#[cfg(feature = "postgresql")] +#[test_each_connector(tags("postgresql"))] +async fn ltree_select_not_match_fulltext(api: &mut dyn TestApi) -> crate::Result<()> { + let table = api.create_table("path ltree").await?; + + let insert = Insert::multi_into(&table, vec!["path"]) + .values(vec!["foo.foo.foo"]) + .values(vec!["foo.foo.baz"]) + .values(vec!["foo.foo.bar"]) + .values(vec!["foo.baz.baz"]) + .values(vec!["foo.baz.bar"]) + .values(vec!["foo.baz.foo"]) + .values(vec!["foo.bar.bar"]) + .values(vec!["foo.bar.baz"]) + .values(vec!["foo.bar.foo"]); + + api.conn().insert(insert.into()).await?; + + let select = + Select::from_table(&table).so_that(col!("path").ltree_does_not_match_fulltext(LtreeQuery::string("b* & !bar"))); + let rows = api.conn().select(select).await?; + + assert_eq!(rows.len(), 6); + + for (i, row) in rows.into_iter().enumerate() { + match i { + 0 => assert_eq!(Value::text("foo.foo.foo"), row["path"]), + 1 => assert_eq!(Value::text("foo.foo.bar"), row["path"]), + 2 => assert_eq!(Value::text("foo.baz.bar"), row["path"]), + 3 => assert_eq!(Value::text("foo.bar.bar"), row["path"]), + 4 => assert_eq!(Value::text("foo.bar.baz"), row["path"]), + _ => assert_eq!(Value::text("foo.bar.foo"), row["path"]), + } + } + + Ok(()) +} + +#[cfg(feature = "postgresql")] +#[test_each_connector(tags("postgresql"))] +async fn ltree_select_match_fulltext_any(api: &mut dyn TestApi) -> crate::Result<()> { + let table = api.create_table("path ltree").await?; + + let insert = Insert::multi_into(&table, vec!["path"]) + .values(vec!["foo.foo.foo"]) + .values(vec!["foo.foo.baz"]) + .values(vec!["foo.foo.bar"]) + .values(vec!["foo.baz.baz"]) + .values(vec!["foo.baz.bar"]) + .values(vec!["foo.baz.foo"]) + .values(vec!["foo.bar.bar"]) + .values(vec!["foo.bar.baz"]) + .values(vec!["foo.bar.foo"]); + + api.conn().insert(insert.into()).await?; + + let condition = col!("path").ltree_match_fulltext(LtreeQuery::array(vec!["b* & !bar"])); + let select = Select::from_table(&table).so_that(condition); + let rows = api.conn().select(select).await; + + assert!(rows.is_err()); + + Ok(()) +} + +#[cfg(feature = "postgresql")] +#[test_each_connector(tags("postgresql"))] +async fn ltree_select_does_not_match_fulltext_any(api: &mut dyn TestApi) -> crate::Result<()> { + let table = api.create_table("path ltree").await?; + + let insert = Insert::multi_into(&table, vec!["path"]) + .values(vec!["foo.foo.foo"]) + .values(vec!["foo.foo.baz"]) + .values(vec!["foo.foo.bar"]) + .values(vec!["foo.baz.baz"]) + .values(vec!["foo.baz.bar"]) + .values(vec!["foo.baz.foo"]) + .values(vec!["foo.bar.bar"]) + .values(vec!["foo.bar.baz"]) + .values(vec!["foo.bar.foo"]); + + api.conn().insert(insert.into()).await?; + + let condition = col!("path").ltree_does_not_match_fulltext(LtreeQuery::array(vec!["b* & !bar"])); + let select = Select::from_table(&table).so_that(condition); + let rows = api.conn().select(select).await; + + assert!(rows.is_err()); + + Ok(()) +} diff --git a/src/visitor.rs b/src/visitor.rs index cb61dae8e..33c10c4c7 100644 --- a/src/visitor.rs +++ b/src/visitor.rs @@ -126,6 +126,18 @@ pub trait Visitor<'a> { #[cfg(any(feature = "postgresql", feature = "mysql"))] fn visit_text_search_relevance(&mut self, text_search_relevance: TextSearchRelevance<'a>) -> Result; + #[cfg(feature = "postgresql")] + fn visit_ltree_is_ancestor(&mut self, left: Expression<'a>, right: LtreeQuery<'a>, not: bool) -> Result; + + #[cfg(feature = "postgresql")] + fn visit_ltree_is_descendant(&mut self, left: Expression<'a>, right: LtreeQuery<'a>, not: bool) -> Result; + + #[cfg(feature = "postgresql")] + fn visit_ltree_match(&mut self, left: Expression<'a>, right: LtreeQuery<'a>, not: bool) -> Result; + + #[cfg(feature = "postgresql")] + fn visit_ltree_match_fulltext(&mut self, left: Expression<'a>, right: LtreeQuery<'a>, not: bool) -> Result; + /// A visit to a value we parameterize fn visit_parameterized(&mut self, value: Value<'a>) -> Result { self.add_parameter(value); @@ -896,6 +908,17 @@ pub trait Visitor<'a> { Compare::Matches(left, right) => self.visit_matches(*left, right, false), #[cfg(feature = "postgresql")] Compare::NotMatches(left, right) => self.visit_matches(*left, right, true), + #[cfg(feature = "postgresql")] + Compare::LtreeCompare(ltree_compare) => match ltree_compare { + LtreeCompare::IsAncestor(left, right) => self.visit_ltree_is_ancestor(*left, right, false), + LtreeCompare::IsNotAncestor(left, right) => self.visit_ltree_is_ancestor(*left, right, true), + LtreeCompare::IsDescendant(left, right) => self.visit_ltree_is_descendant(*left, right, false), + LtreeCompare::IsNotDescendant(left, right) => self.visit_ltree_is_descendant(*left, right, true), + LtreeCompare::Matches(left, right) => self.visit_ltree_match(*left, right, false), + LtreeCompare::DoesNotMatch(left, right) => self.visit_ltree_match(*left, right, true), + LtreeCompare::MatchesFullText(left, right) => self.visit_ltree_match_fulltext(*left, right, false), + LtreeCompare::DoesNotMatchFullText(left, right) => self.visit_ltree_match_fulltext(*left, right, true), + }, } } diff --git a/src/visitor/mssql.rs b/src/visitor/mssql.rs index e22cf2791..5795415f3 100644 --- a/src/visitor/mssql.rs +++ b/src/visitor/mssql.rs @@ -1,4 +1,6 @@ use super::Visitor; +#[cfg(feature = "postgresql")] +use crate::ast::LtreeQuery; #[cfg(all(feature = "json", any(feature = "postgresql", feature = "mysql")))] use crate::prelude::{JsonExtract, JsonType}; use crate::{ @@ -643,6 +645,41 @@ impl<'a> Visitor<'a> for Mssql<'a> { ) -> visitor::Result { unimplemented!("JSON filtering is not yet supported on MSSQL") } + + #[cfg(feature = "postgresql")] + fn visit_ltree_is_ancestor( + &mut self, + _left: Expression<'a>, + _right: LtreeQuery<'a>, + _not: bool, + ) -> visitor::Result { + unimplemented!("Ltree is not supported on MSSQL"); + } + + #[cfg(feature = "postgresql")] + fn visit_ltree_is_descendant( + &mut self, + _left: Expression<'a>, + _right: LtreeQuery<'a>, + _not: bool, + ) -> visitor::Result { + unimplemented!("Ltree is not supported on MSSQL"); + } + + #[cfg(feature = "postgresql")] + fn visit_ltree_match(&mut self, _left: Expression<'a>, _right: LtreeQuery<'a>, _not: bool) -> visitor::Result { + unimplemented!("Ltree is not supported on MSSQL"); + } + + #[cfg(feature = "postgresql")] + fn visit_ltree_match_fulltext( + &mut self, + _left: Expression<'a>, + _right: LtreeQuery<'a>, + _not: bool, + ) -> visitor::Result { + unimplemented!("Ltree is not supported on MSSQL"); + } } #[cfg(test)] diff --git a/src/visitor/mysql.rs b/src/visitor/mysql.rs index d7c35806b..787676bf9 100644 --- a/src/visitor/mysql.rs +++ b/src/visitor/mysql.rs @@ -507,6 +507,41 @@ impl<'a> Visitor<'a> for Mysql<'a> { Ok(()) } + + #[cfg(feature = "postgresql")] + fn visit_ltree_is_ancestor( + &mut self, + _left: Expression<'a>, + _right: LtreeQuery<'a>, + _not: bool, + ) -> visitor::Result { + unimplemented!("Ltree is not supported on MySQL"); + } + + #[cfg(feature = "postgresql")] + fn visit_ltree_is_descendant( + &mut self, + _left: Expression<'a>, + _right: LtreeQuery<'a>, + _not: bool, + ) -> visitor::Result { + unimplemented!("Ltree is not supported on MySQL"); + } + + #[cfg(feature = "postgresql")] + fn visit_ltree_match(&mut self, _left: Expression<'a>, _right: LtreeQuery<'a>, _not: bool) -> visitor::Result { + unimplemented!("Ltree is not supported on MySQL"); + } + + #[cfg(feature = "postgresql")] + fn visit_ltree_match_fulltext( + &mut self, + _left: Expression<'a>, + _right: LtreeQuery<'a>, + _not: bool, + ) -> visitor::Result { + unimplemented!("Ltree is not supported on MySQL"); + } } #[cfg(test)] diff --git a/src/visitor/postgres.rs b/src/visitor/postgres.rs index 30e32e6c9..a42717692 100644 --- a/src/visitor/postgres.rs +++ b/src/visitor/postgres.rs @@ -1,3 +1,5 @@ +use crate::error::Error; +use crate::error::ErrorKind::Unsupported; use crate::{ ast::*, visitor::{self, Visitor}, @@ -416,6 +418,137 @@ impl<'a> Visitor<'a> for Postgres<'a> { Ok(()) } + + #[cfg(feature = "postgresql")] + fn visit_ltree_is_ancestor(&mut self, left: Expression<'a>, query: LtreeQuery<'a>, not: bool) -> visitor::Result { + if not { + self.write("(NOT ")?; + } + + self.visit_expression(left)?; + self.write(" @> ")?; + + match query { + LtreeQuery::String(str) => self.visit_parameterized(Value::text(str))?, + LtreeQuery::Array(query_path) => { + let len = query_path.len(); + self.surround_with("ARRAY[", "]::ltree[]", |s| { + for (i, query_part) in query_path.into_iter().enumerate() { + s.visit_parameterized(Value::text(query_part))?; + + if i < (len - 1) { + s.write(",")?; + } + } + Ok(()) + })?; + } + } + + if not { + self.write(")")?; + } + + Ok(()) + } + + #[cfg(feature = "postgresql")] + fn visit_ltree_is_descendant(&mut self, left: Expression<'a>, query: LtreeQuery<'a>, not: bool) -> visitor::Result { + if not { + self.write("(NOT ")?; + } + + self.visit_expression(left)?; + self.write(" <@ ")?; + + match query { + LtreeQuery::String(str) => self.visit_parameterized(Value::text(str))?, + LtreeQuery::Array(query_path) => { + let len = query_path.len(); + self.surround_with("ARRAY[", "]::ltree[]", |s| { + for (i, query_part) in query_path.into_iter().enumerate() { + s.visit_parameterized(Value::text(query_part))?; + + if i < (len - 1) { + s.write(",")?; + } + } + Ok(()) + })?; + } + } + + if not { + self.write(")")?; + } + + Ok(()) + } + + #[cfg(feature = "postgresql")] + fn visit_ltree_match(&mut self, left: Expression<'a>, query: LtreeQuery<'a>, not: bool) -> visitor::Result { + if not { + self.write("(NOT ")? + } + + self.visit_expression(left)?; + + match query { + LtreeQuery::String(str) => { + self.write(" ~ ")?; + self.visit_parameterized(Value::text(str))?; + } + LtreeQuery::Array(query_path) => { + self.write(" ? ")?; + + let len = query_path.len(); + self.surround_with("ARRAY[", "]::lquery[]", |s| { + for (i, query_part) in query_path.into_iter().enumerate() { + s.visit_parameterized(Value::text(query_part))?; + + if i < (len - 1) { + s.write(",")?; + } + } + Ok(()) + })?; + } + } + + if not { + self.write(")")?; + } + + Ok(()) + } + + #[cfg(feature = "postgresql")] + fn visit_ltree_match_fulltext( + &mut self, + left: Expression<'a>, + query: LtreeQuery<'a>, + not: bool, + ) -> visitor::Result { + match query { + LtreeQuery::String(str) => { + if not { + self.write("(NOT ")?; + } + + self.visit_expression(left)?; + self.write(" @ ")?; + + self.visit_parameterized(Value::text(str))?; + + if not { + self.write(")")?; + } + + Ok(()) + } + LtreeQuery::Array(_) => Err(Error::builder(Unsupported("ltxtquery array".into())).build()), + } + } } #[cfg(test)] @@ -459,7 +592,7 @@ mod tests { } #[test] - #[cfg(feature = "postgres")] + #[cfg(feature = "postgresql")] fn test_returning_insert() { let expected = expected_values( "INSERT INTO \"users\" (\"foo\") VALUES ($1) RETURNING \"foo\"", @@ -833,4 +966,266 @@ mod tests { assert_eq!("SELECT \"User\".*, \"Toto\".* FROM \"User\" LEFT JOIN \"Post\" AS \"p\" ON \"p\".\"userId\" = \"User\".\"id\", \"Toto\"", sql); } + + #[test] + #[cfg(feature = "postgresql")] + fn test_ltree_is_ancestor() { + let expected = expected_values( + r#"SELECT "test".* FROM "test" WHERE "path" @> $1"#, + vec![Value::text("a.b.c")], + ); + + let query = Select::from_table("test") + .so_that(Column::from("path").ltree_is_ancestor(LtreeQuery::string(Cow::from("a.b.c")))); + let (sql, params) = Postgres::build(query).unwrap(); + + assert_eq!(expected.0, sql); + assert_eq!(expected.1, params); + } + + #[test] + #[cfg(feature = "postgresql")] + fn test_ltree_is_ancestor_many() { + let expected = expected_values( + r#"SELECT "test".* FROM "test" WHERE "path" @> ARRAY[$1,$2]::ltree[]"#, + vec![Value::text("a.b.c"), Value::text("d.e.f")], + ); + let query = Select::from_table("test").so_that( + Column::from("path").ltree_is_ancestor(LtreeQuery::array(vec![Cow::from("a.b.c"), Cow::from("d.e.f")])), + ); + let (sql, params) = Postgres::build(query).unwrap(); + + assert_eq!(expected.0, sql); + assert_eq!(expected.1, params); + } + + #[test] + #[cfg(feature = "postgresql")] + fn test_ltree_is_not_ancestor() { + let expected = expected_values( + r#"SELECT "test".* FROM "test" WHERE (NOT "path" @> $1)"#, + vec![Value::text("a.b.c")], + ); + + let query = Select::from_table("test") + .so_that(Column::from("path").ltree_is_not_ancestor(LtreeQuery::string(Cow::from("a.b.c")))); + let (sql, params) = Postgres::build(query).unwrap(); + + assert_eq!(expected.0, sql); + assert_eq!(expected.1, params); + } + + #[test] + #[cfg(feature = "postgresql")] + fn test_ltree_is_not_ancestor_many() { + let expected = expected_values( + r#"SELECT "test".* FROM "test" WHERE (NOT "path" @> ARRAY[$1,$2]::ltree[])"#, + vec![Value::text("a.b.c"), Value::text("d.e.f")], + ); + + let query = Select::from_table("test").so_that( + Column::from("path").ltree_is_not_ancestor(LtreeQuery::array(vec![Cow::from("a.b.c"), Cow::from("d.e.f")])), + ); + let (sql, params) = Postgres::build(query).unwrap(); + + assert_eq!(expected.0, sql); + assert_eq!(expected.1, params); + } + + #[test] + #[cfg(feature = "postgresql")] + fn test_ltree_is_descendant() { + let expected = expected_values( + r#"SELECT "test".* FROM "test" WHERE "path" <@ $1"#, + vec![Value::text("a.b.c")], + ); + + let query = Select::from_table("test") + .so_that(Column::from("path").ltree_is_descendant(LtreeQuery::string(Cow::from("a.b.c")))); + let (sql, params) = Postgres::build(query).unwrap(); + + assert_eq!(expected.0, sql); + assert_eq!(expected.1, params); + } + + #[test] + #[cfg(feature = "postgresql")] + fn test_ltree_is_descendant_many() { + let expected = expected_values( + r#"SELECT "test".* FROM "test" WHERE "path" <@ ARRAY[$1,$2]::ltree[]"#, + vec![Value::text("a.b.c"), Value::text("d.e.f")], + ); + + let query = Select::from_table("test").so_that( + Column::from("path").ltree_is_descendant(LtreeQuery::array(vec![Cow::from("a.b.c"), Cow::from("d.e.f")])), + ); + let (sql, params) = Postgres::build(query).unwrap(); + + assert_eq!(expected.0, sql); + assert_eq!(expected.1, params); + } + + #[test] + #[cfg(feature = "postgresql")] + fn test_ltree_is_not_descendant() { + let expected = expected_values( + r#"SELECT "test".* FROM "test" WHERE (NOT "path" <@ $1)"#, + vec![Value::text("a.b.c")], + ); + + let query = Select::from_table("test") + .so_that(Column::from("path").ltree_is_not_descendant(LtreeQuery::string(Cow::from("a.b.c")))); + let (sql, params) = Postgres::build(query).unwrap(); + + assert_eq!(expected.0, sql); + assert_eq!(expected.1, params); + } + + #[test] + #[cfg(feature = "postgresql")] + fn test_ltree_is_not_descendant_many() { + let expected = expected_values( + r#"SELECT "test".* FROM "test" WHERE (NOT "path" <@ ARRAY[$1,$2]::ltree[])"#, + vec![Value::text("a.b.c"), Value::text("d.e.f")], + ); + + let query = Select::from_table("test").so_that( + Column::from("path") + .ltree_is_not_descendant(LtreeQuery::array(vec![Cow::from("a.b.c"), Cow::from("d.e.f")])), + ); + let (sql, params) = Postgres::build(query).unwrap(); + + assert_eq!(expected.0, sql); + assert_eq!(expected.1, params); + } + + #[test] + #[cfg(feature = "postgresql")] + fn test_ltree_is_match() { + let expected = expected_values( + r#"SELECT "test".* FROM "test" WHERE "path" ~ $1"#, + vec![Value::text("a.b.c")], + ); + + let query = Select::from_table("test") + .so_that(Column::from("path").ltree_match(LtreeQuery::string(Cow::from("a.b.c")))); + let (sql, params) = Postgres::build(query).unwrap(); + + assert_eq!(expected.0, sql); + assert_eq!(expected.1, params); + } + + #[test] + #[cfg(feature = "postgresql")] + fn test_ltree_is_match_many() { + let expected = expected_values( + r#"SELECT "test".* FROM "test" WHERE "path" ? ARRAY[$1,$2]::lquery[]"#, + vec![Value::text("a.b.c"), Value::text("d.e.f")], + ); + + let query = Select::from_table("test") + .so_that(Column::from("path").ltree_match(LtreeQuery::array(vec![Cow::from("a.b.c"), Cow::from("d.e.f")]))); + let (sql, params) = Postgres::build(query).unwrap(); + + assert_eq!(expected.0, sql); + assert_eq!(expected.1, params); + } + + #[test] + #[cfg(feature = "postgresql")] + fn test_ltree_is_not_match() { + let expected = expected_values( + r#"SELECT "test".* FROM "test" WHERE (NOT "path" ~ $1)"#, + vec![Value::text("a.b.c")], + ); + + let query = Select::from_table("test") + .so_that(Column::from("path").ltree_does_not_match(LtreeQuery::string(Cow::from("a.b.c")))); + let (sql, params) = Postgres::build(query).unwrap(); + + assert_eq!(expected.0, sql); + assert_eq!(expected.1, params); + } + + #[test] + #[cfg(feature = "postgresql")] + fn test_ltree_is_not_match_many() { + let expected = expected_values( + r#"SELECT "test".* FROM "test" WHERE (NOT "path" ? ARRAY[$1,$2]::lquery[])"#, + vec![Value::text("a.b.c"), Value::text("d.e.f")], + ); + + let query = Select::from_table("test").so_that( + Column::from("path").ltree_does_not_match(LtreeQuery::array(vec![Cow::from("a.b.c"), Cow::from("d.e.f")])), + ); + let (sql, params) = Postgres::build(query).unwrap(); + + assert_eq!(expected.0, sql); + assert_eq!(expected.1, params); + } + + #[test] + #[cfg(feature = "postgresql")] + fn test_ltree_is_match_fulltext() { + let expected = expected_values( + r#"SELECT "test".* FROM "test" WHERE "path" @ $1"#, + vec![Value::text("a.b.c")], + ); + + let query = Select::from_table("test") + .so_that(Column::from("path").ltree_match_fulltext(LtreeQuery::string(Cow::from("a.b.c")))); + let (sql, params) = Postgres::build(query).unwrap(); + + assert_eq!(expected.0, sql); + assert_eq!(expected.1, params); + } + + #[test] + #[cfg(feature = "postgresql")] + fn test_ltree_is_match_fulltext_many() { + let query = Select::from_table("test").so_that( + Column::from("path").ltree_match_fulltext(LtreeQuery::array(vec![Cow::from("a.b.c"), Cow::from("d.e.f")])), + ); + + // This should error due to arrays of ltxtqueries not being supported + let success = match Postgres::build(query) { + Err(_) => true, + _ => false, + }; + + assert!(success) + } + + #[test] + #[cfg(feature = "postgresql")] + fn test_ltree_is_not_match_fulltext() { + let expected = expected_values( + r#"SELECT "test".* FROM "test" WHERE (NOT "path" @ $1)"#, + vec![Value::text("a.b.c")], + ); + + let query = Select::from_table("test") + .so_that(Column::from("path").ltree_does_not_match_fulltext(LtreeQuery::string(Cow::from("a.b.c")))); + let (sql, params) = Postgres::build(query).unwrap(); + + assert_eq!(expected.0, sql); + assert_eq!(expected.1, params); + } + + #[test] + #[cfg(feature = "postgresql")] + fn test_ltree_is_not_match_fulltext_many() { + let query = Select::from_table("test").so_that( + Column::from("path") + .ltree_does_not_match_fulltext(LtreeQuery::array(vec![Cow::from("a.b.c"), Cow::from("d.e.f")])), + ); + + // This should error due to arrays of ltxtqueries not being supported + let success = match Postgres::build(query) { + Err(_) => true, + _ => false, + }; + + assert!(success); + } } diff --git a/src/visitor/sqlite.rs b/src/visitor/sqlite.rs index 7541674a8..edcac141a 100644 --- a/src/visitor/sqlite.rs +++ b/src/visitor/sqlite.rs @@ -285,6 +285,41 @@ impl<'a> Visitor<'a> for Sqlite<'a> { fn visit_json_extract_first_array_item(&mut self, _extract: JsonExtractFirstArrayElem<'a>) -> visitor::Result { unimplemented!("JSON filtering is not yet supported on SQLite") } + + #[cfg(feature = "postgresql")] + fn visit_ltree_is_ancestor( + &mut self, + _left: Expression<'a>, + _right: LtreeQuery<'a>, + _not: bool, + ) -> visitor::Result { + unimplemented!("Ltree is not supported on SQLite"); + } + + #[cfg(feature = "postgresql")] + fn visit_ltree_is_descendant( + &mut self, + _left: Expression<'a>, + _right: LtreeQuery<'a>, + _not: bool, + ) -> visitor::Result { + unimplemented!("Ltree is not supported on SQLite"); + } + + #[cfg(feature = "postgresql")] + fn visit_ltree_match(&mut self, _left: Expression<'a>, _right: LtreeQuery<'a>, _not: bool) -> visitor::Result { + unimplemented!("Ltree is not supported on SQLite"); + } + + #[cfg(feature = "postgresql")] + fn visit_ltree_match_fulltext( + &mut self, + _left: Expression<'a>, + _right: LtreeQuery<'a>, + _not: bool, + ) -> visitor::Result { + unimplemented!("Ltree is not supported on SQLite"); + } } #[cfg(test)]