Skip to main content

nodedb_types/protocol/
frames.rs

1// SPDX-License-Identifier: Apache-2.0
2
3//! Native protocol request and response frame types.
4
5use serde::{Deserialize, Serialize};
6
7use crate::value::Value;
8
9use super::auth::AuthResponse;
10use super::opcodes::ResponseStatus;
11use super::request_fields::RequestFields;
12
13// ─── Request Frame ──────────────────────────────────────────────────
14
15/// A request sent from client to server over the native protocol.
16///
17/// Serialized as MessagePack. The `op` field selects the handler,
18/// `seq` correlates request to response.
19#[derive(Debug, Clone, Serialize, Deserialize)]
20pub struct NativeRequest {
21    /// Operation code.
22    pub op: super::opcodes::OpCode,
23    /// Client-assigned sequence number for request/response correlation.
24    pub seq: u64,
25    /// Operation-specific fields (flattened into the same map).
26    #[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// ─── Response Frame ─────────────────────────────────────────────────
56
57/// A response sent from server to client over the native protocol.
58#[derive(
59    Debug, Clone, Serialize, Deserialize, zerompk::ToMessagePack, zerompk::FromMessagePack,
60)]
61#[msgpack(map)]
62pub struct NativeResponse {
63    /// Echoed from the request for correlation.
64    pub seq: u64,
65    /// Execution outcome.
66    pub status: ResponseStatus,
67    /// Column names (for query results).
68    #[serde(skip_serializing_if = "Option::is_none")]
69    pub columns: Option<Vec<String>>,
70    /// Row data (for query results). Each row is a Vec of Values.
71    #[serde(skip_serializing_if = "Option::is_none")]
72    pub rows: Option<Vec<Vec<Value>>>,
73    /// Number of rows affected (for writes).
74    #[serde(skip_serializing_if = "Option::is_none")]
75    pub rows_affected: Option<u64>,
76    /// WAL LSN watermark at time of computation.
77    pub watermark_lsn: u64,
78    /// Error details (if status == Error).
79    #[serde(skip_serializing_if = "Option::is_none")]
80    pub error: Option<ErrorPayload>,
81    /// Auth response (if op == Auth and status == Ok).
82    #[serde(skip_serializing_if = "Option::is_none")]
83    pub auth: Option<AuthResponse>,
84    /// Advisory warnings (e.g. password expiry grace period, must_change_password).
85    /// Empty in the common case; `#[serde(default)]` and `#[msgpack(default)]`
86    /// keep this additive and backward-compatible with older clients.
87    #[serde(default, skip_serializing_if = "Vec::is_empty")]
88    #[msgpack(default)]
89    pub warnings: Vec<String>,
90}
91
92/// Error details in a response.
93#[derive(
94    Debug, Clone, Serialize, Deserialize, zerompk::ToMessagePack, zerompk::FromMessagePack,
95)]
96pub struct ErrorPayload {
97    /// SQLSTATE-style error code (e.g., "42P01" for undefined table).
98    pub code: String,
99    /// Human-readable error message.
100    pub message: String,
101}
102
103impl NativeResponse {
104    /// Create a successful response with no data.
105    pub fn ok(seq: u64) -> Self {
106        Self {
107            seq,
108            status: ResponseStatus::Ok,
109            columns: None,
110            rows: None,
111            rows_affected: None,
112            watermark_lsn: 0,
113            error: None,
114            auth: None,
115            warnings: Vec::new(),
116        }
117    }
118
119    /// Create a successful response from a `QueryResult`.
120    pub fn from_query_result(seq: u64, qr: crate::result::QueryResult, lsn: u64) -> Self {
121        Self {
122            seq,
123            status: ResponseStatus::Ok,
124            columns: Some(qr.columns),
125            rows: Some(qr.rows),
126            rows_affected: Some(qr.rows_affected),
127            watermark_lsn: lsn,
128            error: None,
129            auth: None,
130            warnings: Vec::new(),
131        }
132    }
133
134    /// Create an error response.
135    pub fn error(seq: u64, code: impl Into<String>, message: impl Into<String>) -> Self {
136        Self {
137            seq,
138            status: ResponseStatus::Error,
139            columns: None,
140            rows: None,
141            rows_affected: None,
142            watermark_lsn: 0,
143            error: Some(ErrorPayload {
144                code: code.into(),
145                message: message.into(),
146            }),
147            auth: None,
148            warnings: Vec::new(),
149        }
150    }
151
152    /// Create an auth success response.
153    pub fn auth_ok(seq: u64, username: String, tenant_id: u64) -> Self {
154        Self {
155            seq,
156            status: ResponseStatus::Ok,
157            columns: None,
158            rows: None,
159            rows_affected: None,
160            watermark_lsn: 0,
161            error: None,
162            auth: Some(AuthResponse {
163                username,
164                tenant_id,
165            }),
166            warnings: Vec::new(),
167        }
168    }
169
170    /// Create a response with a single "status" column and one row.
171    pub fn status_row(seq: u64, message: impl Into<String>) -> Self {
172        Self {
173            seq,
174            status: ResponseStatus::Ok,
175            columns: Some(vec!["status".into()]),
176            rows: Some(vec![vec![Value::String(message.into())]]),
177            rows_affected: Some(1),
178            watermark_lsn: 0,
179            error: None,
180            auth: None,
181            warnings: Vec::new(),
182        }
183    }
184}