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