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 pub fn route(&mut self, message: BackendMessage) -> Option<SessionItem> {
129 match message {
130 BackendMessage::NoticeResponse(fields) => {
131 let notice = TaggedNotice {
132 command: self.command,
133 fields,
134 };
135 self.pending_notices.push(notice.clone());
136 self.notices.push_back(notice.clone());
137 self.push_async(AsyncEvent::Notice(notice));
138 None
139 }
140 BackendMessage::ParameterStatus { name, value } => {
141 self.parameters.insert(name.clone(), value.clone());
142 let status = ParameterStatus { name, value };
143 self.parameter_statuses.push_back(status.clone());
144 self.push_async(AsyncEvent::ParameterStatus(status));
145 if let Some(startup_parameters) = &self.startup_parameters {
146 self.parameters_changed = self.parameters != *startup_parameters;
147 }
148 None
149 }
150 BackendMessage::NotificationResponse {
151 process_id,
152 channel,
153 payload,
154 } => {
155 let notification = Notification {
156 process_id,
157 channel,
158 payload,
159 };
160 self.notifications.push_back(notification.clone());
161 self.push_async(AsyncEvent::Notification(notification));
162 None
163 }
164 BackendMessage::BackendKeyData {
165 process_id,
166 secret_key,
167 } => {
168 self.cancel_key = Some(CancelKey {
169 process_id,
170 secret_key: secret_key.clone(),
171 });
172 Some(SessionItem::Message(BackendMessage::BackendKeyData {
173 process_id,
174 secret_key,
175 }))
176 }
177 BackendMessage::ReadyForQuery(status) => {
178 self.transaction_status = Some(status);
179 if self.startup_parameters.is_none() {
180 self.startup_parameters = Some(self.parameters.clone());
181 }
182 Some(SessionItem::ReadyForQuery {
183 status,
184 parameters_changed: self.parameters_changed,
185 })
186 }
187 BackendMessage::CommandComplete(tag) => {
188 let command = self.command;
189 let notices = std::mem::take(&mut self.pending_notices);
190 self.command.0 = self.command.0.saturating_add(1);
191 Some(SessionItem::CommandComplete {
192 tag,
193 command,
194 notices,
195 })
196 }
197 message => Some(SessionItem::Message(message)),
198 }
199 }
200
201 #[must_use]
203 pub fn parameters(&self) -> &BTreeMap<Bytes, Bytes> {
204 &self.parameters
205 }
206
207 #[must_use]
209 pub const fn parameters_changed(&self) -> bool {
210 self.parameters_changed
211 }
212
213 #[must_use]
215 pub const fn cancel_key(&self) -> Option<&CancelKey> {
216 self.cancel_key.as_ref()
217 }
218
219 #[must_use]
221 pub const fn transaction_status(&self) -> Option<TransactionStatus> {
222 self.transaction_status
223 }
224
225 pub fn pop_notification(&mut self) -> Option<Notification> {
227 self.notifications.pop_front()
228 }
229
230 pub fn pop_notice(&mut self) -> Option<TaggedNotice> {
232 self.notices.pop_front()
233 }
234
235 pub fn pop_parameter_status(&mut self) -> Option<ParameterStatus> {
237 self.parameter_statuses.pop_front()
238 }
239
240 pub fn pop_async_event(&mut self) -> Option<OrderedAsyncEvent> {
242 self.async_events.pop_front()
243 }
244
245 fn push_async(&mut self, event: AsyncEvent) {
246 let sequence = self.next_async_sequence;
247 self.next_async_sequence = self.next_async_sequence.saturating_add(1);
248 self.async_events.push_back(OrderedAsyncEvent {
249 sequence,
250 command: self.command,
251 event,
252 });
253 }
254}
255
256#[cfg(test)]
257mod tests {
258 use super::*;
259
260 #[test]
261 fn notices_are_attached_to_their_command_boundary() {
262 let mut demux = Demux::default();
263 assert_eq!(
264 demux.route(BackendMessage::NoticeResponse(DiagnosticResponse {
265 fields: vec![crate::codec::DiagnosticField {
266 code: b'M',
267 value: Bytes::from_static(b"notice"),
268 }],
269 })),
270 None
271 );
272 let completion = demux
273 .route(BackendMessage::CommandComplete(Bytes::from_static(
274 b"SELECT 1",
275 )))
276 .expect("command completion advances the session");
277 assert_eq!(
278 completion,
279 SessionItem::CommandComplete {
280 tag: Bytes::from_static(b"SELECT 1"),
281 command: CommandIndex(0),
282 notices: vec![TaggedNotice {
283 command: CommandIndex(0),
284 fields: DiagnosticResponse {
285 fields: vec![crate::codec::DiagnosticField {
286 code: b'M',
287 value: Bytes::from_static(b"notice"),
288 }],
289 },
290 }],
291 }
292 );
293 assert_eq!(
294 demux.pop_notice(),
295 Some(TaggedNotice {
296 command: CommandIndex(0),
297 fields: DiagnosticResponse {
298 fields: vec![crate::codec::DiagnosticField {
299 code: b'M',
300 value: Bytes::from_static(b"notice"),
301 }],
302 },
303 })
304 );
305 assert_eq!(demux.pop_notice(), None);
306 }
307
308 #[test]
309 fn startup_parameters_establish_a_clean_baseline() {
310 let mut demux = Demux::default();
311 assert!(
312 demux
313 .route(BackendMessage::ParameterStatus {
314 name: Bytes::from_static(b"client_encoding"),
315 value: Bytes::from_static(b"UTF8"),
316 })
317 .is_none()
318 );
319 demux.route(BackendMessage::ReadyForQuery(TransactionStatus::Idle));
320 assert!(!demux.parameters_changed());
321
322 demux.route(BackendMessage::ParameterStatus {
323 name: Bytes::from_static(b"client_encoding"),
324 value: Bytes::from_static(b"LATIN1"),
325 });
326 assert!(demux.parameters_changed());
327 }
328
329 #[test]
330 fn parameter_statuses_remain_ordered_for_proxy_forwarding() {
331 let mut demux = Demux::default();
332 for (name, value) in [
333 (b"TimeZone".as_slice(), b"UTC".as_slice()),
334 (b"TimeZone", b"GMT"),
335 ] {
336 assert!(
337 demux
338 .route(BackendMessage::ParameterStatus {
339 name: Bytes::copy_from_slice(name),
340 value: Bytes::copy_from_slice(value),
341 })
342 .is_none()
343 );
344 }
345
346 assert_eq!(
347 demux.pop_parameter_status(),
348 Some(ParameterStatus {
349 name: Bytes::from_static(b"TimeZone"),
350 value: Bytes::from_static(b"UTC"),
351 })
352 );
353 assert_eq!(
354 demux.pop_parameter_status(),
355 Some(ParameterStatus {
356 name: Bytes::from_static(b"TimeZone"),
357 value: Bytes::from_static(b"GMT"),
358 })
359 );
360 assert_eq!(demux.pop_parameter_status(), None);
361 assert_eq!(
362 demux.parameters().get(b"TimeZone".as_slice()),
363 Some(&Bytes::from_static(b"GMT"))
364 );
365 }
366
367 #[test]
368 fn notification_is_not_a_session_transition() {
369 let mut demux = Demux::default();
370 assert!(
371 demux
372 .route(BackendMessage::NotificationResponse {
373 process_id: 42,
374 channel: Bytes::from_static(b"events"),
375 payload: Bytes::from_static(b"payload"),
376 })
377 .is_none()
378 );
379 assert_eq!(
380 demux.pop_notification(),
381 Some(Notification {
382 process_id: 42,
383 channel: Bytes::from_static(b"events"),
384 payload: Bytes::from_static(b"payload"),
385 })
386 );
387 }
388
389 #[test]
390 fn asynchronous_events_preserve_cross_kind_wire_order() {
391 let mut demux = Demux::default();
392 demux.route(BackendMessage::ParameterStatus {
393 name: Bytes::from_static(b"TimeZone"),
394 value: Bytes::from_static(b"UTC"),
395 });
396 demux.route(BackendMessage::NotificationResponse {
397 process_id: 7,
398 channel: Bytes::from_static(b"jobs"),
399 payload: Bytes::from_static(b"ready"),
400 });
401 demux.route(BackendMessage::NoticeResponse(DiagnosticResponse {
402 fields: vec![],
403 }));
404
405 let events = std::iter::from_fn(|| demux.pop_async_event()).collect::<Vec<_>>();
406 assert_eq!(events.len(), 3);
407 assert_eq!(events[0].sequence, 0);
408 assert!(matches!(events[0].event, AsyncEvent::ParameterStatus(_)));
409 assert_eq!(events[1].sequence, 1);
410 assert!(matches!(events[1].event, AsyncEvent::Notification(_)));
411 assert_eq!(events[2].sequence, 2);
412 assert!(matches!(events[2].event, AsyncEvent::Notice(_)));
413 assert!(events.iter().all(|event| event.command == CommandIndex(0)));
414 }
415}