1use 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 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
137pub(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 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
299fn 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 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 let connection = Arc::new(
391 runtime
392 .acquire_wire_connection()
393 .map_err(|error| io::Error::other(error.to_string()))?,
394 );
395
396 let mut stream_session: Option<crate::wire::listener::BulkStreamSession> = None;
401 let mut prepared_stmts = crate::wire::listener::PreparedStatements::default();
402
403 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 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 let mut input_registry = super::input_stream::InputStreamRegistry::new();
428
429 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 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 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 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 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 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 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 MessageKind::OpenStream => {
774 use super::output_stream as os;
775 let frame_id = frame.correlation_id;
776 let sid = frame.stream_id;
777
778 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 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 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 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 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 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 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 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 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 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 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 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 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 } else if let Some(state) = input_registry.remove(sid) {
1168 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 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
1206const 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
1216async 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 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 return Ok(None);
1236 }
1237
1238 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 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 if chosen == "scram-sha-256" {
1311 return perform_scram_handshake(stream, auth_store, hello.correlation_id, server_features)
1312 .await;
1313 }
1314
1315 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 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 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 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
1499async 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 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 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
1655fn 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
1739fn 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 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 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
1892fn 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
1945fn 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 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
1975fn 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 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 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 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 #[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(¶ms_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 #[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 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 #[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 #[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 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 #[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 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 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 #[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 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 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 #[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 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 #[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 #[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 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;