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 {
921 pub(crate) captured_snapshot: Option<SnapshotContext>,
922 pub(crate) tenant: Option<String>,
923 pub(crate) identity: Option<(String, Role)>,
924 pub(crate) auth_principal: Option<crate::auth::UserId>,
925 pub(crate) snapshot: Snapshot,
926 pub(crate) visible_collections: Option<HashSet<String>>,
927}
928
929impl EffectiveScope {
930 pub fn has_capability(&self, _capability: &str) -> bool {
940 false
941 }
942}
943
944impl ReadFrame for EffectiveScope {
945 fn snapshot_context(&self, runtime: &RedDBRuntime) -> SnapshotContext {
946 self.captured_snapshot
947 .clone()
948 .unwrap_or_else(|| SnapshotContext {
949 snapshot: self.snapshot.clone(),
950 manager: runtime.inner.transaction_state.snapshot_manager(),
951 own_xids: HashSet::new(),
952 requires_index_fallback: true,
953 serializable_reader: None,
954 })
955 }
956
957 fn effective_scope(&self) -> Option<&str> {
958 self.tenant.as_deref()
959 }
960 fn identity(&self) -> Option<(&str, Role)> {
961 self.identity.as_ref().map(|(u, r)| (u.as_str(), *r))
962 }
963 fn snapshot(&self) -> &Snapshot {
964 &self.snapshot
965 }
966 fn as_of_floor(&self) -> Option<Xid> {
967 None
968 }
969 fn cache_key(&self) -> &str {
970 ""
971 }
972 fn should_cache_result(&self) -> bool {
973 false
974 }
975 fn required_privilege(&self) -> Privilege {
976 Privilege::Read
977 }
978 fn lock_intent(&self) -> LockIntent {
979 LockIntent::Shared
980 }
981 fn visible_collections(&self) -> Option<&HashSet<String>> {
982 self.visible_collections.as_ref()
983 }
984}
985
986fn own_transaction_xids(
987 ctx: Option<&crate::storage::transaction::snapshot::TxnContext>,
988) -> HashSet<Xid> {
989 let mut set = HashSet::new();
990 if let Some(ctx) = ctx {
991 set.insert(ctx.xid);
992 for (_, sub) in &ctx.savepoints {
993 set.insert(*sub);
994 }
995 for sub in &ctx.released_sub_xids {
996 set.insert(*sub);
997 }
998 }
999 set
1000}
1001
1002impl RedDBRuntime {
1003 pub(crate) fn ai_scope(&self) -> EffectiveScope {
1009 let tenant = super::impl_core::current_tenant();
1010 let identity = super::impl_core::current_auth_identity();
1011 let auth_principal = super::execution_context::current_auth_principal();
1012 let captured_snapshot = super::execution_context::capture_current_snapshot();
1013 let snapshot = captured_snapshot.as_ref().map_or_else(
1014 || self.current_snapshot(),
1015 |context| context.snapshot.clone(),
1016 );
1017 let visible_collections = match (self.inner.auth_store.read().clone(), identity.as_ref()) {
1018 (Some(store), Some((principal, role))) => {
1019 let collections = self.inner.db.store().list_collections();
1020 Some(store.visible_collections_for_scope(
1021 tenant.as_deref(),
1022 *role,
1023 principal,
1024 &collections,
1025 ))
1026 }
1027 _ => None,
1028 };
1029 EffectiveScope {
1030 captured_snapshot,
1031 tenant,
1032 identity,
1033 auth_principal,
1034 snapshot,
1035 visible_collections,
1036 }
1037 }
1038}
1039
1040#[cfg(test)]
1044pub(crate) mod test_support {
1045 use super::{LockIntent, Privilege, ReadFrame};
1046 use crate::auth::Role;
1047 use crate::storage::transaction::snapshot::{Snapshot, Xid};
1048 use std::collections::HashSet;
1049
1050 pub(crate) struct FakeReadFrame {
1054 pub tenant: Option<String>,
1055 pub identity: Option<(String, Role)>,
1056 pub snapshot: Snapshot,
1057 pub visible: Option<HashSet<String>>,
1058 }
1059
1060 impl FakeReadFrame {
1061 pub(crate) fn without_scope() -> Self {
1062 Self {
1063 tenant: None,
1064 identity: None,
1065 snapshot: Snapshot {
1066 xid: 0,
1067 in_progress: HashSet::new(),
1068 },
1069 visible: None,
1070 }
1071 }
1072
1073 pub(crate) fn with_visible(visible: HashSet<String>) -> Self {
1074 Self {
1075 tenant: Some("acme".to_string()),
1076 identity: Some(("alice".to_string(), Role::Read)),
1077 snapshot: Snapshot {
1078 xid: 0,
1079 in_progress: HashSet::new(),
1080 },
1081 visible: Some(visible),
1082 }
1083 }
1084 }
1085
1086 impl ReadFrame for FakeReadFrame {
1087 fn effective_scope(&self) -> Option<&str> {
1088 self.tenant.as_deref()
1089 }
1090 fn identity(&self) -> Option<(&str, Role)> {
1091 self.identity.as_ref().map(|(u, r)| (u.as_str(), *r))
1092 }
1093 fn snapshot(&self) -> &Snapshot {
1094 &self.snapshot
1095 }
1096 fn as_of_floor(&self) -> Option<Xid> {
1097 None
1098 }
1099 fn cache_key(&self) -> &str {
1100 ""
1101 }
1102 fn should_cache_result(&self) -> bool {
1103 false
1104 }
1105 fn required_privilege(&self) -> Privilege {
1106 Privilege::Read
1107 }
1108 fn lock_intent(&self) -> LockIntent {
1109 LockIntent::Shared
1110 }
1111 fn visible_collections(&self) -> Option<&HashSet<String>> {
1112 self.visible.as_ref()
1113 }
1114 }
1115}
1116
1117impl RedDBRuntime {
1118 fn statement_snapshot(
1148 &self,
1149 query: &str,
1150 identity: StatementIdentity<'_>,
1151 ) -> RedDBResult<(Snapshot, Option<Xid>, SnapshotRetentionGuard)> {
1152 match peek_top_level_as_of_with_table(query) {
1153 Some((spec, Some(table))) => {
1154 if !table.starts_with("red_")
1155 && !table.starts_with("red.")
1156 && !self.vcs_is_versioned(&table)?
1157 {
1158 return Err(RedDBError::InvalidConfig(format!(
1159 "AS OF requires a versioned collection — \
1160 `{table}` has not opted in. \
1161 Call vcs.set_versioned(\"{table}\", true) first."
1162 )));
1163 }
1164 let xid = self.vcs_resolve_as_of(spec)?;
1165 let (snapshot, retention) = self.snapshot_manager().retain_snapshot(Snapshot {
1166 xid,
1167 in_progress: HashSet::new(),
1168 });
1169 Ok((snapshot, Some(xid), retention))
1170 }
1171 Some((spec, None)) => {
1172 let xid = self.vcs_resolve_as_of(spec)?;
1173 let (snapshot, retention) = self.snapshot_manager().retain_snapshot(Snapshot {
1174 xid,
1175 in_progress: HashSet::new(),
1176 });
1177 Ok((snapshot, Some(xid), retention))
1178 }
1179 None => match (identity, capture_current_snapshot()) {
1180 (StatementIdentity::Expr(_), Some(outer)) => {
1181 let (snapshot, retention) =
1182 self.snapshot_manager().retain_snapshot(outer.snapshot);
1183 Ok((snapshot, None, retention))
1184 }
1185 _ => {
1186 let (snapshot, retention) = self
1187 .inner
1188 .transaction_state
1189 .retained_current_snapshot(current_connection_id());
1190 Ok((snapshot, None, retention))
1191 }
1192 },
1193 }
1194 }
1195
1196 fn result_cache_safe(&self, conn_id: u64) -> bool {
1197 let has_active_xids = self
1198 .inner
1199 .transaction_state
1200 .snapshot_manager()
1201 .oldest_active_xid()
1202 .is_some();
1203 let in_own_tx = self.inner.transaction_state.in_transaction(conn_id);
1204 !has_active_xids && !in_own_tx
1205 }
1206}
1207
1208fn is_graph_tvf_engine(engine: &str) -> bool {
1213 matches!(engine, "runtime-graph-tvf" | "runtime-graph-tvf-inline")
1214}
1215
1216fn result_cache_key(query: &str) -> String {
1217 let tenant = current_tenant().unwrap_or_default();
1218 let auth = current_auth_identity()
1219 .map(|(user, role)| format!("{}|{:?}", user, role))
1220 .unwrap_or_default();
1221 if tenant.is_empty() && auth.is_empty() {
1222 query.to_string()
1223 } else {
1224 format!("{query}\u{001e}{tenant}\u{001e}{auth}")
1225 }
1226}
1227
1228#[cfg(test)]
1229mod tests {
1230 use super::*;
1231 use crate::api::RedDBOptions;
1232 use crate::runtime::impl_core::{
1233 clear_current_auth_identity, clear_current_tenant, set_current_auth_identity,
1234 set_current_tenant,
1235 };
1236 use crate::runtime::RedDBRuntime;
1237
1238 fn fresh_runtime() -> RedDBRuntime {
1239 RedDBRuntime::with_options(RedDBOptions::in_memory()).expect("in-memory runtime")
1240 }
1241
1242 enum HeldFrame {
1243 Ordinary,
1244 Nested,
1245 Historical,
1246 }
1247
1248 fn verify_frame_survives_vacuum(delete: bool, kind: HeldFrame) {
1249 use crate::runtime::mvcc::{clear_current_connection_id, set_current_connection_id};
1250
1251 reset_thread_locals();
1252 clear_current_connection_id();
1253 let runtime = fresh_runtime();
1254 let execute = |connection, sql: &str| {
1255 set_current_connection_id(connection);
1256 let result = runtime.execute_query(sql);
1257 clear_current_connection_id();
1258 result.expect("valid frame retention fixture statement")
1259 };
1260 execute(1, "CREATE TABLE frame_rows (id TEXT, n INTEGER)");
1261 execute(1, "INSERT INTO frame_rows (id, n) VALUES ('row', 1)");
1262 let historical_xid = runtime.current_snapshot().xid;
1263 if matches!(kind, HeldFrame::Historical) {
1264 runtime
1265 .vcs_set_versioned("frame_rows", true)
1266 .expect("enable historical reads");
1267 }
1268 execute(2, "BEGIN");
1269 let query = if matches!(kind, HeldFrame::Historical) {
1270 format!("SELECT id, n FROM frame_rows AS OF SNAPSHOT {historical_xid}")
1271 } else {
1272 "SELECT id, n FROM frame_rows".to_string()
1273 };
1274 let frame = StatementExecutionFrame::build(&runtime, StatementIdentity::Text(&query))
1275 .expect("frame captures its snapshot");
1276 let frame = if matches!(kind, HeldFrame::Nested) {
1277 let guards = frame.install(&runtime);
1278 let expr = QueryExpr::Table(reddb_rql::ast::TableQuery::new("frame_rows"));
1279 let nested = StatementExecutionFrame::build(&runtime, StatementIdentity::Expr(&expr))
1280 .expect("nested frame inherits the protected snapshot");
1281 drop(guards);
1282 drop(frame);
1283 nested
1284 } else {
1285 frame
1286 };
1287 execute(
1290 2,
1291 if delete {
1292 "DELETE FROM frame_rows WHERE id = 'row'"
1293 } else {
1294 "UPDATE frame_rows SET n = 2 WHERE id = 'row'"
1295 },
1296 );
1297 execute(2, "COMMIT");
1298 execute(1, "VACUUM frame_rows");
1299 {
1300 let _guards = frame.install(&runtime);
1301 let records = super::super::record_search::scan_runtime_table_source_records_limited(
1302 &runtime.db(),
1303 "frame_rows",
1304 None,
1305 )
1306 .expect("captured frame scan succeeds after vacuum");
1307 assert_eq!(
1308 records.len(),
1309 1,
1310 "vacuum reclaimed an active frame's visible row"
1311 );
1312 assert_eq!(
1313 records[0]
1314 .get("n")
1315 .expect("selected value exists")
1316 .to_string(),
1317 "1"
1318 );
1319 }
1320 drop(frame);
1321 execute(1, "VACUUM frame_rows");
1322 let physical = runtime
1323 .db()
1324 .store()
1325 .get_collection("frame_rows")
1326 .expect("fixture collection exists")
1327 .query_all(|_| true)
1328 .len();
1329 assert_eq!(
1330 physical,
1331 usize::from(!delete),
1332 "completed frame leaked its retention pin"
1333 );
1334 clear_current_connection_id();
1335 }
1336
1337 #[test]
1338 fn autocommit_frame_retains_update_until_scan_completes() {
1339 verify_frame_survives_vacuum(false, HeldFrame::Ordinary);
1340 }
1341
1342 #[test]
1343 fn autocommit_frame_retains_delete_until_scan_completes() {
1344 verify_frame_survives_vacuum(true, HeldFrame::Ordinary);
1345 }
1346
1347 #[test]
1348 fn nested_frame_retains_history_after_parent_frame_drops() {
1349 verify_frame_survives_vacuum(false, HeldFrame::Nested);
1350 }
1351
1352 #[test]
1353 fn historical_frame_retains_history_until_scan_completes() {
1354 verify_frame_survives_vacuum(true, HeldFrame::Historical);
1355 }
1356
1357 fn reset_thread_locals() {
1360 clear_current_tenant();
1361 clear_current_auth_identity();
1362 }
1363
1364 #[test]
1365 fn autocommit_select_takes_live_snapshot() {
1366 reset_thread_locals();
1367 let rt = fresh_runtime();
1368 let frame = StatementExecutionFrame::build(&rt, StatementIdentity::Text("SELECT 1"))
1369 .expect("frame builds for SELECT 1");
1370
1371 let f: &dyn ReadFrame = &frame;
1374 assert!(f.as_of_floor().is_none(), "live read has no AS OF floor");
1375 assert_eq!(f.snapshot().xid, rt.snapshot_manager().peek_next_xid() - 1);
1376 assert!(!f.snapshot().sees(rt.snapshot_manager().peek_next_xid(), 0));
1377 }
1378
1379 #[test]
1380 fn autocommit_snapshot_does_not_lose_rows_after_index_publication() {
1381 reset_thread_locals();
1382 let rt = fresh_runtime();
1383 rt.execute_query("CREATE TABLE frame_index_race (id INT PRIMARY KEY, status TEXT)")
1384 .expect("create fixture");
1385 rt.execute_query("INSERT INTO frame_index_race (id, status) VALUES (1, 'old'), (2, 'old')")
1386 .expect("insert fixture");
1387 rt.execute_query("CREATE INDEX frame_status_hash ON frame_index_race (status) USING HASH")
1388 .expect("create index");
1389 let sql = "SELECT id FROM frame_index_race WHERE status = 'old'";
1390 let frame = StatementExecutionFrame::build(&rt, StatementIdentity::Text(sql))
1391 .expect("capture autocommit snapshot");
1392 rt.execute_query("UPDATE frame_index_race SET status = 'new' WHERE id = 1")
1393 .expect("publish changed index entry");
1394 let reddb_rql::ast::QueryExpr::Table(query) =
1395 reddb_rql::modes::parse_multi(sql).expect("parse")
1396 else {
1397 panic!("expected table query");
1398 };
1399 let _guards = frame.install(&rt);
1400 let records =
1401 super::super::query_exec::table::execute_runtime_canonical_table_query_indexed(
1402 &rt.db(),
1403 &query,
1404 Some(rt.index_store_ref()),
1405 )
1406 .expect("read captured snapshot");
1407 let mut ids = records
1408 .iter()
1409 .map(|record| record.get("id").expect("id").to_string())
1410 .collect::<Vec<_>>();
1411 ids.sort();
1412 assert_eq!(
1413 ids,
1414 ["1", "2"],
1415 "current index omitted a visible historical row"
1416 );
1417 }
1418
1419 #[test]
1420 fn captured_dml_snapshot_finds_rows_after_index_publication() {
1421 reset_thread_locals();
1422 let rt = fresh_runtime();
1423 rt.execute_query("CREATE TABLE frame_dml_race (id INT PRIMARY KEY, status TEXT)")
1424 .expect("create fixture");
1425 rt.execute_query("INSERT INTO frame_dml_race (id, status) VALUES (1, 'old'), (2, 'old')")
1426 .expect("insert fixture");
1427 rt.execute_query("CREATE INDEX frame_dml_status ON frame_dml_race (status) USING HASH")
1428 .expect("create index");
1429 let sql = "DELETE FROM frame_dml_race WHERE status = 'old'";
1430 let frame = StatementExecutionFrame::build(&rt, StatementIdentity::Text(sql))
1431 .expect("capture DML snapshot");
1432 rt.execute_query("UPDATE frame_dml_race SET status = 'new' WHERE id = 1")
1433 .expect("publish new indexed value");
1434 let reddb_rql::ast::QueryExpr::Table(query) =
1435 reddb_rql::modes::parse_multi("SELECT id FROM frame_dml_race WHERE status = 'old'")
1436 .expect("parse filter")
1437 else {
1438 panic!("expected table query")
1439 };
1440 let filter = reddb_rql::sql_lowering::effective_table_filter(&query).expect("filter");
1441 let _guards = frame.install(&rt);
1442 let ids = super::super::dml_target_scan::DmlTargetScan::new(
1443 &rt,
1444 "frame_dml_race",
1445 Some(&filter),
1446 None,
1447 )
1448 .find_target_ids()
1449 .expect("find DML targets");
1450 assert_eq!(
1451 ids.len(),
1452 2,
1453 "DML target scan must discover both snapshot-visible rows"
1454 );
1455 }
1456
1457 #[test]
1458 fn captured_snapshot_survives_indexed_update_and_delete_for_range_queries() {
1459 reset_thread_locals();
1460 let rt = fresh_runtime();
1461 rt.execute_query("CREATE TABLE frame_range_race (id INT PRIMARY KEY, score INT)")
1462 .expect("create fixture");
1463 rt.execute_query("INSERT INTO frame_range_race (id, score) VALUES (1, 10), (2, 20)")
1464 .expect("insert fixture");
1465 rt.execute_query("CREATE INDEX frame_score_range ON frame_range_race (score)")
1466 .expect("create index");
1467 let sql = "SELECT id FROM frame_range_race WHERE score >= 10 AND score <= 20 ORDER BY id";
1468 let frame = StatementExecutionFrame::build(&rt, StatementIdentity::Text(sql))
1469 .expect("capture read snapshot");
1470 rt.execute_query("UPDATE frame_range_race SET score = 99 WHERE id = 1")
1471 .expect("update row");
1472 rt.execute_query("DELETE FROM frame_range_race WHERE id = 2")
1473 .expect("delete row");
1474 let reddb_rql::ast::QueryExpr::Table(query) =
1475 reddb_rql::modes::parse_multi(sql).expect("parse")
1476 else {
1477 panic!("expected table query");
1478 };
1479 let _guards = frame.install(&rt);
1480 let records =
1481 super::super::query_exec::table::execute_runtime_canonical_table_query_indexed(
1482 &rt.db(),
1483 &query,
1484 Some(rt.index_store_ref()),
1485 )
1486 .expect("read captured snapshot");
1487 let ids = records
1488 .iter()
1489 .map(|record| record.get("id").expect("id").to_string())
1490 .collect::<Vec<_>>();
1491 assert_eq!(ids, ["1", "2"]);
1492 }
1493
1494 #[test]
1495 fn dml_target_scan_does_not_retry_inside_stored_function_budget() {
1496 use std::sync::atomic::{AtomicUsize, Ordering};
1497 use std::sync::Arc;
1498 reset_thread_locals();
1499 let rt = fresh_runtime();
1500 rt.execute_query("CREATE TABLE dml_budget_guard (id INT, status TEXT)")
1501 .expect("create fixture");
1502 rt.execute_query("INSERT INTO dml_budget_guard (id, status) VALUES (1, 'old')")
1503 .expect("insert fixture");
1504 rt.execute_query("CREATE INDEX dml_budget_status ON dml_budget_guard (status) USING HASH")
1505 .expect("create index");
1506 let frame = StatementExecutionFrame::build(
1507 &rt,
1508 StatementIdentity::Text("DELETE FROM dml_budget_guard WHERE status = 'old'"),
1509 )
1510 .expect("capture snapshot");
1511 let reddb_rql::ast::QueryExpr::Table(query) =
1512 reddb_rql::modes::parse_multi("SELECT id FROM dml_budget_guard WHERE status = 'old'")
1513 .expect("parse filter")
1514 else {
1515 panic!("expected table query")
1516 };
1517 let filter = reddb_rql::sql_lowering::effective_table_filter(&query).expect("filter");
1518 let calls = Arc::new(AtomicUsize::new(0));
1519 let observed = Arc::clone(&calls);
1520 *rt.index_store_ref().mutation_hook.lock() = Some(Arc::new(move |collection, phase| {
1521 if collection == "dml_budget_guard"
1522 && phase == super::super::index_store::MutationTestPhase::DmlIndexLookupStarted
1523 {
1524 observed.fetch_add(1, Ordering::SeqCst);
1525 }
1526 }));
1527 let _budget = super::super::function_budget::Scope::enter(1_000, 10_000)
1528 .expect("stored function budget");
1529 let _guards = frame.install(&rt);
1530 let ids = super::super::dml_target_scan::DmlTargetScan::new(
1531 &rt,
1532 "dml_budget_guard",
1533 Some(&filter),
1534 None,
1535 )
1536 .find_target_ids()
1537 .expect("find DML targets");
1538 assert_eq!(ids.len(), 1);
1539 assert_eq!(
1540 calls.load(Ordering::SeqCst),
1541 0,
1542 "bounded stored functions must not retry indexed discovery"
1543 );
1544 *rt.index_store_ref().mutation_hook.lock() = None;
1545 }
1546
1547 #[test]
1548 fn dml_index_lookup_retries_when_publication_races_target_selection() {
1549 use std::sync::{mpsc, Arc, Mutex};
1550 use std::time::Duration;
1551 reset_thread_locals();
1552 let rt = fresh_runtime();
1553 rt.execute_query("CREATE TABLE dml_lookup_race (id INT PRIMARY KEY, status TEXT)")
1554 .expect("create fixture");
1555 rt.execute_query("INSERT INTO dml_lookup_race (id, status) VALUES (1, 'old'), (2, 'old')")
1556 .expect("insert fixture");
1557 rt.execute_query("CREATE INDEX dml_lookup_status ON dml_lookup_race (status) USING HASH")
1558 .expect("create index");
1559 let frame = StatementExecutionFrame::build(
1560 &rt,
1561 StatementIdentity::Text("DELETE FROM dml_lookup_race WHERE status = 'old'"),
1562 )
1563 .expect("capture snapshot");
1564 let reddb_rql::ast::QueryExpr::Table(query) =
1565 reddb_rql::modes::parse_multi("SELECT id FROM dml_lookup_race WHERE status = 'old'")
1566 .expect("parse filter")
1567 else {
1568 panic!("expected table query")
1569 };
1570 let filter = reddb_rql::sql_lowering::effective_table_filter(&query).expect("filter");
1571 let (started_tx, started_rx) = mpsc::channel();
1572 let (published_tx, published_rx) = mpsc::channel();
1573 let published_rx = Mutex::new(published_rx);
1574 let first = std::sync::atomic::AtomicBool::new(true);
1575 *rt.index_store_ref().mutation_hook.lock() = Some(Arc::new(move |collection, phase| {
1576 if collection == "dml_lookup_race"
1577 && phase == super::super::index_store::MutationTestPhase::DmlIndexLookupStarted
1578 && first.swap(false, std::sync::atomic::Ordering::SeqCst)
1579 {
1580 started_tx.send(()).expect("notify writer");
1581 published_rx
1582 .lock()
1583 .unwrap()
1584 .recv_timeout(Duration::from_secs(10))
1585 .expect("writer published");
1586 }
1587 }));
1588 std::thread::scope(|scope| {
1589 let writer_rt = &rt;
1590 let writer = scope.spawn(move || {
1591 started_rx
1592 .recv_timeout(Duration::from_secs(10))
1593 .expect("lookup started");
1594 writer_rt
1595 .execute_query("UPDATE dml_lookup_race SET status = 'new' WHERE id = 1")
1596 .expect("publish update");
1597 published_tx.send(()).expect("release lookup");
1598 });
1599 let limits = super::super::resource_limits::ResourceLimits {
1600 max_query_duration: Some(Duration::from_secs(30)),
1601 ..Default::default()
1602 };
1603 let slots = std::sync::atomic::AtomicU64::new(0);
1604 let _budget = super::super::query_budget::Scope::enter(&limits, &slots)
1605 .expect("query deadline scope");
1606 let _guards = frame.install(&rt);
1607 let ids = super::super::dml_target_scan::DmlTargetScan::new(
1608 &rt,
1609 "dml_lookup_race",
1610 Some(&filter),
1611 None,
1612 )
1613 .find_target_ids()
1614 .expect("find DML targets");
1615 writer.join().expect("writer thread");
1616 assert_eq!(
1617 ids.len(),
1618 2,
1619 "racing publication must not omit a snapshot-visible row"
1620 );
1621 });
1622 *rt.index_store_ref().mutation_hook.lock() = None;
1623 }
1624
1625 #[test]
1626 fn replica_replay_preserves_snapshots_across_insert_update_and_delete() {
1627 use crate::replication::cdc::{change_record_from_entity, ChangeOperation};
1628 use crate::replication::logical::{ApplyMode, LogicalChangeApplier};
1629 reset_thread_locals();
1630 let source = fresh_runtime();
1631 let replica = fresh_runtime();
1632 for rt in [&source, &replica] {
1633 rt.execute_query("CREATE TABLE replica_snapshot (id INT, status TEXT)")
1634 .expect("schema");
1635 }
1636 replica
1637 .execute_query(
1638 "CREATE INDEX replica_snapshot_status ON replica_snapshot (status) USING HASH",
1639 )
1640 .expect("index");
1641 let sql = "SELECT id FROM replica_snapshot WHERE status = 'old'";
1642 let capture = || {
1643 StatementExecutionFrame::build(&replica, StatementIdentity::Text(sql)).expect("capture")
1644 };
1645 let read = |frame: &StatementExecutionFrame| {
1646 let reddb_rql::ast::QueryExpr::Table(query) =
1647 reddb_rql::modes::parse_multi(sql).expect("parse")
1648 else {
1649 panic!("table")
1650 };
1651 let _guards = frame.install(&replica);
1652 super::super::query_exec::table::execute_runtime_canonical_table_query_indexed(
1653 &replica.db(),
1654 &query,
1655 Some(replica.index_store_ref()),
1656 )
1657 .expect("snapshot read")
1658 .len()
1659 };
1660 let empty = capture();
1661 source
1662 .execute_query("INSERT INTO replica_snapshot (id, status) VALUES (1, 'old')")
1663 .expect("insert");
1664 let applier = LogicalChangeApplier::new(0);
1665 let replay = |lsn, op| {
1666 let entity = source
1667 .db()
1668 .store()
1669 .get_collection("replica_snapshot")
1670 .unwrap()
1671 .query_all(|entity| entity.xmax == 0)
1672 .pop()
1673 .unwrap();
1674 applier
1675 .apply(
1676 &replica.db(),
1677 &change_record_from_entity(
1678 lsn,
1679 1,
1680 op,
1681 "replica_snapshot",
1682 "table",
1683 &entity,
1684 12,
1685 None,
1686 ),
1687 ApplyMode::Replica,
1688 )
1689 .expect("replay");
1690 };
1691 replay(1, ChangeOperation::Insert);
1692 assert_eq!(
1693 read(&empty),
1694 0,
1695 "replay must not backdate an insert into an earlier snapshot"
1696 );
1697 let inserted = capture();
1698 source
1699 .execute_query("UPDATE replica_snapshot SET status = 'new' WHERE id = 1")
1700 .expect("update");
1701 replay(2, ChangeOperation::Update);
1702 assert_eq!(read(&inserted), 1, "old version remains visible");
1703 assert_eq!(read(&capture()), 0, "fresh snapshot uses the updated index");
1704 let all_sql = "SELECT id FROM replica_snapshot";
1706 let before_delete =
1707 StatementExecutionFrame::build(&replica, StatementIdentity::Text(all_sql)).unwrap();
1708 replay(3, ChangeOperation::Delete);
1709 let reddb_rql::ast::QueryExpr::Table(query) =
1710 reddb_rql::modes::parse_multi(all_sql).unwrap()
1711 else {
1712 panic!("table")
1713 };
1714 {
1715 let _guards = before_delete.install(&replica);
1716 assert_eq!(
1717 super::super::query_exec::table::execute_runtime_canonical_table_query_indexed(
1718 &replica.db(),
1719 &query,
1720 Some(replica.index_store_ref())
1721 )
1722 .unwrap()
1723 .len(),
1724 1
1725 );
1726 }
1727 assert_eq!(
1728 replica.execute_query(all_sql).unwrap().result.records.len(),
1729 0
1730 );
1731 }
1732
1733 #[test]
1734 fn autocommit_index_reads_keep_old_rows_while_another_transaction_is_open() {
1735 use crate::runtime::mvcc::{clear_current_connection_id, set_current_connection_id};
1736 reset_thread_locals();
1737 let rt = fresh_runtime();
1738 let execute = |connection, sql: &str| {
1739 set_current_connection_id(connection);
1740 let result = rt.execute_query(sql);
1741 clear_current_connection_id();
1742 result.expect("statement")
1743 };
1744 execute(1, "CREATE TABLE active_writer_rows (id INT, status TEXT)");
1745 execute(
1746 1,
1747 "INSERT INTO active_writer_rows (id, status) VALUES (1, 'old')",
1748 );
1749 execute(
1750 1,
1751 "CREATE INDEX active_writer_status ON active_writer_rows (status) USING HASH",
1752 );
1753 execute(2, "BEGIN");
1754 execute(
1755 2,
1756 "UPDATE active_writer_rows SET status = 'new' WHERE id = 1",
1757 );
1758 assert_eq!(
1759 execute(1, "SELECT id FROM active_writer_rows WHERE status = 'old'")
1760 .result
1761 .records
1762 .len(),
1763 1
1764 );
1765 assert_eq!(
1766 execute(1, "SELECT id FROM active_writer_rows WHERE status = 'new'")
1767 .result
1768 .records
1769 .len(),
1770 0
1771 );
1772 execute(2, "ROLLBACK");
1773 assert_eq!(
1774 execute(1, "SELECT id FROM active_writer_rows WHERE status = 'old'")
1775 .result
1776 .records
1777 .len(),
1778 1
1779 );
1780 }
1781
1782 #[test]
1783 fn frame_captures_identity_and_scope() {
1784 reset_thread_locals();
1785 set_current_tenant("acme".to_string());
1786 set_current_auth_identity("alice".to_string(), Role::Write);
1787
1788 let rt = fresh_runtime();
1789 let frame = StatementExecutionFrame::build(&rt, StatementIdentity::Text("SELECT 1"))
1790 .expect("frame builds");
1791 let f: &dyn ReadFrame = &frame;
1792
1793 assert_eq!(f.effective_scope(), Some("acme"));
1794 let id = f.identity().expect("identity captured");
1795 assert_eq!(id.0, "alice");
1796 assert!(matches!(id.1, Role::Write));
1797
1798 assert!(
1801 f.cache_key().contains("acme") && f.cache_key().contains("alice"),
1802 "cache key folds in scope + identity, got {:?}",
1803 f.cache_key()
1804 );
1805
1806 reset_thread_locals();
1807 }
1808
1809 #[test]
1810 fn as_of_rejects_non_versioned_user_collection() {
1811 reset_thread_locals();
1812 let rt = fresh_runtime();
1813
1814 let err = match StatementExecutionFrame::build(
1818 &rt,
1819 StatementIdentity::Text("SELECT * FROM not_versioned AS OF COMMIT 'deadbeef'"),
1820 ) {
1821 Err(e) => e,
1822 Ok(_) => panic!("AS OF on non-versioned user collection rejected"),
1823 };
1824
1825 let msg = format!("{err}");
1826 assert!(
1827 msg.contains("AS OF requires a versioned collection"),
1828 "expected AS OF rejection, got: {msg}"
1829 );
1830 }
1831
1832 #[test]
1844 fn select_path_routes_through_frame_cache_key() {
1845 reset_thread_locals();
1846 set_current_tenant("acme".to_string());
1847 set_current_auth_identity("alice".to_string(), Role::Read);
1848
1849 let rt = fresh_runtime();
1850 let result = rt
1851 .execute_query("SELECT 1")
1852 .expect("SELECT 1 executes under tenant=acme/identity=alice");
1853 assert_eq!(result.statement_type, "select");
1854
1855 let cache = rt.inner.result_cache.read();
1861 let any_keyed_with_scope = cache
1862 .0
1863 .keys()
1864 .any(|k| k.contains("acme") && k.contains("alice"));
1865 assert!(
1866 any_keyed_with_scope,
1867 "expected at least one result-cache key carrying tenant+identity, \
1868 got keys: {:?}",
1869 cache.0.keys().collect::<Vec<_>>()
1870 );
1871
1872 reset_thread_locals();
1873 }
1874
1875 #[test]
1887 fn volatile_select_does_not_populate_result_cache() {
1888 reset_thread_locals();
1889 let rt = fresh_runtime();
1890
1891 let frame = StatementExecutionFrame::build(
1895 &rt,
1896 StatementIdentity::Text("SELECT pg_advisory_unlock(1)"),
1897 )
1898 .expect("frame");
1899 let f: &dyn ReadFrame = &frame;
1900 assert!(
1901 !f.should_cache_result(),
1902 "volatile builtin must disable result-cache safety"
1903 );
1904
1905 let _ = rt
1910 .execute_query("SELECT pg_advisory_unlock(1)")
1911 .expect("volatile SELECT executes");
1912 let cache = rt.inner.result_cache.read();
1913 let key = result_cache_key("SELECT pg_advisory_unlock(1)");
1914 assert!(
1915 !cache.0.contains_key(&key),
1916 "volatile SELECT must not populate result cache, found key {key:?} in {:?}",
1917 cache.0.keys().collect::<Vec<_>>()
1918 );
1919
1920 reset_thread_locals();
1921 }
1922
1923 #[test]
1924 fn blob_cache_backend_populates_blob_path_without_legacy_write() {
1925 reset_thread_locals();
1926 let rt = fresh_runtime();
1927 rt.inner
1928 .db
1929 .store()
1930 .set_config_tree("runtime.result_cache.backend", &crate::json!("blob_cache"));
1931
1932 let result = rt.execute_query("SELECT 1").expect("SELECT 1 executes");
1933 assert_eq!(result.statement_type, "select");
1934
1935 let key = result_cache_key("SELECT 1");
1936 assert!(
1937 rt.inner
1938 .result_blob_cache
1939 .get("runtime.result_cache", &key)
1940 .is_some(),
1941 "blob backend should stamp the Blob Cache path"
1942 );
1943 assert!(rt.inner.result_blob_entries.read().0.contains_key(&key));
1944 assert!(
1945 !rt.inner.result_cache.read().0.contains_key(&key),
1946 "blob backend should not write the legacy map"
1947 );
1948 }
1949
1950 #[test]
1951 fn blob_cache_backend_keeps_volatile_select_out_of_blob_path() {
1952 reset_thread_locals();
1953 let rt = fresh_runtime();
1954 rt.inner
1955 .db
1956 .store()
1957 .set_config_tree("runtime.result_cache.backend", &crate::json!("blob_cache"));
1958
1959 let _ = rt
1960 .execute_query("SELECT pg_advisory_unlock(1)")
1961 .expect("volatile SELECT executes");
1962 let key = result_cache_key("SELECT pg_advisory_unlock(1)");
1963 assert!(
1964 rt.inner
1965 .result_blob_cache
1966 .get("runtime.result_cache", &key)
1967 .is_none(),
1968 "volatile SELECT must not populate blob result cache"
1969 );
1970 assert!(!rt.inner.result_blob_entries.read().0.contains_key(&key));
1971 }
1972
1973 #[test]
1974 fn shadow_backend_dual_writes_and_reports_no_divergence_on_equal_results() {
1975 reset_thread_locals();
1976 let rt = fresh_runtime();
1977 rt.inner
1978 .db
1979 .store()
1980 .set_config_tree("runtime.result_cache.backend", &crate::json!("shadow"));
1981
1982 let first = rt.execute_query("SELECT 1").expect("first SELECT");
1983 let second = rt.execute_query("SELECT 1").expect("cached SELECT");
1984 assert_eq!(first.result.len(), second.result.len());
1985
1986 let key = result_cache_key("SELECT 1");
1987 assert!(rt.inner.result_cache.read().0.contains_key(&key));
1988 assert!(rt.inner.result_blob_entries.read().0.contains_key(&key));
1989 assert_eq!(rt.result_cache_shadow_divergences(), 0);
1990 assert_eq!(
1991 crate::runtime::METRIC_CACHE_SHADOW_DIVERGENCE_TOTAL,
1992 "cache_shadow_divergence_total"
1993 );
1994 }
1995
1996 #[test]
1997 fn as_of_on_red_collection_records_floor() {
1998 reset_thread_locals();
1999 let rt = fresh_runtime();
2000
2001 let frame = StatementExecutionFrame::build(
2004 &rt,
2005 StatementIdentity::Text("SELECT * FROM red_commits AS OF SNAPSHOT 1"),
2006 )
2007 .expect("AS OF SNAPSHOT 1 on red_commits resolves");
2008
2009 let f: &dyn ReadFrame = &frame;
2010 assert_eq!(
2011 f.as_of_floor(),
2012 Some(1),
2013 "AS OF SNAPSHOT 1 records xid=1 as the floor"
2014 );
2015 assert_eq!(f.snapshot().xid, 1);
2016 assert!(
2017 f.snapshot().in_progress.is_empty(),
2018 "historical reads have no in-progress set"
2019 );
2020 }
2021
2022 #[test]
2028 fn frame_classifies_privilege_and_lock_intent_from_prefix() {
2029 reset_thread_locals();
2030 let rt = fresh_runtime();
2031
2032 let cases = [
2033 ("SELECT 1", Privilege::Read, LockIntent::Shared),
2034 ("LIST KV settings", Privilege::Read, LockIntent::Shared),
2035 (
2036 "KV GET settings.feature",
2037 Privilege::Read,
2038 LockIntent::Shared,
2039 ),
2040 ("VAULT LIST secrets", Privilege::Read, LockIntent::Shared),
2041 (
2042 "INSERT INTO t (id) VALUES (1)",
2043 Privilege::Write,
2044 LockIntent::Exclusive,
2045 ),
2046 (
2047 "KV PUT settings.feature = 'on'",
2048 Privilege::Write,
2049 LockIntent::Exclusive,
2050 ),
2051 (
2052 "VAULT PUT secrets.api = 'x'",
2053 Privilege::Write,
2054 LockIntent::Exclusive,
2055 ),
2056 (
2057 "UPDATE t SET x = 1 WHERE id = 1",
2058 Privilege::Write,
2059 LockIntent::Exclusive,
2060 ),
2061 (
2062 "DELETE FROM t WHERE id = 1",
2063 Privilege::Write,
2064 LockIntent::Exclusive,
2065 ),
2066 (
2067 "CREATE TABLE foo (id INT)",
2068 Privilege::Write,
2069 LockIntent::Exclusive,
2070 ),
2071 ("BEGIN", Privilege::None, LockIntent::None),
2072 ("COMMIT", Privilege::None, LockIntent::None),
2073 ("SET timezone = 'UTC'", Privilege::None, LockIntent::None),
2074 ];
2075
2076 for (q, want_priv, want_lock) in cases {
2077 let frame = StatementExecutionFrame::build(&rt, StatementIdentity::Text(q))
2078 .unwrap_or_else(|e| panic!("frame builds for {q:?}: {e}"));
2079 let f: &dyn ReadFrame = &frame;
2080 assert_eq!(f.required_privilege(), want_priv, "privilege for {q:?}");
2081 assert_eq!(f.lock_intent(), want_lock, "lock intent for {q:?}");
2082 }
2083 }
2084
2085 #[test]
2102 fn insert_under_read_role_denied_via_frame_privilege() {
2103 reset_thread_locals();
2104 set_current_auth_identity("alice".to_string(), Role::Read);
2105
2106 let rt = fresh_runtime();
2107 let frame = StatementExecutionFrame::build(
2113 &rt,
2114 StatementIdentity::Text("INSERT INTO t (id) VALUES (1)"),
2115 )
2116 .expect("frame builds for INSERT");
2117 let f: &dyn ReadFrame = &frame;
2118 assert_eq!(
2119 f.required_privilege(),
2120 Privilege::Write,
2121 "INSERT classified as Write"
2122 );
2123 let id = f.identity().expect("identity captured");
2124 assert!(
2125 !f.required_privilege().is_satisfied_by(id.1),
2126 "Role::Read does not satisfy Privilege::Write — frame must deny"
2127 );
2128
2129 use reddb_rql::ast::{QueryExpr, TableQuery};
2135 let expr = QueryExpr::Table(TableQuery::new("t"));
2136 let err = frame
2137 .check_query_privilege(&rt, &expr)
2138 .expect_err("denied via frame's coarse privilege gate");
2139 let msg = format!("{err}");
2140 assert!(
2141 msg.contains("permission denied") && msg.contains("Write"),
2142 "expected frame-level Write deny, got: {msg}"
2143 );
2144
2145 reset_thread_locals();
2146 }
2147
2148 #[test]
2161 fn large_select_through_frame_arena_returns_all_rows_in_order() {
2162 reset_thread_locals();
2163 let rt = fresh_runtime();
2164 rt.execute_query("CREATE TABLE big (id INT)")
2165 .expect("create table");
2166
2167 const N: usize = 2_500;
2170 for start in (0..N).step_by(250) {
2171 let end = (start + 250).min(N);
2172 let values = (start..end)
2173 .map(|i| format!("({i})"))
2174 .collect::<Vec<_>>()
2175 .join(", ");
2176 rt.execute_query(&format!("INSERT INTO big (id) VALUES {values}"))
2177 .unwrap_or_else(|err| panic!("insert rows {start}..{end}: {err:?}"));
2178 }
2179
2180 let result = rt
2181 .execute_query("SELECT id FROM big ORDER BY id")
2182 .expect("large SELECT executes through the frame arena path");
2183 assert_eq!(result.statement_type, "select");
2184 assert_eq!(
2185 result.result.records.len(),
2186 N,
2187 "every inserted row streams back through the arena-backed channel"
2188 );
2189 for (i, record) in result.result.records.iter().enumerate() {
2190 assert_eq!(
2191 record.get("id"),
2192 Some(&reddb_types::Value::Integer(i as i64)),
2193 "row {i} is byte-identical to the per-request-allocation baseline"
2194 );
2195 }
2196
2197 reset_thread_locals();
2198 }
2199
2200 #[test]
2214 fn parameterized_transport_adapters_delegate_binding_to_runtime() {
2215 let manifest_dir = std::path::Path::new(env!("CARGO_MANIFEST_DIR"));
2216 let adapters = [
2217 ("src/server/handlers_query.rs", "QueryRequestExecutor"),
2218 ("src/rpc_stdio.rs", "QueryRequestExecutor"),
2219 ("src/wire/redwire/session.rs", "QueryRequestExecutor"),
2220 ("src/wire/postgres/server.rs", "QueryRequestExecutor"),
2221 ("src/grpc.rs", "QueryRequestExecutor"),
2222 ];
2223
2224 for (relative, delegation) in adapters {
2225 let path = manifest_dir.join(relative);
2226 let text = std::fs::read_to_string(&path)
2227 .unwrap_or_else(|err| panic!("read {}: {err}", path.display()));
2228 assert!(
2229 text.contains(delegation),
2230 "{relative} should delegate parameterized query execution through {delegation}"
2231 );
2232 assert!(
2233 !text.contains("user_params::bind"),
2234 "{relative} must not bind SQL params in the transport adapter"
2235 );
2236 }
2237
2238 let seam = std::fs::read_to_string(manifest_dir.join("src/runtime/query_request.rs"))
2241 .expect("read src/runtime/query_request.rs");
2242 assert!(
2243 seam.contains("execute_query_with_params"),
2244 "the Request module must execute text SQL through the runtime entrypoint"
2245 );
2246 }
2247
2248 #[test]
2258 fn control_statement_skips_intent_locks_via_frame() {
2259 reset_thread_locals();
2260 let rt = fresh_runtime();
2261
2262 let frame = StatementExecutionFrame::build(&rt, StatementIdentity::Text("BEGIN"))
2263 .expect("frame builds for BEGIN");
2264 let f: &dyn ReadFrame = &frame;
2265 assert_eq!(f.lock_intent(), LockIntent::None);
2266
2267 use reddb_rql::ast::{QueryExpr, TableQuery};
2272 let expr = QueryExpr::Table(TableQuery::new("t"));
2273 let guard = frame
2274 .acquire_intent_locks(&rt, &expr)
2275 .expect("no lock is attempted");
2276 assert!(
2277 guard.is_none(),
2278 "BEGIN frame's lock_intent=None must short-circuit lock acquisition"
2279 );
2280 }
2281
2282 #[test]
2289 fn nested_frame_inherits_outer_statement_snapshot() {
2290 reset_thread_locals();
2291 let rt = fresh_runtime();
2292 rt.execute_query("CREATE TABLE nested_snap (id INT)")
2293 .expect("create table");
2294
2295 let outer = StatementExecutionFrame::build(
2296 &rt,
2297 StatementIdentity::Text("SELECT * FROM nested_snap"),
2298 )
2299 .expect("outer frame builds");
2300 let _outer_guards = outer.install(&rt);
2301
2302 rt.execute_query("INSERT INTO nested_snap (id) VALUES (1)")
2305 .expect("concurrent insert");
2306
2307 let expr = QueryExpr::Table(reddb_rql::ast::TableQuery::new("nested_snap"));
2308 let nested = StatementExecutionFrame::build(&rt, StatementIdentity::Expr(&expr))
2309 .expect("nested frame builds");
2310
2311 assert_eq!(
2312 ReadFrame::snapshot(&nested).xid,
2313 ReadFrame::snapshot(&outer).xid,
2314 "nested execution must inherit the outer statement's snapshot"
2315 );
2316 }
2317
2318 #[test]
2331 fn nested_text_statement_sees_commits_made_since_the_outer_frame() {
2332 reset_thread_locals();
2333 let rt = fresh_runtime();
2334 rt.execute_query("CREATE TABLE nested_text_snap (id INT)")
2335 .expect("create table");
2336
2337 let outer = StatementExecutionFrame::build(
2338 &rt,
2339 StatementIdentity::Text("SELECT * FROM nested_text_snap"),
2340 )
2341 .expect("outer frame builds");
2342 let _outer_guards = outer.install(&rt);
2343
2344 rt.execute_query("INSERT INTO nested_text_snap (id) VALUES (1)")
2345 .expect("concurrent insert");
2346
2347 let nested = StatementExecutionFrame::build(
2348 &rt,
2349 StatementIdentity::Text("SELECT * FROM nested_text_snap"),
2350 )
2351 .expect("nested frame builds");
2352
2353 assert_ne!(
2354 ReadFrame::snapshot(&nested).xid,
2355 ReadFrame::snapshot(&outer).xid,
2356 "a nested text statement must mint a fresh snapshot, not inherit \
2357 the enclosing statement's frozen view"
2358 );
2359 }
2360
2361 #[test]
2366 fn expr_frame_rejects_as_of_instead_of_reading_live() {
2367 reset_thread_locals();
2368 let rt = fresh_runtime();
2369
2370 let mut table = reddb_rql::ast::TableQuery::new("red_commits");
2371 table.as_of = Some(reddb_rql::ast::AsOfClause::Snapshot(1));
2372 let expr = QueryExpr::Table(table);
2373
2374 let err = match StatementExecutionFrame::build(&rt, StatementIdentity::Expr(&expr)) {
2375 Err(err) => err,
2376 Ok(_) => panic!("AS OF without statement text must be refused, not read live"),
2377 };
2378 let msg = format!("{err}");
2379 assert!(
2380 msg.contains("AS OF cannot be resolved from a pre-parsed expression"),
2381 "expected a loud AS OF refusal, got: {msg}"
2382 );
2383 }
2384
2385 #[test]
2389 fn expr_frame_opts_out_of_the_result_cache() {
2390 reset_thread_locals();
2391 let rt = fresh_runtime();
2392
2393 let expr = QueryExpr::Table(reddb_rql::ast::TableQuery::new("t"));
2394 let frame = StatementExecutionFrame::build(&rt, StatementIdentity::Expr(&expr))
2395 .expect("expr frame builds");
2396
2397 assert!(frame.cache_key().is_empty(), "expr frame has no cache key");
2398 assert!(
2399 !frame.can_read_result_cache(),
2400 "expr frame must never serve from the result cache"
2401 );
2402 }
2403}