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_with(conn_id, name, |rollback| {
1594 self.rollback_vault_savepoint(conn_id, rollback.savepoint_xid);
1595 let reverted_updates = self.revive_versioned_updates_since(
1596 conn_id,
1597 rollback.savepoint_xid,
1598 );
1599 let revived =
1600 self.revive_tombstones_since(conn_id, rollback.savepoint_xid);
1601 reverted_updates.map(|updates| (updates, revived))
1602 })?
1603 {
1604 Some((rollback, (reverted_updates, revived))) => {
1605 (
1606 "rollback_to_savepoint",
1607 format!(
1608 "ROLLBACK TO SAVEPOINT {name} — aborted {} sub_xid(s), reverted {reverted_updates} update(s), revived {revived} tombstone(s)",
1609 rollback.aborted_xids.len(),
1610 ),
1611 )
1612 }
1613 None => (
1614 "rollback_to_savepoint",
1615 "ROLLBACK TO outside transaction — no-op".to_string(),
1616 ),
1617 }
1618 }
1619 };
1620 Ok(RuntimeQueryResult::ok_message(
1621 query.to_string(),
1622 &msg,
1623 kind,
1624 ))
1625 }
1626 QueryExpr::CreateSchema(ref q) => {
1639 let store = self.inner.db.store();
1640 let key = format!("schema.{}", q.name);
1641 if store.get_config(&key).is_some() {
1642 if q.if_not_exists {
1643 return Ok(RuntimeQueryResult::ok_message(
1644 query.to_string(),
1645 &format!("schema {} already exists — skipped", q.name),
1646 "create_schema",
1647 ));
1648 }
1649 return Err(RedDBError::Internal(format!(
1650 "schema {} already exists",
1651 q.name
1652 )));
1653 }
1654 store.set_config_tree(&key, &crate::serde_json::Value::Bool(true));
1655 Ok(RuntimeQueryResult::ok_message(
1656 query.to_string(),
1657 &format!("schema {} created", q.name),
1658 "create_schema",
1659 ))
1660 }
1661 QueryExpr::DropSchema(ref q) => {
1662 let store = self.inner.db.store();
1663 let key = format!("schema.{}", q.name);
1664 let existed = store.get_config(&key).is_some();
1665 if !existed && !q.if_exists {
1666 return Err(RedDBError::Internal(format!(
1667 "schema {} does not exist",
1668 q.name
1669 )));
1670 }
1671 store.set_config_tree(&key, &crate::serde_json::Value::Null);
1673 let suffix = if q.cascade {
1674 " (CASCADE accepted — tables untouched)"
1675 } else {
1676 ""
1677 };
1678 Ok(RuntimeQueryResult::ok_message(
1679 query.to_string(),
1680 &format!("schema {} dropped{}", q.name, suffix),
1681 "drop_schema",
1682 ))
1683 }
1684 QueryExpr::CreateSequence(ref q) => {
1685 let store = self.inner.db.store();
1686 let base = format!("sequence.{}", q.name);
1687 let start_key = format!("{base}.start");
1688 let incr_key = format!("{base}.increment");
1689 let curr_key = format!("{base}.current");
1690 if store.get_config(&start_key).is_some() {
1691 if q.if_not_exists {
1692 return Ok(RuntimeQueryResult::ok_message(
1693 query.to_string(),
1694 &format!("sequence {} already exists — skipped", q.name),
1695 "create_sequence",
1696 ));
1697 }
1698 return Err(RedDBError::Internal(format!(
1699 "sequence {} already exists",
1700 q.name
1701 )));
1702 }
1703 let initial_current = q.start - q.increment;
1706 store.set_config_tree(
1707 &start_key,
1708 &crate::serde_json::Value::Number(q.start as f64),
1709 );
1710 store.set_config_tree(
1711 &incr_key,
1712 &crate::serde_json::Value::Number(q.increment as f64),
1713 );
1714 store.set_config_tree(
1715 &curr_key,
1716 &crate::serde_json::Value::Number(initial_current as f64),
1717 );
1718 Ok(RuntimeQueryResult::ok_message(
1719 query.to_string(),
1720 &format!(
1721 "sequence {} created (start={}, increment={})",
1722 q.name, q.start, q.increment
1723 ),
1724 "create_sequence",
1725 ))
1726 }
1727 QueryExpr::DropSequence(ref q) => {
1728 let store = self.inner.db.store();
1729 let base = format!("sequence.{}", q.name);
1730 let existed = store.get_config(&format!("{base}.start")).is_some();
1731 if !existed && !q.if_exists {
1732 return Err(RedDBError::Internal(format!(
1733 "sequence {} does not exist",
1734 q.name
1735 )));
1736 }
1737 for k in ["start", "increment", "current"] {
1738 store.set_config_tree(&format!("{base}.{k}"), &crate::serde_json::Value::Null);
1739 }
1740 Ok(RuntimeQueryResult::ok_message(
1741 query.to_string(),
1742 &format!("sequence {} dropped", q.name),
1743 "drop_sequence",
1744 ))
1745 }
1746 QueryExpr::CreateView(ref q) => {
1756 let mut views = self.inner.views.write();
1757 if views.contains_key(&q.name) && !q.or_replace {
1758 if q.if_not_exists {
1759 return Ok(RuntimeQueryResult::ok_message(
1760 query.to_string(),
1761 &format!("view {} already exists — skipped", q.name),
1762 "create_view",
1763 ));
1764 }
1765 return Err(RedDBError::Internal(format!(
1766 "view {} already exists",
1767 q.name
1768 )));
1769 }
1770 views.insert(q.name.clone(), Arc::new(q.clone()));
1771 drop(views);
1772
1773 if q.materialized {
1775 use crate::storage::cache::result::{MaterializedViewDef, RefreshPolicy};
1776 let refresh = match q.refresh_every_ms {
1777 Some(ms) => RefreshPolicy::Periodic(std::time::Duration::from_millis(ms)),
1778 None => RefreshPolicy::Manual,
1779 };
1780 let dependencies = collect_table_refs(&q.query);
1781 let def = MaterializedViewDef {
1782 name: q.name.clone(),
1783 query: format!("<parsed view {}>", q.name),
1784 dependencies: dependencies.clone(),
1785 refresh,
1786 retention_duration_ms: q.retention_duration_ms,
1787 };
1788 self.inner.materialized_views.write().register(def);
1789
1790 let descriptor =
1796 crate::runtime::continuous_materialized_view::MaterializedViewDescriptor {
1797 name: q.name.clone(),
1798 source_sql: query.to_string(),
1799 source_collections: dependencies,
1800 refresh_every_ms: q.refresh_every_ms,
1801 retention_duration_ms: q.retention_duration_ms,
1802 };
1803 let store = self.inner.db.store();
1804 crate::runtime::continuous_materialized_view::persist_descriptor(
1805 store.as_ref(),
1806 &descriptor,
1807 )?;
1808
1809 self.ensure_materialized_view_backing(&q.name)?;
1816 }
1817 self.invalidate_plan_cache();
1822 self.invalidate_result_cache();
1823
1824 Ok(RuntimeQueryResult::ok_message(
1825 query.to_string(),
1826 &format!(
1827 "{}view {} created",
1828 if q.materialized { "materialized " } else { "" },
1829 q.name
1830 ),
1831 "create_view",
1832 ))
1833 }
1834 QueryExpr::DropView(ref q) => {
1835 let mut views = self.inner.views.write();
1836 let removed = views.remove(&q.name);
1837 let existed = removed.is_some();
1838 let removed_materialized =
1839 removed.as_ref().map(|v| v.materialized).unwrap_or(false);
1840 drop(views);
1841 if q.materialized || existed {
1842 self.inner.materialized_views.write().remove(&q.name);
1844 let store = self.inner.db.store();
1848 crate::runtime::continuous_materialized_view::remove_by_name(
1849 store.as_ref(),
1850 &q.name,
1851 )?;
1852 }
1853 if removed_materialized || q.materialized {
1857 self.drop_materialized_view_backing(&q.name)?;
1858 }
1859 self.invalidate_plan_cache();
1862 self.invalidate_result_cache();
1863 if !existed && !q.if_exists {
1864 return Err(RedDBError::Internal(format!(
1865 "view {} does not exist",
1866 q.name
1867 )));
1868 }
1869 self.invalidate_plan_cache();
1870 Ok(RuntimeQueryResult::ok_message(
1871 query.to_string(),
1872 &format!("view {} dropped", q.name),
1873 "drop_view",
1874 ))
1875 }
1876 QueryExpr::RefreshMaterializedView(ref q) => {
1877 let view = {
1880 let views = self.inner.views.read();
1881 views.get(&q.name).cloned()
1882 };
1883 let view = match view {
1884 Some(v) => v,
1885 None => {
1886 return Err(RedDBError::Internal(format!(
1887 "view {} does not exist",
1888 q.name
1889 )))
1890 }
1891 };
1892 if !view.materialized {
1893 return Err(RedDBError::Internal(format!(
1894 "view {} is not materialized — REFRESH requires \
1895 CREATE MATERIALIZED VIEW",
1896 q.name
1897 )));
1898 }
1899 let started = std::time::Instant::now();
1901 let now_ms = std::time::SystemTime::now()
1902 .duration_since(std::time::UNIX_EPOCH)
1903 .map(|d| d.as_millis() as u64)
1904 .unwrap_or(0);
1905 match self.execute_query_expr((*view.query).clone()) {
1906 Ok(inner_result) => {
1907 let entities =
1914 view_records_to_entities(&q.name, &inner_result.result.records);
1915 let row_count = entities.len() as u64;
1916 let store = self.inner.db.store();
1917 let serialized_records = match store.refresh_collection(&q.name, entities) {
1918 Ok(records) => records,
1919 Err(err) => {
1920 let duration_ms = started.elapsed().as_millis() as u64;
1921 let msg = err.to_string();
1922 self.inner
1923 .materialized_views
1924 .write()
1925 .record_refresh_failure(
1926 &q.name,
1927 msg.clone(),
1928 duration_ms,
1929 now_ms,
1930 );
1931 return Err(RedDBError::Internal(format!(
1932 "REFRESH MATERIALIZED VIEW {}: {msg}",
1933 q.name
1934 )));
1935 }
1936 };
1937
1938 if let Some(ref primary) = self.inner.db.replication {
1944 let lsn = self.inner.cdc.emit(
1945 crate::replication::cdc::ChangeOperation::Refresh,
1946 &q.name,
1947 0,
1948 "refresh",
1949 );
1950 self.invalidate_result_cache_for_table(&q.name);
1951 let timestamp = std::time::SystemTime::now()
1952 .duration_since(std::time::UNIX_EPOCH)
1953 .unwrap_or_default()
1954 .as_millis() as u64;
1955 let record = ChangeRecord::for_refresh(
1956 lsn,
1957 timestamp,
1958 q.name.clone(),
1959 serialized_records,
1960 )
1961 .with_term(self.current_replication_term());
1962 let encoded = record.encode();
1963 primary.append_logical_record(record.lsn, encoded);
1964 }
1965
1966 let duration_ms = started.elapsed().as_millis() as u64;
1967 let serialized = format!("{:?}", inner_result.result);
1968 self.inner
1969 .materialized_views
1970 .write()
1971 .record_refresh_success(
1972 &q.name,
1973 serialized.into_bytes(),
1974 row_count,
1975 duration_ms,
1976 now_ms,
1977 );
1978 self.invalidate_result_cache();
1983 Ok(RuntimeQueryResult::ok_message(
1984 query.to_string(),
1985 &format!("materialized view {} refreshed", q.name),
1986 "refresh_materialized_view",
1987 ))
1988 }
1989 Err(err) => {
1990 let duration_ms = started.elapsed().as_millis() as u64;
1991 let msg = err.to_string();
1992 self.inner
1993 .materialized_views
1994 .write()
1995 .record_refresh_failure(&q.name, msg.clone(), duration_ms, now_ms);
1996 Err(err)
1997 }
1998 }
1999 }
2000 QueryExpr::CreatePolicy(ref q) => {
2007 let key = (q.table.clone(), q.name.clone());
2008 self.inner
2009 .rls_policies
2010 .write()
2011 .insert(key, Arc::new(q.clone()));
2012 self.invalidate_plan_cache();
2013 self.invalidate_result_cache();
2014 self.schema_vocabulary_apply(
2018 crate::runtime::schema_vocabulary::DdlEvent::CreatePolicy {
2019 collection: q.table.clone(),
2020 policy: q.name.clone(),
2021 },
2022 );
2023 Ok(RuntimeQueryResult::ok_message(
2024 query.to_string(),
2025 &format!("policy {} on {} created", q.name, q.table),
2026 "create_policy",
2027 ))
2028 }
2029 QueryExpr::DropPolicy(ref q) => {
2030 let removed = self
2031 .inner
2032 .rls_policies
2033 .write()
2034 .remove(&(q.table.clone(), q.name.clone()))
2035 .is_some();
2036 if !removed && !q.if_exists {
2037 return Err(RedDBError::Internal(format!(
2038 "policy {} on {} does not exist",
2039 q.name, q.table
2040 )));
2041 }
2042 self.invalidate_plan_cache();
2043 self.invalidate_result_cache();
2044 self.schema_vocabulary_apply(
2047 crate::runtime::schema_vocabulary::DdlEvent::DropPolicy {
2048 collection: q.table.clone(),
2049 policy: q.name.clone(),
2050 },
2051 );
2052 Ok(RuntimeQueryResult::ok_message(
2053 query.to_string(),
2054 &format!("policy {} on {} dropped", q.name, q.table),
2055 "drop_policy",
2056 ))
2057 }
2058 QueryExpr::CreateServer(ref q) => {
2069 use crate::storage::fdw::FdwOptions;
2070 let registry = Arc::clone(&self.inner.foreign_tables);
2071 if registry.server(&q.name).is_some() {
2072 if q.if_not_exists {
2073 return Ok(RuntimeQueryResult::ok_message(
2074 query.to_string(),
2075 &format!("server {} already exists — skipped", q.name),
2076 "create_server",
2077 ));
2078 }
2079 return Err(RedDBError::Internal(format!(
2080 "server {} already exists",
2081 q.name
2082 )));
2083 }
2084 let mut opts = FdwOptions::new();
2085 for (k, v) in &q.options {
2086 opts.values.insert(k.clone(), v.clone());
2087 }
2088 registry
2089 .create_server(&q.name, &q.wrapper, opts)
2090 .map_err(|e| RedDBError::Internal(e.to_string()))?;
2091 Ok(RuntimeQueryResult::ok_message(
2092 query.to_string(),
2093 &format!("server {} created (wrapper {})", q.name, q.wrapper),
2094 "create_server",
2095 ))
2096 }
2097 QueryExpr::DropServer(ref q) => {
2098 let existed = self.inner.foreign_tables.drop_server(&q.name);
2099 if !existed && !q.if_exists {
2100 return Err(RedDBError::Internal(format!(
2101 "server {} does not exist",
2102 q.name
2103 )));
2104 }
2105 Ok(RuntimeQueryResult::ok_message(
2106 query.to_string(),
2107 &format!(
2108 "server {} dropped{}",
2109 q.name,
2110 if q.cascade { " (cascade)" } else { "" }
2111 ),
2112 "drop_server",
2113 ))
2114 }
2115 QueryExpr::CreateForeignTable(ref q) => {
2116 use crate::storage::fdw::{FdwOptions, ForeignColumn, ForeignTable};
2117 let registry = Arc::clone(&self.inner.foreign_tables);
2118 if registry.foreign_table(&q.name).is_some() {
2119 if q.if_not_exists {
2120 return Ok(RuntimeQueryResult::ok_message(
2121 query.to_string(),
2122 &format!("foreign table {} already exists — skipped", q.name),
2123 "create_foreign_table",
2124 ));
2125 }
2126 return Err(RedDBError::Internal(format!(
2127 "foreign table {} already exists",
2128 q.name
2129 )));
2130 }
2131 let mut opts = FdwOptions::new();
2132 for (k, v) in &q.options {
2133 opts.values.insert(k.clone(), v.clone());
2134 }
2135 let columns: Vec<ForeignColumn> = q
2136 .columns
2137 .iter()
2138 .map(|c| ForeignColumn {
2139 name: c.name.clone(),
2140 data_type: c.data_type.clone(),
2141 not_null: c.not_null,
2142 })
2143 .collect();
2144 registry
2145 .create_foreign_table(ForeignTable {
2146 name: q.name.clone(),
2147 server_name: q.server.clone(),
2148 columns,
2149 options: opts,
2150 })
2151 .map_err(|e| RedDBError::Internal(e.to_string()))?;
2152 self.invalidate_plan_cache();
2153 Ok(RuntimeQueryResult::ok_message(
2154 query.to_string(),
2155 &format!("foreign table {} created (server {})", q.name, q.server),
2156 "create_foreign_table",
2157 ))
2158 }
2159 QueryExpr::DropForeignTable(ref q) => {
2160 let existed = self.inner.foreign_tables.drop_foreign_table(&q.name);
2161 if !existed && !q.if_exists {
2162 return Err(RedDBError::Internal(format!(
2163 "foreign table {} does not exist",
2164 q.name
2165 )));
2166 }
2167 self.invalidate_plan_cache();
2168 Ok(RuntimeQueryResult::ok_message(
2169 query.to_string(),
2170 &format!("foreign table {} dropped", q.name),
2171 "drop_foreign_table",
2172 ))
2173 }
2174 QueryExpr::CopyFrom(ref q) => {
2180 use crate::storage::import::{CsvConfig, CsvImporter};
2181 self.check_copy_from_privilege(&q.table)?;
2182 confine_copy_from_path(&q.path)?;
2183 let store = self.inner.db.store();
2184 let cfg = CsvConfig {
2185 collection: q.table.clone(),
2186 has_header: q.has_header,
2187 delimiter: q.delimiter.map(|c| c as u8).unwrap_or(b','),
2188 ..CsvConfig::default()
2189 };
2190 let importer = CsvImporter::new(cfg);
2191 let stats = importer
2192 .import_file(&q.path, store.as_ref())
2193 .map_err(|e| RedDBError::Internal(format!("COPY failed: {e}")))?;
2194 self.note_table_write(&q.table);
2196 Ok(RuntimeQueryResult::ok_message(
2197 query.to_string(),
2198 &format!(
2199 "COPY imported {} rows into {} ({} errors skipped, {}ms)",
2200 stats.records_imported, q.table, stats.errors_skipped, stats.duration_ms
2201 ),
2202 "copy_from",
2203 ))
2204 }
2205 QueryExpr::MaintenanceCommand(ref cmd) => {
2221 use reddb_rql::ast::MaintenanceCommand as Mc;
2222 let store = self.inner.db.store();
2223 let (kind, msg) = match cmd {
2224 Mc::Analyze { target } => {
2225 let targets: Vec<String> = match target {
2226 Some(t) => vec![t.clone()],
2227 None => store.list_collections(),
2228 };
2229 for t in &targets {
2230 self.refresh_table_planner_stats(t);
2231 }
2232 (
2233 "analyze",
2234 format!("ANALYZE refreshed stats for {} table(s)", targets.len()),
2235 )
2236 }
2237 Mc::Vacuum { target, full } => {
2238 let targets: Vec<String> = match target {
2239 Some(t) => vec![t.clone()],
2240 None => store.list_collections(),
2241 };
2242 let cutoff_xid = self.mvcc_vacuum_cutoff_xid();
2243 let mut vacuum_stats =
2244 crate::storage::unified::store::MvccVacuumStats::default();
2245 for t in &targets {
2246 let stats = store.vacuum_mvcc_history(t, cutoff_xid).map_err(|e| {
2247 RedDBError::Internal(format!(
2248 "VACUUM MVCC history failed for {t}: {e}"
2249 ))
2250 })?;
2251 if stats.reclaimed_versions > 0 {
2252 self.rebuild_runtime_indexes_for_table(t)?;
2253 }
2254 vacuum_stats.add(&stats);
2255 }
2256 for t in &targets {
2261 self.refresh_table_planner_stats(t);
2262 }
2263 let persisted = if *full {
2267 match store.persist() {
2268 Ok(()) => true,
2269 Err(e) => {
2270 return Err(RedDBError::Internal(format!(
2271 "VACUUM FULL persist failed: {e:?}"
2272 )));
2273 }
2274 }
2275 } else {
2276 false
2277 };
2278 self.invalidate_result_cache();
2280 (
2281 "vacuum",
2282 format!(
2283 "VACUUM{} processed {} table(s): scanned_versions={}, retained_versions={}, reclaimed_versions={}, retained_history_versions={}, reclaimed_history_versions={}, retained_tombstones={}, reclaimed_tombstones={}{}",
2284 if *full { " FULL" } else { "" },
2285 targets.len(),
2286 vacuum_stats.scanned_versions,
2287 vacuum_stats.retained_versions,
2288 vacuum_stats.reclaimed_versions,
2289 vacuum_stats.retained_history_versions,
2290 vacuum_stats.reclaimed_history_versions,
2291 vacuum_stats.retained_tombstones,
2292 vacuum_stats.reclaimed_tombstones,
2293 if persisted {
2294 " (pages flushed to disk)"
2295 } else {
2296 ""
2297 }
2298 ),
2299 )
2300 }
2301 };
2302 Ok(RuntimeQueryResult::ok_message(
2303 query.to_string(),
2304 &msg,
2305 kind,
2306 ))
2307 }
2308 QueryExpr::Grant(ref g) => self.execute_grant_statement(query, g),
2315 QueryExpr::Revoke(ref r) => self.execute_revoke_statement(query, r),
2316 QueryExpr::AlterUser(ref a) => self.execute_alter_user_statement(query, a),
2317 QueryExpr::CreateUser(ref u) => self.execute_create_user_statement(query, u),
2318 QueryExpr::CreateIamPolicy { ref id, ref json } => {
2319 self.execute_create_iam_policy(query, id, json)
2320 }
2321 QueryExpr::DropIamPolicy { ref id } => self.execute_drop_iam_policy(query, id),
2322 QueryExpr::AttachPolicy {
2323 ref policy_id,
2324 ref principal,
2325 } => self.execute_attach_policy(query, policy_id, principal),
2326 QueryExpr::DetachPolicy {
2327 ref policy_id,
2328 ref principal,
2329 } => self.execute_detach_policy(query, policy_id, principal),
2330 QueryExpr::ShowPolicies { ref filter } => {
2331 self.execute_show_policies(query, filter.as_ref())
2332 }
2333 QueryExpr::ShowEffectivePermissions {
2334 ref user,
2335 ref resource,
2336 } => self.execute_show_effective_permissions(query, user, resource.as_ref()),
2337 QueryExpr::SimulatePolicy {
2338 ref user,
2339 ref action,
2340 ref resource,
2341 } => self.execute_simulate_policy(query, user, action, resource),
2342 QueryExpr::LintPolicy { ref source } => self.execute_lint_policy(query, source),
2343 QueryExpr::MigratePolicyMode {
2344 ref target,
2345 dry_run,
2346 } => self.execute_migrate_policy_mode(query, target, dry_run),
2347 QueryExpr::CreateMigration(ref q) => self.execute_create_migration(query, q),
2348 QueryExpr::ApplyMigration(ref q) => self.execute_apply_migration(query, q),
2349 QueryExpr::RollbackMigration(ref q) => self.execute_rollback_migration(query, q),
2350 QueryExpr::ExplainMigration(ref q) => self.execute_explain_migration(query, q),
2351 _ => Err(RedDBError::Query(
2352 "unsupported command in runtime dispatcher".to_string(),
2353 )),
2354 };
2355
2356 if _query_budget.is_some() {
2357 super::query_budget::charge(0)?;
2358 }
2359 if !control_event_specs.is_empty() {
2360 let (outcome, reason) = match &query_result {
2361 Ok(_) => (crate::runtime::control_events::Outcome::Allowed, None),
2362 Err(err) => (control_event_outcome_for_error(err), Some(err.to_string())),
2363 };
2364 for spec in &control_event_specs {
2365 self.emit_control_event(
2366 spec.kind,
2367 outcome,
2368 spec.action,
2369 spec.resource.clone(),
2370 reason.clone(),
2371 spec.fields.clone(),
2372 )?;
2373 }
2374 }
2375
2376 if let (Some(plan), Ok(result)) = (&query_audit_plan, &query_result) {
2377 self.emit_query_audit(
2378 query,
2379 plan,
2380 query_audit_started.elapsed().as_millis() as u64,
2381 result,
2382 );
2383 }
2384
2385 if let Ok(ref result) = query_result {
2392 frame.write_result_cache(self, result, result_cache_scopes);
2393 }
2394
2395 query_result
2396 }
2397
2398 pub fn execute_query_expr(&self, expr: QueryExpr) -> RedDBResult<RuntimeQueryResult> {
2418 let frame = super::statement_frame::StatementExecutionFrame::build(
2419 self,
2420 super::statement_frame::StatementIdentity::Expr(&expr),
2421 )?;
2422 let _frame_guards = Box::new(frame.install(self));
2423 let expr = self.rewrite_view_refs(expr);
2427
2428 self.validate_model_operations_before_auth(&expr)?;
2429 if let Err(err) = self.check_query_privilege(&expr) {
2433 return Err(RedDBError::Query(format!("permission denied: {err}")));
2434 }
2435
2436 frame.release_snapshot_for_maintenance(&expr);
2437 let statement = query_expr_name(&expr);
2438 let mode = detect_mode(statement);
2439 let query_str = statement;
2440
2441 self.dispatch_expr(expr, query_str, mode)
2442 }
2443
2444 pub(super) fn validate_model_operations_before_auth(
2445 &self,
2446 expr: &QueryExpr,
2447 ) -> RedDBResult<()> {
2448 self.validate_vault_table_access(expr)?;
2449 use crate::catalog::CollectionModel;
2450 use crate::runtime::ddl::polymorphic_resolver;
2451 use reddb_rql::ast::KvCommand;
2452
2453 let system_schema_target = match expr {
2454 QueryExpr::DropTable(q) => Some(q.name.as_str()),
2455 QueryExpr::DropGraph(q) => Some(q.name.as_str()),
2456 QueryExpr::DropVector(q) => Some(q.name.as_str()),
2457 QueryExpr::DropDocument(q) => Some(q.name.as_str()),
2458 QueryExpr::DropKv(q) => Some(q.name.as_str()),
2459 QueryExpr::DropCollection(q) => Some(q.name.as_str()),
2460 QueryExpr::Truncate(q) => Some(q.name.as_str()),
2461 _ => None,
2462 };
2463 if system_schema_target.is_some_and(crate::runtime::impl_ddl::is_system_schema_name) {
2464 return Err(RedDBError::Query("system schema is read-only".to_string()));
2465 }
2466
2467 let expected = match expr {
2468 QueryExpr::DropTable(q) => Some((q.name.as_str(), CollectionModel::Table)),
2469 QueryExpr::DropGraph(q) => Some((q.name.as_str(), CollectionModel::Graph)),
2470 QueryExpr::DropVector(q) => Some((q.name.as_str(), CollectionModel::Vector)),
2471 QueryExpr::DropDocument(q) => Some((q.name.as_str(), CollectionModel::Document)),
2472 QueryExpr::DropKv(q) => Some((q.name.as_str(), q.model)),
2473 QueryExpr::DropCollection(q) => q.model.map(|model| (q.name.as_str(), model)),
2474 QueryExpr::Truncate(q) => q.model.map(|model| (q.name.as_str(), model)),
2475 QueryExpr::KvCommand(cmd) => {
2476 let (collection, model) = match cmd {
2477 KvCommand::Put {
2478 collection, model, ..
2479 }
2480 | KvCommand::Get {
2481 collection, model, ..
2482 }
2483 | KvCommand::Incr {
2484 collection, model, ..
2485 }
2486 | KvCommand::Cas {
2487 collection, model, ..
2488 }
2489 | KvCommand::List {
2490 collection, model, ..
2491 }
2492 | KvCommand::Delete {
2493 collection, model, ..
2494 } => (collection.as_str(), *model),
2495 KvCommand::Rotate { collection, .. }
2496 | KvCommand::History { collection, .. }
2497 | KvCommand::Purge { collection, .. } => {
2498 (collection.as_str(), CollectionModel::Vault)
2499 }
2500 KvCommand::InvalidateTags { collection, .. } => {
2501 (collection.as_str(), CollectionModel::Kv)
2502 }
2503 KvCommand::Watch {
2504 collection, model, ..
2505 } => (collection.as_str(), *model),
2506 KvCommand::Reveal { collection, .. } => {
2507 (collection.as_str(), CollectionModel::Vault)
2508 }
2509 };
2510 Some((collection, model))
2511 }
2512 QueryExpr::ConfigCommand(cmd) => {
2513 self.validate_config_command_before_auth(cmd)?;
2514 None
2515 }
2516 _ => None,
2517 };
2518
2519 let Some((name, expected_model)) = expected else {
2520 return Ok(());
2521 };
2522 if let Some(contract) = self.inner.db.collection_contract_arc(name) {
2527 if self.inner.db.store().get_collection(name).is_some() {
2528 return polymorphic_resolver::ensure_model_match(
2529 expected_model,
2530 contract.declared_model,
2531 );
2532 }
2533 }
2534 let snapshot = self.inner.db.catalog_model_snapshot();
2535 let Some(actual_model) = snapshot
2536 .collections
2537 .iter()
2538 .find(|collection| collection.name == name)
2539 .map(|collection| collection.declared_model.unwrap_or(collection.model))
2540 else {
2541 return Ok(());
2542 };
2543 polymorphic_resolver::ensure_model_match(expected_model, actual_model)
2544 }
2545
2546 pub(super) fn rewrite_view_refs(&self, expr: QueryExpr) -> QueryExpr {
2551 if self.inner.views.read().is_empty() {
2553 return expr;
2554 }
2555 self.rewrite_view_refs_inner(expr)
2556 }
2557
2558 fn rewrite_view_refs_inner(&self, expr: QueryExpr) -> QueryExpr {
2559 use reddb_rql::ast::{Filter, TableSource};
2560 match expr {
2561 QueryExpr::Table(mut tq) => {
2562 if let Some(TableSource::Subquery(body)) = tq.source.take() {
2568 tq.source = Some(TableSource::Subquery(Box::new(
2569 self.rewrite_view_refs_inner(*body),
2570 )));
2571 return QueryExpr::Table(tq);
2572 }
2573
2574 let maybe_view = {
2578 let views = self.inner.views.read();
2579 views.get(&tq.table).cloned()
2580 };
2581 let Some(view) = maybe_view else {
2582 return QueryExpr::Table(tq);
2583 };
2584
2585 if view.materialized {
2591 return QueryExpr::Table(tq);
2592 }
2593
2594 let inner_expr = self.rewrite_view_refs_inner((*view.query).clone());
2598
2599 match inner_expr {
2607 QueryExpr::Table(mut inner_tq) => {
2608 if let Some(outer_filter) = tq.filter.take() {
2609 inner_tq.filter = Some(match inner_tq.filter.take() {
2610 Some(existing) => {
2611 Filter::And(Box::new(existing), Box::new(outer_filter))
2612 }
2613 None => outer_filter,
2614 });
2615 inner_tq.where_expr = inner_tq
2623 .filter
2624 .as_ref()
2625 .map(reddb_rql::sql_lowering::filter_to_expr);
2626 }
2627 if let Some(outer_limit) = tq.limit {
2628 inner_tq.limit = Some(match inner_tq.limit {
2629 Some(existing) => existing.min(outer_limit),
2630 None => outer_limit,
2631 });
2632 }
2633 if let Some(outer_offset) = tq.offset {
2634 inner_tq.offset = Some(match inner_tq.offset {
2635 Some(existing) => existing + outer_offset,
2636 None => outer_offset,
2637 });
2638 }
2639 QueryExpr::Table(inner_tq)
2640 }
2641 other => other,
2642 }
2643 }
2644 QueryExpr::Join(mut jq) => {
2645 jq.left = Box::new(self.rewrite_view_refs_inner(*jq.left));
2646 jq.right = Box::new(self.rewrite_view_refs_inner(*jq.right));
2647 QueryExpr::Join(jq)
2648 }
2649 other => other,
2652 }
2653 }
2654
2655 fn resolve_table_expr_subqueries(
2656 &self,
2657 mut table: TableQuery,
2658 frame: &dyn super::statement_frame::ReadFrame,
2659 ) -> RedDBResult<TableQuery> {
2660 match table.source.take() {
2667 Some(TableSource::Subquery(inner)) => {
2668 let inner = self.resolve_select_expr_subqueries(*inner, frame)?;
2669 table.source = Some(TableSource::Subquery(Box::new(inner)));
2670 }
2671 other => table.source = other,
2672 }
2673
2674 let outer_scopes = relation_scopes_for_query(&QueryExpr::Table(table.clone()));
2675 for item in &mut table.select_items {
2676 if let reddb_rql::ast::SelectItem::Expr { expr, .. } = item {
2677 *expr = self.resolve_expr_subqueries(expr.clone(), &outer_scopes, frame)?;
2678 }
2679 }
2680 if let Some(where_expr) = table.where_expr.take() {
2681 table.where_expr =
2682 Some(self.resolve_expr_subqueries(where_expr, &outer_scopes, frame)?);
2683 table.filter = None;
2684 }
2685 if let Some(having_expr) = table.having_expr.take() {
2686 table.having_expr =
2687 Some(self.resolve_expr_subqueries(having_expr, &outer_scopes, frame)?);
2688 table.having = None;
2689 }
2690 for expr in &mut table.group_by_exprs {
2691 *expr = self.resolve_expr_subqueries(expr.clone(), &outer_scopes, frame)?;
2692 }
2693 for clause in &mut table.order_by {
2694 if let Some(expr) = clause.expr.take() {
2695 clause.expr = Some(self.resolve_expr_subqueries(expr, &outer_scopes, frame)?);
2696 }
2697 }
2698 Ok(table)
2699 }
2700
2701 fn resolve_select_expr_subqueries(
2702 &self,
2703 expr: QueryExpr,
2704 frame: &dyn super::statement_frame::ReadFrame,
2705 ) -> RedDBResult<QueryExpr> {
2706 match expr {
2707 QueryExpr::Table(table) => self
2708 .resolve_table_expr_subqueries(table, frame)
2709 .map(QueryExpr::Table),
2710 QueryExpr::Join(mut join) => {
2711 join.left = Box::new(self.resolve_select_expr_subqueries(*join.left, frame)?);
2712 join.right = Box::new(self.resolve_select_expr_subqueries(*join.right, frame)?);
2713 Ok(QueryExpr::Join(join))
2714 }
2715 other => Ok(other),
2716 }
2717 }
2718
2719 fn resolve_expr_subqueries(
2720 &self,
2721 expr: reddb_rql::ast::Expr,
2722 outer_scopes: &[String],
2723 frame: &dyn super::statement_frame::ReadFrame,
2724 ) -> RedDBResult<reddb_rql::ast::Expr> {
2725 use reddb_rql::ast::Expr;
2726
2727 match expr {
2728 Expr::Subquery { query, span } => {
2729 let values = self.execute_expr_subquery_values(query, outer_scopes, frame)?;
2730 if values.len() > 1 {
2731 return Err(RedDBError::Query(
2732 "scalar subquery returned more than one row".to_string(),
2733 ));
2734 }
2735 Ok(Expr::Literal {
2736 value: values.into_iter().next().unwrap_or(Value::Null),
2737 span,
2738 })
2739 }
2740 Expr::BinaryOp { op, lhs, rhs, span } => Ok(Expr::BinaryOp {
2741 op,
2742 lhs: Box::new(self.resolve_expr_subqueries(*lhs, outer_scopes, frame)?),
2743 rhs: Box::new(self.resolve_expr_subqueries(*rhs, outer_scopes, frame)?),
2744 span,
2745 }),
2746 Expr::UnaryOp { op, operand, span } => Ok(Expr::UnaryOp {
2747 op,
2748 operand: Box::new(self.resolve_expr_subqueries(*operand, outer_scopes, frame)?),
2749 span,
2750 }),
2751 Expr::Cast {
2752 inner,
2753 target,
2754 span,
2755 } => Ok(Expr::Cast {
2756 inner: Box::new(self.resolve_expr_subqueries(*inner, outer_scopes, frame)?),
2757 target,
2758 span,
2759 }),
2760 Expr::FunctionCall { name, args, span } => {
2761 let args = args
2762 .into_iter()
2763 .map(|arg| self.resolve_expr_subqueries(arg, outer_scopes, frame))
2764 .collect::<RedDBResult<Vec<_>>>()?;
2765 Ok(Expr::FunctionCall { name, args, span })
2766 }
2767 Expr::WindowFunctionCall {
2768 name,
2769 args,
2770 mut window,
2771 span,
2772 } => {
2773 let args = args
2774 .into_iter()
2775 .map(|arg| self.resolve_expr_subqueries(arg, outer_scopes, frame))
2776 .collect::<RedDBResult<Vec<_>>>()?;
2777 for expr in &mut window.partition_by {
2778 *expr = self.resolve_expr_subqueries(expr.clone(), outer_scopes, frame)?;
2779 }
2780 for clause in &mut window.order_by {
2781 clause.expr =
2782 self.resolve_expr_subqueries(clause.expr.clone(), outer_scopes, frame)?;
2783 }
2784 Ok(Expr::WindowFunctionCall {
2785 name,
2786 args,
2787 window,
2788 span,
2789 })
2790 }
2791 Expr::Case {
2792 branches,
2793 else_,
2794 span,
2795 } => {
2796 let branches = branches
2797 .into_iter()
2798 .map(|(cond, value)| {
2799 Ok((
2800 self.resolve_expr_subqueries(cond, outer_scopes, frame)?,
2801 self.resolve_expr_subqueries(value, outer_scopes, frame)?,
2802 ))
2803 })
2804 .collect::<RedDBResult<Vec<_>>>()?;
2805 let else_ = else_
2806 .map(|expr| self.resolve_expr_subqueries(*expr, outer_scopes, frame))
2807 .transpose()?
2808 .map(Box::new);
2809 Ok(Expr::Case {
2810 branches,
2811 else_,
2812 span,
2813 })
2814 }
2815 Expr::IsNull {
2816 operand,
2817 negated,
2818 span,
2819 } => Ok(Expr::IsNull {
2820 operand: Box::new(self.resolve_expr_subqueries(*operand, outer_scopes, frame)?),
2821 negated,
2822 span,
2823 }),
2824 Expr::InList {
2825 target,
2826 values,
2827 negated,
2828 span,
2829 } => {
2830 let target =
2831 Box::new(self.resolve_expr_subqueries(*target, outer_scopes, frame)?);
2832 let mut resolved = Vec::new();
2833 for value in values {
2834 if let Expr::Subquery { query, .. } = value {
2835 resolved.extend(
2836 self.execute_expr_subquery_values(query, outer_scopes, frame)?
2837 .into_iter()
2838 .map(Expr::lit),
2839 );
2840 } else {
2841 resolved.push(self.resolve_expr_subqueries(value, outer_scopes, frame)?);
2842 }
2843 }
2844 Ok(Expr::InList {
2845 target,
2846 values: resolved,
2847 negated,
2848 span,
2849 })
2850 }
2851 Expr::Between {
2852 target,
2853 low,
2854 high,
2855 negated,
2856 span,
2857 } => Ok(Expr::Between {
2858 target: Box::new(self.resolve_expr_subqueries(*target, outer_scopes, frame)?),
2859 low: Box::new(self.resolve_expr_subqueries(*low, outer_scopes, frame)?),
2860 high: Box::new(self.resolve_expr_subqueries(*high, outer_scopes, frame)?),
2861 negated,
2862 span,
2863 }),
2864 other => Ok(other),
2865 }
2866 }
2867
2868 fn execute_expr_subquery_values(
2869 &self,
2870 subquery: reddb_rql::ast::ExprSubquery,
2871 outer_scopes: &[String],
2872 frame: &dyn super::statement_frame::ReadFrame,
2873 ) -> RedDBResult<Vec<Value>> {
2874 let query = *subquery.query;
2875 if query_references_outer_scope(&query, outer_scopes) {
2876 return Err(RedDBError::Query(
2877 "NOT_YET_SUPPORTED: correlated subqueries are not supported yet; track follow-up issue #470-correlated-subqueries".to_string(),
2878 ));
2879 }
2880 let query = self.rewrite_view_refs(query);
2881 let query = self.resolve_select_expr_subqueries(query, frame)?;
2882 let query = self.authorize_relational_select_expr(query, frame)?;
2883 let result = match query {
2884 QueryExpr::Table(table) => {
2885 execute_runtime_table_query(&self.inner.db, &table, Some(&self.inner.index_store))?
2886 }
2887 QueryExpr::Join(join) => execute_runtime_join_query(self, &join)?,
2888 other => {
2889 return Err(RedDBError::Query(format!(
2890 "expression subquery must be a SELECT query, got {}",
2891 query_expr_name(&other)
2892 )))
2893 }
2894 };
2895 first_column_values(result)
2896 }
2897
2898 fn execute_materialized_graph(
2899 &self,
2900 query: &str,
2901 mode: QueryMode,
2902 statement: &'static str,
2903 expr: &QueryExpr,
2904 ) -> RedDBResult<RuntimeQueryResult> {
2905 let (graph, node_properties, edge_properties) = self.materialize_graph_with_rls()?;
2913 let result =
2914 crate::storage::query::unified::UnifiedExecutor::execute_on_with_graph_properties_checked(
2915 &graph,
2916 expr,
2917 node_properties,
2918 edge_properties,
2919 super::function_budget::graph_work_check(),
2920 )
2921 .map_err(|err| RedDBError::Query(err.to_string()))?;
2922
2923 Ok(RuntimeQueryResult {
2924 query: query.to_string(),
2925 mode,
2926 statement,
2927 engine: "materialized-graph",
2928 result,
2929 affected_rows: 0,
2930 statement_type: "select",
2931 bookmark: None,
2932 notice: None,
2933 })
2934 }
2935
2936 fn dispatch_expr(
2937 &self,
2938 expr: QueryExpr,
2939 query_str: &str,
2940 mode: QueryMode,
2941 ) -> RedDBResult<RuntimeQueryResult> {
2942 let is_read = super::query_budget::is_materialized_read(&expr);
2945 let _budget = if is_read {
2946 Some(super::query_budget::Scope::enter(
2947 self.resource_limits(),
2948 &self.inner.query_budget_slots,
2949 )?)
2950 } else {
2951 None
2952 };
2953 let result = self.dispatch_expr_inner(expr, query_str, mode);
2954 if is_read {
2955 super::query_budget::charge(0)?;
2956 }
2957 let result = result?;
2958 super::query_budget::check_result(self.resource_limits(), &result)?;
2959 if is_read {
2960 super::query_budget::charge(0)?;
2961 }
2962 Ok(result)
2963 }
2964
2965 fn dispatch_expr_inner(
2966 &self,
2967 expr: QueryExpr,
2968 query_str: &str,
2969 mode: QueryMode,
2970 ) -> RedDBResult<RuntimeQueryResult> {
2971 let statement = query_expr_name(&expr);
2972 match expr {
2973 QueryExpr::Function(command) => self.execute_function(query_str, &command),
2974 QueryExpr::Graph(_) | QueryExpr::Path(_) => {
2975 self.execute_materialized_graph(query_str, mode, statement, &expr)
2976 }
2977 QueryExpr::Table(table) => {
2978 let scope = self.ai_scope();
2979 let table = self.resolve_table_expr_subqueries(
2980 table,
2981 &scope as &dyn super::statement_frame::ReadFrame,
2982 )?;
2983 if let Some(TableSource::Function {
2987 name,
2988 args,
2989 named_args,
2990 }) = table.source.clone()
2991 {
2992 return Ok(RuntimeQueryResult {
2993 query: query_str.to_string(),
2994 mode,
2995 statement,
2996 engine: "runtime-graph-tvf",
2997 result: self.execute_table_function(&name, &args, &named_args)?,
2998 affected_rows: 0,
2999 statement_type: "select",
3000 bookmark: None,
3001 notice: None,
3002 });
3003 }
3004 if let Some(TableSource::InlineGraphFunction {
3008 name,
3009 nodes,
3010 edges,
3011 named_args,
3012 }) = table.source.clone()
3013 {
3014 return Ok(RuntimeQueryResult {
3015 query: query_str.to_string(),
3016 mode,
3017 statement,
3018 engine: "runtime-graph-tvf-inline",
3019 result: self.execute_inline_graph_function(
3020 &name,
3021 &nodes,
3022 &edges,
3023 &named_args,
3024 )?,
3025 affected_rows: 0,
3026 statement_type: "select",
3027 bookmark: None,
3028 notice: None,
3029 });
3030 }
3031 if super::red_schema::is_virtual_table(&table.table) {
3032 return Ok(RuntimeQueryResult {
3033 query: query_str.to_string(),
3034 mode,
3035 statement,
3036 engine: "runtime-red-schema",
3037 result: super::red_schema::red_query(
3038 self,
3039 &table.table,
3040 &table,
3041 &scope as &dyn super::statement_frame::ReadFrame,
3042 )?,
3043 affected_rows: 0,
3044 statement_type: "select",
3045 bookmark: None,
3046 notice: None,
3047 });
3048 }
3049 if let Some(view_result) = self.try_resolve_analytics_view(
3051 &table,
3052 &scope as &dyn super::statement_frame::ReadFrame,
3053 )? {
3054 return Ok(RuntimeQueryResult {
3055 query: query_str.to_string(),
3056 mode,
3057 statement,
3058 engine: "runtime-graph-analytics-view",
3059 result: view_result,
3060 affected_rows: 0,
3061 statement_type: "select",
3062 bookmark: None,
3063 notice: None,
3064 });
3065 }
3066 let Some(table_with_rls) = self.authorize_relational_table_select(
3067 table,
3068 &scope as &dyn super::statement_frame::ReadFrame,
3069 )?
3070 else {
3071 return Ok(RuntimeQueryResult {
3072 query: query_str.to_string(),
3073 mode,
3074 statement,
3075 engine: "runtime-table-rls",
3076 result: crate::storage::query::unified::UnifiedResult::empty(),
3077 affected_rows: 0,
3078 statement_type: "select",
3079 bookmark: None,
3080 notice: None,
3081 });
3082 };
3083 Ok(RuntimeQueryResult {
3084 query: query_str.to_string(),
3085 mode,
3086 statement,
3087 engine: "runtime-table",
3088 result: execute_runtime_table_query(
3089 &self.inner.db,
3090 &table_with_rls,
3091 Some(&self.inner.index_store),
3092 )?,
3093 affected_rows: 0,
3094 statement_type: "select",
3095 bookmark: None,
3096 notice: None,
3097 })
3098 }
3099 QueryExpr::Join(join) => {
3100 let scope = self.ai_scope();
3101 let Some(join_with_rls) = self.authorize_relational_join_select(
3102 join,
3103 &scope as &dyn super::statement_frame::ReadFrame,
3104 )?
3105 else {
3106 return Ok(RuntimeQueryResult {
3107 query: query_str.to_string(),
3108 mode,
3109 statement,
3110 engine: "runtime-join-rls",
3111 result: crate::storage::query::unified::UnifiedResult::empty(),
3112 affected_rows: 0,
3113 statement_type: "select",
3114 bookmark: None,
3115 notice: None,
3116 });
3117 };
3118 Ok(RuntimeQueryResult {
3119 query: query_str.to_string(),
3120 mode,
3121 statement,
3122 engine: "runtime-join",
3123 result: execute_runtime_join_query(self, &join_with_rls)?,
3124 affected_rows: 0,
3125 statement_type: "select",
3126 bookmark: None,
3127 notice: None,
3128 })
3129 }
3130 QueryExpr::Vector(vector) => Ok(RuntimeQueryResult {
3131 query: query_str.to_string(),
3132 mode,
3133 statement,
3134 engine: "runtime-vector",
3135 result: execute_runtime_vector_query(self, &vector)?,
3136 affected_rows: 0,
3137 statement_type: "select",
3138 bookmark: None,
3139 notice: None,
3140 }),
3141 QueryExpr::Hybrid(hybrid) => Ok(RuntimeQueryResult {
3142 query: query_str.to_string(),
3143 mode,
3144 statement,
3145 engine: "runtime-hybrid",
3146 result: execute_runtime_hybrid_query(self, &hybrid)?,
3147 affected_rows: 0,
3148 statement_type: "select",
3149 bookmark: None,
3150 notice: None,
3151 }),
3152 QueryExpr::Insert(ref insert) if super::red_schema::is_virtual_table(&insert.table) => {
3153 Err(RedDBError::Query(
3154 super::red_schema::READ_ONLY_ERROR.to_string(),
3155 ))
3156 }
3157 QueryExpr::Update(ref update) if super::red_schema::is_virtual_table(&update.table) => {
3158 Err(RedDBError::Query(
3159 super::red_schema::READ_ONLY_ERROR.to_string(),
3160 ))
3161 }
3162 QueryExpr::Delete(ref delete) if super::red_schema::is_virtual_table(&delete.table) => {
3163 Err(RedDBError::Query(
3164 super::red_schema::READ_ONLY_ERROR.to_string(),
3165 ))
3166 }
3167 QueryExpr::Insert(ref insert) => self
3168 .with_deferred_store_wal_for_dml(self.insert_may_emit_events(insert), || {
3169 self.execute_insert(query_str, insert)
3170 }),
3171 QueryExpr::Update(ref update) => self
3172 .with_deferred_store_wal_for_dml(self.update_may_emit_events(update), || {
3173 self.execute_update(query_str, update)
3174 }),
3175 QueryExpr::Delete(ref delete) => self
3176 .with_deferred_store_wal_for_dml(self.delete_may_emit_events(delete), || {
3177 self.execute_delete(query_str, delete)
3178 }),
3179 QueryExpr::QueueCommand(ref cmd) => self.with_deferred_store_wal_if_transaction(|| {
3180 self.execute_queue_command(query_str, cmd)
3181 }),
3182 QueryExpr::KvCommand(ref cmd) => self.execute_kv_command(query_str, cmd),
3183 QueryExpr::SearchCommand(ref cmd) => self.execute_search_command(query_str, cmd),
3184 QueryExpr::Ask(ref ask) => self.execute_ask(query_str, ask),
3185 _ => Err(RedDBError::Query(format!(
3186 "prepared-statement execution does not support {statement} statements"
3187 ))),
3188 }
3189 }
3190
3191 fn execute_table_function(
3198 &self,
3199 name: &str,
3200 args: &[String],
3201 named_args: &[(String, f64)],
3202 ) -> RedDBResult<crate::storage::query::unified::UnifiedResult> {
3203 if name.eq_ignore_ascii_case("red.diff") {
3204 return self.execute_vcs_diff_tvf(args, named_args);
3205 }
3206 if !is_graph_tvf_name(name) {
3207 return Err(RedDBError::Query(format!("unknown table function: {name}")));
3208 }
3209 if args.len() != 1 {
3211 return Err(RedDBError::Query(format!(
3212 "table function '{name}' takes exactly 1 graph argument, got {}",
3213 args.len()
3214 )));
3215 }
3216
3217 let (nodes, edges) = self.materialize_whole_graph_abstract()?;
3222 self.dispatch_graph_algorithm(name, nodes, edges, named_args)
3223 }
3224
3225 pub(crate) fn revive_versioned_updates_since(
3226 &self,
3227 conn_id: u64,
3228 stamper_xid: u64,
3229 ) -> RedDBResult<usize> {
3230 let candidates = {
3233 let guard = self.inner.pending_versioned_updates.read();
3234 let Some(pending) = guard.get(&conn_id) else {
3235 return Ok(0);
3236 };
3237 pending
3238 .iter()
3239 .rev()
3240 .filter(|entry| entry.3 >= stamper_xid)
3241 .cloned()
3242 .collect::<Vec<_>>()
3243 };
3244 let mut reverted = 0usize;
3245 for candidate in candidates {
3246 let (collection, old_id, new_id, xid, previous_xmax) = &candidate;
3247 let topology_lock = self.index_store_ref().collection_topology_lock(collection);
3248 #[cfg(test)]
3249 self.index_store_ref().mutation_test_hook(
3250 collection,
3251 super::index_store::MutationTestPhase::BeforeUndoTopology,
3252 );
3253 let topology_guard = topology_lock.read();
3254 let mut guard = self.inner.pending_versioned_updates.write();
3255 let Some(pending) = guard.get_mut(&conn_id) else {
3256 continue;
3257 };
3258 let Some(position) = pending.iter().rposition(|entry| entry == &candidate) else {
3261 continue;
3262 };
3263 self.revive_versioned_update(
3264 collection,
3265 *old_id,
3266 *new_id,
3267 *xid,
3268 *previous_xmax,
3269 topology_guard,
3270 )?;
3271 pending.remove(position);
3272 reverted += 1;
3273 if pending.is_empty() {
3274 guard.remove(&conn_id);
3275 }
3276 }
3277 Ok(reverted)
3278 }
3279
3280 fn check_explain_target_privilege(&self, inner_sql: &str) -> RedDBResult<()> {
3286 let parsed =
3287 reddb_rql::parser::parse(inner_sql).map_err(|e| RedDBError::Query(e.to_string()))?;
3288 self.check_query_privilege(&parsed.query)
3289 .map_err(|err| RedDBError::Query(format!("permission denied: {err}")))
3290 }
3291
3292 fn explain_as_rows(&self, raw_query: &str, inner_sql: &str) -> RedDBResult<RuntimeQueryResult> {
3300 self.check_explain_target_privilege(inner_sql)?;
3301 let explain = self.explain_query(inner_sql)?;
3302
3303 let mut columns = vec![
3304 "op".to_string(),
3305 "source".to_string(),
3306 "estimated_rows".to_string(),
3307 "estimated_cost".to_string(),
3308 "depth".to_string(),
3309 ];
3310
3311 let mut records: Vec<crate::storage::query::unified::UnifiedRecord> = Vec::new();
3312
3313 for name in &explain.cte_materializations {
3319 use std::sync::Arc;
3320 let mut rec = crate::storage::query::unified::UnifiedRecord::default();
3321 rec.set_arc(Arc::from("op"), Value::text("CteScan".to_string()));
3322 rec.set_arc(Arc::from("source"), Value::text(name.clone()));
3323 rec.set_arc(Arc::from("estimated_rows"), Value::Float(0.0));
3324 rec.set_arc(Arc::from("estimated_cost"), Value::Float(0.0));
3325 rec.set_arc(Arc::from("depth"), Value::Integer(0));
3326 records.push(rec);
3327 }
3328
3329 let vector_details = explain.statement == "vector";
3330 if vector_details {
3331 columns.extend(["access_path_reason".to_string(), "index_hint".to_string()]);
3332 }
3333 walk_plan_node(&explain.logical_plan.root, 0, &mut records, vector_details);
3334
3335 let result = crate::storage::query::unified::UnifiedResult {
3336 columns,
3337 records,
3338 stats: Default::default(),
3339 pre_serialized_json: None,
3340 };
3341
3342 Ok(RuntimeQueryResult {
3343 query: raw_query.to_string(),
3344 mode: explain.mode,
3345 statement: "explain",
3346 engine: "runtime-explain",
3347 result,
3348 affected_rows: 0,
3349 statement_type: "select",
3350 bookmark: None,
3351 notice: None,
3352 })
3353 }
3354
3355 fn explain_analyze_vector_as_rows(
3356 &self,
3357 raw_query: &str,
3358 expr: QueryExpr,
3359 ) -> RedDBResult<RuntimeQueryResult> {
3360 let QueryExpr::Vector(query) = &expr else {
3361 return Err(RedDBError::Internal(
3362 "expected vector analysis target".to_string(),
3363 ));
3364 };
3365 let source = query.collection.clone();
3366 let execution = self.execute_query_expr(expr)?;
3369 let stats = execution.result.stats;
3370 let vector = stats.vector.as_ref().ok_or_else(|| {
3371 RedDBError::Internal("vector executor did not provide measurements".to_string())
3372 })?;
3373 let fields = vec![
3374 ("op", Value::text(vector.access_path.clone())),
3375 ("source", Value::text(source)),
3376 ("index_used", Value::Boolean(vector.index_used)),
3377 ("mode_requested", Value::text(vector.mode_requested.clone())),
3378 ("mode_executed", Value::text(vector.mode_executed.clone())),
3379 (
3380 "fallback_reason",
3381 vector
3382 .fallback_reason
3383 .as_ref()
3384 .map_or(Value::Null, |reason| Value::text(reason.clone())),
3385 ),
3386 (
3387 "approximate_distance_evaluations",
3388 Value::UnsignedInteger(vector.approximate_distance_evaluations),
3389 ),
3390 (
3391 "peak_topk_entries",
3392 Value::UnsignedInteger(vector.peak_topk_entries),
3393 ),
3394 (
3395 "candidates_examined",
3396 Value::UnsignedInteger(vector.candidates_examined),
3397 ),
3398 (
3399 "metadata_rejected",
3400 Value::UnsignedInteger(vector.metadata_rejected),
3401 ),
3402 ("rls_rejected", Value::UnsignedInteger(vector.rls_rejected)),
3403 (
3404 "visibility_rejected",
3405 Value::UnsignedInteger(vector.visibility_rejected),
3406 ),
3407 (
3408 "exact_distance_evaluations",
3409 Value::UnsignedInteger(vector.exact_distance_evaluations),
3410 ),
3411 ("actual_rows", Value::UnsignedInteger(vector.rows_returned)),
3412 (
3413 "actual_ms",
3414 Value::Float(stats.exec_time_us as f64 / 1000.0),
3415 ),
3416 ("metrics_scope", Value::text("vector_pipeline")),
3417 (
3418 "operators",
3419 Value::Json(vector.operators_json().to_string().into_bytes()),
3420 ),
3421 ];
3422 let columns = fields.iter().map(|(name, _)| name.to_string()).collect();
3423 let mut record = crate::storage::query::unified::UnifiedRecord::default();
3424 for (name, value) in fields {
3425 record.set_owned(name.to_string(), value);
3426 }
3427 Ok(RuntimeQueryResult {
3428 query: raw_query.to_string(),
3429 mode: execution.mode,
3430 statement: "explain_analyze",
3431 engine: "runtime-explain-analyze",
3432 result: crate::storage::query::unified::UnifiedResult {
3433 columns,
3434 records: vec![record],
3435 stats,
3436 pre_serialized_json: None,
3437 },
3438 affected_rows: 0,
3439 statement_type: "select",
3440 bookmark: None,
3441 notice: None,
3442 })
3443 }
3444
3445 fn explain_analyze_as_rows(
3446 &self,
3447 raw_query: &str,
3448 inner_sql: &str,
3449 ) -> RedDBResult<RuntimeQueryResult> {
3450 if strip_keyword_ci(inner_sql.trim_start(), "VECTOR").is_some() {
3451 let expr = parse_multi(inner_sql).map_err(|err| RedDBError::Query(err.to_string()))?;
3452 if matches!(&expr, QueryExpr::Vector(_)) {
3453 return self.explain_analyze_vector_as_rows(raw_query, expr);
3454 }
3455 }
3456 if !starts_with_dml_keyword(inner_sql) {
3457 return Err(RedDBError::Query(
3458 "EXPLAIN ANALYZE currently supports INSERT, UPDATE, DELETE, and VECTOR SEARCH"
3459 .to_string(),
3460 ));
3461 }
3462
3463 self.check_explain_target_privilege(inner_sql)?;
3464 let explain = self.explain_query(inner_sql)?;
3465 let conn_id = current_connection_id();
3466 if self.inner.transaction_state.in_transaction(conn_id) {
3467 return Err(RedDBError::Query(
3468 "EXPLAIN ANALYZE requires no active transaction".to_string(),
3469 ));
3470 }
3471
3472 self.execute_query_inner("BEGIN")?;
3473 let started = std::time::Instant::now();
3474 let execution = self.execute_query_inner(inner_sql);
3475 let elapsed_ms = started.elapsed().as_secs_f64() * 1000.0;
3476 let rollback = self.execute_query_inner("ROLLBACK");
3477
3478 let affected_rows = match execution {
3479 Ok(result) => result.affected_rows,
3480 Err(err) => {
3481 rollback?;
3482 return Err(err);
3483 }
3484 };
3485 rollback?;
3486
3487 let columns = vec![
3488 "op".to_string(),
3489 "source".to_string(),
3490 "estimated_rows".to_string(),
3491 "estimated_cost".to_string(),
3492 "actual_rows".to_string(),
3493 "actual_ms".to_string(),
3494 "depth".to_string(),
3495 ];
3496 let mut records = Vec::new();
3497 walk_analyze_plan_node(
3498 &explain.logical_plan.root,
3499 0,
3500 affected_rows,
3501 elapsed_ms,
3502 &mut records,
3503 );
3504
3505 let result = crate::storage::query::unified::UnifiedResult {
3506 columns,
3507 records,
3508 stats: Default::default(),
3509 pre_serialized_json: None,
3510 };
3511
3512 Ok(RuntimeQueryResult {
3513 query: raw_query.to_string(),
3514 mode: explain.mode,
3515 statement: "explain_analyze",
3516 engine: "runtime-explain-analyze",
3517 result,
3518 affected_rows: 0,
3519 statement_type: "select",
3520 bookmark: None,
3521 notice: None,
3522 })
3523 }
3524}
3525
3526fn strip_explain_analyze_prefix(sql: &str) -> Option<&str> {
3527 let rest = strip_keyword_ci(sql.trim_start(), "EXPLAIN")?.trim_start();
3528 Some(strip_keyword_ci(rest, "ANALYZE")?.trim_start()).filter(|inner| !inner.is_empty())
3529}
3530
3531fn strip_keyword_ci<'a>(sql: &'a str, keyword: &str) -> Option<&'a str> {
3532 let head = sql.get(..keyword.len())?;
3535 let rest = &sql[keyword.len()..];
3536 if !head.eq_ignore_ascii_case(keyword) {
3537 return None;
3538 }
3539 if rest
3540 .chars()
3541 .next()
3542 .is_some_and(|ch| !ch.is_ascii_whitespace())
3543 {
3544 return None;
3545 }
3546 Some(rest)
3547}
3548
3549fn starts_with_dml_keyword(sql: &str) -> bool {
3550 let trimmed = sql.trim_start();
3551 let head_end = trimmed
3552 .find(|ch: char| ch.is_ascii_whitespace())
3553 .unwrap_or(trimmed.len());
3554 let head = &trimmed[..head_end];
3555 head.eq_ignore_ascii_case("INSERT")
3556 || head.eq_ignore_ascii_case("UPDATE")
3557 || head.eq_ignore_ascii_case("DELETE")
3558}
3559
3560fn walk_plan_node(
3561 node: &crate::storage::query::planner::CanonicalLogicalNode,
3562 depth: usize,
3563 out: &mut Vec<crate::storage::query::unified::UnifiedRecord>,
3564 vector_details: bool,
3565) {
3566 use std::sync::Arc;
3567 let mut rec = crate::storage::query::unified::UnifiedRecord::default();
3568 rec.set_arc(Arc::from("op"), Value::text(node.operator.clone()));
3569 rec.set_arc(
3570 Arc::from("source"),
3571 node.source.clone().map(Value::text).unwrap_or(Value::Null),
3572 );
3573 rec.set_arc(
3574 Arc::from("estimated_rows"),
3575 Value::Float(node.estimated_rows),
3576 );
3577 rec.set_arc(
3578 Arc::from("estimated_cost"),
3579 Value::Float(node.operator_cost),
3580 );
3581 rec.set_arc(Arc::from("depth"), Value::Integer(depth as i64));
3582 if vector_details {
3583 for name in ["access_path_reason", "index_hint"] {
3584 rec.set_owned(
3585 name.to_string(),
3586 node.details
3587 .get(name)
3588 .cloned()
3589 .map(Value::text)
3590 .unwrap_or(Value::Null),
3591 );
3592 }
3593 }
3594 out.push(rec);
3595 for child in &node.children {
3596 walk_plan_node(child, depth + 1, out, vector_details);
3597 }
3598}
3599
3600fn walk_analyze_plan_node(
3601 node: &crate::storage::query::planner::CanonicalLogicalNode,
3602 depth: usize,
3603 affected_rows: u64,
3604 elapsed_ms: f64,
3605 out: &mut Vec<crate::storage::query::unified::UnifiedRecord>,
3606) {
3607 use std::sync::Arc;
3608 let mut rec = crate::storage::query::unified::UnifiedRecord::default();
3609 rec.set_arc(Arc::from("op"), Value::text(node.operator.clone()));
3610 rec.set_arc(
3611 Arc::from("source"),
3612 node.source.clone().map(Value::text).unwrap_or(Value::Null),
3613 );
3614 rec.set_arc(
3615 Arc::from("estimated_rows"),
3616 Value::Float(node.estimated_rows),
3617 );
3618 rec.set_arc(
3619 Arc::from("estimated_cost"),
3620 Value::Float(node.operator_cost),
3621 );
3622 rec.set_arc(
3623 Arc::from("actual_rows"),
3624 Value::UnsignedInteger(if depth == 0 { affected_rows } else { 0 }),
3625 );
3626 rec.set_arc(
3627 Arc::from("actual_ms"),
3628 Value::Float(if depth == 0 { elapsed_ms } else { 0.0 }),
3629 );
3630 rec.set_arc(Arc::from("depth"), Value::Integer(depth as i64));
3631 out.push(rec);
3632
3633 for child in &node.children {
3634 walk_analyze_plan_node(child, depth + 1, 0, 0.0, out);
3635 }
3636}
3637
3638#[cfg(test)]
3639mod declared_model_validation_tests {
3640 use super::*;
3641 use crate::catalog::CollectionModel;
3642 use crate::storage::no_malloc_hot_paths::measure_allocations;
3643 use reddb_rql::ast::KvCommand;
3644
3645 fn get(collection: &str) -> QueryExpr {
3646 QueryExpr::KvCommand(KvCommand::Get {
3647 model: CollectionModel::Kv,
3648 collection: collection.into(),
3649 key: "item".into(),
3650 })
3651 }
3652
3653 #[test]
3654 fn declared_model_validation_keeps_mismatches_and_observes_contract_changes() {
3655 let runtime = RedDBRuntime::in_memory().expect("runtime");
3656 runtime
3657 .execute_query("KV PUT declared.item = 1")
3658 .expect("seed");
3659 runtime
3660 .validate_model_operations_before_auth(&get("declared"))
3661 .expect("KV contract");
3662 let db = runtime.db();
3663 for (model, name) in [
3664 (CollectionModel::Table, "table"),
3665 (CollectionModel::Mixed, "mixed"),
3666 ] {
3667 let mut contract = db.collection_contract("declared").expect("contract");
3668 contract.declared_model = model;
3669 db.save_collection_contract(contract)
3670 .expect("replace contract");
3671 let error = runtime
3672 .validate_model_operations_before_auth(&get("declared"))
3673 .expect_err("mismatch");
3674 assert_eq!(
3675 error.to_string(),
3676 format!("INVALID_OPERATION: model mismatch: expected kv, got {name}")
3677 );
3678 }
3679 }
3680
3681 #[test]
3682 fn declared_model_validation_preserves_legacy_inference_and_missing_collections() {
3683 let runtime = RedDBRuntime::in_memory().expect("runtime");
3684 let db = runtime.db();
3685 let store = db.store();
3686 store
3687 .create_collection("legacy")
3688 .expect("legacy collection");
3689 store
3690 .get_collection("legacy")
3691 .expect("manager")
3692 .insert(crate::storage::UnifiedEntity::table_row(
3693 crate::storage::EntityId::new(1),
3694 "legacy",
3695 1,
3696 Vec::new(),
3697 ))
3698 .expect("uncontracted table row");
3699 assert!(db.collection_contract("legacy").is_none());
3700 let error = runtime
3701 .validate_model_operations_before_auth(&get("legacy"))
3702 .expect_err("inferred table");
3703 assert!(error
3704 .to_string()
3705 .contains("model mismatch: expected kv, got table"));
3706 runtime
3707 .validate_model_operations_before_auth(&get("missing"))
3708 .expect("missing stays deferred to execution");
3709 }
3710
3711 #[test]
3712 fn declared_model_validation_does_not_treat_orphan_contracts_as_collections() {
3713 let runtime = RedDBRuntime::in_memory().expect("runtime");
3714 runtime
3715 .execute_query("KV PUT declared.item = 1")
3716 .expect("seed");
3717 let db = runtime.db();
3718 let mut contract = db.collection_contract("declared").expect("contract");
3719 contract.name = "orphan".into();
3720 contract.declared_model = CollectionModel::Table;
3721 db.save_collection_contract(contract)
3722 .expect("orphan contract");
3723 assert!(db.store().get_collection("orphan").is_none());
3724 runtime
3725 .validate_model_operations_before_auth(&get("orphan"))
3726 .expect("no physical collection to validate");
3727 }
3728
3729 #[test]
3730 fn declared_model_validation_allocates_nothing_with_unrelated_collections() {
3731 let runtime = RedDBRuntime::in_memory().expect("runtime");
3732 runtime
3733 .execute_query("KV PUT declared.item = 1")
3734 .expect("seed");
3735 let expr = get("declared");
3736 for unrelated in [0, 64] {
3737 let db = runtime.db();
3738 let store = db.store();
3739 for index in 0..unrelated {
3740 let name = format!("unrelated_{index}");
3741 store
3742 .create_collection(&name)
3743 .expect("unrelated collection");
3744 }
3745 runtime
3746 .validate_model_operations_before_auth(&expr)
3747 .expect("warm contract cache");
3748 let (result, count) =
3749 measure_allocations(|| runtime.validate_model_operations_before_auth(&expr));
3750 result.expect("declared KV");
3751 assert_eq!(
3752 count.allocs, 0,
3753 "validation must only borrow the target contract"
3754 );
3755 }
3756 }
3757}
3758
3759#[cfg(test)]
3760mod multibyte_sql_tests {
3761 use super::*;
3762
3763 #[test]
3764 fn explain_analyze_prefix_ignores_multibyte_text_at_the_keyword_boundary() {
3765 for pad in 0..16 {
3768 for wide in ["é", "日", "😀"] {
3769 let sql = format!("{}{wide}llo", "a".repeat(pad));
3770 assert_eq!(strip_explain_analyze_prefix(&sql), None, "{sql:?}");
3771 }
3772 }
3773 assert_eq!(
3774 strip_explain_analyze_prefix("explain analyze DELETE FROM t"),
3775 Some("DELETE FROM t")
3776 );
3777 }
3778
3779 #[test]
3784 fn multibyte_sql_never_panics_execute_query() {
3785 let rt = RedDBRuntime::in_memory().expect("in-memory runtime");
3786 let seeds = [
3787 "",
3788 "SELECT ",
3789 "SELECT '",
3790 "EXPLAIN ",
3791 "EXPLAIN ANALYZE ",
3792 "CHECKPOINT ",
3793 "CHECKOUT ",
3794 "RESET ",
3795 "MERGE ",
3796 "CHERRY PICK ",
3797 "REVERT ",
3798 "RESOLVE CONFLICT ",
3799 "INSERT INTO t VALUES ('",
3800 ];
3801 let mut panicked = Vec::new();
3802 for seed in seeds {
3803 for pad in 0..16 {
3804 for wide in ["é", "日", "😀"] {
3805 let sql = format!("{seed}{}{wide}llo", "a".repeat(pad));
3806 let outcome = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
3807 let _ = rt.execute_query(&sql);
3808 }));
3809 if outcome.is_err() {
3810 panicked.push(sql);
3811 }
3812 }
3813 }
3814 }
3815 assert!(
3816 panicked.is_empty(),
3817 "execute_query panicked on {} statement(s), first: {:?}",
3818 panicked.len(),
3819 panicked.first()
3820 );
3821 }
3822}
3823
3824#[cfg(test)]
3825mod inline_graph_tvf_tests {
3826 use super::*;
3827
3828 fn scopes_for(sql: &str) -> HashSet<String> {
3829 let expr = reddb_rql::parser::parse(sql).expect("parse").query;
3830 query_expr_result_cache_scopes(&expr)
3831 }
3832
3833 #[test]
3834 fn inline_tvf_cache_scopes_include_source_collections() {
3835 let scopes = scopes_for(
3839 "SELECT * FROM components(nodes => (SELECT id FROM hosts), edges => (SELECT src, dst FROM links))",
3840 );
3841 assert!(scopes.contains("hosts"), "nodes source scoped: {scopes:?}");
3842 assert!(scopes.contains("links"), "edges source scoped: {scopes:?}");
3843 }
3844
3845 #[test]
3846 fn graph_collection_tvf_cache_scope_is_graph_argument() {
3847 let scopes = scopes_for("SELECT * FROM components(g)");
3851 assert!(scopes.contains("g"), "collection form scoped: {scopes:?}");
3852 }
3853
3854 #[test]
3855 fn abstract_degree_centrality_counts_undirected_endpoints() {
3856 let nodes = vec!["a".to_string(), "b".to_string(), "c".to_string()];
3857 let edges = vec![
3858 ("a".to_string(), "b".to_string(), 1.0_f32),
3859 ("b".to_string(), "c".to_string(), 1.0_f32),
3860 ];
3861 let degrees = abstract_degree_centrality(&nodes, &edges);
3862 assert_eq!(
3863 degrees,
3864 vec![
3865 ("a".to_string(), 1),
3866 ("b".to_string(), 2),
3867 ("c".to_string(), 1),
3868 ]
3869 );
3870 }
3871}
3872
3873fn reject_engine_internal_vault_key(key: &str) -> RedDBResult<()> {
3880 let lowered = key.to_ascii_lowercase();
3881 let internal = lowered == "red.secret.aes_key"
3882 || lowered.starts_with("red.iam.")
3883 || lowered.starts_with("red.acl.")
3884 || lowered.starts_with("red.vault.");
3885 if internal {
3886 return Err(RedDBError::Query(format!(
3887 "`{key}` is an engine-internal vault key and cannot be set or deleted through SET/DELETE SECRET"
3888 )));
3889 }
3890 Ok(())
3891}
3892
3893fn confine_copy_from_path(path: &str) -> RedDBResult<()> {
3899 let Ok(root) = std::env::var("REDDB_COPY_IMPORT_ROOT") else {
3900 return Ok(());
3901 };
3902 let root = std::fs::canonicalize(&root).map_err(|e| {
3903 RedDBError::Query(format!(
3904 "REDDB_COPY_IMPORT_ROOT `{root}` is not usable: {e}"
3905 ))
3906 })?;
3907 let resolved = std::fs::canonicalize(path)
3908 .map_err(|e| RedDBError::Query(format!("COPY FROM `{path}`: {e}")))?;
3909 if !resolved.starts_with(&root) || !resolved.is_file() {
3910 return Err(RedDBError::Query(format!(
3911 "COPY FROM `{path}` is outside the configured import root"
3912 )));
3913 }
3914 Ok(())
3915}
3916
3917#[cfg(test)]
3918mod security_gate_tests {
3919 use super::*;
3920 use crate::auth::store::AuthStore;
3921 use crate::auth::{AuthConfig, Role};
3922
3923 struct IdentityGuard;
3926
3927 impl IdentityGuard {
3928 fn install(username: &str, role: Role) -> Self {
3929 set_current_auth_identity(username.to_string(), role);
3930 Self
3931 }
3932 }
3933
3934 impl Drop for IdentityGuard {
3935 fn drop(&mut self) {
3936 clear_current_auth_identity();
3937 }
3938 }
3939
3940 fn runtime_with_user(username: &str, role: Role) -> RedDBRuntime {
3941 let store = std::sync::Arc::new(AuthStore::new(AuthConfig {
3942 enabled: true,
3943 session_ttl_secs: 60,
3944 require_auth: true,
3945 auto_encrypt_storage: false,
3946 vault_enabled: false,
3947 cert: Default::default(),
3948 oauth: Default::default(),
3949 }));
3950 store.create_user(username, "pw", role).expect("user");
3951 let runtime =
3952 RedDBRuntime::with_options(crate::api::RedDBOptions::in_memory()).expect("runtime");
3953 runtime.set_auth_store(store);
3954 runtime
3955 }
3956
3957 fn runtime_with_store(username: &str, role: Role) -> (RedDBRuntime, std::sync::Arc<AuthStore>) {
3958 let store = std::sync::Arc::new(AuthStore::new(AuthConfig {
3959 enabled: true,
3960 session_ttl_secs: 60,
3961 require_auth: true,
3962 auto_encrypt_storage: false,
3963 vault_enabled: false,
3964 cert: Default::default(),
3965 oauth: Default::default(),
3966 }));
3967 store.create_user(username, "pw", role).expect("user");
3968 let runtime =
3969 RedDBRuntime::with_options(crate::api::RedDBOptions::in_memory()).expect("runtime");
3970 runtime.set_auth_store(std::sync::Arc::clone(&store));
3971 (runtime, store)
3972 }
3973
3974 #[test]
3975 fn nested_reads_need_select_on_the_inner_table() {
3976 use crate::auth::privileges::{Action, GrantPrincipal, Resource};
3977 use crate::auth::UserId;
3978 let (runtime, store) = runtime_with_store("ro", Role::Read);
3979 runtime
3980 .execute_query("CREATE TABLE gate_open (a INTEGER)")
3981 .expect("create");
3982 runtime
3983 .execute_query("CREATE TABLE gate_closed (b INTEGER)")
3984 .expect("create");
3985 store
3988 .grant(
3989 &UserId::from_parts(None, "admin"),
3990 Role::Admin,
3991 GrantPrincipal::User(UserId::from_parts(None, "ro")),
3992 Resource::table_from_name("gate_open"),
3993 vec![Action::Select],
3994 false,
3995 None,
3996 )
3997 .expect("grant");
3998 let _identity = IdentityGuard::install("ro", Role::Read);
3999 runtime
4000 .execute_query("SELECT * FROM (SELECT a FROM gate_open) s")
4001 .expect("derived table over a granted table is fine");
4002 for sql in [
4005 "SELECT * FROM (SELECT b FROM gate_closed) s",
4006 "SELECT a FROM gate_open WHERE a IN (SELECT b FROM gate_closed)",
4007 ] {
4008 let err = runtime
4009 .execute_query(sql)
4010 .expect_err("nested read of an ungranted table must be refused");
4011 assert!(
4012 err.to_string().contains("permission denied"),
4013 "{sql}: unexpected error: {err}"
4014 );
4015 }
4016 }
4017
4018 #[test]
4019 fn policy_listings_are_self_or_policy_management() {
4020 let (runtime, store) = runtime_with_store("ro", Role::Read);
4021 store.create_user("other", "pw", Role::Read).expect("user");
4022 let _identity = IdentityGuard::install("ro", Role::Read);
4023 runtime
4024 .execute_query("SHOW EFFECTIVE PERMISSIONS FOR ro")
4025 .expect("a principal may inspect its own permissions");
4026 let err = runtime
4027 .execute_query("SHOW EFFECTIVE PERMISSIONS FOR other")
4028 .expect_err("inspecting another principal is policy management");
4029 assert!(
4030 err.to_string().contains("permission denied"),
4031 "unexpected error: {err}"
4032 );
4033 }
4034
4035 #[test]
4036 fn secret_columns_stay_masked_for_readers_without_secret_read() {
4037 use reddb_types::Value;
4038 let (runtime, store) = runtime_with_store("ro", Role::Read);
4039 store.create_user("root", "pw", Role::Admin).expect("admin");
4040 store.ensure_vault_secret_key();
4041 runtime
4042 .execute_query(
4043 "INSERT INTO creds_gate (name, token) VALUES ('stripe', SECRET('sk_live_abc'))",
4044 )
4045 .expect("insert");
4046
4047 let token_for = |username: &str, role: Role| {
4048 let _identity = IdentityGuard::install(username, role);
4049 let result = runtime
4050 .execute_query("SELECT name, token FROM creds_gate")
4051 .expect("select");
4052 result.result.records[0]
4053 .get("token")
4054 .cloned()
4055 .expect("token column")
4056 };
4057 assert!(
4060 matches!(token_for("ro", Role::Read), Value::Secret(_)),
4061 "a read-role principal must see the column masked"
4062 );
4063 assert!(matches!(token_for("root", Role::Admin), Value::Secret(_)));
4064 }
4065
4066 #[test]
4067 fn store_wide_operations_need_the_admin_tier() {
4068 let runtime = runtime_with_user("bob", Role::Write);
4069 let _identity = IdentityGuard::install("bob", Role::Write);
4070 for sql in ["VACUUM", "SCRUB"] {
4073 let err = runtime
4074 .execute_query(sql)
4075 .expect_err("store-wide operation must be refused for a non-admin");
4076 assert!(
4077 err.to_string().contains("permission denied"),
4078 "{sql}: unexpected error: {err}"
4079 );
4080 }
4081 }
4082
4083 #[test]
4084 fn explain_needs_the_privilege_of_the_statement_it_plans() {
4085 let runtime = runtime_with_user("ro", Role::Read);
4086 let _identity = IdentityGuard::install("ro", Role::Read);
4087 runtime
4088 .execute_query("EXPLAIN SELECT 1 AS one")
4089 .expect("planning a read is a read");
4090 let err = runtime
4093 .execute_query("EXPLAIN INSERT INTO gate_t (a) VALUES (1)")
4094 .expect_err("planning a write needs the write privilege");
4095 assert!(
4096 err.to_string().contains("permission denied"),
4097 "unexpected EXPLAIN error: {err}"
4098 );
4099 }
4100
4101 #[test]
4102 fn copy_from_a_server_path_is_admin_only() {
4103 let runtime = runtime_with_user("bob", Role::Write);
4104 let _identity = IdentityGuard::install("bob", Role::Write);
4105 let err = runtime
4109 .execute_query("COPY leak FROM '/etc/hostname'")
4110 .expect_err("COPY FROM must be refused for a non-admin");
4111 assert!(
4112 err.to_string().contains("permission denied"),
4113 "unexpected COPY error: {err}"
4114 );
4115 }
4116
4117 #[test]
4118 fn vcs_history_rewrites_are_admin_only() {
4119 let runtime = runtime_with_user("bob", Role::Write);
4120 let _identity = IdentityGuard::install("bob", Role::Write);
4121 let err = runtime
4124 .execute_query("RESET HARD TO 'deadbeef'")
4125 .expect_err("RESET must be refused for a non-admin");
4126 assert!(
4127 err.to_string().contains("permission denied"),
4128 "unexpected RESET error: {err}"
4129 );
4130 }
4131
4132 #[test]
4133 fn ai_provider_config_writes_are_admin_only() {
4134 let runtime = runtime_with_user("bob", Role::Write);
4135 let _identity = IdentityGuard::install("bob", Role::Write);
4136 let err = runtime
4139 .execute_query(
4140 "SET CONFIG red.config.ai.providers.openai.tokens.default.secret_ref = 'red.secret.aes_key'",
4141 )
4142 .expect_err("AI provider config must be refused for a non-admin");
4143 assert!(
4144 err.to_string().contains("permission denied"),
4145 "unexpected SET CONFIG error: {err}"
4146 );
4147 }
4148
4149 #[test]
4150 fn engine_internal_vault_keys_are_not_writable_through_set_secret() {
4151 let runtime = runtime_with_user("root", Role::Admin);
4152 let _identity = IdentityGuard::install("root", Role::Admin);
4153 for key in [
4157 "red.iam.policies",
4158 "red.iam.attachments.users",
4159 "red.secret.aes_key",
4160 ] {
4161 let err = runtime
4162 .execute_query(&format!("SET SECRET {key} = 'x'"))
4163 .expect_err("engine-internal vault key must be refused");
4164 assert!(
4165 err.to_string().contains("engine-internal"),
4166 "unexpected error for {key}: {err}"
4167 );
4168 }
4169 }
4170}