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 snapshot: self.snapshot().clone(),
114 manager: runtime.inner.transaction_state.snapshot_manager(),
115 own_xids: HashSet::new(),
116 requires_index_fallback: true,
117 serializable_reader: None,
118 }
119 }
120
121 fn as_of_floor(&self) -> Option<Xid>;
126
127 fn cache_key(&self) -> &str;
130
131 fn should_cache_result(&self) -> bool;
145
146 fn required_privilege(&self) -> Privilege;
155
156 fn lock_intent(&self) -> LockIntent;
165
166 fn visible_collections(&self) -> Option<&std::collections::HashSet<String>>;
179}
180
181pub(crate) fn statement_is_write(query: &str) -> bool {
190 statement_kind(query) == "write"
191}
192
193fn statement_kind(query: &str) -> &'static str {
194 let trimmed = query.trim_start();
195 let trimmed = if let Some(rest) = trimmed.strip_prefix("--") {
198 rest.split_once('\n')
199 .map(|(_, r)| r)
200 .unwrap_or("")
201 .trim_start()
202 } else {
203 trimmed
204 };
205 let mut tokens = trimmed.split(|c: char| c.is_whitespace() || c == '(' || c == ';');
206 let first = tokens.next().unwrap_or("");
207 let second = tokens.next().unwrap_or("");
208 if first.eq_ignore_ascii_case("KV") {
209 if second.eq_ignore_ascii_case("GET")
210 || second.eq_ignore_ascii_case("LIST")
211 || second.eq_ignore_ascii_case("WATCH")
212 {
213 return "read";
214 }
215 return "write";
216 }
217 if first.eq_ignore_ascii_case("VAULT") {
218 if second.eq_ignore_ascii_case("GET")
219 || second.eq_ignore_ascii_case("REVEAL")
220 || second.eq_ignore_ascii_case("UNSEAL")
221 || second.eq_ignore_ascii_case("LIST")
222 || second.eq_ignore_ascii_case("WATCH")
223 || second.eq_ignore_ascii_case("HISTORY")
224 {
225 return "read";
226 }
227 return "write";
228 }
229 let mut buf = [0u8; 16];
231 let bytes = first.as_bytes();
232 let n = bytes.len().min(buf.len());
233 for i in 0..n {
234 buf[i] = bytes[i].to_ascii_uppercase();
235 }
236 match &buf[..n] {
237 b"SELECT" | b"WITH" | b"SHOW" | b"EXPLAIN" | b"DESCRIBE" | b"DESC" | b"RANK"
238 | b"APPROX" | b"APPROXIMATE" | b"ZRANK" | b"ZRANGE" | b"LIST" | b"WATCH" | b"GET"
239 | b"HISTORY" | b"REVEAL" | b"UNSEAL" => "read",
240 b"INSERT" | b"UPDATE" | b"DELETE" | b"UPSERT" | b"MERGE" | b"COPY" | b"TRUNCATE" => "write",
241 b"CREATE" | b"ALTER" | b"DROP" | b"PROMOTE" | b"REINDEX" | b"VACUUM" | b"ANALYZE" => "ddl",
242 b"GRANT" | b"REVOKE" => "admin",
243 b"BEGIN" | b"START" | b"COMMIT" | b"ROLLBACK" | b"SAVEPOINT" | b"RELEASE" | b"END"
244 | b"SET" | b"RESET" | b"PREPARE" | b"EXECUTE" | b"DEALLOCATE" | b"USE" => "control",
245 b"PUT" | b"INCR" | b"DECR" | b"ADD" | b"ROTATE" | b"PURGE" | b"INVALIDATE" => "write",
246 _ => "unknown",
247 }
248}
249
250fn reject_unresolvable_as_of(expr: &QueryExpr) -> RedDBResult<()> {
262 let QueryExpr::Table(table) = expr else {
263 return Ok(());
264 };
265 if table.as_of.is_none() {
266 return Ok(());
267 }
268 Err(RedDBError::InvalidOperation(
269 "AS OF cannot be resolved from a pre-parsed expression — \
270 execute the statement text (execute_query / execute_prepared_query / \
271 execute_query_with_params) so the historical snapshot is resolved"
272 .to_string(),
273 ))
274}
275
276fn classify_privilege(query: &str) -> Privilege {
277 match statement_kind(query) {
278 "read" => Privilege::Read,
279 "write" => Privilege::Write,
280 "ddl" => Privilege::Write,
286 "admin" => Privilege::Admin,
287 _ => Privilege::None,
288 }
289}
290
291fn classify_lock_intent(query: &str) -> LockIntent {
292 match statement_kind(query) {
293 "read" => LockIntent::Shared,
294 "write" | "ddl" => LockIntent::Exclusive,
295 _ => LockIntent::None,
296 }
297}
298
299pub(super) struct StatementExecutionFrame {
300 tx_local_tenant: Option<Option<String>>,
301 snapshot: Snapshot,
302 snapshot_retention: RefCell<Option<SnapshotRetentionGuard>>,
303 own_xids: HashSet<Xid>,
304 serializable_reader: Option<Xid>,
305 cache_key: String,
306 is_volatile_query: bool,
307 cache_safe: bool,
308 effective_scope: Option<String>,
313 identity: Option<(String, Role)>,
316 as_of_floor: Option<Xid>,
319 requires_index_fallback: bool,
322 required_privilege: Privilege,
326 lock_intent: LockIntent,
329 visible_collections: Option<HashSet<String>>,
336 row_arena: Rc<RefCell<super::query_exec::RowBufferArena>>,
344}
345
346pub(super) struct StatementFrameGuards {
347 _tx_local_guard: TxLocalTenantGuard,
348 _config_snapshot_guard: ConfigSnapshotGuard,
349 _secret_store_guard: SecretStoreGuard,
350 _kv_store_guard: KvStoreGuard,
351 _snapshot_guard: CurrentSnapshotGuard,
352}
353
354pub(super) struct PreparedStatement {
355 pub(super) expr: QueryExpr,
356 pub(super) mode: QueryMode,
357}
358
359#[derive(Clone, Copy)]
368pub(super) enum StatementIdentity<'a> {
369 Text(&'a str),
371 Expr(&'a QueryExpr),
383}
384
385impl<'a> StatementIdentity<'a> {
386 fn text(&self) -> &'a str {
390 match self {
391 Self::Text(query) => query,
392 Self::Expr(_) => "",
393 }
394 }
395}
396
397impl StatementExecutionFrame {
398 pub(super) fn build(
399 runtime: &RedDBRuntime,
400 identity_source: StatementIdentity<'_>,
401 ) -> RedDBResult<Self> {
402 let query = identity_source.text();
403 if let StatementIdentity::Expr(expr) = identity_source {
404 reject_unresolvable_as_of(expr)?;
405 }
406 let conn_id = current_connection_id();
407 let tx_local_tenant = runtime.inner.transaction_state.local_tenant(conn_id);
408 let tx_context = runtime.inner.transaction_state.context(conn_id);
409 let own_xids = own_transaction_xids(tx_context.as_ref());
410 let serializable_reader = tx_context
411 .as_ref()
412 .filter(|ctx| {
413 ctx.isolation == crate::storage::transaction::IsolationLevel::Serializable
414 })
415 .map(|ctx| ctx.xid);
416 let (snapshot, as_of_floor, snapshot_retention) =
417 runtime.statement_snapshot(query, identity_source)?;
418 let requires_index_fallback = as_of_floor.is_some() || tx_context.is_some();
419 let (cache_key, is_volatile_query) = match identity_source {
420 StatementIdentity::Text(_) => (
421 result_cache_key(query),
422 query_has_volatile_builtin(query) || query_is_ask_statement(query),
423 ),
424 StatementIdentity::Expr(_) => (String::new(), true),
429 };
430 let cache_safe = runtime.result_cache_safe(conn_id);
431 let effective_scope = current_tenant();
436 let identity = current_auth_identity();
437
438 let required_privilege = classify_privilege(query);
446 let lock_intent = classify_lock_intent(query);
447
448 let visible_collections = match (runtime.inner.auth_store.read().clone(), identity.as_ref())
454 {
455 (Some(store), Some((principal, role))) => {
456 let collections = runtime.inner.db.store().list_collections();
457 Some(store.visible_collections_for_scope(
458 effective_scope.as_deref(),
459 *role,
460 principal,
461 &collections,
462 ))
463 }
464 _ => None,
465 };
466
467 Ok(Self {
468 tx_local_tenant,
469 snapshot,
470 snapshot_retention: RefCell::new(Some(snapshot_retention)),
471 own_xids,
472 serializable_reader,
473 cache_key,
474 is_volatile_query,
475 cache_safe,
476 effective_scope,
477 identity,
478 as_of_floor,
479 requires_index_fallback,
480 required_privilege,
481 lock_intent,
482 visible_collections,
483 row_arena: Rc::new(RefCell::new(super::query_exec::RowBufferArena::new())),
484 })
485 }
486
487 pub(super) fn row_arena(&self) -> Rc<RefCell<super::query_exec::RowBufferArena>> {
492 Rc::clone(&self.row_arena)
493 }
494
495 pub(super) fn install(&self, runtime: &RedDBRuntime) -> StatementFrameGuards {
496 StatementFrameGuards {
497 _tx_local_guard: TxLocalTenantGuard::install(self.tx_local_tenant.clone()),
498 _config_snapshot_guard: ConfigSnapshotGuard::install(
499 Arc::clone(&runtime.inner.db),
500 runtime.inner.auth_store.read().clone(),
501 ),
502 _secret_store_guard: SecretStoreGuard::install(runtime),
503 _kv_store_guard: KvStoreGuard::install(runtime.inner.auth_store.read().clone()),
504 _snapshot_guard: CurrentSnapshotGuard::install(SnapshotContext {
505 snapshot: self.snapshot.clone(),
506 manager: runtime.inner.transaction_state.snapshot_manager(),
507 own_xids: self.own_xids.clone(),
508 requires_index_fallback: self.requires_index_fallback,
509 serializable_reader: self.serializable_reader,
510 }),
511 }
512 }
513
514 pub(super) fn cache_key(&self) -> &str {
515 &self.cache_key
516 }
517
518 pub(super) fn can_read_result_cache(&self) -> bool {
519 <Self as ReadFrame>::should_cache_result(self)
522 }
523
524 pub(super) fn should_write_result_cache(&self, result: &RuntimeQueryResult) -> bool {
525 <Self as ReadFrame>::should_cache_result(self)
530 && result.statement_type == "select"
531 && result.engine != "vault"
532 && result.engine != "runtime-rank"
533 && result.statement != "queue_group_read"
538 && result.engine != "runtime-scrub"
543 && result.result.pre_serialized_json.is_none()
544 && (is_graph_tvf_engine(result.engine) || result.result.records.len() <= 5)
548 }
549
550 pub(super) fn read_result_cache(&self, runtime: &RedDBRuntime) -> Option<RuntimeQueryResult> {
551 if runtime.resource_limits().read_execution_limits_enabled() {
552 return None;
553 }
554 if self.can_read_result_cache() {
555 runtime
556 .get_result_cache_entry(self.cache_key())
557 .map(|mut result| {
558 if let Some(vector) = result.result.stats.vector.as_mut() {
559 vector.cache_hit = true;
560 }
561 result
562 })
563 } else {
564 None
565 }
566 }
567
568 pub(super) fn write_result_cache(
569 &self,
570 runtime: &RedDBRuntime,
571 result: &RuntimeQueryResult,
572 scopes: HashSet<String>,
573 ) {
574 if self.should_write_result_cache(result) {
575 runtime.put_result_cache_entry(
576 self.cache_key(),
577 RuntimeResultCacheEntry {
578 result: result.clone(),
579 cached_at: std::time::Instant::now(),
580 scopes,
581 },
582 );
583 }
584 }
585
586 pub(super) fn prepare_cte(&self, query: &str) -> RedDBResult<Option<QueryExpr>> {
587 if !has_with_prefix(query) {
596 return Ok(None);
597 }
598 let parsed =
599 reddb_rql::parser::parse(query).map_err(|err| RedDBError::Query(err.to_string()))?;
600 if parsed.with_clause.is_some() {
601 let rewritten = crate::storage::query::executors::inline_ctes(parsed)
602 .map_err(|err| RedDBError::Query(err.to_string()))?;
603 return Ok(Some(rewritten));
604 }
605 Ok(None)
609 }
610
611 pub(super) fn prepare_statement(
612 &self,
613 runtime: &RedDBRuntime,
614 query: &str,
615 ) -> RedDBResult<PreparedStatement> {
616 let mode = detect_mode(query);
617 if matches!(mode, QueryMode::Unknown) {
618 return Err(RedDBError::Query("unable to detect query mode".to_string()));
619 }
620
621 let first_word = query
632 .trim()
633 .split_ascii_whitespace()
634 .next()
635 .unwrap_or("")
636 .to_ascii_uppercase();
637 let is_insert = first_word == "INSERT";
638 let bypass_plan_cache = is_insert || self.is_volatile_query;
644
645 let (cache_key, prescan_binds) = if bypass_plan_cache {
651 (String::new(), Vec::new())
652 } else {
653 crate::storage::query::planner::cache_key::normalize_and_extract(query)
654 };
655
656 let expr = if bypass_plan_cache {
657 parse_multi(query).map_err(|err| RedDBError::Query(err.to_string()))?
659 } else {
660 let hit = {
667 let plan_cache = runtime.inner.query_cache.read();
668 plan_cache.peek(&cache_key).map(|cached| {
669 let parameter_count = cached.parameter_count;
670 let optimized = cached.plan.optimized.clone();
671 let exact_query = cached.exact_query.clone();
672 (parameter_count, optimized, exact_query)
673 })
674 };
675
676 if let Some((parameter_count, optimized, exact_query)) = hit {
677 if parameter_count > 0 {
678 let shape_binds = prescan_binds.clone();
680 if let Some(bound) =
681 crate::storage::query::planner::shape::bind_parameterized_query(
682 &optimized,
683 &shape_binds,
684 parameter_count,
685 )
686 {
687 bound
688 } else {
689 parse_multi(query).map_err(|err| RedDBError::Query(err.to_string()))?
692 }
693 } else {
694 if exact_query.as_deref() == Some(query) {
700 optimized
701 } else {
702 parse_multi(query).map_err(|err| RedDBError::Query(err.to_string()))?
703 }
704 }
705 } else {
706 let parsed =
708 parse_multi(query).map_err(|err| RedDBError::Query(err.to_string()))?;
709 let (cached_expr, parameter_count) = if let Some(prepared) =
710 crate::storage::query::planner::shape::parameterize_query_expr(&parsed)
711 {
712 (prepared.shape, prepared.parameter_count)
713 } else {
714 (parsed.clone(), 0)
715 };
716 {
717 let mut pc = runtime.inner.query_cache.write();
718 let plan = crate::storage::query::planner::QueryPlan::new(
719 parsed.clone(),
720 cached_expr,
721 Default::default(),
722 );
723 pc.insert(
724 cache_key.clone(),
725 crate::storage::query::planner::CachedPlan::new(plan)
726 .with_shape_key(cache_key.clone())
727 .with_exact_query(query.to_string())
728 .with_parameter_count(parameter_count),
729 );
730 }
731 parsed
732 }
733 };
734
735 let expr = runtime.rewrite_view_refs(expr);
740
741 Ok(PreparedStatement { expr, mode })
742 }
743
744 pub(super) fn check_query_privilege(
745 &self,
746 runtime: &RedDBRuntime,
747 expr: &QueryExpr,
748 ) -> RedDBResult<()> {
749 if let Some((username, role)) = <Self as ReadFrame>::identity(self) {
759 let needed = <Self as ReadFrame>::required_privilege(self);
760 if !needed.is_satisfied_by(role) {
761 if matches!(needed, Privilege::Admin) && runtime.inner.auth_store.read().is_none() {
770 crate::telemetry::operator_event::OperatorEvent::AuthBypass {
771 principal: username.to_string(),
772 resource: format!("statement requiring {needed:?}"),
773 detail: format!(
774 "auth_store not wired; coarse gate is sole defence (role={role:?})"
775 ),
776 }
777 .emit_global();
778 }
779 return Err(RedDBError::Query(format!(
780 "permission denied: principal=`{username}` role=`{role:?}` lacks {needed:?} privilege"
781 )));
782 }
783 }
784 runtime
785 .check_query_privilege(expr)
786 .map_err(|err| RedDBError::Query(format!("permission denied: {err}")))
787 }
788
789 pub(super) fn prepare_dispatch(
790 &self,
791 runtime: &RedDBRuntime,
792 expr: &QueryExpr,
793 ) -> RedDBResult<Option<crate::runtime::locking::LockerGuard>> {
794 runtime.validate_model_operations_before_auth(expr)?;
795 self.check_query_privilege(runtime, expr)?;
796 self.release_snapshot_for_maintenance(expr);
797 self.acquire_intent_locks(runtime, expr)
798 }
799
800 pub(super) fn release_snapshot_for_maintenance(&self, expr: &QueryExpr) {
801 if matches!(
802 expr,
803 QueryExpr::MaintenanceCommand(reddb_rql::ast::MaintenanceCommand::Vacuum { .. })
804 ) {
805 self.snapshot_retention.borrow_mut().take();
810 }
811 }
812
813 pub(super) fn acquire_intent_locks(
814 &self,
815 runtime: &RedDBRuntime,
816 expr: &QueryExpr,
817 ) -> RedDBResult<Option<crate::runtime::locking::LockerGuard>> {
818 use crate::runtime::lock_manager::LockMode;
819 use crate::runtime::locking::{LockerGuard, Resource};
820
821 if !runtime.config_bool("concurrency.locking.enabled", true) {
822 return Ok(None);
823 }
824 if matches!(<Self as ReadFrame>::lock_intent(self), LockIntent::None) {
829 return Ok(None);
830 }
831 let Some((global_mode, coll_mode)) = intent_lock_modes_for(expr) else {
832 return Ok(None);
833 };
834 let acquire = |guard: &mut LockerGuard, resource: Resource, mode: LockMode| {
837 guard.acquire(resource.clone(), mode).map_err(|err| {
838 RedDBError::Query(format!(
839 "could not acquire {mode:?} lock on {resource:?}: {err}; retry the statement"
840 ))
841 })
842 };
843 let mut guard = LockerGuard::new(runtime.inner.lock_manager.clone());
844 acquire(&mut guard, Resource::Global, global_mode)?;
845 if coll_mode == LockMode::Exclusive {
850 acquire(&mut guard, Resource::Catalog, LockMode::Exclusive)?;
851 }
852 for collection in collections_referenced(expr) {
853 acquire(&mut guard, Resource::Collection(collection), coll_mode)?;
854 }
855 Ok(Some(guard))
856 }
857}
858
859impl ReadFrame for StatementExecutionFrame {
860 fn snapshot_context(&self, runtime: &RedDBRuntime) -> SnapshotContext {
861 SnapshotContext {
862 snapshot: self.snapshot.clone(),
863 manager: runtime.inner.transaction_state.snapshot_manager(),
864 own_xids: self.own_xids.clone(),
865 requires_index_fallback: self.requires_index_fallback,
866 serializable_reader: self.serializable_reader,
867 }
868 }
869
870 fn effective_scope(&self) -> Option<&str> {
871 self.effective_scope.as_deref()
872 }
873
874 fn identity(&self) -> Option<(&str, Role)> {
875 self.identity.as_ref().map(|(u, r)| (u.as_str(), *r))
876 }
877
878 fn snapshot(&self) -> &Snapshot {
879 &self.snapshot
880 }
881
882 fn as_of_floor(&self) -> Option<Xid> {
883 self.as_of_floor
884 }
885
886 fn cache_key(&self) -> &str {
887 &self.cache_key
888 }
889
890 fn should_cache_result(&self) -> bool {
891 !self.is_volatile_query && self.cache_safe
892 }
893
894 fn required_privilege(&self) -> Privilege {
895 self.required_privilege
896 }
897
898 fn lock_intent(&self) -> LockIntent {
899 self.lock_intent
900 }
901
902 fn visible_collections(&self) -> Option<&HashSet<String>> {
903 self.visible_collections.as_ref()
904 }
905}
906
907pub struct EffectiveScope {
920 pub(crate) captured_snapshot: Option<SnapshotContext>,
921 pub(crate) tenant: Option<String>,
922 pub(crate) identity: Option<(String, Role)>,
923 pub(crate) snapshot: Snapshot,
924 pub(crate) visible_collections: Option<HashSet<String>>,
925}
926
927impl EffectiveScope {
928 pub fn has_capability(&self, _capability: &str) -> bool {
938 false
939 }
940}
941
942impl ReadFrame for EffectiveScope {
943 fn snapshot_context(&self, runtime: &RedDBRuntime) -> SnapshotContext {
944 self.captured_snapshot
945 .clone()
946 .unwrap_or_else(|| SnapshotContext {
947 snapshot: self.snapshot.clone(),
948 manager: runtime.inner.transaction_state.snapshot_manager(),
949 own_xids: HashSet::new(),
950 requires_index_fallback: true,
951 serializable_reader: None,
952 })
953 }
954
955 fn effective_scope(&self) -> Option<&str> {
956 self.tenant.as_deref()
957 }
958 fn identity(&self) -> Option<(&str, Role)> {
959 self.identity.as_ref().map(|(u, r)| (u.as_str(), *r))
960 }
961 fn snapshot(&self) -> &Snapshot {
962 &self.snapshot
963 }
964 fn as_of_floor(&self) -> Option<Xid> {
965 None
966 }
967 fn cache_key(&self) -> &str {
968 ""
969 }
970 fn should_cache_result(&self) -> bool {
971 false
972 }
973 fn required_privilege(&self) -> Privilege {
974 Privilege::Read
975 }
976 fn lock_intent(&self) -> LockIntent {
977 LockIntent::Shared
978 }
979 fn visible_collections(&self) -> Option<&HashSet<String>> {
980 self.visible_collections.as_ref()
981 }
982}
983
984fn own_transaction_xids(
985 ctx: Option<&crate::storage::transaction::snapshot::TxnContext>,
986) -> HashSet<Xid> {
987 let mut set = HashSet::new();
988 if let Some(ctx) = ctx {
989 set.insert(ctx.xid);
990 for (_, sub) in &ctx.savepoints {
991 set.insert(*sub);
992 }
993 for sub in &ctx.released_sub_xids {
994 set.insert(*sub);
995 }
996 }
997 set
998}
999
1000impl RedDBRuntime {
1001 pub(crate) fn ai_scope(&self) -> EffectiveScope {
1007 let tenant = super::impl_core::current_tenant();
1008 let identity = super::impl_core::current_auth_identity();
1009 let captured_snapshot = super::execution_context::capture_current_snapshot();
1010 let snapshot = captured_snapshot.as_ref().map_or_else(
1011 || self.current_snapshot(),
1012 |context| context.snapshot.clone(),
1013 );
1014 let visible_collections = match (self.inner.auth_store.read().clone(), identity.as_ref()) {
1015 (Some(store), Some((principal, role))) => {
1016 let collections = self.inner.db.store().list_collections();
1017 Some(store.visible_collections_for_scope(
1018 tenant.as_deref(),
1019 *role,
1020 principal,
1021 &collections,
1022 ))
1023 }
1024 _ => None,
1025 };
1026 EffectiveScope {
1027 captured_snapshot,
1028 tenant,
1029 identity,
1030 snapshot,
1031 visible_collections,
1032 }
1033 }
1034}
1035
1036#[cfg(test)]
1040pub(crate) mod test_support {
1041 use super::{LockIntent, Privilege, ReadFrame};
1042 use crate::auth::Role;
1043 use crate::storage::transaction::snapshot::{Snapshot, Xid};
1044 use std::collections::HashSet;
1045
1046 pub(crate) struct FakeReadFrame {
1050 pub tenant: Option<String>,
1051 pub identity: Option<(String, Role)>,
1052 pub snapshot: Snapshot,
1053 pub visible: Option<HashSet<String>>,
1054 }
1055
1056 impl FakeReadFrame {
1057 pub(crate) fn without_scope() -> Self {
1058 Self {
1059 tenant: None,
1060 identity: None,
1061 snapshot: Snapshot {
1062 xid: 0,
1063 in_progress: HashSet::new(),
1064 },
1065 visible: None,
1066 }
1067 }
1068
1069 pub(crate) fn with_visible(visible: HashSet<String>) -> Self {
1070 Self {
1071 tenant: Some("acme".to_string()),
1072 identity: Some(("alice".to_string(), Role::Read)),
1073 snapshot: Snapshot {
1074 xid: 0,
1075 in_progress: HashSet::new(),
1076 },
1077 visible: Some(visible),
1078 }
1079 }
1080 }
1081
1082 impl ReadFrame for FakeReadFrame {
1083 fn effective_scope(&self) -> Option<&str> {
1084 self.tenant.as_deref()
1085 }
1086 fn identity(&self) -> Option<(&str, Role)> {
1087 self.identity.as_ref().map(|(u, r)| (u.as_str(), *r))
1088 }
1089 fn snapshot(&self) -> &Snapshot {
1090 &self.snapshot
1091 }
1092 fn as_of_floor(&self) -> Option<Xid> {
1093 None
1094 }
1095 fn cache_key(&self) -> &str {
1096 ""
1097 }
1098 fn should_cache_result(&self) -> bool {
1099 false
1100 }
1101 fn required_privilege(&self) -> Privilege {
1102 Privilege::Read
1103 }
1104 fn lock_intent(&self) -> LockIntent {
1105 LockIntent::Shared
1106 }
1107 fn visible_collections(&self) -> Option<&HashSet<String>> {
1108 self.visible.as_ref()
1109 }
1110 }
1111}
1112
1113impl RedDBRuntime {
1114 fn statement_snapshot(
1144 &self,
1145 query: &str,
1146 identity: StatementIdentity<'_>,
1147 ) -> RedDBResult<(Snapshot, Option<Xid>, SnapshotRetentionGuard)> {
1148 match peek_top_level_as_of_with_table(query) {
1149 Some((spec, Some(table))) => {
1150 if !table.starts_with("red_")
1151 && !table.starts_with("red.")
1152 && !self.vcs_is_versioned(&table)?
1153 {
1154 return Err(RedDBError::InvalidConfig(format!(
1155 "AS OF requires a versioned collection — \
1156 `{table}` has not opted in. \
1157 Call vcs.set_versioned(\"{table}\", true) first."
1158 )));
1159 }
1160 let xid = self.vcs_resolve_as_of(spec)?;
1161 let (snapshot, retention) = self.snapshot_manager().retain_snapshot(Snapshot {
1162 xid,
1163 in_progress: HashSet::new(),
1164 });
1165 Ok((snapshot, Some(xid), retention))
1166 }
1167 Some((spec, None)) => {
1168 let xid = self.vcs_resolve_as_of(spec)?;
1169 let (snapshot, retention) = self.snapshot_manager().retain_snapshot(Snapshot {
1170 xid,
1171 in_progress: HashSet::new(),
1172 });
1173 Ok((snapshot, Some(xid), retention))
1174 }
1175 None => match (identity, capture_current_snapshot()) {
1176 (StatementIdentity::Expr(_), Some(outer)) => {
1177 let (snapshot, retention) =
1178 self.snapshot_manager().retain_snapshot(outer.snapshot);
1179 Ok((snapshot, None, retention))
1180 }
1181 _ => {
1182 let (snapshot, retention) = self
1183 .inner
1184 .transaction_state
1185 .retained_current_snapshot(current_connection_id());
1186 Ok((snapshot, None, retention))
1187 }
1188 },
1189 }
1190 }
1191
1192 fn result_cache_safe(&self, conn_id: u64) -> bool {
1193 let has_active_xids = self
1194 .inner
1195 .transaction_state
1196 .snapshot_manager()
1197 .oldest_active_xid()
1198 .is_some();
1199 let in_own_tx = self.inner.transaction_state.in_transaction(conn_id);
1200 !has_active_xids && !in_own_tx
1201 }
1202}
1203
1204fn is_graph_tvf_engine(engine: &str) -> bool {
1209 matches!(engine, "runtime-graph-tvf" | "runtime-graph-tvf-inline")
1210}
1211
1212fn result_cache_key(query: &str) -> String {
1213 let tenant = current_tenant().unwrap_or_default();
1214 let auth = current_auth_identity()
1215 .map(|(user, role)| format!("{}|{:?}", user, role))
1216 .unwrap_or_default();
1217 if tenant.is_empty() && auth.is_empty() {
1218 query.to_string()
1219 } else {
1220 format!("{query}\u{001e}{tenant}\u{001e}{auth}")
1221 }
1222}
1223
1224#[cfg(test)]
1225mod tests {
1226 use super::*;
1227 use crate::api::RedDBOptions;
1228 use crate::runtime::impl_core::{
1229 clear_current_auth_identity, clear_current_tenant, set_current_auth_identity,
1230 set_current_tenant,
1231 };
1232 use crate::runtime::RedDBRuntime;
1233
1234 fn fresh_runtime() -> RedDBRuntime {
1235 RedDBRuntime::with_options(RedDBOptions::in_memory()).expect("in-memory runtime")
1236 }
1237
1238 enum HeldFrame {
1239 Ordinary,
1240 Nested,
1241 Historical,
1242 }
1243
1244 fn verify_frame_survives_vacuum(delete: bool, kind: HeldFrame) {
1245 use crate::runtime::mvcc::{clear_current_connection_id, set_current_connection_id};
1246
1247 reset_thread_locals();
1248 clear_current_connection_id();
1249 let runtime = fresh_runtime();
1250 let execute = |connection, sql: &str| {
1251 set_current_connection_id(connection);
1252 let result = runtime.execute_query(sql);
1253 clear_current_connection_id();
1254 result.expect("valid frame retention fixture statement")
1255 };
1256 execute(1, "CREATE TABLE frame_rows (id TEXT, n INTEGER)");
1257 execute(1, "INSERT INTO frame_rows (id, n) VALUES ('row', 1)");
1258 let historical_xid = runtime.current_snapshot().xid;
1259 if matches!(kind, HeldFrame::Historical) {
1260 runtime
1261 .vcs_set_versioned("frame_rows", true)
1262 .expect("enable historical reads");
1263 }
1264 execute(2, "BEGIN");
1265 let query = if matches!(kind, HeldFrame::Historical) {
1266 format!("SELECT id, n FROM frame_rows AS OF SNAPSHOT {historical_xid}")
1267 } else {
1268 "SELECT id, n FROM frame_rows".to_string()
1269 };
1270 let frame = StatementExecutionFrame::build(&runtime, StatementIdentity::Text(&query))
1271 .expect("frame captures its snapshot");
1272 let frame = if matches!(kind, HeldFrame::Nested) {
1273 let guards = frame.install(&runtime);
1274 let expr = QueryExpr::Table(reddb_rql::ast::TableQuery::new("frame_rows"));
1275 let nested = StatementExecutionFrame::build(&runtime, StatementIdentity::Expr(&expr))
1276 .expect("nested frame inherits the protected snapshot");
1277 drop(guards);
1278 drop(frame);
1279 nested
1280 } else {
1281 frame
1282 };
1283 execute(
1286 2,
1287 if delete {
1288 "DELETE FROM frame_rows WHERE id = 'row'"
1289 } else {
1290 "UPDATE frame_rows SET n = 2 WHERE id = 'row'"
1291 },
1292 );
1293 execute(2, "COMMIT");
1294 execute(1, "VACUUM frame_rows");
1295 {
1296 let _guards = frame.install(&runtime);
1297 let records = super::super::record_search::scan_runtime_table_source_records_limited(
1298 &runtime.db(),
1299 "frame_rows",
1300 None,
1301 )
1302 .expect("captured frame scan succeeds after vacuum");
1303 assert_eq!(
1304 records.len(),
1305 1,
1306 "vacuum reclaimed an active frame's visible row"
1307 );
1308 assert_eq!(
1309 records[0]
1310 .get("n")
1311 .expect("selected value exists")
1312 .to_string(),
1313 "1"
1314 );
1315 }
1316 drop(frame);
1317 execute(1, "VACUUM frame_rows");
1318 let physical = runtime
1319 .db()
1320 .store()
1321 .get_collection("frame_rows")
1322 .expect("fixture collection exists")
1323 .query_all(|_| true)
1324 .len();
1325 assert_eq!(
1326 physical,
1327 usize::from(!delete),
1328 "completed frame leaked its retention pin"
1329 );
1330 clear_current_connection_id();
1331 }
1332
1333 #[test]
1334 fn autocommit_frame_retains_update_until_scan_completes() {
1335 verify_frame_survives_vacuum(false, HeldFrame::Ordinary);
1336 }
1337
1338 #[test]
1339 fn autocommit_frame_retains_delete_until_scan_completes() {
1340 verify_frame_survives_vacuum(true, HeldFrame::Ordinary);
1341 }
1342
1343 #[test]
1344 fn nested_frame_retains_history_after_parent_frame_drops() {
1345 verify_frame_survives_vacuum(false, HeldFrame::Nested);
1346 }
1347
1348 #[test]
1349 fn historical_frame_retains_history_until_scan_completes() {
1350 verify_frame_survives_vacuum(true, HeldFrame::Historical);
1351 }
1352
1353 fn reset_thread_locals() {
1356 clear_current_tenant();
1357 clear_current_auth_identity();
1358 }
1359
1360 #[test]
1361 fn autocommit_select_takes_live_snapshot() {
1362 reset_thread_locals();
1363 let rt = fresh_runtime();
1364 let frame = StatementExecutionFrame::build(&rt, StatementIdentity::Text("SELECT 1"))
1365 .expect("frame builds for SELECT 1");
1366
1367 let f: &dyn ReadFrame = &frame;
1370 assert!(f.as_of_floor().is_none(), "live read has no AS OF floor");
1371 assert_eq!(f.snapshot().xid, rt.snapshot_manager().peek_next_xid() - 1);
1372 assert!(!f.snapshot().sees(rt.snapshot_manager().peek_next_xid(), 0));
1373 }
1374
1375 #[test]
1376 fn autocommit_snapshot_does_not_lose_rows_after_index_publication() {
1377 reset_thread_locals();
1378 let rt = fresh_runtime();
1379 rt.execute_query("CREATE TABLE frame_index_race (id INT PRIMARY KEY, status TEXT)")
1380 .expect("create fixture");
1381 rt.execute_query("INSERT INTO frame_index_race (id, status) VALUES (1, 'old'), (2, 'old')")
1382 .expect("insert fixture");
1383 rt.execute_query("CREATE INDEX frame_status_hash ON frame_index_race (status) USING HASH")
1384 .expect("create index");
1385 let sql = "SELECT id FROM frame_index_race WHERE status = 'old'";
1386 let frame = StatementExecutionFrame::build(&rt, StatementIdentity::Text(sql))
1387 .expect("capture autocommit snapshot");
1388 rt.execute_query("UPDATE frame_index_race SET status = 'new' WHERE id = 1")
1389 .expect("publish changed index entry");
1390 let reddb_rql::ast::QueryExpr::Table(query) =
1391 reddb_rql::modes::parse_multi(sql).expect("parse")
1392 else {
1393 panic!("expected table query");
1394 };
1395 let _guards = frame.install(&rt);
1396 let records =
1397 super::super::query_exec::table::execute_runtime_canonical_table_query_indexed(
1398 &rt.db(),
1399 &query,
1400 Some(rt.index_store_ref()),
1401 )
1402 .expect("read captured snapshot");
1403 let mut ids = records
1404 .iter()
1405 .map(|record| record.get("id").expect("id").to_string())
1406 .collect::<Vec<_>>();
1407 ids.sort();
1408 assert_eq!(
1409 ids,
1410 ["1", "2"],
1411 "current index omitted a visible historical row"
1412 );
1413 }
1414
1415 #[test]
1416 fn captured_dml_snapshot_finds_rows_after_index_publication() {
1417 reset_thread_locals();
1418 let rt = fresh_runtime();
1419 rt.execute_query("CREATE TABLE frame_dml_race (id INT PRIMARY KEY, status TEXT)")
1420 .expect("create fixture");
1421 rt.execute_query("INSERT INTO frame_dml_race (id, status) VALUES (1, 'old'), (2, 'old')")
1422 .expect("insert fixture");
1423 rt.execute_query("CREATE INDEX frame_dml_status ON frame_dml_race (status) USING HASH")
1424 .expect("create index");
1425 let sql = "DELETE FROM frame_dml_race WHERE status = 'old'";
1426 let frame = StatementExecutionFrame::build(&rt, StatementIdentity::Text(sql))
1427 .expect("capture DML snapshot");
1428 rt.execute_query("UPDATE frame_dml_race SET status = 'new' WHERE id = 1")
1429 .expect("publish new indexed value");
1430 let reddb_rql::ast::QueryExpr::Table(query) =
1431 reddb_rql::modes::parse_multi("SELECT id FROM frame_dml_race WHERE status = 'old'")
1432 .expect("parse filter")
1433 else {
1434 panic!("expected table query")
1435 };
1436 let filter = reddb_rql::sql_lowering::effective_table_filter(&query).expect("filter");
1437 let _guards = frame.install(&rt);
1438 let ids = super::super::dml_target_scan::DmlTargetScan::new(
1439 &rt,
1440 "frame_dml_race",
1441 Some(&filter),
1442 None,
1443 )
1444 .find_target_ids()
1445 .expect("find DML targets");
1446 assert_eq!(
1447 ids.len(),
1448 2,
1449 "DML target scan must discover both snapshot-visible rows"
1450 );
1451 }
1452
1453 #[test]
1454 fn captured_snapshot_survives_indexed_update_and_delete_for_range_queries() {
1455 reset_thread_locals();
1456 let rt = fresh_runtime();
1457 rt.execute_query("CREATE TABLE frame_range_race (id INT PRIMARY KEY, score INT)")
1458 .expect("create fixture");
1459 rt.execute_query("INSERT INTO frame_range_race (id, score) VALUES (1, 10), (2, 20)")
1460 .expect("insert fixture");
1461 rt.execute_query("CREATE INDEX frame_score_range ON frame_range_race (score)")
1462 .expect("create index");
1463 let sql = "SELECT id FROM frame_range_race WHERE score >= 10 AND score <= 20 ORDER BY id";
1464 let frame = StatementExecutionFrame::build(&rt, StatementIdentity::Text(sql))
1465 .expect("capture read snapshot");
1466 rt.execute_query("UPDATE frame_range_race SET score = 99 WHERE id = 1")
1467 .expect("update row");
1468 rt.execute_query("DELETE FROM frame_range_race WHERE id = 2")
1469 .expect("delete row");
1470 let reddb_rql::ast::QueryExpr::Table(query) =
1471 reddb_rql::modes::parse_multi(sql).expect("parse")
1472 else {
1473 panic!("expected table query");
1474 };
1475 let _guards = frame.install(&rt);
1476 let records =
1477 super::super::query_exec::table::execute_runtime_canonical_table_query_indexed(
1478 &rt.db(),
1479 &query,
1480 Some(rt.index_store_ref()),
1481 )
1482 .expect("read captured snapshot");
1483 let ids = records
1484 .iter()
1485 .map(|record| record.get("id").expect("id").to_string())
1486 .collect::<Vec<_>>();
1487 assert_eq!(ids, ["1", "2"]);
1488 }
1489
1490 #[test]
1491 fn dml_target_scan_does_not_retry_inside_stored_function_budget() {
1492 use std::sync::atomic::{AtomicUsize, Ordering};
1493 use std::sync::Arc;
1494 reset_thread_locals();
1495 let rt = fresh_runtime();
1496 rt.execute_query("CREATE TABLE dml_budget_guard (id INT, status TEXT)")
1497 .expect("create fixture");
1498 rt.execute_query("INSERT INTO dml_budget_guard (id, status) VALUES (1, 'old')")
1499 .expect("insert fixture");
1500 rt.execute_query("CREATE INDEX dml_budget_status ON dml_budget_guard (status) USING HASH")
1501 .expect("create index");
1502 let frame = StatementExecutionFrame::build(
1503 &rt,
1504 StatementIdentity::Text("DELETE FROM dml_budget_guard WHERE status = 'old'"),
1505 )
1506 .expect("capture snapshot");
1507 let reddb_rql::ast::QueryExpr::Table(query) =
1508 reddb_rql::modes::parse_multi("SELECT id FROM dml_budget_guard WHERE status = 'old'")
1509 .expect("parse filter")
1510 else {
1511 panic!("expected table query")
1512 };
1513 let filter = reddb_rql::sql_lowering::effective_table_filter(&query).expect("filter");
1514 let calls = Arc::new(AtomicUsize::new(0));
1515 let observed = Arc::clone(&calls);
1516 *rt.index_store_ref().mutation_hook.lock() = Some(Arc::new(move |collection, phase| {
1517 if collection == "dml_budget_guard"
1518 && phase == super::super::index_store::MutationTestPhase::DmlIndexLookupStarted
1519 {
1520 observed.fetch_add(1, Ordering::SeqCst);
1521 }
1522 }));
1523 let _budget = super::super::function_budget::Scope::enter(1_000, 10_000)
1524 .expect("stored function budget");
1525 let _guards = frame.install(&rt);
1526 let ids = super::super::dml_target_scan::DmlTargetScan::new(
1527 &rt,
1528 "dml_budget_guard",
1529 Some(&filter),
1530 None,
1531 )
1532 .find_target_ids()
1533 .expect("find DML targets");
1534 assert_eq!(ids.len(), 1);
1535 assert_eq!(
1536 calls.load(Ordering::SeqCst),
1537 0,
1538 "bounded stored functions must not retry indexed discovery"
1539 );
1540 *rt.index_store_ref().mutation_hook.lock() = None;
1541 }
1542
1543 #[test]
1544 fn dml_index_lookup_retries_when_publication_races_target_selection() {
1545 use std::sync::{mpsc, Arc, Mutex};
1546 use std::time::Duration;
1547 reset_thread_locals();
1548 let rt = fresh_runtime();
1549 rt.execute_query("CREATE TABLE dml_lookup_race (id INT PRIMARY KEY, status TEXT)")
1550 .expect("create fixture");
1551 rt.execute_query("INSERT INTO dml_lookup_race (id, status) VALUES (1, 'old'), (2, 'old')")
1552 .expect("insert fixture");
1553 rt.execute_query("CREATE INDEX dml_lookup_status ON dml_lookup_race (status) USING HASH")
1554 .expect("create index");
1555 let frame = StatementExecutionFrame::build(
1556 &rt,
1557 StatementIdentity::Text("DELETE FROM dml_lookup_race WHERE status = 'old'"),
1558 )
1559 .expect("capture snapshot");
1560 let reddb_rql::ast::QueryExpr::Table(query) =
1561 reddb_rql::modes::parse_multi("SELECT id FROM dml_lookup_race WHERE status = 'old'")
1562 .expect("parse filter")
1563 else {
1564 panic!("expected table query")
1565 };
1566 let filter = reddb_rql::sql_lowering::effective_table_filter(&query).expect("filter");
1567 let (started_tx, started_rx) = mpsc::channel();
1568 let (published_tx, published_rx) = mpsc::channel();
1569 let published_rx = Mutex::new(published_rx);
1570 let first = std::sync::atomic::AtomicBool::new(true);
1571 *rt.index_store_ref().mutation_hook.lock() = Some(Arc::new(move |collection, phase| {
1572 if collection == "dml_lookup_race"
1573 && phase == super::super::index_store::MutationTestPhase::DmlIndexLookupStarted
1574 && first.swap(false, std::sync::atomic::Ordering::SeqCst)
1575 {
1576 started_tx.send(()).expect("notify writer");
1577 published_rx
1578 .lock()
1579 .unwrap()
1580 .recv_timeout(Duration::from_secs(10))
1581 .expect("writer published");
1582 }
1583 }));
1584 std::thread::scope(|scope| {
1585 let writer_rt = &rt;
1586 let writer = scope.spawn(move || {
1587 started_rx
1588 .recv_timeout(Duration::from_secs(10))
1589 .expect("lookup started");
1590 writer_rt
1591 .execute_query("UPDATE dml_lookup_race SET status = 'new' WHERE id = 1")
1592 .expect("publish update");
1593 published_tx.send(()).expect("release lookup");
1594 });
1595 let limits = super::super::resource_limits::ResourceLimits {
1596 max_query_duration: Some(Duration::from_secs(30)),
1597 ..Default::default()
1598 };
1599 let slots = std::sync::atomic::AtomicU64::new(0);
1600 let _budget = super::super::query_budget::Scope::enter(&limits, &slots)
1601 .expect("query deadline scope");
1602 let _guards = frame.install(&rt);
1603 let ids = super::super::dml_target_scan::DmlTargetScan::new(
1604 &rt,
1605 "dml_lookup_race",
1606 Some(&filter),
1607 None,
1608 )
1609 .find_target_ids()
1610 .expect("find DML targets");
1611 writer.join().expect("writer thread");
1612 assert_eq!(
1613 ids.len(),
1614 2,
1615 "racing publication must not omit a snapshot-visible row"
1616 );
1617 });
1618 *rt.index_store_ref().mutation_hook.lock() = None;
1619 }
1620
1621 #[test]
1622 fn replica_replay_preserves_snapshots_across_insert_update_and_delete() {
1623 use crate::replication::cdc::{change_record_from_entity, ChangeOperation};
1624 use crate::replication::logical::{ApplyMode, LogicalChangeApplier};
1625 reset_thread_locals();
1626 let source = fresh_runtime();
1627 let replica = fresh_runtime();
1628 for rt in [&source, &replica] {
1629 rt.execute_query("CREATE TABLE replica_snapshot (id INT, status TEXT)")
1630 .expect("schema");
1631 }
1632 replica
1633 .execute_query(
1634 "CREATE INDEX replica_snapshot_status ON replica_snapshot (status) USING HASH",
1635 )
1636 .expect("index");
1637 let sql = "SELECT id FROM replica_snapshot WHERE status = 'old'";
1638 let capture = || {
1639 StatementExecutionFrame::build(&replica, StatementIdentity::Text(sql)).expect("capture")
1640 };
1641 let read = |frame: &StatementExecutionFrame| {
1642 let reddb_rql::ast::QueryExpr::Table(query) =
1643 reddb_rql::modes::parse_multi(sql).expect("parse")
1644 else {
1645 panic!("table")
1646 };
1647 let _guards = frame.install(&replica);
1648 super::super::query_exec::table::execute_runtime_canonical_table_query_indexed(
1649 &replica.db(),
1650 &query,
1651 Some(replica.index_store_ref()),
1652 )
1653 .expect("snapshot read")
1654 .len()
1655 };
1656 let empty = capture();
1657 source
1658 .execute_query("INSERT INTO replica_snapshot (id, status) VALUES (1, 'old')")
1659 .expect("insert");
1660 let applier = LogicalChangeApplier::new(0);
1661 let replay = |lsn, op| {
1662 let entity = source
1663 .db()
1664 .store()
1665 .get_collection("replica_snapshot")
1666 .unwrap()
1667 .query_all(|entity| entity.xmax == 0)
1668 .pop()
1669 .unwrap();
1670 applier
1671 .apply(
1672 &replica.db(),
1673 &change_record_from_entity(
1674 lsn,
1675 1,
1676 op,
1677 "replica_snapshot",
1678 "table",
1679 &entity,
1680 12,
1681 None,
1682 ),
1683 ApplyMode::Replica,
1684 )
1685 .expect("replay");
1686 };
1687 replay(1, ChangeOperation::Insert);
1688 assert_eq!(
1689 read(&empty),
1690 0,
1691 "replay must not backdate an insert into an earlier snapshot"
1692 );
1693 let inserted = capture();
1694 source
1695 .execute_query("UPDATE replica_snapshot SET status = 'new' WHERE id = 1")
1696 .expect("update");
1697 replay(2, ChangeOperation::Update);
1698 assert_eq!(read(&inserted), 1, "old version remains visible");
1699 assert_eq!(read(&capture()), 0, "fresh snapshot uses the updated index");
1700 let all_sql = "SELECT id FROM replica_snapshot";
1702 let before_delete =
1703 StatementExecutionFrame::build(&replica, StatementIdentity::Text(all_sql)).unwrap();
1704 replay(3, ChangeOperation::Delete);
1705 let reddb_rql::ast::QueryExpr::Table(query) =
1706 reddb_rql::modes::parse_multi(all_sql).unwrap()
1707 else {
1708 panic!("table")
1709 };
1710 {
1711 let _guards = before_delete.install(&replica);
1712 assert_eq!(
1713 super::super::query_exec::table::execute_runtime_canonical_table_query_indexed(
1714 &replica.db(),
1715 &query,
1716 Some(replica.index_store_ref())
1717 )
1718 .unwrap()
1719 .len(),
1720 1
1721 );
1722 }
1723 assert_eq!(
1724 replica.execute_query(all_sql).unwrap().result.records.len(),
1725 0
1726 );
1727 }
1728
1729 #[test]
1730 fn autocommit_index_reads_keep_old_rows_while_another_transaction_is_open() {
1731 use crate::runtime::mvcc::{clear_current_connection_id, set_current_connection_id};
1732 reset_thread_locals();
1733 let rt = fresh_runtime();
1734 let execute = |connection, sql: &str| {
1735 set_current_connection_id(connection);
1736 let result = rt.execute_query(sql);
1737 clear_current_connection_id();
1738 result.expect("statement")
1739 };
1740 execute(1, "CREATE TABLE active_writer_rows (id INT, status TEXT)");
1741 execute(
1742 1,
1743 "INSERT INTO active_writer_rows (id, status) VALUES (1, 'old')",
1744 );
1745 execute(
1746 1,
1747 "CREATE INDEX active_writer_status ON active_writer_rows (status) USING HASH",
1748 );
1749 execute(2, "BEGIN");
1750 execute(
1751 2,
1752 "UPDATE active_writer_rows SET status = 'new' WHERE id = 1",
1753 );
1754 assert_eq!(
1755 execute(1, "SELECT id FROM active_writer_rows WHERE status = 'old'")
1756 .result
1757 .records
1758 .len(),
1759 1
1760 );
1761 assert_eq!(
1762 execute(1, "SELECT id FROM active_writer_rows WHERE status = 'new'")
1763 .result
1764 .records
1765 .len(),
1766 0
1767 );
1768 execute(2, "ROLLBACK");
1769 assert_eq!(
1770 execute(1, "SELECT id FROM active_writer_rows WHERE status = 'old'")
1771 .result
1772 .records
1773 .len(),
1774 1
1775 );
1776 }
1777
1778 #[test]
1779 fn frame_captures_identity_and_scope() {
1780 reset_thread_locals();
1781 set_current_tenant("acme".to_string());
1782 set_current_auth_identity("alice".to_string(), Role::Write);
1783
1784 let rt = fresh_runtime();
1785 let frame = StatementExecutionFrame::build(&rt, StatementIdentity::Text("SELECT 1"))
1786 .expect("frame builds");
1787 let f: &dyn ReadFrame = &frame;
1788
1789 assert_eq!(f.effective_scope(), Some("acme"));
1790 let id = f.identity().expect("identity captured");
1791 assert_eq!(id.0, "alice");
1792 assert!(matches!(id.1, Role::Write));
1793
1794 assert!(
1797 f.cache_key().contains("acme") && f.cache_key().contains("alice"),
1798 "cache key folds in scope + identity, got {:?}",
1799 f.cache_key()
1800 );
1801
1802 reset_thread_locals();
1803 }
1804
1805 #[test]
1806 fn as_of_rejects_non_versioned_user_collection() {
1807 reset_thread_locals();
1808 let rt = fresh_runtime();
1809
1810 let err = match StatementExecutionFrame::build(
1814 &rt,
1815 StatementIdentity::Text("SELECT * FROM not_versioned AS OF COMMIT 'deadbeef'"),
1816 ) {
1817 Err(e) => e,
1818 Ok(_) => panic!("AS OF on non-versioned user collection rejected"),
1819 };
1820
1821 let msg = format!("{err}");
1822 assert!(
1823 msg.contains("AS OF requires a versioned collection"),
1824 "expected AS OF rejection, got: {msg}"
1825 );
1826 }
1827
1828 #[test]
1840 fn select_path_routes_through_frame_cache_key() {
1841 reset_thread_locals();
1842 set_current_tenant("acme".to_string());
1843 set_current_auth_identity("alice".to_string(), Role::Read);
1844
1845 let rt = fresh_runtime();
1846 let result = rt
1847 .execute_query("SELECT 1")
1848 .expect("SELECT 1 executes under tenant=acme/identity=alice");
1849 assert_eq!(result.statement_type, "select");
1850
1851 let cache = rt.inner.result_cache.read();
1857 let any_keyed_with_scope = cache
1858 .0
1859 .keys()
1860 .any(|k| k.contains("acme") && k.contains("alice"));
1861 assert!(
1862 any_keyed_with_scope,
1863 "expected at least one result-cache key carrying tenant+identity, \
1864 got keys: {:?}",
1865 cache.0.keys().collect::<Vec<_>>()
1866 );
1867
1868 reset_thread_locals();
1869 }
1870
1871 #[test]
1883 fn volatile_select_does_not_populate_result_cache() {
1884 reset_thread_locals();
1885 let rt = fresh_runtime();
1886
1887 let frame = StatementExecutionFrame::build(
1891 &rt,
1892 StatementIdentity::Text("SELECT pg_advisory_unlock(1)"),
1893 )
1894 .expect("frame");
1895 let f: &dyn ReadFrame = &frame;
1896 assert!(
1897 !f.should_cache_result(),
1898 "volatile builtin must disable result-cache safety"
1899 );
1900
1901 let _ = rt
1906 .execute_query("SELECT pg_advisory_unlock(1)")
1907 .expect("volatile SELECT executes");
1908 let cache = rt.inner.result_cache.read();
1909 let key = result_cache_key("SELECT pg_advisory_unlock(1)");
1910 assert!(
1911 !cache.0.contains_key(&key),
1912 "volatile SELECT must not populate result cache, found key {key:?} in {:?}",
1913 cache.0.keys().collect::<Vec<_>>()
1914 );
1915
1916 reset_thread_locals();
1917 }
1918
1919 #[test]
1920 fn blob_cache_backend_populates_blob_path_without_legacy_write() {
1921 reset_thread_locals();
1922 let rt = fresh_runtime();
1923 rt.inner
1924 .db
1925 .store()
1926 .set_config_tree("runtime.result_cache.backend", &crate::json!("blob_cache"));
1927
1928 let result = rt.execute_query("SELECT 1").expect("SELECT 1 executes");
1929 assert_eq!(result.statement_type, "select");
1930
1931 let key = result_cache_key("SELECT 1");
1932 assert!(
1933 rt.inner
1934 .result_blob_cache
1935 .get("runtime.result_cache", &key)
1936 .is_some(),
1937 "blob backend should stamp the Blob Cache path"
1938 );
1939 assert!(rt.inner.result_blob_entries.read().0.contains_key(&key));
1940 assert!(
1941 !rt.inner.result_cache.read().0.contains_key(&key),
1942 "blob backend should not write the legacy map"
1943 );
1944 }
1945
1946 #[test]
1947 fn blob_cache_backend_keeps_volatile_select_out_of_blob_path() {
1948 reset_thread_locals();
1949 let rt = fresh_runtime();
1950 rt.inner
1951 .db
1952 .store()
1953 .set_config_tree("runtime.result_cache.backend", &crate::json!("blob_cache"));
1954
1955 let _ = rt
1956 .execute_query("SELECT pg_advisory_unlock(1)")
1957 .expect("volatile SELECT executes");
1958 let key = result_cache_key("SELECT pg_advisory_unlock(1)");
1959 assert!(
1960 rt.inner
1961 .result_blob_cache
1962 .get("runtime.result_cache", &key)
1963 .is_none(),
1964 "volatile SELECT must not populate blob result cache"
1965 );
1966 assert!(!rt.inner.result_blob_entries.read().0.contains_key(&key));
1967 }
1968
1969 #[test]
1970 fn shadow_backend_dual_writes_and_reports_no_divergence_on_equal_results() {
1971 reset_thread_locals();
1972 let rt = fresh_runtime();
1973 rt.inner
1974 .db
1975 .store()
1976 .set_config_tree("runtime.result_cache.backend", &crate::json!("shadow"));
1977
1978 let first = rt.execute_query("SELECT 1").expect("first SELECT");
1979 let second = rt.execute_query("SELECT 1").expect("cached SELECT");
1980 assert_eq!(first.result.len(), second.result.len());
1981
1982 let key = result_cache_key("SELECT 1");
1983 assert!(rt.inner.result_cache.read().0.contains_key(&key));
1984 assert!(rt.inner.result_blob_entries.read().0.contains_key(&key));
1985 assert_eq!(rt.result_cache_shadow_divergences(), 0);
1986 assert_eq!(
1987 crate::runtime::METRIC_CACHE_SHADOW_DIVERGENCE_TOTAL,
1988 "cache_shadow_divergence_total"
1989 );
1990 }
1991
1992 #[test]
1993 fn as_of_on_red_collection_records_floor() {
1994 reset_thread_locals();
1995 let rt = fresh_runtime();
1996
1997 let frame = StatementExecutionFrame::build(
2000 &rt,
2001 StatementIdentity::Text("SELECT * FROM red_commits AS OF SNAPSHOT 1"),
2002 )
2003 .expect("AS OF SNAPSHOT 1 on red_commits resolves");
2004
2005 let f: &dyn ReadFrame = &frame;
2006 assert_eq!(
2007 f.as_of_floor(),
2008 Some(1),
2009 "AS OF SNAPSHOT 1 records xid=1 as the floor"
2010 );
2011 assert_eq!(f.snapshot().xid, 1);
2012 assert!(
2013 f.snapshot().in_progress.is_empty(),
2014 "historical reads have no in-progress set"
2015 );
2016 }
2017
2018 #[test]
2024 fn frame_classifies_privilege_and_lock_intent_from_prefix() {
2025 reset_thread_locals();
2026 let rt = fresh_runtime();
2027
2028 let cases = [
2029 ("SELECT 1", Privilege::Read, LockIntent::Shared),
2030 ("LIST KV settings", Privilege::Read, LockIntent::Shared),
2031 (
2032 "KV GET settings.feature",
2033 Privilege::Read,
2034 LockIntent::Shared,
2035 ),
2036 ("VAULT LIST secrets", Privilege::Read, LockIntent::Shared),
2037 (
2038 "INSERT INTO t (id) VALUES (1)",
2039 Privilege::Write,
2040 LockIntent::Exclusive,
2041 ),
2042 (
2043 "KV PUT settings.feature = 'on'",
2044 Privilege::Write,
2045 LockIntent::Exclusive,
2046 ),
2047 (
2048 "VAULT PUT secrets.api = 'x'",
2049 Privilege::Write,
2050 LockIntent::Exclusive,
2051 ),
2052 (
2053 "UPDATE t SET x = 1 WHERE id = 1",
2054 Privilege::Write,
2055 LockIntent::Exclusive,
2056 ),
2057 (
2058 "DELETE FROM t WHERE id = 1",
2059 Privilege::Write,
2060 LockIntent::Exclusive,
2061 ),
2062 (
2063 "CREATE TABLE foo (id INT)",
2064 Privilege::Write,
2065 LockIntent::Exclusive,
2066 ),
2067 ("BEGIN", Privilege::None, LockIntent::None),
2068 ("COMMIT", Privilege::None, LockIntent::None),
2069 ("SET timezone = 'UTC'", Privilege::None, LockIntent::None),
2070 ];
2071
2072 for (q, want_priv, want_lock) in cases {
2073 let frame = StatementExecutionFrame::build(&rt, StatementIdentity::Text(q))
2074 .unwrap_or_else(|e| panic!("frame builds for {q:?}: {e}"));
2075 let f: &dyn ReadFrame = &frame;
2076 assert_eq!(f.required_privilege(), want_priv, "privilege for {q:?}");
2077 assert_eq!(f.lock_intent(), want_lock, "lock intent for {q:?}");
2078 }
2079 }
2080
2081 #[test]
2098 fn insert_under_read_role_denied_via_frame_privilege() {
2099 reset_thread_locals();
2100 set_current_auth_identity("alice".to_string(), Role::Read);
2101
2102 let rt = fresh_runtime();
2103 let frame = StatementExecutionFrame::build(
2109 &rt,
2110 StatementIdentity::Text("INSERT INTO t (id) VALUES (1)"),
2111 )
2112 .expect("frame builds for INSERT");
2113 let f: &dyn ReadFrame = &frame;
2114 assert_eq!(
2115 f.required_privilege(),
2116 Privilege::Write,
2117 "INSERT classified as Write"
2118 );
2119 let id = f.identity().expect("identity captured");
2120 assert!(
2121 !f.required_privilege().is_satisfied_by(id.1),
2122 "Role::Read does not satisfy Privilege::Write — frame must deny"
2123 );
2124
2125 use reddb_rql::ast::{QueryExpr, TableQuery};
2131 let expr = QueryExpr::Table(TableQuery::new("t"));
2132 let err = frame
2133 .check_query_privilege(&rt, &expr)
2134 .expect_err("denied via frame's coarse privilege gate");
2135 let msg = format!("{err}");
2136 assert!(
2137 msg.contains("permission denied") && msg.contains("Write"),
2138 "expected frame-level Write deny, got: {msg}"
2139 );
2140
2141 reset_thread_locals();
2142 }
2143
2144 #[test]
2157 fn large_select_through_frame_arena_returns_all_rows_in_order() {
2158 reset_thread_locals();
2159 let rt = fresh_runtime();
2160 rt.execute_query("CREATE TABLE big (id INT)")
2161 .expect("create table");
2162
2163 const N: usize = 2_500;
2166 for start in (0..N).step_by(250) {
2167 let end = (start + 250).min(N);
2168 let values = (start..end)
2169 .map(|i| format!("({i})"))
2170 .collect::<Vec<_>>()
2171 .join(", ");
2172 rt.execute_query(&format!("INSERT INTO big (id) VALUES {values}"))
2173 .unwrap_or_else(|err| panic!("insert rows {start}..{end}: {err:?}"));
2174 }
2175
2176 let result = rt
2177 .execute_query("SELECT id FROM big ORDER BY id")
2178 .expect("large SELECT executes through the frame arena path");
2179 assert_eq!(result.statement_type, "select");
2180 assert_eq!(
2181 result.result.records.len(),
2182 N,
2183 "every inserted row streams back through the arena-backed channel"
2184 );
2185 for (i, record) in result.result.records.iter().enumerate() {
2186 assert_eq!(
2187 record.get("id"),
2188 Some(&reddb_types::Value::Integer(i as i64)),
2189 "row {i} is byte-identical to the per-request-allocation baseline"
2190 );
2191 }
2192
2193 reset_thread_locals();
2194 }
2195
2196 #[test]
2210 fn parameterized_transport_adapters_delegate_binding_to_runtime() {
2211 let manifest_dir = std::path::Path::new(env!("CARGO_MANIFEST_DIR"));
2212 let adapters = [
2213 ("src/server/handlers_query.rs", "QueryRequestExecutor"),
2214 ("src/rpc_stdio.rs", "QueryRequestExecutor"),
2215 ("src/wire/redwire/session.rs", "QueryRequestExecutor"),
2216 ("src/wire/postgres/server.rs", "QueryRequestExecutor"),
2217 ("src/grpc.rs", "QueryRequestExecutor"),
2218 ];
2219
2220 for (relative, delegation) in adapters {
2221 let path = manifest_dir.join(relative);
2222 let text = std::fs::read_to_string(&path)
2223 .unwrap_or_else(|err| panic!("read {}: {err}", path.display()));
2224 assert!(
2225 text.contains(delegation),
2226 "{relative} should delegate parameterized query execution through {delegation}"
2227 );
2228 assert!(
2229 !text.contains("user_params::bind"),
2230 "{relative} must not bind SQL params in the transport adapter"
2231 );
2232 }
2233
2234 let seam = std::fs::read_to_string(manifest_dir.join("src/runtime/query_request.rs"))
2237 .expect("read src/runtime/query_request.rs");
2238 assert!(
2239 seam.contains("execute_query_with_params"),
2240 "the Request module must execute text SQL through the runtime entrypoint"
2241 );
2242 }
2243
2244 #[test]
2254 fn control_statement_skips_intent_locks_via_frame() {
2255 reset_thread_locals();
2256 let rt = fresh_runtime();
2257
2258 let frame = StatementExecutionFrame::build(&rt, StatementIdentity::Text("BEGIN"))
2259 .expect("frame builds for BEGIN");
2260 let f: &dyn ReadFrame = &frame;
2261 assert_eq!(f.lock_intent(), LockIntent::None);
2262
2263 use reddb_rql::ast::{QueryExpr, TableQuery};
2268 let expr = QueryExpr::Table(TableQuery::new("t"));
2269 let guard = frame
2270 .acquire_intent_locks(&rt, &expr)
2271 .expect("no lock is attempted");
2272 assert!(
2273 guard.is_none(),
2274 "BEGIN frame's lock_intent=None must short-circuit lock acquisition"
2275 );
2276 }
2277
2278 #[test]
2285 fn nested_frame_inherits_outer_statement_snapshot() {
2286 reset_thread_locals();
2287 let rt = fresh_runtime();
2288 rt.execute_query("CREATE TABLE nested_snap (id INT)")
2289 .expect("create table");
2290
2291 let outer = StatementExecutionFrame::build(
2292 &rt,
2293 StatementIdentity::Text("SELECT * FROM nested_snap"),
2294 )
2295 .expect("outer frame builds");
2296 let _outer_guards = outer.install(&rt);
2297
2298 rt.execute_query("INSERT INTO nested_snap (id) VALUES (1)")
2301 .expect("concurrent insert");
2302
2303 let expr = QueryExpr::Table(reddb_rql::ast::TableQuery::new("nested_snap"));
2304 let nested = StatementExecutionFrame::build(&rt, StatementIdentity::Expr(&expr))
2305 .expect("nested frame builds");
2306
2307 assert_eq!(
2308 ReadFrame::snapshot(&nested).xid,
2309 ReadFrame::snapshot(&outer).xid,
2310 "nested execution must inherit the outer statement's snapshot"
2311 );
2312 }
2313
2314 #[test]
2327 fn nested_text_statement_sees_commits_made_since_the_outer_frame() {
2328 reset_thread_locals();
2329 let rt = fresh_runtime();
2330 rt.execute_query("CREATE TABLE nested_text_snap (id INT)")
2331 .expect("create table");
2332
2333 let outer = StatementExecutionFrame::build(
2334 &rt,
2335 StatementIdentity::Text("SELECT * FROM nested_text_snap"),
2336 )
2337 .expect("outer frame builds");
2338 let _outer_guards = outer.install(&rt);
2339
2340 rt.execute_query("INSERT INTO nested_text_snap (id) VALUES (1)")
2341 .expect("concurrent insert");
2342
2343 let nested = StatementExecutionFrame::build(
2344 &rt,
2345 StatementIdentity::Text("SELECT * FROM nested_text_snap"),
2346 )
2347 .expect("nested frame builds");
2348
2349 assert_ne!(
2350 ReadFrame::snapshot(&nested).xid,
2351 ReadFrame::snapshot(&outer).xid,
2352 "a nested text statement must mint a fresh snapshot, not inherit \
2353 the enclosing statement's frozen view"
2354 );
2355 }
2356
2357 #[test]
2362 fn expr_frame_rejects_as_of_instead_of_reading_live() {
2363 reset_thread_locals();
2364 let rt = fresh_runtime();
2365
2366 let mut table = reddb_rql::ast::TableQuery::new("red_commits");
2367 table.as_of = Some(reddb_rql::ast::AsOfClause::Snapshot(1));
2368 let expr = QueryExpr::Table(table);
2369
2370 let err = match StatementExecutionFrame::build(&rt, StatementIdentity::Expr(&expr)) {
2371 Err(err) => err,
2372 Ok(_) => panic!("AS OF without statement text must be refused, not read live"),
2373 };
2374 let msg = format!("{err}");
2375 assert!(
2376 msg.contains("AS OF cannot be resolved from a pre-parsed expression"),
2377 "expected a loud AS OF refusal, got: {msg}"
2378 );
2379 }
2380
2381 #[test]
2385 fn expr_frame_opts_out_of_the_result_cache() {
2386 reset_thread_locals();
2387 let rt = fresh_runtime();
2388
2389 let expr = QueryExpr::Table(reddb_rql::ast::TableQuery::new("t"));
2390 let frame = StatementExecutionFrame::build(&rt, StatementIdentity::Expr(&expr))
2391 .expect("expr frame builds");
2392
2393 assert!(frame.cache_key().is_empty(), "expr frame has no cache key");
2394 assert!(
2395 !frame.can_read_result_cache(),
2396 "expr frame must never serve from the result cache"
2397 );
2398 }
2399}