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