use serde::{Deserialize, Serialize};
use crate::value::Value;
use super::auth::AuthResponse;
use super::opcodes::ResponseStatus;
use super::request_fields::RequestFields;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct NativeRequest {
pub op: super::opcodes::OpCode,
pub seq: u64,
#[serde(flatten)]
pub fields: RequestFields,
}
impl zerompk::ToMessagePack for NativeRequest {
fn write<W: zerompk::Write>(&self, writer: &mut W) -> zerompk::Result<()> {
writer.write_array_len(3)?;
self.op.write(writer)?;
writer.write_u64(self.seq)?;
self.fields.write(writer)
}
}
impl<'a> zerompk::FromMessagePack<'a> for NativeRequest {
fn read<R: zerompk::Read<'a>>(reader: &mut R) -> zerompk::Result<Self> {
let len = reader.read_array_len()?;
if len != 3 {
return Err(zerompk::Error::ArrayLengthMismatch {
expected: 3,
actual: len,
});
}
let op = super::opcodes::OpCode::read(reader)?;
let seq = reader.read_u64()?;
let fields = RequestFields::read(reader)?;
Ok(Self { op, seq, fields })
}
}
#[derive(
Debug, Clone, Serialize, Deserialize, zerompk::ToMessagePack, zerompk::FromMessagePack,
)]
#[msgpack(map)]
pub struct NativeResponse {
pub seq: u64,
pub status: ResponseStatus,
#[serde(skip_serializing_if = "Option::is_none")]
pub columns: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub rows: Option<Vec<Vec<Value>>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub rows_affected: Option<u64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[msgpack(default)]
pub command: Option<String>,
pub watermark_lsn: u64,
#[serde(skip_serializing_if = "Option::is_none")]
pub error: Option<ErrorPayload>,
#[serde(skip_serializing_if = "Option::is_none")]
pub auth: Option<AuthResponse>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
#[msgpack(default)]
pub warnings: Vec<String>,
}
#[derive(
Debug, Clone, Serialize, Deserialize, zerompk::ToMessagePack, zerompk::FromMessagePack,
)]
#[msgpack(map)]
pub struct ErrorPayload {
pub code: String,
pub message: String,
#[serde(default, skip_serializing_if = "is_zero")]
#[msgpack(default)]
pub ndb_code: u16,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[msgpack(default)]
pub details: Option<crate::error::ErrorDetails>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[msgpack(default)]
pub cause: Option<super::error_cause::ErrorCausePayload>,
}
fn is_zero(code: &u16) -> bool {
*code == 0
}
impl NativeResponse {
pub fn ok(seq: u64) -> Self {
Self {
seq,
status: ResponseStatus::Ok,
columns: None,
rows: None,
rows_affected: None,
command: None,
watermark_lsn: 0,
error: None,
auth: None,
warnings: Vec::new(),
}
}
pub fn from_query_result(seq: u64, qr: crate::result::QueryResult, lsn: u64) -> Self {
Self {
seq,
status: ResponseStatus::Ok,
columns: Some(qr.columns),
rows: Some(qr.rows),
rows_affected: Some(qr.rows_affected),
command: qr.command,
watermark_lsn: lsn,
error: None,
auth: None,
warnings: Vec::new(),
}
}
pub fn error(seq: u64, code: impl Into<String>, message: impl Into<String>) -> Self {
Self::error_with_code(seq, code, message, 0)
}
pub fn error_with_code(
seq: u64,
code: impl Into<String>,
message: impl Into<String>,
ndb_code: u16,
) -> Self {
Self {
seq,
status: ResponseStatus::Error,
columns: None,
rows: None,
rows_affected: None,
command: None,
watermark_lsn: 0,
error: Some(ErrorPayload {
code: code.into(),
message: message.into(),
ndb_code,
details: None,
cause: None,
}),
auth: None,
warnings: Vec::new(),
}
}
pub fn with_error_details(mut self, details: crate::error::ErrorDetails) -> Self {
if let Some(payload) = self.error.as_mut() {
payload.details = Some(details);
}
self
}
pub fn with_error_cause(mut self, cause: super::error_cause::ErrorCausePayload) -> Self {
if let Some(payload) = self.error.as_mut() {
payload.cause = Some(cause);
}
self
}
pub fn auth_ok(seq: u64, username: String, tenant_id: u64) -> Self {
Self {
seq,
status: ResponseStatus::Ok,
columns: None,
rows: None,
rows_affected: None,
command: None,
watermark_lsn: 0,
error: None,
auth: Some(AuthResponse {
username,
tenant_id,
}),
warnings: Vec::new(),
}
}
pub fn status_row(seq: u64, message: impl Into<String>) -> Self {
Self {
seq,
status: ResponseStatus::Ok,
columns: Some(vec!["status".into()]),
rows: Some(vec![vec![Value::String(message.into())]]),
rows_affected: Some(1),
command: None,
watermark_lsn: 0,
error: None,
auth: None,
warnings: Vec::new(),
}
}
}
#[cfg(test)]
mod tests {
use super::super::opcodes::OpCode;
use super::super::text_fields::TextFields;
use super::*;
#[test]
fn native_response_ok() {
let r = NativeResponse::ok(42);
assert_eq!(r.seq, 42);
assert_eq!(r.status, ResponseStatus::Ok);
assert!(r.error.is_none());
}
#[test]
fn native_response_error() {
let r = NativeResponse::error(1, "42P01", "collection not found");
assert_eq!(r.status, ResponseStatus::Error);
let e = r.error.unwrap();
assert_eq!(e.code, "42P01");
assert_eq!(e.message, "collection not found");
}
#[test]
fn native_response_from_query_result() {
let qr = crate::result::QueryResult {
columns: vec!["id".into(), "name".into()],
rows: vec![vec![
Value::String("u1".into()),
Value::String("Alice".into()),
]],
rows_affected: 0,
command: None,
};
let r = NativeResponse::from_query_result(5, qr, 100);
assert_eq!(r.seq, 5);
assert_eq!(r.watermark_lsn, 100);
assert_eq!(r.columns.as_ref().unwrap().len(), 2);
assert_eq!(r.rows.as_ref().unwrap().len(), 1);
}
#[test]
fn native_response_status_row() {
let r = NativeResponse::status_row(3, "OK");
assert_eq!(r.columns.as_ref().unwrap(), &["status"]);
assert_eq!(r.rows.as_ref().unwrap()[0][0].as_str(), Some("OK"));
}
#[test]
fn msgpack_roundtrip_request() {
let req = NativeRequest {
op: OpCode::Sql,
seq: 1,
fields: RequestFields::Text(TextFields {
sql: Some("SELECT 1".into()),
..Default::default()
}),
};
let bytes = zerompk::to_msgpack_vec(&req).unwrap();
let decoded: NativeRequest = zerompk::from_msgpack(&bytes).unwrap();
assert_eq!(decoded.op, OpCode::Sql);
assert_eq!(decoded.seq, 1);
}
#[test]
fn msgpack_roundtrip_response() {
let resp = NativeResponse::from_query_result(
7,
crate::result::QueryResult {
columns: vec!["x".into()],
rows: vec![vec![Value::Integer(42)]],
rows_affected: 0,
command: None,
},
99,
);
let bytes = zerompk::to_msgpack_vec(&resp).unwrap();
let decoded: NativeResponse = zerompk::from_msgpack(&bytes).unwrap();
assert_eq!(decoded.seq, 7);
assert_eq!(decoded.watermark_lsn, 99);
assert_eq!(decoded.rows.unwrap()[0][0].as_i64(), Some(42));
}
#[test]
fn error_payload_round_trips_the_numeric_code() {
let frame = NativeResponse::error_with_code(7, "23505", "duplicate key", 1000);
let bytes = zerompk::to_msgpack_vec(&frame).expect("encode");
let decoded: NativeResponse = zerompk::from_msgpack(&bytes).expect("decode");
let payload = decoded.error.expect("error payload survives the wire");
assert_eq!(payload.code, "23505");
assert_eq!(payload.ndb_code, 1000);
}
#[test]
fn error_payload_round_trips_the_details() {
let details = crate::error::ErrorDetails::Overflow {
collection: "counters".into(),
};
let frame = NativeResponse::error_with_code(7, "22003", "overflow on counters", 1021)
.with_error_details(details.clone());
let bytes = zerompk::to_msgpack_vec(&frame).expect("encode");
let decoded: NativeResponse = zerompk::from_msgpack(&bytes).expect("decode");
let payload = decoded.error.expect("error payload survives the wire");
assert_eq!(payload.details, Some(details));
}
#[test]
fn error_payload_round_trips_the_cause() {
let cause = super::super::error_cause::ErrorCausePayload::from(
&crate::error::NodeDbError::division_by_zero(),
);
let frame = NativeResponse::error_with_code(7, "XX000", "snapshot failed", 1602)
.with_error_cause(cause.clone());
let bytes = zerompk::to_msgpack_vec(&frame).expect("encode");
let decoded: NativeResponse = zerompk::from_msgpack(&bytes).expect("decode");
let payload = decoded.error.expect("error payload survives the wire");
assert_eq!(payload.ndb_code, 1602);
assert_eq!(payload.cause, Some(cause));
}
#[test]
fn error_payload_without_numeric_code_still_decodes() {
let mut legacy = vec![0x82u8];
for (key, value) in [("code", "42P01"), ("message", "boom")] {
legacy.push(0xA0 | key.len() as u8);
legacy.extend_from_slice(key.as_bytes());
legacy.push(0xA0 | value.len() as u8);
legacy.extend_from_slice(value.as_bytes());
}
let decoded: ErrorPayload = zerompk::from_msgpack(&legacy).expect("legacy frame decodes");
assert_eq!(decoded.code, "42P01");
assert_eq!(decoded.message, "boom");
assert_eq!(
decoded.ndb_code, 0,
"an absent numeric code must read as the 'not carried' sentinel"
);
}
}