Skip to content

Commit 6674e8b

Browse files
cemoktraBastian Schubertpaolobarboliniabonander
authored
Basic support for ltree (launchbadge#1696)
* support ltree * add default and push to PgLTree * add more derived for ltree * fix copy/paste * Update sqlx-core/src/error.rs Co-authored-by: Paolo Barbolini <paolo@paolo565.org> * PR fixes * ltree with name instead of OID * custom ltree errors * add pop ot PgLTree * do not hide ltree behind feature flag * bytes() instead of chars() * apply extend_display suggestion * add more functions to PgLTree * fix IntoIter * resolve PR annotation * add tests * remove code from arguments * fix array * fix setup isse * fix(postgres): disable `ltree` tests on Postgres 9.6 Co-authored-by: Bastian Schubert <bastian.schubert@crosscard.com> Co-authored-by: Paolo Barbolini <paolo@paolo565.org> Co-authored-by: Austin Bonander <austin@launchbadge.com>
1 parent 8f41f5b commit 6674e8b

File tree

7 files changed

+211
-2
lines changed

7 files changed

+211
-2
lines changed

.github/workflows/sqlx.yml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,10 @@ jobs:
208208
key: ${{ runner.os }}-postgres-${{ matrix.runtime }}-${{ matrix.tls }}-${{ hashFiles('**/Cargo.lock') }}
209209

210210
- uses: actions-rs/cargo@v1
211+
env:
212+
# FIXME: needed to disable `ltree` tests in Postgres 9.6
213+
# but `PgLTree` should just fall back to text format
214+
RUSTFLAGS: --cfg postgres_${{ matrix.postgres }}
211215
with:
212216
command: build
213217
args: >
@@ -225,6 +229,9 @@ jobs:
225229
--features any,postgres,macros,all-types,runtime-${{ matrix.runtime }}-${{ matrix.tls }}
226230
env:
227231
DATABASE_URL: postgres://postgres:password@localhost:5432/sqlx
232+
# FIXME: needed to disable `ltree` tests in Postgres 9.6
233+
# but `PgLTree` should just fall back to text format
234+
RUSTFLAGS: --cfg postgres_${{ matrix.postgres }}
228235

229236
- uses: actions-rs/cargo@v1
230237
with:
@@ -234,6 +241,9 @@ jobs:
234241
--features any,postgres,macros,migrate,all-types,runtime-${{ matrix.runtime }}-${{ matrix.tls }}
235242
env:
236243
DATABASE_URL: postgres://postgres:password@localhost:5432/sqlx?sslmode=verify-ca&sslrootcert=.%2Ftests%2Fcerts%2Fca.crt
244+
# FIXME: needed to disable `ltree` tests in Postgres 9.6
245+
# but `PgLTree` should just fall back to text format
246+
RUSTFLAGS: --cfg postgres_${{ matrix.postgres }}
237247

238248
mysql:
239249
name: MySQL
Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
use crate::decode::Decode;
2+
use crate::encode::{Encode, IsNull};
3+
use crate::error::BoxDynError;
4+
use crate::postgres::{
5+
PgArgumentBuffer, PgHasArrayType, PgTypeInfo, PgValueFormat, PgValueRef, Postgres,
6+
};
7+
use crate::types::Type;
8+
use std::fmt::{self, Display, Formatter};
9+
use std::io::Write;
10+
use std::ops::Deref;
11+
use std::str::FromStr;
12+
13+
/// Represents ltree specific errors
14+
#[derive(Debug, thiserror::Error)]
15+
#[non_exhaustive]
16+
pub enum PgLTreeParseError {
17+
/// LTree labels can only contain [A-Za-z0-9_]
18+
#[error("ltree label cotains invalid characters")]
19+
InvalidLtreeLabel,
20+
21+
/// LTree version not supported
22+
#[error("ltree version not supported")]
23+
InvalidLtreeVersion,
24+
}
25+
26+
/// Container for a Label Tree (`ltree`) in Postgres.
27+
///
28+
/// See https://www.postgresql.org/docs/current/ltree.html
29+
///
30+
/// ### Note: Requires Postgres 13+
31+
///
32+
/// This integration requires that the `ltree` type support the binary format in the Postgres
33+
/// wire protocol, which only became available in Postgres 13.
34+
/// ([Postgres 13.0 Release Notes, Additional Modules][https://www.postgresql.org/docs/13/release-13.html#id-1.11.6.11.5.14])
35+
///
36+
/// Ideally, SQLx's Postgres driver should support falling back to text format for types
37+
/// which don't have `typsend` and `typrecv` entries in `pg_type`, but that work still needs
38+
/// to be done.
39+
///
40+
/// ### Note: Extension Required
41+
/// The `ltree` extension is not enabled by default in Postgres. You will need to do so explicitly:
42+
///
43+
/// ```ignore
44+
/// CREATE EXTENSION IF NOT EXISTS "ltree";
45+
/// ```
46+
#[derive(Clone, Debug, Default, PartialEq)]
47+
pub struct PgLTree {
48+
labels: Vec<String>,
49+
}
50+
51+
impl PgLTree {
52+
/// creates default/empty ltree
53+
pub fn new() -> Self {
54+
Self::default()
55+
}
56+
57+
/// creates ltree from a [Vec<String>] without checking labels
58+
pub fn new_unchecked(labels: Vec<String>) -> Self {
59+
Self { labels }
60+
}
61+
62+
/// creates ltree from an iterator with checking labels
63+
pub fn from_iter<I, S>(labels: I) -> Result<Self, PgLTreeParseError>
64+
where
65+
S: Into<String>,
66+
I: IntoIterator<Item = S>,
67+
{
68+
let mut ltree = Self::default();
69+
for label in labels {
70+
ltree.push(label.into())?;
71+
}
72+
Ok(ltree)
73+
}
74+
75+
/// push a label to ltree
76+
pub fn push(&mut self, label: String) -> Result<(), PgLTreeParseError> {
77+
if label.len() <= 256
78+
&& label
79+
.bytes()
80+
.all(|c| c.is_ascii_alphabetic() || c.is_ascii_digit() || c == b'_')
81+
{
82+
self.labels.push(label);
83+
Ok(())
84+
} else {
85+
Err(PgLTreeParseError::InvalidLtreeLabel)
86+
}
87+
}
88+
89+
/// pop a label from ltree
90+
pub fn pop(&mut self) -> Option<String> {
91+
self.labels.pop()
92+
}
93+
}
94+
95+
impl IntoIterator for PgLTree {
96+
type Item = String;
97+
type IntoIter = std::vec::IntoIter<Self::Item>;
98+
99+
fn into_iter(self) -> Self::IntoIter {
100+
self.labels.into_iter()
101+
}
102+
}
103+
104+
impl FromStr for PgLTree {
105+
type Err = PgLTreeParseError;
106+
107+
fn from_str(s: &str) -> Result<Self, Self::Err> {
108+
Ok(Self {
109+
labels: s.split('.').map(|s| s.to_owned()).collect(),
110+
})
111+
}
112+
}
113+
114+
impl Display for PgLTree {
115+
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
116+
let mut iter = self.labels.iter();
117+
if let Some(label) = iter.next() {
118+
write!(f, "{}", label)?;
119+
for label in iter {
120+
write!(f, ".{}", label)?;
121+
}
122+
}
123+
Ok(())
124+
}
125+
}
126+
127+
impl Deref for PgLTree {
128+
type Target = [String];
129+
130+
fn deref(&self) -> &Self::Target {
131+
&self.labels
132+
}
133+
}
134+
135+
impl Type<Postgres> for PgLTree {
136+
fn type_info() -> PgTypeInfo {
137+
// Since `ltree` is enabled by an extension, it does not have a stable OID.
138+
PgTypeInfo::with_name("ltree")
139+
}
140+
}
141+
142+
impl PgHasArrayType for PgLTree {
143+
fn array_type_info() -> PgTypeInfo {
144+
PgTypeInfo::with_name("_ltree")
145+
}
146+
}
147+
148+
impl Encode<'_, Postgres> for PgLTree {
149+
fn encode_by_ref(&self, buf: &mut PgArgumentBuffer) -> IsNull {
150+
buf.extend(1i8.to_le_bytes());
151+
write!(buf, "{}", self)
152+
.expect("Display implementation panicked while writing to PgArgumentBuffer");
153+
154+
IsNull::No
155+
}
156+
}
157+
158+
impl<'r> Decode<'r, Postgres> for PgLTree {
159+
fn decode(value: PgValueRef<'r>) -> Result<Self, BoxDynError> {
160+
match value.format() {
161+
PgValueFormat::Binary => {
162+
let bytes = value.as_bytes()?;
163+
let version = i8::from_le_bytes([bytes[0]; 1]);
164+
if version != 1 {
165+
return Err(Box::new(PgLTreeParseError::InvalidLtreeVersion));
166+
}
167+
Ok(Self::from_str(std::str::from_utf8(&bytes[1..])?)?)
168+
}
169+
PgValueFormat::Text => Ok(Self::from_str(value.as_str()?)?),
170+
}
171+
}
172+
}

