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
65pub use super::execution_context::{
66 capture_current_snapshot, clear_current_auth_identity, clear_current_connection_id,
67 clear_current_snapshot, clear_current_tenant, current_auth_identity_for_audit,
68 current_connection_id, current_tenant, entity_visible_under_current_snapshot,
69 entity_visible_with_context, set_current_auth_identity, set_current_connection_id,
70 set_current_snapshot, set_current_tenant, snapshot_bundle, with_snapshot_bundle,
71 SnapshotBundle, SnapshotContext,
72};
73pub(crate) use super::execution_context::{
74 config_read_permitted, current_auth_identity, current_config_value, current_kv_value,
75 current_role_projected, current_scope_override, current_secret_value,
76 current_snapshot_requires_index_fallback, current_user_projected, has_scope_override_active,
77 kv_read_permitted, parse_set_local_tenant, update_current_config_value,
78 update_current_kv_value, update_current_secret_value, xids_visible_under_current_snapshot,
79 ConfigSnapshotGuard, CurrentSnapshotGuard, KvStoreGuard, ScopeOverrideGuard, SecretStoreGuard,
80 TxLocalTenantGuard,
81};
82
83pub(super) fn has_with_prefix(sql: &str) -> bool {
88 let trimmed = sql.trim_start();
89 let head_end = trimmed
90 .find(|c: char| c.is_whitespace() || c == '(')
91 .unwrap_or(trimmed.len());
92 trimmed[..head_end].eq_ignore_ascii_case("WITH")
93}
94
95pub(super) fn peek_top_level_as_of_with_table(
100 sql: &str,
101) -> Option<(crate::application::vcs::AsOfSpec, Option<String>)> {
102 if !sql
103 .as_bytes()
104 .windows(5)
105 .any(|w| w.eq_ignore_ascii_case(b"as of"))
106 {
107 return None;
108 }
109 let parsed = reddb_rql::parser::parse(sql).ok()?;
110 let reddb_rql::ast::QueryExpr::Table(table) = parsed.query else {
111 return None;
112 };
113 let clause = table.as_of?;
114 let table_name = if table.table.is_empty() || table.table == "any" {
115 None
116 } else {
117 Some(table.table.clone())
118 };
119 let spec = match clause {
120 reddb_rql::ast::AsOfClause::Commit(h) => crate::application::vcs::AsOfSpec::Commit(h),
121 reddb_rql::ast::AsOfClause::Branch(b) => crate::application::vcs::AsOfSpec::Branch(b),
122 reddb_rql::ast::AsOfClause::Tag(t) => crate::application::vcs::AsOfSpec::Tag(t),
123 reddb_rql::ast::AsOfClause::TimestampMs(ts) => {
124 crate::application::vcs::AsOfSpec::TimestampMs(ts)
125 }
126 reddb_rql::ast::AsOfClause::Snapshot(x) => crate::application::vcs::AsOfSpec::Snapshot(x),
127 };
128 Some((spec, table_name))
129}
130
131pub(super) fn query_has_volatile_builtin(sql: &str) -> bool {
132 const VOLATILE_TOKENS: &[&str] = &[
136 "pg_advisory_lock",
137 "pg_try_advisory_lock",
138 "pg_advisory_unlock",
139 "random()",
140 "$config",
145 "$secret",
146 "$red.secret",
147 "$kv",
148 ];
153 let lowered = sql.to_ascii_lowercase();
154 VOLATILE_TOKENS.iter().any(|t| lowered.contains(t))
155}
156
157pub(super) fn query_is_ask_statement(sql: &str) -> bool {
158 let trimmed = sql.trim_start();
159 let head_end = trimmed
160 .find(|c: char| c.is_whitespace() || c == '(' || c == ';')
161 .unwrap_or(trimmed.len());
162 trimmed[..head_end].eq_ignore_ascii_case("ASK")
163}
164
165pub(super) fn intent_lock_modes_for(
175 expr: &QueryExpr,
176) -> Option<(
177 crate::runtime::lock_manager::LockMode,
178 crate::runtime::lock_manager::LockMode,
179)> {
180 use crate::runtime::lock_manager::LockMode::{Exclusive, IntentExclusive, IntentShared};
181
182 match expr {
183 QueryExpr::Table(_)
185 | QueryExpr::Join(_)
186 | QueryExpr::Vector(_)
187 | QueryExpr::Hybrid(_)
188 | QueryExpr::Graph(_)
189 | QueryExpr::Path(_)
190 | QueryExpr::Ask(_)
191 | QueryExpr::SearchCommand(_)
192 | QueryExpr::GraphCommand(_)
193 | QueryExpr::RankOf(_)
194 | QueryExpr::ApproxRankOf(_)
195 | QueryExpr::RankRange(_)
196 | QueryExpr::QueueSelect(_) => Some((IntentShared, IntentShared)),
197
198 QueryExpr::Insert(_)
206 | QueryExpr::Update(_)
207 | QueryExpr::Delete(_)
208 | QueryExpr::QueueCommand(QueueCommand::Move { .. }) => {
209 Some((IntentExclusive, IntentExclusive))
210 }
211 QueryExpr::QueueCommand(_) => Some((IntentShared, IntentShared)),
212
213 QueryExpr::CreateTable(_)
217 | QueryExpr::CreateCollection(_)
218 | QueryExpr::CreateVector(_)
219 | QueryExpr::DropTable(_)
220 | QueryExpr::DropGraph(_)
221 | QueryExpr::DropVector(_)
222 | QueryExpr::DropDocument(_)
223 | QueryExpr::DropKv(_)
224 | QueryExpr::DropCollection(_)
225 | QueryExpr::Truncate(_)
226 | QueryExpr::AlterTable(_)
227 | QueryExpr::CreateIndex(_)
228 | QueryExpr::DropIndex(_)
229 | QueryExpr::CreateTimeSeries(_)
230 | QueryExpr::CreateMetric(_)
231 | QueryExpr::AlterMetric(_)
232 | QueryExpr::CreateSlo(_)
233 | QueryExpr::DropTimeSeries(_)
234 | QueryExpr::CreateQueue(_)
235 | QueryExpr::AlterQueue(_)
236 | QueryExpr::DropQueue(_)
237 | QueryExpr::CreateTree(_)
238 | QueryExpr::DropTree(_)
239 | QueryExpr::CreatePolicy(_)
240 | QueryExpr::DropPolicy(_)
241 | QueryExpr::CreateView(_)
242 | QueryExpr::DropView(_)
243 | QueryExpr::RefreshMaterializedView(_)
244 | QueryExpr::CreateSchema(_)
245 | QueryExpr::DropSchema(_)
246 | QueryExpr::CreateSequence(_)
247 | QueryExpr::DropSequence(_)
248 | QueryExpr::CreateServer(_)
249 | QueryExpr::DropServer(_)
250 | QueryExpr::CreateForeignTable(_)
251 | QueryExpr::DropForeignTable(_) => Some((IntentExclusive, Exclusive)),
252
253 _ => None,
259 }
260}
261
262pub(super) fn collections_referenced(expr: &QueryExpr) -> Vec<String> {
267 let mut out = Vec::new();
268 walk_collections(expr, &mut out);
269 out.sort();
270 out.dedup();
271 out
272}
273
274impl RedDBRuntime {
275 pub fn execute_query_with_scope(
289 &self,
290 query: &str,
291 scope: crate::runtime::within_clause::ScopeOverride,
292 ) -> RedDBResult<RuntimeQueryResult> {
293 if scope.is_empty() {
294 return self.execute_query(query);
295 }
296 let _scope_guard = ScopeOverrideGuard::install(scope);
297 self.execute_query(query)
298 }
299
300 pub fn execute_query(&self, query: &str) -> RedDBResult<RuntimeQueryResult> {
309 let started = std::time::Instant::now();
310 self.inner.node_load_telemetry.query_start();
311 let result = self.execute_query_inner(query);
312 self.finish_query_lifecycle(query, started, result)
313 }
314
315 pub fn execute_query_with_params(
323 &self,
324 query: &str,
325 params: &[Value],
326 ) -> RedDBResult<RuntimeQueryResult> {
327 if params.is_empty() {
328 return self.execute_query(query);
329 }
330 let started = std::time::Instant::now();
331 self.inner.node_load_telemetry.query_start();
332 let result = self.execute_query_with_params_inner(query, params);
333 self.finish_query_lifecycle(query, started, result)
334 }
335
336 pub fn execute_prepared_query(
347 &self,
348 query: &str,
349 expr: QueryExpr,
350 ) -> RedDBResult<RuntimeQueryResult> {
351 let started = std::time::Instant::now();
352 self.inner.node_load_telemetry.query_start();
353 let result = self.execute_bound_query_expr_in_frame(query, expr);
354 self.finish_query_lifecycle(query, started, result)
355 }
356
357 fn finish_query_lifecycle(
358 &self,
359 query: &str,
360 started: std::time::Instant,
361 mut result: RedDBResult<RuntimeQueryResult>,
362 ) -> RedDBResult<RuntimeQueryResult> {
363 if let Ok(ref mut query_result) = result {
368 if query_result.statement_type == "select" {
369 self.filter_integrity_tombstoned(&mut query_result.result);
370 }
371 }
372 result = result.and_then(|value| {
373 super::query_budget::check_result(self.resource_limits(), &value)?;
374 Ok(value)
375 });
376 let elapsed_ms = started.elapsed().as_millis() as u64;
377
378 let scope = self.ai_scope();
383 let kind = match result
384 .as_ref()
385 .map(|r| r.statement_type)
386 .unwrap_or("select")
387 {
388 "select" => crate::telemetry::slow_query_logger::QueryKind::Select,
389 "insert" => crate::telemetry::slow_query_logger::QueryKind::Insert,
390 "update" => crate::telemetry::slow_query_logger::QueryKind::Update,
391 "delete" => crate::telemetry::slow_query_logger::QueryKind::Delete,
392 _ => crate::telemetry::slow_query_logger::QueryKind::Internal,
393 };
394 let lowered = query.to_ascii_lowercase();
395 let logged_query = if lowered.contains("secret") || lowered.contains("vault") {
396 "secret statement (SQL redacted)".to_string()
397 } else {
398 query.to_string()
399 };
400 self.inner
401 .slow_query_logger
402 .record(kind, elapsed_ms, logged_query, &scope);
403
404 self.inner
410 .query_latency_telemetry
411 .observe(kind, started.elapsed().as_secs_f64());
412
413 self.inner.node_load_telemetry.query_finish();
417
418 if let Ok(ref mut query_result) = result {
419 if matches!(query_result.statement_type, "insert" | "update" | "delete") {
420 let bookmark = crate::replication::CausalBookmark::new(
421 self.current_replication_term(),
422 self.cdc_current_lsn(),
423 );
424 query_result.bookmark = Some(bookmark.encode());
425 }
426 }
427
428 result
429 }
430
431 fn execute_query_with_params_inner(
432 &self,
433 query: &str,
434 params: &[Value],
435 ) -> RedDBResult<RuntimeQueryResult> {
436 let parsed = self.parameterized_query_ast(query)?;
437 let bound = crate::storage::query::user_params::bind(&parsed, params).map_err(|err| {
438 RedDBError::Validation {
439 message: err.to_string(),
440 validation: crate::json!({
441 "code": "INVALID_PARAMS",
442 "surface": "query.params",
443 }),
444 }
445 })?;
446 self.execute_bound_query_expr_in_frame(query, bound)
447 }
448
449 fn parameterized_query_ast(&self, query: &str) -> RedDBResult<QueryExpr> {
452 use crate::storage::query::planner::{CachedPlan, QueryPlan};
453
454 let key = format!("\0parameters\0{query}");
456 if let Some(parsed) = self
457 .inner
458 .query_cache
459 .read()
460 .peek(&key)
461 .filter(|entry| entry.matches_exact_query(query))
462 .map(|entry| entry.plan.original.clone())
463 {
464 return Ok(parsed);
465 }
466 let parsed = parse_multi(query).map_err(|err| RedDBError::Query(err.to_string()))?;
467 let plan = QueryPlan::new(parsed.clone(), parsed.clone(), Default::default());
468 self.inner
469 .query_cache
470 .write()
471 .insert(key, CachedPlan::new(plan).with_exact_query(query));
472 Ok(parsed)
473 }
474
475 fn execute_bound_query_expr_in_frame(
476 &self,
477 query: &str,
478 expr: QueryExpr,
479 ) -> RedDBResult<RuntimeQueryResult> {
480 let rewritten_query = super::red_schema::rewrite_virtual_names(query);
481 let execution_query = rewritten_query.as_deref().unwrap_or(query);
482 let frame = super::statement_frame::StatementExecutionFrame::build(
483 self,
484 super::statement_frame::StatementIdentity::Text(execution_query),
485 )?;
486 let _frame_guards = Box::new(frame.install(self));
490 let _log_span = crate::telemetry::span::query_span(query).entered();
491
492 let expr = self.rewrite_view_refs(expr);
493 let mode = detect_mode(execution_query);
494 let control_event_specs = query_control_event_specs(&expr);
495 let _lock_guard = match frame.prepare_dispatch(self, &expr) {
496 Ok(guard) => guard,
497 Err(err) => {
498 let outcome = control_event_outcome_for_error(&err);
499 for spec in &control_event_specs {
500 self.emit_control_event(
501 spec.kind,
502 outcome,
503 spec.action,
504 spec.resource.clone(),
505 Some(err.to_string()),
506 spec.fields.clone(),
507 )?;
508 }
509 return Err(err);
510 }
511 };
512
513 self.dispatch_expr(expr, query, mode)
514 }
515
516 pub fn causal_session(&self) -> crate::runtime::CausalSession {
517 crate::runtime::CausalSession {
518 runtime: self.clone(),
519 bookmark: None,
520 wait_timeout: std::time::Duration::from_secs(5),
521 }
522 }
523
524 pub fn wait_for_bookmark(
525 &self,
526 bookmark: &crate::replication::CausalBookmark,
527 timeout: std::time::Duration,
528 ) -> RedDBResult<()> {
529 let deadline = std::time::Instant::now() + timeout;
530 loop {
531 let applied_lsn = self.local_contiguous_applied_lsn();
532 if applied_lsn >= bookmark.commit_lsn() {
533 return Ok(());
534 }
535 let now = std::time::Instant::now();
536 if now >= deadline {
537 return Err(RedDBError::InvalidOperation(format!(
538 "timed out waiting for causal bookmark lsn {}; applied={}",
539 bookmark.commit_lsn(),
540 applied_lsn
541 )));
542 }
543 let remaining = deadline.saturating_duration_since(now);
544 std::thread::sleep(remaining.min(std::time::Duration::from_millis(5)));
545 }
546 }
547
548 fn local_contiguous_applied_lsn(&self) -> u64 {
549 match self.inner.db.options().replication.role {
550 crate::replication::ReplicationRole::Replica { .. } => {
551 self.config_u64("red.replication.last_applied_lsn", 0)
552 }
553 _ => self.cdc_current_lsn(),
554 }
555 }
556
557 #[inline(never)]
558 pub(super) fn execute_query_inner(&self, query: &str) -> RedDBResult<RuntimeQueryResult> {
559 if !has_scope_override_active()
570 && !query.trim_start().starts_with("WITHIN")
571 && !query.trim_start().starts_with("within")
572 && !self.inner.query_audit.has_rules()
573 && !self
574 .inner
575 .transaction_state
576 .in_transaction(current_connection_id())
577 {
578 if let Some(result) = self.try_fast_entity_lookup(query) {
579 return result;
580 }
581 }
582
583 match crate::runtime::within_clause::try_strip_within_prefix(query) {
590 Ok(Some((scope, inner))) => {
591 let _scope_guard = ScopeOverrideGuard::install(scope);
592 return self.execute_query_inner(inner);
597 }
598 Ok(None) => {}
599 Err(msg) => return Err(RedDBError::Query(msg)),
600 }
601
602 if let Some(inner) = strip_explain_analyze_prefix(query) {
606 return self.explain_analyze_as_rows(query, inner);
607 }
608
609 if let Some(inner) = strip_explain_prefix(query) {
616 return self.explain_as_rows(query, inner);
617 }
618
619 if let Some(value) = parse_set_local_tenant(query)? {
624 let conn_id = current_connection_id();
625 if !self.inner.transaction_state.in_transaction(conn_id) {
626 return Err(RedDBError::Query(
627 "SET LOCAL TENANT requires an active transaction".to_string(),
628 ));
629 }
630 self.inner
631 .transaction_state
632 .set_local_tenant(conn_id, value.clone());
633 return Ok(RuntimeQueryResult::ok_message(
634 query.to_string(),
635 &match &value {
636 Some(id) => format!("local tenant set: {id}"),
637 None => "local tenant cleared".to_string(),
638 },
639 "set_local_tenant",
640 ));
641 }
642
643 if super::red_schema::is_system_schema_write(query) {
644 return Err(RedDBError::Query(
645 super::red_schema::READ_ONLY_ERROR.to_string(),
646 ));
647 }
648
649 if let Some(command) = parse_runtime_vcs_command(query) {
650 let command = command?;
651 self.check_vcs_command_privilege(&command)?;
652 return self.execute_vcs_command(query, detect_mode(query), command);
653 }
654
655 if let Some(create_source) = super::analytics_source_catalog::parse_create_statement(query)?
656 {
657 return self.execute_create_analytics_source(query, create_source);
658 }
659
660 if let Some(path) = super::metric_descriptor_catalog::parse_read_metric_statement(query) {
666 return Err(super::metric_descriptor_catalog::read_output_unsupported(
667 &path,
668 ));
669 }
670
671 if let Some(parsed) = super::ranking_descriptor_catalog::parse_create_ranking(query) {
676 return self.execute_create_ranking(query, parsed?);
677 }
678 if super::ranking_descriptor_catalog::parse_show_rankings(query) {
679 return self.execute_show_rankings(query);
680 }
681
682 let rewritten_query = super::red_schema::rewrite_virtual_names(query);
683 let execution_query = rewritten_query.as_deref().unwrap_or(query);
684
685 let frame = super::statement_frame::StatementExecutionFrame::build(
686 self,
687 super::statement_frame::StatementIdentity::Text(execution_query),
688 )?;
689 let _frame_guards = Box::new(frame.install(self));
690
691 let _log_span = crate::telemetry::span::query_span(query).entered();
698
699 if let Some(rewritten) = frame.prepare_cte(execution_query)? {
701 return self.execute_query_expr(rewritten);
702 }
703
704 if !self.inner.query_audit.has_rules() {
706 if let Some(result) = self.try_fast_entity_lookup(execution_query) {
707 return result;
708 }
709 }
710
711 if !self.inner.query_audit.has_rules() {
713 if let Some(result) = frame.read_result_cache(self) {
714 return Ok(result);
715 }
716 }
717
718 let prepared = frame.prepare_statement(self, execution_query)?;
719 let mode = prepared.mode;
720 let expr = prepared.expr;
721
722 let _query_budget = if super::query_budget::is_materialized_read(&expr) {
723 Some(self.query_budget_scope()?)
724 } else {
725 None
726 };
727
728 let statement = query_expr_name(&expr);
729 let result_cache_scopes = query_expr_result_cache_scopes(&expr);
730 let control_event_specs = query_control_event_specs(&expr);
731 let query_audit_plan = query_audit_plan(&expr);
732
733 let _lock_guard = match frame.prepare_dispatch(self, &expr) {
734 Ok(guard) => guard,
735 Err(err) => {
736 let outcome = control_event_outcome_for_error(&err);
737 for spec in &control_event_specs {
738 self.emit_control_event(
739 spec.kind,
740 outcome,
741 spec.action,
742 spec.resource.clone(),
743 Some(err.to_string()),
744 spec.fields.clone(),
745 )?;
746 }
747 return Err(err);
748 }
749 };
750 let frame_iface: &dyn super::statement_frame::ReadFrame = &frame;
751 let query_audit_started = std::time::Instant::now();
752
753 let query_result = match expr {
754 QueryExpr::Function(ref command) => self.execute_function(query, command),
755 QueryExpr::Graph(_) | QueryExpr::Path(_) => {
756 self.execute_materialized_graph(query, mode, statement, &expr)
757 }
758 QueryExpr::Table(table) => {
759 let table = self.resolve_table_expr_subqueries(
760 table,
761 &frame as &dyn super::statement_frame::ReadFrame,
762 )?;
763 if let Some(TableSource::Function {
767 name,
768 args,
769 named_args,
770 }) = table.source.clone()
771 {
772 let tvf_result = RuntimeQueryResult {
780 query: query.to_string(),
781 mode,
782 statement,
783 engine: "runtime-graph-tvf",
784 result: self.execute_table_function(&name, &args, &named_args)?,
785 affected_rows: 0,
786 statement_type: "select",
787 bookmark: None,
788 notice: None,
789 };
790 frame.write_result_cache(self, &tvf_result, result_cache_scopes.clone());
791 return Ok(tvf_result);
792 }
793 if let Some(TableSource::InlineGraphFunction {
801 name,
802 nodes,
803 edges,
804 named_args,
805 }) = table.source.clone()
806 {
807 let inline_result = RuntimeQueryResult {
808 query: query.to_string(),
809 mode,
810 statement,
811 engine: "runtime-graph-tvf-inline",
812 result: self.execute_inline_graph_function(
813 &name,
814 &nodes,
815 &edges,
816 &named_args,
817 )?,
818 affected_rows: 0,
819 statement_type: "select",
820 bookmark: None,
821 notice: None,
822 };
823 frame.write_result_cache(self, &inline_result, result_cache_scopes);
824 return Ok(inline_result);
825 }
826 if super::red_schema::is_virtual_table(&table.table) {
827 return Ok(RuntimeQueryResult {
828 query: query.to_string(),
829 mode,
830 statement,
831 engine: "runtime-red-schema",
832 result: super::red_schema::red_query(
833 self,
834 &table.table,
835 &table,
836 &frame as &dyn super::statement_frame::ReadFrame,
837 )?,
838 affected_rows: 0,
839 statement_type: "select",
840 bookmark: None,
841 notice: None,
842 });
843 }
844
845 if let Some(view_result) = self.try_resolve_analytics_view(
849 &table,
850 &frame as &dyn super::statement_frame::ReadFrame,
851 )? {
852 return Ok(RuntimeQueryResult {
853 query: query.to_string(),
854 mode,
855 statement,
856 engine: "runtime-graph-analytics-view",
857 result: view_result,
858 affected_rows: 0,
859 statement_type: "select",
860 bookmark: None,
861 notice: None,
862 });
863 }
864
865 if let Some(result) = self.execute_probabilistic_select(&table)? {
866 return Ok(RuntimeQueryResult {
867 query: query.to_string(),
868 mode,
869 statement,
870 engine: "runtime-probabilistic",
871 result,
872 affected_rows: 0,
873 statement_type: "select",
874 bookmark: None,
875 notice: None,
876 });
877 }
878
879 if self.inner.foreign_tables.is_foreign_table(&table.table) {
887 let records = self
888 .inner
889 .foreign_tables
890 .scan(&table.table)
891 .map_err(|e| RedDBError::Internal(e.to_string()))?;
892 let result = apply_foreign_table_filters(records, &table);
893 return Ok(RuntimeQueryResult {
894 query: query.to_string(),
895 mode,
896 statement,
897 engine: "runtime-fdw",
898 result,
899 affected_rows: 0,
900 statement_type: "select",
901 bookmark: None,
902 notice: None,
903 });
904 }
905
906 let Some(table_with_rls) = self.authorize_relational_table_select(
923 table,
924 &frame as &dyn super::statement_frame::ReadFrame,
925 )?
926 else {
927 let empty = crate::storage::query::unified::UnifiedResult::empty();
928 return Ok(RuntimeQueryResult {
929 query: query.to_string(),
930 mode,
931 statement,
932 engine: "runtime-table-rls",
933 result: empty,
934 affected_rows: 0,
935 statement_type: "select",
936 bookmark: None,
937 notice: None,
938 });
939 };
940 Ok(RuntimeQueryResult {
941 query: query.to_string(),
942 mode,
943 statement,
944 engine: "runtime-table",
945 result: execute_runtime_table_query_in(
952 &self.inner.db,
953 &table_with_rls,
954 Some(&self.inner.index_store),
955 Some(frame.row_arena()),
956 )?,
957 affected_rows: 0,
958 statement_type: "select",
959 bookmark: None,
960 notice: None,
961 })
962 }
963 QueryExpr::Join(join) => {
964 let join_with_rls = match self.authorize_relational_join_select(
973 join,
974 &frame as &dyn super::statement_frame::ReadFrame,
975 )? {
976 Some(j) => j,
977 None => {
978 return Ok(RuntimeQueryResult {
979 query: query.to_string(),
980 mode,
981 statement,
982 engine: "runtime-join-rls",
983 result: crate::storage::query::unified::UnifiedResult::empty(),
984 affected_rows: 0,
985 statement_type: "select",
986 bookmark: None,
987 notice: None,
988 });
989 }
990 };
991 Ok(RuntimeQueryResult {
992 query: query.to_string(),
993 mode,
994 statement,
995 engine: "runtime-join",
996 result: execute_runtime_join_query(self, &join_with_rls)?,
997 affected_rows: 0,
998 statement_type: "select",
999 bookmark: None,
1000 notice: None,
1001 })
1002 }
1003 QueryExpr::Vector(vector) => Ok(RuntimeQueryResult {
1004 query: query.to_string(),
1005 mode,
1006 statement,
1007 engine: "runtime-vector",
1008 result: execute_runtime_vector_query(self, &vector)?,
1009 affected_rows: 0,
1010 statement_type: "select",
1011 bookmark: None,
1012 notice: None,
1013 }),
1014 QueryExpr::Hybrid(hybrid) => Ok(RuntimeQueryResult {
1015 query: query.to_string(),
1016 mode,
1017 statement,
1018 engine: "runtime-hybrid",
1019 result: execute_runtime_hybrid_query(self, &hybrid)?,
1020 affected_rows: 0,
1021 statement_type: "select",
1022 bookmark: None,
1023 notice: None,
1024 }),
1025 QueryExpr::RankOf(ref rank) => self.execute_rank_of(query, rank),
1026 QueryExpr::ApproxRankOf(ref rank) => self.execute_approx_rank_of(query, rank),
1027 QueryExpr::RankRange(ref range) => self.execute_rank_range(query, range),
1028 QueryExpr::Insert(ref insert) if super::red_schema::is_virtual_table(&insert.table) => {
1030 Err(RedDBError::Query(
1031 super::red_schema::READ_ONLY_ERROR.to_string(),
1032 ))
1033 }
1034 QueryExpr::Update(ref update) if super::red_schema::is_virtual_table(&update.table) => {
1035 Err(RedDBError::Query(
1036 super::red_schema::READ_ONLY_ERROR.to_string(),
1037 ))
1038 }
1039 QueryExpr::Delete(ref delete) if super::red_schema::is_virtual_table(&delete.table) => {
1040 Err(RedDBError::Query(
1041 super::red_schema::READ_ONLY_ERROR.to_string(),
1042 ))
1043 }
1044 QueryExpr::Insert(ref insert) => self
1045 .with_deferred_store_wal_for_dml(self.insert_may_emit_events(insert), || {
1046 self.execute_insert(query, insert)
1047 }),
1048 QueryExpr::Update(ref update) => self
1049 .with_deferred_store_wal_for_dml(self.update_may_emit_events(update), || {
1050 self.execute_update(query, update)
1051 }),
1052 QueryExpr::Delete(ref delete) => self
1053 .with_deferred_store_wal_for_dml(self.delete_may_emit_events(delete), || {
1054 self.execute_delete(query, delete)
1055 }),
1056 QueryExpr::CreateTable(ref create) => self.execute_create_table(query, create),
1058 QueryExpr::CreateCollection(ref create) => {
1059 self.execute_create_collection(query, create)
1060 }
1061 QueryExpr::CreateVector(ref create) => self.execute_create_vector(query, create),
1062 QueryExpr::DropTable(ref drop_tbl) => self.execute_drop_table(query, drop_tbl),
1063 QueryExpr::DropGraph(ref drop_graph) => self.execute_drop_graph(query, drop_graph),
1064 QueryExpr::DropVector(ref drop_vector) => self.execute_drop_vector(query, drop_vector),
1065 QueryExpr::DropDocument(ref drop_document) => {
1066 self.execute_drop_document(query, drop_document)
1067 }
1068 QueryExpr::DropKv(ref drop_kv) => self.execute_drop_kv(query, drop_kv),
1069 QueryExpr::DropCollection(ref drop_collection) => {
1070 self.execute_drop_collection(query, drop_collection)
1071 }
1072 QueryExpr::Truncate(ref truncate) => self.execute_truncate(query, truncate),
1073 QueryExpr::AlterTable(ref alter) => self.execute_alter_table(query, alter),
1074 QueryExpr::CreateVcsRef(ref create) => self.execute_create_vcs_ref(query, create),
1075 QueryExpr::DropVcsRef(ref drop_ref) => self.execute_drop_vcs_ref(query, drop_ref),
1076 QueryExpr::ForkStore(ref fork) => self.execute_fork_store(query, fork),
1077 QueryExpr::PromoteFork(ref promote_fork) => {
1078 self.execute_promote_fork(query, promote_fork)
1079 }
1080 QueryExpr::DropFork(ref drop_fork) => self.execute_drop_fork(query, drop_fork),
1081 QueryExpr::ExplainAlter(ref explain) => self.execute_explain_alter(query, explain),
1082 QueryExpr::GraphCommand(ref cmd) => self.execute_graph_command(query, cmd),
1084 QueryExpr::SearchCommand(ref cmd) => self.execute_search_command(query, cmd),
1086 QueryExpr::Ask(ref ask) => self.execute_ask(query, ask),
1088 QueryExpr::CreateIndex(ref create_idx) => self.execute_create_index(query, create_idx),
1089 QueryExpr::DropIndex(ref drop_idx) => self.execute_drop_index(query, drop_idx),
1090 QueryExpr::ProbabilisticCommand(ref cmd) => {
1091 self.execute_probabilistic_command(query, cmd)
1092 }
1093 QueryExpr::CreateTimeSeries(ref ts) => self.execute_create_timeseries(query, ts),
1095 QueryExpr::CreateMetric(ref metric) => self.execute_create_metric(query, metric),
1096 QueryExpr::AlterMetric(ref alter) => self.execute_alter_metric(query, alter),
1097 QueryExpr::CreateSlo(ref slo) => self.execute_create_slo(query, slo),
1098 QueryExpr::DropTimeSeries(ref ts) => self.execute_drop_timeseries(query, ts),
1099 QueryExpr::CreateQueue(ref q) => self.execute_create_queue(query, q),
1101 QueryExpr::AlterQueue(ref q) => self.execute_alter_queue(query, q),
1102 QueryExpr::DropQueue(ref q) => self.execute_drop_queue(query, q),
1103 QueryExpr::QueueSelect(ref q) => self.execute_queue_select(query, q),
1104 QueryExpr::QueueCommand(ref cmd) => self
1105 .with_deferred_store_wal_if_transaction(|| self.execute_queue_command(query, cmd)),
1106 QueryExpr::EventsBackfill(ref backfill) => {
1107 self.execute_events_backfill(query, backfill)
1108 }
1109 QueryExpr::EventsBackfillStatus { ref collection } => Err(RedDBError::Query(format!(
1110 "EVENTS BACKFILL STATUS for '{collection}' is not implemented in this slice"
1111 ))),
1112 QueryExpr::KvCommand(ref cmd) => self.execute_kv_command(query, cmd),
1113 QueryExpr::ConfigCommand(ref cmd) => self.execute_config_command(query, cmd),
1114 QueryExpr::CreateTree(ref tree) => self.execute_create_tree(query, tree),
1115 QueryExpr::DropTree(ref tree) => self.execute_drop_tree(query, tree),
1116 QueryExpr::TreeCommand(ref cmd) => self.execute_tree_command(query, cmd),
1117 QueryExpr::SetConfig { ref key, ref value } => {
1119 if key.starts_with("red.secret.") {
1120 return Err(RedDBError::Query(
1121 "red.secret.* is reserved for vault secrets; use SET SECRET".to_string(),
1122 ));
1123 }
1124 if key.starts_with("red.secrets.") {
1125 return Err(RedDBError::Query(
1126 "red.secrets.* is reserved for vault secrets; use SET SECRET".to_string(),
1127 ));
1128 }
1129 crate::ai::validate_ai_config_key_on_write(key)?;
1133 match self.check_managed_config_write_for_set_config(key) {
1138 Err(err) => Err(err),
1139 Ok(()) => {
1140 self.check_config_write_privilege(key)?;
1141 let store = self.inner.db.store();
1142 let json_val = match value {
1143 Value::Text(s) => crate::serde_json::Value::String(s.to_string()),
1144 Value::Integer(n) => crate::serde_json::Value::Number(*n as f64),
1145 Value::Float(n) => crate::serde_json::Value::Number(*n),
1146 Value::Boolean(b) => crate::serde_json::Value::Bool(*b),
1147 _ => crate::serde_json::Value::String(value.to_string()),
1148 };
1149 store.set_config_tree(key, &json_val);
1150 update_current_config_value(key, value.clone());
1151 self.invalidate_result_cache();
1156 Ok(RuntimeQueryResult::ok_message(
1157 query.to_string(),
1158 &format!("config set: {key}"),
1159 "set",
1160 ))
1161 }
1162 }
1163 }
1164 QueryExpr::SetSecret { ref key, ref value } => {
1166 if key.starts_with("red.config.") {
1167 return Err(RedDBError::Query(
1168 "red.config.* is reserved for config; use SET CONFIG".to_string(),
1169 ));
1170 }
1171 reject_engine_internal_vault_key(key)?;
1172 if key.starts_with("red.secret.") {
1173 self.set_internal_provider_secret(
1174 key,
1175 if value.is_null() {
1176 None
1177 } else {
1178 Some(secret_sql_value_to_string(value)?)
1179 },
1180 )?;
1181 return Ok(RuntimeQueryResult::ok_message(
1182 query.to_string(),
1183 &format!("secret set: {key}"),
1184 "set_secret",
1185 ));
1186 }
1187 let value = if value.is_null() {
1188 None
1189 } else {
1190 Some(secret_sql_value_to_string(value)?)
1191 };
1192 self.set_user_secret(key, value.clone())?;
1193 update_current_secret_value(key, value);
1194 Ok(RuntimeQueryResult::ok_message(
1195 query.to_string(),
1196 &format!("secret set: {key}"),
1197 "set_secret",
1198 ))
1199 }
1200 QueryExpr::DeleteSecret { ref key } => {
1202 reject_engine_internal_vault_key(key)?;
1203 if key.starts_with("red.secret.") {
1204 self.set_internal_provider_secret(key, None)?;
1205 return Ok(RuntimeQueryResult::ok_message(
1206 query.to_string(),
1207 &format!("secret deleted: {key}"),
1208 "delete_secret",
1209 ));
1210 }
1211 self.set_user_secret(key, None)?;
1212 update_current_secret_value(key, None);
1213 Ok(RuntimeQueryResult::ok_message(
1214 query.to_string(),
1215 &format!("secret deleted: {key}"),
1216 "delete_secret",
1217 ))
1218 }
1219 QueryExpr::SetKv { ref key, ref value } => {
1221 let auth_store = self.inner.auth_store.read().clone().ok_or_else(|| {
1222 RedDBError::Query("SET KV requires an auth store".to_string())
1223 })?;
1224 self.check_kv_write_privilege(&auth_store, key)?;
1225 if matches!(value, Value::Null) {
1227 auth_store.plain_kv_delete(key);
1228 update_current_kv_value(key, None);
1229 self.invalidate_result_cache();
1230 return Ok(RuntimeQueryResult::ok_message(
1231 query.to_string(),
1232 &format!("kv deleted: {key}"),
1233 "delete_kv",
1234 ));
1235 }
1236 let value = secret_sql_value_to_string(value)?;
1237 auth_store.plain_kv_set(key.clone(), value.clone());
1238 update_current_kv_value(key, Some(value));
1239 self.invalidate_result_cache();
1240 Ok(RuntimeQueryResult::ok_message(
1241 query.to_string(),
1242 &format!("kv set: {key}"),
1243 "set_kv",
1244 ))
1245 }
1246 QueryExpr::DeleteKv { ref key } => {
1248 let auth_store = self.inner.auth_store.read().clone().ok_or_else(|| {
1249 RedDBError::Query("DELETE KV requires an auth store".to_string())
1250 })?;
1251 self.check_kv_write_privilege(&auth_store, key)?;
1252 let deleted = auth_store.plain_kv_delete(key);
1253 if deleted {
1254 update_current_kv_value(key, None);
1255 }
1256 self.invalidate_result_cache();
1257 Ok(RuntimeQueryResult::ok_message(
1258 query.to_string(),
1259 &format!("kv deleted: {key}"),
1260 if deleted {
1261 "delete_kv"
1262 } else {
1263 "delete_kv_not_found"
1264 },
1265 ))
1266 }
1267 QueryExpr::Scrub { background, budget } => {
1268 self.execute_scrub_query(query, mode, background, budget)
1269 }
1270 QueryExpr::ShowSecrets { ref prefix } => {
1272 let result = self.show_user_secrets(prefix.as_deref())?;
1273 Ok(RuntimeQueryResult {
1274 query: query.to_string(),
1275 mode,
1276 statement: "show_secrets",
1277 engine: "runtime-secret",
1278 result,
1279 affected_rows: 0,
1280 statement_type: "select",
1281 bookmark: None,
1282 notice: None,
1283 })
1284 }
1285 QueryExpr::ShowConfig {
1287 ref prefix,
1288 as_json,
1289 } => {
1290 self.check_config_read_privilege()?;
1291 let store = self.inner.db.store();
1292 let all_collections = store.list_collections();
1293 if !all_collections.contains(&"red_config".to_string()) {
1294 if as_json {
1295 return Ok(show_config_json_result(
1296 query,
1297 mode,
1298 prefix,
1299 crate::serde_json::Value::Object(crate::serde_json::Map::new()),
1300 ));
1301 }
1302 let result = UnifiedResult::with_columns(vec!["key".into(), "value".into()]);
1303 return Ok(RuntimeQueryResult {
1304 query: query.to_string(),
1305 mode,
1306 statement: "show_config",
1307 engine: "runtime-config",
1308 result,
1309 affected_rows: 0,
1310 statement_type: "select",
1311 bookmark: None,
1312 notice: None,
1313 });
1314 }
1315 let manager = store
1316 .get_collection("red_config")
1317 .ok_or_else(|| RedDBError::NotFound("red_config".to_string()))?;
1318 let entities = manager.query_all(|_| true);
1319 let mut latest = std::collections::BTreeMap::<String, (u64, Value, Value)>::new();
1320 for entity in entities {
1321 if let EntityData::Row(ref row) = entity.data {
1322 if let Some(ref named) = row.named {
1323 let key_val = named.get("key").cloned().unwrap_or(Value::Null);
1324 let val = named.get("value").cloned().unwrap_or(Value::Null);
1325 let key_str = match &key_val {
1326 Value::Text(s) => s.as_ref(),
1327 _ => continue,
1328 };
1329 if key_str == super::function_catalog::REGISTRY_KEY {
1330 continue;
1331 }
1332 if let Some(ref pfx) = prefix {
1333 if !key_str.starts_with(pfx.as_str()) {
1334 continue;
1335 }
1336 }
1337 let entity_id = entity.id.raw();
1338 match latest.get(key_str) {
1339 Some((prev_id, _, _)) if *prev_id > entity_id => {}
1340 _ => {
1341 latest.insert(key_str.to_string(), (entity_id, key_val, val));
1342 }
1343 }
1344 }
1345 }
1346 }
1347 if as_json {
1348 let mut tree = crate::serde_json::Value::Object(crate::serde_json::Map::new());
1349 for (key, (_, _, val)) in latest {
1350 let relative = match prefix {
1351 Some(pfx) if key == *pfx => "",
1352 Some(pfx) => key
1353 .strip_prefix(pfx.as_str())
1354 .and_then(|tail| tail.strip_prefix('.'))
1355 .unwrap_or(key.as_str()),
1356 None => key.as_str(),
1357 };
1358 insert_config_json_path(&mut tree, relative, val.to_json());
1359 }
1360 return Ok(show_config_json_result(query, mode, prefix, tree));
1361 }
1362 let mut result = UnifiedResult::with_columns(vec!["key".into(), "value".into()]);
1363 for (_, key_val, val) in latest.into_values() {
1364 let mut record = UnifiedRecord::new();
1365 record.set("key", key_val);
1366 record.set("value", val);
1367 result.push(record);
1368 }
1369 Ok(RuntimeQueryResult {
1370 query: query.to_string(),
1371 mode,
1372 statement: "show_config",
1373 engine: "runtime-config",
1374 result,
1375 affected_rows: 0,
1376 statement_type: "select",
1377 bookmark: None,
1378 notice: None,
1379 })
1380 }
1381 QueryExpr::SetTenant(ref value) => {
1387 self.check_set_tenant_privilege(value.as_deref())?;
1388 match value {
1389 Some(id) => set_current_tenant(id.clone()),
1390 None => clear_current_tenant(),
1391 }
1392 Ok(RuntimeQueryResult::ok_message(
1393 query.to_string(),
1394 &match value {
1395 Some(id) => format!("tenant set: {id}"),
1396 None => "tenant cleared".to_string(),
1397 },
1398 "set_tenant",
1399 ))
1400 }
1401 QueryExpr::ShowTenant => {
1402 let mut result = UnifiedResult::with_columns(vec!["tenant".into()]);
1403 let mut record = UnifiedRecord::new();
1404 record.set(
1405 "tenant",
1406 current_tenant().map(Value::text).unwrap_or(Value::Null),
1407 );
1408 result.push(record);
1409 Ok(RuntimeQueryResult {
1410 query: query.to_string(),
1411 mode,
1412 statement: "show_tenant",
1413 engine: "runtime-tenant",
1414 result,
1415 affected_rows: 0,
1416 statement_type: "select",
1417 bookmark: None,
1418 notice: None,
1419 })
1420 }
1421 QueryExpr::TransactionControl(ref ctl) => {
1433 use crate::storage::transaction::IsolationLevel;
1434 use reddb_rql::ast::TxnControl;
1435
1436 let conn_id = current_connection_id();
1441
1442 let (kind, msg) = match ctl {
1443 TxnControl::Begin(requested_isolation) => {
1444 if self.inner.transaction_state.in_transaction(conn_id) {
1445 return Err(RedDBError::Query("transaction already active".into()));
1446 }
1447 let isolation = requested_isolation
1448 .map(IsolationLevel::from)
1449 .unwrap_or(IsolationLevel::SnapshotIsolation);
1450 let xid = self.inner.transaction_state.begin(conn_id, isolation);
1451 ("begin", format!("BEGIN — xid={xid} (snapshot isolation)"))
1452 }
1453 TxnControl::Commit => {
1454 let has_vault_writes = self
1455 .inner
1456 .pending_vault_writes
1457 .read()
1458 .get(&conn_id)
1459 .is_some_and(|writes| !writes.is_empty());
1460 let vault_store = self.inner.db.store();
1461 let _vault_guard =
1462 has_vault_writes.then(|| vault_store.vault_publication_lock.lock());
1463 let context = self.inner.transaction_state.commit(conn_id, |ctx| {
1464 let mut own_xids = std::collections::HashSet::new();
1465 own_xids.insert(ctx.xid);
1466 for (_, sub) in &ctx.savepoints {
1467 own_xids.insert(*sub);
1468 }
1469 for sub in &ctx.released_sub_xids {
1470 own_xids.insert(*sub);
1471 }
1472 if let Err(err) = self.check_table_row_write_conflicts(
1473 conn_id,
1474 &ctx.snapshot,
1475 &own_xids,
1476 ctx.isolation,
1477 ) {
1478 let undo = self.revive_pending_versioned_updates(conn_id);
1479 self.revive_pending_tombstones(conn_id);
1480 self.discard_pending_kv_watch_events(conn_id);
1481 self.discard_pending_queue_wakes(conn_id);
1482 self.discard_pending_store_wal_actions(conn_id);
1483 self.release_pending_claim_locks(conn_id);
1484 undo?;
1485 return Err(err);
1486 }
1487 if let Err(err) = self.check_queue_dedup_write_conflicts(
1488 conn_id,
1489 &ctx.snapshot,
1490 &own_xids,
1491 ) {
1492 let undo = self.revive_pending_versioned_updates(conn_id);
1493 self.revive_pending_tombstones(conn_id);
1494 self.discard_pending_queue_dedup(conn_id);
1495 self.discard_pending_kv_watch_events(conn_id);
1496 self.discard_pending_queue_wakes(conn_id);
1497 self.discard_pending_store_wal_actions(conn_id);
1498 self.release_pending_claim_locks(conn_id);
1499 undo?;
1500 return Err(err);
1501 }
1502 if let Err(err) = self.prepare_pending_vault_writes(conn_id, ctx) {
1503 let undo = self.revive_pending_versioned_updates(conn_id);
1504 self.revive_pending_tombstones(conn_id);
1505 self.discard_pending_queue_dedup(conn_id);
1506 self.discard_pending_kv_watch_events(conn_id);
1507 self.discard_pending_queue_wakes(conn_id);
1508 self.discard_pending_store_wal_actions(conn_id);
1509 self.release_pending_claim_locks(conn_id);
1510 undo?;
1511 return Err(err);
1512 }
1513 self.restore_pending_write_stamps(conn_id);
1514 if let Err(err) = self.flush_pending_store_wal_actions(conn_id) {
1515 let undo = self.revive_pending_versioned_updates(conn_id);
1516 self.revive_pending_tombstones(conn_id);
1517 self.discard_pending_queue_dedup(conn_id);
1518 self.discard_pending_kv_watch_events(conn_id);
1519 self.discard_pending_queue_wakes(conn_id);
1520 self.release_pending_claim_locks(conn_id);
1521 undo?;
1522 return Err(err);
1523 }
1524 Ok(())
1525 });
1526 if context.is_err() {
1527 self.discard_pending_vault_writes(conn_id)?;
1528 }
1529 let context = context?;
1530 match context {
1531 Some(ctx) => {
1532 self.finalize_pending_vault_writes(conn_id);
1533 self.finalize_pending_versioned_updates(conn_id);
1534 self.finalize_pending_tombstones(conn_id);
1535 self.finalize_pending_queue_dedup(conn_id);
1536 self.finalize_pending_kv_watch_events(conn_id);
1537 self.finalize_pending_queue_wakes(conn_id);
1538 self.release_pending_claim_locks(conn_id);
1539 ("commit", format!("COMMIT — xid={} committed", ctx.xid))
1540 }
1541 None => (
1542 "commit",
1543 "COMMIT outside transaction — no-op (autocommit)".to_string(),
1544 ),
1545 }
1546 }
1547 TxnControl::Rollback => match self.rollback_connection(conn_id)? {
1548 Some(ctx) => ("rollback", format!("ROLLBACK — xid={} aborted", ctx.xid)),
1549 None => (
1550 "rollback",
1551 "ROLLBACK outside transaction — no-op (autocommit)".to_string(),
1552 ),
1553 },
1554 TxnControl::Savepoint(name) => {
1561 match self.inner.transaction_state.savepoint(conn_id, name) {
1562 Some(sub) => ("savepoint", format!("SAVEPOINT {name} — sub_xid={sub}")),
1563 None => (
1564 "savepoint",
1565 "SAVEPOINT outside transaction — no-op".to_string(),
1566 ),
1567 }
1568 }
1569 TxnControl::ReleaseSavepoint(name) => {
1570 match self
1571 .inner
1572 .transaction_state
1573 .release_savepoint(conn_id, name)?
1574 {
1575 Some(released) => (
1576 "release_savepoint",
1577 format!("RELEASE SAVEPOINT {name} — {released} level(s)"),
1578 ),
1579 None => (
1580 "release_savepoint",
1581 "RELEASE outside transaction — no-op".to_string(),
1582 ),
1583 }
1584 }
1585 TxnControl::RollbackToSavepoint(name) => {
1586 match self
1587 .inner
1588 .transaction_state
1589 .rollback_to_savepoint(conn_id, name)?
1590 {
1591 Some(rollback) => {
1592 self.rollback_vault_savepoint(conn_id, rollback.savepoint_xid);
1593 let reverted_updates = self.revive_versioned_updates_since(
1594 conn_id,
1595 rollback.savepoint_xid,
1596 );
1597 let revived =
1598 self.revive_tombstones_since(conn_id, rollback.savepoint_xid);
1599 let reverted_updates = reverted_updates?;
1600 (
1601 "rollback_to_savepoint",
1602 format!(
1603 "ROLLBACK TO SAVEPOINT {name} — aborted {} sub_xid(s), reverted {reverted_updates} update(s), revived {revived} tombstone(s)",
1604 rollback.aborted_xids.len(),
1605 ),
1606 )
1607 }
1608 None => (
1609 "rollback_to_savepoint",
1610 "ROLLBACK TO outside transaction — no-op".to_string(),
1611 ),
1612 }
1613 }
1614 };
1615 Ok(RuntimeQueryResult::ok_message(
1616 query.to_string(),
1617 &msg,
1618 kind,
1619 ))
1620 }
1621 QueryExpr::CreateSchema(ref q) => {
1634 let store = self.inner.db.store();
1635 let key = format!("schema.{}", q.name);
1636 if store.get_config(&key).is_some() {
1637 if q.if_not_exists {
1638 return Ok(RuntimeQueryResult::ok_message(
1639 query.to_string(),
1640 &format!("schema {} already exists — skipped", q.name),
1641 "create_schema",
1642 ));
1643 }
1644 return Err(RedDBError::Internal(format!(
1645 "schema {} already exists",
1646 q.name
1647 )));
1648 }
1649 store.set_config_tree(&key, &crate::serde_json::Value::Bool(true));
1650 Ok(RuntimeQueryResult::ok_message(
1651 query.to_string(),
1652 &format!("schema {} created", q.name),
1653 "create_schema",
1654 ))
1655 }
1656 QueryExpr::DropSchema(ref q) => {
1657 let store = self.inner.db.store();
1658 let key = format!("schema.{}", q.name);
1659 let existed = store.get_config(&key).is_some();
1660 if !existed && !q.if_exists {
1661 return Err(RedDBError::Internal(format!(
1662 "schema {} does not exist",
1663 q.name
1664 )));
1665 }
1666 store.set_config_tree(&key, &crate::serde_json::Value::Null);
1668 let suffix = if q.cascade {
1669 " (CASCADE accepted — tables untouched)"
1670 } else {
1671 ""
1672 };
1673 Ok(RuntimeQueryResult::ok_message(
1674 query.to_string(),
1675 &format!("schema {} dropped{}", q.name, suffix),
1676 "drop_schema",
1677 ))
1678 }
1679 QueryExpr::CreateSequence(ref q) => {
1680 let store = self.inner.db.store();
1681 let base = format!("sequence.{}", q.name);
1682 let start_key = format!("{base}.start");
1683 let incr_key = format!("{base}.increment");
1684 let curr_key = format!("{base}.current");
1685 if store.get_config(&start_key).is_some() {
1686 if q.if_not_exists {
1687 return Ok(RuntimeQueryResult::ok_message(
1688 query.to_string(),
1689 &format!("sequence {} already exists — skipped", q.name),
1690 "create_sequence",
1691 ));
1692 }
1693 return Err(RedDBError::Internal(format!(
1694 "sequence {} already exists",
1695 q.name
1696 )));
1697 }
1698 let initial_current = q.start - q.increment;
1701 store.set_config_tree(
1702 &start_key,
1703 &crate::serde_json::Value::Number(q.start as f64),
1704 );
1705 store.set_config_tree(
1706 &incr_key,
1707 &crate::serde_json::Value::Number(q.increment as f64),
1708 );
1709 store.set_config_tree(
1710 &curr_key,
1711 &crate::serde_json::Value::Number(initial_current as f64),
1712 );
1713 Ok(RuntimeQueryResult::ok_message(
1714 query.to_string(),
1715 &format!(
1716 "sequence {} created (start={}, increment={})",
1717 q.name, q.start, q.increment
1718 ),
1719 "create_sequence",
1720 ))
1721 }
1722 QueryExpr::DropSequence(ref q) => {
1723 let store = self.inner.db.store();
1724 let base = format!("sequence.{}", q.name);
1725 let existed = store.get_config(&format!("{base}.start")).is_some();
1726 if !existed && !q.if_exists {
1727 return Err(RedDBError::Internal(format!(
1728 "sequence {} does not exist",
1729 q.name
1730 )));
1731 }
1732 for k in ["start", "increment", "current"] {
1733 store.set_config_tree(&format!("{base}.{k}"), &crate::serde_json::Value::Null);
1734 }
1735 Ok(RuntimeQueryResult::ok_message(
1736 query.to_string(),
1737 &format!("sequence {} dropped", q.name),
1738 "drop_sequence",
1739 ))
1740 }
1741 QueryExpr::CreateView(ref q) => {
1751 let mut views = self.inner.views.write();
1752 if views.contains_key(&q.name) && !q.or_replace {
1753 if q.if_not_exists {
1754 return Ok(RuntimeQueryResult::ok_message(
1755 query.to_string(),
1756 &format!("view {} already exists — skipped", q.name),
1757 "create_view",
1758 ));
1759 }
1760 return Err(RedDBError::Internal(format!(
1761 "view {} already exists",
1762 q.name
1763 )));
1764 }
1765 views.insert(q.name.clone(), Arc::new(q.clone()));
1766 drop(views);
1767
1768 if q.materialized {
1770 use crate::storage::cache::result::{MaterializedViewDef, RefreshPolicy};
1771 let refresh = match q.refresh_every_ms {
1772 Some(ms) => RefreshPolicy::Periodic(std::time::Duration::from_millis(ms)),
1773 None => RefreshPolicy::Manual,
1774 };
1775 let dependencies = collect_table_refs(&q.query);
1776 let def = MaterializedViewDef {
1777 name: q.name.clone(),
1778 query: format!("<parsed view {}>", q.name),
1779 dependencies: dependencies.clone(),
1780 refresh,
1781 retention_duration_ms: q.retention_duration_ms,
1782 };
1783 self.inner.materialized_views.write().register(def);
1784
1785 let descriptor =
1791 crate::runtime::continuous_materialized_view::MaterializedViewDescriptor {
1792 name: q.name.clone(),
1793 source_sql: query.to_string(),
1794 source_collections: dependencies,
1795 refresh_every_ms: q.refresh_every_ms,
1796 retention_duration_ms: q.retention_duration_ms,
1797 };
1798 let store = self.inner.db.store();
1799 crate::runtime::continuous_materialized_view::persist_descriptor(
1800 store.as_ref(),
1801 &descriptor,
1802 )?;
1803
1804 self.ensure_materialized_view_backing(&q.name)?;
1811 }
1812 self.invalidate_plan_cache();
1817 self.invalidate_result_cache();
1818
1819 Ok(RuntimeQueryResult::ok_message(
1820 query.to_string(),
1821 &format!(
1822 "{}view {} created",
1823 if q.materialized { "materialized " } else { "" },
1824 q.name
1825 ),
1826 "create_view",
1827 ))
1828 }
1829 QueryExpr::DropView(ref q) => {
1830 let mut views = self.inner.views.write();
1831 let removed = views.remove(&q.name);
1832 let existed = removed.is_some();
1833 let removed_materialized =
1834 removed.as_ref().map(|v| v.materialized).unwrap_or(false);
1835 drop(views);
1836 if q.materialized || existed {
1837 self.inner.materialized_views.write().remove(&q.name);
1839 let store = self.inner.db.store();
1843 crate::runtime::continuous_materialized_view::remove_by_name(
1844 store.as_ref(),
1845 &q.name,
1846 )?;
1847 }
1848 if removed_materialized || q.materialized {
1852 self.drop_materialized_view_backing(&q.name)?;
1853 }
1854 self.invalidate_plan_cache();
1857 self.invalidate_result_cache();
1858 if !existed && !q.if_exists {
1859 return Err(RedDBError::Internal(format!(
1860 "view {} does not exist",
1861 q.name
1862 )));
1863 }
1864 self.invalidate_plan_cache();
1865 Ok(RuntimeQueryResult::ok_message(
1866 query.to_string(),
1867 &format!("view {} dropped", q.name),
1868 "drop_view",
1869 ))
1870 }
1871 QueryExpr::RefreshMaterializedView(ref q) => {
1872 let view = {
1875 let views = self.inner.views.read();
1876 views.get(&q.name).cloned()
1877 };
1878 let view = match view {
1879 Some(v) => v,
1880 None => {
1881 return Err(RedDBError::Internal(format!(
1882 "view {} does not exist",
1883 q.name
1884 )))
1885 }
1886 };
1887 if !view.materialized {
1888 return Err(RedDBError::Internal(format!(
1889 "view {} is not materialized — REFRESH requires \
1890 CREATE MATERIALIZED VIEW",
1891 q.name
1892 )));
1893 }
1894 let started = std::time::Instant::now();
1896 let now_ms = std::time::SystemTime::now()
1897 .duration_since(std::time::UNIX_EPOCH)
1898 .map(|d| d.as_millis() as u64)
1899 .unwrap_or(0);
1900 match self.execute_query_expr((*view.query).clone()) {
1901 Ok(inner_result) => {
1902 let entities =
1909 view_records_to_entities(&q.name, &inner_result.result.records);
1910 let row_count = entities.len() as u64;
1911 let store = self.inner.db.store();
1912 let serialized_records = match store.refresh_collection(&q.name, entities) {
1913 Ok(records) => records,
1914 Err(err) => {
1915 let duration_ms = started.elapsed().as_millis() as u64;
1916 let msg = err.to_string();
1917 self.inner
1918 .materialized_views
1919 .write()
1920 .record_refresh_failure(
1921 &q.name,
1922 msg.clone(),
1923 duration_ms,
1924 now_ms,
1925 );
1926 return Err(RedDBError::Internal(format!(
1927 "REFRESH MATERIALIZED VIEW {}: {msg}",
1928 q.name
1929 )));
1930 }
1931 };
1932
1933 if let Some(ref primary) = self.inner.db.replication {
1939 let lsn = self.inner.cdc.emit(
1940 crate::replication::cdc::ChangeOperation::Refresh,
1941 &q.name,
1942 0,
1943 "refresh",
1944 );
1945 self.invalidate_result_cache_for_table(&q.name);
1946 let timestamp = std::time::SystemTime::now()
1947 .duration_since(std::time::UNIX_EPOCH)
1948 .unwrap_or_default()
1949 .as_millis() as u64;
1950 let record = ChangeRecord::for_refresh(
1951 lsn,
1952 timestamp,
1953 q.name.clone(),
1954 serialized_records,
1955 )
1956 .with_term(self.current_replication_term());
1957 let encoded = record.encode();
1958 primary.append_logical_record(record.lsn, encoded);
1959 }
1960
1961 let duration_ms = started.elapsed().as_millis() as u64;
1962 let serialized = format!("{:?}", inner_result.result);
1963 self.inner
1964 .materialized_views
1965 .write()
1966 .record_refresh_success(
1967 &q.name,
1968 serialized.into_bytes(),
1969 row_count,
1970 duration_ms,
1971 now_ms,
1972 );
1973 self.invalidate_result_cache();
1978 Ok(RuntimeQueryResult::ok_message(
1979 query.to_string(),
1980 &format!("materialized view {} refreshed", q.name),
1981 "refresh_materialized_view",
1982 ))
1983 }
1984 Err(err) => {
1985 let duration_ms = started.elapsed().as_millis() as u64;
1986 let msg = err.to_string();
1987 self.inner
1988 .materialized_views
1989 .write()
1990 .record_refresh_failure(&q.name, msg.clone(), duration_ms, now_ms);
1991 Err(err)
1992 }
1993 }
1994 }
1995 QueryExpr::CreatePolicy(ref q) => {
2002 let key = (q.table.clone(), q.name.clone());
2003 self.inner
2004 .rls_policies
2005 .write()
2006 .insert(key, Arc::new(q.clone()));
2007 self.invalidate_plan_cache();
2008 self.invalidate_result_cache();
2009 self.schema_vocabulary_apply(
2013 crate::runtime::schema_vocabulary::DdlEvent::CreatePolicy {
2014 collection: q.table.clone(),
2015 policy: q.name.clone(),
2016 },
2017 );
2018 Ok(RuntimeQueryResult::ok_message(
2019 query.to_string(),
2020 &format!("policy {} on {} created", q.name, q.table),
2021 "create_policy",
2022 ))
2023 }
2024 QueryExpr::DropPolicy(ref q) => {
2025 let removed = self
2026 .inner
2027 .rls_policies
2028 .write()
2029 .remove(&(q.table.clone(), q.name.clone()))
2030 .is_some();
2031 if !removed && !q.if_exists {
2032 return Err(RedDBError::Internal(format!(
2033 "policy {} on {} does not exist",
2034 q.name, q.table
2035 )));
2036 }
2037 self.invalidate_plan_cache();
2038 self.invalidate_result_cache();
2039 self.schema_vocabulary_apply(
2042 crate::runtime::schema_vocabulary::DdlEvent::DropPolicy {
2043 collection: q.table.clone(),
2044 policy: q.name.clone(),
2045 },
2046 );
2047 Ok(RuntimeQueryResult::ok_message(
2048 query.to_string(),
2049 &format!("policy {} on {} dropped", q.name, q.table),
2050 "drop_policy",
2051 ))
2052 }
2053 QueryExpr::CreateServer(ref q) => {
2064 use crate::storage::fdw::FdwOptions;
2065 let registry = Arc::clone(&self.inner.foreign_tables);
2066 if registry.server(&q.name).is_some() {
2067 if q.if_not_exists {
2068 return Ok(RuntimeQueryResult::ok_message(
2069 query.to_string(),
2070 &format!("server {} already exists — skipped", q.name),
2071 "create_server",
2072 ));
2073 }
2074 return Err(RedDBError::Internal(format!(
2075 "server {} already exists",
2076 q.name
2077 )));
2078 }
2079 let mut opts = FdwOptions::new();
2080 for (k, v) in &q.options {
2081 opts.values.insert(k.clone(), v.clone());
2082 }
2083 registry
2084 .create_server(&q.name, &q.wrapper, opts)
2085 .map_err(|e| RedDBError::Internal(e.to_string()))?;
2086 Ok(RuntimeQueryResult::ok_message(
2087 query.to_string(),
2088 &format!("server {} created (wrapper {})", q.name, q.wrapper),
2089 "create_server",
2090 ))
2091 }
2092 QueryExpr::DropServer(ref q) => {
2093 let existed = self.inner.foreign_tables.drop_server(&q.name);
2094 if !existed && !q.if_exists {
2095 return Err(RedDBError::Internal(format!(
2096 "server {} does not exist",
2097 q.name
2098 )));
2099 }
2100 Ok(RuntimeQueryResult::ok_message(
2101 query.to_string(),
2102 &format!(
2103 "server {} dropped{}",
2104 q.name,
2105 if q.cascade { " (cascade)" } else { "" }
2106 ),
2107 "drop_server",
2108 ))
2109 }
2110 QueryExpr::CreateForeignTable(ref q) => {
2111 use crate::storage::fdw::{FdwOptions, ForeignColumn, ForeignTable};
2112 let registry = Arc::clone(&self.inner.foreign_tables);
2113 if registry.foreign_table(&q.name).is_some() {
2114 if q.if_not_exists {
2115 return Ok(RuntimeQueryResult::ok_message(
2116 query.to_string(),
2117 &format!("foreign table {} already exists — skipped", q.name),
2118 "create_foreign_table",
2119 ));
2120 }
2121 return Err(RedDBError::Internal(format!(
2122 "foreign table {} already exists",
2123 q.name
2124 )));
2125 }
2126 let mut opts = FdwOptions::new();
2127 for (k, v) in &q.options {
2128 opts.values.insert(k.clone(), v.clone());
2129 }
2130 let columns: Vec<ForeignColumn> = q
2131 .columns
2132 .iter()
2133 .map(|c| ForeignColumn {
2134 name: c.name.clone(),
2135 data_type: c.data_type.clone(),
2136 not_null: c.not_null,
2137 })
2138 .collect();
2139 registry
2140 .create_foreign_table(ForeignTable {
2141 name: q.name.clone(),
2142 server_name: q.server.clone(),
2143 columns,
2144 options: opts,
2145 })
2146 .map_err(|e| RedDBError::Internal(e.to_string()))?;
2147 self.invalidate_plan_cache();
2148 Ok(RuntimeQueryResult::ok_message(
2149 query.to_string(),
2150 &format!("foreign table {} created (server {})", q.name, q.server),
2151 "create_foreign_table",
2152 ))
2153 }
2154 QueryExpr::DropForeignTable(ref q) => {
2155 let existed = self.inner.foreign_tables.drop_foreign_table(&q.name);
2156 if !existed && !q.if_exists {
2157 return Err(RedDBError::Internal(format!(
2158 "foreign table {} does not exist",
2159 q.name
2160 )));
2161 }
2162 self.invalidate_plan_cache();
2163 Ok(RuntimeQueryResult::ok_message(
2164 query.to_string(),
2165 &format!("foreign table {} dropped", q.name),
2166 "drop_foreign_table",
2167 ))
2168 }
2169 QueryExpr::CopyFrom(ref q) => {
2175 use crate::storage::import::{CsvConfig, CsvImporter};
2176 self.check_copy_from_privilege(&q.table)?;
2177 confine_copy_from_path(&q.path)?;
2178 let store = self.inner.db.store();
2179 let cfg = CsvConfig {
2180 collection: q.table.clone(),
2181 has_header: q.has_header,
2182 delimiter: q.delimiter.map(|c| c as u8).unwrap_or(b','),
2183 ..CsvConfig::default()
2184 };
2185 let importer = CsvImporter::new(cfg);
2186 let stats = importer
2187 .import_file(&q.path, store.as_ref())
2188 .map_err(|e| RedDBError::Internal(format!("COPY failed: {e}")))?;
2189 self.note_table_write(&q.table);
2191 Ok(RuntimeQueryResult::ok_message(
2192 query.to_string(),
2193 &format!(
2194 "COPY imported {} rows into {} ({} errors skipped, {}ms)",
2195 stats.records_imported, q.table, stats.errors_skipped, stats.duration_ms
2196 ),
2197 "copy_from",
2198 ))
2199 }
2200 QueryExpr::MaintenanceCommand(ref cmd) => {
2216 use reddb_rql::ast::MaintenanceCommand as Mc;
2217 let store = self.inner.db.store();
2218 let (kind, msg) = match cmd {
2219 Mc::Analyze { target } => {
2220 let targets: Vec<String> = match target {
2221 Some(t) => vec![t.clone()],
2222 None => store.list_collections(),
2223 };
2224 for t in &targets {
2225 self.refresh_table_planner_stats(t);
2226 }
2227 (
2228 "analyze",
2229 format!("ANALYZE refreshed stats for {} table(s)", targets.len()),
2230 )
2231 }
2232 Mc::Vacuum { target, full } => {
2233 let targets: Vec<String> = match target {
2234 Some(t) => vec![t.clone()],
2235 None => store.list_collections(),
2236 };
2237 let cutoff_xid = self.mvcc_vacuum_cutoff_xid();
2238 let mut vacuum_stats =
2239 crate::storage::unified::store::MvccVacuumStats::default();
2240 for t in &targets {
2241 let stats = store.vacuum_mvcc_history(t, cutoff_xid).map_err(|e| {
2242 RedDBError::Internal(format!(
2243 "VACUUM MVCC history failed for {t}: {e}"
2244 ))
2245 })?;
2246 if stats.reclaimed_versions > 0 {
2247 self.rebuild_runtime_indexes_for_table(t)?;
2248 }
2249 vacuum_stats.add(&stats);
2250 }
2251 for t in &targets {
2256 self.refresh_table_planner_stats(t);
2257 }
2258 let persisted = if *full {
2262 match store.persist() {
2263 Ok(()) => true,
2264 Err(e) => {
2265 return Err(RedDBError::Internal(format!(
2266 "VACUUM FULL persist failed: {e:?}"
2267 )));
2268 }
2269 }
2270 } else {
2271 false
2272 };
2273 self.invalidate_result_cache();
2275 (
2276 "vacuum",
2277 format!(
2278 "VACUUM{} processed {} table(s): scanned_versions={}, retained_versions={}, reclaimed_versions={}, retained_history_versions={}, reclaimed_history_versions={}, retained_tombstones={}, reclaimed_tombstones={}{}",
2279 if *full { " FULL" } else { "" },
2280 targets.len(),
2281 vacuum_stats.scanned_versions,
2282 vacuum_stats.retained_versions,
2283 vacuum_stats.reclaimed_versions,
2284 vacuum_stats.retained_history_versions,
2285 vacuum_stats.reclaimed_history_versions,
2286 vacuum_stats.retained_tombstones,
2287 vacuum_stats.reclaimed_tombstones,
2288 if persisted {
2289 " (pages flushed to disk)"
2290 } else {
2291 ""
2292 }
2293 ),
2294 )
2295 }
2296 };
2297 Ok(RuntimeQueryResult::ok_message(
2298 query.to_string(),
2299 &msg,
2300 kind,
2301 ))
2302 }
2303 QueryExpr::Grant(ref g) => self.execute_grant_statement(query, g),
2310 QueryExpr::Revoke(ref r) => self.execute_revoke_statement(query, r),
2311 QueryExpr::AlterUser(ref a) => self.execute_alter_user_statement(query, a),
2312 QueryExpr::CreateUser(ref u) => self.execute_create_user_statement(query, u),
2313 QueryExpr::CreateIamPolicy { ref id, ref json } => {
2314 self.execute_create_iam_policy(query, id, json)
2315 }
2316 QueryExpr::DropIamPolicy { ref id } => self.execute_drop_iam_policy(query, id),
2317 QueryExpr::AttachPolicy {
2318 ref policy_id,
2319 ref principal,
2320 } => self.execute_attach_policy(query, policy_id, principal),
2321 QueryExpr::DetachPolicy {
2322 ref policy_id,
2323 ref principal,
2324 } => self.execute_detach_policy(query, policy_id, principal),
2325 QueryExpr::ShowPolicies { ref filter } => {
2326 self.execute_show_policies(query, filter.as_ref())
2327 }
2328 QueryExpr::ShowEffectivePermissions {
2329 ref user,
2330 ref resource,
2331 } => self.execute_show_effective_permissions(query, user, resource.as_ref()),
2332 QueryExpr::SimulatePolicy {
2333 ref user,
2334 ref action,
2335 ref resource,
2336 } => self.execute_simulate_policy(query, user, action, resource),
2337 QueryExpr::LintPolicy { ref source } => self.execute_lint_policy(query, source),
2338 QueryExpr::MigratePolicyMode {
2339 ref target,
2340 dry_run,
2341 } => self.execute_migrate_policy_mode(query, target, dry_run),
2342 QueryExpr::CreateMigration(ref q) => self.execute_create_migration(query, q),
2343 QueryExpr::ApplyMigration(ref q) => self.execute_apply_migration(query, q),
2344 QueryExpr::RollbackMigration(ref q) => self.execute_rollback_migration(query, q),
2345 QueryExpr::ExplainMigration(ref q) => self.execute_explain_migration(query, q),
2346 _ => Err(RedDBError::Query(
2347 "unsupported command in runtime dispatcher".to_string(),
2348 )),
2349 };
2350
2351 if _query_budget.is_some() {
2352 super::query_budget::charge(0)?;
2353 }
2354 if !control_event_specs.is_empty() {
2355 let (outcome, reason) = match &query_result {
2356 Ok(_) => (crate::runtime::control_events::Outcome::Allowed, None),
2357 Err(err) => (control_event_outcome_for_error(err), Some(err.to_string())),
2358 };
2359 for spec in &control_event_specs {
2360 self.emit_control_event(
2361 spec.kind,
2362 outcome,
2363 spec.action,
2364 spec.resource.clone(),
2365 reason.clone(),
2366 spec.fields.clone(),
2367 )?;
2368 }
2369 }
2370
2371 if let (Some(plan), Ok(result)) = (&query_audit_plan, &query_result) {
2372 self.emit_query_audit(
2373 query,
2374 plan,
2375 query_audit_started.elapsed().as_millis() as u64,
2376 result,
2377 );
2378 }
2379
2380 if let Ok(ref result) = query_result {
2387 frame.write_result_cache(self, result, result_cache_scopes);
2388 }
2389
2390 query_result
2391 }
2392
2393 pub fn execute_query_expr(&self, expr: QueryExpr) -> RedDBResult<RuntimeQueryResult> {
2413 let frame = super::statement_frame::StatementExecutionFrame::build(
2414 self,
2415 super::statement_frame::StatementIdentity::Expr(&expr),
2416 )?;
2417 let _frame_guards = Box::new(frame.install(self));
2418 let expr = self.rewrite_view_refs(expr);
2422
2423 self.validate_model_operations_before_auth(&expr)?;
2424 if let Err(err) = self.check_query_privilege(&expr) {
2428 return Err(RedDBError::Query(format!("permission denied: {err}")));
2429 }
2430
2431 frame.release_snapshot_for_maintenance(&expr);
2432 let statement = query_expr_name(&expr);
2433 let mode = detect_mode(statement);
2434 let query_str = statement;
2435
2436 self.dispatch_expr(expr, query_str, mode)
2437 }
2438
2439 pub(super) fn validate_model_operations_before_auth(
2440 &self,
2441 expr: &QueryExpr,
2442 ) -> RedDBResult<()> {
2443 self.validate_vault_table_access(expr)?;
2444 use crate::catalog::CollectionModel;
2445 use crate::runtime::ddl::polymorphic_resolver;
2446 use reddb_rql::ast::KvCommand;
2447
2448 let system_schema_target = match expr {
2449 QueryExpr::DropTable(q) => Some(q.name.as_str()),
2450 QueryExpr::DropGraph(q) => Some(q.name.as_str()),
2451 QueryExpr::DropVector(q) => Some(q.name.as_str()),
2452 QueryExpr::DropDocument(q) => Some(q.name.as_str()),
2453 QueryExpr::DropKv(q) => Some(q.name.as_str()),
2454 QueryExpr::DropCollection(q) => Some(q.name.as_str()),
2455 QueryExpr::Truncate(q) => Some(q.name.as_str()),
2456 _ => None,
2457 };
2458 if system_schema_target.is_some_and(crate::runtime::impl_ddl::is_system_schema_name) {
2459 return Err(RedDBError::Query("system schema is read-only".to_string()));
2460 }
2461
2462 let expected = match expr {
2463 QueryExpr::DropTable(q) => Some((q.name.as_str(), CollectionModel::Table)),
2464 QueryExpr::DropGraph(q) => Some((q.name.as_str(), CollectionModel::Graph)),
2465 QueryExpr::DropVector(q) => Some((q.name.as_str(), CollectionModel::Vector)),
2466 QueryExpr::DropDocument(q) => Some((q.name.as_str(), CollectionModel::Document)),
2467 QueryExpr::DropKv(q) => Some((q.name.as_str(), q.model)),
2468 QueryExpr::DropCollection(q) => q.model.map(|model| (q.name.as_str(), model)),
2469 QueryExpr::Truncate(q) => q.model.map(|model| (q.name.as_str(), model)),
2470 QueryExpr::KvCommand(cmd) => {
2471 let (collection, model) = match cmd {
2472 KvCommand::Put {
2473 collection, model, ..
2474 }
2475 | KvCommand::Get {
2476 collection, model, ..
2477 }
2478 | KvCommand::Incr {
2479 collection, model, ..
2480 }
2481 | KvCommand::Cas {
2482 collection, model, ..
2483 }
2484 | KvCommand::List {
2485 collection, model, ..
2486 }
2487 | KvCommand::Delete {
2488 collection, model, ..
2489 } => (collection.as_str(), *model),
2490 KvCommand::Rotate { collection, .. }
2491 | KvCommand::History { collection, .. }
2492 | KvCommand::Purge { collection, .. } => {
2493 (collection.as_str(), CollectionModel::Vault)
2494 }
2495 KvCommand::InvalidateTags { collection, .. } => {
2496 (collection.as_str(), CollectionModel::Kv)
2497 }
2498 KvCommand::Watch {
2499 collection, model, ..
2500 } => (collection.as_str(), *model),
2501 KvCommand::Reveal { collection, .. } => {
2502 (collection.as_str(), CollectionModel::Vault)
2503 }
2504 };
2505 Some((collection, model))
2506 }
2507 QueryExpr::ConfigCommand(cmd) => {
2508 self.validate_config_command_before_auth(cmd)?;
2509 None
2510 }
2511 _ => None,
2512 };
2513
2514 let Some((name, expected_model)) = expected else {
2515 return Ok(());
2516 };
2517 if let Some(contract) = self.inner.db.collection_contract_arc(name) {
2522 if self.inner.db.store().get_collection(name).is_some() {
2523 return polymorphic_resolver::ensure_model_match(
2524 expected_model,
2525 contract.declared_model,
2526 );
2527 }
2528 }
2529 let snapshot = self.inner.db.catalog_model_snapshot();
2530 let Some(actual_model) = snapshot
2531 .collections
2532 .iter()
2533 .find(|collection| collection.name == name)
2534 .map(|collection| collection.declared_model.unwrap_or(collection.model))
2535 else {
2536 return Ok(());
2537 };
2538 polymorphic_resolver::ensure_model_match(expected_model, actual_model)
2539 }
2540
2541 pub(super) fn rewrite_view_refs(&self, expr: QueryExpr) -> QueryExpr {
2546 if self.inner.views.read().is_empty() {
2548 return expr;
2549 }
2550 self.rewrite_view_refs_inner(expr)
2551 }
2552
2553 fn rewrite_view_refs_inner(&self, expr: QueryExpr) -> QueryExpr {
2554 use reddb_rql::ast::{Filter, TableSource};
2555 match expr {
2556 QueryExpr::Table(mut tq) => {
2557 if let Some(TableSource::Subquery(body)) = tq.source.take() {
2563 tq.source = Some(TableSource::Subquery(Box::new(
2564 self.rewrite_view_refs_inner(*body),
2565 )));
2566 return QueryExpr::Table(tq);
2567 }
2568
2569 let maybe_view = {
2573 let views = self.inner.views.read();
2574 views.get(&tq.table).cloned()
2575 };
2576 let Some(view) = maybe_view else {
2577 return QueryExpr::Table(tq);
2578 };
2579
2580 if view.materialized {
2586 return QueryExpr::Table(tq);
2587 }
2588
2589 let inner_expr = self.rewrite_view_refs_inner((*view.query).clone());
2593
2594 match inner_expr {
2602 QueryExpr::Table(mut inner_tq) => {
2603 if let Some(outer_filter) = tq.filter.take() {
2604 inner_tq.filter = Some(match inner_tq.filter.take() {
2605 Some(existing) => {
2606 Filter::And(Box::new(existing), Box::new(outer_filter))
2607 }
2608 None => outer_filter,
2609 });
2610 inner_tq.where_expr = inner_tq
2618 .filter
2619 .as_ref()
2620 .map(reddb_rql::sql_lowering::filter_to_expr);
2621 }
2622 if let Some(outer_limit) = tq.limit {
2623 inner_tq.limit = Some(match inner_tq.limit {
2624 Some(existing) => existing.min(outer_limit),
2625 None => outer_limit,
2626 });
2627 }
2628 if let Some(outer_offset) = tq.offset {
2629 inner_tq.offset = Some(match inner_tq.offset {
2630 Some(existing) => existing + outer_offset,
2631 None => outer_offset,
2632 });
2633 }
2634 QueryExpr::Table(inner_tq)
2635 }
2636 other => other,
2637 }
2638 }
2639 QueryExpr::Join(mut jq) => {
2640 jq.left = Box::new(self.rewrite_view_refs_inner(*jq.left));
2641 jq.right = Box::new(self.rewrite_view_refs_inner(*jq.right));
2642 QueryExpr::Join(jq)
2643 }
2644 other => other,
2647 }
2648 }
2649
2650 fn resolve_table_expr_subqueries(
2651 &self,
2652 mut table: TableQuery,
2653 frame: &dyn super::statement_frame::ReadFrame,
2654 ) -> RedDBResult<TableQuery> {
2655 match table.source.take() {
2662 Some(TableSource::Subquery(inner)) => {
2663 let inner = self.resolve_select_expr_subqueries(*inner, frame)?;
2664 table.source = Some(TableSource::Subquery(Box::new(inner)));
2665 }
2666 other => table.source = other,
2667 }
2668
2669 let outer_scopes = relation_scopes_for_query(&QueryExpr::Table(table.clone()));
2670 for item in &mut table.select_items {
2671 if let reddb_rql::ast::SelectItem::Expr { expr, .. } = item {
2672 *expr = self.resolve_expr_subqueries(expr.clone(), &outer_scopes, frame)?;
2673 }
2674 }
2675 if let Some(where_expr) = table.where_expr.take() {
2676 table.where_expr =
2677 Some(self.resolve_expr_subqueries(where_expr, &outer_scopes, frame)?);
2678 table.filter = None;
2679 }
2680 if let Some(having_expr) = table.having_expr.take() {
2681 table.having_expr =
2682 Some(self.resolve_expr_subqueries(having_expr, &outer_scopes, frame)?);
2683 table.having = None;
2684 }
2685 for expr in &mut table.group_by_exprs {
2686 *expr = self.resolve_expr_subqueries(expr.clone(), &outer_scopes, frame)?;
2687 }
2688 for clause in &mut table.order_by {
2689 if let Some(expr) = clause.expr.take() {
2690 clause.expr = Some(self.resolve_expr_subqueries(expr, &outer_scopes, frame)?);
2691 }
2692 }
2693 Ok(table)
2694 }
2695
2696 fn resolve_select_expr_subqueries(
2697 &self,
2698 expr: QueryExpr,
2699 frame: &dyn super::statement_frame::ReadFrame,
2700 ) -> RedDBResult<QueryExpr> {
2701 match expr {
2702 QueryExpr::Table(table) => self
2703 .resolve_table_expr_subqueries(table, frame)
2704 .map(QueryExpr::Table),
2705 QueryExpr::Join(mut join) => {
2706 join.left = Box::new(self.resolve_select_expr_subqueries(*join.left, frame)?);
2707 join.right = Box::new(self.resolve_select_expr_subqueries(*join.right, frame)?);
2708 Ok(QueryExpr::Join(join))
2709 }
2710 other => Ok(other),
2711 }
2712 }
2713
2714 fn resolve_expr_subqueries(
2715 &self,
2716 expr: reddb_rql::ast::Expr,
2717 outer_scopes: &[String],
2718 frame: &dyn super::statement_frame::ReadFrame,
2719 ) -> RedDBResult<reddb_rql::ast::Expr> {
2720 use reddb_rql::ast::Expr;
2721
2722 match expr {
2723 Expr::Subquery { query, span } => {
2724 let values = self.execute_expr_subquery_values(query, outer_scopes, frame)?;
2725 if values.len() > 1 {
2726 return Err(RedDBError::Query(
2727 "scalar subquery returned more than one row".to_string(),
2728 ));
2729 }
2730 Ok(Expr::Literal {
2731 value: values.into_iter().next().unwrap_or(Value::Null),
2732 span,
2733 })
2734 }
2735 Expr::BinaryOp { op, lhs, rhs, span } => Ok(Expr::BinaryOp {
2736 op,
2737 lhs: Box::new(self.resolve_expr_subqueries(*lhs, outer_scopes, frame)?),
2738 rhs: Box::new(self.resolve_expr_subqueries(*rhs, outer_scopes, frame)?),
2739 span,
2740 }),
2741 Expr::UnaryOp { op, operand, span } => Ok(Expr::UnaryOp {
2742 op,
2743 operand: Box::new(self.resolve_expr_subqueries(*operand, outer_scopes, frame)?),
2744 span,
2745 }),
2746 Expr::Cast {
2747 inner,
2748 target,
2749 span,
2750 } => Ok(Expr::Cast {
2751 inner: Box::new(self.resolve_expr_subqueries(*inner, outer_scopes, frame)?),
2752 target,
2753 span,
2754 }),
2755 Expr::FunctionCall { name, args, span } => {
2756 let args = args
2757 .into_iter()
2758 .map(|arg| self.resolve_expr_subqueries(arg, outer_scopes, frame))
2759 .collect::<RedDBResult<Vec<_>>>()?;
2760 Ok(Expr::FunctionCall { name, args, span })
2761 }
2762 Expr::WindowFunctionCall {
2763 name,
2764 args,
2765 mut window,
2766 span,
2767 } => {
2768 let args = args
2769 .into_iter()
2770 .map(|arg| self.resolve_expr_subqueries(arg, outer_scopes, frame))
2771 .collect::<RedDBResult<Vec<_>>>()?;
2772 for expr in &mut window.partition_by {
2773 *expr = self.resolve_expr_subqueries(expr.clone(), outer_scopes, frame)?;
2774 }
2775 for clause in &mut window.order_by {
2776 clause.expr =
2777 self.resolve_expr_subqueries(clause.expr.clone(), outer_scopes, frame)?;
2778 }
2779 Ok(Expr::WindowFunctionCall {
2780 name,
2781 args,
2782 window,
2783 span,
2784 })
2785 }
2786 Expr::Case {
2787 branches,
2788 else_,
2789 span,
2790 } => {
2791 let branches = branches
2792 .into_iter()
2793 .map(|(cond, value)| {
2794 Ok((
2795 self.resolve_expr_subqueries(cond, outer_scopes, frame)?,
2796 self.resolve_expr_subqueries(value, outer_scopes, frame)?,
2797 ))
2798 })
2799 .collect::<RedDBResult<Vec<_>>>()?;
2800 let else_ = else_
2801 .map(|expr| self.resolve_expr_subqueries(*expr, outer_scopes, frame))
2802 .transpose()?
2803 .map(Box::new);
2804 Ok(Expr::Case {
2805 branches,
2806 else_,
2807 span,
2808 })
2809 }
2810 Expr::IsNull {
2811 operand,
2812 negated,
2813 span,
2814 } => Ok(Expr::IsNull {
2815 operand: Box::new(self.resolve_expr_subqueries(*operand, outer_scopes, frame)?),
2816 negated,
2817 span,
2818 }),
2819 Expr::InList {
2820 target,
2821 values,
2822 negated,
2823 span,
2824 } => {
2825 let target =
2826 Box::new(self.resolve_expr_subqueries(*target, outer_scopes, frame)?);
2827 let mut resolved = Vec::new();
2828 for value in values {
2829 if let Expr::Subquery { query, .. } = value {
2830 resolved.extend(
2831 self.execute_expr_subquery_values(query, outer_scopes, frame)?
2832 .into_iter()
2833 .map(Expr::lit),
2834 );
2835 } else {
2836 resolved.push(self.resolve_expr_subqueries(value, outer_scopes, frame)?);
2837 }
2838 }
2839 Ok(Expr::InList {
2840 target,
2841 values: resolved,
2842 negated,
2843 span,
2844 })
2845 }
2846 Expr::Between {
2847 target,
2848 low,
2849 high,
2850 negated,
2851 span,
2852 } => Ok(Expr::Between {
2853 target: Box::new(self.resolve_expr_subqueries(*target, outer_scopes, frame)?),
2854 low: Box::new(self.resolve_expr_subqueries(*low, outer_scopes, frame)?),
2855 high: Box::new(self.resolve_expr_subqueries(*high, outer_scopes, frame)?),
2856 negated,
2857 span,
2858 }),
2859 other => Ok(other),
2860 }
2861 }
2862
2863 fn execute_expr_subquery_values(
2864 &self,
2865 subquery: reddb_rql::ast::ExprSubquery,
2866 outer_scopes: &[String],
2867 frame: &dyn super::statement_frame::ReadFrame,
2868 ) -> RedDBResult<Vec<Value>> {
2869 let query = *subquery.query;
2870 if query_references_outer_scope(&query, outer_scopes) {
2871 return Err(RedDBError::Query(
2872 "NOT_YET_SUPPORTED: correlated subqueries are not supported yet; track follow-up issue #470-correlated-subqueries".to_string(),
2873 ));
2874 }
2875 let query = self.rewrite_view_refs(query);
2876 let query = self.resolve_select_expr_subqueries(query, frame)?;
2877 let query = self.authorize_relational_select_expr(query, frame)?;
2878 let result = match query {
2879 QueryExpr::Table(table) => {
2880 execute_runtime_table_query(&self.inner.db, &table, Some(&self.inner.index_store))?
2881 }
2882 QueryExpr::Join(join) => execute_runtime_join_query(self, &join)?,
2883 other => {
2884 return Err(RedDBError::Query(format!(
2885 "expression subquery must be a SELECT query, got {}",
2886 query_expr_name(&other)
2887 )))
2888 }
2889 };
2890 first_column_values(result)
2891 }
2892
2893 fn execute_materialized_graph(
2894 &self,
2895 query: &str,
2896 mode: QueryMode,
2897 statement: &'static str,
2898 expr: &QueryExpr,
2899 ) -> RedDBResult<RuntimeQueryResult> {
2900 let (graph, node_properties, edge_properties) = self.materialize_graph_with_rls()?;
2908 let result =
2909 crate::storage::query::unified::UnifiedExecutor::execute_on_with_graph_properties_checked(
2910 &graph,
2911 expr,
2912 node_properties,
2913 edge_properties,
2914 super::function_budget::graph_work_check(),
2915 )
2916 .map_err(|err| RedDBError::Query(err.to_string()))?;
2917
2918 Ok(RuntimeQueryResult {
2919 query: query.to_string(),
2920 mode,
2921 statement,
2922 engine: "materialized-graph",
2923 result,
2924 affected_rows: 0,
2925 statement_type: "select",
2926 bookmark: None,
2927 notice: None,
2928 })
2929 }
2930
2931 fn dispatch_expr(
2932 &self,
2933 expr: QueryExpr,
2934 query_str: &str,
2935 mode: QueryMode,
2936 ) -> RedDBResult<RuntimeQueryResult> {
2937 let is_read = super::query_budget::is_materialized_read(&expr);
2940 let _budget = if is_read {
2941 Some(super::query_budget::Scope::enter(
2942 self.resource_limits(),
2943 &self.inner.query_budget_slots,
2944 )?)
2945 } else {
2946 None
2947 };
2948 let result = self.dispatch_expr_inner(expr, query_str, mode);
2949 if is_read {
2950 super::query_budget::charge(0)?;
2951 }
2952 let result = result?;
2953 super::query_budget::check_result(self.resource_limits(), &result)?;
2954 if is_read {
2955 super::query_budget::charge(0)?;
2956 }
2957 Ok(result)
2958 }
2959
2960 fn dispatch_expr_inner(
2961 &self,
2962 expr: QueryExpr,
2963 query_str: &str,
2964 mode: QueryMode,
2965 ) -> RedDBResult<RuntimeQueryResult> {
2966 let statement = query_expr_name(&expr);
2967 match expr {
2968 QueryExpr::Function(command) => self.execute_function(query_str, &command),
2969 QueryExpr::Graph(_) | QueryExpr::Path(_) => {
2970 self.execute_materialized_graph(query_str, mode, statement, &expr)
2971 }
2972 QueryExpr::Table(table) => {
2973 let scope = self.ai_scope();
2974 let table = self.resolve_table_expr_subqueries(
2975 table,
2976 &scope as &dyn super::statement_frame::ReadFrame,
2977 )?;
2978 if let Some(TableSource::Function {
2982 name,
2983 args,
2984 named_args,
2985 }) = table.source.clone()
2986 {
2987 return Ok(RuntimeQueryResult {
2988 query: query_str.to_string(),
2989 mode,
2990 statement,
2991 engine: "runtime-graph-tvf",
2992 result: self.execute_table_function(&name, &args, &named_args)?,
2993 affected_rows: 0,
2994 statement_type: "select",
2995 bookmark: None,
2996 notice: None,
2997 });
2998 }
2999 if let Some(TableSource::InlineGraphFunction {
3003 name,
3004 nodes,
3005 edges,
3006 named_args,
3007 }) = table.source.clone()
3008 {
3009 return Ok(RuntimeQueryResult {
3010 query: query_str.to_string(),
3011 mode,
3012 statement,
3013 engine: "runtime-graph-tvf-inline",
3014 result: self.execute_inline_graph_function(
3015 &name,
3016 &nodes,
3017 &edges,
3018 &named_args,
3019 )?,
3020 affected_rows: 0,
3021 statement_type: "select",
3022 bookmark: None,
3023 notice: None,
3024 });
3025 }
3026 if super::red_schema::is_virtual_table(&table.table) {
3027 return Ok(RuntimeQueryResult {
3028 query: query_str.to_string(),
3029 mode,
3030 statement,
3031 engine: "runtime-red-schema",
3032 result: super::red_schema::red_query(
3033 self,
3034 &table.table,
3035 &table,
3036 &scope as &dyn super::statement_frame::ReadFrame,
3037 )?,
3038 affected_rows: 0,
3039 statement_type: "select",
3040 bookmark: None,
3041 notice: None,
3042 });
3043 }
3044 if let Some(view_result) = self.try_resolve_analytics_view(
3046 &table,
3047 &scope as &dyn super::statement_frame::ReadFrame,
3048 )? {
3049 return Ok(RuntimeQueryResult {
3050 query: query_str.to_string(),
3051 mode,
3052 statement,
3053 engine: "runtime-graph-analytics-view",
3054 result: view_result,
3055 affected_rows: 0,
3056 statement_type: "select",
3057 bookmark: None,
3058 notice: None,
3059 });
3060 }
3061 let Some(table_with_rls) = self.authorize_relational_table_select(
3062 table,
3063 &scope as &dyn super::statement_frame::ReadFrame,
3064 )?
3065 else {
3066 return Ok(RuntimeQueryResult {
3067 query: query_str.to_string(),
3068 mode,
3069 statement,
3070 engine: "runtime-table-rls",
3071 result: crate::storage::query::unified::UnifiedResult::empty(),
3072 affected_rows: 0,
3073 statement_type: "select",
3074 bookmark: None,
3075 notice: None,
3076 });
3077 };
3078 Ok(RuntimeQueryResult {
3079 query: query_str.to_string(),
3080 mode,
3081 statement,
3082 engine: "runtime-table",
3083 result: execute_runtime_table_query(
3084 &self.inner.db,
3085 &table_with_rls,
3086 Some(&self.inner.index_store),
3087 )?,
3088 affected_rows: 0,
3089 statement_type: "select",
3090 bookmark: None,
3091 notice: None,
3092 })
3093 }
3094 QueryExpr::Join(join) => {
3095 let scope = self.ai_scope();
3096 let Some(join_with_rls) = self.authorize_relational_join_select(
3097 join,
3098 &scope as &dyn super::statement_frame::ReadFrame,
3099 )?
3100 else {
3101 return Ok(RuntimeQueryResult {
3102 query: query_str.to_string(),
3103 mode,
3104 statement,
3105 engine: "runtime-join-rls",
3106 result: crate::storage::query::unified::UnifiedResult::empty(),
3107 affected_rows: 0,
3108 statement_type: "select",
3109 bookmark: None,
3110 notice: None,
3111 });
3112 };
3113 Ok(RuntimeQueryResult {
3114 query: query_str.to_string(),
3115 mode,
3116 statement,
3117 engine: "runtime-join",
3118 result: execute_runtime_join_query(self, &join_with_rls)?,
3119 affected_rows: 0,
3120 statement_type: "select",
3121 bookmark: None,
3122 notice: None,
3123 })
3124 }
3125 QueryExpr::Vector(vector) => Ok(RuntimeQueryResult {
3126 query: query_str.to_string(),
3127 mode,
3128 statement,
3129 engine: "runtime-vector",
3130 result: execute_runtime_vector_query(self, &vector)?,
3131 affected_rows: 0,
3132 statement_type: "select",
3133 bookmark: None,
3134 notice: None,
3135 }),
3136 QueryExpr::Hybrid(hybrid) => Ok(RuntimeQueryResult {
3137 query: query_str.to_string(),
3138 mode,
3139 statement,
3140 engine: "runtime-hybrid",
3141 result: execute_runtime_hybrid_query(self, &hybrid)?,
3142 affected_rows: 0,
3143 statement_type: "select",
3144 bookmark: None,
3145 notice: None,
3146 }),
3147 QueryExpr::Insert(ref insert) if super::red_schema::is_virtual_table(&insert.table) => {
3148 Err(RedDBError::Query(
3149 super::red_schema::READ_ONLY_ERROR.to_string(),
3150 ))
3151 }
3152 QueryExpr::Update(ref update) if super::red_schema::is_virtual_table(&update.table) => {
3153 Err(RedDBError::Query(
3154 super::red_schema::READ_ONLY_ERROR.to_string(),
3155 ))
3156 }
3157 QueryExpr::Delete(ref delete) if super::red_schema::is_virtual_table(&delete.table) => {
3158 Err(RedDBError::Query(
3159 super::red_schema::READ_ONLY_ERROR.to_string(),
3160 ))
3161 }
3162 QueryExpr::Insert(ref insert) => self
3163 .with_deferred_store_wal_for_dml(self.insert_may_emit_events(insert), || {
3164 self.execute_insert(query_str, insert)
3165 }),
3166 QueryExpr::Update(ref update) => self
3167 .with_deferred_store_wal_for_dml(self.update_may_emit_events(update), || {
3168 self.execute_update(query_str, update)
3169 }),
3170 QueryExpr::Delete(ref delete) => self
3171 .with_deferred_store_wal_for_dml(self.delete_may_emit_events(delete), || {
3172 self.execute_delete(query_str, delete)
3173 }),
3174 QueryExpr::QueueCommand(ref cmd) => self.with_deferred_store_wal_if_transaction(|| {
3175 self.execute_queue_command(query_str, cmd)
3176 }),
3177 QueryExpr::KvCommand(ref cmd) => self.execute_kv_command(query_str, cmd),
3178 QueryExpr::SearchCommand(ref cmd) => self.execute_search_command(query_str, cmd),
3179 QueryExpr::Ask(ref ask) => self.execute_ask(query_str, ask),
3180 _ => Err(RedDBError::Query(format!(
3181 "prepared-statement execution does not support {statement} statements"
3182 ))),
3183 }
3184 }
3185
3186 fn execute_table_function(
3193 &self,
3194 name: &str,
3195 args: &[String],
3196 named_args: &[(String, f64)],
3197 ) -> RedDBResult<crate::storage::query::unified::UnifiedResult> {
3198 if name.eq_ignore_ascii_case("red.diff") {
3199 return self.execute_vcs_diff_tvf(args, named_args);
3200 }
3201 if !is_graph_tvf_name(name) {
3202 return Err(RedDBError::Query(format!("unknown table function: {name}")));
3203 }
3204 if args.len() != 1 {
3206 return Err(RedDBError::Query(format!(
3207 "table function '{name}' takes exactly 1 graph argument, got {}",
3208 args.len()
3209 )));
3210 }
3211
3212 let (nodes, edges) = self.materialize_whole_graph_abstract()?;
3217 self.dispatch_graph_algorithm(name, nodes, edges, named_args)
3218 }
3219
3220 pub(crate) fn revive_versioned_updates_since(
3221 &self,
3222 conn_id: u64,
3223 stamper_xid: u64,
3224 ) -> RedDBResult<usize> {
3225 let candidates = {
3228 let guard = self.inner.pending_versioned_updates.read();
3229 let Some(pending) = guard.get(&conn_id) else {
3230 return Ok(0);
3231 };
3232 pending
3233 .iter()
3234 .rev()
3235 .filter(|entry| entry.3 >= stamper_xid)
3236 .cloned()
3237 .collect::<Vec<_>>()
3238 };
3239 let mut reverted = 0usize;
3240 for candidate in candidates {
3241 let (collection, old_id, new_id, xid, previous_xmax) = &candidate;
3242 let topology_lock = self.index_store_ref().collection_topology_lock(collection);
3243 let topology_guard = topology_lock.read();
3244 let mut guard = self.inner.pending_versioned_updates.write();
3245 let Some(pending) = guard.get_mut(&conn_id) else {
3246 continue;
3247 };
3248 let Some(position) = pending.iter().rposition(|entry| entry == &candidate) else {
3251 continue;
3252 };
3253 self.revive_versioned_update(
3254 collection,
3255 *old_id,
3256 *new_id,
3257 *xid,
3258 *previous_xmax,
3259 topology_guard,
3260 )?;
3261 pending.remove(position);
3262 reverted += 1;
3263 if pending.is_empty() {
3264 guard.remove(&conn_id);
3265 }
3266 }
3267 Ok(reverted)
3268 }
3269
3270 fn check_explain_target_privilege(&self, inner_sql: &str) -> RedDBResult<()> {
3276 let parsed =
3277 reddb_rql::parser::parse(inner_sql).map_err(|e| RedDBError::Query(e.to_string()))?;
3278 self.check_query_privilege(&parsed.query)
3279 .map_err(|err| RedDBError::Query(format!("permission denied: {err}")))
3280 }
3281
3282 fn explain_as_rows(&self, raw_query: &str, inner_sql: &str) -> RedDBResult<RuntimeQueryResult> {
3290 self.check_explain_target_privilege(inner_sql)?;
3291 let explain = self.explain_query(inner_sql)?;
3292
3293 let mut columns = vec![
3294 "op".to_string(),
3295 "source".to_string(),
3296 "estimated_rows".to_string(),
3297 "estimated_cost".to_string(),
3298 "depth".to_string(),
3299 ];
3300
3301 let mut records: Vec<crate::storage::query::unified::UnifiedRecord> = Vec::new();
3302
3303 for name in &explain.cte_materializations {
3309 use std::sync::Arc;
3310 let mut rec = crate::storage::query::unified::UnifiedRecord::default();
3311 rec.set_arc(Arc::from("op"), Value::text("CteScan".to_string()));
3312 rec.set_arc(Arc::from("source"), Value::text(name.clone()));
3313 rec.set_arc(Arc::from("estimated_rows"), Value::Float(0.0));
3314 rec.set_arc(Arc::from("estimated_cost"), Value::Float(0.0));
3315 rec.set_arc(Arc::from("depth"), Value::Integer(0));
3316 records.push(rec);
3317 }
3318
3319 let vector_details = explain.statement == "vector";
3320 if vector_details {
3321 columns.extend(["access_path_reason".to_string(), "index_hint".to_string()]);
3322 }
3323 walk_plan_node(&explain.logical_plan.root, 0, &mut records, vector_details);
3324
3325 let result = crate::storage::query::unified::UnifiedResult {
3326 columns,
3327 records,
3328 stats: Default::default(),
3329 pre_serialized_json: None,
3330 };
3331
3332 Ok(RuntimeQueryResult {
3333 query: raw_query.to_string(),
3334 mode: explain.mode,
3335 statement: "explain",
3336 engine: "runtime-explain",
3337 result,
3338 affected_rows: 0,
3339 statement_type: "select",
3340 bookmark: None,
3341 notice: None,
3342 })
3343 }
3344
3345 fn explain_analyze_vector_as_rows(
3346 &self,
3347 raw_query: &str,
3348 expr: QueryExpr,
3349 ) -> RedDBResult<RuntimeQueryResult> {
3350 let QueryExpr::Vector(query) = &expr else {
3351 return Err(RedDBError::Internal(
3352 "expected vector analysis target".to_string(),
3353 ));
3354 };
3355 let source = query.collection.clone();
3356 let execution = self.execute_query_expr(expr)?;
3359 let stats = execution.result.stats;
3360 let vector = stats.vector.as_ref().ok_or_else(|| {
3361 RedDBError::Internal("vector executor did not provide measurements".to_string())
3362 })?;
3363 let fields = vec![
3364 ("op", Value::text(vector.access_path.clone())),
3365 ("source", Value::text(source)),
3366 ("index_used", Value::Boolean(vector.index_used)),
3367 ("mode_requested", Value::text(vector.mode_requested.clone())),
3368 ("mode_executed", Value::text(vector.mode_executed.clone())),
3369 (
3370 "fallback_reason",
3371 vector
3372 .fallback_reason
3373 .as_ref()
3374 .map_or(Value::Null, |reason| Value::text(reason.clone())),
3375 ),
3376 (
3377 "approximate_distance_evaluations",
3378 Value::UnsignedInteger(vector.approximate_distance_evaluations),
3379 ),
3380 (
3381 "peak_topk_entries",
3382 Value::UnsignedInteger(vector.peak_topk_entries),
3383 ),
3384 (
3385 "candidates_examined",
3386 Value::UnsignedInteger(vector.candidates_examined),
3387 ),
3388 (
3389 "metadata_rejected",
3390 Value::UnsignedInteger(vector.metadata_rejected),
3391 ),
3392 ("rls_rejected", Value::UnsignedInteger(vector.rls_rejected)),
3393 (
3394 "visibility_rejected",
3395 Value::UnsignedInteger(vector.visibility_rejected),
3396 ),
3397 (
3398 "exact_distance_evaluations",
3399 Value::UnsignedInteger(vector.exact_distance_evaluations),
3400 ),
3401 ("actual_rows", Value::UnsignedInteger(vector.rows_returned)),
3402 (
3403 "actual_ms",
3404 Value::Float(stats.exec_time_us as f64 / 1000.0),
3405 ),
3406 ("metrics_scope", Value::text("vector_pipeline")),
3407 (
3408 "operators",
3409 Value::Json(vector.operators_json().to_string().into_bytes()),
3410 ),
3411 ];
3412 let columns = fields.iter().map(|(name, _)| name.to_string()).collect();
3413 let mut record = crate::storage::query::unified::UnifiedRecord::default();
3414 for (name, value) in fields {
3415 record.set_owned(name.to_string(), value);
3416 }
3417 Ok(RuntimeQueryResult {
3418 query: raw_query.to_string(),
3419 mode: execution.mode,
3420 statement: "explain_analyze",
3421 engine: "runtime-explain-analyze",
3422 result: crate::storage::query::unified::UnifiedResult {
3423 columns,
3424 records: vec![record],
3425 stats,
3426 pre_serialized_json: None,
3427 },
3428 affected_rows: 0,
3429 statement_type: "select",
3430 bookmark: None,
3431 notice: None,
3432 })
3433 }
3434
3435 fn explain_analyze_as_rows(
3436 &self,
3437 raw_query: &str,
3438 inner_sql: &str,
3439 ) -> RedDBResult<RuntimeQueryResult> {
3440 if strip_keyword_ci(inner_sql.trim_start(), "VECTOR").is_some() {
3441 let expr = parse_multi(inner_sql).map_err(|err| RedDBError::Query(err.to_string()))?;
3442 if matches!(&expr, QueryExpr::Vector(_)) {
3443 return self.explain_analyze_vector_as_rows(raw_query, expr);
3444 }
3445 }
3446 if !starts_with_dml_keyword(inner_sql) {
3447 return Err(RedDBError::Query(
3448 "EXPLAIN ANALYZE currently supports INSERT, UPDATE, DELETE, and VECTOR SEARCH"
3449 .to_string(),
3450 ));
3451 }
3452
3453 self.check_explain_target_privilege(inner_sql)?;
3454 let explain = self.explain_query(inner_sql)?;
3455 let conn_id = current_connection_id();
3456 if self.inner.transaction_state.in_transaction(conn_id) {
3457 return Err(RedDBError::Query(
3458 "EXPLAIN ANALYZE requires no active transaction".to_string(),
3459 ));
3460 }
3461
3462 self.execute_query_inner("BEGIN")?;
3463 let started = std::time::Instant::now();
3464 let execution = self.execute_query_inner(inner_sql);
3465 let elapsed_ms = started.elapsed().as_secs_f64() * 1000.0;
3466 let rollback = self.execute_query_inner("ROLLBACK");
3467
3468 let affected_rows = match execution {
3469 Ok(result) => result.affected_rows,
3470 Err(err) => {
3471 rollback?;
3472 return Err(err);
3473 }
3474 };
3475 rollback?;
3476
3477 let columns = vec![
3478 "op".to_string(),
3479 "source".to_string(),
3480 "estimated_rows".to_string(),
3481 "estimated_cost".to_string(),
3482 "actual_rows".to_string(),
3483 "actual_ms".to_string(),
3484 "depth".to_string(),
3485 ];
3486 let mut records = Vec::new();
3487 walk_analyze_plan_node(
3488 &explain.logical_plan.root,
3489 0,
3490 affected_rows,
3491 elapsed_ms,
3492 &mut records,
3493 );
3494
3495 let result = crate::storage::query::unified::UnifiedResult {
3496 columns,
3497 records,
3498 stats: Default::default(),
3499 pre_serialized_json: None,
3500 };
3501
3502 Ok(RuntimeQueryResult {
3503 query: raw_query.to_string(),
3504 mode: explain.mode,
3505 statement: "explain_analyze",
3506 engine: "runtime-explain-analyze",
3507 result,
3508 affected_rows: 0,
3509 statement_type: "select",
3510 bookmark: None,
3511 notice: None,
3512 })
3513 }
3514}
3515
3516fn strip_explain_analyze_prefix(sql: &str) -> Option<&str> {
3517 let rest = strip_keyword_ci(sql.trim_start(), "EXPLAIN")?.trim_start();
3518 Some(strip_keyword_ci(rest, "ANALYZE")?.trim_start()).filter(|inner| !inner.is_empty())
3519}
3520
3521fn strip_keyword_ci<'a>(sql: &'a str, keyword: &str) -> Option<&'a str> {
3522 let head = sql.get(..keyword.len())?;
3525 let rest = &sql[keyword.len()..];
3526 if !head.eq_ignore_ascii_case(keyword) {
3527 return None;
3528 }
3529 if rest
3530 .chars()
3531 .next()
3532 .is_some_and(|ch| !ch.is_ascii_whitespace())
3533 {
3534 return None;
3535 }
3536 Some(rest)
3537}
3538
3539fn starts_with_dml_keyword(sql: &str) -> bool {
3540 let trimmed = sql.trim_start();
3541 let head_end = trimmed
3542 .find(|ch: char| ch.is_ascii_whitespace())
3543 .unwrap_or(trimmed.len());
3544 let head = &trimmed[..head_end];
3545 head.eq_ignore_ascii_case("INSERT")
3546 || head.eq_ignore_ascii_case("UPDATE")
3547 || head.eq_ignore_ascii_case("DELETE")
3548}
3549
3550fn walk_plan_node(
3551 node: &crate::storage::query::planner::CanonicalLogicalNode,
3552 depth: usize,
3553 out: &mut Vec<crate::storage::query::unified::UnifiedRecord>,
3554 vector_details: bool,
3555) {
3556 use std::sync::Arc;
3557 let mut rec = crate::storage::query::unified::UnifiedRecord::default();
3558 rec.set_arc(Arc::from("op"), Value::text(node.operator.clone()));
3559 rec.set_arc(
3560 Arc::from("source"),
3561 node.source.clone().map(Value::text).unwrap_or(Value::Null),
3562 );
3563 rec.set_arc(
3564 Arc::from("estimated_rows"),
3565 Value::Float(node.estimated_rows),
3566 );
3567 rec.set_arc(
3568 Arc::from("estimated_cost"),
3569 Value::Float(node.operator_cost),
3570 );
3571 rec.set_arc(Arc::from("depth"), Value::Integer(depth as i64));
3572 if vector_details {
3573 for name in ["access_path_reason", "index_hint"] {
3574 rec.set_owned(
3575 name.to_string(),
3576 node.details
3577 .get(name)
3578 .cloned()
3579 .map(Value::text)
3580 .unwrap_or(Value::Null),
3581 );
3582 }
3583 }
3584 out.push(rec);
3585 for child in &node.children {
3586 walk_plan_node(child, depth + 1, out, vector_details);
3587 }
3588}
3589
3590fn walk_analyze_plan_node(
3591 node: &crate::storage::query::planner::CanonicalLogicalNode,
3592 depth: usize,
3593 affected_rows: u64,
3594 elapsed_ms: f64,
3595 out: &mut Vec<crate::storage::query::unified::UnifiedRecord>,
3596) {
3597 use std::sync::Arc;
3598 let mut rec = crate::storage::query::unified::UnifiedRecord::default();
3599 rec.set_arc(Arc::from("op"), Value::text(node.operator.clone()));
3600 rec.set_arc(
3601 Arc::from("source"),
3602 node.source.clone().map(Value::text).unwrap_or(Value::Null),
3603 );
3604 rec.set_arc(
3605 Arc::from("estimated_rows"),
3606 Value::Float(node.estimated_rows),
3607 );
3608 rec.set_arc(
3609 Arc::from("estimated_cost"),
3610 Value::Float(node.operator_cost),
3611 );
3612 rec.set_arc(
3613 Arc::from("actual_rows"),
3614 Value::UnsignedInteger(if depth == 0 { affected_rows } else { 0 }),
3615 );
3616 rec.set_arc(
3617 Arc::from("actual_ms"),
3618 Value::Float(if depth == 0 { elapsed_ms } else { 0.0 }),
3619 );
3620 rec.set_arc(Arc::from("depth"), Value::Integer(depth as i64));
3621 out.push(rec);
3622
3623 for child in &node.children {
3624 walk_analyze_plan_node(child, depth + 1, 0, 0.0, out);
3625 }
3626}
3627
3628#[cfg(test)]
3629mod declared_model_validation_tests {
3630 use super::*;
3631 use crate::catalog::CollectionModel;
3632 use crate::storage::no_malloc_hot_paths::measure_allocations;
3633 use reddb_rql::ast::KvCommand;
3634
3635 fn get(collection: &str) -> QueryExpr {
3636 QueryExpr::KvCommand(KvCommand::Get {
3637 model: CollectionModel::Kv,
3638 collection: collection.into(),
3639 key: "item".into(),
3640 })
3641 }
3642
3643 #[test]
3644 fn declared_model_validation_keeps_mismatches_and_observes_contract_changes() {
3645 let runtime = RedDBRuntime::in_memory().expect("runtime");
3646 runtime
3647 .execute_query("KV PUT declared.item = 1")
3648 .expect("seed");
3649 runtime
3650 .validate_model_operations_before_auth(&get("declared"))
3651 .expect("KV contract");
3652 let db = runtime.db();
3653 for (model, name) in [
3654 (CollectionModel::Table, "table"),
3655 (CollectionModel::Mixed, "mixed"),
3656 ] {
3657 let mut contract = db.collection_contract("declared").expect("contract");
3658 contract.declared_model = model;
3659 db.save_collection_contract(contract)
3660 .expect("replace contract");
3661 let error = runtime
3662 .validate_model_operations_before_auth(&get("declared"))
3663 .expect_err("mismatch");
3664 assert_eq!(
3665 error.to_string(),
3666 format!("INVALID_OPERATION: model mismatch: expected kv, got {name}")
3667 );
3668 }
3669 }
3670
3671 #[test]
3672 fn declared_model_validation_preserves_legacy_inference_and_missing_collections() {
3673 let runtime = RedDBRuntime::in_memory().expect("runtime");
3674 let db = runtime.db();
3675 let store = db.store();
3676 store
3677 .create_collection("legacy")
3678 .expect("legacy collection");
3679 store
3680 .get_collection("legacy")
3681 .expect("manager")
3682 .insert(crate::storage::UnifiedEntity::table_row(
3683 crate::storage::EntityId::new(1),
3684 "legacy",
3685 1,
3686 Vec::new(),
3687 ))
3688 .expect("uncontracted table row");
3689 assert!(db.collection_contract("legacy").is_none());
3690 let error = runtime
3691 .validate_model_operations_before_auth(&get("legacy"))
3692 .expect_err("inferred table");
3693 assert!(error
3694 .to_string()
3695 .contains("model mismatch: expected kv, got table"));
3696 runtime
3697 .validate_model_operations_before_auth(&get("missing"))
3698 .expect("missing stays deferred to execution");
3699 }
3700
3701 #[test]
3702 fn declared_model_validation_does_not_treat_orphan_contracts_as_collections() {
3703 let runtime = RedDBRuntime::in_memory().expect("runtime");
3704 runtime
3705 .execute_query("KV PUT declared.item = 1")
3706 .expect("seed");
3707 let db = runtime.db();
3708 let mut contract = db.collection_contract("declared").expect("contract");
3709 contract.name = "orphan".into();
3710 contract.declared_model = CollectionModel::Table;
3711 db.save_collection_contract(contract)
3712 .expect("orphan contract");
3713 assert!(db.store().get_collection("orphan").is_none());
3714 runtime
3715 .validate_model_operations_before_auth(&get("orphan"))
3716 .expect("no physical collection to validate");
3717 }
3718
3719 #[test]
3720 fn declared_model_validation_allocates_nothing_with_unrelated_collections() {
3721 let runtime = RedDBRuntime::in_memory().expect("runtime");
3722 runtime
3723 .execute_query("KV PUT declared.item = 1")
3724 .expect("seed");
3725 let expr = get("declared");
3726 for unrelated in [0, 64] {
3727 let db = runtime.db();
3728 let store = db.store();
3729 for index in 0..unrelated {
3730 let name = format!("unrelated_{index}");
3731 store
3732 .create_collection(&name)
3733 .expect("unrelated collection");
3734 }
3735 runtime
3736 .validate_model_operations_before_auth(&expr)
3737 .expect("warm contract cache");
3738 let (result, count) =
3739 measure_allocations(|| runtime.validate_model_operations_before_auth(&expr));
3740 result.expect("declared KV");
3741 assert_eq!(
3742 count.allocs, 0,
3743 "validation must only borrow the target contract"
3744 );
3745 }
3746 }
3747}
3748
3749#[cfg(test)]
3750mod multibyte_sql_tests {
3751 use super::*;
3752
3753 #[test]
3754 fn explain_analyze_prefix_ignores_multibyte_text_at_the_keyword_boundary() {
3755 for pad in 0..16 {
3758 for wide in ["é", "日", "😀"] {
3759 let sql = format!("{}{wide}llo", "a".repeat(pad));
3760 assert_eq!(strip_explain_analyze_prefix(&sql), None, "{sql:?}");
3761 }
3762 }
3763 assert_eq!(
3764 strip_explain_analyze_prefix("explain analyze DELETE FROM t"),
3765 Some("DELETE FROM t")
3766 );
3767 }
3768
3769 #[test]
3774 fn multibyte_sql_never_panics_execute_query() {
3775 let rt = RedDBRuntime::in_memory().expect("in-memory runtime");
3776 let seeds = [
3777 "",
3778 "SELECT ",
3779 "SELECT '",
3780 "EXPLAIN ",
3781 "EXPLAIN ANALYZE ",
3782 "CHECKPOINT ",
3783 "CHECKOUT ",
3784 "RESET ",
3785 "MERGE ",
3786 "CHERRY PICK ",
3787 "REVERT ",
3788 "RESOLVE CONFLICT ",
3789 "INSERT INTO t VALUES ('",
3790 ];
3791 let mut panicked = Vec::new();
3792 for seed in seeds {
3793 for pad in 0..16 {
3794 for wide in ["é", "日", "😀"] {
3795 let sql = format!("{seed}{}{wide}llo", "a".repeat(pad));
3796 let outcome = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
3797 let _ = rt.execute_query(&sql);
3798 }));
3799 if outcome.is_err() {
3800 panicked.push(sql);
3801 }
3802 }
3803 }
3804 }
3805 assert!(
3806 panicked.is_empty(),
3807 "execute_query panicked on {} statement(s), first: {:?}",
3808 panicked.len(),
3809 panicked.first()
3810 );
3811 }
3812}
3813
3814#[cfg(test)]
3815mod inline_graph_tvf_tests {
3816 use super::*;
3817
3818 fn scopes_for(sql: &str) -> HashSet<String> {
3819 let expr = reddb_rql::parser::parse(sql).expect("parse").query;
3820 query_expr_result_cache_scopes(&expr)
3821 }
3822
3823 #[test]
3824 fn inline_tvf_cache_scopes_include_source_collections() {
3825 let scopes = scopes_for(
3829 "SELECT * FROM components(nodes => (SELECT id FROM hosts), edges => (SELECT src, dst FROM links))",
3830 );
3831 assert!(scopes.contains("hosts"), "nodes source scoped: {scopes:?}");
3832 assert!(scopes.contains("links"), "edges source scoped: {scopes:?}");
3833 }
3834
3835 #[test]
3836 fn graph_collection_tvf_cache_scope_is_graph_argument() {
3837 let scopes = scopes_for("SELECT * FROM components(g)");
3841 assert!(scopes.contains("g"), "collection form scoped: {scopes:?}");
3842 }
3843
3844 #[test]
3845 fn abstract_degree_centrality_counts_undirected_endpoints() {
3846 let nodes = vec!["a".to_string(), "b".to_string(), "c".to_string()];
3847 let edges = vec![
3848 ("a".to_string(), "b".to_string(), 1.0_f32),
3849 ("b".to_string(), "c".to_string(), 1.0_f32),
3850 ];
3851 let degrees = abstract_degree_centrality(&nodes, &edges);
3852 assert_eq!(
3853 degrees,
3854 vec![
3855 ("a".to_string(), 1),
3856 ("b".to_string(), 2),
3857 ("c".to_string(), 1),
3858 ]
3859 );
3860 }
3861}
3862
3863fn reject_engine_internal_vault_key(key: &str) -> RedDBResult<()> {
3870 let lowered = key.to_ascii_lowercase();
3871 let internal = lowered == "red.secret.aes_key"
3872 || lowered.starts_with("red.iam.")
3873 || lowered.starts_with("red.acl.")
3874 || lowered.starts_with("red.vault.");
3875 if internal {
3876 return Err(RedDBError::Query(format!(
3877 "`{key}` is an engine-internal vault key and cannot be set or deleted through SET/DELETE SECRET"
3878 )));
3879 }
3880 Ok(())
3881}
3882
3883fn confine_copy_from_path(path: &str) -> RedDBResult<()> {
3889 let Ok(root) = std::env::var("REDDB_COPY_IMPORT_ROOT") else {
3890 return Ok(());
3891 };
3892 let root = std::fs::canonicalize(&root).map_err(|e| {
3893 RedDBError::Query(format!(
3894 "REDDB_COPY_IMPORT_ROOT `{root}` is not usable: {e}"
3895 ))
3896 })?;
3897 let resolved = std::fs::canonicalize(path)
3898 .map_err(|e| RedDBError::Query(format!("COPY FROM `{path}`: {e}")))?;
3899 if !resolved.starts_with(&root) || !resolved.is_file() {
3900 return Err(RedDBError::Query(format!(
3901 "COPY FROM `{path}` is outside the configured import root"
3902 )));
3903 }
3904 Ok(())
3905}
3906
3907#[cfg(test)]
3908mod security_gate_tests {
3909 use super::*;
3910 use crate::auth::store::AuthStore;
3911 use crate::auth::{AuthConfig, Role};
3912
3913 struct IdentityGuard;
3916
3917 impl IdentityGuard {
3918 fn install(username: &str, role: Role) -> Self {
3919 set_current_auth_identity(username.to_string(), role);
3920 Self
3921 }
3922 }
3923
3924 impl Drop for IdentityGuard {
3925 fn drop(&mut self) {
3926 clear_current_auth_identity();
3927 }
3928 }
3929
3930 fn runtime_with_user(username: &str, role: Role) -> RedDBRuntime {
3931 let store = std::sync::Arc::new(AuthStore::new(AuthConfig {
3932 enabled: true,
3933 session_ttl_secs: 60,
3934 require_auth: true,
3935 auto_encrypt_storage: false,
3936 vault_enabled: false,
3937 cert: Default::default(),
3938 oauth: Default::default(),
3939 }));
3940 store.create_user(username, "pw", role).expect("user");
3941 let runtime =
3942 RedDBRuntime::with_options(crate::api::RedDBOptions::in_memory()).expect("runtime");
3943 runtime.set_auth_store(store);
3944 runtime
3945 }
3946
3947 fn runtime_with_store(username: &str, role: Role) -> (RedDBRuntime, std::sync::Arc<AuthStore>) {
3948 let store = std::sync::Arc::new(AuthStore::new(AuthConfig {
3949 enabled: true,
3950 session_ttl_secs: 60,
3951 require_auth: true,
3952 auto_encrypt_storage: false,
3953 vault_enabled: false,
3954 cert: Default::default(),
3955 oauth: Default::default(),
3956 }));
3957 store.create_user(username, "pw", role).expect("user");
3958 let runtime =
3959 RedDBRuntime::with_options(crate::api::RedDBOptions::in_memory()).expect("runtime");
3960 runtime.set_auth_store(std::sync::Arc::clone(&store));
3961 (runtime, store)
3962 }
3963
3964 #[test]
3965 fn nested_reads_need_select_on_the_inner_table() {
3966 use crate::auth::privileges::{Action, GrantPrincipal, Resource};
3967 use crate::auth::UserId;
3968 let (runtime, store) = runtime_with_store("ro", Role::Read);
3969 runtime
3970 .execute_query("CREATE TABLE gate_open (a INTEGER)")
3971 .expect("create");
3972 runtime
3973 .execute_query("CREATE TABLE gate_closed (b INTEGER)")
3974 .expect("create");
3975 store
3978 .grant(
3979 &UserId::from_parts(None, "admin"),
3980 Role::Admin,
3981 GrantPrincipal::User(UserId::from_parts(None, "ro")),
3982 Resource::table_from_name("gate_open"),
3983 vec![Action::Select],
3984 false,
3985 None,
3986 )
3987 .expect("grant");
3988 let _identity = IdentityGuard::install("ro", Role::Read);
3989 runtime
3990 .execute_query("SELECT * FROM (SELECT a FROM gate_open) s")
3991 .expect("derived table over a granted table is fine");
3992 for sql in [
3995 "SELECT * FROM (SELECT b FROM gate_closed) s",
3996 "SELECT a FROM gate_open WHERE a IN (SELECT b FROM gate_closed)",
3997 ] {
3998 let err = runtime
3999 .execute_query(sql)
4000 .expect_err("nested read of an ungranted table must be refused");
4001 assert!(
4002 err.to_string().contains("permission denied"),
4003 "{sql}: unexpected error: {err}"
4004 );
4005 }
4006 }
4007
4008 #[test]
4009 fn policy_listings_are_self_or_policy_management() {
4010 let (runtime, store) = runtime_with_store("ro", Role::Read);
4011 store.create_user("other", "pw", Role::Read).expect("user");
4012 let _identity = IdentityGuard::install("ro", Role::Read);
4013 runtime
4014 .execute_query("SHOW EFFECTIVE PERMISSIONS FOR ro")
4015 .expect("a principal may inspect its own permissions");
4016 let err = runtime
4017 .execute_query("SHOW EFFECTIVE PERMISSIONS FOR other")
4018 .expect_err("inspecting another principal is policy management");
4019 assert!(
4020 err.to_string().contains("permission denied"),
4021 "unexpected error: {err}"
4022 );
4023 }
4024
4025 #[test]
4026 fn secret_columns_stay_masked_for_readers_without_secret_read() {
4027 use reddb_types::Value;
4028 let (runtime, store) = runtime_with_store("ro", Role::Read);
4029 store.create_user("root", "pw", Role::Admin).expect("admin");
4030 store.ensure_vault_secret_key();
4031 runtime
4032 .execute_query(
4033 "INSERT INTO creds_gate (name, token) VALUES ('stripe', SECRET('sk_live_abc'))",
4034 )
4035 .expect("insert");
4036
4037 let token_for = |username: &str, role: Role| {
4038 let _identity = IdentityGuard::install(username, role);
4039 let result = runtime
4040 .execute_query("SELECT name, token FROM creds_gate")
4041 .expect("select");
4042 result.result.records[0]
4043 .get("token")
4044 .cloned()
4045 .expect("token column")
4046 };
4047 assert!(
4050 matches!(token_for("ro", Role::Read), Value::Secret(_)),
4051 "a read-role principal must see the column masked"
4052 );
4053 assert!(matches!(token_for("root", Role::Admin), Value::Secret(_)));
4054 }
4055
4056 #[test]
4057 fn store_wide_operations_need_the_admin_tier() {
4058 let runtime = runtime_with_user("bob", Role::Write);
4059 let _identity = IdentityGuard::install("bob", Role::Write);
4060 for sql in ["VACUUM", "SCRUB"] {
4063 let err = runtime
4064 .execute_query(sql)
4065 .expect_err("store-wide operation must be refused for a non-admin");
4066 assert!(
4067 err.to_string().contains("permission denied"),
4068 "{sql}: unexpected error: {err}"
4069 );
4070 }
4071 }
4072
4073 #[test]
4074 fn explain_needs_the_privilege_of_the_statement_it_plans() {
4075 let runtime = runtime_with_user("ro", Role::Read);
4076 let _identity = IdentityGuard::install("ro", Role::Read);
4077 runtime
4078 .execute_query("EXPLAIN SELECT 1 AS one")
4079 .expect("planning a read is a read");
4080 let err = runtime
4083 .execute_query("EXPLAIN INSERT INTO gate_t (a) VALUES (1)")
4084 .expect_err("planning a write needs the write privilege");
4085 assert!(
4086 err.to_string().contains("permission denied"),
4087 "unexpected EXPLAIN error: {err}"
4088 );
4089 }
4090
4091 #[test]
4092 fn copy_from_a_server_path_is_admin_only() {
4093 let runtime = runtime_with_user("bob", Role::Write);
4094 let _identity = IdentityGuard::install("bob", Role::Write);
4095 let err = runtime
4099 .execute_query("COPY leak FROM '/etc/hostname'")
4100 .expect_err("COPY FROM must be refused for a non-admin");
4101 assert!(
4102 err.to_string().contains("permission denied"),
4103 "unexpected COPY error: {err}"
4104 );
4105 }
4106
4107 #[test]
4108 fn vcs_history_rewrites_are_admin_only() {
4109 let runtime = runtime_with_user("bob", Role::Write);
4110 let _identity = IdentityGuard::install("bob", Role::Write);
4111 let err = runtime
4114 .execute_query("RESET HARD TO 'deadbeef'")
4115 .expect_err("RESET must be refused for a non-admin");
4116 assert!(
4117 err.to_string().contains("permission denied"),
4118 "unexpected RESET error: {err}"
4119 );
4120 }
4121
4122 #[test]
4123 fn ai_provider_config_writes_are_admin_only() {
4124 let runtime = runtime_with_user("bob", Role::Write);
4125 let _identity = IdentityGuard::install("bob", Role::Write);
4126 let err = runtime
4129 .execute_query(
4130 "SET CONFIG red.config.ai.providers.openai.tokens.default.secret_ref = 'red.secret.aes_key'",
4131 )
4132 .expect_err("AI provider config must be refused for a non-admin");
4133 assert!(
4134 err.to_string().contains("permission denied"),
4135 "unexpected SET CONFIG error: {err}"
4136 );
4137 }
4138
4139 #[test]
4140 fn engine_internal_vault_keys_are_not_writable_through_set_secret() {
4141 let runtime = runtime_with_user("root", Role::Admin);
4142 let _identity = IdentityGuard::install("root", Role::Admin);
4143 for key in [
4147 "red.iam.policies",
4148 "red.iam.attachments.users",
4149 "red.secret.aes_key",
4150 ] {
4151 let err = runtime
4152 .execute_query(&format!("SET SECRET {key} = 'x'"))
4153 .expect_err("engine-internal vault key must be refused");
4154 assert!(
4155 err.to_string().contains("engine-internal"),
4156 "unexpected error for {key}: {err}"
4157 );
4158 }
4159 }
4160}