@@ -30,6 +30,7 @@ use mz_ore::collections::CollectionExt;
3030use mz_ore:: id_gen:: { IdAllocator , IdAllocatorInnerBitSet , MAX_ORG_ID , org_id_conn_bits} ;
3131use mz_ore:: instrument;
3232use mz_ore:: now:: { EpochMillis , NowFn , to_datetime} ;
33+ use mz_ore:: str:: StrExt ;
3334use mz_ore:: task:: AbortOnDropHandle ;
3435use mz_ore:: thread:: JoinOnDropHandle ;
3536use mz_ore:: tracing:: OpenTelemetryContext ;
@@ -57,7 +58,7 @@ use crate::command::{
5758} ;
5859use crate :: coord:: { Coordinator , ExecuteContextGuard } ;
5960use crate :: error:: AdapterError ;
60- use crate :: metrics:: Metrics ;
61+ use crate :: metrics:: { self , Metrics } ;
6162use crate :: session:: {
6263 EndTransactionAction , PreparedStatement , Session , SessionConfig , StateRevision , TransactionId ,
6364} ;
@@ -735,12 +736,30 @@ impl SessionClient {
735736 ) -> Result < ( ExecuteResponse , Instant ) , AdapterError > {
736737 let execute_started = Instant :: now ( ) ;
737738
739+ let mut outer_ctx_extra = outer_ctx_extra;
740+
741+ // Unroll SQL `EXECUTE <prepared> (...)` so the inner statement
742+ // flows through `try_frontend_peek` below, rather than being
743+ // re-dispatched via `Command::Execute` from the coordinator's
744+ // `Plan::Execute` handler. Without this, a prepared statement
745+ // would route differently from the same statement issued
746+ // directly.
747+ //
748+ // On a successful unroll, `unroll_sql_execute` also returns a
749+ // catalog snapshot (threaded through to avoid taking a second
750+ // one) and begins EXECUTE-level statement logging on the outer
751+ // portal — so `mz_statement_execution_history` records
752+ // `EXECUTE foo (...)`, not the inner SQL — installing the
753+ // resulting `ExecuteContextGuard` into `outer_ctx_extra`.
754+ let ( portal_name, catalog) = self
755+ . unroll_sql_execute ( portal_name, & mut outer_ctx_extra)
756+ . await ?;
757+
738758 // Attempt peek sequencing in the session task.
739759 // If unsupported, fall back to the Coordinator path.
740760 // TODO(peek-seq): wire up cancel_future
741- let mut outer_ctx_extra = outer_ctx_extra;
742761 let peek_result = self
743- . try_frontend_peek ( & portal_name, & mut outer_ctx_extra)
762+ . try_frontend_peek ( & portal_name, catalog , & mut outer_ctx_extra)
744763 . await ?;
745764 if let Some ( resp) = peek_result {
746765 debug ! ( "frontend peek succeeded" ) ;
@@ -769,6 +788,236 @@ impl SessionClient {
769788 Ok ( ( response, execute_started) )
770789 }
771790
791+ /// If the named portal binds a SQL `EXECUTE <prepared>`, resolve the
792+ /// prepared statement, install a fresh portal for the inner statement
793+ /// (carrying the EXECUTE's actual parameter values), and return that
794+ /// portal's name so the caller can run `try_frontend_peek` against it.
795+ ///
796+ /// Only ever unrolls one level: the parser rejects
797+ /// `PREPARE foo AS EXECUTE bar` (matching Postgres), so the inner
798+ /// statement is guaranteed not to be another `EXECUTE`. A failsafe below
799+ /// surfaces an internal error if that invariant is ever violated.
800+ ///
801+ /// When the portal does not bind an `EXECUTE` — the common case —
802+ /// returns the original portal name and `None`, costing only a portal
803+ /// lookup. On unroll, also returns the catalog snapshot taken here so
804+ /// the caller can thread it into `try_frontend_peek`, which reuses it
805+ /// instead of taking its own.
806+ async fn unroll_sql_execute (
807+ & mut self ,
808+ portal_name : String ,
809+ outer_ctx_extra : & mut Option < ExecuteContextGuard > ,
810+ ) -> Result < ( String , Option < Arc < Catalog > > ) , AdapterError > {
811+ let ( stmt, params, outer_logging, outer_lifecycle_timestamps) = {
812+ let session = self . session . as_ref ( ) . expect ( "SessionClient invariant" ) ;
813+ let portal = match session. get_portal_unverified ( & portal_name) {
814+ Some ( p) => p,
815+ // No portal: let `try_frontend_peek` surface the
816+ // standard "missing portal" error.
817+ None => return Ok ( ( portal_name, None ) ) ,
818+ } ;
819+ match & portal. stmt {
820+ Some ( stmt) => (
821+ Arc :: clone ( stmt) ,
822+ portal. parameters . clone ( ) ,
823+ Arc :: clone ( & portal. logging ) ,
824+ portal. lifecycle_timestamps . clone ( ) ,
825+ ) ,
826+ None => return Ok ( ( portal_name, None ) ) ,
827+ }
828+ } ;
829+
830+ // Only EXECUTE statements need unrolling. Bail out before taking a
831+ // catalog snapshot in the (overwhelmingly common) non-EXECUTE case.
832+ if !matches ! ( & * stmt, Statement :: Execute ( _) ) {
833+ return Ok ( ( portal_name, None ) ) ;
834+ }
835+
836+ let catalog = self . catalog_snapshot ( "unroll_sql_execute" ) . await ;
837+
838+ // Validate the outer EXECUTE portal against the (possibly newer)
839+ // catalog: ensures the recorded portal description still matches
840+ // what describing the EXECUTE would produce now.
841+ {
842+ let session = self . session . as_mut ( ) . expect ( "SessionClient invariant" ) ;
843+ Coordinator :: verify_portal ( & catalog, session, & portal_name) ?;
844+ }
845+
846+ // Bump query_total for the outer EXECUTE itself. The inner
847+ // statement gets its own increment inside `try_frontend_peek_inner`
848+ // (or, on bailout, in the coordinator's `handle_execute`).
849+ {
850+ let session = self . session . as_ref ( ) . expect ( "SessionClient invariant" ) ;
851+ session
852+ . metrics ( )
853+ . query_total ( & [
854+ metrics:: session_type_label_value ( session. user ( ) ) ,
855+ metrics:: statement_type_label_value ( & stmt) ,
856+ ] )
857+ . inc ( ) ;
858+ }
859+
860+ // Begin EXECUTE-level statement logging up front, so that planning
861+ // errors below produce an `Errored` end-event in
862+ // `mz_statement_execution_history` rather than no entry at all.
863+ //
864+ // We pass the *outer* portal's `logging` and pgwire-bound `params`
865+ // so the recorded entry shows the user-visible `EXECUTE foo (...)`,
866+ // not the inner SQL. The id (if any) moves into `outer_ctx_extra`
867+ // below for `try_frontend_peek` to retire; on planning error we
868+ // explicitly emit an `Errored` end-event below.
869+ let began_outer_logging = outer_ctx_extra. is_none ( ) ;
870+ let logging_id: Option < crate :: statement_logging:: StatementLoggingId > =
871+ if began_outer_logging {
872+ let session = self . session . as_mut ( ) . expect ( "SessionClient invariant" ) ;
873+ let result = self
874+ . peek_client
875+ . statement_logging_frontend
876+ . begin_statement_execution (
877+ session,
878+ & params,
879+ & outer_logging,
880+ catalog. system_config ( ) ,
881+ outer_lifecycle_timestamps,
882+ ) ;
883+ if let Some ( ( id, began_execution, mseh_update, prepared_statement) ) = result {
884+ self . peek_client . log_began_execution (
885+ began_execution,
886+ mseh_update,
887+ prepared_statement,
888+ ) ;
889+ Some ( id)
890+ } else {
891+ None
892+ }
893+ } else {
894+ None
895+ } ;
896+
897+ let new_portal_name = match self . install_inner_portal_for_execute ( & catalog, & stmt, & params)
898+ {
899+ Ok ( name) => name,
900+ Err ( err) => {
901+ if let Some ( id) = logging_id {
902+ self . peek_client . log_ended_execution (
903+ id,
904+ StatementEndedExecutionReason :: Errored {
905+ error : err. to_string ( ) ,
906+ } ,
907+ ) ;
908+ }
909+ return Err ( err) ;
910+ }
911+ } ;
912+
913+ // Hand off to `outer_ctx_extra` whenever we entered the begin path
914+ // for the outer EXECUTE — even if `begin_statement_execution`
915+ // returned `None` (sampling decided not to sample, or logging is
916+ // disabled for the user). This mirrors the original coord path,
917+ // which always installs a guard via
918+ // `ExecuteContextGuard::new(maybe_uuid, ...)`. Without this, the
919+ // inner portal would be treated as a fresh statement by
920+ // `try_frontend_peek` (or the fallback `Command::Execute` path)
921+ // and re-account its bytes against
922+ // `mz_statement_logging_unsampled_bytes`, double-counting the
923+ // inner SQL.
924+ if began_outer_logging {
925+ // Soft invariant: `try_frontend_peek` takes ownership of
926+ // `outer_ctx_extra` immediately, so this guard's `Drop` is
927+ // unreachable on the normal flow and the dummy channel is
928+ // never used. If a panic does fire `Drop` between here and
929+ // that takeover, the `Aborted` end-event is silently lost
930+ // — an acceptable trade given the panic implies the
931+ // connection is going down anyway.
932+ let ( dummy_tx, _dummy_rx) = mpsc:: unbounded_channel ( ) ;
933+ * outer_ctx_extra = Some ( ExecuteContextGuard :: new ( logging_id, dummy_tx) ) ;
934+ }
935+
936+ Ok ( ( new_portal_name, Some ( catalog) ) )
937+ }
938+
939+ /// Helper for [`Self::unroll_sql_execute`]: plans the outer
940+ /// `Statement::Execute`, verifies the referenced prepared statement, and
941+ /// installs a fresh portal carrying the inner statement plus the
942+ /// EXECUTE's bound parameter values. Returns the new portal's name.
943+ ///
944+ /// Split out so [`Self::unroll_sql_execute`] can wrap the fallible work
945+ /// in a single error-handling site that emits an `Errored` end-event
946+ /// for the EXECUTE-level statement-logging entry.
947+ fn install_inner_portal_for_execute (
948+ & mut self ,
949+ catalog : & Arc < Catalog > ,
950+ stmt : & Arc < Statement < Raw > > ,
951+ params : & mz_sql:: plan:: Params ,
952+ ) -> Result < String , AdapterError > {
953+ use mz_sql:: plan:: Plan ;
954+
955+ let execute_plan = {
956+ let session = self . session . as_mut ( ) . expect ( "SessionClient invariant" ) ;
957+ let conn_catalog = catalog. for_session ( session) ;
958+ let ( resolved_stmt, resolved_ids) =
959+ mz_sql:: names:: resolve ( & conn_catalog, ( * * stmt) . clone ( ) ) ?;
960+ let pcx = session. pcx ( ) ;
961+ let plan = mz_sql:: plan:: plan (
962+ Some ( pcx) ,
963+ & conn_catalog,
964+ resolved_stmt,
965+ params,
966+ & resolved_ids,
967+ ) ?;
968+ match plan {
969+ Plan :: Execute ( plan) => plan,
970+ other => {
971+ // Planning a `Statement::Execute` must yield
972+ // `Plan::Execute`. If it doesn't, the planner
973+ // contract is broken.
974+ return Err ( AdapterError :: Internal ( format ! (
975+ "planning Statement::Execute yielded unexpected plan: {:?}" ,
976+ mz_sql:: plan:: PlanKind :: from( & other) ,
977+ ) ) ) ;
978+ }
979+ }
980+ } ;
981+
982+ // Verify and install the inner portal. Mirrors
983+ // `Coordinator::sequence_execute`. The new portal carries the inner
984+ // prepared statement's `logging`, but `try_frontend_peek` will see
985+ // `outer_ctx_extra=Some(...)` and inherit the EXECUTE-level logging
986+ // instead of starting fresh from this portal.
987+ let session = self . session . as_mut ( ) . expect ( "SessionClient invariant" ) ;
988+ Coordinator :: verify_prepared_statement ( catalog, session, & execute_plan. name ) ?;
989+ let ps = session
990+ . get_prepared_statement_unverified ( & execute_plan. name )
991+ . expect ( "verified above" ) ;
992+ let inner_stmt = ps. stmt ( ) . cloned ( ) ;
993+ let inner_desc = ps. desc ( ) . clone ( ) ;
994+ let state_revision = ps. state_revision ;
995+ let inner_logging = Arc :: clone ( ps. logging ( ) ) ;
996+
997+ // Failsafe: `PREPARE foo AS EXECUTE bar` is rejected by the parser,
998+ // so the resolved inner statement must not be another `EXECUTE`. If
999+ // that ever changes, we'd silently skip frontend sequencing for the
1000+ // deeper EXECUTEs — surface it as an internal error instead.
1001+ if let Some ( inner) = inner_stmt. as_ref ( ) {
1002+ if matches ! ( inner, Statement :: Execute ( _) ) {
1003+ return Err ( AdapterError :: Internal ( format ! (
1004+ "nested EXECUTE: prepared statement {} resolves to another EXECUTE; \
1005+ parser should reject `PREPARE ... AS EXECUTE ...`",
1006+ execute_plan. name. quoted( ) ,
1007+ ) ) ) ;
1008+ }
1009+ }
1010+
1011+ session. create_new_portal (
1012+ inner_stmt,
1013+ inner_logging,
1014+ inner_desc,
1015+ execute_plan. params ,
1016+ Vec :: new ( ) ,
1017+ state_revision,
1018+ )
1019+ }
1020+
7721021 fn now ( & self ) -> EpochMillis {
7731022 ( self . inner ( ) . now ) ( )
7741023 }
@@ -1172,12 +1421,13 @@ impl SessionClient {
11721421 pub ( crate ) async fn try_frontend_peek (
11731422 & mut self ,
11741423 portal_name : & str ,
1424+ catalog : Option < Arc < Catalog > > ,
11751425 outer_ctx_extra : & mut Option < ExecuteContextGuard > ,
11761426 ) -> Result < Option < ExecuteResponse > , AdapterError > {
11771427 if self . enable_frontend_peek_sequencing {
11781428 let session = self . session . as_mut ( ) . expect ( "SessionClient invariant" ) ;
11791429 self . peek_client
1180- . try_frontend_peek ( portal_name, session, outer_ctx_extra)
1430+ . try_frontend_peek ( portal_name, catalog , session, outer_ctx_extra)
11811431 . await
11821432 } else {
11831433 Ok ( None )
0 commit comments