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 b"SCRUB" => "uncached",
247 _ => "unknown",
248 }
249}
250
251fn statement_uses_result_cache(query: &str) -> bool {
252 !matches!(
253 statement_kind(query),
254 "write" | "ddl" | "admin" | "control" | "uncached"
255 ) && !super::red_schema::is_computed_stats_query(query)
256}
257
258fn reject_unresolvable_as_of(expr: &QueryExpr) -> RedDBResult<()> {
270 let QueryExpr::Table(table) = expr else {
271 return Ok(());
272 };
273 if table.as_of.is_none() {
274 return Ok(());
275 }
276 Err(RedDBError::InvalidOperation(
277 "AS OF cannot be resolved from a pre-parsed expression — \
278 execute the statement text (execute_query / execute_prepared_query / \
279 execute_query_with_params) so the historical snapshot is resolved"
280 .to_string(),
281 ))
282}
283
284fn classify_privilege(query: &str) -> Privilege {
285 match statement_kind(query) {
286 "read" => Privilege::Read,
287 "write" => Privilege::Write,
288 "ddl" => Privilege::Write,
294 "admin" => Privilege::Admin,
295 _ => Privilege::None,
296 }
297}
298
299fn classify_lock_intent(query: &str) -> LockIntent {
300 match statement_kind(query) {
301 "read" => LockIntent::Shared,
302 "write" | "ddl" => LockIntent::Exclusive,
303 _ => LockIntent::None,
304 }
305}
306
307pub(super) struct StatementExecutionFrame {
308 tx_local_tenant: Option<Option<String>>,
309 snapshot: Snapshot,
310 snapshot_retention: RefCell<Option<SnapshotRetentionGuard>>,
311 own_xids: HashSet<Xid>,
312 serializable_reader: Option<Xid>,
313 cache_key: String,
314 cache_data_epoch: Option<u64>,
315 cache_revision: u64,
316 is_volatile_query: bool,
317 cache_safe: bool,
318 effective_scope: Option<String>,
323 identity: Option<(String, Role)>,
326 as_of_floor: Option<Xid>,
329 requires_index_fallback: bool,
332 required_privilege: Privilege,
336 lock_intent: LockIntent,
339 visible_collections: Option<HashSet<String>>,
346 row_arena: Rc<RefCell<super::query_exec::RowBufferArena>>,
354}
355
356pub(super) struct StatementFrameGuards {
357 _tx_local_guard: TxLocalTenantGuard,
358 _config_snapshot_guard: ConfigSnapshotGuard,
359 _secret_store_guard: SecretStoreGuard,
360 _kv_store_guard: KvStoreGuard,
361 _snapshot_guard: CurrentSnapshotGuard,
362}
363
364pub(super) struct PreparedStatement {
365 pub(super) expr: QueryExpr,
366 pub(super) mode: QueryMode,
367}
368
369#[derive(Clone, Copy)]
378pub(super) enum StatementIdentity<'a> {
379 Text(&'a str),
381 UncachedText(&'a str),
384 Expr(&'a QueryExpr),
396}
397
398impl<'a> StatementIdentity<'a> {
399 fn text(&self) -> &'a str {
403 match self {
404 Self::Text(query) | Self::UncachedText(query) => query,
405 Self::Expr(_) => "",
406 }
407 }
408}
409
410impl StatementExecutionFrame {
411 pub(super) fn build(
412 runtime: &RedDBRuntime,
413 identity_source: StatementIdentity<'_>,
414 ) -> RedDBResult<Self> {
415 let query = identity_source.text();
416 let uses_result_cache = matches!(identity_source, StatementIdentity::Text(query) if statement_uses_result_cache(query));
417 let (cache_data_epoch, cache_revision) = match identity_source {
418 StatementIdentity::Text(_) if uses_result_cache => runtime.result_cache_snapshot(),
419 StatementIdentity::Text(_)
420 | StatementIdentity::UncachedText(_)
421 | StatementIdentity::Expr(_) => (None, 0),
422 };
423 if let StatementIdentity::Expr(expr) = identity_source {
424 reject_unresolvable_as_of(expr)?;
425 }
426 let conn_id = current_connection_id();
427 let tx_local_tenant = runtime.inner.transaction_state.local_tenant(conn_id);
428 let tx_context = runtime.inner.transaction_state.context(conn_id);
429 let own_xids = own_transaction_xids(tx_context.as_ref());
430 let serializable_reader = tx_context
431 .as_ref()
432 .filter(|ctx| {
433 ctx.isolation == crate::storage::transaction::IsolationLevel::Serializable
434 })
435 .map(|ctx| ctx.xid);
436 let (snapshot, as_of_floor, snapshot_retention) =
437 runtime.statement_snapshot(query, identity_source)?;
438 let requires_index_fallback =
439 as_of_floor.is_some() || tx_context.is_some() || !snapshot.in_progress.is_empty();
440 let (cache_key, is_volatile_query) = match identity_source {
441 StatementIdentity::Text(_) if uses_result_cache => (
442 result_cache_key(query),
443 query_has_volatile_builtin(query) || query_is_ask_statement(query),
444 ),
445 StatementIdentity::Text(_)
450 | StatementIdentity::UncachedText(_)
451 | StatementIdentity::Expr(_) => (String::new(), true),
452 };
453 let cache_safe = runtime.result_cache_safe(conn_id);
454 let effective_scope = current_tenant();
459 let identity = current_auth_identity();
460
461 let required_privilege = classify_privilege(query);
469 let lock_intent = classify_lock_intent(query);
470
471 let visible_collections = match (runtime.inner.auth_store.read().clone(), identity.as_ref())
477 {
478 (Some(store), Some((principal, role))) => {
479 let collections = runtime.inner.db.store().list_collections();
480 Some(store.visible_collections_for_scope(
481 effective_scope.as_deref(),
482 *role,
483 principal,
484 &collections,
485 ))
486 }
487 _ => None,
488 };
489
490 Ok(Self {
491 tx_local_tenant,
492 snapshot,
493 snapshot_retention: RefCell::new(Some(snapshot_retention)),
494 own_xids,
495 serializable_reader,
496 cache_key,
497 cache_data_epoch,
498 cache_revision,
499 is_volatile_query,
500 cache_safe,
501 effective_scope,
502 identity,
503 as_of_floor,
504 requires_index_fallback,
505 required_privilege,
506 lock_intent,
507 visible_collections,
508 row_arena: Rc::new(RefCell::new(super::query_exec::RowBufferArena::new())),
509 })
510 }
511
512 pub(super) fn row_arena(&self) -> Rc<RefCell<super::query_exec::RowBufferArena>> {
517 Rc::clone(&self.row_arena)
518 }
519
520 pub(super) fn install(&self, runtime: &RedDBRuntime) -> StatementFrameGuards {
521 StatementFrameGuards {
522 _tx_local_guard: TxLocalTenantGuard::install(self.tx_local_tenant.clone()),
523 _config_snapshot_guard: ConfigSnapshotGuard::install(
524 Arc::clone(&runtime.inner.db),
525 runtime.inner.auth_store.read().clone(),
526 ),
527 _secret_store_guard: SecretStoreGuard::install(runtime),
528 _kv_store_guard: KvStoreGuard::install(runtime.inner.auth_store.read().clone()),
529 _snapshot_guard: CurrentSnapshotGuard::install(SnapshotContext {
530 snapshot: self.snapshot.clone(),
531 manager: runtime.inner.transaction_state.snapshot_manager(),
532 own_xids: self.own_xids.clone(),
533 requires_index_fallback: self.requires_index_fallback,
534 serializable_reader: self.serializable_reader,
535 }),
536 }
537 }
538
539 pub(super) fn cache_key(&self) -> &str {
540 &self.cache_key
541 }
542
543 pub(super) fn can_read_result_cache(&self) -> bool {
544 <Self as ReadFrame>::should_cache_result(self)
547 }
548
549 pub(super) fn should_write_result_cache(&self, result: &RuntimeQueryResult) -> bool {
550 <Self as ReadFrame>::should_cache_result(self)
555 && result.statement_type == "select"
556 && result.engine != "vault"
557 && result.engine != "runtime-rank"
558 && result.statement != "queue_group_read"
563 && result.engine != "runtime-scrub"
568 && result.result.pre_serialized_json.is_none()
569 && (is_graph_tvf_engine(result.engine) || result.result.records.len() <= 5)
573 }
574
575 pub(super) fn read_result_cache(&self, runtime: &RedDBRuntime) -> Option<RuntimeQueryResult> {
576 if runtime.resource_limits().read_execution_limits_enabled() {
577 return None;
578 }
579 if self.can_read_result_cache() {
580 runtime
581 .get_result_cache_entry(self.cache_key(), self.cache_data_epoch)
582 .map(|mut result| {
583 if let Some(vector) = result.result.stats.vector.as_mut() {
584 vector.cache_hit = true;
585 }
586 result
587 })
588 } else {
589 None
590 }
591 }
592
593 pub(super) fn write_result_cache(
594 &self,
595 runtime: &RedDBRuntime,
596 result: &RuntimeQueryResult,
597 scopes: HashSet<String>,
598 ) {
599 if self.should_write_result_cache(result) {
600 runtime.put_result_cache_entry(
601 self.cache_key(),
602 RuntimeResultCacheEntry {
603 result: result.clone(),
604 cached_at: std::time::Instant::now(),
605 scopes,
606 data_epoch: self.cache_data_epoch.unwrap_or(0),
607 },
608 self.cache_data_epoch,
609 self.cache_revision,
610 );
611 }
612 }
613
614 pub(super) fn prepare_cte(&self, query: &str) -> RedDBResult<Option<QueryExpr>> {
615 if !has_with_prefix(query) {
624 return Ok(None);
625 }
626 let parsed =
627 reddb_rql::parser::parse(query).map_err(|err| RedDBError::Query(err.to_string()))?;
628 if parsed.with_clause.is_some() {
629 let rewritten = crate::storage::query::executors::inline_ctes(parsed)
630 .map_err(|err| RedDBError::Query(err.to_string()))?;
631 return Ok(Some(rewritten));
632 }
633 Ok(None)
637 }
638
639 pub(super) fn prepare_statement(
640 &self,
641 runtime: &RedDBRuntime,
642 query: &str,
643 ) -> RedDBResult<PreparedStatement> {
644 let mode = detect_mode(query);
645 if matches!(mode, QueryMode::Unknown) {
646 return Err(RedDBError::Query("unable to detect query mode".to_string()));
647 }
648
649 let first_word = query
660 .trim()
661 .split_ascii_whitespace()
662 .next()
663 .unwrap_or("")
664 .to_ascii_uppercase();
665 let is_insert = first_word == "INSERT";
666 let bypass_plan_cache = is_insert || self.is_volatile_query;
672
673 let (cache_key, prescan_binds) = if bypass_plan_cache {
679 (String::new(), Vec::new())
680 } else {
681 crate::storage::query::planner::cache_key::normalize_and_extract(query)
682 };
683
684 let expr = if bypass_plan_cache {
685 parse_multi(query).map_err(|err| RedDBError::Query(err.to_string()))?
687 } else {
688 let hit = {
695 let plan_cache = runtime.inner.query_cache.read();
696 plan_cache.peek(&cache_key).map(|cached| {
697 let parameter_count = cached.parameter_count;
698 let optimized = cached.plan.optimized.clone();
699 let exact_query = cached.exact_query.clone();
700 (parameter_count, optimized, exact_query)
701 })
702 };
703
704 if let Some((parameter_count, optimized, exact_query)) = hit {
705 if parameter_count > 0 {
706 let shape_binds = prescan_binds.clone();
708 if let Some(bound) =
709 crate::storage::query::planner::shape::bind_parameterized_query(
710 &optimized,
711 &shape_binds,
712 parameter_count,
713 )
714 {
715 bound
716 } else {
717 parse_multi(query).map_err(|err| RedDBError::Query(err.to_string()))?
720 }
721 } else {
722 if exact_query.as_deref() == Some(query) {
728 optimized
729 } else {
730 parse_multi(query).map_err(|err| RedDBError::Query(err.to_string()))?
731 }
732 }
733 } else {
734 let parsed =
736 parse_multi(query).map_err(|err| RedDBError::Query(err.to_string()))?;
737 let (cached_expr, parameter_count) = if let Some(prepared) =
738 crate::storage::query::planner::shape::parameterize_query_expr(&parsed)
739 {
740 (prepared.shape, prepared.parameter_count)
741 } else {
742 (parsed.clone(), 0)
743 };
744 {
745 let mut pc = runtime.inner.query_cache.write();
746 let plan = crate::storage::query::planner::QueryPlan::new(
747 parsed.clone(),
748 cached_expr,
749 Default::default(),
750 );
751 pc.insert(
752 cache_key.clone(),
753 crate::storage::query::planner::CachedPlan::new(plan)
754 .with_shape_key(cache_key.clone())
755 .with_exact_query(query.to_string())
756 .with_parameter_count(parameter_count),
757 );
758 }
759 parsed
760 }
761 };
762
763 let expr = runtime.rewrite_view_refs(expr);
768
769 Ok(PreparedStatement { expr, mode })
770 }
771
772 pub(super) fn check_query_privilege(
773 &self,
774 runtime: &RedDBRuntime,
775 expr: &QueryExpr,
776 ) -> RedDBResult<()> {
777 if let Some((username, role)) = <Self as ReadFrame>::identity(self) {
787 let needed = <Self as ReadFrame>::required_privilege(self);
788 if !needed.is_satisfied_by(role) {
789 if matches!(needed, Privilege::Admin) && runtime.inner.auth_store.read().is_none() {
798 crate::telemetry::operator_event::OperatorEvent::AuthBypass {
799 principal: username.to_string(),
800 resource: format!("statement requiring {needed:?}"),
801 detail: format!(
802 "auth_store not wired; coarse gate is sole defence (role={role:?})"
803 ),
804 }
805 .emit_global();
806 }
807 return Err(RedDBError::Query(format!(
808 "permission denied: principal=`{username}` role=`{role:?}` lacks {needed:?} privilege"
809 )));
810 }
811 }
812 runtime
813 .check_query_privilege(expr)
814 .map_err(|err| RedDBError::Query(format!("permission denied: {err}")))
815 }
816
817 pub(super) fn prepare_dispatch(
818 &self,
819 runtime: &RedDBRuntime,
820 expr: &QueryExpr,
821 ) -> RedDBResult<Option<crate::runtime::locking::LockerGuard>> {
822 runtime.validate_model_operations_before_auth(expr)?;
823 self.check_query_privilege(runtime, expr)?;
824 self.release_snapshot_for_maintenance(expr);
825 self.acquire_intent_locks(runtime, expr)
826 }
827
828 pub(super) fn release_snapshot_for_maintenance(&self, expr: &QueryExpr) {
829 if matches!(
830 expr,
831 QueryExpr::MaintenanceCommand(reddb_rql::ast::MaintenanceCommand::Vacuum { .. })
832 ) {
833 self.snapshot_retention.borrow_mut().take();
838 }
839 }
840
841 pub(super) fn acquire_intent_locks(
842 &self,
843 runtime: &RedDBRuntime,
844 expr: &QueryExpr,
845 ) -> RedDBResult<Option<crate::runtime::locking::LockerGuard>> {
846 use crate::runtime::lock_manager::LockMode;
847 use crate::runtime::locking::{LockerGuard, Resource};
848
849 if !runtime.config_bool("concurrency.locking.enabled", true) {
850 return Ok(None);
851 }
852 if matches!(<Self as ReadFrame>::lock_intent(self), LockIntent::None) {
857 return Ok(None);
858 }
859 let Some((global_mode, coll_mode)) = intent_lock_modes_for(expr) else {
860 return Ok(None);
861 };
862 let acquire = |guard: &mut LockerGuard, resource: Resource, mode: LockMode| {
865 guard.acquire(resource.clone(), mode).map_err(|err| {
866 RedDBError::Query(format!(
867 "could not acquire {mode:?} lock on {resource:?}: {err}; retry the statement"
868 ))
869 })
870 };
871 let mut guard = LockerGuard::new(runtime.inner.lock_manager.clone());
872 acquire(&mut guard, Resource::Global, global_mode)?;
873 if coll_mode == LockMode::Exclusive {
878 acquire(&mut guard, Resource::Catalog, LockMode::Exclusive)?;
879 }
880 for collection in collections_referenced(expr) {
881 acquire(&mut guard, Resource::Collection(collection), coll_mode)?;
882 }
883 Ok(Some(guard))
884 }
885}
886
887impl ReadFrame for StatementExecutionFrame {
888 fn snapshot_context(&self, runtime: &RedDBRuntime) -> SnapshotContext {
889 SnapshotContext {
890 snapshot: self.snapshot.clone(),
891 manager: runtime.inner.transaction_state.snapshot_manager(),
892 own_xids: self.own_xids.clone(),
893 requires_index_fallback: self.requires_index_fallback,
894 serializable_reader: self.serializable_reader,
895 }
896 }
897
898 fn effective_scope(&self) -> Option<&str> {
899 self.effective_scope.as_deref()
900 }
901
902 fn identity(&self) -> Option<(&str, Role)> {
903 self.identity.as_ref().map(|(u, r)| (u.as_str(), *r))
904 }
905
906 fn snapshot(&self) -> &Snapshot {
907 &self.snapshot
908 }
909
910 fn as_of_floor(&self) -> Option<Xid> {
911 self.as_of_floor
912 }
913
914 fn cache_key(&self) -> &str {
915 &self.cache_key
916 }
917
918 fn should_cache_result(&self) -> bool {
919 !self.is_volatile_query && self.cache_safe
920 }
921
922 fn required_privilege(&self) -> Privilege {
923 self.required_privilege
924 }
925
926 fn lock_intent(&self) -> LockIntent {
927 self.lock_intent
928 }
929
930 fn visible_collections(&self) -> Option<&HashSet<String>> {
931 self.visible_collections.as_ref()
932 }
933}
934
935pub struct EffectiveScope {
949 pub(crate) captured_snapshot: Option<SnapshotContext>,
950 pub(crate) tenant: Option<String>,
951 pub(crate) identity: Option<(String, Role)>,
952 pub(crate) auth_principal: Option<crate::auth::UserId>,
953 pub(crate) snapshot: Snapshot,
954 pub(crate) visible_collections: Option<HashSet<String>>,
955}
956
957impl EffectiveScope {
958 pub fn has_capability(&self, _capability: &str) -> bool {
968 false
969 }
970}
971
972impl ReadFrame for EffectiveScope {
973 fn snapshot_context(&self, runtime: &RedDBRuntime) -> SnapshotContext {
974 self.captured_snapshot
975 .clone()
976 .unwrap_or_else(|| SnapshotContext {
977 snapshot: self.snapshot.clone(),
978 manager: runtime.inner.transaction_state.snapshot_manager(),
979 own_xids: HashSet::new(),
980 requires_index_fallback: true,
981 serializable_reader: None,
982 })
983 }
984
985 fn effective_scope(&self) -> Option<&str> {
986 self.tenant.as_deref()
987 }
988 fn identity(&self) -> Option<(&str, Role)> {
989 self.identity.as_ref().map(|(u, r)| (u.as_str(), *r))
990 }
991 fn snapshot(&self) -> &Snapshot {
992 &self.snapshot
993 }
994 fn as_of_floor(&self) -> Option<Xid> {
995 None
996 }
997 fn cache_key(&self) -> &str {
998 ""
999 }
1000 fn should_cache_result(&self) -> bool {
1001 false
1002 }
1003 fn required_privilege(&self) -> Privilege {
1004 Privilege::Read
1005 }
1006 fn lock_intent(&self) -> LockIntent {
1007 LockIntent::Shared
1008 }
1009 fn visible_collections(&self) -> Option<&HashSet<String>> {
1010 self.visible_collections.as_ref()
1011 }
1012}
1013
1014fn own_transaction_xids(
1015 ctx: Option<&crate::storage::transaction::snapshot::TxnContext>,
1016) -> HashSet<Xid> {
1017 let mut set = HashSet::new();
1018 if let Some(ctx) = ctx {
1019 set.insert(ctx.xid);
1020 for (_, sub) in &ctx.savepoints {
1021 set.insert(*sub);
1022 }
1023 for sub in &ctx.released_sub_xids {
1024 set.insert(*sub);
1025 }
1026 }
1027 set
1028}
1029
1030impl RedDBRuntime {
1031 pub(crate) fn ai_scope(&self) -> EffectiveScope {
1037 let tenant = super::impl_core::current_tenant();
1038 let identity = super::impl_core::current_auth_identity();
1039 let auth_principal = super::execution_context::current_auth_principal();
1040 let captured_snapshot = super::execution_context::capture_current_snapshot();
1041 let snapshot = captured_snapshot.as_ref().map_or_else(
1042 || self.current_snapshot(),
1043 |context| context.snapshot.clone(),
1044 );
1045 let visible_collections = match (self.inner.auth_store.read().clone(), identity.as_ref()) {
1046 (Some(store), Some((principal, role))) => {
1047 let collections = self.inner.db.store().list_collections();
1048 Some(store.visible_collections_for_scope(
1049 tenant.as_deref(),
1050 *role,
1051 principal,
1052 &collections,
1053 ))
1054 }
1055 _ => None,
1056 };
1057 EffectiveScope {
1058 captured_snapshot,
1059 tenant,
1060 identity,
1061 auth_principal,
1062 snapshot,
1063 visible_collections,
1064 }
1065 }
1066}
1067
1068#[cfg(test)]
1072pub(crate) mod test_support {
1073 use super::{LockIntent, Privilege, ReadFrame};
1074 use crate::auth::Role;
1075 use crate::storage::transaction::snapshot::{Snapshot, Xid};
1076 use std::collections::HashSet;
1077
1078 pub(crate) struct FakeReadFrame {
1082 pub tenant: Option<String>,
1083 pub identity: Option<(String, Role)>,
1084 pub snapshot: Snapshot,
1085 pub visible: Option<HashSet<String>>,
1086 }
1087
1088 impl FakeReadFrame {
1089 pub(crate) fn without_scope() -> Self {
1090 Self {
1091 tenant: None,
1092 identity: None,
1093 snapshot: Snapshot {
1094 xid: 0,
1095 in_progress: HashSet::new(),
1096 },
1097 visible: None,
1098 }
1099 }
1100
1101 pub(crate) fn with_visible(visible: HashSet<String>) -> Self {
1102 Self {
1103 tenant: Some("acme".to_string()),
1104 identity: Some(("alice".to_string(), Role::Read)),
1105 snapshot: Snapshot {
1106 xid: 0,
1107 in_progress: HashSet::new(),
1108 },
1109 visible: Some(visible),
1110 }
1111 }
1112 }
1113
1114 impl ReadFrame for FakeReadFrame {
1115 fn effective_scope(&self) -> Option<&str> {
1116 self.tenant.as_deref()
1117 }
1118 fn identity(&self) -> Option<(&str, Role)> {
1119 self.identity.as_ref().map(|(u, r)| (u.as_str(), *r))
1120 }
1121 fn snapshot(&self) -> &Snapshot {
1122 &self.snapshot
1123 }
1124 fn as_of_floor(&self) -> Option<Xid> {
1125 None
1126 }
1127 fn cache_key(&self) -> &str {
1128 ""
1129 }
1130 fn should_cache_result(&self) -> bool {
1131 false
1132 }
1133 fn required_privilege(&self) -> Privilege {
1134 Privilege::Read
1135 }
1136 fn lock_intent(&self) -> LockIntent {
1137 LockIntent::Shared
1138 }
1139 fn visible_collections(&self) -> Option<&HashSet<String>> {
1140 self.visible.as_ref()
1141 }
1142 }
1143}
1144
1145impl RedDBRuntime {
1146 fn statement_snapshot(
1176 &self,
1177 query: &str,
1178 identity: StatementIdentity<'_>,
1179 ) -> RedDBResult<(Snapshot, Option<Xid>, SnapshotRetentionGuard)> {
1180 match peek_top_level_as_of_with_table(query) {
1181 Some((spec, Some(table))) => {
1182 if !table.starts_with("red_")
1183 && !table.starts_with("red.")
1184 && !self.vcs_is_versioned(&table)?
1185 {
1186 return Err(RedDBError::InvalidConfig(format!(
1187 "AS OF requires a versioned collection — \
1188 `{table}` has not opted in. \
1189 Call vcs.set_versioned(\"{table}\", true) first."
1190 )));
1191 }
1192 let xid = self.vcs_resolve_as_of(spec)?;
1193 let (snapshot, retention) = self.snapshot_manager().retain_snapshot(Snapshot {
1194 xid,
1195 in_progress: HashSet::new(),
1196 });
1197 Ok((snapshot, Some(xid), retention))
1198 }
1199 Some((spec, None)) => {
1200 let xid = self.vcs_resolve_as_of(spec)?;
1201 let (snapshot, retention) = self.snapshot_manager().retain_snapshot(Snapshot {
1202 xid,
1203 in_progress: HashSet::new(),
1204 });
1205 Ok((snapshot, Some(xid), retention))
1206 }
1207 None => match (identity, capture_current_snapshot()) {
1208 (StatementIdentity::Expr(_), Some(outer)) => {
1209 let (snapshot, retention) =
1210 self.snapshot_manager().retain_snapshot(outer.snapshot);
1211 Ok((snapshot, None, retention))
1212 }
1213 _ => {
1214 let (snapshot, retention) = self
1215 .inner
1216 .transaction_state
1217 .retained_current_snapshot(current_connection_id());
1218 Ok((snapshot, None, retention))
1219 }
1220 },
1221 }
1222 }
1223
1224 fn result_cache_safe(&self, conn_id: u64) -> bool {
1225 let has_unsafe_xids = self
1226 .inner
1227 .transaction_state
1228 .snapshot_manager()
1229 .has_cache_unsafe_active_xids();
1230 let in_own_tx = self.inner.transaction_state.in_transaction(conn_id);
1231 !has_unsafe_xids && !in_own_tx
1232 }
1233}
1234
1235fn is_graph_tvf_engine(engine: &str) -> bool {
1240 matches!(engine, "runtime-graph-tvf" | "runtime-graph-tvf-inline")
1241}
1242
1243fn result_cache_key(query: &str) -> String {
1244 let tenant = current_tenant().unwrap_or_default();
1245 let auth = current_auth_identity()
1246 .map(|(user, role)| format!("{}|{:?}", user, role))
1247 .unwrap_or_default();
1248 if tenant.is_empty() && auth.is_empty() {
1249 query.to_string()
1250 } else {
1251 format!("{query}\u{001e}{tenant}\u{001e}{auth}")
1252 }
1253}
1254
1255#[cfg(test)]
1256mod tests {
1257 use super::*;
1258 use crate::api::RedDBOptions;
1259 use crate::runtime::impl_core::{
1260 clear_current_auth_identity, clear_current_tenant, set_current_auth_identity,
1261 set_current_tenant,
1262 };
1263 use crate::runtime::RedDBRuntime;
1264
1265 fn fresh_runtime() -> RedDBRuntime {
1266 RedDBRuntime::with_options(RedDBOptions::in_memory()).expect("in-memory runtime")
1267 }
1268
1269 enum HeldFrame {
1270 Ordinary,
1271 Nested,
1272 Historical,
1273 }
1274
1275 fn verify_frame_survives_vacuum(delete: bool, kind: HeldFrame) {
1276 use crate::runtime::mvcc::{clear_current_connection_id, set_current_connection_id};
1277
1278 reset_thread_locals();
1279 clear_current_connection_id();
1280 let runtime = fresh_runtime();
1281 let execute = |connection, sql: &str| {
1282 set_current_connection_id(connection);
1283 let result = runtime.execute_query(sql);
1284 clear_current_connection_id();
1285 result.expect("valid frame retention fixture statement")
1286 };
1287 execute(1, "CREATE TABLE frame_rows (id TEXT, n INTEGER)");
1288 execute(1, "INSERT INTO frame_rows (id, n) VALUES ('row', 1)");
1289 let historical_xid = runtime.current_snapshot().xid;
1290 if matches!(kind, HeldFrame::Historical) {
1291 runtime
1292 .vcs_set_versioned("frame_rows", true)
1293 .expect("enable historical reads");
1294 }
1295 execute(2, "BEGIN");
1296 let query = if matches!(kind, HeldFrame::Historical) {
1297 format!("SELECT id, n FROM frame_rows AS OF SNAPSHOT {historical_xid}")
1298 } else {
1299 "SELECT id, n FROM frame_rows".to_string()
1300 };
1301 let frame = StatementExecutionFrame::build(&runtime, StatementIdentity::Text(&query))
1302 .expect("frame captures its snapshot");
1303 let frame = if matches!(kind, HeldFrame::Nested) {
1304 let guards = frame.install(&runtime);
1305 let expr = QueryExpr::Table(reddb_rql::ast::TableQuery::new("frame_rows"));
1306 let nested = StatementExecutionFrame::build(&runtime, StatementIdentity::Expr(&expr))
1307 .expect("nested frame inherits the protected snapshot");
1308 drop(guards);
1309 drop(frame);
1310 nested
1311 } else {
1312 frame
1313 };
1314 execute(
1317 2,
1318 if delete {
1319 "DELETE FROM frame_rows WHERE id = 'row'"
1320 } else {
1321 "UPDATE frame_rows SET n = 2 WHERE id = 'row'"
1322 },
1323 );
1324 execute(2, "COMMIT");
1325 execute(1, "VACUUM frame_rows");
1326 {
1327 let _guards = frame.install(&runtime);
1328 let records = super::super::record_search::scan_runtime_table_source_records_limited(
1329 &runtime.db(),
1330 "frame_rows",
1331 None,
1332 )
1333 .expect("captured frame scan succeeds after vacuum");
1334 assert_eq!(
1335 records.len(),
1336 1,
1337 "vacuum reclaimed an active frame's visible row"
1338 );
1339 assert_eq!(
1340 records[0]
1341 .get("n")
1342 .expect("selected value exists")
1343 .to_string(),
1344 "1"
1345 );
1346 }
1347 drop(frame);
1348 execute(1, "VACUUM frame_rows");
1349 let physical = runtime
1350 .db()
1351 .store()
1352 .get_collection("frame_rows")
1353 .expect("fixture collection exists")
1354 .query_all(|_| true)
1355 .len();
1356 assert_eq!(
1357 physical,
1358 usize::from(!delete),
1359 "completed frame leaked its retention pin"
1360 );
1361 clear_current_connection_id();
1362 }
1363
1364 #[test]
1365 fn autocommit_frame_retains_update_until_scan_completes() {
1366 verify_frame_survives_vacuum(false, HeldFrame::Ordinary);
1367 }
1368
1369 #[test]
1370 fn autocommit_frame_retains_delete_until_scan_completes() {
1371 verify_frame_survives_vacuum(true, HeldFrame::Ordinary);
1372 }
1373
1374 #[test]
1375 fn nested_frame_retains_history_after_parent_frame_drops() {
1376 verify_frame_survives_vacuum(false, HeldFrame::Nested);
1377 }
1378
1379 #[test]
1380 fn historical_frame_retains_history_until_scan_completes() {
1381 verify_frame_survives_vacuum(true, HeldFrame::Historical);
1382 }
1383
1384 fn reset_thread_locals() {
1387 clear_current_tenant();
1388 clear_current_auth_identity();
1389 }
1390
1391 #[test]
1392 fn autocommit_select_takes_live_snapshot() {
1393 reset_thread_locals();
1394 let rt = fresh_runtime();
1395 let frame = StatementExecutionFrame::build(&rt, StatementIdentity::Text("SELECT 1"))
1396 .expect("frame builds for SELECT 1");
1397
1398 let f: &dyn ReadFrame = &frame;
1401 assert!(f.as_of_floor().is_none(), "live read has no AS OF floor");
1402 assert_eq!(f.snapshot().xid, rt.snapshot_manager().peek_next_xid() - 1);
1403 assert!(!f.snapshot().sees(rt.snapshot_manager().peek_next_xid(), 0));
1404 }
1405
1406 #[test]
1407 fn autocommit_snapshot_does_not_lose_rows_after_index_publication() {
1408 reset_thread_locals();
1409 let rt = fresh_runtime();
1410 rt.execute_query("CREATE TABLE frame_index_race (id INT PRIMARY KEY, status TEXT)")
1411 .expect("create fixture");
1412 rt.execute_query("INSERT INTO frame_index_race (id, status) VALUES (1, 'old'), (2, 'old')")
1413 .expect("insert fixture");
1414 rt.execute_query("CREATE INDEX frame_status_hash ON frame_index_race (status) USING HASH")
1415 .expect("create index");
1416 let sql = "SELECT id FROM frame_index_race WHERE status = 'old'";
1417 let frame = StatementExecutionFrame::build(&rt, StatementIdentity::Text(sql))
1418 .expect("capture autocommit snapshot");
1419 rt.execute_query("UPDATE frame_index_race SET status = 'new' WHERE id = 1")
1420 .expect("publish changed index entry");
1421 let reddb_rql::ast::QueryExpr::Table(query) =
1422 reddb_rql::modes::parse_multi(sql).expect("parse")
1423 else {
1424 panic!("expected table query");
1425 };
1426 let _guards = frame.install(&rt);
1427 let records =
1428 super::super::query_exec::table::execute_runtime_canonical_table_query_indexed(
1429 &rt.db(),
1430 &query,
1431 Some(rt.index_store_ref()),
1432 )
1433 .expect("read captured snapshot");
1434 let mut ids = records
1435 .iter()
1436 .map(|record| record.get("id").expect("id").to_string())
1437 .collect::<Vec<_>>();
1438 ids.sort();
1439 assert_eq!(
1440 ids,
1441 ["1", "2"],
1442 "current index omitted a visible historical row"
1443 );
1444 }
1445
1446 #[test]
1447 fn captured_dml_snapshot_finds_rows_after_index_publication() {
1448 reset_thread_locals();
1449 let rt = fresh_runtime();
1450 rt.execute_query("CREATE TABLE frame_dml_race (id INT PRIMARY KEY, status TEXT)")
1451 .expect("create fixture");
1452 rt.execute_query("INSERT INTO frame_dml_race (id, status) VALUES (1, 'old'), (2, 'old')")
1453 .expect("insert fixture");
1454 rt.execute_query("CREATE INDEX frame_dml_status ON frame_dml_race (status) USING HASH")
1455 .expect("create index");
1456 let sql = "DELETE FROM frame_dml_race WHERE status = 'old'";
1457 let frame = StatementExecutionFrame::build(&rt, StatementIdentity::Text(sql))
1458 .expect("capture DML snapshot");
1459 rt.execute_query("UPDATE frame_dml_race SET status = 'new' WHERE id = 1")
1460 .expect("publish new indexed value");
1461 let reddb_rql::ast::QueryExpr::Table(query) =
1462 reddb_rql::modes::parse_multi("SELECT id FROM frame_dml_race WHERE status = 'old'")
1463 .expect("parse filter")
1464 else {
1465 panic!("expected table query")
1466 };
1467 let filter = reddb_rql::sql_lowering::effective_table_filter(&query).expect("filter");
1468 let _guards = frame.install(&rt);
1469 let ids = super::super::dml_target_scan::DmlTargetScan::new(
1470 &rt,
1471 "frame_dml_race",
1472 Some(&filter),
1473 None,
1474 )
1475 .find_target_ids()
1476 .expect("find DML targets");
1477 assert_eq!(
1478 ids.len(),
1479 2,
1480 "DML target scan must discover both snapshot-visible rows"
1481 );
1482 }
1483
1484 #[test]
1485 fn captured_snapshot_survives_indexed_update_and_delete_for_range_queries() {
1486 reset_thread_locals();
1487 let rt = fresh_runtime();
1488 rt.execute_query("CREATE TABLE frame_range_race (id INT PRIMARY KEY, score INT)")
1489 .expect("create fixture");
1490 rt.execute_query("INSERT INTO frame_range_race (id, score) VALUES (1, 10), (2, 20)")
1491 .expect("insert fixture");
1492 rt.execute_query("CREATE INDEX frame_score_range ON frame_range_race (score)")
1493 .expect("create index");
1494 let sql = "SELECT id FROM frame_range_race WHERE score >= 10 AND score <= 20 ORDER BY id";
1495 let frame = StatementExecutionFrame::build(&rt, StatementIdentity::Text(sql))
1496 .expect("capture read snapshot");
1497 rt.execute_query("UPDATE frame_range_race SET score = 99 WHERE id = 1")
1498 .expect("update row");
1499 rt.execute_query("DELETE FROM frame_range_race WHERE id = 2")
1500 .expect("delete row");
1501 let reddb_rql::ast::QueryExpr::Table(query) =
1502 reddb_rql::modes::parse_multi(sql).expect("parse")
1503 else {
1504 panic!("expected table query");
1505 };
1506 let _guards = frame.install(&rt);
1507 let records =
1508 super::super::query_exec::table::execute_runtime_canonical_table_query_indexed(
1509 &rt.db(),
1510 &query,
1511 Some(rt.index_store_ref()),
1512 )
1513 .expect("read captured snapshot");
1514 let ids = records
1515 .iter()
1516 .map(|record| record.get("id").expect("id").to_string())
1517 .collect::<Vec<_>>();
1518 assert_eq!(ids, ["1", "2"]);
1519 }
1520
1521 #[test]
1522 fn dml_target_scan_does_not_retry_inside_stored_function_budget() {
1523 use std::sync::atomic::{AtomicUsize, Ordering};
1524 use std::sync::Arc;
1525 reset_thread_locals();
1526 let rt = fresh_runtime();
1527 rt.execute_query("CREATE TABLE dml_budget_guard (id INT, status TEXT)")
1528 .expect("create fixture");
1529 rt.execute_query("INSERT INTO dml_budget_guard (id, status) VALUES (1, 'old')")
1530 .expect("insert fixture");
1531 rt.execute_query("CREATE INDEX dml_budget_status ON dml_budget_guard (status) USING HASH")
1532 .expect("create index");
1533 let frame = StatementExecutionFrame::build(
1534 &rt,
1535 StatementIdentity::Text("DELETE FROM dml_budget_guard WHERE status = 'old'"),
1536 )
1537 .expect("capture snapshot");
1538 let reddb_rql::ast::QueryExpr::Table(query) =
1539 reddb_rql::modes::parse_multi("SELECT id FROM dml_budget_guard WHERE status = 'old'")
1540 .expect("parse filter")
1541 else {
1542 panic!("expected table query")
1543 };
1544 let filter = reddb_rql::sql_lowering::effective_table_filter(&query).expect("filter");
1545 let calls = Arc::new(AtomicUsize::new(0));
1546 let observed = Arc::clone(&calls);
1547 *rt.index_store_ref().mutation_hook.lock() = Some(Arc::new(move |collection, phase| {
1548 if collection == "dml_budget_guard"
1549 && phase == super::super::index_store::MutationTestPhase::DmlIndexLookupStarted
1550 {
1551 observed.fetch_add(1, Ordering::SeqCst);
1552 }
1553 }));
1554 let _budget = super::super::function_budget::Scope::enter(1_000, 10_000)
1555 .expect("stored function budget");
1556 let _guards = frame.install(&rt);
1557 let ids = super::super::dml_target_scan::DmlTargetScan::new(
1558 &rt,
1559 "dml_budget_guard",
1560 Some(&filter),
1561 None,
1562 )
1563 .find_target_ids()
1564 .expect("find DML targets");
1565 assert_eq!(ids.len(), 1);
1566 assert_eq!(
1567 calls.load(Ordering::SeqCst),
1568 0,
1569 "bounded stored functions must not retry indexed discovery"
1570 );
1571 *rt.index_store_ref().mutation_hook.lock() = None;
1572 }
1573
1574 #[test]
1575 fn dml_target_selection_preserves_snapshot_when_publication_races() {
1576 use std::sync::{mpsc, Arc, Mutex};
1577 use std::time::Duration;
1578 reset_thread_locals();
1579 let rt = fresh_runtime();
1580 rt.execute_query("CREATE TABLE dml_lookup_race (id INT PRIMARY KEY, status TEXT)")
1581 .expect("create fixture");
1582 rt.execute_query("INSERT INTO dml_lookup_race (id, status) VALUES (1, 'old'), (2, 'old')")
1583 .expect("insert fixture");
1584 rt.execute_query("CREATE INDEX dml_lookup_status ON dml_lookup_race (status) USING HASH")
1585 .expect("create index");
1586 let frame = StatementExecutionFrame::build(
1587 &rt,
1588 StatementIdentity::Text("DELETE FROM dml_lookup_race WHERE status = 'old'"),
1589 )
1590 .expect("capture snapshot");
1591 let reddb_rql::ast::QueryExpr::Table(query) =
1592 reddb_rql::modes::parse_multi("SELECT id FROM dml_lookup_race WHERE status = 'old'")
1593 .expect("parse filter")
1594 else {
1595 panic!("expected table query")
1596 };
1597 let filter = reddb_rql::sql_lowering::effective_table_filter(&query).expect("filter");
1598 let (started_tx, started_rx) = mpsc::channel();
1599 let (published_tx, published_rx) = mpsc::channel();
1600 let published_rx = Mutex::new(published_rx);
1601 let first = std::sync::atomic::AtomicBool::new(true);
1602 *rt.index_store_ref().mutation_hook.lock() = Some(Arc::new(move |collection, phase| {
1603 if collection == "dml_lookup_race"
1604 && phase == super::super::index_store::MutationTestPhase::DmlTargetSelectionStarted
1605 && first.swap(false, std::sync::atomic::Ordering::SeqCst)
1606 {
1607 started_tx.send(()).expect("notify writer");
1608 published_rx
1609 .lock()
1610 .unwrap()
1611 .recv_timeout(Duration::from_secs(10))
1612 .expect("writer published");
1613 }
1614 }));
1615 std::thread::scope(|scope| {
1616 let writer_rt = &rt;
1617 let writer = scope.spawn(move || {
1618 started_rx
1619 .recv_timeout(Duration::from_secs(10))
1620 .expect("lookup started");
1621 writer_rt
1622 .execute_query("UPDATE dml_lookup_race SET status = 'new' WHERE id = 1")
1623 .expect("publish update");
1624 published_tx.send(()).expect("release lookup");
1625 });
1626 let limits = super::super::resource_limits::ResourceLimits {
1627 max_query_duration: Some(Duration::from_secs(30)),
1628 ..Default::default()
1629 };
1630 let slots = std::sync::atomic::AtomicU64::new(0);
1631 let _budget = super::super::query_budget::Scope::enter(&limits, &slots)
1632 .expect("query deadline scope");
1633 let _guards = frame.install(&rt);
1634 let ids = super::super::dml_target_scan::DmlTargetScan::new(
1635 &rt,
1636 "dml_lookup_race",
1637 Some(&filter),
1638 None,
1639 )
1640 .find_target_ids()
1641 .expect("find DML targets");
1642 writer.join().expect("writer thread");
1643 assert_eq!(
1644 ids.len(),
1645 2,
1646 "racing publication must not omit a snapshot-visible row"
1647 );
1648 });
1649 *rt.index_store_ref().mutation_hook.lock() = None;
1650 }
1651
1652 #[test]
1653 fn replica_replay_preserves_snapshots_across_insert_update_and_delete() {
1654 use crate::replication::cdc::{change_record_from_entity, ChangeOperation};
1655 use crate::replication::logical::{ApplyMode, LogicalChangeApplier};
1656 reset_thread_locals();
1657 let source = fresh_runtime();
1658 let replica = fresh_runtime();
1659 for rt in [&source, &replica] {
1660 rt.execute_query("CREATE TABLE replica_snapshot (id INT, status TEXT)")
1661 .expect("schema");
1662 }
1663 replica
1664 .execute_query(
1665 "CREATE INDEX replica_snapshot_status ON replica_snapshot (status) USING HASH",
1666 )
1667 .expect("index");
1668 let sql = "SELECT id FROM replica_snapshot WHERE status = 'old'";
1669 let capture = || {
1670 StatementExecutionFrame::build(&replica, StatementIdentity::Text(sql)).expect("capture")
1671 };
1672 let read = |frame: &StatementExecutionFrame| {
1673 let reddb_rql::ast::QueryExpr::Table(query) =
1674 reddb_rql::modes::parse_multi(sql).expect("parse")
1675 else {
1676 panic!("table")
1677 };
1678 let _guards = frame.install(&replica);
1679 super::super::query_exec::table::execute_runtime_canonical_table_query_indexed(
1680 &replica.db(),
1681 &query,
1682 Some(replica.index_store_ref()),
1683 )
1684 .expect("snapshot read")
1685 .len()
1686 };
1687 let empty = capture();
1688 source
1689 .execute_query("INSERT INTO replica_snapshot (id, status) VALUES (1, 'old')")
1690 .expect("insert");
1691 let applier = LogicalChangeApplier::new(0);
1692 let replay = |lsn, op| {
1693 let entity = source
1694 .db()
1695 .store()
1696 .get_collection("replica_snapshot")
1697 .unwrap()
1698 .query_all(|entity| entity.xmax == 0)
1699 .pop()
1700 .unwrap();
1701 applier
1702 .apply(
1703 &replica.db(),
1704 &change_record_from_entity(
1705 lsn,
1706 1,
1707 op,
1708 "replica_snapshot",
1709 "table",
1710 &entity,
1711 12,
1712 None,
1713 ),
1714 ApplyMode::Replica,
1715 )
1716 .expect("replay");
1717 };
1718 replay(1, ChangeOperation::Insert);
1719 assert_eq!(
1720 read(&empty),
1721 0,
1722 "replay must not backdate an insert into an earlier snapshot"
1723 );
1724 let inserted = capture();
1725 source
1726 .execute_query("UPDATE replica_snapshot SET status = 'new' WHERE id = 1")
1727 .expect("update");
1728 replay(2, ChangeOperation::Update);
1729 assert_eq!(read(&inserted), 1, "old version remains visible");
1730 assert_eq!(read(&capture()), 0, "fresh snapshot uses the updated index");
1731 let all_sql = "SELECT id FROM replica_snapshot";
1733 let before_delete =
1734 StatementExecutionFrame::build(&replica, StatementIdentity::Text(all_sql)).unwrap();
1735 replay(3, ChangeOperation::Delete);
1736 let reddb_rql::ast::QueryExpr::Table(query) =
1737 reddb_rql::modes::parse_multi(all_sql).unwrap()
1738 else {
1739 panic!("table")
1740 };
1741 {
1742 let _guards = before_delete.install(&replica);
1743 assert_eq!(
1744 super::super::query_exec::table::execute_runtime_canonical_table_query_indexed(
1745 &replica.db(),
1746 &query,
1747 Some(replica.index_store_ref())
1748 )
1749 .unwrap()
1750 .len(),
1751 1
1752 );
1753 }
1754 assert_eq!(
1755 replica.execute_query(all_sql).unwrap().result.records.len(),
1756 0
1757 );
1758 }
1759
1760 #[test]
1761 fn autocommit_index_reads_keep_old_rows_while_another_transaction_is_open() {
1762 use crate::runtime::mvcc::{clear_current_connection_id, set_current_connection_id};
1763 reset_thread_locals();
1764 let rt = fresh_runtime();
1765 let execute = |connection, sql: &str| {
1766 set_current_connection_id(connection);
1767 let result = rt.execute_query(sql);
1768 clear_current_connection_id();
1769 result.expect("statement")
1770 };
1771 execute(1, "CREATE TABLE active_writer_rows (id INT, status TEXT)");
1772 execute(
1773 1,
1774 "INSERT INTO active_writer_rows (id, status) VALUES (1, 'old')",
1775 );
1776 execute(
1777 1,
1778 "CREATE INDEX active_writer_status ON active_writer_rows (status) USING HASH",
1779 );
1780 execute(2, "BEGIN");
1781 execute(
1782 2,
1783 "UPDATE active_writer_rows SET status = 'new' WHERE id = 1",
1784 );
1785 assert_eq!(
1786 execute(1, "SELECT id FROM active_writer_rows WHERE status = 'old'")
1787 .result
1788 .records
1789 .len(),
1790 1
1791 );
1792 assert_eq!(
1793 execute(1, "SELECT id FROM active_writer_rows WHERE status = 'new'")
1794 .result
1795 .records
1796 .len(),
1797 0
1798 );
1799 execute(2, "ROLLBACK");
1800 assert_eq!(
1801 execute(1, "SELECT id FROM active_writer_rows WHERE status = 'old'")
1802 .result
1803 .records
1804 .len(),
1805 1
1806 );
1807 }
1808
1809 #[test]
1810 fn frame_captures_identity_and_scope() {
1811 reset_thread_locals();
1812 set_current_tenant("acme".to_string());
1813 set_current_auth_identity("alice".to_string(), Role::Write);
1814
1815 let rt = fresh_runtime();
1816 let frame = StatementExecutionFrame::build(&rt, StatementIdentity::Text("SELECT 1"))
1817 .expect("frame builds");
1818 let f: &dyn ReadFrame = &frame;
1819
1820 assert_eq!(f.effective_scope(), Some("acme"));
1821 let id = f.identity().expect("identity captured");
1822 assert_eq!(id.0, "alice");
1823 assert!(matches!(id.1, Role::Write));
1824
1825 assert!(
1828 f.cache_key().contains("acme") && f.cache_key().contains("alice"),
1829 "cache key folds in scope + identity, got {:?}",
1830 f.cache_key()
1831 );
1832
1833 reset_thread_locals();
1834 }
1835
1836 #[test]
1837 fn as_of_rejects_non_versioned_user_collection() {
1838 reset_thread_locals();
1839 let rt = fresh_runtime();
1840
1841 let err = match StatementExecutionFrame::build(
1845 &rt,
1846 StatementIdentity::Text("SELECT * FROM not_versioned AS OF COMMIT 'deadbeef'"),
1847 ) {
1848 Err(e) => e,
1849 Ok(_) => panic!("AS OF on non-versioned user collection rejected"),
1850 };
1851
1852 let msg = format!("{err}");
1853 assert!(
1854 msg.contains("AS OF requires a versioned collection"),
1855 "expected AS OF rejection, got: {msg}"
1856 );
1857 }
1858
1859 #[test]
1871 fn select_path_routes_through_frame_cache_key() {
1872 reset_thread_locals();
1873 set_current_tenant("acme".to_string());
1874 set_current_auth_identity("alice".to_string(), Role::Read);
1875
1876 let rt = fresh_runtime();
1877 let result = rt
1878 .execute_query("SELECT 1")
1879 .expect("SELECT 1 executes under tenant=acme/identity=alice");
1880 assert_eq!(result.statement_type, "select");
1881
1882 let cache = rt.inner.result_blob_entries.read();
1888 let any_keyed_with_scope = cache
1889 .0
1890 .keys()
1891 .any(|k| k.contains("acme") && k.contains("alice"));
1892 assert!(
1893 any_keyed_with_scope,
1894 "expected at least one result-cache key carrying tenant+identity, \
1895 got keys: {:?}",
1896 cache.0.keys().collect::<Vec<_>>()
1897 );
1898
1899 reset_thread_locals();
1900 }
1901
1902 #[test]
1914 fn volatile_select_does_not_populate_result_cache() {
1915 reset_thread_locals();
1916 let rt = fresh_runtime();
1917
1918 let frame = StatementExecutionFrame::build(
1922 &rt,
1923 StatementIdentity::Text("SELECT pg_advisory_unlock(1)"),
1924 )
1925 .expect("frame");
1926 let f: &dyn ReadFrame = &frame;
1927 assert!(
1928 !f.should_cache_result(),
1929 "volatile builtin must disable result-cache safety"
1930 );
1931
1932 let _ = rt
1937 .execute_query("SELECT pg_advisory_unlock(1)")
1938 .expect("volatile SELECT executes");
1939 let cache = rt.inner.result_blob_entries.read();
1940 let key = result_cache_key("SELECT pg_advisory_unlock(1)");
1941 assert!(
1942 !cache.0.contains_key(&key),
1943 "volatile SELECT must not populate result cache, found key {key:?} in {:?}",
1944 cache.0.keys().collect::<Vec<_>>()
1945 );
1946
1947 reset_thread_locals();
1948 }
1949
1950 #[test]
1951 fn result_cache_populates_blob_path() {
1952 reset_thread_locals();
1953 let rt = fresh_runtime();
1954 let result = rt.execute_query("SELECT 1").expect("SELECT 1 executes");
1955 assert_eq!(result.statement_type, "select");
1956
1957 let key = result_cache_key("SELECT 1");
1958 rt.inner.result_blob_entries.write().5 = Some(std::time::Instant::now());
1959 rt.promote_volatile_result_cache_entry();
1960 assert!(
1961 rt.inner
1962 .result_blob_cache
1963 .get("runtime.result_cache", &key)
1964 .is_some(),
1965 "blob backend should stamp the Blob Cache path"
1966 );
1967 assert!(rt.inner.result_blob_entries.read().0.contains_key(&key));
1968 }
1969
1970 #[test]
1971 fn blob_cache_backend_keeps_volatile_select_out_of_blob_path() {
1972 reset_thread_locals();
1973 let rt = fresh_runtime();
1974 let _ = rt
1975 .execute_query("SELECT pg_advisory_unlock(1)")
1976 .expect("volatile SELECT executes");
1977 let key = result_cache_key("SELECT pg_advisory_unlock(1)");
1978 assert!(
1979 rt.inner
1980 .result_blob_cache
1981 .get("runtime.result_cache", &key)
1982 .is_none(),
1983 "volatile SELECT must not populate blob result cache"
1984 );
1985 assert!(!rt.inner.result_blob_entries.read().0.contains_key(&key));
1986 }
1987
1988 #[test]
1989 fn as_of_on_red_collection_records_floor() {
1990 reset_thread_locals();
1991 let rt = fresh_runtime();
1992
1993 let frame = StatementExecutionFrame::build(
1996 &rt,
1997 StatementIdentity::Text("SELECT * FROM red.vcs.commits AS OF SNAPSHOT 1"),
1998 )
1999 .expect("AS OF SNAPSHOT 1 on red.vcs.commits resolves");
2000
2001 let f: &dyn ReadFrame = &frame;
2002 assert_eq!(
2003 f.as_of_floor(),
2004 Some(1),
2005 "AS OF SNAPSHOT 1 records xid=1 as the floor"
2006 );
2007 assert_eq!(f.snapshot().xid, 1);
2008 assert!(
2009 f.snapshot().in_progress.is_empty(),
2010 "historical reads have no in-progress set"
2011 );
2012 }
2013
2014 #[test]
2020 fn frame_classifies_privilege_and_lock_intent_from_prefix() {
2021 reset_thread_locals();
2022 let rt = fresh_runtime();
2023
2024 let cases = [
2025 ("SELECT 1", Privilege::Read, LockIntent::Shared),
2026 ("LIST KV settings", Privilege::Read, LockIntent::Shared),
2027 (
2028 "KV GET settings.feature",
2029 Privilege::Read,
2030 LockIntent::Shared,
2031 ),
2032 ("VAULT LIST secrets", Privilege::Read, LockIntent::Shared),
2033 (
2034 "INSERT INTO t (id) VALUES (1)",
2035 Privilege::Write,
2036 LockIntent::Exclusive,
2037 ),
2038 (
2039 "KV PUT settings.feature = 'on'",
2040 Privilege::Write,
2041 LockIntent::Exclusive,
2042 ),
2043 (
2044 "VAULT PUT secrets.api = 'x'",
2045 Privilege::Write,
2046 LockIntent::Exclusive,
2047 ),
2048 (
2049 "UPDATE t SET x = 1 WHERE id = 1",
2050 Privilege::Write,
2051 LockIntent::Exclusive,
2052 ),
2053 (
2054 "DELETE FROM t WHERE id = 1",
2055 Privilege::Write,
2056 LockIntent::Exclusive,
2057 ),
2058 (
2059 "CREATE TABLE foo (id INT)",
2060 Privilege::Write,
2061 LockIntent::Exclusive,
2062 ),
2063 ("BEGIN", Privilege::None, LockIntent::None),
2064 ("COMMIT", Privilege::None, LockIntent::None),
2065 ("SET timezone = 'UTC'", Privilege::None, LockIntent::None),
2066 ];
2067
2068 for (q, want_priv, want_lock) in cases {
2069 let frame = StatementExecutionFrame::build(&rt, StatementIdentity::Text(q))
2070 .unwrap_or_else(|e| panic!("frame builds for {q:?}: {e}"));
2071 let f: &dyn ReadFrame = &frame;
2072 assert_eq!(f.required_privilege(), want_priv, "privilege for {q:?}");
2073 assert_eq!(f.lock_intent(), want_lock, "lock intent for {q:?}");
2074 }
2075 }
2076
2077 #[test]
2094 fn insert_under_read_role_denied_via_frame_privilege() {
2095 reset_thread_locals();
2096 set_current_auth_identity("alice".to_string(), Role::Read);
2097
2098 let rt = fresh_runtime();
2099 let frame = StatementExecutionFrame::build(
2105 &rt,
2106 StatementIdentity::Text("INSERT INTO t (id) VALUES (1)"),
2107 )
2108 .expect("frame builds for INSERT");
2109 let f: &dyn ReadFrame = &frame;
2110 assert_eq!(
2111 f.required_privilege(),
2112 Privilege::Write,
2113 "INSERT classified as Write"
2114 );
2115 let id = f.identity().expect("identity captured");
2116 assert!(
2117 !f.required_privilege().is_satisfied_by(id.1),
2118 "Role::Read does not satisfy Privilege::Write — frame must deny"
2119 );
2120
2121 use reddb_rql::ast::{QueryExpr, TableQuery};
2127 let expr = QueryExpr::Table(TableQuery::new("t"));
2128 let err = frame
2129 .check_query_privilege(&rt, &expr)
2130 .expect_err("denied via frame's coarse privilege gate");
2131 let msg = format!("{err}");
2132 assert!(
2133 msg.contains("permission denied") && msg.contains("Write"),
2134 "expected frame-level Write deny, got: {msg}"
2135 );
2136
2137 reset_thread_locals();
2138 }
2139
2140 #[test]
2153 fn large_select_through_frame_arena_returns_all_rows_in_order() {
2154 reset_thread_locals();
2155 let rt = fresh_runtime();
2156 rt.execute_query("CREATE TABLE big (id INT)")
2157 .expect("create table");
2158
2159 const N: usize = 2_500;
2162 for start in (0..N).step_by(250) {
2163 let end = (start + 250).min(N);
2164 let values = (start..end)
2165 .map(|i| format!("({i})"))
2166 .collect::<Vec<_>>()
2167 .join(", ");
2168 rt.execute_query(&format!("INSERT INTO big (id) VALUES {values}"))
2169 .unwrap_or_else(|err| panic!("insert rows {start}..{end}: {err:?}"));
2170 }
2171
2172 let result = rt
2173 .execute_query("SELECT id FROM big ORDER BY id")
2174 .expect("large SELECT executes through the frame arena path");
2175 assert_eq!(result.statement_type, "select");
2176 assert_eq!(
2177 result.result.records.len(),
2178 N,
2179 "every inserted row streams back through the arena-backed channel"
2180 );
2181 for (i, record) in result.result.records.iter().enumerate() {
2182 assert_eq!(
2183 record.get("id"),
2184 Some(&reddb_types::Value::Integer(i as i64)),
2185 "row {i} is byte-identical to the per-request-allocation baseline"
2186 );
2187 }
2188
2189 reset_thread_locals();
2190 }
2191
2192 #[test]
2206 fn parameterized_transport_adapters_delegate_binding_to_runtime() {
2207 let manifest_dir = std::path::Path::new(env!("CARGO_MANIFEST_DIR"));
2208 let adapters = [
2209 ("src/server/handlers_query.rs", "QueryRequestExecutor"),
2210 ("src/rpc_stdio.rs", "QueryRequestExecutor"),
2211 ("src/wire/redwire/session.rs", "QueryRequestExecutor"),
2212 ("src/wire/postgres/server.rs", "QueryRequestExecutor"),
2213 ("src/grpc.rs", "QueryRequestExecutor"),
2214 ];
2215
2216 for (relative, delegation) in adapters {
2217 let path = manifest_dir.join(relative);
2218 let text = std::fs::read_to_string(&path)
2219 .unwrap_or_else(|err| panic!("read {}: {err}", path.display()));
2220 assert!(
2221 text.contains(delegation),
2222 "{relative} should delegate parameterized query execution through {delegation}"
2223 );
2224 assert!(
2225 !text.contains("user_params::bind"),
2226 "{relative} must not bind SQL params in the transport adapter"
2227 );
2228 }
2229
2230 let seam = std::fs::read_to_string(manifest_dir.join("src/runtime/query_request.rs"))
2233 .expect("read src/runtime/query_request.rs");
2234 assert!(
2235 seam.contains("execute_query_with_params"),
2236 "the Request module must execute text SQL through the runtime entrypoint"
2237 );
2238 }
2239
2240 #[test]
2250 fn control_statement_skips_intent_locks_via_frame() {
2251 reset_thread_locals();
2252 let rt = fresh_runtime();
2253
2254 let frame = StatementExecutionFrame::build(&rt, StatementIdentity::Text("BEGIN"))
2255 .expect("frame builds for BEGIN");
2256 let f: &dyn ReadFrame = &frame;
2257 assert_eq!(f.lock_intent(), LockIntent::None);
2258
2259 use reddb_rql::ast::{QueryExpr, TableQuery};
2264 let expr = QueryExpr::Table(TableQuery::new("t"));
2265 let guard = frame
2266 .acquire_intent_locks(&rt, &expr)
2267 .expect("no lock is attempted");
2268 assert!(
2269 guard.is_none(),
2270 "BEGIN frame's lock_intent=None must short-circuit lock acquisition"
2271 );
2272 }
2273
2274 #[test]
2281 fn nested_frame_inherits_outer_statement_snapshot() {
2282 reset_thread_locals();
2283 let rt = fresh_runtime();
2284 rt.execute_query("CREATE TABLE nested_snap (id INT)")
2285 .expect("create table");
2286
2287 let outer = StatementExecutionFrame::build(
2288 &rt,
2289 StatementIdentity::Text("SELECT * FROM nested_snap"),
2290 )
2291 .expect("outer frame builds");
2292 let _outer_guards = outer.install(&rt);
2293
2294 rt.execute_query("INSERT INTO nested_snap (id) VALUES (1)")
2297 .expect("concurrent insert");
2298
2299 let expr = QueryExpr::Table(reddb_rql::ast::TableQuery::new("nested_snap"));
2300 let nested = StatementExecutionFrame::build(&rt, StatementIdentity::Expr(&expr))
2301 .expect("nested frame builds");
2302
2303 assert_eq!(
2304 ReadFrame::snapshot(&nested).xid,
2305 ReadFrame::snapshot(&outer).xid,
2306 "nested execution must inherit the outer statement's snapshot"
2307 );
2308 }
2309
2310 #[test]
2323 fn nested_text_statement_sees_commits_made_since_the_outer_frame() {
2324 reset_thread_locals();
2325 let rt = fresh_runtime();
2326 rt.execute_query("CREATE TABLE nested_text_snap (id INT)")
2327 .expect("create table");
2328
2329 let outer = StatementExecutionFrame::build(
2330 &rt,
2331 StatementIdentity::Text("SELECT * FROM nested_text_snap"),
2332 )
2333 .expect("outer frame builds");
2334 let _outer_guards = outer.install(&rt);
2335
2336 rt.execute_query("INSERT INTO nested_text_snap (id) VALUES (1)")
2337 .expect("concurrent insert");
2338
2339 let nested = StatementExecutionFrame::build(
2340 &rt,
2341 StatementIdentity::Text("SELECT * FROM nested_text_snap"),
2342 )
2343 .expect("nested frame builds");
2344
2345 assert_ne!(
2346 ReadFrame::snapshot(&nested).xid,
2347 ReadFrame::snapshot(&outer).xid,
2348 "a nested text statement must mint a fresh snapshot, not inherit \
2349 the enclosing statement's frozen view"
2350 );
2351 }
2352
2353 #[test]
2358 fn expr_frame_rejects_as_of_instead_of_reading_live() {
2359 reset_thread_locals();
2360 let rt = fresh_runtime();
2361
2362 let mut table = reddb_rql::ast::TableQuery::new("red.vcs.commits");
2363 table.as_of = Some(reddb_rql::ast::AsOfClause::Snapshot(1));
2364 let expr = QueryExpr::Table(table);
2365
2366 let err = match StatementExecutionFrame::build(&rt, StatementIdentity::Expr(&expr)) {
2367 Err(err) => err,
2368 Ok(_) => panic!("AS OF without statement text must be refused, not read live"),
2369 };
2370 let msg = format!("{err}");
2371 assert!(
2372 msg.contains("AS OF cannot be resolved from a pre-parsed expression"),
2373 "expected a loud AS OF refusal, got: {msg}"
2374 );
2375 }
2376
2377 #[test]
2381 fn expr_frame_opts_out_of_the_result_cache() {
2382 reset_thread_locals();
2383 let rt = fresh_runtime();
2384
2385 let expr = QueryExpr::Table(reddb_rql::ast::TableQuery::new("t"));
2386 let frame = StatementExecutionFrame::build(&rt, StatementIdentity::Expr(&expr))
2387 .expect("expr frame builds");
2388
2389 assert!(frame.cache_key().is_empty(), "expr frame has no cache key");
2390 assert!(
2391 !frame.can_read_result_cache(),
2392 "expr frame must never serve from the result cache"
2393 );
2394 }
2395}