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_auth_principal,
70 set_current_connection_id, set_current_snapshot, set_current_tenant, snapshot_bundle,
71 with_snapshot_bundle, 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 self.check_scope_override_privilege(&scope)?;
297 let _scope_guard = ScopeOverrideGuard::install(scope);
298 self.execute_query(query)
299 }
300
301 pub fn execute_query(&self, query: &str) -> RedDBResult<RuntimeQueryResult> {
310 let started = std::time::Instant::now();
311 self.inner.node_load_telemetry.query_start();
312 let receipt = super::transactional_replication::CommitReceiptScope::begin(self);
313 let result = self.execute_query_inner(query);
314 self.finish_query_lifecycle(query, started, result, receipt.lsn())
315 }
316
317 pub fn execute_query_with_params(
325 &self,
326 query: &str,
327 params: &[Value],
328 ) -> RedDBResult<RuntimeQueryResult> {
329 if params.is_empty() {
330 return self.execute_query(query);
331 }
332 let started = std::time::Instant::now();
333 self.inner.node_load_telemetry.query_start();
334 let receipt = super::transactional_replication::CommitReceiptScope::begin(self);
335 let result = self.execute_query_with_params_inner(query, params);
336 self.finish_query_lifecycle(query, started, result, receipt.lsn())
337 }
338
339 pub fn execute_prepared_query(
350 &self,
351 query: &str,
352 expr: QueryExpr,
353 ) -> RedDBResult<RuntimeQueryResult> {
354 let started = std::time::Instant::now();
355 self.inner.node_load_telemetry.query_start();
356 let receipt = super::transactional_replication::CommitReceiptScope::begin(self);
357 let result = self.execute_bound_query_expr_in_frame(query, expr);
358 self.finish_query_lifecycle(query, started, result, receipt.lsn())
359 }
360
361 fn finish_query_lifecycle(
362 &self,
363 query: &str,
364 started: std::time::Instant,
365 mut result: RedDBResult<RuntimeQueryResult>,
366 commit_lsn: Option<u64>,
367 ) -> RedDBResult<RuntimeQueryResult> {
368 result = result.and_then(|mut value| {
373 if value.statement_type == "select" {
374 self.filter_integrity_tombstoned(&mut value.result)?;
375 }
376 super::query_budget::check_result(self.resource_limits(), &value)?;
377 Ok(value)
378 });
379 let elapsed_ms = started.elapsed().as_millis() as u64;
380
381 let scope = self.ai_scope();
386 let kind = match result
387 .as_ref()
388 .map(|r| r.statement_type)
389 .unwrap_or("select")
390 {
391 "select" => crate::telemetry::slow_query_logger::QueryKind::Select,
392 "insert" => crate::telemetry::slow_query_logger::QueryKind::Insert,
393 "update" => crate::telemetry::slow_query_logger::QueryKind::Update,
394 "delete" => crate::telemetry::slow_query_logger::QueryKind::Delete,
395 _ => crate::telemetry::slow_query_logger::QueryKind::Internal,
396 };
397 let logged_query = redact_sql_for_slow_log(query);
398 self.inner
399 .slow_query_logger
400 .record(kind, elapsed_ms, logged_query, &scope);
401
402 self.inner
408 .query_latency_telemetry
409 .observe(kind, started.elapsed().as_secs_f64());
410
411 self.inner.node_load_telemetry.query_finish();
415
416 if let Ok(ref mut query_result) = result {
417 if matches!(
418 query_result.statement_type,
419 "insert" | "update" | "delete" | "commit"
420 ) {
421 let lsn = if self.inner.db.replication.is_some() {
422 commit_lsn
423 } else if self.current_xid().is_none() {
424 Some(self.cdc_current_lsn())
425 } else {
426 None
427 };
428 query_result.bookmark = lsn.map(|lsn| {
429 crate::replication::CausalBookmark::new(self.current_replication_term(), lsn)
430 .encode()
431 });
432 }
433 }
434
435 result
436 }
437
438 fn execute_query_with_params_inner(
439 &self,
440 query: &str,
441 params: &[Value],
442 ) -> RedDBResult<RuntimeQueryResult> {
443 let parsed = self.parameterized_query_ast(query)?;
444 let bound = crate::storage::query::user_params::bind(&parsed, params).map_err(|err| {
445 RedDBError::Validation {
446 message: err.to_string(),
447 validation: crate::json!({
448 "code": "INVALID_PARAMS",
449 "surface": "query.params",
450 }),
451 }
452 })?;
453 self.execute_bound_query_expr_in_frame(query, bound)
454 }
455
456 fn parameterized_query_ast(&self, query: &str) -> RedDBResult<QueryExpr> {
459 use crate::storage::query::planner::{CachedPlan, QueryPlan};
460
461 let key = format!("\0parameters\0{query}");
463 if let Some(parsed) = self
464 .inner
465 .query_cache
466 .read()
467 .peek(&key)
468 .filter(|entry| entry.matches_exact_query(query))
469 .map(|entry| entry.plan.original.clone())
470 {
471 return Ok(parsed);
472 }
473 let parsed = parse_multi(query).map_err(|err| RedDBError::Query(err.to_string()))?;
474 let plan = QueryPlan::new(parsed.clone(), parsed.clone(), Default::default());
475 self.inner
476 .query_cache
477 .write()
478 .insert(key, CachedPlan::new(plan).with_exact_query(query));
479 Ok(parsed)
480 }
481
482 fn execute_bound_query_expr_in_frame(
483 &self,
484 query: &str,
485 expr: QueryExpr,
486 ) -> RedDBResult<RuntimeQueryResult> {
487 let rewritten_query = super::red_schema::rewrite_virtual_names(query);
488 let execution_query = rewritten_query.as_deref().unwrap_or(query);
489 let frame = super::statement_frame::StatementExecutionFrame::build(
490 self,
491 super::statement_frame::StatementIdentity::Text(execution_query),
492 )?;
493 let _frame_guards = Box::new(frame.install(self));
497 let _log_span = crate::telemetry::span::query_span(query).entered();
498
499 let expr = self.rewrite_view_refs(expr);
500 let mode = detect_mode(execution_query);
501 let control_event_specs = query_control_event_specs(&expr);
502 let _lock_guard = match frame.prepare_dispatch(self, &expr) {
503 Ok(guard) => guard,
504 Err(err) => {
505 let outcome = control_event_outcome_for_error(&err);
506 for spec in &control_event_specs {
507 self.emit_control_event(
508 spec.kind,
509 outcome,
510 spec.action,
511 spec.resource.clone(),
512 Some(err.to_string()),
513 spec.fields.clone(),
514 )?;
515 }
516 return Err(err);
517 }
518 };
519
520 self.dispatch_expr(expr, query, mode)
521 }
522
523 pub fn causal_session(&self) -> crate::runtime::CausalSession {
524 crate::runtime::CausalSession {
525 runtime: self.clone(),
526 bookmark: None,
527 wait_timeout: std::time::Duration::from_secs(5),
528 }
529 }
530
531 pub fn wait_for_bookmark(
532 &self,
533 bookmark: &crate::replication::CausalBookmark,
534 timeout: std::time::Duration,
535 ) -> RedDBResult<()> {
536 let deadline = std::time::Instant::now() + timeout;
537 loop {
538 let applied_lsn = self.local_contiguous_applied_lsn();
539 if applied_lsn >= bookmark.commit_lsn() {
540 return Ok(());
541 }
542 let now = std::time::Instant::now();
543 if now >= deadline {
544 return Err(RedDBError::InvalidOperation(format!(
545 "timed out waiting for causal bookmark lsn {}; applied={}",
546 bookmark.commit_lsn(),
547 applied_lsn
548 )));
549 }
550 let remaining = deadline.saturating_duration_since(now);
551 std::thread::sleep(remaining.min(std::time::Duration::from_millis(5)));
552 }
553 }
554
555 fn local_contiguous_applied_lsn(&self) -> u64 {
556 match self.inner.db.options().replication.role {
557 crate::replication::ReplicationRole::Replica { .. } => {
558 self.config_u64("red.replication.last_applied_lsn", 0)
559 }
560 _ => self.replication_commit_lsn(),
561 }
562 }
563
564 #[inline(never)]
565 pub(super) fn execute_query_inner(&self, query: &str) -> RedDBResult<RuntimeQueryResult> {
566 if !has_scope_override_active()
577 && !query.trim_start().starts_with("WITHIN")
578 && !query.trim_start().starts_with("within")
579 && !self.inner.query_audit.has_rules()
580 && !self
581 .inner
582 .transaction_state
583 .in_transaction(current_connection_id())
584 {
585 if let Some(result) = self.try_fast_entity_lookup(query) {
586 return result;
587 }
588 }
589
590 match crate::runtime::within_clause::try_strip_within_prefix(query) {
597 Ok(Some((scope, inner))) => {
598 self.check_scope_override_privilege(&scope)?;
599 let _scope_guard = ScopeOverrideGuard::install(scope);
600 return self.execute_query_inner(inner);
605 }
606 Ok(None) => {}
607 Err(msg) => return Err(RedDBError::Query(msg)),
608 }
609
610 if let Some(inner) = strip_explain_analyze_prefix(query) {
614 return self.explain_analyze_as_rows(query, inner);
615 }
616
617 if let Some(inner) = strip_explain_prefix(query) {
624 return self.explain_as_rows(query, inner);
625 }
626
627 if let Some(value) = parse_set_local_tenant(query)? {
632 self.check_scope_tenant_privilege(value.as_deref())?;
633 let conn_id = current_connection_id();
634 if !self.inner.transaction_state.in_transaction(conn_id) {
635 return Err(RedDBError::Query(
636 "SET LOCAL TENANT requires an active transaction".to_string(),
637 ));
638 }
639 self.inner
640 .transaction_state
641 .set_local_tenant(conn_id, value.clone());
642 return Ok(RuntimeQueryResult::ok_message(
643 query.to_string(),
644 &match &value {
645 Some(id) => format!("local tenant set: {id}"),
646 None => "local tenant cleared".to_string(),
647 },
648 "set_local_tenant",
649 ));
650 }
651
652 if super::red_schema::is_system_schema_write(query) {
653 return Err(RedDBError::Query(
654 super::red_schema::READ_ONLY_ERROR.to_string(),
655 ));
656 }
657
658 if let Some(command) = parse_runtime_vcs_command(query) {
659 let command = command?;
660 self.check_vcs_command_privilege(&command)?;
661 return self.execute_vcs_command(query, detect_mode(query), command);
662 }
663
664 if let Some(create_source) = super::analytics_source_catalog::parse_create_statement(query)?
665 {
666 return self.execute_create_analytics_source(query, create_source);
667 }
668
669 if let Some(path) = super::metric_descriptor_catalog::parse_read_metric_statement(query) {
675 return Err(super::metric_descriptor_catalog::read_output_unsupported(
676 &path,
677 ));
678 }
679
680 if let Some(parsed) = super::ranking_descriptor_catalog::parse_create_ranking(query) {
685 return self.execute_create_ranking(query, parsed?);
686 }
687 if super::ranking_descriptor_catalog::parse_show_rankings(query) {
688 return self.execute_show_rankings(query);
689 }
690
691 let rewritten_query = super::red_schema::rewrite_virtual_names(query);
692 let execution_query = rewritten_query.as_deref().unwrap_or(query);
693
694 let frame = super::statement_frame::StatementExecutionFrame::build(
695 self,
696 super::statement_frame::StatementIdentity::Text(execution_query),
697 )?;
698 let _frame_guards = Box::new(frame.install(self));
699
700 let _log_span = crate::telemetry::span::query_span(query).entered();
707
708 if let Some(rewritten) = frame.prepare_cte(execution_query)? {
710 return self.execute_query_expr(rewritten);
711 }
712
713 if !self.inner.query_audit.has_rules() {
715 if let Some(result) = self.try_fast_entity_lookup(execution_query) {
716 return result;
717 }
718 }
719
720 if !self.inner.query_audit.has_rules() {
722 if let Some(result) = frame.read_result_cache(self) {
723 return Ok(result);
724 }
725 }
726
727 let prepared = frame.prepare_statement(self, execution_query)?;
728 let mode = prepared.mode;
729 let expr = prepared.expr;
730
731 let _query_budget = if super::query_budget::is_materialized_read(&expr) {
732 Some(self.query_budget_scope()?)
733 } else {
734 None
735 };
736
737 let statement = query_expr_name(&expr);
738 let result_cache_scopes = query_expr_result_cache_scopes(&expr);
739 let control_event_specs = query_control_event_specs(&expr);
740 let query_audit_plan = query_audit_plan(&expr);
741
742 let _lock_guard = match frame.prepare_dispatch(self, &expr) {
743 Ok(guard) => guard,
744 Err(err) => {
745 let outcome = control_event_outcome_for_error(&err);
746 for spec in &control_event_specs {
747 self.emit_control_event(
748 spec.kind,
749 outcome,
750 spec.action,
751 spec.resource.clone(),
752 Some(err.to_string()),
753 spec.fields.clone(),
754 )?;
755 }
756 return Err(err);
757 }
758 };
759 let query_audit_started = std::time::Instant::now();
760
761 let query_result = match expr {
762 QueryExpr::Function(ref command) => self.execute_function(query, command),
763 QueryExpr::Graph(_) | QueryExpr::Path(_) => {
764 self.execute_materialized_graph(query, mode, statement, &expr)
765 }
766 QueryExpr::Table(table) => {
767 let table = self.resolve_table_expr_subqueries(
768 table,
769 &frame as &dyn super::statement_frame::ReadFrame,
770 )?;
771 if let Some(TableSource::Function {
775 name,
776 args,
777 named_args,
778 }) = table.source.clone()
779 {
780 let tvf_result = RuntimeQueryResult {
788 query: query.to_string(),
789 mode,
790 statement,
791 engine: "runtime-graph-tvf",
792 result: self.execute_table_function(&name, &args, &named_args)?,
793 affected_rows: 0,
794 statement_type: "select",
795 bookmark: None,
796 notice: None,
797 };
798 frame.write_result_cache(self, &tvf_result, result_cache_scopes.clone());
799 return Ok(tvf_result);
800 }
801 if let Some(TableSource::InlineGraphFunction {
809 name,
810 nodes,
811 edges,
812 named_args,
813 }) = table.source.clone()
814 {
815 let inline_result = RuntimeQueryResult {
816 query: query.to_string(),
817 mode,
818 statement,
819 engine: "runtime-graph-tvf-inline",
820 result: self.execute_inline_graph_function(
821 &name,
822 &nodes,
823 &edges,
824 &named_args,
825 )?,
826 affected_rows: 0,
827 statement_type: "select",
828 bookmark: None,
829 notice: None,
830 };
831 frame.write_result_cache(self, &inline_result, result_cache_scopes);
832 return Ok(inline_result);
833 }
834 if super::red_schema::is_virtual_table(&table.table) {
835 return Ok(RuntimeQueryResult {
836 query: query.to_string(),
837 mode,
838 statement,
839 engine: "runtime-red-schema",
840 result: super::red_schema::red_query(
841 self,
842 &table.table,
843 &table,
844 &frame as &dyn super::statement_frame::ReadFrame,
845 )?,
846 affected_rows: 0,
847 statement_type: "select",
848 bookmark: None,
849 notice: None,
850 });
851 }
852
853 if let Some(view_result) = self.try_resolve_analytics_view(
857 &table,
858 &frame as &dyn super::statement_frame::ReadFrame,
859 )? {
860 return Ok(RuntimeQueryResult {
861 query: query.to_string(),
862 mode,
863 statement,
864 engine: "runtime-graph-analytics-view",
865 result: view_result,
866 affected_rows: 0,
867 statement_type: "select",
868 bookmark: None,
869 notice: None,
870 });
871 }
872
873 if let Some(result) = self.execute_probabilistic_select(&table)? {
874 return Ok(RuntimeQueryResult {
875 query: query.to_string(),
876 mode,
877 statement,
878 engine: "runtime-probabilistic",
879 result,
880 affected_rows: 0,
881 statement_type: "select",
882 bookmark: None,
883 notice: None,
884 });
885 }
886
887 if self.inner.foreign_tables.is_foreign_table(&table.table) {
895 let records = self
896 .inner
897 .foreign_tables
898 .scan(&table.table)
899 .map_err(|e| RedDBError::Internal(e.to_string()))?;
900 let result = apply_foreign_table_filters(records, &table);
901 return Ok(RuntimeQueryResult {
902 query: query.to_string(),
903 mode,
904 statement,
905 engine: "runtime-fdw",
906 result,
907 affected_rows: 0,
908 statement_type: "select",
909 bookmark: None,
910 notice: None,
911 });
912 }
913
914 let Some(table_with_rls) = self.authorize_relational_table_select(
931 table,
932 &frame as &dyn super::statement_frame::ReadFrame,
933 )?
934 else {
935 let empty = crate::storage::query::unified::UnifiedResult::empty();
936 return Ok(RuntimeQueryResult {
937 query: query.to_string(),
938 mode,
939 statement,
940 engine: "runtime-table-rls",
941 result: empty,
942 affected_rows: 0,
943 statement_type: "select",
944 bookmark: None,
945 notice: None,
946 });
947 };
948 Ok(RuntimeQueryResult {
949 query: query.to_string(),
950 mode,
951 statement,
952 engine: "runtime-table",
953 result: execute_runtime_table_query_in(
960 &self.inner.db,
961 &table_with_rls,
962 Some(&self.inner.index_store),
963 Some(frame.row_arena()),
964 )?,
965 affected_rows: 0,
966 statement_type: "select",
967 bookmark: None,
968 notice: None,
969 })
970 }
971 QueryExpr::Join(join) => {
972 let join_with_rls = match self.authorize_relational_join_select(
981 join,
982 &frame as &dyn super::statement_frame::ReadFrame,
983 )? {
984 Some(j) => j,
985 None => {
986 return Ok(RuntimeQueryResult {
987 query: query.to_string(),
988 mode,
989 statement,
990 engine: "runtime-join-rls",
991 result: crate::storage::query::unified::UnifiedResult::empty(),
992 affected_rows: 0,
993 statement_type: "select",
994 bookmark: None,
995 notice: None,
996 });
997 }
998 };
999 Ok(RuntimeQueryResult {
1000 query: query.to_string(),
1001 mode,
1002 statement,
1003 engine: "runtime-join",
1004 result: execute_runtime_join_query(self, &join_with_rls)?,
1005 affected_rows: 0,
1006 statement_type: "select",
1007 bookmark: None,
1008 notice: None,
1009 })
1010 }
1011 QueryExpr::Vector(vector) => Ok(RuntimeQueryResult {
1012 query: query.to_string(),
1013 mode,
1014 statement,
1015 engine: "runtime-vector",
1016 result: execute_runtime_vector_query(self, &vector)?,
1017 affected_rows: 0,
1018 statement_type: "select",
1019 bookmark: None,
1020 notice: None,
1021 }),
1022 QueryExpr::Hybrid(hybrid) => Ok(RuntimeQueryResult {
1023 query: query.to_string(),
1024 mode,
1025 statement,
1026 engine: "runtime-hybrid",
1027 result: execute_runtime_hybrid_query(self, &hybrid)?,
1028 affected_rows: 0,
1029 statement_type: "select",
1030 bookmark: None,
1031 notice: None,
1032 }),
1033 QueryExpr::RankOf(ref rank) => self.execute_rank_of(query, rank),
1034 QueryExpr::ApproxRankOf(ref rank) => self.execute_approx_rank_of(query, rank),
1035 QueryExpr::RankRange(ref range) => self.execute_rank_range(query, range),
1036 QueryExpr::Insert(ref insert) if super::red_schema::is_virtual_table(&insert.table) => {
1038 Err(RedDBError::Query(
1039 super::red_schema::READ_ONLY_ERROR.to_string(),
1040 ))
1041 }
1042 QueryExpr::Update(ref update) if super::red_schema::is_virtual_table(&update.table) => {
1043 Err(RedDBError::Query(
1044 super::red_schema::READ_ONLY_ERROR.to_string(),
1045 ))
1046 }
1047 QueryExpr::Delete(ref delete) if super::red_schema::is_virtual_table(&delete.table) => {
1048 Err(RedDBError::Query(
1049 super::red_schema::READ_ONLY_ERROR.to_string(),
1050 ))
1051 }
1052 QueryExpr::Insert(ref insert) => self
1053 .with_deferred_store_wal_for_dml(self.insert_may_emit_events(insert), || {
1054 self.execute_insert(query, insert)
1055 }),
1056 QueryExpr::Update(ref update) => self
1057 .with_deferred_store_wal_for_dml(self.update_may_emit_events(update), || {
1058 self.execute_update(query, update)
1059 }),
1060 QueryExpr::Delete(ref delete) => self
1061 .with_deferred_store_wal_for_dml(self.delete_may_emit_events(delete), || {
1062 self.execute_delete(query, delete)
1063 }),
1064 QueryExpr::CreateTable(ref create) => self.execute_create_table(query, create),
1066 QueryExpr::CreateCollection(ref create) => {
1067 self.execute_create_collection(query, create)
1068 }
1069 QueryExpr::CreateVector(ref create) => self.execute_create_vector(query, create),
1070 QueryExpr::DropTable(ref drop_tbl) => self.execute_drop_table(query, drop_tbl),
1071 QueryExpr::DropGraph(ref drop_graph) => self.execute_drop_graph(query, drop_graph),
1072 QueryExpr::DropVector(ref drop_vector) => self.execute_drop_vector(query, drop_vector),
1073 QueryExpr::DropDocument(ref drop_document) => {
1074 self.execute_drop_document(query, drop_document)
1075 }
1076 QueryExpr::DropKv(ref drop_kv) => self.execute_drop_kv(query, drop_kv),
1077 QueryExpr::DropCollection(ref drop_collection) => {
1078 self.execute_drop_collection(query, drop_collection)
1079 }
1080 QueryExpr::Truncate(ref truncate) => self.execute_truncate(query, truncate),
1081 QueryExpr::AlterTable(ref alter) => self.execute_alter_table(query, alter),
1082 QueryExpr::CreateVcsRef(ref create) => self.execute_create_vcs_ref(query, create),
1083 QueryExpr::DropVcsRef(ref drop_ref) => self.execute_drop_vcs_ref(query, drop_ref),
1084 QueryExpr::ForkStore(ref fork) => self.execute_fork_store(query, fork),
1085 QueryExpr::PromoteFork(ref promote_fork) => {
1086 self.execute_promote_fork(query, promote_fork)
1087 }
1088 QueryExpr::DropFork(ref drop_fork) => self.execute_drop_fork(query, drop_fork),
1089 QueryExpr::ExplainAlter(ref explain) => self.execute_explain_alter(query, explain),
1090 QueryExpr::GraphCommand(ref cmd) => self.execute_graph_command(query, cmd),
1092 QueryExpr::SearchCommand(ref cmd) => self.execute_search_command(query, cmd),
1094 QueryExpr::Ask(ref ask) => self.execute_ask(query, ask),
1096 QueryExpr::CreateIndex(ref create_idx) => self.execute_create_index(query, create_idx),
1097 QueryExpr::DropIndex(ref drop_idx) => self.execute_drop_index(query, drop_idx),
1098 QueryExpr::ProbabilisticCommand(ref cmd) => {
1099 self.execute_probabilistic_command(query, cmd)
1100 }
1101 QueryExpr::CreateTimeSeries(ref ts) => self.execute_create_timeseries(query, ts),
1103 QueryExpr::CreateMetric(ref metric) => self.execute_create_metric(query, metric),
1104 QueryExpr::AlterMetric(ref alter) => self.execute_alter_metric(query, alter),
1105 QueryExpr::CreateSlo(ref slo) => self.execute_create_slo(query, slo),
1106 QueryExpr::DropTimeSeries(ref ts) => self.execute_drop_timeseries(query, ts),
1107 QueryExpr::CreateQueue(ref q) => self.execute_create_queue(query, q),
1109 QueryExpr::AlterQueue(ref q) => self.execute_alter_queue(query, q),
1110 QueryExpr::DropQueue(ref q) => self.execute_drop_queue(query, q),
1111 QueryExpr::QueueSelect(ref q) => self.execute_queue_select(query, q),
1112 QueryExpr::QueueCommand(ref cmd) => self
1113 .with_deferred_store_wal_if_transaction(|| self.execute_queue_command(query, cmd)),
1114 QueryExpr::EventsBackfill(ref backfill) => {
1115 self.execute_events_backfill(query, backfill)
1116 }
1117 QueryExpr::EventsBackfillStatus { ref collection } => Err(RedDBError::Query(format!(
1118 "EVENTS BACKFILL STATUS for '{collection}' is not implemented in this slice"
1119 ))),
1120 QueryExpr::KvCommand(ref cmd) => self.execute_kv_command(query, cmd),
1121 QueryExpr::ConfigCommand(ref cmd) => self.execute_config_command(query, cmd),
1122 QueryExpr::CreateTree(ref tree) => self.execute_create_tree(query, tree),
1123 QueryExpr::DropTree(ref tree) => self.execute_drop_tree(query, tree),
1124 QueryExpr::TreeCommand(ref cmd) => self.execute_tree_command(query, cmd),
1125 QueryExpr::SetConfig { ref key, ref value } => {
1127 if key.starts_with("red.secret.") {
1128 return Err(RedDBError::Query(
1129 "red.secret.* is reserved for vault secrets; use SET SECRET".to_string(),
1130 ));
1131 }
1132 if key.starts_with("red.secrets.") {
1133 return Err(RedDBError::Query(
1134 "red.secrets.* is reserved for vault secrets; use SET SECRET".to_string(),
1135 ));
1136 }
1137 crate::ai::validate_ai_config_key_on_write(key)?;
1141 match self.check_managed_config_write_for_set_config(key) {
1146 Err(err) => Err(err),
1147 Ok(()) => {
1148 self.check_config_write_privilege(key)?;
1149 let store = self.inner.db.store();
1150 let json_val = match value {
1151 Value::Text(s) => crate::serde_json::Value::String(s.to_string()),
1152 Value::Integer(n) => crate::serde_json::Value::Number(*n as f64),
1153 Value::Float(n) => crate::serde_json::Value::Number(*n),
1154 Value::Boolean(b) => crate::serde_json::Value::Bool(*b),
1155 _ => crate::serde_json::Value::String(value.to_string()),
1156 };
1157 store.set_config_tree(key, &json_val);
1158 update_current_config_value(key, value.clone());
1159 self.invalidate_result_cache();
1164 Ok(RuntimeQueryResult::ok_message(
1165 query.to_string(),
1166 &format!("config set: {key}"),
1167 "set",
1168 ))
1169 }
1170 }
1171 }
1172 QueryExpr::SetSecret { ref key, ref value } => {
1174 if key.starts_with("red.config.") {
1175 return Err(RedDBError::Query(
1176 "red.config.* is reserved for config; use SET CONFIG".to_string(),
1177 ));
1178 }
1179 reject_engine_internal_vault_key(key)?;
1180 if key.starts_with("red.secret.") {
1181 self.set_internal_provider_secret(
1182 key,
1183 if value.is_null() {
1184 None
1185 } else {
1186 Some(secret_sql_value_to_string(value)?)
1187 },
1188 )?;
1189 return Ok(RuntimeQueryResult::ok_message(
1190 query.to_string(),
1191 &format!("secret set: {key}"),
1192 "set_secret",
1193 ));
1194 }
1195 let value = if value.is_null() {
1196 None
1197 } else {
1198 Some(secret_sql_value_to_string(value)?)
1199 };
1200 self.set_user_secret(key, value.clone())?;
1201 update_current_secret_value(key, value);
1202 Ok(RuntimeQueryResult::ok_message(
1203 query.to_string(),
1204 &format!("secret set: {key}"),
1205 "set_secret",
1206 ))
1207 }
1208 QueryExpr::DeleteSecret { ref key } => {
1210 reject_engine_internal_vault_key(key)?;
1211 if key.starts_with("red.secret.") {
1212 self.set_internal_provider_secret(key, None)?;
1213 return Ok(RuntimeQueryResult::ok_message(
1214 query.to_string(),
1215 &format!("secret deleted: {key}"),
1216 "delete_secret",
1217 ));
1218 }
1219 self.set_user_secret(key, None)?;
1220 update_current_secret_value(key, None);
1221 Ok(RuntimeQueryResult::ok_message(
1222 query.to_string(),
1223 &format!("secret deleted: {key}"),
1224 "delete_secret",
1225 ))
1226 }
1227 QueryExpr::SetKv { ref key, ref value } => {
1229 let auth_store = self.inner.auth_store.read().clone().ok_or_else(|| {
1230 RedDBError::Query("SET KV requires an auth store".to_string())
1231 })?;
1232 self.check_kv_write_privilege(&auth_store, key)?;
1233 if matches!(value, Value::Null) {
1235 auth_store.plain_kv_delete(key);
1236 update_current_kv_value(key, None);
1237 self.invalidate_result_cache();
1238 return Ok(RuntimeQueryResult::ok_message(
1239 query.to_string(),
1240 &format!("kv deleted: {key}"),
1241 "delete_kv",
1242 ));
1243 }
1244 let value = secret_sql_value_to_string(value)?;
1245 auth_store.plain_kv_set(key.clone(), value.clone());
1246 update_current_kv_value(key, Some(value));
1247 self.invalidate_result_cache();
1248 Ok(RuntimeQueryResult::ok_message(
1249 query.to_string(),
1250 &format!("kv set: {key}"),
1251 "set_kv",
1252 ))
1253 }
1254 QueryExpr::DeleteKv { ref key } => {
1256 let auth_store = self.inner.auth_store.read().clone().ok_or_else(|| {
1257 RedDBError::Query("DELETE KV requires an auth store".to_string())
1258 })?;
1259 self.check_kv_write_privilege(&auth_store, key)?;
1260 let deleted = auth_store.plain_kv_delete(key);
1261 if deleted {
1262 update_current_kv_value(key, None);
1263 }
1264 self.invalidate_result_cache();
1265 Ok(RuntimeQueryResult::ok_message(
1266 query.to_string(),
1267 &format!("kv deleted: {key}"),
1268 if deleted {
1269 "delete_kv"
1270 } else {
1271 "delete_kv_not_found"
1272 },
1273 ))
1274 }
1275 QueryExpr::Scrub { background, budget } => {
1276 self.execute_scrub_query(query, mode, background, budget)
1277 }
1278 QueryExpr::ShowSecrets { ref prefix } => {
1280 let result = self.show_user_secrets(prefix.as_deref())?;
1281 Ok(RuntimeQueryResult {
1282 query: query.to_string(),
1283 mode,
1284 statement: "show_secrets",
1285 engine: "runtime-secret",
1286 result,
1287 affected_rows: 0,
1288 statement_type: "select",
1289 bookmark: None,
1290 notice: None,
1291 })
1292 }
1293 QueryExpr::ShowConfig {
1295 ref prefix,
1296 as_json,
1297 } => {
1298 self.check_config_read_privilege()?;
1299 let store = self.inner.db.store();
1300 let all_collections = store.list_collections();
1301 if !all_collections.contains(&"red.config".to_string()) {
1302 if as_json {
1303 return Ok(show_config_json_result(
1304 query,
1305 mode,
1306 prefix,
1307 crate::serde_json::Value::Object(crate::serde_json::Map::new()),
1308 ));
1309 }
1310 let result = UnifiedResult::with_columns(vec!["key".into(), "value".into()]);
1311 return Ok(RuntimeQueryResult {
1312 query: query.to_string(),
1313 mode,
1314 statement: "show_config",
1315 engine: "runtime-config",
1316 result,
1317 affected_rows: 0,
1318 statement_type: "select",
1319 bookmark: None,
1320 notice: None,
1321 });
1322 }
1323 let manager = store
1324 .get_collection("red.config")
1325 .ok_or_else(|| RedDBError::NotFound("red.config".to_string()))?;
1326 let entities = manager.query_all(|_| true);
1327 let mut latest = std::collections::BTreeMap::<String, (u64, Value, Value)>::new();
1328 for entity in entities {
1329 if let EntityData::Row(ref row) = entity.data {
1330 if let Some(ref named) = row.named {
1331 let key_val = named.get("key").cloned().unwrap_or(Value::Null);
1332 let val = named.get("value").cloned().unwrap_or(Value::Null);
1333 let key_str = match &key_val {
1334 Value::Text(s) => s.as_ref(),
1335 _ => continue,
1336 };
1337 if key_str == super::function_catalog::REGISTRY_KEY {
1338 continue;
1339 }
1340 if let Some(ref pfx) = prefix {
1341 if !key_str.starts_with(pfx.as_str()) {
1342 continue;
1343 }
1344 }
1345 let entity_id = entity.id.raw();
1346 match latest.get(key_str) {
1347 Some((prev_id, _, _)) if *prev_id > entity_id => {}
1348 _ => {
1349 latest.insert(key_str.to_string(), (entity_id, key_val, val));
1350 }
1351 }
1352 }
1353 }
1354 }
1355 if as_json {
1356 let mut tree = crate::serde_json::Value::Object(crate::serde_json::Map::new());
1357 for (key, (_, _, val)) in latest {
1358 let relative = match prefix {
1359 Some(pfx) if key == *pfx => "",
1360 Some(pfx) => key
1361 .strip_prefix(pfx.as_str())
1362 .and_then(|tail| tail.strip_prefix('.'))
1363 .unwrap_or(key.as_str()),
1364 None => key.as_str(),
1365 };
1366 insert_config_json_path(&mut tree, relative, val.to_json());
1367 }
1368 return Ok(show_config_json_result(query, mode, prefix, tree));
1369 }
1370 let mut result = UnifiedResult::with_columns(vec!["key".into(), "value".into()]);
1371 for (_, key_val, val) in latest.into_values() {
1372 let mut record = UnifiedRecord::new();
1373 record.set("key", key_val);
1374 record.set("value", val);
1375 result.push(record);
1376 }
1377 Ok(RuntimeQueryResult {
1378 query: query.to_string(),
1379 mode,
1380 statement: "show_config",
1381 engine: "runtime-config",
1382 result,
1383 affected_rows: 0,
1384 statement_type: "select",
1385 bookmark: None,
1386 notice: None,
1387 })
1388 }
1389 QueryExpr::SetTenant(ref value) => {
1395 self.check_set_tenant_privilege(value.as_deref())?;
1396 match value {
1397 Some(id) => set_current_tenant(id.clone()),
1398 None => clear_current_tenant(),
1399 }
1400 Ok(RuntimeQueryResult::ok_message(
1401 query.to_string(),
1402 &match value {
1403 Some(id) => format!("tenant set: {id}"),
1404 None => "tenant cleared".to_string(),
1405 },
1406 "set_tenant",
1407 ))
1408 }
1409 QueryExpr::ShowTenant => {
1410 let mut result = UnifiedResult::with_columns(vec!["tenant".into()]);
1411 let mut record = UnifiedRecord::new();
1412 record.set(
1413 "tenant",
1414 current_tenant().map(Value::text).unwrap_or(Value::Null),
1415 );
1416 result.push(record);
1417 Ok(RuntimeQueryResult {
1418 query: query.to_string(),
1419 mode,
1420 statement: "show_tenant",
1421 engine: "runtime-tenant",
1422 result,
1423 affected_rows: 0,
1424 statement_type: "select",
1425 bookmark: None,
1426 notice: None,
1427 })
1428 }
1429 QueryExpr::TransactionControl(ref ctl) => {
1441 use crate::storage::transaction::IsolationLevel;
1442 use reddb_rql::ast::TxnControl;
1443
1444 let conn_id = current_connection_id();
1449
1450 let (kind, msg) = match ctl {
1451 TxnControl::Begin(requested_isolation) => {
1452 if self.inner.transaction_state.in_transaction(conn_id) {
1453 return Err(RedDBError::Query("transaction already active".into()));
1454 }
1455 let isolation = requested_isolation
1456 .map(IsolationLevel::from)
1457 .unwrap_or(IsolationLevel::SnapshotIsolation);
1458 let xid = self.inner.transaction_state.begin(conn_id, isolation);
1459 ("begin", format!("BEGIN — xid={xid} (snapshot isolation)"))
1460 }
1461 TxnControl::Commit => {
1462 let has_vault_writes = self
1463 .inner
1464 .pending_vault_writes
1465 .read()
1466 .get(&conn_id)
1467 .is_some_and(|writes| !writes.is_empty());
1468 let store = self.inner.db.store();
1469 let _vault_guard =
1470 has_vault_writes.then(|| store.vault_publication_lock.lock());
1471 let context = self.inner.transaction_state.commit(conn_id, |ctx| {
1472 let mut own_xids = std::collections::HashSet::new();
1473 own_xids.insert(ctx.xid);
1474 for (_, sub) in &ctx.savepoints {
1475 own_xids.insert(*sub);
1476 }
1477 for sub in &ctx.released_sub_xids {
1478 own_xids.insert(*sub);
1479 }
1480 if let Err(err) = self.check_table_row_write_conflicts(
1481 conn_id,
1482 &ctx.snapshot,
1483 &own_xids,
1484 ctx.isolation,
1485 ) {
1486 let undo = self.revive_pending_versioned_updates(conn_id);
1487 self.revive_pending_tombstones(conn_id);
1488 self.discard_pending_kv_watch_events(conn_id);
1489 self.discard_pending_queue_wakes(conn_id);
1490 self.discard_pending_store_wal_actions(conn_id);
1491 self.release_pending_claim_locks(conn_id);
1492 undo?;
1493 return Err(err);
1494 }
1495 if let Err(err) = self.check_queue_dedup_write_conflicts(
1496 conn_id,
1497 &ctx.snapshot,
1498 &own_xids,
1499 ) {
1500 let undo = self.revive_pending_versioned_updates(conn_id);
1501 self.revive_pending_tombstones(conn_id);
1502 self.discard_pending_queue_dedup(conn_id);
1503 self.discard_pending_kv_watch_events(conn_id);
1504 self.discard_pending_queue_wakes(conn_id);
1505 self.discard_pending_store_wal_actions(conn_id);
1506 self.release_pending_claim_locks(conn_id);
1507 undo?;
1508 return Err(err);
1509 }
1510 if let Err(err) = self.prepare_pending_vault_writes(conn_id, ctx) {
1511 let undo = self.revive_pending_versioned_updates(conn_id);
1512 self.revive_pending_tombstones(conn_id);
1513 self.discard_pending_queue_dedup(conn_id);
1514 self.discard_pending_kv_watch_events(conn_id);
1515 self.discard_pending_queue_wakes(conn_id);
1516 self.discard_pending_store_wal_actions(conn_id);
1517 self.release_pending_claim_locks(conn_id);
1518 undo?;
1519 return Err(err);
1520 }
1521 let publication = store.vault_publication_lock.lock();
1526 self.restore_pending_write_stamps(conn_id);
1527 if let Err(err) = self.flush_pending_store_wal_actions(conn_id) {
1528 drop(publication);
1529 let undo = self.revive_pending_versioned_updates(conn_id);
1530 self.revive_pending_tombstones(conn_id);
1531 self.discard_pending_queue_dedup(conn_id);
1532 self.discard_pending_kv_watch_events(conn_id);
1533 self.discard_pending_queue_wakes(conn_id);
1534 self.release_pending_claim_locks(conn_id);
1535 undo?;
1536 return Err(err);
1537 }
1538 Ok(publication)
1539 });
1540 if context.is_err() {
1541 self.discard_pending_vault_writes(conn_id)?;
1542 }
1543 let context = context?;
1544 match context {
1545 Some(ctx) => {
1546 self.finalize_pending_vault_writes(conn_id);
1547 self.finalize_pending_versioned_updates(conn_id);
1548 self.finalize_pending_tombstones(conn_id);
1549 self.finalize_pending_queue_dedup(conn_id);
1550 self.finalize_pending_kv_watch_events(conn_id);
1551 self.finalize_pending_queue_wakes(conn_id);
1552 self.release_pending_claim_locks(conn_id);
1553 self.recover_transactional_replication().map_err(|error| {
1554 RedDBError::Internal(format!(
1555 "commit is locally durable; replication publication is pending: {error}"
1556 ))
1557 })?;
1558 ("commit", format!("COMMIT — xid={} committed", ctx.xid))
1559 }
1560 None => (
1561 "commit",
1562 "COMMIT outside transaction — no-op (autocommit)".to_string(),
1563 ),
1564 }
1565 }
1566 TxnControl::Rollback => match self.rollback_connection(conn_id)? {
1567 Some(ctx) => ("rollback", format!("ROLLBACK — xid={} aborted", ctx.xid)),
1568 None => (
1569 "rollback",
1570 "ROLLBACK outside transaction — no-op (autocommit)".to_string(),
1571 ),
1572 },
1573 TxnControl::Savepoint(name) => {
1580 match self.inner.transaction_state.savepoint(conn_id, name) {
1581 Some(sub) => ("savepoint", format!("SAVEPOINT {name} — sub_xid={sub}")),
1582 None => (
1583 "savepoint",
1584 "SAVEPOINT outside transaction — no-op".to_string(),
1585 ),
1586 }
1587 }
1588 TxnControl::ReleaseSavepoint(name) => {
1589 match self
1590 .inner
1591 .transaction_state
1592 .release_savepoint(conn_id, name)?
1593 {
1594 Some(released) => (
1595 "release_savepoint",
1596 format!("RELEASE SAVEPOINT {name} — {released} level(s)"),
1597 ),
1598 None => (
1599 "release_savepoint",
1600 "RELEASE outside transaction — no-op".to_string(),
1601 ),
1602 }
1603 }
1604 TxnControl::RollbackToSavepoint(name) => {
1605 match self
1606 .inner
1607 .transaction_state
1608 .rollback_to_savepoint_with(conn_id, name, |rollback| {
1609 self.rollback_vault_savepoint(conn_id, rollback.savepoint_xid);
1610 let reverted_updates = self.revive_versioned_updates_since(
1611 conn_id,
1612 rollback.savepoint_xid,
1613 );
1614 let revived =
1615 self.revive_tombstones_since(conn_id, rollback.savepoint_xid);
1616 reverted_updates.map(|updates| (updates, revived))
1617 })?
1618 {
1619 Some((rollback, (reverted_updates, revived))) => {
1620 (
1621 "rollback_to_savepoint",
1622 format!(
1623 "ROLLBACK TO SAVEPOINT {name} — aborted {} sub_xid(s), reverted {reverted_updates} update(s), revived {revived} tombstone(s)",
1624 rollback.aborted_xids.len(),
1625 ),
1626 )
1627 }
1628 None => (
1629 "rollback_to_savepoint",
1630 "ROLLBACK TO outside transaction — no-op".to_string(),
1631 ),
1632 }
1633 }
1634 };
1635 Ok(RuntimeQueryResult::ok_message(
1636 query.to_string(),
1637 &msg,
1638 kind,
1639 ))
1640 }
1641 QueryExpr::CreateSchema(ref q) => {
1654 let store = self.inner.db.store();
1655 let key = format!("schema.{}", q.name);
1656 if store.get_config(&key).is_some() {
1657 if q.if_not_exists {
1658 return Ok(RuntimeQueryResult::ok_message(
1659 query.to_string(),
1660 &format!("schema {} already exists — skipped", q.name),
1661 "create_schema",
1662 ));
1663 }
1664 return Err(RedDBError::Internal(format!(
1665 "schema {} already exists",
1666 q.name
1667 )));
1668 }
1669 store.set_config_tree(&key, &crate::serde_json::Value::Bool(true));
1670 Ok(RuntimeQueryResult::ok_message(
1671 query.to_string(),
1672 &format!("schema {} created", q.name),
1673 "create_schema",
1674 ))
1675 }
1676 QueryExpr::DropSchema(ref q) => {
1677 let store = self.inner.db.store();
1678 let key = format!("schema.{}", q.name);
1679 let existed = store.get_config(&key).is_some();
1680 if !existed && !q.if_exists {
1681 return Err(RedDBError::Internal(format!(
1682 "schema {} does not exist",
1683 q.name
1684 )));
1685 }
1686 store.set_config_tree(&key, &crate::serde_json::Value::Null);
1688 let suffix = if q.cascade {
1689 " (CASCADE accepted — tables untouched)"
1690 } else {
1691 ""
1692 };
1693 Ok(RuntimeQueryResult::ok_message(
1694 query.to_string(),
1695 &format!("schema {} dropped{}", q.name, suffix),
1696 "drop_schema",
1697 ))
1698 }
1699 QueryExpr::CreateSequence(ref q) => {
1700 let store = self.inner.db.store();
1701 let base = format!("sequence.{}", q.name);
1702 let start_key = format!("{base}.start");
1703 let incr_key = format!("{base}.increment");
1704 let curr_key = format!("{base}.current");
1705 if store.get_config(&start_key).is_some() {
1706 if q.if_not_exists {
1707 return Ok(RuntimeQueryResult::ok_message(
1708 query.to_string(),
1709 &format!("sequence {} already exists — skipped", q.name),
1710 "create_sequence",
1711 ));
1712 }
1713 return Err(RedDBError::Internal(format!(
1714 "sequence {} already exists",
1715 q.name
1716 )));
1717 }
1718 let initial_current = q.start - q.increment;
1721 store.set_config_tree(
1722 &start_key,
1723 &crate::serde_json::Value::Number(q.start as f64),
1724 );
1725 store.set_config_tree(
1726 &incr_key,
1727 &crate::serde_json::Value::Number(q.increment as f64),
1728 );
1729 store.set_config_tree(
1730 &curr_key,
1731 &crate::serde_json::Value::Number(initial_current as f64),
1732 );
1733 Ok(RuntimeQueryResult::ok_message(
1734 query.to_string(),
1735 &format!(
1736 "sequence {} created (start={}, increment={})",
1737 q.name, q.start, q.increment
1738 ),
1739 "create_sequence",
1740 ))
1741 }
1742 QueryExpr::DropSequence(ref q) => {
1743 let store = self.inner.db.store();
1744 let base = format!("sequence.{}", q.name);
1745 let existed = store.get_config(&format!("{base}.start")).is_some();
1746 if !existed && !q.if_exists {
1747 return Err(RedDBError::Internal(format!(
1748 "sequence {} does not exist",
1749 q.name
1750 )));
1751 }
1752 for k in ["start", "increment", "current"] {
1753 store.set_config_tree(&format!("{base}.{k}"), &crate::serde_json::Value::Null);
1754 }
1755 Ok(RuntimeQueryResult::ok_message(
1756 query.to_string(),
1757 &format!("sequence {} dropped", q.name),
1758 "drop_sequence",
1759 ))
1760 }
1761 QueryExpr::CreateView(ref q) => {
1771 let mut views = self.inner.views.write();
1772 if views.contains_key(&q.name) && !q.or_replace {
1773 if q.if_not_exists {
1774 return Ok(RuntimeQueryResult::ok_message(
1775 query.to_string(),
1776 &format!("view {} already exists — skipped", q.name),
1777 "create_view",
1778 ));
1779 }
1780 return Err(RedDBError::Internal(format!(
1781 "view {} already exists",
1782 q.name
1783 )));
1784 }
1785 views.insert(q.name.clone(), Arc::new(q.clone()));
1786 drop(views);
1787
1788 if q.materialized {
1790 use crate::storage::cache::result::{MaterializedViewDef, RefreshPolicy};
1791 let refresh = match q.refresh_every_ms {
1792 Some(ms) => RefreshPolicy::Periodic(std::time::Duration::from_millis(ms)),
1793 None => RefreshPolicy::Manual,
1794 };
1795 let dependencies = collect_table_refs(&q.query);
1796 let def = MaterializedViewDef {
1797 name: q.name.clone(),
1798 query: format!("<parsed view {}>", q.name),
1799 dependencies: dependencies.clone(),
1800 refresh,
1801 retention_duration_ms: q.retention_duration_ms,
1802 };
1803 self.inner.materialized_views.write().register(def);
1804
1805 let descriptor =
1811 crate::runtime::continuous_materialized_view::MaterializedViewDescriptor {
1812 name: q.name.clone(),
1813 source_sql: query.to_string(),
1814 source_collections: dependencies,
1815 refresh_every_ms: q.refresh_every_ms,
1816 retention_duration_ms: q.retention_duration_ms,
1817 };
1818 let store = self.inner.db.store();
1819 crate::runtime::continuous_materialized_view::persist_descriptor(
1820 store.as_ref(),
1821 &descriptor,
1822 )?;
1823
1824 self.ensure_materialized_view_backing(&q.name)?;
1831 }
1832 self.invalidate_plan_cache();
1837 self.invalidate_result_cache();
1838
1839 Ok(RuntimeQueryResult::ok_message(
1840 query.to_string(),
1841 &format!(
1842 "{}view {} created",
1843 if q.materialized { "materialized " } else { "" },
1844 q.name
1845 ),
1846 "create_view",
1847 ))
1848 }
1849 QueryExpr::DropView(ref q) => {
1850 let mut views = self.inner.views.write();
1851 let removed = views.remove(&q.name);
1852 let existed = removed.is_some();
1853 let removed_materialized =
1854 removed.as_ref().map(|v| v.materialized).unwrap_or(false);
1855 drop(views);
1856 if q.materialized || existed {
1857 self.inner.materialized_views.write().remove(&q.name);
1859 let store = self.inner.db.store();
1863 crate::runtime::continuous_materialized_view::remove_by_name(
1864 store.as_ref(),
1865 &q.name,
1866 )?;
1867 }
1868 if removed_materialized || q.materialized {
1872 self.drop_materialized_view_backing(&q.name)?;
1873 }
1874 self.invalidate_plan_cache();
1877 self.invalidate_result_cache();
1878 if !existed && !q.if_exists {
1879 return Err(RedDBError::Internal(format!(
1880 "view {} does not exist",
1881 q.name
1882 )));
1883 }
1884 self.invalidate_plan_cache();
1885 Ok(RuntimeQueryResult::ok_message(
1886 query.to_string(),
1887 &format!("view {} dropped", q.name),
1888 "drop_view",
1889 ))
1890 }
1891 QueryExpr::RefreshMaterializedView(ref q) => {
1892 let view = {
1895 let views = self.inner.views.read();
1896 views.get(&q.name).cloned()
1897 };
1898 let view = match view {
1899 Some(v) => v,
1900 None => {
1901 return Err(RedDBError::Internal(format!(
1902 "view {} does not exist",
1903 q.name
1904 )))
1905 }
1906 };
1907 if !view.materialized {
1908 return Err(RedDBError::Internal(format!(
1909 "view {} is not materialized — REFRESH requires \
1910 CREATE MATERIALIZED VIEW",
1911 q.name
1912 )));
1913 }
1914 let started = std::time::Instant::now();
1916 let now_ms = std::time::SystemTime::now()
1917 .duration_since(std::time::UNIX_EPOCH)
1918 .map(|d| d.as_millis() as u64)
1919 .unwrap_or(0);
1920 match self.execute_query_expr((*view.query).clone()) {
1921 Ok(inner_result) => {
1922 let entities =
1929 view_records_to_entities(&q.name, &inner_result.result.records);
1930 let row_count = entities.len() as u64;
1931 let store = self.inner.db.store();
1932 let serialized_records = match store.refresh_collection(&q.name, entities) {
1933 Ok(records) => records,
1934 Err(err) => {
1935 let duration_ms = started.elapsed().as_millis() as u64;
1936 let msg = err.to_string();
1937 self.inner
1938 .materialized_views
1939 .write()
1940 .record_refresh_failure(
1941 &q.name,
1942 msg.clone(),
1943 duration_ms,
1944 now_ms,
1945 );
1946 return Err(RedDBError::Internal(format!(
1947 "REFRESH MATERIALIZED VIEW {}: {msg}",
1948 q.name
1949 )));
1950 }
1951 };
1952
1953 if self.inner.db.replication.is_some() {
1959 let lsn = self.inner.cdc.emit(
1960 crate::replication::cdc::ChangeOperation::Refresh,
1961 &q.name,
1962 0,
1963 "refresh",
1964 );
1965 self.invalidate_result_cache_for_table(&q.name);
1966 let timestamp = std::time::SystemTime::now()
1967 .duration_since(std::time::UNIX_EPOCH)
1968 .unwrap_or_default()
1969 .as_millis() as u64;
1970 let record = ChangeRecord::for_refresh(
1971 lsn,
1972 timestamp,
1973 q.name.clone(),
1974 serialized_records,
1975 )
1976 .with_term(self.current_replication_term());
1977 self.stage_or_publish_logical_record(record)?;
1978 }
1979
1980 let duration_ms = started.elapsed().as_millis() as u64;
1981 let serialized = format!("{:?}", inner_result.result);
1982 self.inner
1983 .materialized_views
1984 .write()
1985 .record_refresh_success(
1986 &q.name,
1987 serialized.into_bytes(),
1988 row_count,
1989 duration_ms,
1990 now_ms,
1991 );
1992 self.invalidate_result_cache();
1997 Ok(RuntimeQueryResult::ok_message(
1998 query.to_string(),
1999 &format!("materialized view {} refreshed", q.name),
2000 "refresh_materialized_view",
2001 ))
2002 }
2003 Err(err) => {
2004 let duration_ms = started.elapsed().as_millis() as u64;
2005 let msg = err.to_string();
2006 self.inner
2007 .materialized_views
2008 .write()
2009 .record_refresh_failure(&q.name, msg.clone(), duration_ms, now_ms);
2010 Err(err)
2011 }
2012 }
2013 }
2014 QueryExpr::CreatePolicy(ref q) => {
2021 let key = (q.table.clone(), q.name.clone());
2022 self.inner
2023 .rls_policies
2024 .write()
2025 .insert(key, Arc::new(q.clone()));
2026 self.invalidate_plan_cache();
2027 self.invalidate_result_cache();
2028 self.schema_vocabulary_apply(
2032 crate::runtime::schema_vocabulary::DdlEvent::CreatePolicy {
2033 collection: q.table.clone(),
2034 policy: q.name.clone(),
2035 },
2036 );
2037 Ok(RuntimeQueryResult::ok_message(
2038 query.to_string(),
2039 &format!("policy {} on {} created", q.name, q.table),
2040 "create_policy",
2041 ))
2042 }
2043 QueryExpr::DropPolicy(ref q) => {
2044 let removed = self
2045 .inner
2046 .rls_policies
2047 .write()
2048 .remove(&(q.table.clone(), q.name.clone()))
2049 .is_some();
2050 if !removed && !q.if_exists {
2051 return Err(RedDBError::Internal(format!(
2052 "policy {} on {} does not exist",
2053 q.name, q.table
2054 )));
2055 }
2056 self.invalidate_plan_cache();
2057 self.invalidate_result_cache();
2058 self.schema_vocabulary_apply(
2061 crate::runtime::schema_vocabulary::DdlEvent::DropPolicy {
2062 collection: q.table.clone(),
2063 policy: q.name.clone(),
2064 },
2065 );
2066 Ok(RuntimeQueryResult::ok_message(
2067 query.to_string(),
2068 &format!("policy {} on {} dropped", q.name, q.table),
2069 "drop_policy",
2070 ))
2071 }
2072 QueryExpr::CreateServer(ref q) => {
2083 use crate::storage::fdw::FdwOptions;
2084 let registry = Arc::clone(&self.inner.foreign_tables);
2085 if registry.server(&q.name).is_some() {
2086 if q.if_not_exists {
2087 return Ok(RuntimeQueryResult::ok_message(
2088 query.to_string(),
2089 &format!("server {} already exists — skipped", q.name),
2090 "create_server",
2091 ));
2092 }
2093 return Err(RedDBError::Internal(format!(
2094 "server {} already exists",
2095 q.name
2096 )));
2097 }
2098 let mut opts = FdwOptions::new();
2099 for (k, v) in &q.options {
2100 opts.values.insert(k.clone(), v.clone());
2101 }
2102 registry
2103 .create_server(&q.name, &q.wrapper, opts)
2104 .map_err(|e| RedDBError::Internal(e.to_string()))?;
2105 Ok(RuntimeQueryResult::ok_message(
2106 query.to_string(),
2107 &format!("server {} created (wrapper {})", q.name, q.wrapper),
2108 "create_server",
2109 ))
2110 }
2111 QueryExpr::DropServer(ref q) => {
2112 let existed = self.inner.foreign_tables.drop_server(&q.name);
2113 if !existed && !q.if_exists {
2114 return Err(RedDBError::Internal(format!(
2115 "server {} does not exist",
2116 q.name
2117 )));
2118 }
2119 Ok(RuntimeQueryResult::ok_message(
2120 query.to_string(),
2121 &format!(
2122 "server {} dropped{}",
2123 q.name,
2124 if q.cascade { " (cascade)" } else { "" }
2125 ),
2126 "drop_server",
2127 ))
2128 }
2129 QueryExpr::CreateForeignTable(ref q) => {
2130 use crate::storage::fdw::{FdwOptions, ForeignColumn, ForeignTable};
2131 let registry = Arc::clone(&self.inner.foreign_tables);
2132 if registry.foreign_table(&q.name).is_some() {
2133 if q.if_not_exists {
2134 return Ok(RuntimeQueryResult::ok_message(
2135 query.to_string(),
2136 &format!("foreign table {} already exists — skipped", q.name),
2137 "create_foreign_table",
2138 ));
2139 }
2140 return Err(RedDBError::Internal(format!(
2141 "foreign table {} already exists",
2142 q.name
2143 )));
2144 }
2145 let mut opts = FdwOptions::new();
2146 for (k, v) in &q.options {
2147 opts.values.insert(k.clone(), v.clone());
2148 }
2149 let columns: Vec<ForeignColumn> = q
2150 .columns
2151 .iter()
2152 .map(|c| ForeignColumn {
2153 name: c.name.clone(),
2154 data_type: c.data_type.clone(),
2155 not_null: c.not_null,
2156 })
2157 .collect();
2158 registry
2159 .create_foreign_table(ForeignTable {
2160 name: q.name.clone(),
2161 server_name: q.server.clone(),
2162 columns,
2163 options: opts,
2164 })
2165 .map_err(|e| RedDBError::Internal(e.to_string()))?;
2166 self.invalidate_plan_cache();
2167 Ok(RuntimeQueryResult::ok_message(
2168 query.to_string(),
2169 &format!("foreign table {} created (server {})", q.name, q.server),
2170 "create_foreign_table",
2171 ))
2172 }
2173 QueryExpr::DropForeignTable(ref q) => {
2174 let existed = self.inner.foreign_tables.drop_foreign_table(&q.name);
2175 if !existed && !q.if_exists {
2176 return Err(RedDBError::Internal(format!(
2177 "foreign table {} does not exist",
2178 q.name
2179 )));
2180 }
2181 self.invalidate_plan_cache();
2182 Ok(RuntimeQueryResult::ok_message(
2183 query.to_string(),
2184 &format!("foreign table {} dropped", q.name),
2185 "drop_foreign_table",
2186 ))
2187 }
2188 QueryExpr::CopyFrom(ref q) => {
2194 use crate::storage::import::{CsvConfig, CsvImporter};
2195 self.check_copy_from_privilege(&q.table)?;
2196 confine_copy_from_path(&q.path)?;
2197 let store = self.inner.db.store();
2198 let cfg = CsvConfig {
2199 collection: q.table.clone(),
2200 has_header: q.has_header,
2201 delimiter: q.delimiter.map(|c| c as u8).unwrap_or(b','),
2202 ..CsvConfig::default()
2203 };
2204 let importer = CsvImporter::new(cfg);
2205 let stats = importer
2206 .import_file(&q.path, store.as_ref())
2207 .map_err(|e| RedDBError::Internal(format!("COPY failed: {e}")))?;
2208 self.note_table_write(&q.table);
2210 Ok(RuntimeQueryResult::ok_message(
2211 query.to_string(),
2212 &format!(
2213 "COPY imported {} rows into {} ({} errors skipped, {}ms)",
2214 stats.records_imported, q.table, stats.errors_skipped, stats.duration_ms
2215 ),
2216 "copy_from",
2217 ))
2218 }
2219 QueryExpr::MaintenanceCommand(ref cmd) => {
2235 use reddb_rql::ast::MaintenanceCommand as Mc;
2236 let store = self.inner.db.store();
2237 let (kind, msg) = match cmd {
2238 Mc::Analyze { target } => {
2239 let targets: Vec<String> = match target {
2240 Some(t) => vec![t.clone()],
2241 None => store.list_collections(),
2242 };
2243 for t in &targets {
2244 self.refresh_table_planner_stats(t);
2245 }
2246 (
2247 "analyze",
2248 format!("ANALYZE refreshed stats for {} table(s)", targets.len()),
2249 )
2250 }
2251 Mc::Vacuum { target, full } => {
2252 let targets: Vec<String> = match target {
2253 Some(t) => vec![t.clone()],
2254 None => store.list_collections(),
2255 };
2256 let cutoff_xid = self.mvcc_vacuum_cutoff_xid();
2257 let mut vacuum_stats =
2258 crate::storage::unified::store::MvccVacuumStats::default();
2259 for t in &targets {
2260 let stats = store.vacuum_mvcc_history(t, cutoff_xid).map_err(|e| {
2261 RedDBError::Internal(format!(
2262 "VACUUM MVCC history failed for {t}: {e}"
2263 ))
2264 })?;
2265 if stats.reclaimed_versions > 0 {
2266 self.rebuild_runtime_indexes_for_table(t)?;
2267 }
2268 vacuum_stats.add(&stats);
2269 }
2270 for t in &targets {
2275 self.refresh_table_planner_stats(t);
2276 }
2277 let persisted = if *full {
2281 match store.persist() {
2282 Ok(()) => true,
2283 Err(e) => {
2284 return Err(RedDBError::Internal(format!(
2285 "VACUUM FULL persist failed: {e:?}"
2286 )));
2287 }
2288 }
2289 } else {
2290 false
2291 };
2292 self.invalidate_result_cache();
2294 (
2295 "vacuum",
2296 format!(
2297 "VACUUM{} processed {} table(s): scanned_versions={}, retained_versions={}, reclaimed_versions={}, retained_history_versions={}, reclaimed_history_versions={}, retained_tombstones={}, reclaimed_tombstones={}{}",
2298 if *full { " FULL" } else { "" },
2299 targets.len(),
2300 vacuum_stats.scanned_versions,
2301 vacuum_stats.retained_versions,
2302 vacuum_stats.reclaimed_versions,
2303 vacuum_stats.retained_history_versions,
2304 vacuum_stats.reclaimed_history_versions,
2305 vacuum_stats.retained_tombstones,
2306 vacuum_stats.reclaimed_tombstones,
2307 if persisted {
2308 " (pages flushed to disk)"
2309 } else {
2310 ""
2311 }
2312 ),
2313 )
2314 }
2315 };
2316 Ok(RuntimeQueryResult::ok_message(
2317 query.to_string(),
2318 &msg,
2319 kind,
2320 ))
2321 }
2322 QueryExpr::Grant(ref g) => self.execute_grant_statement(query, g),
2329 QueryExpr::Revoke(ref r) => self.execute_revoke_statement(query, r),
2330 QueryExpr::AlterUser(ref a) => self.execute_alter_user_statement(query, a),
2331 QueryExpr::CreateUser(ref u) => self.execute_create_user_statement(query, u),
2332 QueryExpr::CreateIamPolicy { ref id, ref json } => {
2333 self.execute_create_iam_policy(query, id, json)
2334 }
2335 QueryExpr::DropIamPolicy { ref id } => self.execute_drop_iam_policy(query, id),
2336 QueryExpr::AttachPolicy {
2337 ref policy_id,
2338 ref principal,
2339 } => self.execute_attach_policy(query, policy_id, principal),
2340 QueryExpr::DetachPolicy {
2341 ref policy_id,
2342 ref principal,
2343 } => self.execute_detach_policy(query, policy_id, principal),
2344 QueryExpr::ShowPolicies { ref filter } => {
2345 self.execute_show_policies(query, filter.as_ref())
2346 }
2347 QueryExpr::ShowEffectivePermissions {
2348 ref user,
2349 ref resource,
2350 } => self.execute_show_effective_permissions(query, user, resource.as_ref()),
2351 QueryExpr::SimulatePolicy {
2352 ref user,
2353 ref action,
2354 ref resource,
2355 } => self.execute_simulate_policy(query, user, action, resource),
2356 QueryExpr::LintPolicy { ref source } => self.execute_lint_policy(query, source),
2357 QueryExpr::MigratePolicyMode {
2358 ref target,
2359 dry_run,
2360 } => self.execute_migrate_policy_mode(query, target, dry_run),
2361 QueryExpr::CreateMigration(ref q) => self.execute_create_migration(query, q),
2362 QueryExpr::ApplyMigration(ref q) => self.execute_apply_migration(query, q),
2363 QueryExpr::RollbackMigration(ref q) => self.execute_rollback_migration(query, q),
2364 QueryExpr::ExplainMigration(ref q) => self.execute_explain_migration(query, q),
2365 _ => Err(RedDBError::Query(
2366 "unsupported command in runtime dispatcher".to_string(),
2367 )),
2368 };
2369
2370 if _query_budget.is_some() {
2371 super::query_budget::charge(0)?;
2372 }
2373 if !control_event_specs.is_empty() {
2374 let (outcome, reason) = match &query_result {
2375 Ok(_) => (crate::runtime::control_events::Outcome::Allowed, None),
2376 Err(err) => (control_event_outcome_for_error(err), Some(err.to_string())),
2377 };
2378 for spec in &control_event_specs {
2379 self.emit_control_event(
2380 spec.kind,
2381 outcome,
2382 spec.action,
2383 spec.resource.clone(),
2384 reason.clone(),
2385 spec.fields.clone(),
2386 )?;
2387 }
2388 }
2389
2390 if let (Some(plan), Ok(result)) = (&query_audit_plan, &query_result) {
2391 self.emit_query_audit(
2392 query,
2393 plan,
2394 query_audit_started.elapsed().as_millis() as u64,
2395 result,
2396 );
2397 }
2398
2399 if let Ok(ref result) = query_result {
2406 frame.write_result_cache(self, result, result_cache_scopes);
2407 }
2408
2409 query_result
2410 }
2411
2412 pub fn execute_query_expr(&self, expr: QueryExpr) -> RedDBResult<RuntimeQueryResult> {
2432 let frame = super::statement_frame::StatementExecutionFrame::build(
2433 self,
2434 super::statement_frame::StatementIdentity::Expr(&expr),
2435 )?;
2436 let _frame_guards = Box::new(frame.install(self));
2437 let expr = self.rewrite_view_refs(expr);
2441
2442 self.validate_model_operations_before_auth(&expr)?;
2443 if let Err(err) = self.check_query_privilege(&expr) {
2447 return Err(RedDBError::Query(format!("permission denied: {err}")));
2448 }
2449
2450 frame.release_snapshot_for_maintenance(&expr);
2451 let statement = query_expr_name(&expr);
2452 let mode = detect_mode(statement);
2453 let query_str = statement;
2454
2455 self.dispatch_expr(expr, query_str, mode)
2456 }
2457
2458 pub(super) fn validate_model_operations_before_auth(
2459 &self,
2460 expr: &QueryExpr,
2461 ) -> RedDBResult<()> {
2462 self.validate_vault_table_access(expr)?;
2463 use crate::catalog::CollectionModel;
2464 use crate::runtime::ddl::polymorphic_resolver;
2465 use reddb_rql::ast::KvCommand;
2466
2467 let system_schema_target = match expr {
2468 QueryExpr::DropTable(q) => Some(q.name.as_str()),
2469 QueryExpr::DropGraph(q) => Some(q.name.as_str()),
2470 QueryExpr::DropVector(q) => Some(q.name.as_str()),
2471 QueryExpr::DropDocument(q) => Some(q.name.as_str()),
2472 QueryExpr::DropKv(q) => Some(q.name.as_str()),
2473 QueryExpr::DropCollection(q) => Some(q.name.as_str()),
2474 QueryExpr::Truncate(q) => Some(q.name.as_str()),
2475 _ => None,
2476 };
2477 if system_schema_target.is_some_and(crate::runtime::impl_ddl::is_system_schema_name) {
2478 return Err(RedDBError::Query("system schema is read-only".to_string()));
2479 }
2480
2481 let expected = match expr {
2482 QueryExpr::DropTable(q) => Some((q.name.as_str(), CollectionModel::Table)),
2483 QueryExpr::DropGraph(q) => Some((q.name.as_str(), CollectionModel::Graph)),
2484 QueryExpr::DropVector(q) => Some((q.name.as_str(), CollectionModel::Vector)),
2485 QueryExpr::DropDocument(q) => Some((q.name.as_str(), CollectionModel::Document)),
2486 QueryExpr::DropKv(q) => Some((q.name.as_str(), q.model)),
2487 QueryExpr::DropCollection(q) => q.model.map(|model| (q.name.as_str(), model)),
2488 QueryExpr::Truncate(q) => q.model.map(|model| (q.name.as_str(), model)),
2489 QueryExpr::KvCommand(cmd) => {
2490 let (collection, model) = match cmd {
2491 KvCommand::Put {
2492 collection, model, ..
2493 }
2494 | KvCommand::Get {
2495 collection, model, ..
2496 }
2497 | KvCommand::Incr {
2498 collection, model, ..
2499 }
2500 | KvCommand::Cas {
2501 collection, model, ..
2502 }
2503 | KvCommand::List {
2504 collection, model, ..
2505 }
2506 | KvCommand::Delete {
2507 collection, model, ..
2508 } => (collection.as_str(), *model),
2509 KvCommand::Rotate { collection, .. }
2510 | KvCommand::History { collection, .. }
2511 | KvCommand::Purge { collection, .. } => {
2512 (collection.as_str(), CollectionModel::Vault)
2513 }
2514 KvCommand::InvalidateTags { collection, .. } => {
2515 (collection.as_str(), CollectionModel::Kv)
2516 }
2517 KvCommand::Watch {
2518 collection, model, ..
2519 } => (collection.as_str(), *model),
2520 KvCommand::Reveal { collection, .. } => {
2521 (collection.as_str(), CollectionModel::Vault)
2522 }
2523 };
2524 Some((collection, model))
2525 }
2526 QueryExpr::ConfigCommand(cmd) => {
2527 self.validate_config_command_before_auth(cmd)?;
2528 None
2529 }
2530 _ => None,
2531 };
2532
2533 let Some((name, expected_model)) = expected else {
2534 return Ok(());
2535 };
2536 if let Some(contract) = self.inner.db.collection_contract_arc(name) {
2541 if self.inner.db.store().get_collection(name).is_some() {
2542 return polymorphic_resolver::ensure_model_match(
2543 expected_model,
2544 contract.declared_model,
2545 );
2546 }
2547 }
2548 let snapshot = self.inner.db.catalog_model_snapshot();
2549 let Some(actual_model) = snapshot
2550 .collections
2551 .iter()
2552 .find(|collection| collection.name == name)
2553 .map(|collection| collection.declared_model.unwrap_or(collection.model))
2554 else {
2555 return Ok(());
2556 };
2557 polymorphic_resolver::ensure_model_match(expected_model, actual_model)
2558 }
2559
2560 pub(super) fn rewrite_view_refs(&self, expr: QueryExpr) -> QueryExpr {
2565 if self.inner.views.read().is_empty() {
2567 return expr;
2568 }
2569 self.rewrite_view_refs_inner(expr)
2570 }
2571
2572 fn rewrite_view_refs_inner(&self, expr: QueryExpr) -> QueryExpr {
2573 use reddb_rql::ast::{Filter, TableSource};
2574 match expr {
2575 QueryExpr::Table(mut tq) => {
2576 if let Some(TableSource::Subquery(body)) = tq.source.take() {
2582 tq.source = Some(TableSource::Subquery(Box::new(
2583 self.rewrite_view_refs_inner(*body),
2584 )));
2585 return QueryExpr::Table(tq);
2586 }
2587
2588 let maybe_view = {
2592 let views = self.inner.views.read();
2593 views.get(&tq.table).cloned()
2594 };
2595 let Some(view) = maybe_view else {
2596 return QueryExpr::Table(tq);
2597 };
2598
2599 if view.materialized {
2605 return QueryExpr::Table(tq);
2606 }
2607
2608 let inner_expr = self.rewrite_view_refs_inner((*view.query).clone());
2612
2613 match inner_expr {
2621 QueryExpr::Table(mut inner_tq) => {
2622 if let Some(outer_filter) = tq.filter.take() {
2623 inner_tq.filter = Some(match inner_tq.filter.take() {
2624 Some(existing) => {
2625 Filter::And(Box::new(existing), Box::new(outer_filter))
2626 }
2627 None => outer_filter,
2628 });
2629 inner_tq.where_expr = inner_tq
2637 .filter
2638 .as_ref()
2639 .map(reddb_rql::sql_lowering::filter_to_expr);
2640 }
2641 if let Some(outer_limit) = tq.limit {
2642 inner_tq.limit = Some(match inner_tq.limit {
2643 Some(existing) => existing.min(outer_limit),
2644 None => outer_limit,
2645 });
2646 }
2647 if let Some(outer_offset) = tq.offset {
2648 inner_tq.offset = Some(match inner_tq.offset {
2649 Some(existing) => existing + outer_offset,
2650 None => outer_offset,
2651 });
2652 }
2653 QueryExpr::Table(inner_tq)
2654 }
2655 other => other,
2656 }
2657 }
2658 QueryExpr::Join(mut jq) => {
2659 jq.left = Box::new(self.rewrite_view_refs_inner(*jq.left));
2660 jq.right = Box::new(self.rewrite_view_refs_inner(*jq.right));
2661 QueryExpr::Join(jq)
2662 }
2663 other => other,
2666 }
2667 }
2668
2669 fn resolve_table_expr_subqueries(
2670 &self,
2671 mut table: TableQuery,
2672 frame: &dyn super::statement_frame::ReadFrame,
2673 ) -> RedDBResult<TableQuery> {
2674 match table.source.take() {
2681 Some(TableSource::Subquery(inner)) => {
2682 let inner = self.resolve_select_expr_subqueries(*inner, frame)?;
2683 table.source = Some(TableSource::Subquery(Box::new(inner)));
2684 }
2685 other => table.source = other,
2686 }
2687
2688 let outer_scopes = relation_scopes_for_query(&QueryExpr::Table(table.clone()));
2689 for item in &mut table.select_items {
2690 if let reddb_rql::ast::SelectItem::Expr { expr, .. } = item {
2691 *expr = self.resolve_expr_subqueries(expr.clone(), &outer_scopes, frame)?;
2692 }
2693 }
2694 if let Some(where_expr) = table.where_expr.take() {
2695 table.where_expr =
2696 Some(self.resolve_expr_subqueries(where_expr, &outer_scopes, frame)?);
2697 table.filter = None;
2698 }
2699 if let Some(having_expr) = table.having_expr.take() {
2700 table.having_expr =
2701 Some(self.resolve_expr_subqueries(having_expr, &outer_scopes, frame)?);
2702 table.having = None;
2703 }
2704 for expr in &mut table.group_by_exprs {
2705 *expr = self.resolve_expr_subqueries(expr.clone(), &outer_scopes, frame)?;
2706 }
2707 for clause in &mut table.order_by {
2708 if let Some(expr) = clause.expr.take() {
2709 clause.expr = Some(self.resolve_expr_subqueries(expr, &outer_scopes, frame)?);
2710 }
2711 }
2712 Ok(table)
2713 }
2714
2715 fn resolve_select_expr_subqueries(
2716 &self,
2717 expr: QueryExpr,
2718 frame: &dyn super::statement_frame::ReadFrame,
2719 ) -> RedDBResult<QueryExpr> {
2720 match expr {
2721 QueryExpr::Table(table) => self
2722 .resolve_table_expr_subqueries(table, frame)
2723 .map(QueryExpr::Table),
2724 QueryExpr::Join(mut join) => {
2725 join.left = Box::new(self.resolve_select_expr_subqueries(*join.left, frame)?);
2726 join.right = Box::new(self.resolve_select_expr_subqueries(*join.right, frame)?);
2727 Ok(QueryExpr::Join(join))
2728 }
2729 other => Ok(other),
2730 }
2731 }
2732
2733 fn resolve_expr_subqueries(
2734 &self,
2735 expr: reddb_rql::ast::Expr,
2736 outer_scopes: &[String],
2737 frame: &dyn super::statement_frame::ReadFrame,
2738 ) -> RedDBResult<reddb_rql::ast::Expr> {
2739 use reddb_rql::ast::Expr;
2740
2741 match expr {
2742 Expr::Subquery { query, span } => {
2743 let values = self.execute_expr_subquery_values(query, outer_scopes, frame)?;
2744 if values.len() > 1 {
2745 return Err(RedDBError::Query(
2746 "scalar subquery returned more than one row".to_string(),
2747 ));
2748 }
2749 Ok(Expr::Literal {
2750 value: values.into_iter().next().unwrap_or(Value::Null),
2751 span,
2752 })
2753 }
2754 Expr::BinaryOp { op, lhs, rhs, span } => Ok(Expr::BinaryOp {
2755 op,
2756 lhs: Box::new(self.resolve_expr_subqueries(*lhs, outer_scopes, frame)?),
2757 rhs: Box::new(self.resolve_expr_subqueries(*rhs, outer_scopes, frame)?),
2758 span,
2759 }),
2760 Expr::UnaryOp { op, operand, span } => Ok(Expr::UnaryOp {
2761 op,
2762 operand: Box::new(self.resolve_expr_subqueries(*operand, outer_scopes, frame)?),
2763 span,
2764 }),
2765 Expr::Cast {
2766 inner,
2767 target,
2768 span,
2769 } => Ok(Expr::Cast {
2770 inner: Box::new(self.resolve_expr_subqueries(*inner, outer_scopes, frame)?),
2771 target,
2772 span,
2773 }),
2774 Expr::FunctionCall { name, args, span } => {
2775 let args = args
2776 .into_iter()
2777 .map(|arg| self.resolve_expr_subqueries(arg, outer_scopes, frame))
2778 .collect::<RedDBResult<Vec<_>>>()?;
2779 Ok(Expr::FunctionCall { name, args, span })
2780 }
2781 Expr::WindowFunctionCall {
2782 name,
2783 args,
2784 mut window,
2785 span,
2786 } => {
2787 let args = args
2788 .into_iter()
2789 .map(|arg| self.resolve_expr_subqueries(arg, outer_scopes, frame))
2790 .collect::<RedDBResult<Vec<_>>>()?;
2791 for expr in &mut window.partition_by {
2792 *expr = self.resolve_expr_subqueries(expr.clone(), outer_scopes, frame)?;
2793 }
2794 for clause in &mut window.order_by {
2795 clause.expr =
2796 self.resolve_expr_subqueries(clause.expr.clone(), outer_scopes, frame)?;
2797 }
2798 Ok(Expr::WindowFunctionCall {
2799 name,
2800 args,
2801 window,
2802 span,
2803 })
2804 }
2805 Expr::Case {
2806 branches,
2807 else_,
2808 span,
2809 } => {
2810 let branches = branches
2811 .into_iter()
2812 .map(|(cond, value)| {
2813 Ok((
2814 self.resolve_expr_subqueries(cond, outer_scopes, frame)?,
2815 self.resolve_expr_subqueries(value, outer_scopes, frame)?,
2816 ))
2817 })
2818 .collect::<RedDBResult<Vec<_>>>()?;
2819 let else_ = else_
2820 .map(|expr| self.resolve_expr_subqueries(*expr, outer_scopes, frame))
2821 .transpose()?
2822 .map(Box::new);
2823 Ok(Expr::Case {
2824 branches,
2825 else_,
2826 span,
2827 })
2828 }
2829 Expr::IsNull {
2830 operand,
2831 negated,
2832 span,
2833 } => Ok(Expr::IsNull {
2834 operand: Box::new(self.resolve_expr_subqueries(*operand, outer_scopes, frame)?),
2835 negated,
2836 span,
2837 }),
2838 Expr::InList {
2839 target,
2840 values,
2841 negated,
2842 span,
2843 } => {
2844 let target =
2845 Box::new(self.resolve_expr_subqueries(*target, outer_scopes, frame)?);
2846 let mut resolved = Vec::new();
2847 for value in values {
2848 if let Expr::Subquery { query, .. } = value {
2849 resolved.extend(
2850 self.execute_expr_subquery_values(query, outer_scopes, frame)?
2851 .into_iter()
2852 .map(Expr::lit),
2853 );
2854 } else {
2855 resolved.push(self.resolve_expr_subqueries(value, outer_scopes, frame)?);
2856 }
2857 }
2858 Ok(Expr::InList {
2859 target,
2860 values: resolved,
2861 negated,
2862 span,
2863 })
2864 }
2865 Expr::Between {
2866 target,
2867 low,
2868 high,
2869 negated,
2870 span,
2871 } => Ok(Expr::Between {
2872 target: Box::new(self.resolve_expr_subqueries(*target, outer_scopes, frame)?),
2873 low: Box::new(self.resolve_expr_subqueries(*low, outer_scopes, frame)?),
2874 high: Box::new(self.resolve_expr_subqueries(*high, outer_scopes, frame)?),
2875 negated,
2876 span,
2877 }),
2878 other => Ok(other),
2879 }
2880 }
2881
2882 fn execute_expr_subquery_values(
2883 &self,
2884 subquery: reddb_rql::ast::ExprSubquery,
2885 outer_scopes: &[String],
2886 frame: &dyn super::statement_frame::ReadFrame,
2887 ) -> RedDBResult<Vec<Value>> {
2888 let query = *subquery.query;
2889 if query_references_outer_scope(&query, outer_scopes) {
2890 return Err(RedDBError::Query(
2891 "NOT_YET_SUPPORTED: correlated subqueries are not supported yet; track follow-up issue #470-correlated-subqueries".to_string(),
2892 ));
2893 }
2894 let query = self.rewrite_view_refs(query);
2895 let query = self.resolve_select_expr_subqueries(query, frame)?;
2896 let query = self.authorize_relational_select_expr(query, frame)?;
2897 let result = match query {
2898 QueryExpr::Table(table) => {
2899 execute_runtime_table_query(&self.inner.db, &table, Some(&self.inner.index_store))?
2900 }
2901 QueryExpr::Join(join) => execute_runtime_join_query(self, &join)?,
2902 other => {
2903 return Err(RedDBError::Query(format!(
2904 "expression subquery must be a SELECT query, got {}",
2905 query_expr_name(&other)
2906 )))
2907 }
2908 };
2909 first_column_values(result)
2910 }
2911
2912 fn execute_materialized_graph(
2913 &self,
2914 query: &str,
2915 mode: QueryMode,
2916 statement: &'static str,
2917 expr: &QueryExpr,
2918 ) -> RedDBResult<RuntimeQueryResult> {
2919 let (graph, node_properties, edge_properties) = self.materialize_graph_with_rls()?;
2927 let result =
2928 crate::storage::query::unified::UnifiedExecutor::execute_on_with_graph_properties_checked(
2929 &graph,
2930 expr,
2931 node_properties,
2932 edge_properties,
2933 super::function_budget::graph_work_check(),
2934 )
2935 .map_err(|err| RedDBError::Query(err.to_string()))?;
2936
2937 Ok(RuntimeQueryResult {
2938 query: query.to_string(),
2939 mode,
2940 statement,
2941 engine: "materialized-graph",
2942 result,
2943 affected_rows: 0,
2944 statement_type: "select",
2945 bookmark: None,
2946 notice: None,
2947 })
2948 }
2949
2950 fn dispatch_expr(
2951 &self,
2952 expr: QueryExpr,
2953 query_str: &str,
2954 mode: QueryMode,
2955 ) -> RedDBResult<RuntimeQueryResult> {
2956 let is_read = super::query_budget::is_materialized_read(&expr);
2959 let _budget = if is_read {
2960 Some(super::query_budget::Scope::enter(
2961 self.resource_limits(),
2962 &self.inner.query_budget_slots,
2963 )?)
2964 } else {
2965 None
2966 };
2967 let result = self.dispatch_expr_inner(expr, query_str, mode);
2968 if is_read {
2969 super::query_budget::charge(0)?;
2970 }
2971 let result = result?;
2972 super::query_budget::check_result(self.resource_limits(), &result)?;
2973 if is_read {
2974 super::query_budget::charge(0)?;
2975 }
2976 Ok(result)
2977 }
2978
2979 fn dispatch_expr_inner(
2980 &self,
2981 expr: QueryExpr,
2982 query_str: &str,
2983 mode: QueryMode,
2984 ) -> RedDBResult<RuntimeQueryResult> {
2985 let statement = query_expr_name(&expr);
2986 match expr {
2987 QueryExpr::Function(command) => self.execute_function(query_str, &command),
2988 QueryExpr::Graph(_) | QueryExpr::Path(_) => {
2989 self.execute_materialized_graph(query_str, mode, statement, &expr)
2990 }
2991 QueryExpr::Table(table) => {
2992 let scope = self.ai_scope();
2993 let table = self.resolve_table_expr_subqueries(
2994 table,
2995 &scope as &dyn super::statement_frame::ReadFrame,
2996 )?;
2997 if let Some(TableSource::Function {
3001 name,
3002 args,
3003 named_args,
3004 }) = table.source.clone()
3005 {
3006 return Ok(RuntimeQueryResult {
3007 query: query_str.to_string(),
3008 mode,
3009 statement,
3010 engine: "runtime-graph-tvf",
3011 result: self.execute_table_function(&name, &args, &named_args)?,
3012 affected_rows: 0,
3013 statement_type: "select",
3014 bookmark: None,
3015 notice: None,
3016 });
3017 }
3018 if let Some(TableSource::InlineGraphFunction {
3022 name,
3023 nodes,
3024 edges,
3025 named_args,
3026 }) = table.source.clone()
3027 {
3028 return Ok(RuntimeQueryResult {
3029 query: query_str.to_string(),
3030 mode,
3031 statement,
3032 engine: "runtime-graph-tvf-inline",
3033 result: self.execute_inline_graph_function(
3034 &name,
3035 &nodes,
3036 &edges,
3037 &named_args,
3038 )?,
3039 affected_rows: 0,
3040 statement_type: "select",
3041 bookmark: None,
3042 notice: None,
3043 });
3044 }
3045 if super::red_schema::is_virtual_table(&table.table) {
3046 return Ok(RuntimeQueryResult {
3047 query: query_str.to_string(),
3048 mode,
3049 statement,
3050 engine: "runtime-red-schema",
3051 result: super::red_schema::red_query(
3052 self,
3053 &table.table,
3054 &table,
3055 &scope as &dyn super::statement_frame::ReadFrame,
3056 )?,
3057 affected_rows: 0,
3058 statement_type: "select",
3059 bookmark: None,
3060 notice: None,
3061 });
3062 }
3063 if let Some(view_result) = self.try_resolve_analytics_view(
3065 &table,
3066 &scope as &dyn super::statement_frame::ReadFrame,
3067 )? {
3068 return Ok(RuntimeQueryResult {
3069 query: query_str.to_string(),
3070 mode,
3071 statement,
3072 engine: "runtime-graph-analytics-view",
3073 result: view_result,
3074 affected_rows: 0,
3075 statement_type: "select",
3076 bookmark: None,
3077 notice: None,
3078 });
3079 }
3080 let Some(table_with_rls) = self.authorize_relational_table_select(
3081 table,
3082 &scope as &dyn super::statement_frame::ReadFrame,
3083 )?
3084 else {
3085 return Ok(RuntimeQueryResult {
3086 query: query_str.to_string(),
3087 mode,
3088 statement,
3089 engine: "runtime-table-rls",
3090 result: crate::storage::query::unified::UnifiedResult::empty(),
3091 affected_rows: 0,
3092 statement_type: "select",
3093 bookmark: None,
3094 notice: None,
3095 });
3096 };
3097 Ok(RuntimeQueryResult {
3098 query: query_str.to_string(),
3099 mode,
3100 statement,
3101 engine: "runtime-table",
3102 result: execute_runtime_table_query(
3103 &self.inner.db,
3104 &table_with_rls,
3105 Some(&self.inner.index_store),
3106 )?,
3107 affected_rows: 0,
3108 statement_type: "select",
3109 bookmark: None,
3110 notice: None,
3111 })
3112 }
3113 QueryExpr::Join(join) => {
3114 let scope = self.ai_scope();
3115 let Some(join_with_rls) = self.authorize_relational_join_select(
3116 join,
3117 &scope as &dyn super::statement_frame::ReadFrame,
3118 )?
3119 else {
3120 return Ok(RuntimeQueryResult {
3121 query: query_str.to_string(),
3122 mode,
3123 statement,
3124 engine: "runtime-join-rls",
3125 result: crate::storage::query::unified::UnifiedResult::empty(),
3126 affected_rows: 0,
3127 statement_type: "select",
3128 bookmark: None,
3129 notice: None,
3130 });
3131 };
3132 Ok(RuntimeQueryResult {
3133 query: query_str.to_string(),
3134 mode,
3135 statement,
3136 engine: "runtime-join",
3137 result: execute_runtime_join_query(self, &join_with_rls)?,
3138 affected_rows: 0,
3139 statement_type: "select",
3140 bookmark: None,
3141 notice: None,
3142 })
3143 }
3144 QueryExpr::Vector(vector) => Ok(RuntimeQueryResult {
3145 query: query_str.to_string(),
3146 mode,
3147 statement,
3148 engine: "runtime-vector",
3149 result: execute_runtime_vector_query(self, &vector)?,
3150 affected_rows: 0,
3151 statement_type: "select",
3152 bookmark: None,
3153 notice: None,
3154 }),
3155 QueryExpr::Hybrid(hybrid) => Ok(RuntimeQueryResult {
3156 query: query_str.to_string(),
3157 mode,
3158 statement,
3159 engine: "runtime-hybrid",
3160 result: execute_runtime_hybrid_query(self, &hybrid)?,
3161 affected_rows: 0,
3162 statement_type: "select",
3163 bookmark: None,
3164 notice: None,
3165 }),
3166 QueryExpr::Insert(ref insert) if super::red_schema::is_virtual_table(&insert.table) => {
3167 Err(RedDBError::Query(
3168 super::red_schema::READ_ONLY_ERROR.to_string(),
3169 ))
3170 }
3171 QueryExpr::Update(ref update) if super::red_schema::is_virtual_table(&update.table) => {
3172 Err(RedDBError::Query(
3173 super::red_schema::READ_ONLY_ERROR.to_string(),
3174 ))
3175 }
3176 QueryExpr::Delete(ref delete) if super::red_schema::is_virtual_table(&delete.table) => {
3177 Err(RedDBError::Query(
3178 super::red_schema::READ_ONLY_ERROR.to_string(),
3179 ))
3180 }
3181 QueryExpr::Insert(ref insert) => self
3182 .with_deferred_store_wal_for_dml(self.insert_may_emit_events(insert), || {
3183 self.execute_insert(query_str, insert)
3184 }),
3185 QueryExpr::Update(ref update) => self
3186 .with_deferred_store_wal_for_dml(self.update_may_emit_events(update), || {
3187 self.execute_update(query_str, update)
3188 }),
3189 QueryExpr::Delete(ref delete) => self
3190 .with_deferred_store_wal_for_dml(self.delete_may_emit_events(delete), || {
3191 self.execute_delete(query_str, delete)
3192 }),
3193 QueryExpr::QueueCommand(ref cmd) => self.with_deferred_store_wal_if_transaction(|| {
3194 self.execute_queue_command(query_str, cmd)
3195 }),
3196 QueryExpr::KvCommand(ref cmd) => self.execute_kv_command(query_str, cmd),
3197 QueryExpr::SearchCommand(ref cmd) => self.execute_search_command(query_str, cmd),
3198 QueryExpr::Ask(ref ask) => self.execute_ask(query_str, ask),
3199 _ => Err(RedDBError::Query(format!(
3200 "prepared-statement execution does not support {statement} statements"
3201 ))),
3202 }
3203 }
3204
3205 fn execute_table_function(
3212 &self,
3213 name: &str,
3214 args: &[String],
3215 named_args: &[(String, f64)],
3216 ) -> RedDBResult<crate::storage::query::unified::UnifiedResult> {
3217 if name.eq_ignore_ascii_case("red.diff") {
3218 return self.execute_vcs_diff_tvf(args, named_args);
3219 }
3220 if !is_graph_tvf_name(name) {
3221 return Err(RedDBError::Query(format!("unknown table function: {name}")));
3222 }
3223 if args.len() != 1 {
3225 return Err(RedDBError::Query(format!(
3226 "table function '{name}' takes exactly 1 graph argument, got {}",
3227 args.len()
3228 )));
3229 }
3230
3231 let (nodes, edges) = self.materialize_whole_graph_abstract()?;
3236 self.dispatch_graph_algorithm(name, nodes, edges, named_args)
3237 }
3238
3239 pub(crate) fn revive_versioned_updates_since(
3240 &self,
3241 conn_id: u64,
3242 stamper_xid: u64,
3243 ) -> RedDBResult<usize> {
3244 let candidates = {
3247 let guard = self.inner.pending_versioned_updates.read();
3248 let Some(pending) = guard.get(&conn_id) else {
3249 return Ok(0);
3250 };
3251 pending
3252 .iter()
3253 .rev()
3254 .filter(|entry| entry.3 >= stamper_xid)
3255 .cloned()
3256 .collect::<Vec<_>>()
3257 };
3258 let mut reverted = 0usize;
3259 for candidate in candidates {
3260 let (collection, old_id, new_id, xid, previous_xmax) = &candidate;
3261 let topology_lock = self.index_store_ref().collection_topology_lock(collection);
3262 #[cfg(test)]
3263 self.index_store_ref().mutation_test_hook(
3264 collection,
3265 super::index_store::MutationTestPhase::BeforeUndoTopology,
3266 );
3267 let topology_guard = topology_lock.read();
3268 let mut guard = self.inner.pending_versioned_updates.write();
3269 let Some(pending) = guard.get_mut(&conn_id) else {
3270 continue;
3271 };
3272 let Some(position) = pending.iter().rposition(|entry| entry == &candidate) else {
3275 continue;
3276 };
3277 self.revive_versioned_update(
3278 collection,
3279 *old_id,
3280 *new_id,
3281 *xid,
3282 *previous_xmax,
3283 topology_guard,
3284 )?;
3285 pending.remove(position);
3286 reverted += 1;
3287 if pending.is_empty() {
3288 guard.remove(&conn_id);
3289 }
3290 }
3291 Ok(reverted)
3292 }
3293
3294 fn check_explain_target_privilege(&self, inner_sql: &str) -> RedDBResult<()> {
3300 let parsed =
3301 reddb_rql::parser::parse(inner_sql).map_err(|e| RedDBError::Query(e.to_string()))?;
3302 self.check_query_privilege(&parsed.query)
3303 .map_err(|err| RedDBError::Query(format!("permission denied: {err}")))
3304 }
3305
3306 fn explain_as_rows(&self, raw_query: &str, inner_sql: &str) -> RedDBResult<RuntimeQueryResult> {
3314 self.check_explain_target_privilege(inner_sql)?;
3315 let explain = self.explain_query(inner_sql)?;
3316
3317 let mut columns = vec![
3318 "op".to_string(),
3319 "source".to_string(),
3320 "estimated_rows".to_string(),
3321 "estimated_cost".to_string(),
3322 "depth".to_string(),
3323 ];
3324
3325 let mut records: Vec<crate::storage::query::unified::UnifiedRecord> = Vec::new();
3326
3327 for name in &explain.cte_materializations {
3333 use std::sync::Arc;
3334 let mut rec = crate::storage::query::unified::UnifiedRecord::default();
3335 rec.set_arc(Arc::from("op"), Value::text("CteScan".to_string()));
3336 rec.set_arc(Arc::from("source"), Value::text(name.clone()));
3337 rec.set_arc(Arc::from("estimated_rows"), Value::Float(0.0));
3338 rec.set_arc(Arc::from("estimated_cost"), Value::Float(0.0));
3339 rec.set_arc(Arc::from("depth"), Value::Integer(0));
3340 records.push(rec);
3341 }
3342
3343 let vector_details = explain.statement == "vector";
3344 if vector_details {
3345 columns.extend(["access_path_reason".to_string(), "index_hint".to_string()]);
3346 }
3347 walk_plan_node(&explain.logical_plan.root, 0, &mut records, vector_details);
3348
3349 let result = crate::storage::query::unified::UnifiedResult {
3350 columns,
3351 records,
3352 stats: Default::default(),
3353 pre_serialized_json: None,
3354 };
3355
3356 Ok(RuntimeQueryResult {
3357 query: raw_query.to_string(),
3358 mode: explain.mode,
3359 statement: "explain",
3360 engine: "runtime-explain",
3361 result,
3362 affected_rows: 0,
3363 statement_type: "select",
3364 bookmark: None,
3365 notice: None,
3366 })
3367 }
3368
3369 fn explain_analyze_vector_as_rows(
3370 &self,
3371 raw_query: &str,
3372 expr: QueryExpr,
3373 ) -> RedDBResult<RuntimeQueryResult> {
3374 let QueryExpr::Vector(query) = &expr else {
3375 return Err(RedDBError::Internal(
3376 "expected vector analysis target".to_string(),
3377 ));
3378 };
3379 let source = query.collection.clone();
3380 let execution = self.execute_query_expr(expr)?;
3383 let stats = execution.result.stats;
3384 let vector = stats.vector.as_ref().ok_or_else(|| {
3385 RedDBError::Internal("vector executor did not provide measurements".to_string())
3386 })?;
3387 let fields = vec![
3388 ("op", Value::text(vector.access_path.clone())),
3389 ("source", Value::text(source)),
3390 ("index_used", Value::Boolean(vector.index_used)),
3391 ("mode_requested", Value::text(vector.mode_requested.clone())),
3392 ("mode_executed", Value::text(vector.mode_executed.clone())),
3393 (
3394 "fallback_reason",
3395 vector
3396 .fallback_reason
3397 .as_ref()
3398 .map_or(Value::Null, |reason| Value::text(reason.clone())),
3399 ),
3400 (
3401 "approximate_distance_evaluations",
3402 Value::UnsignedInteger(vector.approximate_distance_evaluations),
3403 ),
3404 (
3405 "peak_topk_entries",
3406 Value::UnsignedInteger(vector.peak_topk_entries),
3407 ),
3408 (
3409 "candidates_examined",
3410 Value::UnsignedInteger(vector.candidates_examined),
3411 ),
3412 (
3413 "metadata_rejected",
3414 Value::UnsignedInteger(vector.metadata_rejected),
3415 ),
3416 ("rls_rejected", Value::UnsignedInteger(vector.rls_rejected)),
3417 (
3418 "visibility_rejected",
3419 Value::UnsignedInteger(vector.visibility_rejected),
3420 ),
3421 (
3422 "exact_distance_evaluations",
3423 Value::UnsignedInteger(vector.exact_distance_evaluations),
3424 ),
3425 ("actual_rows", Value::UnsignedInteger(vector.rows_returned)),
3426 (
3427 "actual_ms",
3428 Value::Float(stats.exec_time_us as f64 / 1000.0),
3429 ),
3430 ("metrics_scope", Value::text("vector_pipeline")),
3431 (
3432 "operators",
3433 Value::Json(vector.operators_json().to_string().into_bytes()),
3434 ),
3435 ];
3436 let columns = fields.iter().map(|(name, _)| name.to_string()).collect();
3437 let mut record = crate::storage::query::unified::UnifiedRecord::default();
3438 for (name, value) in fields {
3439 record.set_owned(name.to_string(), value);
3440 }
3441 Ok(RuntimeQueryResult {
3442 query: raw_query.to_string(),
3443 mode: execution.mode,
3444 statement: "explain_analyze",
3445 engine: "runtime-explain-analyze",
3446 result: crate::storage::query::unified::UnifiedResult {
3447 columns,
3448 records: vec![record],
3449 stats,
3450 pre_serialized_json: None,
3451 },
3452 affected_rows: 0,
3453 statement_type: "select",
3454 bookmark: None,
3455 notice: None,
3456 })
3457 }
3458
3459 fn explain_analyze_as_rows(
3460 &self,
3461 raw_query: &str,
3462 inner_sql: &str,
3463 ) -> RedDBResult<RuntimeQueryResult> {
3464 if strip_keyword_ci(inner_sql.trim_start(), "VECTOR").is_some() {
3465 let expr = parse_multi(inner_sql).map_err(|err| RedDBError::Query(err.to_string()))?;
3466 if matches!(&expr, QueryExpr::Vector(_)) {
3467 return self.explain_analyze_vector_as_rows(raw_query, expr);
3468 }
3469 }
3470 if !starts_with_dml_keyword(inner_sql) {
3471 return Err(RedDBError::Query(
3472 "EXPLAIN ANALYZE currently supports INSERT, UPDATE, DELETE, and VECTOR SEARCH"
3473 .to_string(),
3474 ));
3475 }
3476
3477 self.check_explain_target_privilege(inner_sql)?;
3478 let explain = self.explain_query(inner_sql)?;
3479 let conn_id = current_connection_id();
3480 if self.inner.transaction_state.in_transaction(conn_id) {
3481 return Err(RedDBError::Query(
3482 "EXPLAIN ANALYZE requires no active transaction".to_string(),
3483 ));
3484 }
3485
3486 self.execute_query_inner("BEGIN")?;
3487 let started = std::time::Instant::now();
3488 let execution = self.execute_query_inner(inner_sql);
3489 let elapsed_ms = started.elapsed().as_secs_f64() * 1000.0;
3490 let rollback = self.execute_query_inner("ROLLBACK");
3491
3492 let affected_rows = match execution {
3493 Ok(result) => result.affected_rows,
3494 Err(err) => {
3495 rollback?;
3496 return Err(err);
3497 }
3498 };
3499 rollback?;
3500
3501 let columns = vec![
3502 "op".to_string(),
3503 "source".to_string(),
3504 "estimated_rows".to_string(),
3505 "estimated_cost".to_string(),
3506 "actual_rows".to_string(),
3507 "actual_ms".to_string(),
3508 "depth".to_string(),
3509 ];
3510 let mut records = Vec::new();
3511 walk_analyze_plan_node(
3512 &explain.logical_plan.root,
3513 0,
3514 affected_rows,
3515 elapsed_ms,
3516 &mut records,
3517 );
3518
3519 let result = crate::storage::query::unified::UnifiedResult {
3520 columns,
3521 records,
3522 stats: Default::default(),
3523 pre_serialized_json: None,
3524 };
3525
3526 Ok(RuntimeQueryResult {
3527 query: raw_query.to_string(),
3528 mode: explain.mode,
3529 statement: "explain_analyze",
3530 engine: "runtime-explain-analyze",
3531 result,
3532 affected_rows: 0,
3533 statement_type: "select",
3534 bookmark: None,
3535 notice: None,
3536 })
3537 }
3538}
3539
3540fn redact_sql_for_slow_log(query: &str) -> String {
3541 let lowered = query.to_ascii_lowercase();
3542 if lowered.contains("secret") || lowered.contains("vault") {
3543 return "secret statement (SQL redacted)".to_string();
3544 }
3545 let credential_candidate = lowered.contains("password")
3546 && ((lowered.contains("create") && lowered.contains("user"))
3547 || (lowered.contains("alter") && lowered.contains("user")));
3548 if !credential_candidate {
3549 return query.to_string();
3550 }
3551 match parse_multi(query) {
3552 Ok(QueryExpr::CreateUser(stmt)) => {
3553 let target = UserId::from_parts(stmt.tenant.as_deref(), &stmt.username);
3554 format!("CREATE USER {target} PASSWORD '***' ROLE {}", stmt.role)
3555 }
3556 Ok(QueryExpr::AlterUser(stmt))
3557 if stmt
3558 .attributes
3559 .iter()
3560 .any(|attr| matches!(attr, reddb_rql::ast::AlterUserAttribute::Password(_))) =>
3561 {
3562 let target = UserId::from_parts(stmt.tenant.as_deref(), &stmt.username);
3563 format!("ALTER USER {target} PASSWORD '***'")
3564 }
3565 Ok(_) => "credential statement (SQL redacted)".to_string(),
3566 Err(_) => "credential statement (SQL redacted)".to_string(),
3567 }
3568}
3569
3570fn strip_explain_analyze_prefix(sql: &str) -> Option<&str> {
3571 let rest = strip_keyword_ci(sql.trim_start(), "EXPLAIN")?.trim_start();
3572 Some(strip_keyword_ci(rest, "ANALYZE")?.trim_start()).filter(|inner| !inner.is_empty())
3573}
3574
3575fn strip_keyword_ci<'a>(sql: &'a str, keyword: &str) -> Option<&'a str> {
3576 let head = sql.get(..keyword.len())?;
3579 let rest = &sql[keyword.len()..];
3580 if !head.eq_ignore_ascii_case(keyword) {
3581 return None;
3582 }
3583 if rest
3584 .chars()
3585 .next()
3586 .is_some_and(|ch| !ch.is_ascii_whitespace())
3587 {
3588 return None;
3589 }
3590 Some(rest)
3591}
3592
3593fn starts_with_dml_keyword(sql: &str) -> bool {
3594 let trimmed = sql.trim_start();
3595 let head_end = trimmed
3596 .find(|ch: char| ch.is_ascii_whitespace())
3597 .unwrap_or(trimmed.len());
3598 let head = &trimmed[..head_end];
3599 head.eq_ignore_ascii_case("INSERT")
3600 || head.eq_ignore_ascii_case("UPDATE")
3601 || head.eq_ignore_ascii_case("DELETE")
3602}
3603
3604fn walk_plan_node(
3605 node: &crate::storage::query::planner::CanonicalLogicalNode,
3606 depth: usize,
3607 out: &mut Vec<crate::storage::query::unified::UnifiedRecord>,
3608 vector_details: bool,
3609) {
3610 use std::sync::Arc;
3611 let mut rec = crate::storage::query::unified::UnifiedRecord::default();
3612 rec.set_arc(Arc::from("op"), Value::text(node.operator.clone()));
3613 rec.set_arc(
3614 Arc::from("source"),
3615 node.source.clone().map(Value::text).unwrap_or(Value::Null),
3616 );
3617 rec.set_arc(
3618 Arc::from("estimated_rows"),
3619 Value::Float(node.estimated_rows),
3620 );
3621 rec.set_arc(
3622 Arc::from("estimated_cost"),
3623 Value::Float(node.operator_cost),
3624 );
3625 rec.set_arc(Arc::from("depth"), Value::Integer(depth as i64));
3626 if vector_details {
3627 for name in ["access_path_reason", "index_hint"] {
3628 rec.set_owned(
3629 name.to_string(),
3630 node.details
3631 .get(name)
3632 .cloned()
3633 .map(Value::text)
3634 .unwrap_or(Value::Null),
3635 );
3636 }
3637 }
3638 out.push(rec);
3639 for child in &node.children {
3640 walk_plan_node(child, depth + 1, out, vector_details);
3641 }
3642}
3643
3644fn walk_analyze_plan_node(
3645 node: &crate::storage::query::planner::CanonicalLogicalNode,
3646 depth: usize,
3647 affected_rows: u64,
3648 elapsed_ms: f64,
3649 out: &mut Vec<crate::storage::query::unified::UnifiedRecord>,
3650) {
3651 use std::sync::Arc;
3652 let mut rec = crate::storage::query::unified::UnifiedRecord::default();
3653 rec.set_arc(Arc::from("op"), Value::text(node.operator.clone()));
3654 rec.set_arc(
3655 Arc::from("source"),
3656 node.source.clone().map(Value::text).unwrap_or(Value::Null),
3657 );
3658 rec.set_arc(
3659 Arc::from("estimated_rows"),
3660 Value::Float(node.estimated_rows),
3661 );
3662 rec.set_arc(
3663 Arc::from("estimated_cost"),
3664 Value::Float(node.operator_cost),
3665 );
3666 rec.set_arc(
3667 Arc::from("actual_rows"),
3668 Value::UnsignedInteger(if depth == 0 { affected_rows } else { 0 }),
3669 );
3670 rec.set_arc(
3671 Arc::from("actual_ms"),
3672 Value::Float(if depth == 0 { elapsed_ms } else { 0.0 }),
3673 );
3674 rec.set_arc(Arc::from("depth"), Value::Integer(depth as i64));
3675 out.push(rec);
3676
3677 for child in &node.children {
3678 walk_analyze_plan_node(child, depth + 1, 0, 0.0, out);
3679 }
3680}
3681
3682#[cfg(test)]
3683mod declared_model_validation_tests {
3684 use super::*;
3685 use crate::catalog::CollectionModel;
3686 use crate::storage::no_malloc_hot_paths::measure_allocations;
3687 use reddb_rql::ast::KvCommand;
3688
3689 fn get(collection: &str) -> QueryExpr {
3690 QueryExpr::KvCommand(KvCommand::Get {
3691 model: CollectionModel::Kv,
3692 collection: collection.into(),
3693 key: "item".into(),
3694 })
3695 }
3696
3697 #[test]
3698 fn declared_model_validation_keeps_mismatches_and_observes_contract_changes() {
3699 let runtime = RedDBRuntime::in_memory().expect("runtime");
3700 runtime
3701 .execute_query("KV PUT declared.item = 1")
3702 .expect("seed");
3703 runtime
3704 .validate_model_operations_before_auth(&get("declared"))
3705 .expect("KV contract");
3706 let db = runtime.db();
3707 for (model, name) in [
3708 (CollectionModel::Table, "table"),
3709 (CollectionModel::Mixed, "mixed"),
3710 ] {
3711 let mut contract = db.collection_contract("declared").expect("contract");
3712 contract.declared_model = model;
3713 db.save_collection_contract(contract)
3714 .expect("replace contract");
3715 let error = runtime
3716 .validate_model_operations_before_auth(&get("declared"))
3717 .expect_err("mismatch");
3718 assert_eq!(
3719 error.to_string(),
3720 format!("INVALID_OPERATION: model mismatch: expected kv, got {name}")
3721 );
3722 }
3723 }
3724
3725 #[test]
3726 fn declared_model_validation_preserves_legacy_inference_and_missing_collections() {
3727 let runtime = RedDBRuntime::in_memory().expect("runtime");
3728 let db = runtime.db();
3729 let store = db.store();
3730 store
3731 .create_collection("legacy")
3732 .expect("legacy collection");
3733 store
3734 .get_collection("legacy")
3735 .expect("manager")
3736 .insert(crate::storage::UnifiedEntity::table_row(
3737 crate::storage::EntityId::new(1),
3738 "legacy",
3739 1,
3740 Vec::new(),
3741 ))
3742 .expect("uncontracted table row");
3743 assert!(db.collection_contract("legacy").is_none());
3744 let error = runtime
3745 .validate_model_operations_before_auth(&get("legacy"))
3746 .expect_err("inferred table");
3747 assert!(error
3748 .to_string()
3749 .contains("model mismatch: expected kv, got table"));
3750 runtime
3751 .validate_model_operations_before_auth(&get("missing"))
3752 .expect("missing stays deferred to execution");
3753 }
3754
3755 #[test]
3756 fn declared_model_validation_does_not_treat_orphan_contracts_as_collections() {
3757 let runtime = RedDBRuntime::in_memory().expect("runtime");
3758 runtime
3759 .execute_query("KV PUT declared.item = 1")
3760 .expect("seed");
3761 let db = runtime.db();
3762 let mut contract = db.collection_contract("declared").expect("contract");
3763 contract.name = "orphan".into();
3764 contract.declared_model = CollectionModel::Table;
3765 db.save_collection_contract(contract)
3766 .expect("orphan contract");
3767 assert!(db.store().get_collection("orphan").is_none());
3768 runtime
3769 .validate_model_operations_before_auth(&get("orphan"))
3770 .expect("no physical collection to validate");
3771 }
3772
3773 #[test]
3774 fn declared_model_validation_allocates_nothing_with_unrelated_collections() {
3775 let runtime = RedDBRuntime::in_memory().expect("runtime");
3776 runtime
3777 .execute_query("KV PUT declared.item = 1")
3778 .expect("seed");
3779 let expr = get("declared");
3780 for unrelated in [0, 64] {
3781 let db = runtime.db();
3782 let store = db.store();
3783 for index in 0..unrelated {
3784 let name = format!("unrelated_{index}");
3785 store
3786 .create_collection(&name)
3787 .expect("unrelated collection");
3788 }
3789 runtime
3790 .validate_model_operations_before_auth(&expr)
3791 .expect("warm contract cache");
3792 let (result, count) =
3793 measure_allocations(|| runtime.validate_model_operations_before_auth(&expr));
3794 result.expect("declared KV");
3795 assert_eq!(
3796 count.allocs, 0,
3797 "validation must only borrow the target contract"
3798 );
3799 }
3800 }
3801}
3802
3803#[cfg(test)]
3804mod multibyte_sql_tests {
3805 use super::*;
3806
3807 #[test]
3808 fn explain_analyze_prefix_ignores_multibyte_text_at_the_keyword_boundary() {
3809 for pad in 0..16 {
3812 for wide in ["é", "日", "😀"] {
3813 let sql = format!("{}{wide}llo", "a".repeat(pad));
3814 assert_eq!(strip_explain_analyze_prefix(&sql), None, "{sql:?}");
3815 }
3816 }
3817 assert_eq!(
3818 strip_explain_analyze_prefix("explain analyze DELETE FROM t"),
3819 Some("DELETE FROM t")
3820 );
3821 }
3822
3823 #[test]
3828 fn multibyte_sql_never_panics_execute_query() {
3829 let rt = RedDBRuntime::in_memory().expect("in-memory runtime");
3830 let seeds = [
3831 "",
3832 "SELECT ",
3833 "SELECT '",
3834 "EXPLAIN ",
3835 "EXPLAIN ANALYZE ",
3836 "CHECKPOINT ",
3837 "CHECKOUT ",
3838 "RESET ",
3839 "MERGE ",
3840 "CHERRY PICK ",
3841 "REVERT ",
3842 "RESOLVE CONFLICT ",
3843 "INSERT INTO t VALUES ('",
3844 ];
3845 let mut panicked = Vec::new();
3846 for seed in seeds {
3847 for pad in 0..16 {
3848 for wide in ["é", "日", "😀"] {
3849 let sql = format!("{seed}{}{wide}llo", "a".repeat(pad));
3850 let outcome = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
3851 let _ = rt.execute_query(&sql);
3852 }));
3853 if outcome.is_err() {
3854 panicked.push(sql);
3855 }
3856 }
3857 }
3858 }
3859 assert!(
3860 panicked.is_empty(),
3861 "execute_query panicked on {} statement(s), first: {:?}",
3862 panicked.len(),
3863 panicked.first()
3864 );
3865 }
3866}
3867
3868#[cfg(test)]
3869mod inline_graph_tvf_tests {
3870 use super::*;
3871
3872 fn scopes_for(sql: &str) -> HashSet<String> {
3873 let expr = reddb_rql::parser::parse(sql).expect("parse").query;
3874 query_expr_result_cache_scopes(&expr)
3875 }
3876
3877 #[test]
3878 fn inline_tvf_cache_scopes_include_source_collections() {
3879 let scopes = scopes_for(
3883 "SELECT * FROM components(nodes => (SELECT id FROM hosts), edges => (SELECT src, dst FROM links))",
3884 );
3885 assert!(scopes.contains("hosts"), "nodes source scoped: {scopes:?}");
3886 assert!(scopes.contains("links"), "edges source scoped: {scopes:?}");
3887 }
3888
3889 #[test]
3890 fn graph_collection_tvf_cache_scope_is_graph_argument() {
3891 let scopes = scopes_for("SELECT * FROM components(g)");
3895 assert!(scopes.contains("g"), "collection form scoped: {scopes:?}");
3896 }
3897
3898 #[test]
3899 fn abstract_degree_centrality_counts_undirected_endpoints() {
3900 let nodes = vec!["a".to_string(), "b".to_string(), "c".to_string()];
3901 let edges = vec![
3902 ("a".to_string(), "b".to_string(), 1.0_f32),
3903 ("b".to_string(), "c".to_string(), 1.0_f32),
3904 ];
3905 let degrees = abstract_degree_centrality(&nodes, &edges);
3906 assert_eq!(
3907 degrees,
3908 vec![
3909 ("a".to_string(), 1),
3910 ("b".to_string(), 2),
3911 ("c".to_string(), 1),
3912 ]
3913 );
3914 }
3915}
3916
3917fn reject_engine_internal_vault_key(key: &str) -> RedDBResult<()> {
3924 let lowered = key.to_ascii_lowercase();
3925 let internal = lowered == "red.secret.aes_key"
3926 || lowered.starts_with("red.iam.")
3927 || lowered.starts_with("red.acl.")
3928 || lowered.starts_with("red.vault.");
3929 if internal {
3930 return Err(RedDBError::Query(format!(
3931 "`{key}` is an engine-internal vault key and cannot be set or deleted through SET/DELETE SECRET"
3932 )));
3933 }
3934 Ok(())
3935}
3936
3937fn confine_copy_from_path(path: &str) -> RedDBResult<()> {
3943 let Ok(root) = std::env::var("REDDB_COPY_IMPORT_ROOT") else {
3944 return Ok(());
3945 };
3946 let root = std::fs::canonicalize(&root).map_err(|e| {
3947 RedDBError::Query(format!(
3948 "REDDB_COPY_IMPORT_ROOT `{root}` is not usable: {e}"
3949 ))
3950 })?;
3951 let resolved = std::fs::canonicalize(path)
3952 .map_err(|e| RedDBError::Query(format!("COPY FROM `{path}`: {e}")))?;
3953 if !resolved.starts_with(&root) || !resolved.is_file() {
3954 return Err(RedDBError::Query(format!(
3955 "COPY FROM `{path}` is outside the configured import root"
3956 )));
3957 }
3958 Ok(())
3959}
3960
3961#[cfg(test)]
3962mod security_gate_tests {
3963 use super::*;
3964 use crate::auth::store::AuthStore;
3965 use crate::auth::{AuthConfig, Role};
3966
3967 struct IdentityGuard;
3970
3971 impl IdentityGuard {
3972 fn install(username: &str, role: Role) -> Self {
3973 set_current_auth_identity(username.to_string(), role);
3974 Self
3975 }
3976 }
3977
3978 impl Drop for IdentityGuard {
3979 fn drop(&mut self) {
3980 clear_current_auth_identity();
3981 }
3982 }
3983
3984 struct ScopedIdentityGuard;
3985
3986 impl ScopedIdentityGuard {
3987 fn install(tenant: &str, username: &str, role: Role) -> Self {
3988 crate::runtime::execution_context::set_current_auth_principal(
3989 crate::auth::UserId::scoped(tenant, username),
3990 role,
3991 );
3992 set_current_tenant(tenant.to_string());
3993 Self
3994 }
3995 }
3996
3997 impl Drop for ScopedIdentityGuard {
3998 fn drop(&mut self) {
3999 clear_current_auth_identity();
4000 clear_current_tenant();
4001 }
4002 }
4003
4004 fn runtime_with_user(username: &str, role: Role) -> RedDBRuntime {
4005 let store = std::sync::Arc::new(AuthStore::new(AuthConfig {
4006 enabled: true,
4007 session_ttl_secs: 60,
4008 require_auth: true,
4009 auto_encrypt_storage: false,
4010 vault_enabled: false,
4011 cert: Default::default(),
4012 oauth: Default::default(),
4013 }));
4014 store.create_user(username, "pw", role).expect("user");
4015 let runtime =
4016 RedDBRuntime::with_options(crate::api::RedDBOptions::in_memory()).expect("runtime");
4017 runtime.set_auth_store(store);
4018 runtime
4019 }
4020
4021 fn runtime_with_store(username: &str, role: Role) -> (RedDBRuntime, std::sync::Arc<AuthStore>) {
4022 let store = std::sync::Arc::new(AuthStore::new(AuthConfig {
4023 enabled: true,
4024 session_ttl_secs: 60,
4025 require_auth: true,
4026 auto_encrypt_storage: false,
4027 vault_enabled: false,
4028 cert: Default::default(),
4029 oauth: Default::default(),
4030 }));
4031 store.create_user(username, "pw", role).expect("user");
4032 let runtime =
4033 RedDBRuntime::with_options(crate::api::RedDBOptions::in_memory()).expect("runtime");
4034 runtime.set_auth_store(std::sync::Arc::clone(&store));
4035 (runtime, store)
4036 }
4037
4038 #[test]
4039 fn tenant_credential_cannot_override_credential_scope() {
4040 use crate::runtime::within_clause::{FieldOverride, ScopeOverride};
4041
4042 let store = std::sync::Arc::new(AuthStore::new(AuthConfig {
4043 enabled: true,
4044 require_auth: true,
4045 ..AuthConfig::default()
4046 }));
4047 store
4048 .create_user_in_tenant(Some("acme"), "admin", "pw", Role::Admin)
4049 .expect("tenant admin");
4050 let runtime =
4051 RedDBRuntime::with_options(crate::api::RedDBOptions::in_memory()).expect("runtime");
4052 runtime.set_auth_store(store);
4053 let _identity = ScopedIdentityGuard::install("acme", "admin", Role::Admin);
4054
4055 runtime
4056 .execute_query("WITHIN TENANT 'acme' SELECT 1")
4057 .expect("own tenant remains valid");
4058 for sql in [
4059 "WITHIN TENANT 'globex' SELECT 1",
4060 "WITHIN TENANT NULL SELECT 1",
4061 "WITHIN TENANT 'acme' USER 'other' SELECT 1",
4062 "WITHIN TENANT 'acme' AS ROLE 'read' SELECT 1",
4063 ] {
4064 assert!(runtime.execute_query(sql).is_err(), "must reject {sql}");
4065 }
4066 assert!(runtime
4067 .execute_query_with_scope(
4068 "SELECT 1",
4069 ScopeOverride {
4070 tenant: FieldOverride::Set("globex".to_string()),
4071 ..ScopeOverride::default()
4072 },
4073 )
4074 .is_err());
4075
4076 set_current_connection_id(44_201);
4077 runtime.execute_query("BEGIN").expect("begin");
4078 assert!(runtime.execute_query("SET LOCAL TENANT 'globex'").is_err());
4079 runtime.execute_query("ROLLBACK").expect("rollback");
4080 clear_current_connection_id();
4081 }
4082
4083 #[test]
4084 fn tenant_admin_sql_user_lifecycle_is_tenant_bounded() {
4085 let store = std::sync::Arc::new(AuthStore::new(AuthConfig {
4086 enabled: true,
4087 require_auth: true,
4088 ..AuthConfig::default()
4089 }));
4090 store
4091 .create_user_in_tenant(Some("acme"), "admin", "pw", Role::Admin)
4092 .expect("tenant admin");
4093 let runtime =
4094 RedDBRuntime::with_options(crate::api::RedDBOptions::in_memory()).expect("runtime");
4095 runtime.set_auth_store(std::sync::Arc::clone(&store));
4096 let _identity = ScopedIdentityGuard::install("acme", "admin", Role::Admin);
4097
4098 runtime
4099 .execute_query("CREATE USER acme.alice PASSWORD 'pw' ROLE read")
4100 .expect("same-tenant create");
4101 assert!(runtime
4102 .execute_query("CREATE USER globex.alice PASSWORD 'pw' ROLE read")
4103 .is_err());
4104 assert!(runtime
4105 .execute_query("ALTER USER globex.alice PASSWORD 'owned'")
4106 .is_err());
4107 let cross_tenant_allow = crate::auth::policies::Policy::from_json_str(
4108 r#"{
4109 "id":"tenant-cross-user",
4110 "version":1,
4111 "statements":[{
4112 "effect":"allow",
4113 "actions":["user:create","user:password:change"],
4114 "resources":["user:tenant/globex/*"]
4115 }]
4116 }"#,
4117 )
4118 .expect("cross-tenant policy");
4119 store.put_policy(cross_tenant_allow).expect("put policy");
4120 store
4121 .attach_policy(
4122 crate::auth::store::PrincipalRef::User(crate::auth::UserId::scoped(
4123 "acme", "admin",
4124 )),
4125 "tenant-cross-user",
4126 )
4127 .expect("attach policy");
4128 assert!(runtime
4129 .execute_query("CREATE USER globex.bob PASSWORD 'pw' ROLE read")
4130 .is_err());
4131 assert!(store.get_user(Some("globex"), "alice").is_none());
4132 assert!(store.get_user(Some("globex"), "bob").is_none());
4133 }
4134
4135 #[test]
4136 fn slow_log_redacts_user_password_statements() {
4137 for sql in [
4138 "CREATE USER alice PASSWORD 'super-secret' ROLE admin",
4139 "CREATE USER acme.alice PASSWORD 'super-secret' ROLE read",
4140 "ALTER USER alice PASSWORD 'super-secret'",
4141 "AlTeR UsEr acme.alice PaSsWoRd 'super-secret'",
4142 "EXPLAIN CREATE USER alice PASSWORD 'super-secret' ROLE read",
4143 "EXPLAIN ALTER USER alice PASSWORD 'super-secret'",
4144 "ALTER USER alice PASSWORD 'unterminated",
4145 ] {
4146 let redacted = redact_sql_for_slow_log(sql);
4147 assert!(!redacted.contains("super-secret"), "leaked from {sql}");
4148 assert!(!redacted.contains("unterminated"), "leaked from {sql}");
4149 }
4150 assert_eq!(
4151 redact_sql_for_slow_log("SELECT password FROM accounts"),
4152 "SELECT password FROM accounts"
4153 );
4154 }
4155
4156 #[test]
4157 fn nested_reads_need_select_on_the_inner_table() {
4158 use crate::auth::privileges::{Action, GrantPrincipal, Resource};
4159 use crate::auth::UserId;
4160 let (runtime, store) = runtime_with_store("ro", Role::Read);
4161 runtime
4162 .execute_query("CREATE TABLE gate_open (a INTEGER)")
4163 .expect("create");
4164 runtime
4165 .execute_query("CREATE TABLE gate_closed (b INTEGER)")
4166 .expect("create");
4167 store
4170 .grant(
4171 &UserId::from_parts(None, "admin"),
4172 Role::Admin,
4173 GrantPrincipal::User(UserId::from_parts(None, "ro")),
4174 Resource::table_from_name("gate_open"),
4175 vec![Action::Select],
4176 false,
4177 None,
4178 )
4179 .expect("grant");
4180 let _identity = IdentityGuard::install("ro", Role::Read);
4181 runtime
4182 .execute_query("SELECT * FROM (SELECT a FROM gate_open) s")
4183 .expect("derived table over a granted table is fine");
4184 for sql in [
4187 "SELECT * FROM (SELECT b FROM gate_closed) s",
4188 "SELECT a FROM gate_open WHERE a IN (SELECT b FROM gate_closed)",
4189 ] {
4190 let err = runtime
4191 .execute_query(sql)
4192 .expect_err("nested read of an ungranted table must be refused");
4193 assert!(
4194 err.to_string().contains("permission denied"),
4195 "{sql}: unexpected error: {err}"
4196 );
4197 }
4198 }
4199
4200 #[test]
4201 fn policy_listings_are_self_or_policy_management() {
4202 let (runtime, store) = runtime_with_store("ro", Role::Read);
4203 store.create_user("other", "pw", Role::Read).expect("user");
4204 let _identity = IdentityGuard::install("ro", Role::Read);
4205 runtime
4206 .execute_query("SHOW EFFECTIVE PERMISSIONS FOR ro")
4207 .expect("a principal may inspect its own permissions");
4208 let err = runtime
4209 .execute_query("SHOW EFFECTIVE PERMISSIONS FOR other")
4210 .expect_err("inspecting another principal is policy management");
4211 assert!(
4212 err.to_string().contains("permission denied"),
4213 "unexpected error: {err}"
4214 );
4215 }
4216
4217 #[test]
4218 fn secret_columns_stay_masked_for_readers_without_secret_read() {
4219 use reddb_types::Value;
4220 let (runtime, store) = runtime_with_store("ro", Role::Read);
4221 store.create_user("root", "pw", Role::Admin).expect("admin");
4222 store.ensure_vault_secret_key();
4223 runtime
4224 .execute_query(
4225 "INSERT INTO creds_gate (name, token) VALUES ('stripe', SECRET('sk_live_abc'))",
4226 )
4227 .expect("insert");
4228
4229 let token_for = |username: &str, role: Role| {
4230 let _identity = IdentityGuard::install(username, role);
4231 let result = runtime
4232 .execute_query("SELECT name, token FROM creds_gate")
4233 .expect("select");
4234 result.result.records[0]
4235 .get("token")
4236 .cloned()
4237 .expect("token column")
4238 };
4239 assert!(
4242 matches!(token_for("ro", Role::Read), Value::Secret(_)),
4243 "a read-role principal must see the column masked"
4244 );
4245 assert!(matches!(token_for("root", Role::Admin), Value::Secret(_)));
4246 }
4247
4248 #[test]
4249 fn store_wide_operations_need_the_admin_tier() {
4250 let runtime = runtime_with_user("bob", Role::Write);
4251 let _identity = IdentityGuard::install("bob", Role::Write);
4252 for sql in ["VACUUM", "SCRUB"] {
4255 let err = runtime
4256 .execute_query(sql)
4257 .expect_err("store-wide operation must be refused for a non-admin");
4258 assert!(
4259 err.to_string().contains("permission denied"),
4260 "{sql}: unexpected error: {err}"
4261 );
4262 }
4263 }
4264
4265 #[test]
4266 fn explain_needs_the_privilege_of_the_statement_it_plans() {
4267 let runtime = runtime_with_user("ro", Role::Read);
4268 let _identity = IdentityGuard::install("ro", Role::Read);
4269 runtime
4270 .execute_query("EXPLAIN SELECT 1 AS one")
4271 .expect("planning a read is a read");
4272 let err = runtime
4275 .execute_query("EXPLAIN INSERT INTO gate_t (a) VALUES (1)")
4276 .expect_err("planning a write needs the write privilege");
4277 assert!(
4278 err.to_string().contains("permission denied"),
4279 "unexpected EXPLAIN error: {err}"
4280 );
4281 }
4282
4283 #[test]
4284 fn copy_from_a_server_path_is_admin_only() {
4285 let runtime = runtime_with_user("bob", Role::Write);
4286 let _identity = IdentityGuard::install("bob", Role::Write);
4287 let err = runtime
4291 .execute_query("COPY leak FROM '/etc/hostname'")
4292 .expect_err("COPY FROM must be refused for a non-admin");
4293 assert!(
4294 err.to_string().contains("permission denied"),
4295 "unexpected COPY error: {err}"
4296 );
4297 }
4298
4299 #[test]
4300 fn vcs_history_rewrites_are_admin_only() {
4301 let runtime = runtime_with_user("bob", Role::Write);
4302 let _identity = IdentityGuard::install("bob", Role::Write);
4303 let err = runtime
4306 .execute_query("RESET HARD TO 'deadbeef'")
4307 .expect_err("RESET must be refused for a non-admin");
4308 assert!(
4309 err.to_string().contains("permission denied"),
4310 "unexpected RESET error: {err}"
4311 );
4312 }
4313
4314 #[test]
4315 fn ai_provider_config_writes_are_admin_only() {
4316 let runtime = runtime_with_user("bob", Role::Write);
4317 let _identity = IdentityGuard::install("bob", Role::Write);
4318 let err = runtime
4321 .execute_query(
4322 "SET CONFIG red.config.ai.providers.openai.tokens.default.secret_ref = 'red.secret.aes_key'",
4323 )
4324 .expect_err("AI provider config must be refused for a non-admin");
4325 assert!(
4326 err.to_string().contains("permission denied"),
4327 "unexpected SET CONFIG error: {err}"
4328 );
4329 }
4330
4331 #[test]
4332 fn engine_internal_vault_keys_are_not_writable_through_set_secret() {
4333 let runtime = runtime_with_user("root", Role::Admin);
4334 let _identity = IdentityGuard::install("root", Role::Admin);
4335 for key in [
4339 "red.iam.policies",
4340 "red.iam.attachments.users",
4341 "red.secret.aes_key",
4342 ] {
4343 let err = runtime
4344 .execute_query(&format!("SET SECRET {key} = 'x'"))
4345 .expect_err("engine-internal vault key must be refused");
4346 assert!(
4347 err.to_string().contains("engine-internal"),
4348 "unexpected error for {key}: {err}"
4349 );
4350 }
4351 }
4352}