Skip to main content

reddb_server/wire/redwire/
session.rs

1//! Per-connection RedWire session: handshake → frame loop → bye.
2//!
3//! Dispatches the full RedWire frame set:
4//!   - Hello / AuthResponse (handshake only — once)
5//!   - Query / BulkInsert / Get / Delete (data plane)
6//!   - QueryBinary / BulkInsertBinary / BulkInsertPrevalidated
7//!     (binary fast paths)
8//!   - BulkStreamStart/Rows/Commit (streaming bulk)
9//!   - Prepare / ExecutePrepared (prepared statements)
10//!   - Ping / Pong / Bye (lifecycle)
11
12use std::io;
13use std::sync::Arc;
14
15use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt};
16use tokio::sync::mpsc;
17
18use crate::application::{OperationContext, OperationContextFactory, OperationContextInput};
19use crate::auth::store::AuthStore;
20use crate::auth::Role;
21use crate::runtime::query_request::{
22    ParamValue, PreparedRegistry, QueryRequest, QueryRequestExecutor,
23};
24use crate::runtime::RedDBRuntime;
25use crate::serde_json::{self, Value as JsonValue};
26use reddb_wire::query_with_params::{
27    decode_query_with_params_request, ParamValue as RedWireParamValue, FEATURE_PARAMS,
28};
29use reddb_wire::redwire::operations::{
30    decode_delete_payload, decode_get_payload, decode_insert_dispatch_payload,
31    encode_bulk_ok_payload_from_json_id_literals, encode_delete_ok_payload,
32    encode_get_result_payload,
33};
34use reddb_wire::redwire::scram::{ScramServerHandshake, ScramServerInput, ScramServerOutput};
35
36use super::auth::{build_auth_ok, pick_auth_method, validate_auth_response, AuthOutcome};
37use super::validate_minor_version;
38use reddb_wire::redwire::handshake::{
39    build_auth_fail_payload, build_auth_ok_frame_from_payload, build_hello_ack_frame,
40    expect_auth_response_payload, parse_auth_response_oauth_jwt, Hello,
41};
42use reddb_wire::redwire::{
43    build_dispatch_reply_frame, build_error_frame_lossy, build_reply_frame,
44    choose_hello_minor_version, decode_frame, encode_frame, read_frame_async,
45    rewrap_length_prefixed_handler_response, Frame, MessageDirection, MessageKind, REDWIRE_MAGIC,
46};
47
48enum QueryResponse {
49    Frame(Frame),
50    Legacy(crate::wire::listener::LegacyQueryResponse, u64),
51}
52
53struct AuthedSession {
54    username: String,
55    role: Role,
56    tenant: Option<String>,
57    session_id: String,
58    _lease: Option<crate::auth::store::ConnectionLease>,
59}
60
61fn acquire_local_connection_lease(
62    auth_store: Option<&Arc<AuthStore>>,
63    tenant: Option<&str>,
64    username: &str,
65) -> Result<Option<crate::auth::store::ConnectionLease>, String> {
66    let Some(store) = auth_store else {
67        return Ok(None);
68    };
69    let principal = crate::auth::UserId::from_parts(tenant, username);
70    if store.get_user(tenant, username).is_none() {
71        return Ok(None);
72    }
73    store
74        .acquire_connection_lease(&principal)
75        .map(Some)
76        .map_err(|_| "authentication refused".to_string())
77}
78
79async fn send_auth_refusal<S>(stream: &mut S, correlation_id: u64, reason: &str) -> io::Result<()>
80where
81    S: AsyncWrite + Unpin,
82{
83    let frame = encode_frame(&reply_frame_or_io_error(
84        correlation_id,
85        MessageKind::AuthFail,
86        build_auth_fail_payload(reason),
87    )?);
88    stream.write_all(&frame).await
89}
90
91struct RedWireAuthenticatedSessionPolicy;
92
93impl crate::server::route_catalog::CommandPolicyEngine for RedWireAuthenticatedSessionPolicy {
94    fn allows(
95        &self,
96        _ctx: &OperationContext,
97        _command: &crate::server::route_catalog::CommandSpec,
98    ) -> bool {
99        // Reaching the frame loop means the RedWire handshake accepted this
100        // session (including explicitly configured anonymous sessions). The
101        // catalog authorizer still owns command lookup and the mandatory
102        // authorization consult, while this adapter preserves the handshake's
103        // existing credential decision.
104        true
105    }
106}
107
108fn redwire_operation_context(
109    session: &AuthedSession,
110    connection_id: u64,
111    correlation_id: u64,
112) -> OperationContext {
113    OperationContextFactory::build(OperationContextInput {
114        request_id: Some(format!("redwire-{}-{correlation_id}", session.session_id)),
115        connection_id: Some(connection_id),
116        principal: Some(session.username.clone()),
117        tenant: session.tenant.clone(),
118        ..OperationContextInput::default()
119    })
120}
121
122fn authorize_redwire_frame<P: crate::server::route_catalog::CommandPolicyEngine + ?Sized>(
123    ctx: &OperationContext,
124    kind: MessageKind,
125    policy: &P,
126) -> Result<(), crate::server::route_catalog::CommandAuthorizationError> {
127    let Some(command_id) = redwire_command_id(kind) else {
128        return Ok(());
129    };
130    crate::server::route_catalog::CommandAuthorizer::new(
131        crate::server::discovered_route_catalog(),
132        policy,
133    )
134    .authorize(ctx, command_id)
135}
136
137/// Canonical command id for every RedWire frame kind this server dispatches.
138/// `Hello` and `AuthResponse` are handled by the handshake; the other bound
139/// kinds have real arms in `handle_session`. `None` means the wire vocabulary
140/// declares the kind but this server does not dispatch it.
141pub(crate) const fn redwire_command_id(kind: MessageKind) -> Option<&'static str> {
142    match kind {
143        MessageKind::Hello
144        | MessageKind::HelloAck
145        | MessageKind::AuthRequest
146        | MessageKind::AuthResponse
147        | MessageKind::AuthOk
148        | MessageKind::AuthFail => Some("auth.login"),
149
150        MessageKind::Bye | MessageKind::Ping | MessageKind::Pong => Some("health.live"),
151
152        MessageKind::Query
153        | MessageKind::Result
154        | MessageKind::Error
155        | MessageKind::QueryBinary
156        | MessageKind::Prepare
157        | MessageKind::PreparedOk
158        | MessageKind::ExecutePrepared
159        | MessageKind::QueryWithParams
160        | MessageKind::QueueWaitOpen
161        | MessageKind::QueueEventPush
162        | MessageKind::QueueWaitTimeout
163        | MessageKind::MovedRedirect
164        | MessageKind::Compress => Some("query.execute"),
165
166        MessageKind::BulkInsert
167        | MessageKind::BulkOk
168        | MessageKind::BulkInsertBinary
169        | MessageKind::BulkInsertPrevalidated
170        | MessageKind::BulkStreamStart
171        | MessageKind::BulkStreamRows
172        | MessageKind::BulkStreamCommit
173        | MessageKind::BulkStreamAck => Some("collections.batch.insert"),
174
175        MessageKind::Get => Some("collections.entities.get"),
176        MessageKind::Delete | MessageKind::DeleteOk => Some("collections.entities.delete"),
177
178        MessageKind::OpenStream
179        | MessageKind::OpenAck
180        | MessageKind::RowDescription
181        | MessageKind::StreamEnd => Some("streams.query.output"),
182        MessageKind::StreamChunk => Some("streams.input"),
183        MessageKind::Cancel | MessageKind::StreamCancel | MessageKind::StreamError => {
184            Some("streams.query.cancel")
185        }
186
187        MessageKind::SetSession
188        | MessageKind::Notice
189        | MessageKind::VectorSearch
190        | MessageKind::GraphTraverse => None,
191    }
192}
193
194struct RedWireExecutionContextGuard {
195    previous_connection_id: u64,
196    previous_tenant: Option<String>,
197    previous_identity: Option<(String, Role)>,
198    previous_principal: Option<crate::auth::UserId>,
199}
200
201impl RedWireExecutionContextGuard {
202    fn install(ctx: &OperationContext, role: Role) -> Self {
203        let previous_connection_id = crate::runtime::execution_context::current_connection_id();
204        if let Some(connection_id) = ctx.connection_id {
205            crate::runtime::execution_context::set_current_connection_id(connection_id);
206        }
207        let previous_tenant = crate::runtime::execution_context::current_tenant();
208        let previous_identity = crate::runtime::execution_context::current_auth_identity();
209        let previous_principal = crate::runtime::execution_context::current_auth_principal();
210        match &ctx.tenant {
211            Some(tenant) => crate::runtime::execution_context::set_current_tenant(tenant.clone()),
212            None => crate::runtime::execution_context::clear_current_tenant(),
213        }
214        // Anonymous sessions exist only when server auth is disabled (the
215        // handshake refuses anonymous otherwise) and historically ran with
216        // NO installed identity — enforcement off. Installing the handshake's
217        // `Read` role here would reject every anonymous write (issue #2149
218        // review). The tenant install above still applies for RLS.
219        if ctx.audit_principal == "anonymous" {
220            crate::runtime::execution_context::clear_current_auth_identity();
221        } else {
222            crate::runtime::execution_context::set_current_auth_principal(
223                crate::auth::UserId::from_parts(ctx.tenant.as_deref(), &ctx.audit_principal),
224                role,
225            );
226        }
227        Self {
228            previous_connection_id,
229            previous_tenant,
230            previous_identity,
231            previous_principal,
232        }
233    }
234}
235
236impl Drop for RedWireExecutionContextGuard {
237    fn drop(&mut self) {
238        crate::runtime::execution_context::set_current_connection_id(self.previous_connection_id);
239        match (
240            self.previous_principal.take(),
241            self.previous_identity.take(),
242        ) {
243            (Some(principal), Some((_, role))) => {
244                crate::runtime::execution_context::set_current_auth_principal(principal, role)
245            }
246            _ => crate::runtime::execution_context::clear_current_auth_identity(),
247        }
248        match self.previous_tenant.take() {
249            Some(tenant) => crate::runtime::execution_context::set_current_tenant(tenant),
250            None => crate::runtime::execution_context::clear_current_tenant(),
251        }
252    }
253}
254
255pub(super) fn execute_with_redwire_context<T>(
256    ctx: &OperationContext,
257    role: Role,
258    execute: impl FnOnce() -> T,
259) -> T {
260    let _guard = RedWireExecutionContextGuard::install(ctx, role);
261    execute()
262}
263
264#[derive(Debug)]
265pub(super) enum RedWireExecutionError {
266    QueueFull,
267    QueueTimeout,
268    Closed,
269    Task(tokio::task::JoinError),
270}
271
272impl RedWireExecutionError {
273    pub(super) fn code(&self) -> &'static str {
274        match self {
275            Self::QueueFull => "execution_queue_full",
276            Self::QueueTimeout => "execution_queue_timeout",
277            Self::Closed => "execution_admission_closed",
278            Self::Task(_) => "execution_task_failed",
279        }
280    }
281
282    pub(super) fn client_message(&self) -> String {
283        match self {
284            Self::QueueFull => {
285                "execution_queue_full: engine execution queue is full; retryable=true".to_string()
286            }
287            Self::QueueTimeout => {
288                "execution_queue_timeout: deadline expired while waiting to execute; retryable=true"
289                    .to_string()
290            }
291            Self::Closed => {
292                "execution_admission_closed: engine execution admission is unavailable".to_string()
293            }
294            Self::Task(error) => format!("engine execution task failed: {error}"),
295        }
296    }
297}
298
299/// Keep RedWire's thread-local identity and transaction context on the
300/// connection thread while allowing Tokio to replace this worker for other
301/// async tasks during synchronous engine work.
302fn run_redwire_blocking<T>(execute: impl FnOnce() -> T) -> T {
303    use tokio::runtime::{Handle, RuntimeFlavor};
304
305    match Handle::try_current().map(|handle| handle.runtime_flavor()) {
306        Ok(RuntimeFlavor::MultiThread) => tokio::task::block_in_place(execute),
307        _ => execute(),
308    }
309}
310
311pub(super) async fn redwire_spawn_blocking<F, R>(
312    runtime: &RedDBRuntime,
313    execute: F,
314) -> Result<R, RedWireExecutionError>
315where
316    F: FnOnce() -> R + Send + 'static,
317    R: Send + 'static,
318{
319    let permit = redwire_execution_permit(runtime).await?;
320    redwire_spawn_blocking_with_permit(permit, execute).await
321}
322
323async fn redwire_execution_permit(
324    runtime: &RedDBRuntime,
325) -> Result<crate::transport::execution_admission::ExecutionPermit, RedWireExecutionError> {
326    use crate::transport::execution_admission::ExecutionAdmissionError;
327
328    let admission = runtime.execution_admission();
329    match tokio::time::timeout(runtime.execution_queue_timeout(), admission.acquire()).await {
330        Ok(Ok(permit)) => Ok(permit),
331        Ok(Err(ExecutionAdmissionError::QueueFull)) => Err(RedWireExecutionError::QueueFull),
332        Ok(Err(ExecutionAdmissionError::Closed)) => Err(RedWireExecutionError::Closed),
333        Err(_) => Err(RedWireExecutionError::QueueTimeout),
334    }
335}
336
337async fn redwire_spawn_blocking_with_permit<F, R>(
338    permit: crate::transport::execution_admission::ExecutionPermit,
339    execute: F,
340) -> Result<R, RedWireExecutionError>
341where
342    F: FnOnce() -> R + Send + 'static,
343    R: Send + 'static,
344{
345    tokio::task::spawn_blocking(move || {
346        let _permit = permit;
347        execute()
348    })
349    .await
350    .map_err(RedWireExecutionError::Task)
351}
352
353async fn send_execution_error(
354    out_tx: &super::output_stream::FrameTx,
355    correlation_id: u64,
356    error: RedWireExecutionError,
357) -> io::Result<()> {
358    queue_send(
359        out_tx,
360        build_error_frame_lossy(correlation_id, &error.client_message()),
361    )
362    .await
363}
364
365pub async fn handle_session<S>(
366    mut stream: S,
367    runtime: Arc<RedDBRuntime>,
368    auth_store: Option<Arc<AuthStore>>,
369    oauth: Option<Arc<crate::auth::oauth::OAuthValidator>>,
370) -> io::Result<()>
371where
372    S: AsyncRead + AsyncWrite + Unpin + Send + 'static,
373{
374    // Discriminator byte was already consumed by the service-router
375    // detector when it dispatched here. If callers wire this from
376    // a non-router path they must consume it themselves first.
377    let session = perform_handshake(
378        &mut stream,
379        runtime.as_ref(),
380        auth_store.as_ref(),
381        oauth.as_deref(),
382    )
383    .await?;
384    if session.is_none() {
385        return Ok(());
386    }
387    let session = session.unwrap();
388    // Own the runtime lease through EOF/Bye/error so teardown rolls back
389    // this connection's transaction before its ID can be reused.
390    let connection = Arc::new(
391        runtime
392            .acquire_wire_connection()
393            .map_err(|error| io::Error::other(error.to_string()))?,
394    );
395
396    // Per-connection state for prepared statements + streaming
397    // bulk inserts. Owned by the session; dropped on disconnect. The
398    // prepared registry inside `prepared_stmts` is also the Request
399    // module's execution scope for this connection's queries.
400    let mut stream_session: Option<crate::wire::listener::BulkStreamSession> = None;
401    let mut prepared_stmts = crate::wire::listener::PreparedStatements::default();
402
403    // After handshake, split the socket so reads and writes are
404    // independent: this is what makes RedWire multiplex (PRD #759
405    // S3) — two concurrent output-stream workers can interleave
406    // their chunks back to the client without contending on the
407    // reader side. All outbound bytes are routed through an
408    // bounded, byte-admitted mpsc; one drain task writes complete frames.
409    let (mut reader, writer) = tokio::io::split(stream);
410    let (out_tx, out_rx) = mpsc::channel::<bytes::Bytes>(OUTBOUND_QUEUE_FRAMES);
411    let out_tx = super::output_stream::FrameTx::new(out_tx, Arc::clone(&runtime));
412    let _writer_task = out_tx.spawn_writer(out_rx, writer);
413
414    // Per-connection output-stream registry (issue #762). Tracks
415    // active stream workers so a `StreamCancel` for one stream_id
416    // does not disturb the rest of the connection.
417    let stream_registry = Arc::new(super::output_stream::StreamRegistry::new());
418    let mut output_stream_tasks: tokio::task::JoinSet<()> = tokio::task::JoinSet::new();
419
420    // Per-connection input-stream registry (issue #764 / S5). Input
421    // streams are driven inline from this reader loop — each
422    // `StreamChunk` commits synchronously — so the registry is a plain
423    // owned map rather than the `Arc<Mutex<…>>` the spawned output
424    // workers share. Output and input streams are keyed by `stream_id`
425    // in separate registries, so the two multiplex on one connection
426    // without colliding (AC #2).
427    let mut input_registry = super::input_stream::InputStreamRegistry::new();
428
429    // In-flight live queue-wait tasks (issue #920). Unlike the
430    // output-stream workers — which notice a closed connection the
431    // instant they try to push and self-terminate — a queue-wait task
432    // parks on the registry's async wake head and would otherwise linger
433    // until its `wait_ms` deadline after the client disconnects, holding
434    // a registry slot reference and a tokio worker the whole time. Owning
435    // the tasks in a `JoinSet` scoped to this connection fixes that: when
436    // the frame loop returns (Bye / EOF / I/O error), the set is dropped
437    // and every still-parked wait is aborted, dropping its waiter and so
438    // releasing the slot reference promptly (AC #1, AC #3). The
439    // registry's own `cancel_all` still drives the server-shutdown path
440    // (AC #2/#4) independently of this connection-scoped abort.
441    let mut queue_wait_tasks: tokio::task::JoinSet<()> = tokio::task::JoinSet::new();
442
443    loop {
444        while output_stream_tasks.try_join_next().is_some() {}
445        // Reap finished wait tasks so the set does not accumulate joined
446        // handles over a long-lived connection. Non-blocking — only
447        // already-complete tasks are drained.
448        while queue_wait_tasks.try_join_next().is_some() {}
449
450        let incoming = tokio::select! {
451            _ = out_tx.failed() => return Err(io::Error::other("redwire: outbound response unavailable")),
452            frame = read_frame_async(&mut reader) => frame,
453        };
454        let frame = match incoming {
455            Ok(frame) => frame,
456            Err(reddb_wire::redwire::RedWireIoError::Io(err))
457                if err.kind() == io::ErrorKind::UnexpectedEof =>
458            {
459                return Ok(())
460            }
461            Err(err) => return Err(redwire_io_err(err)),
462        };
463
464        let operation_context =
465            redwire_operation_context(&session, connection.id(), frame.correlation_id);
466        if let Err(error) = authorize_redwire_frame(
467            &operation_context,
468            frame.kind,
469            &RedWireAuthenticatedSessionPolicy,
470        ) {
471            let err_frame = build_error_frame_lossy(frame.correlation_id, &error.to_string());
472            queue_send(&out_tx, err_frame).await?;
473            continue;
474        }
475
476        // Catalog-driven direction gate: server-only kinds (PreparedOk,
477        // AuthOk/Fail, BulkOk, …) must never arrive *from* a client.
478        // The catalog (`MessageKind::direction`) is the single source
479        // of truth — see `frame.rs::catalog_tests::direction_matrix_is_pinned`.
480        if frame.kind.direction() == MessageDirection::ServerToClient {
481            let err_frame = build_error_frame_lossy(
482                frame.correlation_id,
483                &format!("redwire: {:?} is server-only", frame.kind),
484            );
485            queue_send(&out_tx, err_frame).await?;
486            continue;
487        }
488
489        match frame.kind {
490            MessageKind::Bye => {
491                let bye = reply_frame_or_io_error(frame.correlation_id, MessageKind::Bye, vec![])?;
492                let _ = out_tx.send_frame(bye).await;
493                return Ok(());
494            }
495            MessageKind::Ping => {
496                let pong =
497                    reply_frame_or_io_error(frame.correlation_id, MessageKind::Pong, vec![])?;
498                queue_send(&out_tx, pong).await?;
499            }
500            MessageKind::Query
501            | MessageKind::QueryWithParams
502            | MessageKind::QueryBinary
503            | MessageKind::ExecutePrepared => {
504                // The synchronous engine can park on QUEUE READ WAIT. Keep
505                // it off Tokio's I/O workers, and install its thread-local
506                // security/transaction context on the blocking worker itself.
507                let permit = match redwire_execution_permit(runtime.as_ref()).await {
508                    Ok(permit) => permit,
509                    Err(error) => {
510                        send_execution_error(&out_tx, frame.correlation_id, error).await?;
511                        continue;
512                    }
513                };
514                let task_runtime = Arc::clone(&runtime);
515                let connection = Arc::clone(&connection);
516                let statements = std::mem::take(&mut prepared_stmts);
517                let role = session.role;
518                let correlation_id = frame.correlation_id;
519                let result = redwire_spawn_blocking_with_permit(permit, move || {
520                    // A started blocking task cannot be aborted. Retain the
521                    // lease until execution ends, even if the session is dropped.
522                    let _connection = connection;
523                    let response = execute_with_redwire_context(&operation_context, role, || {
524                        match frame.kind {
525                            MessageKind::Query => QueryResponse::Frame(run_query(
526                                &task_runtime,
527                                statements.registry(),
528                                &frame,
529                            )),
530                            MessageKind::QueryWithParams => QueryResponse::Frame(
531                                run_query_with_params(&task_runtime, statements.registry(), &frame),
532                            ),
533                            MessageKind::QueryBinary => QueryResponse::Legacy(
534                                crate::wire::listener::handle_query_binary(
535                                    &task_runtime,
536                                    &frame.payload,
537                                ),
538                                frame.correlation_id,
539                            ),
540                            MessageKind::ExecutePrepared => QueryResponse::Legacy(
541                                crate::wire::listener::handle_execute_prepared(
542                                    &task_runtime,
543                                    &frame.payload,
544                                    &statements,
545                                ),
546                                frame.correlation_id,
547                            ),
548                            _ => unreachable!("only query frames enter the blocking dispatcher"),
549                        }
550                    });
551                    (response, statements)
552                })
553                .await;
554                let (response, statements) = match result {
555                    Ok(result) => result,
556                    Err(error) => {
557                        send_execution_error(&out_tx, correlation_id, error).await?;
558                        prepared_stmts = crate::wire::listener::PreparedStatements::default();
559                        continue;
560                    }
561                };
562                prepared_stmts = statements;
563                match response {
564                    QueryResponse::Frame(frame) => queue_send(&out_tx, frame).await?,
565                    QueryResponse::Legacy(response, correlation_id) => {
566                        out_tx
567                            .send_legacy_response(response, correlation_id)
568                            .await?
569                    }
570                }
571            }
572            // BulkInsert handles both single-row and bulk shapes off
573            // the same frame kind: payload `payload` = single,
574            // payload `payloads` = array.
575            MessageKind::BulkInsert => {
576                let task_runtime = Arc::clone(&runtime);
577                let role = session.role;
578                let correlation_id = frame.correlation_id;
579                let result = redwire_spawn_blocking(runtime.as_ref(), move || {
580                    execute_with_redwire_context(&operation_context, role, || {
581                        run_insert_dispatch(&task_runtime, &frame)
582                    })
583                })
584                .await;
585                match result {
586                    Ok(response) => queue_send(&out_tx, response).await?,
587                    Err(error) => send_execution_error(&out_tx, correlation_id, error).await?,
588                }
589            }
590            MessageKind::BulkInsertBinary => {
591                let task_runtime = Arc::clone(&runtime);
592                let role = session.role;
593                let correlation_id = frame.correlation_id;
594                let result = redwire_spawn_blocking(runtime.as_ref(), move || {
595                    execute_with_redwire_context(&operation_context, role, || {
596                        crate::wire::listener::handle_bulk_insert_binary(
597                            &task_runtime,
598                            &frame.payload,
599                        )
600                    })
601                })
602                .await;
603                match result {
604                    Ok(raw) => {
605                        queue_send(
606                            &out_tx,
607                            rewrap_length_prefixed_handler_response(&raw, correlation_id),
608                        )
609                        .await?
610                    }
611                    Err(error) => send_execution_error(&out_tx, correlation_id, error).await?,
612                }
613            }
614            MessageKind::BulkInsertPrevalidated => {
615                let task_runtime = Arc::clone(&runtime);
616                let role = session.role;
617                let correlation_id = frame.correlation_id;
618                let result = redwire_spawn_blocking(runtime.as_ref(), move || {
619                    execute_with_redwire_context(&operation_context, role, || {
620                        crate::wire::listener::handle_bulk_insert_binary_prevalidated(
621                            &task_runtime,
622                            &frame.payload,
623                        )
624                    })
625                })
626                .await;
627                match result {
628                    Ok(raw) => {
629                        queue_send(
630                            &out_tx,
631                            rewrap_length_prefixed_handler_response(&raw, correlation_id),
632                        )
633                        .await?
634                    }
635                    Err(error) => send_execution_error(&out_tx, correlation_id, error).await?,
636                }
637            }
638            // Streaming bulk insert (PG COPY equivalent).
639            MessageKind::BulkStreamStart => {
640                let raw =
641                    crate::wire::listener::handle_stream_start(&frame.payload, &mut stream_session);
642                queue_send(
643                    &out_tx,
644                    rewrap_length_prefixed_handler_response(&raw, frame.correlation_id),
645                )
646                .await?;
647            }
648            MessageKind::BulkStreamRows => {
649                let permit = match redwire_execution_permit(runtime.as_ref()).await {
650                    Ok(permit) => permit,
651                    Err(error) => {
652                        send_execution_error(&out_tx, frame.correlation_id, error).await?;
653                        continue;
654                    }
655                };
656                let task_runtime = Arc::clone(&runtime);
657                let role = session.role;
658                let correlation_id = frame.correlation_id;
659                let task_session = stream_session.take();
660                let result = redwire_spawn_blocking_with_permit(permit, move || {
661                    let mut task_session = task_session;
662                    let raw = execute_with_redwire_context(&operation_context, role, || {
663                        crate::wire::listener::handle_stream_rows(
664                            &task_runtime,
665                            &frame.payload,
666                            &mut task_session,
667                        )
668                    });
669                    (raw, task_session)
670                })
671                .await;
672                match result {
673                    Ok((raw, returned_session)) => {
674                        stream_session = returned_session;
675                        if !raw.is_empty() {
676                            queue_send(
677                                &out_tx,
678                                rewrap_length_prefixed_handler_response(&raw, correlation_id),
679                            )
680                            .await?;
681                        }
682                    }
683                    Err(error) => send_execution_error(&out_tx, correlation_id, error).await?,
684                }
685            }
686            MessageKind::BulkStreamCommit => {
687                let permit = match redwire_execution_permit(runtime.as_ref()).await {
688                    Ok(permit) => permit,
689                    Err(error) => {
690                        send_execution_error(&out_tx, frame.correlation_id, error).await?;
691                        continue;
692                    }
693                };
694                let task_runtime = Arc::clone(&runtime);
695                let role = session.role;
696                let correlation_id = frame.correlation_id;
697                let task_session = stream_session.take();
698                let result = redwire_spawn_blocking_with_permit(permit, move || {
699                    let mut task_session = task_session;
700                    let raw = execute_with_redwire_context(&operation_context, role, || {
701                        crate::wire::listener::handle_stream_commit(
702                            &task_runtime,
703                            &mut task_session,
704                        )
705                    });
706                    (raw, task_session)
707                })
708                .await;
709                match result {
710                    Ok((raw, returned_session)) => {
711                        stream_session = returned_session;
712                        queue_send(
713                            &out_tx,
714                            rewrap_length_prefixed_handler_response(&raw, correlation_id),
715                        )
716                        .await?;
717                    }
718                    Err(error) => send_execution_error(&out_tx, correlation_id, error).await?,
719                }
720            }
721            MessageKind::Prepare => {
722                let raw = execute_with_redwire_context(&operation_context, session.role, || {
723                    crate::wire::listener::handle_prepare(
724                        &runtime,
725                        &frame.payload,
726                        &mut prepared_stmts,
727                    )
728                });
729                queue_send(
730                    &out_tx,
731                    rewrap_length_prefixed_handler_response(&raw, frame.correlation_id),
732                )
733                .await?;
734            }
735            MessageKind::Get => {
736                let task_runtime = Arc::clone(&runtime);
737                let role = session.role;
738                let correlation_id = frame.correlation_id;
739                let result = redwire_spawn_blocking(runtime.as_ref(), move || {
740                    execute_with_redwire_context(&operation_context, role, || {
741                        run_get(&task_runtime, &frame)
742                    })
743                })
744                .await;
745                match result {
746                    Ok(response) => queue_send(&out_tx, response).await?,
747                    Err(error) => send_execution_error(&out_tx, correlation_id, error).await?,
748                }
749            }
750            MessageKind::Delete => {
751                let task_runtime = Arc::clone(&runtime);
752                let role = session.role;
753                let correlation_id = frame.correlation_id;
754                let result = redwire_spawn_blocking(runtime.as_ref(), move || {
755                    execute_with_redwire_context(&operation_context, role, || {
756                        run_delete(&task_runtime, &frame)
757                    })
758                })
759                .await;
760                match result {
761                    Ok(response) => queue_send(&out_tx, response).await?,
762                    Err(error) => send_execution_error(&out_tx, correlation_id, error).await?,
763                }
764            }
765            // Output-stream lifecycle (issue #762 / PRD #759 S3).
766            //
767            // OpenStream: parse payload, register the stream_id with
768            // the per-connection registry, and spawn a worker that
769            // emits OpenAck → StreamChunk* → StreamEnd through the
770            // shared outbound channel. The dispatch loop returns to
771            // reading immediately so concurrent streams interleave
772            // on the wire (AC #2).
773            MessageKind::OpenStream => {
774                use super::output_stream as os;
775                let frame_id = frame.correlation_id;
776                let sid = frame.stream_id;
777
778                // Input-stream open (issue #764 / S5). Distinguished by
779                // `direction: "in"` in the payload; the output path
780                // below (the default) keeps owning `sql`-bearing opens.
781                // Input streams commit chunks inline in this loop, so
782                // they are registered in the owned `input_registry`
783                // rather than spawning a worker.
784                if super::input_stream::open_stream_is_input(&frame.payload) {
785                    use super::input_stream as is;
786                    let req = match is::parse_open_input(&frame.payload) {
787                        Ok(r) => r,
788                        Err(e) => {
789                            let err = is::build_input_stream_error_frame(
790                                frame_id,
791                                sid,
792                                e.code(),
793                                e.message(),
794                                0,
795                                0,
796                            )?;
797                            queue_send(&out_tx, err).await?;
798                            continue;
799                        }
800                    };
801                    let in_tx = runtime.connection_in_transaction(connection.id());
802                    let config = match crate::server::output_stream::StreamConfig::load(&runtime) {
803                        Ok(config) => config,
804                        Err(error) => {
805                            let err = is::build_input_stream_error_frame(
806                                frame_id,
807                                sid,
808                                "stream_config_unavailable",
809                                &error.to_string(),
810                                0,
811                                runtime.cdc_current_lsn(),
812                            )?;
813                            queue_send(&out_tx, err).await?;
814                            continue;
815                        }
816                    };
817                    let snapshot_lsn = runtime.cdc_current_lsn();
818                    let clock = crate::server::output_stream::SystemClock;
819                    let lease = match is::open_input_lease(config, snapshot_lsn, in_tx, &clock) {
820                        Ok(l) => l,
821                        Err(e) => {
822                            let err = is::build_input_stream_error_frame(
823                                frame_id,
824                                sid,
825                                e.code(),
826                                e.message(),
827                                0,
828                                snapshot_lsn,
829                            )?;
830                            queue_send(&out_tx, err).await?;
831                            continue;
832                        }
833                    };
834                    let lease_id = lease.id;
835                    let lease_snapshot = lease.snapshot_lsn;
836                    let state = is::InputStreamState::new(lease, req.target, req.columns);
837                    if let Err(e) = input_registry.register(sid, state) {
838                        let err = is::build_input_stream_error_frame(
839                            frame_id,
840                            sid,
841                            e.code(),
842                            e.message(),
843                            0,
844                            snapshot_lsn,
845                        )?;
846                        queue_send(&out_tx, err).await?;
847                        continue;
848                    }
849                    let ack =
850                        os::build_open_ack_frame(frame_id, sid, lease_id, lease_snapshot, false)
851                            .map_err(|e| io::Error::other(format!("build OpenAck: {e}")))?;
852                    queue_send(&out_tx, ack).await?;
853                    continue;
854                }
855
856                let req = match os::parse_open_stream(&frame.payload) {
857                    Ok(r) => r,
858                    Err(e) => {
859                        let err =
860                            os::build_stream_error_frame(frame_id, sid, e.code(), e.message())?;
861                        queue_send(&out_tx, err).await?;
862                        continue;
863                    }
864                };
865                let cancel_rx = match stream_registry.register(sid).await {
866                    Ok(rx) => rx,
867                    Err(e) => {
868                        let err =
869                            os::build_stream_error_frame(frame_id, sid, e.code(), e.message())?;
870                        queue_send(&out_tx, err).await?;
871                        continue;
872                    }
873                };
874                let runtime_ref = Arc::clone(&runtime);
875                let registry_ref = Arc::clone(&stream_registry);
876                let send = out_tx.clone();
877                let mut stream_context = operation_context.clone();
878                let stream_role = session.role;
879                let in_tx = runtime.connection_in_transaction(connection.id());
880                // Streams execute in autocommit independently of a later BEGIN
881                // on the session. The worker owns its ID until safe teardown;
882                // the JoinSet aborts workers when the frame loop disconnects.
883                let stream_connection = runtime
884                    .acquire_wire_connection()
885                    .map_err(|error| io::Error::other(error.to_string()))?;
886                stream_context.connection_id = Some(stream_connection.id());
887                output_stream_tasks.spawn(async move {
888                    let _connection = stream_connection;
889                    os::run_output_stream(
890                        runtime_ref,
891                        frame_id,
892                        sid,
893                        req,
894                        in_tx,
895                        cancel_rx,
896                        send,
897                        stream_context,
898                        stream_role,
899                    )
900                    .await;
901                    registry_ref.unregister(sid).await;
902                });
903            }
904            // Live queue wait (issue #917 / PRD #915). Parse the open
905            // request, then spawn a task that awaits the runtime's async
906            // wait edge (parks on the registry's async wake head — no
907            // blocking OS thread) and pushes a `QueueEventPush` the
908            // instant a message becomes deliverable. The dispatch loop
909            // returns to reading immediately so the wait multiplexes
910            // with other frames on the connection.
911            MessageKind::QueueWaitOpen => {
912                use super::queue_wait as qw;
913                let frame_id = frame.correlation_id;
914                let sid = frame.stream_id;
915                let req = match qw::parse_queue_wait_open(&frame.payload) {
916                    Ok(r) => r,
917                    Err(e) => {
918                        let err =
919                            qw::build_queue_wait_error_frame(frame_id, sid, e.code(), e.message())
920                                .map_err(|e| {
921                                    io::Error::other(format!("build queue-wait error: {e}"))
922                                })?;
923                        queue_send(&out_tx, err).await?;
924                        continue;
925                    }
926                };
927                // Server max-wait cap (issue #919, AC #3). Reject an
928                // over-cap budget with an explicit error *before*
929                // spawning the wait task — no waiter is registered and
930                // the budget is never silently shortened.
931                if let Err(msg) = runtime.redwire_queue_wait_cap_check(req.wait_ms) {
932                    let err = qw::build_queue_wait_error_frame(
933                        frame_id,
934                        sid,
935                        qw::WAIT_EXCEEDS_CAP_CODE,
936                        &msg,
937                    )
938                    .map_err(|e| io::Error::other(format!("build queue-wait cap error: {e}")))?;
939                    queue_send(&out_tx, err).await?;
940                    continue;
941                }
942                let runtime_ref = Arc::clone(&runtime);
943                let out = out_tx.clone();
944                let queue_name = req.queue.clone();
945                let wait_ms = req.wait_ms;
946                let auth_identity = Some((session.username.clone(), session.role));
947                let auth_principal = Some(crate::auth::UserId::from_parts(
948                    session.tenant.as_deref(),
949                    &session.username,
950                ));
951                let tenant = session.tenant.clone();
952                // Owned by the connection-scoped `JoinSet` so a client
953                // disconnect (frame loop return) aborts a still-parked
954                // wait and releases its registry slot promptly (#920).
955                queue_wait_tasks.spawn(async move {
956                    use crate::runtime::RedwireWaitOutcome;
957                    match runtime_ref
958                        .redwire_queue_wait_json(
959                            &req.queue,
960                            req.group.as_deref(),
961                            &req.consumer,
962                            req.count,
963                            req.wait_ms,
964                            auth_identity,
965                            auth_principal,
966                            tenant,
967                        )
968                        .await
969                    {
970                        // Happy path: push each delivered message.
971                        Ok(RedwireWaitOutcome::Delivered(messages)) => {
972                            for message in messages {
973                                match qw::build_event_push_frame(frame_id, sid, &message) {
974                                    Ok(push) => {
975                                        if queue_send(&out, push).await.is_err() {
976                                            return;
977                                        }
978                                    }
979                                    Err(_) => return,
980                                }
981                            }
982                        }
983                        // Deadline elapsed with nothing deliverable: a
984                        // distinct timeout frame, not an empty push and
985                        // not an error (AC #1 / AC #2).
986                        Ok(RedwireWaitOutcome::TimedOut) => {
987                            if let Ok(t) = qw::build_queue_wait_timeout_frame(
988                                frame_id,
989                                sid,
990                                &queue_name,
991                                wait_ms,
992                            ) {
993                                let _ = queue_send(&out, t).await;
994                            }
995                        }
996                        // Server-side cancellation: a StreamError with
997                        // the distinct cancellation code so the client
998                        // never confuses it with a timeout (AC #2).
999                        Ok(RedwireWaitOutcome::Cancelled) => {
1000                            if let Ok(ef) = qw::build_queue_wait_error_frame(
1001                                frame_id,
1002                                sid,
1003                                qw::WAIT_CANCELLED_CODE,
1004                                "queue wait cancelled by server",
1005                            ) {
1006                                let _ = queue_send(&out, ef).await;
1007                            }
1008                        }
1009                        // A genuine runtime failure. Server-shutdown
1010                        // cancellation is surfaced as the distinct
1011                        // `RedwireWaitOutcome::Cancelled` arm above, so an
1012                        // `Err` here is never a cancellation (#920 AC #2).
1013                        Err(err) => {
1014                            if let Ok(ef) = qw::build_queue_wait_error_frame(
1015                                frame_id,
1016                                sid,
1017                                qw::WAIT_FAILED_CODE,
1018                                &err.to_string(),
1019                            ) {
1020                                let _ = queue_send(&out, ef).await;
1021                            }
1022                        }
1023                    }
1024                });
1025            }
1026            // Input-stream chunk (issue #764 / S5). A `StreamChunk`
1027            // from the client carries a chunk of rows for an open
1028            // input stream. Each chunk commits synchronously and
1029            // atomically; success is silent (await the next chunk), a
1030            // `terminal: true` chunk closes the stream with a
1031            // `StreamEnd`, and a commit failure emits one `StreamError`
1032            // (carrying `recoverable_rid`) after which no further
1033            // frames are produced for this `stream_id` (AC #3).
1034            MessageKind::StreamChunk => {
1035                use super::input_stream as is;
1036                use crate::server::output_stream::{Clock, SystemClock};
1037                let frame_id = frame.correlation_id;
1038                let sid = frame.stream_id;
1039                if !input_registry.contains(sid) {
1040                    // No input stream for this id — protocol violation,
1041                    // surfaced as StreamError rather than a drop.
1042                    let err = is::build_input_stream_error_frame(
1043                        frame_id,
1044                        sid,
1045                        "unknown_stream",
1046                        "no active input stream for this stream_id",
1047                        0,
1048                        0,
1049                    )?;
1050                    queue_send(&out_tx, err).await?;
1051                    continue;
1052                }
1053                // BEGIN may arrive after OpenStream. Never report a chunk as
1054                // committed if it would instead join the session transaction.
1055                if runtime.connection_in_transaction(connection.id()) {
1056                    let state = input_registry.remove(sid).expect("stream presence checked");
1057                    let error = crate::server::output_stream::OpenStreamError::TransactionActive;
1058                    let frame = is::build_input_stream_error_frame(
1059                        frame_id,
1060                        sid,
1061                        error.code(),
1062                        error.message(),
1063                        state.chunk_count,
1064                        state.committed_rid,
1065                    )?;
1066                    queue_send(&out_tx, frame).await?;
1067                    continue;
1068                }
1069                let chunk = match is::parse_input_chunk(&frame.payload) {
1070                    Ok(c) => c,
1071                    Err(e) => {
1072                        let state = input_registry
1073                            .remove(sid)
1074                            .expect("stream presence checked above");
1075                        let err = is::build_input_stream_error_frame(
1076                            frame_id,
1077                            sid,
1078                            e.code(),
1079                            e.message(),
1080                            state.chunk_count,
1081                            state.committed_rid,
1082                        )?;
1083                        queue_send(&out_tx, err).await?;
1084                        continue;
1085                    }
1086                };
1087                let execution_permit = match redwire_execution_permit(runtime.as_ref()).await {
1088                    Ok(permit) => permit,
1089                    Err(error) => {
1090                        // StreamError is terminal for an input stream. Earlier
1091                        // chunks remain durable and recoverable_rid identifies
1092                        // the last successful commit.
1093                        let state = input_registry
1094                            .remove(sid)
1095                            .expect("stream presence checked above");
1096                        let err = is::build_input_stream_error_frame(
1097                            frame_id,
1098                            sid,
1099                            error.code(),
1100                            &error.client_message(),
1101                            state.chunk_count,
1102                            state.committed_rid,
1103                        )?;
1104                        queue_send(&out_tx, err).await?;
1105                        continue;
1106                    }
1107                };
1108                let commit_result = {
1109                    let _execution_permit = execution_permit;
1110                    let state = input_registry
1111                        .get_mut(sid)
1112                        .expect("stream presence checked above");
1113                    if state.lease.snapshot_expired(SystemClock.now_ms()) {
1114                        Err((
1115                            "snapshot_expired".to_string(),
1116                            "stream snapshot pin TTL elapsed".to_string(),
1117                        ))
1118                    } else {
1119                        run_redwire_blocking(|| {
1120                            execute_with_redwire_context(&operation_context, session.role, || {
1121                                state.commit_chunk(&runtime, &chunk.rows)
1122                            })
1123                        })
1124                    }
1125                };
1126                match commit_result {
1127                    Err((code, message)) => {
1128                        let state = input_registry
1129                            .remove(sid)
1130                            .expect("stream presence checked above");
1131                        let err = is::build_input_stream_error_frame(
1132                            frame_id,
1133                            sid,
1134                            &code,
1135                            &message,
1136                            state.chunk_count,
1137                            state.committed_rid,
1138                        )?;
1139                        queue_send(&out_tx, err).await?;
1140                    }
1141                    Ok(()) => {
1142                        if chunk.terminal {
1143                            let state = input_registry
1144                                .remove(sid)
1145                                .expect("stream presence checked above");
1146                            let end = is::build_input_stream_end_frame(
1147                                frame_id,
1148                                sid,
1149                                state.row_count,
1150                                state.chunk_count,
1151                                state.committed_rid,
1152                                state.snapshot_lsn,
1153                                false,
1154                            )?;
1155                            queue_send(&out_tx, end).await?;
1156                        }
1157                    }
1158                }
1159            }
1160            MessageKind::StreamCancel => {
1161                use super::input_stream as is;
1162                use super::output_stream as os;
1163                let sid = frame.stream_id;
1164                if stream_registry.cancel(sid).await {
1165                    // Output stream cancelled — its worker emits the
1166                    // terminal StreamEnd(cancelled=true) itself.
1167                } else if let Some(state) = input_registry.remove(sid) {
1168                    // AC #4 — input-stream cancel: the in-flight (not
1169                    // yet committed) chunk is discarded by dropping the
1170                    // state; prior per-chunk commits stay durable. Emit
1171                    // a terminal StreamEnd with cancelled=true so the
1172                    // client can drop its bookkeeping.
1173                    let end = is::build_input_stream_end_frame(
1174                        frame.correlation_id,
1175                        sid,
1176                        state.row_count,
1177                        state.chunk_count,
1178                        state.committed_rid,
1179                        state.snapshot_lsn,
1180                        true,
1181                    )?;
1182                    queue_send(&out_tx, end).await?;
1183                } else {
1184                    // AC #6: protocol violation surfaces as a
1185                    // StreamError envelope, not a connection drop.
1186                    let err = os::build_stream_error_frame(
1187                        frame.correlation_id,
1188                        sid,
1189                        "unknown_stream",
1190                        "no active stream for this stream_id",
1191                    )?;
1192                    queue_send(&out_tx, err).await?;
1193                }
1194            }
1195            other => {
1196                let err_frame = build_error_frame_lossy(
1197                    frame.correlation_id,
1198                    &format!("redwire: cannot dispatch {other:?} yet"),
1199                );
1200                queue_send(&out_tx, err_frame).await?;
1201            }
1202        }
1203    }
1204}
1205
1206/// Frames a connection may have queued for its socket writer before the
1207/// producers block. Bounded so a peer that stops reading cannot make the
1208/// server buffer an output stream without limit; the writer task drains it
1209/// as fast as the socket accepts bytes.
1210const OUTBOUND_QUEUE_FRAMES: usize = 256;
1211
1212async fn queue_send(out_tx: &super::output_stream::FrameTx, frame: Frame) -> io::Result<()> {
1213    out_tx.send_frame(frame).await
1214}
1215
1216/// Run the handshake. Returns `Ok(None)` when the client disconnected
1217/// or the auth was refused (the failure frame is already on the wire).
1218async fn perform_handshake<S>(
1219    stream: &mut S,
1220    runtime: &RedDBRuntime,
1221    auth_store: Option<&Arc<AuthStore>>,
1222    oauth: Option<&crate::auth::oauth::OAuthValidator>,
1223) -> io::Result<Option<AuthedSession>>
1224where
1225    S: AsyncRead + AsyncWrite + Unpin + Send,
1226{
1227    // Step 1: read minor version byte.
1228    let mut minor_buf = [0u8; 1];
1229    stream.read_exact(&mut minor_buf).await?;
1230    let minor = minor_buf[0];
1231    if validate_minor_version(minor).is_err() {
1232        // Future client speaking a version we don't know — refuse
1233        // immediately. We do not send a frame because the client
1234        // hasn't agreed on the framing version yet.
1235        return Ok(None);
1236    }
1237
1238    // Step 2: read the Hello frame.
1239    let hello = read_frame(stream).await?;
1240    if hello.kind != MessageKind::Hello {
1241        let fail = encode_frame(&reply_frame_or_io_error(
1242            hello.correlation_id,
1243            MessageKind::AuthFail,
1244            build_auth_fail_payload("first frame after magic must be Hello"),
1245        )?);
1246        let _ = stream.write_all(&fail).await;
1247        return Ok(None);
1248    }
1249    let hello_msg = match Hello::from_payload(&hello.payload) {
1250        Ok(h) => h,
1251        Err(e) => {
1252            let fail = encode_frame(&reply_frame_or_io_error(
1253                hello.correlation_id,
1254                MessageKind::AuthFail,
1255                build_auth_fail_payload(&e),
1256            )?);
1257            let _ = stream.write_all(&fail).await;
1258            return Ok(None);
1259        }
1260    };
1261
1262    let Some(chosen_version) = choose_hello_minor_version(&hello_msg.versions) else {
1263        let fail = encode_frame(&reply_frame_or_io_error(
1264            hello.correlation_id,
1265            MessageKind::AuthFail,
1266            build_auth_fail_payload("no overlapping protocol version"),
1267        )?);
1268        let _ = stream.write_all(&fail).await;
1269        return Ok(None);
1270    };
1271
1272    let server_anon_ok = auth_store.map(|s| !s.is_enabled()).unwrap_or(true);
1273    let chosen = match pick_auth_method(&hello_msg.auth_methods, server_anon_ok) {
1274        Some(m) => m,
1275        None => {
1276            let fail = encode_frame(&reply_frame_or_io_error(
1277                hello.correlation_id,
1278                MessageKind::AuthFail,
1279                build_auth_fail_payload("no overlapping auth method"),
1280            )?);
1281            let _ = stream.write_all(&fail).await;
1282            return Ok(None);
1283        }
1284    };
1285
1286    // Step 3: HelloAck.
1287    //
1288    // HelloAck is sent before any AuthResponse arrives, so the
1289    // caller is unauthenticated at this point. The TopologyAdvertiser
1290    // collapses anonymous to primary-only per ADR 0008 §3 — that's
1291    // the correct payload for the bootstrap path. Authenticated
1292    // principals get the full replica list via the gRPC `Topology`
1293    // RPC after the connection is established.
1294    let server_features = FEATURE_PARAMS;
1295    let topology = build_topology_for_hello_ack(runtime);
1296    let ack_frame = build_hello_ack_frame(
1297        hello.correlation_id,
1298        chosen_version,
1299        chosen,
1300        server_features,
1301        topology.as_ref(),
1302    )
1303    .map_err(|e| io::Error::other(format!("build HelloAck: {e}")))?;
1304    let ack = encode_frame(&ack_frame);
1305    stream.write_all(&ack).await?;
1306
1307    // SCRAM is a 3-RTT challenge/response exchange. Branch off to
1308    // its own state machine before the 1-RTT bearer/anonymous
1309    // path runs.
1310    if chosen == "scram-sha-256" {
1311        return perform_scram_handshake(stream, auth_store, hello.correlation_id, server_features)
1312            .await;
1313    }
1314
1315    // Step 4: AuthResponse (no challenge for the 1-RTT methods —
1316    // bearer/anonymous send their proof in the first AuthResponse).
1317    let resp = read_frame(stream).await?;
1318    let auth_payload = match expect_auth_response_payload(resp.kind, &resp.payload, "AuthResponse")
1319    {
1320        Ok(payload) => payload,
1321        Err(err) => {
1322            let fail = encode_frame(&reply_frame_or_io_error(
1323                resp.correlation_id,
1324                MessageKind::AuthFail,
1325                build_auth_fail_payload(&err.to_string()),
1326            )?);
1327            let _ = stream.write_all(&fail).await;
1328            return Ok(None);
1329        }
1330    };
1331
1332    // OAuth-JWT branch. The `jwt` field carries either a browser access
1333    // JWT (the hybrid-token model, issue #936) or a federated IdP token
1334    // validated by the configured `OAuthValidator`. The browser access
1335    // token is tried *first* and independently of `oauth` being wired, so
1336    // a deployment that runs the browser credential layer without any
1337    // external OAuth IdP still authenticates. mTLS stays native-only
1338    // (ADR 0036) — the browser presents this access JWT and nothing else.
1339    if chosen == "oauth-jwt" {
1340        let raw = match parse_auth_response_oauth_jwt(auth_payload) {
1341            Ok(raw) if !raw.is_empty() => raw,
1342            _ => {
1343                let fail = encode_frame(&reply_frame_or_io_error(
1344                    resp.correlation_id,
1345                    MessageKind::AuthFail,
1346                    build_auth_fail_payload("oauth-jwt: AuthResponse missing 'jwt' string"),
1347                )?);
1348                let _ = stream.write_all(&fail).await;
1349                return Ok(None);
1350            }
1351        };
1352
1353        // 1. Browser hybrid-token access JWT (issue #936). A *valid*
1354        //    access token (correct issuer/audience/signature, `typ:
1355        //    access`, unexpired) authenticates the session directly.
1356        //    Anything else (expired, wrong type, or simply not one of our
1357        //    tokens — e.g. a foreign IdP RS256 token) falls through to the
1358        //    OAuth validator below; the net effect is "valid browser
1359        //    token accepted, expired/invalid rejected" (AC #2). The stream
1360        //    lease (ADR 0029) then decouples this token's expiry from any
1361        //    stream the session opens, so a later refresh never tears down
1362        //    in-flight work (AC #3).
1363        if let Some(authority) = runtime.browser_token_authority() {
1364            let now = std::time::SystemTime::now()
1365                .duration_since(std::time::UNIX_EPOCH)
1366                .map(|d| d.as_secs() as i64)
1367                .unwrap_or(0);
1368            if let Ok(identity) = authority.validate_access(&raw, now) {
1369                let lease = match acquire_local_connection_lease(
1370                    auth_store,
1371                    identity.tenant.as_deref(),
1372                    &identity.username,
1373                ) {
1374                    Ok(lease) => lease,
1375                    Err(reason) => {
1376                        send_auth_refusal(stream, resp.correlation_id, &reason).await?;
1377                        return Ok(None);
1378                    }
1379                };
1380                let session_id = super::auth::new_session_id_for_scram();
1381                let ok = encode_frame(&reply_frame_or_io_error(
1382                    resp.correlation_id,
1383                    MessageKind::AuthOk,
1384                    build_auth_ok(
1385                        &session_id,
1386                        &identity.username,
1387                        identity.role,
1388                        server_features,
1389                    ),
1390                )?);
1391                stream.write_all(&ok).await?;
1392                return Ok(Some(AuthedSession {
1393                    username: identity.username,
1394                    role: identity.role,
1395                    tenant: identity.tenant,
1396                    session_id,
1397                    _lease: lease,
1398                }));
1399            }
1400        }
1401
1402        // 2. Federated OAuth-JWT (RS256/ES256 against a configured IdP).
1403        let validator = match oauth {
1404            Some(v) => v,
1405            None => {
1406                let fail = encode_frame(&reply_frame_or_io_error(
1407                    resp.correlation_id,
1408                    MessageKind::AuthFail,
1409                    build_auth_fail_payload(
1410                        "oauth-jwt: token rejected (no browser-token authority or OAuth validator accepted it)",
1411                    ),
1412                )?);
1413                let _ = stream.write_all(&fail).await;
1414                return Ok(None);
1415            }
1416        };
1417        match super::auth::validate_oauth_jwt_full(validator, &raw) {
1418            Ok((tenant, username, role)) => {
1419                let lease = match acquire_local_connection_lease(
1420                    auth_store,
1421                    tenant.as_deref(),
1422                    &username,
1423                ) {
1424                    Ok(lease) => lease,
1425                    Err(reason) => {
1426                        send_auth_refusal(stream, resp.correlation_id, &reason).await?;
1427                        return Ok(None);
1428                    }
1429                };
1430                let session_id = super::auth::new_session_id_for_scram();
1431                let ok = encode_frame(&reply_frame_or_io_error(
1432                    resp.correlation_id,
1433                    MessageKind::AuthOk,
1434                    build_auth_ok(&session_id, &username, role, server_features),
1435                )?);
1436                stream.write_all(&ok).await?;
1437                return Ok(Some(AuthedSession {
1438                    username,
1439                    role,
1440                    tenant,
1441                    session_id,
1442                    _lease: lease,
1443                }));
1444            }
1445            Err(reason) => {
1446                let fail = encode_frame(&reply_frame_or_io_error(
1447                    resp.correlation_id,
1448                    MessageKind::AuthFail,
1449                    build_auth_fail_payload(&format!("oauth-jwt: {reason}")),
1450                )?);
1451                let _ = stream.write_all(&fail).await;
1452                return Ok(None);
1453            }
1454        }
1455    }
1456
1457    match validate_auth_response(chosen, auth_payload, auth_store.map(AsRef::as_ref)) {
1458        AuthOutcome::Authenticated {
1459            username,
1460            role,
1461            tenant,
1462            session_id,
1463        } => {
1464            let lease =
1465                match acquire_local_connection_lease(auth_store, tenant.as_deref(), &username) {
1466                    Ok(lease) => lease,
1467                    Err(reason) => {
1468                        send_auth_refusal(stream, resp.correlation_id, &reason).await?;
1469                        return Ok(None);
1470                    }
1471                };
1472            let ok_frame = build_auth_ok_frame_from_payload(
1473                resp.correlation_id,
1474                build_auth_ok(&session_id, &username, role, server_features),
1475            )
1476            .map_err(|e| io::Error::other(format!("build AuthOk: {e}")))?;
1477            let ok = encode_frame(&ok_frame);
1478            stream.write_all(&ok).await?;
1479            Ok(Some(AuthedSession {
1480                username,
1481                role,
1482                tenant,
1483                session_id,
1484                _lease: lease,
1485            }))
1486        }
1487        AuthOutcome::Refused(reason) => {
1488            let fail = encode_frame(&reply_frame_or_io_error(
1489                resp.correlation_id,
1490                MessageKind::AuthFail,
1491                build_auth_fail_payload(&reason),
1492            )?);
1493            let _ = stream.write_all(&fail).await;
1494            Ok(None)
1495        }
1496    }
1497}
1498
1499/// 3-RTT SCRAM-SHA-256 server handshake (RFC 5802 + RFC 7677).
1500///
1501/// ```text
1502/// C → S  AuthResponse(client-first-message)         (already received as client-first)
1503/// S → C  AuthRequest(server-first-message)
1504/// C → S  AuthResponse(client-final-message)
1505/// S → C  AuthOk(v=server-signature)
1506/// ```
1507async fn perform_scram_handshake<S>(
1508    stream: &mut S,
1509    auth_store: Option<&Arc<AuthStore>>,
1510    initial_correlation: u64,
1511    server_features: u32,
1512) -> io::Result<Option<AuthedSession>>
1513where
1514    S: AsyncRead + AsyncWrite + Unpin + Send,
1515{
1516    let store = match auth_store {
1517        Some(s) => s,
1518        None => {
1519            let fail = encode_frame(&reply_frame_or_io_error(
1520                initial_correlation,
1521                MessageKind::AuthFail,
1522                build_auth_fail_payload("scram-sha-256 requires an AuthStore"),
1523            )?);
1524            let _ = stream.write_all(&fail).await;
1525            return Ok(None);
1526        }
1527    };
1528
1529    let mut handshake = ScramServerHandshake::new(
1530        super::auth::new_server_nonce(),
1531        crate::auth::store::random_bytes(16),
1532    );
1533
1534    // The wire handshake doesn't yet learn
1535    // a tenant before the SCRAM exchange completes, so we resolve
1536    // against the platform tenant. Tenant-scoped users authenticate
1537    // through the JWT path (which carries the tenant claim) or a
1538    // future explicit `tenant` extension to the AuthRequest payload.
1539    let client_first = read_frame(stream).await?;
1540    let username = match handshake.step(ScramServerInput::ClientMessage {
1541        correlation_id: client_first.correlation_id,
1542        kind: client_first.kind,
1543        payload: client_first.payload,
1544    }) {
1545        ScramServerOutput::NeedVerifier { username } => username,
1546        ScramServerOutput::Failed {
1547            correlation_id,
1548            reason,
1549        } => {
1550            let fail = encode_frame(&reply_frame_or_io_error(
1551                correlation_id,
1552                MessageKind::AuthFail,
1553                build_auth_fail_payload(&reason),
1554            )?);
1555            let _ = stream.write_all(&fail).await;
1556            return Ok(None);
1557        }
1558        _ => unreachable!("client-first step must request a verifier or fail"),
1559    };
1560    let verifier = store.lookup_scram_verifier_global(&username);
1561    let (challenge_correlation_id, challenge_payload) =
1562        match handshake.step(ScramServerInput::Verifier(verifier)) {
1563            ScramServerOutput::Challenge {
1564                correlation_id,
1565                payload,
1566            } => (correlation_id, payload),
1567            _ => unreachable!("verifier step must produce a SCRAM challenge"),
1568        };
1569    let req = encode_frame(&reply_frame_or_io_error(
1570        challenge_correlation_id,
1571        MessageKind::AuthRequest,
1572        challenge_payload,
1573    )?);
1574    stream.write_all(&req).await?;
1575
1576    let client_final = read_frame(stream).await?;
1577    let (final_correlation_id, username, server_signature) =
1578        match handshake.step(ScramServerInput::ClientMessage {
1579            correlation_id: client_final.correlation_id,
1580            kind: client_final.kind,
1581            payload: client_final.payload,
1582        }) {
1583            ScramServerOutput::Authenticated {
1584                correlation_id,
1585                username,
1586                server_signature,
1587            } => (correlation_id, username, server_signature),
1588            ScramServerOutput::Failed {
1589                correlation_id,
1590                reason,
1591            } => {
1592                let fail = encode_frame(&reply_frame_or_io_error(
1593                    correlation_id,
1594                    MessageKind::AuthFail,
1595                    build_auth_fail_payload(&reason),
1596                )?);
1597                let _ = stream.write_all(&fail).await;
1598                return Ok(None);
1599            }
1600            _ => unreachable!("client-final step must authenticate or fail"),
1601        };
1602    // Bind the session to the same platform principal whose verifier was
1603    // checked above; a tenant-local namesake is a different identity.
1604    let user = store.get_user(None, &username);
1605    let role = user
1606        .as_ref()
1607        .map(|u| u.role)
1608        .unwrap_or(crate::auth::Role::Read);
1609    let lease = match acquire_local_connection_lease(auth_store, None, &username) {
1610        Ok(Some(lease)) => lease,
1611        Ok(None) | Err(_) => {
1612            send_auth_refusal(stream, final_correlation_id, "authentication refused").await?;
1613            return Ok(None);
1614        }
1615    };
1616    let session_id = super::auth::new_session_id_for_scram();
1617    let ok_payload = super::auth::build_scram_auth_ok(
1618        &session_id,
1619        &username,
1620        role,
1621        server_features,
1622        &server_signature,
1623    );
1624    let ok = encode_frame(&reply_frame_or_io_error(
1625        final_correlation_id,
1626        MessageKind::AuthOk,
1627        ok_payload,
1628    )?);
1629    stream.write_all(&ok).await?;
1630    Ok(Some(AuthedSession {
1631        username,
1632        role,
1633        tenant: user.and_then(|u| u.tenant_id),
1634        session_id,
1635        _lease: Some(lease),
1636    }))
1637}
1638
1639async fn read_frame<S>(stream: &mut S) -> io::Result<Frame>
1640where
1641    S: AsyncRead + Unpin + Send,
1642{
1643    read_frame_async(stream).await.map_err(redwire_io_err)
1644}
1645
1646fn redwire_io_err(err: reddb_wire::redwire::RedWireIoError) -> io::Error {
1647    match err {
1648        reddb_wire::redwire::RedWireIoError::Io(err) => err,
1649        reddb_wire::redwire::RedWireIoError::Frame(err) => {
1650            io::Error::other(format!("decode frame: {err}"))
1651        }
1652    }
1653}
1654
1655/// `Query` (0x01) answers with the pinned summary payload — statement type
1656/// plus affected rows — not the full result envelope. `QueryWithParams`
1657/// (0x28) is the frame that carries records back; clients pick the frame by
1658/// the reply shape they want.
1659fn run_query(runtime: &RedDBRuntime, prepared: &PreparedRegistry, frame: &Frame) -> Frame {
1660    let sql = match std::str::from_utf8(&frame.payload) {
1661        Ok(s) => s,
1662        Err(_) => {
1663            return build_error_frame_lossy(
1664                frame.correlation_id,
1665                "Query payload must be UTF-8 SQL",
1666            );
1667        }
1668    };
1669    let result =
1670        QueryRequestExecutor::new(runtime, prepared).execute(QueryRequest::sql(sql, Vec::new()));
1671    crate::presentation::query_result::summary_frame(
1672        frame.correlation_id,
1673        result
1674            .as_ref()
1675            .map(|result| &**result)
1676            .map_err(crate::api::client_facing_message),
1677    )
1678}
1679
1680fn run_query_with_params(
1681    runtime: &RedDBRuntime,
1682    prepared: &PreparedRegistry,
1683    frame: &Frame,
1684) -> Frame {
1685    let request = match decode_query_with_params_request(&frame.payload) {
1686        Ok(decoded) => decoded,
1687        Err(err) => return build_error_frame_lossy(frame.correlation_id, &err.to_string()),
1688    };
1689    let commit_policy = match parse_redwire_commit_policy(request.options.commit_policy.as_deref())
1690    {
1691        Ok(policy) => policy,
1692        Err(err) => return build_error_frame_lossy(frame.correlation_id, &err),
1693    };
1694    let params = request
1695        .params
1696        .into_iter()
1697        .map(param_to_request_value)
1698        .collect::<Vec<_>>();
1699    let mut query = QueryRequest::sql(request.sql, params);
1700    if let Some(policy) = commit_policy {
1701        query = query.with_commit_policy(policy);
1702    }
1703    let result = QueryRequestExecutor::new(runtime, prepared).execute(query);
1704    crate::presentation::query_result::envelope_frame(
1705        frame.correlation_id,
1706        result
1707            .as_ref()
1708            .map(|result| &**result)
1709            .map_err(crate::api::client_facing_message),
1710    )
1711}
1712
1713fn parse_redwire_commit_policy(
1714    value: Option<&str>,
1715) -> Result<Option<crate::replication::CommitPolicy>, String> {
1716    let Some(value) = value else {
1717        return Ok(None);
1718    };
1719    crate::replication::CommitPolicy::parse_strict(value)
1720        .map(Some)
1721        .ok_or_else(|| format!("invalid commit_policy value '{value}'"))
1722}
1723
1724fn param_to_request_value(value: RedWireParamValue) -> ParamValue {
1725    match value {
1726        RedWireParamValue::Null => ParamValue::Null,
1727        RedWireParamValue::Bool(value) => ParamValue::Bool(value),
1728        RedWireParamValue::Int(value) => ParamValue::Int64(value),
1729        RedWireParamValue::Float(value) => ParamValue::Float64(value),
1730        RedWireParamValue::Text(value) => ParamValue::Text(value),
1731        RedWireParamValue::Bytes(value) => ParamValue::Bytes(value),
1732        RedWireParamValue::Vector(value) => ParamValue::Vector(value),
1733        RedWireParamValue::Json(value) => ParamValue::Json(value),
1734        RedWireParamValue::Timestamp(value) => ParamValue::Timestamp(value),
1735        RedWireParamValue::Uuid(value) => ParamValue::Uuid(value),
1736    }
1737}
1738
1739/// Insert dispatch — handles single-row, bulk, and the analytics
1740/// batch shape off the same `BulkInsert` (0x04) frame:
1741///   - `{ "collection": "...", "payload": {...} }` → single insert
1742///   - `{ "collection": "...", "payloads": [...] }` → bulk insert
1743///   - `{ "collection": "...", "payloads": [...], "idempotency_key": "...",
1744///       "batch": true? }` → analytics `BatchInsertEndpoint`
1745///     (issue #587) — all-or-nothing commit with
1746///     `AnalyticsSchemaRegistry` validation up front and replay served
1747///     from the process-wide cache shared with the HTTP (#582) and
1748///     gRPC (#585) mirrors. Either an `idempotency_key` OR `batch:
1749///     true` flips the dispatch — the literal idempotency key in the
1750///     frame is the canonical signal in the brief, the boolean lets a
1751///     client opt into the validation semantics without committing to
1752///     a cache window.
1753///
1754/// Mirrors the JSON-RPC `insert` / `bulk_insert` method shapes
1755/// from `rpc_stdio.rs` so both transports agree on the payload.
1756/// `INSERT … RETURNING rid` for one JSON row. A bare SQL INSERT result
1757/// carries only `affected_rows`; the engine-assigned id is exposed
1758/// through RETURNING, which is what `insert_result_to_json` reads to
1759/// fill the BulkOk `id`/`ids` keys.
1760fn insert_sql_returning_rid<'a, I>(collection: &str, fields: I) -> String
1761where
1762    I: Iterator<Item = (&'a String, &'a crate::json::Value)>,
1763{
1764    let mut sql = crate::rpc_stdio::build_insert_sql(collection, fields);
1765    sql.push_str(" RETURNING rid");
1766    sql
1767}
1768
1769fn run_insert_dispatch(runtime: &RedDBRuntime, frame: &Frame) -> Frame {
1770    let request = match decode_insert_dispatch_payload(&frame.payload) {
1771        Ok(request) => request,
1772        Err(err) => return build_error_frame_lossy(frame.correlation_id, &err.to_string()),
1773    };
1774    let collection = request.collection.as_str();
1775    if let Err(msg) = crate::rpc_stdio::ensure_sql_collection(collection) {
1776        return build_error_frame_lossy(frame.correlation_id, &msg);
1777    }
1778    let payload = request.payload.map(wire_json_to_server_json);
1779    let payloads = request.payloads.map(|rows| {
1780        rows.into_iter()
1781            .map(wire_json_to_server_json)
1782            .collect::<Vec<_>>()
1783    });
1784
1785    // Analytics batch-insert path (issue #587). Either field flips the
1786    // dispatch — the brief carries `idempotency_key` as the canonical
1787    // signal; the optional `batch: true` boolean exists for callers
1788    // that want the validation contract without committing to a
1789    // replay window.
1790    let idempotency_key = request.idempotency_key.as_deref();
1791    if idempotency_key.is_some() || request.batch {
1792        let items = match payloads.as_deref() {
1793            Some(rows) => rows,
1794            None => {
1795                return build_error_frame_lossy(
1796                    frame.correlation_id,
1797                    "BatchInsert: missing 'payloads' array",
1798                )
1799            }
1800        };
1801        let outcome = crate::server::handlers_entity::process_batch_insert(
1802            runtime,
1803            collection,
1804            items,
1805            idempotency_key,
1806        );
1807        // Mirror the HTTP transport's status convention: 2xx → BulkOk,
1808        // everything else → Error frame (the body carries the typed
1809        // code/row_index envelope so the client can decode it without
1810        // an out-of-band header).
1811        let kind = if (200..300).contains(&outcome.status) {
1812            MessageKind::BulkOk
1813        } else {
1814            MessageKind::Error
1815        };
1816        return build_dispatch_reply_frame(frame.correlation_id, kind, outcome.body);
1817    }
1818
1819    if let Some(rows) = payloads.as_ref() {
1820        let mut objects = Vec::with_capacity(rows.len());
1821        for entry in rows {
1822            objects.push(match entry.as_object() {
1823                Some(o) => o,
1824                None => {
1825                    return build_error_frame_lossy(
1826                        frame.correlation_id,
1827                        "Insert: each payload must be a JSON object",
1828                    )
1829                }
1830            });
1831        }
1832
1833        if crate::rpc_stdio::should_bulk_insert_graph(runtime, collection, &objects) {
1834            return match crate::rpc_stdio::bulk_insert_graph(runtime, collection, &objects) {
1835                Ok(body) => {
1836                    let payload = body.to_string_compact().into_bytes();
1837                    build_dispatch_reply_frame(frame.correlation_id, MessageKind::BulkOk, payload)
1838                }
1839                Err(err) => build_error_frame_lossy(frame.correlation_id, &err.to_string()),
1840            };
1841        }
1842
1843        let mut affected: u64 = 0;
1844        let mut ids = Vec::with_capacity(objects.len());
1845        for row in objects {
1846            let sql = insert_sql_returning_rid(collection, row.iter());
1847            match runtime.execute_query(&sql) {
1848                Ok(qr) => {
1849                    affected += qr.affected_rows;
1850                    if let Some(id) = crate::rpc_stdio::insert_result_to_json(&qr).get("id") {
1851                        ids.push(id.clone());
1852                    }
1853                }
1854                Err(err) => {
1855                    return build_error_frame_lossy(
1856                        frame.correlation_id,
1857                        &crate::api::client_facing_message(&err),
1858                    )
1859                }
1860            }
1861        }
1862        let payload = encode_bulk_ok_payload_from_json_id_literals(
1863            affected,
1864            ids.iter().map(|id| id.to_string()),
1865        );
1866        return build_dispatch_reply_frame(frame.correlation_id, MessageKind::BulkOk, payload);
1867    }
1868
1869    let row = match payload.as_ref().and_then(|x| x.as_object()) {
1870        Some(o) => o,
1871        None => {
1872            return build_error_frame_lossy(
1873                frame.correlation_id,
1874                "Insert: missing 'payload' object or 'payloads' array",
1875            )
1876        }
1877    };
1878    let sql = insert_sql_returning_rid(collection, row.iter());
1879    match runtime.execute_query(&sql) {
1880        Ok(qr) => {
1881            let body = crate::rpc_stdio::insert_result_to_json(&qr);
1882            let payload = body.to_string_compact().into_bytes();
1883            build_dispatch_reply_frame(frame.correlation_id, MessageKind::BulkOk, payload)
1884        }
1885        Err(err) => build_error_frame_lossy(
1886            frame.correlation_id,
1887            &crate::api::client_facing_message(&err),
1888        ),
1889    }
1890}
1891
1892/// Build the primary-only topology payload embedded in HelloAck
1893/// (issue #167). Threads an anonymous auth context through
1894/// `TopologyAdvertiser::advertise` because the principal is not yet
1895/// known at HelloAck time — ADR 0008 §3 collapses anonymous to a
1896/// primary-only payload, which is exactly the bootstrap shape we
1897/// want here.
1898///
1899/// Returns `None` for non-primary roles or when the engine is not
1900/// running with replication enabled. Old clients that don't
1901/// understand the `topology` JSON key ignore it cleanly (ADR §4),
1902/// so the absent-vs-present distinction is benign.
1903fn build_topology_for_hello_ack(runtime: &RedDBRuntime) -> Option<reddb_wire::topology::Topology> {
1904    use crate::auth::middleware::AuthResult;
1905    use crate::replication::{LagConfig, TopologyAdvertiser};
1906    use reddb_wire::topology::Endpoint;
1907
1908    let db = runtime.db();
1909    let primary_endpoint = Endpoint {
1910        addr: runtime.config_string("red.redwire.advertise_addr", ""),
1911        region: db.options().replication.region.clone(),
1912    };
1913    let (replicas, current_lsn, epoch) = match db.replication.as_ref() {
1914        Some(repl) => (
1915            repl.replica_snapshots(),
1916            repl.wal_buffer.current_lsn(),
1917            repl.topology_epoch(),
1918        ),
1919        None => (Vec::new(), 0u64, 0u64),
1920    };
1921    let lag = LagConfig::from_now();
1922    Some(TopologyAdvertiser::advertise(
1923        &replicas,
1924        &AuthResult::Anonymous,
1925        epoch,
1926        primary_endpoint,
1927        current_lsn,
1928        &lag,
1929    ))
1930}
1931
1932fn reply_frame_or_io_error(
1933    correlation_id: u64,
1934    kind: MessageKind,
1935    payload: Vec<u8>,
1936) -> io::Result<Frame> {
1937    build_reply_frame(correlation_id, kind, payload)
1938        .map_err(|e| io::Error::other(format!("build {kind:?}: {e}")))
1939}
1940
1941fn wire_json_to_server_json(value: impl std::fmt::Display) -> JsonValue {
1942    serde_json::from_str::<JsonValue>(&value.to_string()).unwrap_or(JsonValue::Null)
1943}
1944
1945/// Get payload shape: `{ "collection": "...", "id": "..." }`.
1946/// Bridges to `SELECT * FROM <coll> WHERE rid = <id> LIMIT 1`.
1947/// Reply: Result frame with the row, or empty `{}` when not found.
1948fn run_get(runtime: &RedDBRuntime, frame: &Frame) -> Frame {
1949    let request = match decode_get_payload(&frame.payload) {
1950        Ok(request) => request,
1951        Err(err) => return build_error_frame_lossy(frame.correlation_id, &err.to_string()),
1952    };
1953    let collection = request.collection.as_str();
1954    if let Err(msg) = crate::rpc_stdio::ensure_sql_collection(collection) {
1955        return build_error_frame_lossy(frame.correlation_id, &msg);
1956    }
1957    let Some(rid) = crate::rpc_stdio::rid_from_str(&request.id) else {
1958        let payload = encode_get_result_payload(false);
1959        return build_dispatch_reply_frame(frame.correlation_id, MessageKind::Result, payload);
1960    };
1961    let sql = format!("SELECT * FROM {collection} WHERE rid = {rid} LIMIT 1");
1962    match runtime.execute_query(&sql) {
1963        Ok(qr) => {
1964            // Preserve the existing Get envelope: presence only.
1965            let payload = encode_get_result_payload(!qr.result.records.is_empty());
1966            build_dispatch_reply_frame(frame.correlation_id, MessageKind::Result, payload)
1967        }
1968        Err(err) => build_error_frame_lossy(
1969            frame.correlation_id,
1970            &crate::api::client_facing_message(&err),
1971        ),
1972    }
1973}
1974
1975/// Delete payload shape: `{ "collection": "...", "id": "..." }`.
1976/// Bridges to `DELETE FROM <coll> WHERE rid = <id>`.
1977/// Reply: DeleteOk frame with `{ affected }`.
1978fn run_delete(runtime: &RedDBRuntime, frame: &Frame) -> Frame {
1979    let request = match decode_delete_payload(&frame.payload) {
1980        Ok(request) => request,
1981        Err(err) => return build_error_frame_lossy(frame.correlation_id, &err.to_string()),
1982    };
1983    let collection = request.collection.as_str();
1984    if let Err(msg) = crate::rpc_stdio::ensure_sql_collection(collection) {
1985        return build_error_frame_lossy(frame.correlation_id, &msg);
1986    }
1987    let Some(rid) = crate::rpc_stdio::rid_from_str(&request.id) else {
1988        let payload = encode_delete_ok_payload(0);
1989        return build_dispatch_reply_frame(frame.correlation_id, MessageKind::DeleteOk, payload);
1990    };
1991    let sql = format!("DELETE FROM {collection} WHERE rid = {rid}");
1992    match runtime.execute_query(&sql) {
1993        Ok(qr) => {
1994            let payload = encode_delete_ok_payload(qr.affected_rows);
1995            build_dispatch_reply_frame(frame.correlation_id, MessageKind::DeleteOk, payload)
1996        }
1997        Err(err) => build_error_frame_lossy(
1998            frame.correlation_id,
1999            &crate::api::client_facing_message(&err),
2000        ),
2001    }
2002}
2003
2004#[cfg(test)]
2005mod tests {
2006    use super::*;
2007
2008    use crate::runtime::RedDBRuntime;
2009    use std::cell::Cell;
2010    use std::sync::{Mutex, OnceLock};
2011    use tokio::io::{AsyncReadExt, AsyncWriteExt};
2012
2013    fn env_lock() -> &'static Mutex<()> {
2014        static LOCK: OnceLock<Mutex<()>> = OnceLock::new();
2015        LOCK.get_or_init(|| Mutex::new(()))
2016    }
2017
2018    fn all_message_kinds() -> Vec<MessageKind> {
2019        (u8::MIN..=u8::MAX)
2020            .filter_map(MessageKind::from_u8)
2021            .collect()
2022    }
2023
2024    #[tokio::test]
2025    async fn redwire_execution_refuses_when_shared_runtime_queue_is_full() {
2026        let runtime = RedDBRuntime::in_memory().expect("runtime");
2027        runtime.configure_execution_admission(1, 0, 100);
2028        let admission = runtime.execution_admission();
2029        let active = admission.acquire().await.expect("active slot");
2030
2031        let error = redwire_execution_permit(&runtime).await.unwrap_err();
2032        assert_eq!(error.code(), "execution_queue_full");
2033        drop(active);
2034    }
2035
2036    #[tokio::test]
2037    async fn redwire_execution_reports_queue_deadline_separately() {
2038        let runtime = RedDBRuntime::in_memory().expect("runtime");
2039        runtime.configure_execution_admission(1, 1, 1);
2040        let admission = runtime.execution_admission();
2041        let active = admission.acquire().await.expect("active slot");
2042
2043        let error = redwire_execution_permit(&runtime).await.unwrap_err();
2044        assert_eq!(error.code(), "execution_queue_timeout");
2045        drop(active);
2046    }
2047
2048    #[test]
2049    fn every_redwire_frame_kind_resolves_to_a_catalog_command() {
2050        let catalog = crate::server::discovered_route_catalog();
2051
2052        for kind in all_message_kinds() {
2053            let Some(command_id) = redwire_command_id(kind) else {
2054                continue;
2055            };
2056            assert!(
2057                catalog.command(command_id).is_some(),
2058                "{kind:?} maps to missing catalog command {command_id}"
2059            );
2060        }
2061    }
2062
2063    #[test]
2064    fn command_coverage_matrix_reports_redwire_bindings() {
2065        let matrix = crate::server::route_catalog::render_command_coverage_matrix(
2066            crate::server::discovered_route_catalog(),
2067        );
2068
2069        assert!(
2070            matrix.contains("| command | auth requirement | HTTP | gRPC | MCP | stdio | RedWire |")
2071        );
2072        // Pin this transport's binding column. The stdio column describes
2073        // individual local/remote method dispositions, covered by catalog tests.
2074        for (command, expected) in [
2075            ("query.execute", "served"),
2076            ("catalog.snapshot", "undeclared"),
2077        ] {
2078            let row = matrix
2079                .lines()
2080                .find(|line| line.starts_with(&format!("| {command} |")))
2081                .expect("catalog command has a coverage row");
2082            assert_eq!(
2083                row.rsplit('|').nth(1).map(str::trim),
2084                Some(expected),
2085                "RedWire coverage for {command}: {row}"
2086            );
2087        }
2088    }
2089
2090    struct DenyAndCountPolicy(Cell<usize>);
2091
2092    impl crate::server::route_catalog::CommandPolicyEngine for DenyAndCountPolicy {
2093        fn allows(
2094            &self,
2095            _ctx: &OperationContext,
2096            _command: &crate::server::route_catalog::CommandSpec,
2097        ) -> bool {
2098            self.0.set(self.0.get() + 1);
2099            false
2100        }
2101    }
2102
2103    #[test]
2104    fn redwire_dispatch_consults_catalog_authorization() {
2105        let policy = DenyAndCountPolicy(Cell::new(0));
2106        let ctx = OperationContext::read_only("redwire-auth-test").with_principal("reader");
2107
2108        let error = authorize_redwire_frame(&ctx, MessageKind::Query, &policy)
2109            .expect_err("denying policy must reject the frame");
2110
2111        assert_eq!(policy.0.get(), 1);
2112        assert_eq!(
2113            error,
2114            crate::server::route_catalog::CommandAuthorizationError::Denied {
2115                command_id: "query.execute"
2116            }
2117        );
2118    }
2119
2120    #[test]
2121    fn redwire_local_admission_enforces_expiry_and_connection_limit() {
2122        let store = Arc::new(AuthStore::new(crate::auth::AuthConfig {
2123            enabled: true,
2124            require_auth: true,
2125            ..crate::auth::AuthConfig::default()
2126        }));
2127        store
2128            .create_user("alice", "secret", Role::Read)
2129            .expect("user");
2130        let id = crate::auth::UserId::platform("alice");
2131        store
2132            .set_user_attributes(
2133                &id,
2134                crate::auth::UserAttributes {
2135                    valid_until: Some(1),
2136                    ..crate::auth::UserAttributes::default()
2137                },
2138            )
2139            .expect("expired attributes");
2140        assert!(acquire_local_connection_lease(Some(&store), None, "alice").is_err());
2141
2142        store
2143            .set_user_attributes(
2144                &id,
2145                crate::auth::UserAttributes {
2146                    connection_limit: Some(1),
2147                    ..crate::auth::UserAttributes::default()
2148                },
2149            )
2150            .expect("limited attributes");
2151        let first = acquire_local_connection_lease(Some(&store), None, "alice")
2152            .expect("first admission")
2153            .expect("local lease");
2154        assert!(acquire_local_connection_lease(Some(&store), None, "alice").is_err());
2155        drop(first);
2156        assert!(acquire_local_connection_lease(Some(&store), None, "alice")
2157            .expect("slot released")
2158            .is_some());
2159    }
2160
2161    #[test]
2162    fn redwire_query_uses_operation_context_tenant_for_rls() {
2163        crate::runtime::execution_context::clear_current_tenant();
2164        crate::runtime::execution_context::clear_current_auth_identity();
2165        let runtime = RedDBRuntime::in_memory().expect("runtime");
2166        let prepared = PreparedRegistry::new();
2167        runtime
2168            .execute_query("CREATE TABLE docs (id INT, tenant_id TEXT)")
2169            .expect("create table");
2170        runtime
2171            .execute_query(
2172                "INSERT INTO docs (id, tenant_id) VALUES \
2173                 (1, 'acme'), (2, 'acme'), (3, 'globex')",
2174            )
2175            .expect("seed rows");
2176        runtime
2177            .execute_query(
2178                "CREATE POLICY tenant_read ON docs FOR SELECT \
2179                 USING (tenant_id = CURRENT_TENANT())",
2180            )
2181            .expect("create policy");
2182        runtime
2183            .execute_query("ALTER TABLE docs ENABLE ROW LEVEL SECURITY")
2184            .expect("enable rls");
2185
2186        let payload =
2187            reddb_wire::query_with_params::encode_query_with_params("SELECT id FROM docs", &[])
2188                .expect("encode query");
2189        let frame = reddb_wire::redwire::build_query_with_params_frame(7, payload)
2190            .expect("build query frame");
2191
2192        let visible_rows = |tenant: &str| {
2193            let ctx = OperationContextFactory::build(OperationContextInput {
2194                request_id: Some(format!("redwire-rls-{tenant}")),
2195                principal: Some("reader".to_string()),
2196                tenant: Some(tenant.to_string()),
2197                ..OperationContextInput::default()
2198            });
2199            let reply = execute_with_redwire_context(&ctx, Role::Read, || {
2200                run_query_with_params(&runtime, &prepared, &frame)
2201            });
2202            assert_eq!(reply.kind, MessageKind::Result);
2203            let body: JsonValue = serde_json::from_slice(&reply.payload).expect("result envelope");
2204            body.get("result")
2205                .and_then(|result| result.get("records"))
2206                .and_then(JsonValue::as_array)
2207                .map_or(0, |records| records.len())
2208        };
2209
2210        assert_eq!(visible_rows("acme"), 2);
2211        assert_eq!(visible_rows("globex"), 1);
2212        assert!(crate::runtime::execution_context::current_tenant().is_none());
2213        assert!(crate::runtime::execution_context::current_auth_identity().is_none());
2214    }
2215
2216    struct EnvGuard {
2217        previous: Vec<(&'static str, Option<String>)>,
2218    }
2219
2220    impl EnvGuard {
2221        fn set(vars: &[(&'static str, &'static str)]) -> Self {
2222            let previous = vars
2223                .iter()
2224                .map(|(key, _)| (*key, std::env::var(key).ok()))
2225                .collect();
2226            for (key, value) in vars {
2227                std::env::set_var(key, value);
2228            }
2229            Self { previous }
2230        }
2231    }
2232
2233    impl Drop for EnvGuard {
2234        fn drop(&mut self) {
2235            for (key, value) in self.previous.iter().rev() {
2236                match value {
2237                    Some(value) => std::env::set_var(key, value),
2238                    None => std::env::remove_var(key),
2239                }
2240            }
2241        }
2242    }
2243
2244    fn temp_data_path(name: &str) -> std::path::PathBuf {
2245        let mut path = std::env::temp_dir();
2246        path.push(format!(
2247            "reddb-redwire-{name}-{}-{}.rdb",
2248            std::process::id(),
2249            crate::utils::now_unix_millis()
2250        ));
2251        let _ = std::fs::remove_file(&path);
2252        path
2253    }
2254
2255    fn bulk_insert_frame(correlation_id: u64, payload: Vec<u8>) -> Frame {
2256        reddb_wire::redwire::build_bulk_insert_frame(correlation_id, payload)
2257            .expect("build bulk insert frame")
2258    }
2259
2260    fn create_graph_collection(runtime: &RedDBRuntime, name: &str) {
2261        let db = runtime.db();
2262        db.store()
2263            .create_collection(name)
2264            .expect("create collection");
2265        let now = std::time::SystemTime::now()
2266            .duration_since(std::time::UNIX_EPOCH)
2267            .unwrap_or_default()
2268            .as_millis();
2269        db.save_collection_contract(crate::physical::CollectionContract {
2270            name: name.to_string(),
2271            declared_model: crate::catalog::CollectionModel::Graph,
2272            schema_mode: crate::catalog::SchemaMode::Dynamic,
2273            origin: crate::physical::ContractOrigin::Explicit,
2274            version: 1,
2275            created_at_unix_ms: now,
2276            updated_at_unix_ms: now,
2277            default_ttl_ms: None,
2278            vector_dimension: None,
2279            vector_metric: None,
2280            context_index_fields: Vec::new(),
2281            declared_columns: Vec::new(),
2282            table_def: None,
2283            timestamps_enabled: false,
2284            context_index_enabled: false,
2285            metrics_raw_retention_ms: None,
2286            metrics_rollup_policies: Vec::new(),
2287            metrics_tenant_identity: None,
2288            metrics_namespace: None,
2289            append_only: false,
2290            subscriptions: Vec::new(),
2291            analytics_config: Vec::new(),
2292            session_key: None,
2293            session_gap_ms: None,
2294            retention_duration_ms: None,
2295            analytical_storage: None,
2296
2297            ai_policy: None,
2298        })
2299        .expect("save graph contract");
2300    }
2301
2302    #[test]
2303    fn magic_byte_is_0xfe() {
2304        assert_eq!(REDWIRE_MAGIC, 0xFE);
2305    }
2306
2307    #[test]
2308    fn redwire_bulk_insert_graph_rows_returns_ids() {
2309        let runtime = RedDBRuntime::in_memory().expect("runtime");
2310        create_graph_collection(&runtime, "network");
2311
2312        let nodes = bulk_insert_frame(
2313            7,
2314            br#"{"collection":"network","payloads":[{"label":"Host","name":"app"},{"label":"Host","name":"db"}]}"#.to_vec(),
2315        );
2316        let nodes_reply = run_insert_dispatch(&runtime, &nodes);
2317        assert_eq!(nodes_reply.kind, MessageKind::BulkOk);
2318        let node_body: JsonValue =
2319            serde_json::from_slice(&nodes_reply.payload).expect("nodes json");
2320        assert_eq!(
2321            node_body.get("affected").and_then(JsonValue::as_u64),
2322            Some(2)
2323        );
2324        let ids = node_body
2325            .get("ids")
2326            .and_then(JsonValue::as_array)
2327            .expect("node ids");
2328        assert_eq!(ids.len(), 2);
2329
2330        let from = ids[0].as_u64().expect("from id");
2331        let to = ids[1].as_u64().expect("to id");
2332        let edges = bulk_insert_frame(
2333            8,
2334            format!(
2335                r#"{{"collection":"network","payloads":[{{"label":"connects","from":{from},"to":{to},"role":"primary"}}]}}"#
2336            )
2337            .into_bytes(),
2338        );
2339        let edges_reply = run_insert_dispatch(&runtime, &edges);
2340        assert_eq!(edges_reply.kind, MessageKind::BulkOk);
2341        let edge_body: JsonValue =
2342            serde_json::from_slice(&edges_reply.payload).expect("edges json");
2343        assert_eq!(
2344            edge_body.get("affected").and_then(JsonValue::as_u64),
2345            Some(1)
2346        );
2347        assert_eq!(
2348            edge_body
2349                .get("ids")
2350                .and_then(JsonValue::as_array)
2351                .map(|ids| ids.len()),
2352            Some(1)
2353        );
2354    }
2355
2356    #[test]
2357    fn bulk_insert_dispatch_surfaces_row_ids() {
2358        let runtime = RedDBRuntime::in_memory().expect("runtime");
2359
2360        let single = bulk_insert_frame(
2361            9,
2362            br#"{"collection":"rows_rid","payload":{"name":"eve"}}"#.to_vec(),
2363        );
2364        let reply = run_insert_dispatch(&runtime, &single);
2365        assert_eq!(
2366            reply.kind,
2367            MessageKind::BulkOk,
2368            "body={:?}",
2369            String::from_utf8_lossy(&reply.payload)
2370        );
2371        let body: JsonValue = serde_json::from_slice(&reply.payload).expect("single json");
2372        assert_eq!(body.get("affected").and_then(JsonValue::as_u64), Some(1));
2373        assert!(
2374            body.get("id").and_then(JsonValue::as_u64).is_some(),
2375            "single insert must carry the assigned id: {body}"
2376        );
2377
2378        let batch = bulk_insert_frame(
2379            10,
2380            br#"{"collection":"rows_rid","payloads":[{"name":"a"},{"name":"b"}]}"#.to_vec(),
2381        );
2382        let reply = run_insert_dispatch(&runtime, &batch);
2383        assert_eq!(reply.kind, MessageKind::BulkOk);
2384        let body: JsonValue = serde_json::from_slice(&reply.payload).expect("batch json");
2385        assert_eq!(body.get("affected").and_then(JsonValue::as_u64), Some(2));
2386        let ids: Vec<u64> = body
2387            .get("ids")
2388            .and_then(JsonValue::as_array)
2389            .expect("ids array")
2390            .iter()
2391            .map(|id| id.as_u64().expect("numeric id"))
2392            .collect();
2393        assert_eq!(ids.len(), 2);
2394        assert_ne!(ids[0], ids[1], "ids must be distinct");
2395    }
2396
2397    #[test]
2398    fn get_and_delete_dispatch_address_rows_by_rid() {
2399        use reddb_wire::redwire::operations::{
2400            decode_delete_ok_affected, decode_get_result_payload, encode_key_payload,
2401        };
2402        use reddb_wire::redwire::{build_delete_frame, build_get_frame};
2403
2404        let runtime = RedDBRuntime::in_memory().expect("runtime");
2405        let insert = bulk_insert_frame(
2406            11,
2407            br#"{"collection":"rows_rid_del","payload":{"name":"k"}}"#.to_vec(),
2408        );
2409        let reply = run_insert_dispatch(&runtime, &insert);
2410        assert_eq!(reply.kind, MessageKind::BulkOk);
2411        let body: JsonValue = serde_json::from_slice(&reply.payload).expect("insert json");
2412        let rid = body
2413            .get("id")
2414            .and_then(JsonValue::as_u64)
2415            .expect("assigned id");
2416        let rid = rid.to_string();
2417
2418        let get = build_get_frame(12, encode_key_payload("rows_rid_del", &rid)).expect("get");
2419        let reply = run_get(&runtime, &get);
2420        assert_eq!(reply.kind, MessageKind::Result);
2421        let found = decode_get_result_payload(&reply.payload).expect("get payload");
2422        assert_eq!(found.get("found").and_then(|v| v.as_bool()), Some(true));
2423
2424        // A non-numeric id names no row: not found / 0 affected, and the
2425        // text is never spliced into SQL.
2426        for id in ["1 OR 1=1", "'1'", "rid_that_does_not_exist"] {
2427            let get = build_get_frame(13, encode_key_payload("rows_rid_del", id)).expect("get");
2428            let reply = run_get(&runtime, &get);
2429            assert_eq!(reply.kind, MessageKind::Result, "{id:?}");
2430            let found = decode_get_result_payload(&reply.payload).expect("get payload");
2431            assert_eq!(found.get("found").and_then(|v| v.as_bool()), Some(false));
2432
2433            let del = build_delete_frame(13, encode_key_payload("rows_rid_del", id)).expect("del");
2434            let reply = run_delete(&runtime, &del);
2435            assert_eq!(reply.kind, MessageKind::DeleteOk, "{id:?}");
2436            assert_eq!(
2437                decode_delete_ok_affected(&reply.payload).expect("affected"),
2438                0
2439            );
2440        }
2441        // A collection that is not a bare identifier is refused outright.
2442        let del = build_delete_frame(13, encode_key_payload("rows_rid_del WHERE 1=1 --", &rid))
2443            .expect("del");
2444        let reply = run_delete(&runtime, &del);
2445        assert_eq!(reply.kind, MessageKind::Error);
2446
2447        let del = build_delete_frame(14, encode_key_payload("rows_rid_del", &rid)).expect("del");
2448        let reply = run_delete(&runtime, &del);
2449        assert_eq!(reply.kind, MessageKind::DeleteOk);
2450        assert_eq!(
2451            decode_delete_ok_affected(&reply.payload).expect("affected"),
2452            1
2453        );
2454
2455        let reply = run_delete(&runtime, &del);
2456        assert_eq!(reply.kind, MessageKind::DeleteOk);
2457        assert_eq!(
2458            decode_delete_ok_affected(&reply.payload).expect("affected"),
2459            0
2460        );
2461    }
2462
2463    #[test]
2464    fn redwire_query_with_params_preserves_json_columns() {
2465        let runtime = RedDBRuntime::in_memory().expect("runtime");
2466        let prepared = PreparedRegistry::new();
2467        runtime
2468            .execute_query("KV PUT proj.a.b.c.d = 12")
2469            .expect("put nested number");
2470        runtime
2471            .execute_query("KV PUT proj.a.b.e = 'x'")
2472            .expect("put nested string");
2473
2474        let payload =
2475            reddb_wire::query_with_params::encode_query_with_params("LIST KV proj AS JSON", &[])
2476                .expect("encode query with params");
2477        let frame = reddb_wire::redwire::build_query_with_params_frame(99, payload)
2478            .expect("query-with-params frame");
2479        let reply = run_query_with_params(&runtime, &prepared, &frame);
2480
2481        assert_eq!(
2482            reply.kind,
2483            MessageKind::Result,
2484            "body={}",
2485            String::from_utf8_lossy(&reply.payload)
2486        );
2487        let body: JsonValue = serde_json::from_slice(&reply.payload).expect("result json");
2488        let value = body
2489            .get("result")
2490            .and_then(|result| result.get("records"))
2491            .and_then(JsonValue::as_array)
2492            .and_then(|records| records.first())
2493            .and_then(|record| record.get("values"))
2494            .and_then(|values| values.get("value"))
2495            .expect("json value column");
2496
2497        assert_eq!(
2498            value
2499                .get("a")
2500                .and_then(|a| a.get("b"))
2501                .and_then(|b| b.get("c"))
2502                .and_then(|c| c.get("d"))
2503                .and_then(JsonValue::as_f64),
2504            Some(12.0)
2505        );
2506        assert_eq!(
2507            value
2508                .get("a")
2509                .and_then(|a| a.get("b"))
2510                .and_then(|b| b.get("e"))
2511                .and_then(JsonValue::as_str),
2512            Some("x")
2513        );
2514    }
2515
2516    /// The two query frames answer with deliberately different shapes:
2517    /// `Query` (0x01) replies with the summary payload the protocol pins,
2518    /// `QueryWithParams` (0x28) with the full result envelope. Clients pick
2519    /// the frame by the reply they want, so neither may drift into the other.
2520    #[test]
2521    fn redwire_query_replies_summary_and_query_with_params_replies_full_envelope() {
2522        let runtime = RedDBRuntime::in_memory().expect("runtime");
2523        let prepared = PreparedRegistry::new();
2524
2525        let query =
2526            reddb_wire::redwire::build_query_frame(98, "SELECT 7 AS value").expect("query frame");
2527        let query_reply = run_query(&runtime, &prepared, &query);
2528        assert_eq!(query_reply.kind, MessageKind::Result);
2529        let summary =
2530            reddb_wire::redwire::operations::decode_query_result_payload(&query_reply.payload)
2531                .expect("Query replies with the summary payload");
2532        assert_eq!(
2533            summary.get("statement").and_then(|value| value.as_str()),
2534            Some("select")
2535        );
2536        assert_eq!(
2537            summary.get("affected").and_then(|value| value.as_u64()),
2538            Some(0),
2539            "affected is pinned even when it is 0, got {summary}"
2540        );
2541        assert!(
2542            summary.get("result").is_none(),
2543            "Query must not carry the full result envelope, got {summary}"
2544        );
2545
2546        let payload = reddb_wire::query_with_params::encode_query_with_params(
2547            "SELECT $1 AS value",
2548            &[reddb_wire::query_with_params::ParamValue::Int(7)],
2549        )
2550        .expect("encode query with params");
2551        let query_with_params = reddb_wire::redwire::build_query_with_params_frame(99, payload)
2552            .expect("query-with-params frame");
2553        let params_reply = run_query_with_params(&runtime, &prepared, &query_with_params);
2554        assert_eq!(params_reply.kind, MessageKind::Result);
2555        let body: JsonValue =
2556            serde_json::from_slice(&params_reply.payload).expect("full result envelope");
2557        assert_eq!(
2558            body.get("result")
2559                .and_then(|result| result.get("records"))
2560                .and_then(JsonValue::as_array)
2561                .and_then(|records| records.first())
2562                .and_then(|record| record.get("values"))
2563                .and_then(|values| values.get("value"))
2564                .and_then(JsonValue::as_f64),
2565            Some(7.0)
2566        );
2567    }
2568
2569    #[test]
2570    fn cancelled_redwire_query_releases_shared_query_capacity() {
2571        let _lock = env_lock().lock().expect("environment lock");
2572        let _env = EnvGuard::set(&[("RED_MAX_CONCURRENT_QUERIES", "1")]);
2573        let runtime = RedDBRuntime::in_memory().expect("runtime");
2574        let prepared = PreparedRegistry::new();
2575        let query = reddb_wire::redwire::build_query_frame(100, "SELECT 1 AS healthy")
2576            .expect("query frame");
2577
2578        let cancellation = crate::runtime::query_budget::RequestCancellation::new();
2579        let request_scope = cancellation.install();
2580        cancellation.cancel();
2581        let cancelled = run_query(&runtime, &prepared, &query);
2582        assert_eq!(cancelled.kind, MessageKind::Error);
2583        assert!(
2584            String::from_utf8_lossy(&cancelled.payload).contains("duration:query_cancelled"),
2585            "unexpected cancellation payload: {}",
2586            String::from_utf8_lossy(&cancelled.payload)
2587        );
2588        drop(request_scope);
2589
2590        let recovered = run_query(&runtime, &prepared, &query);
2591        assert_eq!(
2592            recovered.kind,
2593            MessageKind::Result,
2594            "cancelled query must release the only shared admission slot"
2595        );
2596    }
2597
2598    /// Issue #2139 AC1 — the prepared frames run on the connection's shared
2599    /// registry, so a prepared id is scoped to the connection that minted it,
2600    /// DDL invalidates the shape, and the kill switch closes both frames.
2601    /// Driven through the same handler + rewrap the session loop uses, so the
2602    /// legacy reply encodings are exercised too.
2603    #[test]
2604    fn redwire_prepared_requests_use_connection_registry_guards() {
2605        let runtime = RedDBRuntime::in_memory().expect("runtime");
2606        runtime
2607            .execute_query("CREATE TABLE redwire_prepared (id INTEGER)")
2608            .expect("create table");
2609        runtime
2610            .execute_query("INSERT INTO redwire_prepared (id) VALUES (7)")
2611            .expect("seed row");
2612        let mut first_connection = crate::wire::listener::PreparedStatements::default();
2613        let second_connection = crate::wire::listener::PreparedStatements::default();
2614
2615        let prepare_payload = reddb_wire::redwire::encode_prepare_payload(
2616            41,
2617            "SELECT * FROM redwire_prepared WHERE id = 7",
2618        )
2619        .expect("encode prepare");
2620        let prepared = rewrap_length_prefixed_handler_response(
2621            &crate::wire::listener::handle_prepare(
2622                &runtime,
2623                &prepare_payload,
2624                &mut first_connection,
2625            ),
2626            100,
2627        );
2628        assert_eq!(prepared.kind, MessageKind::PreparedOk);
2629        let parameter_count = u16::from_le_bytes([prepared.payload[4], prepared.payload[5]]);
2630        assert_eq!(
2631            parameter_count, 1,
2632            "the literal `7` is auto-parameterized into one bind"
2633        );
2634
2635        let execute_payload = reddb_wire::redwire::encode_execute_prepared_payload(
2636            41,
2637            &[reddb_wire::legacy::WireValue::I64(7)],
2638        )
2639        .expect("encode execute prepared");
2640        let executed = rewrap_length_prefixed_handler_response(
2641            &crate::wire::listener::handle_execute_prepared(
2642                &runtime,
2643                &execute_payload,
2644                &first_connection,
2645            ),
2646            101,
2647        );
2648        assert_eq!(executed.kind, MessageKind::Result);
2649
2650        let wrong_connection = rewrap_length_prefixed_handler_response(
2651            &crate::wire::listener::handle_execute_prepared(
2652                &runtime,
2653                &execute_payload,
2654                &second_connection,
2655            ),
2656            102,
2657        );
2658        assert_eq!(wrong_connection.kind, MessageKind::Error);
2659        assert!(
2660            String::from_utf8_lossy(&wrong_connection.payload).contains("unknown prepared stmt_id")
2661        );
2662
2663        runtime
2664            .execute_query("CREATE TABLE redwire_prepared_epoch (id INTEGER)")
2665            .expect("advance DDL epoch");
2666        let stale = rewrap_length_prefixed_handler_response(
2667            &crate::wire::listener::handle_execute_prepared(
2668                &runtime,
2669                &execute_payload,
2670                &first_connection,
2671            ),
2672            103,
2673        );
2674        assert_eq!(stale.kind, MessageKind::Error);
2675        assert_eq!(
2676            String::from_utf8_lossy(&stale.payload),
2677            "prepared_needs_replan"
2678        );
2679
2680        first_connection.registry().disable();
2681        let disabled = rewrap_length_prefixed_handler_response(
2682            &crate::wire::listener::handle_execute_prepared(
2683                &runtime,
2684                &execute_payload,
2685                &first_connection,
2686            ),
2687            104,
2688        );
2689        assert_eq!(disabled.kind, MessageKind::Error);
2690        assert_eq!(
2691            String::from_utf8_lossy(&disabled.payload),
2692            "prepared statements disabled"
2693        );
2694    }
2695
2696    #[test]
2697    fn redwire_query_with_params_request_policy_strengthens_to_ack_n() {
2698        let _env_lock = env_lock().lock().expect("env lock");
2699        let _env = EnvGuard::set(&[
2700            ("RED_PRIMARY_COMMIT_POLICY", "local"),
2701            ("RED_REPLICATION_ACK_TIMEOUT_MS", "20"),
2702            ("RED_COMMIT_FAIL_ON_TIMEOUT", "true"),
2703        ]);
2704        let data_path = temp_data_path("request-ack-n");
2705        let runtime = RedDBRuntime::with_options(
2706            crate::api::RedDBOptions::persistent(&data_path)
2707                .with_replication(crate::replication::ReplicationConfig::primary()),
2708        )
2709        .expect("runtime");
2710        let prepared = PreparedRegistry::new();
2711
2712        let payload = reddb_wire::query_with_params::encode_query_with_params_request(
2713            "INSERT INTO redwire_request_ack (id, name) VALUES (1, 'alpha')",
2714            &[],
2715            &reddb_wire::query_with_params::QueryWithParamsOptions {
2716                commit_policy: Some("ack_n=1".to_string()),
2717            },
2718        )
2719        .expect("encode query with request policy");
2720        let frame = reddb_wire::redwire::build_query_with_params_frame(100, payload)
2721            .expect("query-with-params frame");
2722        let reply = run_query_with_params(&runtime, &prepared, &frame);
2723
2724        assert_eq!(reply.kind, MessageKind::Error);
2725        let body = String::from_utf8_lossy(&reply.payload);
2726        assert!(
2727            body.contains("commit policy timed out") && body.contains("RED_COMMIT_FAIL_ON_TIMEOUT"),
2728            "request ack_n should wait for replica ack, got {body}"
2729        );
2730        let _ = std::fs::remove_file(data_path);
2731    }
2732
2733    #[test]
2734    fn redwire_query_with_params_rejects_request_policy_below_floor() {
2735        let _env_lock = env_lock().lock().expect("env lock");
2736        let _env = EnvGuard::set(&[("RED_PRIMARY_COMMIT_POLICY", "quorum")]);
2737        let runtime = RedDBRuntime::in_memory().expect("runtime");
2738        let prepared = PreparedRegistry::new();
2739        runtime
2740            .execute_query("CREATE TABLE redwire_request_floor (id INTEGER, name TEXT)")
2741            .expect("create table");
2742
2743        let payload = reddb_wire::query_with_params::encode_query_with_params_request(
2744            "INSERT INTO redwire_request_floor (id, name) VALUES (1, 'alpha')",
2745            &[],
2746            &reddb_wire::query_with_params::QueryWithParamsOptions {
2747                commit_policy: Some("local".to_string()),
2748            },
2749        )
2750        .expect("encode query with request policy");
2751        let frame = reddb_wire::redwire::build_query_with_params_frame(101, payload)
2752            .expect("query-with-params frame");
2753        let reply = run_query_with_params(&runtime, &prepared, &frame);
2754
2755        assert_eq!(reply.kind, MessageKind::Error);
2756        let body = String::from_utf8_lossy(&reply.payload);
2757        assert!(
2758            body.contains("COMMIT_POLICY_BELOW_FLOOR"),
2759            "typed floor violation should be surfaced, got {body}"
2760        );
2761
2762        // Ordering is observable, so pin it: the Request seam rejects a
2763        // below-floor policy *before* the write lands. RedWire used to run
2764        // the INSERT and then fail the policy check, leaving the row behind.
2765        let rows = runtime
2766            .execute_query("SELECT id FROM redwire_request_floor")
2767            .expect("read back the rejected write");
2768        assert!(
2769            rows.result.records.is_empty(),
2770            "a policy-rejected write must not be visible"
2771        );
2772    }
2773
2774    /// The pre-write policy check is for mutations only. A read carrying a
2775    /// below-floor policy still returns rows, exactly as it did before the
2776    /// Request seam existed.
2777    #[test]
2778    fn redwire_query_with_params_read_ignores_request_policy_below_floor() {
2779        let _env_lock = env_lock().lock().expect("env lock");
2780        let _env = EnvGuard::set(&[("RED_PRIMARY_COMMIT_POLICY", "quorum")]);
2781        let runtime = RedDBRuntime::in_memory().expect("runtime");
2782        let prepared = PreparedRegistry::new();
2783        runtime
2784            .execute_query("CREATE TABLE redwire_request_read (id INTEGER)")
2785            .expect("create table");
2786        runtime
2787            .execute_query("INSERT INTO redwire_request_read (id) VALUES (1)")
2788            .expect("seed row");
2789
2790        let payload = reddb_wire::query_with_params::encode_query_with_params_request(
2791            "SELECT id FROM redwire_request_read",
2792            &[],
2793            &reddb_wire::query_with_params::QueryWithParamsOptions {
2794                commit_policy: Some("local".to_string()),
2795            },
2796        )
2797        .expect("encode query with request policy");
2798        let frame = reddb_wire::redwire::build_query_with_params_frame(102, payload)
2799            .expect("query-with-params frame");
2800        let reply = run_query_with_params(&runtime, &prepared, &frame);
2801
2802        assert_eq!(
2803            reply.kind,
2804            MessageKind::Result,
2805            "body={}",
2806            String::from_utf8_lossy(&reply.payload)
2807        );
2808    }
2809
2810    // ── Issue #587 — BatchInsertEndpoint RedWire mirror ──────────────
2811    //
2812    // The brief carries the rows + idempotency key in the existing
2813    // `BulkInsert` (0x04) frame: the presence of `idempotency_key` in
2814    // the JSON payload flips the dispatch onto the analytics batch
2815    // path (all-or-nothing commit, AnalyticsSchemaRegistry validation,
2816    // process-wide cache shared with HTTP #582 and gRPC #585). Each
2817    // test below maps to one acceptance bullet.
2818
2819    /// Bullet 1 — wire form: `BulkInsert` payload with
2820    /// `idempotency_key` routes to the batch path; success returns a
2821    /// `BulkOk` frame carrying `{"ok":true,"count":N}`. Bullet 5 —
2822    /// every row commits in submission order (we read them back and
2823    /// assert ascending storage order matches insertion order).
2824    #[test]
2825    fn redwire_batch_insert_happy_path_returns_bulkok_with_count() {
2826        let runtime = RedDBRuntime::in_memory().expect("runtime");
2827        runtime
2828            .execute_query("CREATE TABLE events_587_ok (id INTEGER, name TEXT)")
2829            .expect("create table");
2830
2831        let frame = bulk_insert_frame(
2832            100,
2833            br#"{
2834                "collection":"events_587_ok",
2835                "idempotency_key":"k-ok",
2836                "payloads":[
2837                    {"fields":{"id":1,"name":"a"}},
2838                    {"fields":{"id":2,"name":"b"}},
2839                    {"fields":{"id":3,"name":"c"}}
2840                ]
2841            }"#
2842            .to_vec(),
2843        );
2844        let reply = run_insert_dispatch(&runtime, &frame);
2845        assert_eq!(
2846            reply.kind,
2847            MessageKind::BulkOk,
2848            "body={:?}",
2849            String::from_utf8_lossy(&reply.payload)
2850        );
2851        let body: JsonValue = serde_json::from_slice(&reply.payload).expect("ok body json");
2852        assert_eq!(body.get("ok").and_then(JsonValue::as_bool), Some(true));
2853        assert_eq!(body.get("count").and_then(JsonValue::as_u64), Some(3));
2854
2855        // Submission-order commit — every row landed and the scan can
2856        // see them all. (CDC ordering is a property of
2857        // `create_rows_batch`, which the shared
2858        // `process_batch_insert` re-uses; we pin the user-observable
2859        // surface here.)
2860        let qr = runtime
2861            .execute_query("SELECT name FROM events_587_ok ORDER BY id ASC")
2862            .expect("scan");
2863        let names: Vec<String> = qr
2864            .result
2865            .records
2866            .iter()
2867            .filter_map(|record| match record.get("name") {
2868                Some(reddb_types::Value::Text(s)) => Some(s.to_string()),
2869                _ => None,
2870            })
2871            .collect();
2872        assert_eq!(names, vec!["a", "b", "c"]);
2873    }
2874
2875    /// Bullet 3 — row K's failure rolls back the whole batch; the
2876    /// reply is an `Error` frame whose JSON body carries the failing
2877    /// `row_index` so the client can pinpoint the broken row without
2878    /// re-uploading.
2879    #[test]
2880    fn redwire_batch_insert_row_failure_rolls_back_with_row_index() {
2881        let runtime = RedDBRuntime::in_memory().expect("runtime");
2882        runtime
2883            .execute_query("CREATE TABLE events_587_rollback (id INTEGER, name TEXT)")
2884            .expect("create table");
2885
2886        // Row index 1 omits the required `fields` envelope — the parse
2887        // step rejects before any commit fires.
2888        let frame = bulk_insert_frame(
2889            101,
2890            br#"{
2891                "collection":"events_587_rollback",
2892                "idempotency_key":"k-rollback",
2893                "payloads":[
2894                    {"fields":{"id":1,"name":"a"}},
2895                    {"not_fields":{"id":2}},
2896                    {"fields":{"id":3,"name":"c"}}
2897                ]
2898            }"#
2899            .to_vec(),
2900        );
2901        let reply = run_insert_dispatch(&runtime, &frame);
2902        assert_eq!(reply.kind, MessageKind::Error);
2903        let body: JsonValue = serde_json::from_slice(&reply.payload).expect("err body json");
2904        assert_eq!(body.get("ok").and_then(JsonValue::as_bool), Some(false));
2905        assert_eq!(
2906            body.get("code").and_then(JsonValue::as_str),
2907            Some("RowParseFailure")
2908        );
2909        assert_eq!(body.get("row_index").and_then(JsonValue::as_u64), Some(1));
2910
2911        // Storage untouched — row 0 was never committed even though
2912        // it would have parsed cleanly on its own.
2913        let qr = runtime
2914            .execute_query("SELECT name FROM events_587_rollback")
2915            .expect("scan");
2916        assert!(
2917            qr.result.records.is_empty(),
2918            "row 0 leaked despite row 1 rejection: {} rows present",
2919            qr.result.records.len()
2920        );
2921    }
2922
2923    /// Bullet 2 — `idempotency_key` carried in the frame; the
2924    /// process-wide cache (shared with HTTP slice 4) replays a
2925    /// previous success byte-for-byte even when the retry's body
2926    /// differs from the original. The HTTP slice 4 already pins the
2927    /// cross-call behaviour at its boundary; this test pins the
2928    /// RedWire boundary plus the cross-transport sharing (a retry on
2929    /// the same key via HTTP returns the body RedWire just produced).
2930    #[test]
2931    fn redwire_batch_insert_idempotency_key_replays_cached_result() {
2932        let runtime = RedDBRuntime::in_memory().expect("runtime");
2933        runtime
2934            .execute_query("CREATE TABLE events_587_dedup (id INTEGER, name TEXT)")
2935            .expect("create table");
2936
2937        // Use a process-unique key so this test doesn't trample
2938        // (or get trampled by) the HTTP-side dedup test that shares
2939        // the global cache.
2940        let key = format!(
2941            "redwire-587-{}",
2942            std::time::SystemTime::now()
2943                .duration_since(std::time::UNIX_EPOCH)
2944                .unwrap()
2945                .as_nanos()
2946        );
2947
2948        let frame1 = bulk_insert_frame(
2949            200,
2950            format!(
2951                r#"{{
2952                    "collection":"events_587_dedup",
2953                    "idempotency_key":"{key}",
2954                    "payloads":[{{"fields":{{"id":1,"name":"first"}}}}]
2955                }}"#
2956            )
2957            .into_bytes(),
2958        );
2959        let reply1 = run_insert_dispatch(&runtime, &frame1);
2960        assert_eq!(reply1.kind, MessageKind::BulkOk);
2961        let body1 = reply1.payload.clone();
2962
2963        // Replay with the same key + DIFFERENT body — the cache
2964        // returns the original bytes verbatim and the second row is
2965        // not committed.
2966        let frame2 = bulk_insert_frame(
2967            201,
2968            format!(
2969                r#"{{
2970                    "collection":"events_587_dedup",
2971                    "idempotency_key":"{key}",
2972                    "payloads":[{{"fields":{{"id":2,"name":"second"}}}}]
2973                }}"#
2974            )
2975            .into_bytes(),
2976        );
2977        let reply2 = run_insert_dispatch(&runtime, &frame2);
2978        assert_eq!(reply2.kind, MessageKind::BulkOk);
2979        assert_eq!(
2980            reply2.payload, body1,
2981            "replay must return cached body byte-for-byte"
2982        );
2983
2984        let qr = runtime
2985            .execute_query("SELECT name FROM events_587_dedup")
2986            .expect("scan");
2987        assert_eq!(
2988            qr.result.records.len(),
2989            1,
2990            "replay re-executed and committed the second row"
2991        );
2992    }
2993
2994    /// Bullet 2 (cont.) — the cache is *shared with HTTP slice 4*: a
2995    /// RedWire submission populates the cache, and a same-key HTTP
2996    /// retry returns the cached body verbatim.
2997    #[test]
2998    fn redwire_batch_insert_cache_shared_with_http_transport() {
2999        use crate::runtime::batch_insert::global_cache;
3000
3001        let runtime = RedDBRuntime::in_memory().expect("runtime");
3002        runtime
3003            .execute_query("CREATE TABLE events_587_shared (id INTEGER, name TEXT)")
3004            .expect("create table");
3005
3006        let key = format!(
3007            "shared-cache-587-{}",
3008            std::time::SystemTime::now()
3009                .duration_since(std::time::UNIX_EPOCH)
3010                .unwrap()
3011                .as_nanos()
3012        );
3013
3014        let frame = bulk_insert_frame(
3015            300,
3016            format!(
3017                r#"{{
3018                    "collection":"events_587_shared",
3019                    "idempotency_key":"{key}",
3020                    "payloads":[{{"fields":{{"id":1,"name":"x"}}}}]
3021                }}"#
3022            )
3023            .into_bytes(),
3024        );
3025        let reply = run_insert_dispatch(&runtime, &frame);
3026        assert_eq!(reply.kind, MessageKind::BulkOk);
3027
3028        // Look the entry up directly via the process-wide cache that
3029        // both HTTP and RedWire share. A hit here is the entire
3030        // "shared with HTTP slice 4" contract.
3031        let hit = global_cache()
3032            .lookup("events_587_shared", &key, std::time::Instant::now())
3033            .expect("shared cache must serve the RedWire write to HTTP");
3034        assert_eq!(hit.status, 200);
3035        assert_eq!(hit.body, reply.payload);
3036    }
3037
3038    /// Bullet 4 — schema-validation failure mirrors the other
3039    /// transports: a row that the `AnalyticsSchemaRegistry` rejects
3040    /// surfaces as `RowSchemaRejected` with the offending `row_index`,
3041    /// and the batch leaves the collection untouched.
3042    #[test]
3043    fn redwire_batch_insert_schema_validation_rejects_unknown_field() {
3044        use crate::runtime::analytics_schema_registry as reg;
3045
3046        let runtime = RedDBRuntime::in_memory().expect("runtime");
3047        runtime
3048            .execute_query("CREATE TABLE events_587_schema (event_name TEXT, payload TEXT)")
3049            .expect("create table");
3050
3051        let schema =
3052            r#"{"type":"object","properties":{"url":{"type":"string"}},"required":["url"]}"#;
3053        reg::register(runtime.db().store().as_ref(), "click_587", schema).expect("register schema");
3054
3055        let frame = bulk_insert_frame(
3056            400,
3057            br#"{
3058                "collection":"events_587_schema",
3059                "idempotency_key":"k-schema",
3060                "payloads":[
3061                    {"fields":{"event_name":"click_587","payload":"{\"url\":\"/a\"}"}},
3062                    {"fields":{"event_name":"click_587","payload":"{\"url\":\"/b\",\"extra\":1}"}}
3063                ]
3064            }"#
3065            .to_vec(),
3066        );
3067        let reply = run_insert_dispatch(&runtime, &frame);
3068        assert_eq!(reply.kind, MessageKind::Error);
3069        let body: JsonValue = serde_json::from_slice(&reply.payload).expect("err body json");
3070        assert_eq!(
3071            body.get("code").and_then(JsonValue::as_str),
3072            Some("RowSchemaRejected")
3073        );
3074        assert_eq!(body.get("row_index").and_then(JsonValue::as_u64), Some(1));
3075
3076        let qr = runtime
3077            .execute_query("SELECT event_name FROM events_587_schema")
3078            .expect("scan");
3079        assert!(
3080            qr.result.records.is_empty(),
3081            "row 0 leaked despite row 1 schema rejection"
3082        );
3083    }
3084
3085    /// Bullet 4 (cont.) — oversize fails with `BatchTooLarge` and a
3086    /// 413-equivalent status; the storage is never touched.
3087    ///
3088    /// Build one row past the default ceiling rather than mutating
3089    /// `RED_BATCH_MAX_ROWS`. The env var is process-wide and the
3090    /// `cargo test` runner schedules tests in parallel; a `set_var`
3091    /// here leaks into sibling tests in this crate (e.g. the
3092    /// row-failure case sees its 3-row batch flagged as oversize).
3093    /// The HTTP slice 4 test takes the same "build past the default"
3094    /// route for the same reason.
3095    #[test]
3096    fn redwire_batch_insert_oversize_returns_error_before_storage() {
3097        let runtime = RedDBRuntime::in_memory().expect("runtime");
3098        runtime
3099            .execute_query("CREATE TABLE events_587_oversize (id INTEGER, name TEXT)")
3100            .expect("create table");
3101
3102        // Default `red.batch.max_rows = 10_000`; submit one more.
3103        let max = 10_000usize;
3104        let mut payloads = String::with_capacity(max * 32);
3105        payloads.push('[');
3106        for i in 0..(max + 1) {
3107            if i > 0 {
3108                payloads.push(',');
3109            }
3110            payloads.push_str(&format!(r#"{{"fields":{{"id":{i},"name":"x"}}}}"#));
3111        }
3112        payloads.push(']');
3113        let frame_body = format!(
3114            r#"{{"collection":"events_587_oversize","idempotency_key":"k-oversize-587","payloads":{payloads}}}"#
3115        );
3116        let frame = bulk_insert_frame(500, frame_body.into_bytes());
3117        let reply = run_insert_dispatch(&runtime, &frame);
3118
3119        assert_eq!(reply.kind, MessageKind::Error);
3120        let body: JsonValue = serde_json::from_slice(&reply.payload).expect("err body json");
3121        assert_eq!(
3122            body.get("code").and_then(JsonValue::as_str),
3123            Some("BatchTooLarge")
3124        );
3125        let qr = runtime
3126            .execute_query("SELECT name FROM events_587_oversize")
3127            .expect("scan");
3128        assert!(
3129            qr.result.records.is_empty(),
3130            "oversize batch leaked rows into storage"
3131        );
3132    }
3133}
3134
3135#[cfg(test)]
3136mod session_bulk_stream_tests;