1use super::*;
2use crate::auth::column_policy_gate::ColumnAccessRequest;
3use crate::auth::UserId;
4use crate::catalog::CollectionModel;
5use crate::replication::cdc::ChangeRecord;
6use reddb_rql::ast::TableSource;
7pub(crate) use super::authz::policy_columns::*;
12
13pub(crate) use super::graph_tvf::{abstract_degree_centrality, is_graph_tvf_name};
18pub(crate) use super::rls_injection::{
19 apply_foreign_table_filters, inject_rls_filters, inject_rls_into_join, rls_is_enabled,
20 rls_policy_filter, rls_policy_filter_for_kind,
21};
22pub(crate) use super::vcs_command::{
28 parse_runtime_vcs_command, strip_explain_prefix, walk_collections,
29};
30
31pub(crate) use super::impl_audit_control::{
43 control_event_outcome_for_error, query_audit_plan, query_control_event_specs,
44};
45pub(crate) use super::impl_config_secret::{
47 insert_config_json_path, secret_sql_value_to_string, seed_storage_deploy_config,
48 show_config_json_result,
49};
50pub(super) use super::impl_core_outer_scope::{
51 first_column_values, query_references_outer_scope, relation_scopes_for_query,
52};
53pub(crate) use super::impl_core_result_cache_scopes::collect_table_refs;
54pub(super) use super::impl_core_result_cache_scopes::query_expr_result_cache_scopes;
55
56pub(crate) use super::impl_lifecycle::{table_row_index_fields, view_records_to_entities};
64
65#[cfg(test)]
66thread_local! {
67 static RESULT_CACHE_MISS_EXECUTIONS: std::cell::Cell<u64> = const { std::cell::Cell::new(0) };
68}
69
70#[cfg(test)]
71fn reset_result_cache_miss_executions() {
72 RESULT_CACHE_MISS_EXECUTIONS.with(|count| count.set(0));
73}
74
75#[cfg(test)]
76fn result_cache_miss_executions() -> u64 {
77 RESULT_CACHE_MISS_EXECUTIONS.with(std::cell::Cell::get)
78}
79
80pub use super::execution_context::{
81 capture_current_snapshot, clear_current_auth_identity, clear_current_connection_id,
82 clear_current_snapshot, clear_current_tenant, current_auth_identity_for_audit,
83 current_connection_id, current_tenant, entity_visible_under_current_snapshot,
84 entity_visible_with_context, set_current_auth_identity, set_current_auth_principal,
85 set_current_connection_id, set_current_snapshot, set_current_tenant, snapshot_bundle,
86 with_snapshot_bundle, SnapshotBundle, SnapshotContext,
87};
88pub(crate) use super::execution_context::{
89 config_read_permitted, current_auth_identity, current_config_value, current_kv_value,
90 current_role_projected, current_scope_override, current_secret_value,
91 current_snapshot_requires_index_fallback, current_user_projected, has_scope_override_active,
92 kv_read_permitted, parse_set_local_tenant, update_current_config_value,
93 update_current_kv_value, update_current_secret_value, xids_visible_under_current_snapshot,
94 ConfigSnapshotGuard, CurrentSnapshotGuard, KvStoreGuard, ScopeOverrideGuard, SecretStoreGuard,
95 TxLocalTenantGuard,
96};
97
98pub(super) fn has_with_prefix(sql: &str) -> bool {
103 let trimmed = sql.trim_start();
104 let head_end = trimmed
105 .find(|c: char| c.is_whitespace() || c == '(')
106 .unwrap_or(trimmed.len());
107 trimmed[..head_end].eq_ignore_ascii_case("WITH")
108}
109
110pub(super) fn peek_top_level_as_of_with_table(
115 sql: &str,
116) -> Option<(crate::application::vcs::AsOfSpec, Option<String>)> {
117 if !sql
118 .as_bytes()
119 .windows(5)
120 .any(|w| w.eq_ignore_ascii_case(b"as of"))
121 {
122 return None;
123 }
124 let parsed = reddb_rql::parser::parse(sql).ok()?;
125 let reddb_rql::ast::QueryExpr::Table(table) = parsed.query else {
126 return None;
127 };
128 let clause = table.as_of?;
129 let table_name = if table.table.is_empty() || table.table == "any" {
130 None
131 } else {
132 Some(table.table.clone())
133 };
134 let spec = match clause {
135 reddb_rql::ast::AsOfClause::Commit(h) => crate::application::vcs::AsOfSpec::Commit(h),
136 reddb_rql::ast::AsOfClause::Branch(b) => crate::application::vcs::AsOfSpec::Branch(b),
137 reddb_rql::ast::AsOfClause::Tag(t) => crate::application::vcs::AsOfSpec::Tag(t),
138 reddb_rql::ast::AsOfClause::TimestampMs(ts) => {
139 crate::application::vcs::AsOfSpec::TimestampMs(ts)
140 }
141 reddb_rql::ast::AsOfClause::Snapshot(x) => crate::application::vcs::AsOfSpec::Snapshot(x),
142 };
143 Some((spec, table_name))
144}
145
146pub(super) fn query_has_volatile_builtin(sql: &str) -> bool {
147 const VOLATILE_TOKENS: &[&str] = &[
151 "pg_advisory_lock",
152 "pg_try_advisory_lock",
153 "pg_advisory_unlock",
154 "random()",
155 "$config",
160 "$secret",
161 "$red.secret",
162 "$kv",
163 ];
168 let lowered = sql.to_ascii_lowercase();
169 VOLATILE_TOKENS.iter().any(|t| lowered.contains(t))
170}
171
172pub(super) fn query_is_ask_statement(sql: &str) -> bool {
173 let trimmed = sql.trim_start();
174 let head_end = trimmed
175 .find(|c: char| c.is_whitespace() || c == '(' || c == ';')
176 .unwrap_or(trimmed.len());
177 trimmed[..head_end].eq_ignore_ascii_case("ASK")
178}
179
180pub(super) fn intent_lock_modes_for(
190 expr: &QueryExpr,
191) -> Option<(
192 crate::runtime::lock_manager::LockMode,
193 crate::runtime::lock_manager::LockMode,
194)> {
195 use crate::runtime::lock_manager::LockMode::{Exclusive, IntentExclusive, IntentShared};
196
197 match expr {
198 QueryExpr::Table(_)
200 | QueryExpr::Join(_)
201 | QueryExpr::Vector(_)
202 | QueryExpr::Hybrid(_)
203 | QueryExpr::Graph(_)
204 | QueryExpr::Path(_)
205 | QueryExpr::Ask(_)
206 | QueryExpr::SearchCommand(_)
207 | QueryExpr::GraphCommand(_)
208 | QueryExpr::RankOf(_)
209 | QueryExpr::ApproxRankOf(_)
210 | QueryExpr::RankRange(_)
211 | QueryExpr::QueueSelect(_) => Some((IntentShared, IntentShared)),
212
213 QueryExpr::Insert(_)
221 | QueryExpr::Update(_)
222 | QueryExpr::Delete(_)
223 | QueryExpr::QueueCommand(QueueCommand::Move { .. }) => {
224 Some((IntentExclusive, IntentExclusive))
225 }
226 QueryExpr::QueueCommand(_) => Some((IntentShared, IntentShared)),
227
228 QueryExpr::CreateTable(_)
232 | QueryExpr::CreateCollection(_)
233 | QueryExpr::CreateVector(_)
234 | QueryExpr::DropTable(_)
235 | QueryExpr::DropGraph(_)
236 | QueryExpr::DropVector(_)
237 | QueryExpr::DropDocument(_)
238 | QueryExpr::DropKv(_)
239 | QueryExpr::DropCollection(_)
240 | QueryExpr::Truncate(_)
241 | QueryExpr::AlterTable(_)
242 | QueryExpr::CreateIndex(_)
243 | QueryExpr::DropIndex(_)
244 | QueryExpr::CreateTimeSeries(_)
245 | QueryExpr::CreateMetric(_)
246 | QueryExpr::AlterMetric(_)
247 | QueryExpr::CreateSlo(_)
248 | QueryExpr::DropTimeSeries(_)
249 | QueryExpr::CreateQueue(_)
250 | QueryExpr::AlterQueue(_)
251 | QueryExpr::DropQueue(_)
252 | QueryExpr::CreateTree(_)
253 | QueryExpr::DropTree(_)
254 | QueryExpr::CreatePolicy(_)
255 | QueryExpr::DropPolicy(_)
256 | QueryExpr::CreateView(_)
257 | QueryExpr::DropView(_)
258 | QueryExpr::RefreshMaterializedView(_)
259 | QueryExpr::CreateSchema(_)
260 | QueryExpr::DropSchema(_)
261 | QueryExpr::CreateSequence(_)
262 | QueryExpr::DropSequence(_)
263 | QueryExpr::CreateServer(_)
264 | QueryExpr::DropServer(_)
265 | QueryExpr::CreateForeignTable(_)
266 | QueryExpr::DropForeignTable(_) => Some((IntentExclusive, Exclusive)),
267
268 _ => None,
274 }
275}
276
277pub(super) fn collections_referenced(expr: &QueryExpr) -> Vec<String> {
282 let mut out = Vec::new();
283 walk_collections(expr, &mut out);
284 out.sort();
285 out.dedup();
286 out
287}
288
289impl RedDBRuntime {
290 pub fn execute_query_with_scope(
304 &self,
305 query: &str,
306 scope: crate::runtime::within_clause::ScopeOverride,
307 ) -> RedDBResult<RuntimeQueryResult> {
308 if scope.is_empty() {
309 return self.execute_query(query);
310 }
311 self.check_scope_override_privilege(&scope)?;
312 let _scope_guard = ScopeOverrideGuard::install(scope);
313 self.execute_query(query)
314 }
315
316 pub fn execute_query(&self, query: &str) -> RedDBResult<RuntimeQueryResult> {
325 let started = std::time::Instant::now();
326 self.inner.node_load_telemetry.query_start();
327 let receipt = super::transactional_replication::CommitReceiptScope::begin(self);
328 let result = self.execute_query_inner(query);
329 self.finish_query_lifecycle(query, started, result, receipt.lsn())
330 }
331
332 pub fn execute_query_with_params(
340 &self,
341 query: &str,
342 params: &[Value],
343 ) -> RedDBResult<RuntimeQueryResult> {
344 if params.is_empty() {
345 return self.execute_query(query);
346 }
347 let started = std::time::Instant::now();
348 self.inner.node_load_telemetry.query_start();
349 let receipt = super::transactional_replication::CommitReceiptScope::begin(self);
350 let result = self.execute_query_with_params_inner(query, params);
351 self.finish_query_lifecycle(query, started, result, receipt.lsn())
352 }
353
354 pub fn execute_prepared_query(
365 &self,
366 query: &str,
367 expr: QueryExpr,
368 ) -> RedDBResult<RuntimeQueryResult> {
369 let started = std::time::Instant::now();
370 self.inner.node_load_telemetry.query_start();
371 let receipt = super::transactional_replication::CommitReceiptScope::begin(self);
372 let result = self.execute_bound_query_expr_in_frame(query, expr);
373 self.finish_query_lifecycle(query, started, result, receipt.lsn())
374 }
375
376 fn finish_query_lifecycle(
377 &self,
378 query: &str,
379 started: std::time::Instant,
380 mut result: RedDBResult<RuntimeQueryResult>,
381 commit_lsn: Option<u64>,
382 ) -> RedDBResult<RuntimeQueryResult> {
383 result = result.and_then(|mut value| {
388 if value.statement_type == "select" {
389 self.filter_integrity_tombstoned(&mut value.result)?;
390 }
391 super::query_budget::check_result(self.resource_limits(), &value)?;
392 Ok(value)
393 });
394 let elapsed_ms = started.elapsed().as_millis() as u64;
395
396 let scope = self.ai_scope();
401 let kind = match result
402 .as_ref()
403 .map(|r| r.statement_type)
404 .unwrap_or("select")
405 {
406 "select" => crate::telemetry::slow_query_logger::QueryKind::Select,
407 "insert" => crate::telemetry::slow_query_logger::QueryKind::Insert,
408 "update" => crate::telemetry::slow_query_logger::QueryKind::Update,
409 "delete" => crate::telemetry::slow_query_logger::QueryKind::Delete,
410 _ => crate::telemetry::slow_query_logger::QueryKind::Internal,
411 };
412 let logged_query = redact_sql_for_slow_log(query);
413 self.inner
414 .slow_query_logger
415 .record(kind, elapsed_ms, logged_query, &scope);
416
417 self.inner
423 .query_latency_telemetry
424 .observe(kind, started.elapsed().as_secs_f64());
425
426 self.inner.node_load_telemetry.query_finish();
430
431 if let Ok(ref mut query_result) = result {
432 if matches!(
433 query_result.statement_type,
434 "insert" | "update" | "delete" | "commit"
435 ) {
436 let lsn = if self.inner.db.replication.is_some() {
437 commit_lsn
438 } else if self.current_xid().is_none() {
439 Some(self.cdc_current_lsn())
440 } else {
441 None
442 };
443 query_result.bookmark = lsn.map(|lsn| {
444 crate::replication::CausalBookmark::new(self.current_replication_term(), lsn)
445 .encode()
446 });
447 }
448 }
449
450 result
451 }
452
453 fn execute_query_with_params_inner(
454 &self,
455 query: &str,
456 params: &[Value],
457 ) -> RedDBResult<RuntimeQueryResult> {
458 let parsed = self.parameterized_query_ast(query)?;
459 let bound = crate::storage::query::user_params::bind(&parsed, params).map_err(|err| {
460 RedDBError::Validation {
461 message: err.to_string(),
462 validation: crate::json!({
463 "code": "INVALID_PARAMS",
464 "surface": "query.params",
465 }),
466 }
467 })?;
468 self.execute_bound_query_expr_in_frame(query, bound)
469 }
470
471 fn parameterized_query_ast(&self, query: &str) -> RedDBResult<QueryExpr> {
474 use crate::storage::query::planner::{CachedPlan, QueryPlan};
475
476 let key = format!("\0parameters\0{query}");
478 if let Some(parsed) = self
479 .inner
480 .query_cache
481 .read()
482 .peek(&key)
483 .filter(|entry| entry.matches_exact_query(query))
484 .map(|entry| entry.plan.original.clone())
485 {
486 return Ok(parsed);
487 }
488 let parsed = parse_multi(query).map_err(|err| RedDBError::Query(err.to_string()))?;
489 let plan = QueryPlan::new(parsed.clone(), parsed.clone(), Default::default());
490 self.inner
491 .query_cache
492 .write()
493 .insert(key, CachedPlan::new(plan).with_exact_query(query));
494 Ok(parsed)
495 }
496
497 fn execute_bound_query_expr_in_frame(
498 &self,
499 query: &str,
500 expr: QueryExpr,
501 ) -> RedDBResult<RuntimeQueryResult> {
502 let rewritten_query = super::red_schema::rewrite_virtual_names(query);
503 let execution_query = rewritten_query.as_deref().unwrap_or(query);
504 let frame = super::statement_frame::StatementExecutionFrame::build(
505 self,
506 super::statement_frame::StatementIdentity::Text(execution_query),
507 )?;
508 let _frame_guards = Box::new(frame.install(self));
512 let _log_span = crate::telemetry::span::query_span(query).entered();
513
514 let expr = self.rewrite_view_refs(expr);
515 let mode = detect_mode(execution_query);
516 let control_event_specs = query_control_event_specs(&expr);
517 let _lock_guard = match frame.prepare_dispatch(self, &expr) {
518 Ok(guard) => guard,
519 Err(err) => {
520 let outcome = control_event_outcome_for_error(&err);
521 for spec in &control_event_specs {
522 self.emit_control_event(
523 spec.kind,
524 outcome,
525 spec.action,
526 spec.resource.clone(),
527 Some(err.to_string()),
528 spec.fields.clone(),
529 )?;
530 }
531 return Err(err);
532 }
533 };
534
535 self.dispatch_expr(expr, query, mode)
536 }
537
538 pub fn causal_session(&self) -> crate::runtime::CausalSession {
539 crate::runtime::CausalSession {
540 runtime: self.clone(),
541 bookmark: None,
542 wait_timeout: std::time::Duration::from_secs(5),
543 }
544 }
545
546 pub fn wait_for_bookmark(
547 &self,
548 bookmark: &crate::replication::CausalBookmark,
549 timeout: std::time::Duration,
550 ) -> RedDBResult<()> {
551 let deadline = std::time::Instant::now() + timeout;
552 loop {
553 let applied_lsn = self.local_contiguous_applied_lsn();
554 if applied_lsn >= bookmark.commit_lsn() {
555 return Ok(());
556 }
557 let now = std::time::Instant::now();
558 if now >= deadline {
559 return Err(RedDBError::InvalidOperation(format!(
560 "timed out waiting for causal bookmark lsn {}; applied={}",
561 bookmark.commit_lsn(),
562 applied_lsn
563 )));
564 }
565 let remaining = deadline.saturating_duration_since(now);
566 std::thread::sleep(remaining.min(std::time::Duration::from_millis(5)));
567 }
568 }
569
570 fn local_contiguous_applied_lsn(&self) -> u64 {
571 match self.inner.db.options().replication.role {
572 crate::replication::ReplicationRole::Replica { .. } => {
573 self.config_u64("red.replication.last_applied_lsn", 0)
574 }
575 _ => self.replication_commit_lsn(),
576 }
577 }
578
579 #[inline(never)]
580 pub(super) fn execute_query_inner(&self, query: &str) -> RedDBResult<RuntimeQueryResult> {
581 if !has_scope_override_active()
592 && !query.trim_start().starts_with("WITHIN")
593 && !query.trim_start().starts_with("within")
594 && !self.inner.query_audit.has_rules()
595 && !self
596 .inner
597 .transaction_state
598 .in_transaction(current_connection_id())
599 {
600 if let Some(result) = self.try_fast_entity_lookup(query) {
601 return result;
602 }
603 }
604
605 match crate::runtime::within_clause::try_strip_within_prefix(query) {
612 Ok(Some((scope, inner))) => {
613 self.check_scope_override_privilege(&scope)?;
614 let _scope_guard = ScopeOverrideGuard::install(scope);
615 return self.execute_query_inner(inner);
620 }
621 Ok(None) => {}
622 Err(msg) => return Err(RedDBError::Query(msg)),
623 }
624
625 if let Some(inner) = strip_explain_analyze_prefix(query) {
629 return self.explain_analyze_as_rows(query, inner);
630 }
631
632 if let Some(inner) = strip_explain_prefix(query) {
639 return self.explain_as_rows(query, inner);
640 }
641
642 if let Some(value) = parse_set_local_tenant(query)? {
647 self.check_scope_tenant_privilege(value.as_deref())?;
648 let conn_id = current_connection_id();
649 if !self.inner.transaction_state.in_transaction(conn_id) {
650 return Err(RedDBError::Query(
651 "SET LOCAL TENANT requires an active transaction".to_string(),
652 ));
653 }
654 self.inner
655 .transaction_state
656 .set_local_tenant(conn_id, value.clone());
657 return Ok(RuntimeQueryResult::ok_message(
658 query.to_string(),
659 &match &value {
660 Some(id) => format!("local tenant set: {id}"),
661 None => "local tenant cleared".to_string(),
662 },
663 "set_local_tenant",
664 ));
665 }
666
667 if super::red_schema::is_system_schema_write(query) {
668 return Err(RedDBError::Query(
669 super::red_schema::READ_ONLY_ERROR.to_string(),
670 ));
671 }
672
673 if let Some(command) = parse_runtime_vcs_command(query) {
674 let command = command?;
675 self.check_vcs_command_privilege(&command)?;
676 return self.execute_vcs_command(query, detect_mode(query), command);
677 }
678
679 if let Some(create_source) = super::analytics_source_catalog::parse_create_statement(query)?
680 {
681 return self.execute_create_analytics_source(query, create_source);
682 }
683
684 if let Some(path) = super::metric_descriptor_catalog::parse_read_metric_statement(query) {
690 return Err(super::metric_descriptor_catalog::read_output_unsupported(
691 &path,
692 ));
693 }
694
695 if let Some(parsed) = super::ranking_descriptor_catalog::parse_create_ranking(query) {
700 return self.execute_create_ranking(query, parsed?);
701 }
702 if super::ranking_descriptor_catalog::parse_show_rankings(query) {
703 return self.execute_show_rankings(query);
704 }
705
706 let rewritten_query = super::red_schema::rewrite_virtual_names(query);
707 let execution_query = rewritten_query.as_deref().unwrap_or(query);
708
709 let frame = super::statement_frame::StatementExecutionFrame::build(
710 self,
711 super::statement_frame::StatementIdentity::Text(execution_query),
712 )?;
713 let _frame_guards = Box::new(frame.install(self));
714
715 let _log_span = crate::telemetry::span::query_span(query).entered();
722
723 if let Some(rewritten) = frame.prepare_cte(execution_query)? {
725 return self.execute_query_expr(rewritten);
726 }
727
728 if !self.inner.query_audit.has_rules() {
730 if let Some(result) = self.try_fast_entity_lookup(execution_query) {
731 return result;
732 }
733 }
734
735 if !self.inner.query_audit.has_rules() {
737 if let Some(result) = frame.read_result_cache(self) {
738 return Ok(result);
739 }
740 }
741
742 #[cfg(test)]
743 RESULT_CACHE_MISS_EXECUTIONS.with(|count| count.set(count.get() + 1));
744
745 let prepared = frame.prepare_statement(self, execution_query)?;
746 let mode = prepared.mode;
747 let expr = prepared.expr;
748
749 let _query_budget = if super::query_budget::is_materialized_read(&expr) {
750 Some(self.query_budget_scope()?)
751 } else {
752 None
753 };
754
755 let statement = query_expr_name(&expr);
756 let result_cache_scopes = query_expr_result_cache_scopes(&expr);
757 let control_event_specs = query_control_event_specs(&expr);
758 let query_audit_plan = query_audit_plan(&expr);
759
760 let _lock_guard = match frame.prepare_dispatch(self, &expr) {
761 Ok(guard) => guard,
762 Err(err) => {
763 let outcome = control_event_outcome_for_error(&err);
764 for spec in &control_event_specs {
765 self.emit_control_event(
766 spec.kind,
767 outcome,
768 spec.action,
769 spec.resource.clone(),
770 Some(err.to_string()),
771 spec.fields.clone(),
772 )?;
773 }
774 return Err(err);
775 }
776 };
777 let query_audit_started = std::time::Instant::now();
778
779 let query_result = match expr {
780 QueryExpr::Function(ref command) => self.execute_function(query, command),
781 QueryExpr::Graph(_) | QueryExpr::Path(_) => {
782 self.execute_materialized_graph(query, mode, statement, &expr)
783 }
784 QueryExpr::Table(table) => {
785 let table = self.resolve_table_expr_subqueries(
786 table,
787 &frame as &dyn super::statement_frame::ReadFrame,
788 )?;
789 if let Some(TableSource::Function {
793 name,
794 args,
795 named_args,
796 }) = table.source.clone()
797 {
798 let tvf_result = RuntimeQueryResult {
806 query: query.to_string(),
807 mode,
808 statement,
809 engine: "runtime-graph-tvf",
810 result: self.execute_table_function(&name, &args, &named_args)?,
811 affected_rows: 0,
812 statement_type: "select",
813 bookmark: None,
814 notice: None,
815 };
816 frame.write_result_cache(self, &tvf_result, result_cache_scopes.clone());
817 return Ok(tvf_result);
818 }
819 if let Some(TableSource::InlineGraphFunction {
827 name,
828 nodes,
829 edges,
830 named_args,
831 }) = table.source.clone()
832 {
833 let inline_result = RuntimeQueryResult {
834 query: query.to_string(),
835 mode,
836 statement,
837 engine: "runtime-graph-tvf-inline",
838 result: self.execute_inline_graph_function(
839 &name,
840 &nodes,
841 &edges,
842 &named_args,
843 )?,
844 affected_rows: 0,
845 statement_type: "select",
846 bookmark: None,
847 notice: None,
848 };
849 frame.write_result_cache(self, &inline_result, result_cache_scopes);
850 return Ok(inline_result);
851 }
852 if super::red_schema::is_virtual_table(&table.table) {
853 return Ok(RuntimeQueryResult {
854 query: query.to_string(),
855 mode,
856 statement,
857 engine: "runtime-red-schema",
858 result: super::red_schema::red_query(
859 self,
860 &table.table,
861 &table,
862 &frame as &dyn super::statement_frame::ReadFrame,
863 )?,
864 affected_rows: 0,
865 statement_type: "select",
866 bookmark: None,
867 notice: None,
868 });
869 }
870
871 if let Some(view_result) = self.try_resolve_analytics_view(
875 &table,
876 &frame as &dyn super::statement_frame::ReadFrame,
877 )? {
878 return Ok(RuntimeQueryResult {
879 query: query.to_string(),
880 mode,
881 statement,
882 engine: "runtime-graph-analytics-view",
883 result: view_result,
884 affected_rows: 0,
885 statement_type: "select",
886 bookmark: None,
887 notice: None,
888 });
889 }
890
891 if let Some(result) = self.execute_probabilistic_select(&table)? {
892 return Ok(RuntimeQueryResult {
893 query: query.to_string(),
894 mode,
895 statement,
896 engine: "runtime-probabilistic",
897 result,
898 affected_rows: 0,
899 statement_type: "select",
900 bookmark: None,
901 notice: None,
902 });
903 }
904
905 if self.inner.foreign_tables.is_foreign_table(&table.table) {
913 let records = self
914 .inner
915 .foreign_tables
916 .scan(&table.table)
917 .map_err(|e| RedDBError::Internal(e.to_string()))?;
918 let result = apply_foreign_table_filters(records, &table);
919 return Ok(RuntimeQueryResult {
920 query: query.to_string(),
921 mode,
922 statement,
923 engine: "runtime-fdw",
924 result,
925 affected_rows: 0,
926 statement_type: "select",
927 bookmark: None,
928 notice: None,
929 });
930 }
931
932 let Some(table_with_rls) = self.authorize_relational_table_select(
949 table,
950 &frame as &dyn super::statement_frame::ReadFrame,
951 )?
952 else {
953 let empty = crate::storage::query::unified::UnifiedResult::empty();
954 return Ok(RuntimeQueryResult {
955 query: query.to_string(),
956 mode,
957 statement,
958 engine: "runtime-table-rls",
959 result: empty,
960 affected_rows: 0,
961 statement_type: "select",
962 bookmark: None,
963 notice: None,
964 });
965 };
966 Ok(RuntimeQueryResult {
967 query: query.to_string(),
968 mode,
969 statement,
970 engine: "runtime-table",
971 result: execute_runtime_table_query_in(
978 &self.inner.db,
979 &table_with_rls,
980 Some(&self.inner.index_store),
981 Some(frame.row_arena()),
982 )?,
983 affected_rows: 0,
984 statement_type: "select",
985 bookmark: None,
986 notice: None,
987 })
988 }
989 QueryExpr::Join(join) => {
990 let join_with_rls = match self.authorize_relational_join_select(
999 join,
1000 &frame as &dyn super::statement_frame::ReadFrame,
1001 )? {
1002 Some(j) => j,
1003 None => {
1004 return Ok(RuntimeQueryResult {
1005 query: query.to_string(),
1006 mode,
1007 statement,
1008 engine: "runtime-join-rls",
1009 result: crate::storage::query::unified::UnifiedResult::empty(),
1010 affected_rows: 0,
1011 statement_type: "select",
1012 bookmark: None,
1013 notice: None,
1014 });
1015 }
1016 };
1017 Ok(RuntimeQueryResult {
1018 query: query.to_string(),
1019 mode,
1020 statement,
1021 engine: "runtime-join",
1022 result: execute_runtime_join_query(self, &join_with_rls)?,
1023 affected_rows: 0,
1024 statement_type: "select",
1025 bookmark: None,
1026 notice: None,
1027 })
1028 }
1029 QueryExpr::Vector(vector) => Ok(RuntimeQueryResult {
1030 query: query.to_string(),
1031 mode,
1032 statement,
1033 engine: "runtime-vector",
1034 result: execute_runtime_vector_query(self, &vector)?,
1035 affected_rows: 0,
1036 statement_type: "select",
1037 bookmark: None,
1038 notice: None,
1039 }),
1040 QueryExpr::Hybrid(hybrid) => Ok(RuntimeQueryResult {
1041 query: query.to_string(),
1042 mode,
1043 statement,
1044 engine: "runtime-hybrid",
1045 result: execute_runtime_hybrid_query(self, &hybrid)?,
1046 affected_rows: 0,
1047 statement_type: "select",
1048 bookmark: None,
1049 notice: None,
1050 }),
1051 QueryExpr::RankOf(ref rank) => self.execute_rank_of(query, rank),
1052 QueryExpr::ApproxRankOf(ref rank) => self.execute_approx_rank_of(query, rank),
1053 QueryExpr::RankRange(ref range) => self.execute_rank_range(query, range),
1054 QueryExpr::Insert(ref insert) if super::red_schema::is_virtual_table(&insert.table) => {
1056 Err(RedDBError::Query(
1057 super::red_schema::READ_ONLY_ERROR.to_string(),
1058 ))
1059 }
1060 QueryExpr::Update(ref update) if super::red_schema::is_virtual_table(&update.table) => {
1061 Err(RedDBError::Query(
1062 super::red_schema::READ_ONLY_ERROR.to_string(),
1063 ))
1064 }
1065 QueryExpr::Delete(ref delete) if super::red_schema::is_virtual_table(&delete.table) => {
1066 Err(RedDBError::Query(
1067 super::red_schema::READ_ONLY_ERROR.to_string(),
1068 ))
1069 }
1070 QueryExpr::Insert(ref insert) => self
1071 .with_deferred_store_wal_for_dml(self.insert_may_emit_events(insert), || {
1072 self.execute_insert(query, insert)
1073 }),
1074 QueryExpr::Update(ref update) => self
1075 .with_deferred_store_wal_for_dml(self.update_may_emit_events(update), || {
1076 self.execute_update(query, update)
1077 }),
1078 QueryExpr::Delete(ref delete) => self
1079 .with_deferred_store_wal_for_dml(self.delete_may_emit_events(delete), || {
1080 self.execute_delete(query, delete)
1081 }),
1082 QueryExpr::CreateTable(ref create) => self.execute_create_table(query, create),
1084 QueryExpr::CreateCollection(ref create) => {
1085 self.execute_create_collection(query, create)
1086 }
1087 QueryExpr::CreateVector(ref create) => self.execute_create_vector(query, create),
1088 QueryExpr::DropTable(ref drop_tbl) => self.execute_drop_table(query, drop_tbl),
1089 QueryExpr::DropGraph(ref drop_graph) => self.execute_drop_graph(query, drop_graph),
1090 QueryExpr::DropVector(ref drop_vector) => self.execute_drop_vector(query, drop_vector),
1091 QueryExpr::DropDocument(ref drop_document) => {
1092 self.execute_drop_document(query, drop_document)
1093 }
1094 QueryExpr::DropKv(ref drop_kv) => self.execute_drop_kv(query, drop_kv),
1095 QueryExpr::DropCollection(ref drop_collection) => {
1096 self.execute_drop_collection(query, drop_collection)
1097 }
1098 QueryExpr::Truncate(ref truncate) => self.execute_truncate(query, truncate),
1099 QueryExpr::AlterTable(ref alter) => self.execute_alter_table(query, alter),
1100 QueryExpr::CreateVcsRef(ref create) => self.execute_create_vcs_ref(query, create),
1101 QueryExpr::DropVcsRef(ref drop_ref) => self.execute_drop_vcs_ref(query, drop_ref),
1102 QueryExpr::ForkStore(ref fork) => self.execute_fork_store(query, fork),
1103 QueryExpr::PromoteFork(ref promote_fork) => {
1104 self.execute_promote_fork(query, promote_fork)
1105 }
1106 QueryExpr::DropFork(ref drop_fork) => self.execute_drop_fork(query, drop_fork),
1107 QueryExpr::ExplainAlter(ref explain) => self.execute_explain_alter(query, explain),
1108 QueryExpr::GraphCommand(ref cmd) => self.execute_graph_command(query, cmd),
1110 QueryExpr::SearchCommand(ref cmd) => self.execute_search_command(query, cmd),
1112 QueryExpr::Ask(ref ask) => self.execute_ask(query, ask),
1114 QueryExpr::CreateIndex(ref create_idx) => self.execute_create_index(query, create_idx),
1115 QueryExpr::DropIndex(ref drop_idx) => self.execute_drop_index(query, drop_idx),
1116 QueryExpr::ProbabilisticCommand(ref cmd) => {
1117 self.execute_probabilistic_command(query, cmd)
1118 }
1119 QueryExpr::CreateTimeSeries(ref ts) => self.execute_create_timeseries(query, ts),
1121 QueryExpr::CreateMetric(ref metric) => self.execute_create_metric(query, metric),
1122 QueryExpr::AlterMetric(ref alter) => self.execute_alter_metric(query, alter),
1123 QueryExpr::CreateSlo(ref slo) => self.execute_create_slo(query, slo),
1124 QueryExpr::DropTimeSeries(ref ts) => self.execute_drop_timeseries(query, ts),
1125 QueryExpr::CreateQueue(ref q) => self.execute_create_queue(query, q),
1127 QueryExpr::AlterQueue(ref q) => self.execute_alter_queue(query, q),
1128 QueryExpr::DropQueue(ref q) => self.execute_drop_queue(query, q),
1129 QueryExpr::QueueSelect(ref q) => self.execute_queue_select(query, q),
1130 QueryExpr::QueueCommand(ref cmd) => self
1131 .with_deferred_store_wal_if_transaction(|| self.execute_queue_command(query, cmd)),
1132 QueryExpr::EventsBackfill(ref backfill) => {
1133 self.execute_events_backfill(query, backfill)
1134 }
1135 QueryExpr::EventsBackfillStatus { ref collection } => Err(RedDBError::Query(format!(
1136 "EVENTS BACKFILL STATUS for '{collection}' is not implemented in this slice"
1137 ))),
1138 QueryExpr::KvCommand(ref cmd) => self.execute_kv_command(query, cmd),
1139 QueryExpr::ConfigCommand(ref cmd) => self.execute_config_command(query, cmd),
1140 QueryExpr::CreateTree(ref tree) => self.execute_create_tree(query, tree),
1141 QueryExpr::DropTree(ref tree) => self.execute_drop_tree(query, tree),
1142 QueryExpr::TreeCommand(ref cmd) => self.execute_tree_command(query, cmd),
1143 QueryExpr::SetConfig { ref key, ref value } => {
1145 if key.starts_with("red.secret.") {
1146 return Err(RedDBError::Query(
1147 "red.secret.* is reserved for vault secrets; use SET SECRET".to_string(),
1148 ));
1149 }
1150 if key.starts_with("red.secrets.") {
1151 return Err(RedDBError::Query(
1152 "red.secrets.* is reserved for vault secrets; use SET SECRET".to_string(),
1153 ));
1154 }
1155 crate::ai::validate_ai_config_key_on_write(key)?;
1159 match self.check_managed_config_write_for_set_config(key) {
1164 Err(err) => Err(err),
1165 Ok(()) => {
1166 self.check_config_write_privilege(key)?;
1167 let store = self.inner.db.store();
1168 let json_val = match value {
1169 Value::Text(s) => crate::serde_json::Value::String(s.to_string()),
1170 Value::Integer(n) => crate::serde_json::Value::Number(*n as f64),
1171 Value::Float(n) => crate::serde_json::Value::Number(*n),
1172 Value::Boolean(b) => crate::serde_json::Value::Bool(*b),
1173 _ => crate::serde_json::Value::String(value.to_string()),
1174 };
1175 store.set_config_tree(key, &json_val);
1176 update_current_config_value(key, value.clone());
1177 self.invalidate_result_cache();
1182 Ok(RuntimeQueryResult::ok_message(
1183 query.to_string(),
1184 &format!("config set: {key}"),
1185 "set",
1186 ))
1187 }
1188 }
1189 }
1190 QueryExpr::SetSecret { ref key, ref value } => {
1192 if key.starts_with("red.config.") {
1193 return Err(RedDBError::Query(
1194 "red.config.* is reserved for config; use SET CONFIG".to_string(),
1195 ));
1196 }
1197 reject_engine_internal_vault_key(key)?;
1198 if key.starts_with("red.secret.") {
1199 self.set_internal_provider_secret(
1200 key,
1201 if value.is_null() {
1202 None
1203 } else {
1204 Some(secret_sql_value_to_string(value)?)
1205 },
1206 )?;
1207 return Ok(RuntimeQueryResult::ok_message(
1208 query.to_string(),
1209 &format!("secret set: {key}"),
1210 "set_secret",
1211 ));
1212 }
1213 let value = if value.is_null() {
1214 None
1215 } else {
1216 Some(secret_sql_value_to_string(value)?)
1217 };
1218 self.set_user_secret(key, value.clone())?;
1219 update_current_secret_value(key, value);
1220 Ok(RuntimeQueryResult::ok_message(
1221 query.to_string(),
1222 &format!("secret set: {key}"),
1223 "set_secret",
1224 ))
1225 }
1226 QueryExpr::DeleteSecret { ref key } => {
1228 reject_engine_internal_vault_key(key)?;
1229 if key.starts_with("red.secret.") {
1230 self.set_internal_provider_secret(key, None)?;
1231 return Ok(RuntimeQueryResult::ok_message(
1232 query.to_string(),
1233 &format!("secret deleted: {key}"),
1234 "delete_secret",
1235 ));
1236 }
1237 self.set_user_secret(key, None)?;
1238 update_current_secret_value(key, None);
1239 Ok(RuntimeQueryResult::ok_message(
1240 query.to_string(),
1241 &format!("secret deleted: {key}"),
1242 "delete_secret",
1243 ))
1244 }
1245 QueryExpr::SetKv { ref key, ref value } => {
1247 let auth_store = self.inner.auth_store.read().clone().ok_or_else(|| {
1248 RedDBError::Query("SET KV requires an auth store".to_string())
1249 })?;
1250 self.check_kv_write_privilege(&auth_store, key)?;
1251 if matches!(value, Value::Null) {
1253 auth_store.plain_kv_delete(key);
1254 update_current_kv_value(key, None);
1255 self.invalidate_result_cache();
1256 return Ok(RuntimeQueryResult::ok_message(
1257 query.to_string(),
1258 &format!("kv deleted: {key}"),
1259 "delete_kv",
1260 ));
1261 }
1262 let value = secret_sql_value_to_string(value)?;
1263 auth_store.plain_kv_set(key.clone(), value.clone());
1264 update_current_kv_value(key, Some(value));
1265 self.invalidate_result_cache();
1266 Ok(RuntimeQueryResult::ok_message(
1267 query.to_string(),
1268 &format!("kv set: {key}"),
1269 "set_kv",
1270 ))
1271 }
1272 QueryExpr::DeleteKv { ref key } => {
1274 let auth_store = self.inner.auth_store.read().clone().ok_or_else(|| {
1275 RedDBError::Query("DELETE KV requires an auth store".to_string())
1276 })?;
1277 self.check_kv_write_privilege(&auth_store, key)?;
1278 let deleted = auth_store.plain_kv_delete(key);
1279 if deleted {
1280 update_current_kv_value(key, None);
1281 }
1282 self.invalidate_result_cache();
1283 Ok(RuntimeQueryResult::ok_message(
1284 query.to_string(),
1285 &format!("kv deleted: {key}"),
1286 if deleted {
1287 "delete_kv"
1288 } else {
1289 "delete_kv_not_found"
1290 },
1291 ))
1292 }
1293 QueryExpr::Scrub { background, budget } => {
1294 self.execute_scrub_query(query, mode, background, budget)
1295 }
1296 QueryExpr::ShowSecrets { ref prefix } => {
1298 let result = self.show_user_secrets(prefix.as_deref())?;
1299 Ok(RuntimeQueryResult {
1300 query: query.to_string(),
1301 mode,
1302 statement: "show_secrets",
1303 engine: "runtime-secret",
1304 result,
1305 affected_rows: 0,
1306 statement_type: "select",
1307 bookmark: None,
1308 notice: None,
1309 })
1310 }
1311 QueryExpr::ShowConfig {
1313 ref prefix,
1314 as_json,
1315 } => {
1316 self.check_config_read_privilege()?;
1317 let store = self.inner.db.store();
1318 let all_collections = store.list_collections();
1319 if !all_collections.contains(&"red.config".to_string()) {
1320 if as_json {
1321 return Ok(show_config_json_result(
1322 query,
1323 mode,
1324 prefix,
1325 crate::serde_json::Value::Object(crate::serde_json::Map::new()),
1326 ));
1327 }
1328 let result = UnifiedResult::with_columns(vec!["key".into(), "value".into()]);
1329 return Ok(RuntimeQueryResult {
1330 query: query.to_string(),
1331 mode,
1332 statement: "show_config",
1333 engine: "runtime-config",
1334 result,
1335 affected_rows: 0,
1336 statement_type: "select",
1337 bookmark: None,
1338 notice: None,
1339 });
1340 }
1341 let manager = store
1342 .get_collection("red.config")
1343 .ok_or_else(|| RedDBError::NotFound("red.config".to_string()))?;
1344 let entities = manager.query_all(|_| true);
1345 let mut latest = std::collections::BTreeMap::<String, (u64, Value, Value)>::new();
1346 for entity in entities {
1347 if let EntityData::Row(ref row) = entity.data {
1348 if let Some(ref named) = row.named {
1349 let key_val = named.get("key").cloned().unwrap_or(Value::Null);
1350 let val = named.get("value").cloned().unwrap_or(Value::Null);
1351 let key_str = match &key_val {
1352 Value::Text(s) => s.as_ref(),
1353 _ => continue,
1354 };
1355 if key_str == super::function_catalog::REGISTRY_KEY {
1356 continue;
1357 }
1358 if let Some(ref pfx) = prefix {
1359 if !key_str.starts_with(pfx.as_str()) {
1360 continue;
1361 }
1362 }
1363 let entity_id = entity.id.raw();
1364 match latest.get(key_str) {
1365 Some((prev_id, _, _)) if *prev_id > entity_id => {}
1366 _ => {
1367 latest.insert(key_str.to_string(), (entity_id, key_val, val));
1368 }
1369 }
1370 }
1371 }
1372 }
1373 if as_json {
1374 let mut tree = crate::serde_json::Value::Object(crate::serde_json::Map::new());
1375 for (key, (_, _, val)) in latest {
1376 let relative = match prefix {
1377 Some(pfx) if key == *pfx => "",
1378 Some(pfx) => key
1379 .strip_prefix(pfx.as_str())
1380 .and_then(|tail| tail.strip_prefix('.'))
1381 .unwrap_or(key.as_str()),
1382 None => key.as_str(),
1383 };
1384 insert_config_json_path(&mut tree, relative, val.to_json());
1385 }
1386 return Ok(show_config_json_result(query, mode, prefix, tree));
1387 }
1388 let mut result = UnifiedResult::with_columns(vec!["key".into(), "value".into()]);
1389 for (_, key_val, val) in latest.into_values() {
1390 let mut record = UnifiedRecord::new();
1391 record.set("key", key_val);
1392 record.set("value", val);
1393 result.push(record);
1394 }
1395 Ok(RuntimeQueryResult {
1396 query: query.to_string(),
1397 mode,
1398 statement: "show_config",
1399 engine: "runtime-config",
1400 result,
1401 affected_rows: 0,
1402 statement_type: "select",
1403 bookmark: None,
1404 notice: None,
1405 })
1406 }
1407 QueryExpr::SetTenant(ref value) => {
1413 self.check_set_tenant_privilege(value.as_deref())?;
1414 match value {
1415 Some(id) => set_current_tenant(id.clone()),
1416 None => clear_current_tenant(),
1417 }
1418 Ok(RuntimeQueryResult::ok_message(
1419 query.to_string(),
1420 &match value {
1421 Some(id) => format!("tenant set: {id}"),
1422 None => "tenant cleared".to_string(),
1423 },
1424 "set_tenant",
1425 ))
1426 }
1427 QueryExpr::ShowTenant => {
1428 let mut result = UnifiedResult::with_columns(vec!["tenant".into()]);
1429 let mut record = UnifiedRecord::new();
1430 record.set(
1431 "tenant",
1432 current_tenant().map(Value::text).unwrap_or(Value::Null),
1433 );
1434 result.push(record);
1435 Ok(RuntimeQueryResult {
1436 query: query.to_string(),
1437 mode,
1438 statement: "show_tenant",
1439 engine: "runtime-tenant",
1440 result,
1441 affected_rows: 0,
1442 statement_type: "select",
1443 bookmark: None,
1444 notice: None,
1445 })
1446 }
1447 QueryExpr::TransactionControl(ref ctl) => {
1459 use crate::storage::transaction::IsolationLevel;
1460 use reddb_rql::ast::TxnControl;
1461
1462 let conn_id = current_connection_id();
1467
1468 let (kind, msg) = match ctl {
1469 TxnControl::Begin(requested_isolation) => {
1470 if self.inner.transaction_state.in_transaction(conn_id) {
1471 return Err(RedDBError::Query("transaction already active".into()));
1472 }
1473 let isolation = requested_isolation
1474 .map(IsolationLevel::from)
1475 .unwrap_or(IsolationLevel::SnapshotIsolation);
1476 let xid = self.inner.transaction_state.begin(conn_id, isolation);
1477 ("begin", format!("BEGIN — xid={xid} (snapshot isolation)"))
1478 }
1479 TxnControl::Commit => {
1480 let has_vault_writes = self
1481 .inner
1482 .pending_vault_writes
1483 .read()
1484 .get(&conn_id)
1485 .is_some_and(|writes| !writes.is_empty());
1486 let store = self.inner.db.store();
1487 let _vault_guard =
1488 has_vault_writes.then(|| store.vault_publication_lock.lock());
1489 let context = self.inner.transaction_state.commit(conn_id, |ctx| {
1490 let mut own_xids = std::collections::HashSet::new();
1491 own_xids.insert(ctx.xid);
1492 for (_, sub) in &ctx.savepoints {
1493 own_xids.insert(*sub);
1494 }
1495 for sub in &ctx.released_sub_xids {
1496 own_xids.insert(*sub);
1497 }
1498 if let Err(err) = self.check_table_row_write_conflicts(
1499 conn_id,
1500 &ctx.snapshot,
1501 &own_xids,
1502 ctx.isolation,
1503 ) {
1504 let undo = self.revive_pending_versioned_updates(conn_id);
1505 self.revive_pending_tombstones(conn_id);
1506 self.discard_pending_kv_watch_events(conn_id);
1507 self.discard_pending_queue_wakes(conn_id);
1508 self.discard_pending_store_wal_actions(conn_id);
1509 self.release_pending_claim_locks(conn_id);
1510 undo?;
1511 return Err(err);
1512 }
1513 if let Err(err) = self.check_queue_dedup_write_conflicts(
1514 conn_id,
1515 &ctx.snapshot,
1516 &own_xids,
1517 ) {
1518 let undo = self.revive_pending_versioned_updates(conn_id);
1519 self.revive_pending_tombstones(conn_id);
1520 self.discard_pending_queue_dedup(conn_id);
1521 self.discard_pending_kv_watch_events(conn_id);
1522 self.discard_pending_queue_wakes(conn_id);
1523 self.discard_pending_store_wal_actions(conn_id);
1524 self.release_pending_claim_locks(conn_id);
1525 undo?;
1526 return Err(err);
1527 }
1528 if let Err(err) = self.prepare_pending_vault_writes(conn_id, ctx) {
1529 let undo = self.revive_pending_versioned_updates(conn_id);
1530 self.revive_pending_tombstones(conn_id);
1531 self.discard_pending_queue_dedup(conn_id);
1532 self.discard_pending_kv_watch_events(conn_id);
1533 self.discard_pending_queue_wakes(conn_id);
1534 self.discard_pending_store_wal_actions(conn_id);
1535 self.release_pending_claim_locks(conn_id);
1536 undo?;
1537 return Err(err);
1538 }
1539 let publication = store.vault_publication_lock.lock();
1544 self.restore_pending_write_stamps(conn_id);
1545 if let Err(err) = self.flush_pending_store_wal_actions(conn_id) {
1546 drop(publication);
1547 let undo = self.revive_pending_versioned_updates(conn_id);
1548 self.revive_pending_tombstones(conn_id);
1549 self.discard_pending_queue_dedup(conn_id);
1550 self.discard_pending_kv_watch_events(conn_id);
1551 self.discard_pending_queue_wakes(conn_id);
1552 self.release_pending_claim_locks(conn_id);
1553 undo?;
1554 return Err(err);
1555 }
1556 #[cfg(test)]
1557 self.index_store_ref().mutation_test_hook(
1558 "",
1559 super::index_store::MutationTestPhase::TransactionWalCommitted,
1560 );
1561 Ok(publication)
1562 });
1563 if context.is_err() {
1564 self.discard_pending_vault_writes(conn_id)?;
1565 }
1566 let context = context?;
1567 match context {
1568 Some(ctx) => {
1569 self.finalize_pending_vault_writes(conn_id);
1570 self.finalize_pending_versioned_updates(conn_id);
1571 self.finalize_pending_tombstones(conn_id);
1572 self.finalize_pending_queue_dedup(conn_id);
1573 self.finalize_pending_kv_watch_events(conn_id);
1574 self.finalize_pending_queue_wakes(conn_id);
1575 self.release_pending_claim_locks(conn_id);
1576 self.recover_transactional_replication().map_err(|error| {
1577 RedDBError::Internal(format!(
1578 "commit is locally durable; replication publication is pending: {error}"
1579 ))
1580 })?;
1581 ("commit", format!("COMMIT — xid={} committed", ctx.xid))
1582 }
1583 None => (
1584 "commit",
1585 "COMMIT outside transaction — no-op (autocommit)".to_string(),
1586 ),
1587 }
1588 }
1589 TxnControl::Rollback => match self.rollback_connection(conn_id)? {
1590 Some(ctx) => ("rollback", format!("ROLLBACK — xid={} aborted", ctx.xid)),
1591 None => (
1592 "rollback",
1593 "ROLLBACK outside transaction — no-op (autocommit)".to_string(),
1594 ),
1595 },
1596 TxnControl::Savepoint(name) => {
1603 match self.inner.transaction_state.savepoint(conn_id, name) {
1604 Some(sub) => ("savepoint", format!("SAVEPOINT {name} — sub_xid={sub}")),
1605 None => (
1606 "savepoint",
1607 "SAVEPOINT outside transaction — no-op".to_string(),
1608 ),
1609 }
1610 }
1611 TxnControl::ReleaseSavepoint(name) => {
1612 match self
1613 .inner
1614 .transaction_state
1615 .release_savepoint(conn_id, name)?
1616 {
1617 Some(released) => (
1618 "release_savepoint",
1619 format!("RELEASE SAVEPOINT {name} — {released} level(s)"),
1620 ),
1621 None => (
1622 "release_savepoint",
1623 "RELEASE outside transaction — no-op".to_string(),
1624 ),
1625 }
1626 }
1627 TxnControl::RollbackToSavepoint(name) => {
1628 match self
1629 .inner
1630 .transaction_state
1631 .rollback_to_savepoint_with(conn_id, name, |rollback| {
1632 self.rollback_vault_savepoint(conn_id, rollback.savepoint_xid);
1633 let reverted_updates = self.revive_versioned_updates_since(
1634 conn_id,
1635 rollback.savepoint_xid,
1636 );
1637 let revived =
1638 self.revive_tombstones_since(conn_id, rollback.savepoint_xid);
1639 reverted_updates.map(|updates| (updates, revived))
1640 })?
1641 {
1642 Some((rollback, (reverted_updates, revived))) => {
1643 (
1644 "rollback_to_savepoint",
1645 format!(
1646 "ROLLBACK TO SAVEPOINT {name} — aborted {} sub_xid(s), reverted {reverted_updates} update(s), revived {revived} tombstone(s)",
1647 rollback.aborted_xids.len(),
1648 ),
1649 )
1650 }
1651 None => (
1652 "rollback_to_savepoint",
1653 "ROLLBACK TO outside transaction — no-op".to_string(),
1654 ),
1655 }
1656 }
1657 };
1658 Ok(RuntimeQueryResult::ok_message(
1659 query.to_string(),
1660 &msg,
1661 kind,
1662 ))
1663 }
1664 QueryExpr::CreateSchema(ref q) => {
1677 let store = self.inner.db.store();
1678 let key = format!("schema.{}", q.name);
1679 if store.get_config(&key).is_some() {
1680 if q.if_not_exists {
1681 return Ok(RuntimeQueryResult::ok_message(
1682 query.to_string(),
1683 &format!("schema {} already exists — skipped", q.name),
1684 "create_schema",
1685 ));
1686 }
1687 return Err(RedDBError::Internal(format!(
1688 "schema {} already exists",
1689 q.name
1690 )));
1691 }
1692 store.set_config_tree(&key, &crate::serde_json::Value::Bool(true));
1693 Ok(RuntimeQueryResult::ok_message(
1694 query.to_string(),
1695 &format!("schema {} created", q.name),
1696 "create_schema",
1697 ))
1698 }
1699 QueryExpr::DropSchema(ref q) => {
1700 let store = self.inner.db.store();
1701 let key = format!("schema.{}", q.name);
1702 let existed = store.get_config(&key).is_some();
1703 if !existed && !q.if_exists {
1704 return Err(RedDBError::Internal(format!(
1705 "schema {} does not exist",
1706 q.name
1707 )));
1708 }
1709 store.set_config_tree(&key, &crate::serde_json::Value::Null);
1711 let suffix = if q.cascade {
1712 " (CASCADE accepted — tables untouched)"
1713 } else {
1714 ""
1715 };
1716 Ok(RuntimeQueryResult::ok_message(
1717 query.to_string(),
1718 &format!("schema {} dropped{}", q.name, suffix),
1719 "drop_schema",
1720 ))
1721 }
1722 QueryExpr::CreateSequence(ref q) => {
1723 let store = self.inner.db.store();
1724 let base = format!("sequence.{}", q.name);
1725 let start_key = format!("{base}.start");
1726 let incr_key = format!("{base}.increment");
1727 let curr_key = format!("{base}.current");
1728 if store.get_config(&start_key).is_some() {
1729 if q.if_not_exists {
1730 return Ok(RuntimeQueryResult::ok_message(
1731 query.to_string(),
1732 &format!("sequence {} already exists — skipped", q.name),
1733 "create_sequence",
1734 ));
1735 }
1736 return Err(RedDBError::Internal(format!(
1737 "sequence {} already exists",
1738 q.name
1739 )));
1740 }
1741 let initial_current = q.start - q.increment;
1744 store.set_config_tree(
1745 &start_key,
1746 &crate::serde_json::Value::Number(q.start as f64),
1747 );
1748 store.set_config_tree(
1749 &incr_key,
1750 &crate::serde_json::Value::Number(q.increment as f64),
1751 );
1752 store.set_config_tree(
1753 &curr_key,
1754 &crate::serde_json::Value::Number(initial_current as f64),
1755 );
1756 Ok(RuntimeQueryResult::ok_message(
1757 query.to_string(),
1758 &format!(
1759 "sequence {} created (start={}, increment={})",
1760 q.name, q.start, q.increment
1761 ),
1762 "create_sequence",
1763 ))
1764 }
1765 QueryExpr::DropSequence(ref q) => {
1766 let store = self.inner.db.store();
1767 let base = format!("sequence.{}", q.name);
1768 let existed = store.get_config(&format!("{base}.start")).is_some();
1769 if !existed && !q.if_exists {
1770 return Err(RedDBError::Internal(format!(
1771 "sequence {} does not exist",
1772 q.name
1773 )));
1774 }
1775 for k in ["start", "increment", "current"] {
1776 store.set_config_tree(&format!("{base}.{k}"), &crate::serde_json::Value::Null);
1777 }
1778 Ok(RuntimeQueryResult::ok_message(
1779 query.to_string(),
1780 &format!("sequence {} dropped", q.name),
1781 "drop_sequence",
1782 ))
1783 }
1784 QueryExpr::CreateView(ref q) => {
1794 let mut views = self.inner.views.write();
1795 if views.contains_key(&q.name) && !q.or_replace {
1796 if q.if_not_exists {
1797 return Ok(RuntimeQueryResult::ok_message(
1798 query.to_string(),
1799 &format!("view {} already exists — skipped", q.name),
1800 "create_view",
1801 ));
1802 }
1803 return Err(RedDBError::Internal(format!(
1804 "view {} already exists",
1805 q.name
1806 )));
1807 }
1808 views.insert(q.name.clone(), Arc::new(q.clone()));
1809 drop(views);
1810
1811 if q.materialized {
1813 use crate::storage::cache::result::{MaterializedViewDef, RefreshPolicy};
1814 let refresh = match q.refresh_every_ms {
1815 Some(ms) => RefreshPolicy::Periodic(std::time::Duration::from_millis(ms)),
1816 None => RefreshPolicy::Manual,
1817 };
1818 let dependencies = collect_table_refs(&q.query);
1819 let def = MaterializedViewDef {
1820 name: q.name.clone(),
1821 query: format!("<parsed view {}>", q.name),
1822 dependencies: dependencies.clone(),
1823 refresh,
1824 retention_duration_ms: q.retention_duration_ms,
1825 };
1826 self.inner.materialized_views.write().register(def);
1827
1828 let descriptor =
1834 crate::runtime::continuous_materialized_view::MaterializedViewDescriptor {
1835 name: q.name.clone(),
1836 source_sql: query.to_string(),
1837 source_collections: dependencies,
1838 refresh_every_ms: q.refresh_every_ms,
1839 retention_duration_ms: q.retention_duration_ms,
1840 };
1841 let store = self.inner.db.store();
1842 crate::runtime::continuous_materialized_view::persist_descriptor(
1843 store.as_ref(),
1844 &descriptor,
1845 )?;
1846
1847 self.ensure_materialized_view_backing(&q.name)?;
1854 }
1855 self.invalidate_plan_cache();
1860 self.invalidate_result_cache();
1861
1862 Ok(RuntimeQueryResult::ok_message(
1863 query.to_string(),
1864 &format!(
1865 "{}view {} created",
1866 if q.materialized { "materialized " } else { "" },
1867 q.name
1868 ),
1869 "create_view",
1870 ))
1871 }
1872 QueryExpr::DropView(ref q) => {
1873 let mut views = self.inner.views.write();
1874 let removed = views.remove(&q.name);
1875 let existed = removed.is_some();
1876 let removed_materialized =
1877 removed.as_ref().map(|v| v.materialized).unwrap_or(false);
1878 drop(views);
1879 if q.materialized || existed {
1880 self.inner.materialized_views.write().remove(&q.name);
1882 let store = self.inner.db.store();
1886 crate::runtime::continuous_materialized_view::remove_by_name(
1887 store.as_ref(),
1888 &q.name,
1889 )?;
1890 }
1891 if removed_materialized || q.materialized {
1895 self.drop_materialized_view_backing(&q.name)?;
1896 }
1897 self.invalidate_plan_cache();
1900 self.invalidate_result_cache();
1901 if !existed && !q.if_exists {
1902 return Err(RedDBError::Internal(format!(
1903 "view {} does not exist",
1904 q.name
1905 )));
1906 }
1907 self.invalidate_plan_cache();
1908 Ok(RuntimeQueryResult::ok_message(
1909 query.to_string(),
1910 &format!("view {} dropped", q.name),
1911 "drop_view",
1912 ))
1913 }
1914 QueryExpr::RefreshMaterializedView(ref q) => {
1915 let view = {
1918 let views = self.inner.views.read();
1919 views.get(&q.name).cloned()
1920 };
1921 let view = match view {
1922 Some(v) => v,
1923 None => {
1924 return Err(RedDBError::Internal(format!(
1925 "view {} does not exist",
1926 q.name
1927 )))
1928 }
1929 };
1930 if !view.materialized {
1931 return Err(RedDBError::Internal(format!(
1932 "view {} is not materialized — REFRESH requires \
1933 CREATE MATERIALIZED VIEW",
1934 q.name
1935 )));
1936 }
1937 let started = std::time::Instant::now();
1939 let now_ms = std::time::SystemTime::now()
1940 .duration_since(std::time::UNIX_EPOCH)
1941 .map(|d| d.as_millis() as u64)
1942 .unwrap_or(0);
1943 match self.execute_query_expr((*view.query).clone()) {
1944 Ok(inner_result) => {
1945 let entities =
1952 view_records_to_entities(&q.name, &inner_result.result.records);
1953 let row_count = entities.len() as u64;
1954 let store = self.inner.db.store();
1955 let serialized_records = match store.refresh_collection(&q.name, entities) {
1956 Ok(records) => records,
1957 Err(err) => {
1958 let duration_ms = started.elapsed().as_millis() as u64;
1959 let msg = err.to_string();
1960 self.inner
1961 .materialized_views
1962 .write()
1963 .record_refresh_failure(
1964 &q.name,
1965 msg.clone(),
1966 duration_ms,
1967 now_ms,
1968 );
1969 return Err(RedDBError::Internal(format!(
1970 "REFRESH MATERIALIZED VIEW {}: {msg}",
1971 q.name
1972 )));
1973 }
1974 };
1975
1976 if self.inner.db.replication.is_some() {
1982 let lsn = self.inner.cdc.emit(
1983 crate::replication::cdc::ChangeOperation::Refresh,
1984 &q.name,
1985 0,
1986 "refresh",
1987 );
1988 self.invalidate_result_cache_for_table(&q.name);
1989 let timestamp = std::time::SystemTime::now()
1990 .duration_since(std::time::UNIX_EPOCH)
1991 .unwrap_or_default()
1992 .as_millis() as u64;
1993 let record = ChangeRecord::for_refresh(
1994 lsn,
1995 timestamp,
1996 q.name.clone(),
1997 serialized_records,
1998 )
1999 .with_term(self.current_replication_term());
2000 self.stage_or_publish_logical_record(record)?;
2001 }
2002
2003 let duration_ms = started.elapsed().as_millis() as u64;
2004 let serialized = format!("{:?}", inner_result.result);
2005 self.inner
2006 .materialized_views
2007 .write()
2008 .record_refresh_success(
2009 &q.name,
2010 serialized.into_bytes(),
2011 row_count,
2012 duration_ms,
2013 now_ms,
2014 );
2015 self.invalidate_result_cache();
2020 Ok(RuntimeQueryResult::ok_message(
2021 query.to_string(),
2022 &format!("materialized view {} refreshed", q.name),
2023 "refresh_materialized_view",
2024 ))
2025 }
2026 Err(err) => {
2027 let duration_ms = started.elapsed().as_millis() as u64;
2028 let msg = err.to_string();
2029 self.inner
2030 .materialized_views
2031 .write()
2032 .record_refresh_failure(&q.name, msg.clone(), duration_ms, now_ms);
2033 Err(err)
2034 }
2035 }
2036 }
2037 QueryExpr::CreatePolicy(ref q) => {
2044 let key = (q.table.clone(), q.name.clone());
2045 self.inner
2046 .rls_policies
2047 .write()
2048 .insert(key, Arc::new(q.clone()));
2049 self.invalidate_plan_cache();
2050 self.invalidate_result_cache();
2051 self.schema_vocabulary_apply(
2055 crate::runtime::schema_vocabulary::DdlEvent::CreatePolicy {
2056 collection: q.table.clone(),
2057 policy: q.name.clone(),
2058 },
2059 );
2060 Ok(RuntimeQueryResult::ok_message(
2061 query.to_string(),
2062 &format!("policy {} on {} created", q.name, q.table),
2063 "create_policy",
2064 ))
2065 }
2066 QueryExpr::DropPolicy(ref q) => {
2067 let removed = self
2068 .inner
2069 .rls_policies
2070 .write()
2071 .remove(&(q.table.clone(), q.name.clone()))
2072 .is_some();
2073 if !removed && !q.if_exists {
2074 return Err(RedDBError::Internal(format!(
2075 "policy {} on {} does not exist",
2076 q.name, q.table
2077 )));
2078 }
2079 self.invalidate_plan_cache();
2080 self.invalidate_result_cache();
2081 self.schema_vocabulary_apply(
2084 crate::runtime::schema_vocabulary::DdlEvent::DropPolicy {
2085 collection: q.table.clone(),
2086 policy: q.name.clone(),
2087 },
2088 );
2089 Ok(RuntimeQueryResult::ok_message(
2090 query.to_string(),
2091 &format!("policy {} on {} dropped", q.name, q.table),
2092 "drop_policy",
2093 ))
2094 }
2095 QueryExpr::CreateServer(ref q) => {
2106 use crate::storage::fdw::FdwOptions;
2107 let registry = Arc::clone(&self.inner.foreign_tables);
2108 if registry.server(&q.name).is_some() {
2109 if q.if_not_exists {
2110 return Ok(RuntimeQueryResult::ok_message(
2111 query.to_string(),
2112 &format!("server {} already exists — skipped", q.name),
2113 "create_server",
2114 ));
2115 }
2116 return Err(RedDBError::Internal(format!(
2117 "server {} already exists",
2118 q.name
2119 )));
2120 }
2121 let mut opts = FdwOptions::new();
2122 for (k, v) in &q.options {
2123 opts.values.insert(k.clone(), v.clone());
2124 }
2125 registry
2126 .create_server(&q.name, &q.wrapper, opts)
2127 .map_err(|e| RedDBError::Internal(e.to_string()))?;
2128 Ok(RuntimeQueryResult::ok_message(
2129 query.to_string(),
2130 &format!("server {} created (wrapper {})", q.name, q.wrapper),
2131 "create_server",
2132 ))
2133 }
2134 QueryExpr::DropServer(ref q) => {
2135 let existed = self.inner.foreign_tables.drop_server(&q.name);
2136 if !existed && !q.if_exists {
2137 return Err(RedDBError::Internal(format!(
2138 "server {} does not exist",
2139 q.name
2140 )));
2141 }
2142 Ok(RuntimeQueryResult::ok_message(
2143 query.to_string(),
2144 &format!(
2145 "server {} dropped{}",
2146 q.name,
2147 if q.cascade { " (cascade)" } else { "" }
2148 ),
2149 "drop_server",
2150 ))
2151 }
2152 QueryExpr::CreateForeignTable(ref q) => {
2153 use crate::storage::fdw::{FdwOptions, ForeignColumn, ForeignTable};
2154 let registry = Arc::clone(&self.inner.foreign_tables);
2155 if registry.foreign_table(&q.name).is_some() {
2156 if q.if_not_exists {
2157 return Ok(RuntimeQueryResult::ok_message(
2158 query.to_string(),
2159 &format!("foreign table {} already exists — skipped", q.name),
2160 "create_foreign_table",
2161 ));
2162 }
2163 return Err(RedDBError::Internal(format!(
2164 "foreign table {} already exists",
2165 q.name
2166 )));
2167 }
2168 let mut opts = FdwOptions::new();
2169 for (k, v) in &q.options {
2170 opts.values.insert(k.clone(), v.clone());
2171 }
2172 let columns: Vec<ForeignColumn> = q
2173 .columns
2174 .iter()
2175 .map(|c| ForeignColumn {
2176 name: c.name.clone(),
2177 data_type: c.data_type.clone(),
2178 not_null: c.not_null,
2179 })
2180 .collect();
2181 registry
2182 .create_foreign_table(ForeignTable {
2183 name: q.name.clone(),
2184 server_name: q.server.clone(),
2185 columns,
2186 options: opts,
2187 })
2188 .map_err(|e| RedDBError::Internal(e.to_string()))?;
2189 self.invalidate_plan_cache();
2190 Ok(RuntimeQueryResult::ok_message(
2191 query.to_string(),
2192 &format!("foreign table {} created (server {})", q.name, q.server),
2193 "create_foreign_table",
2194 ))
2195 }
2196 QueryExpr::DropForeignTable(ref q) => {
2197 let existed = self.inner.foreign_tables.drop_foreign_table(&q.name);
2198 if !existed && !q.if_exists {
2199 return Err(RedDBError::Internal(format!(
2200 "foreign table {} does not exist",
2201 q.name
2202 )));
2203 }
2204 self.invalidate_plan_cache();
2205 Ok(RuntimeQueryResult::ok_message(
2206 query.to_string(),
2207 &format!("foreign table {} dropped", q.name),
2208 "drop_foreign_table",
2209 ))
2210 }
2211 QueryExpr::CopyFrom(ref q) => {
2217 use crate::storage::import::{CsvConfig, CsvImporter};
2218 self.check_copy_from_privilege(&q.table)?;
2219 confine_copy_from_path(&q.path)?;
2220 let store = self.inner.db.store();
2221 let cfg = CsvConfig {
2222 collection: q.table.clone(),
2223 has_header: q.has_header,
2224 delimiter: q.delimiter.map(|c| c as u8).unwrap_or(b','),
2225 ..CsvConfig::default()
2226 };
2227 let importer = CsvImporter::new(cfg);
2228 let stats = importer
2229 .import_file(&q.path, store.as_ref())
2230 .map_err(|e| RedDBError::Internal(format!("COPY failed: {e}")))?;
2231 self.note_table_write(&q.table);
2233 Ok(RuntimeQueryResult::ok_message(
2234 query.to_string(),
2235 &format!(
2236 "COPY imported {} rows into {} ({} errors skipped, {}ms)",
2237 stats.records_imported, q.table, stats.errors_skipped, stats.duration_ms
2238 ),
2239 "copy_from",
2240 ))
2241 }
2242 QueryExpr::MaintenanceCommand(ref cmd) => {
2258 use reddb_rql::ast::MaintenanceCommand as Mc;
2259 let store = self.inner.db.store();
2260 let (kind, msg) = match cmd {
2261 Mc::Analyze { target } => {
2262 let targets: Vec<String> = match target {
2263 Some(t) => vec![t.clone()],
2264 None => store.list_collections(),
2265 };
2266 for t in &targets {
2267 self.refresh_table_planner_stats(t);
2268 }
2269 (
2270 "analyze",
2271 format!("ANALYZE refreshed stats for {} table(s)", targets.len()),
2272 )
2273 }
2274 Mc::Vacuum { target, full } => {
2275 let targets: Vec<String> = match target {
2276 Some(t) => vec![t.clone()],
2277 None => store.list_collections(),
2278 };
2279 let cutoff_xid = self.mvcc_vacuum_cutoff_xid();
2280 let mut vacuum_stats =
2281 crate::storage::unified::store::MvccVacuumStats::default();
2282 for t in &targets {
2283 let stats = store.vacuum_mvcc_history(t, cutoff_xid).map_err(|e| {
2284 RedDBError::Internal(format!(
2285 "VACUUM MVCC history failed for {t}: {e}"
2286 ))
2287 })?;
2288 if stats.reclaimed_versions > 0 {
2289 self.rebuild_runtime_indexes_for_table(t)?;
2290 }
2291 vacuum_stats.add(&stats);
2292 }
2293 for t in &targets {
2298 self.refresh_table_planner_stats(t);
2299 }
2300 let persisted = if *full {
2304 match store.persist() {
2305 Ok(()) => true,
2306 Err(e) => {
2307 return Err(RedDBError::Internal(format!(
2308 "VACUUM FULL persist failed: {e:?}"
2309 )));
2310 }
2311 }
2312 } else {
2313 false
2314 };
2315 self.invalidate_result_cache();
2317 (
2318 "vacuum",
2319 format!(
2320 "VACUUM{} processed {} table(s): scanned_versions={}, retained_versions={}, reclaimed_versions={}, retained_history_versions={}, reclaimed_history_versions={}, retained_tombstones={}, reclaimed_tombstones={}{}",
2321 if *full { " FULL" } else { "" },
2322 targets.len(),
2323 vacuum_stats.scanned_versions,
2324 vacuum_stats.retained_versions,
2325 vacuum_stats.reclaimed_versions,
2326 vacuum_stats.retained_history_versions,
2327 vacuum_stats.reclaimed_history_versions,
2328 vacuum_stats.retained_tombstones,
2329 vacuum_stats.reclaimed_tombstones,
2330 if persisted {
2331 " (pages flushed to disk)"
2332 } else {
2333 ""
2334 }
2335 ),
2336 )
2337 }
2338 };
2339 Ok(RuntimeQueryResult::ok_message(
2340 query.to_string(),
2341 &msg,
2342 kind,
2343 ))
2344 }
2345 QueryExpr::Grant(ref g) => self.execute_grant_statement(query, g),
2352 QueryExpr::Revoke(ref r) => self.execute_revoke_statement(query, r),
2353 QueryExpr::AlterUser(ref a) => self.execute_alter_user_statement(query, a),
2354 QueryExpr::CreateUser(ref u) => self.execute_create_user_statement(query, u),
2355 QueryExpr::CreateIamPolicy { ref id, ref json } => {
2356 self.execute_create_iam_policy(query, id, json)
2357 }
2358 QueryExpr::DropIamPolicy { ref id } => self.execute_drop_iam_policy(query, id),
2359 QueryExpr::AttachPolicy {
2360 ref policy_id,
2361 ref principal,
2362 } => self.execute_attach_policy(query, policy_id, principal),
2363 QueryExpr::DetachPolicy {
2364 ref policy_id,
2365 ref principal,
2366 } => self.execute_detach_policy(query, policy_id, principal),
2367 QueryExpr::ShowPolicies { ref filter } => {
2368 self.execute_show_policies(query, filter.as_ref())
2369 }
2370 QueryExpr::ShowEffectivePermissions {
2371 ref user,
2372 ref resource,
2373 } => self.execute_show_effective_permissions(query, user, resource.as_ref()),
2374 QueryExpr::SimulatePolicy {
2375 ref user,
2376 ref action,
2377 ref resource,
2378 } => self.execute_simulate_policy(query, user, action, resource),
2379 QueryExpr::LintPolicy { ref source } => self.execute_lint_policy(query, source),
2380 QueryExpr::MigratePolicyMode {
2381 ref target,
2382 dry_run,
2383 } => self.execute_migrate_policy_mode(query, target, dry_run),
2384 QueryExpr::CreateMigration(ref q) => self.execute_create_migration(query, q),
2385 QueryExpr::ApplyMigration(ref q) => self.execute_apply_migration(query, q),
2386 QueryExpr::RollbackMigration(ref q) => self.execute_rollback_migration(query, q),
2387 QueryExpr::ExplainMigration(ref q) => self.execute_explain_migration(query, q),
2388 _ => Err(RedDBError::Query(
2389 "unsupported command in runtime dispatcher".to_string(),
2390 )),
2391 };
2392
2393 if _query_budget.is_some() {
2394 super::query_budget::charge(0)?;
2395 }
2396 if !control_event_specs.is_empty() {
2397 let (outcome, reason) = match &query_result {
2398 Ok(_) => (crate::runtime::control_events::Outcome::Allowed, None),
2399 Err(err) => (control_event_outcome_for_error(err), Some(err.to_string())),
2400 };
2401 for spec in &control_event_specs {
2402 self.emit_control_event(
2403 spec.kind,
2404 outcome,
2405 spec.action,
2406 spec.resource.clone(),
2407 reason.clone(),
2408 spec.fields.clone(),
2409 )?;
2410 }
2411 }
2412
2413 if let (Some(plan), Ok(result)) = (&query_audit_plan, &query_result) {
2414 self.emit_query_audit(
2415 query,
2416 plan,
2417 query_audit_started.elapsed().as_millis() as u64,
2418 result,
2419 );
2420 }
2421
2422 if let Ok(ref result) = query_result {
2429 frame.write_result_cache(self, result, result_cache_scopes);
2430 }
2431
2432 query_result
2433 }
2434
2435 pub fn execute_query_expr(&self, expr: QueryExpr) -> RedDBResult<RuntimeQueryResult> {
2455 let frame = super::statement_frame::StatementExecutionFrame::build(
2456 self,
2457 super::statement_frame::StatementIdentity::Expr(&expr),
2458 )?;
2459 let _frame_guards = Box::new(frame.install(self));
2460 let expr = self.rewrite_view_refs(expr);
2464
2465 self.validate_model_operations_before_auth(&expr)?;
2466 if let Err(err) = self.check_query_privilege(&expr) {
2470 return Err(RedDBError::Query(format!("permission denied: {err}")));
2471 }
2472
2473 frame.release_snapshot_for_maintenance(&expr);
2474 let statement = query_expr_name(&expr);
2475 let mode = detect_mode(statement);
2476 let query_str = statement;
2477
2478 self.dispatch_expr(expr, query_str, mode)
2479 }
2480
2481 pub(super) fn validate_model_operations_before_auth(
2482 &self,
2483 expr: &QueryExpr,
2484 ) -> RedDBResult<()> {
2485 self.validate_vault_table_access(expr)?;
2486 use crate::catalog::CollectionModel;
2487 use crate::runtime::ddl::polymorphic_resolver;
2488 use reddb_rql::ast::KvCommand;
2489
2490 let system_schema_target = match expr {
2491 QueryExpr::DropTable(q) => Some(q.name.as_str()),
2492 QueryExpr::DropGraph(q) => Some(q.name.as_str()),
2493 QueryExpr::DropVector(q) => Some(q.name.as_str()),
2494 QueryExpr::DropDocument(q) => Some(q.name.as_str()),
2495 QueryExpr::DropKv(q) => Some(q.name.as_str()),
2496 QueryExpr::DropCollection(q) => Some(q.name.as_str()),
2497 QueryExpr::Truncate(q) => Some(q.name.as_str()),
2498 _ => None,
2499 };
2500 if system_schema_target.is_some_and(crate::runtime::impl_ddl::is_system_schema_name) {
2501 return Err(RedDBError::Query("system schema is read-only".to_string()));
2502 }
2503
2504 let expected = match expr {
2505 QueryExpr::DropTable(q) => Some((q.name.as_str(), CollectionModel::Table)),
2506 QueryExpr::DropGraph(q) => Some((q.name.as_str(), CollectionModel::Graph)),
2507 QueryExpr::DropVector(q) => Some((q.name.as_str(), CollectionModel::Vector)),
2508 QueryExpr::DropDocument(q) => Some((q.name.as_str(), CollectionModel::Document)),
2509 QueryExpr::DropKv(q) => Some((q.name.as_str(), q.model)),
2510 QueryExpr::DropCollection(q) => q.model.map(|model| (q.name.as_str(), model)),
2511 QueryExpr::Truncate(q) => q.model.map(|model| (q.name.as_str(), model)),
2512 QueryExpr::KvCommand(cmd) => {
2513 let (collection, model) = match cmd {
2514 KvCommand::Put {
2515 collection, model, ..
2516 }
2517 | KvCommand::Get {
2518 collection, model, ..
2519 }
2520 | KvCommand::Incr {
2521 collection, model, ..
2522 }
2523 | KvCommand::Cas {
2524 collection, model, ..
2525 }
2526 | KvCommand::List {
2527 collection, model, ..
2528 }
2529 | KvCommand::Delete {
2530 collection, model, ..
2531 } => (collection.as_str(), *model),
2532 KvCommand::Rotate { collection, .. }
2533 | KvCommand::History { collection, .. }
2534 | KvCommand::Purge { collection, .. } => {
2535 (collection.as_str(), CollectionModel::Vault)
2536 }
2537 KvCommand::InvalidateTags { collection, .. } => {
2538 (collection.as_str(), CollectionModel::Kv)
2539 }
2540 KvCommand::Watch {
2541 collection, model, ..
2542 } => (collection.as_str(), *model),
2543 KvCommand::Reveal { collection, .. } => {
2544 (collection.as_str(), CollectionModel::Vault)
2545 }
2546 };
2547 Some((collection, model))
2548 }
2549 QueryExpr::ConfigCommand(cmd) => {
2550 self.validate_config_command_before_auth(cmd)?;
2551 None
2552 }
2553 _ => None,
2554 };
2555
2556 let Some((name, expected_model)) = expected else {
2557 return Ok(());
2558 };
2559 if let Some(contract) = self.inner.db.collection_contract_arc(name) {
2564 if self.inner.db.store().get_collection(name).is_some() {
2565 return polymorphic_resolver::ensure_model_match(
2566 expected_model,
2567 contract.declared_model,
2568 );
2569 }
2570 }
2571 let snapshot = self.inner.db.catalog_model_snapshot();
2572 let Some(actual_model) = snapshot
2573 .collections
2574 .iter()
2575 .find(|collection| collection.name == name)
2576 .map(|collection| collection.declared_model.unwrap_or(collection.model))
2577 else {
2578 return Ok(());
2579 };
2580 polymorphic_resolver::ensure_model_match(expected_model, actual_model)
2581 }
2582
2583 pub(super) fn rewrite_view_refs(&self, expr: QueryExpr) -> QueryExpr {
2588 if self.inner.views.read().is_empty() {
2590 return expr;
2591 }
2592 self.rewrite_view_refs_inner(expr)
2593 }
2594
2595 fn rewrite_view_refs_inner(&self, expr: QueryExpr) -> QueryExpr {
2596 use reddb_rql::ast::{Filter, TableSource};
2597 match expr {
2598 QueryExpr::Table(mut tq) => {
2599 if let Some(TableSource::Subquery(body)) = tq.source.take() {
2605 tq.source = Some(TableSource::Subquery(Box::new(
2606 self.rewrite_view_refs_inner(*body),
2607 )));
2608 return QueryExpr::Table(tq);
2609 }
2610
2611 let maybe_view = {
2615 let views = self.inner.views.read();
2616 views.get(&tq.table).cloned()
2617 };
2618 let Some(view) = maybe_view else {
2619 return QueryExpr::Table(tq);
2620 };
2621
2622 if view.materialized {
2628 return QueryExpr::Table(tq);
2629 }
2630
2631 let inner_expr = self.rewrite_view_refs_inner((*view.query).clone());
2635
2636 match inner_expr {
2644 QueryExpr::Table(mut inner_tq) => {
2645 if let Some(outer_filter) = tq.filter.take() {
2646 inner_tq.filter = Some(match inner_tq.filter.take() {
2647 Some(existing) => {
2648 Filter::And(Box::new(existing), Box::new(outer_filter))
2649 }
2650 None => outer_filter,
2651 });
2652 inner_tq.where_expr = inner_tq
2660 .filter
2661 .as_ref()
2662 .map(reddb_rql::sql_lowering::filter_to_expr);
2663 }
2664 if let Some(outer_limit) = tq.limit {
2665 inner_tq.limit = Some(match inner_tq.limit {
2666 Some(existing) => existing.min(outer_limit),
2667 None => outer_limit,
2668 });
2669 }
2670 if let Some(outer_offset) = tq.offset {
2671 inner_tq.offset = Some(match inner_tq.offset {
2672 Some(existing) => existing + outer_offset,
2673 None => outer_offset,
2674 });
2675 }
2676 QueryExpr::Table(inner_tq)
2677 }
2678 other => other,
2679 }
2680 }
2681 QueryExpr::Join(mut jq) => {
2682 jq.left = Box::new(self.rewrite_view_refs_inner(*jq.left));
2683 jq.right = Box::new(self.rewrite_view_refs_inner(*jq.right));
2684 QueryExpr::Join(jq)
2685 }
2686 other => other,
2689 }
2690 }
2691
2692 fn resolve_table_expr_subqueries(
2693 &self,
2694 mut table: TableQuery,
2695 frame: &dyn super::statement_frame::ReadFrame,
2696 ) -> RedDBResult<TableQuery> {
2697 match table.source.take() {
2704 Some(TableSource::Subquery(inner)) => {
2705 let inner = self.resolve_select_expr_subqueries(*inner, frame)?;
2706 table.source = Some(TableSource::Subquery(Box::new(inner)));
2707 }
2708 other => table.source = other,
2709 }
2710
2711 let outer_scopes = relation_scopes_for_query(&QueryExpr::Table(table.clone()));
2712 for item in &mut table.select_items {
2713 if let reddb_rql::ast::SelectItem::Expr { expr, .. } = item {
2714 *expr = self.resolve_expr_subqueries(expr.clone(), &outer_scopes, frame)?;
2715 }
2716 }
2717 if let Some(where_expr) = table.where_expr.take() {
2718 table.where_expr =
2719 Some(self.resolve_expr_subqueries(where_expr, &outer_scopes, frame)?);
2720 table.filter = None;
2721 }
2722 if let Some(having_expr) = table.having_expr.take() {
2723 table.having_expr =
2724 Some(self.resolve_expr_subqueries(having_expr, &outer_scopes, frame)?);
2725 table.having = None;
2726 }
2727 for expr in &mut table.group_by_exprs {
2728 *expr = self.resolve_expr_subqueries(expr.clone(), &outer_scopes, frame)?;
2729 }
2730 for clause in &mut table.order_by {
2731 if let Some(expr) = clause.expr.take() {
2732 clause.expr = Some(self.resolve_expr_subqueries(expr, &outer_scopes, frame)?);
2733 }
2734 }
2735 Ok(table)
2736 }
2737
2738 fn resolve_select_expr_subqueries(
2739 &self,
2740 expr: QueryExpr,
2741 frame: &dyn super::statement_frame::ReadFrame,
2742 ) -> RedDBResult<QueryExpr> {
2743 match expr {
2744 QueryExpr::Table(table) => self
2745 .resolve_table_expr_subqueries(table, frame)
2746 .map(QueryExpr::Table),
2747 QueryExpr::Join(mut join) => {
2748 join.left = Box::new(self.resolve_select_expr_subqueries(*join.left, frame)?);
2749 join.right = Box::new(self.resolve_select_expr_subqueries(*join.right, frame)?);
2750 Ok(QueryExpr::Join(join))
2751 }
2752 other => Ok(other),
2753 }
2754 }
2755
2756 fn resolve_expr_subqueries(
2757 &self,
2758 expr: reddb_rql::ast::Expr,
2759 outer_scopes: &[String],
2760 frame: &dyn super::statement_frame::ReadFrame,
2761 ) -> RedDBResult<reddb_rql::ast::Expr> {
2762 use reddb_rql::ast::Expr;
2763
2764 match expr {
2765 Expr::Subquery { query, span } => {
2766 let values = self.execute_expr_subquery_values(query, outer_scopes, frame)?;
2767 if values.len() > 1 {
2768 return Err(RedDBError::Query(
2769 "scalar subquery returned more than one row".to_string(),
2770 ));
2771 }
2772 Ok(Expr::Literal {
2773 value: values.into_iter().next().unwrap_or(Value::Null),
2774 span,
2775 })
2776 }
2777 Expr::BinaryOp { op, lhs, rhs, span } => Ok(Expr::BinaryOp {
2778 op,
2779 lhs: Box::new(self.resolve_expr_subqueries(*lhs, outer_scopes, frame)?),
2780 rhs: Box::new(self.resolve_expr_subqueries(*rhs, outer_scopes, frame)?),
2781 span,
2782 }),
2783 Expr::UnaryOp { op, operand, span } => Ok(Expr::UnaryOp {
2784 op,
2785 operand: Box::new(self.resolve_expr_subqueries(*operand, outer_scopes, frame)?),
2786 span,
2787 }),
2788 Expr::Cast {
2789 inner,
2790 target,
2791 span,
2792 } => Ok(Expr::Cast {
2793 inner: Box::new(self.resolve_expr_subqueries(*inner, outer_scopes, frame)?),
2794 target,
2795 span,
2796 }),
2797 Expr::FunctionCall { name, args, span } => {
2798 let args = args
2799 .into_iter()
2800 .map(|arg| self.resolve_expr_subqueries(arg, outer_scopes, frame))
2801 .collect::<RedDBResult<Vec<_>>>()?;
2802 Ok(Expr::FunctionCall { name, args, span })
2803 }
2804 Expr::WindowFunctionCall {
2805 name,
2806 args,
2807 mut window,
2808 span,
2809 } => {
2810 let args = args
2811 .into_iter()
2812 .map(|arg| self.resolve_expr_subqueries(arg, outer_scopes, frame))
2813 .collect::<RedDBResult<Vec<_>>>()?;
2814 for expr in &mut window.partition_by {
2815 *expr = self.resolve_expr_subqueries(expr.clone(), outer_scopes, frame)?;
2816 }
2817 for clause in &mut window.order_by {
2818 clause.expr =
2819 self.resolve_expr_subqueries(clause.expr.clone(), outer_scopes, frame)?;
2820 }
2821 Ok(Expr::WindowFunctionCall {
2822 name,
2823 args,
2824 window,
2825 span,
2826 })
2827 }
2828 Expr::Case {
2829 branches,
2830 else_,
2831 span,
2832 } => {
2833 let branches = branches
2834 .into_iter()
2835 .map(|(cond, value)| {
2836 Ok((
2837 self.resolve_expr_subqueries(cond, outer_scopes, frame)?,
2838 self.resolve_expr_subqueries(value, outer_scopes, frame)?,
2839 ))
2840 })
2841 .collect::<RedDBResult<Vec<_>>>()?;
2842 let else_ = else_
2843 .map(|expr| self.resolve_expr_subqueries(*expr, outer_scopes, frame))
2844 .transpose()?
2845 .map(Box::new);
2846 Ok(Expr::Case {
2847 branches,
2848 else_,
2849 span,
2850 })
2851 }
2852 Expr::IsNull {
2853 operand,
2854 negated,
2855 span,
2856 } => Ok(Expr::IsNull {
2857 operand: Box::new(self.resolve_expr_subqueries(*operand, outer_scopes, frame)?),
2858 negated,
2859 span,
2860 }),
2861 Expr::InList {
2862 target,
2863 values,
2864 negated,
2865 span,
2866 } => {
2867 let target =
2868 Box::new(self.resolve_expr_subqueries(*target, outer_scopes, frame)?);
2869 let mut resolved = Vec::new();
2870 for value in values {
2871 if let Expr::Subquery { query, .. } = value {
2872 resolved.extend(
2873 self.execute_expr_subquery_values(query, outer_scopes, frame)?
2874 .into_iter()
2875 .map(Expr::lit),
2876 );
2877 } else {
2878 resolved.push(self.resolve_expr_subqueries(value, outer_scopes, frame)?);
2879 }
2880 }
2881 Ok(Expr::InList {
2882 target,
2883 values: resolved,
2884 negated,
2885 span,
2886 })
2887 }
2888 Expr::Between {
2889 target,
2890 low,
2891 high,
2892 negated,
2893 span,
2894 } => Ok(Expr::Between {
2895 target: Box::new(self.resolve_expr_subqueries(*target, outer_scopes, frame)?),
2896 low: Box::new(self.resolve_expr_subqueries(*low, outer_scopes, frame)?),
2897 high: Box::new(self.resolve_expr_subqueries(*high, outer_scopes, frame)?),
2898 negated,
2899 span,
2900 }),
2901 other => Ok(other),
2902 }
2903 }
2904
2905 fn execute_expr_subquery_values(
2906 &self,
2907 subquery: reddb_rql::ast::ExprSubquery,
2908 outer_scopes: &[String],
2909 frame: &dyn super::statement_frame::ReadFrame,
2910 ) -> RedDBResult<Vec<Value>> {
2911 let query = *subquery.query;
2912 if query_references_outer_scope(&query, outer_scopes) {
2913 return Err(RedDBError::Query(
2914 "NOT_YET_SUPPORTED: correlated subqueries are not supported yet; track follow-up issue #470-correlated-subqueries".to_string(),
2915 ));
2916 }
2917 let query = self.rewrite_view_refs(query);
2918 let query = self.resolve_select_expr_subqueries(query, frame)?;
2919 let query = self.authorize_relational_select_expr(query, frame)?;
2920 let result = match query {
2921 QueryExpr::Table(table) => {
2922 execute_runtime_table_query(&self.inner.db, &table, Some(&self.inner.index_store))?
2923 }
2924 QueryExpr::Join(join) => execute_runtime_join_query(self, &join)?,
2925 other => {
2926 return Err(RedDBError::Query(format!(
2927 "expression subquery must be a SELECT query, got {}",
2928 query_expr_name(&other)
2929 )))
2930 }
2931 };
2932 first_column_values(result)
2933 }
2934
2935 fn execute_materialized_graph(
2936 &self,
2937 query: &str,
2938 mode: QueryMode,
2939 statement: &'static str,
2940 expr: &QueryExpr,
2941 ) -> RedDBResult<RuntimeQueryResult> {
2942 let (graph, node_properties, edge_properties) = self.materialize_graph_with_rls()?;
2950 let result =
2951 crate::storage::query::unified::UnifiedExecutor::execute_on_with_graph_properties_checked(
2952 &graph,
2953 expr,
2954 node_properties,
2955 edge_properties,
2956 super::function_budget::graph_work_check(),
2957 )
2958 .map_err(|err| RedDBError::Query(err.to_string()))?;
2959
2960 Ok(RuntimeQueryResult {
2961 query: query.to_string(),
2962 mode,
2963 statement,
2964 engine: "materialized-graph",
2965 result,
2966 affected_rows: 0,
2967 statement_type: "select",
2968 bookmark: None,
2969 notice: None,
2970 })
2971 }
2972
2973 fn dispatch_expr(
2974 &self,
2975 expr: QueryExpr,
2976 query_str: &str,
2977 mode: QueryMode,
2978 ) -> RedDBResult<RuntimeQueryResult> {
2979 let is_read = super::query_budget::is_materialized_read(&expr);
2982 let _budget = if is_read {
2983 Some(super::query_budget::Scope::enter(
2984 self.resource_limits(),
2985 &self.inner.query_budget_slots,
2986 )?)
2987 } else {
2988 None
2989 };
2990 let result = self.dispatch_expr_inner(expr, query_str, mode);
2991 if is_read {
2992 super::query_budget::charge(0)?;
2993 }
2994 let result = result?;
2995 super::query_budget::check_result(self.resource_limits(), &result)?;
2996 if is_read {
2997 super::query_budget::charge(0)?;
2998 }
2999 Ok(result)
3000 }
3001
3002 fn dispatch_expr_inner(
3003 &self,
3004 expr: QueryExpr,
3005 query_str: &str,
3006 mode: QueryMode,
3007 ) -> RedDBResult<RuntimeQueryResult> {
3008 let statement = query_expr_name(&expr);
3009 match expr {
3010 QueryExpr::Function(command) => self.execute_function(query_str, &command),
3011 QueryExpr::Graph(_) | QueryExpr::Path(_) => {
3012 self.execute_materialized_graph(query_str, mode, statement, &expr)
3013 }
3014 QueryExpr::Table(table) => {
3015 let scope = self.ai_scope();
3016 let table = self.resolve_table_expr_subqueries(
3017 table,
3018 &scope as &dyn super::statement_frame::ReadFrame,
3019 )?;
3020 if let Some(TableSource::Function {
3024 name,
3025 args,
3026 named_args,
3027 }) = table.source.clone()
3028 {
3029 return Ok(RuntimeQueryResult {
3030 query: query_str.to_string(),
3031 mode,
3032 statement,
3033 engine: "runtime-graph-tvf",
3034 result: self.execute_table_function(&name, &args, &named_args)?,
3035 affected_rows: 0,
3036 statement_type: "select",
3037 bookmark: None,
3038 notice: None,
3039 });
3040 }
3041 if let Some(TableSource::InlineGraphFunction {
3045 name,
3046 nodes,
3047 edges,
3048 named_args,
3049 }) = table.source.clone()
3050 {
3051 return Ok(RuntimeQueryResult {
3052 query: query_str.to_string(),
3053 mode,
3054 statement,
3055 engine: "runtime-graph-tvf-inline",
3056 result: self.execute_inline_graph_function(
3057 &name,
3058 &nodes,
3059 &edges,
3060 &named_args,
3061 )?,
3062 affected_rows: 0,
3063 statement_type: "select",
3064 bookmark: None,
3065 notice: None,
3066 });
3067 }
3068 if super::red_schema::is_virtual_table(&table.table) {
3069 return Ok(RuntimeQueryResult {
3070 query: query_str.to_string(),
3071 mode,
3072 statement,
3073 engine: "runtime-red-schema",
3074 result: super::red_schema::red_query(
3075 self,
3076 &table.table,
3077 &table,
3078 &scope as &dyn super::statement_frame::ReadFrame,
3079 )?,
3080 affected_rows: 0,
3081 statement_type: "select",
3082 bookmark: None,
3083 notice: None,
3084 });
3085 }
3086 if let Some(view_result) = self.try_resolve_analytics_view(
3088 &table,
3089 &scope as &dyn super::statement_frame::ReadFrame,
3090 )? {
3091 return Ok(RuntimeQueryResult {
3092 query: query_str.to_string(),
3093 mode,
3094 statement,
3095 engine: "runtime-graph-analytics-view",
3096 result: view_result,
3097 affected_rows: 0,
3098 statement_type: "select",
3099 bookmark: None,
3100 notice: None,
3101 });
3102 }
3103 let Some(table_with_rls) = self.authorize_relational_table_select(
3104 table,
3105 &scope as &dyn super::statement_frame::ReadFrame,
3106 )?
3107 else {
3108 return Ok(RuntimeQueryResult {
3109 query: query_str.to_string(),
3110 mode,
3111 statement,
3112 engine: "runtime-table-rls",
3113 result: crate::storage::query::unified::UnifiedResult::empty(),
3114 affected_rows: 0,
3115 statement_type: "select",
3116 bookmark: None,
3117 notice: None,
3118 });
3119 };
3120 Ok(RuntimeQueryResult {
3121 query: query_str.to_string(),
3122 mode,
3123 statement,
3124 engine: "runtime-table",
3125 result: execute_runtime_table_query(
3126 &self.inner.db,
3127 &table_with_rls,
3128 Some(&self.inner.index_store),
3129 )?,
3130 affected_rows: 0,
3131 statement_type: "select",
3132 bookmark: None,
3133 notice: None,
3134 })
3135 }
3136 QueryExpr::Join(join) => {
3137 let scope = self.ai_scope();
3138 let Some(join_with_rls) = self.authorize_relational_join_select(
3139 join,
3140 &scope as &dyn super::statement_frame::ReadFrame,
3141 )?
3142 else {
3143 return Ok(RuntimeQueryResult {
3144 query: query_str.to_string(),
3145 mode,
3146 statement,
3147 engine: "runtime-join-rls",
3148 result: crate::storage::query::unified::UnifiedResult::empty(),
3149 affected_rows: 0,
3150 statement_type: "select",
3151 bookmark: None,
3152 notice: None,
3153 });
3154 };
3155 Ok(RuntimeQueryResult {
3156 query: query_str.to_string(),
3157 mode,
3158 statement,
3159 engine: "runtime-join",
3160 result: execute_runtime_join_query(self, &join_with_rls)?,
3161 affected_rows: 0,
3162 statement_type: "select",
3163 bookmark: None,
3164 notice: None,
3165 })
3166 }
3167 QueryExpr::Vector(vector) => Ok(RuntimeQueryResult {
3168 query: query_str.to_string(),
3169 mode,
3170 statement,
3171 engine: "runtime-vector",
3172 result: execute_runtime_vector_query(self, &vector)?,
3173 affected_rows: 0,
3174 statement_type: "select",
3175 bookmark: None,
3176 notice: None,
3177 }),
3178 QueryExpr::Hybrid(hybrid) => Ok(RuntimeQueryResult {
3179 query: query_str.to_string(),
3180 mode,
3181 statement,
3182 engine: "runtime-hybrid",
3183 result: execute_runtime_hybrid_query(self, &hybrid)?,
3184 affected_rows: 0,
3185 statement_type: "select",
3186 bookmark: None,
3187 notice: None,
3188 }),
3189 QueryExpr::Insert(ref insert) if super::red_schema::is_virtual_table(&insert.table) => {
3190 Err(RedDBError::Query(
3191 super::red_schema::READ_ONLY_ERROR.to_string(),
3192 ))
3193 }
3194 QueryExpr::Update(ref update) if super::red_schema::is_virtual_table(&update.table) => {
3195 Err(RedDBError::Query(
3196 super::red_schema::READ_ONLY_ERROR.to_string(),
3197 ))
3198 }
3199 QueryExpr::Delete(ref delete) if super::red_schema::is_virtual_table(&delete.table) => {
3200 Err(RedDBError::Query(
3201 super::red_schema::READ_ONLY_ERROR.to_string(),
3202 ))
3203 }
3204 QueryExpr::Insert(ref insert) => self
3205 .with_deferred_store_wal_for_dml(self.insert_may_emit_events(insert), || {
3206 self.execute_insert(query_str, insert)
3207 }),
3208 QueryExpr::Update(ref update) => self
3209 .with_deferred_store_wal_for_dml(self.update_may_emit_events(update), || {
3210 self.execute_update(query_str, update)
3211 }),
3212 QueryExpr::Delete(ref delete) => self
3213 .with_deferred_store_wal_for_dml(self.delete_may_emit_events(delete), || {
3214 self.execute_delete(query_str, delete)
3215 }),
3216 QueryExpr::QueueCommand(ref cmd) => self.with_deferred_store_wal_if_transaction(|| {
3217 self.execute_queue_command(query_str, cmd)
3218 }),
3219 QueryExpr::KvCommand(ref cmd) => self.execute_kv_command(query_str, cmd),
3220 QueryExpr::SearchCommand(ref cmd) => self.execute_search_command(query_str, cmd),
3221 QueryExpr::Ask(ref ask) => self.execute_ask(query_str, ask),
3222 _ => Err(RedDBError::Query(format!(
3223 "prepared-statement execution does not support {statement} statements"
3224 ))),
3225 }
3226 }
3227
3228 fn execute_table_function(
3235 &self,
3236 name: &str,
3237 args: &[String],
3238 named_args: &[(String, f64)],
3239 ) -> RedDBResult<crate::storage::query::unified::UnifiedResult> {
3240 if name.eq_ignore_ascii_case("red.diff") {
3241 return self.execute_vcs_diff_tvf(args, named_args);
3242 }
3243 if !is_graph_tvf_name(name) {
3244 return Err(RedDBError::Query(format!("unknown table function: {name}")));
3245 }
3246 if args.len() != 1 {
3248 return Err(RedDBError::Query(format!(
3249 "table function '{name}' takes exactly 1 graph argument, got {}",
3250 args.len()
3251 )));
3252 }
3253
3254 let (nodes, edges) = self.materialize_whole_graph_abstract()?;
3259 self.dispatch_graph_algorithm(name, nodes, edges, named_args)
3260 }
3261
3262 pub(crate) fn revive_versioned_updates_since(
3263 &self,
3264 conn_id: u64,
3265 stamper_xid: u64,
3266 ) -> RedDBResult<usize> {
3267 let candidates = {
3270 let guard = self.inner.pending_versioned_updates.read();
3271 let Some(pending) = guard.get(&conn_id) else {
3272 return Ok(0);
3273 };
3274 pending
3275 .iter()
3276 .rev()
3277 .filter(|entry| entry.3 >= stamper_xid)
3278 .cloned()
3279 .collect::<Vec<_>>()
3280 };
3281 let mut reverted = 0usize;
3282 for candidate in candidates {
3283 let (collection, old_id, new_id, xid, previous_xmax) = &candidate;
3284 let topology_lock = self.index_store_ref().collection_topology_lock(collection);
3285 #[cfg(test)]
3286 self.index_store_ref().mutation_test_hook(
3287 collection,
3288 super::index_store::MutationTestPhase::BeforeUndoTopology,
3289 );
3290 let topology_guard = topology_lock.read();
3291 let mut guard = self.inner.pending_versioned_updates.write();
3292 let Some(pending) = guard.get_mut(&conn_id) else {
3293 continue;
3294 };
3295 let Some(position) = pending.iter().rposition(|entry| entry == &candidate) else {
3298 continue;
3299 };
3300 self.revive_versioned_update(
3301 collection,
3302 *old_id,
3303 *new_id,
3304 *xid,
3305 *previous_xmax,
3306 topology_guard,
3307 )?;
3308 pending.remove(position);
3309 reverted += 1;
3310 if pending.is_empty() {
3311 guard.remove(&conn_id);
3312 }
3313 }
3314 Ok(reverted)
3315 }
3316
3317 fn check_explain_target_privilege(&self, inner_sql: &str) -> RedDBResult<()> {
3323 let parsed =
3324 reddb_rql::parser::parse(inner_sql).map_err(|e| RedDBError::Query(e.to_string()))?;
3325 self.check_query_privilege(&parsed.query)
3326 .map_err(|err| RedDBError::Query(format!("permission denied: {err}")))
3327 }
3328
3329 fn explain_as_rows(&self, raw_query: &str, inner_sql: &str) -> RedDBResult<RuntimeQueryResult> {
3337 self.check_explain_target_privilege(inner_sql)?;
3338 let explain = self.explain_query(inner_sql)?;
3339
3340 let mut columns = vec![
3341 "op".to_string(),
3342 "source".to_string(),
3343 "estimated_rows".to_string(),
3344 "estimated_cost".to_string(),
3345 "depth".to_string(),
3346 ];
3347
3348 let mut records: Vec<crate::storage::query::unified::UnifiedRecord> = Vec::new();
3349
3350 for name in &explain.cte_materializations {
3356 use std::sync::Arc;
3357 let mut rec = crate::storage::query::unified::UnifiedRecord::default();
3358 rec.set_arc(Arc::from("op"), Value::text("CteScan".to_string()));
3359 rec.set_arc(Arc::from("source"), Value::text(name.clone()));
3360 rec.set_arc(Arc::from("estimated_rows"), Value::Float(0.0));
3361 rec.set_arc(Arc::from("estimated_cost"), Value::Float(0.0));
3362 rec.set_arc(Arc::from("depth"), Value::Integer(0));
3363 records.push(rec);
3364 }
3365
3366 let vector_details = explain.statement == "vector";
3367 if vector_details {
3368 columns.extend(["access_path_reason".to_string(), "index_hint".to_string()]);
3369 }
3370 walk_plan_node(&explain.logical_plan.root, 0, &mut records, vector_details);
3371
3372 let result = crate::storage::query::unified::UnifiedResult {
3373 columns,
3374 records,
3375 stats: Default::default(),
3376 pre_serialized_json: None,
3377 };
3378
3379 Ok(RuntimeQueryResult {
3380 query: raw_query.to_string(),
3381 mode: explain.mode,
3382 statement: "explain",
3383 engine: "runtime-explain",
3384 result,
3385 affected_rows: 0,
3386 statement_type: "select",
3387 bookmark: None,
3388 notice: None,
3389 })
3390 }
3391
3392 fn explain_analyze_vector_as_rows(
3393 &self,
3394 raw_query: &str,
3395 expr: QueryExpr,
3396 ) -> RedDBResult<RuntimeQueryResult> {
3397 let QueryExpr::Vector(query) = &expr else {
3398 return Err(RedDBError::Internal(
3399 "expected vector analysis target".to_string(),
3400 ));
3401 };
3402 let source = query.collection.clone();
3403 let execution = self.execute_query_expr(expr)?;
3406 let stats = execution.result.stats;
3407 let vector = stats.vector.as_ref().ok_or_else(|| {
3408 RedDBError::Internal("vector executor did not provide measurements".to_string())
3409 })?;
3410 let fields = vec![
3411 ("op", Value::text(vector.access_path.clone())),
3412 ("source", Value::text(source)),
3413 ("index_used", Value::Boolean(vector.index_used)),
3414 ("mode_requested", Value::text(vector.mode_requested.clone())),
3415 ("mode_executed", Value::text(vector.mode_executed.clone())),
3416 (
3417 "fallback_reason",
3418 vector
3419 .fallback_reason
3420 .as_ref()
3421 .map_or(Value::Null, |reason| Value::text(reason.clone())),
3422 ),
3423 (
3424 "approximate_distance_evaluations",
3425 Value::UnsignedInteger(vector.approximate_distance_evaluations),
3426 ),
3427 (
3428 "peak_topk_entries",
3429 Value::UnsignedInteger(vector.peak_topk_entries),
3430 ),
3431 (
3432 "candidates_examined",
3433 Value::UnsignedInteger(vector.candidates_examined),
3434 ),
3435 (
3436 "metadata_rejected",
3437 Value::UnsignedInteger(vector.metadata_rejected),
3438 ),
3439 ("rls_rejected", Value::UnsignedInteger(vector.rls_rejected)),
3440 (
3441 "visibility_rejected",
3442 Value::UnsignedInteger(vector.visibility_rejected),
3443 ),
3444 (
3445 "exact_distance_evaluations",
3446 Value::UnsignedInteger(vector.exact_distance_evaluations),
3447 ),
3448 ("actual_rows", Value::UnsignedInteger(vector.rows_returned)),
3449 (
3450 "actual_ms",
3451 Value::Float(stats.exec_time_us as f64 / 1000.0),
3452 ),
3453 ("metrics_scope", Value::text("vector_pipeline")),
3454 (
3455 "operators",
3456 Value::Json(vector.operators_json().to_string().into_bytes()),
3457 ),
3458 ];
3459 let columns = fields.iter().map(|(name, _)| name.to_string()).collect();
3460 let mut record = crate::storage::query::unified::UnifiedRecord::default();
3461 for (name, value) in fields {
3462 record.set_owned(name.to_string(), value);
3463 }
3464 Ok(RuntimeQueryResult {
3465 query: raw_query.to_string(),
3466 mode: execution.mode,
3467 statement: "explain_analyze",
3468 engine: "runtime-explain-analyze",
3469 result: crate::storage::query::unified::UnifiedResult {
3470 columns,
3471 records: vec![record],
3472 stats,
3473 pre_serialized_json: None,
3474 },
3475 affected_rows: 0,
3476 statement_type: "select",
3477 bookmark: None,
3478 notice: None,
3479 })
3480 }
3481
3482 fn explain_analyze_as_rows(
3483 &self,
3484 raw_query: &str,
3485 inner_sql: &str,
3486 ) -> RedDBResult<RuntimeQueryResult> {
3487 if strip_keyword_ci(inner_sql.trim_start(), "VECTOR").is_some() {
3488 let expr = parse_multi(inner_sql).map_err(|err| RedDBError::Query(err.to_string()))?;
3489 if matches!(&expr, QueryExpr::Vector(_)) {
3490 return self.explain_analyze_vector_as_rows(raw_query, expr);
3491 }
3492 }
3493 if !starts_with_dml_keyword(inner_sql) {
3494 return Err(RedDBError::Query(
3495 "EXPLAIN ANALYZE currently supports INSERT, UPDATE, DELETE, and VECTOR SEARCH"
3496 .to_string(),
3497 ));
3498 }
3499
3500 self.check_explain_target_privilege(inner_sql)?;
3501 let explain = self.explain_query(inner_sql)?;
3502 let conn_id = current_connection_id();
3503 if self.inner.transaction_state.in_transaction(conn_id) {
3504 return Err(RedDBError::Query(
3505 "EXPLAIN ANALYZE requires no active transaction".to_string(),
3506 ));
3507 }
3508
3509 self.execute_query_inner("BEGIN")?;
3510 let started = std::time::Instant::now();
3511 let execution = self.execute_query_inner(inner_sql);
3512 let elapsed_ms = started.elapsed().as_secs_f64() * 1000.0;
3513 let rollback = self.execute_query_inner("ROLLBACK");
3514
3515 let affected_rows = match execution {
3516 Ok(result) => result.affected_rows,
3517 Err(err) => {
3518 rollback?;
3519 return Err(err);
3520 }
3521 };
3522 rollback?;
3523
3524 let columns = vec![
3525 "op".to_string(),
3526 "source".to_string(),
3527 "estimated_rows".to_string(),
3528 "estimated_cost".to_string(),
3529 "actual_rows".to_string(),
3530 "actual_ms".to_string(),
3531 "depth".to_string(),
3532 ];
3533 let mut records = Vec::new();
3534 walk_analyze_plan_node(
3535 &explain.logical_plan.root,
3536 0,
3537 affected_rows,
3538 elapsed_ms,
3539 &mut records,
3540 );
3541
3542 let result = crate::storage::query::unified::UnifiedResult {
3543 columns,
3544 records,
3545 stats: Default::default(),
3546 pre_serialized_json: None,
3547 };
3548
3549 Ok(RuntimeQueryResult {
3550 query: raw_query.to_string(),
3551 mode: explain.mode,
3552 statement: "explain_analyze",
3553 engine: "runtime-explain-analyze",
3554 result,
3555 affected_rows: 0,
3556 statement_type: "select",
3557 bookmark: None,
3558 notice: None,
3559 })
3560 }
3561}
3562
3563fn redact_sql_for_slow_log(query: &str) -> String {
3564 let lowered = query.to_ascii_lowercase();
3565 if lowered.contains("secret") || lowered.contains("vault") {
3566 return "secret statement (SQL redacted)".to_string();
3567 }
3568 let credential_candidate = lowered.contains("password")
3569 && ((lowered.contains("create") && lowered.contains("user"))
3570 || (lowered.contains("alter") && lowered.contains("user")));
3571 if !credential_candidate {
3572 return query.to_string();
3573 }
3574 match parse_multi(query) {
3575 Ok(QueryExpr::CreateUser(stmt)) => {
3576 let target = UserId::from_parts(stmt.tenant.as_deref(), &stmt.username);
3577 format!("CREATE USER {target} PASSWORD '***' ROLE {}", stmt.role)
3578 }
3579 Ok(QueryExpr::AlterUser(stmt))
3580 if stmt
3581 .attributes
3582 .iter()
3583 .any(|attr| matches!(attr, reddb_rql::ast::AlterUserAttribute::Password(_))) =>
3584 {
3585 let target = UserId::from_parts(stmt.tenant.as_deref(), &stmt.username);
3586 format!("ALTER USER {target} PASSWORD '***'")
3587 }
3588 Ok(_) => "credential statement (SQL redacted)".to_string(),
3589 Err(_) => "credential statement (SQL redacted)".to_string(),
3590 }
3591}
3592
3593fn strip_explain_analyze_prefix(sql: &str) -> Option<&str> {
3594 let rest = strip_keyword_ci(sql.trim_start(), "EXPLAIN")?.trim_start();
3595 Some(strip_keyword_ci(rest, "ANALYZE")?.trim_start()).filter(|inner| !inner.is_empty())
3596}
3597
3598fn strip_keyword_ci<'a>(sql: &'a str, keyword: &str) -> Option<&'a str> {
3599 let head = sql.get(..keyword.len())?;
3602 let rest = &sql[keyword.len()..];
3603 if !head.eq_ignore_ascii_case(keyword) {
3604 return None;
3605 }
3606 if rest
3607 .chars()
3608 .next()
3609 .is_some_and(|ch| !ch.is_ascii_whitespace())
3610 {
3611 return None;
3612 }
3613 Some(rest)
3614}
3615
3616fn starts_with_dml_keyword(sql: &str) -> bool {
3617 let trimmed = sql.trim_start();
3618 let head_end = trimmed
3619 .find(|ch: char| ch.is_ascii_whitespace())
3620 .unwrap_or(trimmed.len());
3621 let head = &trimmed[..head_end];
3622 head.eq_ignore_ascii_case("INSERT")
3623 || head.eq_ignore_ascii_case("UPDATE")
3624 || head.eq_ignore_ascii_case("DELETE")
3625}
3626
3627fn walk_plan_node(
3628 node: &crate::storage::query::planner::CanonicalLogicalNode,
3629 depth: usize,
3630 out: &mut Vec<crate::storage::query::unified::UnifiedRecord>,
3631 vector_details: bool,
3632) {
3633 use std::sync::Arc;
3634 let mut rec = crate::storage::query::unified::UnifiedRecord::default();
3635 rec.set_arc(Arc::from("op"), Value::text(node.operator.clone()));
3636 rec.set_arc(
3637 Arc::from("source"),
3638 node.source.clone().map(Value::text).unwrap_or(Value::Null),
3639 );
3640 rec.set_arc(
3641 Arc::from("estimated_rows"),
3642 Value::Float(node.estimated_rows),
3643 );
3644 rec.set_arc(
3645 Arc::from("estimated_cost"),
3646 Value::Float(node.operator_cost),
3647 );
3648 rec.set_arc(Arc::from("depth"), Value::Integer(depth as i64));
3649 if vector_details {
3650 for name in ["access_path_reason", "index_hint"] {
3651 rec.set_owned(
3652 name.to_string(),
3653 node.details
3654 .get(name)
3655 .cloned()
3656 .map(Value::text)
3657 .unwrap_or(Value::Null),
3658 );
3659 }
3660 }
3661 out.push(rec);
3662 for child in &node.children {
3663 walk_plan_node(child, depth + 1, out, vector_details);
3664 }
3665}
3666
3667fn walk_analyze_plan_node(
3668 node: &crate::storage::query::planner::CanonicalLogicalNode,
3669 depth: usize,
3670 affected_rows: u64,
3671 elapsed_ms: f64,
3672 out: &mut Vec<crate::storage::query::unified::UnifiedRecord>,
3673) {
3674 use std::sync::Arc;
3675 let mut rec = crate::storage::query::unified::UnifiedRecord::default();
3676 rec.set_arc(Arc::from("op"), Value::text(node.operator.clone()));
3677 rec.set_arc(
3678 Arc::from("source"),
3679 node.source.clone().map(Value::text).unwrap_or(Value::Null),
3680 );
3681 rec.set_arc(
3682 Arc::from("estimated_rows"),
3683 Value::Float(node.estimated_rows),
3684 );
3685 rec.set_arc(
3686 Arc::from("estimated_cost"),
3687 Value::Float(node.operator_cost),
3688 );
3689 rec.set_arc(
3690 Arc::from("actual_rows"),
3691 Value::UnsignedInteger(if depth == 0 { affected_rows } else { 0 }),
3692 );
3693 rec.set_arc(
3694 Arc::from("actual_ms"),
3695 Value::Float(if depth == 0 { elapsed_ms } else { 0.0 }),
3696 );
3697 rec.set_arc(Arc::from("depth"), Value::Integer(depth as i64));
3698 out.push(rec);
3699
3700 for child in &node.children {
3701 walk_analyze_plan_node(child, depth + 1, 0, 0.0, out);
3702 }
3703}
3704
3705#[cfg(test)]
3706mod declared_model_validation_tests {
3707 use super::*;
3708 use crate::catalog::CollectionModel;
3709 use crate::storage::no_malloc_hot_paths::measure_allocations;
3710 use reddb_rql::ast::KvCommand;
3711
3712 fn get(collection: &str) -> QueryExpr {
3713 QueryExpr::KvCommand(KvCommand::Get {
3714 model: CollectionModel::Kv,
3715 collection: collection.into(),
3716 key: "item".into(),
3717 })
3718 }
3719
3720 #[test]
3721 fn declared_model_validation_keeps_mismatches_and_observes_contract_changes() {
3722 let runtime = RedDBRuntime::in_memory().expect("runtime");
3723 runtime
3724 .execute_query("KV PUT declared.item = 1")
3725 .expect("seed");
3726 runtime
3727 .validate_model_operations_before_auth(&get("declared"))
3728 .expect("KV contract");
3729 let db = runtime.db();
3730 for (model, name) in [
3731 (CollectionModel::Table, "table"),
3732 (CollectionModel::Mixed, "mixed"),
3733 ] {
3734 let mut contract = db.collection_contract("declared").expect("contract");
3735 contract.declared_model = model;
3736 db.save_collection_contract(contract)
3737 .expect("replace contract");
3738 let error = runtime
3739 .validate_model_operations_before_auth(&get("declared"))
3740 .expect_err("mismatch");
3741 assert_eq!(
3742 error.to_string(),
3743 format!("INVALID_OPERATION: model mismatch: expected kv, got {name}")
3744 );
3745 }
3746 }
3747
3748 #[test]
3749 fn declared_model_validation_preserves_legacy_inference_and_missing_collections() {
3750 let runtime = RedDBRuntime::in_memory().expect("runtime");
3751 let db = runtime.db();
3752 let store = db.store();
3753 store
3754 .create_collection("legacy")
3755 .expect("legacy collection");
3756 store
3757 .get_collection("legacy")
3758 .expect("manager")
3759 .insert(crate::storage::UnifiedEntity::table_row(
3760 crate::storage::EntityId::new(1),
3761 "legacy",
3762 1,
3763 Vec::new(),
3764 ))
3765 .expect("uncontracted table row");
3766 assert!(db.collection_contract("legacy").is_none());
3767 let error = runtime
3768 .validate_model_operations_before_auth(&get("legacy"))
3769 .expect_err("inferred table");
3770 assert!(error
3771 .to_string()
3772 .contains("model mismatch: expected kv, got table"));
3773 runtime
3774 .validate_model_operations_before_auth(&get("missing"))
3775 .expect("missing stays deferred to execution");
3776 }
3777
3778 #[test]
3779 fn declared_model_validation_does_not_treat_orphan_contracts_as_collections() {
3780 let runtime = RedDBRuntime::in_memory().expect("runtime");
3781 runtime
3782 .execute_query("KV PUT declared.item = 1")
3783 .expect("seed");
3784 let db = runtime.db();
3785 let mut contract = db.collection_contract("declared").expect("contract");
3786 contract.name = "orphan".into();
3787 contract.declared_model = CollectionModel::Table;
3788 db.save_collection_contract(contract)
3789 .expect("orphan contract");
3790 assert!(db.store().get_collection("orphan").is_none());
3791 runtime
3792 .validate_model_operations_before_auth(&get("orphan"))
3793 .expect("no physical collection to validate");
3794 }
3795
3796 #[test]
3797 fn declared_model_validation_allocates_nothing_with_unrelated_collections() {
3798 let runtime = RedDBRuntime::in_memory().expect("runtime");
3799 runtime
3800 .execute_query("KV PUT declared.item = 1")
3801 .expect("seed");
3802 let expr = get("declared");
3803 for unrelated in [0, 64] {
3804 let db = runtime.db();
3805 let store = db.store();
3806 for index in 0..unrelated {
3807 let name = format!("unrelated_{index}");
3808 store
3809 .create_collection(&name)
3810 .expect("unrelated collection");
3811 }
3812 runtime
3813 .validate_model_operations_before_auth(&expr)
3814 .expect("warm contract cache");
3815 let (result, count) =
3816 measure_allocations(|| runtime.validate_model_operations_before_auth(&expr));
3817 result.expect("declared KV");
3818 assert_eq!(
3819 count.allocs, 0,
3820 "validation must only borrow the target contract"
3821 );
3822 }
3823 }
3824}
3825
3826#[cfg(test)]
3827mod multibyte_sql_tests {
3828 use super::*;
3829
3830 #[test]
3831 fn explain_analyze_prefix_ignores_multibyte_text_at_the_keyword_boundary() {
3832 for pad in 0..16 {
3835 for wide in ["é", "日", "😀"] {
3836 let sql = format!("{}{wide}llo", "a".repeat(pad));
3837 assert_eq!(strip_explain_analyze_prefix(&sql), None, "{sql:?}");
3838 }
3839 }
3840 assert_eq!(
3841 strip_explain_analyze_prefix("explain analyze DELETE FROM t"),
3842 Some("DELETE FROM t")
3843 );
3844 }
3845
3846 #[test]
3851 fn multibyte_sql_never_panics_execute_query() {
3852 let rt = RedDBRuntime::in_memory().expect("in-memory runtime");
3853 let seeds = [
3854 "",
3855 "SELECT ",
3856 "SELECT '",
3857 "EXPLAIN ",
3858 "EXPLAIN ANALYZE ",
3859 "CHECKPOINT ",
3860 "CHECKOUT ",
3861 "RESET ",
3862 "MERGE ",
3863 "CHERRY PICK ",
3864 "REVERT ",
3865 "RESOLVE CONFLICT ",
3866 "INSERT INTO t VALUES ('",
3867 ];
3868 let mut panicked = Vec::new();
3869 for seed in seeds {
3870 for pad in 0..16 {
3871 for wide in ["é", "日", "😀"] {
3872 let sql = format!("{seed}{}{wide}llo", "a".repeat(pad));
3873 let outcome = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
3874 let _ = rt.execute_query(&sql);
3875 }));
3876 if outcome.is_err() {
3877 panicked.push(sql);
3878 }
3879 }
3880 }
3881 }
3882 assert!(
3883 panicked.is_empty(),
3884 "execute_query panicked on {} statement(s), first: {:?}",
3885 panicked.len(),
3886 panicked.first()
3887 );
3888 }
3889}
3890
3891#[cfg(test)]
3892mod inline_graph_tvf_tests {
3893 use super::*;
3894
3895 fn scopes_for(sql: &str) -> HashSet<String> {
3896 let expr = reddb_rql::parser::parse(sql).expect("parse").query;
3897 query_expr_result_cache_scopes(&expr)
3898 }
3899
3900 #[test]
3901 fn inline_tvf_cache_scopes_include_source_collections() {
3902 let scopes = scopes_for(
3906 "SELECT * FROM components(nodes => (SELECT id FROM hosts), edges => (SELECT src, dst FROM links))",
3907 );
3908 assert!(scopes.contains("hosts"), "nodes source scoped: {scopes:?}");
3909 assert!(scopes.contains("links"), "edges source scoped: {scopes:?}");
3910 }
3911
3912 #[test]
3913 fn graph_collection_tvf_cache_scope_is_graph_argument() {
3914 let scopes = scopes_for("SELECT * FROM components(g)");
3918 assert!(scopes.contains("g"), "collection form scoped: {scopes:?}");
3919 }
3920
3921 #[test]
3922 fn abstract_degree_centrality_counts_undirected_endpoints() {
3923 let nodes = vec!["a".to_string(), "b".to_string(), "c".to_string()];
3924 let edges = vec![
3925 ("a".to_string(), "b".to_string(), 1.0_f32),
3926 ("b".to_string(), "c".to_string(), 1.0_f32),
3927 ];
3928 let degrees = abstract_degree_centrality(&nodes, &edges);
3929 assert_eq!(
3930 degrees,
3931 vec![
3932 ("a".to_string(), 1),
3933 ("b".to_string(), 2),
3934 ("c".to_string(), 1),
3935 ]
3936 );
3937 }
3938}
3939
3940fn reject_engine_internal_vault_key(key: &str) -> RedDBResult<()> {
3947 let lowered = key.to_ascii_lowercase();
3948 let internal = lowered == "red.secret.aes_key"
3949 || lowered.starts_with("red.iam.")
3950 || lowered.starts_with("red.acl.")
3951 || lowered.starts_with("red.vault.");
3952 if internal {
3953 return Err(RedDBError::Query(format!(
3954 "`{key}` is an engine-internal vault key and cannot be set or deleted through SET/DELETE SECRET"
3955 )));
3956 }
3957 Ok(())
3958}
3959
3960fn confine_copy_from_path(path: &str) -> RedDBResult<()> {
3966 let Ok(root) = std::env::var("REDDB_COPY_IMPORT_ROOT") else {
3967 return Ok(());
3968 };
3969 let root = std::fs::canonicalize(&root).map_err(|e| {
3970 RedDBError::Query(format!(
3971 "REDDB_COPY_IMPORT_ROOT `{root}` is not usable: {e}"
3972 ))
3973 })?;
3974 let resolved = std::fs::canonicalize(path)
3975 .map_err(|e| RedDBError::Query(format!("COPY FROM `{path}`: {e}")))?;
3976 if !resolved.starts_with(&root) || !resolved.is_file() {
3977 return Err(RedDBError::Query(format!(
3978 "COPY FROM `{path}` is outside the configured import root"
3979 )));
3980 }
3981 Ok(())
3982}
3983
3984#[cfg(test)]
3985mod result_cache_execution_tests {
3986 use super::*;
3987
3988 const QUERY: &str = "SELECT SUM(n) AS n FROM result_cache_probe";
3989
3990 fn runtime_with_rows(cache_enabled: bool) -> RedDBRuntime {
3991 let runtime = RedDBRuntime::in_memory().expect("runtime");
3992 runtime
3993 .execute_query("CREATE TABLE result_cache_probe (id INT, n INT)")
3994 .expect("create table");
3995 runtime
3996 .execute_query(
3997 "INSERT INTO result_cache_probe (id, n) VALUES (1, 10), (2, 20), (3, 30)",
3998 )
3999 .expect("seed rows");
4000 runtime
4001 .execute_query(&format!(
4002 "SET CONFIG runtime.result_cache.enabled = {cache_enabled}"
4003 ))
4004 .expect("configure result cache");
4005 runtime
4006 }
4007
4008 fn aggregate_value(runtime: &RedDBRuntime) -> i64 {
4009 let result = runtime.execute_query(QUERY).expect("aggregate query");
4010 match result.result.records[0].get("n") {
4011 Some(reddb_types::Value::Integer(value)) => *value,
4012 other => panic!("expected integer aggregate, got {other:?}"),
4013 }
4014 }
4015
4016 #[test]
4017 fn hot_hit_bypasses_query_execution_pipeline() {
4018 let runtime = runtime_with_rows(true);
4019 reset_result_cache_miss_executions();
4020 let before = runtime.result_cache_metrics();
4021
4022 assert_eq!(aggregate_value(&runtime), 60);
4023 assert_eq!(result_cache_miss_executions(), 1);
4024 assert_eq!(aggregate_value(&runtime), 60);
4025
4026 let after = runtime.result_cache_metrics();
4027 assert_eq!(
4028 result_cache_miss_executions(),
4029 1,
4030 "hit re-entered the executor"
4031 );
4032 assert_eq!(after.0 - before.0, 1, "second read must be a cache hit");
4033 assert_eq!(after.1 - before.1, 1, "first read must populate the cache");
4034 }
4035
4036 #[test]
4037 fn disabled_cache_executes_every_read() {
4038 let runtime = runtime_with_rows(false);
4039 reset_result_cache_miss_executions();
4040 let before = runtime.result_cache_metrics();
4041
4042 assert_eq!(aggregate_value(&runtime), 60);
4043 assert_eq!(aggregate_value(&runtime), 60);
4044
4045 assert_eq!(result_cache_miss_executions(), 2);
4046 assert_eq!(
4047 runtime.result_cache_metrics(),
4048 before,
4049 "disabled cache must not report lookups"
4050 );
4051 }
4052
4053 #[test]
4054 fn table_write_forces_one_reexecution_then_hits_again() {
4055 let runtime = runtime_with_rows(true);
4056 assert_eq!(aggregate_value(&runtime), 60);
4057 assert_eq!(aggregate_value(&runtime), 60);
4058 runtime
4059 .execute_query("INSERT INTO result_cache_probe (id, n) VALUES (4, 40)")
4060 .expect("mutate cached table");
4061 reset_result_cache_miss_executions();
4062 let before = runtime.result_cache_metrics();
4063
4064 assert_eq!(aggregate_value(&runtime), 100);
4065 assert_eq!(result_cache_miss_executions(), 1);
4066 assert_eq!(aggregate_value(&runtime), 100);
4067
4068 let after = runtime.result_cache_metrics();
4069 assert_eq!(result_cache_miss_executions(), 1);
4070 assert_eq!(after.0 - before.0, 1, "post-write result must become hot");
4071 assert_eq!(
4072 after.1 - before.1,
4073 1,
4074 "write must invalidate the old result"
4075 );
4076 }
4077}
4078
4079#[cfg(test)]
4080mod security_gate_tests {
4081 use super::*;
4082 use crate::auth::store::AuthStore;
4083 use crate::auth::{AuthConfig, Role};
4084
4085 struct IdentityGuard;
4088
4089 impl IdentityGuard {
4090 fn install(username: &str, role: Role) -> Self {
4091 set_current_auth_identity(username.to_string(), role);
4092 Self
4093 }
4094 }
4095
4096 impl Drop for IdentityGuard {
4097 fn drop(&mut self) {
4098 clear_current_auth_identity();
4099 }
4100 }
4101
4102 struct ScopedIdentityGuard;
4103
4104 impl ScopedIdentityGuard {
4105 fn install(tenant: &str, username: &str, role: Role) -> Self {
4106 crate::runtime::execution_context::set_current_auth_principal(
4107 crate::auth::UserId::scoped(tenant, username),
4108 role,
4109 );
4110 set_current_tenant(tenant.to_string());
4111 Self
4112 }
4113 }
4114
4115 impl Drop for ScopedIdentityGuard {
4116 fn drop(&mut self) {
4117 clear_current_auth_identity();
4118 clear_current_tenant();
4119 }
4120 }
4121
4122 fn runtime_with_user(username: &str, role: Role) -> RedDBRuntime {
4123 let store = std::sync::Arc::new(AuthStore::new(AuthConfig {
4124 enabled: true,
4125 session_ttl_secs: 60,
4126 require_auth: true,
4127 auto_encrypt_storage: false,
4128 vault_enabled: false,
4129 cert: Default::default(),
4130 oauth: Default::default(),
4131 }));
4132 store.create_user(username, "pw", role).expect("user");
4133 let runtime =
4134 RedDBRuntime::with_options(crate::api::RedDBOptions::in_memory()).expect("runtime");
4135 runtime.set_auth_store(store);
4136 runtime
4137 }
4138
4139 fn runtime_with_store(username: &str, role: Role) -> (RedDBRuntime, std::sync::Arc<AuthStore>) {
4140 let store = std::sync::Arc::new(AuthStore::new(AuthConfig {
4141 enabled: true,
4142 session_ttl_secs: 60,
4143 require_auth: true,
4144 auto_encrypt_storage: false,
4145 vault_enabled: false,
4146 cert: Default::default(),
4147 oauth: Default::default(),
4148 }));
4149 store.create_user(username, "pw", role).expect("user");
4150 let runtime =
4151 RedDBRuntime::with_options(crate::api::RedDBOptions::in_memory()).expect("runtime");
4152 runtime.set_auth_store(std::sync::Arc::clone(&store));
4153 (runtime, store)
4154 }
4155
4156 #[test]
4157 fn tenant_credential_cannot_override_credential_scope() {
4158 use crate::runtime::within_clause::{FieldOverride, ScopeOverride};
4159
4160 let store = std::sync::Arc::new(AuthStore::new(AuthConfig {
4161 enabled: true,
4162 require_auth: true,
4163 ..AuthConfig::default()
4164 }));
4165 store
4166 .create_user_in_tenant(Some("acme"), "admin", "pw", Role::Admin)
4167 .expect("tenant admin");
4168 let runtime =
4169 RedDBRuntime::with_options(crate::api::RedDBOptions::in_memory()).expect("runtime");
4170 runtime.set_auth_store(store);
4171 let _identity = ScopedIdentityGuard::install("acme", "admin", Role::Admin);
4172
4173 runtime
4174 .execute_query("WITHIN TENANT 'acme' SELECT 1")
4175 .expect("own tenant remains valid");
4176 for sql in [
4177 "WITHIN TENANT 'globex' SELECT 1",
4178 "WITHIN TENANT NULL SELECT 1",
4179 "WITHIN TENANT 'acme' USER 'other' SELECT 1",
4180 "WITHIN TENANT 'acme' AS ROLE 'read' SELECT 1",
4181 ] {
4182 assert!(runtime.execute_query(sql).is_err(), "must reject {sql}");
4183 }
4184 assert!(runtime
4185 .execute_query_with_scope(
4186 "SELECT 1",
4187 ScopeOverride {
4188 tenant: FieldOverride::Set("globex".to_string()),
4189 ..ScopeOverride::default()
4190 },
4191 )
4192 .is_err());
4193
4194 set_current_connection_id(44_201);
4195 runtime.execute_query("BEGIN").expect("begin");
4196 assert!(runtime.execute_query("SET LOCAL TENANT 'globex'").is_err());
4197 runtime.execute_query("ROLLBACK").expect("rollback");
4198 clear_current_connection_id();
4199 }
4200
4201 #[test]
4202 fn tenant_admin_sql_user_lifecycle_is_tenant_bounded() {
4203 let store = std::sync::Arc::new(AuthStore::new(AuthConfig {
4204 enabled: true,
4205 require_auth: true,
4206 ..AuthConfig::default()
4207 }));
4208 store
4209 .create_user_in_tenant(Some("acme"), "admin", "pw", Role::Admin)
4210 .expect("tenant admin");
4211 let runtime =
4212 RedDBRuntime::with_options(crate::api::RedDBOptions::in_memory()).expect("runtime");
4213 runtime.set_auth_store(std::sync::Arc::clone(&store));
4214 let _identity = ScopedIdentityGuard::install("acme", "admin", Role::Admin);
4215
4216 runtime
4217 .execute_query("CREATE USER acme.alice PASSWORD 'pw' ROLE read")
4218 .expect("same-tenant create");
4219 assert!(runtime
4220 .execute_query("CREATE USER globex.alice PASSWORD 'pw' ROLE read")
4221 .is_err());
4222 assert!(runtime
4223 .execute_query("ALTER USER globex.alice PASSWORD 'owned'")
4224 .is_err());
4225 let cross_tenant_allow = crate::auth::policies::Policy::from_json_str(
4226 r#"{
4227 "id":"tenant-cross-user",
4228 "version":1,
4229 "statements":[{
4230 "effect":"allow",
4231 "actions":["user:create","user:password:change"],
4232 "resources":["user:tenant/globex/*"]
4233 }]
4234 }"#,
4235 )
4236 .expect("cross-tenant policy");
4237 store.put_policy(cross_tenant_allow).expect("put policy");
4238 store
4239 .attach_policy(
4240 crate::auth::store::PrincipalRef::User(crate::auth::UserId::scoped(
4241 "acme", "admin",
4242 )),
4243 "tenant-cross-user",
4244 )
4245 .expect("attach policy");
4246 assert!(runtime
4247 .execute_query("CREATE USER globex.bob PASSWORD 'pw' ROLE read")
4248 .is_err());
4249 assert!(store.get_user(Some("globex"), "alice").is_none());
4250 assert!(store.get_user(Some("globex"), "bob").is_none());
4251 }
4252
4253 #[test]
4254 fn slow_log_redacts_user_password_statements() {
4255 for sql in [
4256 "CREATE USER alice PASSWORD 'super-secret' ROLE admin",
4257 "CREATE USER acme.alice PASSWORD 'super-secret' ROLE read",
4258 "ALTER USER alice PASSWORD 'super-secret'",
4259 "AlTeR UsEr acme.alice PaSsWoRd 'super-secret'",
4260 "EXPLAIN CREATE USER alice PASSWORD 'super-secret' ROLE read",
4261 "EXPLAIN ALTER USER alice PASSWORD 'super-secret'",
4262 "ALTER USER alice PASSWORD 'unterminated",
4263 ] {
4264 let redacted = redact_sql_for_slow_log(sql);
4265 assert!(!redacted.contains("super-secret"), "leaked from {sql}");
4266 assert!(!redacted.contains("unterminated"), "leaked from {sql}");
4267 }
4268 assert_eq!(
4269 redact_sql_for_slow_log("SELECT password FROM accounts"),
4270 "SELECT password FROM accounts"
4271 );
4272 }
4273
4274 #[test]
4275 fn nested_reads_need_select_on_the_inner_table() {
4276 use crate::auth::privileges::{Action, GrantPrincipal, Resource};
4277 use crate::auth::UserId;
4278 let (runtime, store) = runtime_with_store("ro", Role::Read);
4279 runtime
4280 .execute_query("CREATE TABLE gate_open (a INTEGER)")
4281 .expect("create");
4282 runtime
4283 .execute_query("CREATE TABLE gate_closed (b INTEGER)")
4284 .expect("create");
4285 store
4288 .grant(
4289 &UserId::from_parts(None, "admin"),
4290 Role::Admin,
4291 GrantPrincipal::User(UserId::from_parts(None, "ro")),
4292 Resource::table_from_name("gate_open"),
4293 vec![Action::Select],
4294 false,
4295 None,
4296 )
4297 .expect("grant");
4298 let _identity = IdentityGuard::install("ro", Role::Read);
4299 runtime
4300 .execute_query("SELECT * FROM (SELECT a FROM gate_open) s")
4301 .expect("derived table over a granted table is fine");
4302 for sql in [
4305 "SELECT * FROM (SELECT b FROM gate_closed) s",
4306 "SELECT a FROM gate_open WHERE a IN (SELECT b FROM gate_closed)",
4307 ] {
4308 let err = runtime
4309 .execute_query(sql)
4310 .expect_err("nested read of an ungranted table must be refused");
4311 assert!(
4312 err.to_string().contains("permission denied"),
4313 "{sql}: unexpected error: {err}"
4314 );
4315 }
4316 }
4317
4318 #[test]
4319 fn policy_listings_are_self_or_policy_management() {
4320 let (runtime, store) = runtime_with_store("ro", Role::Read);
4321 store.create_user("other", "pw", Role::Read).expect("user");
4322 let _identity = IdentityGuard::install("ro", Role::Read);
4323 runtime
4324 .execute_query("SHOW EFFECTIVE PERMISSIONS FOR ro")
4325 .expect("a principal may inspect its own permissions");
4326 let err = runtime
4327 .execute_query("SHOW EFFECTIVE PERMISSIONS FOR other")
4328 .expect_err("inspecting another principal is policy management");
4329 assert!(
4330 err.to_string().contains("permission denied"),
4331 "unexpected error: {err}"
4332 );
4333 }
4334
4335 #[test]
4336 fn secret_columns_stay_masked_for_readers_without_secret_read() {
4337 use reddb_types::Value;
4338 let (runtime, store) = runtime_with_store("ro", Role::Read);
4339 store.create_user("root", "pw", Role::Admin).expect("admin");
4340 store.ensure_vault_secret_key();
4341 runtime
4342 .execute_query(
4343 "INSERT INTO creds_gate (name, token) VALUES ('stripe', SECRET('sk_live_abc'))",
4344 )
4345 .expect("insert");
4346
4347 let token_for = |username: &str, role: Role| {
4348 let _identity = IdentityGuard::install(username, role);
4349 let result = runtime
4350 .execute_query("SELECT name, token FROM creds_gate")
4351 .expect("select");
4352 result.result.records[0]
4353 .get("token")
4354 .cloned()
4355 .expect("token column")
4356 };
4357 assert!(
4360 matches!(token_for("ro", Role::Read), Value::Secret(_)),
4361 "a read-role principal must see the column masked"
4362 );
4363 assert!(matches!(token_for("root", Role::Admin), Value::Secret(_)));
4364 }
4365
4366 #[test]
4367 fn store_wide_operations_need_the_admin_tier() {
4368 let runtime = runtime_with_user("bob", Role::Write);
4369 let _identity = IdentityGuard::install("bob", Role::Write);
4370 for sql in ["VACUUM", "SCRUB"] {
4373 let err = runtime
4374 .execute_query(sql)
4375 .expect_err("store-wide operation must be refused for a non-admin");
4376 assert!(
4377 err.to_string().contains("permission denied"),
4378 "{sql}: unexpected error: {err}"
4379 );
4380 }
4381 }
4382
4383 #[test]
4384 fn explain_needs_the_privilege_of_the_statement_it_plans() {
4385 let runtime = runtime_with_user("ro", Role::Read);
4386 let _identity = IdentityGuard::install("ro", Role::Read);
4387 runtime
4388 .execute_query("EXPLAIN SELECT 1 AS one")
4389 .expect("planning a read is a read");
4390 let err = runtime
4393 .execute_query("EXPLAIN INSERT INTO gate_t (a) VALUES (1)")
4394 .expect_err("planning a write needs the write privilege");
4395 assert!(
4396 err.to_string().contains("permission denied"),
4397 "unexpected EXPLAIN error: {err}"
4398 );
4399 }
4400
4401 #[test]
4402 fn copy_from_a_server_path_is_admin_only() {
4403 let runtime = runtime_with_user("bob", Role::Write);
4404 let _identity = IdentityGuard::install("bob", Role::Write);
4405 let err = runtime
4409 .execute_query("COPY leak FROM '/etc/hostname'")
4410 .expect_err("COPY FROM must be refused for a non-admin");
4411 assert!(
4412 err.to_string().contains("permission denied"),
4413 "unexpected COPY error: {err}"
4414 );
4415 }
4416
4417 #[test]
4418 fn vcs_history_rewrites_are_admin_only() {
4419 let runtime = runtime_with_user("bob", Role::Write);
4420 let _identity = IdentityGuard::install("bob", Role::Write);
4421 let err = runtime
4424 .execute_query("RESET HARD TO 'deadbeef'")
4425 .expect_err("RESET must be refused for a non-admin");
4426 assert!(
4427 err.to_string().contains("permission denied"),
4428 "unexpected RESET error: {err}"
4429 );
4430 }
4431
4432 #[test]
4433 fn ai_provider_config_writes_are_admin_only() {
4434 let runtime = runtime_with_user("bob", Role::Write);
4435 let _identity = IdentityGuard::install("bob", Role::Write);
4436 let err = runtime
4439 .execute_query(
4440 "SET CONFIG red.config.ai.providers.openai.tokens.default.secret_ref = 'red.secret.aes_key'",
4441 )
4442 .expect_err("AI provider config must be refused for a non-admin");
4443 assert!(
4444 err.to_string().contains("permission denied"),
4445 "unexpected SET CONFIG error: {err}"
4446 );
4447 }
4448
4449 #[test]
4450 fn engine_internal_vault_keys_are_not_writable_through_set_secret() {
4451 let runtime = runtime_with_user("root", Role::Admin);
4452 let _identity = IdentityGuard::install("root", Role::Admin);
4453 for key in [
4457 "red.iam.policies",
4458 "red.iam.attachments.users",
4459 "red.secret.aes_key",
4460 ] {
4461 let err = runtime
4462 .execute_query(&format!("SET SECRET {key} = 'x'"))
4463 .expect_err("engine-internal vault key must be refused");
4464 assert!(
4465 err.to_string().contains("engine-internal"),
4466 "unexpected error for {key}: {err}"
4467 );
4468 }
4469 }
4470}