1use std::cell::RefCell;
2use std::collections::HashSet;
3use std::rc::Rc;
4use std::sync::Arc;
5
6use super::execution_context::capture_current_snapshot;
7use super::impl_core::{
8 collections_referenced, current_auth_identity, current_connection_id, current_tenant,
9 has_with_prefix, intent_lock_modes_for, peek_top_level_as_of_with_table,
10 query_has_volatile_builtin, query_is_ask_statement, ConfigSnapshotGuard, CurrentSnapshotGuard,
11 KvStoreGuard, SecretStoreGuard, SnapshotContext, TxLocalTenantGuard,
12};
13use super::{RedDBRuntime, RuntimeQueryResult, RuntimeResultCacheEntry};
14use crate::api::{RedDBError, RedDBResult};
15use crate::auth::Role;
16use crate::storage::transaction::snapshot::{Snapshot, SnapshotRetentionGuard, Xid};
17use reddb_rql::ast::QueryExpr;
18use reddb_rql::modes::{detect_mode, parse_multi, QueryMode};
19
20#[derive(Debug, Clone, Copy, PartialEq, Eq)]
30pub(crate) enum Privilege {
31 Read,
34 Write,
38 Admin,
41 None,
45}
46
47impl Privilege {
48 pub(crate) fn is_satisfied_by(self, role: Role) -> bool {
52 match self {
53 Self::None => true,
54 Self::Read => role.can_read(),
55 Self::Write => role.can_write(),
56 Self::Admin => role.can_admin(),
57 }
58 }
59}
60
61#[derive(Debug, Clone, Copy, PartialEq, Eq)]
67pub(crate) enum LockIntent {
68 None,
71 Shared,
74 Exclusive,
79}
80
81pub(crate) trait ReadFrame {
94 fn effective_scope(&self) -> Option<&str>;
98
99 fn identity(&self) -> Option<(&str, Role)>;
103
104 fn snapshot(&self) -> &Snapshot;
109
110 fn snapshot_context(&self, runtime: &RedDBRuntime) -> SnapshotContext {
112 SnapshotContext {
113 branch: None,
114 snapshot: self.snapshot().clone(),
115 manager: runtime.inner.transaction_state.snapshot_manager(),
116 own_xids: HashSet::new(),
117 requires_index_fallback: true,
118 serializable_reader: None,
119 }
120 }
121
122 fn as_of_floor(&self) -> Option<Xid>;
127
128 fn cache_key(&self) -> &str;
131
132 fn should_cache_result(&self) -> bool;
146
147 fn required_privilege(&self) -> Privilege;
156
157 fn lock_intent(&self) -> LockIntent;
166
167 fn visible_collections(&self) -> Option<&std::collections::HashSet<String>>;
180}
181
182pub(crate) fn statement_is_write(query: &str) -> bool {
191 statement_kind(query) == "write"
192}
193
194fn statement_kind(query: &str) -> &'static str {
195 let trimmed = query.trim_start();
196 let trimmed = if let Some(rest) = trimmed.strip_prefix("--") {
199 rest.split_once('\n')
200 .map(|(_, r)| r)
201 .unwrap_or("")
202 .trim_start()
203 } else {
204 trimmed
205 };
206 let mut tokens = trimmed.split(|c: char| c.is_whitespace() || c == '(' || c == ';');
207 let first = tokens.next().unwrap_or("");
208 let second = tokens.next().unwrap_or("");
209 if first.eq_ignore_ascii_case("KV") {
210 if second.eq_ignore_ascii_case("GET")
211 || second.eq_ignore_ascii_case("LIST")
212 || second.eq_ignore_ascii_case("WATCH")
213 {
214 return "read";
215 }
216 return "write";
217 }
218 if first.eq_ignore_ascii_case("VAULT") {
219 if second.eq_ignore_ascii_case("GET")
220 || second.eq_ignore_ascii_case("REVEAL")
221 || second.eq_ignore_ascii_case("UNSEAL")
222 || second.eq_ignore_ascii_case("LIST")
223 || second.eq_ignore_ascii_case("WATCH")
224 || second.eq_ignore_ascii_case("HISTORY")
225 {
226 return "read";
227 }
228 return "write";
229 }
230 let mut buf = [0u8; 16];
232 let bytes = first.as_bytes();
233 let n = bytes.len().min(buf.len());
234 for i in 0..n {
235 buf[i] = bytes[i].to_ascii_uppercase();
236 }
237 match &buf[..n] {
238 b"SELECT" | b"WITH" | b"SHOW" | b"EXPLAIN" | b"DESCRIBE" | b"DESC" | b"RANK"
239 | b"APPROX" | b"APPROXIMATE" | b"ZRANK" | b"ZRANGE" | b"LIST" | b"WATCH" | b"GET"
240 | b"HISTORY" | b"REVEAL" | b"UNSEAL" => "read",
241 b"INSERT" | b"UPDATE" | b"DELETE" | b"UPSERT" | b"MERGE" | b"COPY" | b"TRUNCATE" => "write",
242 b"CREATE" | b"ALTER" | b"DROP" | b"PROMOTE" | b"REINDEX" | b"VACUUM" | b"ANALYZE" => "ddl",
243 b"GRANT" | b"REVOKE" => "admin",
244 b"BEGIN" | b"START" | b"COMMIT" | b"ROLLBACK" | b"SAVEPOINT" | b"RELEASE" | b"END"
245 | b"SET" | b"RESET" | b"PREPARE" | b"EXECUTE" | b"DEALLOCATE" | b"USE" => "control",
246 b"PUT" | b"INCR" | b"DECR" | b"ADD" | b"ROTATE" | b"PURGE" | b"INVALIDATE" => "write",
247 b"SCRUB" => "uncached",
248 _ => "unknown",
249 }
250}
251
252fn statement_uses_result_cache(query: &str) -> bool {
253 !matches!(
254 statement_kind(query),
255 "write" | "ddl" | "admin" | "control" | "uncached"
256 ) && !super::red_schema::is_computed_stats_query(query)
257}
258
259fn reject_unresolvable_as_of(expr: &QueryExpr) -> RedDBResult<()> {
271 let QueryExpr::Table(table) = expr else {
272 return Ok(());
273 };
274 if table.as_of.is_none() {
275 return Ok(());
276 }
277 Err(RedDBError::InvalidOperation(
278 "AS OF cannot be resolved from a pre-parsed expression — \
279 execute the statement text (execute_query / execute_prepared_query / \
280 execute_query_with_params) so the historical snapshot is resolved"
281 .to_string(),
282 ))
283}
284
285fn classify_privilege(query: &str) -> Privilege {
286 match statement_kind(query) {
287 "read" => Privilege::Read,
288 "write" => Privilege::Write,
289 "ddl" => Privilege::Write,
295 "admin" => Privilege::Admin,
296 _ => Privilege::None,
297 }
298}
299
300fn classify_lock_intent(query: &str) -> LockIntent {
301 match statement_kind(query) {
302 "read" => LockIntent::Shared,
303 "write" | "ddl" => LockIntent::Exclusive,
304 _ => LockIntent::None,
305 }
306}
307
308pub(super) struct StatementExecutionFrame {
309 branch: Option<Arc<reddb_engine::transaction::read::BranchReadView>>,
310 tx_local_tenant: Option<Option<String>>,
311 snapshot: Snapshot,
312 snapshot_retention: RefCell<Option<SnapshotRetentionGuard>>,
313 own_xids: HashSet<Xid>,
314 serializable_reader: Option<Xid>,
315 cache_key: String,
316 cache_data_epoch: Option<u64>,
317 cache_revision: u64,
318 is_volatile_query: bool,
319 cache_safe: bool,
320 effective_scope: Option<String>,
325 identity: Option<(String, Role)>,
328 as_of_floor: Option<Xid>,
331 requires_index_fallback: bool,
334 required_privilege: Privilege,
338 lock_intent: LockIntent,
341 visible_collections: Option<HashSet<String>>,
348 row_arena: Rc<RefCell<super::query_exec::RowBufferArena>>,
356}
357
358pub(super) struct StatementFrameGuards {
359 _tx_local_guard: TxLocalTenantGuard,
360 _config_snapshot_guard: ConfigSnapshotGuard,
361 _secret_store_guard: SecretStoreGuard,
362 _kv_store_guard: KvStoreGuard,
363 _snapshot_guard: CurrentSnapshotGuard,
364}
365
366pub(super) struct PreparedStatement {
367 pub(super) expr: QueryExpr,
368 pub(super) mode: QueryMode,
369}
370
371#[derive(Clone, Copy)]
380pub(super) enum StatementIdentity<'a> {
381 Text(&'a str),
383 UncachedText(&'a str),
386 Expr(&'a QueryExpr),
398}
399
400impl<'a> StatementIdentity<'a> {
401 fn text(&self) -> &'a str {
405 match self {
406 Self::Text(query) | Self::UncachedText(query) => query,
407 Self::Expr(_) => "",
408 }
409 }
410}
411
412impl StatementExecutionFrame {
413 pub(super) fn build(
414 runtime: &RedDBRuntime,
415 identity_source: StatementIdentity<'_>,
416 ) -> RedDBResult<Self> {
417 let query = identity_source.text();
418 let uses_result_cache = matches!(identity_source, StatementIdentity::Text(query) if statement_uses_result_cache(query));
419 let (cache_data_epoch, cache_revision) = match identity_source {
420 StatementIdentity::Text(_) if uses_result_cache => runtime.result_cache_snapshot(),
421 StatementIdentity::Text(_)
422 | StatementIdentity::UncachedText(_)
423 | StatementIdentity::Expr(_) => (None, 0),
424 };
425 if let StatementIdentity::Expr(expr) = identity_source {
426 reject_unresolvable_as_of(expr)?;
427 }
428 let conn_id = current_connection_id();
429 let tx_local_tenant = runtime.inner.transaction_state.local_tenant(conn_id);
430 let tx_context = runtime.inner.transaction_state.context(conn_id);
431 let mut own_xids = own_transaction_xids(tx_context.as_ref());
432 let graph_insert_xid = runtime.graph_insert_xid();
433 if let Some(xid) = graph_insert_xid {
434 own_xids.insert(xid);
435 }
436 let serializable_reader = tx_context
437 .as_ref()
438 .filter(|ctx| {
439 ctx.isolation == crate::storage::transaction::IsolationLevel::Serializable
440 })
441 .map(|ctx| ctx.xid);
442 let (snapshot, as_of_floor, snapshot_retention, branch) = {
443 let store = runtime.db().store();
444 let uses_vcs = runtime.vcs_has_versioned_collections()?;
445 let _publication = uses_vcs.then(|| store.vault_publication_lock.lock());
446 let (snapshot, as_of_floor, retention) =
447 runtime.statement_snapshot(query, identity_source)?;
448 let as_of = peek_top_level_as_of_with_table(query).map(|(spec, _)| spec);
449 let branch = if uses_vcs {
450 runtime.vcs_branch_read_view(&snapshot, as_of.as_ref())?
451 } else {
452 None
453 };
454 (snapshot, as_of_floor, retention, branch)
455 };
456 if as_of_floor.is_some() {
457 own_xids.clear();
458 }
459 let requires_index_fallback = as_of_floor.is_some()
460 || tx_context.is_some()
461 || graph_insert_xid.is_some()
462 || branch.is_some()
463 || !snapshot.in_progress.is_empty();
464 let (cache_key, is_volatile_query) = match identity_source {
465 StatementIdentity::Text(_) if uses_result_cache => (
466 result_cache_key(query),
467 query_has_volatile_builtin(query) || query_is_ask_statement(query),
468 ),
469 StatementIdentity::Text(_)
474 | StatementIdentity::UncachedText(_)
475 | StatementIdentity::Expr(_) => (String::new(), true),
476 };
477 let cache_safe = as_of_floor.is_none()
480 && branch.is_none()
481 && graph_insert_xid.is_none()
482 && runtime.result_cache_safe(conn_id);
483 let effective_scope = current_tenant();
488 let identity = current_auth_identity();
489
490 let required_privilege = classify_privilege(query);
498 let lock_intent = classify_lock_intent(query);
499
500 let visible_collections = match (runtime.inner.auth_store.read().clone(), identity.as_ref())
506 {
507 (Some(store), Some((principal, role))) => {
508 let collections = runtime.inner.db.store().list_collections();
509 Some(store.visible_collections_for_scope(
510 effective_scope.as_deref(),
511 *role,
512 principal,
513 &collections,
514 ))
515 }
516 _ => None,
517 };
518
519 Ok(Self {
520 branch,
521 tx_local_tenant,
522 snapshot,
523 snapshot_retention: RefCell::new(Some(snapshot_retention)),
524 own_xids,
525 serializable_reader,
526 cache_key,
527 cache_data_epoch,
528 cache_revision,
529 is_volatile_query,
530 cache_safe,
531 effective_scope,
532 identity,
533 as_of_floor,
534 requires_index_fallback,
535 required_privilege,
536 lock_intent,
537 visible_collections,
538 row_arena: Rc::new(RefCell::new(super::query_exec::RowBufferArena::new())),
539 })
540 }
541
542 pub(super) fn row_arena(&self) -> Rc<RefCell<super::query_exec::RowBufferArena>> {
547 Rc::clone(&self.row_arena)
548 }
549
550 pub(super) fn install(&self, runtime: &RedDBRuntime) -> StatementFrameGuards {
551 StatementFrameGuards {
552 _tx_local_guard: TxLocalTenantGuard::install(self.tx_local_tenant.clone()),
553 _config_snapshot_guard: ConfigSnapshotGuard::install(
554 Arc::clone(&runtime.inner.db),
555 runtime.inner.auth_store.read().clone(),
556 ),
557 _secret_store_guard: SecretStoreGuard::install(runtime),
558 _kv_store_guard: KvStoreGuard::install(runtime.inner.auth_store.read().clone()),
559 _snapshot_guard: CurrentSnapshotGuard::install(SnapshotContext {
560 branch: self.branch.clone(),
561 snapshot: self.snapshot.clone(),
562 manager: runtime.inner.transaction_state.snapshot_manager(),
563 own_xids: self.own_xids.clone(),
564 requires_index_fallback: self.requires_index_fallback,
565 serializable_reader: self.serializable_reader,
566 }),
567 }
568 }
569
570 pub(super) fn cache_key(&self) -> &str {
571 &self.cache_key
572 }
573
574 pub(super) fn can_read_result_cache(&self) -> bool {
575 <Self as ReadFrame>::should_cache_result(self)
578 }
579
580 pub(super) fn should_write_result_cache(&self, result: &RuntimeQueryResult) -> bool {
581 <Self as ReadFrame>::should_cache_result(self)
586 && result.statement_type == "select"
587 && result.engine != "vault"
588 && result.engine != "runtime-rank"
589 && result.statement != "queue_group_read"
594 && result.engine != "runtime-scrub"
599 && result.result.pre_serialized_json.is_none()
600 && (is_graph_tvf_engine(result.engine) || result.result.records.len() <= 5)
604 }
605
606 pub(super) fn read_result_cache(&self, runtime: &RedDBRuntime) -> Option<RuntimeQueryResult> {
607 if runtime.resource_limits().read_execution_limits_enabled() {
608 return None;
609 }
610 if self.can_read_result_cache() {
611 runtime
612 .get_result_cache_entry(self.cache_key(), self.cache_data_epoch)
613 .map(|mut result| {
614 if let Some(vector) = result.result.stats.vector.as_mut() {
615 vector.cache_hit = true;
616 }
617 result
618 })
619 } else {
620 None
621 }
622 }
623
624 pub(super) fn write_result_cache(
625 &self,
626 runtime: &RedDBRuntime,
627 result: &RuntimeQueryResult,
628 scopes: HashSet<String>,
629 ) {
630 if self.should_write_result_cache(result) {
631 runtime.put_result_cache_entry(
632 self.cache_key(),
633 RuntimeResultCacheEntry {
634 result: result.clone(),
635 cached_at: std::time::Instant::now(),
636 scopes,
637 data_epoch: self.cache_data_epoch.unwrap_or(0),
638 },
639 self.cache_data_epoch,
640 self.cache_revision,
641 );
642 }
643 }
644
645 pub(super) fn prepare_cte(&self, query: &str) -> RedDBResult<Option<QueryExpr>> {
646 if !has_with_prefix(query) {
655 return Ok(None);
656 }
657 let parsed =
658 reddb_rql::parser::parse(query).map_err(|err| RedDBError::Query(err.to_string()))?;
659 if parsed.with_clause.is_some() {
660 let rewritten = crate::storage::query::executors::inline_ctes(parsed)
661 .map_err(|err| RedDBError::Query(err.to_string()))?;
662 return Ok(Some(rewritten));
663 }
664 Ok(None)
668 }
669
670 pub(super) fn prepare_statement(
671 &self,
672 runtime: &RedDBRuntime,
673 query: &str,
674 ) -> RedDBResult<PreparedStatement> {
675 let mode = detect_mode(query);
676 if matches!(mode, QueryMode::Unknown) {
677 return Err(RedDBError::Query("unable to detect query mode".to_string()));
678 }
679
680 let first_word = query
691 .trim()
692 .split_ascii_whitespace()
693 .next()
694 .unwrap_or("")
695 .to_ascii_uppercase();
696 let is_insert = first_word == "INSERT";
697 let bypass_plan_cache = is_insert || self.is_volatile_query;
703
704 let (cache_key, prescan_binds) = if bypass_plan_cache {
710 (String::new(), Vec::new())
711 } else {
712 crate::storage::query::planner::cache_key::normalize_and_extract(query)
713 };
714
715 let expr = if bypass_plan_cache {
716 parse_multi(query).map_err(|err| RedDBError::Query(err.to_string()))?
718 } else {
719 let hit = {
726 let plan_cache = runtime.inner.query_cache.read();
727 plan_cache.peek(&cache_key).map(|cached| {
728 let parameter_count = cached.parameter_count;
729 let optimized = cached.plan.optimized.clone();
730 let exact_query = cached.exact_query.clone();
731 (parameter_count, optimized, exact_query)
732 })
733 };
734
735 if let Some((parameter_count, optimized, exact_query)) = hit {
736 if parameter_count > 0 {
737 let shape_binds = prescan_binds.clone();
739 if let Some(bound) =
740 crate::storage::query::planner::shape::bind_parameterized_query(
741 &optimized,
742 &shape_binds,
743 parameter_count,
744 )
745 {
746 bound
747 } else {
748 parse_multi(query).map_err(|err| RedDBError::Query(err.to_string()))?
751 }
752 } else {
753 if exact_query.as_deref() == Some(query) {
759 optimized
760 } else {
761 parse_multi(query).map_err(|err| RedDBError::Query(err.to_string()))?
762 }
763 }
764 } else {
765 let parsed =
767 parse_multi(query).map_err(|err| RedDBError::Query(err.to_string()))?;
768 let (cached_expr, parameter_count) = if let Some(prepared) =
769 crate::storage::query::planner::shape::parameterize_query_expr(&parsed)
770 {
771 (prepared.shape, prepared.parameter_count)
772 } else {
773 (parsed.clone(), 0)
774 };
775 {
776 let mut pc = runtime.inner.query_cache.write();
777 let plan = crate::storage::query::planner::QueryPlan::new(
778 parsed.clone(),
779 cached_expr,
780 Default::default(),
781 );
782 pc.insert(
783 cache_key.clone(),
784 crate::storage::query::planner::CachedPlan::new(plan)
785 .with_shape_key(cache_key.clone())
786 .with_exact_query(query.to_string())
787 .with_parameter_count(parameter_count),
788 );
789 }
790 parsed
791 }
792 };
793
794 let expr = runtime.rewrite_view_refs(expr);
799
800 Ok(PreparedStatement { expr, mode })
801 }
802
803 pub(super) fn check_query_privilege(
804 &self,
805 runtime: &RedDBRuntime,
806 expr: &QueryExpr,
807 ) -> RedDBResult<()> {
808 if let Some((username, role)) = <Self as ReadFrame>::identity(self) {
818 let needed = <Self as ReadFrame>::required_privilege(self);
819 if !needed.is_satisfied_by(role) {
820 if matches!(needed, Privilege::Admin) && runtime.inner.auth_store.read().is_none() {
829 crate::telemetry::operator_event::OperatorEvent::AuthBypass {
830 principal: username.to_string(),
831 resource: format!("statement requiring {needed:?}"),
832 detail: format!(
833 "auth_store not wired; coarse gate is sole defence (role={role:?})"
834 ),
835 }
836 .emit_global();
837 }
838 return Err(RedDBError::Query(format!(
839 "permission denied: principal=`{username}` role=`{role:?}` lacks {needed:?} privilege"
840 )));
841 }
842 }
843 runtime
844 .check_query_privilege(expr)
845 .map_err(|err| RedDBError::Query(format!("permission denied: {err}")))
846 }
847
848 pub(super) fn prepare_dispatch(
849 &self,
850 runtime: &RedDBRuntime,
851 expr: &QueryExpr,
852 ) -> RedDBResult<Option<crate::runtime::locking::LockerGuard>> {
853 runtime.validate_model_operations_before_auth(expr)?;
854 self.check_query_privilege(runtime, expr)?;
855 self.release_snapshot_for_maintenance(expr);
856 self.acquire_intent_locks(runtime, expr)
857 }
858
859 pub(super) fn release_snapshot_for_maintenance(&self, expr: &QueryExpr) {
860 if matches!(
861 expr,
862 QueryExpr::MaintenanceCommand(reddb_rql::ast::MaintenanceCommand::Vacuum { .. })
863 ) {
864 self.snapshot_retention.borrow_mut().take();
869 }
870 }
871
872 pub(super) fn acquire_intent_locks(
873 &self,
874 runtime: &RedDBRuntime,
875 expr: &QueryExpr,
876 ) -> RedDBResult<Option<crate::runtime::locking::LockerGuard>> {
877 use crate::runtime::lock_manager::LockMode;
878 use crate::runtime::locking::{LockerGuard, Resource};
879
880 if !runtime.config_bool("concurrency.locking.enabled", true) {
881 return Ok(None);
882 }
883 if matches!(<Self as ReadFrame>::lock_intent(self), LockIntent::None) {
888 return Ok(None);
889 }
890 let Some((global_mode, coll_mode)) = intent_lock_modes_for(expr) else {
891 return Ok(None);
892 };
893 let acquire = |guard: &mut LockerGuard, resource: Resource, mode: LockMode| {
896 guard.acquire(resource.clone(), mode).map_err(|err| {
897 RedDBError::Query(format!(
898 "could not acquire {mode:?} lock on {resource:?}: {err}; retry the statement"
899 ))
900 })
901 };
902 let mut guard = LockerGuard::new(runtime.inner.lock_manager.clone());
903 acquire(&mut guard, Resource::Global, global_mode)?;
904 if coll_mode == LockMode::Exclusive {
909 acquire(&mut guard, Resource::Catalog, LockMode::Exclusive)?;
910 }
911 for collection in collections_referenced(expr) {
912 acquire(&mut guard, Resource::Collection(collection), coll_mode)?;
913 }
914 Ok(Some(guard))
915 }
916}
917
918impl ReadFrame for StatementExecutionFrame {
919 fn snapshot_context(&self, runtime: &RedDBRuntime) -> SnapshotContext {
920 SnapshotContext {
921 branch: self.branch.clone(),
922 snapshot: self.snapshot.clone(),
923 manager: runtime.inner.transaction_state.snapshot_manager(),
924 own_xids: self.own_xids.clone(),
925 requires_index_fallback: self.requires_index_fallback,
926 serializable_reader: self.serializable_reader,
927 }
928 }
929
930 fn effective_scope(&self) -> Option<&str> {
931 self.effective_scope.as_deref()
932 }
933
934 fn identity(&self) -> Option<(&str, Role)> {
935 self.identity.as_ref().map(|(u, r)| (u.as_str(), *r))
936 }
937
938 fn snapshot(&self) -> &Snapshot {
939 &self.snapshot
940 }
941
942 fn as_of_floor(&self) -> Option<Xid> {
943 self.as_of_floor
944 }
945
946 fn cache_key(&self) -> &str {
947 &self.cache_key
948 }
949
950 fn should_cache_result(&self) -> bool {
951 !self.is_volatile_query && self.cache_safe
952 }
953
954 fn required_privilege(&self) -> Privilege {
955 self.required_privilege
956 }
957
958 fn lock_intent(&self) -> LockIntent {
959 self.lock_intent
960 }
961
962 fn visible_collections(&self) -> Option<&HashSet<String>> {
963 self.visible_collections.as_ref()
964 }
965}
966
967pub struct EffectiveScope {
981 pub(crate) captured_snapshot: Option<SnapshotContext>,
982 pub(crate) tenant: Option<String>,
983 pub(crate) identity: Option<(String, Role)>,
984 pub(crate) auth_principal: Option<crate::auth::UserId>,
985 pub(crate) snapshot: Snapshot,
986 pub(crate) visible_collections: Option<HashSet<String>>,
987}
988
989impl EffectiveScope {
990 pub fn has_capability(&self, _capability: &str) -> bool {
1000 false
1001 }
1002}
1003
1004impl ReadFrame for EffectiveScope {
1005 fn snapshot_context(&self, runtime: &RedDBRuntime) -> SnapshotContext {
1006 self.captured_snapshot
1007 .clone()
1008 .unwrap_or_else(|| SnapshotContext {
1009 branch: None,
1010 snapshot: self.snapshot.clone(),
1011 manager: runtime.inner.transaction_state.snapshot_manager(),
1012 own_xids: HashSet::new(),
1013 requires_index_fallback: true,
1014 serializable_reader: None,
1015 })
1016 }
1017
1018 fn effective_scope(&self) -> Option<&str> {
1019 self.tenant.as_deref()
1020 }
1021 fn identity(&self) -> Option<(&str, Role)> {
1022 self.identity.as_ref().map(|(u, r)| (u.as_str(), *r))
1023 }
1024 fn snapshot(&self) -> &Snapshot {
1025 &self.snapshot
1026 }
1027 fn as_of_floor(&self) -> Option<Xid> {
1028 None
1029 }
1030 fn cache_key(&self) -> &str {
1031 ""
1032 }
1033 fn should_cache_result(&self) -> bool {
1034 false
1035 }
1036 fn required_privilege(&self) -> Privilege {
1037 Privilege::Read
1038 }
1039 fn lock_intent(&self) -> LockIntent {
1040 LockIntent::Shared
1041 }
1042 fn visible_collections(&self) -> Option<&HashSet<String>> {
1043 self.visible_collections.as_ref()
1044 }
1045}
1046
1047fn own_transaction_xids(
1048 ctx: Option<&crate::storage::transaction::snapshot::TxnContext>,
1049) -> HashSet<Xid> {
1050 let mut set = HashSet::new();
1051 if let Some(ctx) = ctx {
1052 set.insert(ctx.xid);
1053 for (_, sub) in &ctx.savepoints {
1054 set.insert(*sub);
1055 }
1056 for sub in &ctx.released_sub_xids {
1057 set.insert(*sub);
1058 }
1059 }
1060 set
1061}
1062
1063impl RedDBRuntime {
1064 pub(crate) fn ai_scope(&self) -> EffectiveScope {
1070 let tenant = super::impl_core::current_tenant();
1071 let identity = super::impl_core::current_auth_identity();
1072 let auth_principal = super::execution_context::current_auth_principal();
1073 let captured_snapshot = super::execution_context::capture_current_snapshot();
1074 let snapshot = captured_snapshot.as_ref().map_or_else(
1075 || self.current_snapshot(),
1076 |context| context.snapshot.clone(),
1077 );
1078 let visible_collections = match (self.inner.auth_store.read().clone(), identity.as_ref()) {
1079 (Some(store), Some((principal, role))) => {
1080 let collections = self.inner.db.store().list_collections();
1081 Some(store.visible_collections_for_scope(
1082 tenant.as_deref(),
1083 *role,
1084 principal,
1085 &collections,
1086 ))
1087 }
1088 _ => None,
1089 };
1090 EffectiveScope {
1091 captured_snapshot,
1092 tenant,
1093 identity,
1094 auth_principal,
1095 snapshot,
1096 visible_collections,
1097 }
1098 }
1099}
1100
1101#[cfg(test)]
1105pub(crate) mod test_support {
1106 use super::{LockIntent, Privilege, ReadFrame};
1107 use crate::auth::Role;
1108 use crate::storage::transaction::snapshot::{Snapshot, Xid};
1109 use std::collections::HashSet;
1110
1111 pub(crate) struct FakeReadFrame {
1115 pub tenant: Option<String>,
1116 pub identity: Option<(String, Role)>,
1117 pub snapshot: Snapshot,
1118 pub visible: Option<HashSet<String>>,
1119 }
1120
1121 impl FakeReadFrame {
1122 pub(crate) fn without_scope() -> Self {
1123 Self {
1124 tenant: None,
1125 identity: None,
1126 snapshot: Snapshot {
1127 xid: 0,
1128 in_progress: HashSet::new(),
1129 },
1130 visible: None,
1131 }
1132 }
1133
1134 pub(crate) fn with_visible(visible: HashSet<String>) -> Self {
1135 Self {
1136 tenant: Some("acme".to_string()),
1137 identity: Some(("alice".to_string(), Role::Read)),
1138 snapshot: Snapshot {
1139 xid: 0,
1140 in_progress: HashSet::new(),
1141 },
1142 visible: Some(visible),
1143 }
1144 }
1145 }
1146
1147 impl ReadFrame for FakeReadFrame {
1148 fn effective_scope(&self) -> Option<&str> {
1149 self.tenant.as_deref()
1150 }
1151 fn identity(&self) -> Option<(&str, Role)> {
1152 self.identity.as_ref().map(|(u, r)| (u.as_str(), *r))
1153 }
1154 fn snapshot(&self) -> &Snapshot {
1155 &self.snapshot
1156 }
1157 fn as_of_floor(&self) -> Option<Xid> {
1158 None
1159 }
1160 fn cache_key(&self) -> &str {
1161 ""
1162 }
1163 fn should_cache_result(&self) -> bool {
1164 false
1165 }
1166 fn required_privilege(&self) -> Privilege {
1167 Privilege::Read
1168 }
1169 fn lock_intent(&self) -> LockIntent {
1170 LockIntent::Shared
1171 }
1172 fn visible_collections(&self) -> Option<&HashSet<String>> {
1173 self.visible.as_ref()
1174 }
1175 }
1176}
1177
1178impl RedDBRuntime {
1179 fn statement_snapshot(
1209 &self,
1210 query: &str,
1211 identity: StatementIdentity<'_>,
1212 ) -> RedDBResult<(Snapshot, Option<Xid>, SnapshotRetentionGuard)> {
1213 match peek_top_level_as_of_with_table(query) {
1214 Some((spec, Some(table))) => {
1215 if !table.starts_with("red_")
1216 && !table.starts_with("red.")
1217 && !self.vcs_is_versioned(&table)?
1218 {
1219 return Err(RedDBError::InvalidConfig(format!(
1220 "AS OF requires a versioned collection — \
1221 `{table}` has not opted in. \
1222 Call vcs.set_versioned(\"{table}\", true) first."
1223 )));
1224 }
1225 let snapshot = self.vcs_resolve_snapshot(spec)?;
1226 let xid = snapshot.xid;
1227 let (snapshot, retention) = self.snapshot_manager().retain_snapshot(snapshot);
1228 Ok((snapshot, Some(xid), retention))
1229 }
1230 Some((spec, None)) => {
1231 let snapshot = self.vcs_resolve_snapshot(spec)?;
1232 let xid = snapshot.xid;
1233 let (snapshot, retention) = self.snapshot_manager().retain_snapshot(snapshot);
1234 Ok((snapshot, Some(xid), retention))
1235 }
1236 None => match (identity, capture_current_snapshot()) {
1237 (StatementIdentity::Expr(_), Some(outer)) => {
1238 let (snapshot, retention) =
1239 self.snapshot_manager().retain_snapshot(outer.snapshot);
1240 Ok((snapshot, None, retention))
1241 }
1242 _ => {
1243 let (snapshot, retention) = self
1244 .inner
1245 .transaction_state
1246 .retained_current_snapshot(current_connection_id());
1247 Ok((snapshot, None, retention))
1248 }
1249 },
1250 }
1251 }
1252
1253 fn result_cache_safe(&self, conn_id: u64) -> bool {
1254 let has_unsafe_xids = self
1255 .inner
1256 .transaction_state
1257 .snapshot_manager()
1258 .has_cache_unsafe_active_xids();
1259 let in_own_tx = self.inner.transaction_state.in_transaction(conn_id);
1260 !has_unsafe_xids && !in_own_tx
1261 }
1262}
1263
1264fn is_graph_tvf_engine(engine: &str) -> bool {
1269 matches!(engine, "runtime-graph-tvf" | "runtime-graph-tvf-inline")
1270}
1271
1272fn result_cache_key(query: &str) -> String {
1273 let tenant = current_tenant().unwrap_or_default();
1274 let auth = current_auth_identity()
1275 .map(|(user, role)| format!("{}|{:?}", user, role))
1276 .unwrap_or_default();
1277 if tenant.is_empty() && auth.is_empty() {
1278 query.to_string()
1279 } else {
1280 format!("{query}\u{001e}{tenant}\u{001e}{auth}")
1281 }
1282}
1283
1284#[cfg(test)]
1285mod tests {
1286 use super::*;
1287 use crate::api::RedDBOptions;
1288 use crate::runtime::impl_core::{
1289 clear_current_auth_identity, clear_current_tenant, set_current_auth_identity,
1290 set_current_tenant,
1291 };
1292 use crate::runtime::RedDBRuntime;
1293
1294 fn fresh_runtime() -> RedDBRuntime {
1295 RedDBRuntime::with_options(RedDBOptions::in_memory()).expect("in-memory runtime")
1296 }
1297
1298 enum HeldFrame {
1299 Ordinary,
1300 Nested,
1301 Historical,
1302 }
1303
1304 fn verify_frame_survives_vacuum(delete: bool, kind: HeldFrame) {
1305 use crate::runtime::mvcc::{clear_current_connection_id, set_current_connection_id};
1306
1307 reset_thread_locals();
1308 clear_current_connection_id();
1309 let runtime = fresh_runtime();
1310 let execute = |connection, sql: &str| {
1311 set_current_connection_id(connection);
1312 let result = runtime.execute_query(sql);
1313 clear_current_connection_id();
1314 result.expect("valid frame retention fixture statement")
1315 };
1316 execute(1, "CREATE TABLE frame_rows (id TEXT, n INTEGER)");
1317 execute(1, "INSERT INTO frame_rows (id, n) VALUES ('row', 1)");
1318 let historical_xid = runtime.current_snapshot().xid;
1319 if matches!(kind, HeldFrame::Historical) {
1320 runtime
1321 .vcs_set_versioned("frame_rows", true)
1322 .expect("enable historical reads");
1323 }
1324 execute(2, "BEGIN");
1325 let query = if matches!(kind, HeldFrame::Historical) {
1326 format!("SELECT id, n FROM frame_rows AS OF SNAPSHOT {historical_xid}")
1327 } else {
1328 "SELECT id, n FROM frame_rows".to_string()
1329 };
1330 let frame = StatementExecutionFrame::build(&runtime, StatementIdentity::Text(&query))
1331 .expect("frame captures its snapshot");
1332 let frame = if matches!(kind, HeldFrame::Nested) {
1333 let guards = frame.install(&runtime);
1334 let expr = QueryExpr::Table(reddb_rql::ast::TableQuery::new("frame_rows"));
1335 let nested = StatementExecutionFrame::build(&runtime, StatementIdentity::Expr(&expr))
1336 .expect("nested frame inherits the protected snapshot");
1337 drop(guards);
1338 drop(frame);
1339 nested
1340 } else {
1341 frame
1342 };
1343 execute(
1346 2,
1347 if delete {
1348 "DELETE FROM frame_rows WHERE id = 'row'"
1349 } else {
1350 "UPDATE frame_rows SET n = 2 WHERE id = 'row'"
1351 },
1352 );
1353 execute(2, "COMMIT");
1354 execute(1, "VACUUM frame_rows");
1355 {
1356 let _guards = frame.install(&runtime);
1357 let records = super::super::record_search::scan_runtime_table_source_records_limited(
1358 &runtime.db(),
1359 "frame_rows",
1360 None,
1361 )
1362 .expect("captured frame scan succeeds after vacuum");
1363 assert_eq!(
1364 records.len(),
1365 1,
1366 "vacuum reclaimed an active frame's visible row"
1367 );
1368 assert_eq!(
1369 records[0]
1370 .get("n")
1371 .expect("selected value exists")
1372 .to_string(),
1373 "1"
1374 );
1375 }
1376 drop(frame);
1377 execute(1, "VACUUM frame_rows");
1378 let physical = runtime
1379 .db()
1380 .store()
1381 .get_collection("frame_rows")
1382 .expect("fixture collection exists")
1383 .query_all(|_| true)
1384 .len();
1385 assert_eq!(
1386 physical,
1387 usize::from(!delete),
1388 "completed frame leaked its retention pin"
1389 );
1390 clear_current_connection_id();
1391 }
1392
1393 #[test]
1394 fn autocommit_frame_retains_update_until_scan_completes() {
1395 verify_frame_survives_vacuum(false, HeldFrame::Ordinary);
1396 }
1397
1398 #[test]
1399 fn autocommit_frame_retains_delete_until_scan_completes() {
1400 verify_frame_survives_vacuum(true, HeldFrame::Ordinary);
1401 }
1402
1403 #[test]
1404 fn nested_frame_retains_history_after_parent_frame_drops() {
1405 verify_frame_survives_vacuum(false, HeldFrame::Nested);
1406 }
1407
1408 #[test]
1409 fn historical_frame_retains_history_until_scan_completes() {
1410 verify_frame_survives_vacuum(true, HeldFrame::Historical);
1411 }
1412
1413 fn reset_thread_locals() {
1416 clear_current_tenant();
1417 clear_current_auth_identity();
1418 }
1419
1420 #[test]
1421 fn autocommit_select_takes_live_snapshot() {
1422 reset_thread_locals();
1423 let rt = fresh_runtime();
1424 let frame = StatementExecutionFrame::build(&rt, StatementIdentity::Text("SELECT 1"))
1425 .expect("frame builds for SELECT 1");
1426
1427 let f: &dyn ReadFrame = &frame;
1430 assert!(f.as_of_floor().is_none(), "live read has no AS OF floor");
1431 assert_eq!(f.snapshot().xid, rt.snapshot_manager().peek_next_xid() - 1);
1432 assert!(!f.snapshot().sees(rt.snapshot_manager().peek_next_xid(), 0));
1433 }
1434
1435 #[test]
1436 fn autocommit_snapshot_does_not_lose_rows_after_index_publication() {
1437 reset_thread_locals();
1438 let rt = fresh_runtime();
1439 rt.execute_query("CREATE TABLE frame_index_race (id INT PRIMARY KEY, status TEXT)")
1440 .expect("create fixture");
1441 rt.execute_query("INSERT INTO frame_index_race (id, status) VALUES (1, 'old'), (2, 'old')")
1442 .expect("insert fixture");
1443 rt.execute_query("CREATE INDEX frame_status_hash ON frame_index_race (status) USING HASH")
1444 .expect("create index");
1445 let sql = "SELECT id FROM frame_index_race WHERE status = 'old'";
1446 let frame = StatementExecutionFrame::build(&rt, StatementIdentity::Text(sql))
1447 .expect("capture autocommit snapshot");
1448 rt.execute_query("UPDATE frame_index_race SET status = 'new' WHERE id = 1")
1449 .expect("publish changed index entry");
1450 let reddb_rql::ast::QueryExpr::Table(query) =
1451 reddb_rql::modes::parse_multi(sql).expect("parse")
1452 else {
1453 panic!("expected table query");
1454 };
1455 let _guards = frame.install(&rt);
1456 let records =
1457 super::super::query_exec::table::execute_runtime_canonical_table_query_indexed(
1458 &rt.db(),
1459 &query,
1460 Some(rt.index_store_ref()),
1461 )
1462 .expect("read captured snapshot");
1463 let mut ids = records
1464 .iter()
1465 .map(|record| record.get("id").expect("id").to_string())
1466 .collect::<Vec<_>>();
1467 ids.sort();
1468 assert_eq!(
1469 ids,
1470 ["1", "2"],
1471 "current index omitted a visible historical row"
1472 );
1473 }
1474
1475 #[test]
1476 fn captured_dml_snapshot_finds_rows_after_index_publication() {
1477 reset_thread_locals();
1478 let rt = fresh_runtime();
1479 rt.execute_query("CREATE TABLE frame_dml_race (id INT PRIMARY KEY, status TEXT)")
1480 .expect("create fixture");
1481 rt.execute_query("INSERT INTO frame_dml_race (id, status) VALUES (1, 'old'), (2, 'old')")
1482 .expect("insert fixture");
1483 rt.execute_query("CREATE INDEX frame_dml_status ON frame_dml_race (status) USING HASH")
1484 .expect("create index");
1485 let sql = "DELETE FROM frame_dml_race WHERE status = 'old'";
1486 let frame = StatementExecutionFrame::build(&rt, StatementIdentity::Text(sql))
1487 .expect("capture DML snapshot");
1488 rt.execute_query("UPDATE frame_dml_race SET status = 'new' WHERE id = 1")
1489 .expect("publish new indexed value");
1490 let reddb_rql::ast::QueryExpr::Table(query) =
1491 reddb_rql::modes::parse_multi("SELECT id FROM frame_dml_race WHERE status = 'old'")
1492 .expect("parse filter")
1493 else {
1494 panic!("expected table query")
1495 };
1496 let filter = reddb_rql::sql_lowering::effective_table_filter(&query).expect("filter");
1497 let _guards = frame.install(&rt);
1498 let ids = super::super::dml_target_scan::DmlTargetScan::new(
1499 &rt,
1500 "frame_dml_race",
1501 Some(&filter),
1502 None,
1503 )
1504 .find_target_ids()
1505 .expect("find DML targets");
1506 assert_eq!(
1507 ids.len(),
1508 2,
1509 "DML target scan must discover both snapshot-visible rows"
1510 );
1511 }
1512
1513 #[test]
1514 fn captured_snapshot_survives_indexed_update_and_delete_for_range_queries() {
1515 reset_thread_locals();
1516 let rt = fresh_runtime();
1517 rt.execute_query("CREATE TABLE frame_range_race (id INT PRIMARY KEY, score INT)")
1518 .expect("create fixture");
1519 rt.execute_query("INSERT INTO frame_range_race (id, score) VALUES (1, 10), (2, 20)")
1520 .expect("insert fixture");
1521 rt.execute_query("CREATE INDEX frame_score_range ON frame_range_race (score)")
1522 .expect("create index");
1523 let sql = "SELECT id FROM frame_range_race WHERE score >= 10 AND score <= 20 ORDER BY id";
1524 let frame = StatementExecutionFrame::build(&rt, StatementIdentity::Text(sql))
1525 .expect("capture read snapshot");
1526 rt.execute_query("UPDATE frame_range_race SET score = 99 WHERE id = 1")
1527 .expect("update row");
1528 rt.execute_query("DELETE FROM frame_range_race WHERE id = 2")
1529 .expect("delete row");
1530 let reddb_rql::ast::QueryExpr::Table(query) =
1531 reddb_rql::modes::parse_multi(sql).expect("parse")
1532 else {
1533 panic!("expected table query");
1534 };
1535 let _guards = frame.install(&rt);
1536 let records =
1537 super::super::query_exec::table::execute_runtime_canonical_table_query_indexed(
1538 &rt.db(),
1539 &query,
1540 Some(rt.index_store_ref()),
1541 )
1542 .expect("read captured snapshot");
1543 let ids = records
1544 .iter()
1545 .map(|record| record.get("id").expect("id").to_string())
1546 .collect::<Vec<_>>();
1547 assert_eq!(ids, ["1", "2"]);
1548 }
1549
1550 #[test]
1551 fn dml_target_scan_does_not_retry_inside_stored_function_budget() {
1552 use std::sync::atomic::{AtomicUsize, Ordering};
1553 use std::sync::Arc;
1554 reset_thread_locals();
1555 let rt = fresh_runtime();
1556 rt.execute_query("CREATE TABLE dml_budget_guard (id INT, status TEXT)")
1557 .expect("create fixture");
1558 rt.execute_query("INSERT INTO dml_budget_guard (id, status) VALUES (1, 'old')")
1559 .expect("insert fixture");
1560 rt.execute_query("CREATE INDEX dml_budget_status ON dml_budget_guard (status) USING HASH")
1561 .expect("create index");
1562 let frame = StatementExecutionFrame::build(
1563 &rt,
1564 StatementIdentity::Text("DELETE FROM dml_budget_guard WHERE status = 'old'"),
1565 )
1566 .expect("capture snapshot");
1567 let reddb_rql::ast::QueryExpr::Table(query) =
1568 reddb_rql::modes::parse_multi("SELECT id FROM dml_budget_guard WHERE status = 'old'")
1569 .expect("parse filter")
1570 else {
1571 panic!("expected table query")
1572 };
1573 let filter = reddb_rql::sql_lowering::effective_table_filter(&query).expect("filter");
1574 let calls = Arc::new(AtomicUsize::new(0));
1575 let observed = Arc::clone(&calls);
1576 *rt.index_store_ref().mutation_hook.lock() = Some(Arc::new(move |collection, phase| {
1577 if collection == "dml_budget_guard"
1578 && phase == super::super::index_store::MutationTestPhase::DmlIndexLookupStarted
1579 {
1580 observed.fetch_add(1, Ordering::SeqCst);
1581 }
1582 }));
1583 let _budget = super::super::function_budget::Scope::enter(1_000, 10_000)
1584 .expect("stored function budget");
1585 let _guards = frame.install(&rt);
1586 let ids = super::super::dml_target_scan::DmlTargetScan::new(
1587 &rt,
1588 "dml_budget_guard",
1589 Some(&filter),
1590 None,
1591 )
1592 .find_target_ids()
1593 .expect("find DML targets");
1594 assert_eq!(ids.len(), 1);
1595 assert_eq!(
1596 calls.load(Ordering::SeqCst),
1597 0,
1598 "bounded stored functions must not retry indexed discovery"
1599 );
1600 *rt.index_store_ref().mutation_hook.lock() = None;
1601 }
1602
1603 #[test]
1604 fn dml_target_selection_preserves_snapshot_when_publication_races() {
1605 use std::sync::{mpsc, Arc, Mutex};
1606 use std::time::Duration;
1607 reset_thread_locals();
1608 let rt = fresh_runtime();
1609 rt.execute_query("CREATE TABLE dml_lookup_race (id INT PRIMARY KEY, status TEXT)")
1610 .expect("create fixture");
1611 rt.execute_query("INSERT INTO dml_lookup_race (id, status) VALUES (1, 'old'), (2, 'old')")
1612 .expect("insert fixture");
1613 rt.execute_query("CREATE INDEX dml_lookup_status ON dml_lookup_race (status) USING HASH")
1614 .expect("create index");
1615 let frame = StatementExecutionFrame::build(
1616 &rt,
1617 StatementIdentity::Text("DELETE FROM dml_lookup_race WHERE status = 'old'"),
1618 )
1619 .expect("capture snapshot");
1620 let reddb_rql::ast::QueryExpr::Table(query) =
1621 reddb_rql::modes::parse_multi("SELECT id FROM dml_lookup_race WHERE status = 'old'")
1622 .expect("parse filter")
1623 else {
1624 panic!("expected table query")
1625 };
1626 let filter = reddb_rql::sql_lowering::effective_table_filter(&query).expect("filter");
1627 let (started_tx, started_rx) = mpsc::channel();
1628 let (published_tx, published_rx) = mpsc::channel();
1629 let published_rx = Mutex::new(published_rx);
1630 let first = std::sync::atomic::AtomicBool::new(true);
1631 *rt.index_store_ref().mutation_hook.lock() = Some(Arc::new(move |collection, phase| {
1632 if collection == "dml_lookup_race"
1633 && phase == super::super::index_store::MutationTestPhase::DmlTargetSelectionStarted
1634 && first.swap(false, std::sync::atomic::Ordering::SeqCst)
1635 {
1636 started_tx.send(()).expect("notify writer");
1637 published_rx
1638 .lock()
1639 .unwrap()
1640 .recv_timeout(Duration::from_secs(10))
1641 .expect("writer published");
1642 }
1643 }));
1644 std::thread::scope(|scope| {
1645 let writer_rt = &rt;
1646 let writer = scope.spawn(move || {
1647 started_rx
1648 .recv_timeout(Duration::from_secs(10))
1649 .expect("lookup started");
1650 writer_rt
1651 .execute_query("UPDATE dml_lookup_race SET status = 'new' WHERE id = 1")
1652 .expect("publish update");
1653 published_tx.send(()).expect("release lookup");
1654 });
1655 let limits = super::super::resource_limits::ResourceLimits {
1656 max_query_duration: Some(Duration::from_secs(30)),
1657 ..Default::default()
1658 };
1659 let slots = std::sync::atomic::AtomicU64::new(0);
1660 let _budget = super::super::query_budget::Scope::enter(&limits, &slots)
1661 .expect("query deadline scope");
1662 let _guards = frame.install(&rt);
1663 let ids = super::super::dml_target_scan::DmlTargetScan::new(
1664 &rt,
1665 "dml_lookup_race",
1666 Some(&filter),
1667 None,
1668 )
1669 .find_target_ids()
1670 .expect("find DML targets");
1671 writer.join().expect("writer thread");
1672 assert_eq!(
1673 ids.len(),
1674 2,
1675 "racing publication must not omit a snapshot-visible row"
1676 );
1677 });
1678 *rt.index_store_ref().mutation_hook.lock() = None;
1679 }
1680
1681 #[test]
1682 fn replica_replay_preserves_snapshots_across_insert_update_and_delete() {
1683 use crate::replication::cdc::{change_record_from_entity, ChangeOperation};
1684 use crate::replication::logical::{ApplyMode, LogicalChangeApplier};
1685 reset_thread_locals();
1686 let source = fresh_runtime();
1687 let replica = fresh_runtime();
1688 for rt in [&source, &replica] {
1689 rt.execute_query("CREATE TABLE replica_snapshot (id INT, status TEXT)")
1690 .expect("schema");
1691 }
1692 replica
1693 .execute_query(
1694 "CREATE INDEX replica_snapshot_status ON replica_snapshot (status) USING HASH",
1695 )
1696 .expect("index");
1697 let sql = "SELECT id FROM replica_snapshot WHERE status = 'old'";
1698 let capture = || {
1699 StatementExecutionFrame::build(&replica, StatementIdentity::Text(sql)).expect("capture")
1700 };
1701 let read = |frame: &StatementExecutionFrame| {
1702 let reddb_rql::ast::QueryExpr::Table(query) =
1703 reddb_rql::modes::parse_multi(sql).expect("parse")
1704 else {
1705 panic!("table")
1706 };
1707 let _guards = frame.install(&replica);
1708 super::super::query_exec::table::execute_runtime_canonical_table_query_indexed(
1709 &replica.db(),
1710 &query,
1711 Some(replica.index_store_ref()),
1712 )
1713 .expect("snapshot read")
1714 .len()
1715 };
1716 let empty = capture();
1717 source
1718 .execute_query("INSERT INTO replica_snapshot (id, status) VALUES (1, 'old')")
1719 .expect("insert");
1720 let applier = LogicalChangeApplier::new(0);
1721 let replay = |lsn, op| {
1722 let entity = source
1723 .db()
1724 .store()
1725 .get_collection("replica_snapshot")
1726 .unwrap()
1727 .query_all(|entity| entity.xmax == 0)
1728 .pop()
1729 .unwrap();
1730 applier
1731 .apply(
1732 &replica.db(),
1733 &change_record_from_entity(
1734 lsn,
1735 1,
1736 op,
1737 "replica_snapshot",
1738 "table",
1739 &entity,
1740 12,
1741 None,
1742 ),
1743 ApplyMode::Replica,
1744 )
1745 .expect("replay");
1746 };
1747 replay(1, ChangeOperation::Insert);
1748 assert_eq!(
1749 read(&empty),
1750 0,
1751 "replay must not backdate an insert into an earlier snapshot"
1752 );
1753 let inserted = capture();
1754 source
1755 .execute_query("UPDATE replica_snapshot SET status = 'new' WHERE id = 1")
1756 .expect("update");
1757 replay(2, ChangeOperation::Update);
1758 assert_eq!(read(&inserted), 1, "old version remains visible");
1759 assert_eq!(read(&capture()), 0, "fresh snapshot uses the updated index");
1760 let all_sql = "SELECT id FROM replica_snapshot";
1762 let before_delete =
1763 StatementExecutionFrame::build(&replica, StatementIdentity::Text(all_sql)).unwrap();
1764 replay(3, ChangeOperation::Delete);
1765 let reddb_rql::ast::QueryExpr::Table(query) =
1766 reddb_rql::modes::parse_multi(all_sql).unwrap()
1767 else {
1768 panic!("table")
1769 };
1770 {
1771 let _guards = before_delete.install(&replica);
1772 assert_eq!(
1773 super::super::query_exec::table::execute_runtime_canonical_table_query_indexed(
1774 &replica.db(),
1775 &query,
1776 Some(replica.index_store_ref())
1777 )
1778 .unwrap()
1779 .len(),
1780 1
1781 );
1782 }
1783 assert_eq!(
1784 replica.execute_query(all_sql).unwrap().result.records.len(),
1785 0
1786 );
1787 }
1788
1789 #[test]
1790 fn autocommit_index_reads_keep_old_rows_while_another_transaction_is_open() {
1791 use crate::runtime::mvcc::{clear_current_connection_id, set_current_connection_id};
1792 reset_thread_locals();
1793 let rt = fresh_runtime();
1794 let execute = |connection, sql: &str| {
1795 set_current_connection_id(connection);
1796 let result = rt.execute_query(sql);
1797 clear_current_connection_id();
1798 result.expect("statement")
1799 };
1800 execute(1, "CREATE TABLE active_writer_rows (id INT, status TEXT)");
1801 execute(
1802 1,
1803 "INSERT INTO active_writer_rows (id, status) VALUES (1, 'old')",
1804 );
1805 execute(
1806 1,
1807 "CREATE INDEX active_writer_status ON active_writer_rows (status) USING HASH",
1808 );
1809 execute(2, "BEGIN");
1810 execute(
1811 2,
1812 "UPDATE active_writer_rows SET status = 'new' WHERE id = 1",
1813 );
1814 assert_eq!(
1815 execute(1, "SELECT id FROM active_writer_rows WHERE status = 'old'")
1816 .result
1817 .records
1818 .len(),
1819 1
1820 );
1821 assert_eq!(
1822 execute(1, "SELECT id FROM active_writer_rows WHERE status = 'new'")
1823 .result
1824 .records
1825 .len(),
1826 0
1827 );
1828 execute(2, "ROLLBACK");
1829 assert_eq!(
1830 execute(1, "SELECT id FROM active_writer_rows WHERE status = 'old'")
1831 .result
1832 .records
1833 .len(),
1834 1
1835 );
1836 }
1837
1838 #[test]
1839 fn frame_captures_identity_and_scope() {
1840 reset_thread_locals();
1841 set_current_tenant("acme".to_string());
1842 set_current_auth_identity("alice".to_string(), Role::Write);
1843
1844 let rt = fresh_runtime();
1845 let frame = StatementExecutionFrame::build(&rt, StatementIdentity::Text("SELECT 1"))
1846 .expect("frame builds");
1847 let f: &dyn ReadFrame = &frame;
1848
1849 assert_eq!(f.effective_scope(), Some("acme"));
1850 let id = f.identity().expect("identity captured");
1851 assert_eq!(id.0, "alice");
1852 assert!(matches!(id.1, Role::Write));
1853
1854 assert!(
1857 f.cache_key().contains("acme") && f.cache_key().contains("alice"),
1858 "cache key folds in scope + identity, got {:?}",
1859 f.cache_key()
1860 );
1861
1862 reset_thread_locals();
1863 }
1864
1865 #[test]
1866 fn as_of_rejects_non_versioned_user_collection() {
1867 reset_thread_locals();
1868 let rt = fresh_runtime();
1869
1870 let err = match StatementExecutionFrame::build(
1874 &rt,
1875 StatementIdentity::Text("SELECT * FROM not_versioned AS OF COMMIT 'deadbeef'"),
1876 ) {
1877 Err(e) => e,
1878 Ok(_) => panic!("AS OF on non-versioned user collection rejected"),
1879 };
1880
1881 let msg = format!("{err}");
1882 assert!(
1883 msg.contains("AS OF requires a versioned collection"),
1884 "expected AS OF rejection, got: {msg}"
1885 );
1886 }
1887
1888 #[test]
1900 fn select_path_routes_through_frame_cache_key() {
1901 reset_thread_locals();
1902 set_current_tenant("acme".to_string());
1903 set_current_auth_identity("alice".to_string(), Role::Read);
1904
1905 let rt = fresh_runtime();
1906 let result = rt
1907 .execute_query("SELECT 1")
1908 .expect("SELECT 1 executes under tenant=acme/identity=alice");
1909 assert_eq!(result.statement_type, "select");
1910
1911 let cache = rt.inner.result_blob_entries.read();
1917 let any_keyed_with_scope = cache
1918 .0
1919 .keys()
1920 .any(|k| k.contains("acme") && k.contains("alice"));
1921 assert!(
1922 any_keyed_with_scope,
1923 "expected at least one result-cache key carrying tenant+identity, \
1924 got keys: {:?}",
1925 cache.0.keys().collect::<Vec<_>>()
1926 );
1927
1928 reset_thread_locals();
1929 }
1930
1931 #[test]
1943 fn volatile_select_does_not_populate_result_cache() {
1944 reset_thread_locals();
1945 let rt = fresh_runtime();
1946
1947 let frame = StatementExecutionFrame::build(
1951 &rt,
1952 StatementIdentity::Text("SELECT pg_advisory_unlock(1)"),
1953 )
1954 .expect("frame");
1955 let f: &dyn ReadFrame = &frame;
1956 assert!(
1957 !f.should_cache_result(),
1958 "volatile builtin must disable result-cache safety"
1959 );
1960
1961 let _ = rt
1966 .execute_query("SELECT pg_advisory_unlock(1)")
1967 .expect("volatile SELECT executes");
1968 let cache = rt.inner.result_blob_entries.read();
1969 let key = result_cache_key("SELECT pg_advisory_unlock(1)");
1970 assert!(
1971 !cache.0.contains_key(&key),
1972 "volatile SELECT must not populate result cache, found key {key:?} in {:?}",
1973 cache.0.keys().collect::<Vec<_>>()
1974 );
1975
1976 reset_thread_locals();
1977 }
1978
1979 #[test]
1980 fn result_cache_populates_blob_path() {
1981 reset_thread_locals();
1982 let rt = fresh_runtime();
1983 let result = rt.execute_query("SELECT 1").expect("SELECT 1 executes");
1984 assert_eq!(result.statement_type, "select");
1985
1986 let key = result_cache_key("SELECT 1");
1987 rt.inner.result_blob_entries.write().5 = Some(std::time::Instant::now());
1988 rt.promote_volatile_result_cache_entry();
1989 assert!(
1990 rt.inner
1991 .result_blob_cache
1992 .get("runtime.result_cache", &key)
1993 .is_some(),
1994 "blob backend should stamp the Blob Cache path"
1995 );
1996 assert!(rt.inner.result_blob_entries.read().0.contains_key(&key));
1997 }
1998
1999 #[test]
2000 fn blob_cache_backend_keeps_volatile_select_out_of_blob_path() {
2001 reset_thread_locals();
2002 let rt = fresh_runtime();
2003 let _ = rt
2004 .execute_query("SELECT pg_advisory_unlock(1)")
2005 .expect("volatile SELECT executes");
2006 let key = result_cache_key("SELECT pg_advisory_unlock(1)");
2007 assert!(
2008 rt.inner
2009 .result_blob_cache
2010 .get("runtime.result_cache", &key)
2011 .is_none(),
2012 "volatile SELECT must not populate blob result cache"
2013 );
2014 assert!(!rt.inner.result_blob_entries.read().0.contains_key(&key));
2015 }
2016
2017 #[test]
2018 fn as_of_on_red_collection_records_floor() {
2019 reset_thread_locals();
2020 let rt = fresh_runtime();
2021
2022 let frame = StatementExecutionFrame::build(
2025 &rt,
2026 StatementIdentity::Text("SELECT * FROM red.vcs.commits AS OF SNAPSHOT 1"),
2027 )
2028 .expect("AS OF SNAPSHOT 1 on red.vcs.commits resolves");
2029
2030 let f: &dyn ReadFrame = &frame;
2031 assert_eq!(
2032 f.as_of_floor(),
2033 Some(1),
2034 "AS OF SNAPSHOT 1 records xid=1 as the floor"
2035 );
2036 assert_eq!(f.snapshot().xid, 1);
2037 assert!(
2038 f.snapshot().in_progress.is_empty(),
2039 "historical reads have no in-progress set"
2040 );
2041 }
2042
2043 #[test]
2049 fn frame_classifies_privilege_and_lock_intent_from_prefix() {
2050 reset_thread_locals();
2051 let rt = fresh_runtime();
2052
2053 let cases = [
2054 ("SELECT 1", Privilege::Read, LockIntent::Shared),
2055 ("LIST KV settings", Privilege::Read, LockIntent::Shared),
2056 (
2057 "KV GET settings.feature",
2058 Privilege::Read,
2059 LockIntent::Shared,
2060 ),
2061 ("VAULT LIST secrets", Privilege::Read, LockIntent::Shared),
2062 (
2063 "INSERT INTO t (id) VALUES (1)",
2064 Privilege::Write,
2065 LockIntent::Exclusive,
2066 ),
2067 (
2068 "KV PUT settings.feature = 'on'",
2069 Privilege::Write,
2070 LockIntent::Exclusive,
2071 ),
2072 (
2073 "VAULT PUT secrets.api = 'x'",
2074 Privilege::Write,
2075 LockIntent::Exclusive,
2076 ),
2077 (
2078 "UPDATE t SET x = 1 WHERE id = 1",
2079 Privilege::Write,
2080 LockIntent::Exclusive,
2081 ),
2082 (
2083 "DELETE FROM t WHERE id = 1",
2084 Privilege::Write,
2085 LockIntent::Exclusive,
2086 ),
2087 (
2088 "CREATE TABLE foo (id INT)",
2089 Privilege::Write,
2090 LockIntent::Exclusive,
2091 ),
2092 ("BEGIN", Privilege::None, LockIntent::None),
2093 ("COMMIT", Privilege::None, LockIntent::None),
2094 ("SET timezone = 'UTC'", Privilege::None, LockIntent::None),
2095 ];
2096
2097 for (q, want_priv, want_lock) in cases {
2098 let frame = StatementExecutionFrame::build(&rt, StatementIdentity::Text(q))
2099 .unwrap_or_else(|e| panic!("frame builds for {q:?}: {e}"));
2100 let f: &dyn ReadFrame = &frame;
2101 assert_eq!(f.required_privilege(), want_priv, "privilege for {q:?}");
2102 assert_eq!(f.lock_intent(), want_lock, "lock intent for {q:?}");
2103 }
2104 }
2105
2106 #[test]
2123 fn insert_under_read_role_denied_via_frame_privilege() {
2124 reset_thread_locals();
2125 set_current_auth_identity("alice".to_string(), Role::Read);
2126
2127 let rt = fresh_runtime();
2128 let frame = StatementExecutionFrame::build(
2134 &rt,
2135 StatementIdentity::Text("INSERT INTO t (id) VALUES (1)"),
2136 )
2137 .expect("frame builds for INSERT");
2138 let f: &dyn ReadFrame = &frame;
2139 assert_eq!(
2140 f.required_privilege(),
2141 Privilege::Write,
2142 "INSERT classified as Write"
2143 );
2144 let id = f.identity().expect("identity captured");
2145 assert!(
2146 !f.required_privilege().is_satisfied_by(id.1),
2147 "Role::Read does not satisfy Privilege::Write — frame must deny"
2148 );
2149
2150 use reddb_rql::ast::{QueryExpr, TableQuery};
2156 let expr = QueryExpr::Table(TableQuery::new("t"));
2157 let err = frame
2158 .check_query_privilege(&rt, &expr)
2159 .expect_err("denied via frame's coarse privilege gate");
2160 let msg = format!("{err}");
2161 assert!(
2162 msg.contains("permission denied") && msg.contains("Write"),
2163 "expected frame-level Write deny, got: {msg}"
2164 );
2165
2166 reset_thread_locals();
2167 }
2168
2169 #[test]
2182 fn large_select_through_frame_arena_returns_all_rows_in_order() {
2183 reset_thread_locals();
2184 let rt = fresh_runtime();
2185 rt.execute_query("CREATE TABLE big (id INT)")
2186 .expect("create table");
2187
2188 const N: usize = 2_500;
2191 for start in (0..N).step_by(250) {
2192 let end = (start + 250).min(N);
2193 let values = (start..end)
2194 .map(|i| format!("({i})"))
2195 .collect::<Vec<_>>()
2196 .join(", ");
2197 rt.execute_query(&format!("INSERT INTO big (id) VALUES {values}"))
2198 .unwrap_or_else(|err| panic!("insert rows {start}..{end}: {err:?}"));
2199 }
2200
2201 let result = rt
2202 .execute_query("SELECT id FROM big ORDER BY id")
2203 .expect("large SELECT executes through the frame arena path");
2204 assert_eq!(result.statement_type, "select");
2205 assert_eq!(
2206 result.result.records.len(),
2207 N,
2208 "every inserted row streams back through the arena-backed channel"
2209 );
2210 for (i, record) in result.result.records.iter().enumerate() {
2211 assert_eq!(
2212 record.get("id"),
2213 Some(&reddb_types::Value::Integer(i as i64)),
2214 "row {i} is byte-identical to the per-request-allocation baseline"
2215 );
2216 }
2217
2218 reset_thread_locals();
2219 }
2220
2221 #[test]
2235 fn parameterized_transport_adapters_delegate_binding_to_runtime() {
2236 let manifest_dir = std::path::Path::new(env!("CARGO_MANIFEST_DIR"));
2237 let adapters = [
2238 ("src/server/handlers_query.rs", "QueryRequestExecutor"),
2239 ("src/rpc_stdio.rs", "QueryRequestExecutor"),
2240 ("src/wire/redwire/session.rs", "QueryRequestExecutor"),
2241 ("src/wire/postgres/server.rs", "QueryRequestExecutor"),
2242 ("src/grpc.rs", "QueryRequestExecutor"),
2243 ];
2244
2245 for (relative, delegation) in adapters {
2246 let path = manifest_dir.join(relative);
2247 let text = std::fs::read_to_string(&path)
2248 .unwrap_or_else(|err| panic!("read {}: {err}", path.display()));
2249 assert!(
2250 text.contains(delegation),
2251 "{relative} should delegate parameterized query execution through {delegation}"
2252 );
2253 assert!(
2254 !text.contains("user_params::bind"),
2255 "{relative} must not bind SQL params in the transport adapter"
2256 );
2257 }
2258
2259 let seam = std::fs::read_to_string(manifest_dir.join("src/runtime/query_request.rs"))
2262 .expect("read src/runtime/query_request.rs");
2263 assert!(
2264 seam.contains("execute_query_with_params"),
2265 "the Request module must execute text SQL through the runtime entrypoint"
2266 );
2267 }
2268
2269 #[test]
2279 fn control_statement_skips_intent_locks_via_frame() {
2280 reset_thread_locals();
2281 let rt = fresh_runtime();
2282
2283 let frame = StatementExecutionFrame::build(&rt, StatementIdentity::Text("BEGIN"))
2284 .expect("frame builds for BEGIN");
2285 let f: &dyn ReadFrame = &frame;
2286 assert_eq!(f.lock_intent(), LockIntent::None);
2287
2288 use reddb_rql::ast::{QueryExpr, TableQuery};
2293 let expr = QueryExpr::Table(TableQuery::new("t"));
2294 let guard = frame
2295 .acquire_intent_locks(&rt, &expr)
2296 .expect("no lock is attempted");
2297 assert!(
2298 guard.is_none(),
2299 "BEGIN frame's lock_intent=None must short-circuit lock acquisition"
2300 );
2301 }
2302
2303 #[test]
2310 fn nested_frame_inherits_outer_statement_snapshot() {
2311 reset_thread_locals();
2312 let rt = fresh_runtime();
2313 rt.execute_query("CREATE TABLE nested_snap (id INT)")
2314 .expect("create table");
2315
2316 let outer = StatementExecutionFrame::build(
2317 &rt,
2318 StatementIdentity::Text("SELECT * FROM nested_snap"),
2319 )
2320 .expect("outer frame builds");
2321 let _outer_guards = outer.install(&rt);
2322
2323 rt.execute_query("INSERT INTO nested_snap (id) VALUES (1)")
2326 .expect("concurrent insert");
2327
2328 let expr = QueryExpr::Table(reddb_rql::ast::TableQuery::new("nested_snap"));
2329 let nested = StatementExecutionFrame::build(&rt, StatementIdentity::Expr(&expr))
2330 .expect("nested frame builds");
2331
2332 assert_eq!(
2333 ReadFrame::snapshot(&nested).xid,
2334 ReadFrame::snapshot(&outer).xid,
2335 "nested execution must inherit the outer statement's snapshot"
2336 );
2337 }
2338
2339 #[test]
2352 fn nested_text_statement_sees_commits_made_since_the_outer_frame() {
2353 reset_thread_locals();
2354 let rt = fresh_runtime();
2355 rt.execute_query("CREATE TABLE nested_text_snap (id INT)")
2356 .expect("create table");
2357
2358 let outer = StatementExecutionFrame::build(
2359 &rt,
2360 StatementIdentity::Text("SELECT * FROM nested_text_snap"),
2361 )
2362 .expect("outer frame builds");
2363 let _outer_guards = outer.install(&rt);
2364
2365 rt.execute_query("INSERT INTO nested_text_snap (id) VALUES (1)")
2366 .expect("concurrent insert");
2367
2368 let nested = StatementExecutionFrame::build(
2369 &rt,
2370 StatementIdentity::Text("SELECT * FROM nested_text_snap"),
2371 )
2372 .expect("nested frame builds");
2373
2374 assert_ne!(
2375 ReadFrame::snapshot(&nested).xid,
2376 ReadFrame::snapshot(&outer).xid,
2377 "a nested text statement must mint a fresh snapshot, not inherit \
2378 the enclosing statement's frozen view"
2379 );
2380 }
2381
2382 #[test]
2387 fn expr_frame_rejects_as_of_instead_of_reading_live() {
2388 reset_thread_locals();
2389 let rt = fresh_runtime();
2390
2391 let mut table = reddb_rql::ast::TableQuery::new("red.vcs.commits");
2392 table.as_of = Some(reddb_rql::ast::AsOfClause::Snapshot(1));
2393 let expr = QueryExpr::Table(table);
2394
2395 let err = match StatementExecutionFrame::build(&rt, StatementIdentity::Expr(&expr)) {
2396 Err(err) => err,
2397 Ok(_) => panic!("AS OF without statement text must be refused, not read live"),
2398 };
2399 let msg = format!("{err}");
2400 assert!(
2401 msg.contains("AS OF cannot be resolved from a pre-parsed expression"),
2402 "expected a loud AS OF refusal, got: {msg}"
2403 );
2404 }
2405
2406 #[test]
2410 fn expr_frame_opts_out_of_the_result_cache() {
2411 reset_thread_locals();
2412 let rt = fresh_runtime();
2413
2414 let expr = QueryExpr::Table(reddb_rql::ast::TableQuery::new("t"));
2415 let frame = StatementExecutionFrame::build(&rt, StatementIdentity::Expr(&expr))
2416 .expect("expr frame builds");
2417
2418 assert!(frame.cache_key().is_empty(), "expr frame has no cache key");
2419 assert!(
2420 !frame.can_read_result_cache(),
2421 "expr frame must never serve from the result cache"
2422 );
2423 }
2424}