1use super::*;
2use crate::auth::column_policy_gate::ColumnAccessRequest;
3use crate::auth::UserId;
4use crate::replication::cdc::ChangeRecord;
5use reddb_rql::ast::TableSource;
6pub(crate) use super::authz::policy_columns::*;
11
12pub(crate) use super::graph_tvf::{abstract_degree_centrality, is_graph_tvf_name};
17pub(crate) use super::rls_injection::{
18 apply_foreign_table_filters, inject_rls_filters, inject_rls_into_join, rls_is_enabled,
19 rls_policy_filter, rls_policy_filter_for_kind,
20};
21pub(crate) use super::vcs_command::{
27 parse_runtime_vcs_command, strip_explain_prefix, walk_collections,
28};
29
30pub(crate) use super::impl_audit_control::{
42 control_event_outcome_for_error, query_audit_plan, query_control_event_specs,
43};
44pub(crate) use super::impl_config_secret::{
46 insert_config_json_path, secret_sql_value_to_string, seed_storage_deploy_config,
47 show_config_json_result, show_secrets_allows_key,
48};
49pub(super) use super::impl_core_outer_scope::{
50 first_column_values, query_references_outer_scope, relation_scopes_for_query,
51};
52pub(crate) use super::impl_core_result_cache_scopes::collect_table_refs;
53pub(super) use super::impl_core_result_cache_scopes::query_expr_result_cache_scopes;
54
55pub(crate) use super::impl_lifecycle::{table_row_index_fields, view_records_to_entities};
63
64pub use super::execution_context::{
65 capture_current_snapshot, clear_current_auth_identity, clear_current_connection_id,
66 clear_current_snapshot, clear_current_tenant, current_auth_identity_for_audit,
67 current_connection_id, current_tenant, entity_visible_under_current_snapshot,
68 entity_visible_with_context, set_current_auth_identity, set_current_connection_id,
69 set_current_snapshot, set_current_tenant, snapshot_bundle, with_snapshot_bundle,
70 SnapshotBundle, SnapshotContext,
71};
72pub(crate) use super::execution_context::{
73 config_read_permitted, current_auth_identity, current_config_value, current_kv_value,
74 current_role_projected, current_scope_override, current_secret_value,
75 current_snapshot_requires_index_fallback, current_user_projected, has_scope_override_active,
76 kv_read_permitted, parse_set_local_tenant, update_current_config_value,
77 update_current_kv_value, update_current_secret_value, xids_visible_under_current_snapshot,
78 ConfigSnapshotGuard, CurrentSnapshotGuard, KvStoreGuard, ScopeOverrideGuard, SecretStoreGuard,
79 TxLocalTenantGuard,
80};
81
82pub(super) fn has_with_prefix(sql: &str) -> bool {
87 let trimmed = sql.trim_start();
88 let head_end = trimmed
89 .find(|c: char| c.is_whitespace() || c == '(')
90 .unwrap_or(trimmed.len());
91 trimmed[..head_end].eq_ignore_ascii_case("WITH")
92}
93
94pub(super) fn peek_top_level_as_of_with_table(
99 sql: &str,
100) -> Option<(crate::application::vcs::AsOfSpec, Option<String>)> {
101 if !sql
102 .as_bytes()
103 .windows(5)
104 .any(|w| w.eq_ignore_ascii_case(b"as of"))
105 {
106 return None;
107 }
108 let parsed = reddb_rql::parser::parse(sql).ok()?;
109 let reddb_rql::ast::QueryExpr::Table(table) = parsed.query else {
110 return None;
111 };
112 let clause = table.as_of?;
113 let table_name = if table.table.is_empty() || table.table == "any" {
114 None
115 } else {
116 Some(table.table.clone())
117 };
118 let spec = match clause {
119 reddb_rql::ast::AsOfClause::Commit(h) => crate::application::vcs::AsOfSpec::Commit(h),
120 reddb_rql::ast::AsOfClause::Branch(b) => crate::application::vcs::AsOfSpec::Branch(b),
121 reddb_rql::ast::AsOfClause::Tag(t) => crate::application::vcs::AsOfSpec::Tag(t),
122 reddb_rql::ast::AsOfClause::TimestampMs(ts) => {
123 crate::application::vcs::AsOfSpec::TimestampMs(ts)
124 }
125 reddb_rql::ast::AsOfClause::Snapshot(x) => crate::application::vcs::AsOfSpec::Snapshot(x),
126 };
127 Some((spec, table_name))
128}
129
130pub(super) fn query_has_volatile_builtin(sql: &str) -> bool {
131 const VOLATILE_TOKENS: &[&str] = &[
135 "pg_advisory_lock",
136 "pg_try_advisory_lock",
137 "pg_advisory_unlock",
138 "random()",
139 "$config",
144 "$secret",
145 "$kv",
146 ];
151 let lowered = sql.to_ascii_lowercase();
152 VOLATILE_TOKENS.iter().any(|t| lowered.contains(t))
153}
154
155pub(super) fn query_is_ask_statement(sql: &str) -> bool {
156 let trimmed = sql.trim_start();
157 let head_end = trimmed
158 .find(|c: char| c.is_whitespace() || c == '(' || c == ';')
159 .unwrap_or(trimmed.len());
160 trimmed[..head_end].eq_ignore_ascii_case("ASK")
161}
162
163pub(super) fn intent_lock_modes_for(
173 expr: &QueryExpr,
174) -> Option<(
175 crate::runtime::lock_manager::LockMode,
176 crate::runtime::lock_manager::LockMode,
177)> {
178 use crate::runtime::lock_manager::LockMode::{Exclusive, IntentExclusive, IntentShared};
179
180 match expr {
181 QueryExpr::Table(_)
183 | QueryExpr::Join(_)
184 | QueryExpr::Vector(_)
185 | QueryExpr::Hybrid(_)
186 | QueryExpr::Graph(_)
187 | QueryExpr::Path(_)
188 | QueryExpr::Ask(_)
189 | QueryExpr::SearchCommand(_)
190 | QueryExpr::GraphCommand(_)
191 | QueryExpr::RankOf(_)
192 | QueryExpr::ApproxRankOf(_)
193 | QueryExpr::RankRange(_)
194 | QueryExpr::QueueSelect(_) => Some((IntentShared, IntentShared)),
195
196 QueryExpr::Insert(_)
204 | QueryExpr::Update(_)
205 | QueryExpr::Delete(_)
206 | QueryExpr::QueueCommand(QueueCommand::Move { .. }) => {
207 Some((IntentExclusive, IntentExclusive))
208 }
209 QueryExpr::QueueCommand(_) => Some((IntentShared, IntentShared)),
210
211 QueryExpr::CreateTable(_)
215 | QueryExpr::CreateCollection(_)
216 | QueryExpr::CreateVector(_)
217 | QueryExpr::DropTable(_)
218 | QueryExpr::DropGraph(_)
219 | QueryExpr::DropVector(_)
220 | QueryExpr::DropDocument(_)
221 | QueryExpr::DropKv(_)
222 | QueryExpr::DropCollection(_)
223 | QueryExpr::Truncate(_)
224 | QueryExpr::AlterTable(_)
225 | QueryExpr::CreateIndex(_)
226 | QueryExpr::DropIndex(_)
227 | QueryExpr::CreateTimeSeries(_)
228 | QueryExpr::CreateMetric(_)
229 | QueryExpr::AlterMetric(_)
230 | QueryExpr::CreateSlo(_)
231 | QueryExpr::DropTimeSeries(_)
232 | QueryExpr::CreateQueue(_)
233 | QueryExpr::AlterQueue(_)
234 | QueryExpr::DropQueue(_)
235 | QueryExpr::CreateTree(_)
236 | QueryExpr::DropTree(_)
237 | QueryExpr::CreatePolicy(_)
238 | QueryExpr::DropPolicy(_)
239 | QueryExpr::CreateView(_)
240 | QueryExpr::DropView(_)
241 | QueryExpr::RefreshMaterializedView(_)
242 | QueryExpr::CreateSchema(_)
243 | QueryExpr::DropSchema(_)
244 | QueryExpr::CreateSequence(_)
245 | QueryExpr::DropSequence(_)
246 | QueryExpr::CreateServer(_)
247 | QueryExpr::DropServer(_)
248 | QueryExpr::CreateForeignTable(_)
249 | QueryExpr::DropForeignTable(_) => Some((IntentExclusive, Exclusive)),
250
251 _ => None,
257 }
258}
259
260pub(super) fn collections_referenced(expr: &QueryExpr) -> Vec<String> {
265 let mut out = Vec::new();
266 walk_collections(expr, &mut out);
267 out.sort();
268 out.dedup();
269 out
270}
271
272impl RedDBRuntime {
273 pub fn execute_query_with_scope(
287 &self,
288 query: &str,
289 scope: crate::runtime::within_clause::ScopeOverride,
290 ) -> RedDBResult<RuntimeQueryResult> {
291 if scope.is_empty() {
292 return self.execute_query(query);
293 }
294 let _scope_guard = ScopeOverrideGuard::install(scope);
295 self.execute_query(query)
296 }
297
298 pub fn execute_query(&self, query: &str) -> RedDBResult<RuntimeQueryResult> {
307 let started = std::time::Instant::now();
308 self.inner.node_load_telemetry.query_start();
309 let result = self.execute_query_inner(query);
310 self.finish_query_lifecycle(query, started, result)
311 }
312
313 pub fn execute_query_with_params(
321 &self,
322 query: &str,
323 params: &[Value],
324 ) -> RedDBResult<RuntimeQueryResult> {
325 if params.is_empty() {
326 return self.execute_query(query);
327 }
328 let started = std::time::Instant::now();
329 self.inner.node_load_telemetry.query_start();
330 let result = self.execute_query_with_params_inner(query, params);
331 self.finish_query_lifecycle(query, started, result)
332 }
333
334 pub fn execute_prepared_query(
345 &self,
346 query: &str,
347 expr: QueryExpr,
348 ) -> RedDBResult<RuntimeQueryResult> {
349 let started = std::time::Instant::now();
350 self.inner.node_load_telemetry.query_start();
351 let result = self.execute_bound_query_expr_in_frame(query, expr);
352 self.finish_query_lifecycle(query, started, result)
353 }
354
355 fn finish_query_lifecycle(
356 &self,
357 query: &str,
358 started: std::time::Instant,
359 mut result: RedDBResult<RuntimeQueryResult>,
360 ) -> RedDBResult<RuntimeQueryResult> {
361 if let Ok(ref mut query_result) = result {
366 if query_result.statement_type == "select" {
367 self.filter_integrity_tombstoned(&mut query_result.result);
368 }
369 }
370 let elapsed_ms = started.elapsed().as_millis() as u64;
371
372 let scope = self.ai_scope();
377 let kind = match result
378 .as_ref()
379 .map(|r| r.statement_type)
380 .unwrap_or("select")
381 {
382 "select" => crate::telemetry::slow_query_logger::QueryKind::Select,
383 "insert" => crate::telemetry::slow_query_logger::QueryKind::Insert,
384 "update" => crate::telemetry::slow_query_logger::QueryKind::Update,
385 "delete" => crate::telemetry::slow_query_logger::QueryKind::Delete,
386 _ => crate::telemetry::slow_query_logger::QueryKind::Internal,
387 };
388 self.inner
394 .slow_query_logger
395 .record(kind, elapsed_ms, query.to_string(), &scope);
396
397 self.inner
403 .query_latency_telemetry
404 .observe(kind, started.elapsed().as_secs_f64());
405
406 self.inner.node_load_telemetry.query_finish();
410
411 if let Ok(ref mut query_result) = result {
412 if matches!(query_result.statement_type, "insert" | "update" | "delete") {
413 let bookmark = crate::replication::CausalBookmark::new(
414 self.current_replication_term(),
415 self.cdc_current_lsn(),
416 );
417 query_result.bookmark = Some(bookmark.encode());
418 }
419 }
420
421 result
422 }
423
424 fn execute_query_with_params_inner(
425 &self,
426 query: &str,
427 params: &[Value],
428 ) -> RedDBResult<RuntimeQueryResult> {
429 let parsed = self.parameterized_query_ast(query)?;
430 let bound = crate::storage::query::user_params::bind(&parsed, params).map_err(|err| {
431 RedDBError::Validation {
432 message: err.to_string(),
433 validation: crate::json!({
434 "code": "INVALID_PARAMS",
435 "surface": "query.params",
436 }),
437 }
438 })?;
439 self.execute_bound_query_expr_in_frame(query, bound)
440 }
441
442 fn parameterized_query_ast(&self, query: &str) -> RedDBResult<QueryExpr> {
445 use crate::storage::query::planner::{CachedPlan, QueryPlan};
446
447 let key = format!("\0parameters\0{query}");
449 if let Some(parsed) = self
450 .inner
451 .query_cache
452 .read()
453 .peek(&key)
454 .filter(|entry| entry.matches_exact_query(query))
455 .map(|entry| entry.plan.original.clone())
456 {
457 return Ok(parsed);
458 }
459 let parsed = parse_multi(query).map_err(|err| RedDBError::Query(err.to_string()))?;
460 let plan = QueryPlan::new(parsed.clone(), parsed.clone(), Default::default());
461 self.inner
462 .query_cache
463 .write()
464 .insert(key, CachedPlan::new(plan).with_exact_query(query));
465 Ok(parsed)
466 }
467
468 fn execute_bound_query_expr_in_frame(
469 &self,
470 query: &str,
471 expr: QueryExpr,
472 ) -> RedDBResult<RuntimeQueryResult> {
473 let rewritten_query = super::red_schema::rewrite_virtual_names(query);
474 let execution_query = rewritten_query.as_deref().unwrap_or(query);
475 let frame = super::statement_frame::StatementExecutionFrame::build(
476 self,
477 super::statement_frame::StatementIdentity::Text(execution_query),
478 )?;
479 let _frame_guards = Box::new(frame.install(self));
483 let _log_span = crate::telemetry::span::query_span(query).entered();
484
485 let expr = self.rewrite_view_refs(expr);
486 let mode = detect_mode(execution_query);
487 let control_event_specs = query_control_event_specs(&expr);
488 let _lock_guard = match frame.prepare_dispatch(self, &expr) {
489 Ok(guard) => guard,
490 Err(err) => {
491 let outcome = control_event_outcome_for_error(&err);
492 for spec in &control_event_specs {
493 self.emit_control_event(
494 spec.kind,
495 outcome,
496 spec.action,
497 spec.resource.clone(),
498 Some(err.to_string()),
499 spec.fields.clone(),
500 )?;
501 }
502 return Err(err);
503 }
504 };
505
506 let mut result = self.dispatch_expr(expr, query, mode)?;
507 if result.statement_type == "select" {
508 self.apply_secret_decryption(&mut result);
509 }
510 Ok(result)
511 }
512
513 pub fn causal_session(&self) -> crate::runtime::CausalSession {
514 crate::runtime::CausalSession {
515 runtime: self.clone(),
516 bookmark: None,
517 wait_timeout: std::time::Duration::from_secs(5),
518 }
519 }
520
521 pub fn wait_for_bookmark(
522 &self,
523 bookmark: &crate::replication::CausalBookmark,
524 timeout: std::time::Duration,
525 ) -> RedDBResult<()> {
526 let deadline = std::time::Instant::now() + timeout;
527 loop {
528 let applied_lsn = self.local_contiguous_applied_lsn();
529 if applied_lsn >= bookmark.commit_lsn() {
530 return Ok(());
531 }
532 let now = std::time::Instant::now();
533 if now >= deadline {
534 return Err(RedDBError::InvalidOperation(format!(
535 "timed out waiting for causal bookmark lsn {}; applied={}",
536 bookmark.commit_lsn(),
537 applied_lsn
538 )));
539 }
540 let remaining = deadline.saturating_duration_since(now);
541 std::thread::sleep(remaining.min(std::time::Duration::from_millis(5)));
542 }
543 }
544
545 fn local_contiguous_applied_lsn(&self) -> u64 {
546 match self.inner.db.options().replication.role {
547 crate::replication::ReplicationRole::Replica { .. } => {
548 self.config_u64("red.replication.last_applied_lsn", 0)
549 }
550 _ => self.cdc_current_lsn(),
551 }
552 }
553
554 #[inline(never)]
555 pub(super) fn execute_query_inner(&self, query: &str) -> RedDBResult<RuntimeQueryResult> {
556 if !has_scope_override_active()
567 && !query.trim_start().starts_with("WITHIN")
568 && !query.trim_start().starts_with("within")
569 && !self.inner.query_audit.has_rules()
570 && !self
571 .inner
572 .transaction_state
573 .in_transaction(current_connection_id())
574 {
575 if let Some(result) = self.try_fast_entity_lookup(query) {
576 return result;
577 }
578 }
579
580 match crate::runtime::within_clause::try_strip_within_prefix(query) {
587 Ok(Some((scope, inner))) => {
588 let _scope_guard = ScopeOverrideGuard::install(scope);
589 return self.execute_query_inner(inner);
594 }
595 Ok(None) => {}
596 Err(msg) => return Err(RedDBError::Query(msg)),
597 }
598
599 if let Some(inner) = strip_explain_analyze_prefix(query) {
603 return self.explain_analyze_as_rows(query, inner);
604 }
605
606 if let Some(inner) = strip_explain_prefix(query) {
613 return self.explain_as_rows(query, inner);
614 }
615
616 if let Some(value) = parse_set_local_tenant(query)? {
621 let conn_id = current_connection_id();
622 if !self.inner.transaction_state.in_transaction(conn_id) {
623 return Err(RedDBError::Query(
624 "SET LOCAL TENANT requires an active transaction".to_string(),
625 ));
626 }
627 self.inner
628 .transaction_state
629 .set_local_tenant(conn_id, value.clone());
630 return Ok(RuntimeQueryResult::ok_message(
631 query.to_string(),
632 &match &value {
633 Some(id) => format!("local tenant set: {id}"),
634 None => "local tenant cleared".to_string(),
635 },
636 "set_local_tenant",
637 ));
638 }
639
640 if super::red_schema::is_system_schema_write(query) {
641 return Err(RedDBError::Query(
642 super::red_schema::READ_ONLY_ERROR.to_string(),
643 ));
644 }
645
646 if let Some(command) = parse_runtime_vcs_command(query) {
647 let command = command?;
648 self.check_vcs_command_privilege(&command)?;
649 return self.execute_vcs_command(query, detect_mode(query), command);
650 }
651
652 if let Some(create_source) = super::analytics_source_catalog::parse_create_statement(query)?
653 {
654 return self.execute_create_analytics_source(query, create_source);
655 }
656
657 if let Some(path) = super::metric_descriptor_catalog::parse_read_metric_statement(query) {
663 return Err(super::metric_descriptor_catalog::read_output_unsupported(
664 &path,
665 ));
666 }
667
668 if let Some(parsed) = super::ranking_descriptor_catalog::parse_create_ranking(query) {
673 return self.execute_create_ranking(query, parsed?);
674 }
675 if super::ranking_descriptor_catalog::parse_show_rankings(query) {
676 return self.execute_show_rankings(query);
677 }
678
679 let rewritten_query = super::red_schema::rewrite_virtual_names(query);
680 let execution_query = rewritten_query.as_deref().unwrap_or(query);
681
682 let frame = super::statement_frame::StatementExecutionFrame::build(
683 self,
684 super::statement_frame::StatementIdentity::Text(execution_query),
685 )?;
686 let _frame_guards = Box::new(frame.install(self));
687
688 let _log_span = crate::telemetry::span::query_span(query).entered();
695
696 if let Some(rewritten) = frame.prepare_cte(execution_query)? {
698 return self.execute_query_expr(rewritten);
699 }
700
701 if !self.inner.query_audit.has_rules() {
703 if let Some(result) = self.try_fast_entity_lookup(execution_query) {
704 return result;
705 }
706 }
707
708 if !self.inner.query_audit.has_rules() {
710 if let Some(result) = frame.read_result_cache(self) {
711 return Ok(result);
712 }
713 }
714
715 let prepared = frame.prepare_statement(self, execution_query)?;
716 let mode = prepared.mode;
717 let expr = prepared.expr;
718
719 let statement = query_expr_name(&expr);
720 let result_cache_scopes = query_expr_result_cache_scopes(&expr);
721 let control_event_specs = query_control_event_specs(&expr);
722 let query_audit_plan = query_audit_plan(&expr);
723
724 let _lock_guard = match frame.prepare_dispatch(self, &expr) {
725 Ok(guard) => guard,
726 Err(err) => {
727 let outcome = control_event_outcome_for_error(&err);
728 for spec in &control_event_specs {
729 self.emit_control_event(
730 spec.kind,
731 outcome,
732 spec.action,
733 spec.resource.clone(),
734 Some(err.to_string()),
735 spec.fields.clone(),
736 )?;
737 }
738 return Err(err);
739 }
740 };
741 let frame_iface: &dyn super::statement_frame::ReadFrame = &frame;
742 let query_audit_started = std::time::Instant::now();
743
744 let query_result = match expr {
745 QueryExpr::Function(ref command) => self.execute_function(query, command),
746 QueryExpr::Graph(_) | QueryExpr::Path(_) => {
747 self.execute_materialized_graph(query, mode, statement, &expr)
748 }
749 QueryExpr::Table(table) => {
750 let table = self.resolve_table_expr_subqueries(
751 table,
752 &frame as &dyn super::statement_frame::ReadFrame,
753 )?;
754 if let Some(TableSource::Function {
758 name,
759 args,
760 named_args,
761 }) = table.source.clone()
762 {
763 let tvf_result = RuntimeQueryResult {
771 query: query.to_string(),
772 mode,
773 statement,
774 engine: "runtime-graph-tvf",
775 result: self.execute_table_function(&name, &args, &named_args)?,
776 affected_rows: 0,
777 statement_type: "select",
778 bookmark: None,
779 notice: None,
780 };
781 frame.write_result_cache(self, &tvf_result, result_cache_scopes.clone());
782 return Ok(tvf_result);
783 }
784 if let Some(TableSource::InlineGraphFunction {
792 name,
793 nodes,
794 edges,
795 named_args,
796 }) = table.source.clone()
797 {
798 let inline_result = RuntimeQueryResult {
799 query: query.to_string(),
800 mode,
801 statement,
802 engine: "runtime-graph-tvf-inline",
803 result: self.execute_inline_graph_function(
804 &name,
805 &nodes,
806 &edges,
807 &named_args,
808 )?,
809 affected_rows: 0,
810 statement_type: "select",
811 bookmark: None,
812 notice: None,
813 };
814 frame.write_result_cache(self, &inline_result, result_cache_scopes);
815 return Ok(inline_result);
816 }
817 if super::red_schema::is_virtual_table(&table.table) {
818 return Ok(RuntimeQueryResult {
819 query: query.to_string(),
820 mode,
821 statement,
822 engine: "runtime-red-schema",
823 result: super::red_schema::red_query(
824 self,
825 &table.table,
826 &table,
827 &frame as &dyn super::statement_frame::ReadFrame,
828 )?,
829 affected_rows: 0,
830 statement_type: "select",
831 bookmark: None,
832 notice: None,
833 });
834 }
835
836 if let Some(view_result) = self.try_resolve_analytics_view(
840 &table,
841 &frame as &dyn super::statement_frame::ReadFrame,
842 )? {
843 return Ok(RuntimeQueryResult {
844 query: query.to_string(),
845 mode,
846 statement,
847 engine: "runtime-graph-analytics-view",
848 result: view_result,
849 affected_rows: 0,
850 statement_type: "select",
851 bookmark: None,
852 notice: None,
853 });
854 }
855
856 if let Some(result) = self.execute_probabilistic_select(&table)? {
857 return Ok(RuntimeQueryResult {
858 query: query.to_string(),
859 mode,
860 statement,
861 engine: "runtime-probabilistic",
862 result,
863 affected_rows: 0,
864 statement_type: "select",
865 bookmark: None,
866 notice: None,
867 });
868 }
869
870 if self.inner.foreign_tables.is_foreign_table(&table.table) {
878 let records = self
879 .inner
880 .foreign_tables
881 .scan(&table.table)
882 .map_err(|e| RedDBError::Internal(e.to_string()))?;
883 let result = apply_foreign_table_filters(records, &table);
884 return Ok(RuntimeQueryResult {
885 query: query.to_string(),
886 mode,
887 statement,
888 engine: "runtime-fdw",
889 result,
890 affected_rows: 0,
891 statement_type: "select",
892 bookmark: None,
893 notice: None,
894 });
895 }
896
897 let Some(table_with_rls) = self.authorize_relational_table_select(
914 table,
915 &frame as &dyn super::statement_frame::ReadFrame,
916 )?
917 else {
918 let empty = crate::storage::query::unified::UnifiedResult::empty();
919 return Ok(RuntimeQueryResult {
920 query: query.to_string(),
921 mode,
922 statement,
923 engine: "runtime-table-rls",
924 result: empty,
925 affected_rows: 0,
926 statement_type: "select",
927 bookmark: None,
928 notice: None,
929 });
930 };
931 Ok(RuntimeQueryResult {
932 query: query.to_string(),
933 mode,
934 statement,
935 engine: "runtime-table",
936 result: execute_runtime_table_query_in(
943 &self.inner.db,
944 &table_with_rls,
945 Some(&self.inner.index_store),
946 Some(frame.row_arena()),
947 )?,
948 affected_rows: 0,
949 statement_type: "select",
950 bookmark: None,
951 notice: None,
952 })
953 }
954 QueryExpr::Join(join) => {
955 let join_with_rls = match self.authorize_relational_join_select(
964 join,
965 &frame as &dyn super::statement_frame::ReadFrame,
966 )? {
967 Some(j) => j,
968 None => {
969 return Ok(RuntimeQueryResult {
970 query: query.to_string(),
971 mode,
972 statement,
973 engine: "runtime-join-rls",
974 result: crate::storage::query::unified::UnifiedResult::empty(),
975 affected_rows: 0,
976 statement_type: "select",
977 bookmark: None,
978 notice: None,
979 });
980 }
981 };
982 Ok(RuntimeQueryResult {
983 query: query.to_string(),
984 mode,
985 statement,
986 engine: "runtime-join",
987 result: execute_runtime_join_query(self, &join_with_rls)?,
988 affected_rows: 0,
989 statement_type: "select",
990 bookmark: None,
991 notice: None,
992 })
993 }
994 QueryExpr::Vector(vector) => Ok(RuntimeQueryResult {
995 query: query.to_string(),
996 mode,
997 statement,
998 engine: "runtime-vector",
999 result: execute_runtime_vector_query(self, &vector)?,
1000 affected_rows: 0,
1001 statement_type: "select",
1002 bookmark: None,
1003 notice: None,
1004 }),
1005 QueryExpr::Hybrid(hybrid) => Ok(RuntimeQueryResult {
1006 query: query.to_string(),
1007 mode,
1008 statement,
1009 engine: "runtime-hybrid",
1010 result: execute_runtime_hybrid_query(self, &hybrid)?,
1011 affected_rows: 0,
1012 statement_type: "select",
1013 bookmark: None,
1014 notice: None,
1015 }),
1016 QueryExpr::RankOf(ref rank) => self.execute_rank_of(query, rank),
1017 QueryExpr::ApproxRankOf(ref rank) => self.execute_approx_rank_of(query, rank),
1018 QueryExpr::RankRange(ref range) => self.execute_rank_range(query, range),
1019 QueryExpr::Insert(ref insert) if super::red_schema::is_virtual_table(&insert.table) => {
1021 Err(RedDBError::Query(
1022 super::red_schema::READ_ONLY_ERROR.to_string(),
1023 ))
1024 }
1025 QueryExpr::Update(ref update) if super::red_schema::is_virtual_table(&update.table) => {
1026 Err(RedDBError::Query(
1027 super::red_schema::READ_ONLY_ERROR.to_string(),
1028 ))
1029 }
1030 QueryExpr::Delete(ref delete) if super::red_schema::is_virtual_table(&delete.table) => {
1031 Err(RedDBError::Query(
1032 super::red_schema::READ_ONLY_ERROR.to_string(),
1033 ))
1034 }
1035 QueryExpr::Insert(ref insert) => self
1036 .with_deferred_store_wal_for_dml(self.insert_may_emit_events(insert), || {
1037 self.execute_insert(query, insert)
1038 }),
1039 QueryExpr::Update(ref update) => self
1040 .with_deferred_store_wal_for_dml(self.update_may_emit_events(update), || {
1041 self.execute_update(query, update)
1042 }),
1043 QueryExpr::Delete(ref delete) => self
1044 .with_deferred_store_wal_for_dml(self.delete_may_emit_events(delete), || {
1045 self.execute_delete(query, delete)
1046 }),
1047 QueryExpr::CreateTable(ref create) => self.execute_create_table(query, create),
1049 QueryExpr::CreateCollection(ref create) => {
1050 self.execute_create_collection(query, create)
1051 }
1052 QueryExpr::CreateVector(ref create) => self.execute_create_vector(query, create),
1053 QueryExpr::DropTable(ref drop_tbl) => self.execute_drop_table(query, drop_tbl),
1054 QueryExpr::DropGraph(ref drop_graph) => self.execute_drop_graph(query, drop_graph),
1055 QueryExpr::DropVector(ref drop_vector) => self.execute_drop_vector(query, drop_vector),
1056 QueryExpr::DropDocument(ref drop_document) => {
1057 self.execute_drop_document(query, drop_document)
1058 }
1059 QueryExpr::DropKv(ref drop_kv) => self.execute_drop_kv(query, drop_kv),
1060 QueryExpr::DropCollection(ref drop_collection) => {
1061 self.execute_drop_collection(query, drop_collection)
1062 }
1063 QueryExpr::Truncate(ref truncate) => self.execute_truncate(query, truncate),
1064 QueryExpr::AlterTable(ref alter) => self.execute_alter_table(query, alter),
1065 QueryExpr::CreateVcsRef(ref create) => self.execute_create_vcs_ref(query, create),
1066 QueryExpr::DropVcsRef(ref drop_ref) => self.execute_drop_vcs_ref(query, drop_ref),
1067 QueryExpr::ForkStore(ref fork) => self.execute_fork_store(query, fork),
1068 QueryExpr::PromoteFork(ref promote_fork) => {
1069 self.execute_promote_fork(query, promote_fork)
1070 }
1071 QueryExpr::DropFork(ref drop_fork) => self.execute_drop_fork(query, drop_fork),
1072 QueryExpr::ExplainAlter(ref explain) => self.execute_explain_alter(query, explain),
1073 QueryExpr::GraphCommand(ref cmd) => self.execute_graph_command(query, cmd),
1075 QueryExpr::SearchCommand(ref cmd) => self.execute_search_command(query, cmd),
1077 QueryExpr::Ask(ref ask) => self.execute_ask(query, ask),
1079 QueryExpr::CreateIndex(ref create_idx) => self.execute_create_index(query, create_idx),
1080 QueryExpr::DropIndex(ref drop_idx) => self.execute_drop_index(query, drop_idx),
1081 QueryExpr::ProbabilisticCommand(ref cmd) => {
1082 self.execute_probabilistic_command(query, cmd)
1083 }
1084 QueryExpr::CreateTimeSeries(ref ts) => self.execute_create_timeseries(query, ts),
1086 QueryExpr::CreateMetric(ref metric) => self.execute_create_metric(query, metric),
1087 QueryExpr::AlterMetric(ref alter) => self.execute_alter_metric(query, alter),
1088 QueryExpr::CreateSlo(ref slo) => self.execute_create_slo(query, slo),
1089 QueryExpr::DropTimeSeries(ref ts) => self.execute_drop_timeseries(query, ts),
1090 QueryExpr::CreateQueue(ref q) => self.execute_create_queue(query, q),
1092 QueryExpr::AlterQueue(ref q) => self.execute_alter_queue(query, q),
1093 QueryExpr::DropQueue(ref q) => self.execute_drop_queue(query, q),
1094 QueryExpr::QueueSelect(ref q) => self.execute_queue_select(query, q),
1095 QueryExpr::QueueCommand(ref cmd) => self
1096 .with_deferred_store_wal_if_transaction(|| self.execute_queue_command(query, cmd)),
1097 QueryExpr::EventsBackfill(ref backfill) => {
1098 self.execute_events_backfill(query, backfill)
1099 }
1100 QueryExpr::EventsBackfillStatus { ref collection } => Err(RedDBError::Query(format!(
1101 "EVENTS BACKFILL STATUS for '{collection}' is not implemented in this slice"
1102 ))),
1103 QueryExpr::KvCommand(ref cmd) => self.execute_kv_command(query, cmd),
1104 QueryExpr::ConfigCommand(ref cmd) => self.execute_config_command(query, cmd),
1105 QueryExpr::CreateTree(ref tree) => self.execute_create_tree(query, tree),
1106 QueryExpr::DropTree(ref tree) => self.execute_drop_tree(query, tree),
1107 QueryExpr::TreeCommand(ref cmd) => self.execute_tree_command(query, cmd),
1108 QueryExpr::SetConfig { ref key, ref value } => {
1110 if key.starts_with("red.secret.") {
1111 return Err(RedDBError::Query(
1112 "red.secret.* is reserved for vault secrets; use SET SECRET".to_string(),
1113 ));
1114 }
1115 if key.starts_with("red.secrets.") {
1116 return Err(RedDBError::Query(
1117 "red.secrets.* is reserved for vault secrets; use SET SECRET".to_string(),
1118 ));
1119 }
1120 crate::ai::validate_ai_config_key_on_write(key)?;
1124 match self.check_managed_config_write_for_set_config(key) {
1129 Err(err) => Err(err),
1130 Ok(()) => {
1131 self.check_config_write_privilege(key)?;
1132 let store = self.inner.db.store();
1133 let json_val = match value {
1134 Value::Text(s) => crate::serde_json::Value::String(s.to_string()),
1135 Value::Integer(n) => crate::serde_json::Value::Number(*n as f64),
1136 Value::Float(n) => crate::serde_json::Value::Number(*n),
1137 Value::Boolean(b) => crate::serde_json::Value::Bool(*b),
1138 _ => crate::serde_json::Value::String(value.to_string()),
1139 };
1140 store.set_config_tree(key, &json_val);
1141 update_current_config_value(key, value.clone());
1142 self.invalidate_result_cache();
1147 Ok(RuntimeQueryResult::ok_message(
1148 query.to_string(),
1149 &format!("config set: {key}"),
1150 "set",
1151 ))
1152 }
1153 }
1154 }
1155 QueryExpr::SetSecret { ref key, ref value } => {
1157 if key.starts_with("red.config.") {
1158 return Err(RedDBError::Query(
1159 "red.config.* is reserved for config; use SET CONFIG".to_string(),
1160 ));
1161 }
1162 reject_engine_internal_vault_key(key)?;
1163 let auth_store = self.inner.auth_store.read().clone().ok_or_else(|| {
1164 RedDBError::Query("SET SECRET requires an enabled, unsealed vault".to_string())
1165 })?;
1166 self.check_secret_write_privilege(&auth_store, key)?;
1167 if matches!(value, Value::Null) {
1168 auth_store
1169 .vault_kv_try_delete(key)
1170 .map_err(|err| RedDBError::Query(err.to_string()))?;
1171 update_current_secret_value(key, None);
1172 self.invalidate_result_cache();
1173 return Ok(RuntimeQueryResult::ok_message(
1174 query.to_string(),
1175 &format!("secret deleted: {key}"),
1176 "delete_secret",
1177 ));
1178 }
1179 let value = secret_sql_value_to_string(value)?;
1180 auth_store
1181 .vault_kv_try_set(key.clone(), value.clone())
1182 .map_err(|err| RedDBError::Query(err.to_string()))?;
1183 update_current_secret_value(key, Some(value));
1184 self.invalidate_result_cache();
1185 Ok(RuntimeQueryResult::ok_message(
1186 query.to_string(),
1187 &format!("secret set: {key}"),
1188 "set_secret",
1189 ))
1190 }
1191 QueryExpr::DeleteSecret { ref key } => {
1193 reject_engine_internal_vault_key(key)?;
1194 let auth_store = self.inner.auth_store.read().clone().ok_or_else(|| {
1195 RedDBError::Query(
1196 "DELETE SECRET requires an enabled, unsealed vault".to_string(),
1197 )
1198 })?;
1199 self.check_secret_write_privilege(&auth_store, key)?;
1200 let deleted = auth_store
1201 .vault_kv_try_delete(key)
1202 .map_err(|err| RedDBError::Query(err.to_string()))?;
1203 if deleted {
1204 update_current_secret_value(key, None);
1205 }
1206 self.invalidate_result_cache();
1207 Ok(RuntimeQueryResult::ok_message(
1208 query.to_string(),
1209 &format!("secret deleted: {key}"),
1210 if deleted {
1211 "delete_secret"
1212 } else {
1213 "delete_secret_not_found"
1214 },
1215 ))
1216 }
1217 QueryExpr::SetKv { ref key, ref value } => {
1219 let auth_store = self.inner.auth_store.read().clone().ok_or_else(|| {
1220 RedDBError::Query("SET KV requires an auth store".to_string())
1221 })?;
1222 self.check_kv_write_privilege(&auth_store, key)?;
1223 if matches!(value, Value::Null) {
1225 auth_store.plain_kv_delete(key);
1226 update_current_kv_value(key, None);
1227 self.invalidate_result_cache();
1228 return Ok(RuntimeQueryResult::ok_message(
1229 query.to_string(),
1230 &format!("kv deleted: {key}"),
1231 "delete_kv",
1232 ));
1233 }
1234 let value = secret_sql_value_to_string(value)?;
1235 auth_store.plain_kv_set(key.clone(), value.clone());
1236 update_current_kv_value(key, Some(value));
1237 self.invalidate_result_cache();
1238 Ok(RuntimeQueryResult::ok_message(
1239 query.to_string(),
1240 &format!("kv set: {key}"),
1241 "set_kv",
1242 ))
1243 }
1244 QueryExpr::DeleteKv { ref key } => {
1246 let auth_store = self.inner.auth_store.read().clone().ok_or_else(|| {
1247 RedDBError::Query("DELETE KV requires an auth store".to_string())
1248 })?;
1249 self.check_kv_write_privilege(&auth_store, key)?;
1250 let deleted = auth_store.plain_kv_delete(key);
1251 if deleted {
1252 update_current_kv_value(key, None);
1253 }
1254 self.invalidate_result_cache();
1255 Ok(RuntimeQueryResult::ok_message(
1256 query.to_string(),
1257 &format!("kv deleted: {key}"),
1258 if deleted {
1259 "delete_kv"
1260 } else {
1261 "delete_kv_not_found"
1262 },
1263 ))
1264 }
1265 QueryExpr::Scrub { background, budget } => {
1266 self.execute_scrub_query(query, mode, background, budget)
1267 }
1268 QueryExpr::ShowSecrets { ref prefix } => {
1270 let auth_store = self.inner.auth_store.read().clone().ok_or_else(|| {
1271 RedDBError::Query("SHOW SECRET requires an enabled, unsealed vault".to_string())
1272 })?;
1273 if !auth_store.is_vault_backed() {
1274 return Err(RedDBError::Query(
1275 "SHOW SECRET requires an enabled, unsealed vault".to_string(),
1276 ));
1277 }
1278 let mut keys = auth_store.vault_kv_keys();
1279 keys.sort();
1280 let mut result = UnifiedResult::with_columns(vec![
1281 "key".into(),
1282 "value".into(),
1283 "status".into(),
1284 ]);
1285 for key in keys {
1286 if !show_secrets_allows_key(&key) {
1287 continue;
1288 }
1289 if let Some(ref pfx) = prefix {
1290 if !key.starts_with(pfx) {
1291 continue;
1292 }
1293 }
1294 let mut record = UnifiedRecord::new();
1295 record.set("key", Value::text(key));
1296 record.set("value", Value::text("***"));
1297 record.set("status", Value::text("active"));
1298 result.push(record);
1299 }
1300 Ok(RuntimeQueryResult {
1301 query: query.to_string(),
1302 mode,
1303 statement: "show_secrets",
1304 engine: "runtime-secret",
1305 result,
1306 affected_rows: 0,
1307 statement_type: "select",
1308 bookmark: None,
1309 notice: None,
1310 })
1311 }
1312 QueryExpr::ShowConfig {
1314 ref prefix,
1315 as_json,
1316 } => {
1317 self.check_config_read_privilege()?;
1318 let store = self.inner.db.store();
1319 let all_collections = store.list_collections();
1320 if !all_collections.contains(&"red_config".to_string()) {
1321 if as_json {
1322 return Ok(show_config_json_result(
1323 query,
1324 mode,
1325 prefix,
1326 crate::serde_json::Value::Object(crate::serde_json::Map::new()),
1327 ));
1328 }
1329 let result = UnifiedResult::with_columns(vec!["key".into(), "value".into()]);
1330 return Ok(RuntimeQueryResult {
1331 query: query.to_string(),
1332 mode,
1333 statement: "show_config",
1334 engine: "runtime-config",
1335 result,
1336 affected_rows: 0,
1337 statement_type: "select",
1338 bookmark: None,
1339 notice: None,
1340 });
1341 }
1342 let manager = store
1343 .get_collection("red_config")
1344 .ok_or_else(|| RedDBError::NotFound("red_config".to_string()))?;
1345 let entities = manager.query_all(|_| true);
1346 let mut latest = std::collections::BTreeMap::<String, (u64, Value, Value)>::new();
1347 for entity in entities {
1348 if let EntityData::Row(ref row) = entity.data {
1349 if let Some(ref named) = row.named {
1350 let key_val = named.get("key").cloned().unwrap_or(Value::Null);
1351 let val = named.get("value").cloned().unwrap_or(Value::Null);
1352 let key_str = match &key_val {
1353 Value::Text(s) => s.as_ref(),
1354 _ => continue,
1355 };
1356 if key_str == super::function_catalog::REGISTRY_KEY {
1357 continue;
1358 }
1359 if let Some(ref pfx) = prefix {
1360 if !key_str.starts_with(pfx.as_str()) {
1361 continue;
1362 }
1363 }
1364 let entity_id = entity.id.raw();
1365 match latest.get(key_str) {
1366 Some((prev_id, _, _)) if *prev_id > entity_id => {}
1367 _ => {
1368 latest.insert(key_str.to_string(), (entity_id, key_val, val));
1369 }
1370 }
1371 }
1372 }
1373 }
1374 if as_json {
1375 let mut tree = crate::serde_json::Value::Object(crate::serde_json::Map::new());
1376 for (key, (_, _, val)) in latest {
1377 let relative = match prefix {
1378 Some(pfx) if key == *pfx => "",
1379 Some(pfx) => key
1380 .strip_prefix(pfx.as_str())
1381 .and_then(|tail| tail.strip_prefix('.'))
1382 .unwrap_or(key.as_str()),
1383 None => key.as_str(),
1384 };
1385 insert_config_json_path(&mut tree, relative, val.to_json());
1386 }
1387 return Ok(show_config_json_result(query, mode, prefix, tree));
1388 }
1389 let mut result = UnifiedResult::with_columns(vec!["key".into(), "value".into()]);
1390 for (_, key_val, val) in latest.into_values() {
1391 let mut record = UnifiedRecord::new();
1392 record.set("key", key_val);
1393 record.set("value", val);
1394 result.push(record);
1395 }
1396 Ok(RuntimeQueryResult {
1397 query: query.to_string(),
1398 mode,
1399 statement: "show_config",
1400 engine: "runtime-config",
1401 result,
1402 affected_rows: 0,
1403 statement_type: "select",
1404 bookmark: None,
1405 notice: None,
1406 })
1407 }
1408 QueryExpr::SetTenant(ref value) => {
1414 self.check_set_tenant_privilege(value.as_deref())?;
1415 match value {
1416 Some(id) => set_current_tenant(id.clone()),
1417 None => clear_current_tenant(),
1418 }
1419 Ok(RuntimeQueryResult::ok_message(
1420 query.to_string(),
1421 &match value {
1422 Some(id) => format!("tenant set: {id}"),
1423 None => "tenant cleared".to_string(),
1424 },
1425 "set_tenant",
1426 ))
1427 }
1428 QueryExpr::ShowTenant => {
1429 let mut result = UnifiedResult::with_columns(vec!["tenant".into()]);
1430 let mut record = UnifiedRecord::new();
1431 record.set(
1432 "tenant",
1433 current_tenant().map(Value::text).unwrap_or(Value::Null),
1434 );
1435 result.push(record);
1436 Ok(RuntimeQueryResult {
1437 query: query.to_string(),
1438 mode,
1439 statement: "show_tenant",
1440 engine: "runtime-tenant",
1441 result,
1442 affected_rows: 0,
1443 statement_type: "select",
1444 bookmark: None,
1445 notice: None,
1446 })
1447 }
1448 QueryExpr::TransactionControl(ref ctl) => {
1460 use crate::storage::transaction::IsolationLevel;
1461 use reddb_rql::ast::TxnControl;
1462
1463 let conn_id = current_connection_id();
1468
1469 let (kind, msg) = match ctl {
1470 TxnControl::Begin(requested_isolation) => {
1471 let isolation = requested_isolation
1472 .map(IsolationLevel::from)
1473 .unwrap_or(IsolationLevel::SnapshotIsolation);
1474 let xid = self.inner.transaction_state.begin(conn_id, isolation);
1475 ("begin", format!("BEGIN — xid={xid} (snapshot isolation)"))
1476 }
1477 TxnControl::Commit => {
1478 let context = self.inner.transaction_state.commit(conn_id, |ctx| {
1479 let mut own_xids = std::collections::HashSet::new();
1480 own_xids.insert(ctx.xid);
1481 for (_, sub) in &ctx.savepoints {
1482 own_xids.insert(*sub);
1483 }
1484 for sub in &ctx.released_sub_xids {
1485 own_xids.insert(*sub);
1486 }
1487 if let Err(err) = self.check_table_row_write_conflicts(
1488 conn_id,
1489 &ctx.snapshot,
1490 &own_xids,
1491 ctx.isolation,
1492 ) {
1493 let undo = self.revive_pending_versioned_updates(conn_id);
1494 self.revive_pending_tombstones(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.check_queue_dedup_write_conflicts(
1503 conn_id,
1504 &ctx.snapshot,
1505 &own_xids,
1506 ) {
1507 let undo = self.revive_pending_versioned_updates(conn_id);
1508 self.revive_pending_tombstones(conn_id);
1509 self.discard_pending_queue_dedup(conn_id);
1510 self.discard_pending_kv_watch_events(conn_id);
1511 self.discard_pending_queue_wakes(conn_id);
1512 self.discard_pending_store_wal_actions(conn_id);
1513 self.release_pending_claim_locks(conn_id);
1514 undo?;
1515 return Err(err);
1516 }
1517 self.restore_pending_write_stamps(conn_id);
1518 if let Err(err) = self.flush_pending_store_wal_actions(conn_id) {
1519 let undo = self.revive_pending_versioned_updates(conn_id);
1520 self.revive_pending_tombstones(conn_id);
1521 self.discard_pending_queue_dedup(conn_id);
1522 self.discard_pending_kv_watch_events(conn_id);
1523 self.discard_pending_queue_wakes(conn_id);
1524 self.release_pending_claim_locks(conn_id);
1525 undo?;
1526 return Err(err);
1527 }
1528 Ok(())
1529 })?;
1530 match context {
1531 Some(ctx) => {
1532 self.finalize_pending_versioned_updates(conn_id);
1533 self.finalize_pending_tombstones(conn_id);
1534 self.finalize_pending_queue_dedup(conn_id);
1535 self.finalize_pending_kv_watch_events(conn_id);
1536 self.finalize_pending_queue_wakes(conn_id);
1537 self.release_pending_claim_locks(conn_id);
1538 ("commit", format!("COMMIT — xid={} committed", ctx.xid))
1539 }
1540 None => (
1541 "commit",
1542 "COMMIT outside transaction — no-op (autocommit)".to_string(),
1543 ),
1544 }
1545 }
1546 TxnControl::Rollback => {
1547 match self.inner.transaction_state.rollback(conn_id) {
1548 Some(ctx) => {
1549 let undo = self.revive_pending_versioned_updates(conn_id);
1553 self.revive_pending_tombstones(conn_id);
1554 self.discard_pending_queue_dedup(conn_id);
1555 self.discard_pending_kv_watch_events(conn_id);
1556 self.discard_pending_queue_wakes(conn_id);
1557 self.discard_pending_store_wal_actions(conn_id);
1558 self.release_pending_claim_locks(conn_id);
1559 undo?;
1560 ("rollback", format!("ROLLBACK — xid={} aborted", ctx.xid))
1561 }
1562 None => (
1563 "rollback",
1564 "ROLLBACK outside transaction — no-op (autocommit)".to_string(),
1565 ),
1566 }
1567 }
1568 TxnControl::Savepoint(name) => {
1575 match self.inner.transaction_state.savepoint(conn_id, name) {
1576 Some(sub) => ("savepoint", format!("SAVEPOINT {name} — sub_xid={sub}")),
1577 None => (
1578 "savepoint",
1579 "SAVEPOINT outside transaction — no-op".to_string(),
1580 ),
1581 }
1582 }
1583 TxnControl::ReleaseSavepoint(name) => {
1584 match self
1585 .inner
1586 .transaction_state
1587 .release_savepoint(conn_id, name)?
1588 {
1589 Some(released) => (
1590 "release_savepoint",
1591 format!("RELEASE SAVEPOINT {name} — {released} level(s)"),
1592 ),
1593 None => (
1594 "release_savepoint",
1595 "RELEASE outside transaction — no-op".to_string(),
1596 ),
1597 }
1598 }
1599 TxnControl::RollbackToSavepoint(name) => {
1600 match self
1601 .inner
1602 .transaction_state
1603 .rollback_to_savepoint(conn_id, name)?
1604 {
1605 Some(rollback) => {
1606 let reverted_updates = self.revive_versioned_updates_since(
1607 conn_id,
1608 rollback.savepoint_xid,
1609 );
1610 let revived =
1611 self.revive_tombstones_since(conn_id, rollback.savepoint_xid);
1612 let reverted_updates = reverted_updates?;
1613 (
1614 "rollback_to_savepoint",
1615 format!(
1616 "ROLLBACK TO SAVEPOINT {name} — aborted {} sub_xid(s), reverted {reverted_updates} update(s), revived {revived} tombstone(s)",
1617 rollback.aborted_xids.len(),
1618 ),
1619 )
1620 }
1621 None => (
1622 "rollback_to_savepoint",
1623 "ROLLBACK TO outside transaction — no-op".to_string(),
1624 ),
1625 }
1626 }
1627 };
1628 Ok(RuntimeQueryResult::ok_message(
1629 query.to_string(),
1630 &msg,
1631 kind,
1632 ))
1633 }
1634 QueryExpr::CreateSchema(ref q) => {
1647 let store = self.inner.db.store();
1648 let key = format!("schema.{}", q.name);
1649 if store.get_config(&key).is_some() {
1650 if q.if_not_exists {
1651 return Ok(RuntimeQueryResult::ok_message(
1652 query.to_string(),
1653 &format!("schema {} already exists — skipped", q.name),
1654 "create_schema",
1655 ));
1656 }
1657 return Err(RedDBError::Internal(format!(
1658 "schema {} already exists",
1659 q.name
1660 )));
1661 }
1662 store.set_config_tree(&key, &crate::serde_json::Value::Bool(true));
1663 Ok(RuntimeQueryResult::ok_message(
1664 query.to_string(),
1665 &format!("schema {} created", q.name),
1666 "create_schema",
1667 ))
1668 }
1669 QueryExpr::DropSchema(ref q) => {
1670 let store = self.inner.db.store();
1671 let key = format!("schema.{}", q.name);
1672 let existed = store.get_config(&key).is_some();
1673 if !existed && !q.if_exists {
1674 return Err(RedDBError::Internal(format!(
1675 "schema {} does not exist",
1676 q.name
1677 )));
1678 }
1679 store.set_config_tree(&key, &crate::serde_json::Value::Null);
1681 let suffix = if q.cascade {
1682 " (CASCADE accepted — tables untouched)"
1683 } else {
1684 ""
1685 };
1686 Ok(RuntimeQueryResult::ok_message(
1687 query.to_string(),
1688 &format!("schema {} dropped{}", q.name, suffix),
1689 "drop_schema",
1690 ))
1691 }
1692 QueryExpr::CreateSequence(ref q) => {
1693 let store = self.inner.db.store();
1694 let base = format!("sequence.{}", q.name);
1695 let start_key = format!("{base}.start");
1696 let incr_key = format!("{base}.increment");
1697 let curr_key = format!("{base}.current");
1698 if store.get_config(&start_key).is_some() {
1699 if q.if_not_exists {
1700 return Ok(RuntimeQueryResult::ok_message(
1701 query.to_string(),
1702 &format!("sequence {} already exists — skipped", q.name),
1703 "create_sequence",
1704 ));
1705 }
1706 return Err(RedDBError::Internal(format!(
1707 "sequence {} already exists",
1708 q.name
1709 )));
1710 }
1711 let initial_current = q.start - q.increment;
1714 store.set_config_tree(
1715 &start_key,
1716 &crate::serde_json::Value::Number(q.start as f64),
1717 );
1718 store.set_config_tree(
1719 &incr_key,
1720 &crate::serde_json::Value::Number(q.increment as f64),
1721 );
1722 store.set_config_tree(
1723 &curr_key,
1724 &crate::serde_json::Value::Number(initial_current as f64),
1725 );
1726 Ok(RuntimeQueryResult::ok_message(
1727 query.to_string(),
1728 &format!(
1729 "sequence {} created (start={}, increment={})",
1730 q.name, q.start, q.increment
1731 ),
1732 "create_sequence",
1733 ))
1734 }
1735 QueryExpr::DropSequence(ref q) => {
1736 let store = self.inner.db.store();
1737 let base = format!("sequence.{}", q.name);
1738 let existed = store.get_config(&format!("{base}.start")).is_some();
1739 if !existed && !q.if_exists {
1740 return Err(RedDBError::Internal(format!(
1741 "sequence {} does not exist",
1742 q.name
1743 )));
1744 }
1745 for k in ["start", "increment", "current"] {
1746 store.set_config_tree(&format!("{base}.{k}"), &crate::serde_json::Value::Null);
1747 }
1748 Ok(RuntimeQueryResult::ok_message(
1749 query.to_string(),
1750 &format!("sequence {} dropped", q.name),
1751 "drop_sequence",
1752 ))
1753 }
1754 QueryExpr::CreateView(ref q) => {
1764 let mut views = self.inner.views.write();
1765 if views.contains_key(&q.name) && !q.or_replace {
1766 if q.if_not_exists {
1767 return Ok(RuntimeQueryResult::ok_message(
1768 query.to_string(),
1769 &format!("view {} already exists — skipped", q.name),
1770 "create_view",
1771 ));
1772 }
1773 return Err(RedDBError::Internal(format!(
1774 "view {} already exists",
1775 q.name
1776 )));
1777 }
1778 views.insert(q.name.clone(), Arc::new(q.clone()));
1779 drop(views);
1780
1781 if q.materialized {
1783 use crate::storage::cache::result::{MaterializedViewDef, RefreshPolicy};
1784 let refresh = match q.refresh_every_ms {
1785 Some(ms) => RefreshPolicy::Periodic(std::time::Duration::from_millis(ms)),
1786 None => RefreshPolicy::Manual,
1787 };
1788 let dependencies = collect_table_refs(&q.query);
1789 let def = MaterializedViewDef {
1790 name: q.name.clone(),
1791 query: format!("<parsed view {}>", q.name),
1792 dependencies: dependencies.clone(),
1793 refresh,
1794 retention_duration_ms: q.retention_duration_ms,
1795 };
1796 self.inner.materialized_views.write().register(def);
1797
1798 let descriptor =
1804 crate::runtime::continuous_materialized_view::MaterializedViewDescriptor {
1805 name: q.name.clone(),
1806 source_sql: query.to_string(),
1807 source_collections: dependencies,
1808 refresh_every_ms: q.refresh_every_ms,
1809 retention_duration_ms: q.retention_duration_ms,
1810 };
1811 let store = self.inner.db.store();
1812 crate::runtime::continuous_materialized_view::persist_descriptor(
1813 store.as_ref(),
1814 &descriptor,
1815 )?;
1816
1817 self.ensure_materialized_view_backing(&q.name)?;
1824 }
1825 self.invalidate_plan_cache();
1830 self.invalidate_result_cache();
1831
1832 Ok(RuntimeQueryResult::ok_message(
1833 query.to_string(),
1834 &format!(
1835 "{}view {} created",
1836 if q.materialized { "materialized " } else { "" },
1837 q.name
1838 ),
1839 "create_view",
1840 ))
1841 }
1842 QueryExpr::DropView(ref q) => {
1843 let mut views = self.inner.views.write();
1844 let removed = views.remove(&q.name);
1845 let existed = removed.is_some();
1846 let removed_materialized =
1847 removed.as_ref().map(|v| v.materialized).unwrap_or(false);
1848 drop(views);
1849 if q.materialized || existed {
1850 self.inner.materialized_views.write().remove(&q.name);
1852 let store = self.inner.db.store();
1856 crate::runtime::continuous_materialized_view::remove_by_name(
1857 store.as_ref(),
1858 &q.name,
1859 )?;
1860 }
1861 if removed_materialized || q.materialized {
1865 self.drop_materialized_view_backing(&q.name)?;
1866 }
1867 self.invalidate_plan_cache();
1870 self.invalidate_result_cache();
1871 if !existed && !q.if_exists {
1872 return Err(RedDBError::Internal(format!(
1873 "view {} does not exist",
1874 q.name
1875 )));
1876 }
1877 self.invalidate_plan_cache();
1878 Ok(RuntimeQueryResult::ok_message(
1879 query.to_string(),
1880 &format!("view {} dropped", q.name),
1881 "drop_view",
1882 ))
1883 }
1884 QueryExpr::RefreshMaterializedView(ref q) => {
1885 let view = {
1888 let views = self.inner.views.read();
1889 views.get(&q.name).cloned()
1890 };
1891 let view = match view {
1892 Some(v) => v,
1893 None => {
1894 return Err(RedDBError::Internal(format!(
1895 "view {} does not exist",
1896 q.name
1897 )))
1898 }
1899 };
1900 if !view.materialized {
1901 return Err(RedDBError::Internal(format!(
1902 "view {} is not materialized — REFRESH requires \
1903 CREATE MATERIALIZED VIEW",
1904 q.name
1905 )));
1906 }
1907 let started = std::time::Instant::now();
1909 let now_ms = std::time::SystemTime::now()
1910 .duration_since(std::time::UNIX_EPOCH)
1911 .map(|d| d.as_millis() as u64)
1912 .unwrap_or(0);
1913 match self.execute_query_expr((*view.query).clone()) {
1914 Ok(inner_result) => {
1915 let entities =
1922 view_records_to_entities(&q.name, &inner_result.result.records);
1923 let row_count = entities.len() as u64;
1924 let store = self.inner.db.store();
1925 let serialized_records = match store.refresh_collection(&q.name, entities) {
1926 Ok(records) => records,
1927 Err(err) => {
1928 let duration_ms = started.elapsed().as_millis() as u64;
1929 let msg = err.to_string();
1930 self.inner
1931 .materialized_views
1932 .write()
1933 .record_refresh_failure(
1934 &q.name,
1935 msg.clone(),
1936 duration_ms,
1937 now_ms,
1938 );
1939 return Err(RedDBError::Internal(format!(
1940 "REFRESH MATERIALIZED VIEW {}: {msg}",
1941 q.name
1942 )));
1943 }
1944 };
1945
1946 if let Some(ref primary) = self.inner.db.replication {
1952 let lsn = self.inner.cdc.emit(
1953 crate::replication::cdc::ChangeOperation::Refresh,
1954 &q.name,
1955 0,
1956 "refresh",
1957 );
1958 self.invalidate_result_cache_for_table(&q.name);
1959 let timestamp = std::time::SystemTime::now()
1960 .duration_since(std::time::UNIX_EPOCH)
1961 .unwrap_or_default()
1962 .as_millis() as u64;
1963 let record = ChangeRecord::for_refresh(
1964 lsn,
1965 timestamp,
1966 q.name.clone(),
1967 serialized_records,
1968 )
1969 .with_term(self.current_replication_term());
1970 let encoded = record.encode();
1971 primary.append_logical_record(record.lsn, encoded);
1972 }
1973
1974 let duration_ms = started.elapsed().as_millis() as u64;
1975 let serialized = format!("{:?}", inner_result.result);
1976 self.inner
1977 .materialized_views
1978 .write()
1979 .record_refresh_success(
1980 &q.name,
1981 serialized.into_bytes(),
1982 row_count,
1983 duration_ms,
1984 now_ms,
1985 );
1986 self.invalidate_result_cache();
1991 Ok(RuntimeQueryResult::ok_message(
1992 query.to_string(),
1993 &format!("materialized view {} refreshed", q.name),
1994 "refresh_materialized_view",
1995 ))
1996 }
1997 Err(err) => {
1998 let duration_ms = started.elapsed().as_millis() as u64;
1999 let msg = err.to_string();
2000 self.inner
2001 .materialized_views
2002 .write()
2003 .record_refresh_failure(&q.name, msg.clone(), duration_ms, now_ms);
2004 Err(err)
2005 }
2006 }
2007 }
2008 QueryExpr::CreatePolicy(ref q) => {
2015 let key = (q.table.clone(), q.name.clone());
2016 self.inner
2017 .rls_policies
2018 .write()
2019 .insert(key, Arc::new(q.clone()));
2020 self.invalidate_plan_cache();
2021 self.invalidate_result_cache();
2022 self.schema_vocabulary_apply(
2026 crate::runtime::schema_vocabulary::DdlEvent::CreatePolicy {
2027 collection: q.table.clone(),
2028 policy: q.name.clone(),
2029 },
2030 );
2031 Ok(RuntimeQueryResult::ok_message(
2032 query.to_string(),
2033 &format!("policy {} on {} created", q.name, q.table),
2034 "create_policy",
2035 ))
2036 }
2037 QueryExpr::DropPolicy(ref q) => {
2038 let removed = self
2039 .inner
2040 .rls_policies
2041 .write()
2042 .remove(&(q.table.clone(), q.name.clone()))
2043 .is_some();
2044 if !removed && !q.if_exists {
2045 return Err(RedDBError::Internal(format!(
2046 "policy {} on {} does not exist",
2047 q.name, q.table
2048 )));
2049 }
2050 self.invalidate_plan_cache();
2051 self.invalidate_result_cache();
2052 self.schema_vocabulary_apply(
2055 crate::runtime::schema_vocabulary::DdlEvent::DropPolicy {
2056 collection: q.table.clone(),
2057 policy: q.name.clone(),
2058 },
2059 );
2060 Ok(RuntimeQueryResult::ok_message(
2061 query.to_string(),
2062 &format!("policy {} on {} dropped", q.name, q.table),
2063 "drop_policy",
2064 ))
2065 }
2066 QueryExpr::CreateServer(ref q) => {
2077 use crate::storage::fdw::FdwOptions;
2078 let registry = Arc::clone(&self.inner.foreign_tables);
2079 if registry.server(&q.name).is_some() {
2080 if q.if_not_exists {
2081 return Ok(RuntimeQueryResult::ok_message(
2082 query.to_string(),
2083 &format!("server {} already exists — skipped", q.name),
2084 "create_server",
2085 ));
2086 }
2087 return Err(RedDBError::Internal(format!(
2088 "server {} already exists",
2089 q.name
2090 )));
2091 }
2092 let mut opts = FdwOptions::new();
2093 for (k, v) in &q.options {
2094 opts.values.insert(k.clone(), v.clone());
2095 }
2096 registry
2097 .create_server(&q.name, &q.wrapper, opts)
2098 .map_err(|e| RedDBError::Internal(e.to_string()))?;
2099 Ok(RuntimeQueryResult::ok_message(
2100 query.to_string(),
2101 &format!("server {} created (wrapper {})", q.name, q.wrapper),
2102 "create_server",
2103 ))
2104 }
2105 QueryExpr::DropServer(ref q) => {
2106 let existed = self.inner.foreign_tables.drop_server(&q.name);
2107 if !existed && !q.if_exists {
2108 return Err(RedDBError::Internal(format!(
2109 "server {} does not exist",
2110 q.name
2111 )));
2112 }
2113 Ok(RuntimeQueryResult::ok_message(
2114 query.to_string(),
2115 &format!(
2116 "server {} dropped{}",
2117 q.name,
2118 if q.cascade { " (cascade)" } else { "" }
2119 ),
2120 "drop_server",
2121 ))
2122 }
2123 QueryExpr::CreateForeignTable(ref q) => {
2124 use crate::storage::fdw::{FdwOptions, ForeignColumn, ForeignTable};
2125 let registry = Arc::clone(&self.inner.foreign_tables);
2126 if registry.foreign_table(&q.name).is_some() {
2127 if q.if_not_exists {
2128 return Ok(RuntimeQueryResult::ok_message(
2129 query.to_string(),
2130 &format!("foreign table {} already exists — skipped", q.name),
2131 "create_foreign_table",
2132 ));
2133 }
2134 return Err(RedDBError::Internal(format!(
2135 "foreign table {} already exists",
2136 q.name
2137 )));
2138 }
2139 let mut opts = FdwOptions::new();
2140 for (k, v) in &q.options {
2141 opts.values.insert(k.clone(), v.clone());
2142 }
2143 let columns: Vec<ForeignColumn> = q
2144 .columns
2145 .iter()
2146 .map(|c| ForeignColumn {
2147 name: c.name.clone(),
2148 data_type: c.data_type.clone(),
2149 not_null: c.not_null,
2150 })
2151 .collect();
2152 registry
2153 .create_foreign_table(ForeignTable {
2154 name: q.name.clone(),
2155 server_name: q.server.clone(),
2156 columns,
2157 options: opts,
2158 })
2159 .map_err(|e| RedDBError::Internal(e.to_string()))?;
2160 self.invalidate_plan_cache();
2161 Ok(RuntimeQueryResult::ok_message(
2162 query.to_string(),
2163 &format!("foreign table {} created (server {})", q.name, q.server),
2164 "create_foreign_table",
2165 ))
2166 }
2167 QueryExpr::DropForeignTable(ref q) => {
2168 let existed = self.inner.foreign_tables.drop_foreign_table(&q.name);
2169 if !existed && !q.if_exists {
2170 return Err(RedDBError::Internal(format!(
2171 "foreign table {} does not exist",
2172 q.name
2173 )));
2174 }
2175 self.invalidate_plan_cache();
2176 Ok(RuntimeQueryResult::ok_message(
2177 query.to_string(),
2178 &format!("foreign table {} dropped", q.name),
2179 "drop_foreign_table",
2180 ))
2181 }
2182 QueryExpr::CopyFrom(ref q) => {
2188 use crate::storage::import::{CsvConfig, CsvImporter};
2189 self.check_copy_from_privilege(&q.table)?;
2190 confine_copy_from_path(&q.path)?;
2191 let store = self.inner.db.store();
2192 let cfg = CsvConfig {
2193 collection: q.table.clone(),
2194 has_header: q.has_header,
2195 delimiter: q.delimiter.map(|c| c as u8).unwrap_or(b','),
2196 ..CsvConfig::default()
2197 };
2198 let importer = CsvImporter::new(cfg);
2199 let stats = importer
2200 .import_file(&q.path, store.as_ref())
2201 .map_err(|e| RedDBError::Internal(format!("COPY failed: {e}")))?;
2202 self.note_table_write(&q.table);
2204 Ok(RuntimeQueryResult::ok_message(
2205 query.to_string(),
2206 &format!(
2207 "COPY imported {} rows into {} ({} errors skipped, {}ms)",
2208 stats.records_imported, q.table, stats.errors_skipped, stats.duration_ms
2209 ),
2210 "copy_from",
2211 ))
2212 }
2213 QueryExpr::MaintenanceCommand(ref cmd) => {
2229 use reddb_rql::ast::MaintenanceCommand as Mc;
2230 let store = self.inner.db.store();
2231 let (kind, msg) = match cmd {
2232 Mc::Analyze { target } => {
2233 let targets: Vec<String> = match target {
2234 Some(t) => vec![t.clone()],
2235 None => store.list_collections(),
2236 };
2237 for t in &targets {
2238 self.refresh_table_planner_stats(t);
2239 }
2240 (
2241 "analyze",
2242 format!("ANALYZE refreshed stats for {} table(s)", targets.len()),
2243 )
2244 }
2245 Mc::Vacuum { target, full } => {
2246 let targets: Vec<String> = match target {
2247 Some(t) => vec![t.clone()],
2248 None => store.list_collections(),
2249 };
2250 let cutoff_xid = self.mvcc_vacuum_cutoff_xid();
2251 let mut vacuum_stats =
2252 crate::storage::unified::store::MvccVacuumStats::default();
2253 for t in &targets {
2254 let stats = store.vacuum_mvcc_history(t, cutoff_xid).map_err(|e| {
2255 RedDBError::Internal(format!(
2256 "VACUUM MVCC history failed for {t}: {e}"
2257 ))
2258 })?;
2259 if stats.reclaimed_versions > 0 {
2260 self.rebuild_runtime_indexes_for_table(t)?;
2261 }
2262 vacuum_stats.add(&stats);
2263 }
2264 self.inner
2265 .transaction_state
2266 .snapshot_manager()
2267 .prune_aborted(cutoff_xid);
2268 for t in &targets {
2270 self.refresh_table_planner_stats(t);
2271 }
2272 let persisted = if *full {
2276 match store.persist() {
2277 Ok(()) => true,
2278 Err(e) => {
2279 return Err(RedDBError::Internal(format!(
2280 "VACUUM FULL persist failed: {e:?}"
2281 )));
2282 }
2283 }
2284 } else {
2285 false
2286 };
2287 self.invalidate_result_cache();
2289 (
2290 "vacuum",
2291 format!(
2292 "VACUUM{} processed {} table(s): scanned_versions={}, retained_versions={}, reclaimed_versions={}, retained_history_versions={}, reclaimed_history_versions={}, retained_tombstones={}, reclaimed_tombstones={}{}",
2293 if *full { " FULL" } else { "" },
2294 targets.len(),
2295 vacuum_stats.scanned_versions,
2296 vacuum_stats.retained_versions,
2297 vacuum_stats.reclaimed_versions,
2298 vacuum_stats.retained_history_versions,
2299 vacuum_stats.reclaimed_history_versions,
2300 vacuum_stats.retained_tombstones,
2301 vacuum_stats.reclaimed_tombstones,
2302 if persisted {
2303 " (pages flushed to disk)"
2304 } else {
2305 ""
2306 }
2307 ),
2308 )
2309 }
2310 };
2311 Ok(RuntimeQueryResult::ok_message(
2312 query.to_string(),
2313 &msg,
2314 kind,
2315 ))
2316 }
2317 QueryExpr::Grant(ref g) => self.execute_grant_statement(query, g),
2324 QueryExpr::Revoke(ref r) => self.execute_revoke_statement(query, r),
2325 QueryExpr::AlterUser(ref a) => self.execute_alter_user_statement(query, a),
2326 QueryExpr::CreateUser(ref u) => self.execute_create_user_statement(query, u),
2327 QueryExpr::CreateIamPolicy { ref id, ref json } => {
2328 self.execute_create_iam_policy(query, id, json)
2329 }
2330 QueryExpr::DropIamPolicy { ref id } => self.execute_drop_iam_policy(query, id),
2331 QueryExpr::AttachPolicy {
2332 ref policy_id,
2333 ref principal,
2334 } => self.execute_attach_policy(query, policy_id, principal),
2335 QueryExpr::DetachPolicy {
2336 ref policy_id,
2337 ref principal,
2338 } => self.execute_detach_policy(query, policy_id, principal),
2339 QueryExpr::ShowPolicies { ref filter } => {
2340 self.execute_show_policies(query, filter.as_ref())
2341 }
2342 QueryExpr::ShowEffectivePermissions {
2343 ref user,
2344 ref resource,
2345 } => self.execute_show_effective_permissions(query, user, resource.as_ref()),
2346 QueryExpr::SimulatePolicy {
2347 ref user,
2348 ref action,
2349 ref resource,
2350 } => self.execute_simulate_policy(query, user, action, resource),
2351 QueryExpr::LintPolicy { ref source } => self.execute_lint_policy(query, source),
2352 QueryExpr::MigratePolicyMode {
2353 ref target,
2354 dry_run,
2355 } => self.execute_migrate_policy_mode(query, target, dry_run),
2356 QueryExpr::CreateMigration(ref q) => self.execute_create_migration(query, q),
2357 QueryExpr::ApplyMigration(ref q) => self.execute_apply_migration(query, q),
2358 QueryExpr::RollbackMigration(ref q) => self.execute_rollback_migration(query, q),
2359 QueryExpr::ExplainMigration(ref q) => self.execute_explain_migration(query, q),
2360 _ => Err(RedDBError::Query(
2361 "unsupported command in runtime dispatcher".to_string(),
2362 )),
2363 };
2364
2365 if !control_event_specs.is_empty() {
2366 let (outcome, reason) = match &query_result {
2367 Ok(_) => (crate::runtime::control_events::Outcome::Allowed, None),
2368 Err(err) => (control_event_outcome_for_error(err), Some(err.to_string())),
2369 };
2370 for spec in &control_event_specs {
2371 self.emit_control_event(
2372 spec.kind,
2373 outcome,
2374 spec.action,
2375 spec.resource.clone(),
2376 reason.clone(),
2377 spec.fields.clone(),
2378 )?;
2379 }
2380 }
2381
2382 if let (Some(plan), Ok(result)) = (&query_audit_plan, &query_result) {
2383 self.emit_query_audit(
2384 query,
2385 plan,
2386 query_audit_started.elapsed().as_millis() as u64,
2387 result,
2388 );
2389 }
2390
2391 let mut query_result = query_result;
2395 if let Ok(ref mut result) = query_result {
2396 if result.statement_type == "select" {
2397 self.apply_secret_decryption(result);
2398 }
2399 }
2400
2401 if let Ok(ref result) = query_result {
2408 frame.write_result_cache(self, result, result_cache_scopes);
2409 }
2410
2411 query_result
2412 }
2413
2414 pub fn execute_query_expr(&self, expr: QueryExpr) -> RedDBResult<RuntimeQueryResult> {
2434 let frame = super::statement_frame::StatementExecutionFrame::build(
2435 self,
2436 super::statement_frame::StatementIdentity::Expr(&expr),
2437 )?;
2438 let _frame_guards = Box::new(frame.install(self));
2439 let expr = self.rewrite_view_refs(expr);
2443
2444 self.validate_model_operations_before_auth(&expr)?;
2445 if let Err(err) = self.check_query_privilege(&expr) {
2449 return Err(RedDBError::Query(format!("permission denied: {err}")));
2450 }
2451
2452 let statement = query_expr_name(&expr);
2453 let mode = detect_mode(statement);
2454 let query_str = statement;
2455
2456 let result = self.dispatch_expr(expr, query_str, mode)?;
2457 let mut r = result;
2458 if r.statement_type == "select" {
2459 self.apply_secret_decryption(&mut r);
2460 }
2461 Ok(r)
2462 }
2463
2464 pub(super) fn validate_model_operations_before_auth(
2465 &self,
2466 expr: &QueryExpr,
2467 ) -> RedDBResult<()> {
2468 use crate::catalog::CollectionModel;
2469 use crate::runtime::ddl::polymorphic_resolver;
2470 use reddb_rql::ast::KvCommand;
2471
2472 let system_schema_target = match expr {
2473 QueryExpr::DropTable(q) => Some(q.name.as_str()),
2474 QueryExpr::DropGraph(q) => Some(q.name.as_str()),
2475 QueryExpr::DropVector(q) => Some(q.name.as_str()),
2476 QueryExpr::DropDocument(q) => Some(q.name.as_str()),
2477 QueryExpr::DropKv(q) => Some(q.name.as_str()),
2478 QueryExpr::DropCollection(q) => Some(q.name.as_str()),
2479 QueryExpr::Truncate(q) => Some(q.name.as_str()),
2480 _ => None,
2481 };
2482 if system_schema_target.is_some_and(crate::runtime::impl_ddl::is_system_schema_name) {
2483 return Err(RedDBError::Query("system schema is read-only".to_string()));
2484 }
2485
2486 let expected = match expr {
2487 QueryExpr::DropTable(q) => Some((q.name.as_str(), CollectionModel::Table)),
2488 QueryExpr::DropGraph(q) => Some((q.name.as_str(), CollectionModel::Graph)),
2489 QueryExpr::DropVector(q) => Some((q.name.as_str(), CollectionModel::Vector)),
2490 QueryExpr::DropDocument(q) => Some((q.name.as_str(), CollectionModel::Document)),
2491 QueryExpr::DropKv(q) => Some((q.name.as_str(), q.model)),
2492 QueryExpr::DropCollection(q) => q.model.map(|model| (q.name.as_str(), model)),
2493 QueryExpr::Truncate(q) => q.model.map(|model| (q.name.as_str(), model)),
2494 QueryExpr::KvCommand(cmd) => {
2495 let (collection, model) = match cmd {
2496 KvCommand::Put {
2497 collection, model, ..
2498 }
2499 | KvCommand::Get {
2500 collection, model, ..
2501 }
2502 | KvCommand::Incr {
2503 collection, model, ..
2504 }
2505 | KvCommand::Cas {
2506 collection, model, ..
2507 }
2508 | KvCommand::List {
2509 collection, model, ..
2510 }
2511 | KvCommand::Delete {
2512 collection, model, ..
2513 } => (collection.as_str(), *model),
2514 KvCommand::Rotate { collection, .. }
2515 | KvCommand::History { collection, .. }
2516 | KvCommand::Purge { collection, .. } => {
2517 (collection.as_str(), CollectionModel::Vault)
2518 }
2519 KvCommand::InvalidateTags { collection, .. } => {
2520 (collection.as_str(), CollectionModel::Kv)
2521 }
2522 KvCommand::Watch {
2523 collection, model, ..
2524 } => (collection.as_str(), *model),
2525 KvCommand::Unseal { collection, .. } => {
2526 (collection.as_str(), CollectionModel::Vault)
2527 }
2528 };
2529 Some((collection, model))
2530 }
2531 QueryExpr::ConfigCommand(cmd) => {
2532 self.validate_config_command_before_auth(cmd)?;
2533 None
2534 }
2535 _ => None,
2536 };
2537
2538 let Some((name, expected_model)) = expected else {
2539 return Ok(());
2540 };
2541 let snapshot = self.inner.db.catalog_model_snapshot();
2542 let Some(actual_model) = snapshot
2543 .collections
2544 .iter()
2545 .find(|collection| collection.name == name)
2546 .map(|collection| collection.declared_model.unwrap_or(collection.model))
2547 else {
2548 return Ok(());
2549 };
2550 polymorphic_resolver::ensure_model_match(expected_model, actual_model)
2551 }
2552
2553 pub(super) fn rewrite_view_refs(&self, expr: QueryExpr) -> QueryExpr {
2558 if self.inner.views.read().is_empty() {
2560 return expr;
2561 }
2562 self.rewrite_view_refs_inner(expr)
2563 }
2564
2565 fn rewrite_view_refs_inner(&self, expr: QueryExpr) -> QueryExpr {
2566 use reddb_rql::ast::{Filter, TableSource};
2567 match expr {
2568 QueryExpr::Table(mut tq) => {
2569 if let Some(TableSource::Subquery(body)) = tq.source.take() {
2575 tq.source = Some(TableSource::Subquery(Box::new(
2576 self.rewrite_view_refs_inner(*body),
2577 )));
2578 return QueryExpr::Table(tq);
2579 }
2580
2581 let maybe_view = {
2585 let views = self.inner.views.read();
2586 views.get(&tq.table).cloned()
2587 };
2588 let Some(view) = maybe_view else {
2589 return QueryExpr::Table(tq);
2590 };
2591
2592 if view.materialized {
2598 return QueryExpr::Table(tq);
2599 }
2600
2601 let inner_expr = self.rewrite_view_refs_inner((*view.query).clone());
2605
2606 match inner_expr {
2614 QueryExpr::Table(mut inner_tq) => {
2615 if let Some(outer_filter) = tq.filter.take() {
2616 inner_tq.filter = Some(match inner_tq.filter.take() {
2617 Some(existing) => {
2618 Filter::And(Box::new(existing), Box::new(outer_filter))
2619 }
2620 None => outer_filter,
2621 });
2622 inner_tq.where_expr = inner_tq
2630 .filter
2631 .as_ref()
2632 .map(reddb_rql::sql_lowering::filter_to_expr);
2633 }
2634 if let Some(outer_limit) = tq.limit {
2635 inner_tq.limit = Some(match inner_tq.limit {
2636 Some(existing) => existing.min(outer_limit),
2637 None => outer_limit,
2638 });
2639 }
2640 if let Some(outer_offset) = tq.offset {
2641 inner_tq.offset = Some(match inner_tq.offset {
2642 Some(existing) => existing + outer_offset,
2643 None => outer_offset,
2644 });
2645 }
2646 QueryExpr::Table(inner_tq)
2647 }
2648 other => other,
2649 }
2650 }
2651 QueryExpr::Join(mut jq) => {
2652 jq.left = Box::new(self.rewrite_view_refs_inner(*jq.left));
2653 jq.right = Box::new(self.rewrite_view_refs_inner(*jq.right));
2654 QueryExpr::Join(jq)
2655 }
2656 other => other,
2659 }
2660 }
2661
2662 fn resolve_table_expr_subqueries(
2663 &self,
2664 mut table: TableQuery,
2665 frame: &dyn super::statement_frame::ReadFrame,
2666 ) -> RedDBResult<TableQuery> {
2667 match table.source.take() {
2674 Some(TableSource::Subquery(inner)) => {
2675 let inner = self.resolve_select_expr_subqueries(*inner, frame)?;
2676 table.source = Some(TableSource::Subquery(Box::new(inner)));
2677 }
2678 other => table.source = other,
2679 }
2680
2681 let outer_scopes = relation_scopes_for_query(&QueryExpr::Table(table.clone()));
2682 for item in &mut table.select_items {
2683 if let reddb_rql::ast::SelectItem::Expr { expr, .. } = item {
2684 *expr = self.resolve_expr_subqueries(expr.clone(), &outer_scopes, frame)?;
2685 }
2686 }
2687 if let Some(where_expr) = table.where_expr.take() {
2688 table.where_expr =
2689 Some(self.resolve_expr_subqueries(where_expr, &outer_scopes, frame)?);
2690 table.filter = None;
2691 }
2692 if let Some(having_expr) = table.having_expr.take() {
2693 table.having_expr =
2694 Some(self.resolve_expr_subqueries(having_expr, &outer_scopes, frame)?);
2695 table.having = None;
2696 }
2697 for expr in &mut table.group_by_exprs {
2698 *expr = self.resolve_expr_subqueries(expr.clone(), &outer_scopes, frame)?;
2699 }
2700 for clause in &mut table.order_by {
2701 if let Some(expr) = clause.expr.take() {
2702 clause.expr = Some(self.resolve_expr_subqueries(expr, &outer_scopes, frame)?);
2703 }
2704 }
2705 Ok(table)
2706 }
2707
2708 fn resolve_select_expr_subqueries(
2709 &self,
2710 expr: QueryExpr,
2711 frame: &dyn super::statement_frame::ReadFrame,
2712 ) -> RedDBResult<QueryExpr> {
2713 match expr {
2714 QueryExpr::Table(table) => self
2715 .resolve_table_expr_subqueries(table, frame)
2716 .map(QueryExpr::Table),
2717 QueryExpr::Join(mut join) => {
2718 join.left = Box::new(self.resolve_select_expr_subqueries(*join.left, frame)?);
2719 join.right = Box::new(self.resolve_select_expr_subqueries(*join.right, frame)?);
2720 Ok(QueryExpr::Join(join))
2721 }
2722 other => Ok(other),
2723 }
2724 }
2725
2726 fn resolve_expr_subqueries(
2727 &self,
2728 expr: reddb_rql::ast::Expr,
2729 outer_scopes: &[String],
2730 frame: &dyn super::statement_frame::ReadFrame,
2731 ) -> RedDBResult<reddb_rql::ast::Expr> {
2732 use reddb_rql::ast::Expr;
2733
2734 match expr {
2735 Expr::Subquery { query, span } => {
2736 let values = self.execute_expr_subquery_values(query, outer_scopes, frame)?;
2737 if values.len() > 1 {
2738 return Err(RedDBError::Query(
2739 "scalar subquery returned more than one row".to_string(),
2740 ));
2741 }
2742 Ok(Expr::Literal {
2743 value: values.into_iter().next().unwrap_or(Value::Null),
2744 span,
2745 })
2746 }
2747 Expr::BinaryOp { op, lhs, rhs, span } => Ok(Expr::BinaryOp {
2748 op,
2749 lhs: Box::new(self.resolve_expr_subqueries(*lhs, outer_scopes, frame)?),
2750 rhs: Box::new(self.resolve_expr_subqueries(*rhs, outer_scopes, frame)?),
2751 span,
2752 }),
2753 Expr::UnaryOp { op, operand, span } => Ok(Expr::UnaryOp {
2754 op,
2755 operand: Box::new(self.resolve_expr_subqueries(*operand, outer_scopes, frame)?),
2756 span,
2757 }),
2758 Expr::Cast {
2759 inner,
2760 target,
2761 span,
2762 } => Ok(Expr::Cast {
2763 inner: Box::new(self.resolve_expr_subqueries(*inner, outer_scopes, frame)?),
2764 target,
2765 span,
2766 }),
2767 Expr::FunctionCall { name, args, span } => {
2768 let args = args
2769 .into_iter()
2770 .map(|arg| self.resolve_expr_subqueries(arg, outer_scopes, frame))
2771 .collect::<RedDBResult<Vec<_>>>()?;
2772 Ok(Expr::FunctionCall { name, args, span })
2773 }
2774 Expr::Case {
2775 branches,
2776 else_,
2777 span,
2778 } => {
2779 let branches = branches
2780 .into_iter()
2781 .map(|(cond, value)| {
2782 Ok((
2783 self.resolve_expr_subqueries(cond, outer_scopes, frame)?,
2784 self.resolve_expr_subqueries(value, outer_scopes, frame)?,
2785 ))
2786 })
2787 .collect::<RedDBResult<Vec<_>>>()?;
2788 let else_ = else_
2789 .map(|expr| self.resolve_expr_subqueries(*expr, outer_scopes, frame))
2790 .transpose()?
2791 .map(Box::new);
2792 Ok(Expr::Case {
2793 branches,
2794 else_,
2795 span,
2796 })
2797 }
2798 Expr::IsNull {
2799 operand,
2800 negated,
2801 span,
2802 } => Ok(Expr::IsNull {
2803 operand: Box::new(self.resolve_expr_subqueries(*operand, outer_scopes, frame)?),
2804 negated,
2805 span,
2806 }),
2807 Expr::InList {
2808 target,
2809 values,
2810 negated,
2811 span,
2812 } => {
2813 let target =
2814 Box::new(self.resolve_expr_subqueries(*target, outer_scopes, frame)?);
2815 let mut resolved = Vec::new();
2816 for value in values {
2817 if let Expr::Subquery { query, .. } = value {
2818 resolved.extend(
2819 self.execute_expr_subquery_values(query, outer_scopes, frame)?
2820 .into_iter()
2821 .map(Expr::lit),
2822 );
2823 } else {
2824 resolved.push(self.resolve_expr_subqueries(value, outer_scopes, frame)?);
2825 }
2826 }
2827 Ok(Expr::InList {
2828 target,
2829 values: resolved,
2830 negated,
2831 span,
2832 })
2833 }
2834 Expr::Between {
2835 target,
2836 low,
2837 high,
2838 negated,
2839 span,
2840 } => Ok(Expr::Between {
2841 target: Box::new(self.resolve_expr_subqueries(*target, outer_scopes, frame)?),
2842 low: Box::new(self.resolve_expr_subqueries(*low, outer_scopes, frame)?),
2843 high: Box::new(self.resolve_expr_subqueries(*high, outer_scopes, frame)?),
2844 negated,
2845 span,
2846 }),
2847 other => Ok(other),
2848 }
2849 }
2850
2851 fn execute_expr_subquery_values(
2852 &self,
2853 subquery: reddb_rql::ast::ExprSubquery,
2854 outer_scopes: &[String],
2855 frame: &dyn super::statement_frame::ReadFrame,
2856 ) -> RedDBResult<Vec<Value>> {
2857 let query = *subquery.query;
2858 if query_references_outer_scope(&query, outer_scopes) {
2859 return Err(RedDBError::Query(
2860 "NOT_YET_SUPPORTED: correlated subqueries are not supported yet; track follow-up issue #470-correlated-subqueries".to_string(),
2861 ));
2862 }
2863 let query = self.rewrite_view_refs(query);
2864 let query = self.resolve_select_expr_subqueries(query, frame)?;
2865 let query = self.authorize_relational_select_expr(query, frame)?;
2866 let result = match query {
2867 QueryExpr::Table(table) => {
2868 execute_runtime_table_query(&self.inner.db, &table, Some(&self.inner.index_store))?
2869 }
2870 QueryExpr::Join(join) => execute_runtime_join_query(self, &join)?,
2871 other => {
2872 return Err(RedDBError::Query(format!(
2873 "expression subquery must be a SELECT query, got {}",
2874 query_expr_name(&other)
2875 )))
2876 }
2877 };
2878 first_column_values(result)
2879 }
2880
2881 fn execute_materialized_graph(
2882 &self,
2883 query: &str,
2884 mode: QueryMode,
2885 statement: &'static str,
2886 expr: &QueryExpr,
2887 ) -> RedDBResult<RuntimeQueryResult> {
2888 let (graph, node_properties, edge_properties) = self.materialize_graph_with_rls()?;
2896 let result =
2897 crate::storage::query::unified::UnifiedExecutor::execute_on_with_graph_properties_checked(
2898 &graph,
2899 expr,
2900 node_properties,
2901 edge_properties,
2902 super::function_budget::graph_work_check(),
2903 )
2904 .map_err(|err| RedDBError::Query(err.to_string()))?;
2905
2906 Ok(RuntimeQueryResult {
2907 query: query.to_string(),
2908 mode,
2909 statement,
2910 engine: "materialized-graph",
2911 result,
2912 affected_rows: 0,
2913 statement_type: "select",
2914 bookmark: None,
2915 notice: None,
2916 })
2917 }
2918
2919 fn dispatch_expr(
2920 &self,
2921 expr: QueryExpr,
2922 query_str: &str,
2923 mode: QueryMode,
2924 ) -> RedDBResult<RuntimeQueryResult> {
2925 let statement = query_expr_name(&expr);
2926 match expr {
2927 QueryExpr::Function(command) => self.execute_function(query_str, &command),
2928 QueryExpr::Graph(_) | QueryExpr::Path(_) => {
2929 self.execute_materialized_graph(query_str, mode, statement, &expr)
2930 }
2931 QueryExpr::Table(table) => {
2932 let scope = self.ai_scope();
2933 let table = self.resolve_table_expr_subqueries(
2934 table,
2935 &scope as &dyn super::statement_frame::ReadFrame,
2936 )?;
2937 if let Some(TableSource::Function {
2941 name,
2942 args,
2943 named_args,
2944 }) = table.source.clone()
2945 {
2946 return Ok(RuntimeQueryResult {
2947 query: query_str.to_string(),
2948 mode,
2949 statement,
2950 engine: "runtime-graph-tvf",
2951 result: self.execute_table_function(&name, &args, &named_args)?,
2952 affected_rows: 0,
2953 statement_type: "select",
2954 bookmark: None,
2955 notice: None,
2956 });
2957 }
2958 if let Some(TableSource::InlineGraphFunction {
2962 name,
2963 nodes,
2964 edges,
2965 named_args,
2966 }) = table.source.clone()
2967 {
2968 return Ok(RuntimeQueryResult {
2969 query: query_str.to_string(),
2970 mode,
2971 statement,
2972 engine: "runtime-graph-tvf-inline",
2973 result: self.execute_inline_graph_function(
2974 &name,
2975 &nodes,
2976 &edges,
2977 &named_args,
2978 )?,
2979 affected_rows: 0,
2980 statement_type: "select",
2981 bookmark: None,
2982 notice: None,
2983 });
2984 }
2985 if super::red_schema::is_virtual_table(&table.table) {
2986 return Ok(RuntimeQueryResult {
2987 query: query_str.to_string(),
2988 mode,
2989 statement,
2990 engine: "runtime-red-schema",
2991 result: super::red_schema::red_query(
2992 self,
2993 &table.table,
2994 &table,
2995 &scope as &dyn super::statement_frame::ReadFrame,
2996 )?,
2997 affected_rows: 0,
2998 statement_type: "select",
2999 bookmark: None,
3000 notice: None,
3001 });
3002 }
3003 if let Some(view_result) = self.try_resolve_analytics_view(
3005 &table,
3006 &scope as &dyn super::statement_frame::ReadFrame,
3007 )? {
3008 return Ok(RuntimeQueryResult {
3009 query: query_str.to_string(),
3010 mode,
3011 statement,
3012 engine: "runtime-graph-analytics-view",
3013 result: view_result,
3014 affected_rows: 0,
3015 statement_type: "select",
3016 bookmark: None,
3017 notice: None,
3018 });
3019 }
3020 let Some(table_with_rls) = self.authorize_relational_table_select(
3021 table,
3022 &scope as &dyn super::statement_frame::ReadFrame,
3023 )?
3024 else {
3025 return Ok(RuntimeQueryResult {
3026 query: query_str.to_string(),
3027 mode,
3028 statement,
3029 engine: "runtime-table-rls",
3030 result: crate::storage::query::unified::UnifiedResult::empty(),
3031 affected_rows: 0,
3032 statement_type: "select",
3033 bookmark: None,
3034 notice: None,
3035 });
3036 };
3037 Ok(RuntimeQueryResult {
3038 query: query_str.to_string(),
3039 mode,
3040 statement,
3041 engine: "runtime-table",
3042 result: execute_runtime_table_query(
3043 &self.inner.db,
3044 &table_with_rls,
3045 Some(&self.inner.index_store),
3046 )?,
3047 affected_rows: 0,
3048 statement_type: "select",
3049 bookmark: None,
3050 notice: None,
3051 })
3052 }
3053 QueryExpr::Join(join) => {
3054 let scope = self.ai_scope();
3055 let Some(join_with_rls) = self.authorize_relational_join_select(
3056 join,
3057 &scope as &dyn super::statement_frame::ReadFrame,
3058 )?
3059 else {
3060 return Ok(RuntimeQueryResult {
3061 query: query_str.to_string(),
3062 mode,
3063 statement,
3064 engine: "runtime-join-rls",
3065 result: crate::storage::query::unified::UnifiedResult::empty(),
3066 affected_rows: 0,
3067 statement_type: "select",
3068 bookmark: None,
3069 notice: None,
3070 });
3071 };
3072 Ok(RuntimeQueryResult {
3073 query: query_str.to_string(),
3074 mode,
3075 statement,
3076 engine: "runtime-join",
3077 result: execute_runtime_join_query(self, &join_with_rls)?,
3078 affected_rows: 0,
3079 statement_type: "select",
3080 bookmark: None,
3081 notice: None,
3082 })
3083 }
3084 QueryExpr::Vector(vector) => Ok(RuntimeQueryResult {
3085 query: query_str.to_string(),
3086 mode,
3087 statement,
3088 engine: "runtime-vector",
3089 result: execute_runtime_vector_query(self, &vector)?,
3090 affected_rows: 0,
3091 statement_type: "select",
3092 bookmark: None,
3093 notice: None,
3094 }),
3095 QueryExpr::Hybrid(hybrid) => Ok(RuntimeQueryResult {
3096 query: query_str.to_string(),
3097 mode,
3098 statement,
3099 engine: "runtime-hybrid",
3100 result: execute_runtime_hybrid_query(self, &hybrid)?,
3101 affected_rows: 0,
3102 statement_type: "select",
3103 bookmark: None,
3104 notice: None,
3105 }),
3106 QueryExpr::Insert(ref insert) if super::red_schema::is_virtual_table(&insert.table) => {
3107 Err(RedDBError::Query(
3108 super::red_schema::READ_ONLY_ERROR.to_string(),
3109 ))
3110 }
3111 QueryExpr::Update(ref update) if super::red_schema::is_virtual_table(&update.table) => {
3112 Err(RedDBError::Query(
3113 super::red_schema::READ_ONLY_ERROR.to_string(),
3114 ))
3115 }
3116 QueryExpr::Delete(ref delete) if super::red_schema::is_virtual_table(&delete.table) => {
3117 Err(RedDBError::Query(
3118 super::red_schema::READ_ONLY_ERROR.to_string(),
3119 ))
3120 }
3121 QueryExpr::Insert(ref insert) => self
3122 .with_deferred_store_wal_for_dml(self.insert_may_emit_events(insert), || {
3123 self.execute_insert(query_str, insert)
3124 }),
3125 QueryExpr::Update(ref update) => self
3126 .with_deferred_store_wal_for_dml(self.update_may_emit_events(update), || {
3127 self.execute_update(query_str, update)
3128 }),
3129 QueryExpr::Delete(ref delete) => self
3130 .with_deferred_store_wal_for_dml(self.delete_may_emit_events(delete), || {
3131 self.execute_delete(query_str, delete)
3132 }),
3133 QueryExpr::QueueCommand(ref cmd) => self.with_deferred_store_wal_if_transaction(|| {
3134 self.execute_queue_command(query_str, cmd)
3135 }),
3136 QueryExpr::KvCommand(ref cmd) => self.execute_kv_command(query_str, cmd),
3137 QueryExpr::SearchCommand(ref cmd) => self.execute_search_command(query_str, cmd),
3138 QueryExpr::Ask(ref ask) => self.execute_ask(query_str, ask),
3139 _ => Err(RedDBError::Query(format!(
3140 "prepared-statement execution does not support {statement} statements"
3141 ))),
3142 }
3143 }
3144
3145 fn execute_table_function(
3152 &self,
3153 name: &str,
3154 args: &[String],
3155 named_args: &[(String, f64)],
3156 ) -> RedDBResult<crate::storage::query::unified::UnifiedResult> {
3157 if name.eq_ignore_ascii_case("red.diff") {
3158 return self.execute_vcs_diff_tvf(args, named_args);
3159 }
3160 if !is_graph_tvf_name(name) {
3161 return Err(RedDBError::Query(format!("unknown table function: {name}")));
3162 }
3163 if args.len() != 1 {
3165 return Err(RedDBError::Query(format!(
3166 "table function '{name}' takes exactly 1 graph argument, got {}",
3167 args.len()
3168 )));
3169 }
3170
3171 let (nodes, edges) = self.materialize_whole_graph_abstract()?;
3176 self.dispatch_graph_algorithm(name, nodes, edges, named_args)
3177 }
3178
3179 pub(crate) fn revive_versioned_updates_since(
3180 &self,
3181 conn_id: u64,
3182 stamper_xid: u64,
3183 ) -> RedDBResult<usize> {
3184 let candidates = {
3187 let guard = self.inner.pending_versioned_updates.read();
3188 let Some(pending) = guard.get(&conn_id) else {
3189 return Ok(0);
3190 };
3191 pending
3192 .iter()
3193 .rev()
3194 .filter(|entry| entry.3 >= stamper_xid)
3195 .cloned()
3196 .collect::<Vec<_>>()
3197 };
3198 let mut reverted = 0usize;
3199 for candidate in candidates {
3200 let (collection, old_id, new_id, xid, previous_xmax) = &candidate;
3201 let topology_lock = self.index_store_ref().collection_topology_lock(collection);
3202 let topology_guard = topology_lock.read();
3203 let mut guard = self.inner.pending_versioned_updates.write();
3204 let Some(pending) = guard.get_mut(&conn_id) else {
3205 continue;
3206 };
3207 let Some(position) = pending.iter().rposition(|entry| entry == &candidate) else {
3210 continue;
3211 };
3212 self.revive_versioned_update(
3213 collection,
3214 *old_id,
3215 *new_id,
3216 *xid,
3217 *previous_xmax,
3218 topology_guard,
3219 )?;
3220 pending.remove(position);
3221 reverted += 1;
3222 if pending.is_empty() {
3223 guard.remove(&conn_id);
3224 }
3225 }
3226 Ok(reverted)
3227 }
3228
3229 fn check_explain_target_privilege(&self, inner_sql: &str) -> RedDBResult<()> {
3235 let parsed =
3236 reddb_rql::parser::parse(inner_sql).map_err(|e| RedDBError::Query(e.to_string()))?;
3237 self.check_query_privilege(&parsed.query)
3238 .map_err(|err| RedDBError::Query(format!("permission denied: {err}")))
3239 }
3240
3241 fn explain_as_rows(&self, raw_query: &str, inner_sql: &str) -> RedDBResult<RuntimeQueryResult> {
3249 self.check_explain_target_privilege(inner_sql)?;
3250 let explain = self.explain_query(inner_sql)?;
3251
3252 let mut columns = vec![
3253 "op".to_string(),
3254 "source".to_string(),
3255 "estimated_rows".to_string(),
3256 "estimated_cost".to_string(),
3257 "depth".to_string(),
3258 ];
3259
3260 let mut records: Vec<crate::storage::query::unified::UnifiedRecord> = Vec::new();
3261
3262 for name in &explain.cte_materializations {
3268 use std::sync::Arc;
3269 let mut rec = crate::storage::query::unified::UnifiedRecord::default();
3270 rec.set_arc(Arc::from("op"), Value::text("CteScan".to_string()));
3271 rec.set_arc(Arc::from("source"), Value::text(name.clone()));
3272 rec.set_arc(Arc::from("estimated_rows"), Value::Float(0.0));
3273 rec.set_arc(Arc::from("estimated_cost"), Value::Float(0.0));
3274 rec.set_arc(Arc::from("depth"), Value::Integer(0));
3275 records.push(rec);
3276 }
3277
3278 let vector_details = explain.statement == "vector";
3279 if vector_details {
3280 columns.extend(["access_path_reason".to_string(), "index_hint".to_string()]);
3281 }
3282 walk_plan_node(&explain.logical_plan.root, 0, &mut records, vector_details);
3283
3284 let result = crate::storage::query::unified::UnifiedResult {
3285 columns,
3286 records,
3287 stats: Default::default(),
3288 pre_serialized_json: None,
3289 };
3290
3291 Ok(RuntimeQueryResult {
3292 query: raw_query.to_string(),
3293 mode: explain.mode,
3294 statement: "explain",
3295 engine: "runtime-explain",
3296 result,
3297 affected_rows: 0,
3298 statement_type: "select",
3299 bookmark: None,
3300 notice: None,
3301 })
3302 }
3303
3304 fn explain_analyze_vector_as_rows(
3305 &self,
3306 raw_query: &str,
3307 expr: QueryExpr,
3308 ) -> RedDBResult<RuntimeQueryResult> {
3309 let QueryExpr::Vector(query) = &expr else {
3310 return Err(RedDBError::Internal(
3311 "expected vector analysis target".to_string(),
3312 ));
3313 };
3314 let source = query.collection.clone();
3315 let execution = self.execute_query_expr(expr)?;
3318 let stats = execution.result.stats;
3319 let vector = stats.vector.as_ref().ok_or_else(|| {
3320 RedDBError::Internal("vector executor did not provide measurements".to_string())
3321 })?;
3322 let fields = vec![
3323 ("op", Value::text(vector.access_path.clone())),
3324 ("source", Value::text(source)),
3325 ("index_used", Value::Boolean(vector.index_used)),
3326 ("mode_requested", Value::text(vector.mode_requested.clone())),
3327 ("mode_executed", Value::text(vector.mode_executed.clone())),
3328 (
3329 "fallback_reason",
3330 vector
3331 .fallback_reason
3332 .as_ref()
3333 .map_or(Value::Null, |reason| Value::text(reason.clone())),
3334 ),
3335 (
3336 "approximate_distance_evaluations",
3337 Value::UnsignedInteger(vector.approximate_distance_evaluations),
3338 ),
3339 (
3340 "peak_topk_entries",
3341 Value::UnsignedInteger(vector.peak_topk_entries),
3342 ),
3343 (
3344 "candidates_examined",
3345 Value::UnsignedInteger(vector.candidates_examined),
3346 ),
3347 (
3348 "metadata_rejected",
3349 Value::UnsignedInteger(vector.metadata_rejected),
3350 ),
3351 ("rls_rejected", Value::UnsignedInteger(vector.rls_rejected)),
3352 (
3353 "visibility_rejected",
3354 Value::UnsignedInteger(vector.visibility_rejected),
3355 ),
3356 (
3357 "exact_distance_evaluations",
3358 Value::UnsignedInteger(vector.exact_distance_evaluations),
3359 ),
3360 ("actual_rows", Value::UnsignedInteger(vector.rows_returned)),
3361 (
3362 "actual_ms",
3363 Value::Float(stats.exec_time_us as f64 / 1000.0),
3364 ),
3365 ("metrics_scope", Value::text("vector_pipeline")),
3366 (
3367 "operators",
3368 Value::Json(vector.operators_json().to_string().into_bytes()),
3369 ),
3370 ];
3371 let columns = fields.iter().map(|(name, _)| name.to_string()).collect();
3372 let mut record = crate::storage::query::unified::UnifiedRecord::default();
3373 for (name, value) in fields {
3374 record.set_owned(name.to_string(), value);
3375 }
3376 Ok(RuntimeQueryResult {
3377 query: raw_query.to_string(),
3378 mode: execution.mode,
3379 statement: "explain_analyze",
3380 engine: "runtime-explain-analyze",
3381 result: crate::storage::query::unified::UnifiedResult {
3382 columns,
3383 records: vec![record],
3384 stats,
3385 pre_serialized_json: None,
3386 },
3387 affected_rows: 0,
3388 statement_type: "select",
3389 bookmark: None,
3390 notice: None,
3391 })
3392 }
3393
3394 fn explain_analyze_as_rows(
3395 &self,
3396 raw_query: &str,
3397 inner_sql: &str,
3398 ) -> RedDBResult<RuntimeQueryResult> {
3399 if strip_keyword_ci(inner_sql.trim_start(), "VECTOR").is_some() {
3400 let expr = parse_multi(inner_sql).map_err(|err| RedDBError::Query(err.to_string()))?;
3401 if matches!(&expr, QueryExpr::Vector(_)) {
3402 return self.explain_analyze_vector_as_rows(raw_query, expr);
3403 }
3404 }
3405 if !starts_with_dml_keyword(inner_sql) {
3406 return Err(RedDBError::Query(
3407 "EXPLAIN ANALYZE currently supports INSERT, UPDATE, DELETE, and VECTOR SEARCH"
3408 .to_string(),
3409 ));
3410 }
3411
3412 self.check_explain_target_privilege(inner_sql)?;
3413 let explain = self.explain_query(inner_sql)?;
3414 let conn_id = current_connection_id();
3415 if self.inner.transaction_state.in_transaction(conn_id) {
3416 return Err(RedDBError::Query(
3417 "EXPLAIN ANALYZE requires no active transaction".to_string(),
3418 ));
3419 }
3420
3421 self.execute_query_inner("BEGIN")?;
3422 let started = std::time::Instant::now();
3423 let execution = self.execute_query_inner(inner_sql);
3424 let elapsed_ms = started.elapsed().as_secs_f64() * 1000.0;
3425 let rollback = self.execute_query_inner("ROLLBACK");
3426
3427 let affected_rows = match execution {
3428 Ok(result) => result.affected_rows,
3429 Err(err) => {
3430 rollback?;
3431 return Err(err);
3432 }
3433 };
3434 rollback?;
3435
3436 let columns = vec![
3437 "op".to_string(),
3438 "source".to_string(),
3439 "estimated_rows".to_string(),
3440 "estimated_cost".to_string(),
3441 "actual_rows".to_string(),
3442 "actual_ms".to_string(),
3443 "depth".to_string(),
3444 ];
3445 let mut records = Vec::new();
3446 walk_analyze_plan_node(
3447 &explain.logical_plan.root,
3448 0,
3449 affected_rows,
3450 elapsed_ms,
3451 &mut records,
3452 );
3453
3454 let result = crate::storage::query::unified::UnifiedResult {
3455 columns,
3456 records,
3457 stats: Default::default(),
3458 pre_serialized_json: None,
3459 };
3460
3461 Ok(RuntimeQueryResult {
3462 query: raw_query.to_string(),
3463 mode: explain.mode,
3464 statement: "explain_analyze",
3465 engine: "runtime-explain-analyze",
3466 result,
3467 affected_rows: 0,
3468 statement_type: "select",
3469 bookmark: None,
3470 notice: None,
3471 })
3472 }
3473}
3474
3475fn strip_explain_analyze_prefix(sql: &str) -> Option<&str> {
3476 let rest = strip_keyword_ci(sql.trim_start(), "EXPLAIN")?.trim_start();
3477 Some(strip_keyword_ci(rest, "ANALYZE")?.trim_start()).filter(|inner| !inner.is_empty())
3478}
3479
3480fn strip_keyword_ci<'a>(sql: &'a str, keyword: &str) -> Option<&'a str> {
3481 if sql.len() < keyword.len() {
3482 return None;
3483 }
3484 let (head, rest) = sql.split_at(keyword.len());
3485 if !head.eq_ignore_ascii_case(keyword) {
3486 return None;
3487 }
3488 if rest
3489 .chars()
3490 .next()
3491 .is_some_and(|ch| !ch.is_ascii_whitespace())
3492 {
3493 return None;
3494 }
3495 Some(rest)
3496}
3497
3498fn starts_with_dml_keyword(sql: &str) -> bool {
3499 let trimmed = sql.trim_start();
3500 let head_end = trimmed
3501 .find(|ch: char| ch.is_ascii_whitespace())
3502 .unwrap_or(trimmed.len());
3503 let head = &trimmed[..head_end];
3504 head.eq_ignore_ascii_case("INSERT")
3505 || head.eq_ignore_ascii_case("UPDATE")
3506 || head.eq_ignore_ascii_case("DELETE")
3507}
3508
3509fn walk_plan_node(
3510 node: &crate::storage::query::planner::CanonicalLogicalNode,
3511 depth: usize,
3512 out: &mut Vec<crate::storage::query::unified::UnifiedRecord>,
3513 vector_details: bool,
3514) {
3515 use std::sync::Arc;
3516 let mut rec = crate::storage::query::unified::UnifiedRecord::default();
3517 rec.set_arc(Arc::from("op"), Value::text(node.operator.clone()));
3518 rec.set_arc(
3519 Arc::from("source"),
3520 node.source.clone().map(Value::text).unwrap_or(Value::Null),
3521 );
3522 rec.set_arc(
3523 Arc::from("estimated_rows"),
3524 Value::Float(node.estimated_rows),
3525 );
3526 rec.set_arc(
3527 Arc::from("estimated_cost"),
3528 Value::Float(node.operator_cost),
3529 );
3530 rec.set_arc(Arc::from("depth"), Value::Integer(depth as i64));
3531 if vector_details {
3532 for name in ["access_path_reason", "index_hint"] {
3533 rec.set_owned(
3534 name.to_string(),
3535 node.details
3536 .get(name)
3537 .cloned()
3538 .map(Value::text)
3539 .unwrap_or(Value::Null),
3540 );
3541 }
3542 }
3543 out.push(rec);
3544 for child in &node.children {
3545 walk_plan_node(child, depth + 1, out, vector_details);
3546 }
3547}
3548
3549fn walk_analyze_plan_node(
3550 node: &crate::storage::query::planner::CanonicalLogicalNode,
3551 depth: usize,
3552 affected_rows: u64,
3553 elapsed_ms: f64,
3554 out: &mut Vec<crate::storage::query::unified::UnifiedRecord>,
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(
3572 Arc::from("actual_rows"),
3573 Value::UnsignedInteger(if depth == 0 { affected_rows } else { 0 }),
3574 );
3575 rec.set_arc(
3576 Arc::from("actual_ms"),
3577 Value::Float(if depth == 0 { elapsed_ms } else { 0.0 }),
3578 );
3579 rec.set_arc(Arc::from("depth"), Value::Integer(depth as i64));
3580 out.push(rec);
3581
3582 for child in &node.children {
3583 walk_analyze_plan_node(child, depth + 1, 0, 0.0, out);
3584 }
3585}
3586
3587#[cfg(test)]
3588mod inline_graph_tvf_tests {
3589 use super::*;
3590
3591 fn scopes_for(sql: &str) -> HashSet<String> {
3592 let expr = reddb_rql::parser::parse(sql).expect("parse").query;
3593 query_expr_result_cache_scopes(&expr)
3594 }
3595
3596 #[test]
3597 fn inline_tvf_cache_scopes_include_source_collections() {
3598 let scopes = scopes_for(
3602 "SELECT * FROM components(nodes => (SELECT id FROM hosts), edges => (SELECT src, dst FROM links))",
3603 );
3604 assert!(scopes.contains("hosts"), "nodes source scoped: {scopes:?}");
3605 assert!(scopes.contains("links"), "edges source scoped: {scopes:?}");
3606 }
3607
3608 #[test]
3609 fn graph_collection_tvf_cache_scope_is_graph_argument() {
3610 let scopes = scopes_for("SELECT * FROM components(g)");
3614 assert!(scopes.contains("g"), "collection form scoped: {scopes:?}");
3615 }
3616
3617 #[test]
3618 fn abstract_degree_centrality_counts_undirected_endpoints() {
3619 let nodes = vec!["a".to_string(), "b".to_string(), "c".to_string()];
3620 let edges = vec![
3621 ("a".to_string(), "b".to_string(), 1.0_f32),
3622 ("b".to_string(), "c".to_string(), 1.0_f32),
3623 ];
3624 let degrees = abstract_degree_centrality(&nodes, &edges);
3625 assert_eq!(
3626 degrees,
3627 vec![
3628 ("a".to_string(), 1),
3629 ("b".to_string(), 2),
3630 ("c".to_string(), 1),
3631 ]
3632 );
3633 }
3634}
3635
3636fn reject_engine_internal_vault_key(key: &str) -> RedDBResult<()> {
3643 let lowered = key.to_ascii_lowercase();
3644 let internal = lowered == "red.secret.aes_key"
3645 || lowered.starts_with("red.iam.")
3646 || lowered.starts_with("red.acl.")
3647 || lowered.starts_with("red.vault.");
3648 if internal {
3649 return Err(RedDBError::Query(format!(
3650 "`{key}` is an engine-internal vault key and cannot be set or deleted through SET/DELETE SECRET"
3651 )));
3652 }
3653 Ok(())
3654}
3655
3656fn confine_copy_from_path(path: &str) -> RedDBResult<()> {
3662 let Ok(root) = std::env::var("REDDB_COPY_IMPORT_ROOT") else {
3663 return Ok(());
3664 };
3665 let root = std::fs::canonicalize(&root).map_err(|e| {
3666 RedDBError::Query(format!(
3667 "REDDB_COPY_IMPORT_ROOT `{root}` is not usable: {e}"
3668 ))
3669 })?;
3670 let resolved = std::fs::canonicalize(path)
3671 .map_err(|e| RedDBError::Query(format!("COPY FROM `{path}`: {e}")))?;
3672 if !resolved.starts_with(&root) || !resolved.is_file() {
3673 return Err(RedDBError::Query(format!(
3674 "COPY FROM `{path}` is outside the configured import root"
3675 )));
3676 }
3677 Ok(())
3678}
3679
3680#[cfg(test)]
3681mod security_gate_tests {
3682 use super::*;
3683 use crate::auth::store::AuthStore;
3684 use crate::auth::{AuthConfig, Role};
3685
3686 struct IdentityGuard;
3689
3690 impl IdentityGuard {
3691 fn install(username: &str, role: Role) -> Self {
3692 set_current_auth_identity(username.to_string(), role);
3693 Self
3694 }
3695 }
3696
3697 impl Drop for IdentityGuard {
3698 fn drop(&mut self) {
3699 clear_current_auth_identity();
3700 }
3701 }
3702
3703 fn runtime_with_user(username: &str, role: Role) -> RedDBRuntime {
3704 let store = std::sync::Arc::new(AuthStore::new(AuthConfig {
3705 enabled: true,
3706 session_ttl_secs: 60,
3707 require_auth: true,
3708 auto_encrypt_storage: false,
3709 vault_enabled: false,
3710 cert: Default::default(),
3711 oauth: Default::default(),
3712 }));
3713 store.create_user(username, "pw", role).expect("user");
3714 let runtime =
3715 RedDBRuntime::with_options(crate::api::RedDBOptions::in_memory()).expect("runtime");
3716 runtime.set_auth_store(store);
3717 runtime
3718 }
3719
3720 fn runtime_with_store(username: &str, role: Role) -> (RedDBRuntime, std::sync::Arc<AuthStore>) {
3721 let store = std::sync::Arc::new(AuthStore::new(AuthConfig {
3722 enabled: true,
3723 session_ttl_secs: 60,
3724 require_auth: true,
3725 auto_encrypt_storage: false,
3726 vault_enabled: false,
3727 cert: Default::default(),
3728 oauth: Default::default(),
3729 }));
3730 store.create_user(username, "pw", role).expect("user");
3731 let runtime =
3732 RedDBRuntime::with_options(crate::api::RedDBOptions::in_memory()).expect("runtime");
3733 runtime.set_auth_store(std::sync::Arc::clone(&store));
3734 (runtime, store)
3735 }
3736
3737 #[test]
3738 fn nested_reads_need_select_on_the_inner_table() {
3739 use crate::auth::privileges::{Action, GrantPrincipal, Resource};
3740 use crate::auth::UserId;
3741 let (runtime, store) = runtime_with_store("ro", Role::Read);
3742 runtime
3743 .execute_query("CREATE TABLE gate_open (a INTEGER)")
3744 .expect("create");
3745 runtime
3746 .execute_query("CREATE TABLE gate_closed (b INTEGER)")
3747 .expect("create");
3748 store
3751 .grant(
3752 &UserId::from_parts(None, "admin"),
3753 Role::Admin,
3754 GrantPrincipal::User(UserId::from_parts(None, "ro")),
3755 Resource::table_from_name("gate_open"),
3756 vec![Action::Select],
3757 false,
3758 None,
3759 )
3760 .expect("grant");
3761 let _identity = IdentityGuard::install("ro", Role::Read);
3762 runtime
3763 .execute_query("SELECT * FROM (SELECT a FROM gate_open) s")
3764 .expect("derived table over a granted table is fine");
3765 for sql in [
3768 "SELECT * FROM (SELECT b FROM gate_closed) s",
3769 "SELECT a FROM gate_open WHERE a IN (SELECT b FROM gate_closed)",
3770 ] {
3771 let err = runtime
3772 .execute_query(sql)
3773 .expect_err("nested read of an ungranted table must be refused");
3774 assert!(
3775 err.to_string().contains("permission denied"),
3776 "{sql}: unexpected error: {err}"
3777 );
3778 }
3779 }
3780
3781 #[test]
3782 fn policy_listings_are_self_or_policy_management() {
3783 let (runtime, store) = runtime_with_store("ro", Role::Read);
3784 store.create_user("other", "pw", Role::Read).expect("user");
3785 let _identity = IdentityGuard::install("ro", Role::Read);
3786 runtime
3787 .execute_query("SHOW EFFECTIVE PERMISSIONS FOR ro")
3788 .expect("a principal may inspect its own permissions");
3789 let err = runtime
3790 .execute_query("SHOW EFFECTIVE PERMISSIONS FOR other")
3791 .expect_err("inspecting another principal is policy management");
3792 assert!(
3793 err.to_string().contains("permission denied"),
3794 "unexpected error: {err}"
3795 );
3796 }
3797
3798 #[test]
3799 fn secret_columns_stay_masked_for_readers_without_secret_read() {
3800 use reddb_types::Value;
3801 let (runtime, store) = runtime_with_store("ro", Role::Read);
3802 store.create_user("root", "pw", Role::Admin).expect("admin");
3803 store.ensure_vault_secret_key();
3804 runtime
3805 .execute_query(
3806 "INSERT INTO creds_gate (name, token) VALUES ('stripe', SECRET('sk_live_abc'))",
3807 )
3808 .expect("insert");
3809
3810 let token_for = |username: &str, role: Role| {
3811 let _identity = IdentityGuard::install(username, role);
3812 let result = runtime
3813 .execute_query("SELECT name, token FROM creds_gate")
3814 .expect("select");
3815 result.result.records[0]
3816 .get("token")
3817 .cloned()
3818 .expect("token column")
3819 };
3820 assert!(
3823 matches!(token_for("ro", Role::Read), Value::Secret(_)),
3824 "a read-role principal must see the column masked"
3825 );
3826 assert_eq!(
3827 token_for("root", Role::Admin),
3828 Value::text("sk_live_abc".to_string())
3829 );
3830 }
3831
3832 #[test]
3833 fn store_wide_operations_need_the_admin_tier() {
3834 let runtime = runtime_with_user("bob", Role::Write);
3835 let _identity = IdentityGuard::install("bob", Role::Write);
3836 for sql in ["VACUUM", "SCRUB", "SHOW SECRETS"] {
3839 let err = runtime
3840 .execute_query(sql)
3841 .expect_err("store-wide operation must be refused for a non-admin");
3842 assert!(
3843 err.to_string().contains("permission denied"),
3844 "{sql}: unexpected error: {err}"
3845 );
3846 }
3847 }
3848
3849 #[test]
3850 fn explain_needs_the_privilege_of_the_statement_it_plans() {
3851 let runtime = runtime_with_user("ro", Role::Read);
3852 let _identity = IdentityGuard::install("ro", Role::Read);
3853 runtime
3854 .execute_query("EXPLAIN SELECT 1 AS one")
3855 .expect("planning a read is a read");
3856 let err = runtime
3859 .execute_query("EXPLAIN INSERT INTO gate_t (a) VALUES (1)")
3860 .expect_err("planning a write needs the write privilege");
3861 assert!(
3862 err.to_string().contains("permission denied"),
3863 "unexpected EXPLAIN error: {err}"
3864 );
3865 }
3866
3867 #[test]
3868 fn copy_from_a_server_path_is_admin_only() {
3869 let runtime = runtime_with_user("bob", Role::Write);
3870 let _identity = IdentityGuard::install("bob", Role::Write);
3871 let err = runtime
3875 .execute_query("COPY leak FROM '/etc/hostname'")
3876 .expect_err("COPY FROM must be refused for a non-admin");
3877 assert!(
3878 err.to_string().contains("permission denied"),
3879 "unexpected COPY error: {err}"
3880 );
3881 }
3882
3883 #[test]
3884 fn vcs_history_rewrites_are_admin_only() {
3885 let runtime = runtime_with_user("bob", Role::Write);
3886 let _identity = IdentityGuard::install("bob", Role::Write);
3887 let err = runtime
3890 .execute_query("RESET HARD TO 'deadbeef'")
3891 .expect_err("RESET must be refused for a non-admin");
3892 assert!(
3893 err.to_string().contains("permission denied"),
3894 "unexpected RESET error: {err}"
3895 );
3896 }
3897
3898 #[test]
3899 fn ai_provider_config_writes_are_admin_only() {
3900 let runtime = runtime_with_user("bob", Role::Write);
3901 let _identity = IdentityGuard::install("bob", Role::Write);
3902 let err = runtime
3905 .execute_query(
3906 "SET CONFIG red.config.ai.providers.openai.tokens.default.secret_ref = 'red.secret.aes_key'",
3907 )
3908 .expect_err("AI provider config must be refused for a non-admin");
3909 assert!(
3910 err.to_string().contains("permission denied"),
3911 "unexpected SET CONFIG error: {err}"
3912 );
3913 }
3914
3915 #[test]
3916 fn engine_internal_vault_keys_are_not_writable_through_set_secret() {
3917 let runtime = runtime_with_user("root", Role::Admin);
3918 let _identity = IdentityGuard::install("root", Role::Admin);
3919 for key in [
3923 "red.iam.policies",
3924 "red.iam.attachments.users",
3925 "red.secret.aes_key",
3926 ] {
3927 let err = runtime
3928 .execute_query(&format!("SET SECRET {key} = 'x'"))
3929 .expect_err("engine-internal vault key must be refused");
3930 assert!(
3931 err.to_string().contains("engine-internal"),
3932 "unexpected error for {key}: {err}"
3933 );
3934 }
3935 }
3936}