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