Skip to main content

fraiseql_wire/json/
validate.rs

1//! Result schema validation
2
3use crate::protocol::{BackendMessage, FieldDescription};
4use crate::util::oid::is_json_oid;
5use crate::{Error, Result};
6
7/// Validate that `RowDescription` matches our requirements
8///
9/// # Errors
10///
11/// Returns `Error::Protocol` if the message is not a `RowDescription`.
12/// Returns `Error::InvalidSchema` if the result does not have exactly one column named
13/// `data` with a json/jsonb type OID.
14pub fn validate_row_description(msg: &BackendMessage) -> Result<()> {
15    let fields = match msg {
16        BackendMessage::RowDescription(fields) => fields,
17        _ => return Err(Error::Protocol("expected RowDescription".into())),
18    };
19
20    // Must have exactly one column
21    if fields.len() != 1 {
22        return Err(Error::InvalidSchema(format!(
23            "expected 1 column, got {}",
24            fields.len()
25        )));
26    }
27
28    let field = &fields[0];
29
30    // Column must be named "data"
31    if field.name != "data" {
32        return Err(Error::InvalidSchema(format!(
33            "expected column named 'data', got '{}'",
34            field.name
35        )));
36    }
37
38    // Type must be json or jsonb
39    if !is_json_oid(field.type_oid) {
40        return Err(Error::InvalidSchema(format!(
41            "expected json/jsonb type, got OID {}",
42            field.type_oid
43        )));
44    }
45
46    Ok(())
47}
48
49/// Extract field description from `RowDescription`
50///
51/// # Errors
52///
53/// Returns `Error::Protocol` if the message is not a `RowDescription`.
54pub fn extract_field_description(msg: &BackendMessage) -> Result<FieldDescription> {
55    let fields = match msg {
56        BackendMessage::RowDescription(fields) => fields,
57        _ => return Err(Error::Protocol("expected RowDescription".into())),
58    };
59
60    Ok(fields[0].clone())
61}
62
63#[cfg(test)]
64mod tests {
65    use super::*;
66    use crate::util::oid::JSON_OID;
67
68    #[test]
69    fn test_valid_row_description() {
70        let field = FieldDescription {
71            name: "data".to_string(),
72            table_oid: 0,
73            column_attr: 0,
74            type_oid: JSON_OID,
75            type_size: -1,
76            type_modifier: -1,
77            format_code: 0,
78        };
79
80        let msg = BackendMessage::RowDescription(vec![field]);
81        assert!(validate_row_description(&msg).is_ok());
82    }
83
84    #[test]
85    fn test_wrong_column_name() {
86        let field = FieldDescription {
87            name: "wrong".to_string(),
88            table_oid: 0,
89            column_attr: 0,
90            type_oid: JSON_OID,
91            type_size: -1,
92            type_modifier: -1,
93            format_code: 0,
94        };
95
96        let msg = BackendMessage::RowDescription(vec![field]);
97        assert!(validate_row_description(&msg).is_err());
98    }
99
100    #[test]
101    fn test_wrong_type() {
102        let field = FieldDescription {
103            name: "data".to_string(),
104            table_oid: 0,
105            column_attr: 0,
106            type_oid: 23, // INT4
107            type_size: 4,
108            type_modifier: -1,
109            format_code: 0,
110        };
111
112        let msg = BackendMessage::RowDescription(vec![field]);
113        assert!(validate_row_description(&msg).is_err());
114    }
115
116    #[test]
117    fn test_multiple_columns() {
118        let field1 = FieldDescription {
119            name: "data".to_string(),
120            table_oid: 0,
121            column_attr: 0,
122            type_oid: JSON_OID,
123            type_size: -1,
124            type_modifier: -1,
125            format_code: 0,
126        };
127        let field2 = field1.clone();
128
129        let msg = BackendMessage::RowDescription(vec![field1, field2]);
130        assert!(validate_row_description(&msg).is_err());
131    }
132}