1use std::io;
13use std::sync::Arc;
14
15use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt};
16use tokio::sync::{mpsc, Mutex as TokioMutex};
17
18use crate::auth::store::AuthStore;
19use crate::auth::Role;
20use crate::runtime::RedDBRuntime;
21use crate::serde_json::{self, Value as JsonValue};
22use reddb_wire::query_with_params::{
23 decode_query_with_params_request, ParamValue as RedWireParamValue, FEATURE_PARAMS,
24};
25use reddb_wire::redwire::operations::{
26 decode_delete_payload, decode_get_payload, decode_insert_dispatch_payload,
27 encode_bulk_ok_payload_from_json_id_literals, encode_delete_ok_payload,
28 encode_get_result_payload, encode_query_result_summary_payload,
29};
30
31use super::auth::{build_auth_ok, pick_auth_method, validate_auth_response, AuthOutcome};
32use super::validate_minor_version;
33use reddb_wire::redwire::handshake::{
34 build_auth_fail_payload, build_auth_ok_frame_from_payload, build_hello_ack_frame,
35 expect_auth_response_payload, parse_auth_response_oauth_jwt, Hello,
36};
37use reddb_wire::redwire::{
38 build_dispatch_reply_frame, build_error_frame_lossy, build_reply_frame,
39 choose_hello_minor_version, decode_frame, encode_frame, read_frame_async,
40 rewrap_length_prefixed_handler_response, Frame, MessageDirection, MessageKind, REDWIRE_MAGIC,
41};
42
43#[derive(Debug)]
44struct AuthedSession {
45 username: String,
46 role: Role,
47 tenant: Option<String>,
48 #[allow(dead_code)]
49 session_id: String,
50}
51
52pub async fn handle_session<S>(
53 mut stream: S,
54 runtime: Arc<RedDBRuntime>,
55 auth_store: Option<Arc<AuthStore>>,
56 oauth: Option<Arc<crate::auth::oauth::OAuthValidator>>,
57) -> io::Result<()>
58where
59 S: AsyncRead + AsyncWrite + Unpin + Send + 'static,
60{
61 let session = perform_handshake(
65 &mut stream,
66 runtime.as_ref(),
67 auth_store.as_deref(),
68 oauth.as_deref(),
69 )
70 .await?;
71 if session.is_none() {
72 return Ok(());
73 }
74 let session = session.unwrap();
75
76 let mut stream_session: Option<crate::wire::listener::BulkStreamSession> = None;
79 let mut prepared_stmts: std::collections::HashMap<u32, crate::wire::listener::PreparedStmt> =
80 std::collections::HashMap::new();
81
82 let (mut reader, writer) = tokio::io::split(stream);
90 let writer = Arc::new(TokioMutex::new(writer));
91 let (out_tx, mut out_rx) = mpsc::unbounded_channel::<Vec<u8>>();
92 let writer_drain = Arc::clone(&writer);
93 tokio::spawn(async move {
94 while let Some(bytes) = out_rx.recv().await {
95 let mut w = writer_drain.lock().await;
96 if w.write_all(&bytes).await.is_err() {
97 return;
98 }
99 }
100 });
101
102 let stream_registry = Arc::new(super::output_stream::StreamRegistry::new());
106
107 let mut input_registry = super::input_stream::InputStreamRegistry::new();
115
116 let mut queue_wait_tasks: tokio::task::JoinSet<()> = tokio::task::JoinSet::new();
129
130 loop {
131 while queue_wait_tasks.try_join_next().is_some() {}
135
136 let frame = match read_frame_async(&mut reader).await {
137 Ok(frame) => frame,
138 Err(reddb_wire::redwire::RedWireIoError::Io(err))
139 if err.kind() == io::ErrorKind::UnexpectedEof =>
140 {
141 return Ok(())
142 }
143 Err(err) => return Err(redwire_io_err(err)),
144 };
145
146 if frame.kind.direction() == MessageDirection::ServerToClient {
151 let err_frame = build_error_frame_lossy(
152 frame.correlation_id,
153 &format!("redwire: {:?} is server-only", frame.kind),
154 );
155 queue_send(&out_tx, encode_frame(&err_frame))?;
156 continue;
157 }
158
159 match frame.kind {
160 MessageKind::Bye => {
161 let bye = encode_frame(&reply_frame_or_io_error(
162 frame.correlation_id,
163 MessageKind::Bye,
164 vec![],
165 )?);
166 let _ = out_tx.send(bye);
167 return Ok(());
168 }
169 MessageKind::Ping => {
170 let pong = encode_frame(&reply_frame_or_io_error(
171 frame.correlation_id,
172 MessageKind::Pong,
173 vec![],
174 )?);
175 queue_send(&out_tx, pong)?;
176 }
177 MessageKind::Query => {
178 let response = run_query(&runtime, &frame);
179 queue_send(&out_tx, encode_frame(&response))?;
180 }
181 MessageKind::QueryWithParams => {
182 let response = run_query_with_params(&runtime, &frame);
183 queue_send(&out_tx, encode_frame(&response))?;
184 }
185 MessageKind::BulkInsert => {
189 let response = run_insert_dispatch(&runtime, &frame);
190 queue_send(&out_tx, encode_frame(&response))?;
191 }
192 MessageKind::BulkInsertBinary => {
193 let raw =
194 crate::wire::listener::handle_bulk_insert_binary(&runtime, &frame.payload);
195 queue_send(
196 &out_tx,
197 encode_frame(&rewrap_length_prefixed_handler_response(
198 &raw,
199 frame.correlation_id,
200 )),
201 )?;
202 }
203 MessageKind::BulkInsertPrevalidated => {
204 let raw = crate::wire::listener::handle_bulk_insert_binary_prevalidated(
205 &runtime,
206 &frame.payload,
207 );
208 queue_send(
209 &out_tx,
210 encode_frame(&rewrap_length_prefixed_handler_response(
211 &raw,
212 frame.correlation_id,
213 )),
214 )?;
215 }
216 MessageKind::QueryBinary => {
217 let raw = crate::wire::listener::handle_query_binary(&runtime, &frame.payload);
218 queue_send(
219 &out_tx,
220 encode_frame(&rewrap_length_prefixed_handler_response(
221 &raw,
222 frame.correlation_id,
223 )),
224 )?;
225 }
226 MessageKind::BulkStreamStart => {
228 let raw =
229 crate::wire::listener::handle_stream_start(&frame.payload, &mut stream_session);
230 queue_send(
231 &out_tx,
232 encode_frame(&rewrap_length_prefixed_handler_response(
233 &raw,
234 frame.correlation_id,
235 )),
236 )?;
237 }
238 MessageKind::BulkStreamRows => {
239 let raw = crate::wire::listener::handle_stream_rows(
240 &runtime,
241 &frame.payload,
242 &mut stream_session,
243 );
244 if !raw.is_empty() {
245 queue_send(
246 &out_tx,
247 encode_frame(&rewrap_length_prefixed_handler_response(
248 &raw,
249 frame.correlation_id,
250 )),
251 )?;
252 }
253 }
254 MessageKind::BulkStreamCommit => {
255 let raw =
256 crate::wire::listener::handle_stream_commit(&runtime, &mut stream_session);
257 queue_send(
258 &out_tx,
259 encode_frame(&rewrap_length_prefixed_handler_response(
260 &raw,
261 frame.correlation_id,
262 )),
263 )?;
264 }
265 MessageKind::Prepare => {
266 let raw = crate::wire::listener::handle_prepare(
267 &runtime,
268 &frame.payload,
269 &mut prepared_stmts,
270 );
271 queue_send(
272 &out_tx,
273 encode_frame(&rewrap_length_prefixed_handler_response(
274 &raw,
275 frame.correlation_id,
276 )),
277 )?;
278 }
279 MessageKind::ExecutePrepared => {
280 let raw = crate::wire::listener::handle_execute_prepared(
281 &runtime,
282 &frame.payload,
283 &prepared_stmts,
284 );
285 queue_send(
286 &out_tx,
287 encode_frame(&rewrap_length_prefixed_handler_response(
288 &raw,
289 frame.correlation_id,
290 )),
291 )?;
292 }
293 MessageKind::Get => {
294 let response = run_get(&runtime, &frame);
295 queue_send(&out_tx, encode_frame(&response))?;
296 }
297 MessageKind::Delete => {
298 let response = run_delete(&runtime, &frame);
299 queue_send(&out_tx, encode_frame(&response))?;
300 }
301 MessageKind::OpenStream => {
310 use super::output_stream as os;
311 let frame_id = frame.correlation_id;
312 let sid = frame.stream_id;
313
314 if super::input_stream::open_stream_is_input(&frame.payload) {
321 use super::input_stream as is;
322 let req = match is::parse_open_input(&frame.payload) {
323 Ok(r) => r,
324 Err(e) => {
325 let err = is::build_input_stream_error_frame(
326 frame_id,
327 sid,
328 e.code(),
329 e.message(),
330 0,
331 0,
332 )?;
333 queue_send(&out_tx, encode_frame(&err))?;
334 continue;
335 }
336 };
337 let in_tx = runtime.connection_in_transaction(0);
338 let config = crate::server::output_stream::StreamConfig::load(&runtime);
339 let snapshot_lsn = runtime.cdc_current_lsn();
340 let clock = crate::server::output_stream::SystemClock;
341 let lease = match is::open_input_lease(config, snapshot_lsn, in_tx, &clock) {
342 Ok(l) => l,
343 Err(e) => {
344 let err = is::build_input_stream_error_frame(
345 frame_id,
346 sid,
347 e.code(),
348 e.message(),
349 0,
350 snapshot_lsn,
351 )?;
352 queue_send(&out_tx, encode_frame(&err))?;
353 continue;
354 }
355 };
356 let lease_id = lease.id;
357 let lease_snapshot = lease.snapshot_lsn;
358 let state = is::InputStreamState::new(lease, req.target, req.columns);
359 if let Err(e) = input_registry.register(sid, state) {
360 let err = is::build_input_stream_error_frame(
361 frame_id,
362 sid,
363 e.code(),
364 e.message(),
365 0,
366 snapshot_lsn,
367 )?;
368 queue_send(&out_tx, encode_frame(&err))?;
369 continue;
370 }
371 let ack =
372 os::build_open_ack_frame(frame_id, sid, lease_id, lease_snapshot, false)
373 .map_err(|e| io::Error::other(format!("build OpenAck: {e}")))?;
374 queue_send(&out_tx, encode_frame(&ack))?;
375 continue;
376 }
377
378 let req = match os::parse_open_stream(&frame.payload) {
379 Ok(r) => r,
380 Err(e) => {
381 let err =
382 os::build_stream_error_frame(frame_id, sid, e.code(), e.message())?;
383 queue_send(&out_tx, encode_frame(&err))?;
384 continue;
385 }
386 };
387 let cancel_rx = match stream_registry.register(sid).await {
388 Ok(rx) => rx,
389 Err(e) => {
390 let err =
391 os::build_stream_error_frame(frame_id, sid, e.code(), e.message())?;
392 queue_send(&out_tx, encode_frame(&err))?;
393 continue;
394 }
395 };
396 let runtime_ref = Arc::clone(&runtime);
397 let registry_ref = Arc::clone(&stream_registry);
398 let send = os::FrameTx::new(out_tx.clone());
399 let in_tx = runtime.connection_in_transaction(0);
405 tokio::spawn(async move {
406 os::run_output_stream(runtime_ref, frame_id, sid, req, in_tx, cancel_rx, send)
407 .await;
408 registry_ref.unregister(sid).await;
409 });
410 }
411 MessageKind::QueueWaitOpen => {
419 use super::queue_wait as qw;
420 let frame_id = frame.correlation_id;
421 let sid = frame.stream_id;
422 let req = match qw::parse_queue_wait_open(&frame.payload) {
423 Ok(r) => r,
424 Err(e) => {
425 let err =
426 qw::build_queue_wait_error_frame(frame_id, sid, e.code(), e.message())
427 .map_err(|e| {
428 io::Error::other(format!("build queue-wait error: {e}"))
429 })?;
430 queue_send(&out_tx, encode_frame(&err))?;
431 continue;
432 }
433 };
434 if let Err(msg) = runtime.redwire_queue_wait_cap_check(req.wait_ms) {
439 let err = qw::build_queue_wait_error_frame(
440 frame_id,
441 sid,
442 qw::WAIT_EXCEEDS_CAP_CODE,
443 &msg,
444 )
445 .map_err(|e| io::Error::other(format!("build queue-wait cap error: {e}")))?;
446 queue_send(&out_tx, encode_frame(&err))?;
447 continue;
448 }
449 let runtime_ref = Arc::clone(&runtime);
450 let out = out_tx.clone();
451 let queue_name = req.queue.clone();
452 let wait_ms = req.wait_ms;
453 let auth_identity = Some((session.username.clone(), session.role));
454 let tenant = session.tenant.clone();
455 queue_wait_tasks.spawn(async move {
459 use crate::runtime::RedwireWaitOutcome;
460 match runtime_ref
461 .redwire_queue_wait_json(
462 &req.queue,
463 req.group.as_deref(),
464 &req.consumer,
465 req.count,
466 req.wait_ms,
467 auth_identity,
468 tenant,
469 )
470 .await
471 {
472 Ok(RedwireWaitOutcome::Delivered(messages)) => {
474 for message in messages {
475 match qw::build_event_push_frame(frame_id, sid, &message) {
476 Ok(push) => {
477 if queue_send(&out, encode_frame(&push)).is_err() {
478 return;
479 }
480 }
481 Err(_) => return,
482 }
483 }
484 }
485 Ok(RedwireWaitOutcome::TimedOut) => {
489 if let Ok(t) = qw::build_queue_wait_timeout_frame(
490 frame_id,
491 sid,
492 &queue_name,
493 wait_ms,
494 ) {
495 let _ = queue_send(&out, encode_frame(&t));
496 }
497 }
498 Ok(RedwireWaitOutcome::Cancelled) => {
502 if let Ok(ef) = qw::build_queue_wait_error_frame(
503 frame_id,
504 sid,
505 qw::WAIT_CANCELLED_CODE,
506 "queue wait cancelled by server",
507 ) {
508 let _ = queue_send(&out, encode_frame(&ef));
509 }
510 }
511 Err(err) => {
516 if let Ok(ef) = qw::build_queue_wait_error_frame(
517 frame_id,
518 sid,
519 qw::WAIT_FAILED_CODE,
520 &err.to_string(),
521 ) {
522 let _ = queue_send(&out, encode_frame(&ef));
523 }
524 }
525 }
526 });
527 }
528 MessageKind::StreamChunk => {
537 use super::input_stream as is;
538 use crate::server::output_stream::{Clock, SystemClock};
539 let frame_id = frame.correlation_id;
540 let sid = frame.stream_id;
541 if !input_registry.contains(sid) {
542 let err = is::build_input_stream_error_frame(
545 frame_id,
546 sid,
547 "unknown_stream",
548 "no active input stream for this stream_id",
549 0,
550 0,
551 )?;
552 queue_send(&out_tx, encode_frame(&err))?;
553 continue;
554 }
555 let chunk = match is::parse_input_chunk(&frame.payload) {
556 Ok(c) => c,
557 Err(e) => {
558 let state = input_registry
559 .remove(sid)
560 .expect("stream presence checked above");
561 let err = is::build_input_stream_error_frame(
562 frame_id,
563 sid,
564 e.code(),
565 e.message(),
566 state.chunk_count,
567 state.committed_rid,
568 )?;
569 queue_send(&out_tx, encode_frame(&err))?;
570 continue;
571 }
572 };
573 let commit_result = {
574 let state = input_registry
575 .get_mut(sid)
576 .expect("stream presence checked above");
577 if state.lease.snapshot_expired(SystemClock.now_ms()) {
578 Err((
579 "snapshot_expired".to_string(),
580 "stream snapshot pin TTL elapsed".to_string(),
581 ))
582 } else {
583 state.commit_chunk(&runtime, &chunk.rows)
584 }
585 };
586 match commit_result {
587 Err((code, message)) => {
588 let state = input_registry
589 .remove(sid)
590 .expect("stream presence checked above");
591 let err = is::build_input_stream_error_frame(
592 frame_id,
593 sid,
594 &code,
595 &message,
596 state.chunk_count,
597 state.committed_rid,
598 )?;
599 queue_send(&out_tx, encode_frame(&err))?;
600 }
601 Ok(()) => {
602 if chunk.terminal {
603 let state = input_registry
604 .remove(sid)
605 .expect("stream presence checked above");
606 let end = is::build_input_stream_end_frame(
607 frame_id,
608 sid,
609 state.row_count,
610 state.chunk_count,
611 state.committed_rid,
612 state.snapshot_lsn,
613 false,
614 )?;
615 queue_send(&out_tx, encode_frame(&end))?;
616 }
617 }
618 }
619 }
620 MessageKind::StreamCancel => {
621 use super::input_stream as is;
622 use super::output_stream as os;
623 let sid = frame.stream_id;
624 if stream_registry.cancel(sid).await {
625 } else if let Some(state) = input_registry.remove(sid) {
628 let end = is::build_input_stream_end_frame(
634 frame.correlation_id,
635 sid,
636 state.row_count,
637 state.chunk_count,
638 state.committed_rid,
639 state.snapshot_lsn,
640 true,
641 )?;
642 queue_send(&out_tx, encode_frame(&end))?;
643 } else {
644 let err = os::build_stream_error_frame(
647 frame.correlation_id,
648 sid,
649 "unknown_stream",
650 "no active stream for this stream_id",
651 )?;
652 queue_send(&out_tx, encode_frame(&err))?;
653 }
654 }
655 other => {
656 let err_frame = build_error_frame_lossy(
657 frame.correlation_id,
658 &format!("redwire: cannot dispatch {other:?} yet"),
659 );
660 queue_send(&out_tx, encode_frame(&err_frame))?;
661 }
662 }
663 }
664}
665
666#[inline]
667fn queue_send(out_tx: &mpsc::UnboundedSender<Vec<u8>>, bytes: Vec<u8>) -> io::Result<()> {
668 out_tx
669 .send(bytes)
670 .map_err(|_| io::Error::other("redwire: write channel closed"))
671}
672
673async fn perform_handshake<S>(
676 stream: &mut S,
677 runtime: &RedDBRuntime,
678 auth_store: Option<&AuthStore>,
679 oauth: Option<&crate::auth::oauth::OAuthValidator>,
680) -> io::Result<Option<AuthedSession>>
681where
682 S: AsyncRead + AsyncWrite + Unpin + Send,
683{
684 let mut minor_buf = [0u8; 1];
686 stream.read_exact(&mut minor_buf).await?;
687 let minor = minor_buf[0];
688 if validate_minor_version(minor).is_err() {
689 return Ok(None);
693 }
694
695 let hello = read_frame(stream).await?;
697 if hello.kind != MessageKind::Hello {
698 let fail = encode_frame(&reply_frame_or_io_error(
699 hello.correlation_id,
700 MessageKind::AuthFail,
701 build_auth_fail_payload("first frame after magic must be Hello"),
702 )?);
703 let _ = stream.write_all(&fail).await;
704 return Ok(None);
705 }
706 let hello_msg = match Hello::from_payload(&hello.payload) {
707 Ok(h) => h,
708 Err(e) => {
709 let fail = encode_frame(&reply_frame_or_io_error(
710 hello.correlation_id,
711 MessageKind::AuthFail,
712 build_auth_fail_payload(&e),
713 )?);
714 let _ = stream.write_all(&fail).await;
715 return Ok(None);
716 }
717 };
718
719 let Some(chosen_version) = choose_hello_minor_version(&hello_msg.versions) else {
720 let fail = encode_frame(&reply_frame_or_io_error(
721 hello.correlation_id,
722 MessageKind::AuthFail,
723 build_auth_fail_payload("no overlapping protocol version"),
724 )?);
725 let _ = stream.write_all(&fail).await;
726 return Ok(None);
727 };
728
729 let server_anon_ok = auth_store.map(|s| !s.is_enabled()).unwrap_or(true);
730 let chosen = match pick_auth_method(&hello_msg.auth_methods, server_anon_ok) {
731 Some(m) => m,
732 None => {
733 let fail = encode_frame(&reply_frame_or_io_error(
734 hello.correlation_id,
735 MessageKind::AuthFail,
736 build_auth_fail_payload("no overlapping auth method"),
737 )?);
738 let _ = stream.write_all(&fail).await;
739 return Ok(None);
740 }
741 };
742
743 let server_features = FEATURE_PARAMS;
752 let topology = build_topology_for_hello_ack(runtime);
753 let ack_frame = build_hello_ack_frame(
754 hello.correlation_id,
755 chosen_version,
756 chosen,
757 server_features,
758 topology.as_ref(),
759 )
760 .map_err(|e| io::Error::other(format!("build HelloAck: {e}")))?;
761 let ack = encode_frame(&ack_frame);
762 stream.write_all(&ack).await?;
763
764 if chosen == "scram-sha-256" {
768 return perform_scram_handshake(stream, auth_store, hello.correlation_id, server_features)
769 .await;
770 }
771
772 let resp = read_frame(stream).await?;
775 let auth_payload = match expect_auth_response_payload(resp.kind, &resp.payload, "AuthResponse")
776 {
777 Ok(payload) => payload,
778 Err(err) => {
779 let fail = encode_frame(&reply_frame_or_io_error(
780 resp.correlation_id,
781 MessageKind::AuthFail,
782 build_auth_fail_payload(&err.to_string()),
783 )?);
784 let _ = stream.write_all(&fail).await;
785 return Ok(None);
786 }
787 };
788
789 if chosen == "oauth-jwt" {
797 let raw = match parse_auth_response_oauth_jwt(auth_payload) {
798 Ok(raw) if !raw.is_empty() => raw,
799 _ => {
800 let fail = encode_frame(&reply_frame_or_io_error(
801 resp.correlation_id,
802 MessageKind::AuthFail,
803 build_auth_fail_payload("oauth-jwt: AuthResponse missing 'jwt' string"),
804 )?);
805 let _ = stream.write_all(&fail).await;
806 return Ok(None);
807 }
808 };
809
810 if let Some(authority) = runtime.browser_token_authority() {
821 let now = std::time::SystemTime::now()
822 .duration_since(std::time::UNIX_EPOCH)
823 .map(|d| d.as_secs() as i64)
824 .unwrap_or(0);
825 if let Ok(identity) = authority.validate_access(&raw, now) {
826 let session_id = super::auth::new_session_id_for_scram();
827 let ok = encode_frame(&reply_frame_or_io_error(
828 resp.correlation_id,
829 MessageKind::AuthOk,
830 build_auth_ok(
831 &session_id,
832 &identity.username,
833 identity.role,
834 server_features,
835 ),
836 )?);
837 stream.write_all(&ok).await?;
838 return Ok(Some(AuthedSession {
839 username: identity.username,
840 role: identity.role,
841 tenant: identity.tenant,
842 session_id,
843 }));
844 }
845 }
846
847 let validator = match oauth {
849 Some(v) => v,
850 None => {
851 let fail = encode_frame(&reply_frame_or_io_error(
852 resp.correlation_id,
853 MessageKind::AuthFail,
854 build_auth_fail_payload(
855 "oauth-jwt: token rejected (no browser-token authority or OAuth validator accepted it)",
856 ),
857 )?);
858 let _ = stream.write_all(&fail).await;
859 return Ok(None);
860 }
861 };
862 match super::auth::validate_oauth_jwt_full(validator, &raw) {
863 Ok((tenant, username, role)) => {
864 let session_id = super::auth::new_session_id_for_scram();
865 let ok = encode_frame(&reply_frame_or_io_error(
866 resp.correlation_id,
867 MessageKind::AuthOk,
868 build_auth_ok(&session_id, &username, role, server_features),
869 )?);
870 stream.write_all(&ok).await?;
871 return Ok(Some(AuthedSession {
872 username,
873 role,
874 tenant,
875 session_id,
876 }));
877 }
878 Err(reason) => {
879 let fail = encode_frame(&reply_frame_or_io_error(
880 resp.correlation_id,
881 MessageKind::AuthFail,
882 build_auth_fail_payload(&format!("oauth-jwt: {reason}")),
883 )?);
884 let _ = stream.write_all(&fail).await;
885 return Ok(None);
886 }
887 }
888 }
889
890 match validate_auth_response(chosen, auth_payload, auth_store) {
891 AuthOutcome::Authenticated {
892 username,
893 role,
894 tenant,
895 session_id,
896 } => {
897 let ok_frame = build_auth_ok_frame_from_payload(
898 resp.correlation_id,
899 build_auth_ok(&session_id, &username, role, server_features),
900 )
901 .map_err(|e| io::Error::other(format!("build AuthOk: {e}")))?;
902 let ok = encode_frame(&ok_frame);
903 stream.write_all(&ok).await?;
904 Ok(Some(AuthedSession {
905 username,
906 role,
907 tenant,
908 session_id,
909 }))
910 }
911 AuthOutcome::Refused(reason) => {
912 let fail = encode_frame(&reply_frame_or_io_error(
913 resp.correlation_id,
914 MessageKind::AuthFail,
915 build_auth_fail_payload(&reason),
916 )?);
917 let _ = stream.write_all(&fail).await;
918 Ok(None)
919 }
920 }
921}
922
923async fn perform_scram_handshake<S>(
932 stream: &mut S,
933 auth_store: Option<&AuthStore>,
934 initial_correlation: u64,
935 server_features: u32,
936) -> io::Result<Option<AuthedSession>>
937where
938 S: AsyncRead + AsyncWrite + Unpin + Send,
939{
940 let store = match auth_store {
941 Some(s) => s,
942 None => {
943 let fail = encode_frame(&reply_frame_or_io_error(
944 initial_correlation,
945 MessageKind::AuthFail,
946 build_auth_fail_payload("scram-sha-256 requires an AuthStore"),
947 )?);
948 let _ = stream.write_all(&fail).await;
949 return Ok(None);
950 }
951 };
952
953 let cf = read_frame(stream).await?;
955 let cf_payload = match expect_auth_response_payload(
956 cf.kind,
957 &cf.payload,
958 "AuthResponse(client-first-message)",
959 ) {
960 Ok(payload) => payload,
961 Err(err) => {
962 let fail = encode_frame(&reply_frame_or_io_error(
963 cf.correlation_id,
964 MessageKind::AuthFail,
965 build_auth_fail_payload(&err.to_string()),
966 )?);
967 let _ = stream.write_all(&fail).await;
968 return Ok(None);
969 }
970 };
971 let (username, client_nonce, client_first_bare) =
972 match reddb_wire::redwire::handshake::parse_scram_client_first(cf_payload) {
973 Ok(t) => t,
974 Err(e) => {
975 let fail = encode_frame(&reply_frame_or_io_error(
976 cf.correlation_id,
977 MessageKind::AuthFail,
978 build_auth_fail_payload(&format!("scram client-first: {e}")),
979 )?);
980 let _ = stream.write_all(&fail).await;
981 return Ok(None);
982 }
983 };
984
985 let verifier = store.lookup_scram_verifier_global(&username);
994 let (salt, iter, stored_key, server_key, user_known) = match &verifier {
995 Some(v) => (v.salt.clone(), v.iter, v.stored_key, v.server_key, true),
996 None => (
997 crate::auth::store::random_bytes(16),
998 crate::auth::scram::DEFAULT_ITER,
999 [0u8; 32],
1000 [0u8; 32],
1001 false,
1002 ),
1003 };
1004
1005 let server_nonce = super::auth::new_server_nonce();
1007 let server_first = reddb_wire::redwire::handshake::build_scram_server_first(
1008 &client_nonce,
1009 &server_nonce,
1010 &salt,
1011 iter,
1012 );
1013 let req = encode_frame(&reply_frame_or_io_error(
1014 cf.correlation_id,
1015 MessageKind::AuthRequest,
1016 server_first.as_bytes().to_vec(),
1017 )?);
1018 stream.write_all(&req).await?;
1019
1020 let cfinal = read_frame(stream).await?;
1022 let cfinal_payload = match expect_auth_response_payload(
1023 cfinal.kind,
1024 &cfinal.payload,
1025 "AuthResponse(client-final-message)",
1026 ) {
1027 Ok(payload) => payload,
1028 Err(err) => {
1029 let fail = encode_frame(&reply_frame_or_io_error(
1030 cfinal.correlation_id,
1031 MessageKind::AuthFail,
1032 build_auth_fail_payload(&err.to_string()),
1033 )?);
1034 let _ = stream.write_all(&fail).await;
1035 return Ok(None);
1036 }
1037 };
1038 let (combined_nonce, presented_proof, client_final_no_proof) =
1039 match reddb_wire::redwire::handshake::parse_scram_client_final(cfinal_payload) {
1040 Ok(t) => t,
1041 Err(e) => {
1042 let fail = encode_frame(&reply_frame_or_io_error(
1043 cfinal.correlation_id,
1044 MessageKind::AuthFail,
1045 build_auth_fail_payload(&format!("scram client-final: {e}")),
1046 )?);
1047 let _ = stream.write_all(&fail).await;
1048 return Ok(None);
1049 }
1050 };
1051 let expected_combined = format!("{client_nonce}{server_nonce}");
1052 if combined_nonce != expected_combined {
1053 let fail = encode_frame(&reply_frame_or_io_error(
1054 cfinal.correlation_id,
1055 MessageKind::AuthFail,
1056 build_auth_fail_payload("scram nonce mismatch — replay protection failed"),
1057 )?);
1058 let _ = stream.write_all(&fail).await;
1059 return Ok(None);
1060 }
1061
1062 let auth_message =
1064 crate::auth::scram::auth_message(&client_first_bare, &server_first, &client_final_no_proof);
1065 let proof_ok = if user_known {
1066 let v = crate::auth::scram::ScramVerifier {
1067 salt: salt.clone(),
1068 iter,
1069 stored_key,
1070 server_key,
1071 };
1072 crate::auth::scram::verify_client_proof(&v, &auth_message, &presented_proof)
1073 } else {
1074 false
1075 };
1076 if !proof_ok {
1077 let fail = encode_frame(&reply_frame_or_io_error(
1078 cfinal.correlation_id,
1079 MessageKind::AuthFail,
1080 build_auth_fail_payload("invalid SCRAM proof"),
1081 )?);
1082 let _ = stream.write_all(&fail).await;
1083 return Ok(None);
1084 }
1085
1086 let user = store
1088 .list_users()
1089 .into_iter()
1090 .find(|u| u.username == username);
1091 let role = user
1092 .as_ref()
1093 .map(|u| u.role)
1094 .unwrap_or(crate::auth::Role::Read);
1095 let server_sig = crate::auth::scram::server_signature(&server_key, &auth_message);
1096 let session_id = super::auth::new_session_id_for_scram();
1097 let ok_payload = super::auth::build_scram_auth_ok(
1098 &session_id,
1099 &username,
1100 role,
1101 server_features,
1102 &server_sig,
1103 );
1104 let ok = encode_frame(&reply_frame_or_io_error(
1105 cfinal.correlation_id,
1106 MessageKind::AuthOk,
1107 ok_payload,
1108 )?);
1109 stream.write_all(&ok).await?;
1110 Ok(Some(AuthedSession {
1111 username,
1112 role,
1113 tenant: user.and_then(|u| u.tenant_id),
1114 session_id,
1115 }))
1116}
1117
1118async fn read_frame<S>(stream: &mut S) -> io::Result<Frame>
1119where
1120 S: AsyncRead + Unpin + Send,
1121{
1122 read_frame_async(stream).await.map_err(redwire_io_err)
1123}
1124
1125fn redwire_io_err(err: reddb_wire::redwire::RedWireIoError) -> io::Error {
1126 match err {
1127 reddb_wire::redwire::RedWireIoError::Io(err) => err,
1128 reddb_wire::redwire::RedWireIoError::Frame(err) => {
1129 io::Error::other(format!("decode frame: {err}"))
1130 }
1131 }
1132}
1133
1134fn run_query(runtime: &RedDBRuntime, frame: &Frame) -> Frame {
1135 let sql = match std::str::from_utf8(&frame.payload) {
1136 Ok(s) => s,
1137 Err(_) => {
1138 return build_error_frame_lossy(
1139 frame.correlation_id,
1140 "Query payload must be UTF-8 SQL",
1141 );
1142 }
1143 };
1144 match runtime.execute_query(sql) {
1145 Ok(result) => {
1146 let payload =
1147 encode_query_result_summary_payload(result.statement_type, result.affected_rows);
1148 build_dispatch_reply_frame(frame.correlation_id, MessageKind::Result, payload)
1149 }
1150 Err(err) => build_error_frame_lossy(frame.correlation_id, &err.to_string()),
1151 }
1152}
1153
1154fn run_query_with_params(runtime: &RedDBRuntime, frame: &Frame) -> Frame {
1155 let request = match decode_query_with_params_request(&frame.payload) {
1156 Ok(decoded) => decoded,
1157 Err(err) => return build_error_frame_lossy(frame.correlation_id, &err.to_string()),
1158 };
1159 let commit_policy = match parse_redwire_commit_policy(request.options.commit_policy.as_deref())
1160 {
1161 Ok(policy) => policy,
1162 Err(err) => return build_error_frame_lossy(frame.correlation_id, &err),
1163 };
1164 let params = request
1165 .params
1166 .into_iter()
1167 .map(param_to_schema_value)
1168 .collect::<Vec<_>>();
1169 match runtime.execute_query_with_params(&request.sql, ¶ms) {
1170 Ok(result) => {
1171 let is_mutation = matches!(result.statement_type, "insert" | "update" | "delete");
1172 if is_mutation {
1173 let post_lsn = runtime.cdc_current_lsn();
1174 if let Err(err) = runtime.enforce_commit_policy_for_request(post_lsn, commit_policy)
1175 {
1176 return build_error_frame_lossy(frame.correlation_id, &err.to_string());
1177 }
1178 }
1179 let payload =
1180 crate::presentation::query_result_json::runtime_query_json(&result, &None, &None)
1181 .to_string_compact()
1182 .into_bytes();
1183 build_dispatch_reply_frame(frame.correlation_id, MessageKind::Result, payload)
1184 }
1185 Err(err) => build_error_frame_lossy(frame.correlation_id, &err.to_string()),
1186 }
1187}
1188
1189fn parse_redwire_commit_policy(
1190 value: Option<&str>,
1191) -> Result<Option<crate::replication::CommitPolicy>, String> {
1192 let Some(value) = value else {
1193 return Ok(None);
1194 };
1195 crate::replication::CommitPolicy::parse_strict(value)
1196 .map(Some)
1197 .ok_or_else(|| format!("invalid commit_policy value '{value}'"))
1198}
1199
1200fn param_to_schema_value(value: RedWireParamValue) -> crate::storage::schema::Value {
1201 use crate::storage::schema::Value;
1202 match value {
1203 RedWireParamValue::Null => Value::Null,
1204 RedWireParamValue::Bool(value) => Value::Boolean(value),
1205 RedWireParamValue::Int(value) => Value::Integer(value),
1206 RedWireParamValue::Float(value) => Value::Float(value),
1207 RedWireParamValue::Text(value) => Value::Text(Arc::from(value.as_str())),
1208 RedWireParamValue::Bytes(value) => Value::Blob(value),
1209 RedWireParamValue::Vector(value) => Value::Vector(value),
1210 RedWireParamValue::Json(value) => Value::Json(value),
1211 RedWireParamValue::Timestamp(value) => Value::Timestamp(value),
1212 RedWireParamValue::Uuid(value) => Value::Uuid(value),
1213 }
1214}
1215
1216fn run_insert_dispatch(runtime: &RedDBRuntime, frame: &Frame) -> Frame {
1234 let request = match decode_insert_dispatch_payload(&frame.payload) {
1235 Ok(request) => request,
1236 Err(err) => return build_error_frame_lossy(frame.correlation_id, &err.to_string()),
1237 };
1238 let collection = request.collection.as_str();
1239 let payload = request.payload.map(wire_json_to_server_json);
1240 let payloads = request.payloads.map(|rows| {
1241 rows.into_iter()
1242 .map(wire_json_to_server_json)
1243 .collect::<Vec<_>>()
1244 });
1245
1246 let idempotency_key = request.idempotency_key.as_deref();
1252 if idempotency_key.is_some() || request.batch {
1253 let items = match payloads.as_deref() {
1254 Some(rows) => rows,
1255 None => {
1256 return build_error_frame_lossy(
1257 frame.correlation_id,
1258 "BatchInsert: missing 'payloads' array",
1259 )
1260 }
1261 };
1262 let outcome = crate::server::handlers_entity::process_batch_insert(
1263 runtime,
1264 collection,
1265 items,
1266 idempotency_key,
1267 );
1268 let kind = if (200..300).contains(&outcome.status) {
1273 MessageKind::BulkOk
1274 } else {
1275 MessageKind::Error
1276 };
1277 return build_dispatch_reply_frame(frame.correlation_id, kind, outcome.body);
1278 }
1279
1280 if let Some(rows) = payloads.as_ref() {
1281 let mut objects = Vec::with_capacity(rows.len());
1282 for entry in rows {
1283 objects.push(match entry.as_object() {
1284 Some(o) => o,
1285 None => {
1286 return build_error_frame_lossy(
1287 frame.correlation_id,
1288 "Insert: each payload must be a JSON object",
1289 )
1290 }
1291 });
1292 }
1293
1294 if crate::rpc_stdio::should_bulk_insert_graph(runtime, collection, &objects) {
1295 return match crate::rpc_stdio::bulk_insert_graph(runtime, collection, &objects) {
1296 Ok(body) => {
1297 let payload = body.to_string_compact().into_bytes();
1298 build_dispatch_reply_frame(frame.correlation_id, MessageKind::BulkOk, payload)
1299 }
1300 Err(err) => build_error_frame_lossy(frame.correlation_id, &err.to_string()),
1301 };
1302 }
1303
1304 let mut affected: u64 = 0;
1305 let mut ids = Vec::with_capacity(objects.len());
1306 for row in objects {
1307 let sql = crate::rpc_stdio::build_insert_sql(collection, row.iter());
1308 match runtime.execute_query(&sql) {
1309 Ok(qr) => {
1310 affected += qr.affected_rows;
1311 if let Some(id) = crate::rpc_stdio::insert_result_to_json(&qr).get("id") {
1312 ids.push(id.clone());
1313 }
1314 }
1315 Err(err) => return build_error_frame_lossy(frame.correlation_id, &err.to_string()),
1316 }
1317 }
1318 let payload = encode_bulk_ok_payload_from_json_id_literals(
1319 affected,
1320 ids.iter().map(|id| id.to_string()),
1321 );
1322 return build_dispatch_reply_frame(frame.correlation_id, MessageKind::BulkOk, payload);
1323 }
1324
1325 let row = match payload.as_ref().and_then(|x| x.as_object()) {
1326 Some(o) => o,
1327 None => {
1328 return build_error_frame_lossy(
1329 frame.correlation_id,
1330 "Insert: missing 'payload' object or 'payloads' array",
1331 )
1332 }
1333 };
1334 let sql = crate::rpc_stdio::build_insert_sql(collection, row.iter());
1335 match runtime.execute_query(&sql) {
1336 Ok(qr) => {
1337 let body = crate::rpc_stdio::insert_result_to_json(&qr);
1338 let payload = body.to_string_compact().into_bytes();
1339 build_dispatch_reply_frame(frame.correlation_id, MessageKind::BulkOk, payload)
1340 }
1341 Err(err) => build_error_frame_lossy(frame.correlation_id, &err.to_string()),
1342 }
1343}
1344
1345fn build_topology_for_hello_ack(runtime: &RedDBRuntime) -> Option<reddb_wire::topology::Topology> {
1357 use crate::auth::middleware::AuthResult;
1358 use crate::replication::{LagConfig, TopologyAdvertiser};
1359 use reddb_wire::topology::Endpoint;
1360
1361 let db = runtime.db();
1362 let primary_endpoint = Endpoint {
1363 addr: runtime.config_string("red.redwire.advertise_addr", ""),
1364 region: db.options().replication.region.clone(),
1365 };
1366 let (replicas, current_lsn, epoch) = match db.replication.as_ref() {
1367 Some(repl) => (
1368 repl.replica_snapshots(),
1369 repl.wal_buffer.current_lsn(),
1370 repl.topology_epoch(),
1371 ),
1372 None => (Vec::new(), 0u64, 0u64),
1373 };
1374 let lag = LagConfig::from_now();
1375 Some(TopologyAdvertiser::advertise(
1376 &replicas,
1377 &AuthResult::Anonymous,
1378 epoch,
1379 primary_endpoint,
1380 current_lsn,
1381 &lag,
1382 ))
1383}
1384
1385fn reply_frame_or_io_error(
1386 correlation_id: u64,
1387 kind: MessageKind,
1388 payload: Vec<u8>,
1389) -> io::Result<Frame> {
1390 build_reply_frame(correlation_id, kind, payload)
1391 .map_err(|e| io::Error::other(format!("build {kind:?}: {e}")))
1392}
1393
1394fn wire_json_to_server_json(value: impl std::fmt::Display) -> JsonValue {
1395 serde_json::from_str::<JsonValue>(&value.to_string()).unwrap_or(JsonValue::Null)
1396}
1397
1398fn run_get(runtime: &RedDBRuntime, frame: &Frame) -> Frame {
1402 let request = match decode_get_payload(&frame.payload) {
1403 Ok(request) => request,
1404 Err(err) => return build_error_frame_lossy(frame.correlation_id, &err.to_string()),
1405 };
1406 let id_lit = crate::rpc_stdio::value_to_sql_literal(&JsonValue::String(request.id));
1409 let sql = format!(
1410 "SELECT * FROM {} WHERE _id = {id_lit} LIMIT 1",
1411 request.collection
1412 );
1413 match runtime.execute_query(&sql) {
1414 Ok(qr) => {
1415 let payload = encode_get_result_payload(!qr.result.records.is_empty());
1417 build_dispatch_reply_frame(frame.correlation_id, MessageKind::Result, payload)
1418 }
1419 Err(err) => build_error_frame_lossy(frame.correlation_id, &err.to_string()),
1420 }
1421}
1422
1423fn run_delete(runtime: &RedDBRuntime, frame: &Frame) -> Frame {
1427 let request = match decode_delete_payload(&frame.payload) {
1428 Ok(request) => request,
1429 Err(err) => return build_error_frame_lossy(frame.correlation_id, &err.to_string()),
1430 };
1431 let id_lit = crate::rpc_stdio::value_to_sql_literal(&JsonValue::String(request.id));
1432 let sql = format!("DELETE FROM {} WHERE _id = {id_lit}", request.collection);
1433 match runtime.execute_query(&sql) {
1434 Ok(qr) => {
1435 let payload = encode_delete_ok_payload(qr.affected_rows);
1436 build_dispatch_reply_frame(frame.correlation_id, MessageKind::DeleteOk, payload)
1437 }
1438 Err(err) => build_error_frame_lossy(frame.correlation_id, &err.to_string()),
1439 }
1440}
1441
1442#[cfg(test)]
1443mod tests {
1444 use super::*;
1445
1446 use crate::runtime::RedDBRuntime;
1447 use std::sync::{Mutex, OnceLock};
1448 use tokio::io::{AsyncReadExt, AsyncWriteExt};
1449
1450 fn env_lock() -> &'static Mutex<()> {
1451 static LOCK: OnceLock<Mutex<()>> = OnceLock::new();
1452 LOCK.get_or_init(|| Mutex::new(()))
1453 }
1454
1455 struct EnvGuard {
1456 previous: Vec<(&'static str, Option<String>)>,
1457 }
1458
1459 impl EnvGuard {
1460 fn set(vars: &[(&'static str, &'static str)]) -> Self {
1461 let previous = vars
1462 .iter()
1463 .map(|(key, _)| (*key, std::env::var(key).ok()))
1464 .collect();
1465 for (key, value) in vars {
1466 std::env::set_var(key, value);
1467 }
1468 Self { previous }
1469 }
1470 }
1471
1472 impl Drop for EnvGuard {
1473 fn drop(&mut self) {
1474 for (key, value) in self.previous.iter().rev() {
1475 match value {
1476 Some(value) => std::env::set_var(key, value),
1477 None => std::env::remove_var(key),
1478 }
1479 }
1480 }
1481 }
1482
1483 fn temp_data_path(name: &str) -> std::path::PathBuf {
1484 let mut path = std::env::temp_dir();
1485 path.push(format!(
1486 "reddb-redwire-{name}-{}-{}.rdb",
1487 std::process::id(),
1488 crate::utils::now_unix_millis()
1489 ));
1490 let _ = std::fs::remove_file(&path);
1491 path
1492 }
1493
1494 fn bulk_insert_frame(correlation_id: u64, payload: Vec<u8>) -> Frame {
1495 reddb_wire::redwire::build_bulk_insert_frame(correlation_id, payload)
1496 .expect("build bulk insert frame")
1497 }
1498
1499 fn create_graph_collection(runtime: &RedDBRuntime, name: &str) {
1500 let db = runtime.db();
1501 db.store()
1502 .create_collection(name)
1503 .expect("create collection");
1504 let now = std::time::SystemTime::now()
1505 .duration_since(std::time::UNIX_EPOCH)
1506 .unwrap_or_default()
1507 .as_millis();
1508 db.save_collection_contract(crate::physical::CollectionContract {
1509 name: name.to_string(),
1510 declared_model: crate::catalog::CollectionModel::Graph,
1511 schema_mode: crate::catalog::SchemaMode::Dynamic,
1512 origin: crate::physical::ContractOrigin::Explicit,
1513 version: 1,
1514 created_at_unix_ms: now,
1515 updated_at_unix_ms: now,
1516 default_ttl_ms: None,
1517 vector_dimension: None,
1518 vector_metric: None,
1519 context_index_fields: Vec::new(),
1520 declared_columns: Vec::new(),
1521 table_def: None,
1522 timestamps_enabled: false,
1523 context_index_enabled: false,
1524 metrics_raw_retention_ms: None,
1525 metrics_rollup_policies: Vec::new(),
1526 metrics_tenant_identity: None,
1527 metrics_namespace: None,
1528 append_only: false,
1529 subscriptions: Vec::new(),
1530 analytics_config: Vec::new(),
1531 session_key: None,
1532 session_gap_ms: None,
1533 retention_duration_ms: None,
1534 analytical_storage: None,
1535
1536 ai_policy: None,
1537 })
1538 .expect("save graph contract");
1539 }
1540
1541 #[test]
1542 fn magic_byte_is_0xfe() {
1543 assert_eq!(REDWIRE_MAGIC, 0xFE);
1544 }
1545
1546 #[test]
1547 fn redwire_bulk_insert_graph_rows_returns_ids() {
1548 let runtime = RedDBRuntime::in_memory().expect("runtime");
1549 create_graph_collection(&runtime, "network");
1550
1551 let nodes = bulk_insert_frame(
1552 7,
1553 br#"{"collection":"network","payloads":[{"label":"Host","name":"app"},{"label":"Host","name":"db"}]}"#.to_vec(),
1554 );
1555 let nodes_reply = run_insert_dispatch(&runtime, &nodes);
1556 assert_eq!(nodes_reply.kind, MessageKind::BulkOk);
1557 let node_body: JsonValue =
1558 serde_json::from_slice(&nodes_reply.payload).expect("nodes json");
1559 assert_eq!(
1560 node_body.get("affected").and_then(JsonValue::as_u64),
1561 Some(2)
1562 );
1563 let ids = node_body
1564 .get("ids")
1565 .and_then(JsonValue::as_array)
1566 .expect("node ids");
1567 assert_eq!(ids.len(), 2);
1568
1569 let from = ids[0].as_u64().expect("from id");
1570 let to = ids[1].as_u64().expect("to id");
1571 let edges = bulk_insert_frame(
1572 8,
1573 format!(
1574 r#"{{"collection":"network","payloads":[{{"label":"connects","from":{from},"to":{to},"role":"primary"}}]}}"#
1575 )
1576 .into_bytes(),
1577 );
1578 let edges_reply = run_insert_dispatch(&runtime, &edges);
1579 assert_eq!(edges_reply.kind, MessageKind::BulkOk);
1580 let edge_body: JsonValue =
1581 serde_json::from_slice(&edges_reply.payload).expect("edges json");
1582 assert_eq!(
1583 edge_body.get("affected").and_then(JsonValue::as_u64),
1584 Some(1)
1585 );
1586 assert_eq!(
1587 edge_body
1588 .get("ids")
1589 .and_then(JsonValue::as_array)
1590 .map(|ids| ids.len()),
1591 Some(1)
1592 );
1593 }
1594
1595 #[test]
1596 fn redwire_query_with_params_preserves_json_columns() {
1597 let runtime = RedDBRuntime::in_memory().expect("runtime");
1598 runtime
1599 .execute_query("KV PUT proj.a.b.c.d = 12")
1600 .expect("put nested number");
1601 runtime
1602 .execute_query("KV PUT proj.a.b.e = 'x'")
1603 .expect("put nested string");
1604
1605 let payload =
1606 reddb_wire::query_with_params::encode_query_with_params("LIST KV proj AS JSON", &[])
1607 .expect("encode query with params");
1608 let frame = reddb_wire::redwire::build_query_with_params_frame(99, payload)
1609 .expect("query-with-params frame");
1610 let reply = run_query_with_params(&runtime, &frame);
1611
1612 assert_eq!(
1613 reply.kind,
1614 MessageKind::Result,
1615 "body={}",
1616 String::from_utf8_lossy(&reply.payload)
1617 );
1618 let body: JsonValue = serde_json::from_slice(&reply.payload).expect("result json");
1619 let value = body
1620 .get("result")
1621 .and_then(|result| result.get("records"))
1622 .and_then(JsonValue::as_array)
1623 .and_then(|records| records.first())
1624 .and_then(|record| record.get("values"))
1625 .and_then(|values| values.get("value"))
1626 .expect("json value column");
1627
1628 assert_eq!(
1629 value
1630 .get("a")
1631 .and_then(|a| a.get("b"))
1632 .and_then(|b| b.get("c"))
1633 .and_then(|c| c.get("d"))
1634 .and_then(JsonValue::as_f64),
1635 Some(12.0)
1636 );
1637 assert_eq!(
1638 value
1639 .get("a")
1640 .and_then(|a| a.get("b"))
1641 .and_then(|b| b.get("e"))
1642 .and_then(JsonValue::as_str),
1643 Some("x")
1644 );
1645 }
1646
1647 #[test]
1648 fn redwire_query_with_params_request_policy_strengthens_to_ack_n() {
1649 let _env_lock = env_lock().lock().expect("env lock");
1650 let _env = EnvGuard::set(&[
1651 ("RED_PRIMARY_COMMIT_POLICY", "local"),
1652 ("RED_REPLICATION_ACK_TIMEOUT_MS", "20"),
1653 ("RED_COMMIT_FAIL_ON_TIMEOUT", "true"),
1654 ]);
1655 let data_path = temp_data_path("request-ack-n");
1656 let runtime = RedDBRuntime::with_options(
1657 crate::api::RedDBOptions::persistent(&data_path)
1658 .with_replication(crate::replication::ReplicationConfig::primary()),
1659 )
1660 .expect("runtime");
1661
1662 let payload = reddb_wire::query_with_params::encode_query_with_params_request(
1663 "INSERT INTO redwire_request_ack (id, name) VALUES (1, 'alpha')",
1664 &[],
1665 &reddb_wire::query_with_params::QueryWithParamsOptions {
1666 commit_policy: Some("ack_n=1".to_string()),
1667 },
1668 )
1669 .expect("encode query with request policy");
1670 let frame = reddb_wire::redwire::build_query_with_params_frame(100, payload)
1671 .expect("query-with-params frame");
1672 let reply = run_query_with_params(&runtime, &frame);
1673
1674 assert_eq!(reply.kind, MessageKind::Error);
1675 let body = String::from_utf8_lossy(&reply.payload);
1676 assert!(
1677 body.contains("commit policy timed out") && body.contains("RED_COMMIT_FAIL_ON_TIMEOUT"),
1678 "request ack_n should wait for replica ack, got {body}"
1679 );
1680 let _ = std::fs::remove_file(data_path);
1681 }
1682
1683 #[test]
1684 fn redwire_query_with_params_rejects_request_policy_below_floor() {
1685 let _env_lock = env_lock().lock().expect("env lock");
1686 let _env = EnvGuard::set(&[("RED_PRIMARY_COMMIT_POLICY", "quorum")]);
1687 let runtime = RedDBRuntime::in_memory().expect("runtime");
1688
1689 let payload = reddb_wire::query_with_params::encode_query_with_params_request(
1690 "INSERT INTO redwire_request_floor (id, name) VALUES (1, 'alpha')",
1691 &[],
1692 &reddb_wire::query_with_params::QueryWithParamsOptions {
1693 commit_policy: Some("local".to_string()),
1694 },
1695 )
1696 .expect("encode query with request policy");
1697 let frame = reddb_wire::redwire::build_query_with_params_frame(101, payload)
1698 .expect("query-with-params frame");
1699 let reply = run_query_with_params(&runtime, &frame);
1700
1701 assert_eq!(reply.kind, MessageKind::Error);
1702 let body = String::from_utf8_lossy(&reply.payload);
1703 assert!(
1704 body.contains("COMMIT_POLICY_BELOW_FLOOR"),
1705 "typed floor violation should be surfaced, got {body}"
1706 );
1707 }
1708
1709 #[test]
1724 fn redwire_batch_insert_happy_path_returns_bulkok_with_count() {
1725 let runtime = RedDBRuntime::in_memory().expect("runtime");
1726 runtime
1727 .execute_query("CREATE TABLE events_587_ok (id INTEGER, name TEXT)")
1728 .expect("create table");
1729
1730 let frame = bulk_insert_frame(
1731 100,
1732 br#"{
1733 "collection":"events_587_ok",
1734 "idempotency_key":"k-ok",
1735 "payloads":[
1736 {"fields":{"id":1,"name":"a"}},
1737 {"fields":{"id":2,"name":"b"}},
1738 {"fields":{"id":3,"name":"c"}}
1739 ]
1740 }"#
1741 .to_vec(),
1742 );
1743 let reply = run_insert_dispatch(&runtime, &frame);
1744 assert_eq!(
1745 reply.kind,
1746 MessageKind::BulkOk,
1747 "body={:?}",
1748 String::from_utf8_lossy(&reply.payload)
1749 );
1750 let body: JsonValue = serde_json::from_slice(&reply.payload).expect("ok body json");
1751 assert_eq!(body.get("ok").and_then(JsonValue::as_bool), Some(true));
1752 assert_eq!(body.get("count").and_then(JsonValue::as_u64), Some(3));
1753
1754 let qr = runtime
1760 .execute_query("SELECT name FROM events_587_ok ORDER BY id ASC")
1761 .expect("scan");
1762 let names: Vec<String> = qr
1763 .result
1764 .records
1765 .iter()
1766 .filter_map(|record| match record.get("name") {
1767 Some(crate::storage::schema::Value::Text(s)) => Some(s.to_string()),
1768 _ => None,
1769 })
1770 .collect();
1771 assert_eq!(names, vec!["a", "b", "c"]);
1772 }
1773
1774 #[test]
1779 fn redwire_batch_insert_row_failure_rolls_back_with_row_index() {
1780 let runtime = RedDBRuntime::in_memory().expect("runtime");
1781 runtime
1782 .execute_query("CREATE TABLE events_587_rollback (id INTEGER, name TEXT)")
1783 .expect("create table");
1784
1785 let frame = bulk_insert_frame(
1788 101,
1789 br#"{
1790 "collection":"events_587_rollback",
1791 "idempotency_key":"k-rollback",
1792 "payloads":[
1793 {"fields":{"id":1,"name":"a"}},
1794 {"not_fields":{"id":2}},
1795 {"fields":{"id":3,"name":"c"}}
1796 ]
1797 }"#
1798 .to_vec(),
1799 );
1800 let reply = run_insert_dispatch(&runtime, &frame);
1801 assert_eq!(reply.kind, MessageKind::Error);
1802 let body: JsonValue = serde_json::from_slice(&reply.payload).expect("err body json");
1803 assert_eq!(body.get("ok").and_then(JsonValue::as_bool), Some(false));
1804 assert_eq!(
1805 body.get("code").and_then(JsonValue::as_str),
1806 Some("RowParseFailure")
1807 );
1808 assert_eq!(body.get("row_index").and_then(JsonValue::as_u64), Some(1));
1809
1810 let qr = runtime
1813 .execute_query("SELECT name FROM events_587_rollback")
1814 .expect("scan");
1815 assert!(
1816 qr.result.records.is_empty(),
1817 "row 0 leaked despite row 1 rejection: {} rows present",
1818 qr.result.records.len()
1819 );
1820 }
1821
1822 #[test]
1830 fn redwire_batch_insert_idempotency_key_replays_cached_result() {
1831 let runtime = RedDBRuntime::in_memory().expect("runtime");
1832 runtime
1833 .execute_query("CREATE TABLE events_587_dedup (id INTEGER, name TEXT)")
1834 .expect("create table");
1835
1836 let key = format!(
1840 "redwire-587-{}",
1841 std::time::SystemTime::now()
1842 .duration_since(std::time::UNIX_EPOCH)
1843 .unwrap()
1844 .as_nanos()
1845 );
1846
1847 let frame1 = bulk_insert_frame(
1848 200,
1849 format!(
1850 r#"{{
1851 "collection":"events_587_dedup",
1852 "idempotency_key":"{key}",
1853 "payloads":[{{"fields":{{"id":1,"name":"first"}}}}]
1854 }}"#
1855 )
1856 .into_bytes(),
1857 );
1858 let reply1 = run_insert_dispatch(&runtime, &frame1);
1859 assert_eq!(reply1.kind, MessageKind::BulkOk);
1860 let body1 = reply1.payload.clone();
1861
1862 let frame2 = bulk_insert_frame(
1866 201,
1867 format!(
1868 r#"{{
1869 "collection":"events_587_dedup",
1870 "idempotency_key":"{key}",
1871 "payloads":[{{"fields":{{"id":2,"name":"second"}}}}]
1872 }}"#
1873 )
1874 .into_bytes(),
1875 );
1876 let reply2 = run_insert_dispatch(&runtime, &frame2);
1877 assert_eq!(reply2.kind, MessageKind::BulkOk);
1878 assert_eq!(
1879 reply2.payload, body1,
1880 "replay must return cached body byte-for-byte"
1881 );
1882
1883 let qr = runtime
1884 .execute_query("SELECT name FROM events_587_dedup")
1885 .expect("scan");
1886 assert_eq!(
1887 qr.result.records.len(),
1888 1,
1889 "replay re-executed and committed the second row"
1890 );
1891 }
1892
1893 #[test]
1897 fn redwire_batch_insert_cache_shared_with_http_transport() {
1898 use crate::runtime::batch_insert::global_cache;
1899
1900 let runtime = RedDBRuntime::in_memory().expect("runtime");
1901 runtime
1902 .execute_query("CREATE TABLE events_587_shared (id INTEGER, name TEXT)")
1903 .expect("create table");
1904
1905 let key = format!(
1906 "shared-cache-587-{}",
1907 std::time::SystemTime::now()
1908 .duration_since(std::time::UNIX_EPOCH)
1909 .unwrap()
1910 .as_nanos()
1911 );
1912
1913 let frame = bulk_insert_frame(
1914 300,
1915 format!(
1916 r#"{{
1917 "collection":"events_587_shared",
1918 "idempotency_key":"{key}",
1919 "payloads":[{{"fields":{{"id":1,"name":"x"}}}}]
1920 }}"#
1921 )
1922 .into_bytes(),
1923 );
1924 let reply = run_insert_dispatch(&runtime, &frame);
1925 assert_eq!(reply.kind, MessageKind::BulkOk);
1926
1927 let hit = global_cache()
1931 .lookup("events_587_shared", &key, std::time::Instant::now())
1932 .expect("shared cache must serve the RedWire write to HTTP");
1933 assert_eq!(hit.status, 200);
1934 assert_eq!(hit.body, reply.payload);
1935 }
1936
1937 #[test]
1942 fn redwire_batch_insert_schema_validation_rejects_unknown_field() {
1943 use crate::runtime::analytics_schema_registry as reg;
1944
1945 let runtime = RedDBRuntime::in_memory().expect("runtime");
1946 runtime
1947 .execute_query("CREATE TABLE events_587_schema (event_name TEXT, payload TEXT)")
1948 .expect("create table");
1949
1950 let schema =
1951 r#"{"type":"object","properties":{"url":{"type":"string"}},"required":["url"]}"#;
1952 reg::register(runtime.db().store().as_ref(), "click_587", schema).expect("register schema");
1953
1954 let frame = bulk_insert_frame(
1955 400,
1956 br#"{
1957 "collection":"events_587_schema",
1958 "idempotency_key":"k-schema",
1959 "payloads":[
1960 {"fields":{"event_name":"click_587","payload":"{\"url\":\"/a\"}"}},
1961 {"fields":{"event_name":"click_587","payload":"{\"url\":\"/b\",\"extra\":1}"}}
1962 ]
1963 }"#
1964 .to_vec(),
1965 );
1966 let reply = run_insert_dispatch(&runtime, &frame);
1967 assert_eq!(reply.kind, MessageKind::Error);
1968 let body: JsonValue = serde_json::from_slice(&reply.payload).expect("err body json");
1969 assert_eq!(
1970 body.get("code").and_then(JsonValue::as_str),
1971 Some("RowSchemaRejected")
1972 );
1973 assert_eq!(body.get("row_index").and_then(JsonValue::as_u64), Some(1));
1974
1975 let qr = runtime
1976 .execute_query("SELECT event_name FROM events_587_schema")
1977 .expect("scan");
1978 assert!(
1979 qr.result.records.is_empty(),
1980 "row 0 leaked despite row 1 schema rejection"
1981 );
1982 }
1983
1984 #[test]
1995 fn redwire_batch_insert_oversize_returns_error_before_storage() {
1996 let runtime = RedDBRuntime::in_memory().expect("runtime");
1997 runtime
1998 .execute_query("CREATE TABLE events_587_oversize (id INTEGER, name TEXT)")
1999 .expect("create table");
2000
2001 let max = 10_000usize;
2003 let mut payloads = String::with_capacity(max * 32);
2004 payloads.push('[');
2005 for i in 0..(max + 1) {
2006 if i > 0 {
2007 payloads.push(',');
2008 }
2009 payloads.push_str(&format!(r#"{{"fields":{{"id":{i},"name":"x"}}}}"#));
2010 }
2011 payloads.push(']');
2012 let frame_body = format!(
2013 r#"{{"collection":"events_587_oversize","idempotency_key":"k-oversize-587","payloads":{payloads}}}"#
2014 );
2015 let frame = bulk_insert_frame(500, frame_body.into_bytes());
2016 let reply = run_insert_dispatch(&runtime, &frame);
2017
2018 assert_eq!(reply.kind, MessageKind::Error);
2019 let body: JsonValue = serde_json::from_slice(&reply.payload).expect("err body json");
2020 assert_eq!(
2021 body.get("code").and_then(JsonValue::as_str),
2022 Some("BatchTooLarge")
2023 );
2024 let qr = runtime
2025 .execute_query("SELECT name FROM events_587_oversize")
2026 .expect("scan");
2027 assert!(
2028 qr.result.records.is_empty(),
2029 "oversize batch leaked rows into storage"
2030 );
2031 }
2032}
2033
2034#[cfg(test)]
2035mod session_bulk_stream_tests;