Skip to content

Commit 4560e93

Browse files
jonasrichardtyt2y3
andauthored
Add postgres range type support (#1007)
## PR Info - Closes #69 ## New Features - [ ] Provide a new type `RangeType` to implement Postgres range type - [ ] Provide range type as an element type of Postgres Array ## Doubts - [ ] Not sure where to implement `hashable-value` features. I put that in the `with_range.rs` - [ ] It implements time ranges with Chrono and Time I left out Jiff now - [ ] Not sure how it plays together with `with-json` - [ ] How to make RangeType from std Rust types (tuple, std range, how to describe exclusive and inclusive bounds?) - [ ] A refactor PR is going on which may affect this branch (#1004) --------- Co-authored-by: Chris Tsang <[email protected]>
1 parent b42ccb0 commit 4560e93

File tree

22 files changed

+391
-49
lines changed

22 files changed

+391
-49
lines changed

Cargo.toml

Lines changed: 28 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -30,16 +30,30 @@ path = "src/lib.rs"
3030
[dependencies]
3131
inherent = "1.0"
3232
sea-query-derive = { version = "1.0.0-rc", path = "sea-query-derive", optional = true }
33-
serde = { version = "1", default-features = false, optional = true, features = ["std", "derive"] }
34-
serde_json = { version = "1", default-features = false, optional = true, features = ["std"] }
35-
chrono = { version = "0.4.27", default-features = false, optional = true, features = ["clock"] }
33+
sea-query-postgres-types = { version = "0.8.0-rc.9", path = "sea-query-postgres-types", optional = true }
34+
serde = { version = "1", default-features = false, optional = true, features = [
35+
"std",
36+
"derive",
37+
] }
38+
serde_json = { version = "1", default-features = false, optional = true, features = [
39+
"std",
40+
] }
41+
chrono = { version = "0.4.27", default-features = false, optional = true, features = [
42+
"clock",
43+
] }
3644
postgres-types = { version = "0", default-features = false, optional = true }
3745
pgvector = { version = "~0.4", default-features = false, optional = true }
3846
rust_decimal = { version = "1", default-features = false, optional = true }
3947
bigdecimal = { version = "0.4", default-features = false, optional = true }
4048
uuid = { version = "1", default-features = false, optional = true }
41-
time = { version = "0.3.36", default-features = false, optional = true, features = ["macros", "formatting"] }
42-
jiff = { version = "0.2.15", default-features = false, optional = true, features = ["std", "perf-inline"] }
49+
time = { version = "0.3.36", default-features = false, optional = true, features = [
50+
"macros",
51+
"formatting",
52+
] }
53+
jiff = { version = "0.2.15", default-features = false, optional = true, features = [
54+
"std",
55+
"perf-inline",
56+
] }
4357
ipnetwork = { version = "0.20", default-features = false, optional = true }
4458
mac_address = { version = "1.1", default-features = false, optional = true }
4559
ordered-float = { version = "4.6", default-features = false, optional = true }
@@ -56,13 +70,21 @@ audit = []
5670
backend-mysql = []
5771
backend-postgres = []
5872
backend-sqlite = []
59-
default = ["derive", "audit", "backend-mysql", "backend-postgres", "backend-sqlite", "itoa"]
73+
default = [
74+
"derive",
75+
"audit",
76+
"backend-mysql",
77+
"backend-postgres",
78+
"backend-sqlite",
79+
"itoa",
80+
]
6081
derive = ["sea-query-derive"]
6182
attr = ["sea-query-derive"]
6283
hashable-value = ["ordered-float"]
6384
postgres-array = []
6485
postgres-vector = ["pgvector"]
6586
postgres-interval = []
87+
postgres-range = ["sea-query-postgres-types"]
6688
serde = [
6789
"dep:serde",
6890
"chrono?/serde",

build-tools/rustclippy.sh

100644100755
File mode changed.

build-tools/rustfmt.sh

100644100755
File mode changed.
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
[package]
2+
name = "sea-query-postgres-types"
3+
version = "0.8.0-rc.9"
4+
authors = ["Richard Jonas <[email protected]>"]
5+
edition = "2024"
6+
description = "Postgres types for using SeaQuery with SQLx"
7+
license = "MIT OR Apache-2.0"
8+
documentation = "https://docs.rs/sea-query"
9+
repository = "https://github.com/SeaQL/sea-query"
10+
categories = ["database"]
11+
keywords = ["database", "sql", "mysql", "postgres", "sqlite"]
12+
rust-version = "1.85.0"
13+
14+
[lib]
15+
16+
[dependencies]
17+
bytes = { version = "1", default-features = false }
18+
chrono = { version = "0.4", default-features = false, optional = true, features = [
19+
"clock",
20+
] }
21+
ordered-float = { version = "4.6", default-features = false, optional = false }
22+
postgres-protocol = { version = "0.6", default-features = false }
23+
postgres-types = { version = "0.2", default-features = false }
24+
serde = { version = "1", default-features = false, optional = false, features = [
25+
"std",
26+
"derive",
27+
] }
28+
serde_json = { version = "1", default-features = false, optional = false, features = [
29+
"std",
30+
] }
31+
time = { version = "0.3.36", default-features = false, optional = true, features = [
32+
"macros",
33+
"formatting",
34+
] }
35+
36+
[features]
37+
hashable-value = []
38+
postgres-range = []
39+
serde = ["chrono?/serde", "time?/serde"]
40+
with-chrono = ["chrono"]
41+
with-time = ["time"]
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
pub mod range;
Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
1+
//#[cfg(feature = "hashable-value")]
2+
use std::hash::{Hash, Hasher};
3+
use std::{
4+
error::Error,
5+
fmt::{Debug, Display},
6+
};
7+
8+
use bytes::BytesMut;
9+
use postgres_protocol::types;
10+
use postgres_types::{IsNull, Kind, ToSql, Type, to_sql_checked};
11+
12+
#[derive(Clone, Debug, Eq, PartialEq, Hash, serde::Serialize, serde::Deserialize)]
13+
//#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
14+
pub enum RangeBound<T: Clone + Display + ToSql> {
15+
Exclusive(T),
16+
Inclusive(T),
17+
Unbounded,
18+
}
19+
20+
#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
21+
//#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
22+
pub enum RangeType {
23+
Int4Range(RangeBound<i32>, RangeBound<i32>),
24+
Int8Range(RangeBound<i64>, RangeBound<i64>),
25+
NumRange(RangeBound<f64>, RangeBound<f64>),
26+
}
27+
28+
impl RangeType {
29+
pub fn empty(&self) -> bool {
30+
matches!(
31+
self,
32+
&RangeType::Int4Range(RangeBound::Unbounded, RangeBound::Unbounded)
33+
| &RangeType::Int8Range(RangeBound::Unbounded, RangeBound::Unbounded)
34+
| &RangeType::NumRange(RangeBound::Unbounded, RangeBound::Unbounded)
35+
)
36+
}
37+
}
38+
39+
impl Default for RangeType {
40+
fn default() -> Self {
41+
Self::Int4Range(RangeBound::Unbounded, RangeBound::Unbounded)
42+
}
43+
}
44+
45+
impl Display for RangeType {
46+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
47+
match self {
48+
RangeType::Int4Range(a, b) => display_range(a, b, f),
49+
RangeType::Int8Range(a, b) => display_range(a, b, f),
50+
RangeType::NumRange(a, b) => display_range(a, b, f),
51+
}
52+
}
53+
}
54+
55+
fn display_range<T: Clone + Display + ToSql>(
56+
a: &RangeBound<T>,
57+
b: &RangeBound<T>,
58+
f: &mut std::fmt::Formatter<'_>,
59+
) -> std::fmt::Result {
60+
match a {
61+
RangeBound::Exclusive(v) => {
62+
f.write_fmt(format_args!("[{v},"))?;
63+
}
64+
RangeBound::Inclusive(v) => {
65+
f.write_fmt(format_args!("({v},"))?;
66+
}
67+
RangeBound::Unbounded => {
68+
f.write_str("(,")?;
69+
}
70+
}
71+
72+
match b {
73+
RangeBound::Exclusive(v) => {
74+
f.write_fmt(format_args!("{v}]"))?;
75+
}
76+
RangeBound::Inclusive(v) => {
77+
f.write_fmt(format_args!("{v})"))?;
78+
}
79+
RangeBound::Unbounded => {
80+
f.write_str(")")?;
81+
}
82+
}
83+
84+
Ok(())
85+
}
86+
87+
// TODO even if I put Hash impl behind feature gate, compilation fails
88+
//#[cfg(feature = "hashable-value")]
89+
impl Hash for RangeType {
90+
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
91+
core::mem::discriminant(self).hash(state);
92+
match self {
93+
RangeType::Int4Range(a, b) => {
94+
a.hash(state);
95+
b.hash(state);
96+
}
97+
RangeType::Int8Range(a, b) => {
98+
a.hash(state);
99+
b.hash(state);
100+
}
101+
RangeType::NumRange(a, b) => {
102+
hash_range_bound(a, state);
103+
hash_range_bound(b, state);
104+
}
105+
}
106+
}
107+
}
108+
109+
//#[cfg(feature = "hashable-value")]
110+
fn hash_range_bound<H: Hasher>(rb: &RangeBound<f64>, state: &mut H) {
111+
match rb {
112+
RangeBound::Exclusive(v) => ordered_float::OrderedFloat(*v).hash(state),
113+
RangeBound::Inclusive(v) => ordered_float::OrderedFloat(*v).hash(state),
114+
RangeBound::Unbounded => (),
115+
}
116+
}
117+
118+
impl ToSql for RangeType {
119+
fn to_sql(
120+
&self,
121+
ty: &postgres_types::Type,
122+
buf: &mut BytesMut,
123+
) -> Result<postgres_types::IsNull, Box<dyn std::error::Error + Sync + Send>>
124+
where
125+
Self: Sized,
126+
{
127+
let element_type = match *ty.kind() {
128+
Kind::Range(ref ty) => ty,
129+
_ => return Err(format!("unexpected type {:?}", ty).into()),
130+
};
131+
132+
if self.empty() {
133+
types::empty_range_to_sql(buf);
134+
} else {
135+
types::range_to_sql(
136+
|buf| match self {
137+
RangeType::Int4Range(lower, _) => bound_to_sql(lower, element_type, buf),
138+
RangeType::Int8Range(lower, _) => bound_to_sql(lower, element_type, buf),
139+
RangeType::NumRange(lower, _) => bound_to_sql(lower, element_type, buf),
140+
},
141+
|buf| match self {
142+
RangeType::Int4Range(_, upper) => bound_to_sql(upper, element_type, buf),
143+
RangeType::Int8Range(_, upper) => bound_to_sql(upper, element_type, buf),
144+
RangeType::NumRange(_, upper) => bound_to_sql(upper, element_type, buf),
145+
},
146+
buf,
147+
)?;
148+
}
149+
150+
Ok(postgres_types::IsNull::No)
151+
}
152+
153+
fn accepts(ty: &postgres_types::Type) -> bool
154+
where
155+
Self: Sized,
156+
{
157+
matches!(ty.kind(), &Kind::Range(_))
158+
}
159+
160+
to_sql_checked!();
161+
}
162+
163+
fn bound_to_sql<T>(
164+
bound: &RangeBound<T>,
165+
ty: &Type,
166+
buf: &mut BytesMut,
167+
) -> Result<types::RangeBound<postgres_protocol::IsNull>, Box<dyn Error + Sync + Send>>
168+
where
169+
T: Clone + Display + ToSql,
170+
{
171+
match bound {
172+
RangeBound::Exclusive(v) => {
173+
let is_null = match v.to_sql(ty, buf)? {
174+
IsNull::Yes => postgres_protocol::IsNull::Yes,
175+
IsNull::No => postgres_protocol::IsNull::No,
176+
};
177+
178+
Ok(types::RangeBound::Exclusive(is_null))
179+
}
180+
RangeBound::Inclusive(v) => {
181+
let is_null = match v.to_sql(ty, buf)? {
182+
IsNull::Yes => postgres_protocol::IsNull::Yes,
183+
IsNull::No => postgres_protocol::IsNull::No,
184+
};
185+
186+
Ok(types::RangeBound::Inclusive(is_null))
187+
}
188+
RangeBound::Unbounded => Ok(types::RangeBound::Unbounded),
189+
}
190+
}

