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