Skip to main content

postgresql/
shape.rs

1//! Compile-time result-shape verification.
2//!
3//! [`RowShape`] describes the columns a target type expects. The `query_as!`
4//! macro compares it against the columns the server reports, at compile time,
5//! so a mismatch is a compile error rather than a runtime decode failure.
6
7use crate::{
8    encode::StaticType,
9    interval::Interval,
10    json::Json,
11    types::{
12        BPCHAROID, BYTEAOID, CIDROID, DATEOID, INETOID, JSONBOID, JSONOID, NAMEOID, Oid, TEXTOID, TIMEOID,
13        TIMESTAMPOID, TIMESTAMPTZOID, TIMETZOID, VARCHAROID, array_oid_checked,
14    },
15};
16
17/// One expected result column.
18#[derive(Debug, Clone, Copy, PartialEq, Eq)]
19pub struct ColumnSpec {
20    /// Expected column name; empty means "any name" (positional match).
21    pub name: &'static str,
22    /// The canonical OID of the Rust type.
23    pub type_oid: Oid,
24    /// Whether the Rust type is nullable (`Option<...>`).
25    pub nullable: bool,
26    /// Whether `nullable` is known. For server-side expression columns the
27    /// nullability cannot be derived, so it is not enforced.
28    pub known: bool,
29}
30
31/// A type whose result columns can be checked at compile time.
32pub trait RowShape {
33    /// The columns this type expects.
34    const SHAPE: &'static [ColumnSpec];
35}
36
37const fn str_eq(a: &str, b: &str) -> bool {
38    let a = a.as_bytes();
39    let b = b.as_bytes();
40    if a.len() != b.len() {
41        return false;
42    }
43    let mut i = 0;
44    while i < a.len() {
45        if a[i] != b[i] {
46            return false;
47        }
48        i += 1;
49    }
50    true
51}
52
53/// Returns `true` if a value with canonical OID `rust_oid` can be decoded from
54/// a column of type `column_oid`.
55///
56/// PostgreSQL is loose about a few equivalences (for example `varchar` and
57/// `text` both decode to `String`), which are accounted for here.
58pub const fn decodes_from(rust_oid: Oid, column_oid: Oid) -> bool {
59    if rust_oid == column_oid {
60        return true;
61    }
62    match rust_oid {
63        TEXTOID => matches!(column_oid, VARCHAROID | BPCHAROID | NAMEOID),
64        TIMESTAMPTZOID => matches!(column_oid, DATEOID | TIMESTAMPOID | TIMEOID | TIMETZOID),
65        JSONBOID => column_oid == JSONOID,
66        INETOID => column_oid == CIDROID,
67        _ => false,
68    }
69}
70
71/// Verifies that a described result set matches the target type's shape.
72///
73/// Evaluated at compile time by the `query_as!` macro; a mismatch panics during
74/// constant evaluation, producing a compile error.
75pub const fn verify_shape(expected: &[ColumnSpec], actual: &[ColumnSpec]) {
76    if expected.len() != actual.len() {
77        panic!("postgresql: the query returns a different number of columns than the target type expects");
78    }
79    let mut i = 0;
80    while i < expected.len() {
81        let e = expected[i];
82        let a = actual[i];
83        if !decodes_from(e.type_oid, a.type_oid) {
84            panic!("postgresql: a result column has a type incompatible with its target field");
85        }
86        if e.nullable != a.nullable && a.known {
87            panic!(
88                "postgresql: a result column's nullability does not match its target field (wrap it in Option or make the column NOT NULL)"
89            );
90        }
91        if !e.name.is_empty() && !str_eq(e.name, a.name) {
92            panic!("postgresql: a result column name does not match its target field name");
93        }
94        i += 1;
95    }
96}
97
98macro_rules! scalar_shape {
99    ($($ty:ty),* $(,)?) => {
100        $(
101            impl RowShape for $ty {
102                const SHAPE: &'static [ColumnSpec] = &[ColumnSpec {
103                    name: "",
104                    type_oid: <$ty as StaticType>::OID,
105                    nullable: <$ty as StaticType>::NULLABLE,
106                    known: true,
107                }];
108            }
109        )*
110    };
111}
112
113scalar_shape!(
114    i16,
115    i32,
116    i64,
117    f32,
118    f64,
119    bool,
120    String,
121    uuid::Uuid,
122    time::DateTime,
123    serde_json::Value,
124    ipnetwork::IpNetwork,
125    Interval,
126);
127
128impl<T: StaticType> RowShape for Option<T> {
129    const SHAPE: &'static [ColumnSpec] = &[ColumnSpec {
130        name: "",
131        type_oid: T::OID,
132        nullable: true,
133        known: true,
134    }];
135}
136
137impl<T: StaticType> RowShape for Vec<T> {
138    const SHAPE: &'static [ColumnSpec] = &[ColumnSpec {
139        name: "",
140        type_oid: array_oid_checked(T::OID),
141        nullable: false,
142        known: true,
143    }];
144}
145
146macro_rules! tuple_shape {
147    ($($name:ident),+) => {
148        impl<$($name: StaticType),+> RowShape for ($($name,)+) {
149            const SHAPE: &'static [ColumnSpec] = &[
150                $(ColumnSpec {
151                    name: "",
152                    type_oid: <$name as StaticType>::OID,
153                    nullable: <$name as StaticType>::NULLABLE,
154                    known: true,
155                },)+
156            ];
157        }
158    };
159}
160
161tuple_shape!(A);
162tuple_shape!(A, B);
163tuple_shape!(A, B, C);
164tuple_shape!(A, B, C, D);
165tuple_shape!(A, B, C, D, E);
166tuple_shape!(A, B, C, D, E, F);
167tuple_shape!(A, B, C, D, E, F, G);
168tuple_shape!(A, B, C, D, E, F, G, H);
169
170impl<T> RowShape for Json<T> {
171    const SHAPE: &'static [ColumnSpec] = &[ColumnSpec {
172        name: "",
173        type_oid: JSONBOID,
174        nullable: false,
175        known: true,
176    }];
177}
178
179// `Vec<u8>` is BYTEA, not an array of `u8` (which has no PostgreSQL type).
180impl RowShape for Vec<u8> {
181    const SHAPE: &'static [ColumnSpec] = &[ColumnSpec {
182        name: "",
183        type_oid: BYTEAOID,
184        nullable: false,
185        known: true,
186    }];
187}