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 .and_then(|arr| {
703 arr.iter()
704 .map(try_json_value_to_schema_value)
705 .collect::<Result<Vec<_>, _>>()
706 .map_err(|message| (error_code::INVALID_PARAMS, message))
707 })
708 })
709 .transpose()?;
710
711 if let Some(binds) = bind_values {
712 let qr = runtime
713 .execute_query_with_params(sql, &binds)
714 .map_err(|e| {
715 if matches!(e, crate::api::RedDBError::Validation { .. }) {
716 (error_code::INVALID_PARAMS, e.to_string())
717 } else {
718 (error_code::QUERY_ERROR, e.to_string())
719 }
720 })?;
721 return Ok(query_result_to_json(&qr));
722 }
723
724 let qr = runtime
725 .execute_query(sql)
726 .map_err(|e| (error_code::QUERY_ERROR, e.to_string()))?;
727 Ok(query_result_to_json(&qr))
728 }
729
730 "prepare" => {
739 use crate::storage::query::modes::parse_multi;
740 use crate::storage::query::planner::shape::parameterize_query_expr;
741
742 let sql = params.get("sql").and_then(Value::as_str).ok_or((
743 error_code::INVALID_PARAMS,
744 "missing 'sql' string".to_string(),
745 ))?;
746 let parsed = parse_multi(sql).map_err(|e| (error_code::QUERY_ERROR, e.to_string()))?;
747 let (shape, parameter_count) = if let Some(prepared) = parameterize_query_expr(&parsed)
748 {
749 (prepared.shape, prepared.parameter_count)
750 } else {
751 (parsed, 0)
752 };
753 let id = session.next_prepared_id;
754 session.next_prepared_id = session.next_prepared_id.saturating_add(1);
755 session.prepared.insert(
756 id,
757 StdioPreparedStatement {
758 shape,
759 parameter_count,
760 },
761 );
762 Ok(Value::Object(
763 [
764 ("prepared_id".to_string(), Value::Number(id as f64)),
765 (
766 "parameter_count".to_string(),
767 Value::Number(parameter_count as f64),
768 ),
769 ]
770 .into_iter()
771 .collect(),
772 ))
773 }
774
775 "execute_prepared" => {
776 use crate::storage::query::planner::shape::bind_parameterized_query;
777 use crate::storage::schema::Value as SV;
778
779 let id = params
780 .get("prepared_id")
781 .and_then(Value::as_f64)
782 .map(|n| n as u64)
783 .ok_or((
784 error_code::INVALID_PARAMS,
785 "missing 'prepared_id'".to_string(),
786 ))?;
787
788 let stmt = session.prepared.get(&id).ok_or((
789 error_code::QUERY_ERROR,
790 format!("no prepared statement with id {id}"),
791 ))?;
792
793 let binds_json: Vec<Value> = params
795 .get("binds")
796 .and_then(Value::as_array)
797 .map(|a| a.to_vec())
798 .unwrap_or_default();
799 if binds_json.len() != stmt.parameter_count {
800 return Err((
801 error_code::INVALID_PARAMS,
802 format!(
803 "expected {} bind values, got {}",
804 stmt.parameter_count,
805 binds_json.len()
806 ),
807 ));
808 }
809
810 let binds: Vec<SV> = binds_json
812 .iter()
813 .map(try_json_value_to_schema_value)
814 .collect::<Result<Vec<_>, _>>()
815 .map_err(|message| (error_code::INVALID_PARAMS, message))?;
816
817 let expr = if stmt.parameter_count == 0 {
819 stmt.shape.clone()
820 } else {
821 bind_parameterized_query(&stmt.shape, &binds, stmt.parameter_count)
822 .ok_or((error_code::QUERY_ERROR, "bind failed".to_string()))?
823 };
824
825 let qr = runtime
826 .execute_query_expr(expr)
827 .map_err(|e| (error_code::QUERY_ERROR, e.to_string()))?;
828 Ok(query_result_to_json(&qr))
829 }
830
831 "insert" => {
832 let collection = params.get("collection").and_then(Value::as_str).ok_or((
833 error_code::INVALID_PARAMS,
834 "missing 'collection' string".to_string(),
835 ))?;
836 let payload = params.get("payload").ok_or((
837 error_code::INVALID_PARAMS,
838 "missing 'payload' object".to_string(),
839 ))?;
840 let payload_obj = payload.as_object().ok_or((
841 error_code::INVALID_PARAMS,
842 "'payload' must be a JSON object".to_string(),
843 ))?;
844 if let Some(tx) = session.current_tx_mut() {
845 let sql = build_insert_sql(collection, payload_obj.iter());
846 tx.write_set.push(PendingSql::Insert(sql));
847 return Ok(pending_tx_response(tx.tx_id));
848 }
849
850 let output = runtime
851 .create_row(flat_payload_to_row_input(collection, payload_obj))
852 .map_err(|e| (error_code::QUERY_ERROR, e.to_string()))?;
853 let mut out = json::Map::new();
854 out.insert("affected".to_string(), Value::Number(1.0));
855 out.insert("id".to_string(), Value::String(output.id.raw().to_string()));
856 Ok(Value::Object(out))
857 }
858
859 "bulk_insert" => {
860 let collection = params.get("collection").and_then(Value::as_str).ok_or((
861 error_code::INVALID_PARAMS,
862 "missing 'collection' string".to_string(),
863 ))?;
864 let payloads = params.get("payloads").and_then(Value::as_array).ok_or((
865 error_code::INVALID_PARAMS,
866 "missing 'payloads' array".to_string(),
867 ))?;
868
869 let mut objects = Vec::with_capacity(payloads.len());
870 for entry in payloads {
871 objects.push(entry.as_object().ok_or((
872 error_code::INVALID_PARAMS,
873 "each payload must be a JSON object".to_string(),
874 ))?);
875 }
876
877 if let Some(tx) = session.current_tx_mut() {
878 let mut buffered: u64 = 0;
879 for obj in &objects {
880 let sql = build_insert_sql(collection, obj.iter());
881 tx.write_set.push(PendingSql::Insert(sql));
882 buffered += 1;
883 }
884 let tx_id = tx.tx_id;
885 return Ok(Value::Object(
886 [
887 ("affected".to_string(), Value::Number(0.0)),
888 ("buffered".to_string(), Value::Number(buffered as f64)),
889 ("pending".to_string(), Value::Bool(true)),
890 ("tx_id".to_string(), Value::Number(tx_id as f64)),
891 ]
892 .into_iter()
893 .collect(),
894 ));
895 }
896
897 if should_bulk_insert_graph(runtime, collection, &objects) {
898 return bulk_insert_graph(runtime, collection, &objects)
899 .map_err(|e| (error_code::QUERY_ERROR, e.to_string()));
900 }
901
902 let mut total_affected: u64 = 0;
903 let mut ids = Vec::with_capacity(objects.len());
904 for chunk in objects.chunks(STDIO_BULK_INSERT_CHUNK_ROWS) {
905 let rows = chunk
906 .iter()
907 .map(|obj| flat_payload_to_row_input(collection, obj))
908 .collect();
909 let outputs = runtime
910 .create_rows_batch(CreateRowsBatchInput {
911 collection: collection.to_string(),
912 rows,
913 suppress_events: false,
914 })
915 .map_err(|e| (error_code::QUERY_ERROR, e.to_string()))?;
916 total_affected += outputs.len() as u64;
917 ids.extend(
918 outputs
919 .into_iter()
920 .map(|output| Value::String(output.id.raw().to_string())),
921 );
922 }
923 let mut out = json::Map::new();
924 out.insert("affected".to_string(), Value::Number(total_affected as f64));
925 out.insert("ids".to_string(), Value::Array(ids));
926 Ok(Value::Object(out))
927 }
928
929 "get" => {
930 let collection = params.get("collection").and_then(Value::as_str).ok_or((
931 error_code::INVALID_PARAMS,
932 "missing 'collection' string".to_string(),
933 ))?;
934 let id = params.get("id").and_then(Value::as_str).ok_or((
935 error_code::INVALID_PARAMS,
936 "missing 'id' string".to_string(),
937 ))?;
938 let sql = format!("SELECT * FROM {collection} WHERE rid = {id} LIMIT 1");
939 let qr = runtime
940 .execute_query(&sql)
941 .map_err(|e| (error_code::QUERY_ERROR, e.to_string()))?;
942 let entity = qr
943 .result
944 .records
945 .first()
946 .map(record_to_json_object)
947 .unwrap_or(Value::Null);
948 Ok(Value::Object(
949 [("entity".to_string(), entity)].into_iter().collect(),
950 ))
951 }
952
953 "delete" => {
954 let collection = params.get("collection").and_then(Value::as_str).ok_or((
955 error_code::INVALID_PARAMS,
956 "missing 'collection' string".to_string(),
957 ))?;
958 let id = params.get("id").and_then(Value::as_str).ok_or((
959 error_code::INVALID_PARAMS,
960 "missing 'id' string".to_string(),
961 ))?;
962 let sql = format!("DELETE FROM {collection} WHERE rid = {id}");
963
964 if let Some(tx) = session.current_tx_mut() {
965 tx.write_set.push(PendingSql::Delete(sql));
966 return Ok(pending_tx_response(tx.tx_id));
967 }
968
969 let qr = runtime
970 .execute_query(&sql)
971 .map_err(|e| (error_code::QUERY_ERROR, e.to_string()))?;
972 Ok(Value::Object(
973 [(
974 "affected".to_string(),
975 Value::Number(qr.affected_rows as f64),
976 )]
977 .into_iter()
978 .collect(),
979 ))
980 }
981
982 "close" => {
983 let _ = session.take_tx();
988 session.clear_cursors();
989 let _ = runtime.checkpoint();
990 Ok(Value::Null)
991 }
992
993 "auth.login"
998 | "auth.whoami"
999 | "auth.change_password"
1000 | "auth.create_api_key"
1001 | "auth.revoke_api_key" => {
1002 let _ = (session, params);
1003 Err((
1004 error_code::INVALID_REQUEST,
1005 format!(
1006 "{method}: auth methods are only available on grpc:// connections; \
1007 embedded modes (memory://, file://) inherit caller privileges"
1008 ),
1009 ))
1010 }
1011
1012 other => Err((
1013 error_code::INVALID_REQUEST,
1014 format!("unknown method: {other}"),
1015 )),
1016 }
1017}
1018
1019fn success_response(id: &Value, result: &Value, is_close: bool) -> String {
1024 let mut envelope = json::Map::new();
1029 envelope.insert("jsonrpc".to_string(), Value::String("2.0".to_string()));
1030 envelope.insert("id".to_string(), id.clone());
1031 envelope.insert("result".to_string(), result.clone());
1032 if is_close {
1033 envelope.insert("__close__".to_string(), Value::Bool(true));
1034 }
1035 Value::Object(envelope).to_string_compact()
1036}
1037
1038fn error_response(id: &Value, code: &str, message: &str) -> String {
1039 let mut err = json::Map::new();
1040 err.insert("code".to_string(), Value::String(code.to_string()));
1041 err.insert("message".to_string(), Value::String(message.to_string()));
1042 err.insert("data".to_string(), Value::Null);
1043
1044 let mut envelope = json::Map::new();
1045 envelope.insert("jsonrpc".to_string(), Value::String("2.0".to_string()));
1046 envelope.insert("id".to_string(), id.clone());
1047 envelope.insert("error".to_string(), Value::Object(err));
1048 Value::Object(envelope).to_string_compact()
1049}
1050
1051fn pending_tx_response(tx_id: u64) -> Value {
1058 Value::Object(
1059 [
1060 ("affected".to_string(), Value::Number(0.0)),
1061 ("pending".to_string(), Value::Bool(true)),
1062 ("tx_id".to_string(), Value::Number(tx_id as f64)),
1063 ]
1064 .into_iter()
1065 .collect(),
1066 )
1067}
1068
1069pub(crate) fn build_insert_sql<'a, I>(collection: &str, fields: I) -> String
1070where
1071 I: Iterator<Item = (&'a String, &'a Value)>,
1072{
1073 let mut cols = Vec::new();
1074 let mut vals = Vec::new();
1075 for (k, v) in fields {
1076 cols.push(k.clone());
1077 vals.push(value_to_sql_literal(v));
1078 }
1079 format!(
1080 "INSERT INTO {collection} ({}) VALUES ({})",
1081 cols.join(", "),
1082 vals.join(", "),
1083 )
1084}
1085
1086fn flat_payload_to_row_input(
1087 collection: &str,
1088 payload: &json::Map<String, Value>,
1089) -> CreateRowInput {
1090 CreateRowInput {
1091 collection: collection.to_string(),
1092 fields: payload
1093 .iter()
1094 .map(|(key, value)| (key.clone(), json_value_to_schema_value(value)))
1095 .collect(),
1096 metadata: Vec::new(),
1097 node_links: Vec::new(),
1098 vector_links: Vec::new(),
1099 }
1100}
1101
1102fn bulk_insert_chunk_count(row_count: usize) -> usize {
1103 if row_count == 0 {
1104 0
1105 } else {
1106 ((row_count - 1) / STDIO_BULK_INSERT_CHUNK_ROWS) + 1
1107 }
1108}
1109
1110pub(crate) fn should_bulk_insert_graph(
1111 runtime: &RedDBRuntime,
1112 collection: &str,
1113 payloads: &[&json::Map<String, Value>],
1114) -> bool {
1115 let graph_shaped = payloads
1116 .iter()
1117 .all(|payload| payload.get("label").and_then(Value::as_str).is_some());
1118 if !graph_shaped {
1119 return false;
1120 }
1121
1122 matches!(
1123 runtime
1124 .db()
1125 .catalog_model_snapshot()
1126 .collections
1127 .iter()
1128 .find(|descriptor| descriptor.name == collection)
1129 .map(|descriptor| descriptor.declared_model.unwrap_or(descriptor.model)),
1130 Some(crate::catalog::CollectionModel::Graph | crate::catalog::CollectionModel::Mixed)
1131 )
1132}
1133
1134pub(crate) fn bulk_insert_graph(
1135 runtime: &RedDBRuntime,
1136 collection: &str,
1137 payloads: &[&json::Map<String, Value>],
1138) -> crate::RedDBResult<Value> {
1139 use crate::application::entity_payload::{parse_create_edge_input, parse_create_node_input};
1140 use crate::application::ports::RuntimeEntityPort;
1141
1142 let mut ids = Vec::with_capacity(payloads.len());
1143 for payload in payloads {
1144 let input_payload = normalize_flat_graph_payload(payload);
1145 let id = if payload.contains_key("from") || payload.contains_key("to") {
1146 runtime
1147 .create_edge(parse_create_edge_input(
1148 collection.to_string(),
1149 &input_payload,
1150 )?)?
1151 .id
1152 } else {
1153 runtime
1154 .create_node(parse_create_node_input(
1155 collection.to_string(),
1156 &input_payload,
1157 )?)?
1158 .id
1159 };
1160 ids.push(Value::Number(id.raw() as f64));
1161 }
1162
1163 let mut out = json::Map::new();
1164 out.insert("affected".to_string(), Value::Number(ids.len() as f64));
1165 out.insert("ids".to_string(), Value::Array(ids));
1166 Ok(Value::Object(out))
1167}
1168
1169fn normalize_flat_graph_payload(payload: &json::Map<String, Value>) -> Value {
1170 if payload.contains_key("properties") || payload.contains_key("fields") {
1171 return Value::Object(payload.clone());
1172 }
1173
1174 let is_edge = payload.contains_key("from") || payload.contains_key("to");
1175 let mut normalized = payload.clone();
1176 let mut properties = json::Map::new();
1177 for (key, value) in payload {
1178 let reserved = if is_edge {
1179 matches!(
1180 key.as_str(),
1181 "label"
1182 | "from"
1183 | "to"
1184 | "weight"
1185 | "metadata"
1186 | "properties"
1187 | "fields"
1188 | "_ttl_ms"
1189 | "_expires_at"
1190 )
1191 } else {
1192 matches!(
1193 key.as_str(),
1194 "label"
1195 | "node_type"
1196 | "metadata"
1197 | "links"
1198 | "embeddings"
1199 | "properties"
1200 | "fields"
1201 | "_ttl_ms"
1202 | "_expires_at"
1203 )
1204 };
1205 if !reserved {
1206 properties.insert(key.clone(), value.clone());
1207 }
1208 }
1209 if !properties.is_empty() {
1210 normalized.insert("properties".to_string(), Value::Object(properties));
1211 }
1212 Value::Object(normalized)
1213}
1214
1215pub(crate) fn value_to_sql_literal(v: &Value) -> String {
1216 match v {
1217 Value::Null => "NULL".to_string(),
1218 Value::Bool(b) => b.to_string(),
1219 Value::Integer(n) => n.to_string(),
1220 Value::Number(n) => {
1221 if n.fract() == 0.0 {
1222 format!("{}", *n as i64)
1223 } else {
1224 n.to_string()
1225 }
1226 }
1227 Value::String(s) => format!("'{}'", s.replace('\'', "''")),
1228 other => format!("'{}'", other.to_string_compact().replace('\'', "''")),
1229 }
1230}
1231
1232pub(crate) fn query_result_to_json(qr: &RuntimeQueryResult) -> Value {
1233 if let Some(ask) = ask_query_result_to_json(qr) {
1234 return ask;
1235 }
1236
1237 let mut envelope = json::Map::new();
1238 envelope.insert(
1239 "statement".to_string(),
1240 Value::String(qr.statement_type.to_string()),
1241 );
1242 envelope.insert(
1243 "affected".to_string(),
1244 Value::Number(qr.affected_rows as f64),
1245 );
1246
1247 let mut columns = Vec::new();
1248 if let Some(first) = qr.result.records.first() {
1249 let mut keys: Vec<String> = first
1250 .column_names()
1251 .into_iter()
1252 .map(|k| k.to_string())
1253 .collect();
1254 keys.sort();
1255 columns = keys.into_iter().map(Value::String).collect();
1256 }
1257 envelope.insert("columns".to_string(), Value::Array(columns));
1258
1259 let rows: Vec<Value> = qr
1260 .result
1261 .records
1262 .iter()
1263 .map(record_to_json_object)
1264 .collect();
1265 envelope.insert("rows".to_string(), Value::Array(rows));
1266
1267 Value::Object(envelope)
1268}
1269
1270fn ask_query_result_to_json(qr: &RuntimeQueryResult) -> Option<Value> {
1271 if qr.statement != "ask" {
1272 return None;
1273 }
1274 let row = qr.result.records.first()?;
1275 let answer = text_field(row, "answer")?;
1276 let provider = text_field(row, "provider").unwrap_or_default();
1277 let model = text_field(row, "model").unwrap_or_default();
1278 let sources_flat_json = json_field(row, "sources_flat").unwrap_or(Value::Array(Vec::new()));
1279 let citations_json = json_field(row, "citations").unwrap_or(Value::Array(Vec::new()));
1280 let validation_json = json_field(row, "validation").unwrap_or(Value::Object(json::Map::new()));
1281
1282 let effective_mode = match text_field(row, "mode").as_deref() {
1283 Some("lenient") => crate::runtime::ai::ask_response_envelope::Mode::Lenient,
1284 _ => crate::runtime::ai::ask_response_envelope::Mode::Strict,
1285 };
1286
1287 let result = crate::runtime::ai::ask_response_envelope::AskResult {
1288 answer,
1289 sources_flat: envelope_sources_flat(&sources_flat_json),
1290 citations: envelope_citations(&citations_json),
1291 validation: envelope_validation(&validation_json),
1292 cache_hit: bool_field(row, "cache_hit").unwrap_or(false),
1293 provider,
1294 model,
1295 prompt_tokens: u32_field(row, "prompt_tokens").unwrap_or(0),
1296 completion_tokens: u32_field(row, "completion_tokens").unwrap_or(0),
1297 cost_usd: f64_field(row, "cost_usd").unwrap_or(0.0),
1298 effective_mode,
1299 retry_count: u32_field(row, "retry_count").unwrap_or(0),
1300 };
1301 Some(crate::runtime::ai::ask_response_envelope::build(&result))
1302}
1303
1304fn record_field<'a>(record: &'a UnifiedRecord, name: &str) -> Option<&'a SchemaValue> {
1305 record
1306 .iter_fields()
1307 .find_map(|(key, value)| (key.as_ref() == name).then_some(value))
1308}
1309
1310fn text_field(record: &UnifiedRecord, name: &str) -> Option<String> {
1311 match record_field(record, name)? {
1312 SchemaValue::Text(s) => Some(s.to_string()),
1313 SchemaValue::Email(s)
1314 | SchemaValue::Url(s)
1315 | SchemaValue::NodeRef(s)
1316 | SchemaValue::EdgeRef(s) => Some(s.clone()),
1317 other => Some(format!("{other}")),
1318 }
1319}
1320
1321fn u32_field(record: &UnifiedRecord, name: &str) -> Option<u32> {
1322 match record_field(record, name)? {
1323 SchemaValue::Integer(n) => (*n >= 0).then_some((*n).min(u32::MAX as i64) as u32),
1324 SchemaValue::UnsignedInteger(n) => Some((*n).min(u32::MAX as u64) as u32),
1325 SchemaValue::BigInt(n)
1326 | SchemaValue::TimestampMs(n)
1327 | SchemaValue::Timestamp(n)
1328 | SchemaValue::Duration(n)
1329 | SchemaValue::Decimal(n) => (*n >= 0).then_some((*n).min(u32::MAX as i64) as u32),
1330 SchemaValue::Float(n) => (*n >= 0.0).then_some((*n).min(u32::MAX as f64) as u32),
1331 _ => None,
1332 }
1333}
1334
1335fn f64_field(record: &UnifiedRecord, name: &str) -> Option<f64> {
1336 match record_field(record, name)? {
1337 SchemaValue::Integer(n) => Some(*n as f64),
1338 SchemaValue::UnsignedInteger(n) => Some(*n as f64),
1339 SchemaValue::BigInt(n)
1340 | SchemaValue::TimestampMs(n)
1341 | SchemaValue::Timestamp(n)
1342 | SchemaValue::Duration(n)
1343 | SchemaValue::Decimal(n) => Some(*n as f64),
1344 SchemaValue::Float(n) => Some(*n),
1345 _ => None,
1346 }
1347}
1348
1349fn bool_field(record: &UnifiedRecord, name: &str) -> Option<bool> {
1350 match record_field(record, name)? {
1351 SchemaValue::Boolean(value) => Some(*value),
1352 _ => None,
1353 }
1354}
1355
1356fn json_field(record: &UnifiedRecord, name: &str) -> Option<Value> {
1357 match record_field(record, name)? {
1358 SchemaValue::Json(bytes) => {
1359 crate::document_body::decode_container_to_json(bytes)
1361 .or_else(|| json::from_slice(bytes).ok())
1362 }
1363 SchemaValue::Text(text) => json::from_str(text).ok(),
1364 _ => None,
1365 }
1366}
1367
1368fn envelope_sources_flat(
1369 value: &Value,
1370) -> Vec<crate::runtime::ai::ask_response_envelope::SourceRow> {
1371 value
1372 .as_array()
1373 .unwrap_or(&[])
1374 .iter()
1375 .filter_map(|source| {
1376 let urn = source.get("urn").and_then(Value::as_str)?.to_string();
1377 let payload = source
1378 .get("payload")
1379 .and_then(Value::as_str)
1380 .map(ToString::to_string)
1381 .unwrap_or_else(|| source.to_string_compact());
1382 Some(crate::runtime::ai::ask_response_envelope::SourceRow { urn, payload })
1383 })
1384 .collect()
1385}
1386
1387fn envelope_citations(value: &Value) -> Vec<crate::runtime::ai::ask_response_envelope::Citation> {
1388 value
1389 .as_array()
1390 .unwrap_or(&[])
1391 .iter()
1392 .filter_map(|citation| {
1393 let marker = citation.get("marker").and_then(Value::as_u64)?;
1394 let urn = citation.get("urn").and_then(Value::as_str)?.to_string();
1395 Some(crate::runtime::ai::ask_response_envelope::Citation {
1396 marker: marker.min(u32::MAX as u64) as u32,
1397 urn,
1398 })
1399 })
1400 .collect()
1401}
1402
1403fn envelope_validation(value: &Value) -> crate::runtime::ai::ask_response_envelope::Validation {
1404 crate::runtime::ai::ask_response_envelope::Validation {
1405 ok: value.get("ok").and_then(Value::as_bool).unwrap_or(true),
1406 warnings: validation_items(value, "warnings")
1407 .into_iter()
1408 .map(
1409 |(kind, detail)| crate::runtime::ai::ask_response_envelope::ValidationWarning {
1410 kind,
1411 detail,
1412 },
1413 )
1414 .collect(),
1415 errors: validation_items(value, "errors")
1416 .into_iter()
1417 .map(
1418 |(kind, detail)| crate::runtime::ai::ask_response_envelope::ValidationError {
1419 kind,
1420 detail,
1421 },
1422 )
1423 .collect(),
1424 }
1425}
1426
1427fn validation_items(value: &Value, key: &str) -> Vec<(String, String)> {
1428 value
1429 .get(key)
1430 .and_then(Value::as_array)
1431 .unwrap_or(&[])
1432 .iter()
1433 .filter_map(|item| {
1434 Some((
1435 item.get("kind").and_then(Value::as_str)?.to_string(),
1436 item.get("detail")
1437 .and_then(Value::as_str)
1438 .unwrap_or("")
1439 .to_string(),
1440 ))
1441 })
1442 .collect()
1443}
1444
1445pub(crate) fn insert_result_to_json(qr: &RuntimeQueryResult) -> Value {
1446 let mut envelope = json::Map::new();
1447 envelope.insert(
1448 "affected".to_string(),
1449 Value::Number(qr.affected_rows as f64),
1450 );
1451 if let Some(first) = qr.result.records.first() {
1453 if let Some(id_val) = first
1454 .iter_fields()
1455 .find(|(k, _)| {
1456 let s: &str = k;
1457 s == "_entity_id"
1458 })
1459 .map(|(_, v)| schema_value_to_json(v))
1460 {
1461 envelope.insert("id".to_string(), id_val);
1462 }
1463 }
1464 Value::Object(envelope)
1465}
1466
1467fn record_to_json_object(record: &UnifiedRecord) -> Value {
1468 let mut map = json::Map::new();
1469 let mut entries: Vec<(&str, &SchemaValue)> =
1472 record.iter_fields().map(|(k, v)| (k.as_ref(), v)).collect();
1473 entries.sort_by(|a, b| a.0.cmp(b.0));
1474 for (k, v) in entries {
1475 map.insert(k.to_string(), schema_value_to_json(v));
1476 }
1477 Value::Object(map)
1478}
1479
1480fn schema_value_to_json(v: &SchemaValue) -> Value {
1481 match v {
1482 SchemaValue::Null => Value::Null,
1483 SchemaValue::Boolean(b) => Value::Bool(*b),
1484 SchemaValue::Integer(n) => exact_i64_to_json(*n),
1485 SchemaValue::UnsignedInteger(n) => exact_u64_to_json(*n),
1486 SchemaValue::Float(n) if n.is_finite() => Value::Number(*n),
1487 SchemaValue::Float(n) => {
1488 let token = if n.is_nan() {
1489 "NaN"
1490 } else if n.is_sign_positive() {
1491 "Infinity"
1492 } else {
1493 "-Infinity"
1494 };
1495 single_key_object("$float", Value::String(token.to_string()))
1496 }
1497 SchemaValue::BigInt(n) => exact_i64_to_json(*n),
1498 SchemaValue::TimestampMs(n) | SchemaValue::Duration(n) => exact_i64_to_json(*n),
1499 SchemaValue::Decimal(n) => exact_decimal_to_json(SchemaValue::Decimal(*n).display_string()),
1500 SchemaValue::DecimalText(n) => exact_decimal_to_json(n.clone()),
1501 SchemaValue::Timestamp(n) => single_key_object("$ts", Value::String(n.to_string())),
1502 SchemaValue::Password(_) | SchemaValue::Secret(_) => Value::String("***".to_string()),
1503 SchemaValue::Text(s) => Value::String(s.to_string()),
1504 SchemaValue::Blob(bytes) => {
1505 single_key_object("$bytes", Value::String(base64_encode(bytes)))
1506 }
1507 SchemaValue::Json(bytes) => {
1508 crate::presentation::entity_json::storage_json_bytes_to_json(bytes)
1509 }
1510 SchemaValue::Uuid(bytes) => single_key_object("$uuid", Value::String(format_uuid(bytes))),
1511 SchemaValue::Email(s)
1512 | SchemaValue::Url(s)
1513 | SchemaValue::NodeRef(s)
1514 | SchemaValue::EdgeRef(s) => Value::String(s.clone()),
1515 other => Value::String(format!("{other}")),
1516 }
1517}
1518
1519fn single_key_object(key: &str, value: Value) -> Value {
1520 Value::Object([(key.to_string(), value)].into_iter().collect())
1521}
1522
1523const MAX_JSON_SAFE_INTEGER: i64 = 9_007_199_254_740_991;
1524
1525fn exact_i64_to_json(value: i64) -> Value {
1526 if (-MAX_JSON_SAFE_INTEGER..=MAX_JSON_SAFE_INTEGER).contains(&value) {
1527 Value::Integer(value)
1528 } else {
1529 single_key_object("$int", Value::String(value.to_string()))
1530 }
1531}
1532
1533fn exact_u64_to_json(value: u64) -> Value {
1534 if value <= MAX_JSON_SAFE_INTEGER as u64 {
1535 Value::Integer(value as i64)
1536 } else {
1537 single_key_object("$uint", Value::String(value.to_string()))
1538 }
1539}
1540
1541fn exact_decimal_to_json(value: String) -> Value {
1542 single_key_object("$decimal", Value::String(value))
1543}
1544
1545pub(crate) fn json_value_to_schema_value(v: &Value) -> SchemaValue {
1549 try_json_value_to_schema_value(v)
1550 .unwrap_or_else(|_| SchemaValue::Json(crate::json::to_vec(v).unwrap_or_default()))
1551}
1552
1553fn try_json_value_to_schema_value(v: &Value) -> Result<SchemaValue, String> {
1554 match v {
1555 Value::Null => Ok(SchemaValue::Null),
1556 Value::Bool(b) => Ok(SchemaValue::Boolean(*b)),
1557 Value::Integer(n) => Ok(SchemaValue::Integer(*n)),
1558 Value::Number(n) => {
1559 if n.is_finite() && n.fract() == 0.0 && *n >= i64::MIN as f64 && *n <= i64::MAX as f64 {
1560 Ok(SchemaValue::Integer(*n as i64))
1561 } else {
1562 Ok(SchemaValue::Float(*n))
1563 }
1564 }
1565 Value::Decimal(n) => Ok(SchemaValue::DecimalText(n.clone())),
1566 Value::String(s) => Ok(SchemaValue::text(s.clone())),
1567 Value::Array(items) => {
1568 if items
1572 .iter()
1573 .all(|v| matches!(v, Value::Integer(_) | Value::Number(_)))
1574 {
1575 let floats: Vec<f32> = items
1576 .iter()
1577 .map(|v| v.as_f64().unwrap_or(0.0) as f32)
1578 .collect();
1579 Ok(SchemaValue::Vector(floats))
1580 } else {
1581 Ok(SchemaValue::Json(
1582 crate::json::to_vec(v).unwrap_or_default(),
1583 ))
1584 }
1585 }
1586 Value::Object(map) => {
1587 if map.len() == 1 {
1588 if let Some(Value::String(encoded)) = map.get("$bytes") {
1589 if let Ok(bytes) = base64_decode(encoded) {
1590 return Ok(SchemaValue::Blob(bytes));
1591 }
1592 }
1593 if let Some(value) = map.get("$ts") {
1594 if let Some(ts) = json_i64(value) {
1595 return Ok(SchemaValue::Timestamp(ts));
1596 }
1597 }
1598 if let Some(Value::String(value)) = map.get("$uuid") {
1599 if let Ok(uuid) = crate::crypto::Uuid::parse_str(value) {
1600 return Ok(SchemaValue::Uuid(*uuid.as_bytes()));
1601 }
1602 }
1603 if let Some(Value::String(value)) = map.get("$float") {
1604 return Ok(match value.as_str() {
1605 "NaN" => SchemaValue::Float(f64::NAN),
1606 "Infinity" | "+Infinity" | "inf" | "+inf" => {
1607 SchemaValue::Float(f64::INFINITY)
1608 }
1609 "-Infinity" | "-inf" => SchemaValue::Float(f64::NEG_INFINITY),
1610 _ => SchemaValue::Json(crate::json::to_vec(v).unwrap_or_default()),
1611 });
1612 }
1613 if map.contains_key("$number") || map.contains_key("$decimalText") {
1614 return Err("superseded exact-number envelope".to_string());
1615 }
1616 if let Some(value) = map.get("$int") {
1617 return json_i64(value)
1618 .map(SchemaValue::Integer)
1619 .ok_or_else(|| "invalid $int exact-number envelope".to_string());
1620 }
1621 if let Some(value) = map.get("$uint") {
1622 if let Value::String(value) = value {
1623 return value
1624 .parse::<u64>()
1625 .map(SchemaValue::UnsignedInteger)
1626 .map_err(|_| "invalid $uint exact-number envelope".to_string());
1627 }
1628 return Err("invalid $uint exact-number envelope".to_string());
1629 }
1630 if let Some(Value::String(value)) = map.get("$decimal") {
1631 return Ok(SchemaValue::DecimalText(value.clone()));
1632 }
1633 }
1634 Ok(SchemaValue::Json(
1635 crate::json::to_vec(v).unwrap_or_default(),
1636 ))
1637 }
1638 }
1639}
1640
1641fn json_i64(value: &Value) -> Option<i64> {
1642 match value {
1643 Value::Integer(n) => Some(*n),
1644 Value::Number(n) => {
1645 if n.is_finite() && n.fract() == 0.0 && *n >= i64::MIN as f64 && *n <= i64::MAX as f64 {
1646 Some(*n as i64)
1647 } else {
1648 None
1649 }
1650 }
1651 Value::String(s) => s.parse::<i64>().ok(),
1652 _ => None,
1653 }
1654}
1655
1656fn format_uuid(bytes: &[u8; 16]) -> String {
1657 format!(
1658 "{:02x}{:02x}{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}",
1659 bytes[0],
1660 bytes[1],
1661 bytes[2],
1662 bytes[3],
1663 bytes[4],
1664 bytes[5],
1665 bytes[6],
1666 bytes[7],
1667 bytes[8],
1668 bytes[9],
1669 bytes[10],
1670 bytes[11],
1671 bytes[12],
1672 bytes[13],
1673 bytes[14],
1674 bytes[15]
1675 )
1676}
1677
1678fn base64_encode(bytes: &[u8]) -> String {
1679 const TABLE: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
1680 let mut out = String::with_capacity(bytes.len().div_ceil(3) * 4);
1681 let mut chunks = bytes.chunks_exact(3);
1682 for chunk in chunks.by_ref() {
1683 let n = ((chunk[0] as u32) << 16) | ((chunk[1] as u32) << 8) | chunk[2] as u32;
1684 out.push(TABLE[((n >> 18) & 0x3f) as usize] as char);
1685 out.push(TABLE[((n >> 12) & 0x3f) as usize] as char);
1686 out.push(TABLE[((n >> 6) & 0x3f) as usize] as char);
1687 out.push(TABLE[(n & 0x3f) as usize] as char);
1688 }
1689 match chunks.remainder() {
1690 [] => {}
1691 [a] => {
1692 let n = (*a as u32) << 16;
1693 out.push(TABLE[((n >> 18) & 0x3f) as usize] as char);
1694 out.push(TABLE[((n >> 12) & 0x3f) as usize] as char);
1695 out.push('=');
1696 out.push('=');
1697 }
1698 [a, b] => {
1699 let n = ((*a as u32) << 16) | ((*b as u32) << 8);
1700 out.push(TABLE[((n >> 18) & 0x3f) as usize] as char);
1701 out.push(TABLE[((n >> 12) & 0x3f) as usize] as char);
1702 out.push(TABLE[((n >> 6) & 0x3f) as usize] as char);
1703 out.push('=');
1704 }
1705 _ => unreachable!(),
1706 }
1707 out
1708}
1709
1710fn base64_decode(input: &str) -> Result<Vec<u8>, String> {
1711 let bytes = input.as_bytes();
1712 if !bytes.len().is_multiple_of(4) {
1713 return Err("base64 length must be a multiple of 4".to_string());
1714 }
1715 let mut out = Vec::with_capacity(bytes.len() / 4 * 3);
1716 for chunk in bytes.chunks_exact(4) {
1717 let pad = chunk.iter().rev().take_while(|&&b| b == b'=').count();
1718 let a = base64_value(chunk[0])?;
1719 let b = base64_value(chunk[1])?;
1720 let c = if chunk[2] == b'=' {
1721 0
1722 } else {
1723 base64_value(chunk[2])?
1724 };
1725 let d = if chunk[3] == b'=' {
1726 0
1727 } else {
1728 base64_value(chunk[3])?
1729 };
1730 let n = ((a as u32) << 18) | ((b as u32) << 12) | ((c as u32) << 6) | d as u32;
1731 out.push(((n >> 16) & 0xff) as u8);
1732 if pad < 2 {
1733 out.push(((n >> 8) & 0xff) as u8);
1734 }
1735 if pad < 1 {
1736 out.push((n & 0xff) as u8);
1737 }
1738 }
1739 Ok(out)
1740}
1741
1742fn base64_value(byte: u8) -> Result<u8, String> {
1743 match byte {
1744 b'A'..=b'Z' => Ok(byte - b'A'),
1745 b'a'..=b'z' => Ok(byte - b'a' + 26),
1746 b'0'..=b'9' => Ok(byte - b'0' + 52),
1747 b'+' => Ok(62),
1748 b'/' => Ok(63),
1749 b'=' => Ok(0),
1750 _ => Err(format!("invalid base64 character: {}", byte as char)),
1751 }
1752}
1753
1754fn dispatch_method_remote(
1764 client: &AsyncMutex<RedDBClient>,
1765 tokio_rt: &tokio::runtime::Runtime,
1766 method: &str,
1767 params: &Value,
1768) -> Result<Value, (&'static str, String)> {
1769 match method {
1770 "version" => Ok(Value::Object(
1771 [
1772 (
1773 "version".to_string(),
1774 Value::String(env!("CARGO_PKG_VERSION").to_string()),
1775 ),
1776 (
1777 "protocol".to_string(),
1778 Value::String(PROTOCOL_VERSION.to_string()),
1779 ),
1780 ]
1781 .into_iter()
1782 .collect(),
1783 )),
1784
1785 "health" => {
1786 let result = tokio_rt.block_on(async {
1787 let mut guard = client.lock().await;
1788 guard.health_status().await
1789 });
1790 match result {
1791 Ok(status) => Ok(Value::Object(
1792 [
1793 ("ok".to_string(), Value::Bool(status.healthy)),
1794 ("state".to_string(), Value::String(status.state)),
1795 (
1796 "checked_at_unix_ms".to_string(),
1797 Value::Number(status.checked_at_unix_ms as f64),
1798 ),
1799 (
1800 "version".to_string(),
1801 Value::String(env!("CARGO_PKG_VERSION").to_string()),
1802 ),
1803 ]
1804 .into_iter()
1805 .collect(),
1806 )),
1807 Err(e) => Err((error_code::INTERNAL_ERROR, e.to_string())),
1808 }
1809 }
1810
1811 "query" => {
1812 let sql = params.get("sql").and_then(Value::as_str).ok_or((
1813 error_code::INVALID_PARAMS,
1814 "missing 'sql' string".to_string(),
1815 ))?;
1816 let json_str = tokio_rt
1817 .block_on(async {
1818 let mut guard = client.lock().await;
1819 guard.query(sql).await
1820 })
1821 .map_err(|e| (error_code::QUERY_ERROR, e.to_string()))?;
1822 let parsed = json::from_str::<Value>(&json_str)
1827 .map_err(|e| (error_code::INTERNAL_ERROR, format!("bad server JSON: {e}")))?;
1828 Ok(parsed)
1829 }
1830
1831 "insert" => {
1832 let collection = params.get("collection").and_then(Value::as_str).ok_or((
1833 error_code::INVALID_PARAMS,
1834 "missing 'collection' string".to_string(),
1835 ))?;
1836 let payload = params.get("payload").ok_or((
1837 error_code::INVALID_PARAMS,
1838 "missing 'payload' object".to_string(),
1839 ))?;
1840 if payload.as_object().is_none() {
1841 return Err((
1842 error_code::INVALID_PARAMS,
1843 "'payload' must be a JSON object".to_string(),
1844 ));
1845 }
1846 let payload_json = payload.to_string_compact();
1847 let reply = tokio_rt
1848 .block_on(async {
1849 let mut guard = client.lock().await;
1850 guard.create_row_entity(collection, &payload_json).await
1851 })
1852 .map_err(|e| (error_code::QUERY_ERROR, e.to_string()))?;
1853 let mut out = json::Map::new();
1854 out.insert("affected".to_string(), Value::Number(1.0));
1855 out.insert("id".to_string(), Value::String(reply.id.to_string()));
1856 Ok(Value::Object(out))
1857 }
1858
1859 "bulk_insert" => {
1860 let collection = params.get("collection").and_then(Value::as_str).ok_or((
1861 error_code::INVALID_PARAMS,
1862 "missing 'collection' string".to_string(),
1863 ))?;
1864 let payloads = params.get("payloads").and_then(Value::as_array).ok_or((
1865 error_code::INVALID_PARAMS,
1866 "missing 'payloads' array".to_string(),
1867 ))?;
1868 let mut encoded = Vec::with_capacity(payloads.len());
1869 for entry in payloads {
1870 if entry.as_object().is_none() {
1871 return Err((
1872 error_code::INVALID_PARAMS,
1873 "each payload must be a JSON object".to_string(),
1874 ));
1875 }
1876 encoded.push(entry.to_string_compact());
1877 }
1878 let status = tokio_rt
1879 .block_on(async {
1880 let mut guard = client.lock().await;
1881 guard.bulk_create_rows(collection, encoded).await
1882 })
1883 .map_err(|e| (error_code::QUERY_ERROR, e.to_string()))?;
1884 Ok(Value::Object(
1885 [
1886 ("affected".to_string(), Value::Number(status.count as f64)),
1887 (
1888 "ids".to_string(),
1889 Value::Array(
1890 status
1891 .ids
1892 .into_iter()
1893 .map(|id| Value::Number(id as f64))
1894 .collect(),
1895 ),
1896 ),
1897 ]
1898 .into_iter()
1899 .collect(),
1900 ))
1901 }
1902
1903 "get" => {
1904 let collection = params.get("collection").and_then(Value::as_str).ok_or((
1905 error_code::INVALID_PARAMS,
1906 "missing 'collection' string".to_string(),
1907 ))?;
1908 let id = params.get("id").and_then(Value::as_str).ok_or((
1909 error_code::INVALID_PARAMS,
1910 "missing 'id' string".to_string(),
1911 ))?;
1912 let sql = format!("SELECT * FROM {collection} WHERE rid = {id} LIMIT 1");
1913 let json_str = tokio_rt
1914 .block_on(async {
1915 let mut guard = client.lock().await;
1916 guard.query(&sql).await
1917 })
1918 .map_err(|e| (error_code::QUERY_ERROR, e.to_string()))?;
1919 let parsed = json::from_str::<Value>(&json_str)
1920 .map_err(|e| (error_code::INTERNAL_ERROR, format!("bad server JSON: {e}")))?;
1921 let entity = parsed
1924 .get("rows")
1925 .and_then(Value::as_array)
1926 .and_then(|rows| rows.first().cloned())
1927 .unwrap_or(Value::Null);
1928 Ok(Value::Object(
1929 [("entity".to_string(), entity)].into_iter().collect(),
1930 ))
1931 }
1932
1933 "delete" => {
1934 let collection = params.get("collection").and_then(Value::as_str).ok_or((
1935 error_code::INVALID_PARAMS,
1936 "missing 'collection' string".to_string(),
1937 ))?;
1938 let id = params.get("id").and_then(Value::as_str).ok_or((
1939 error_code::INVALID_PARAMS,
1940 "missing 'id' string".to_string(),
1941 ))?;
1942 let id = id.parse::<u64>().map_err(|_| {
1943 (
1944 error_code::INVALID_PARAMS,
1945 "id must be a numeric string".to_string(),
1946 )
1947 })?;
1948 let _reply = tokio_rt
1949 .block_on(async {
1950 let mut guard = client.lock().await;
1951 guard.delete_entity(collection, id).await
1952 })
1953 .map_err(|e| (error_code::QUERY_ERROR, e.to_string()))?;
1954 Ok(Value::Object(
1955 [("affected".to_string(), Value::Number(1.0))]
1956 .into_iter()
1957 .collect(),
1958 ))
1959 }
1960
1961 "close" => Ok(Value::Null),
1962
1963 other => Err((
1964 error_code::INVALID_REQUEST,
1965 format!("unknown method: {other}"),
1966 )),
1967 }
1968}
1969
1970#[cfg(test)]
1971mod tests {
1972 use super::*;
1973 use crate::json::json;
1974 use proptest::prelude::*;
1975
1976 fn make_runtime() -> RedDBRuntime {
1977 RedDBRuntime::in_memory().expect("in-memory runtime")
1978 }
1979
1980 fn create_graph_collection(rt: &RedDBRuntime, name: &str) {
1981 let db = rt.db();
1982 db.store()
1983 .create_collection(name)
1984 .expect("create collection");
1985 let now = std::time::SystemTime::now()
1986 .duration_since(std::time::UNIX_EPOCH)
1987 .unwrap_or_default()
1988 .as_millis();
1989 db.save_collection_contract(crate::physical::CollectionContract {
1990 name: name.to_string(),
1991 declared_model: crate::catalog::CollectionModel::Graph,
1992 schema_mode: crate::catalog::SchemaMode::Dynamic,
1993 origin: crate::physical::ContractOrigin::Explicit,
1994 version: 1,
1995 created_at_unix_ms: now,
1996 updated_at_unix_ms: now,
1997 default_ttl_ms: None,
1998 vector_dimension: None,
1999 vector_metric: None,
2000 context_index_fields: Vec::new(),
2001 declared_columns: Vec::new(),
2002 table_def: None,
2003 timestamps_enabled: false,
2004 context_index_enabled: false,
2005 metrics_raw_retention_ms: None,
2006 metrics_rollup_policies: Vec::new(),
2007 metrics_tenant_identity: None,
2008 metrics_namespace: None,
2009 append_only: false,
2010 subscriptions: Vec::new(),
2011 analytics_config: Vec::new(),
2012 session_key: None,
2013 session_gap_ms: None,
2014 retention_duration_ms: None,
2015 analytical_storage: None,
2016
2017 ai_policy: None,
2018 })
2019 .expect("save graph contract");
2020 }
2021
2022 fn handle(rt: &RedDBRuntime, line: &str) -> String {
2023 let mut session = Session::new();
2024 handle_line(&Backend::Local(rt), &mut session, line)
2025 }
2026
2027 fn query_request(id: u64, sql: &str) -> String {
2028 let mut params = json::Map::new();
2029 params.insert("sql".to_string(), Value::String(sql.to_string()));
2030
2031 let mut request = json::Map::new();
2032 request.insert("jsonrpc".to_string(), Value::String("2.0".to_string()));
2033 request.insert("id".to_string(), Value::Number(id as f64));
2034 request.insert("method".to_string(), Value::String("query".to_string()));
2035 request.insert("params".to_string(), Value::Object(params));
2036 Value::Object(request).to_string_compact()
2037 }
2038
2039 fn query_request_with_params(id: u64, sql: &str, binds: Vec<Value>) -> String {
2040 let mut params = json::Map::new();
2041 params.insert("sql".to_string(), Value::String(sql.to_string()));
2042 params.insert("params".to_string(), Value::Array(binds));
2043
2044 let mut request = json::Map::new();
2045 request.insert("jsonrpc".to_string(), Value::String("2.0".to_string()));
2046 request.insert("id".to_string(), Value::Number(id as f64));
2047 request.insert("method".to_string(), Value::String("query".to_string()));
2048 request.insert("params".to_string(), Value::Object(params));
2049 Value::Object(request).to_string_compact()
2050 }
2051
2052 fn with_session<F>(rt: &RedDBRuntime, f: F)
2055 where
2056 F: FnOnce(&dyn Fn(&str) -> String, &RedDBRuntime),
2057 {
2058 let session = std::cell::RefCell::new(Session::new());
2059 let call = |line: &str| -> String {
2060 let mut s = session.borrow_mut();
2061 handle_line(&Backend::Local(rt), &mut s, line)
2062 };
2063 f(&call, rt);
2064 }
2065
2066 fn result_rows(response: &str) -> Vec<Value> {
2067 json::from_str::<Value>(response)
2068 .expect("json response")
2069 .get("result")
2070 .and_then(|result| result.get("rows"))
2071 .and_then(Value::as_array)
2072 .map(|rows| rows.to_vec())
2073 .unwrap_or_default()
2074 }
2075
2076 fn result_name_kind(response: &str) -> Vec<(String, String)> {
2077 result_rows(response)
2078 .into_iter()
2079 .map(|row| {
2080 let object = row.as_object().expect("row object");
2081 let name = object
2082 .get("name")
2083 .and_then(Value::as_str)
2084 .expect("row name")
2085 .to_string();
2086 let kind = object
2087 .get("kind")
2088 .and_then(Value::as_str)
2089 .expect("row kind")
2090 .to_string();
2091 (name, kind)
2092 })
2093 .collect()
2094 }
2095
2096 fn json_scalar_param() -> impl Strategy<Value = Value> {
2097 prop_oneof![
2098 Just(Value::Null),
2099 any::<bool>().prop_map(Value::Bool),
2100 (-1000_i64..1000_i64).prop_map(|n| Value::Number(n as f64)),
2101 "[a-z']{0,8}".prop_map(Value::String),
2102 ]
2103 }
2104
2105 fn sql_literal_for_json(value: &Value) -> String {
2106 match value {
2107 Value::Null => "NULL".to_string(),
2108 Value::Bool(true) => "TRUE".to_string(),
2109 Value::Bool(false) => "FALSE".to_string(),
2110 Value::Number(n) => format!("{n:.0}"),
2111 Value::String(s) => format!("'{}'", s.replace('\'', "''")),
2112 _ => panic!("unsupported scalar param: {value:?}"),
2113 }
2114 }
2115
2116 #[test]
2117 fn version_method_returns_version_and_protocol() {
2118 let rt = make_runtime();
2119 let line = r#"{"jsonrpc":"2.0","id":1,"method":"version","params":{}}"#;
2120 let resp = handle(&rt, line);
2121 assert!(resp.contains("\"id\":1"));
2122 assert!(resp.contains("\"protocol\":\"1.0\""));
2123 assert!(resp.contains("\"version\""));
2124 }
2125
2126 #[test]
2127 fn health_method_returns_ok_true() {
2128 let rt = make_runtime();
2129 let resp = handle(
2130 &rt,
2131 r#"{"jsonrpc":"2.0","id":"abc","method":"health","params":{}}"#,
2132 );
2133 assert!(resp.contains("\"ok\":true"));
2134 assert!(resp.contains("\"id\":\"abc\""));
2135 }
2136
2137 #[test]
2138 fn parse_error_for_invalid_json() {
2139 let rt = make_runtime();
2140 let resp = handle(&rt, "not json {");
2141 assert!(resp.contains("\"code\":\"PARSE_ERROR\""));
2142 assert!(resp.contains("\"id\":null"));
2143 }
2144
2145 #[test]
2146 fn invalid_request_when_method_missing() {
2147 let rt = make_runtime();
2148 let resp = handle(&rt, r#"{"jsonrpc":"2.0","id":1,"params":{}}"#);
2149 assert!(resp.contains("\"code\":\"INVALID_REQUEST\""));
2150 }
2151
2152 #[test]
2153 fn unknown_method_is_invalid_request() {
2154 let rt = make_runtime();
2155 let resp = handle(
2156 &rt,
2157 r#"{"jsonrpc":"2.0","id":1,"method":"frobnicate","params":{}}"#,
2158 );
2159 assert!(resp.contains("\"code\":\"INVALID_REQUEST\""));
2160 assert!(resp.contains("frobnicate"));
2161 }
2162
2163 #[test]
2164 fn invalid_params_when_query_sql_missing() {
2165 let rt = make_runtime();
2166 let resp = handle(
2167 &rt,
2168 r#"{"jsonrpc":"2.0","id":1,"method":"query","params":{}}"#,
2169 );
2170 assert!(resp.contains("\"code\":\"INVALID_PARAMS\""));
2171 }
2172
2173 #[test]
2174 fn query_params_reject_superseded_exact_number_envelope() {
2175 let rt = make_runtime();
2176 let resp = handle(
2177 &rt,
2178 r#"{"jsonrpc":"2.0","id":1,"method":"query","params":{"sql":"SELECT $1","params":[{"$number":"1"}]}}"#,
2179 );
2180 assert!(resp.contains("\"code\":\"INVALID_PARAMS\""), "got: {resp}");
2181 assert!(
2182 resp.contains("superseded exact-number envelope"),
2183 "got: {resp}"
2184 );
2185 }
2186
2187 #[test]
2188 fn close_method_marks_response_for_shutdown() {
2189 let rt = make_runtime();
2190 let resp = handle(
2191 &rt,
2192 r#"{"jsonrpc":"2.0","id":1,"method":"close","params":{}}"#,
2193 );
2194 assert!(resp.contains("\"__close__\":true"));
2195 }
2196
2197 #[test]
2198 fn query_with_int_text_params_round_trips() {
2199 let rt = make_runtime();
2200 let _ = handle(
2201 &rt,
2202 r#"{"jsonrpc":"2.0","id":1,"method":"query","params":{"sql":"CREATE TABLE p (id INTEGER, name TEXT)"}}"#,
2203 );
2204 let _ = handle(
2205 &rt,
2206 r#"{"jsonrpc":"2.0","id":2,"method":"query","params":{"sql":"INSERT INTO p (id, name) VALUES (1, 'Alice')"}}"#,
2207 );
2208 let _ = handle(
2209 &rt,
2210 r#"{"jsonrpc":"2.0","id":3,"method":"query","params":{"sql":"INSERT INTO p (id, name) VALUES (2, 'Bob')"}}"#,
2211 );
2212 let resp = handle(
2213 &rt,
2214 r#"{"jsonrpc":"2.0","id":4,"method":"query","params":{"sql":"SELECT * FROM p WHERE id = $1 AND name = $2","params":[1,"Alice"]}}"#,
2215 );
2216 assert!(resp.contains("\"Alice\""), "got: {resp}");
2217 assert!(!resp.contains("\"Bob\""), "got: {resp}");
2218 }
2219
2220 #[test]
2221 fn query_with_question_params_covers_select_insert_update_delete() {
2222 let rt = make_runtime();
2223 let create = handle(
2224 &rt,
2225 &query_request(1, "CREATE TABLE qp (id INTEGER, name TEXT)"),
2226 );
2227 assert!(!create.contains("\"error\""), "got: {create}");
2228
2229 let inserted = handle(
2230 &rt,
2231 &query_request_with_params(
2232 2,
2233 "INSERT INTO qp (id, name) VALUES (?, ?)",
2234 vec![json!(1), json!("O'Reilly")],
2235 ),
2236 );
2237 assert!(inserted.contains("\"affected\":1"), "got: {inserted}");
2238
2239 let selected = handle(
2240 &rt,
2241 &query_request_with_params(3, "SELECT name FROM qp WHERE id = ?", vec![json!(1)]),
2242 );
2243 let rows = result_rows(&selected);
2244 assert_eq!(rows.len(), 1, "got: {selected}");
2245 assert_eq!(
2246 rows[0].get("name").and_then(Value::as_str),
2247 Some("O'Reilly")
2248 );
2249
2250 let selected_numbered = handle(
2251 &rt,
2252 &query_request_with_params(
2253 4,
2254 "SELECT name FROM qp WHERE name = ?1 AND id = ?2",
2255 vec![json!("O'Reilly"), json!(1)],
2256 ),
2257 );
2258 assert_eq!(
2259 result_rows(&selected_numbered).len(),
2260 1,
2261 "got: {selected_numbered}"
2262 );
2263
2264 let updated = handle(
2265 &rt,
2266 &query_request_with_params(
2267 5,
2268 "UPDATE qp SET name = ? WHERE id = ?",
2269 vec![json!("Alice"), json!(1)],
2270 ),
2271 );
2272 assert!(updated.contains("\"affected\":1"), "got: {updated}");
2273
2274 let deleted = handle(
2275 &rt,
2276 &query_request_with_params(6, "DELETE FROM qp WHERE name = ?", vec![json!("Alice")]),
2277 );
2278 assert!(deleted.contains("\"affected\":1"), "got: {deleted}");
2279
2280 let remaining = handle(&rt, &query_request(7, "SELECT * FROM qp"));
2281 assert!(result_rows(&remaining).is_empty(), "got: {remaining}");
2282 }
2283
2284 #[test]
2285 fn query_with_params_insert_and_search_round_trip() {
2286 let rt = make_runtime();
2287 let insert = handle(
2288 &rt,
2289 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"]}}"#,
2290 );
2291 assert!(insert.contains("\"affected\":1"), "got: {insert}");
2292
2293 let search = handle(
2294 &rt,
2295 r#"{"jsonrpc":"2.0","id":2,"method":"query","params":{"sql":"SEARCH SIMILAR $1 COLLECTION bun_embeddings LIMIT 1","params":[[1.0,0.0]]}}"#,
2296 );
2297 assert!(search.contains("\"rows\""), "got: {search}");
2298 assert!(search.contains("\"score\":1"), "got: {search}");
2299 assert!(!search.contains("\"error\""), "got: {search}");
2300 }
2301
2302 #[test]
2303 fn query_with_question_vector_param_round_trips() {
2304 let rt = make_runtime();
2305 let insert = handle(
2306 &rt,
2307 &query_request_with_params(
2308 1,
2309 "INSERT INTO question_embeddings VECTOR (dense, content) VALUES (?, ?)",
2310 vec![json!([1.0, 0.0]), json!("question vector")],
2311 ),
2312 );
2313 assert!(insert.contains("\"affected\":1"), "got: {insert}");
2314
2315 let search = handle(
2316 &rt,
2317 &query_request_with_params(
2318 2,
2319 "SEARCH SIMILAR ? COLLECTION question_embeddings LIMIT 1",
2320 vec![json!([1.0, 0.0])],
2321 ),
2322 );
2323 assert!(search.contains("\"rows\""), "got: {search}");
2324 assert!(search.contains("\"score\":1"), "got: {search}");
2325 assert!(!search.contains("\"error\""), "got: {search}");
2326 }
2327
2328 #[test]
2329 fn query_with_typed_json_rpc_params_round_trips() {
2330 let rt = make_runtime();
2331 let create = handle(
2332 &rt,
2333 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)"}}"#,
2334 );
2335 assert!(!create.contains("\"error\""), "got: {create}");
2336
2337 let insert = handle(
2338 &rt,
2339 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"}]}}"#,
2340 );
2341 assert!(insert.contains("\"affected\":1"), "got: {insert}");
2342
2343 let selected = handle(
2344 &rt,
2345 r#"{"jsonrpc":"2.0","id":3,"method":"query","params":{"sql":"SELECT * FROM value_params"}}"#,
2346 );
2347 assert!(selected.contains("\"ok\":true"), "got: {selected}");
2348 assert!(selected.contains("\"$float\":\"NaN\""), "got: {selected}");
2349 assert!(
2350 selected.contains("\"$bytes\":\"3q2+7w==\""),
2351 "got: {selected}"
2352 );
2353 assert!(
2354 selected.contains("\"body\":{\"a\":null,\"z\":[1,{\"a\":true}]}"),
2355 "got: {selected}"
2356 );
2357 assert!(
2358 selected.contains("\"$ts\":\"1700000000123456789\""),
2359 "got: {selected}"
2360 );
2361 assert!(
2362 selected.contains("\"$uuid\":\"00112233-4455-6677-8899-aabbccddeeff\""),
2363 "got: {selected}"
2364 );
2365 }
2366
2367 #[test]
2368 fn select_timeseries_tags_decodes_json_payload() {
2369 let rt = make_runtime();
2370 let create = handle(&rt, &query_request(1, "CREATE TIMESERIES ts1"));
2371 assert!(!create.contains("\"error\""), "got: {create}");
2372
2373 let insert = handle(
2374 &rt,
2375 &query_request(
2376 2,
2377 r#"INSERT INTO ts1 (metric, value, tags, timestamp) VALUES ('cpu', 85, '{"host":"a"}', 1000)"#,
2378 ),
2379 );
2380 assert!(insert.contains("\"affected\":1"), "got: {insert}");
2381
2382 let selected = handle(&rt, &query_request(3, "SELECT tags FROM ts1"));
2383 assert!(!selected.contains("<json"), "got: {selected}");
2384 let response = json::from_str::<Value>(&selected).expect("response json");
2385 let tags = response
2386 .get("result")
2387 .and_then(|result| result.get("rows"))
2388 .and_then(Value::as_array)
2389 .and_then(|rows| rows.first())
2390 .and_then(|row| row.get("tags"))
2391 .expect("tags field");
2392 assert_eq!(tags, &json!({"host": "a"}));
2393 }
2394
2395 #[test]
2396 fn select_table_json_column_round_trips_after_single_parse() {
2397 let rt = make_runtime();
2398 let create = handle(&rt, &query_request(1, "CREATE TABLE docs (payload JSON)"));
2399 assert!(!create.contains("\"error\""), "got: {create}");
2400
2401 let original = r#"{"nested":{"items":[1,true,"x"],"object":{"k":"v"}}}"#;
2402 let insert_sql = format!("INSERT INTO docs (payload) VALUES ({original})");
2403 let insert = handle(&rt, &query_request(2, &insert_sql));
2404 assert!(insert.contains("\"affected\":1"), "got: {insert}");
2405
2406 let selected = handle(&rt, &query_request(3, "SELECT payload FROM docs"));
2407 assert!(!selected.contains("<json"), "got: {selected}");
2408 let response = json::from_str::<Value>(&selected).expect("response json");
2409 let payload = response
2410 .get("result")
2411 .and_then(|result| result.get("rows"))
2412 .and_then(Value::as_array)
2413 .and_then(|rows| rows.first())
2414 .and_then(|row| row.get("payload"))
2415 .expect("payload field");
2416 let expected = json::from_str::<Value>(original).expect("expected json");
2417 assert_eq!(payload, &expected);
2418
2419 let payload_text = payload.to_string_compact();
2420 assert_eq!(
2421 json::from_str::<Value>(&payload_text).expect("single parse"),
2422 expected
2423 );
2424 }
2425
2426 #[test]
2427 fn select_json_corruption_falls_back_to_code_and_hex() {
2428 use crate::storage::query::unified::UnifiedResult;
2429
2430 let mut result = UnifiedResult::with_columns(vec!["payload".into()]);
2431 let mut record = UnifiedRecord::new();
2432 record.set("payload", SchemaValue::Json(b"{not json".to_vec()));
2433 result.push(record);
2434
2435 let json = query_result_to_json(&RuntimeQueryResult {
2436 query: "SELECT payload FROM docs".to_string(),
2437 mode: crate::storage::query::modes::QueryMode::Sql,
2438 statement: "select",
2439 engine: "runtime-table",
2440 result,
2441 affected_rows: 0,
2442 statement_type: "select",
2443 bookmark: None,
2444 notice: None,
2445 });
2446
2447 let payload = json
2448 .get("rows")
2449 .and_then(Value::as_array)
2450 .and_then(|rows| rows.first())
2451 .and_then(|row| row.get("payload"))
2452 .expect("payload field");
2453 assert_eq!(
2454 payload.get("code").and_then(Value::as_str),
2455 Some("INVALID_JSON")
2456 );
2457 assert_eq!(
2458 payload.get("hex").and_then(Value::as_str),
2459 Some("7b6e6f74206a736f6e")
2460 );
2461 }
2462
2463 #[test]
2464 fn json_value_to_schema_value_decodes_typed_envelopes() {
2465 let SchemaValue::Blob(bytes) = json_value_to_schema_value(&json!({ "$bytes": "AAECAw==" }))
2466 else {
2467 panic!("expected blob");
2468 };
2469 assert_eq!(bytes, vec![0, 1, 2, 3]);
2470
2471 assert_eq!(
2472 json_value_to_schema_value(&json!({ "$ts": "9223372036854775807" })),
2473 SchemaValue::Timestamp(i64::MAX)
2474 );
2475
2476 let SchemaValue::Uuid(bytes) = json_value_to_schema_value(&json!({
2477 "$uuid": "00112233-4455-6677-8899-aabbccddeeff"
2478 })) else {
2479 panic!("expected uuid");
2480 };
2481 assert_eq!(
2482 bytes,
2483 [
2484 0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xaa, 0xbb, 0xcc, 0xdd,
2485 0xee, 0xff
2486 ]
2487 );
2488
2489 let SchemaValue::Float(value) =
2490 json_value_to_schema_value(&json!({ "$float": "-Infinity" }))
2491 else {
2492 panic!("expected float");
2493 };
2494 assert!(value.is_infinite() && value.is_sign_negative());
2495
2496 assert_eq!(
2497 json_value_to_schema_value(&json!({ "$int": "9007199254740993" })),
2498 SchemaValue::Integer(9_007_199_254_740_993)
2499 );
2500 assert_eq!(
2501 json_value_to_schema_value(&json!({ "$uint": "9223372036854775808" })),
2502 SchemaValue::UnsignedInteger(i64::MAX as u64 + 1)
2503 );
2504 assert_eq!(
2505 json_value_to_schema_value(&json!({ "$decimal": "3.14159265358979323846" })),
2506 SchemaValue::DecimalText("3.14159265358979323846".to_string())
2507 );
2508 }
2509
2510 #[test]
2511 fn query_result_json_preserves_exact_numbers() {
2512 use crate::storage::query::unified::UnifiedResult;
2513
2514 let mut result =
2515 UnifiedResult::with_columns(vec!["big".into(), "unsigned".into(), "decimal".into()]);
2516 let mut record = UnifiedRecord::new();
2517 record.set("big", SchemaValue::Integer(9_007_199_254_740_993));
2518 record.set(
2519 "unsigned",
2520 SchemaValue::UnsignedInteger(i64::MAX as u64 + 1),
2521 );
2522 record.set(
2523 "decimal",
2524 SchemaValue::DecimalText("3.14159265358979323846".to_string()),
2525 );
2526 result.push(record);
2527
2528 let json = query_result_to_json(&RuntimeQueryResult {
2529 query: "SELECT exact".to_string(),
2530 mode: crate::storage::query::modes::QueryMode::Sql,
2531 statement: "select",
2532 engine: "runtime-table",
2533 result,
2534 affected_rows: 0,
2535 statement_type: "select",
2536 bookmark: None,
2537 notice: None,
2538 });
2539
2540 let row = json
2541 .get("rows")
2542 .and_then(Value::as_array)
2543 .and_then(|rows| rows.first())
2544 .expect("row");
2545 assert_eq!(
2546 row.get("big"),
2547 Some(&single_key_object(
2548 "$int",
2549 Value::String("9007199254740993".to_string())
2550 ))
2551 );
2552 assert_eq!(
2553 row.get("unsigned"),
2554 Some(&single_key_object(
2555 "$uint",
2556 Value::String("9223372036854775808".to_string())
2557 ))
2558 );
2559 assert_eq!(
2560 row.get("decimal"),
2561 Some(&single_key_object(
2562 "$decimal",
2563 Value::String("3.14159265358979323846".to_string())
2564 ))
2565 );
2566 }
2567
2568 #[test]
2569 fn query_with_params_arity_mismatch_rejected() {
2570 let rt = make_runtime();
2571 let _ = handle(
2572 &rt,
2573 r#"{"jsonrpc":"2.0","id":1,"method":"query","params":{"sql":"CREATE TABLE pa (id INTEGER)"}}"#,
2574 );
2575 let resp = handle(
2576 &rt,
2577 r#"{"jsonrpc":"2.0","id":2,"method":"query","params":{"sql":"SELECT * FROM pa WHERE id = $1","params":[1,2]}}"#,
2578 );
2579 assert!(resp.contains("\"INVALID_PARAMS\""), "got: {resp}");
2580 }
2581
2582 #[test]
2583 fn query_with_question_params_arity_mismatch_rejected() {
2584 let rt = make_runtime();
2585 let _ = handle(&rt, &query_request(1, "CREATE TABLE qpa (id INTEGER)"));
2586 let resp = handle(
2587 &rt,
2588 &query_request_with_params(
2589 2,
2590 "SELECT * FROM qpa WHERE id = ?",
2591 vec![json!(1), json!(2)],
2592 ),
2593 );
2594 assert!(resp.contains("\"INVALID_PARAMS\""), "got: {resp}");
2595 assert!(resp.contains("SQL expects 1, got 2"), "got: {resp}");
2596 }
2597
2598 #[test]
2599 fn query_with_params_gap_rejected() {
2600 let rt = make_runtime();
2601 let _ = handle(
2602 &rt,
2603 r#"{"jsonrpc":"2.0","id":1,"method":"query","params":{"sql":"CREATE TABLE pg (a INTEGER, b INTEGER)"}}"#,
2604 );
2605 let resp = handle(
2606 &rt,
2607 r#"{"jsonrpc":"2.0","id":2,"method":"query","params":{"sql":"SELECT * FROM pg WHERE a = $1 AND b = $3","params":[1,2,3]}}"#,
2608 );
2609 assert!(resp.contains("\"INVALID_PARAMS\""), "got: {resp}");
2610 }
2611
2612 #[test]
2613 fn query_with_question_numbered_gap_rejected() {
2614 let rt = make_runtime();
2615 let _ = handle(&rt, &query_request(1, "CREATE TABLE qpg (id INTEGER)"));
2616 let resp = handle(
2617 &rt,
2618 &query_request_with_params(
2619 2,
2620 "SELECT * FROM qpg WHERE id = ?2",
2621 vec![json!(1), json!(2)],
2622 ),
2623 );
2624 assert!(resp.contains("\"INVALID_PARAMS\""), "got: {resp}");
2625 assert!(resp.contains("parameter $`1` is missing"), "got: {resp}");
2626 }
2627
2628 #[test]
2629 fn query_with_question_params_type_mismatch_names_slot() {
2630 let rt = make_runtime();
2631 let _ = handle(&rt, &query_request(1, "CREATE TABLE qpt (id INTEGER)"));
2632 let resp = handle(
2633 &rt,
2634 &query_request_with_params(
2635 2,
2636 "INSERT INTO qpt (id) VALUES (?)",
2637 vec![json!("not-an-integer")],
2638 ),
2639 );
2640 assert!(resp.contains("\"QUERY_ERROR\""), "got: {resp}");
2641 assert!(resp.contains("id"), "got: {resp}");
2642 assert!(resp.contains("integer"), "got: {resp}");
2643 }
2644
2645 #[test]
2646 fn query_select_one_returns_rows() {
2647 let rt = make_runtime();
2648 let resp = handle(
2649 &rt,
2650 r#"{"jsonrpc":"2.0","id":1,"method":"query","params":{"sql":"SELECT 1 AS one"}}"#,
2651 );
2652 assert!(resp.contains("\"result\""));
2653 assert!(!resp.contains("\"error\""));
2654 }
2655
2656 #[test]
2657 fn ask_query_result_uses_canonical_envelope() {
2658 use crate::storage::query::unified::UnifiedResult;
2659
2660 let mut result = UnifiedResult::with_columns(vec![
2661 "answer".into(),
2662 "provider".into(),
2663 "model".into(),
2664 "prompt_tokens".into(),
2665 "completion_tokens".into(),
2666 "sources_count".into(),
2667 "sources_flat".into(),
2668 "citations".into(),
2669 "validation".into(),
2670 ]);
2671 let mut record = UnifiedRecord::new();
2672 record.set("answer", SchemaValue::text("Deploy failed [^1]."));
2673 record.set("provider", SchemaValue::text("openai"));
2674 record.set("model", SchemaValue::text("gpt-4o-mini"));
2675 record.set("prompt_tokens", SchemaValue::Integer(11));
2676 record.set("completion_tokens", SchemaValue::Integer(7));
2677 record.set(
2678 "sources_flat",
2679 SchemaValue::Json(
2680 br#"[{"urn":"urn:reddb:row:deployments:1","kind":"row","collection":"deployments","id":"1"}]"#.to_vec(),
2681 ),
2682 );
2683 record.set(
2684 "citations",
2685 SchemaValue::Json(br#"[{"marker":1,"urn":"urn:reddb:row:deployments:1"}]"#.to_vec()),
2686 );
2687 record.set(
2688 "validation",
2689 SchemaValue::Json(br#"{"ok":true,"warnings":[],"errors":[]}"#.to_vec()),
2690 );
2691 result.push(record);
2692
2693 let json = query_result_to_json(&RuntimeQueryResult {
2694 query: "ASK 'why did deploy fail?'".to_string(),
2695 mode: crate::storage::query::modes::QueryMode::Sql,
2696 statement: "ask",
2697 engine: "runtime-ai",
2698 result,
2699 affected_rows: 0,
2700 statement_type: "select",
2701 bookmark: None,
2702 notice: None,
2703 });
2704
2705 assert_eq!(
2706 json.get("answer").and_then(Value::as_str),
2707 Some("Deploy failed [^1].")
2708 );
2709 assert_eq!(json.get("cache_hit").and_then(Value::as_bool), Some(false));
2710 assert_eq!(json.get("cost_usd").and_then(Value::as_f64), Some(0.0));
2711 assert_eq!(json.get("mode").and_then(Value::as_str), Some("strict"));
2712 assert_eq!(json.get("retry_count").and_then(Value::as_u64), Some(0));
2713 assert!(
2714 json.get("rows").is_none(),
2715 "ASK envelope must not be row-wrapped: {json}"
2716 );
2717 assert!(
2718 json.get("sources_flat")
2719 .and_then(Value::as_array)
2720 .is_some_and(|sources| sources.len() == 1
2721 && sources[0].get("payload").and_then(Value::as_str).is_some()),
2722 "sources_flat must be a parsed array: {json}"
2723 );
2724 assert!(
2725 json.get("citations")
2726 .and_then(Value::as_array)
2727 .is_some_and(|citations| citations.len() == 1),
2728 "citations must be a parsed array: {json}"
2729 );
2730 assert_eq!(
2731 json.get("validation")
2732 .and_then(|v| v.get("ok"))
2733 .and_then(Value::as_bool),
2734 Some(true)
2735 );
2736 }
2737
2738 #[test]
2743 fn tx_begin_returns_tx_id_and_isolation() {
2744 let rt = make_runtime();
2745 with_session(&rt, |call, _| {
2746 let resp = call(r#"{"jsonrpc":"2.0","id":1,"method":"tx.begin","params":null}"#);
2747 assert!(resp.contains("\"tx_id\":1"));
2748 assert!(resp.contains("\"isolation\":\"read_committed_deferred\""));
2749 assert!(!resp.contains("\"error\""));
2750 });
2751 }
2752
2753 #[test]
2754 fn tx_begin_twice_returns_already_open() {
2755 let rt = make_runtime();
2756 with_session(&rt, |call, _| {
2757 let _ = call(r#"{"jsonrpc":"2.0","id":1,"method":"tx.begin","params":null}"#);
2758 let resp = call(r#"{"jsonrpc":"2.0","id":2,"method":"tx.begin","params":null}"#);
2759 assert!(resp.contains("\"code\":\"TX_ALREADY_OPEN\""));
2760 });
2761 }
2762
2763 #[test]
2764 fn tx_commit_without_begin_returns_no_tx_open() {
2765 let rt = make_runtime();
2766 with_session(&rt, |call, _| {
2767 let resp = call(r#"{"jsonrpc":"2.0","id":1,"method":"tx.commit","params":null}"#);
2768 assert!(resp.contains("\"code\":\"NO_TX_OPEN\""));
2769 });
2770 }
2771
2772 #[test]
2773 fn tx_rollback_without_begin_returns_no_tx_open() {
2774 let rt = make_runtime();
2775 with_session(&rt, |call, _| {
2776 let resp = call(r#"{"jsonrpc":"2.0","id":1,"method":"tx.rollback","params":null}"#);
2777 assert!(resp.contains("\"code\":\"NO_TX_OPEN\""));
2778 });
2779 }
2780
2781 #[test]
2782 fn insert_inside_tx_returns_pending_envelope() {
2783 let rt = make_runtime();
2784 let _ = handle(
2786 &rt,
2787 r#"{"jsonrpc":"2.0","id":1,"method":"query","params":{"sql":"CREATE TABLE users (name TEXT)"}}"#,
2788 );
2789 with_session(&rt, |call, _| {
2790 let _ = call(r#"{"jsonrpc":"2.0","id":1,"method":"tx.begin","params":null}"#);
2791 let resp = call(
2792 r#"{"jsonrpc":"2.0","id":2,"method":"insert","params":{"collection":"users","payload":{"name":"alice"}}}"#,
2793 );
2794 assert!(resp.contains("\"pending\":true"));
2795 assert!(resp.contains("\"tx_id\":1"));
2796 assert!(resp.contains("\"affected\":0"));
2797 });
2798 }
2799
2800 #[test]
2801 fn begin_insert_rollback_does_not_persist() {
2802 let rt = make_runtime();
2803 let _ = handle(
2804 &rt,
2805 r#"{"jsonrpc":"2.0","id":1,"method":"query","params":{"sql":"CREATE TABLE u (name TEXT)"}}"#,
2806 );
2807 with_session(&rt, |call, _| {
2808 let _ = call(r#"{"jsonrpc":"2.0","id":1,"method":"tx.begin","params":null}"#);
2809 let _ = call(
2810 r#"{"jsonrpc":"2.0","id":2,"method":"insert","params":{"collection":"u","payload":{"name":"ghost"}}}"#,
2811 );
2812 let rollback = call(r#"{"jsonrpc":"2.0","id":3,"method":"tx.rollback","params":null}"#);
2813 assert!(rollback.contains("\"ops_discarded\":1"));
2814 assert!(rollback.contains("\"tx_id\":1"));
2815 });
2816 let resp = handle(
2818 &rt,
2819 r#"{"jsonrpc":"2.0","id":9,"method":"query","params":{"sql":"SELECT * FROM u"}}"#,
2820 );
2821 assert!(!resp.contains("\"ghost\""));
2822 }
2823
2824 #[test]
2825 fn begin_insert_commit_persists() {
2826 let rt = make_runtime();
2827 let _ = handle(
2828 &rt,
2829 r#"{"jsonrpc":"2.0","id":1,"method":"query","params":{"sql":"CREATE TABLE u2 (name TEXT)"}}"#,
2830 );
2831 with_session(&rt, |call, _| {
2832 let _ = call(r#"{"jsonrpc":"2.0","id":1,"method":"tx.begin","params":null}"#);
2833 let _ = call(
2834 r#"{"jsonrpc":"2.0","id":2,"method":"insert","params":{"collection":"u2","payload":{"name":"alice"}}}"#,
2835 );
2836 let _ = call(
2837 r#"{"jsonrpc":"2.0","id":3,"method":"insert","params":{"collection":"u2","payload":{"name":"bob"}}}"#,
2838 );
2839 let commit = call(r#"{"jsonrpc":"2.0","id":4,"method":"tx.commit","params":null}"#);
2840 assert!(commit.contains("\"ops_replayed\":2"));
2841 assert!(!commit.contains("\"error\""));
2842 });
2843 let resp = handle(
2844 &rt,
2845 r#"{"jsonrpc":"2.0","id":9,"method":"query","params":{"sql":"SELECT * FROM u2"}}"#,
2846 );
2847 assert!(resp.contains("\"alice\""));
2848 assert!(resp.contains("\"bob\""));
2849 }
2850
2851 #[test]
2852 fn bulk_insert_inside_tx_buffers_everything() {
2853 let rt = make_runtime();
2854 let _ = handle(
2855 &rt,
2856 r#"{"jsonrpc":"2.0","id":1,"method":"query","params":{"sql":"CREATE TABLE u3 (name TEXT)"}}"#,
2857 );
2858 with_session(&rt, |call, _| {
2859 let _ = call(r#"{"jsonrpc":"2.0","id":1,"method":"tx.begin","params":null}"#);
2860 let resp = call(
2861 r#"{"jsonrpc":"2.0","id":2,"method":"bulk_insert","params":{"collection":"u3","payloads":[{"name":"a"},{"name":"b"},{"name":"c"}]}}"#,
2862 );
2863 assert!(resp.contains("\"buffered\":3"));
2864 assert!(resp.contains("\"pending\":true"));
2865 assert!(resp.contains("\"affected\":0"));
2866
2867 let commit = call(r#"{"jsonrpc":"2.0","id":3,"method":"tx.commit","params":null}"#);
2868 assert!(commit.contains("\"ops_replayed\":3"));
2869 });
2870 }
2871
2872 #[test]
2873 fn bulk_insert_chunks_at_internal_500_row_limit() {
2874 assert_eq!(bulk_insert_chunk_count(0), 0);
2875 assert_eq!(bulk_insert_chunk_count(1), 1);
2876 assert_eq!(bulk_insert_chunk_count(500), 1);
2877 assert_eq!(bulk_insert_chunk_count(501), 2);
2878 assert_eq!(bulk_insert_chunk_count(1000), 2);
2879 assert_eq!(bulk_insert_chunk_count(1001), 3);
2880 }
2881
2882 proptest! {
2883 #![proptest_config(ProptestConfig {
2884 cases: 12,
2885 ..ProptestConfig::default()
2886 })]
2887
2888 #[test]
2889 fn bulk_insert_matches_sequential_insert_state(
2890 names in proptest::collection::vec("[a-z]{1,8}", 1usize..20)
2891 ) {
2892 let rt = make_runtime();
2893 let payloads = names
2894 .iter()
2895 .map(|name| format!(r#"{{"name":"{name}","kind":"bulk"}}"#))
2896 .collect::<Vec<_>>();
2897 let payload_array = payloads.join(",");
2898
2899 let bulk = handle(
2900 &rt,
2901 &format!(
2902 r#"{{"jsonrpc":"2.0","id":1,"method":"bulk_insert","params":{{"collection":"bulk_prop","payloads":[{payload_array}]}}}}"#
2903 ),
2904 );
2905 let bulk_result = json::from_str::<Value>(&bulk).expect("bulk json");
2906 let bulk_ids = bulk_result
2907 .get("result")
2908 .and_then(|result| result.get("ids"))
2909 .and_then(Value::as_array)
2910 .expect("bulk ids");
2911 prop_assert_eq!(bulk_ids.len(), names.len());
2912
2913 for (index, payload) in payloads.iter().enumerate() {
2914 let insert = handle(
2915 &rt,
2916 &format!(
2917 r#"{{"jsonrpc":"2.0","id":{},"method":"insert","params":{{"collection":"seq_prop","payload":{payload}}}}}"#,
2918 index + 10
2919 ),
2920 );
2921 let insert_result = json::from_str::<Value>(&insert).expect("insert json");
2922 prop_assert!(
2923 insert_result
2924 .get("result")
2925 .and_then(|result| result.get("id"))
2926 .is_some(),
2927 "insert response missing id: {insert}"
2928 );
2929 }
2930
2931 let bulk_rows = result_name_kind(&handle(
2932 &rt,
2933 r#"{"jsonrpc":"2.0","id":99,"method":"query","params":{"sql":"SELECT name, kind FROM bulk_prop ORDER BY rid"}}"#,
2934 ));
2935 let seq_rows = result_name_kind(&handle(
2936 &rt,
2937 r#"{"jsonrpc":"2.0","id":100,"method":"query","params":{"sql":"SELECT name, kind FROM seq_prop ORDER BY rid"}}"#,
2938 ));
2939 prop_assert_eq!(bulk_rows, seq_rows);
2940 }
2941
2942 #[test]
2943 fn question_param_select_matches_inlined_literal(value in json_scalar_param()) {
2944 let rt = make_runtime();
2945 let bound = handle(
2946 &rt,
2947 &query_request_with_params(1, "SELECT ? AS v", vec![value.clone()]),
2948 );
2949 let inline_sql = format!("SELECT {} AS v", sql_literal_for_json(&value));
2950 let inlined = handle(&rt, &query_request(2, &inline_sql));
2951 prop_assert_eq!(
2952 result_rows(&bound),
2953 result_rows(&inlined),
2954 "bound={}, inlined={}",
2955 bound,
2956 inlined
2957 );
2958 }
2959 }
2960
2961 #[test]
2962 fn bulk_insert_graph_nodes_accepts_flat_rows_and_returns_ids() {
2963 let rt = make_runtime();
2964 create_graph_collection(&rt, "social");
2965
2966 let resp = handle(
2967 &rt,
2968 r#"{"jsonrpc":"2.0","id":2,"method":"bulk_insert","params":{"collection":"social","payloads":[{"label":"User","name":"alice"},{"label":"User","name":"bob"}]}}"#,
2969 );
2970 let envelope: Value = json::from_str(&resp).expect("json response");
2971 let result = envelope.get("result").expect("result");
2972 assert_eq!(result.get("affected").and_then(Value::as_u64), Some(2));
2973 assert_eq!(
2974 result
2975 .get("ids")
2976 .and_then(Value::as_array)
2977 .map(|ids| ids.len()),
2978 Some(2)
2979 );
2980
2981 let query = handle(
2982 &rt,
2983 r#"{"jsonrpc":"2.0","id":3,"method":"query","params":{"sql":"MATCH (n:User) RETURN n.name"}}"#,
2984 );
2985 assert!(query.contains("\"alice\""), "got: {query}");
2986 assert!(query.contains("\"bob\""), "got: {query}");
2987 }
2988
2989 #[test]
2990 fn bulk_insert_graph_edges_accepts_flat_rows_and_returns_ids() {
2991 let rt = make_runtime();
2992 create_graph_collection(&rt, "network");
2993 let nodes = handle(
2994 &rt,
2995 r#"{"jsonrpc":"2.0","id":2,"method":"bulk_insert","params":{"collection":"network","payloads":[{"label":"Host","name":"app"},{"label":"Host","name":"db"}]}}"#,
2996 );
2997 let envelope: Value = json::from_str(&nodes).expect("node response");
2998 let ids = envelope
2999 .get("result")
3000 .and_then(|r| r.get("ids"))
3001 .and_then(Value::as_array)
3002 .expect("node ids");
3003 let from = ids[0].as_u64().expect("from id");
3004 let to = ids[1].as_u64().expect("to id");
3005
3006 let resp = handle(
3007 &rt,
3008 &format!(
3009 r#"{{"jsonrpc":"2.0","id":3,"method":"bulk_insert","params":{{"collection":"network","payloads":[{{"label":"connects","from":{from},"to":{to},"weight":0.5,"role":"primary"}}]}}}}"#
3010 ),
3011 );
3012 let envelope: Value = json::from_str(&resp).expect("edge response");
3013 let result = envelope.get("result").expect("result");
3014 assert_eq!(result.get("affected").and_then(Value::as_u64), Some(1));
3015 assert_eq!(
3016 result
3017 .get("ids")
3018 .and_then(Value::as_array)
3019 .map(|ids| ids.len()),
3020 Some(1)
3021 );
3022 }
3023
3024 #[test]
3025 fn delete_inside_tx_is_buffered() {
3026 let rt = make_runtime();
3027 let _ = handle(
3029 &rt,
3030 r#"{"jsonrpc":"2.0","id":1,"method":"query","params":{"sql":"CREATE TABLE u4 (name TEXT)"}}"#,
3031 );
3032 let _ = handle(
3033 &rt,
3034 r#"{"jsonrpc":"2.0","id":2,"method":"query","params":{"sql":"INSERT INTO u4 (name) VALUES ('keep')"}}"#,
3035 );
3036 with_session(&rt, |call, _| {
3037 let _ = call(r#"{"jsonrpc":"2.0","id":1,"method":"tx.begin","params":null}"#);
3038 let resp = call(
3039 r#"{"jsonrpc":"2.0","id":2,"method":"delete","params":{"collection":"u4","id":"1"}}"#,
3040 );
3041 assert!(resp.contains("\"pending\":true"));
3042 let _ = call(r#"{"jsonrpc":"2.0","id":3,"method":"tx.rollback","params":null}"#);
3043 });
3044 let resp = handle(
3046 &rt,
3047 r#"{"jsonrpc":"2.0","id":9,"method":"query","params":{"sql":"SELECT * FROM u4"}}"#,
3048 );
3049 assert!(resp.contains("\"keep\""));
3050 }
3051
3052 #[test]
3053 fn close_with_open_tx_auto_rollbacks() {
3054 let rt = make_runtime();
3055 let _ = handle(
3056 &rt,
3057 r#"{"jsonrpc":"2.0","id":1,"method":"query","params":{"sql":"CREATE TABLE u5 (name TEXT)"}}"#,
3058 );
3059 with_session(&rt, |call, _| {
3060 let _ = call(r#"{"jsonrpc":"2.0","id":1,"method":"tx.begin","params":null}"#);
3061 let _ = call(
3062 r#"{"jsonrpc":"2.0","id":2,"method":"insert","params":{"collection":"u5","payload":{"name":"ghost"}}}"#,
3063 );
3064 let close = call(r#"{"jsonrpc":"2.0","id":3,"method":"close","params":null}"#);
3065 assert!(close.contains("\"__close__\":true"));
3066 assert!(!close.contains("\"error\""));
3067 });
3068 let resp = handle(
3069 &rt,
3070 r#"{"jsonrpc":"2.0","id":9,"method":"query","params":{"sql":"SELECT * FROM u5"}}"#,
3071 );
3072 assert!(!resp.contains("\"ghost\""));
3073 }
3074
3075 fn seed_numbers_table(rt: &RedDBRuntime, table: &str, count: u32) {
3080 let _ = handle(
3081 rt,
3082 &format!(
3083 r#"{{"jsonrpc":"2.0","id":1,"method":"query","params":{{"sql":"CREATE TABLE {table} (n INTEGER)"}}}}"#,
3084 ),
3085 );
3086 for i in 0..count {
3087 let _ = handle(
3088 rt,
3089 &format!(
3090 r#"{{"jsonrpc":"2.0","id":2,"method":"query","params":{{"sql":"INSERT INTO {table} (n) VALUES ({i})"}}}}"#,
3091 ),
3092 );
3093 }
3094 }
3095
3096 #[test]
3097 fn cursor_open_returns_id_columns_and_total() {
3098 let rt = make_runtime();
3099 seed_numbers_table(&rt, "nums1", 3);
3100 with_session(&rt, |call, _| {
3101 let resp = call(
3102 r#"{"jsonrpc":"2.0","id":1,"method":"query.open","params":{"sql":"SELECT n FROM nums1"}}"#,
3103 );
3104 assert!(resp.contains("\"cursor_id\":1"));
3105 assert!(resp.contains("\"total_rows\":3"));
3106 assert!(resp.contains("\"columns\""));
3107 assert!(!resp.contains("\"error\""));
3108 });
3109 }
3110
3111 #[test]
3112 fn cursor_next_chunks_rows_and_signals_done() {
3113 let rt = make_runtime();
3114 seed_numbers_table(&rt, "nums2", 5);
3115 with_session(&rt, |call, _| {
3116 let _ = call(
3117 r#"{"jsonrpc":"2.0","id":1,"method":"query.open","params":{"sql":"SELECT n FROM nums2"}}"#,
3118 );
3119 let first = call(
3120 r#"{"jsonrpc":"2.0","id":2,"method":"query.next","params":{"cursor_id":1,"batch_size":2}}"#,
3121 );
3122 assert!(first.contains("\"done\":false"));
3123 assert!(first.contains("\"remaining\":3"));
3124
3125 let second = call(
3126 r#"{"jsonrpc":"2.0","id":3,"method":"query.next","params":{"cursor_id":1,"batch_size":2}}"#,
3127 );
3128 assert!(second.contains("\"done\":false"));
3129 assert!(second.contains("\"remaining\":1"));
3130
3131 let third = call(
3132 r#"{"jsonrpc":"2.0","id":4,"method":"query.next","params":{"cursor_id":1,"batch_size":2}}"#,
3133 );
3134 assert!(third.contains("\"done\":true"));
3135 assert!(third.contains("\"remaining\":0"));
3136 });
3137 }
3138
3139 #[test]
3140 fn cursor_auto_drops_when_exhausted() {
3141 let rt = make_runtime();
3142 seed_numbers_table(&rt, "nums3", 2);
3143 with_session(&rt, |call, _| {
3144 let _ = call(
3145 r#"{"jsonrpc":"2.0","id":1,"method":"query.open","params":{"sql":"SELECT n FROM nums3"}}"#,
3146 );
3147 let _ = call(
3148 r#"{"jsonrpc":"2.0","id":2,"method":"query.next","params":{"cursor_id":1,"batch_size":100}}"#,
3149 );
3150 let resp = call(
3153 r#"{"jsonrpc":"2.0","id":3,"method":"query.next","params":{"cursor_id":1,"batch_size":100}}"#,
3154 );
3155 assert!(resp.contains("\"code\":\"CURSOR_NOT_FOUND\""));
3156 });
3157 }
3158
3159 #[test]
3160 fn cursor_close_removes_it() {
3161 let rt = make_runtime();
3162 seed_numbers_table(&rt, "nums4", 3);
3163 with_session(&rt, |call, _| {
3164 let _ = call(
3165 r#"{"jsonrpc":"2.0","id":1,"method":"query.open","params":{"sql":"SELECT n FROM nums4"}}"#,
3166 );
3167 let close =
3168 call(r#"{"jsonrpc":"2.0","id":2,"method":"query.close","params":{"cursor_id":1}}"#);
3169 assert!(close.contains("\"closed\":true"));
3170 let after = call(
3171 r#"{"jsonrpc":"2.0","id":3,"method":"query.next","params":{"cursor_id":1,"batch_size":10}}"#,
3172 );
3173 assert!(after.contains("\"code\":\"CURSOR_NOT_FOUND\""));
3174 });
3175 }
3176
3177 #[test]
3178 fn cursor_close_unknown_errors() {
3179 let rt = make_runtime();
3180 with_session(&rt, |call, _| {
3181 let resp = call(
3182 r#"{"jsonrpc":"2.0","id":1,"method":"query.close","params":{"cursor_id":9999}}"#,
3183 );
3184 assert!(resp.contains("\"code\":\"CURSOR_NOT_FOUND\""));
3185 });
3186 }
3187
3188 #[test]
3189 fn cursor_next_without_cursor_id_errors() {
3190 let rt = make_runtime();
3191 with_session(&rt, |call, _| {
3192 let resp = call(r#"{"jsonrpc":"2.0","id":1,"method":"query.next","params":{}}"#);
3193 assert!(resp.contains("\"code\":\"INVALID_PARAMS\""));
3194 });
3195 }
3196
3197 #[test]
3198 fn cursor_default_batch_size_returns_all_when_smaller_than_default() {
3199 let rt = make_runtime();
3200 seed_numbers_table(&rt, "nums5", 7);
3201 with_session(&rt, |call, _| {
3202 let _ = call(
3203 r#"{"jsonrpc":"2.0","id":1,"method":"query.open","params":{"sql":"SELECT n FROM nums5"}}"#,
3204 );
3205 let resp =
3207 call(r#"{"jsonrpc":"2.0","id":2,"method":"query.next","params":{"cursor_id":1}}"#);
3208 assert!(resp.contains("\"done\":true"));
3209 assert!(resp.contains("\"remaining\":0"));
3210 });
3211 }
3212
3213 #[test]
3214 fn close_method_drops_open_cursors() {
3215 let rt = make_runtime();
3216 seed_numbers_table(&rt, "nums6", 3);
3217 with_session(&rt, |call, _| {
3220 let _ = call(
3221 r#"{"jsonrpc":"2.0","id":1,"method":"query.open","params":{"sql":"SELECT n FROM nums6"}}"#,
3222 );
3223 let close = call(r#"{"jsonrpc":"2.0","id":2,"method":"close","params":null}"#);
3224 assert!(close.contains("\"__close__\":true"));
3225 let after = call(
3227 r#"{"jsonrpc":"2.0","id":3,"method":"query.next","params":{"cursor_id":1,"batch_size":10}}"#,
3228 );
3229 assert!(after.contains("\"code\":\"CURSOR_NOT_FOUND\""));
3230 });
3231 }
3232
3233 #[test]
3234 fn cursor_independent_of_transaction_state() {
3235 let rt = make_runtime();
3236 seed_numbers_table(&rt, "nums7", 4);
3237 with_session(&rt, |call, _| {
3238 let _ = call(
3240 r#"{"jsonrpc":"2.0","id":1,"method":"query.open","params":{"sql":"SELECT n FROM nums7"}}"#,
3241 );
3242 let _ = call(r#"{"jsonrpc":"2.0","id":2,"method":"tx.begin","params":null}"#);
3243 let _ = call(r#"{"jsonrpc":"2.0","id":3,"method":"tx.commit","params":null}"#);
3244 let resp = call(
3245 r#"{"jsonrpc":"2.0","id":4,"method":"query.next","params":{"cursor_id":1,"batch_size":10}}"#,
3246 );
3247 assert!(resp.contains("\"done\":true"));
3248 assert!(!resp.contains("\"error\""));
3249 });
3250 }
3251
3252 #[test]
3253 fn second_tx_after_commit_gets_fresh_id() {
3254 let rt = make_runtime();
3255 let _ = handle(
3256 &rt,
3257 r#"{"jsonrpc":"2.0","id":1,"method":"query","params":{"sql":"CREATE TABLE u6 (name TEXT)"}}"#,
3258 );
3259 with_session(&rt, |call, _| {
3260 let first = call(r#"{"jsonrpc":"2.0","id":1,"method":"tx.begin","params":null}"#);
3261 assert!(first.contains("\"tx_id\":1"));
3262 let _ = call(
3263 r#"{"jsonrpc":"2.0","id":2,"method":"insert","params":{"collection":"u6","payload":{"name":"x"}}}"#,
3264 );
3265 let _ = call(r#"{"jsonrpc":"2.0","id":3,"method":"tx.commit","params":null}"#);
3266
3267 let second = call(r#"{"jsonrpc":"2.0","id":4,"method":"tx.begin","params":null}"#);
3268 assert!(second.contains("\"tx_id\":2"));
3269 let _ = call(r#"{"jsonrpc":"2.0","id":5,"method":"tx.rollback","params":null}"#);
3270 });
3271 }
3272
3273 #[test]
3274 fn prepare_and_execute_prepared_statement() {
3275 let rt = make_runtime();
3276 let _ = handle(
3278 &rt,
3279 r#"{"jsonrpc":"2.0","id":1,"method":"query","params":{"sql":"CREATE TABLE ps_test (n INTEGER)"}}"#,
3280 );
3281 let _ = handle(
3282 &rt,
3283 r#"{"jsonrpc":"2.0","id":2,"method":"query","params":{"sql":"INSERT INTO ps_test (n) VALUES (42)"}}"#,
3284 );
3285
3286 with_session(&rt, |call, _| {
3287 let prep = call(
3289 r#"{"jsonrpc":"2.0","id":3,"method":"prepare","params":{"sql":"SELECT n FROM ps_test WHERE n = 42"}}"#,
3290 );
3291 assert!(prep.contains("\"prepared_id\""), "prepare response: {prep}");
3292
3293 let id: u64 = {
3295 let v: crate::json::Value = crate::json::from_str(&prep).expect("json");
3296 let result = v.get("result").expect("result");
3297 result
3298 .get("prepared_id")
3299 .and_then(|n| n.as_f64())
3300 .expect("prepared_id") as u64
3301 };
3302
3303 let exec = call(&format!(
3305 r#"{{"jsonrpc":"2.0","id":4,"method":"execute_prepared","params":{{"prepared_id":{id},"binds":[42]}}}}"#
3306 ));
3307 assert!(
3309 exec.contains("\"rows\""),
3310 "execute_prepared response: {exec}"
3311 );
3312 assert!(exec.contains("42"), "expected row with n=42 in: {exec}");
3313 });
3314 }
3315}