1use std::collections::{BTreeMap, VecDeque};
4
5use bytes::Bytes;
6
7use crate::codec::{BackendMessage, DiagnosticResponse, TransactionStatus};
8
9#[derive(Clone, Copy, Debug, Default, Eq, Ord, PartialEq, PartialOrd)]
11pub struct CommandIndex(pub u64);
12
13#[derive(Clone, Debug, Eq, PartialEq)]
14pub struct TaggedNotice {
16 pub command: CommandIndex,
18 pub fields: DiagnosticResponse,
20}
21
22#[derive(Clone, Debug, Eq, PartialEq)]
23pub struct Notification {
25 pub process_id: u32,
27 pub channel: Bytes,
29 pub payload: Bytes,
31}
32
33#[derive(Clone, Debug, Eq, PartialEq)]
35pub struct ParameterStatus {
36 pub name: Bytes,
38 pub value: Bytes,
40}
41
42#[derive(Clone, Debug, Eq, PartialEq)]
44pub enum AsyncEvent {
45 Notice(TaggedNotice),
47 ParameterStatus(ParameterStatus),
49 Notification(Notification),
51}
52
53#[derive(Clone, Debug, Eq, PartialEq)]
55pub struct OrderedAsyncEvent {
56 pub sequence: u64,
58 pub command: CommandIndex,
60 pub event: AsyncEvent,
62}
63
64#[derive(Clone, Eq, Hash, PartialEq)]
65pub struct CancelKey {
67 pub process_id: u32,
69 pub secret_key: Bytes,
71}
72
73impl std::fmt::Debug for CancelKey {
74 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
75 formatter
76 .debug_struct("CancelKey")
77 .field("process_id", &self.process_id)
78 .field("secret_key", &"[REDACTED]")
79 .finish()
80 }
81}
82
83#[derive(Clone, Debug, Eq, PartialEq)]
85pub enum SessionItem {
86 Message(BackendMessage),
88 ReadyForQuery {
90 status: TransactionStatus,
92 parameters_changed: bool,
94 },
95 CommandComplete {
97 tag: Bytes,
99 command: CommandIndex,
101 notices: Vec<TaggedNotice>,
103 },
104}
105
106#[derive(Debug, Default)]
108pub struct Demux {
109 command: CommandIndex,
110 pending_notices: Vec<TaggedNotice>,
111 notices: VecDeque<TaggedNotice>,
112 notifications: VecDeque<Notification>,
113 parameter_statuses: VecDeque<ParameterStatus>,
114 async_events: VecDeque<OrderedAsyncEvent>,
115 next_async_sequence: u64,
116 parameters: BTreeMap<Bytes, Bytes>,
117 startup_parameters: Option<BTreeMap<Bytes, Bytes>>,
118 parameters_changed: bool,
119 cancel_key: Option<CancelKey>,
120 transaction_status: Option<TransactionStatus>,
121}
122
123impl Demux {
124 #[must_use]
129 pub fn is_asynchronous(message: &BackendMessage) -> bool {
130 matches!(
131 message,
132 BackendMessage::NoticeResponse(_)
133 | BackendMessage::ParameterStatus { .. }
134 | BackendMessage::NotificationResponse { .. }
135 | BackendMessage::BackendKeyData { .. }
136 )
137 }
138
139 pub fn route(&mut self, message: BackendMessage) -> Option<SessionItem> {
144 match message {
145 BackendMessage::NoticeResponse(fields) => {
146 let notice = TaggedNotice {
147 command: self.command,
148 fields,
149 };
150 self.pending_notices.push(notice.clone());
151 self.notices.push_back(notice.clone());
152 self.push_async(AsyncEvent::Notice(notice));
153 None
154 }
155 BackendMessage::ParameterStatus { name, value } => {
156 self.parameters.insert(name.clone(), value.clone());
157 let status = ParameterStatus { name, value };
158 self.parameter_statuses.push_back(status.clone());
159 self.push_async(AsyncEvent::ParameterStatus(status));
160 if let Some(startup_parameters) = &self.startup_parameters {
161 self.parameters_changed = self.parameters != *startup_parameters;
162 }
163 None
164 }
165 BackendMessage::NotificationResponse {
166 process_id,
167 channel,
168 payload,
169 } => {
170 let notification = Notification {
171 process_id,
172 channel,
173 payload,
174 };
175 self.notifications.push_back(notification.clone());
176 self.push_async(AsyncEvent::Notification(notification));
177 None
178 }
179 BackendMessage::BackendKeyData {
180 process_id,
181 secret_key,
182 } => {
183 self.cancel_key = Some(CancelKey {
184 process_id,
185 secret_key: secret_key.clone(),
186 });
187 Some(SessionItem::Message(BackendMessage::BackendKeyData {
188 process_id,
189 secret_key,
190 }))
191 }
192 BackendMessage::ReadyForQuery(status) => {
193 self.transaction_status = Some(status);
194 if self.startup_parameters.is_none() {
195 self.startup_parameters = Some(self.parameters.clone());
196 }
197 Some(SessionItem::ReadyForQuery {
198 status,
199 parameters_changed: self.parameters_changed,
200 })
201 }
202 BackendMessage::CommandComplete(tag) => {
203 let command = self.command;
204 let notices = std::mem::take(&mut self.pending_notices);
205 self.command.0 = self.command.0.saturating_add(1);
206 Some(SessionItem::CommandComplete {
207 tag,
208 command,
209 notices,
210 })
211 }
212 message => Some(SessionItem::Message(message)),
213 }
214 }
215
216 #[must_use]
218 pub fn parameters(&self) -> &BTreeMap<Bytes, Bytes> {
219 &self.parameters
220 }
221
222 #[must_use]
224 pub const fn parameters_changed(&self) -> bool {
225 self.parameters_changed
226 }
227
228 #[must_use]
230 pub const fn cancel_key(&self) -> Option<&CancelKey> {
231 self.cancel_key.as_ref()
232 }
233
234 #[must_use]
236 pub const fn transaction_status(&self) -> Option<TransactionStatus> {
237 self.transaction_status
238 }
239
240 pub fn pop_notification(&mut self) -> Option<Notification> {
242 self.notifications.pop_front()
243 }
244
245 pub fn pop_notice(&mut self) -> Option<TaggedNotice> {
247 self.notices.pop_front()
248 }
249
250 pub fn pop_parameter_status(&mut self) -> Option<ParameterStatus> {
252 self.parameter_statuses.pop_front()
253 }
254
255 pub fn pop_async_event(&mut self) -> Option<OrderedAsyncEvent> {
257 self.async_events.pop_front()
258 }
259
260 fn push_async(&mut self, event: AsyncEvent) {
261 let sequence = self.next_async_sequence;
262 self.next_async_sequence = self.next_async_sequence.saturating_add(1);
263 self.async_events.push_back(OrderedAsyncEvent {
264 sequence,
265 command: self.command,
266 event,
267 });
268 }
269}
270
271#[cfg(test)]
272mod tests {
273 use super::*;
274
275 #[test]
276 fn notices_are_attached_to_their_command_boundary() {
277 let mut demux = Demux::default();
278 assert_eq!(
279 demux.route(BackendMessage::NoticeResponse(DiagnosticResponse {
280 fields: vec![crate::codec::DiagnosticField {
281 code: b'M',
282 value: Bytes::from_static(b"notice"),
283 }],
284 })),
285 None
286 );
287 let completion = demux
288 .route(BackendMessage::CommandComplete(Bytes::from_static(
289 b"SELECT 1",
290 )))
291 .expect("command completion advances the session");
292 assert_eq!(
293 completion,
294 SessionItem::CommandComplete {
295 tag: Bytes::from_static(b"SELECT 1"),
296 command: CommandIndex(0),
297 notices: vec![TaggedNotice {
298 command: CommandIndex(0),
299 fields: DiagnosticResponse {
300 fields: vec![crate::codec::DiagnosticField {
301 code: b'M',
302 value: Bytes::from_static(b"notice"),
303 }],
304 },
305 }],
306 }
307 );
308 assert_eq!(
309 demux.pop_notice(),
310 Some(TaggedNotice {
311 command: CommandIndex(0),
312 fields: DiagnosticResponse {
313 fields: vec![crate::codec::DiagnosticField {
314 code: b'M',
315 value: Bytes::from_static(b"notice"),
316 }],
317 },
318 })
319 );
320 assert_eq!(demux.pop_notice(), None);
321 }
322
323 #[test]
324 fn startup_parameters_establish_a_clean_baseline() {
325 let mut demux = Demux::default();
326 assert!(
327 demux
328 .route(BackendMessage::ParameterStatus {
329 name: Bytes::from_static(b"client_encoding"),
330 value: Bytes::from_static(b"UTF8"),
331 })
332 .is_none()
333 );
334 demux.route(BackendMessage::ReadyForQuery(TransactionStatus::Idle));
335 assert!(!demux.parameters_changed());
336
337 demux.route(BackendMessage::ParameterStatus {
338 name: Bytes::from_static(b"client_encoding"),
339 value: Bytes::from_static(b"LATIN1"),
340 });
341 assert!(demux.parameters_changed());
342 }
343
344 #[test]
345 fn parameter_statuses_remain_ordered_for_proxy_forwarding() {
346 let mut demux = Demux::default();
347 for (name, value) in [
348 (b"TimeZone".as_slice(), b"UTC".as_slice()),
349 (b"TimeZone", b"GMT"),
350 ] {
351 assert!(
352 demux
353 .route(BackendMessage::ParameterStatus {
354 name: Bytes::copy_from_slice(name),
355 value: Bytes::copy_from_slice(value),
356 })
357 .is_none()
358 );
359 }
360
361 assert_eq!(
362 demux.pop_parameter_status(),
363 Some(ParameterStatus {
364 name: Bytes::from_static(b"TimeZone"),
365 value: Bytes::from_static(b"UTC"),
366 })
367 );
368 assert_eq!(
369 demux.pop_parameter_status(),
370 Some(ParameterStatus {
371 name: Bytes::from_static(b"TimeZone"),
372 value: Bytes::from_static(b"GMT"),
373 })
374 );
375 assert_eq!(demux.pop_parameter_status(), None);
376 assert_eq!(
377 demux.parameters().get(b"TimeZone".as_slice()),
378 Some(&Bytes::from_static(b"GMT"))
379 );
380 }
381
382 #[test]
383 fn notification_is_not_a_session_transition() {
384 let mut demux = Demux::default();
385 assert!(
386 demux
387 .route(BackendMessage::NotificationResponse {
388 process_id: 42,
389 channel: Bytes::from_static(b"events"),
390 payload: Bytes::from_static(b"payload"),
391 })
392 .is_none()
393 );
394 assert_eq!(
395 demux.pop_notification(),
396 Some(Notification {
397 process_id: 42,
398 channel: Bytes::from_static(b"events"),
399 payload: Bytes::from_static(b"payload"),
400 })
401 );
402 }
403
404 #[test]
405 fn asynchronous_events_preserve_cross_kind_wire_order() {
406 let mut demux = Demux::default();
407 demux.route(BackendMessage::ParameterStatus {
408 name: Bytes::from_static(b"TimeZone"),
409 value: Bytes::from_static(b"UTC"),
410 });
411 demux.route(BackendMessage::NotificationResponse {
412 process_id: 7,
413 channel: Bytes::from_static(b"jobs"),
414 payload: Bytes::from_static(b"ready"),
415 });
416 demux.route(BackendMessage::NoticeResponse(DiagnosticResponse {
417 fields: vec![],
418 }));
419
420 let events = std::iter::from_fn(|| demux.pop_async_event()).collect::<Vec<_>>();
421 assert_eq!(events.len(), 3);
422 assert_eq!(events[0].sequence, 0);
423 assert!(matches!(events[0].event, AsyncEvent::ParameterStatus(_)));
424 assert_eq!(events[1].sequence, 1);
425 assert!(matches!(events[1].event, AsyncEvent::Notification(_)));
426 assert_eq!(events[2].sequence, 2);
427 assert!(matches!(events[2].event, AsyncEvent::Notice(_)));
428 assert!(events.iter().all(|event| event.command == CommandIndex(0)));
429 }
430}