sqlx-core/src/postgres/types/mod.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,7 @@ mod bytes;
168168
mod float;
169169
mod int;
170170
mod interval;
171+
mod ltree;
171172
mod money;
172173
mod range;
173174
mod record;
@@ -210,6 +211,8 @@ mod bit_vec;
210211

211212
pub use array::PgHasArrayType;
212213
pub use interval::PgInterval;
214+
pub use ltree::PgLTree;
215+
pub use ltree::PgLTreeParseError;
213216
pub use money::PgMoney;
214217
pub use range::PgRange;
215218

sqlx-macros/src/database/postgres.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ impl_database_ext! {
1818

1919
sqlx::postgres::types::PgMoney,
2020

21+
sqlx::postgres::types::PgLTree,
22+
2123
#[cfg(feature = "uuid")]
2224
sqlx::types::Uuid,
2325

sqlx-test/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,7 @@ macro_rules! __test_prepared_type {
158158

159159
$(
160160
let query = format!($sql, $text);
161+
println!("{query}");
161162

162163
let row = sqlx::query(&query)
163164
.bind($value)

tests/postgres/setup.sql

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
-- https://www.postgresql.org/docs/current/ltree.html
2+
CREATE EXTENSION IF NOT EXISTS ltree;
3+
14
-- https://www.postgresql.org/docs/current/sql-createtype.html
25
CREATE TYPE status AS ENUM ('new', 'open', 'closed');
36

tests/postgres/types.rs

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,11 @@
11
extern crate time_ as time;
22

33
use std::ops::Bound;
4-
#[cfg(feature = "decimal")]
5-
use std::str::FromStr;
64

75
use sqlx::postgres::types::{PgInterval, PgMoney, PgRange};
86
use sqlx::postgres::Postgres;
97
use sqlx_test::{test_decode_type, test_prepared_type, test_type};
8+
use std::str::FromStr;
109

1110
test_type!(null<Option<i16>>(Postgres,
1211
"NULL::int2" == None::<i16>
@@ -513,3 +512,22 @@ test_prepared_type!(money<PgMoney>(Postgres, "123.45::money" == PgMoney(12345)))
513512
test_prepared_type!(money_vec<Vec<PgMoney>>(Postgres,
514513
"array[123.45,420.00,666.66]::money[]" == vec![PgMoney(12345), PgMoney(42000), PgMoney(66666)],
515514
));
515+
516+
// FIXME: needed to disable `ltree` tests in Postgres 9.6
517+
// but `PgLTree` should just fall back to text format
518+
#[cfg(postgres_14)]
519+
test_type!(ltree<sqlx::postgres::types::PgLTree>(Postgres,
520+
"'Foo.Bar.Baz.Quux'::ltree" == sqlx::postgres::types::PgLTree::from_str("Foo.Bar.Baz.Quux").unwrap(),
521+
"'Alpha.Beta.Delta.Gamma'::ltree" == sqlx::postgres::types::PgLTree::from_iter(["Alpha", "Beta", "Delta", "Gamma"]).unwrap(),
522+
));
523+
524+
// FIXME: needed to disable `ltree` tests in Postgres 9.6
525+
// but `PgLTree` should just fall back to text format
526+
#[cfg(postgres_14)]
527+
test_type!(ltree_vec<Vec<sqlx::postgres::types::PgLTree>>(Postgres,
528+
"array['Foo.Bar.Baz.Quux', 'Alpha.Beta.Delta.Gamma']::ltree[]" ==
529+
vec![
530+
sqlx::postgres::types::PgLTree::from_str("Foo.Bar.Baz.Quux").unwrap(),
531+
sqlx::postgres::types::PgLTree::from_iter(["Alpha", "Beta", "Delta", "Gamma"]).unwrap()
532+
]
533+
));

0 commit comments

Comments
 (0)