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 self.inner
2242 .transaction_state
2243 .snapshot_manager()
2244 .prune_aborted(cutoff_xid);
2245 for t in &targets {
2247 self.refresh_table_planner_stats(t);
2248 }
2249 let persisted = if *full {
2253 match store.persist() {
2254 Ok(()) => true,
2255 Err(e) => {
2256 return Err(RedDBError::Internal(format!(
2257 "VACUUM FULL persist failed: {e:?}"
2258 )));
2259 }
2260 }
2261 } else {
2262 false
2263 };
2264 self.invalidate_result_cache();
2266 (
2267 "vacuum",
2268 format!(
2269 "VACUUM{} processed {} table(s): scanned_versions={}, retained_versions={}, reclaimed_versions={}, retained_history_versions={}, reclaimed_history_versions={}, retained_tombstones={}, reclaimed_tombstones={}{}",
2270 if *full { " FULL" } else { "" },
2271 targets.len(),
2272 vacuum_stats.scanned_versions,
2273 vacuum_stats.retained_versions,
2274 vacuum_stats.reclaimed_versions,
2275 vacuum_stats.retained_history_versions,
2276 vacuum_stats.reclaimed_history_versions,
2277 vacuum_stats.retained_tombstones,
2278 vacuum_stats.reclaimed_tombstones,
2279 if persisted {
2280 " (pages flushed to disk)"
2281 } else {
2282 ""
2283 }
2284 ),
2285 )
2286 }
2287 };
2288 Ok(RuntimeQueryResult::ok_message(
2289 query.to_string(),
2290 &msg,
2291 kind,
2292 ))
2293 }
2294 QueryExpr::Grant(ref g) => self.execute_grant_statement(query, g),
2301 QueryExpr::Revoke(ref r) => self.execute_revoke_statement(query, r),
2302 QueryExpr::AlterUser(ref a) => self.execute_alter_user_statement(query, a),
2303 QueryExpr::CreateUser(ref u) => self.execute_create_user_statement(query, u),
2304 QueryExpr::CreateIamPolicy { ref id, ref json } => {
2305 self.execute_create_iam_policy(query, id, json)
2306 }
2307 QueryExpr::DropIamPolicy { ref id } => self.execute_drop_iam_policy(query, id),
2308 QueryExpr::AttachPolicy {
2309 ref policy_id,
2310 ref principal,
2311 } => self.execute_attach_policy(query, policy_id, principal),
2312 QueryExpr::DetachPolicy {
2313 ref policy_id,
2314 ref principal,
2315 } => self.execute_detach_policy(query, policy_id, principal),
2316 QueryExpr::ShowPolicies { ref filter } => {
2317 self.execute_show_policies(query, filter.as_ref())
2318 }
2319 QueryExpr::ShowEffectivePermissions {
2320 ref user,
2321 ref resource,
2322 } => self.execute_show_effective_permissions(query, user, resource.as_ref()),
2323 QueryExpr::SimulatePolicy {
2324 ref user,
2325 ref action,
2326 ref resource,
2327 } => self.execute_simulate_policy(query, user, action, resource),
2328 QueryExpr::LintPolicy { ref source } => self.execute_lint_policy(query, source),
2329 QueryExpr::MigratePolicyMode {
2330 ref target,
2331 dry_run,
2332 } => self.execute_migrate_policy_mode(query, target, dry_run),
2333 QueryExpr::CreateMigration(ref q) => self.execute_create_migration(query, q),
2334 QueryExpr::ApplyMigration(ref q) => self.execute_apply_migration(query, q),
2335 QueryExpr::RollbackMigration(ref q) => self.execute_rollback_migration(query, q),
2336 QueryExpr::ExplainMigration(ref q) => self.execute_explain_migration(query, q),
2337 _ => Err(RedDBError::Query(
2338 "unsupported command in runtime dispatcher".to_string(),
2339 )),
2340 };
2341
2342 if !control_event_specs.is_empty() {
2343 let (outcome, reason) = match &query_result {
2344 Ok(_) => (crate::runtime::control_events::Outcome::Allowed, None),
2345 Err(err) => (control_event_outcome_for_error(err), Some(err.to_string())),
2346 };
2347 for spec in &control_event_specs {
2348 self.emit_control_event(
2349 spec.kind,
2350 outcome,
2351 spec.action,
2352 spec.resource.clone(),
2353 reason.clone(),
2354 spec.fields.clone(),
2355 )?;
2356 }
2357 }
2358
2359 if let (Some(plan), Ok(result)) = (&query_audit_plan, &query_result) {
2360 self.emit_query_audit(
2361 query,
2362 plan,
2363 query_audit_started.elapsed().as_millis() as u64,
2364 result,
2365 );
2366 }
2367
2368 if let Ok(ref result) = query_result {
2375 frame.write_result_cache(self, result, result_cache_scopes);
2376 }
2377
2378 query_result
2379 }
2380
2381 pub fn execute_query_expr(&self, expr: QueryExpr) -> RedDBResult<RuntimeQueryResult> {
2401 let frame = super::statement_frame::StatementExecutionFrame::build(
2402 self,
2403 super::statement_frame::StatementIdentity::Expr(&expr),
2404 )?;
2405 let _frame_guards = Box::new(frame.install(self));
2406 let expr = self.rewrite_view_refs(expr);
2410
2411 self.validate_model_operations_before_auth(&expr)?;
2412 if let Err(err) = self.check_query_privilege(&expr) {
2416 return Err(RedDBError::Query(format!("permission denied: {err}")));
2417 }
2418
2419 let statement = query_expr_name(&expr);
2420 let mode = detect_mode(statement);
2421 let query_str = statement;
2422
2423 self.dispatch_expr(expr, query_str, mode)
2424 }
2425
2426 pub(super) fn validate_model_operations_before_auth(
2427 &self,
2428 expr: &QueryExpr,
2429 ) -> RedDBResult<()> {
2430 self.validate_vault_table_access(expr)?;
2431 use crate::catalog::CollectionModel;
2432 use crate::runtime::ddl::polymorphic_resolver;
2433 use reddb_rql::ast::KvCommand;
2434
2435 let system_schema_target = match expr {
2436 QueryExpr::DropTable(q) => Some(q.name.as_str()),
2437 QueryExpr::DropGraph(q) => Some(q.name.as_str()),
2438 QueryExpr::DropVector(q) => Some(q.name.as_str()),
2439 QueryExpr::DropDocument(q) => Some(q.name.as_str()),
2440 QueryExpr::DropKv(q) => Some(q.name.as_str()),
2441 QueryExpr::DropCollection(q) => Some(q.name.as_str()),
2442 QueryExpr::Truncate(q) => Some(q.name.as_str()),
2443 _ => None,
2444 };
2445 if system_schema_target.is_some_and(crate::runtime::impl_ddl::is_system_schema_name) {
2446 return Err(RedDBError::Query("system schema is read-only".to_string()));
2447 }
2448
2449 let expected = match expr {
2450 QueryExpr::DropTable(q) => Some((q.name.as_str(), CollectionModel::Table)),
2451 QueryExpr::DropGraph(q) => Some((q.name.as_str(), CollectionModel::Graph)),
2452 QueryExpr::DropVector(q) => Some((q.name.as_str(), CollectionModel::Vector)),
2453 QueryExpr::DropDocument(q) => Some((q.name.as_str(), CollectionModel::Document)),
2454 QueryExpr::DropKv(q) => Some((q.name.as_str(), q.model)),
2455 QueryExpr::DropCollection(q) => q.model.map(|model| (q.name.as_str(), model)),
2456 QueryExpr::Truncate(q) => q.model.map(|model| (q.name.as_str(), model)),
2457 QueryExpr::KvCommand(cmd) => {
2458 let (collection, model) = match cmd {
2459 KvCommand::Put {
2460 collection, model, ..
2461 }
2462 | KvCommand::Get {
2463 collection, model, ..
2464 }
2465 | KvCommand::Incr {
2466 collection, model, ..
2467 }
2468 | KvCommand::Cas {
2469 collection, model, ..
2470 }
2471 | KvCommand::List {
2472 collection, model, ..
2473 }
2474 | KvCommand::Delete {
2475 collection, model, ..
2476 } => (collection.as_str(), *model),
2477 KvCommand::Rotate { collection, .. }
2478 | KvCommand::History { collection, .. }
2479 | KvCommand::Purge { collection, .. } => {
2480 (collection.as_str(), CollectionModel::Vault)
2481 }
2482 KvCommand::InvalidateTags { collection, .. } => {
2483 (collection.as_str(), CollectionModel::Kv)
2484 }
2485 KvCommand::Watch {
2486 collection, model, ..
2487 } => (collection.as_str(), *model),
2488 KvCommand::Reveal { collection, .. } => {
2489 (collection.as_str(), CollectionModel::Vault)
2490 }
2491 };
2492 Some((collection, model))
2493 }
2494 QueryExpr::ConfigCommand(cmd) => {
2495 self.validate_config_command_before_auth(cmd)?;
2496 None
2497 }
2498 _ => None,
2499 };
2500
2501 let Some((name, expected_model)) = expected else {
2502 return Ok(());
2503 };
2504 let snapshot = self.inner.db.catalog_model_snapshot();
2505 let Some(actual_model) = snapshot
2506 .collections
2507 .iter()
2508 .find(|collection| collection.name == name)
2509 .map(|collection| collection.declared_model.unwrap_or(collection.model))
2510 else {
2511 return Ok(());
2512 };
2513 polymorphic_resolver::ensure_model_match(expected_model, actual_model)
2514 }
2515
2516 pub(super) fn rewrite_view_refs(&self, expr: QueryExpr) -> QueryExpr {
2521 if self.inner.views.read().is_empty() {
2523 return expr;
2524 }
2525 self.rewrite_view_refs_inner(expr)
2526 }
2527
2528 fn rewrite_view_refs_inner(&self, expr: QueryExpr) -> QueryExpr {
2529 use reddb_rql::ast::{Filter, TableSource};
2530 match expr {
2531 QueryExpr::Table(mut tq) => {
2532 if let Some(TableSource::Subquery(body)) = tq.source.take() {
2538 tq.source = Some(TableSource::Subquery(Box::new(
2539 self.rewrite_view_refs_inner(*body),
2540 )));
2541 return QueryExpr::Table(tq);
2542 }
2543
2544 let maybe_view = {
2548 let views = self.inner.views.read();
2549 views.get(&tq.table).cloned()
2550 };
2551 let Some(view) = maybe_view else {
2552 return QueryExpr::Table(tq);
2553 };
2554
2555 if view.materialized {
2561 return QueryExpr::Table(tq);
2562 }
2563
2564 let inner_expr = self.rewrite_view_refs_inner((*view.query).clone());
2568
2569 match inner_expr {
2577 QueryExpr::Table(mut inner_tq) => {
2578 if let Some(outer_filter) = tq.filter.take() {
2579 inner_tq.filter = Some(match inner_tq.filter.take() {
2580 Some(existing) => {
2581 Filter::And(Box::new(existing), Box::new(outer_filter))
2582 }
2583 None => outer_filter,
2584 });
2585 inner_tq.where_expr = inner_tq
2593 .filter
2594 .as_ref()
2595 .map(reddb_rql::sql_lowering::filter_to_expr);
2596 }
2597 if let Some(outer_limit) = tq.limit {
2598 inner_tq.limit = Some(match inner_tq.limit {
2599 Some(existing) => existing.min(outer_limit),
2600 None => outer_limit,
2601 });
2602 }
2603 if let Some(outer_offset) = tq.offset {
2604 inner_tq.offset = Some(match inner_tq.offset {
2605 Some(existing) => existing + outer_offset,
2606 None => outer_offset,
2607 });
2608 }
2609 QueryExpr::Table(inner_tq)
2610 }
2611 other => other,
2612 }
2613 }
2614 QueryExpr::Join(mut jq) => {
2615 jq.left = Box::new(self.rewrite_view_refs_inner(*jq.left));
2616 jq.right = Box::new(self.rewrite_view_refs_inner(*jq.right));
2617 QueryExpr::Join(jq)
2618 }
2619 other => other,
2622 }
2623 }
2624
2625 fn resolve_table_expr_subqueries(
2626 &self,
2627 mut table: TableQuery,
2628 frame: &dyn super::statement_frame::ReadFrame,
2629 ) -> RedDBResult<TableQuery> {
2630 match table.source.take() {
2637 Some(TableSource::Subquery(inner)) => {
2638 let inner = self.resolve_select_expr_subqueries(*inner, frame)?;
2639 table.source = Some(TableSource::Subquery(Box::new(inner)));
2640 }
2641 other => table.source = other,
2642 }
2643
2644 let outer_scopes = relation_scopes_for_query(&QueryExpr::Table(table.clone()));
2645 for item in &mut table.select_items {
2646 if let reddb_rql::ast::SelectItem::Expr { expr, .. } = item {
2647 *expr = self.resolve_expr_subqueries(expr.clone(), &outer_scopes, frame)?;
2648 }
2649 }
2650 if let Some(where_expr) = table.where_expr.take() {
2651 table.where_expr =
2652 Some(self.resolve_expr_subqueries(where_expr, &outer_scopes, frame)?);
2653 table.filter = None;
2654 }
2655 if let Some(having_expr) = table.having_expr.take() {
2656 table.having_expr =
2657 Some(self.resolve_expr_subqueries(having_expr, &outer_scopes, frame)?);
2658 table.having = None;
2659 }
2660 for expr in &mut table.group_by_exprs {
2661 *expr = self.resolve_expr_subqueries(expr.clone(), &outer_scopes, frame)?;
2662 }
2663 for clause in &mut table.order_by {
2664 if let Some(expr) = clause.expr.take() {
2665 clause.expr = Some(self.resolve_expr_subqueries(expr, &outer_scopes, frame)?);
2666 }
2667 }
2668 Ok(table)
2669 }
2670
2671 fn resolve_select_expr_subqueries(
2672 &self,
2673 expr: QueryExpr,
2674 frame: &dyn super::statement_frame::ReadFrame,
2675 ) -> RedDBResult<QueryExpr> {
2676 match expr {
2677 QueryExpr::Table(table) => self
2678 .resolve_table_expr_subqueries(table, frame)
2679 .map(QueryExpr::Table),
2680 QueryExpr::Join(mut join) => {
2681 join.left = Box::new(self.resolve_select_expr_subqueries(*join.left, frame)?);
2682 join.right = Box::new(self.resolve_select_expr_subqueries(*join.right, frame)?);
2683 Ok(QueryExpr::Join(join))
2684 }
2685 other => Ok(other),
2686 }
2687 }
2688
2689 fn resolve_expr_subqueries(
2690 &self,
2691 expr: reddb_rql::ast::Expr,
2692 outer_scopes: &[String],
2693 frame: &dyn super::statement_frame::ReadFrame,
2694 ) -> RedDBResult<reddb_rql::ast::Expr> {
2695 use reddb_rql::ast::Expr;
2696
2697 match expr {
2698 Expr::Subquery { query, span } => {
2699 let values = self.execute_expr_subquery_values(query, outer_scopes, frame)?;
2700 if values.len() > 1 {
2701 return Err(RedDBError::Query(
2702 "scalar subquery returned more than one row".to_string(),
2703 ));
2704 }
2705 Ok(Expr::Literal {
2706 value: values.into_iter().next().unwrap_or(Value::Null),
2707 span,
2708 })
2709 }
2710 Expr::BinaryOp { op, lhs, rhs, span } => Ok(Expr::BinaryOp {
2711 op,
2712 lhs: Box::new(self.resolve_expr_subqueries(*lhs, outer_scopes, frame)?),
2713 rhs: Box::new(self.resolve_expr_subqueries(*rhs, outer_scopes, frame)?),
2714 span,
2715 }),
2716 Expr::UnaryOp { op, operand, span } => Ok(Expr::UnaryOp {
2717 op,
2718 operand: Box::new(self.resolve_expr_subqueries(*operand, outer_scopes, frame)?),
2719 span,
2720 }),
2721 Expr::Cast {
2722 inner,
2723 target,
2724 span,
2725 } => Ok(Expr::Cast {
2726 inner: Box::new(self.resolve_expr_subqueries(*inner, outer_scopes, frame)?),
2727 target,
2728 span,
2729 }),
2730 Expr::FunctionCall { name, args, span } => {
2731 let args = args
2732 .into_iter()
2733 .map(|arg| self.resolve_expr_subqueries(arg, outer_scopes, frame))
2734 .collect::<RedDBResult<Vec<_>>>()?;
2735 Ok(Expr::FunctionCall { name, args, span })
2736 }
2737 Expr::WindowFunctionCall {
2738 name,
2739 args,
2740 mut window,
2741 span,
2742 } => {
2743 let args = args
2744 .into_iter()
2745 .map(|arg| self.resolve_expr_subqueries(arg, outer_scopes, frame))
2746 .collect::<RedDBResult<Vec<_>>>()?;
2747 for expr in &mut window.partition_by {
2748 *expr = self.resolve_expr_subqueries(expr.clone(), outer_scopes, frame)?;
2749 }
2750 for clause in &mut window.order_by {
2751 clause.expr =
2752 self.resolve_expr_subqueries(clause.expr.clone(), outer_scopes, frame)?;
2753 }
2754 Ok(Expr::WindowFunctionCall {
2755 name,
2756 args,
2757 window,
2758 span,
2759 })
2760 }
2761 Expr::Case {
2762 branches,
2763 else_,
2764 span,
2765 } => {
2766 let branches = branches
2767 .into_iter()
2768 .map(|(cond, value)| {
2769 Ok((
2770 self.resolve_expr_subqueries(cond, outer_scopes, frame)?,
2771 self.resolve_expr_subqueries(value, outer_scopes, frame)?,
2772 ))
2773 })
2774 .collect::<RedDBResult<Vec<_>>>()?;
2775 let else_ = else_
2776 .map(|expr| self.resolve_expr_subqueries(*expr, outer_scopes, frame))
2777 .transpose()?
2778 .map(Box::new);
2779 Ok(Expr::Case {
2780 branches,
2781 else_,
2782 span,
2783 })
2784 }
2785 Expr::IsNull {
2786 operand,
2787 negated,
2788 span,
2789 } => Ok(Expr::IsNull {
2790 operand: Box::new(self.resolve_expr_subqueries(*operand, outer_scopes, frame)?),
2791 negated,
2792 span,
2793 }),
2794 Expr::InList {
2795 target,
2796 values,
2797 negated,
2798 span,
2799 } => {
2800 let target =
2801 Box::new(self.resolve_expr_subqueries(*target, outer_scopes, frame)?);
2802 let mut resolved = Vec::new();
2803 for value in values {
2804 if let Expr::Subquery { query, .. } = value {
2805 resolved.extend(
2806 self.execute_expr_subquery_values(query, outer_scopes, frame)?
2807 .into_iter()
2808 .map(Expr::lit),
2809 );
2810 } else {
2811 resolved.push(self.resolve_expr_subqueries(value, outer_scopes, frame)?);
2812 }
2813 }
2814 Ok(Expr::InList {
2815 target,
2816 values: resolved,
2817 negated,
2818 span,
2819 })
2820 }
2821 Expr::Between {
2822 target,
2823 low,
2824 high,
2825 negated,
2826 span,
2827 } => Ok(Expr::Between {
2828 target: Box::new(self.resolve_expr_subqueries(*target, outer_scopes, frame)?),
2829 low: Box::new(self.resolve_expr_subqueries(*low, outer_scopes, frame)?),
2830 high: Box::new(self.resolve_expr_subqueries(*high, outer_scopes, frame)?),
2831 negated,
2832 span,
2833 }),
2834 other => Ok(other),
2835 }
2836 }
2837
2838 fn execute_expr_subquery_values(
2839 &self,
2840 subquery: reddb_rql::ast::ExprSubquery,
2841 outer_scopes: &[String],
2842 frame: &dyn super::statement_frame::ReadFrame,
2843 ) -> RedDBResult<Vec<Value>> {
2844 let query = *subquery.query;
2845 if query_references_outer_scope(&query, outer_scopes) {
2846 return Err(RedDBError::Query(
2847 "NOT_YET_SUPPORTED: correlated subqueries are not supported yet; track follow-up issue #470-correlated-subqueries".to_string(),
2848 ));
2849 }
2850 let query = self.rewrite_view_refs(query);
2851 let query = self.resolve_select_expr_subqueries(query, frame)?;
2852 let query = self.authorize_relational_select_expr(query, frame)?;
2853 let result = match query {
2854 QueryExpr::Table(table) => {
2855 execute_runtime_table_query(&self.inner.db, &table, Some(&self.inner.index_store))?
2856 }
2857 QueryExpr::Join(join) => execute_runtime_join_query(self, &join)?,
2858 other => {
2859 return Err(RedDBError::Query(format!(
2860 "expression subquery must be a SELECT query, got {}",
2861 query_expr_name(&other)
2862 )))
2863 }
2864 };
2865 first_column_values(result)
2866 }
2867
2868 fn execute_materialized_graph(
2869 &self,
2870 query: &str,
2871 mode: QueryMode,
2872 statement: &'static str,
2873 expr: &QueryExpr,
2874 ) -> RedDBResult<RuntimeQueryResult> {
2875 let (graph, node_properties, edge_properties) = self.materialize_graph_with_rls()?;
2883 let result =
2884 crate::storage::query::unified::UnifiedExecutor::execute_on_with_graph_properties_checked(
2885 &graph,
2886 expr,
2887 node_properties,
2888 edge_properties,
2889 super::function_budget::graph_work_check(),
2890 )
2891 .map_err(|err| RedDBError::Query(err.to_string()))?;
2892
2893 Ok(RuntimeQueryResult {
2894 query: query.to_string(),
2895 mode,
2896 statement,
2897 engine: "materialized-graph",
2898 result,
2899 affected_rows: 0,
2900 statement_type: "select",
2901 bookmark: None,
2902 notice: None,
2903 })
2904 }
2905
2906 fn dispatch_expr(
2907 &self,
2908 expr: QueryExpr,
2909 query_str: &str,
2910 mode: QueryMode,
2911 ) -> RedDBResult<RuntimeQueryResult> {
2912 let statement = query_expr_name(&expr);
2913 match expr {
2914 QueryExpr::Function(command) => self.execute_function(query_str, &command),
2915 QueryExpr::Graph(_) | QueryExpr::Path(_) => {
2916 self.execute_materialized_graph(query_str, mode, statement, &expr)
2917 }
2918 QueryExpr::Table(table) => {
2919 let scope = self.ai_scope();
2920 let table = self.resolve_table_expr_subqueries(
2921 table,
2922 &scope as &dyn super::statement_frame::ReadFrame,
2923 )?;
2924 if let Some(TableSource::Function {
2928 name,
2929 args,
2930 named_args,
2931 }) = table.source.clone()
2932 {
2933 return Ok(RuntimeQueryResult {
2934 query: query_str.to_string(),
2935 mode,
2936 statement,
2937 engine: "runtime-graph-tvf",
2938 result: self.execute_table_function(&name, &args, &named_args)?,
2939 affected_rows: 0,
2940 statement_type: "select",
2941 bookmark: None,
2942 notice: None,
2943 });
2944 }
2945 if let Some(TableSource::InlineGraphFunction {
2949 name,
2950 nodes,
2951 edges,
2952 named_args,
2953 }) = table.source.clone()
2954 {
2955 return Ok(RuntimeQueryResult {
2956 query: query_str.to_string(),
2957 mode,
2958 statement,
2959 engine: "runtime-graph-tvf-inline",
2960 result: self.execute_inline_graph_function(
2961 &name,
2962 &nodes,
2963 &edges,
2964 &named_args,
2965 )?,
2966 affected_rows: 0,
2967 statement_type: "select",
2968 bookmark: None,
2969 notice: None,
2970 });
2971 }
2972 if super::red_schema::is_virtual_table(&table.table) {
2973 return Ok(RuntimeQueryResult {
2974 query: query_str.to_string(),
2975 mode,
2976 statement,
2977 engine: "runtime-red-schema",
2978 result: super::red_schema::red_query(
2979 self,
2980 &table.table,
2981 &table,
2982 &scope as &dyn super::statement_frame::ReadFrame,
2983 )?,
2984 affected_rows: 0,
2985 statement_type: "select",
2986 bookmark: None,
2987 notice: None,
2988 });
2989 }
2990 if let Some(view_result) = self.try_resolve_analytics_view(
2992 &table,
2993 &scope as &dyn super::statement_frame::ReadFrame,
2994 )? {
2995 return Ok(RuntimeQueryResult {
2996 query: query_str.to_string(),
2997 mode,
2998 statement,
2999 engine: "runtime-graph-analytics-view",
3000 result: view_result,
3001 affected_rows: 0,
3002 statement_type: "select",
3003 bookmark: None,
3004 notice: None,
3005 });
3006 }
3007 let Some(table_with_rls) = self.authorize_relational_table_select(
3008 table,
3009 &scope as &dyn super::statement_frame::ReadFrame,
3010 )?
3011 else {
3012 return Ok(RuntimeQueryResult {
3013 query: query_str.to_string(),
3014 mode,
3015 statement,
3016 engine: "runtime-table-rls",
3017 result: crate::storage::query::unified::UnifiedResult::empty(),
3018 affected_rows: 0,
3019 statement_type: "select",
3020 bookmark: None,
3021 notice: None,
3022 });
3023 };
3024 Ok(RuntimeQueryResult {
3025 query: query_str.to_string(),
3026 mode,
3027 statement,
3028 engine: "runtime-table",
3029 result: execute_runtime_table_query(
3030 &self.inner.db,
3031 &table_with_rls,
3032 Some(&self.inner.index_store),
3033 )?,
3034 affected_rows: 0,
3035 statement_type: "select",
3036 bookmark: None,
3037 notice: None,
3038 })
3039 }
3040 QueryExpr::Join(join) => {
3041 let scope = self.ai_scope();
3042 let Some(join_with_rls) = self.authorize_relational_join_select(
3043 join,
3044 &scope as &dyn super::statement_frame::ReadFrame,
3045 )?
3046 else {
3047 return Ok(RuntimeQueryResult {
3048 query: query_str.to_string(),
3049 mode,
3050 statement,
3051 engine: "runtime-join-rls",
3052 result: crate::storage::query::unified::UnifiedResult::empty(),
3053 affected_rows: 0,
3054 statement_type: "select",
3055 bookmark: None,
3056 notice: None,
3057 });
3058 };
3059 Ok(RuntimeQueryResult {
3060 query: query_str.to_string(),
3061 mode,
3062 statement,
3063 engine: "runtime-join",
3064 result: execute_runtime_join_query(self, &join_with_rls)?,
3065 affected_rows: 0,
3066 statement_type: "select",
3067 bookmark: None,
3068 notice: None,
3069 })
3070 }
3071 QueryExpr::Vector(vector) => Ok(RuntimeQueryResult {
3072 query: query_str.to_string(),
3073 mode,
3074 statement,
3075 engine: "runtime-vector",
3076 result: execute_runtime_vector_query(self, &vector)?,
3077 affected_rows: 0,
3078 statement_type: "select",
3079 bookmark: None,
3080 notice: None,
3081 }),
3082 QueryExpr::Hybrid(hybrid) => Ok(RuntimeQueryResult {
3083 query: query_str.to_string(),
3084 mode,
3085 statement,
3086 engine: "runtime-hybrid",
3087 result: execute_runtime_hybrid_query(self, &hybrid)?,
3088 affected_rows: 0,
3089 statement_type: "select",
3090 bookmark: None,
3091 notice: None,
3092 }),
3093 QueryExpr::Insert(ref insert) if super::red_schema::is_virtual_table(&insert.table) => {
3094 Err(RedDBError::Query(
3095 super::red_schema::READ_ONLY_ERROR.to_string(),
3096 ))
3097 }
3098 QueryExpr::Update(ref update) if super::red_schema::is_virtual_table(&update.table) => {
3099 Err(RedDBError::Query(
3100 super::red_schema::READ_ONLY_ERROR.to_string(),
3101 ))
3102 }
3103 QueryExpr::Delete(ref delete) if super::red_schema::is_virtual_table(&delete.table) => {
3104 Err(RedDBError::Query(
3105 super::red_schema::READ_ONLY_ERROR.to_string(),
3106 ))
3107 }
3108 QueryExpr::Insert(ref insert) => self
3109 .with_deferred_store_wal_for_dml(self.insert_may_emit_events(insert), || {
3110 self.execute_insert(query_str, insert)
3111 }),
3112 QueryExpr::Update(ref update) => self
3113 .with_deferred_store_wal_for_dml(self.update_may_emit_events(update), || {
3114 self.execute_update(query_str, update)
3115 }),
3116 QueryExpr::Delete(ref delete) => self
3117 .with_deferred_store_wal_for_dml(self.delete_may_emit_events(delete), || {
3118 self.execute_delete(query_str, delete)
3119 }),
3120 QueryExpr::QueueCommand(ref cmd) => self.with_deferred_store_wal_if_transaction(|| {
3121 self.execute_queue_command(query_str, cmd)
3122 }),
3123 QueryExpr::KvCommand(ref cmd) => self.execute_kv_command(query_str, cmd),
3124 QueryExpr::SearchCommand(ref cmd) => self.execute_search_command(query_str, cmd),
3125 QueryExpr::Ask(ref ask) => self.execute_ask(query_str, ask),
3126 _ => Err(RedDBError::Query(format!(
3127 "prepared-statement execution does not support {statement} statements"
3128 ))),
3129 }
3130 }
3131
3132 fn execute_table_function(
3139 &self,
3140 name: &str,
3141 args: &[String],
3142 named_args: &[(String, f64)],
3143 ) -> RedDBResult<crate::storage::query::unified::UnifiedResult> {
3144 if name.eq_ignore_ascii_case("red.diff") {
3145 return self.execute_vcs_diff_tvf(args, named_args);
3146 }
3147 if !is_graph_tvf_name(name) {
3148 return Err(RedDBError::Query(format!("unknown table function: {name}")));
3149 }
3150 if args.len() != 1 {
3152 return Err(RedDBError::Query(format!(
3153 "table function '{name}' takes exactly 1 graph argument, got {}",
3154 args.len()
3155 )));
3156 }
3157
3158 let (nodes, edges) = self.materialize_whole_graph_abstract()?;
3163 self.dispatch_graph_algorithm(name, nodes, edges, named_args)
3164 }
3165
3166 pub(crate) fn revive_versioned_updates_since(
3167 &self,
3168 conn_id: u64,
3169 stamper_xid: u64,
3170 ) -> RedDBResult<usize> {
3171 let candidates = {
3174 let guard = self.inner.pending_versioned_updates.read();
3175 let Some(pending) = guard.get(&conn_id) else {
3176 return Ok(0);
3177 };
3178 pending
3179 .iter()
3180 .rev()
3181 .filter(|entry| entry.3 >= stamper_xid)
3182 .cloned()
3183 .collect::<Vec<_>>()
3184 };
3185 let mut reverted = 0usize;
3186 for candidate in candidates {
3187 let (collection, old_id, new_id, xid, previous_xmax) = &candidate;
3188 let topology_lock = self.index_store_ref().collection_topology_lock(collection);
3189 let topology_guard = topology_lock.read();
3190 let mut guard = self.inner.pending_versioned_updates.write();
3191 let Some(pending) = guard.get_mut(&conn_id) else {
3192 continue;
3193 };
3194 let Some(position) = pending.iter().rposition(|entry| entry == &candidate) else {
3197 continue;
3198 };
3199 self.revive_versioned_update(
3200 collection,
3201 *old_id,
3202 *new_id,
3203 *xid,
3204 *previous_xmax,
3205 topology_guard,
3206 )?;
3207 pending.remove(position);
3208 reverted += 1;
3209 if pending.is_empty() {
3210 guard.remove(&conn_id);
3211 }
3212 }
3213 Ok(reverted)
3214 }
3215
3216 fn check_explain_target_privilege(&self, inner_sql: &str) -> RedDBResult<()> {
3222 let parsed =
3223 reddb_rql::parser::parse(inner_sql).map_err(|e| RedDBError::Query(e.to_string()))?;
3224 self.check_query_privilege(&parsed.query)
3225 .map_err(|err| RedDBError::Query(format!("permission denied: {err}")))
3226 }
3227
3228 fn explain_as_rows(&self, raw_query: &str, inner_sql: &str) -> RedDBResult<RuntimeQueryResult> {
3236 self.check_explain_target_privilege(inner_sql)?;
3237 let explain = self.explain_query(inner_sql)?;
3238
3239 let mut columns = vec![
3240 "op".to_string(),
3241 "source".to_string(),
3242 "estimated_rows".to_string(),
3243 "estimated_cost".to_string(),
3244 "depth".to_string(),
3245 ];
3246
3247 let mut records: Vec<crate::storage::query::unified::UnifiedRecord> = Vec::new();
3248
3249 for name in &explain.cte_materializations {
3255 use std::sync::Arc;
3256 let mut rec = crate::storage::query::unified::UnifiedRecord::default();
3257 rec.set_arc(Arc::from("op"), Value::text("CteScan".to_string()));
3258 rec.set_arc(Arc::from("source"), Value::text(name.clone()));
3259 rec.set_arc(Arc::from("estimated_rows"), Value::Float(0.0));
3260 rec.set_arc(Arc::from("estimated_cost"), Value::Float(0.0));
3261 rec.set_arc(Arc::from("depth"), Value::Integer(0));
3262 records.push(rec);
3263 }
3264
3265 let vector_details = explain.statement == "vector";
3266 if vector_details {
3267 columns.extend(["access_path_reason".to_string(), "index_hint".to_string()]);
3268 }
3269 walk_plan_node(&explain.logical_plan.root, 0, &mut records, vector_details);
3270
3271 let result = crate::storage::query::unified::UnifiedResult {
3272 columns,
3273 records,
3274 stats: Default::default(),
3275 pre_serialized_json: None,
3276 };
3277
3278 Ok(RuntimeQueryResult {
3279 query: raw_query.to_string(),
3280 mode: explain.mode,
3281 statement: "explain",
3282 engine: "runtime-explain",
3283 result,
3284 affected_rows: 0,
3285 statement_type: "select",
3286 bookmark: None,
3287 notice: None,
3288 })
3289 }
3290
3291 fn explain_analyze_vector_as_rows(
3292 &self,
3293 raw_query: &str,
3294 expr: QueryExpr,
3295 ) -> RedDBResult<RuntimeQueryResult> {
3296 let QueryExpr::Vector(query) = &expr else {
3297 return Err(RedDBError::Internal(
3298 "expected vector analysis target".to_string(),
3299 ));
3300 };
3301 let source = query.collection.clone();
3302 let execution = self.execute_query_expr(expr)?;
3305 let stats = execution.result.stats;
3306 let vector = stats.vector.as_ref().ok_or_else(|| {
3307 RedDBError::Internal("vector executor did not provide measurements".to_string())
3308 })?;
3309 let fields = vec![
3310 ("op", Value::text(vector.access_path.clone())),
3311 ("source", Value::text(source)),
3312 ("index_used", Value::Boolean(vector.index_used)),
3313 ("mode_requested", Value::text(vector.mode_requested.clone())),
3314 ("mode_executed", Value::text(vector.mode_executed.clone())),
3315 (
3316 "fallback_reason",
3317 vector
3318 .fallback_reason
3319 .as_ref()
3320 .map_or(Value::Null, |reason| Value::text(reason.clone())),
3321 ),
3322 (
3323 "approximate_distance_evaluations",
3324 Value::UnsignedInteger(vector.approximate_distance_evaluations),
3325 ),
3326 (
3327 "peak_topk_entries",
3328 Value::UnsignedInteger(vector.peak_topk_entries),
3329 ),
3330 (
3331 "candidates_examined",
3332 Value::UnsignedInteger(vector.candidates_examined),
3333 ),
3334 (
3335 "metadata_rejected",
3336 Value::UnsignedInteger(vector.metadata_rejected),
3337 ),
3338 ("rls_rejected", Value::UnsignedInteger(vector.rls_rejected)),
3339 (
3340 "visibility_rejected",
3341 Value::UnsignedInteger(vector.visibility_rejected),
3342 ),
3343 (
3344 "exact_distance_evaluations",
3345 Value::UnsignedInteger(vector.exact_distance_evaluations),
3346 ),
3347 ("actual_rows", Value::UnsignedInteger(vector.rows_returned)),
3348 (
3349 "actual_ms",
3350 Value::Float(stats.exec_time_us as f64 / 1000.0),
3351 ),
3352 ("metrics_scope", Value::text("vector_pipeline")),
3353 (
3354 "operators",
3355 Value::Json(vector.operators_json().to_string().into_bytes()),
3356 ),
3357 ];
3358 let columns = fields.iter().map(|(name, _)| name.to_string()).collect();
3359 let mut record = crate::storage::query::unified::UnifiedRecord::default();
3360 for (name, value) in fields {
3361 record.set_owned(name.to_string(), value);
3362 }
3363 Ok(RuntimeQueryResult {
3364 query: raw_query.to_string(),
3365 mode: execution.mode,
3366 statement: "explain_analyze",
3367 engine: "runtime-explain-analyze",
3368 result: crate::storage::query::unified::UnifiedResult {
3369 columns,
3370 records: vec![record],
3371 stats,
3372 pre_serialized_json: None,
3373 },
3374 affected_rows: 0,
3375 statement_type: "select",
3376 bookmark: None,
3377 notice: None,
3378 })
3379 }
3380
3381 fn explain_analyze_as_rows(
3382 &self,
3383 raw_query: &str,
3384 inner_sql: &str,
3385 ) -> RedDBResult<RuntimeQueryResult> {
3386 if strip_keyword_ci(inner_sql.trim_start(), "VECTOR").is_some() {
3387 let expr = parse_multi(inner_sql).map_err(|err| RedDBError::Query(err.to_string()))?;
3388 if matches!(&expr, QueryExpr::Vector(_)) {
3389 return self.explain_analyze_vector_as_rows(raw_query, expr);
3390 }
3391 }
3392 if !starts_with_dml_keyword(inner_sql) {
3393 return Err(RedDBError::Query(
3394 "EXPLAIN ANALYZE currently supports INSERT, UPDATE, DELETE, and VECTOR SEARCH"
3395 .to_string(),
3396 ));
3397 }
3398
3399 self.check_explain_target_privilege(inner_sql)?;
3400 let explain = self.explain_query(inner_sql)?;
3401 let conn_id = current_connection_id();
3402 if self.inner.transaction_state.in_transaction(conn_id) {
3403 return Err(RedDBError::Query(
3404 "EXPLAIN ANALYZE requires no active transaction".to_string(),
3405 ));
3406 }
3407
3408 self.execute_query_inner("BEGIN")?;
3409 let started = std::time::Instant::now();
3410 let execution = self.execute_query_inner(inner_sql);
3411 let elapsed_ms = started.elapsed().as_secs_f64() * 1000.0;
3412 let rollback = self.execute_query_inner("ROLLBACK");
3413
3414 let affected_rows = match execution {
3415 Ok(result) => result.affected_rows,
3416 Err(err) => {
3417 rollback?;
3418 return Err(err);
3419 }
3420 };
3421 rollback?;
3422
3423 let columns = vec![
3424 "op".to_string(),
3425 "source".to_string(),
3426 "estimated_rows".to_string(),
3427 "estimated_cost".to_string(),
3428 "actual_rows".to_string(),
3429 "actual_ms".to_string(),
3430 "depth".to_string(),
3431 ];
3432 let mut records = Vec::new();
3433 walk_analyze_plan_node(
3434 &explain.logical_plan.root,
3435 0,
3436 affected_rows,
3437 elapsed_ms,
3438 &mut records,
3439 );
3440
3441 let result = crate::storage::query::unified::UnifiedResult {
3442 columns,
3443 records,
3444 stats: Default::default(),
3445 pre_serialized_json: None,
3446 };
3447
3448 Ok(RuntimeQueryResult {
3449 query: raw_query.to_string(),
3450 mode: explain.mode,
3451 statement: "explain_analyze",
3452 engine: "runtime-explain-analyze",
3453 result,
3454 affected_rows: 0,
3455 statement_type: "select",
3456 bookmark: None,
3457 notice: None,
3458 })
3459 }
3460}
3461
3462fn strip_explain_analyze_prefix(sql: &str) -> Option<&str> {
3463 let rest = strip_keyword_ci(sql.trim_start(), "EXPLAIN")?.trim_start();
3464 Some(strip_keyword_ci(rest, "ANALYZE")?.trim_start()).filter(|inner| !inner.is_empty())
3465}
3466
3467fn strip_keyword_ci<'a>(sql: &'a str, keyword: &str) -> Option<&'a str> {
3468 let head = sql.get(..keyword.len())?;
3471 let rest = &sql[keyword.len()..];
3472 if !head.eq_ignore_ascii_case(keyword) {
3473 return None;
3474 }
3475 if rest
3476 .chars()
3477 .next()
3478 .is_some_and(|ch| !ch.is_ascii_whitespace())
3479 {
3480 return None;
3481 }
3482 Some(rest)
3483}
3484
3485fn starts_with_dml_keyword(sql: &str) -> bool {
3486 let trimmed = sql.trim_start();
3487 let head_end = trimmed
3488 .find(|ch: char| ch.is_ascii_whitespace())
3489 .unwrap_or(trimmed.len());
3490 let head = &trimmed[..head_end];
3491 head.eq_ignore_ascii_case("INSERT")
3492 || head.eq_ignore_ascii_case("UPDATE")
3493 || head.eq_ignore_ascii_case("DELETE")
3494}
3495
3496fn walk_plan_node(
3497 node: &crate::storage::query::planner::CanonicalLogicalNode,
3498 depth: usize,
3499 out: &mut Vec<crate::storage::query::unified::UnifiedRecord>,
3500 vector_details: bool,
3501) {
3502 use std::sync::Arc;
3503 let mut rec = crate::storage::query::unified::UnifiedRecord::default();
3504 rec.set_arc(Arc::from("op"), Value::text(node.operator.clone()));
3505 rec.set_arc(
3506 Arc::from("source"),
3507 node.source.clone().map(Value::text).unwrap_or(Value::Null),
3508 );
3509 rec.set_arc(
3510 Arc::from("estimated_rows"),
3511 Value::Float(node.estimated_rows),
3512 );
3513 rec.set_arc(
3514 Arc::from("estimated_cost"),
3515 Value::Float(node.operator_cost),
3516 );
3517 rec.set_arc(Arc::from("depth"), Value::Integer(depth as i64));
3518 if vector_details {
3519 for name in ["access_path_reason", "index_hint"] {
3520 rec.set_owned(
3521 name.to_string(),
3522 node.details
3523 .get(name)
3524 .cloned()
3525 .map(Value::text)
3526 .unwrap_or(Value::Null),
3527 );
3528 }
3529 }
3530 out.push(rec);
3531 for child in &node.children {
3532 walk_plan_node(child, depth + 1, out, vector_details);
3533 }
3534}
3535
3536fn walk_analyze_plan_node(
3537 node: &crate::storage::query::planner::CanonicalLogicalNode,
3538 depth: usize,
3539 affected_rows: u64,
3540 elapsed_ms: f64,
3541 out: &mut Vec<crate::storage::query::unified::UnifiedRecord>,
3542) {
3543 use std::sync::Arc;
3544 let mut rec = crate::storage::query::unified::UnifiedRecord::default();
3545 rec.set_arc(Arc::from("op"), Value::text(node.operator.clone()));
3546 rec.set_arc(
3547 Arc::from("source"),
3548 node.source.clone().map(Value::text).unwrap_or(Value::Null),
3549 );
3550 rec.set_arc(
3551 Arc::from("estimated_rows"),
3552 Value::Float(node.estimated_rows),
3553 );
3554 rec.set_arc(
3555 Arc::from("estimated_cost"),
3556 Value::Float(node.operator_cost),
3557 );
3558 rec.set_arc(
3559 Arc::from("actual_rows"),
3560 Value::UnsignedInteger(if depth == 0 { affected_rows } else { 0 }),
3561 );
3562 rec.set_arc(
3563 Arc::from("actual_ms"),
3564 Value::Float(if depth == 0 { elapsed_ms } else { 0.0 }),
3565 );
3566 rec.set_arc(Arc::from("depth"), Value::Integer(depth as i64));
3567 out.push(rec);
3568
3569 for child in &node.children {
3570 walk_analyze_plan_node(child, depth + 1, 0, 0.0, out);
3571 }
3572}
3573
3574#[cfg(test)]
3575mod multibyte_sql_tests {
3576 use super::*;
3577
3578 #[test]
3579 fn explain_analyze_prefix_ignores_multibyte_text_at_the_keyword_boundary() {
3580 for pad in 0..16 {
3583 for wide in ["é", "日", "😀"] {
3584 let sql = format!("{}{wide}llo", "a".repeat(pad));
3585 assert_eq!(strip_explain_analyze_prefix(&sql), None, "{sql:?}");
3586 }
3587 }
3588 assert_eq!(
3589 strip_explain_analyze_prefix("explain analyze DELETE FROM t"),
3590 Some("DELETE FROM t")
3591 );
3592 }
3593
3594 #[test]
3599 fn multibyte_sql_never_panics_execute_query() {
3600 let rt = RedDBRuntime::in_memory().expect("in-memory runtime");
3601 let seeds = [
3602 "",
3603 "SELECT ",
3604 "SELECT '",
3605 "EXPLAIN ",
3606 "EXPLAIN ANALYZE ",
3607 "CHECKPOINT ",
3608 "CHECKOUT ",
3609 "RESET ",
3610 "MERGE ",
3611 "CHERRY PICK ",
3612 "REVERT ",
3613 "RESOLVE CONFLICT ",
3614 "INSERT INTO t VALUES ('",
3615 ];
3616 let mut panicked = Vec::new();
3617 for seed in seeds {
3618 for pad in 0..16 {
3619 for wide in ["é", "日", "😀"] {
3620 let sql = format!("{seed}{}{wide}llo", "a".repeat(pad));
3621 let outcome = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
3622 let _ = rt.execute_query(&sql);
3623 }));
3624 if outcome.is_err() {
3625 panicked.push(sql);
3626 }
3627 }
3628 }
3629 }
3630 assert!(
3631 panicked.is_empty(),
3632 "execute_query panicked on {} statement(s), first: {:?}",
3633 panicked.len(),
3634 panicked.first()
3635 );
3636 }
3637}
3638
3639#[cfg(test)]
3640mod inline_graph_tvf_tests {
3641 use super::*;
3642
3643 fn scopes_for(sql: &str) -> HashSet<String> {
3644 let expr = reddb_rql::parser::parse(sql).expect("parse").query;
3645 query_expr_result_cache_scopes(&expr)
3646 }
3647
3648 #[test]
3649 fn inline_tvf_cache_scopes_include_source_collections() {
3650 let scopes = scopes_for(
3654 "SELECT * FROM components(nodes => (SELECT id FROM hosts), edges => (SELECT src, dst FROM links))",
3655 );
3656 assert!(scopes.contains("hosts"), "nodes source scoped: {scopes:?}");
3657 assert!(scopes.contains("links"), "edges source scoped: {scopes:?}");
3658 }
3659
3660 #[test]
3661 fn graph_collection_tvf_cache_scope_is_graph_argument() {
3662 let scopes = scopes_for("SELECT * FROM components(g)");
3666 assert!(scopes.contains("g"), "collection form scoped: {scopes:?}");
3667 }
3668
3669 #[test]
3670 fn abstract_degree_centrality_counts_undirected_endpoints() {
3671 let nodes = vec!["a".to_string(), "b".to_string(), "c".to_string()];
3672 let edges = vec![
3673 ("a".to_string(), "b".to_string(), 1.0_f32),
3674 ("b".to_string(), "c".to_string(), 1.0_f32),
3675 ];
3676 let degrees = abstract_degree_centrality(&nodes, &edges);
3677 assert_eq!(
3678 degrees,
3679 vec![
3680 ("a".to_string(), 1),
3681 ("b".to_string(), 2),
3682 ("c".to_string(), 1),
3683 ]
3684 );
3685 }
3686}
3687
3688fn reject_engine_internal_vault_key(key: &str) -> RedDBResult<()> {
3695 let lowered = key.to_ascii_lowercase();
3696 let internal = lowered == "red.secret.aes_key"
3697 || lowered.starts_with("red.iam.")
3698 || lowered.starts_with("red.acl.")
3699 || lowered.starts_with("red.vault.");
3700 if internal {
3701 return Err(RedDBError::Query(format!(
3702 "`{key}` is an engine-internal vault key and cannot be set or deleted through SET/DELETE SECRET"
3703 )));
3704 }
3705 Ok(())
3706}
3707
3708fn confine_copy_from_path(path: &str) -> RedDBResult<()> {
3714 let Ok(root) = std::env::var("REDDB_COPY_IMPORT_ROOT") else {
3715 return Ok(());
3716 };
3717 let root = std::fs::canonicalize(&root).map_err(|e| {
3718 RedDBError::Query(format!(
3719 "REDDB_COPY_IMPORT_ROOT `{root}` is not usable: {e}"
3720 ))
3721 })?;
3722 let resolved = std::fs::canonicalize(path)
3723 .map_err(|e| RedDBError::Query(format!("COPY FROM `{path}`: {e}")))?;
3724 if !resolved.starts_with(&root) || !resolved.is_file() {
3725 return Err(RedDBError::Query(format!(
3726 "COPY FROM `{path}` is outside the configured import root"
3727 )));
3728 }
3729 Ok(())
3730}
3731
3732#[cfg(test)]
3733mod security_gate_tests {
3734 use super::*;
3735 use crate::auth::store::AuthStore;
3736 use crate::auth::{AuthConfig, Role};
3737
3738 struct IdentityGuard;
3741
3742 impl IdentityGuard {
3743 fn install(username: &str, role: Role) -> Self {
3744 set_current_auth_identity(username.to_string(), role);
3745 Self
3746 }
3747 }
3748
3749 impl Drop for IdentityGuard {
3750 fn drop(&mut self) {
3751 clear_current_auth_identity();
3752 }
3753 }
3754
3755 fn runtime_with_user(username: &str, role: Role) -> RedDBRuntime {
3756 let store = std::sync::Arc::new(AuthStore::new(AuthConfig {
3757 enabled: true,
3758 session_ttl_secs: 60,
3759 require_auth: true,
3760 auto_encrypt_storage: false,
3761 vault_enabled: false,
3762 cert: Default::default(),
3763 oauth: Default::default(),
3764 }));
3765 store.create_user(username, "pw", role).expect("user");
3766 let runtime =
3767 RedDBRuntime::with_options(crate::api::RedDBOptions::in_memory()).expect("runtime");
3768 runtime.set_auth_store(store);
3769 runtime
3770 }
3771
3772 fn runtime_with_store(username: &str, role: Role) -> (RedDBRuntime, std::sync::Arc<AuthStore>) {
3773 let store = std::sync::Arc::new(AuthStore::new(AuthConfig {
3774 enabled: true,
3775 session_ttl_secs: 60,
3776 require_auth: true,
3777 auto_encrypt_storage: false,
3778 vault_enabled: false,
3779 cert: Default::default(),
3780 oauth: Default::default(),
3781 }));
3782 store.create_user(username, "pw", role).expect("user");
3783 let runtime =
3784 RedDBRuntime::with_options(crate::api::RedDBOptions::in_memory()).expect("runtime");
3785 runtime.set_auth_store(std::sync::Arc::clone(&store));
3786 (runtime, store)
3787 }
3788
3789 #[test]
3790 fn nested_reads_need_select_on_the_inner_table() {
3791 use crate::auth::privileges::{Action, GrantPrincipal, Resource};
3792 use crate::auth::UserId;
3793 let (runtime, store) = runtime_with_store("ro", Role::Read);
3794 runtime
3795 .execute_query("CREATE TABLE gate_open (a INTEGER)")
3796 .expect("create");
3797 runtime
3798 .execute_query("CREATE TABLE gate_closed (b INTEGER)")
3799 .expect("create");
3800 store
3803 .grant(
3804 &UserId::from_parts(None, "admin"),
3805 Role::Admin,
3806 GrantPrincipal::User(UserId::from_parts(None, "ro")),
3807 Resource::table_from_name("gate_open"),
3808 vec![Action::Select],
3809 false,
3810 None,
3811 )
3812 .expect("grant");
3813 let _identity = IdentityGuard::install("ro", Role::Read);
3814 runtime
3815 .execute_query("SELECT * FROM (SELECT a FROM gate_open) s")
3816 .expect("derived table over a granted table is fine");
3817 for sql in [
3820 "SELECT * FROM (SELECT b FROM gate_closed) s",
3821 "SELECT a FROM gate_open WHERE a IN (SELECT b FROM gate_closed)",
3822 ] {
3823 let err = runtime
3824 .execute_query(sql)
3825 .expect_err("nested read of an ungranted table must be refused");
3826 assert!(
3827 err.to_string().contains("permission denied"),
3828 "{sql}: unexpected error: {err}"
3829 );
3830 }
3831 }
3832
3833 #[test]
3834 fn policy_listings_are_self_or_policy_management() {
3835 let (runtime, store) = runtime_with_store("ro", Role::Read);
3836 store.create_user("other", "pw", Role::Read).expect("user");
3837 let _identity = IdentityGuard::install("ro", Role::Read);
3838 runtime
3839 .execute_query("SHOW EFFECTIVE PERMISSIONS FOR ro")
3840 .expect("a principal may inspect its own permissions");
3841 let err = runtime
3842 .execute_query("SHOW EFFECTIVE PERMISSIONS FOR other")
3843 .expect_err("inspecting another principal is policy management");
3844 assert!(
3845 err.to_string().contains("permission denied"),
3846 "unexpected error: {err}"
3847 );
3848 }
3849
3850 #[test]
3851 fn secret_columns_stay_masked_for_readers_without_secret_read() {
3852 use reddb_types::Value;
3853 let (runtime, store) = runtime_with_store("ro", Role::Read);
3854 store.create_user("root", "pw", Role::Admin).expect("admin");
3855 store.ensure_vault_secret_key();
3856 runtime
3857 .execute_query(
3858 "INSERT INTO creds_gate (name, token) VALUES ('stripe', SECRET('sk_live_abc'))",
3859 )
3860 .expect("insert");
3861
3862 let token_for = |username: &str, role: Role| {
3863 let _identity = IdentityGuard::install(username, role);
3864 let result = runtime
3865 .execute_query("SELECT name, token FROM creds_gate")
3866 .expect("select");
3867 result.result.records[0]
3868 .get("token")
3869 .cloned()
3870 .expect("token column")
3871 };
3872 assert!(
3875 matches!(token_for("ro", Role::Read), Value::Secret(_)),
3876 "a read-role principal must see the column masked"
3877 );
3878 assert!(matches!(token_for("root", Role::Admin), Value::Secret(_)));
3879 }
3880
3881 #[test]
3882 fn store_wide_operations_need_the_admin_tier() {
3883 let runtime = runtime_with_user("bob", Role::Write);
3884 let _identity = IdentityGuard::install("bob", Role::Write);
3885 for sql in ["VACUUM", "SCRUB"] {
3888 let err = runtime
3889 .execute_query(sql)
3890 .expect_err("store-wide operation must be refused for a non-admin");
3891 assert!(
3892 err.to_string().contains("permission denied"),
3893 "{sql}: unexpected error: {err}"
3894 );
3895 }
3896 }
3897
3898 #[test]
3899 fn explain_needs_the_privilege_of_the_statement_it_plans() {
3900 let runtime = runtime_with_user("ro", Role::Read);
3901 let _identity = IdentityGuard::install("ro", Role::Read);
3902 runtime
3903 .execute_query("EXPLAIN SELECT 1 AS one")
3904 .expect("planning a read is a read");
3905 let err = runtime
3908 .execute_query("EXPLAIN INSERT INTO gate_t (a) VALUES (1)")
3909 .expect_err("planning a write needs the write privilege");
3910 assert!(
3911 err.to_string().contains("permission denied"),
3912 "unexpected EXPLAIN error: {err}"
3913 );
3914 }
3915
3916 #[test]
3917 fn copy_from_a_server_path_is_admin_only() {
3918 let runtime = runtime_with_user("bob", Role::Write);
3919 let _identity = IdentityGuard::install("bob", Role::Write);
3920 let err = runtime
3924 .execute_query("COPY leak FROM '/etc/hostname'")
3925 .expect_err("COPY FROM must be refused for a non-admin");
3926 assert!(
3927 err.to_string().contains("permission denied"),
3928 "unexpected COPY error: {err}"
3929 );
3930 }
3931
3932 #[test]
3933 fn vcs_history_rewrites_are_admin_only() {
3934 let runtime = runtime_with_user("bob", Role::Write);
3935 let _identity = IdentityGuard::install("bob", Role::Write);
3936 let err = runtime
3939 .execute_query("RESET HARD TO 'deadbeef'")
3940 .expect_err("RESET must be refused for a non-admin");
3941 assert!(
3942 err.to_string().contains("permission denied"),
3943 "unexpected RESET error: {err}"
3944 );
3945 }
3946
3947 #[test]
3948 fn ai_provider_config_writes_are_admin_only() {
3949 let runtime = runtime_with_user("bob", Role::Write);
3950 let _identity = IdentityGuard::install("bob", Role::Write);
3951 let err = runtime
3954 .execute_query(
3955 "SET CONFIG red.config.ai.providers.openai.tokens.default.secret_ref = 'red.secret.aes_key'",
3956 )
3957 .expect_err("AI provider config must be refused for a non-admin");
3958 assert!(
3959 err.to_string().contains("permission denied"),
3960 "unexpected SET CONFIG error: {err}"
3961 );
3962 }
3963
3964 #[test]
3965 fn engine_internal_vault_keys_are_not_writable_through_set_secret() {
3966 let runtime = runtime_with_user("root", Role::Admin);
3967 let _identity = IdentityGuard::install("root", Role::Admin);
3968 for key in [
3972 "red.iam.policies",
3973 "red.iam.attachments.users",
3974 "red.secret.aes_key",
3975 ] {
3976 let err = runtime
3977 .execute_query(&format!("SET SECRET {key} = 'x'"))
3978 .expect_err("engine-internal vault key must be refused");
3979 assert!(
3980 err.to_string().contains("engine-internal"),
3981 "unexpected error for {key}: {err}"
3982 );
3983 }
3984 }
3985}