Skip to main content

pg_proto/
pipeline.rs

1//! Bounded, payload-free orchestration for proxy request pipelines.
2//!
3//! The ledger in this module records protocol obligations, not wire messages.
4//! Applications retain ownership of decoded messages until [`FrontendAction`] or
5//! [`BackendAction`] tells them to forward, emit, retry, or discard the value.
6//! [`Pipeline::accept_frontend`] and [`Pipeline::accept_backend`] are the canonical
7//! ledger interface. Their typed counterparts additionally dispatch accepted
8//! messages through phase-specific middleware before committing them.
9//! Callers receiving [`crate::demux::SessionItem`] values should first consume
10//! any pooling or attribution evidence they need, then convert the item with
11//! [`crate::demux::SessionItem::into_backend_message`] before backend acceptance.
12
13use std::{collections::VecDeque, convert::Infallible, sync::Arc};
14
15use tokio::sync::Notify;
16
17use crate::{
18    codec::{BackendMessage, FrontendMessage},
19    demux::Demux,
20    grammar::backend,
21    middleware::{
22        AsynchronousBackendMessage, ChainError, MessageMiddleware, Middleware,
23        ReconstructableMessage as _, Then,
24    },
25};
26
27/// Stable identity of an accepted frontend operation.
28#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
29pub struct OperationId(u64);
30
31/// Pipeline policy which preserves the historical lock-step behaviour.
32#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
33pub struct NoPipeline;
34
35/// Configuration for a bounded frontend operation pipeline.
36#[derive(Clone, Copy, Debug, Eq, PartialEq)]
37pub struct BoundedPipeline {
38    max_operations: usize,
39}
40
41impl BoundedPipeline {
42    /// Creates a pipeline with a non-zero operation-count limit.
43    ///
44    /// # Errors
45    ///
46    /// Returns an error when `max_operations` is zero.
47    pub fn new(max_operations: usize) -> Result<Self, PipelineConfigError> {
48        if max_operations == 0 {
49            return Err(PipelineConfigError);
50        }
51        Ok(Self { max_operations })
52    }
53
54    /// Returns the maximum number of incomplete operations.
55    #[must_use]
56    pub const fn max_operations(self) -> usize {
57        self.max_operations
58    }
59}
60
61/// A zero operation-count limit is not a usable pipeline configuration.
62#[derive(Clone, Copy, Debug, Eq, PartialEq)]
63pub struct PipelineConfigError;
64
65impl std::fmt::Display for PipelineConfigError {
66    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
67        formatter.write_str("pipeline operation limit must be non-zero")
68    }
69}
70
71impl std::error::Error for PipelineConfigError {}
72
73mod private {
74    pub trait Sealed {}
75}
76
77/// Configuration accepted by [`Pipeline`].
78pub trait PipelinePolicy: private::Sealed + Copy {
79    /// Maximum number of incomplete operation records.
80    fn operation_limit(self) -> usize;
81}
82
83impl private::Sealed for NoPipeline {}
84impl PipelinePolicy for NoPipeline {
85    fn operation_limit(self) -> usize {
86        1
87    }
88}
89
90impl private::Sealed for BoundedPipeline {}
91impl PipelinePolicy for BoundedPipeline {
92    fn operation_limit(self) -> usize {
93        self.max_operations
94    }
95}
96
97/// Whether an accepted frontend operation is locally handled or forwarded.
98#[derive(Clone, Copy, Debug, Eq, PartialEq)]
99pub enum FrontendHandling {
100    /// Send the returned message to the upstream connection.
101    Forward,
102    /// Do not send the request upstream; application code will synthesize its response.
103    Local,
104}
105
106/// Application action for one successfully projected frontend message.
107#[derive(Debug, Eq, PartialEq)]
108pub enum FrontendAction {
109    /// Forward the owned message upstream.
110    Forward {
111        /// Accepted operation identity.
112        id: OperationId,
113        /// Original, unretained frontend message.
114        message: FrontendMessage,
115    },
116    /// The operation is locally handled and the message can be discarded.
117    Discard {
118        /// Accepted operation identity.
119        id: OperationId,
120    },
121}
122
123/// Position of a successfully accepted operation.
124#[derive(Debug, Eq, PartialEq)]
125pub enum FrontendAdmission {
126    /// Nothing earlier prevents this operation's response from being emitted.
127    Immediate(FrontendAction),
128    /// The operation was accepted but an earlier response must be emitted first.
129    Waiting(FrontendAction),
130}
131
132impl FrontendAdmission {
133    /// Returns the application action, discarding only the positional annotation.
134    #[must_use]
135    pub fn into_action(self) -> FrontendAction {
136        match self {
137            Self::Immediate(action) | Self::Waiting(action) => action,
138        }
139    }
140}
141
142/// Why a frontend message could not be accepted.
143#[derive(Debug, Eq, PartialEq)]
144pub enum FrontendProjectionError {
145    /// The bounded ledger is full; the unchanged message may be retried.
146    Capacity(Box<FrontendMessage>),
147    /// The message is not legal in the projected frontend protocol state.
148    Illegal {
149        /// Projected state at rejection.
150        state: PipelineState,
151        /// Unchanged illegal message.
152        message: Box<FrontendMessage>,
153    },
154}
155
156/// Application action for a backend message.
157#[derive(Debug, Eq, PartialEq)]
158pub enum BackendAction {
159    /// Emit this owned message to the downstream client now.
160    Emit(BackendMessage),
161    /// An earlier operation must complete; retry this unchanged message later.
162    Deferred(BackendMessage),
163}
164
165/// A backend message was not legal for any outstanding operation.
166#[derive(Debug, Eq, PartialEq)]
167pub struct BackendProjectionError {
168    /// Current response-side state.
169    pub state: PipelineState,
170    /// Unchanged illegal message.
171    pub message: BackendMessage,
172}
173
174/// Public summary of the pipeline's projected frontend state.
175#[derive(Clone, Copy, Debug, Eq, PartialEq)]
176pub enum PipelineState {
177    /// A simple or extended cycle may begin.
178    Ready,
179    /// Extended-query messages are being accepted.
180    Extended,
181    /// An extended error discards messages through `Sync`.
182    ExtendedError,
183    /// COPY IN accepts frontend data.
184    CopyIn,
185    /// COPY OUT accepts only backend data.
186    CopyOut,
187    /// COPY BOTH accepts data in both directions.
188    CopyBoth,
189    /// The connection has terminated.
190    Terminated,
191}
192
193/// Error returned while dispatching a pipeline message through typed middleware.
194#[derive(Debug)]
195pub enum PipelineMiddlewareError<MiddlewareError, ProjectionError> {
196    /// Middleware rejected the phase-typed message.
197    Middleware(MiddlewareError),
198    /// The pipeline rejected the original or rewritten message.
199    Projection(ProjectionError),
200}
201
202macro_rules! frontend_pipeline_phases {
203    ($consumer:ident) => {
204        $consumer! {
205            Ready => frontend_ready => backend::ReadyExternalMessage,
206            Building => frontend_building => backend::BuildingExternalMessage,
207            ExtendedError => frontend_extended_error => backend::ExtendedErrorExternalMessage,
208            SimpleCopyIn => frontend_simple_copy_in => backend::SimpleCopyInExternalMessage,
209            ExtendedCopyIn => frontend_extended_copy_in => backend::ExtendedCopyInExternalMessage,
210            SimpleCopyBoth => frontend_simple_copy_both => backend::SimpleCopyBothExternalMessage,
211            ExtendedCopyBoth => frontend_extended_copy_both => backend::ExtendedCopyBothExternalMessage,
212        }
213    };
214}
215
216macro_rules! backend_pipeline_phases {
217    ($consumer:ident) => {
218        $consumer! {
219            Asynchronous => backend_asynchronous => AsynchronousBackendMessage,
220            Simple => backend_simple => backend::SimpleInternalMessage,
221            SimpleError => backend_simple_error => backend::SimpleErrorInternalMessage,
222            ParseResponse => backend_parse_response => backend::ParseResponseInternalMessage,
223            BindResponse => backend_bind_response => backend::BindResponseInternalMessage,
224            DescribeResponse => backend_describe_response => backend::DescribeResponseInternalMessage,
225            ExecuteResponse => backend_execute_response => backend::ExecuteResponseInternalMessage,
226            CloseResponse => backend_close_response => backend::CloseResponseInternalMessage,
227            SyncResponse => backend_sync_response => backend::SyncResponseInternalMessage,
228            FunctionResponse => backend_function_response => backend::FunctionResponseInternalMessage,
229            FunctionReady => backend_function_ready => backend::FunctionReadyInternalMessage,
230            SimpleCopyInDone => backend_simple_copy_in_done => backend::SimpleCopyInDoneInternalMessage,
231            SimpleCopyInFailed => backend_simple_copy_in_failed => backend::SimpleCopyInFailedInternalMessage,
232            SimpleCopyOut => backend_simple_copy_out => backend::SimpleCopyOutInternalMessage,
233            SimpleCopyOutDone => backend_simple_copy_out_done => backend::SimpleCopyOutDoneInternalMessage,
234            SimpleCopyReady => backend_simple_copy_ready => backend::SimpleCopyReadyInternalMessage,
235            ExtendedCopyInDone => backend_extended_copy_in_done => backend::ExtendedCopyInDoneInternalMessage,
236            ExtendedCopyInFailed => backend_extended_copy_in_failed => backend::ExtendedCopyInFailedInternalMessage,
237            ExtendedCopyOut => backend_extended_copy_out => backend::ExtendedCopyOutInternalMessage,
238            ExtendedCopyOutDone => backend_extended_copy_out_done => backend::ExtendedCopyOutDoneInternalMessage,
239            SimpleCopyBoth => backend_simple_copy_both => backend::SimpleCopyBothInternalMessage,
240            SimpleCopyBothClientDone => backend_simple_copy_both_client_done => backend::SimpleCopyBothClientDoneInternalMessage,
241            SimpleCopyBothDone => backend_simple_copy_both_done => backend::SimpleCopyBothDoneInternalMessage,
242            SimpleCopyBothFailed => backend_simple_copy_both_failed => backend::SimpleCopyBothFailedInternalMessage,
243            ExtendedCopyBoth => backend_extended_copy_both => backend::ExtendedCopyBothInternalMessage,
244            ExtendedCopyBothClientDone => backend_extended_copy_both_client_done => backend::ExtendedCopyBothClientDoneInternalMessage,
245            ExtendedCopyBothDone => backend_extended_copy_both_done => backend::ExtendedCopyBothDoneInternalMessage,
246            ExtendedCopyBothFailed => backend_extended_copy_both_failed => backend::ExtendedCopyBothFailedInternalMessage,
247        }
248    };
249}
250
251macro_rules! declare_pipeline_hooks {
252    ($($phase:ident => $method:ident => $message:path),+ $(,)?) => {
253        $(
254            #[doc = concat!("Intercepts backend messages in generated `", stringify!($message), "` phase.")]
255            async fn $method(
256                &mut self,
257                _state: &mut State,
258                message: $message,
259            ) -> Result<$message, Self::Error> {
260                Ok(message)
261            }
262        )+
263    };
264}
265
266/// Async middleware for frontend messages selected from the runtime ledger phase.
267#[allow(async_fn_in_trait)]
268pub trait FrontendPipelineMiddleware<State> {
269    /// An error which prevents the message from continuing through the pipeline.
270    type Error;
271    frontend_pipeline_phases!(declare_pipeline_hooks);
272}
273
274/// Async middleware for backend messages selected from the runtime ledger phase.
275#[allow(async_fn_in_trait)]
276pub trait BackendPipelineMiddleware<State> {
277    /// An error which prevents the message from continuing through the pipeline.
278    type Error;
279    backend_pipeline_phases!(declare_pipeline_hooks);
280}
281
282impl<State> FrontendPipelineMiddleware<State> for crate::middleware::Identity {
283    type Error = Infallible;
284}
285
286impl<State> BackendPipelineMiddleware<State> for crate::middleware::Identity {
287    type Error = Infallible;
288}
289
290macro_rules! chained_pipeline_hooks {
291    ($($phase:ident => $method:ident => $message:ty),+ $(,)?) => {
292        $(
293            async fn $method(
294                &mut self,
295                state: &mut State,
296                message: $message,
297            ) -> Result<$message, Self::Error> {
298                let (first, second) = self.parts_mut();
299                let message = first
300                    .$method(state, message)
301                    .await
302                    .map_err(ChainError::First)?;
303                second
304                    .$method(state, message)
305                    .await
306                    .map_err(ChainError::Second)
307            }
308        )+
309    };
310}
311
312impl<State, First, Second> FrontendPipelineMiddleware<State> for Then<First, Second>
313where
314    First: FrontendPipelineMiddleware<State>,
315    Second: FrontendPipelineMiddleware<State>,
316{
317    type Error = ChainError<First::Error, Second::Error>;
318
319    frontend_pipeline_phases!(chained_pipeline_hooks);
320}
321
322impl<State, First, Second> BackendPipelineMiddleware<State> for Then<First, Second>
323where
324    First: BackendPipelineMiddleware<State>,
325    Second: BackendPipelineMiddleware<State>,
326{
327    type Error = ChainError<First::Error, Second::Error>;
328    backend_pipeline_phases!(chained_pipeline_hooks);
329}
330
331/// Adapts direction-wide async middleware to every typed pipeline hook.
332pub struct PipelineWireAdapter<Handler> {
333    handler: Handler,
334}
335
336impl<Handler> PipelineWireAdapter<Handler> {
337    /// Wraps direction-wide middleware for runtime phase dispatch.
338    pub const fn new(handler: Handler) -> Self {
339        Self { handler }
340    }
341
342    /// Returns the wrapped direction-wide middleware.
343    pub fn into_inner(self) -> Handler {
344        self.handler
345    }
346}
347
348/// Failure from direction-wide middleware adapted to typed pipeline dispatch.
349#[derive(Debug)]
350pub enum FrontendPipelineWireAdapterError<Error> {
351    /// The wrapped middleware rejected a message.
352    Middleware(Error),
353    /// The wrapped middleware returned a frontend message illegal in the selected phase.
354    IllegalFrontend(FrontendMessage),
355}
356
357/// Failure from backend wire middleware adapted to typed pipeline dispatch.
358#[derive(Debug)]
359pub enum BackendPipelineWireAdapterError<Error> {
360    /// The wrapped middleware rejected a message.
361    Middleware(Error),
362    /// The wrapped middleware returned a message illegal in the selected phase.
363    Illegal(BackendMessage),
364}
365
366macro_rules! pipeline_adapter_frontend_hooks {
367    ($($phase:ident => $method:ident => $message:ty),+ $(,)?) => {
368        $(
369            async fn $method(
370                &mut self,
371                state: &mut State,
372                message: $message,
373            ) -> Result<$message, Self::Error> {
374                let message: FrontendMessage = message.into();
375                let message = self
376                    .handler
377                    .intercept(state, message)
378                    .await
379                    .map_err(FrontendPipelineWireAdapterError::Middleware)?;
380                <$message>::try_from(message)
381                    .map_err(FrontendPipelineWireAdapterError::IllegalFrontend)
382            }
383        )+
384    };
385}
386
387macro_rules! pipeline_adapter_backend_hooks {
388    ($ignored:ident => $async_method:ident => AsynchronousBackendMessage, $($phase:ident => $method:ident => $message:ty),+ $(,)?) => {
389        async fn $async_method(
390            &mut self,
391            state: &mut State,
392            message: AsynchronousBackendMessage,
393        ) -> Result<AsynchronousBackendMessage, Self::Error> {
394            let message = self.handler.intercept(state, message.into_wire()).await
395                .map_err(BackendPipelineWireAdapterError::Middleware)?;
396            AsynchronousBackendMessage::try_from(message)
397                .map_err(BackendPipelineWireAdapterError::Illegal)
398        }
399        $(
400            async fn $method(
401                &mut self,
402                state: &mut State,
403                message: $message,
404            ) -> Result<$message, Self::Error> {
405                let message: BackendMessage = message.into();
406                let message = self
407                    .handler
408                    .intercept(state, message)
409                    .await
410                    .map_err(BackendPipelineWireAdapterError::Middleware)?;
411                <$message>::try_from(message).map_err(BackendPipelineWireAdapterError::Illegal)
412            }
413        )+
414    };
415}
416
417impl<State, Handler> FrontendPipelineMiddleware<State> for PipelineWireAdapter<Handler>
418where
419    Handler: MessageMiddleware<FrontendMessage, State>,
420{
421    type Error = FrontendPipelineWireAdapterError<Handler::Error>;
422    frontend_pipeline_phases!(pipeline_adapter_frontend_hooks);
423}
424
425impl<State, Handler> BackendPipelineMiddleware<State> for PipelineWireAdapter<Handler>
426where
427    Handler: MessageMiddleware<BackendMessage, State>,
428{
429    type Error = BackendPipelineWireAdapterError<Handler::Error>;
430    backend_pipeline_phases!(pipeline_adapter_backend_hooks);
431}
432
433#[derive(Clone, Copy, Debug, Eq, PartialEq)]
434enum RequestState {
435    Ready,
436    Extended { bound: bool },
437    ExtendedError,
438    CopyIn,
439    CopyOut,
440    CopyBoth,
441    Terminated,
442}
443
444impl RequestState {
445    const fn public(self) -> PipelineState {
446        match self {
447            Self::Ready => PipelineState::Ready,
448            Self::Extended { .. } => PipelineState::Extended,
449            Self::ExtendedError => PipelineState::ExtendedError,
450            Self::CopyIn => PipelineState::CopyIn,
451            Self::CopyOut => PipelineState::CopyOut,
452            Self::CopyBoth => PipelineState::CopyBoth,
453            Self::Terminated => PipelineState::Terminated,
454        }
455    }
456}
457
458#[derive(Clone, Copy, Debug, Eq, PartialEq)]
459enum Origin {
460    Forwarded,
461    Local,
462}
463
464#[derive(Clone, Copy, Debug, Eq, PartialEq)]
465enum OperationKind {
466    Query,
467    FunctionCall,
468    Parse,
469    Bind,
470    Describe,
471    Execute,
472    Close,
473    Flush,
474    Sync,
475    CopyData,
476    CopyDone,
477    CopyFail,
478    Terminate,
479}
480
481#[derive(Clone, Copy, Debug, Eq, PartialEq)]
482enum PreparedResponse {
483    Asynchronous,
484    Emit {
485        head: Operation,
486        response_state: backend::RuntimeState,
487    },
488    Deferred,
489    Illegal,
490}
491
492#[derive(Clone, Copy, Debug, Eq, PartialEq)]
493enum FrontendPhase {
494    Ready,
495    Building,
496    ExtendedError,
497    SimpleCopyIn,
498    ExtendedCopyIn,
499    SimpleCopyBoth,
500    ExtendedCopyBoth,
501}
502
503#[derive(Clone, Copy, Debug)]
504struct PreparedFrontend {
505    phase: FrontendPhase,
506    request_state: RequestState,
507}
508
509#[derive(Clone, Copy, Debug, Eq, PartialEq)]
510struct Operation {
511    id: OperationId,
512    kind: OperationKind,
513    origin: Origin,
514    discarded: bool,
515    response_state: backend::RuntimeState,
516}
517
518/// A bounded ledger coordinating independently owned frontend and backend values.
519#[derive(Debug)]
520pub struct Pipeline<P = NoPipeline> {
521    policy: P,
522    operations: VecDeque<Operation>,
523    request_state: RequestState,
524    response_state: Option<PipelineState>,
525    next_id: u64,
526    changed: Arc<Notify>,
527}
528
529impl Default for Pipeline<NoPipeline> {
530    fn default() -> Self {
531        Self::new(NoPipeline)
532    }
533}
534
535impl<P: PipelinePolicy> Pipeline<P> {
536    /// Creates an empty pipeline using `policy`.
537    #[must_use]
538    pub fn new(policy: P) -> Self {
539        Self {
540            policy,
541            operations: VecDeque::new(),
542            request_state: RequestState::Ready,
543            response_state: None,
544            next_id: 0,
545            changed: Arc::new(Notify::new()),
546        }
547    }
548
549    /// Returns the projected frontend protocol state.
550    #[must_use]
551    pub fn state(&self) -> PipelineState {
552        self.response_state
553            .unwrap_or_else(|| self.request_state.public())
554    }
555
556    /// Returns the number of incomplete lightweight operation records.
557    #[must_use]
558    pub fn len(&self) -> usize {
559        self.operations.len()
560    }
561
562    /// Reports whether no operations remain outstanding.
563    #[must_use]
564    pub fn is_empty(&self) -> bool {
565        self.operations.is_empty()
566    }
567
568    /// Projects and accepts one frontend message without retaining its payload.
569    ///
570    /// Capacity and legality failures return the original owned message. A
571    /// capacity failure does not mutate either projected state or the ledger.
572    ///
573    /// # Errors
574    ///
575    /// Returns the unchanged boxed message when capacity is exhausted or the
576    /// message is illegal in the projected state.
577    pub fn accept_frontend(
578        &mut self,
579        message: FrontendMessage,
580        handling: FrontendHandling,
581    ) -> Result<FrontendAdmission, FrontendProjectionError> {
582        let prepared = self.prepare_frontend(&message)?;
583        Ok(self.commit_frontend(prepared, message, handling))
584    }
585
586    fn prepare_frontend(
587        &self,
588        message: &FrontendMessage,
589    ) -> Result<PreparedFrontend, FrontendProjectionError> {
590        if self.operations.len() == self.policy.operation_limit() {
591            return Err(FrontendProjectionError::Capacity(Box::new(message.clone())));
592        }
593        if project_frontend(self.request_state, message).is_none() {
594            return Err(FrontendProjectionError::Illegal {
595                state: self.state(),
596                message: Box::new(message.clone()),
597            });
598        }
599        Ok(PreparedFrontend {
600            phase: frontend_phase(
601                self.request_state,
602                self.operations.front().map(|operation| operation.kind),
603            ),
604            request_state: self.request_state,
605        })
606    }
607
608    fn commit_frontend(
609        &mut self,
610        prepared: PreparedFrontend,
611        message: FrontendMessage,
612        handling: FrontendHandling,
613    ) -> FrontendAdmission {
614        let (kind, next_state) = classify_frontend(prepared.request_state, &message)
615            .expect("phase-typed frontend replacement has a ledger classification");
616        let waiting = !self.operations.is_empty();
617        let id = OperationId(self.next_id);
618        self.next_id = self.next_id.saturating_add(1);
619        self.request_state = next_state;
620        let origin = match handling {
621            FrontendHandling::Forward => Origin::Forwarded,
622            FrontendHandling::Local => Origin::Local,
623        };
624        if let Some(head) = self.operations.front_mut()
625            && let Some(event) = backend::project_external(head.response_state, &message)
626            && let Some(transition) = backend::transition(head.response_state, event)
627        {
628            head.response_state = transition.target;
629        }
630        self.operations.push_back(Operation {
631            id,
632            kind,
633            origin,
634            discarded: matches!(self.request_state, RequestState::ExtendedError)
635                && kind != OperationKind::Sync,
636            response_state: initial_response_state(kind),
637        });
638        let action = match handling {
639            FrontendHandling::Forward => FrontendAction::Forward { id, message },
640            FrontendHandling::Local => FrontendAction::Discard { id },
641        };
642        let admission = if waiting {
643            FrontendAdmission::Waiting(action)
644        } else {
645            FrontendAdmission::Immediate(action)
646        };
647        self.remove_inert_heads();
648        admission
649    }
650
651    /// Projects, asynchronously intercepts, and accepts one frontend message.
652    ///
653    /// The ledger selects the phase-specific middleware hook at runtime. The
654    /// selected hook can only return a message legal in that same phase.
655    /// Middleware is not invoked when capacity is exhausted.
656    ///
657    /// # Errors
658    ///
659    /// Returns a middleware error, an illegal original or replacement message,
660    /// or the unchanged message when capacity is exhausted.
661    pub async fn accept_frontend_typed<State, Handler>(
662        &mut self,
663        middleware: &mut Middleware<State, Handler>,
664        message: FrontendMessage,
665        handling: FrontendHandling,
666    ) -> Result<FrontendAdmission, PipelineMiddlewareError<Handler::Error, FrontendProjectionError>>
667    where
668        Handler: FrontendPipelineMiddleware<State>,
669    {
670        let prepared = self
671            .prepare_frontend(&message)
672            .map_err(PipelineMiddlewareError::Projection)?;
673
674        let message = self
675            .intercept_frontend(prepared.phase, middleware, message)
676            .await
677            .map_err(PipelineMiddlewareError::Middleware)?;
678        if !message.is_reconstructable() {
679            return Err(PipelineMiddlewareError::Projection(
680                FrontendProjectionError::Illegal {
681                    state: self.state(),
682                    message: Box::new(message),
683                },
684            ));
685        }
686        Ok(self.commit_frontend(prepared, message, handling))
687    }
688
689    /// Projects one upstream backend message and preserves response order.
690    ///
691    /// # Errors
692    ///
693    /// Returns an unchanged response which cannot belong to any outstanding operation.
694    pub fn accept_backend(
695        &mut self,
696        message: BackendMessage,
697    ) -> Result<BackendAction, BackendProjectionError> {
698        self.accept_response(None, message)
699    }
700
701    /// Intercepts an emittable backend response through its operation-typed hook.
702    ///
703    /// Responses belonging to a later operation are returned unchanged as
704    /// [`BackendAction::Deferred`] and are intercepted only when retried at the
705    /// response head. Asynchronous messages use their non-advancing hook.
706    ///
707    /// # Errors
708    ///
709    /// Returns a middleware error or an unchanged response which cannot belong
710    /// to any outstanding operation.
711    pub async fn accept_backend_typed<State, Handler>(
712        &mut self,
713        middleware: &mut Middleware<State, Handler>,
714        message: BackendMessage,
715    ) -> Result<BackendAction, PipelineMiddlewareError<Handler::Error, BackendProjectionError>>
716    where
717        Handler: BackendPipelineMiddleware<State>,
718    {
719        self.accept_response_typed(None, middleware, message).await
720    }
721
722    /// Attempts to register and emit a locally synthesized response.
723    ///
724    /// The message is returned as [`BackendAction::Deferred`] when `id` has not
725    /// reached the head. No backend payload is retained by the ledger.
726    ///
727    /// # Errors
728    ///
729    /// Returns the unchanged message when it is illegal for the named operation.
730    pub fn try_emit_local(
731        &mut self,
732        id: OperationId,
733        message: BackendMessage,
734    ) -> Result<BackendAction, BackendProjectionError> {
735        self.accept_response(Some(id), message)
736    }
737
738    /// Typed-middleware counterpart to [`Self::try_emit_local`].
739    ///
740    /// Deferred local responses are not intercepted until their operation reaches
741    /// the response head.
742    ///
743    /// # Errors
744    ///
745    /// Returns a middleware error or an illegal response for the named operation.
746    pub async fn try_emit_local_typed<State, Handler>(
747        &mut self,
748        middleware: &mut Middleware<State, Handler>,
749        id: OperationId,
750        message: BackendMessage,
751    ) -> Result<BackendAction, PipelineMiddlewareError<Handler::Error, BackendProjectionError>>
752    where
753        Handler: BackendPipelineMiddleware<State>,
754    {
755        self.accept_response_typed(Some(id), middleware, message)
756            .await
757    }
758
759    /// Waits until a local operation reaches the response head.
760    ///
761    /// Cancellation is safe: polling this future never reserves or removes a
762    /// ledger entry. The caller should then invoke [`Self::try_emit_local`].
763    pub async fn wait_until_emittable(&self, id: OperationId) {
764        loop {
765            let notified = self.changed.notified();
766            if self
767                .operations
768                .front()
769                .is_some_and(|operation| operation.id == id)
770            {
771                return;
772            }
773            notified.await;
774        }
775    }
776
777    async fn intercept_frontend<State, Handler>(
778        &self,
779        phase: FrontendPhase,
780        middleware: &mut Middleware<State, Handler>,
781        message: FrontendMessage,
782    ) -> Result<FrontendMessage, Handler::Error>
783    where
784        Handler: FrontendPipelineMiddleware<State>,
785    {
786        let (state, handler) = middleware.parts_mut();
787        macro_rules! dispatch {
788            ($message:expr, $type:path, $handler:ident, $state:ident, $method:ident) => {{
789                let Ok(typed) = <$type>::try_from($message) else {
790                    unreachable!("frontend message was prevalidated for pipeline phase")
791                };
792                $handler.$method($state, typed).await?.into()
793            }};
794        }
795
796        macro_rules! dispatch_catalogue {
797            ($($catalogue_phase:ident => $method:ident => $message_type:path),+ $(,)?) => {
798                match phase {
799                    $(
800                        FrontendPhase::$catalogue_phase =>
801                            dispatch!(message, $message_type, handler, state, $method),
802                    )+
803                }
804            };
805        }
806
807        Ok(frontend_pipeline_phases!(dispatch_catalogue))
808    }
809
810    #[allow(clippy::too_many_lines)]
811    async fn accept_response_typed<State, Handler>(
812        &mut self,
813        local_id: Option<OperationId>,
814        middleware: &mut Middleware<State, Handler>,
815        message: BackendMessage,
816    ) -> Result<BackendAction, PipelineMiddlewareError<Handler::Error, BackendProjectionError>>
817    where
818        Handler: BackendPipelineMiddleware<State>,
819    {
820        let prepared = self.prepare_response(local_id, &message);
821        if matches!(prepared, PreparedResponse::Deferred) {
822            return Ok(BackendAction::Deferred(message));
823        }
824        if matches!(prepared, PreparedResponse::Illegal) {
825            return Err(PipelineMiddlewareError::Projection(
826                BackendProjectionError {
827                    state: self.state(),
828                    message,
829                },
830            ));
831        }
832
833        let (state, handler) = middleware.parts_mut();
834        let message = match prepared {
835            PreparedResponse::Asynchronous => {
836                let Ok(typed) = AsynchronousBackendMessage::try_from(message) else {
837                    unreachable!("asynchronous response was prevalidated")
838                };
839                handler
840                    .backend_asynchronous(state, typed)
841                    .await
842                    .map_err(PipelineMiddlewareError::Middleware)?
843                    .into_wire()
844            }
845            PreparedResponse::Emit { response_state, .. } => {
846                macro_rules! dispatch {
847                    ($message:ty, $method:ident) => {{
848                        let typed = match <$message>::try_from(message) {
849                            Ok(typed) => typed,
850                            Err(message) => {
851                                return Err(PipelineMiddlewareError::Projection(
852                                    BackendProjectionError {
853                                        state: self.state(),
854                                        message,
855                                    },
856                                ));
857                            }
858                        };
859                        handler
860                            .$method(state, typed)
861                            .await
862                            .map_err(PipelineMiddlewareError::Middleware)?
863                            .into_wire()
864                    }};
865                }
866
867                macro_rules! dispatch_catalogue {
868                    ($ignored:ident => $ignored_method:ident => AsynchronousBackendMessage,
869                     $($catalogue_phase:ident => $method:ident => $message_type:path),+ $(,)?) => {
870                        match response_state {
871                            $(
872                                backend::RuntimeState::$catalogue_phase =>
873                                    dispatch!($message_type, $method),
874                            )+
875                            _ => unreachable!("response phase has no backend-selected transition"),
876                        }
877                    };
878                }
879
880                backend_pipeline_phases!(dispatch_catalogue)
881            }
882            PreparedResponse::Deferred | PreparedResponse::Illegal => unreachable!(),
883        };
884
885        if !message.is_reconstructable() {
886            return Err(PipelineMiddlewareError::Projection(
887                BackendProjectionError {
888                    state: self.state(),
889                    message,
890                },
891            ));
892        }
893        self.commit_response(prepared, message)
894            .map_err(PipelineMiddlewareError::Projection)
895    }
896
897    fn prepare_response(
898        &self,
899        local_id: Option<OperationId>,
900        message: &BackendMessage,
901    ) -> PreparedResponse {
902        if is_asynchronous(message) {
903            return PreparedResponse::Asynchronous;
904        }
905        let Some(head) = self.operations.front().copied() else {
906            return PreparedResponse::Illegal;
907        };
908        if let Some(id) = local_id {
909            if head.id != id {
910                return PreparedResponse::Deferred;
911            }
912            if head.origin != Origin::Local {
913                return PreparedResponse::Illegal;
914            }
915        } else if head.origin == Origin::Local {
916            return if self.operations.iter().skip(1).any(|operation| {
917                operation.origin == Origin::Forwarded && response_fits(*operation, message)
918            }) {
919                PreparedResponse::Deferred
920            } else {
921                PreparedResponse::Illegal
922            };
923        }
924        if head.discarded || !response_fits(head, message) {
925            return if self
926                .operations
927                .iter()
928                .skip(1)
929                .any(|operation| response_fits(*operation, message))
930            {
931                PreparedResponse::Deferred
932            } else {
933                PreparedResponse::Illegal
934            };
935        }
936        PreparedResponse::Emit {
937            head,
938            response_state: head.response_state,
939        }
940    }
941
942    fn accept_response(
943        &mut self,
944        local_id: Option<OperationId>,
945        message: BackendMessage,
946    ) -> Result<BackendAction, BackendProjectionError> {
947        let prepared = self.prepare_response(local_id, &message);
948        self.commit_response(prepared, message)
949    }
950
951    fn commit_response(
952        &mut self,
953        prepared: PreparedResponse,
954        message: BackendMessage,
955    ) -> Result<BackendAction, BackendProjectionError> {
956        let PreparedResponse::Emit {
957            head,
958            response_state,
959        } = prepared
960        else {
961            return match prepared {
962                PreparedResponse::Asynchronous => Ok(BackendAction::Emit(message)),
963                PreparedResponse::Deferred => Ok(BackendAction::Deferred(message)),
964                PreparedResponse::Illegal => Err(BackendProjectionError {
965                    state: self.state(),
966                    message,
967                }),
968                PreparedResponse::Emit { .. } => unreachable!(),
969            };
970        };
971        let event = backend::project_internal(response_state, &message)
972            .expect("response was validated against its generated backend phase");
973        let next_response_state = backend::transition(response_state, event)
974            .expect("projected backend event has a generated transition")
975            .target;
976        let terminal = response_is_terminal(head.kind, &message);
977        let error = matches!(message, BackendMessage::ErrorResponse(_));
978        let copy_state = response_copy_state(next_response_state);
979        if terminal {
980            self.operations.pop_front();
981            if error && is_extended_kind(head.kind) {
982                self.enter_extended_error();
983            }
984        } else if let Some(head) = self.operations.front_mut() {
985            head.response_state = next_response_state;
986        }
987        if !terminal {
988            self.response_state = copy_state.map(RequestState::public);
989        }
990        if let Some(state) = copy_state
991            && matches!(state, RequestState::CopyIn | RequestState::CopyBoth)
992        {
993            self.request_state = state;
994        }
995        if terminal {
996            self.response_state = None;
997            match head.kind {
998                OperationKind::Sync | OperationKind::Query => {
999                    self.request_state = RequestState::Ready;
1000                }
1001                OperationKind::Execute if !error => {
1002                    self.request_state = RequestState::Extended { bound: true };
1003                }
1004                _ => {}
1005            }
1006        }
1007        self.remove_inert_heads();
1008        self.changed.notify_waiters();
1009        Ok(BackendAction::Emit(message))
1010    }
1011
1012    fn enter_extended_error(&mut self) {
1013        self.request_state = RequestState::ExtendedError;
1014        self.response_state = None;
1015        for operation in &mut self.operations {
1016            if operation.kind == OperationKind::Sync {
1017                break;
1018            }
1019            operation.discarded = true;
1020        }
1021    }
1022
1023    fn remove_inert_heads(&mut self) {
1024        let previous_len = self.operations.len();
1025        while self.operations.front().is_some_and(|operation| {
1026            operation.kind == OperationKind::Flush
1027                || operation.kind == OperationKind::CopyData
1028                || operation.kind == OperationKind::CopyDone
1029                || operation.kind == OperationKind::CopyFail
1030                || operation.kind == OperationKind::Terminate
1031                || operation.discarded
1032        }) {
1033            self.operations.pop_front();
1034        }
1035        if self.operations.len() != previous_len {
1036            self.changed.notify_waiters();
1037        }
1038    }
1039}
1040
1041fn project_frontend(
1042    state: RequestState,
1043    message: &FrontendMessage,
1044) -> Option<(OperationKind, RequestState)> {
1045    use RequestState as S;
1046    let generated_state = match state {
1047        S::Ready => backend::RuntimeState::Ready,
1048        S::Extended { .. } => backend::RuntimeState::Building,
1049        S::ExtendedError => backend::RuntimeState::ExtendedError,
1050        S::CopyIn => backend::RuntimeState::ExtendedCopyIn,
1051        S::CopyOut => backend::RuntimeState::ExtendedCopyOut,
1052        S::CopyBoth => backend::RuntimeState::ExtendedCopyBoth,
1053        S::Terminated => backend::RuntimeState::Terminated,
1054    };
1055    backend::project_external(generated_state, message)?;
1056    classify_frontend(state, message)
1057}
1058
1059fn classify_frontend(
1060    state: RequestState,
1061    message: &FrontendMessage,
1062) -> Option<(OperationKind, RequestState)> {
1063    use FrontendMessage as F;
1064    use OperationKind as O;
1065    use RequestState as S;
1066
1067    match (state, message) {
1068        (S::Ready, F::Query(_)) => Some((O::Query, S::Ready)),
1069        (S::Ready, F::FunctionCall(_)) => Some((O::FunctionCall, S::Ready)),
1070        (S::Ready, F::Parse(_)) => Some((O::Parse, S::Extended { bound: false })),
1071        (S::Ready | S::Extended { .. }, F::Bind(_)) => Some((O::Bind, S::Extended { bound: true })),
1072        (S::Ready, F::Describe(_)) => Some((O::Describe, S::Extended { bound: false })),
1073        (S::Ready | S::Extended { .. }, F::Execute(_)) => {
1074            Some((O::Execute, S::Extended { bound: true }))
1075        }
1076        (S::Ready, F::Close(_)) => Some((O::Close, S::Extended { bound: false })),
1077        (S::Ready, F::Terminate) => Some((O::Terminate, S::Terminated)),
1078        (S::Extended { bound }, F::Parse(_)) => Some((O::Parse, S::Extended { bound })),
1079        (S::Extended { bound }, F::Describe(_)) => Some((O::Describe, S::Extended { bound })),
1080        (S::Extended { bound }, F::Close(_)) => Some((O::Close, S::Extended { bound })),
1081        (S::Extended { bound }, F::Flush) => Some((O::Flush, S::Extended { bound })),
1082        (S::Extended { .. } | S::ExtendedError, F::Sync) => Some((O::Sync, S::Ready)),
1083        (S::ExtendedError, _) => Some((classify_discard(message)?, S::ExtendedError)),
1084        (S::CopyIn, F::CopyData(_)) => Some((O::CopyData, S::CopyIn)),
1085        (S::CopyIn, F::CopyDone) => Some((O::CopyDone, S::Extended { bound: true })),
1086        (S::CopyIn, F::CopyFail(_)) => Some((O::CopyFail, S::ExtendedError)),
1087        (S::CopyBoth, F::CopyData(_)) => Some((O::CopyData, S::CopyBoth)),
1088        (S::CopyBoth, F::CopyDone) => Some((O::CopyDone, S::CopyBoth)),
1089        _ => None,
1090    }
1091}
1092
1093fn frontend_phase(state: RequestState, response_head: Option<OperationKind>) -> FrontendPhase {
1094    match (state, response_head) {
1095        (RequestState::Ready, _) => FrontendPhase::Ready,
1096        (RequestState::Extended { .. }, _) => FrontendPhase::Building,
1097        (RequestState::ExtendedError, _) => FrontendPhase::ExtendedError,
1098        (RequestState::CopyIn, Some(OperationKind::Query)) => FrontendPhase::SimpleCopyIn,
1099        (RequestState::CopyIn, Some(OperationKind::Execute)) => FrontendPhase::ExtendedCopyIn,
1100        (RequestState::CopyBoth, Some(OperationKind::Query)) => FrontendPhase::SimpleCopyBoth,
1101        (RequestState::CopyBoth, Some(OperationKind::Execute)) => FrontendPhase::ExtendedCopyBoth,
1102        (RequestState::CopyIn | RequestState::CopyBoth, _) => {
1103            unreachable!("COPY phase must belong to Query or Execute")
1104        }
1105        (RequestState::CopyOut | RequestState::Terminated, _) => {
1106            unreachable!("non-accepting frontend phase cannot be prepared")
1107        }
1108    }
1109}
1110
1111fn classify_discard(message: &FrontendMessage) -> Option<OperationKind> {
1112    Some(match message {
1113        FrontendMessage::Parse(_) => OperationKind::Parse,
1114        FrontendMessage::Bind(_) => OperationKind::Bind,
1115        FrontendMessage::Describe(_) => OperationKind::Describe,
1116        FrontendMessage::Execute(_) => OperationKind::Execute,
1117        FrontendMessage::Close(_) => OperationKind::Close,
1118        FrontendMessage::Flush => OperationKind::Flush,
1119        FrontendMessage::Query(_) => OperationKind::Query,
1120        FrontendMessage::FunctionCall(_) => OperationKind::FunctionCall,
1121        FrontendMessage::CopyData(_) => OperationKind::CopyData,
1122        FrontendMessage::CopyDone => OperationKind::CopyDone,
1123        FrontendMessage::CopyFail(_) => OperationKind::CopyFail,
1124        FrontendMessage::Terminate => OperationKind::Terminate,
1125        FrontendMessage::PasswordResponse(_) => return None,
1126        FrontendMessage::Sync => unreachable!("Sync is classified before discard"),
1127    })
1128}
1129
1130const fn initial_response_state(kind: OperationKind) -> backend::RuntimeState {
1131    use OperationKind as O;
1132    match kind {
1133        O::Query => backend::RuntimeState::Simple,
1134        O::FunctionCall => backend::RuntimeState::FunctionResponse,
1135        O::Parse => backend::RuntimeState::ParseResponse,
1136        O::Bind => backend::RuntimeState::BindResponse,
1137        O::Describe => backend::RuntimeState::DescribeResponse,
1138        O::Execute => backend::RuntimeState::ExecuteResponse,
1139        O::Close => backend::RuntimeState::CloseResponse,
1140        O::Sync => backend::RuntimeState::SyncResponse,
1141        O::Flush | O::CopyData | O::CopyDone | O::CopyFail | O::Terminate => {
1142            backend::RuntimeState::Terminated
1143        }
1144    }
1145}
1146
1147fn response_fits(operation: Operation, message: &BackendMessage) -> bool {
1148    backend::project_internal(operation.response_state, message).is_some()
1149}
1150
1151fn response_is_terminal(kind: OperationKind, message: &BackendMessage) -> bool {
1152    use BackendMessage as B;
1153    use OperationKind as O;
1154    match kind {
1155        O::Query | O::FunctionCall | O::Sync => matches!(message, B::ReadyForQuery(_)),
1156        O::Parse => matches!(message, B::ParseComplete | B::ErrorResponse(_)),
1157        O::Bind => matches!(message, B::BindComplete | B::ErrorResponse(_)),
1158        O::Describe => matches!(
1159            message,
1160            B::RowDescription(_) | B::NoData | B::ErrorResponse(_)
1161        ),
1162        O::Execute => matches!(
1163            message,
1164            B::CommandComplete(_) | B::PortalSuspended | B::ErrorResponse(_)
1165        ),
1166        O::Close => matches!(message, B::CloseComplete | B::ErrorResponse(_)),
1167        O::CopyDone => matches!(
1168            message,
1169            B::CopyDone | B::CommandComplete(_) | B::ErrorResponse(_)
1170        ),
1171        O::CopyFail => matches!(message, B::ErrorResponse(_)),
1172        O::Flush | O::CopyData | O::Terminate => true,
1173    }
1174}
1175
1176fn is_extended_kind(kind: OperationKind) -> bool {
1177    !matches!(
1178        kind,
1179        OperationKind::Query | OperationKind::FunctionCall | OperationKind::Terminate
1180    )
1181}
1182
1183fn response_copy_state(state: backend::RuntimeState) -> Option<RequestState> {
1184    use backend::RuntimeState as S;
1185    match state {
1186        S::SimpleCopyIn | S::ExtendedCopyIn => Some(RequestState::CopyIn),
1187        S::SimpleCopyOut | S::ExtendedCopyOut => Some(RequestState::CopyOut),
1188        S::SimpleCopyBoth
1189        | S::SimpleCopyBothClientDone
1190        | S::SimpleCopyBothServerDone
1191        | S::ExtendedCopyBoth
1192        | S::ExtendedCopyBothClientDone
1193        | S::ExtendedCopyBothServerDone => Some(RequestState::CopyBoth),
1194        _ => None,
1195    }
1196}
1197
1198fn is_asynchronous(message: &BackendMessage) -> bool {
1199    Demux::is_asynchronous(message)
1200}