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