use crate::protocol::FieldDescription;
use crate::types::PostgresType;
use crate::types::{FromSqlBase, FromSqlBinary, FromSqlText, ToSql};
use serde_json::Value;
use std::error::Error;
#[derive(Debug, Clone, PartialEq)]
pub struct Json(pub Value);
#[derive(Debug, Clone, PartialEq)]
pub struct Jsonb(pub Value);
impl<'a> FromSqlBase<'a> for Value {
fn accepts_postgres_type(oid: i32) -> bool {
oid == PostgresType::JSON.oid || oid == PostgresType::JSONB.oid
}
}
impl<'a> FromSqlBinary<'a> for Value {
fn from_sql_binary(
raw: &'a [u8],
field: &FieldDescription,
) -> Result<Self, Box<dyn Error + Sync + Send>> {
if field.data_type_oid == PostgresType::JSONB.oid {
if raw.is_empty() {
return Err("JSONB data cannot be empty".into());
}
let version = raw[0];
if version != 1 {
return Err(format!("Unsupported JSONB version number: {version}").into());
}
let json_text = &raw[1..];
serde_json::from_slice(json_text).map_err(|e| {
format!(
"Failed to parse JSONB from binary data: {e}. Error occurred when parsing field {field:?}"
)
.into()
})
} else {
serde_json::from_slice(raw).map_err(|e| {
format!(
"Failed to parse JSON from binary data: {e}. Error occurred when parsing field {field:?}"
)
.into()
})
}
}
}
impl<'a> FromSqlText<'a> for Value {
fn from_sql_text(
raw: &'a str,
field: &FieldDescription,
) -> Result<Self, Box<dyn Error + Sync + Send>> {
serde_json::from_str(raw).map_err(|e| {
format!(
"Failed to parse JSON/JSONB from text '{raw}': {e}. Error occurred when parsing field {field:?}"
)
.into()
})
}
}
impl ToSql for Value {
fn to_sql_binary(
&self,
target_buffer: &mut Vec<u8>,
) -> Result<(), Box<dyn Error + Sync + Send>> {
target_buffer.push(1); serde_json::to_writer(target_buffer, self)
.map_err(|e| format!("Failed to serialize JSON/JSONB to binary: {e}").into())
}
}
impl ToSql for Json {
fn to_sql_binary(
&self,
target_buffer: &mut Vec<u8>,
) -> Result<(), Box<dyn Error + Sync + Send>> {
serde_json::to_writer(target_buffer, &self.0)
.map_err(|e| format!("Failed to serialize JSON to binary: {e}").into())
}
}
impl ToSql for Jsonb {
fn to_sql_binary(
&self,
target_buffer: &mut Vec<u8>,
) -> Result<(), Box<dyn Error + Sync + Send>> {
target_buffer.push(1); serde_json::to_writer(target_buffer, &self.0)
.map_err(|e| format!("Failed to serialize JSONB to binary: {e}").into())
}
}
impl<'a> FromSqlBase<'a> for Json {
fn accepts_postgres_type(oid: i32) -> bool {
oid == PostgresType::JSON.oid
}
}
impl<'a> FromSqlBinary<'a> for Json {
fn from_sql_binary(
raw: &'a [u8],
field: &FieldDescription,
) -> Result<Self, Box<dyn Error + Sync + Send>> {
let value = Value::from_sql_binary(raw, field)?;
Ok(Json(value))
}
}
impl<'a> FromSqlText<'a> for Json {
fn from_sql_text(
raw: &'a str,
field: &FieldDescription,
) -> Result<Self, Box<dyn Error + Sync + Send>> {
let value = Value::from_sql_text(raw, field)?;
Ok(Json(value))
}
}
impl<'a> FromSqlBase<'a> for Jsonb {
fn accepts_postgres_type(oid: i32) -> bool {
oid == PostgresType::JSONB.oid
}
}
impl<'a> FromSqlBinary<'a> for Jsonb {
fn from_sql_binary(
raw: &'a [u8],
field: &FieldDescription,
) -> Result<Self, Box<dyn Error + Sync + Send>> {
let value = Value::from_sql_binary(raw, field)?;
Ok(Jsonb(value))
}
}
impl<'a> FromSqlText<'a> for Jsonb {
fn from_sql_text(
raw: &'a str,
field: &FieldDescription,
) -> Result<Self, Box<dyn Error + Sync + Send>> {
let value = Value::from_sql_text(raw, field)?;
Ok(Jsonb(value))
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[cfg(feature = "tokio")]
mod tokio_connection {
use super::*;
use crate::test_helpers::get_settings;
use crate::tokio_connection::new_client;
use tokio::test;
#[test]
async fn test_json_type() {
let mut client = new_client(get_settings()).await.unwrap();
let empty_object = json!({});
let value: Value = client.read_single_value("select '{}'::json;", &[]).await;
assert_eq!(value, empty_object);
let empty_array = json!([]);
let value: Value = client.read_single_value("select '[]'::json;", &[]).await;
assert_eq!(value, empty_array);
let complex_json = json!({
"name": "test",
"age": 30,
"active": true,
"tags": ["rust", "postgresql"],
"metadata": {
"created": "2024-01-15",
"version": 1
}
});
let value: Value = client.read_single_value(
r#"select '{"name":"test","age":30,"active":true,"tags":["rust","postgresql"],"metadata":{"created":"2024-01-15","version":1}}'::json;"#,
&[]
).await;
assert_eq!(value, complex_json);
client.execute_non_query_simple("drop table if exists test_json_table; create table test_json_table(value json);").await.unwrap();
let json_param = Json(complex_json.clone());
client
.execute_non_query("insert into test_json_table values ($1);", &[&json_param])
.await
.unwrap();
let retrieved: Value = client
.read_single_value("select value from test_json_table;", &[])
.await;
assert_eq!(retrieved, complex_json);
let null_value: Option<Value> =
client.read_single_value("select null::json;", &[]).await;
assert_eq!(null_value, None);
}
#[test]
async fn test_json_multiple_values() {
let mut client = new_client(get_settings()).await.unwrap();
client.execute_non_query_simple("drop table if exists test_json_multi; create table test_json_multi(id int, value json);").await.unwrap();
let test_values = vec![
(1, json!({})),
(2, json!([1, 2, 3])),
(3, json!({"test": "value", "number": 42})),
(4, json!(null)),
(5, json!("simple string")),
(6, json!(true)),
(7, json!(123.456)),
];
for (id, json_val) in &test_values {
let json_param = Json(json_val.clone());
client
.execute_non_query(
"insert into test_json_multi values ($1, $2);",
&[id, &json_param],
)
.await
.unwrap();
}
for (expected_id, expected_json) in &test_values {
let retrieved: Value = client
.read_single_value(
"select value from test_json_multi where id = $1;",
&[expected_id],
)
.await;
assert_eq!(&retrieved, expected_json, "Failed for ID {expected_id}");
}
}
#[test]
async fn test_json_escaping_roundtrip() {
let mut client = new_client(get_settings()).await.unwrap();
client.execute_non_query_simple("drop table if exists test_json_escaping; create table test_json_escaping(id int, value json);").await.unwrap();
let escaping_test_cases = vec![
(1, json!({"quote": "He said \"hello\" to me"})),
(2, json!({"backslash": "C:\\Users\\name\\file.txt"})),
(3, json!({"newline": "line1\nline2\nline3"})),
(4, json!({"tab": "col1\tcol2\tcol3"})),
(5, json!({"unicode": "emoji: 😊 and math: ∑"})),
(
6,
json!({"mixed": "Quote: \"text\", Path: C:\\temp\\file\nNext line"}),
),
(
7,
json!({"nested_object": {"inner_quote": "nested \"value\" here"}}),
),
(8, json!(["array", "with \"quotes\"", "and\nnewlines"])),
(9, json!({"control_chars": "\u{0001}\u{0002}\u{0003}"})),
(10, json!({"empty_and_quotes": "", "quotes": "\"\""})),
];
for (id, json_val) in &escaping_test_cases {
let json_param = Json(json_val.clone());
client
.execute_non_query(
"insert into test_json_escaping values ($1, $2);",
&[id, &json_param],
)
.await
.unwrap();
}
for (expected_id, expected_json) in &escaping_test_cases {
let retrieved: Value = client
.read_single_value(
"select value from test_json_escaping where id = $1;",
&[expected_id],
)
.await;
assert_eq!(
&retrieved, expected_json,
"JSON escaping failed for test case ID {expected_id}"
);
}
let complex_case = json!({
"message": "Error: \"file not found\" at C:\\temp\\data.json",
"details": {
"path": "C:\\Users\\john\\Documents\\file with spaces.txt",
"error_code": 404,
"trace": "line1\nline2\nline3"
},
"tags": ["error", "\"critical\"", "needs\tescaping"]
});
let complex_json_param = Json(complex_case.clone());
client
.execute_non_query(
"insert into test_json_escaping values ($1, $2);",
&[&99, &complex_json_param],
)
.await
.unwrap();
let retrieved_complex: Value = client
.read_single_value(
"select value from test_json_escaping where id = $1;",
&[&99],
)
.await;
assert_eq!(
retrieved_complex, complex_case,
"Complex JSON escaping case failed"
);
}
#[test]
async fn test_json_error_handling() {
let mut client = new_client(get_settings()).await.unwrap();
let result = client
.try_read_single_value::<Value>("select '{invalid json'::json;", &[])
.await;
assert!(
result.is_err(),
"Expected PostgreSQL to reject invalid JSON syntax"
);
}
#[test]
async fn test_jsonb_type() {
let mut client = new_client(get_settings()).await.unwrap();
let empty_object = json!({});
let value: Value = client
.read_single_value_dual_mode("select '{}'::jsonb")
.await;
assert_eq!(value, empty_object);
let empty_array = json!([]);
let value: Value = client
.read_single_value_dual_mode("select '[]'::jsonb")
.await;
assert_eq!(value, empty_array);
let complex_jsonb = json!({
"name": "test",
"age": 30,
"active": true,
"tags": ["rust", "postgresql"],
"metadata": {
"created": "2024-01-15",
"version": 1
}
});
let value: Value = client.read_single_value_dual_mode(
r#"select '{"name":"test","age":30,"active":true,"tags":["rust","postgresql"],"metadata":{"created":"2024-01-15","version":1}}'::jsonb"#
).await;
assert_eq!(value, complex_jsonb);
client.execute_non_query_simple("drop table if exists test_jsonb_table; create table test_jsonb_table(value jsonb);").await.unwrap();
let jsonb_param = Jsonb(complex_jsonb.clone());
client
.execute_non_query("insert into test_jsonb_table values ($1);", &[&jsonb_param])
.await
.unwrap();
let retrieved: Value = client
.read_single_value("select value from test_jsonb_table;", &[])
.await;
assert_eq!(retrieved, complex_jsonb);
let null_value: Option<Value> = client
.read_single_value_dual_mode("select null::jsonb")
.await;
assert_eq!(null_value, None);
}
#[test]
async fn test_jsonb_vs_json_differences() {
let mut client = new_client(get_settings()).await.unwrap();
client.execute_non_query_simple("drop table if exists test_jsonb_vs_json; create table test_jsonb_vs_json(id int, json_val json, jsonb_val jsonb);").await.unwrap();
let json_with_spaces = r#"{ "z_last": 3 , "a_first": 1, "middle": 2 }"#;
let json_value: Value = serde_json::from_str(json_with_spaces).unwrap();
let json_param = Json(json_value.clone());
let jsonb_param = Jsonb(json_value.clone());
client
.execute_non_query(
"insert into test_jsonb_vs_json values (1, $1, $2);",
&[&json_param, &jsonb_param],
)
.await
.unwrap();
let json_val: Value = client
.read_single_value("select json_val from test_jsonb_vs_json where id = 1;", &[])
.await;
let jsonb_val: Value = client
.read_single_value(
"select jsonb_val from test_jsonb_vs_json where id = 1;",
&[],
)
.await;
let expected = json!({"z_last": 3, "a_first": 1, "middle": 2});
assert_eq!(json_val, expected);
assert_eq!(jsonb_val, expected);
}
#[test]
async fn test_jsonb_array_support() {
let mut client = new_client(get_settings()).await.unwrap();
client.execute_non_query_simple("drop table if exists test_jsonb_arrays; create table test_jsonb_arrays(value jsonb);").await.unwrap();
let json_array_value = json!([
{"type": "user", "id": 1},
{"type": "admin", "id": 2},
[1, 2, 3],
"simple string",
null
]);
let jsonb_param = Jsonb(json_array_value.clone());
client
.execute_non_query(
"insert into test_jsonb_arrays values ($1);",
&[&jsonb_param],
)
.await
.unwrap();
let retrieved_array: Value = client
.read_single_value("select value from test_jsonb_arrays;", &[])
.await;
assert_eq!(retrieved_array, json_array_value);
}
#[test]
async fn test_jsonb_error_handling() {
let mut client = new_client(get_settings()).await.unwrap();
let result = client
.try_read_single_value_simple::<Value>("select '{invalid json'::jsonb;")
.await;
assert!(
result.is_err(),
"Expected PostgreSQL to reject invalid JSONB syntax"
);
}
#[test]
async fn test_jsonb_version_handling() {
let mut client = new_client(get_settings()).await.unwrap();
client.execute_non_query_simple("drop table if exists test_jsonb_version; create table test_jsonb_version(value jsonb);").await.unwrap();
let test_json = json!({"version_test": true, "data": [1, 2, 3]});
let jsonb_param = Jsonb(test_json.clone());
client
.execute_non_query(
"insert into test_jsonb_version values ($1);",
&[&jsonb_param],
)
.await
.unwrap();
let retrieved: Value = client
.read_single_value("select value from test_jsonb_version;", &[])
.await;
assert_eq!(retrieved, test_json);
}
#[test]
async fn test_jsonb_parameter_binding_types() {
let mut client = new_client(get_settings()).await.unwrap();
client.execute_non_query_simple("drop table if exists test_json_jsonb_params; create table test_json_jsonb_params(id int, json_col json, jsonb_col jsonb);").await.unwrap();
let test_value = json!({
"test": "parameter binding",
"numbers": [1, 2, 3],
"nested": {
"key": "value"
}
});
let json_param = Json(test_value.clone());
let jsonb_param = Jsonb(test_value.clone());
client
.execute_non_query(
"insert into test_json_jsonb_params values ($1, $2, $3);",
&[&1, &json_param, &jsonb_param],
)
.await
.unwrap();
let json_result: Value = client
.read_single_value(
"select json_col from test_json_jsonb_params where id = 1;",
&[],
)
.await;
let jsonb_result: Value = client
.read_single_value(
"select jsonb_col from test_json_jsonb_params where id = 1;",
&[],
)
.await;
assert_eq!(json_result, test_value);
assert_eq!(jsonb_result, test_value);
}
#[test]
async fn test_jsonb_escaping_roundtrip() {
let mut client = new_client(get_settings()).await.unwrap();
client.execute_non_query_simple("drop table if exists test_jsonb_escaping; create table test_jsonb_escaping(id int, value jsonb);").await.unwrap();
let escaping_test_cases = vec![
(1, json!({"quote": "He said \"hello\" to me"})),
(2, json!({"backslash": "C:\\Users\\name\\file.txt"})),
(3, json!({"newline": "line1\nline2\nline3"})),
(4, json!({"tab": "col1\tcol2\tcol3"})),
(5, json!({"unicode": "emoji: 😊 and math: ∑"})),
(
6,
json!({"mixed": "Quote: \"text\", Path: C:\\temp\\file\nNext line"}),
),
(
7,
json!({"nested_object": {"inner_quote": "nested \"value\" here"}}),
),
(8, json!(["array", "with \"quotes\"", "and\nnewlines"])),
(9, json!({"control_chars": "\u{0001}\u{0002}\u{0003}"})),
(10, json!({"empty_and_quotes": "", "quotes": "\"\""})),
];
for (id, json_val) in &escaping_test_cases {
let jsonb_param = Jsonb(json_val.clone());
client
.execute_non_query(
"insert into test_jsonb_escaping values ($1, $2);",
&[id, &jsonb_param],
)
.await
.unwrap();
}
for (expected_id, expected_json) in &escaping_test_cases {
let retrieved: Value = client
.read_single_value(
"select value from test_jsonb_escaping where id = $1;",
&[expected_id],
)
.await;
assert_eq!(
&retrieved, expected_json,
"JSONB escaping failed for test case ID {expected_id}"
);
}
let complex_case = json!({
"message": "Error: \"file not found\" at C:\\temp\\data.json",
"details": {
"path": "C:\\Users\\john\\Documents\\file with spaces.txt",
"error_code": 404,
"trace": "line1\nline2\nline3"
},
"tags": ["error", "\"critical\"", "needs\tescaping"]
});
let complex_jsonb_param = Jsonb(complex_case.clone());
client
.execute_non_query(
"insert into test_jsonb_escaping values ($1, $2);",
&[&99, &complex_jsonb_param],
)
.await
.unwrap();
let retrieved_complex: Value = client
.read_single_value(
"select value from test_jsonb_escaping where id = $1;",
&[&99],
)
.await;
assert_eq!(
retrieved_complex, complex_case,
"Complex JSONB escaping case failed"
);
}
#[test]
async fn test_default_value_behavior() {
let mut client = new_client(get_settings()).await.unwrap();
client.execute_non_query_simple("drop table if exists test_default_behavior; create table test_default_behavior(id int, jsonb_col jsonb);").await.unwrap();
let test_value = json!({
"default_test": true,
"message": "serde_json::Value should default to JSONB format",
"data": [1, 2, 3]
});
client
.execute_non_query(
"insert into test_default_behavior values ($1, $2);",
&[&1, &test_value],
)
.await
.unwrap();
let retrieved: Value = client
.read_single_value(
"select jsonb_col from test_default_behavior where id = 1;",
&[],
)
.await;
assert_eq!(retrieved, test_value);
}
}
}