Skip to main content

meerkat_sqlite/
json_column.rs

1//! Read boundary for UTF-8 JSON payload columns.
2//!
3//! Meerkat writes JSON payload columns as BLOB (`serde_json::to_vec`), but
4//! SQLite column affinity does not rewrite values, so carried stores written
5//! by external hosts can hold the same UTF-8 JSON as TEXT. Both physical
6//! encodings are byte-identical UTF-8 JSON; a typed read boundary accepts
7//! either so one legacy TEXT row cannot fail every read of the table.
8//!
9//! (Moved here from `meerkat-store`, which re-exports it, so every SQLite
10//! store — including ones below `meerkat-store` in the dependency order —
11//! stops re-solving the TEXT-vs-BLOB question.)
12
13use rusqlite::types::{FromSql, FromSqlError, FromSqlResult, ValueRef};
14
15/// UTF-8 JSON payload read from a SQLite column, tolerant of both the
16/// canonical BLOB encoding and legacy TEXT rows.
17pub struct JsonColumnBytes(Vec<u8>);
18
19impl JsonColumnBytes {
20    pub fn into_bytes(self) -> Vec<u8> {
21        self.0
22    }
23}
24
25impl FromSql for JsonColumnBytes {
26    fn column_result(value: ValueRef<'_>) -> FromSqlResult<Self> {
27        match value {
28            ValueRef::Text(bytes) | ValueRef::Blob(bytes) => Ok(Self(bytes.to_vec())),
29            ValueRef::Null | ValueRef::Integer(_) | ValueRef::Real(_) => {
30                Err(FromSqlError::InvalidType)
31            }
32        }
33    }
34}
35
36#[cfg(test)]
37#[allow(clippy::expect_used, clippy::unwrap_used, clippy::panic)]
38mod tests {
39    use super::*;
40    use rusqlite::Connection;
41
42    #[test]
43    fn accepts_text_and_blob_rejects_others() {
44        let conn = Connection::open_in_memory().expect("open");
45        conn.execute_batch(
46            "CREATE TABLE t (v);
47             INSERT INTO t VALUES (CAST('{\"a\":1}' AS BLOB));
48             INSERT INTO t VALUES ('{\"a\":2}');
49             INSERT INTO t VALUES (42);",
50        )
51        .expect("seed");
52        let mut stmt = conn
53            .prepare("SELECT v FROM t ORDER BY rowid")
54            .expect("prepare");
55        let mut rows = stmt.query([]).expect("query");
56
57        let blob_row = rows.next().expect("row").expect("present");
58        let blob: JsonColumnBytes = blob_row.get(0).expect("blob accepted");
59        assert_eq!(blob.into_bytes(), b"{\"a\":1}");
60
61        let text_row = rows.next().expect("row").expect("present");
62        let text: JsonColumnBytes = text_row.get(0).expect("text accepted");
63        assert_eq!(text.into_bytes(), b"{\"a\":2}");
64
65        let int_row = rows.next().expect("row").expect("present");
66        match int_row.get::<_, JsonColumnBytes>(0) {
67            Err(rusqlite::Error::InvalidColumnType(..)) => {}
68            Err(other) => panic!("wrong error for integer column: {other}"),
69            Ok(_) => panic!("integer column must be rejected"),
70        }
71    }
72}