From 944b72974f751ecd6ac72447af753cec7b88320e Mon Sep 17 00:00:00 2001 From: Matt Oliver Date: Thu, 3 Mar 2022 00:06:46 -0600 Subject: [PATCH] Add ltree, lquery and ltxtquery support --- postgres-protocol/Cargo.toml | 2 +- postgres-protocol/src/types/mod.rs | 16 ++++++ postgres-types/Cargo.toml | 4 +- postgres-types/src/lib.rs | 44 +++++++++++---- tokio-postgres/Cargo.toml | 6 +-- tokio-postgres/tests/test/types/mod.rs | 75 ++++++++++++++++++++++++++ 6 files changed, 131 insertions(+), 16 deletions(-) diff --git a/postgres-protocol/Cargo.toml b/postgres-protocol/Cargo.toml index 2010e88a..a4716907 100644 --- a/postgres-protocol/Cargo.toml +++ b/postgres-protocol/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "postgres-protocol" -version = "0.6.3" +version = "0.6.4" authors = ["Steven Fackler "] edition = "2018" description = "Low level Postgres protocol APIs" diff --git a/postgres-protocol/src/types/mod.rs b/postgres-protocol/src/types/mod.rs index a595f5a3..5939d9f0 100644 --- a/postgres-protocol/src/types/mod.rs +++ b/postgres-protocol/src/types/mod.rs @@ -1059,3 +1059,19 @@ impl Inet { self.netmask } } + +/// Serializes a Postgres l{tree,query,txtquery} string +#[inline] +pub fn ltree_to_sql(v: &str, buf: &mut BytesMut) { + // A version number is prepended to an Ltree string per spec + buf.put_u8(1); + // Append the rest of the query + buf.put_slice(v.as_bytes()); +} + +/// Deserialize a Postgres l{tree,query,txtquery} string +#[inline] +pub fn ltree_from_sql(buf: &[u8]) -> Result<&str, StdBox> { + // Remove the version number from the front of the string per spec + Ok(str::from_utf8(&buf[1..])?) +} diff --git a/postgres-types/Cargo.toml b/postgres-types/Cargo.toml index 9d470f37..000d71ea 100644 --- a/postgres-types/Cargo.toml +++ b/postgres-types/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "postgres-types" -version = "0.2.2" +version = "0.2.3" authors = ["Steven Fackler "] edition = "2018" license = "MIT/Apache-2.0" @@ -28,7 +28,7 @@ with-time-0_3 = ["time-03"] [dependencies] bytes = "1.0" fallible-iterator = "0.2" -postgres-protocol = { version = "0.6.1", path = "../postgres-protocol" } +postgres-protocol = { version = "0.6.4", path = "../postgres-protocol" } postgres-derive = { version = "0.4.0", optional = true, path = "../postgres-derive" } array-init = { version = "2", optional = true } diff --git a/postgres-types/src/lib.rs b/postgres-types/src/lib.rs index 394f938f..bf7a1cae 100644 --- a/postgres-types/src/lib.rs +++ b/postgres-types/src/lib.rs @@ -594,8 +594,8 @@ impl<'a> FromSql<'a> for &'a [u8] { } impl<'a> FromSql<'a> for String { - fn from_sql(_: &Type, raw: &'a [u8]) -> Result> { - types::text_from_sql(raw).map(ToString::to_string) + fn from_sql(ty: &Type, raw: &'a [u8]) -> Result> { + <&str as FromSql>::from_sql(ty, raw).map(ToString::to_string) } fn accepts(ty: &Type) -> bool { @@ -604,8 +604,8 @@ impl<'a> FromSql<'a> for String { } impl<'a> FromSql<'a> for Box { - fn from_sql(_: &Type, raw: &'a [u8]) -> Result, Box> { - types::text_from_sql(raw) + fn from_sql(ty: &Type, raw: &'a [u8]) -> Result, Box> { + <&str as FromSql>::from_sql(ty, raw) .map(ToString::to_string) .map(String::into_boxed_str) } @@ -616,14 +616,26 @@ impl<'a> FromSql<'a> for Box { } impl<'a> FromSql<'a> for &'a str { - fn from_sql(_: &Type, raw: &'a [u8]) -> Result<&'a str, Box> { - types::text_from_sql(raw) + fn from_sql(ty: &Type, raw: &'a [u8]) -> Result<&'a str, Box> { + match *ty { + ref ty if ( + ty.name() == "ltree" || + ty.name() == "lquery" || + ty.name() == "ltxtquery" + ) => types::ltree_from_sql(raw), + _ => types::text_from_sql(raw) + } } fn accepts(ty: &Type) -> bool { match *ty { Type::VARCHAR | Type::TEXT | Type::BPCHAR | Type::NAME | Type::UNKNOWN => true, - ref ty if ty.name() == "citext" => true, + ref ty if ( + ty.name() == "citext" || + ty.name() == "ltree" || + ty.name() == "lquery" || + ty.name() == "ltxtquery" + ) => true, _ => false, } } @@ -924,15 +936,27 @@ impl ToSql for Vec { } impl<'a> ToSql for &'a str { - fn to_sql(&self, _: &Type, w: &mut BytesMut) -> Result> { - types::text_to_sql(*self, w); + fn to_sql(&self, ty: &Type, w: &mut BytesMut) -> Result> { + match ty { + ref ty if ( + ty.name() == "ltree" || + ty.name() == "lquery" || + ty.name() == "ltxtquery" + ) => types::ltree_to_sql(*self, w), + _ => types::text_to_sql(*self, w) + } Ok(IsNull::No) } fn accepts(ty: &Type) -> bool { match *ty { Type::VARCHAR | Type::TEXT | Type::BPCHAR | Type::NAME | Type::UNKNOWN => true, - ref ty if ty.name() == "citext" => true, + ref ty if ( + ty.name() == "citext" || + ty.name() == "ltree" || + ty.name() == "lquery" || + ty.name() == "ltxtquery" + ) => true, _ => false, } } diff --git a/tokio-postgres/Cargo.toml b/tokio-postgres/Cargo.toml index 94371af5..82e71fb1 100644 --- a/tokio-postgres/Cargo.toml +++ b/tokio-postgres/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tokio-postgres" -version = "0.7.5" +version = "0.7.6" authors = ["Steven Fackler "] edition = "2018" license = "MIT/Apache-2.0" @@ -50,8 +50,8 @@ parking_lot = "0.12" percent-encoding = "2.0" pin-project-lite = "0.2" phf = "0.10" -postgres-protocol = { version = "0.6.1", path = "../postgres-protocol" } -postgres-types = { version = "0.2.2", path = "../postgres-types" } +postgres-protocol = { version = "0.6.4", path = "../postgres-protocol" } +postgres-types = { version = "0.2.3", path = "../postgres-types" } socket2 = "0.4" tokio = { version = "1.0", features = ["io-util"] } tokio-util = { version = "0.7", features = ["codec"] } diff --git a/tokio-postgres/tests/test/types/mod.rs b/tokio-postgres/tests/test/types/mod.rs index 604e2de3..0ec329a4 100644 --- a/tokio-postgres/tests/test/types/mod.rs +++ b/tokio-postgres/tests/test/types/mod.rs @@ -648,3 +648,78 @@ async fn inet() { ) .await; } + +#[tokio::test] +async fn ltree() { + let client = connect("user=postgres").await; + client.execute("CREATE EXTENSION IF NOT EXISTS ltree;", &[]).await.unwrap(); + + test_type("ltree", &[ + (Some("b.c.d".to_owned()), "'b.c.d'"), + (None, "NULL"), + ]).await; +} + +#[tokio::test] +async fn ltree_any() { + let client = connect("user=postgres").await; + client.execute("CREATE EXTENSION IF NOT EXISTS ltree;", &[]).await.unwrap(); + + test_type("ltree[]", &[ + (Some(vec![]), "ARRAY[]"), + (Some(vec!["a.b.c".to_string()]), "ARRAY['a.b.c']"), + (Some(vec!["a.b.c".to_string(), "e.f.g".to_string()]), "ARRAY['a.b.c','e.f.g']"), + (None, "NULL"), + ]).await; +} + +#[tokio::test] +async fn lquery() { + let client = connect("user=postgres").await; + client.execute("CREATE EXTENSION IF NOT EXISTS ltree;", &[]).await.unwrap(); + + test_type("lquery", &[ + (Some("b.c.d".to_owned()), "'b.c.d'"), + (Some("b.c.*".to_owned()), "'b.c.*'"), + (Some("b.*{1,2}.d|e".to_owned()), "'b.*{1,2}.d|e'"), + (None, "NULL"), + ]).await; +} + +#[tokio::test] +async fn lquery_any() { + let client = connect("user=postgres").await; + client.execute("CREATE EXTENSION IF NOT EXISTS ltree;", &[]).await.unwrap(); + + test_type("lquery[]", &[ + (Some(vec![]), "ARRAY[]"), + (Some(vec!["b.c.*".to_string()]), "ARRAY['b.c.*']"), + (Some(vec!["b.c.*".to_string(), "b.*{1,2}.d|e".to_string()]), "ARRAY['b.c.*','b.*{1,2}.d|e']"), + (None, "NULL"), + ]).await; +} + +#[tokio::test] +async fn ltxtquery() { + let client = connect("user=postgres").await; + client.execute("CREATE EXTENSION IF NOT EXISTS ltree;", &[]).await.unwrap(); + + test_type("ltxtquery", &[ + (Some("b & c & d".to_owned()), "'b & c & d'"), + (Some("b@* & !c".to_owned()), "'b@* & !c'"), + (None, "NULL"), + ]).await; +} + +#[tokio::test] +async fn ltxtquery_any() { + let client = connect("user=postgres").await; + client.execute("CREATE EXTENSION IF NOT EXISTS ltree;", &[]).await.unwrap(); + + test_type("ltxtquery[]", &[ + (Some(vec![]), "ARRAY[]"), + (Some(vec!["b & c & d".to_string()]), "ARRAY['b & c & d']"), + (Some(vec!["b & c & d".to_string(), "b@* & !c".to_string()]), "ARRAY['b & c & d','b@* & !c']"), + (None, "NULL"), + ]).await; +}