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