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