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(crate) 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/// Sealed configuration accepted by the intermediary builder.
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(crate) 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(crate) 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(crate) 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(crate) 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(crate) 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(crate) 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(crate) 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(crate) 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(crate) struct PipelineWireAdapter<Handler> {
333    handler: Handler,
334}
335
336impl<Handler> PipelineWireAdapter<Handler> {
337    /// Wraps direction-wide middleware for runtime phase dispatch.
338    pub(crate) const fn new(handler: Handler) -> Self {
339        Self { handler }
340    }
341
342    /// Returns the wrapped direction-wide middleware.
343    pub(crate) 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(crate) 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(crate) 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(crate) 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(crate) 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(crate) 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(crate) fn len(&self) -> usize {
559        self.operations.len()
560    }
561
562    /// Reports whether no operations remain outstanding.
563    #[must_use]
564    pub(crate) 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(crate) 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            && !matches!(
592                self.request_state,
593                RequestState::CopyIn | RequestState::CopyBoth
594            )
595        {
596            return Err(FrontendProjectionError::Capacity(Box::new(message.clone())));
597        }
598        if project_frontend(self.request_state, message).is_none() {
599            return Err(FrontendProjectionError::Illegal {
600                state: self.state(),
601                message: Box::new(message.clone()),
602            });
603        }
604        Ok(PreparedFrontend {
605            phase: frontend_phase(
606                self.request_state,
607                self.operations.front().map(|operation| operation.kind),
608            ),
609            request_state: self.request_state,
610        })
611    }
612
613    fn commit_frontend(
614        &mut self,
615        prepared: PreparedFrontend,
616        message: FrontendMessage,
617        handling: FrontendHandling,
618    ) -> FrontendAdmission {
619        let (kind, next_state) = classify_frontend(prepared.request_state, &message)
620            .expect("phase-typed frontend replacement has a ledger classification");
621        let waiting = !self.operations.is_empty();
622        let id = OperationId(self.next_id);
623        self.next_id = self.next_id.saturating_add(1);
624        self.request_state = next_state;
625        let origin = match handling {
626            FrontendHandling::Forward => Origin::Forwarded,
627            FrontendHandling::Local => Origin::Local,
628        };
629        if let Some(head) = self.operations.front_mut()
630            && let Some(event) = backend::project_external(head.response_state, &message)
631            && let Some(transition) = backend::transition(head.response_state, event)
632        {
633            head.response_state = transition.target;
634        }
635        self.operations.push_back(Operation {
636            id,
637            kind,
638            origin,
639            discarded: matches!(self.request_state, RequestState::ExtendedError)
640                && kind != OperationKind::Sync,
641            response_state: initial_response_state(kind),
642        });
643        let action = match handling {
644            FrontendHandling::Forward => FrontendAction::Forward { id, message },
645            FrontendHandling::Local => FrontendAction::Discard { id },
646        };
647        let admission = if waiting {
648            FrontendAdmission::Waiting(action)
649        } else {
650            FrontendAdmission::Immediate(action)
651        };
652        self.remove_inert_heads();
653        admission
654    }
655
656    /// Projects, asynchronously intercepts, and accepts one frontend message.
657    ///
658    /// The ledger selects the phase-specific middleware hook at runtime. The
659    /// selected hook can only return a message legal in that same phase.
660    /// Middleware is not invoked when capacity is exhausted.
661    ///
662    /// # Errors
663    ///
664    /// Returns a middleware error, an illegal original or replacement message,
665    /// or the unchanged message when capacity is exhausted.
666    pub(crate) async fn accept_frontend_typed<State, Handler>(
667        &mut self,
668        middleware: &mut Middleware<State, Handler>,
669        message: FrontendMessage,
670        handling: FrontendHandling,
671    ) -> Result<FrontendAdmission, PipelineMiddlewareError<Handler::Error, FrontendProjectionError>>
672    where
673        Handler: FrontendPipelineMiddleware<State>,
674    {
675        let prepared = self
676            .prepare_frontend(&message)
677            .map_err(PipelineMiddlewareError::Projection)?;
678
679        let message = self
680            .intercept_frontend(prepared.phase, middleware, message)
681            .await
682            .map_err(PipelineMiddlewareError::Middleware)?;
683        if !message.is_reconstructable() {
684            return Err(PipelineMiddlewareError::Projection(
685                FrontendProjectionError::Illegal {
686                    state: self.state(),
687                    message: Box::new(message),
688                },
689            ));
690        }
691        Ok(self.commit_frontend(prepared, message, handling))
692    }
693
694    /// Projects one upstream backend message and preserves response order.
695    ///
696    /// # Errors
697    ///
698    /// Returns an unchanged response which cannot belong to any outstanding operation.
699    pub(crate) fn accept_backend(
700        &mut self,
701        message: BackendMessage,
702    ) -> Result<BackendAction, BackendProjectionError> {
703        self.accept_response(None, message)
704    }
705
706    /// Intercepts an emittable backend response through its operation-typed hook.
707    ///
708    /// Responses belonging to a later operation are returned unchanged as
709    /// [`BackendAction::Deferred`] and are intercepted only when retried at the
710    /// response head. Asynchronous messages use their non-advancing hook.
711    ///
712    /// # Errors
713    ///
714    /// Returns a middleware error or an unchanged response which cannot belong
715    /// to any outstanding operation.
716    pub(crate) async fn accept_backend_typed<State, Handler>(
717        &mut self,
718        middleware: &mut Middleware<State, Handler>,
719        message: BackendMessage,
720    ) -> Result<BackendAction, PipelineMiddlewareError<Handler::Error, BackendProjectionError>>
721    where
722        Handler: BackendPipelineMiddleware<State>,
723    {
724        self.accept_response_typed(None, middleware, message).await
725    }
726
727    /// Attempts to register and emit a locally synthesized response.
728    ///
729    /// The message is returned as [`BackendAction::Deferred`] when `id` has not
730    /// reached the head. No backend payload is retained by the ledger.
731    ///
732    /// # Errors
733    ///
734    /// Returns the unchanged message when it is illegal for the named operation.
735    pub(crate) fn try_emit_local(
736        &mut self,
737        id: OperationId,
738        message: BackendMessage,
739    ) -> Result<BackendAction, BackendProjectionError> {
740        self.accept_response(Some(id), message)
741    }
742
743    /// Typed-middleware counterpart to [`Self::try_emit_local`].
744    ///
745    /// Deferred local responses are not intercepted until their operation reaches
746    /// the response head.
747    ///
748    /// # Errors
749    ///
750    /// Returns a middleware error or an illegal response for the named operation.
751    pub(crate) async fn try_emit_local_typed<State, Handler>(
752        &mut self,
753        middleware: &mut Middleware<State, Handler>,
754        id: OperationId,
755        message: BackendMessage,
756    ) -> Result<BackendAction, PipelineMiddlewareError<Handler::Error, BackendProjectionError>>
757    where
758        Handler: BackendPipelineMiddleware<State>,
759    {
760        self.accept_response_typed(Some(id), middleware, message)
761            .await
762    }
763
764    /// Waits until a local operation reaches the response head.
765    ///
766    /// Cancellation is safe: polling this future never reserves or removes a
767    /// ledger entry. The caller should then invoke [`Self::try_emit_local`].
768    pub(crate) async fn wait_until_emittable(&self, id: OperationId) {
769        loop {
770            let notified = self.changed.notified();
771            if self
772                .operations
773                .front()
774                .is_some_and(|operation| operation.id == id)
775            {
776                return;
777            }
778            notified.await;
779        }
780    }
781
782    async fn intercept_frontend<State, Handler>(
783        &self,
784        phase: FrontendPhase,
785        middleware: &mut Middleware<State, Handler>,
786        message: FrontendMessage,
787    ) -> Result<FrontendMessage, Handler::Error>
788    where
789        Handler: FrontendPipelineMiddleware<State>,
790    {
791        let (state, handler) = middleware.parts_mut();
792        macro_rules! dispatch {
793            ($message:expr, $type:path, $handler:ident, $state:ident, $method:ident) => {{
794                let Ok(typed) = <$type>::try_from($message) else {
795                    unreachable!("frontend message was prevalidated for pipeline phase")
796                };
797                $handler.$method($state, typed).await?.into()
798            }};
799        }
800
801        macro_rules! dispatch_catalogue {
802            ($($catalogue_phase:ident => $method:ident => $message_type:path),+ $(,)?) => {
803                match phase {
804                    $(
805                        FrontendPhase::$catalogue_phase =>
806                            dispatch!(message, $message_type, handler, state, $method),
807                    )+
808                }
809            };
810        }
811
812        Ok(frontend_pipeline_phases!(dispatch_catalogue))
813    }
814
815    #[allow(clippy::too_many_lines)]
816    async fn accept_response_typed<State, Handler>(
817        &mut self,
818        local_id: Option<OperationId>,
819        middleware: &mut Middleware<State, Handler>,
820        message: BackendMessage,
821    ) -> Result<BackendAction, PipelineMiddlewareError<Handler::Error, BackendProjectionError>>
822    where
823        Handler: BackendPipelineMiddleware<State>,
824    {
825        let prepared = self.prepare_response(local_id, &message);
826        if matches!(prepared, PreparedResponse::Deferred) {
827            return Ok(BackendAction::Deferred(message));
828        }
829        if matches!(prepared, PreparedResponse::Illegal) {
830            return Err(PipelineMiddlewareError::Projection(
831                BackendProjectionError {
832                    state: self.state(),
833                    message,
834                },
835            ));
836        }
837
838        let (state, handler) = middleware.parts_mut();
839        let message = match prepared {
840            PreparedResponse::Asynchronous => {
841                let Ok(typed) = AsynchronousBackendMessage::try_from(message) else {
842                    unreachable!("asynchronous response was prevalidated")
843                };
844                handler
845                    .backend_asynchronous(state, typed)
846                    .await
847                    .map_err(PipelineMiddlewareError::Middleware)?
848                    .into_wire()
849            }
850            PreparedResponse::Emit { response_state, .. } => {
851                macro_rules! dispatch {
852                    ($message:ty, $method:ident) => {{
853                        let typed = match <$message>::try_from(message) {
854                            Ok(typed) => typed,
855                            Err(message) => {
856                                return Err(PipelineMiddlewareError::Projection(
857                                    BackendProjectionError {
858                                        state: self.state(),
859                                        message,
860                                    },
861                                ));
862                            }
863                        };
864                        handler
865                            .$method(state, typed)
866                            .await
867                            .map_err(PipelineMiddlewareError::Middleware)?
868                            .into_wire()
869                    }};
870                }
871
872                macro_rules! dispatch_catalogue {
873                    ($ignored:ident => $ignored_method:ident => AsynchronousBackendMessage,
874                     $($catalogue_phase:ident => $method:ident => $message_type:path),+ $(,)?) => {
875                        match response_state {
876                            $(
877                                backend::RuntimeState::$catalogue_phase =>
878                                    dispatch!($message_type, $method),
879                            )+
880                            _ => unreachable!("response phase has no backend-selected transition"),
881                        }
882                    };
883                }
884
885                backend_pipeline_phases!(dispatch_catalogue)
886            }
887            PreparedResponse::Deferred | PreparedResponse::Illegal => unreachable!(),
888        };
889
890        if !message.is_reconstructable() {
891            return Err(PipelineMiddlewareError::Projection(
892                BackendProjectionError {
893                    state: self.state(),
894                    message,
895                },
896            ));
897        }
898        self.commit_response(prepared, message)
899            .map_err(PipelineMiddlewareError::Projection)
900    }
901
902    fn prepare_response(
903        &self,
904        local_id: Option<OperationId>,
905        message: &BackendMessage,
906    ) -> PreparedResponse {
907        if is_asynchronous(message) {
908            return PreparedResponse::Asynchronous;
909        }
910        let Some(head) = self.operations.front().copied() else {
911            return PreparedResponse::Illegal;
912        };
913        if let Some(id) = local_id {
914            if head.id != id {
915                return PreparedResponse::Deferred;
916            }
917            if head.origin != Origin::Local {
918                return PreparedResponse::Illegal;
919            }
920        } else if head.origin == Origin::Local {
921            return if self.operations.iter().skip(1).any(|operation| {
922                operation.origin == Origin::Forwarded && response_fits(*operation, message)
923            }) {
924                PreparedResponse::Deferred
925            } else {
926                PreparedResponse::Illegal
927            };
928        }
929        if head.discarded || !response_fits(head, message) {
930            return if self
931                .operations
932                .iter()
933                .skip(1)
934                .any(|operation| response_fits(*operation, message))
935            {
936                PreparedResponse::Deferred
937            } else {
938                PreparedResponse::Illegal
939            };
940        }
941        PreparedResponse::Emit {
942            head,
943            response_state: head.response_state,
944        }
945    }
946
947    fn accept_response(
948        &mut self,
949        local_id: Option<OperationId>,
950        message: BackendMessage,
951    ) -> Result<BackendAction, BackendProjectionError> {
952        let prepared = self.prepare_response(local_id, &message);
953        self.commit_response(prepared, message)
954    }
955
956    fn commit_response(
957        &mut self,
958        prepared: PreparedResponse,
959        message: BackendMessage,
960    ) -> Result<BackendAction, BackendProjectionError> {
961        let PreparedResponse::Emit {
962            head,
963            response_state,
964        } = prepared
965        else {
966            return match prepared {
967                PreparedResponse::Asynchronous => Ok(BackendAction::Emit(message)),
968                PreparedResponse::Deferred => Ok(BackendAction::Deferred(message)),
969                PreparedResponse::Illegal => Err(BackendProjectionError {
970                    state: self.state(),
971                    message,
972                }),
973                PreparedResponse::Emit { .. } => unreachable!(),
974            };
975        };
976        let event = backend::project_internal(response_state, &message)
977            .expect("response was validated against its generated backend phase");
978        let next_response_state = backend::transition(response_state, event)
979            .expect("projected backend event has a generated transition")
980            .target;
981        let terminal = response_is_terminal(head.kind, &message);
982        let error = matches!(message, BackendMessage::ErrorResponse(_));
983        let copy_state = response_copy_state(next_response_state);
984        if terminal {
985            self.operations.pop_front();
986            if error && is_extended_kind(head.kind) {
987                self.enter_extended_error();
988            }
989        } else if let Some(head) = self.operations.front_mut() {
990            head.response_state = next_response_state;
991        }
992        if !terminal {
993            self.response_state = copy_state.map(RequestState::public);
994        }
995        if let Some(state) = copy_state
996            && matches!(state, RequestState::CopyIn | RequestState::CopyBoth)
997        {
998            self.request_state = state;
999        }
1000        if terminal {
1001            self.response_state = None;
1002            match head.kind {
1003                OperationKind::Sync | OperationKind::Query => {
1004                    self.request_state = RequestState::Ready;
1005                }
1006                OperationKind::Execute if !error => {
1007                    self.request_state = RequestState::Extended { bound: true };
1008                }
1009                _ => {}
1010            }
1011        }
1012        self.remove_inert_heads();
1013        self.changed.notify_waiters();
1014        Ok(BackendAction::Emit(message))
1015    }
1016
1017    fn enter_extended_error(&mut self) {
1018        self.request_state = RequestState::ExtendedError;
1019        self.response_state = None;
1020        for operation in &mut self.operations {
1021            if operation.kind == OperationKind::Sync {
1022                break;
1023            }
1024            operation.discarded = true;
1025        }
1026    }
1027
1028    fn remove_inert_heads(&mut self) {
1029        let previous_len = self.operations.len();
1030        while self.operations.front().is_some_and(|operation| {
1031            operation.kind == OperationKind::Flush
1032                || operation.kind == OperationKind::CopyData
1033                || operation.kind == OperationKind::CopyDone
1034                || operation.kind == OperationKind::CopyFail
1035                || operation.kind == OperationKind::Terminate
1036                || operation.discarded
1037        }) {
1038            self.operations.pop_front();
1039        }
1040        if self.operations.len() != previous_len {
1041            self.changed.notify_waiters();
1042        }
1043    }
1044}
1045
1046fn project_frontend(
1047    state: RequestState,
1048    message: &FrontendMessage,
1049) -> Option<(OperationKind, RequestState)> {
1050    use RequestState as S;
1051    let generated_state = match state {
1052        S::Ready => backend::RuntimeState::Ready,
1053        S::Extended { .. } => backend::RuntimeState::Building,
1054        S::ExtendedError => backend::RuntimeState::ExtendedError,
1055        S::CopyIn => backend::RuntimeState::ExtendedCopyIn,
1056        S::CopyOut => backend::RuntimeState::ExtendedCopyOut,
1057        S::CopyBoth => backend::RuntimeState::ExtendedCopyBoth,
1058        S::Terminated => backend::RuntimeState::Terminated,
1059    };
1060    backend::project_external(generated_state, message)?;
1061    classify_frontend(state, message)
1062}
1063
1064fn classify_frontend(
1065    state: RequestState,
1066    message: &FrontendMessage,
1067) -> Option<(OperationKind, RequestState)> {
1068    use FrontendMessage as F;
1069    use OperationKind as O;
1070    use RequestState as S;
1071
1072    match (state, message) {
1073        (S::Ready, F::Query(_)) => Some((O::Query, S::Ready)),
1074        (S::Ready, F::FunctionCall(_)) => Some((O::FunctionCall, S::Ready)),
1075        (S::Ready, F::Parse(_)) => Some((O::Parse, S::Extended { bound: false })),
1076        (S::Ready | S::Extended { .. }, F::Bind(_)) => Some((O::Bind, S::Extended { bound: true })),
1077        (S::Ready, F::Describe(_)) => Some((O::Describe, S::Extended { bound: false })),
1078        (S::Ready | S::Extended { .. }, F::Execute(_)) => {
1079            Some((O::Execute, S::Extended { bound: true }))
1080        }
1081        (S::Ready, F::Close(_)) => Some((O::Close, S::Extended { bound: false })),
1082        (S::Ready, F::Terminate) => Some((O::Terminate, S::Terminated)),
1083        (S::Extended { bound }, F::Parse(_)) => Some((O::Parse, S::Extended { bound })),
1084        (S::Extended { bound }, F::Describe(_)) => Some((O::Describe, S::Extended { bound })),
1085        (S::Extended { bound }, F::Close(_)) => Some((O::Close, S::Extended { bound })),
1086        (S::Extended { bound }, F::Flush) => Some((O::Flush, S::Extended { bound })),
1087        (S::Extended { .. } | S::ExtendedError, F::Sync) => Some((O::Sync, S::Ready)),
1088        (S::ExtendedError, _) => Some((classify_discard(message)?, S::ExtendedError)),
1089        (S::CopyIn, F::CopyData(_)) => Some((O::CopyData, S::CopyIn)),
1090        (S::CopyIn, F::CopyDone) => Some((O::CopyDone, S::Extended { bound: true })),
1091        (S::CopyIn, F::CopyFail(_)) => Some((O::CopyFail, S::ExtendedError)),
1092        (S::CopyBoth, F::CopyData(_)) => Some((O::CopyData, S::CopyBoth)),
1093        (S::CopyBoth, F::CopyDone) => Some((O::CopyDone, S::CopyBoth)),
1094        _ => None,
1095    }
1096}
1097
1098fn frontend_phase(state: RequestState, response_head: Option<OperationKind>) -> FrontendPhase {
1099    match (state, response_head) {
1100        (RequestState::Ready, _) => FrontendPhase::Ready,
1101        (RequestState::Extended { .. }, _) => FrontendPhase::Building,
1102        (RequestState::ExtendedError, _) => FrontendPhase::ExtendedError,
1103        (RequestState::CopyIn, Some(OperationKind::Query)) => FrontendPhase::SimpleCopyIn,
1104        (RequestState::CopyIn, Some(OperationKind::Execute)) => FrontendPhase::ExtendedCopyIn,
1105        (RequestState::CopyBoth, Some(OperationKind::Query)) => FrontendPhase::SimpleCopyBoth,
1106        (RequestState::CopyBoth, Some(OperationKind::Execute)) => FrontendPhase::ExtendedCopyBoth,
1107        (RequestState::CopyIn | RequestState::CopyBoth, _) => {
1108            unreachable!("COPY phase must belong to Query or Execute")
1109        }
1110        (RequestState::CopyOut | RequestState::Terminated, _) => {
1111            unreachable!("non-accepting frontend phase cannot be prepared")
1112        }
1113    }
1114}
1115
1116fn classify_discard(message: &FrontendMessage) -> Option<OperationKind> {
1117    Some(match message {
1118        FrontendMessage::Parse(_) => OperationKind::Parse,
1119        FrontendMessage::Bind(_) => OperationKind::Bind,
1120        FrontendMessage::Describe(_) => OperationKind::Describe,
1121        FrontendMessage::Execute(_) => OperationKind::Execute,
1122        FrontendMessage::Close(_) => OperationKind::Close,
1123        FrontendMessage::Flush => OperationKind::Flush,
1124        FrontendMessage::Query(_) => OperationKind::Query,
1125        FrontendMessage::FunctionCall(_) => OperationKind::FunctionCall,
1126        FrontendMessage::CopyData(_) => OperationKind::CopyData,
1127        FrontendMessage::CopyDone => OperationKind::CopyDone,
1128        FrontendMessage::CopyFail(_) => OperationKind::CopyFail,
1129        FrontendMessage::Terminate => OperationKind::Terminate,
1130        FrontendMessage::PasswordResponse(_) => return None,
1131        FrontendMessage::Sync => unreachable!("Sync is classified before discard"),
1132    })
1133}
1134
1135const fn initial_response_state(kind: OperationKind) -> backend::RuntimeState {
1136    use OperationKind as O;
1137    match kind {
1138        O::Query => backend::RuntimeState::Simple,
1139        O::FunctionCall => backend::RuntimeState::FunctionResponse,
1140        O::Parse => backend::RuntimeState::ParseResponse,
1141        O::Bind => backend::RuntimeState::BindResponse,
1142        O::Describe => backend::RuntimeState::DescribeResponse,
1143        O::Execute => backend::RuntimeState::ExecuteResponse,
1144        O::Close => backend::RuntimeState::CloseResponse,
1145        O::Sync => backend::RuntimeState::SyncResponse,
1146        O::Flush | O::CopyData | O::CopyDone | O::CopyFail | O::Terminate => {
1147            backend::RuntimeState::Terminated
1148        }
1149    }
1150}
1151
1152fn response_fits(operation: Operation, message: &BackendMessage) -> bool {
1153    backend::project_internal(operation.response_state, message).is_some()
1154}
1155
1156fn response_is_terminal(kind: OperationKind, message: &BackendMessage) -> bool {
1157    use BackendMessage as B;
1158    use OperationKind as O;
1159    match kind {
1160        O::Query | O::FunctionCall | O::Sync => matches!(message, B::ReadyForQuery(_)),
1161        O::Parse => matches!(message, B::ParseComplete | B::ErrorResponse(_)),
1162        O::Bind => matches!(message, B::BindComplete | B::ErrorResponse(_)),
1163        O::Describe => matches!(
1164            message,
1165            B::RowDescription(_) | B::NoData | B::ErrorResponse(_)
1166        ),
1167        O::Execute => matches!(
1168            message,
1169            B::CommandComplete(_) | B::PortalSuspended | B::ErrorResponse(_)
1170        ),
1171        O::Close => matches!(message, B::CloseComplete | B::ErrorResponse(_)),
1172        O::CopyDone => matches!(
1173            message,
1174            B::CopyDone | B::CommandComplete(_) | B::ErrorResponse(_)
1175        ),
1176        O::CopyFail => matches!(message, B::ErrorResponse(_)),
1177        O::Flush | O::CopyData | O::Terminate => true,
1178    }
1179}
1180
1181fn is_extended_kind(kind: OperationKind) -> bool {
1182    !matches!(
1183        kind,
1184        OperationKind::Query | OperationKind::FunctionCall | OperationKind::Terminate
1185    )
1186}
1187
1188fn response_copy_state(state: backend::RuntimeState) -> Option<RequestState> {
1189    use backend::RuntimeState as S;
1190    match state {
1191        S::SimpleCopyIn | S::ExtendedCopyIn => Some(RequestState::CopyIn),
1192        S::SimpleCopyOut | S::ExtendedCopyOut => Some(RequestState::CopyOut),
1193        S::SimpleCopyBoth
1194        | S::SimpleCopyBothClientDone
1195        | S::SimpleCopyBothServerDone
1196        | S::ExtendedCopyBoth
1197        | S::ExtendedCopyBothClientDone
1198        | S::ExtendedCopyBothServerDone => Some(RequestState::CopyBoth),
1199        _ => None,
1200    }
1201}
1202
1203fn is_asynchronous(message: &BackendMessage) -> bool {
1204    Demux::is_asynchronous(message)
1205}