1use serde::{Deserialize, Serialize};
6
7use crate::value::Value;
8
9use super::auth::AuthResponse;
10use super::opcodes::ResponseStatus;
11use super::request_fields::RequestFields;
12
13#[derive(Debug, Clone, Serialize, Deserialize)]
20pub struct NativeRequest {
21 pub op: super::opcodes::OpCode,
23 pub seq: u64,
25 #[serde(flatten)]
27 pub fields: RequestFields,
28}
29
30impl zerompk::ToMessagePack for NativeRequest {
31 fn write<W: zerompk::Write>(&self, writer: &mut W) -> zerompk::Result<()> {
32 writer.write_array_len(3)?;
33 self.op.write(writer)?;
34 writer.write_u64(self.seq)?;
35 self.fields.write(writer)
36 }
37}
38
39impl<'a> zerompk::FromMessagePack<'a> for NativeRequest {
40 fn read<R: zerompk::Read<'a>>(reader: &mut R) -> zerompk::Result<Self> {
41 let len = reader.read_array_len()?;
42 if len != 3 {
43 return Err(zerompk::Error::ArrayLengthMismatch {
44 expected: 3,
45 actual: len,
46 });
47 }
48 let op = super::opcodes::OpCode::read(reader)?;
49 let seq = reader.read_u64()?;
50 let fields = RequestFields::read(reader)?;
51 Ok(Self { op, seq, fields })
52 }
53}
54
55#[derive(
59 Debug, Clone, Serialize, Deserialize, zerompk::ToMessagePack, zerompk::FromMessagePack,
60)]
61#[msgpack(map)]
62pub struct NativeResponse {
63 pub seq: u64,
65 pub status: ResponseStatus,
67 #[serde(skip_serializing_if = "Option::is_none")]
69 pub columns: Option<Vec<String>>,
70 #[serde(skip_serializing_if = "Option::is_none")]
72 pub rows: Option<Vec<Vec<Value>>>,
73 #[serde(skip_serializing_if = "Option::is_none")]
75 pub rows_affected: Option<u64>,
76 #[serde(default, skip_serializing_if = "Option::is_none")]
80 #[msgpack(default)]
81 pub command: Option<String>,
82 pub watermark_lsn: u64,
84 #[serde(skip_serializing_if = "Option::is_none")]
86 pub error: Option<ErrorPayload>,
87 #[serde(skip_serializing_if = "Option::is_none")]
89 pub auth: Option<AuthResponse>,
90 #[serde(default, skip_serializing_if = "Vec::is_empty")]
94 #[msgpack(default)]
95 pub warnings: Vec<String>,
96}
97
98#[derive(
106 Debug, Clone, Serialize, Deserialize, zerompk::ToMessagePack, zerompk::FromMessagePack,
107)]
108#[msgpack(map)]
109pub struct ErrorPayload {
110 pub code: String,
112 pub message: String,
114 #[serde(default, skip_serializing_if = "is_zero")]
126 #[msgpack(default)]
127 pub ndb_code: u16,
128 #[serde(default, skip_serializing_if = "Option::is_none")]
131 #[msgpack(default)]
132 pub details: Option<crate::error::ErrorDetails>,
133 #[serde(default, skip_serializing_if = "Option::is_none")]
137 #[msgpack(default)]
138 pub cause: Option<super::error_cause::ErrorCausePayload>,
139}
140
141fn is_zero(code: &u16) -> bool {
144 *code == 0
145}
146
147impl NativeResponse {
148 pub fn ok(seq: u64) -> Self {
150 Self {
151 seq,
152 status: ResponseStatus::Ok,
153 columns: None,
154 rows: None,
155 rows_affected: None,
156 command: None,
157 watermark_lsn: 0,
158 error: None,
159 auth: None,
160 warnings: Vec::new(),
161 }
162 }
163
164 pub fn from_query_result(seq: u64, qr: crate::result::QueryResult, lsn: u64) -> Self {
166 Self {
167 seq,
168 status: ResponseStatus::Ok,
169 columns: Some(qr.columns),
170 rows: Some(qr.rows),
171 rows_affected: Some(qr.rows_affected),
172 command: qr.command,
173 watermark_lsn: lsn,
174 error: None,
175 auth: None,
176 warnings: Vec::new(),
177 }
178 }
179
180 pub fn error(seq: u64, code: impl Into<String>, message: impl Into<String>) -> Self {
186 Self::error_with_code(seq, code, message, 0)
187 }
188
189 pub fn error_with_code(
193 seq: u64,
194 code: impl Into<String>,
195 message: impl Into<String>,
196 ndb_code: u16,
197 ) -> Self {
198 Self {
199 seq,
200 status: ResponseStatus::Error,
201 columns: None,
202 rows: None,
203 rows_affected: None,
204 command: None,
205 watermark_lsn: 0,
206 error: Some(ErrorPayload {
207 code: code.into(),
208 message: message.into(),
209 ndb_code,
210 details: None,
211 cause: None,
212 }),
213 auth: None,
214 warnings: Vec::new(),
215 }
216 }
217
218 pub fn with_error_details(mut self, details: crate::error::ErrorDetails) -> Self {
221 if let Some(payload) = self.error.as_mut() {
222 payload.details = Some(details);
223 }
224 self
225 }
226
227 pub fn with_error_cause(mut self, cause: super::error_cause::ErrorCausePayload) -> Self {
230 if let Some(payload) = self.error.as_mut() {
231 payload.cause = Some(cause);
232 }
233 self
234 }
235
236 pub fn auth_ok(seq: u64, username: String, tenant_id: u64) -> Self {
238 Self {
239 seq,
240 status: ResponseStatus::Ok,
241 columns: None,
242 rows: None,
243 rows_affected: None,
244 command: None,
245 watermark_lsn: 0,
246 error: None,
247 auth: Some(AuthResponse {
248 username,
249 tenant_id,
250 }),
251 warnings: Vec::new(),
252 }
253 }
254
255 pub fn status_row(seq: u64, message: impl Into<String>) -> Self {
257 Self {
258 seq,
259 status: ResponseStatus::Ok,
260 columns: Some(vec!["status".into()]),
261 rows: Some(vec![vec![Value::String(message.into())]]),
262 rows_affected: Some(1),
263 command: None,
264 watermark_lsn: 0,
265 error: None,
266 auth: None,
267 warnings: Vec::new(),
268 }
269 }
270}
271
272#[cfg(test)]
273mod tests {
274 use super::super::opcodes::OpCode;
275 use super::super::text_fields::TextFields;
276 use super::*;
277
278 #[test]
279 fn native_response_ok() {
280 let r = NativeResponse::ok(42);
281 assert_eq!(r.seq, 42);
282 assert_eq!(r.status, ResponseStatus::Ok);
283 assert!(r.error.is_none());
284 }
285
286 #[test]
287 fn native_response_error() {
288 let r = NativeResponse::error(1, "42P01", "collection not found");
289 assert_eq!(r.status, ResponseStatus::Error);
290 let e = r.error.unwrap();
291 assert_eq!(e.code, "42P01");
292 assert_eq!(e.message, "collection not found");
293 }
294
295 #[test]
296 fn native_response_from_query_result() {
297 let qr = crate::result::QueryResult {
298 columns: vec!["id".into(), "name".into()],
299 rows: vec![vec![
300 Value::String("u1".into()),
301 Value::String("Alice".into()),
302 ]],
303 rows_affected: 0,
304 command: None,
305 };
306 let r = NativeResponse::from_query_result(5, qr, 100);
307 assert_eq!(r.seq, 5);
308 assert_eq!(r.watermark_lsn, 100);
309 assert_eq!(r.columns.as_ref().unwrap().len(), 2);
310 assert_eq!(r.rows.as_ref().unwrap().len(), 1);
311 }
312
313 #[test]
314 fn native_response_status_row() {
315 let r = NativeResponse::status_row(3, "OK");
316 assert_eq!(r.columns.as_ref().unwrap(), &["status"]);
317 assert_eq!(r.rows.as_ref().unwrap()[0][0].as_str(), Some("OK"));
318 }
319
320 #[test]
321 fn msgpack_roundtrip_request() {
322 let req = NativeRequest {
323 op: OpCode::Sql,
324 seq: 1,
325 fields: RequestFields::Text(TextFields {
326 sql: Some("SELECT 1".into()),
327 ..Default::default()
328 }),
329 };
330 let bytes = zerompk::to_msgpack_vec(&req).unwrap();
331 let decoded: NativeRequest = zerompk::from_msgpack(&bytes).unwrap();
332 assert_eq!(decoded.op, OpCode::Sql);
333 assert_eq!(decoded.seq, 1);
334 }
335
336 #[test]
337 fn msgpack_roundtrip_response() {
338 let resp = NativeResponse::from_query_result(
339 7,
340 crate::result::QueryResult {
341 columns: vec!["x".into()],
342 rows: vec![vec![Value::Integer(42)]],
343 rows_affected: 0,
344 command: None,
345 },
346 99,
347 );
348 let bytes = zerompk::to_msgpack_vec(&resp).unwrap();
349 let decoded: NativeResponse = zerompk::from_msgpack(&bytes).unwrap();
350 assert_eq!(decoded.seq, 7);
351 assert_eq!(decoded.watermark_lsn, 99);
352 assert_eq!(decoded.rows.unwrap()[0][0].as_i64(), Some(42));
353 }
354
355 #[test]
356 fn error_payload_round_trips_the_numeric_code() {
357 let frame = NativeResponse::error_with_code(7, "23505", "duplicate key", 1000);
358 let bytes = zerompk::to_msgpack_vec(&frame).expect("encode");
359 let decoded: NativeResponse = zerompk::from_msgpack(&bytes).expect("decode");
360 let payload = decoded.error.expect("error payload survives the wire");
361 assert_eq!(payload.code, "23505");
362 assert_eq!(payload.ndb_code, 1000);
363 }
364
365 #[test]
366 fn error_payload_round_trips_the_details() {
367 let details = crate::error::ErrorDetails::Overflow {
368 collection: "counters".into(),
369 };
370 let frame = NativeResponse::error_with_code(7, "22003", "overflow on counters", 1021)
371 .with_error_details(details.clone());
372 let bytes = zerompk::to_msgpack_vec(&frame).expect("encode");
373 let decoded: NativeResponse = zerompk::from_msgpack(&bytes).expect("decode");
374 let payload = decoded.error.expect("error payload survives the wire");
375 assert_eq!(payload.details, Some(details));
376 }
377
378 #[test]
379 fn error_payload_round_trips_the_cause() {
380 let cause = super::super::error_cause::ErrorCausePayload::from(
381 &crate::error::NodeDbError::division_by_zero(),
382 );
383 let frame = NativeResponse::error_with_code(7, "XX000", "snapshot failed", 1602)
384 .with_error_cause(cause.clone());
385 let bytes = zerompk::to_msgpack_vec(&frame).expect("encode");
386 let decoded: NativeResponse = zerompk::from_msgpack(&bytes).expect("decode");
387 let payload = decoded.error.expect("error payload survives the wire");
388 assert_eq!(payload.ndb_code, 1602);
389 assert_eq!(payload.cause, Some(cause));
390 }
391
392 #[test]
393 fn error_payload_without_numeric_code_still_decodes() {
394 let mut legacy = vec![0x82u8];
398 for (key, value) in [("code", "42P01"), ("message", "boom")] {
399 legacy.push(0xA0 | key.len() as u8);
400 legacy.extend_from_slice(key.as_bytes());
401 legacy.push(0xA0 | value.len() as u8);
402 legacy.extend_from_slice(value.as_bytes());
403 }
404 let decoded: ErrorPayload = zerompk::from_msgpack(&legacy).expect("legacy frame decodes");
405 assert_eq!(decoded.code, "42P01");
406 assert_eq!(decoded.message, "boom");
407 assert_eq!(
408 decoded.ndb_code, 0,
409 "an absent numeric code must read as the 'not carried' sentinel"
410 );
411 }
412}