Skip to content

Commit 56945d7

Browse files
feat(citext): support postgres citext (launchbadge#2478)
* feat(citext): implement citext for postgres * feat(citext): add citext -> String conversion test * feat(citext): fix ltree -> citree * feat(citext): add citext to the setup.sql * chore: address nits to launchbadge#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 * doc: add `PgCiText` to `postgres::types` listing * chore: restore missing trailing line break to `tests/postgres/setup.sql` --------- Co-authored-by: Austin Bonander <[email protected]>
1 parent 3c2471e commit 56945d7

File tree

8 files changed

+162
-3
lines changed

8 files changed

+162
-3
lines changed

sqlx-postgres/src/any.rs

+2
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ use sqlx_core::connection::Connection;
1313
use sqlx_core::database::Database;
1414
use sqlx_core::describe::Describe;
1515
use sqlx_core::executor::Executor;
16+
use sqlx_core::ext::ustr::UStr;
1617
use sqlx_core::transaction::TransactionManager;
1718

1819
sqlx_core::declare_driver_with_optional_migrate!(DRIVER = Postgres);
@@ -178,6 +179,7 @@ impl<'a> TryFrom<&'a PgTypeInfo> for AnyTypeInfo {
178179
PgType::Float8 => AnyTypeInfoKind::Double,
179180
PgType::Bytea => AnyTypeInfoKind::Blob,
180181
PgType::Text => AnyTypeInfoKind::Text,
182+
PgType::DeclareWithName(UStr::Static("citext")) => AnyTypeInfoKind::Text,
181183
_ => {
182184
return Err(sqlx_core::Error::AnyDriverError(
183185
format!("Any driver does not support the Postgres type {pg_type:?}").into(),

sqlx-postgres/src/type_info.rs

+2
Original file line numberDiff line numberDiff line change
@@ -457,6 +457,7 @@ impl PgType {
457457
PgType::Int8RangeArray => Oid(3927),
458458
PgType::Jsonpath => Oid(4072),
459459
PgType::JsonpathArray => Oid(4073),
460+
460461
PgType::Custom(ty) => ty.oid,
461462

462463
PgType::DeclareWithOid(oid) => *oid,
@@ -874,6 +875,7 @@ impl PgType {
874875
PgType::Unknown => None,
875876
// There is no `VoidArray`
876877
PgType::Void => None,
878+
877879
PgType::Custom(ty) => match &ty.kind {
878880
PgTypeKind::Simple => None,
879881
PgTypeKind::Pseudo => None,

sqlx-postgres/src/types/citext.rs

+106
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
use crate::types::array_compatible;
2+
use crate::{PgArgumentBuffer, PgHasArrayType, PgTypeInfo, PgValueRef, Postgres};
3+
use sqlx_core::decode::Decode;
4+
use sqlx_core::encode::{Encode, IsNull};
5+
use sqlx_core::error::BoxDynError;
6+
use sqlx_core::types::Type;
7+
use std::fmt;
8+
use std::fmt::{Debug, Display, Formatter};
9+
use std::ops::Deref;
10+
use std::str::FromStr;
11+
12+
/// Case-insensitive text (`citext`) support for Postgres.
13+
///
14+
/// Note that SQLx considers the `citext` type to be compatible with `String`
15+
/// and its various derivatives, so direct usage of this type is generally unnecessary.
16+
///
17+
/// However, it may be needed, for example, when binding a `citext[]` array,
18+
/// as Postgres will generally not accept a `text[]` array (mapped from `Vec<String>`) in its place.
19+
///
20+
/// See [the Postgres manual, Appendix F, Section 10][PG.F.10] for details on using `citext`.
21+
///
22+
/// [PG.F.10]: https://www.postgresql.org/docs/current/citext.html
23+
///
24+
/// ### Note: Extension Required
25+
/// The `citext` extension is not enabled by default in Postgres. You will need to do so explicitly:
26+
///
27+
/// ```ignore
28+
/// CREATE EXTENSION IF NOT EXISTS "citext";
29+
/// ```
30+
///
31+
/// ### Note: `PartialEq` is Case-Sensitive
32+
/// This type derives `PartialEq` which forwards to the implementation on `String`, which
33+
/// is case-sensitive. This impl exists mainly for testing.
34+
///
35+
/// To properly emulate the case-insensitivity of `citext` would require use of locale-aware
36+
/// functions in `libc`, and even then would require querying the locale of the database server
37+
/// and setting it locally, which is unsafe.
38+
#[derive(Clone, Debug, Default, PartialEq)]
39+
pub struct PgCiText(pub String);
40+
41+
impl Type<Postgres> for PgCiText {
42+
fn type_info() -> PgTypeInfo {
43+
// Since `citext` is enabled by an extension, it does not have a stable OID.
44+
PgTypeInfo::with_name("citext")
45+
}
46+
47+
fn compatible(ty: &PgTypeInfo) -> bool {
48+
<&str as Type<Postgres>>::compatible(ty)
49+
}
50+
}
51+
52+
impl Deref for PgCiText {
53+
type Target = str;
54+
55+
fn deref(&self) -> &Self::Target {
56+
self.0.as_str()
57+
}
58+
}
59+
60+
impl From<String> for PgCiText {
61+
fn from(value: String) -> Self {
62+
Self(value)
63+
}
64+
}
65+
66+
impl From<PgCiText> for String {
67+
fn from(value: PgCiText) -> Self {
68+
value.0
69+
}
70+
}
71+
72+
impl FromStr for PgCiText {
73+
type Err = core::convert::Infallible;
74+
75+
fn from_str(s: &str) -> Result<Self, Self::Err> {
76+
Ok(PgCiText(s.parse()?))
77+
}
78+
}
79+
80+
impl Display for PgCiText {
81+
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
82+
f.write_str(&self.0)
83+
}
84+
}
85+
86+
impl PgHasArrayType for PgCiText {
87+
fn array_type_info() -> PgTypeInfo {
88+
PgTypeInfo::with_name("_citext")
89+
}
90+
91+
fn array_compatible(ty: &PgTypeInfo) -> bool {
92+
array_compatible::<&str>(ty)
93+
}
94+
}
95+
96+
impl Encode<'_, Postgres> for PgCiText {
97+
fn encode_by_ref(&self, buf: &mut PgArgumentBuffer) -> IsNull {
98+
<&str as Encode<Postgres>>::encode(&**self, buf)
99+
}
100+
}
101+
102+
impl Decode<'_, Postgres> for PgCiText {
103+
fn decode(value: PgValueRef<'_>) -> Result<Self, BoxDynError> {
104+
Ok(PgCiText(value.as_str()?.to_owned()))
105+
}
106+
}

sqlx-postgres/src/types/mod.rs

+8-1
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,19 @@
1111
//! | `i64` | BIGINT, BIGSERIAL, INT8 |
1212
//! | `f32` | REAL, FLOAT4 |
1313
//! | `f64` | DOUBLE PRECISION, FLOAT8 |
14-
//! | `&str`, [`String`] | VARCHAR, CHAR(N), TEXT, NAME |
14+
//! | `&str`, [`String`] | VARCHAR, CHAR(N), TEXT, NAME, CITEXT |
1515
//! | `&[u8]`, `Vec<u8>` | BYTEA |
1616
//! | `()` | VOID |
1717
//! | [`PgInterval`] | INTERVAL |
1818
//! | [`PgRange<T>`](PgRange) | INT8RANGE, INT4RANGE, TSRANGE, TSTZRANGE, DATERANGE, NUMRANGE |
1919
//! | [`PgMoney`] | MONEY |
2020
//! | [`PgLTree`] | LTREE |
2121
//! | [`PgLQuery`] | LQUERY |
22+
//! | [`PgCiText`] | CITEXT<sup>1</sup> |
23+
//!
24+
//! <sup>1</sup> SQLx generally considers `CITEXT` to be compatible with `String`, `&str`, etc.,
25+
//! but this wrapper type is available for edge cases, such as `CITEXT[]` which Postgres
26+
//! does not consider to be compatible with `TEXT[]`.
2227
//!
2328
//! ### [`bigdecimal`](https://crates.io/crates/bigdecimal)
2429
//! Requires the `bigdecimal` Cargo feature flag.
@@ -175,6 +180,7 @@ pub(crate) use sqlx_core::types::{Json, Type};
175180
mod array;
176181
mod bool;
177182
mod bytes;
183+
mod citext;
178184
mod float;
179185
mod int;
180186
mod interval;
@@ -224,6 +230,7 @@ mod mac_address;
224230
mod bit_vec;
225231

226232
pub use array::PgHasArrayType;
233+
pub use citext::PgCiText;
227234
pub use interval::PgInterval;
228235
pub use lquery::PgLQuery;
229236
pub use lquery::PgLQueryLevel;

sqlx-postgres/src/types/str.rs

+1
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ impl Type<Postgres> for str {
1818
PgTypeInfo::BPCHAR,
1919
PgTypeInfo::VARCHAR,
2020
PgTypeInfo::UNKNOWN,
21+
PgTypeInfo::with_name("citext"),
2122
]
2223
.contains(ty)
2324
}

tests/postgres/macros.rs

+25
Original file line numberDiff line numberDiff line change
@@ -611,3 +611,28 @@ async fn test_bind_arg_override_wildcard() -> anyhow::Result<()> {
611611

612612
Ok(())
613613
}
614+
615+
#[sqlx_macros::test]
616+
async fn test_to_from_citext() -> anyhow::Result<()> {
617+
// Ensure that the macros consider `CITEXT` to be compatible with `String` and friends
618+
619+
let mut conn = new::<Postgres>().await?;
620+
621+
let mut tx = conn.begin().await?;
622+
623+
let foo_in = "Hello, world!";
624+
625+
sqlx::query!("insert into test_citext(foo) values ($1)", foo_in)
626+
.execute(&mut *tx)
627+
.await?;
628+
629+
let foo_out: String = sqlx::query_scalar!("select foo from test_citext")
630+
.fetch_one(&mut *tx)
631+
.await?;
632+
633+
assert_eq!(foo_in, foo_out);
634+
635+
tx.rollback().await?;
636+
637+
Ok(())
638+
}

tests/postgres/setup.sql

+7
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
-- https://www.postgresql.org/docs/current/ltree.html
22
CREATE EXTENSION IF NOT EXISTS ltree;
33

4+
-- https://www.postgresql.org/docs/current/citext.html
5+
CREATE EXTENSION IF NOT EXISTS citext;
6+
47
-- https://www.postgresql.org/docs/current/sql-createtype.html
58
CREATE TYPE status AS ENUM ('new', 'open', 'closed');
69

@@ -44,3 +47,7 @@ CREATE TABLE products (
4447

4548
CREATE OR REPLACE PROCEDURE forty_two(INOUT forty_two INT = NULL)
4649
LANGUAGE plpgsql AS 'begin forty_two := 42; end;';
50+
51+
CREATE TABLE test_citext (
52+
foo CITEXT NOT NULL
53+
);

tests/postgres/types.rs

+11-2
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ extern crate time_ as time;
22

33
use std::ops::Bound;
44

5-
use sqlx::postgres::types::{Oid, PgInterval, PgMoney, PgRange};
5+
use sqlx::postgres::types::{Oid, PgCiText, PgInterval, PgMoney, PgRange};
66
use sqlx::postgres::Postgres;
77
use sqlx_test::{test_decode_type, test_prepared_type, test_type};
88

@@ -65,6 +65,7 @@ test_type!(str<&str>(Postgres,
6565
"'identifier'::name" == "identifier",
6666
"'five'::char(4)" == "five",
6767
"'more text'::varchar" == "more text",
68+
"'case insensitive searching'::citext" == "case insensitive searching",
6869
));
6970

7071
test_type!(string<String>(Postgres,
@@ -79,7 +80,7 @@ test_type!(string_vec<Vec<String>>(Postgres,
7980
== vec!["", "\""],
8081

8182
"array['Hello, World', '', 'Goodbye']::text[]"
82-
== vec!["Hello, World", "", "Goodbye"]
83+
== vec!["Hello, World", "", "Goodbye"],
8384
));
8485

8586
test_type!(string_array<[String; 3]>(Postgres,
@@ -550,6 +551,14 @@ test_prepared_type!(money_vec<Vec<PgMoney>>(Postgres,
550551
"array[123.45,420.00,666.66]::money[]" == vec![PgMoney(12345), PgMoney(42000), PgMoney(66666)],
551552
));
552553

554+
test_prepared_type!(citext_array<Vec<PgCiText>>(Postgres,
555+
"array['one','two','three']::citext[]" == vec![
556+
PgCiText("one".to_string()),
557+
PgCiText("two".to_string()),
558+
PgCiText("three".to_string()),
559+
],
560+
));
561+
553562
// FIXME: needed to disable `ltree` tests in version that don't have a binary format for it
554563
// but `PgLTree` should just fall back to text format
555564
#[cfg(any(postgres_14, postgres_15))]

0 commit comments

Comments
 (0)