Skip to content

Commit a706674

Browse files
committed
feat: add ipnet support
1 parent 6c2a29f commit a706674

File tree

21 files changed

+294
-18
lines changed

21 files changed

+294
-18
lines changed

Cargo.lock

+8
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

+3
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ _unstable-all-types = [
6868
"json",
6969
"time",
7070
"chrono",
71+
"ipnet",
7172
"ipnetwork",
7273
"mac_address",
7374
"uuid",
@@ -116,6 +117,7 @@ json = ["sqlx-macros?/json", "sqlx-mysql?/json", "sqlx-postgres?/json", "sqlx-sq
116117
bigdecimal = ["sqlx-core/bigdecimal", "sqlx-macros?/bigdecimal", "sqlx-mysql?/bigdecimal", "sqlx-postgres?/bigdecimal"]
117118
bit-vec = ["sqlx-core/bit-vec", "sqlx-macros?/bit-vec", "sqlx-postgres?/bit-vec"]
118119
chrono = ["sqlx-core/chrono", "sqlx-macros?/chrono", "sqlx-mysql?/chrono", "sqlx-postgres?/chrono", "sqlx-sqlite?/chrono"]
120+
ipnet = ["sqlx-core/ipnet", "sqlx-macros?/ipnet", "sqlx-postgres?/ipnet"]
119121
ipnetwork = ["sqlx-core/ipnetwork", "sqlx-macros?/ipnetwork", "sqlx-postgres?/ipnetwork"]
120122
mac_address = ["sqlx-core/mac_address", "sqlx-macros?/mac_address", "sqlx-postgres?/mac_address"]
121123
rust_decimal = ["sqlx-core/rust_decimal", "sqlx-macros?/rust_decimal", "sqlx-mysql?/rust_decimal", "sqlx-postgres?/rust_decimal"]
@@ -142,6 +144,7 @@ sqlx = { version = "=0.8.3", path = ".", default-features = false }
142144
bigdecimal = "0.4.0"
143145
bit-vec = "0.6.3"
144146
chrono = { version = "0.4.34", default-features = false, features = ["std", "clock"] }
147+
ipnet = "2.3.0"
145148
ipnetwork = "0.20.0"
146149
mac_address = "1.1.5"
147150
rust_decimal = { version = "1.26.1", default-features = false, features = ["std"] }

README.md

+2
Original file line numberDiff line numberDiff line change
@@ -220,6 +220,8 @@ be removed in the future.
220220

221221
- `rust_decimal`: Add support for `NUMERIC` using the `rust_decimal` crate.
222222

223+
- `ipnet`: Add support for `INET` and `CIDR` (in postgres) using the `ipnet` crate.
224+
223225
- `ipnetwork`: Add support for `INET` and `CIDR` (in postgres) using the `ipnetwork` crate.
224226

225227
- `json`: Add support for `JSON` and `JSONB` (in postgres) using the `serde_json` crate.

sqlx-core/Cargo.toml

+1
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ bit-vec = { workspace = true, optional = true }
4949
bigdecimal = { workspace = true, optional = true }
5050
rust_decimal = { workspace = true, optional = true }
5151
time = { workspace = true, optional = true }
52+
ipnet = { workspace = true, optional = true }
5253
ipnetwork = { workspace = true, optional = true }
5354
mac_address = { workspace = true, optional = true }
5455
uuid = { workspace = true, optional = true }

sqlx-core/src/types/mod.rs

+7
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,13 @@ pub use bigdecimal::BigDecimal;
6767
#[doc(no_inline)]
6868
pub use rust_decimal::Decimal;
6969

70+
#[cfg(feature = "ipnet")]
71+
#[cfg_attr(docsrs, doc(cfg(feature = "ipnet")))]
72+
pub mod ipnet {
73+
#[doc(no_inline)]
74+
pub use ipnet::{IpNet, Ipv4Net, Ipv6Net};
75+
}
76+
7077
#[cfg(feature = "ipnetwork")]
7178
#[cfg_attr(docsrs, doc(cfg(feature = "ipnetwork")))]
7279
pub mod ipnetwork {

sqlx-macros-core/Cargo.toml

+1
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ json = ["sqlx-core/json", "sqlx-mysql?/json", "sqlx-postgres?/json", "sqlx-sqlit
3838
bigdecimal = ["sqlx-core/bigdecimal", "sqlx-mysql?/bigdecimal", "sqlx-postgres?/bigdecimal"]
3939
bit-vec = ["sqlx-core/bit-vec", "sqlx-postgres?/bit-vec"]
4040
chrono = ["sqlx-core/chrono", "sqlx-mysql?/chrono", "sqlx-postgres?/chrono", "sqlx-sqlite?/chrono"]
41+
ipnet = ["sqlx-core/ipnet", "sqlx-postgres?/ipnet"]
4142
ipnetwork = ["sqlx-core/ipnetwork", "sqlx-postgres?/ipnetwork"]
4243
mac_address = ["sqlx-core/mac_address", "sqlx-postgres?/mac_address"]
4344
rust_decimal = ["sqlx-core/rust_decimal", "sqlx-mysql?/rust_decimal", "sqlx-postgres?/rust_decimal"]

sqlx-macros/Cargo.toml

+1
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ sqlite-unbundled = ["sqlx-macros-core/sqlite-unbundled"]
3737
bigdecimal = ["sqlx-macros-core/bigdecimal"]
3838
bit-vec = ["sqlx-macros-core/bit-vec"]
3939
chrono = ["sqlx-macros-core/chrono"]
40+
ipnet = ["sqlx-macros-core/ipnet"]
4041
ipnetwork = ["sqlx-macros-core/ipnetwork"]
4142
mac_address = ["sqlx-macros-core/mac_address"]
4243
rust_decimal = ["sqlx-macros-core/rust_decimal"]

sqlx-postgres/Cargo.toml

+2
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ offline = ["sqlx-core/offline"]
1919
bigdecimal = ["dep:bigdecimal", "dep:num-bigint", "sqlx-core/bigdecimal"]
2020
bit-vec = ["dep:bit-vec", "sqlx-core/bit-vec"]
2121
chrono = ["dep:chrono", "sqlx-core/chrono"]
22+
ipnet = ["dep:ipnet", "sqlx-core/ipnet"]
2223
ipnetwork = ["dep:ipnetwork", "sqlx-core/ipnetwork"]
2324
mac_address = ["dep:mac_address", "sqlx-core/mac_address"]
2425
rust_decimal = ["dep:rust_decimal", "rust_decimal/maths", "sqlx-core/rust_decimal"]
@@ -43,6 +44,7 @@ sha2 = { version = "0.10.0", default-features = false }
4344
bigdecimal = { workspace = true, optional = true }
4445
bit-vec = { workspace = true, optional = true }
4546
chrono = { workspace = true, optional = true }
47+
ipnet = { workspace = true, optional = true }
4648
ipnetwork = { workspace = true, optional = true }
4749
mac_address = { workspace = true, optional = true }
4850
rust_decimal = { workspace = true, optional = true }

sqlx-postgres/src/type_checking.rs

+6
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,9 @@ impl_type_checking!(
7777
#[cfg(feature = "rust_decimal")]
7878
sqlx::types::Decimal,
7979

80+
#[cfg(all(feature = "ipnet", not(feature = "ipnetwork")))]
81+
sqlx::types::ipnet::IpNet,
82+
8083
#[cfg(feature = "ipnetwork")]
8184
sqlx::types::ipnetwork::IpNetwork,
8285

@@ -138,6 +141,9 @@ impl_type_checking!(
138141
#[cfg(feature = "rust_decimal")]
139142
Vec<sqlx::types::Decimal> | &[sqlx::types::Decimal],
140143

144+
#[cfg(all(feature = "ipnet", not(feature = "ipnetwork")))]
145+
Vec<sqlx::types::ipnet::IpNet> | &[sqlx::types::ipnet::IpNet],
146+
141147
#[cfg(feature = "ipnetwork")]
142148
Vec<sqlx::types::ipnetwork::IpNetwork> | &[sqlx::types::ipnetwork::IpNetwork],
143149

sqlx-postgres/src/type_info.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -256,7 +256,7 @@ impl PgTypeInfo {
256256
]
257257
.contains(self)
258258
{
259-
Some("ipnetwork")
259+
Some("ipnet")
260260
} else if [PgTypeInfo::MACADDR].contains(self) {
261261
Some("mac_address")
262262
} else if [PgTypeInfo::NUMERIC, PgTypeInfo::NUMERIC_ARRAY].contains(self) {
+62
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
use std::net::IpAddr;
2+
3+
use ipnet::IpNet;
4+
5+
use crate::decode::Decode;
6+
use crate::encode::{Encode, IsNull};
7+
use crate::error::BoxDynError;
8+
use crate::types::Type;
9+
use crate::{PgArgumentBuffer, PgHasArrayType, PgTypeInfo, PgValueRef, Postgres};
10+
11+
impl Type<Postgres> for IpAddr
12+
where
13+
IpNet: Type<Postgres>,
14+
{
15+
fn type_info() -> PgTypeInfo {
16+
IpNet::type_info()
17+
}
18+
19+
fn compatible(ty: &PgTypeInfo) -> bool {
20+
IpNet::compatible(ty)
21+
}
22+
}
23+
24+
impl PgHasArrayType for IpAddr {
25+
fn array_type_info() -> PgTypeInfo {
26+
<IpNet as PgHasArrayType>::array_type_info()
27+
}
28+
29+
fn array_compatible(ty: &PgTypeInfo) -> bool {
30+
<IpNet as PgHasArrayType>::array_compatible(ty)
31+
}
32+
}
33+
34+
impl<'db> Encode<'db, Postgres> for IpAddr
35+
where
36+
IpNet: Encode<'db, Postgres>,
37+
{
38+
fn encode_by_ref(&self, buf: &mut PgArgumentBuffer) -> Result<IsNull, BoxDynError> {
39+
IpNet::from(*self).encode_by_ref(buf)
40+
}
41+
42+
fn size_hint(&self) -> usize {
43+
IpNet::from(*self).size_hint()
44+
}
45+
}
46+
47+
impl<'db> Decode<'db, Postgres> for IpAddr
48+
where
49+
IpNet: Decode<'db, Postgres>,
50+
{
51+
fn decode(value: PgValueRef<'db>) -> Result<Self, BoxDynError> {
52+
let ipnet = IpNet::decode(value)?;
53+
54+
if matches!(ipnet, IpNet::V4(_)) && ipnet.prefix_len() != 32
55+
|| matches!(ipnet, IpNet::V6(_)) && ipnet.prefix_len() != 128
56+
{
57+
Err("lossy decode from inet/cidr")?
58+
}
59+
60+
Ok(ipnet.addr())
61+
}
62+
}
+123
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
use std::net::{Ipv4Addr, Ipv6Addr};
2+
3+
#[cfg(feature = "ipnet")]
4+
use ipnet::{IpNet, Ipv4Net, Ipv6Net};
5+
6+
use crate::decode::Decode;
7+
use crate::encode::{Encode, IsNull};
8+
use crate::error::BoxDynError;
9+
use crate::types::Type;
10+
use crate::{PgArgumentBuffer, PgHasArrayType, PgTypeInfo, PgValueFormat, PgValueRef, Postgres};
11+
12+
// https://github.com/postgres/postgres/blob/574925bfd0a8175f6e161936ea11d9695677ba09/src/include/utils/inet.h#L39
13+
14+
// Technically this is a magic number here but it doesn't make sense to drag in the whole of `libc`
15+
// just for one constant.
16+
const PGSQL_AF_INET: u8 = 2; // AF_INET
17+
const PGSQL_AF_INET6: u8 = PGSQL_AF_INET + 1;
18+
19+
impl Type<Postgres> for IpNet {
20+
fn type_info() -> PgTypeInfo {
21+
PgTypeInfo::INET
22+
}
23+
24+
fn compatible(ty: &PgTypeInfo) -> bool {
25+
*ty == PgTypeInfo::CIDR || *ty == PgTypeInfo::INET
26+
}
27+
}
28+
29+
impl PgHasArrayType for IpNet {
30+
fn array_type_info() -> PgTypeInfo {
31+
PgTypeInfo::INET_ARRAY
32+
}
33+
34+
fn array_compatible(ty: &PgTypeInfo) -> bool {
35+
*ty == PgTypeInfo::CIDR_ARRAY || *ty == PgTypeInfo::INET_ARRAY
36+
}
37+
}
38+
39+
impl Encode<'_, Postgres> for IpNet {
40+
fn encode_by_ref(&self, buf: &mut PgArgumentBuffer) -> Result<IsNull, BoxDynError> {
41+
// https://github.com/postgres/postgres/blob/574925bfd0a8175f6e161936ea11d9695677ba09/src/backend/utils/adt/network.c#L293
42+
// https://github.com/postgres/postgres/blob/574925bfd0a8175f6e161936ea11d9695677ba09/src/backend/utils/adt/network.c#L271
43+
44+
match self {
45+
IpNet::V4(net) => {
46+
buf.push(PGSQL_AF_INET); // ip_family
47+
buf.push(net.prefix_len()); // ip_bits
48+
buf.push(0); // is_cidr
49+
buf.push(4); // nb (number of bytes)
50+
buf.extend_from_slice(&net.addr().octets()) // address
51+
}
52+
53+
IpNet::V6(net) => {
54+
buf.push(PGSQL_AF_INET6); // ip_family
55+
buf.push(net.prefix_len()); // ip_bits
56+
buf.push(0); // is_cidr
57+
buf.push(16); // nb (number of bytes)
58+
buf.extend_from_slice(&net.addr().octets()); // address
59+
}
60+
}
61+
62+
Ok(IsNull::No)
63+
}
64+
65+
fn size_hint(&self) -> usize {
66+
match self {
67+
IpNet::V4(_) => 8,
68+
IpNet::V6(_) => 20,
69+
}
70+
}
71+
}
72+
73+
impl Decode<'_, Postgres> for IpNet {
74+
fn decode(value: PgValueRef<'_>) -> Result<Self, BoxDynError> {
75+
let bytes = match value.format() {
76+
PgValueFormat::Binary => value.as_bytes()?,
77+
PgValueFormat::Text => {
78+
return Ok(value.as_str()?.parse()?);
79+
}
80+
};
81+
82+
if bytes.len() >= 8 {
83+
let family = bytes[0];
84+
let prefix = bytes[1];
85+
let _is_cidr = bytes[2] != 0;
86+
let len = bytes[3];
87+
88+
match family {
89+
PGSQL_AF_INET => {
90+
if bytes.len() == 8 && len == 4 {
91+
let inet = Ipv4Net::new(
92+
Ipv4Addr::new(bytes[4], bytes[5], bytes[6], bytes[7]),
93+
prefix,
94+
)?;
95+
96+
return Ok(IpNet::V4(inet));
97+
}
98+
}
99+
100+
PGSQL_AF_INET6 => {
101+
if bytes.len() == 20 && len == 16 {
102+
let inet = Ipv6Net::new(
103+
Ipv6Addr::from([
104+
bytes[4], bytes[5], bytes[6], bytes[7], bytes[8], bytes[9],
105+
bytes[10], bytes[11], bytes[12], bytes[13], bytes[14], bytes[15],
106+
bytes[16], bytes[17], bytes[18], bytes[19],
107+
]),
108+
prefix,
109+
)?;
110+
111+
return Ok(IpNet::V6(inet));
112+
}
113+
}
114+
115+
_ => {
116+
return Err(format!("unknown ip family {family}").into());
117+
}
118+
}
119+
}
120+
121+
Err("invalid data received when expecting an INET".into())
122+
}
123+
}

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

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
mod ipaddr;
2+
3+
// Parent module is named after the `ipnet` crate, this is named after the `IpNet` type.
4+
#[allow(clippy::module_inception)]
5+
mod ipnet;
File renamed without changes.
+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
// Prefer `ipnet` over `ipnetwork`, as it is more featureful and more widely used.
2+
#[cfg(not(feature = "ipnet"))]
3+
mod ipaddr;
4+
5+
// Parent module is named after the `ipnetwork` crate, this is named after the `IpNetwork` type.
6+
#[allow(clippy::module_inception)]
7+
mod ipnetwork;

sqlx-postgres/src/types/mod.rs

+18-7
Original file line numberDiff line numberDiff line change
@@ -81,20 +81,31 @@
8181
//! |---------------------------------------|------------------------------------------------------|
8282
//! | `uuid::Uuid` | UUID |
8383
//!
84-
//! ### [`ipnetwork`](https://crates.io/crates/ipnetwork)
84+
//! ### [`ipnet`](https://crates.io/crates/ipnet)
8585
//!
86-
//! Requires the `ipnetwork` Cargo feature flag.
86+
//! Requires the `ipnet` Cargo feature flag (takes precedence over `ipnetwork` if both are used).
8787
//!
8888
//! | Rust type | Postgres type(s) |
8989
//! |---------------------------------------|------------------------------------------------------|
90-
//! | `ipnetwork::IpNetwork` | INET, CIDR |
90+
//! | `ipnet::IpNet` | INET, CIDR |
9191
//! | `std::net::IpAddr` | INET, CIDR |
9292
//!
9393
//! Note that because `IpAddr` does not support network prefixes, it is an error to attempt to decode
9494
//! an `IpAddr` from a `INET` or `CIDR` value with a network prefix smaller than the address' full width:
9595
//! `/32` for IPv4 addresses and `/128` for IPv6 addresses.
9696
//!
97-
//! `IpNetwork` does not have this limitation.
97+
//! `IpNet` does not have this limitation.
98+
//!
99+
//! ### [`ipnetwork`](https://crates.io/crates/ipnetwork)
100+
//!
101+
//! Requires the `ipnetwork` Cargo feature flag.
102+
//!
103+
//! | Rust type | Postgres type(s) |
104+
//! |---------------------------------------|------------------------------------------------------|
105+
//! | `ipnetwork::IpNetwork` | INET, CIDR |
106+
//! | `std::net::IpAddr` | INET, CIDR |
107+
//!
108+
//! The same `IpAddr` limitation for smaller network prefixes applies as with `ipnet`.
98109
//!
99110
//! ### [`mac_address`](https://crates.io/crates/mac_address)
100111
//!
@@ -244,11 +255,11 @@ mod time;
244255
#[cfg(feature = "uuid")]
245256
mod uuid;
246257

247-
#[cfg(feature = "ipnetwork")]
248-
mod ipnetwork;
258+
#[cfg(feature = "ipnet")]
259+
mod ipnet;
249260

250261
#[cfg(feature = "ipnetwork")]
251-
mod ipaddr;
262+
mod ipnetwork;
252263

253264
#[cfg(feature = "mac_address")]
254265
mod mac_address;

0 commit comments

Comments
 (0)