1use std::io::{BufRead, BufReader, Stdin, Write};
19use std::panic::AssertUnwindSafe;
20
21use tokio::sync::Mutex as AsyncMutex;
22
23use crate::application::entity::{CreateRowInput, CreateRowsBatchInput};
24use crate::application::ports::RuntimeEntityPort;
25use crate::json::{self as json, Value};
26use crate::runtime::query_request::{
27 ParamValue, PreparedId, PreparedRegistry, QueryRequest, QueryRequestExecutor,
28};
29use crate::runtime::{RedDBRuntime, RuntimeQueryResult};
30use crate::storage::query::unified::UnifiedRecord;
31use reddb_grpc_proto::RedDBClient;
32use reddb_types::Value as SchemaValue;
33
34enum Backend<'a> {
47 Local(&'a RedDBRuntime),
48 Remote(Box<RemoteBackend<'a>>),
49}
50
51struct RemoteBackend<'a> {
52 client: AsyncMutex<RedDBClient>,
53 tokio_rt: &'a tokio::runtime::Runtime,
54}
55
56pub const PROTOCOL_VERSION: &str = "1.0";
58const STDIO_BULK_INSERT_CHUNK_ROWS: usize = 500;
59
60#[derive(Clone, Copy, Debug, Eq, PartialEq)]
62pub(crate) enum StdioRemoteDisposition {
63 Served,
64 Unsupported { error_code: &'static str },
65}
66
67#[derive(Clone, Copy, Debug, Eq, PartialEq)]
69pub(crate) struct StdioMethodCatalogEntry {
70 pub(crate) method: &'static str,
71 pub(crate) command_id: &'static str,
72 pub(crate) remote: StdioRemoteDisposition,
73}
74
75const fn stdio_method(
76 method: &'static str,
77 command_id: &'static str,
78 remote: StdioRemoteDisposition,
79) -> StdioMethodCatalogEntry {
80 StdioMethodCatalogEntry {
81 method,
82 command_id,
83 remote,
84 }
85}
86
87const STDIO_METHOD_CATALOG: [StdioMethodCatalogEntry; 16] = [
91 stdio_method(
92 "tx.begin",
93 "query.execute",
94 StdioRemoteDisposition::Unsupported {
95 error_code: error_code::TX_NOT_SUPPORTED_REMOTE,
96 },
97 ),
98 stdio_method(
99 "tx.commit",
100 "query.execute",
101 StdioRemoteDisposition::Unsupported {
102 error_code: error_code::TX_NOT_SUPPORTED_REMOTE,
103 },
104 ),
105 stdio_method(
106 "query.open",
107 "streams.query.output",
108 StdioRemoteDisposition::Unsupported {
109 error_code: error_code::CURSOR_NOT_SUPPORTED_REMOTE,
110 },
111 ),
112 stdio_method(
113 "query.next",
114 "streams.query.output",
115 StdioRemoteDisposition::Unsupported {
116 error_code: error_code::CURSOR_NOT_SUPPORTED_REMOTE,
117 },
118 ),
119 stdio_method(
120 "query.close",
121 "streams.query.cancel",
122 StdioRemoteDisposition::Unsupported {
123 error_code: error_code::CURSOR_NOT_SUPPORTED_REMOTE,
124 },
125 ),
126 stdio_method(
127 "tx.rollback",
128 "query.execute",
129 StdioRemoteDisposition::Unsupported {
130 error_code: error_code::TX_NOT_SUPPORTED_REMOTE,
131 },
132 ),
133 stdio_method("version", "health.live", StdioRemoteDisposition::Served),
134 stdio_method(
135 "health",
136 "ops.health.aggregate",
137 StdioRemoteDisposition::Served,
138 ),
139 stdio_method("query", "query.execute", StdioRemoteDisposition::Served),
140 stdio_method(
141 "prepare",
142 "query.execute",
143 StdioRemoteDisposition::Unsupported {
144 error_code: error_code::PREPARED_NOT_SUPPORTED_REMOTE,
145 },
146 ),
147 stdio_method(
148 "execute_prepared",
149 "query.execute",
150 StdioRemoteDisposition::Unsupported {
151 error_code: error_code::PREPARED_NOT_SUPPORTED_REMOTE,
152 },
153 ),
154 stdio_method(
155 "insert",
156 "collections.rows.create",
157 StdioRemoteDisposition::Served,
158 ),
159 stdio_method(
160 "bulk_insert",
161 "collections.bulk.rows",
162 StdioRemoteDisposition::Served,
163 ),
164 stdio_method(
165 "get",
166 "collections.entities.get",
167 StdioRemoteDisposition::Served,
168 ),
169 stdio_method(
170 "delete",
171 "collections.entities.delete",
172 StdioRemoteDisposition::Served,
173 ),
174 stdio_method("close", "health.live", StdioRemoteDisposition::Served),
175];
176
177pub(crate) fn stdio_method_catalog() -> &'static [StdioMethodCatalogEntry] {
178 &STDIO_METHOD_CATALOG
179}
180
181pub mod error_code {
183 pub const PARSE_ERROR: &str = "PARSE_ERROR";
184 pub const INVALID_REQUEST: &str = "INVALID_REQUEST";
185 pub const INVALID_PARAMS: &str = "INVALID_PARAMS";
186 pub const QUERY_ERROR: &str = "QUERY_ERROR";
187 pub const NOT_FOUND: &str = "NOT_FOUND";
188 pub const INTERNAL_ERROR: &str = "INTERNAL_ERROR";
189 pub const TX_ALREADY_OPEN: &str = "TX_ALREADY_OPEN";
192 pub const NO_TX_OPEN: &str = "NO_TX_OPEN";
195 pub const TX_REPLAY_FAILED: &str = "TX_REPLAY_FAILED";
199 pub const TX_NOT_SUPPORTED_REMOTE: &str = "TX_NOT_SUPPORTED_REMOTE";
201 pub const CURSOR_NOT_SUPPORTED_REMOTE: &str = "CURSOR_NOT_SUPPORTED_REMOTE";
203 pub const PREPARED_NOT_SUPPORTED_REMOTE: &str = "PREPARED_NOT_SUPPORTED_REMOTE";
205 pub const CURSOR_NOT_FOUND: &str = "CURSOR_NOT_FOUND";
209 pub const CURSOR_LIMIT_EXCEEDED: &str = "CURSOR_LIMIT_EXCEEDED";
211}
212
213pub(crate) const MAX_CURSORS_PER_SESSION: usize = 64;
217pub(crate) const DEFAULT_CURSOR_BATCH_SIZE: usize = 100;
221pub(crate) const MAX_CURSOR_BATCH_SIZE: usize = 10_000;
224
225pub(crate) struct Session {
245 next_tx_id: u64,
246 current_tx: Option<OpenTx>,
247 next_cursor_id: u64,
248 cursors: std::collections::HashMap<u64, Cursor>,
249 prepared: PreparedRegistry,
251}
252
253impl Session {
254 pub(crate) fn new() -> Self {
255 Self {
256 next_tx_id: 1,
257 current_tx: None,
258 next_cursor_id: 1,
259 cursors: std::collections::HashMap::new(),
260 prepared: PreparedRegistry::new(),
261 }
262 }
263
264 fn open_tx(&mut self) -> Result<u64, (&'static str, String)> {
265 if let Some(tx) = &self.current_tx {
266 return Err((
267 error_code::TX_ALREADY_OPEN,
268 format!("transaction {} already open in this session", tx.tx_id),
269 ));
270 }
271 let tx_id = self.next_tx_id;
272 self.next_tx_id = self.next_tx_id.saturating_add(1);
273 self.current_tx = Some(OpenTx {
274 tx_id,
275 write_set: Vec::new(),
276 });
277 Ok(tx_id)
278 }
279
280 fn take_tx(&mut self) -> Option<OpenTx> {
281 self.current_tx.take()
282 }
283
284 fn current_tx_mut(&mut self) -> Option<&mut OpenTx> {
285 self.current_tx.as_mut()
286 }
287
288 #[allow(dead_code)]
289 fn has_tx(&self) -> bool {
290 self.current_tx.is_some()
291 }
292
293 fn insert_cursor(&mut self, cursor: Cursor) -> Result<u64, (&'static str, String)> {
296 if self.cursors.len() >= MAX_CURSORS_PER_SESSION {
297 return Err((
298 error_code::CURSOR_LIMIT_EXCEEDED,
299 format!(
300 "session already holds {} cursors (max {}) — close some before opening new ones",
301 self.cursors.len(),
302 MAX_CURSORS_PER_SESSION
303 ),
304 ));
305 }
306 let id = self.next_cursor_id;
307 self.next_cursor_id = self.next_cursor_id.saturating_add(1);
308 let mut cursor = cursor;
309 cursor.cursor_id = id;
310 self.cursors.insert(id, cursor);
311 Ok(id)
312 }
313
314 fn cursor_mut(&mut self, id: u64) -> Option<&mut Cursor> {
315 self.cursors.get_mut(&id)
316 }
317
318 fn drop_cursor(&mut self, id: u64) -> Option<Cursor> {
319 self.cursors.remove(&id)
320 }
321
322 fn clear_cursors(&mut self) {
323 self.cursors.clear();
324 }
325}
326
327impl Default for Session {
328 fn default() -> Self {
329 Self::new()
330 }
331}
332
333struct OpenTx {
335 tx_id: u64,
336 write_set: Vec<PendingSql>,
337}
338
339enum PendingSql {
343 Insert(String),
344 Delete(String),
345 #[allow(dead_code)] Update(String),
347}
348
349impl PendingSql {
350 fn sql(&self) -> &str {
351 match self {
352 PendingSql::Insert(s) | PendingSql::Delete(s) | PendingSql::Update(s) => s,
353 }
354 }
355}
356
357pub(crate) struct Cursor {
369 cursor_id: u64,
370 columns: Vec<String>,
371 rows: Vec<UnifiedRecord>,
372 position: usize,
373}
374
375impl Cursor {
376 fn new(columns: Vec<String>, rows: Vec<UnifiedRecord>) -> Self {
377 Self {
378 cursor_id: 0, columns,
380 rows,
381 position: 0,
382 }
383 }
384
385 fn total(&self) -> usize {
386 self.rows.len()
387 }
388
389 fn remaining(&self) -> usize {
390 self.rows.len().saturating_sub(self.position)
391 }
392
393 fn is_exhausted(&self) -> bool {
394 self.position >= self.rows.len()
395 }
396
397 fn take_batch(&mut self, batch_size: usize) -> &[UnifiedRecord] {
400 let end = (self.position + batch_size).min(self.rows.len());
401 let slice = &self.rows[self.position..end];
402 self.position = end;
403 slice
404 }
405}
406
407pub fn run(runtime: &RedDBRuntime) -> i32 {
413 run_with_io(runtime, std::io::stdin(), &mut std::io::stdout())
414}
415
416pub fn run_remote(endpoint: &str, token: Option<String>) -> i32 {
423 let tokio_rt = match tokio::runtime::Builder::new_current_thread()
424 .enable_all()
425 .build()
426 {
427 Ok(rt) => rt,
428 Err(e) => {
429 tracing::error!(err = %e, "rpc: failed to build tokio runtime");
430 return 1;
431 }
432 };
433 let client = match tokio_rt.block_on(RedDBClient::connect(endpoint, token)) {
434 Ok(c) => c,
435 Err(e) => {
436 tracing::error!(endpoint, err = %e, "rpc: failed to connect");
437 return 1;
438 }
439 };
440 let backend = Backend::Remote(Box::new(RemoteBackend {
441 client: AsyncMutex::new(client),
442 tokio_rt: &tokio_rt,
443 }));
444 run_backend(&backend, std::io::stdin(), &mut std::io::stdout())
445}
446
447pub fn run_with_io<W: Write>(runtime: &RedDBRuntime, stdin: Stdin, stdout: &mut W) -> i32 {
449 run_backend(&Backend::Local(runtime), stdin, stdout)
450}
451
452pub struct EmbeddedRpcSession {
466 runtime: std::sync::Arc<RedDBRuntime>,
467 session: Session,
468 conn_id: u64,
469}
470
471impl EmbeddedRpcSession {
472 pub fn new(runtime: std::sync::Arc<RedDBRuntime>) -> Self {
473 Self {
474 runtime,
475 session: Session::new(),
476 conn_id: next_stdio_conn_id(),
477 }
478 }
479
480 pub fn handle(&mut self, line: &str) -> (String, bool) {
485 crate::runtime::impl_core::set_current_connection_id(self.conn_id);
486 let response = handle_line(&Backend::Local(&self.runtime), &mut self.session, line);
487 crate::runtime::impl_core::clear_current_connection_id();
488 let closed = response.contains("\"__close__\":true");
489 (response, closed)
490 }
491}
492
493impl Drop for EmbeddedRpcSession {
494 fn drop(&mut self) {
495 let _ = self.session.take_tx();
497 }
498}
499
500static STDIO_SESSION_CONN_ID: std::sync::atomic::AtomicU64 =
507 std::sync::atomic::AtomicU64::new(1_000_000);
508
509fn next_stdio_conn_id() -> u64 {
510 STDIO_SESSION_CONN_ID.fetch_add(1, std::sync::atomic::Ordering::Relaxed)
511}
512
513fn run_backend<W: Write>(backend: &Backend<'_>, stdin: Stdin, stdout: &mut W) -> i32 {
514 let reader = BufReader::new(stdin.lock());
515 let mut session = Session::new();
516 let conn_id = next_stdio_conn_id();
520 crate::runtime::impl_core::set_current_connection_id(conn_id);
521 for line_result in reader.lines() {
522 let line = match line_result {
523 Ok(l) => l,
524 Err(e) => {
525 let _ = writeln!(
526 stdout,
527 "{}",
528 error_response(&Value::Null, error_code::INTERNAL_ERROR, &e.to_string())
529 );
530 let _ = stdout.flush();
531 return 1;
532 }
533 };
534 if line.trim().is_empty() {
535 continue;
536 }
537 let response = handle_line(backend, &mut session, &line);
538 if writeln!(stdout, "{}", response).is_err() || stdout.flush().is_err() {
539 return 1;
540 }
541 if response.contains("\"__close__\":true") {
542 return 0;
543 }
544 }
545 let _ = session.take_tx();
549 crate::runtime::impl_core::clear_current_connection_id();
550 0
551}
552
553fn handle_line(backend: &Backend<'_>, session: &mut Session, line: &str) -> String {
557 let parsed: Value = match json::from_str(line) {
558 Ok(v) => v,
559 Err(err) => {
560 return error_response(
561 &Value::Null,
562 error_code::PARSE_ERROR,
563 &format!("invalid JSON: {err}"),
564 );
565 }
566 };
567
568 let id = parsed.get("id").cloned().unwrap_or(Value::Null);
569
570 let method = match parsed.get("method").and_then(Value::as_str) {
571 Some(m) => m.to_string(),
572 None => {
573 return error_response(&id, error_code::INVALID_REQUEST, "missing 'method' field");
574 }
575 };
576
577 let params = parsed.get("params").cloned().unwrap_or(Value::Null);
578
579 let dispatch = std::panic::catch_unwind(AssertUnwindSafe(|| {
580 let entry = stdio_method_catalog()
581 .iter()
582 .find(|entry| entry.method == method);
583 if !method.starts_with("auth.") && entry.is_none() {
584 return Err((
585 error_code::INVALID_REQUEST,
586 format!("unknown method: {method}"),
587 ));
588 }
589 if let Some(entry) = entry {
590 if crate::server::command_catalog()
591 .command(entry.command_id)
592 .is_none()
593 {
594 return Err((
595 error_code::INTERNAL_ERROR,
596 format!(
597 "stdio method {method} names missing catalog command {}",
598 entry.command_id
599 ),
600 ));
601 }
602 }
603
604 match backend {
605 Backend::Local(rt) => dispatch_method(rt, session, &method, ¶ms),
606 Backend::Remote(remote) => match entry.map(|entry| entry.remote) {
607 Some(StdioRemoteDisposition::Unsupported { error_code }) => Err((
608 error_code,
609 format!("{method} is not supported over remote gRPC yet"),
610 )),
611 Some(StdioRemoteDisposition::Served) | None => {
612 dispatch_method_remote(&remote.client, remote.tokio_rt, &method, ¶ms)
613 }
614 },
615 }
616 }));
617
618 match dispatch {
619 Ok(Ok(result)) => success_response(&id, &result, method == "close"),
620 Ok(Err((code, msg))) => error_response(&id, code, &msg),
621 Err(_) => error_response(&id, error_code::INTERNAL_ERROR, "handler panicked (caught)"),
622 }
623}
624
625fn dispatch_method(
628 runtime: &RedDBRuntime,
629 session: &mut Session,
630 method: &str,
631 params: &Value,
632) -> Result<Value, (&'static str, String)> {
633 match method {
634 "tx.begin" => {
635 let tx_id = session.open_tx()?;
636 Ok(Value::Object(
637 [
638 ("tx_id".to_string(), Value::Number(tx_id as f64)),
639 (
640 "isolation".to_string(),
641 Value::String("read_committed_deferred".to_string()),
642 ),
643 ]
644 .into_iter()
645 .collect(),
646 ))
647 }
648
649 "tx.commit" => {
650 let tx = session.take_tx().ok_or((
651 error_code::NO_TX_OPEN,
652 "no transaction is open in this session".to_string(),
653 ))?;
654 let tx_id = tx.tx_id;
655 let op_count = tx.write_set.len();
656
657 let replay: Result<(u64, usize), (usize, String)> = (|| {
663 runtime
664 .execute_query("BEGIN")
665 .map_err(|e| (0usize, format!("BEGIN: {e}")))?;
666 let mut total_affected: u64 = 0;
667 for (idx, op) in tx.write_set.iter().enumerate() {
668 match runtime.execute_query(op.sql()) {
669 Ok(qr) => total_affected += qr.affected_rows,
670 Err(e) => {
671 let _ = runtime.execute_query("ROLLBACK");
672 return Err((idx, e.to_string()));
673 }
674 }
675 }
676 runtime
677 .execute_query("COMMIT")
678 .map_err(|e| (op_count, format!("COMMIT: {e}")))?;
679 Ok((total_affected, op_count))
680 })();
681
682 match replay {
683 Ok((affected, replayed)) => Ok(Value::Object(
684 [
685 ("tx_id".to_string(), Value::Number(tx_id as f64)),
686 ("ops_replayed".to_string(), Value::Number(replayed as f64)),
687 ("affected".to_string(), Value::Number(affected as f64)),
688 ]
689 .into_iter()
690 .collect(),
691 )),
692 Err((failed_idx, msg)) => Err((
693 error_code::TX_REPLAY_FAILED,
694 format!(
695 "tx {tx_id} replay failed at op {failed_idx}/{op_count}: {msg} \
696 (ops 0..{failed_idx} already applied and are NOT rolled back)"
697 ),
698 )),
699 }
700 }
701
702 "query.open" => {
703 let sql = params.get("sql").and_then(Value::as_str).ok_or((
704 error_code::INVALID_PARAMS,
705 "missing 'sql' string".to_string(),
706 ))?;
707 let qr = runtime
708 .execute_query(sql)
709 .map_err(|e| (error_code::QUERY_ERROR, e.to_string()))?;
710
711 let columns: Vec<String> = qr
715 .result
716 .records
717 .first()
718 .map(|first| {
719 let mut keys: Vec<String> = first
720 .column_names()
721 .into_iter()
722 .map(|k| k.to_string())
723 .collect();
724 keys.sort();
725 keys
726 })
727 .unwrap_or_default();
728
729 let cursor = Cursor::new(columns.clone(), qr.result.records);
730 let total = cursor.total();
731 let cursor_id = session.insert_cursor(cursor)?;
732
733 Ok(Value::Object(
734 [
735 ("cursor_id".to_string(), Value::Number(cursor_id as f64)),
736 (
737 "columns".to_string(),
738 Value::Array(columns.into_iter().map(Value::String).collect()),
739 ),
740 ("total_rows".to_string(), Value::Number(total as f64)),
741 ]
742 .into_iter()
743 .collect(),
744 ))
745 }
746
747 "query.next" => {
748 let cursor_id = params
749 .get("cursor_id")
750 .and_then(|v| v.as_f64())
751 .map(|n| n as u64)
752 .ok_or((
753 error_code::INVALID_PARAMS,
754 "missing 'cursor_id' number".to_string(),
755 ))?;
756 let batch_size = params
757 .get("batch_size")
758 .and_then(|v| v.as_f64())
759 .map(|n| n as usize)
760 .unwrap_or(DEFAULT_CURSOR_BATCH_SIZE)
761 .clamp(1, MAX_CURSOR_BATCH_SIZE);
762
763 let (rows, done, remaining) = {
766 let cursor = session.cursor_mut(cursor_id).ok_or((
767 error_code::CURSOR_NOT_FOUND,
768 format!("cursor {cursor_id} not found"),
769 ))?;
770 let batch = cursor.take_batch(batch_size);
771 let rows_json: Vec<Value> = batch.iter().map(record_to_json_object).collect();
772 (rows_json, cursor.is_exhausted(), cursor.remaining())
773 };
774
775 if done {
776 let _ = session.drop_cursor(cursor_id);
779 }
780
781 Ok(Value::Object(
782 [
783 ("cursor_id".to_string(), Value::Number(cursor_id as f64)),
784 ("rows".to_string(), Value::Array(rows)),
785 ("done".to_string(), Value::Bool(done)),
786 ("remaining".to_string(), Value::Number(remaining as f64)),
787 ]
788 .into_iter()
789 .collect(),
790 ))
791 }
792
793 "query.close" => {
794 let cursor_id = params
795 .get("cursor_id")
796 .and_then(|v| v.as_f64())
797 .map(|n| n as u64)
798 .ok_or((
799 error_code::INVALID_PARAMS,
800 "missing 'cursor_id' number".to_string(),
801 ))?;
802 let existed = session.drop_cursor(cursor_id).is_some();
803 if !existed {
804 return Err((
805 error_code::CURSOR_NOT_FOUND,
806 format!("cursor {cursor_id} not found"),
807 ));
808 }
809 Ok(Value::Object(
810 [
811 ("cursor_id".to_string(), Value::Number(cursor_id as f64)),
812 ("closed".to_string(), Value::Bool(true)),
813 ]
814 .into_iter()
815 .collect(),
816 ))
817 }
818
819 "tx.rollback" => {
820 let tx = session.take_tx().ok_or((
821 error_code::NO_TX_OPEN,
822 "no transaction is open in this session".to_string(),
823 ))?;
824 let ops_discarded = tx.write_set.len();
825 Ok(Value::Object(
826 [
827 ("tx_id".to_string(), Value::Number(tx.tx_id as f64)),
828 (
829 "ops_discarded".to_string(),
830 Value::Number(ops_discarded as f64),
831 ),
832 ]
833 .into_iter()
834 .collect(),
835 ))
836 }
837
838 "version" => Ok(Value::Object(
839 [
840 (
841 "version".to_string(),
842 Value::String(env!("CARGO_PKG_VERSION").to_string()),
843 ),
844 (
845 "protocol".to_string(),
846 Value::String(PROTOCOL_VERSION.to_string()),
847 ),
848 ]
849 .into_iter()
850 .collect(),
851 )),
852
853 "health" => Ok(Value::Object(
854 [
855 ("ok".to_string(), Value::Bool(true)),
856 (
857 "version".to_string(),
858 Value::String(env!("CARGO_PKG_VERSION").to_string()),
859 ),
860 ]
861 .into_iter()
862 .collect(),
863 )),
864
865 "query" => {
866 let sql = params.get("sql").and_then(Value::as_str).ok_or((
867 error_code::INVALID_PARAMS,
868 "missing 'sql' string".to_string(),
869 ))?;
870
871 let bind_values: Vec<ParamValue> = params
874 .get("params")
875 .map(|v| {
876 v.as_array()
877 .ok_or((
878 error_code::INVALID_PARAMS,
879 "'params' must be an array".to_string(),
880 ))
881 .and_then(|arr| {
882 arr.iter()
883 .map(ParamValue::decode_json)
884 .collect::<Result<Vec<_>, _>>()
885 .map_err(|message| (error_code::INVALID_PARAMS, message))
886 })
887 })
888 .transpose()?
889 .unwrap_or_default();
890
891 let qr = QueryRequestExecutor::new(runtime, &session.prepared)
892 .execute(QueryRequest::sql(sql, bind_values))
893 .map_err(|error| {
894 if matches!(error, crate::api::RedDBError::Validation { .. }) {
895 (error_code::INVALID_PARAMS, error.to_string())
896 } else {
897 (error_code::QUERY_ERROR, error.to_string())
898 }
899 })?;
900 Ok(query_result_to_json(&qr))
901 }
902
903 "prepare" => {
912 let sql = params.get("sql").and_then(Value::as_str).ok_or((
913 error_code::INVALID_PARAMS,
914 "missing 'sql' string".to_string(),
915 ))?;
916 let prepared = session
917 .prepared
918 .prepare_auto_parameterized(runtime, sql)
919 .map_err(|error| (error_code::QUERY_ERROR, error.to_string()))?;
920 Ok(Value::Object(
921 [
922 (
923 "prepared_id".to_string(),
924 Value::Number(prepared.id.into_raw() as f64),
925 ),
926 (
927 "parameter_count".to_string(),
928 Value::Number(prepared.parameter_count as f64),
929 ),
930 ]
931 .into_iter()
932 .collect(),
933 ))
934 }
935
936 "execute_prepared" => {
937 let id = params
938 .get("prepared_id")
939 .and_then(Value::as_f64)
940 .map(|n| n as u64)
941 .ok_or((
942 error_code::INVALID_PARAMS,
943 "missing 'prepared_id'".to_string(),
944 ))?;
945
946 let binds_json: Vec<Value> = params
948 .get("binds")
949 .and_then(Value::as_array)
950 .map(|a| a.to_vec())
951 .unwrap_or_default();
952 let binds: Vec<ParamValue> = binds_json
953 .iter()
954 .map(ParamValue::decode_json)
955 .collect::<Result<Vec<_>, _>>()
956 .map_err(|message| (error_code::INVALID_PARAMS, message))?;
957
958 let qr = QueryRequestExecutor::new(runtime, &session.prepared)
959 .execute(QueryRequest::prepared(PreparedId::from_raw(id), binds))
960 .map_err(|error| {
961 if matches!(error, crate::api::RedDBError::Validation { .. }) {
962 (error_code::INVALID_PARAMS, error.to_string())
963 } else {
964 (error_code::QUERY_ERROR, error.to_string())
965 }
966 })?;
967 Ok(query_result_to_json(&qr))
968 }
969
970 "insert" => {
971 let collection = params.get("collection").and_then(Value::as_str).ok_or((
972 error_code::INVALID_PARAMS,
973 "missing 'collection' string".to_string(),
974 ))?;
975 let payload = params.get("payload").ok_or((
976 error_code::INVALID_PARAMS,
977 "missing 'payload' object".to_string(),
978 ))?;
979 let payload_obj = payload.as_object().ok_or((
980 error_code::INVALID_PARAMS,
981 "'payload' must be a JSON object".to_string(),
982 ))?;
983 if let Some(tx) = session.current_tx_mut() {
984 ensure_sql_collection(collection).map_err(|m| (error_code::INVALID_PARAMS, m))?;
985 let sql = build_insert_sql(collection, payload_obj.iter());
986 tx.write_set.push(PendingSql::Insert(sql));
987 return Ok(pending_tx_response(tx.tx_id));
988 }
989
990 let output = runtime
991 .create_row(flat_payload_to_row_input(collection, payload_obj))
992 .map_err(|e| (error_code::QUERY_ERROR, e.to_string()))?;
993 let mut out = json::Map::new();
994 out.insert("affected".to_string(), Value::Number(1.0));
995 out.insert("id".to_string(), Value::String(output.id.raw().to_string()));
996 Ok(Value::Object(out))
997 }
998
999 "bulk_insert" => {
1000 let collection = params.get("collection").and_then(Value::as_str).ok_or((
1001 error_code::INVALID_PARAMS,
1002 "missing 'collection' string".to_string(),
1003 ))?;
1004 let payloads = params.get("payloads").and_then(Value::as_array).ok_or((
1005 error_code::INVALID_PARAMS,
1006 "missing 'payloads' array".to_string(),
1007 ))?;
1008
1009 let mut objects = Vec::with_capacity(payloads.len());
1010 for entry in payloads {
1011 objects.push(entry.as_object().ok_or((
1012 error_code::INVALID_PARAMS,
1013 "each payload must be a JSON object".to_string(),
1014 ))?);
1015 }
1016
1017 if let Some(tx) = session.current_tx_mut() {
1018 let mut buffered: u64 = 0;
1019 for obj in &objects {
1020 let sql = build_insert_sql(collection, obj.iter());
1021 tx.write_set.push(PendingSql::Insert(sql));
1022 buffered += 1;
1023 }
1024 let tx_id = tx.tx_id;
1025 return Ok(Value::Object(
1026 [
1027 ("affected".to_string(), Value::Number(0.0)),
1028 ("buffered".to_string(), Value::Number(buffered as f64)),
1029 ("pending".to_string(), Value::Bool(true)),
1030 ("tx_id".to_string(), Value::Number(tx_id as f64)),
1031 ]
1032 .into_iter()
1033 .collect(),
1034 ));
1035 }
1036
1037 if should_bulk_insert_graph(runtime, collection, &objects) {
1038 return bulk_insert_graph(runtime, collection, &objects)
1039 .map_err(|e| (error_code::QUERY_ERROR, e.to_string()));
1040 }
1041
1042 let mut total_affected: u64 = 0;
1043 let mut ids = Vec::with_capacity(objects.len());
1044 for chunk in objects.chunks(STDIO_BULK_INSERT_CHUNK_ROWS) {
1045 let rows = chunk
1046 .iter()
1047 .map(|obj| flat_payload_to_row_input(collection, obj))
1048 .collect();
1049 let outputs = runtime
1050 .create_rows_batch(CreateRowsBatchInput {
1051 collection: collection.to_string(),
1052 rows,
1053 suppress_events: false,
1054 })
1055 .map_err(|e| (error_code::QUERY_ERROR, e.to_string()))?;
1056 total_affected += outputs.len() as u64;
1057 ids.extend(
1058 outputs
1059 .into_iter()
1060 .map(|output| Value::String(output.id.raw().to_string())),
1061 );
1062 }
1063 let mut out = json::Map::new();
1064 out.insert("affected".to_string(), Value::Number(total_affected as f64));
1065 out.insert("ids".to_string(), Value::Array(ids));
1066 Ok(Value::Object(out))
1067 }
1068
1069 "get" => {
1070 let collection = params.get("collection").and_then(Value::as_str).ok_or((
1071 error_code::INVALID_PARAMS,
1072 "missing 'collection' string".to_string(),
1073 ))?;
1074 let id = params.get("id").and_then(Value::as_str).ok_or((
1075 error_code::INVALID_PARAMS,
1076 "missing 'id' string".to_string(),
1077 ))?;
1078 ensure_sql_collection(collection).map_err(|m| (error_code::INVALID_PARAMS, m))?;
1079 let Some(rid) = rid_from_str(id) else {
1080 return Ok(Value::Object(
1081 [("entity".to_string(), Value::Null)].into_iter().collect(),
1082 ));
1083 };
1084 let sql = format!("SELECT * FROM {collection} WHERE rid = {rid} LIMIT 1");
1085 let qr = runtime
1086 .execute_query(&sql)
1087 .map_err(|e| (error_code::QUERY_ERROR, e.to_string()))?;
1088 let entity = qr
1089 .result
1090 .records
1091 .first()
1092 .map(record_to_json_object)
1093 .unwrap_or(Value::Null);
1094 Ok(Value::Object(
1095 [("entity".to_string(), entity)].into_iter().collect(),
1096 ))
1097 }
1098
1099 "delete" => {
1100 let collection = params.get("collection").and_then(Value::as_str).ok_or((
1101 error_code::INVALID_PARAMS,
1102 "missing 'collection' string".to_string(),
1103 ))?;
1104 let id = params.get("id").and_then(Value::as_str).ok_or((
1105 error_code::INVALID_PARAMS,
1106 "missing 'id' string".to_string(),
1107 ))?;
1108 ensure_sql_collection(collection).map_err(|m| (error_code::INVALID_PARAMS, m))?;
1109 let Some(rid) = rid_from_str(id) else {
1110 return Ok(Value::Object(
1111 [("affected".to_string(), Value::Number(0.0))]
1112 .into_iter()
1113 .collect(),
1114 ));
1115 };
1116 let sql = format!("DELETE FROM {collection} WHERE rid = {rid}");
1117
1118 if let Some(tx) = session.current_tx_mut() {
1119 tx.write_set.push(PendingSql::Delete(sql));
1120 return Ok(pending_tx_response(tx.tx_id));
1121 }
1122
1123 let qr = runtime
1124 .execute_query(&sql)
1125 .map_err(|e| (error_code::QUERY_ERROR, e.to_string()))?;
1126 Ok(Value::Object(
1127 [(
1128 "affected".to_string(),
1129 Value::Number(qr.affected_rows as f64),
1130 )]
1131 .into_iter()
1132 .collect(),
1133 ))
1134 }
1135
1136 "close" => {
1137 let _ = session.take_tx();
1142 session.clear_cursors();
1143 let _ = runtime.checkpoint();
1144 Ok(Value::Null)
1145 }
1146
1147 "auth.login"
1152 | "auth.whoami"
1153 | "auth.change_password"
1154 | "auth.create_api_key"
1155 | "auth.revoke_api_key" => {
1156 let _ = (session, params);
1157 Err((
1158 error_code::INVALID_REQUEST,
1159 format!(
1160 "{method}: auth methods are only available on grpc:// connections; \
1161 embedded modes (memory://, file://) inherit caller privileges"
1162 ),
1163 ))
1164 }
1165
1166 other => Err((
1167 error_code::INVALID_REQUEST,
1168 format!("unknown method: {other}"),
1169 )),
1170 }
1171}
1172
1173fn success_response(id: &Value, result: &Value, is_close: bool) -> String {
1178 let mut envelope = json::Map::new();
1183 envelope.insert("jsonrpc".to_string(), Value::String("2.0".to_string()));
1184 envelope.insert("id".to_string(), id.clone());
1185 envelope.insert("result".to_string(), result.clone());
1186 if is_close {
1187 envelope.insert("__close__".to_string(), Value::Bool(true));
1188 }
1189 Value::Object(envelope).to_string_compact()
1190}
1191
1192fn error_response(id: &Value, code: &str, message: &str) -> String {
1193 let mut err = json::Map::new();
1194 err.insert("code".to_string(), Value::String(code.to_string()));
1195 err.insert("message".to_string(), Value::String(message.to_string()));
1196 err.insert("data".to_string(), Value::Null);
1197
1198 let mut envelope = json::Map::new();
1199 envelope.insert("jsonrpc".to_string(), Value::String("2.0".to_string()));
1200 envelope.insert("id".to_string(), id.clone());
1201 envelope.insert("error".to_string(), Value::Object(err));
1202 Value::Object(envelope).to_string_compact()
1203}
1204
1205fn pending_tx_response(tx_id: u64) -> Value {
1212 Value::Object(
1213 [
1214 ("affected".to_string(), Value::Number(0.0)),
1215 ("pending".to_string(), Value::Bool(true)),
1216 ("tx_id".to_string(), Value::Number(tx_id as f64)),
1217 ]
1218 .into_iter()
1219 .collect(),
1220 )
1221}
1222
1223pub(crate) fn ensure_sql_collection(collection: &str) -> Result<(), String> {
1228 if crate::server::handlers_query::is_safe_sql_identifier(collection) {
1229 Ok(())
1230 } else {
1231 Err(format!(
1232 "invalid collection name '{collection}': expected [A-Za-z_][A-Za-z0-9_]*"
1233 ))
1234 }
1235}
1236
1237pub(crate) fn rid_from_str(id: &str) -> Option<u64> {
1241 id.parse::<u64>().ok()
1242}
1243
1244pub(crate) fn build_insert_sql<'a, I>(collection: &str, fields: I) -> String
1245where
1246 I: Iterator<Item = (&'a String, &'a Value)>,
1247{
1248 let mut cols = Vec::new();
1249 let mut vals = Vec::new();
1250 for (k, v) in fields {
1251 cols.push(k.clone());
1252 vals.push(value_to_sql_literal(v));
1253 }
1254 format!(
1255 "INSERT INTO {collection} ({}) VALUES ({})",
1256 cols.join(", "),
1257 vals.join(", "),
1258 )
1259}
1260
1261fn flat_payload_to_row_input(
1262 collection: &str,
1263 payload: &json::Map<String, Value>,
1264) -> CreateRowInput {
1265 CreateRowInput {
1266 collection: collection.to_string(),
1267 fields: payload
1268 .iter()
1269 .map(|(key, value)| {
1270 (
1271 key.clone(),
1272 SchemaValue::from(ParamValue::decode_json_lossy(value)),
1273 )
1274 })
1275 .collect(),
1276 metadata: Vec::new(),
1277 node_links: Vec::new(),
1278 vector_links: Vec::new(),
1279 }
1280}
1281
1282fn bulk_insert_chunk_count(row_count: usize) -> usize {
1283 if row_count == 0 {
1284 0
1285 } else {
1286 ((row_count - 1) / STDIO_BULK_INSERT_CHUNK_ROWS) + 1
1287 }
1288}
1289
1290pub(crate) fn should_bulk_insert_graph(
1291 runtime: &RedDBRuntime,
1292 collection: &str,
1293 payloads: &[&json::Map<String, Value>],
1294) -> bool {
1295 let graph_shaped = payloads
1296 .iter()
1297 .all(|payload| payload.get("label").and_then(Value::as_str).is_some());
1298 if !graph_shaped {
1299 return false;
1300 }
1301
1302 matches!(
1303 runtime
1304 .db()
1305 .catalog_model_snapshot()
1306 .collections
1307 .iter()
1308 .find(|descriptor| descriptor.name == collection)
1309 .map(|descriptor| descriptor.declared_model.unwrap_or(descriptor.model)),
1310 Some(crate::catalog::CollectionModel::Graph | crate::catalog::CollectionModel::Mixed)
1311 )
1312}
1313
1314pub(crate) fn bulk_insert_graph(
1315 runtime: &RedDBRuntime,
1316 collection: &str,
1317 payloads: &[&json::Map<String, Value>],
1318) -> crate::RedDBResult<Value> {
1319 use crate::application::entity_payload::{parse_create_edge_input, parse_create_node_input};
1320 use crate::application::ports::RuntimeEntityPort;
1321
1322 let mut ids = Vec::with_capacity(payloads.len());
1323 for payload in payloads {
1324 let input_payload = normalize_flat_graph_payload(payload);
1325 let id = if payload.contains_key("from") || payload.contains_key("to") {
1326 runtime
1327 .create_edge(parse_create_edge_input(
1328 collection.to_string(),
1329 &input_payload,
1330 )?)?
1331 .id
1332 } else {
1333 runtime
1334 .create_node(parse_create_node_input(
1335 collection.to_string(),
1336 &input_payload,
1337 )?)?
1338 .id
1339 };
1340 ids.push(Value::Number(id.raw() as f64));
1341 }
1342
1343 let mut out = json::Map::new();
1344 out.insert("affected".to_string(), Value::Number(ids.len() as f64));
1345 out.insert("ids".to_string(), Value::Array(ids));
1346 Ok(Value::Object(out))
1347}
1348
1349fn normalize_flat_graph_payload(payload: &json::Map<String, Value>) -> Value {
1350 if payload.contains_key("properties") || payload.contains_key("fields") {
1351 return Value::Object(payload.clone());
1352 }
1353
1354 let is_edge = payload.contains_key("from") || payload.contains_key("to");
1355 let mut normalized = payload.clone();
1356 let mut properties = json::Map::new();
1357 for (key, value) in payload {
1358 let reserved = if is_edge {
1359 matches!(
1360 key.as_str(),
1361 "label"
1362 | "from"
1363 | "to"
1364 | "weight"
1365 | "metadata"
1366 | "properties"
1367 | "fields"
1368 | "_ttl_ms"
1369 | "_expires_at"
1370 )
1371 } else {
1372 matches!(
1373 key.as_str(),
1374 "label"
1375 | "node_type"
1376 | "metadata"
1377 | "links"
1378 | "embeddings"
1379 | "properties"
1380 | "fields"
1381 | "_ttl_ms"
1382 | "_expires_at"
1383 )
1384 };
1385 if !reserved {
1386 properties.insert(key.clone(), value.clone());
1387 }
1388 }
1389 if !properties.is_empty() {
1390 normalized.insert("properties".to_string(), Value::Object(properties));
1391 }
1392 Value::Object(normalized)
1393}
1394
1395pub(crate) fn value_to_sql_literal(v: &Value) -> String {
1396 match v {
1397 Value::Null => "NULL".to_string(),
1398 Value::Bool(b) => b.to_string(),
1399 Value::Integer(n) => n.to_string(),
1400 Value::Number(n) => {
1401 if n.fract() == 0.0 {
1402 format!("{}", *n as i64)
1403 } else {
1404 n.to_string()
1405 }
1406 }
1407 Value::String(s) => format!("'{}'", s.replace('\'', "''")),
1408 other => format!("'{}'", other.to_string_compact().replace('\'', "''")),
1409 }
1410}
1411
1412pub(crate) fn query_result_to_json(qr: &RuntimeQueryResult) -> Value {
1415 crate::presentation::query_result::tagged_summary(qr)
1416}
1417
1418pub(crate) fn insert_result_to_json(qr: &RuntimeQueryResult) -> Value {
1419 let mut envelope = json::Map::new();
1420 envelope.insert(
1421 "affected".to_string(),
1422 Value::Number(qr.affected_rows as f64),
1423 );
1424 if let Some(first) = qr.result.records.first() {
1428 if let Some(id_val) = first
1429 .iter_fields()
1430 .find(|(k, _)| {
1431 let s: &str = k;
1432 s == "rid"
1433 })
1434 .map(|(_, v)| schema_value_to_json(v))
1435 {
1436 envelope.insert("id".to_string(), id_val);
1437 }
1438 }
1439 Value::Object(envelope)
1440}
1441
1442fn record_to_json_object(record: &UnifiedRecord) -> Value {
1443 crate::presentation::query_result::tagged_record(record)
1444}
1445
1446fn schema_value_to_json(v: &SchemaValue) -> Value {
1447 crate::presentation::query_result::tagged_value(v)
1448}
1449
1450fn dispatch_method_remote(
1460 client: &AsyncMutex<RedDBClient>,
1461 tokio_rt: &tokio::runtime::Runtime,
1462 method: &str,
1463 params: &Value,
1464) -> Result<Value, (&'static str, String)> {
1465 match method {
1466 "version" => Ok(Value::Object(
1467 [
1468 (
1469 "version".to_string(),
1470 Value::String(env!("CARGO_PKG_VERSION").to_string()),
1471 ),
1472 (
1473 "protocol".to_string(),
1474 Value::String(PROTOCOL_VERSION.to_string()),
1475 ),
1476 ]
1477 .into_iter()
1478 .collect(),
1479 )),
1480
1481 "health" => {
1482 let result = tokio_rt.block_on(async {
1483 let mut guard = client.lock().await;
1484 guard.health_status().await
1485 });
1486 match result {
1487 Ok(status) => Ok(Value::Object(
1488 [
1489 ("ok".to_string(), Value::Bool(status.healthy)),
1490 ("state".to_string(), Value::String(status.state)),
1491 (
1492 "checked_at_unix_ms".to_string(),
1493 Value::Number(status.checked_at_unix_ms as f64),
1494 ),
1495 (
1496 "version".to_string(),
1497 Value::String(env!("CARGO_PKG_VERSION").to_string()),
1498 ),
1499 ]
1500 .into_iter()
1501 .collect(),
1502 )),
1503 Err(e) => Err((error_code::INTERNAL_ERROR, e.to_string())),
1504 }
1505 }
1506
1507 "query" => {
1508 let sql = params.get("sql").and_then(Value::as_str).ok_or((
1509 error_code::INVALID_PARAMS,
1510 "missing 'sql' string".to_string(),
1511 ))?;
1512 let json_str = tokio_rt
1513 .block_on(async {
1514 let mut guard = client.lock().await;
1515 guard.query(sql).await
1516 })
1517 .map_err(|e| (error_code::QUERY_ERROR, e.to_string()))?;
1518 let parsed = json::from_str::<Value>(&json_str)
1523 .map_err(|e| (error_code::INTERNAL_ERROR, format!("bad server JSON: {e}")))?;
1524 Ok(parsed)
1525 }
1526
1527 "insert" => {
1528 let collection = params.get("collection").and_then(Value::as_str).ok_or((
1529 error_code::INVALID_PARAMS,
1530 "missing 'collection' string".to_string(),
1531 ))?;
1532 let payload = params.get("payload").ok_or((
1533 error_code::INVALID_PARAMS,
1534 "missing 'payload' object".to_string(),
1535 ))?;
1536 if payload.as_object().is_none() {
1537 return Err((
1538 error_code::INVALID_PARAMS,
1539 "'payload' must be a JSON object".to_string(),
1540 ));
1541 }
1542 let payload_json = payload.to_string_compact();
1543 let reply = tokio_rt
1544 .block_on(async {
1545 let mut guard = client.lock().await;
1546 guard.create_row_entity(collection, &payload_json).await
1547 })
1548 .map_err(|e| (error_code::QUERY_ERROR, e.to_string()))?;
1549 let mut out = json::Map::new();
1550 out.insert("affected".to_string(), Value::Number(1.0));
1551 out.insert("id".to_string(), Value::String(reply.id.to_string()));
1552 Ok(Value::Object(out))
1553 }
1554
1555 "bulk_insert" => {
1556 let collection = params.get("collection").and_then(Value::as_str).ok_or((
1557 error_code::INVALID_PARAMS,
1558 "missing 'collection' string".to_string(),
1559 ))?;
1560 let payloads = params.get("payloads").and_then(Value::as_array).ok_or((
1561 error_code::INVALID_PARAMS,
1562 "missing 'payloads' array".to_string(),
1563 ))?;
1564 let mut encoded = Vec::with_capacity(payloads.len());
1565 for entry in payloads {
1566 if entry.as_object().is_none() {
1567 return Err((
1568 error_code::INVALID_PARAMS,
1569 "each payload must be a JSON object".to_string(),
1570 ));
1571 }
1572 encoded.push(entry.to_string_compact());
1573 }
1574 let status = tokio_rt
1575 .block_on(async {
1576 let mut guard = client.lock().await;
1577 guard.bulk_create_rows(collection, encoded).await
1578 })
1579 .map_err(|e| (error_code::QUERY_ERROR, e.to_string()))?;
1580 Ok(Value::Object(
1581 [
1582 ("affected".to_string(), Value::Number(status.count as f64)),
1583 (
1584 "ids".to_string(),
1585 Value::Array(
1586 status
1587 .items
1588 .into_iter()
1589 .map(|item| Value::Number(item.id as f64))
1590 .collect(),
1591 ),
1592 ),
1593 ]
1594 .into_iter()
1595 .collect(),
1596 ))
1597 }
1598
1599 "get" => {
1600 let collection = params.get("collection").and_then(Value::as_str).ok_or((
1601 error_code::INVALID_PARAMS,
1602 "missing 'collection' string".to_string(),
1603 ))?;
1604 let id = params.get("id").and_then(Value::as_str).ok_or((
1605 error_code::INVALID_PARAMS,
1606 "missing 'id' string".to_string(),
1607 ))?;
1608 ensure_sql_collection(collection).map_err(|m| (error_code::INVALID_PARAMS, m))?;
1609 let Some(rid) = rid_from_str(id) else {
1610 return Ok(Value::Object(
1611 [("entity".to_string(), Value::Null)].into_iter().collect(),
1612 ));
1613 };
1614 let sql = format!("SELECT * FROM {collection} WHERE rid = {rid} LIMIT 1");
1615 let json_str = tokio_rt
1616 .block_on(async {
1617 let mut guard = client.lock().await;
1618 guard.query(&sql).await
1619 })
1620 .map_err(|e| (error_code::QUERY_ERROR, e.to_string()))?;
1621 let parsed = json::from_str::<Value>(&json_str)
1622 .map_err(|e| (error_code::INTERNAL_ERROR, format!("bad server JSON: {e}")))?;
1623 let entity = parsed
1626 .get("rows")
1627 .and_then(Value::as_array)
1628 .and_then(|rows| rows.first().cloned())
1629 .unwrap_or(Value::Null);
1630 Ok(Value::Object(
1631 [("entity".to_string(), entity)].into_iter().collect(),
1632 ))
1633 }
1634
1635 "delete" => {
1636 let collection = params.get("collection").and_then(Value::as_str).ok_or((
1637 error_code::INVALID_PARAMS,
1638 "missing 'collection' string".to_string(),
1639 ))?;
1640 let id = params.get("id").and_then(Value::as_str).ok_or((
1641 error_code::INVALID_PARAMS,
1642 "missing 'id' string".to_string(),
1643 ))?;
1644 let id = id.parse::<u64>().map_err(|_| {
1645 (
1646 error_code::INVALID_PARAMS,
1647 "id must be a numeric string".to_string(),
1648 )
1649 })?;
1650 let _reply = tokio_rt
1651 .block_on(async {
1652 let mut guard = client.lock().await;
1653 guard.delete_entity(collection, id).await
1654 })
1655 .map_err(|e| (error_code::QUERY_ERROR, e.to_string()))?;
1656 Ok(Value::Object(
1657 [("affected".to_string(), Value::Number(1.0))]
1658 .into_iter()
1659 .collect(),
1660 ))
1661 }
1662
1663 "close" => Ok(Value::Null),
1664
1665 other => Err((
1666 error_code::INVALID_REQUEST,
1667 format!("unknown method: {other}"),
1668 )),
1669 }
1670}
1671
1672#[cfg(test)]
1673mod tests {
1674 use super::*;
1675 use crate::json::json;
1676 use crate::presentation::query_result::single_key_object;
1677 use proptest::prelude::*;
1678
1679 fn make_runtime() -> RedDBRuntime {
1680 RedDBRuntime::in_memory().expect("in-memory runtime")
1681 }
1682
1683 fn create_graph_collection(rt: &RedDBRuntime, name: &str) {
1684 let db = rt.db();
1685 db.store()
1686 .create_collection(name)
1687 .expect("create collection");
1688 let now = std::time::SystemTime::now()
1689 .duration_since(std::time::UNIX_EPOCH)
1690 .unwrap_or_default()
1691 .as_millis();
1692 db.save_collection_contract(crate::physical::CollectionContract {
1693 name: name.to_string(),
1694 description: None,
1695 description_generated: false,
1696 declared_model: crate::catalog::CollectionModel::Graph,
1697 schema_mode: crate::catalog::SchemaMode::Dynamic,
1698 origin: crate::physical::ContractOrigin::Explicit,
1699 version: 1,
1700 created_at_unix_ms: now,
1701 updated_at_unix_ms: now,
1702 default_ttl_ms: None,
1703 vector_dimension: None,
1704 vector_metric: None,
1705 context_index_fields: Vec::new(),
1706 declared_columns: Vec::new(),
1707 table_def: None,
1708 timestamps_enabled: false,
1709 context_index_enabled: false,
1710 metrics_raw_retention_ms: None,
1711 metrics_rollup_policies: Vec::new(),
1712 metrics_tenant_identity: None,
1713 metrics_namespace: None,
1714 append_only: false,
1715 subscriptions: Vec::new(),
1716 analytics_config: Vec::new(),
1717 session_key: None,
1718 session_gap_ms: None,
1719 retention_duration_ms: None,
1720 analytical_storage: None,
1721
1722 ai_policy: None,
1723 })
1724 .expect("save graph contract");
1725 }
1726
1727 fn handle(rt: &RedDBRuntime, line: &str) -> String {
1728 let mut session = Session::new();
1729 handle_line(&Backend::Local(rt), &mut session, line)
1730 }
1731
1732 fn query_request(id: u64, sql: &str) -> String {
1733 let mut params = json::Map::new();
1734 params.insert("sql".to_string(), Value::String(sql.to_string()));
1735
1736 let mut request = json::Map::new();
1737 request.insert("jsonrpc".to_string(), Value::String("2.0".to_string()));
1738 request.insert("id".to_string(), Value::Number(id as f64));
1739 request.insert("method".to_string(), Value::String("query".to_string()));
1740 request.insert("params".to_string(), Value::Object(params));
1741 Value::Object(request).to_string_compact()
1742 }
1743
1744 fn query_request_with_params(id: u64, sql: &str, binds: Vec<Value>) -> String {
1745 let mut params = json::Map::new();
1746 params.insert("sql".to_string(), Value::String(sql.to_string()));
1747 params.insert("params".to_string(), Value::Array(binds));
1748
1749 let mut request = json::Map::new();
1750 request.insert("jsonrpc".to_string(), Value::String("2.0".to_string()));
1751 request.insert("id".to_string(), Value::Number(id as f64));
1752 request.insert("method".to_string(), Value::String("query".to_string()));
1753 request.insert("params".to_string(), Value::Object(params));
1754 Value::Object(request).to_string_compact()
1755 }
1756
1757 fn with_session<F>(rt: &RedDBRuntime, f: F)
1760 where
1761 F: FnOnce(&dyn Fn(&str) -> String, &RedDBRuntime),
1762 {
1763 let session = std::cell::RefCell::new(Session::new());
1764 let call = |line: &str| -> String {
1765 let mut s = session.borrow_mut();
1766 handle_line(&Backend::Local(rt), &mut s, line)
1767 };
1768 f(&call, rt);
1769 }
1770
1771 fn result_rows(response: &str) -> Vec<Value> {
1772 json::from_str::<Value>(response)
1773 .expect("json response")
1774 .get("result")
1775 .and_then(|result| result.get("rows"))
1776 .and_then(Value::as_array)
1777 .map(|rows| rows.to_vec())
1778 .unwrap_or_default()
1779 }
1780
1781 fn result_name_kind(response: &str) -> Vec<(String, String)> {
1782 result_rows(response)
1783 .into_iter()
1784 .map(|row| {
1785 let object = row.as_object().expect("row object");
1786 let name = object
1787 .get("name")
1788 .and_then(Value::as_str)
1789 .expect("row name")
1790 .to_string();
1791 let kind = object
1792 .get("kind")
1793 .and_then(Value::as_str)
1794 .expect("row kind")
1795 .to_string();
1796 (name, kind)
1797 })
1798 .collect()
1799 }
1800
1801 fn json_scalar_param() -> impl Strategy<Value = Value> {
1802 prop_oneof![
1803 Just(Value::Null),
1804 any::<bool>().prop_map(Value::Bool),
1805 (-1000_i64..1000_i64).prop_map(|n| Value::Number(n as f64)),
1806 "[a-z']{0,8}".prop_map(Value::String),
1807 ]
1808 }
1809
1810 fn sql_literal_for_json(value: &Value) -> String {
1811 match value {
1812 Value::Null => "NULL".to_string(),
1813 Value::Bool(true) => "TRUE".to_string(),
1814 Value::Bool(false) => "FALSE".to_string(),
1815 Value::Number(n) => format!("{n:.0}"),
1816 Value::String(s) => format!("'{}'", s.replace('\'', "''")),
1817 _ => panic!("unsupported scalar param: {value:?}"),
1818 }
1819 }
1820
1821 #[test]
1822 fn version_method_returns_version_and_protocol() {
1823 let rt = make_runtime();
1824 let line = r#"{"jsonrpc":"2.0","id":1,"method":"version","params":{}}"#;
1825 let resp = handle(&rt, line);
1826 assert!(resp.contains("\"id\":1"));
1827 assert!(resp.contains("\"protocol\":\"1.0\""));
1828 assert!(resp.contains("\"version\""));
1829 }
1830
1831 #[test]
1832 fn health_method_returns_ok_true() {
1833 let rt = make_runtime();
1834 let resp = handle(
1835 &rt,
1836 r#"{"jsonrpc":"2.0","id":"abc","method":"health","params":{}}"#,
1837 );
1838 assert!(resp.contains("\"ok\":true"));
1839 assert!(resp.contains("\"id\":\"abc\""));
1840 }
1841
1842 #[test]
1843 fn parse_error_for_invalid_json() {
1844 let rt = make_runtime();
1845 let resp = handle(&rt, "not json {");
1846 assert!(resp.contains("\"code\":\"PARSE_ERROR\""));
1847 assert!(resp.contains("\"id\":null"));
1848 }
1849
1850 #[test]
1851 fn invalid_request_when_method_missing() {
1852 let rt = make_runtime();
1853 let resp = handle(&rt, r#"{"jsonrpc":"2.0","id":1,"params":{}}"#);
1854 assert!(resp.contains("\"code\":\"INVALID_REQUEST\""));
1855 }
1856
1857 #[test]
1858 fn unknown_method_is_invalid_request() {
1859 let rt = make_runtime();
1860 let resp = handle(
1861 &rt,
1862 r#"{"jsonrpc":"2.0","id":1,"method":"frobnicate","params":{}}"#,
1863 );
1864 assert!(resp.contains("\"code\":\"INVALID_REQUEST\""));
1865 assert!(resp.contains("frobnicate"));
1866 }
1867
1868 #[test]
1869 fn invalid_params_when_query_sql_missing() {
1870 let rt = make_runtime();
1871 let resp = handle(
1872 &rt,
1873 r#"{"jsonrpc":"2.0","id":1,"method":"query","params":{}}"#,
1874 );
1875 assert!(resp.contains("\"code\":\"INVALID_PARAMS\""));
1876 }
1877
1878 #[test]
1879 fn query_params_reject_superseded_exact_number_envelope() {
1880 let rt = make_runtime();
1881 let resp = handle(
1882 &rt,
1883 r#"{"jsonrpc":"2.0","id":1,"method":"query","params":{"sql":"SELECT $1","params":[{"$number":"1"}]}}"#,
1884 );
1885 assert!(resp.contains("\"code\":\"INVALID_PARAMS\""), "got: {resp}");
1886 assert!(
1887 resp.contains("superseded exact-number envelope"),
1888 "got: {resp}"
1889 );
1890 }
1891
1892 #[test]
1893 fn embedded_session_serves_the_stdio_protocol_without_a_process() {
1894 let rt = std::sync::Arc::new(make_runtime());
1895 let mut a = EmbeddedRpcSession::new(rt.clone());
1896 let mut b = EmbeddedRpcSession::new(rt);
1897
1898 let (resp, closed) = a.handle(r#"{"jsonrpc":"2.0","id":1,"method":"version","params":{}}"#);
1899 assert!(resp.contains("\"result\""), "got: {resp}");
1900 assert!(!closed);
1901
1902 let (resp, _) = a.handle(
1903 r#"{"jsonrpc":"2.0","id":2,"method":"query","params":{"sql":"CREATE TABLE emb (id INTEGER, name TEXT)"}}"#,
1904 );
1905 assert!(resp.contains("\"result\""), "got: {resp}");
1906 let (resp, _) = a.handle(
1907 r#"{"jsonrpc":"2.0","id":3,"method":"query","params":{"sql":"INSERT INTO emb (id, name) VALUES (1, 'x')"}}"#,
1908 );
1909 assert!(resp.contains("\"result\""), "got: {resp}");
1910
1911 let (resp, _) = b.handle(
1913 r#"{"jsonrpc":"2.0","id":4,"method":"query","params":{"sql":"SELECT id, name FROM emb"}}"#,
1914 );
1915 assert!(resp.contains("\"x\""), "got: {resp}");
1916
1917 let (resp, closed) = a.handle(r#"{"jsonrpc":"2.0","id":5,"method":"close","params":{}}"#);
1918 assert!(closed, "got: {resp}");
1919 }
1920
1921 #[test]
1922 fn embedded_session_transactions_are_scoped_to_the_session() {
1923 let rt = std::sync::Arc::new(make_runtime());
1924 let mut a = EmbeddedRpcSession::new(rt.clone());
1925 let mut b = EmbeddedRpcSession::new(rt);
1926 let _ = a.handle(
1927 r#"{"jsonrpc":"2.0","id":1,"method":"query","params":{"sql":"CREATE TABLE emb_tx (id INTEGER)"}}"#,
1928 );
1929
1930 let (begin, _) = a.handle(r#"{"jsonrpc":"2.0","id":2,"method":"tx.begin","params":{}}"#);
1931 assert!(begin.contains("\"result\""), "got: {begin}");
1932 let (begin_b, _) = b.handle(r#"{"jsonrpc":"2.0","id":3,"method":"tx.begin","params":{}}"#);
1934 assert!(begin_b.contains("\"result\""), "got: {begin_b}");
1935 let (again, _) = a.handle(r#"{"jsonrpc":"2.0","id":4,"method":"tx.begin","params":{}}"#);
1936 assert!(again.contains("TX_ALREADY_OPEN"), "got: {again}");
1937 }
1938
1939 #[test]
1940 fn close_method_marks_response_for_shutdown() {
1941 let rt = make_runtime();
1942 let resp = handle(
1943 &rt,
1944 r#"{"jsonrpc":"2.0","id":1,"method":"close","params":{}}"#,
1945 );
1946 assert!(resp.contains("\"__close__\":true"));
1947 }
1948
1949 #[test]
1950 fn query_with_int_text_params_round_trips() {
1951 let rt = make_runtime();
1952 let _ = handle(
1953 &rt,
1954 r#"{"jsonrpc":"2.0","id":1,"method":"query","params":{"sql":"CREATE TABLE p (id INTEGER, name TEXT)"}}"#,
1955 );
1956 let _ = handle(
1957 &rt,
1958 r#"{"jsonrpc":"2.0","id":2,"method":"query","params":{"sql":"INSERT INTO p (id, name) VALUES (1, 'Alice')"}}"#,
1959 );
1960 let _ = handle(
1961 &rt,
1962 r#"{"jsonrpc":"2.0","id":3,"method":"query","params":{"sql":"INSERT INTO p (id, name) VALUES (2, 'Bob')"}}"#,
1963 );
1964 let resp = handle(
1965 &rt,
1966 r#"{"jsonrpc":"2.0","id":4,"method":"query","params":{"sql":"SELECT * FROM p WHERE id = $1 AND name = $2","params":[1,"Alice"]}}"#,
1967 );
1968 assert!(resp.contains("\"Alice\""), "got: {resp}");
1969 assert!(!resp.contains("\"Bob\""), "got: {resp}");
1970 }
1971
1972 #[test]
1973 fn query_with_question_params_covers_select_insert_update_delete() {
1974 let rt = make_runtime();
1975 let create = handle(
1976 &rt,
1977 &query_request(1, "CREATE TABLE qp (id INTEGER, name TEXT)"),
1978 );
1979 assert!(!create.contains("\"error\""), "got: {create}");
1980
1981 let inserted = handle(
1982 &rt,
1983 &query_request_with_params(
1984 2,
1985 "INSERT INTO qp (id, name) VALUES (?, ?)",
1986 vec![json!(1), json!("O'Reilly")],
1987 ),
1988 );
1989 assert!(inserted.contains("\"affected\":1"), "got: {inserted}");
1990
1991 let selected = handle(
1992 &rt,
1993 &query_request_with_params(3, "SELECT name FROM qp WHERE id = ?", vec![json!(1)]),
1994 );
1995 let rows = result_rows(&selected);
1996 assert_eq!(rows.len(), 1, "got: {selected}");
1997 assert_eq!(
1998 rows[0].get("name").and_then(Value::as_str),
1999 Some("O'Reilly")
2000 );
2001
2002 let selected_numbered = handle(
2003 &rt,
2004 &query_request_with_params(
2005 4,
2006 "SELECT name FROM qp WHERE name = ?1 AND id = ?2",
2007 vec![json!("O'Reilly"), json!(1)],
2008 ),
2009 );
2010 assert_eq!(
2011 result_rows(&selected_numbered).len(),
2012 1,
2013 "got: {selected_numbered}"
2014 );
2015
2016 let updated = handle(
2017 &rt,
2018 &query_request_with_params(
2019 5,
2020 "UPDATE qp SET name = ? WHERE id = ?",
2021 vec![json!("Alice"), json!(1)],
2022 ),
2023 );
2024 assert!(updated.contains("\"affected\":1"), "got: {updated}");
2025
2026 let deleted = handle(
2027 &rt,
2028 &query_request_with_params(6, "DELETE FROM qp WHERE name = ?", vec![json!("Alice")]),
2029 );
2030 assert!(deleted.contains("\"affected\":1"), "got: {deleted}");
2031
2032 let remaining = handle(&rt, &query_request(7, "SELECT * FROM qp"));
2033 assert!(result_rows(&remaining).is_empty(), "got: {remaining}");
2034 }
2035
2036 #[test]
2037 fn query_with_params_insert_and_search_round_trip() {
2038 let rt = make_runtime();
2039 let insert = handle(
2040 &rt,
2041 r#"{"jsonrpc":"2.0","id":1,"method":"query","params":{"sql":"INSERT INTO bun_embeddings VECTOR (dense, content) VALUES ($1, $2)","params":[[1.0,0.0],"bun vector"]}}"#,
2042 );
2043 assert!(insert.contains("\"affected\":1"), "got: {insert}");
2044
2045 let search = handle(
2046 &rt,
2047 r#"{"jsonrpc":"2.0","id":2,"method":"query","params":{"sql":"SEARCH SIMILAR $1 COLLECTION bun_embeddings LIMIT 1","params":[[1.0,0.0]]}}"#,
2048 );
2049 assert!(search.contains("\"rows\""), "got: {search}");
2050 assert!(search.contains("\"score\":1"), "got: {search}");
2051 assert!(!search.contains("\"error\""), "got: {search}");
2052 }
2053
2054 #[test]
2055 fn query_with_question_vector_param_round_trips() {
2056 let rt = make_runtime();
2057 let insert = handle(
2058 &rt,
2059 &query_request_with_params(
2060 1,
2061 "INSERT INTO question_embeddings VECTOR (dense, content) VALUES (?, ?)",
2062 vec![json!([1.0, 0.0]), json!("question vector")],
2063 ),
2064 );
2065 assert!(insert.contains("\"affected\":1"), "got: {insert}");
2066
2067 let search = handle(
2068 &rt,
2069 &query_request_with_params(
2070 2,
2071 "SEARCH SIMILAR ? COLLECTION question_embeddings LIMIT 1",
2072 vec![json!([1.0, 0.0])],
2073 ),
2074 );
2075 assert!(search.contains("\"rows\""), "got: {search}");
2076 assert!(search.contains("\"score\":1"), "got: {search}");
2077 assert!(!search.contains("\"error\""), "got: {search}");
2078 }
2079
2080 #[test]
2081 fn query_with_typed_json_rpc_params_round_trips() {
2082 let rt = make_runtime();
2083 let create = handle(
2084 &rt,
2085 r#"{"jsonrpc":"2.0","id":1,"method":"query","params":{"sql":"CREATE TABLE value_params (ok BOOLEAN, score FLOAT, payload BLOB, body JSON, seen_at TIMESTAMP, ident UUID)"}}"#,
2086 );
2087 assert!(!create.contains("\"error\""), "got: {create}");
2088
2089 let insert = handle(
2090 &rt,
2091 r#"{"jsonrpc":"2.0","id":2,"method":"query","params":{"sql":"INSERT INTO value_params (ok, score, payload, body, seen_at, ident) VALUES ($1, $2, $3, $4, $5, $6)","params":[true,{"$float":"NaN"},{"$bytes":"3q2+7w=="},{"z":[1,{"a":true}],"a":null},{"$ts":"1700000000123456789"},{"$uuid":"00112233-4455-6677-8899-aabbccddeeff"}]}}"#,
2092 );
2093 assert!(insert.contains("\"affected\":1"), "got: {insert}");
2094
2095 let selected = handle(
2096 &rt,
2097 r#"{"jsonrpc":"2.0","id":3,"method":"query","params":{"sql":"SELECT * FROM value_params"}}"#,
2098 );
2099 assert!(selected.contains("\"ok\":true"), "got: {selected}");
2100 assert!(selected.contains("\"$float\":\"NaN\""), "got: {selected}");
2101 assert!(
2102 selected.contains("\"$bytes\":\"3q2+7w==\""),
2103 "got: {selected}"
2104 );
2105 assert!(
2106 selected.contains("\"body\":{\"a\":null,\"z\":[1,{\"a\":true}]}"),
2107 "got: {selected}"
2108 );
2109 assert!(
2110 selected.contains("\"$ts\":\"1700000000123456789\""),
2111 "got: {selected}"
2112 );
2113 assert!(
2114 selected.contains("\"$uuid\":\"00112233-4455-6677-8899-aabbccddeeff\""),
2115 "got: {selected}"
2116 );
2117 }
2118
2119 #[test]
2120 fn select_timeseries_tags_decodes_json_payload() {
2121 let rt = make_runtime();
2122 let create = handle(&rt, &query_request(1, "CREATE TIMESERIES ts1"));
2123 assert!(!create.contains("\"error\""), "got: {create}");
2124
2125 let insert = handle(
2126 &rt,
2127 &query_request(
2128 2,
2129 r#"INSERT INTO ts1 (metric, value, tags, timestamp) VALUES ('cpu', 85, '{"host":"a"}', 1000)"#,
2130 ),
2131 );
2132 assert!(insert.contains("\"affected\":1"), "got: {insert}");
2133
2134 let selected = handle(&rt, &query_request(3, "SELECT tags FROM ts1"));
2135 assert!(!selected.contains("<json"), "got: {selected}");
2136 let response = json::from_str::<Value>(&selected).expect("response json");
2137 let tags = response
2138 .get("result")
2139 .and_then(|result| result.get("rows"))
2140 .and_then(Value::as_array)
2141 .and_then(|rows| rows.first())
2142 .and_then(|row| row.get("tags"))
2143 .expect("tags field");
2144 assert_eq!(tags, &json!({"host": "a"}));
2145 }
2146
2147 #[test]
2148 fn select_table_json_column_round_trips_after_single_parse() {
2149 let rt = make_runtime();
2150 let create = handle(&rt, &query_request(1, "CREATE TABLE docs (payload JSON)"));
2151 assert!(!create.contains("\"error\""), "got: {create}");
2152
2153 let original = r#"{"nested":{"items":[1,true,"x"],"object":{"k":"v"}}}"#;
2154 let insert_sql = format!("INSERT INTO docs (payload) VALUES ({original})");
2155 let insert = handle(&rt, &query_request(2, &insert_sql));
2156 assert!(insert.contains("\"affected\":1"), "got: {insert}");
2157
2158 let selected = handle(&rt, &query_request(3, "SELECT payload FROM docs"));
2159 assert!(!selected.contains("<json"), "got: {selected}");
2160 let response = json::from_str::<Value>(&selected).expect("response json");
2161 let payload = response
2162 .get("result")
2163 .and_then(|result| result.get("rows"))
2164 .and_then(Value::as_array)
2165 .and_then(|rows| rows.first())
2166 .and_then(|row| row.get("payload"))
2167 .expect("payload field");
2168 let expected = json::from_str::<Value>(original).expect("expected json");
2169 assert_eq!(payload, &expected);
2170
2171 let payload_text = payload.to_string_compact();
2172 assert_eq!(
2173 json::from_str::<Value>(&payload_text).expect("single parse"),
2174 expected
2175 );
2176 }
2177
2178 #[test]
2179 fn select_json_corruption_falls_back_to_code_and_hex() {
2180 use crate::storage::query::unified::UnifiedResult;
2181
2182 let mut result = UnifiedResult::with_columns(vec!["payload".into()]);
2183 let mut record = UnifiedRecord::new();
2184 record.set("payload", SchemaValue::Json(b"{not json".to_vec()));
2185 result.push(record);
2186
2187 let json = query_result_to_json(&RuntimeQueryResult {
2188 query: "SELECT payload FROM docs".to_string(),
2189 mode: reddb_rql::modes::QueryMode::Sql,
2190 statement: "select",
2191 engine: "runtime-table",
2192 result,
2193 affected_rows: 0,
2194 statement_type: "select",
2195 bookmark: None,
2196 notice: None,
2197 });
2198
2199 let payload = json
2200 .get("rows")
2201 .and_then(Value::as_array)
2202 .and_then(|rows| rows.first())
2203 .and_then(|row| row.get("payload"))
2204 .expect("payload field");
2205 assert_eq!(
2206 payload.get("code").and_then(Value::as_str),
2207 Some("INVALID_JSON")
2208 );
2209 assert_eq!(
2210 payload.get("hex").and_then(Value::as_str),
2211 Some("7b6e6f74206a736f6e")
2212 );
2213 }
2214
2215 #[test]
2216 fn query_result_json_preserves_exact_numbers() {
2217 use crate::storage::query::unified::UnifiedResult;
2218
2219 let mut result =
2220 UnifiedResult::with_columns(vec!["big".into(), "unsigned".into(), "decimal".into()]);
2221 let mut record = UnifiedRecord::new();
2222 record.set("big", SchemaValue::Integer(9_007_199_254_740_993));
2223 record.set(
2224 "unsigned",
2225 SchemaValue::UnsignedInteger(i64::MAX as u64 + 1),
2226 );
2227 record.set(
2228 "decimal",
2229 SchemaValue::DecimalText("3.14159265358979323846".to_string()),
2230 );
2231 result.push(record);
2232
2233 let json = query_result_to_json(&RuntimeQueryResult {
2234 query: "SELECT exact".to_string(),
2235 mode: reddb_rql::modes::QueryMode::Sql,
2236 statement: "select",
2237 engine: "runtime-table",
2238 result,
2239 affected_rows: 0,
2240 statement_type: "select",
2241 bookmark: None,
2242 notice: None,
2243 });
2244
2245 let row = json
2246 .get("rows")
2247 .and_then(Value::as_array)
2248 .and_then(|rows| rows.first())
2249 .expect("row");
2250 assert_eq!(
2251 row.get("big"),
2252 Some(&single_key_object(
2253 "$int",
2254 Value::String("9007199254740993".to_string())
2255 ))
2256 );
2257 assert_eq!(
2258 row.get("unsigned"),
2259 Some(&single_key_object(
2260 "$uint",
2261 Value::String("9223372036854775808".to_string())
2262 ))
2263 );
2264 assert_eq!(
2265 row.get("decimal"),
2266 Some(&single_key_object(
2267 "$decimal",
2268 Value::String("3.14159265358979323846".to_string())
2269 ))
2270 );
2271 }
2272
2273 #[test]
2274 fn query_with_params_arity_mismatch_rejected() {
2275 let rt = make_runtime();
2276 let _ = handle(
2277 &rt,
2278 r#"{"jsonrpc":"2.0","id":1,"method":"query","params":{"sql":"CREATE TABLE pa (id INTEGER)"}}"#,
2279 );
2280 let resp = handle(
2281 &rt,
2282 r#"{"jsonrpc":"2.0","id":2,"method":"query","params":{"sql":"SELECT * FROM pa WHERE id = $1","params":[1,2]}}"#,
2283 );
2284 assert!(resp.contains("\"INVALID_PARAMS\""), "got: {resp}");
2285 }
2286
2287 #[test]
2288 fn query_with_question_params_arity_mismatch_rejected() {
2289 let rt = make_runtime();
2290 let _ = handle(&rt, &query_request(1, "CREATE TABLE qpa (id INTEGER)"));
2291 let resp = handle(
2292 &rt,
2293 &query_request_with_params(
2294 2,
2295 "SELECT * FROM qpa WHERE id = ?",
2296 vec![json!(1), json!(2)],
2297 ),
2298 );
2299 assert!(resp.contains("\"INVALID_PARAMS\""), "got: {resp}");
2300 assert!(resp.contains("SQL expects 1, got 2"), "got: {resp}");
2301 }
2302
2303 #[test]
2304 fn query_with_params_gap_rejected() {
2305 let rt = make_runtime();
2306 let _ = handle(
2307 &rt,
2308 r#"{"jsonrpc":"2.0","id":1,"method":"query","params":{"sql":"CREATE TABLE pg (a INTEGER, b INTEGER)"}}"#,
2309 );
2310 let resp = handle(
2311 &rt,
2312 r#"{"jsonrpc":"2.0","id":2,"method":"query","params":{"sql":"SELECT * FROM pg WHERE a = $1 AND b = $3","params":[1,2,3]}}"#,
2313 );
2314 assert!(resp.contains("\"INVALID_PARAMS\""), "got: {resp}");
2315 }
2316
2317 #[test]
2318 fn query_with_question_numbered_gap_rejected() {
2319 let rt = make_runtime();
2320 let _ = handle(&rt, &query_request(1, "CREATE TABLE qpg (id INTEGER)"));
2321 let resp = handle(
2322 &rt,
2323 &query_request_with_params(
2324 2,
2325 "SELECT * FROM qpg WHERE id = ?2",
2326 vec![json!(1), json!(2)],
2327 ),
2328 );
2329 assert!(resp.contains("\"INVALID_PARAMS\""), "got: {resp}");
2330 assert!(resp.contains("parameter $`1` is missing"), "got: {resp}");
2331 }
2332
2333 #[test]
2334 fn query_with_question_params_type_mismatch_names_slot() {
2335 let rt = make_runtime();
2336 let _ = handle(&rt, &query_request(1, "CREATE TABLE qpt (id INTEGER)"));
2337 let resp = handle(
2338 &rt,
2339 &query_request_with_params(
2340 2,
2341 "INSERT INTO qpt (id) VALUES (?)",
2342 vec![json!("not-an-integer")],
2343 ),
2344 );
2345 assert!(resp.contains("\"QUERY_ERROR\""), "got: {resp}");
2346 assert!(resp.contains("id"), "got: {resp}");
2347 assert!(resp.contains("integer"), "got: {resp}");
2348 }
2349
2350 #[test]
2351 fn query_select_one_returns_rows() {
2352 let rt = make_runtime();
2353 let resp = handle(
2354 &rt,
2355 r#"{"jsonrpc":"2.0","id":1,"method":"query","params":{"sql":"SELECT 1 AS one"}}"#,
2356 );
2357 assert!(resp.contains("\"result\""));
2358 assert!(!resp.contains("\"error\""));
2359 }
2360
2361 #[test]
2362 fn ask_query_result_uses_canonical_envelope() {
2363 use crate::storage::query::unified::UnifiedResult;
2364
2365 let mut result = UnifiedResult::with_columns(vec![
2366 "answer".into(),
2367 "provider".into(),
2368 "model".into(),
2369 "prompt_tokens".into(),
2370 "completion_tokens".into(),
2371 "sources_count".into(),
2372 "sources_flat".into(),
2373 "citations".into(),
2374 "validation".into(),
2375 ]);
2376 let mut record = UnifiedRecord::new();
2377 record.set("answer", SchemaValue::text("Deploy failed [^1]."));
2378 record.set("provider", SchemaValue::text("openai"));
2379 record.set("model", SchemaValue::text("gpt-4o-mini"));
2380 record.set("prompt_tokens", SchemaValue::Integer(11));
2381 record.set("completion_tokens", SchemaValue::Integer(7));
2382 record.set(
2383 "sources_flat",
2384 SchemaValue::Json(
2385 br#"[{"urn":"urn:reddb:row:deployments:1","kind":"row","collection":"deployments","id":"1"}]"#.to_vec(),
2386 ),
2387 );
2388 record.set(
2389 "citations",
2390 SchemaValue::Json(br#"[{"marker":1,"urn":"urn:reddb:row:deployments:1"}]"#.to_vec()),
2391 );
2392 record.set(
2393 "validation",
2394 SchemaValue::Json(br#"{"ok":true,"warnings":[],"errors":[]}"#.to_vec()),
2395 );
2396 result.push(record);
2397
2398 let json = query_result_to_json(&RuntimeQueryResult {
2399 query: "ASK 'why did deploy fail?'".to_string(),
2400 mode: reddb_rql::modes::QueryMode::Sql,
2401 statement: "ask",
2402 engine: "runtime-ai",
2403 result,
2404 affected_rows: 0,
2405 statement_type: "select",
2406 bookmark: None,
2407 notice: None,
2408 });
2409
2410 assert_eq!(
2411 json.get("answer").and_then(Value::as_str),
2412 Some("Deploy failed [^1].")
2413 );
2414 assert_eq!(json.get("cache_hit").and_then(Value::as_bool), Some(false));
2415 assert_eq!(json.get("cost_usd").and_then(Value::as_f64), Some(0.0));
2416 assert_eq!(json.get("mode").and_then(Value::as_str), Some("strict"));
2417 assert_eq!(json.get("retry_count").and_then(Value::as_u64), Some(0));
2418 assert!(
2419 json.get("rows").is_none(),
2420 "ASK envelope must not be row-wrapped: {json}"
2421 );
2422 assert!(
2423 json.get("sources_flat")
2424 .and_then(Value::as_array)
2425 .is_some_and(|sources| sources.len() == 1
2426 && sources[0].get("payload").and_then(Value::as_str).is_some()),
2427 "sources_flat must be a parsed array: {json}"
2428 );
2429 assert!(
2430 json.get("citations")
2431 .and_then(Value::as_array)
2432 .is_some_and(|citations| citations.len() == 1),
2433 "citations must be a parsed array: {json}"
2434 );
2435 assert_eq!(
2436 json.get("validation")
2437 .and_then(|v| v.get("ok"))
2438 .and_then(Value::as_bool),
2439 Some(true)
2440 );
2441 }
2442
2443 #[test]
2448 fn tx_begin_returns_tx_id_and_isolation() {
2449 let rt = make_runtime();
2450 with_session(&rt, |call, _| {
2451 let resp = call(r#"{"jsonrpc":"2.0","id":1,"method":"tx.begin","params":null}"#);
2452 assert!(resp.contains("\"tx_id\":1"));
2453 assert!(resp.contains("\"isolation\":\"read_committed_deferred\""));
2454 assert!(!resp.contains("\"error\""));
2455 });
2456 }
2457
2458 #[test]
2459 fn tx_begin_twice_returns_already_open() {
2460 let rt = make_runtime();
2461 with_session(&rt, |call, _| {
2462 let _ = call(r#"{"jsonrpc":"2.0","id":1,"method":"tx.begin","params":null}"#);
2463 let resp = call(r#"{"jsonrpc":"2.0","id":2,"method":"tx.begin","params":null}"#);
2464 assert!(resp.contains("\"code\":\"TX_ALREADY_OPEN\""));
2465 });
2466 }
2467
2468 #[test]
2469 fn tx_commit_without_begin_returns_no_tx_open() {
2470 let rt = make_runtime();
2471 with_session(&rt, |call, _| {
2472 let resp = call(r#"{"jsonrpc":"2.0","id":1,"method":"tx.commit","params":null}"#);
2473 assert!(resp.contains("\"code\":\"NO_TX_OPEN\""));
2474 });
2475 }
2476
2477 #[test]
2478 fn tx_rollback_without_begin_returns_no_tx_open() {
2479 let rt = make_runtime();
2480 with_session(&rt, |call, _| {
2481 let resp = call(r#"{"jsonrpc":"2.0","id":1,"method":"tx.rollback","params":null}"#);
2482 assert!(resp.contains("\"code\":\"NO_TX_OPEN\""));
2483 });
2484 }
2485
2486 #[test]
2487 fn insert_inside_tx_returns_pending_envelope() {
2488 let rt = make_runtime();
2489 let _ = handle(
2491 &rt,
2492 r#"{"jsonrpc":"2.0","id":1,"method":"query","params":{"sql":"CREATE TABLE users (name TEXT)"}}"#,
2493 );
2494 with_session(&rt, |call, _| {
2495 let _ = call(r#"{"jsonrpc":"2.0","id":1,"method":"tx.begin","params":null}"#);
2496 let resp = call(
2497 r#"{"jsonrpc":"2.0","id":2,"method":"insert","params":{"collection":"users","payload":{"name":"alice"}}}"#,
2498 );
2499 assert!(resp.contains("\"pending\":true"));
2500 assert!(resp.contains("\"tx_id\":1"));
2501 assert!(resp.contains("\"affected\":0"));
2502 });
2503 }
2504
2505 #[test]
2506 fn begin_insert_rollback_does_not_persist() {
2507 let rt = make_runtime();
2508 let _ = handle(
2509 &rt,
2510 r#"{"jsonrpc":"2.0","id":1,"method":"query","params":{"sql":"CREATE TABLE u (name TEXT)"}}"#,
2511 );
2512 with_session(&rt, |call, _| {
2513 let _ = call(r#"{"jsonrpc":"2.0","id":1,"method":"tx.begin","params":null}"#);
2514 let _ = call(
2515 r#"{"jsonrpc":"2.0","id":2,"method":"insert","params":{"collection":"u","payload":{"name":"ghost"}}}"#,
2516 );
2517 let rollback = call(r#"{"jsonrpc":"2.0","id":3,"method":"tx.rollback","params":null}"#);
2518 assert!(rollback.contains("\"ops_discarded\":1"));
2519 assert!(rollback.contains("\"tx_id\":1"));
2520 });
2521 let resp = handle(
2523 &rt,
2524 r#"{"jsonrpc":"2.0","id":9,"method":"query","params":{"sql":"SELECT * FROM u"}}"#,
2525 );
2526 assert!(!resp.contains("\"ghost\""));
2527 }
2528
2529 #[test]
2530 fn begin_insert_commit_persists() {
2531 let rt = make_runtime();
2532 let _ = handle(
2533 &rt,
2534 r#"{"jsonrpc":"2.0","id":1,"method":"query","params":{"sql":"CREATE TABLE u2 (name TEXT)"}}"#,
2535 );
2536 with_session(&rt, |call, _| {
2537 let _ = call(r#"{"jsonrpc":"2.0","id":1,"method":"tx.begin","params":null}"#);
2538 let _ = call(
2539 r#"{"jsonrpc":"2.0","id":2,"method":"insert","params":{"collection":"u2","payload":{"name":"alice"}}}"#,
2540 );
2541 let _ = call(
2542 r#"{"jsonrpc":"2.0","id":3,"method":"insert","params":{"collection":"u2","payload":{"name":"bob"}}}"#,
2543 );
2544 let commit = call(r#"{"jsonrpc":"2.0","id":4,"method":"tx.commit","params":null}"#);
2545 assert!(commit.contains("\"ops_replayed\":2"));
2546 assert!(!commit.contains("\"error\""));
2547 });
2548 let resp = handle(
2549 &rt,
2550 r#"{"jsonrpc":"2.0","id":9,"method":"query","params":{"sql":"SELECT * FROM u2"}}"#,
2551 );
2552 assert!(resp.contains("\"alice\""));
2553 assert!(resp.contains("\"bob\""));
2554 }
2555
2556 #[test]
2557 fn bulk_insert_inside_tx_buffers_everything() {
2558 let rt = make_runtime();
2559 let _ = handle(
2560 &rt,
2561 r#"{"jsonrpc":"2.0","id":1,"method":"query","params":{"sql":"CREATE TABLE u3 (name TEXT)"}}"#,
2562 );
2563 with_session(&rt, |call, _| {
2564 let _ = call(r#"{"jsonrpc":"2.0","id":1,"method":"tx.begin","params":null}"#);
2565 let resp = call(
2566 r#"{"jsonrpc":"2.0","id":2,"method":"bulk_insert","params":{"collection":"u3","payloads":[{"name":"a"},{"name":"b"},{"name":"c"}]}}"#,
2567 );
2568 assert!(resp.contains("\"buffered\":3"));
2569 assert!(resp.contains("\"pending\":true"));
2570 assert!(resp.contains("\"affected\":0"));
2571
2572 let commit = call(r#"{"jsonrpc":"2.0","id":3,"method":"tx.commit","params":null}"#);
2573 assert!(commit.contains("\"ops_replayed\":3"));
2574 });
2575 }
2576
2577 #[test]
2578 fn bulk_insert_chunks_at_internal_500_row_limit() {
2579 assert_eq!(bulk_insert_chunk_count(0), 0);
2580 assert_eq!(bulk_insert_chunk_count(1), 1);
2581 assert_eq!(bulk_insert_chunk_count(500), 1);
2582 assert_eq!(bulk_insert_chunk_count(501), 2);
2583 assert_eq!(bulk_insert_chunk_count(1000), 2);
2584 assert_eq!(bulk_insert_chunk_count(1001), 3);
2585 }
2586
2587 proptest! {
2588 #![proptest_config(ProptestConfig {
2589 cases: 12,
2590 ..ProptestConfig::default()
2591 })]
2592
2593 #[test]
2594 fn bulk_insert_matches_sequential_insert_state(
2595 names in proptest::collection::vec("[a-z]{1,8}", 1usize..20)
2596 ) {
2597 let rt = make_runtime();
2598 let payloads = names
2599 .iter()
2600 .map(|name| format!(r#"{{"name":"{name}","kind":"bulk"}}"#))
2601 .collect::<Vec<_>>();
2602 let payload_array = payloads.join(",");
2603
2604 let bulk = handle(
2605 &rt,
2606 &format!(
2607 r#"{{"jsonrpc":"2.0","id":1,"method":"bulk_insert","params":{{"collection":"bulk_prop","payloads":[{payload_array}]}}}}"#
2608 ),
2609 );
2610 let bulk_result = json::from_str::<Value>(&bulk).expect("bulk json");
2611 let bulk_ids = bulk_result
2612 .get("result")
2613 .and_then(|result| result.get("ids"))
2614 .and_then(Value::as_array)
2615 .expect("bulk ids");
2616 prop_assert_eq!(bulk_ids.len(), names.len());
2617
2618 for (index, payload) in payloads.iter().enumerate() {
2619 let insert = handle(
2620 &rt,
2621 &format!(
2622 r#"{{"jsonrpc":"2.0","id":{},"method":"insert","params":{{"collection":"seq_prop","payload":{payload}}}}}"#,
2623 index + 10
2624 ),
2625 );
2626 let insert_result = json::from_str::<Value>(&insert).expect("insert json");
2627 prop_assert!(
2628 insert_result
2629 .get("result")
2630 .and_then(|result| result.get("id"))
2631 .is_some(),
2632 "insert response missing id: {insert}"
2633 );
2634 }
2635
2636 let bulk_rows = result_name_kind(&handle(
2637 &rt,
2638 r#"{"jsonrpc":"2.0","id":99,"method":"query","params":{"sql":"SELECT name, kind FROM bulk_prop ORDER BY rid"}}"#,
2639 ));
2640 let seq_rows = result_name_kind(&handle(
2641 &rt,
2642 r#"{"jsonrpc":"2.0","id":100,"method":"query","params":{"sql":"SELECT name, kind FROM seq_prop ORDER BY rid"}}"#,
2643 ));
2644 prop_assert_eq!(bulk_rows, seq_rows);
2645 }
2646
2647 #[test]
2648 fn question_param_select_matches_inlined_literal(value in json_scalar_param()) {
2649 let rt = make_runtime();
2650 let bound = handle(
2651 &rt,
2652 &query_request_with_params(1, "SELECT ? AS v", vec![value.clone()]),
2653 );
2654 let inline_sql = format!("SELECT {} AS v", sql_literal_for_json(&value));
2655 let inlined = handle(&rt, &query_request(2, &inline_sql));
2656 prop_assert_eq!(
2657 result_rows(&bound),
2658 result_rows(&inlined),
2659 "bound={}, inlined={}",
2660 bound,
2661 inlined
2662 );
2663 }
2664 }
2665
2666 #[test]
2667 fn bulk_insert_graph_nodes_accepts_flat_rows_and_returns_ids() {
2668 let rt = make_runtime();
2669 create_graph_collection(&rt, "social");
2670
2671 let resp = handle(
2672 &rt,
2673 r#"{"jsonrpc":"2.0","id":2,"method":"bulk_insert","params":{"collection":"social","payloads":[{"label":"User","name":"alice"},{"label":"User","name":"bob"}]}}"#,
2674 );
2675 let envelope: Value = json::from_str(&resp).expect("json response");
2676 let result = envelope.get("result").expect("result");
2677 assert_eq!(result.get("affected").and_then(Value::as_u64), Some(2));
2678 assert_eq!(
2679 result
2680 .get("ids")
2681 .and_then(Value::as_array)
2682 .map(|ids| ids.len()),
2683 Some(2)
2684 );
2685
2686 let query = handle(
2687 &rt,
2688 r#"{"jsonrpc":"2.0","id":3,"method":"query","params":{"sql":"MATCH (n:User) RETURN n.name"}}"#,
2689 );
2690 assert!(query.contains("\"alice\""), "got: {query}");
2691 assert!(query.contains("\"bob\""), "got: {query}");
2692 }
2693
2694 #[test]
2695 fn bulk_insert_graph_edges_accepts_flat_rows_and_returns_ids() {
2696 let rt = make_runtime();
2697 create_graph_collection(&rt, "network");
2698 let nodes = handle(
2699 &rt,
2700 r#"{"jsonrpc":"2.0","id":2,"method":"bulk_insert","params":{"collection":"network","payloads":[{"label":"Host","name":"app"},{"label":"Host","name":"db"}]}}"#,
2701 );
2702 let envelope: Value = json::from_str(&nodes).expect("node response");
2703 let ids = envelope
2704 .get("result")
2705 .and_then(|r| r.get("ids"))
2706 .and_then(Value::as_array)
2707 .expect("node ids");
2708 let from = ids[0].as_u64().expect("from id");
2709 let to = ids[1].as_u64().expect("to id");
2710
2711 let resp = handle(
2712 &rt,
2713 &format!(
2714 r#"{{"jsonrpc":"2.0","id":3,"method":"bulk_insert","params":{{"collection":"network","payloads":[{{"label":"connects","from":{from},"to":{to},"weight":0.5,"role":"primary"}}]}}}}"#
2715 ),
2716 );
2717 let envelope: Value = json::from_str(&resp).expect("edge response");
2718 let result = envelope.get("result").expect("result");
2719 assert_eq!(result.get("affected").and_then(Value::as_u64), Some(1));
2720 assert_eq!(
2721 result
2722 .get("ids")
2723 .and_then(Value::as_array)
2724 .map(|ids| ids.len()),
2725 Some(1)
2726 );
2727 }
2728
2729 #[test]
2730 fn delete_inside_tx_is_buffered() {
2731 let rt = make_runtime();
2732 let _ = handle(
2734 &rt,
2735 r#"{"jsonrpc":"2.0","id":1,"method":"query","params":{"sql":"CREATE TABLE u4 (name TEXT)"}}"#,
2736 );
2737 let _ = handle(
2738 &rt,
2739 r#"{"jsonrpc":"2.0","id":2,"method":"query","params":{"sql":"INSERT INTO u4 (name) VALUES ('keep')"}}"#,
2740 );
2741 with_session(&rt, |call, _| {
2742 let _ = call(r#"{"jsonrpc":"2.0","id":1,"method":"tx.begin","params":null}"#);
2743 let resp = call(
2744 r#"{"jsonrpc":"2.0","id":2,"method":"delete","params":{"collection":"u4","id":"1"}}"#,
2745 );
2746 assert!(resp.contains("\"pending\":true"));
2747 let _ = call(r#"{"jsonrpc":"2.0","id":3,"method":"tx.rollback","params":null}"#);
2748 });
2749 let resp = handle(
2751 &rt,
2752 r#"{"jsonrpc":"2.0","id":9,"method":"query","params":{"sql":"SELECT * FROM u4"}}"#,
2753 );
2754 assert!(resp.contains("\"keep\""));
2755 }
2756
2757 #[test]
2758 fn close_with_open_tx_auto_rollbacks() {
2759 let rt = make_runtime();
2760 let _ = handle(
2761 &rt,
2762 r#"{"jsonrpc":"2.0","id":1,"method":"query","params":{"sql":"CREATE TABLE u5 (name TEXT)"}}"#,
2763 );
2764 with_session(&rt, |call, _| {
2765 let _ = call(r#"{"jsonrpc":"2.0","id":1,"method":"tx.begin","params":null}"#);
2766 let _ = call(
2767 r#"{"jsonrpc":"2.0","id":2,"method":"insert","params":{"collection":"u5","payload":{"name":"ghost"}}}"#,
2768 );
2769 let close = call(r#"{"jsonrpc":"2.0","id":3,"method":"close","params":null}"#);
2770 assert!(close.contains("\"__close__\":true"));
2771 assert!(!close.contains("\"error\""));
2772 });
2773 let resp = handle(
2774 &rt,
2775 r#"{"jsonrpc":"2.0","id":9,"method":"query","params":{"sql":"SELECT * FROM u5"}}"#,
2776 );
2777 assert!(!resp.contains("\"ghost\""));
2778 }
2779
2780 fn seed_numbers_table(rt: &RedDBRuntime, table: &str, count: u32) {
2785 let _ = handle(
2786 rt,
2787 &format!(
2788 r#"{{"jsonrpc":"2.0","id":1,"method":"query","params":{{"sql":"CREATE TABLE {table} (n INTEGER)"}}}}"#,
2789 ),
2790 );
2791 for i in 0..count {
2792 let _ = handle(
2793 rt,
2794 &format!(
2795 r#"{{"jsonrpc":"2.0","id":2,"method":"query","params":{{"sql":"INSERT INTO {table} (n) VALUES ({i})"}}}}"#,
2796 ),
2797 );
2798 }
2799 }
2800
2801 #[test]
2802 fn cursor_open_returns_id_columns_and_total() {
2803 let rt = make_runtime();
2804 seed_numbers_table(&rt, "nums1", 3);
2805 with_session(&rt, |call, _| {
2806 let resp = call(
2807 r#"{"jsonrpc":"2.0","id":1,"method":"query.open","params":{"sql":"SELECT n FROM nums1"}}"#,
2808 );
2809 assert!(resp.contains("\"cursor_id\":1"));
2810 assert!(resp.contains("\"total_rows\":3"));
2811 assert!(resp.contains("\"columns\""));
2812 assert!(!resp.contains("\"error\""));
2813 });
2814 }
2815
2816 #[test]
2817 fn cursor_next_chunks_rows_and_signals_done() {
2818 let rt = make_runtime();
2819 seed_numbers_table(&rt, "nums2", 5);
2820 with_session(&rt, |call, _| {
2821 let _ = call(
2822 r#"{"jsonrpc":"2.0","id":1,"method":"query.open","params":{"sql":"SELECT n FROM nums2"}}"#,
2823 );
2824 let first = call(
2825 r#"{"jsonrpc":"2.0","id":2,"method":"query.next","params":{"cursor_id":1,"batch_size":2}}"#,
2826 );
2827 assert!(first.contains("\"done\":false"));
2828 assert!(first.contains("\"remaining\":3"));
2829
2830 let second = call(
2831 r#"{"jsonrpc":"2.0","id":3,"method":"query.next","params":{"cursor_id":1,"batch_size":2}}"#,
2832 );
2833 assert!(second.contains("\"done\":false"));
2834 assert!(second.contains("\"remaining\":1"));
2835
2836 let third = call(
2837 r#"{"jsonrpc":"2.0","id":4,"method":"query.next","params":{"cursor_id":1,"batch_size":2}}"#,
2838 );
2839 assert!(third.contains("\"done\":true"));
2840 assert!(third.contains("\"remaining\":0"));
2841 });
2842 }
2843
2844 #[test]
2845 fn cursor_auto_drops_when_exhausted() {
2846 let rt = make_runtime();
2847 seed_numbers_table(&rt, "nums3", 2);
2848 with_session(&rt, |call, _| {
2849 let _ = call(
2850 r#"{"jsonrpc":"2.0","id":1,"method":"query.open","params":{"sql":"SELECT n FROM nums3"}}"#,
2851 );
2852 let _ = call(
2853 r#"{"jsonrpc":"2.0","id":2,"method":"query.next","params":{"cursor_id":1,"batch_size":100}}"#,
2854 );
2855 let resp = call(
2858 r#"{"jsonrpc":"2.0","id":3,"method":"query.next","params":{"cursor_id":1,"batch_size":100}}"#,
2859 );
2860 assert!(resp.contains("\"code\":\"CURSOR_NOT_FOUND\""));
2861 });
2862 }
2863
2864 #[test]
2865 fn cursor_close_removes_it() {
2866 let rt = make_runtime();
2867 seed_numbers_table(&rt, "nums4", 3);
2868 with_session(&rt, |call, _| {
2869 let _ = call(
2870 r#"{"jsonrpc":"2.0","id":1,"method":"query.open","params":{"sql":"SELECT n FROM nums4"}}"#,
2871 );
2872 let close =
2873 call(r#"{"jsonrpc":"2.0","id":2,"method":"query.close","params":{"cursor_id":1}}"#);
2874 assert!(close.contains("\"closed\":true"));
2875 let after = call(
2876 r#"{"jsonrpc":"2.0","id":3,"method":"query.next","params":{"cursor_id":1,"batch_size":10}}"#,
2877 );
2878 assert!(after.contains("\"code\":\"CURSOR_NOT_FOUND\""));
2879 });
2880 }
2881
2882 #[test]
2883 fn cursor_close_unknown_errors() {
2884 let rt = make_runtime();
2885 with_session(&rt, |call, _| {
2886 let resp = call(
2887 r#"{"jsonrpc":"2.0","id":1,"method":"query.close","params":{"cursor_id":9999}}"#,
2888 );
2889 assert!(resp.contains("\"code\":\"CURSOR_NOT_FOUND\""));
2890 });
2891 }
2892
2893 #[test]
2894 fn cursor_next_without_cursor_id_errors() {
2895 let rt = make_runtime();
2896 with_session(&rt, |call, _| {
2897 let resp = call(r#"{"jsonrpc":"2.0","id":1,"method":"query.next","params":{}}"#);
2898 assert!(resp.contains("\"code\":\"INVALID_PARAMS\""));
2899 });
2900 }
2901
2902 #[test]
2903 fn cursor_default_batch_size_returns_all_when_smaller_than_default() {
2904 let rt = make_runtime();
2905 seed_numbers_table(&rt, "nums5", 7);
2906 with_session(&rt, |call, _| {
2907 let _ = call(
2908 r#"{"jsonrpc":"2.0","id":1,"method":"query.open","params":{"sql":"SELECT n FROM nums5"}}"#,
2909 );
2910 let resp =
2912 call(r#"{"jsonrpc":"2.0","id":2,"method":"query.next","params":{"cursor_id":1}}"#);
2913 assert!(resp.contains("\"done\":true"));
2914 assert!(resp.contains("\"remaining\":0"));
2915 });
2916 }
2917
2918 #[test]
2919 fn close_method_drops_open_cursors() {
2920 let rt = make_runtime();
2921 seed_numbers_table(&rt, "nums6", 3);
2922 with_session(&rt, |call, _| {
2925 let _ = call(
2926 r#"{"jsonrpc":"2.0","id":1,"method":"query.open","params":{"sql":"SELECT n FROM nums6"}}"#,
2927 );
2928 let close = call(r#"{"jsonrpc":"2.0","id":2,"method":"close","params":null}"#);
2929 assert!(close.contains("\"__close__\":true"));
2930 let after = call(
2932 r#"{"jsonrpc":"2.0","id":3,"method":"query.next","params":{"cursor_id":1,"batch_size":10}}"#,
2933 );
2934 assert!(after.contains("\"code\":\"CURSOR_NOT_FOUND\""));
2935 });
2936 }
2937
2938 #[test]
2939 fn cursor_independent_of_transaction_state() {
2940 let rt = make_runtime();
2941 seed_numbers_table(&rt, "nums7", 4);
2942 with_session(&rt, |call, _| {
2943 let _ = call(
2945 r#"{"jsonrpc":"2.0","id":1,"method":"query.open","params":{"sql":"SELECT n FROM nums7"}}"#,
2946 );
2947 let _ = call(r#"{"jsonrpc":"2.0","id":2,"method":"tx.begin","params":null}"#);
2948 let _ = call(r#"{"jsonrpc":"2.0","id":3,"method":"tx.commit","params":null}"#);
2949 let resp = call(
2950 r#"{"jsonrpc":"2.0","id":4,"method":"query.next","params":{"cursor_id":1,"batch_size":10}}"#,
2951 );
2952 assert!(resp.contains("\"done\":true"));
2953 assert!(!resp.contains("\"error\""));
2954 });
2955 }
2956
2957 #[test]
2958 fn second_tx_after_commit_gets_fresh_id() {
2959 let rt = make_runtime();
2960 let _ = handle(
2961 &rt,
2962 r#"{"jsonrpc":"2.0","id":1,"method":"query","params":{"sql":"CREATE TABLE u6 (name TEXT)"}}"#,
2963 );
2964 with_session(&rt, |call, _| {
2965 let first = call(r#"{"jsonrpc":"2.0","id":1,"method":"tx.begin","params":null}"#);
2966 assert!(first.contains("\"tx_id\":1"));
2967 let _ = call(
2968 r#"{"jsonrpc":"2.0","id":2,"method":"insert","params":{"collection":"u6","payload":{"name":"x"}}}"#,
2969 );
2970 let _ = call(r#"{"jsonrpc":"2.0","id":3,"method":"tx.commit","params":null}"#);
2971
2972 let second = call(r#"{"jsonrpc":"2.0","id":4,"method":"tx.begin","params":null}"#);
2973 assert!(second.contains("\"tx_id\":2"));
2974 let _ = call(r#"{"jsonrpc":"2.0","id":5,"method":"tx.rollback","params":null}"#);
2975 });
2976 }
2977
2978 #[test]
2979 fn prepare_and_execute_prepared_statement() {
2980 let rt = make_runtime();
2981 let _ = handle(
2983 &rt,
2984 r#"{"jsonrpc":"2.0","id":1,"method":"query","params":{"sql":"CREATE TABLE ps_test (n INTEGER)"}}"#,
2985 );
2986 let _ = handle(
2987 &rt,
2988 r#"{"jsonrpc":"2.0","id":2,"method":"query","params":{"sql":"INSERT INTO ps_test (n) VALUES (42)"}}"#,
2989 );
2990
2991 with_session(&rt, |call, _| {
2992 let prep = call(
2994 r#"{"jsonrpc":"2.0","id":3,"method":"prepare","params":{"sql":"SELECT n FROM ps_test WHERE n = 42"}}"#,
2995 );
2996 assert!(prep.contains("\"prepared_id\""), "prepare response: {prep}");
2997
2998 let id: u64 = {
3000 let v: crate::json::Value = crate::json::from_str(&prep).expect("json");
3001 let result = v.get("result").expect("result");
3002 result
3003 .get("prepared_id")
3004 .and_then(|n| n.as_f64())
3005 .expect("prepared_id") as u64
3006 };
3007
3008 let exec = call(&format!(
3010 r#"{{"jsonrpc":"2.0","id":4,"method":"execute_prepared","params":{{"prepared_id":{id},"binds":[42]}}}}"#
3011 ));
3012 assert!(
3014 exec.contains("\"rows\""),
3015 "execute_prepared response: {exec}"
3016 );
3017 assert!(exec.contains("42"), "expected row with n=42 in: {exec}");
3018 });
3019 }
3020}