sea-query-postgres/Cargo.toml

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,20 +4,21 @@
44
[package]
55
name = "sea-query-postgres"
66
version = "0.6.0-rc.1"
7-
authors = [ "Ivan Krivosheev <[email protected]>" ]
7+
authors = ["Ivan Krivosheev <[email protected]>"]
88
edition = "2024"
99
description = "Binder traits for connecting sea-query with postgres driver"
1010
license = "MIT OR Apache-2.0"
1111
documentation = "https://docs.rs/sea-query"
1212
repository = "https://github.com/SeaQL/sea-query"
13-
categories = [ "database" ]
14-
keywords = [ "database", "sql", "postgres" ]
13+
categories = ["database"]
14+
keywords = ["database", "sql", "postgres"]
1515
rust-version = "1.85.0"
1616

1717
[lib]
1818

1919
[dependencies]
2020
sea-query = { version = "1.0.0-rc.1", path = "..", default-features = false }
21+
sea-query-postgres-types = { version = "0.8.0-rc.9", path = "../sea-query-postgres-types", default-features = false }
2122
postgres-types = { version = "0.2", default-features = false }
2223
pgvector = { version = "~0.4", default-features = false, optional = true }
2324
bytes = { version = "1", default-features = false }
@@ -36,6 +37,17 @@ with-bigdecimal = ["sea-query/with-bigdecimal", "bigdecimal"]
3637
with-uuid = ["postgres-types/with-uuid-1", "sea-query/with-uuid"]
3738
with-time = ["postgres-types/with-time-0_3", "sea-query/with-time"]
3839
postgres-array = ["postgres-types/array-impls", "sea-query/postgres-array"]
40+
postgres-range = ["sea-query/postgres-range"]
3941
postgres-vector = ["sea-query/postgres-vector", "pgvector/postgres"]
40-
with-ipnetwork = ["postgres-types/with-cidr-0_2", "sea-query/with-ipnetwork", "ipnetwork", "cidr"]
41-
with-mac_address = ["postgres-types/with-eui48-1", "sea-query/with-mac_address", "mac_address", "eui48"]
42+
with-ipnetwork = [
43+
"postgres-types/with-cidr-0_2",
44+
"sea-query/with-ipnetwork",
45+
"ipnetwork",
46+
"cidr",
47+
]
48+
with-mac_address = [
49+
"postgres-types/with-eui48-1",
50+
"sea-query/with-mac_address",
51+
"mac_address",
52+
"eui48",
53+
]

sea-query-postgres/src/lib.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,10 @@ impl ToSql for PostgresValue {
136136
use eui48::MacAddress;
137137
v.map(|v| MacAddress::new(v.bytes())).to_sql(ty, out)
138138
}
139+
#[cfg(feature = "postgres-range")]
140+
Value::Range(None) => Ok(IsNull::Yes),
141+
#[cfg(feature = "postgres-range")]
142+
Value::Range(Some(v)) => v.to_sql(ty, out),
139143
}
140144
}
141145

0 commit comments

Comments
 (0)