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::{Result, WireError};
6
7/// Validate that `RowDescription` matches our requirements
8///
9/// # Errors
10///
11/// Returns [`WireError::Protocol`] if the message is not a `RowDescription`.
12/// Returns [`WireError::InvalidSchema`] if the description does not have exactly one column
13/// named `"data"` with a JSON or JSONB type OID.
14pub fn validate_row_description(msg: &BackendMessage) -> Result<()> {
15    let fields = match msg {
16        BackendMessage::RowDescription(fields) => fields,
17        _ => return Err(WireError::Protocol("expected RowDescription".into())),
18    };
19
20    // Must have exactly one column
21    if fields.len() != 1 {
22        return Err(WireError::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(WireError::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(WireError::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 [`WireError::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(WireError::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        validate_row_description(&msg)
82            .unwrap_or_else(|e| panic!("expected Ok for valid RowDescription: {e}"));
83    }
84
85    #[test]
86    fn test_wrong_column_name() {
87        let field = FieldDescription {
88            name: "wrong".to_string(),
89            table_oid: 0,
90            column_attr: 0,
91            type_oid: JSON_OID,
92            type_size: -1,
93            type_modifier: -1,
94            format_code: 0,
95        };
96
97        let msg = BackendMessage::RowDescription(vec![field]);
98        let result = validate_row_description(&msg);
99        assert!(
100            matches!(result, Err(WireError::InvalidSchema(_))),
101            "expected InvalidSchema error for wrong column name, got: {result:?}"
102        );
103    }
104
105    #[test]
106    fn test_wrong_type() {
107        let field = FieldDescription {
108            name: "data".to_string(),
109            table_oid: 0,
110            column_attr: 0,
111            type_oid: 23, // INT4
112            type_size: 4,
113            type_modifier: -1,
114            format_code: 0,
115        };
116
117        let msg = BackendMessage::RowDescription(vec![field]);
118        let result = validate_row_description(&msg);
119        assert!(
120            matches!(result, Err(WireError::InvalidSchema(_))),
121            "expected InvalidSchema error for wrong type OID, got: {result:?}"
122        );
123    }
124
125    #[test]
126    fn test_multiple_columns() {
127        let field1 = FieldDescription {
128            name: "data".to_string(),
129            table_oid: 0,
130            column_attr: 0,
131            type_oid: JSON_OID,
132            type_size: -1,
133            type_modifier: -1,
134            format_code: 0,
135        };
136        let field2 = field1.clone();
137
138        let msg = BackendMessage::RowDescription(vec![field1, field2]);
139        let result = validate_row_description(&msg);
140        assert!(
141            matches!(result, Err(WireError::InvalidSchema(_))),
142            "expected InvalidSchema error for multiple columns, got: {result:?}"
143        );
144    }
145}