Skip to main content

ogc_cql2/ds/
types.rs

1// SPDX-License-Identifier: Apache-2.0
2
3#![warn(missing_docs)]
4
5//! Enough code to ease integration of PostgreSQL + PostGIS types w/ `sqlx`.
6//!
7
8use crate::{G, wkb::PostGisBinary};
9use jiff::{
10    Span, Zoned,
11    civil::{Date, DateTime},
12    fmt::temporal::DateTimeParser,
13    tz::TimeZone,
14};
15use sqlx::{
16    Decode, Postgres, Type,
17    error::BoxDynError,
18    postgres::{PgHasArrayType, PgTypeInfo, PgValueFormat, PgValueRef},
19};
20
21static PARSER: DateTimeParser = DateTimeParser::new();
22
23// ===== PostGIS geometry type ================================================
24
25impl Type<Postgres> for G {
26    fn type_info() -> PgTypeInfo {
27        PgTypeInfo::with_name("geometry")
28    }
29}
30
31impl PgHasArrayType for G {
32    fn array_type_info() -> PgTypeInfo {
33        PgTypeInfo::with_name("_geometry")
34    }
35}
36
37impl<'r> Decode<'r, Postgres> for G {
38    fn decode(value: PgValueRef<'r>) -> Result<Self, BoxDynError> {
39        match value.format() {
40            PgValueFormat::Binary => {
41                let bytes = value.as_bytes()?;
42                let ewkb = PostGisBinary::try_from(bytes)?;
43                Ok(ewkb.geom())
44            }
45            PgValueFormat::Text => Err("Failed decoding a PostGIS geometry column".into()),
46        }
47    }
48}
49
50// ===== PostgreSQL DATE type =================================================
51
52/// Our representation of a PostgreSQL DATE type to ease w/ sqlx and jiff.
53#[derive(Debug)]
54pub struct PgDate(pub Date);
55
56impl Type<Postgres> for PgDate {
57    fn type_info() -> PgTypeInfo {
58        PgTypeInfo::with_name("date")
59    }
60}
61
62impl PgHasArrayType for PgDate {
63    fn array_type_info() -> PgTypeInfo {
64        PgTypeInfo::with_name("_date")
65    }
66}
67
68impl<'r> Decode<'r, Postgres> for PgDate {
69    fn decode(value: PgValueRef<'r>) -> Result<Self, BoxDynError> {
70        Ok(match value.format() {
71            PgValueFormat::Binary => {
72                // DATE is encoded as number of days since 2000-01-01...
73                let days: i32 = Decode::<Postgres>::decode(value)?;
74                let span = Span::new().try_days(days)?;
75                let pg_epoch = Date::new(2000, 1, 1)?;
76                PgDate(pg_epoch.checked_add(span)?)
77            }
78            PgValueFormat::Text => {
79                let it = PARSER.parse_date(value.as_str()?)?;
80                PgDate(it)
81            }
82        })
83    }
84}
85
86// ===== PostgreSQL TIMESTAMPTZ type ==========================================
87
88/// Our representation of a PostgreSQL TIMESTAMP type to use w/ sqlx and jiff.
89#[derive(Debug)]
90pub struct PgTimestamp(pub Zoned);
91
92impl Type<Postgres> for PgTimestamp {
93    fn type_info() -> PgTypeInfo {
94        PgTypeInfo::with_name("timestamp")
95    }
96}
97
98impl PgHasArrayType for PgTimestamp {
99    fn array_type_info() -> PgTypeInfo {
100        PgTypeInfo::with_name("_timestamp")
101    }
102}
103
104impl<'r> Decode<'r, Postgres> for PgTimestamp {
105    fn decode(value: PgValueRef<'r>) -> Result<Self, BoxDynError> {
106        Ok(match value.format() {
107            PgValueFormat::Binary => {
108                // TIMESTAMP is encoded as number of microseconds since 2000-01-01...
109                let micro_secs: i64 = Decode::<Postgres>::decode(value)?;
110                let span = Span::new().try_microseconds(micro_secs)?;
111                let pg_epoch = DateTime::new(2000, 1, 1, 0, 0, 0, 0)?;
112                let z = pg_epoch.checked_add(span)?.to_zoned(TimeZone::UTC)?;
113                PgTimestamp(z)
114            }
115            PgValueFormat::Text => {
116                let it = format!("{}Z", value.as_str()?).parse()?;
117                PgTimestamp(it)
118            }
119        })
120    }
121}