From 133c251ae5f7a0ee69d694fd08ada6a242b71a15 Mon Sep 17 00:00:00 2001 From: Grant Date: Sun, 14 May 2023 14:09:54 -0400 Subject: [PATCH 1/7] feat(citext): implement citext for postgres --- sqlx-postgres/src/any.rs | 2 + sqlx-postgres/src/type_info.rs | 2 + sqlx-postgres/src/types/citext.rs | 91 +++++++++++++++++++++++++++++++ sqlx-postgres/src/types/mod.rs | 2 + sqlx-postgres/src/types/str.rs | 1 + tests/postgres/types.rs | 12 +++- 6 files changed, 108 insertions(+), 2 deletions(-) create mode 100644 sqlx-postgres/src/types/citext.rs diff --git a/sqlx-postgres/src/any.rs b/sqlx-postgres/src/any.rs index 4d1c593dff..1c5f7ea33b 100644 --- a/sqlx-postgres/src/any.rs +++ b/sqlx-postgres/src/any.rs @@ -13,6 +13,7 @@ use sqlx_core::connection::Connection; use sqlx_core::database::Database; use sqlx_core::describe::Describe; use sqlx_core::executor::Executor; +use sqlx_core::ext::ustr::UStr; use sqlx_core::transaction::TransactionManager; sqlx_core::declare_driver_with_optional_migrate!(DRIVER = Postgres); @@ -179,6 +180,7 @@ impl<'a> TryFrom<&'a PgTypeInfo> for AnyTypeInfo { PgType::Float8 => AnyTypeInfoKind::Double, PgType::Bytea => AnyTypeInfoKind::Blob, PgType::Text => AnyTypeInfoKind::Text, + PgType::DeclareWithName(UStr::Static("citext")) => AnyTypeInfoKind::Text, _ => { return Err(sqlx_core::Error::AnyDriverError( format!( diff --git a/sqlx-postgres/src/type_info.rs b/sqlx-postgres/src/type_info.rs index ae211d0d3a..1c03ea20e0 100644 --- a/sqlx-postgres/src/type_info.rs +++ b/sqlx-postgres/src/type_info.rs @@ -438,6 +438,7 @@ impl PgType { PgType::Int8RangeArray => Oid(3927), PgType::Jsonpath => Oid(4072), PgType::JsonpathArray => Oid(4073), + PgType::Custom(ty) => ty.oid, PgType::DeclareWithOid(oid) => *oid, @@ -855,6 +856,7 @@ impl PgType { PgType::Unknown => None, // There is no `VoidArray` PgType::Void => None, + PgType::Custom(ty) => match &ty.kind { PgTypeKind::Simple => None, PgTypeKind::Pseudo => None, diff --git a/sqlx-postgres/src/types/citext.rs b/sqlx-postgres/src/types/citext.rs new file mode 100644 index 0000000000..8d421ffccf --- /dev/null +++ b/sqlx-postgres/src/types/citext.rs @@ -0,0 +1,91 @@ +use crate::types::array_compatible; +use crate::{PgArgumentBuffer, PgHasArrayType, PgTypeInfo, PgValueRef, Postgres}; +use sqlx_core::decode::Decode; +use sqlx_core::encode::{Encode, IsNull}; +use sqlx_core::error::BoxDynError; +use sqlx_core::types::Type; +use std::fmt; +use std::fmt::{Debug, Display, Formatter}; +use std::ops::Deref; +use std::str::FromStr; + +/// Text type for case insensitive searching in Postgres. +/// +/// See https://www.postgresql.org/docs/current/citext.html +/// +/// ### Note: Extension Required +/// The `citext` extension is not enabled by default in Postgres. You will need to do so explicitly: +/// +/// ```ignore +/// CREATE EXTENSION IF NOT EXISTS "citext"; +/// ``` + +#[derive(Clone, Debug, Default, PartialEq)] +pub struct PgCitext(String); + +impl PgCitext { + pub fn new(s: String) -> Self { + Self(s) + } +} + +impl Type for PgCitext { + fn type_info() -> PgTypeInfo { + // Since `ltree` is enabled by an extension, it does not have a stable OID. + PgTypeInfo::with_name("citext") + } + + fn compatible(ty: &PgTypeInfo) -> bool { + <&str as Type>::compatible(ty) + } +} + +impl Deref for PgCitext { + type Target = str; + + fn deref(&self) -> &Self::Target { + self.0.as_str() + } +} + +impl From for PgCitext { + fn from(value: String) -> Self { + Self::new(value) + } +} + +impl FromStr for PgCitext { + type Err = core::convert::Infallible; + + fn from_str(s: &str) -> Result { + Ok(PgCitext(s.parse()?)) + } +} + +impl Display for PgCitext { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.0) + } +} + +impl PgHasArrayType for PgCitext { + fn array_type_info() -> PgTypeInfo { + PgTypeInfo::with_name("_citext") + } + + fn array_compatible(ty: &PgTypeInfo) -> bool { + array_compatible::<&str>(ty) + } +} + +impl Encode<'_, Postgres> for PgCitext { + fn encode_by_ref(&self, buf: &mut PgArgumentBuffer) -> IsNull { + <&str as Encode>::encode(&**self, buf) + } +} + +impl Decode<'_, Postgres> for PgCitext { + fn decode(value: PgValueRef<'_>) -> Result { + Ok(PgCitext(value.as_str()?.to_owned())) + } +} diff --git a/sqlx-postgres/src/types/mod.rs b/sqlx-postgres/src/types/mod.rs index 8749fe28ba..5e547f42eb 100644 --- a/sqlx-postgres/src/types/mod.rs +++ b/sqlx-postgres/src/types/mod.rs @@ -175,6 +175,7 @@ pub(crate) use sqlx_core::types::{Json, Type}; mod array; mod bool; mod bytes; +mod citext; mod float; mod int; mod interval; @@ -224,6 +225,7 @@ mod mac_address; mod bit_vec; pub use array::PgHasArrayType; +pub use citext::PgCitext; pub use interval::PgInterval; pub use lquery::PgLQuery; pub use lquery::PgLQueryLevel; diff --git a/sqlx-postgres/src/types/str.rs b/sqlx-postgres/src/types/str.rs index 53dda1f446..a0f3f77f24 100644 --- a/sqlx-postgres/src/types/str.rs +++ b/sqlx-postgres/src/types/str.rs @@ -18,6 +18,7 @@ impl Type for str { PgTypeInfo::BPCHAR, PgTypeInfo::VARCHAR, PgTypeInfo::UNKNOWN, + PgTypeInfo::with_name("citext"), ] .contains(ty) } diff --git a/tests/postgres/types.rs b/tests/postgres/types.rs index 2b2de07f0f..44c1421e13 100644 --- a/tests/postgres/types.rs +++ b/tests/postgres/types.rs @@ -2,7 +2,7 @@ extern crate time_ as time; use std::ops::Bound; -use sqlx::postgres::types::{Oid, PgInterval, PgMoney, PgRange}; +use sqlx::postgres::types::{Oid, PgCitext, PgInterval, PgMoney, PgRange}; use sqlx::postgres::Postgres; use sqlx_test::{test_decode_type, test_prepared_type, test_type}; @@ -79,7 +79,7 @@ test_type!(string_vec>(Postgres, == vec!["", "\""], "array['Hello, World', '', 'Goodbye']::text[]" - == vec!["Hello, World", "", "Goodbye"] + == vec!["Hello, World", "", "Goodbye"], )); test_type!(string_array<[String; 3]>(Postgres, @@ -549,6 +549,14 @@ test_prepared_type!(money_vec>(Postgres, "array[123.45,420.00,666.66]::money[]" == vec![PgMoney(12345), PgMoney(42000), PgMoney(66666)], )); +test_prepared_type!(citext_array>(Postgres, + "array['one','two','three']::citext[]" == vec![ + PgCitext::new("one".to_string()), + PgCitext::new("two".to_string()), + PgCitext::new("three".to_string()), + ], +)); + // FIXME: needed to disable `ltree` tests in version that don't have a binary format for it // but `PgLTree` should just fall back to text format #[cfg(any(postgres_14, postgres_15))] From fc13ac100ef67f3724226dd2508f7173381763b3 Mon Sep 17 00:00:00 2001 From: Grant Date: Sun, 14 May 2023 14:39:14 -0400 Subject: [PATCH 2/7] feat(citext): add citext -> String conversion test --- tests/postgres/types.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/postgres/types.rs b/tests/postgres/types.rs index 44c1421e13..7c5f3ea363 100644 --- a/tests/postgres/types.rs +++ b/tests/postgres/types.rs @@ -65,6 +65,7 @@ test_type!(str<&str>(Postgres, "'identifier'::name" == "identifier", "'five'::char(4)" == "five", "'more text'::varchar" == "more text", + "'case insensitive searching'::citext" == "case insensitive searching", )); test_type!(string(Postgres, From e69d813f8a507f7abbd3d48f317a2bf972540c86 Mon Sep 17 00:00:00 2001 From: Grant Date: Sun, 14 May 2023 14:57:11 -0400 Subject: [PATCH 3/7] feat(citext): fix ltree -> citree --- sqlx-postgres/src/types/citext.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sqlx-postgres/src/types/citext.rs b/sqlx-postgres/src/types/citext.rs index 8d421ffccf..aca7f7bde9 100644 --- a/sqlx-postgres/src/types/citext.rs +++ b/sqlx-postgres/src/types/citext.rs @@ -31,7 +31,7 @@ impl PgCitext { impl Type for PgCitext { fn type_info() -> PgTypeInfo { - // Since `ltree` is enabled by an extension, it does not have a stable OID. + // Since `citext` is enabled by an extension, it does not have a stable OID. PgTypeInfo::with_name("citext") } From 032e6c17a1bcdb5d83ff6b3d72d545e3c047679a Mon Sep 17 00:00:00 2001 From: Grant Date: Sun, 14 May 2023 16:18:38 -0400 Subject: [PATCH 4/7] feat(citext): add citext to the setup.sql --- tests/postgres/setup.sql | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/postgres/setup.sql b/tests/postgres/setup.sql index 039e08d86f..70cf6f114e 100644 --- a/tests/postgres/setup.sql +++ b/tests/postgres/setup.sql @@ -1,6 +1,9 @@ -- https://www.postgresql.org/docs/current/ltree.html CREATE EXTENSION IF NOT EXISTS ltree; +-- https://www.postgresql.org/docs/current/citext.html +CREATE EXTENSION IF NOT EXISTS citext; + -- https://www.postgresql.org/docs/current/sql-createtype.html CREATE TYPE status AS ENUM ('new', 'open', 'closed'); From eb4f1bb068b6de0eb7c4dba6ab19e9560b0f6093 Mon Sep 17 00:00:00 2001 From: Austin Bonander Date: Wed, 11 Oct 2023 17:29:19 -0700 Subject: [PATCH 5/7] chore: address nits to #2478 * Rename `PgCitext` to `PgCiText` * Document when use of `PgCiText` is warranted * Document potentially surprising `PartialEq` behavior * Test that the macros consider `CITEXT` to be compatible with `String` and friends --- sqlx-postgres/src/types/citext.rs | 59 +++++++++++++++++++------------ sqlx-postgres/src/types/mod.rs | 2 +- tests/postgres/macros.rs | 25 +++++++++++++ tests/postgres/setup.sql | 4 +++ tests/postgres/types.rs | 10 +++--- 5 files changed, 72 insertions(+), 28 deletions(-) diff --git a/sqlx-postgres/src/types/citext.rs b/sqlx-postgres/src/types/citext.rs index aca7f7bde9..db19ec06fa 100644 --- a/sqlx-postgres/src/types/citext.rs +++ b/sqlx-postgres/src/types/citext.rs @@ -9,9 +9,17 @@ use std::fmt::{Debug, Display, Formatter}; use std::ops::Deref; use std::str::FromStr; -/// Text type for case insensitive searching in Postgres. +/// Case-insensitive text (`CITEXT`) support for Postgres. /// -/// See https://www.postgresql.org/docs/current/citext.html +/// Note that SQLx considers the `CITEXT` type to be compatible with `String` +/// and its various derivatives, so direct usage of this type is generally unnecessary. +/// +/// However, it may be needed, for example, when binding a `CITEXT[]` array, +/// as Postgres will generally not accept a `TEXT[]` array (mapped from `Vec`) in its place. +/// +/// See [the Postgres manual, Appendix F, Section 10][PG.F.10] for details on using `CITEXT`. +/// +/// [PG.F.10]: https://www.postgresql.org/docs/current/citext.html /// /// ### Note: Extension Required /// The `citext` extension is not enabled by default in Postgres. You will need to do so explicitly: @@ -19,17 +27,18 @@ use std::str::FromStr; /// ```ignore /// CREATE EXTENSION IF NOT EXISTS "citext"; /// ``` - +/// +/// ### Note: `PartialEq` is Case-Sensitive +/// This type derives `PartialEq` which forwards to the implementation on `String`, which +/// is case-sensitive. This impl exists mainly for testing. +/// +/// To properly emulate the case-insensitivity of `CITEXT` would require use of locale-aware +/// functions in `libc`, and even then would require querying the locale of the database server +/// and setting it locally, which is unsafe. #[derive(Clone, Debug, Default, PartialEq)] -pub struct PgCitext(String); +pub struct PgCiText(pub String); -impl PgCitext { - pub fn new(s: String) -> Self { - Self(s) - } -} - -impl Type for PgCitext { +impl Type for PgCiText { fn type_info() -> PgTypeInfo { // Since `citext` is enabled by an extension, it does not have a stable OID. PgTypeInfo::with_name("citext") @@ -40,7 +49,7 @@ impl Type for PgCitext { } } -impl Deref for PgCitext { +impl Deref for PgCiText { type Target = str; fn deref(&self) -> &Self::Target { @@ -48,27 +57,33 @@ impl Deref for PgCitext { } } -impl From for PgCitext { +impl From for PgCiText { fn from(value: String) -> Self { - Self::new(value) + Self(value) + } +} + +impl From for String { + fn from(value: PgCiText) -> Self { + value.0 } } -impl FromStr for PgCitext { +impl FromStr for PgCiText { type Err = core::convert::Infallible; fn from_str(s: &str) -> Result { - Ok(PgCitext(s.parse()?)) + Ok(PgCiText(s.parse()?)) } } -impl Display for PgCitext { +impl Display for PgCiText { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - write!(f, "{}", self.0) + f.write_str(&self.0) } } -impl PgHasArrayType for PgCitext { +impl PgHasArrayType for PgCiText { fn array_type_info() -> PgTypeInfo { PgTypeInfo::with_name("_citext") } @@ -78,14 +93,14 @@ impl PgHasArrayType for PgCitext { } } -impl Encode<'_, Postgres> for PgCitext { +impl Encode<'_, Postgres> for PgCiText { fn encode_by_ref(&self, buf: &mut PgArgumentBuffer) -> IsNull { <&str as Encode>::encode(&**self, buf) } } -impl Decode<'_, Postgres> for PgCitext { +impl Decode<'_, Postgres> for PgCiText { fn decode(value: PgValueRef<'_>) -> Result { - Ok(PgCitext(value.as_str()?.to_owned())) + Ok(PgCiText(value.as_str()?.to_owned())) } } diff --git a/sqlx-postgres/src/types/mod.rs b/sqlx-postgres/src/types/mod.rs index 5e547f42eb..30e3b8ffa2 100644 --- a/sqlx-postgres/src/types/mod.rs +++ b/sqlx-postgres/src/types/mod.rs @@ -225,7 +225,7 @@ mod mac_address; mod bit_vec; pub use array::PgHasArrayType; -pub use citext::PgCitext; +pub use citext::PgCiText; pub use interval::PgInterval; pub use lquery::PgLQuery; pub use lquery::PgLQueryLevel; diff --git a/tests/postgres/macros.rs b/tests/postgres/macros.rs index be2271e9a5..8587ac1994 100644 --- a/tests/postgres/macros.rs +++ b/tests/postgres/macros.rs @@ -611,3 +611,28 @@ async fn test_bind_arg_override_wildcard() -> anyhow::Result<()> { Ok(()) } + +#[sqlx_macros::test] +async fn test_to_from_citext() -> anyhow::Result<()> { + // Ensure that the macros consider `CITEXT` to be compatible with `String` and friends + + let mut conn = new::().await?; + + let mut tx = conn.begin().await?; + + let foo_in = "Hello, world!"; + + sqlx::query!("insert into test_citext(foo) values ($1)", foo_in) + .execute(&mut *tx) + .await?; + + let foo_out: String = sqlx::query_scalar!("select foo from test_citext") + .fetch_one(&mut *tx) + .await?; + + assert_eq!(foo_in, foo_out); + + tx.rollback().await?; + + Ok(()) +} diff --git a/tests/postgres/setup.sql b/tests/postgres/setup.sql index 70cf6f114e..9dab4c2bd4 100644 --- a/tests/postgres/setup.sql +++ b/tests/postgres/setup.sql @@ -47,3 +47,7 @@ CREATE TABLE products ( CREATE OR REPLACE PROCEDURE forty_two(INOUT forty_two INT = NULL) LANGUAGE plpgsql AS 'begin forty_two := 42; end;'; + +CREATE TABLE test_citext ( + foo CITEXT NOT NULL +); \ No newline at end of file diff --git a/tests/postgres/types.rs b/tests/postgres/types.rs index 7c5f3ea363..648ec9e598 100644 --- a/tests/postgres/types.rs +++ b/tests/postgres/types.rs @@ -2,7 +2,7 @@ extern crate time_ as time; use std::ops::Bound; -use sqlx::postgres::types::{Oid, PgCitext, PgInterval, PgMoney, PgRange}; +use sqlx::postgres::types::{Oid, PgCiText, PgInterval, PgMoney, PgRange}; use sqlx::postgres::Postgres; use sqlx_test::{test_decode_type, test_prepared_type, test_type}; @@ -550,11 +550,11 @@ test_prepared_type!(money_vec>(Postgres, "array[123.45,420.00,666.66]::money[]" == vec![PgMoney(12345), PgMoney(42000), PgMoney(66666)], )); -test_prepared_type!(citext_array>(Postgres, +test_prepared_type!(citext_array>(Postgres, "array['one','two','three']::citext[]" == vec![ - PgCitext::new("one".to_string()), - PgCitext::new("two".to_string()), - PgCitext::new("three".to_string()), + PgCiText("one".to_string()), + PgCiText("two".to_string()), + PgCiText("three".to_string()), ], )); From 1d67133741c60add65883c60476281a2656c1762 Mon Sep 17 00:00:00 2001 From: Austin Bonander Date: Wed, 11 Oct 2023 17:39:06 -0700 Subject: [PATCH 6/7] doc: add `PgCiText` to `postgres::types` listing --- sqlx-postgres/src/types/citext.rs | 12 ++++++------ sqlx-postgres/src/types/mod.rs | 7 ++++++- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/sqlx-postgres/src/types/citext.rs b/sqlx-postgres/src/types/citext.rs index db19ec06fa..9fc131d7ca 100644 --- a/sqlx-postgres/src/types/citext.rs +++ b/sqlx-postgres/src/types/citext.rs @@ -9,15 +9,15 @@ use std::fmt::{Debug, Display, Formatter}; use std::ops::Deref; use std::str::FromStr; -/// Case-insensitive text (`CITEXT`) support for Postgres. +/// Case-insensitive text (`citext`) support for Postgres. /// -/// Note that SQLx considers the `CITEXT` type to be compatible with `String` +/// Note that SQLx considers the `citext` type to be compatible with `String` /// and its various derivatives, so direct usage of this type is generally unnecessary. /// -/// However, it may be needed, for example, when binding a `CITEXT[]` array, -/// as Postgres will generally not accept a `TEXT[]` array (mapped from `Vec`) in its place. +/// However, it may be needed, for example, when binding a `citext[]` array, +/// as Postgres will generally not accept a `text[]` array (mapped from `Vec`) in its place. /// -/// See [the Postgres manual, Appendix F, Section 10][PG.F.10] for details on using `CITEXT`. +/// See [the Postgres manual, Appendix F, Section 10][PG.F.10] for details on using `citext`. /// /// [PG.F.10]: https://www.postgresql.org/docs/current/citext.html /// @@ -32,7 +32,7 @@ use std::str::FromStr; /// This type derives `PartialEq` which forwards to the implementation on `String`, which /// is case-sensitive. This impl exists mainly for testing. /// -/// To properly emulate the case-insensitivity of `CITEXT` would require use of locale-aware +/// To properly emulate the case-insensitivity of `citext` would require use of locale-aware /// functions in `libc`, and even then would require querying the locale of the database server /// and setting it locally, which is unsafe. #[derive(Clone, Debug, Default, PartialEq)] diff --git a/sqlx-postgres/src/types/mod.rs b/sqlx-postgres/src/types/mod.rs index 30e3b8ffa2..02960a6a09 100644 --- a/sqlx-postgres/src/types/mod.rs +++ b/sqlx-postgres/src/types/mod.rs @@ -11,7 +11,7 @@ //! | `i64` | BIGINT, BIGSERIAL, INT8 | //! | `f32` | REAL, FLOAT4 | //! | `f64` | DOUBLE PRECISION, FLOAT8 | -//! | `&str`, [`String`] | VARCHAR, CHAR(N), TEXT, NAME | +//! | `&str`, [`String`] | VARCHAR, CHAR(N), TEXT, NAME, CITEXT | //! | `&[u8]`, `Vec` | BYTEA | //! | `()` | VOID | //! | [`PgInterval`] | INTERVAL | @@ -19,6 +19,11 @@ //! | [`PgMoney`] | MONEY | //! | [`PgLTree`] | LTREE | //! | [`PgLQuery`] | LQUERY | +//! | [`PgCiText`] | CITEXT1 | +//! +//! 1 SQLx generally considers `CITEXT` to be compatible with `String`, `&str`, etc., +//! but this wrapper type is available for edge cases, such as `CITEXT[]` which Postgres +//! does not consider to be compatible with `TEXT[]`. //! //! ### [`bigdecimal`](https://crates.io/crates/bigdecimal) //! Requires the `bigdecimal` Cargo feature flag. From 42867d7403545814fe6677d3a9e69bb4e0e8c9c3 Mon Sep 17 00:00:00 2001 From: Austin Bonander Date: Wed, 11 Oct 2023 17:43:26 -0700 Subject: [PATCH 7/7] chore: restore missing trailing line break to `tests/postgres/setup.sql` --- tests/postgres/setup.sql | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/postgres/setup.sql b/tests/postgres/setup.sql index 9dab4c2bd4..5a415324d8 100644 --- a/tests/postgres/setup.sql +++ b/tests/postgres/setup.sql @@ -50,4 +50,4 @@ CREATE OR REPLACE PROCEDURE forty_two(INOUT forty_two INT = NULL) CREATE TABLE test_citext ( foo CITEXT NOT NULL -); \ No newline at end of file +);