1use super::*;
2use crate::application::entity::metadata_to_json;
3use crate::auth::column_policy_gate::ColumnAccessRequest;
4use crate::auth::UserId;
5use crate::replication::cdc::ChangeRecord;
6use crate::replication::logical::{ApplyMode, LogicalChangeApplier};
7use crate::storage::query::ast::TableSource;
8
9thread_local! {
10 static CURRENT_CONN_ID: std::cell::Cell<u64> = const { std::cell::Cell::new(0) };
14
15 static CURRENT_AUTH_IDENTITY: std::cell::RefCell<Option<(String, crate::auth::Role)>> =
23 const { std::cell::RefCell::new(None) };
24
25 static CURRENT_SNAPSHOT: std::cell::RefCell<Option<SnapshotContext>> =
35 const { std::cell::RefCell::new(None) };
36
37 static HAS_SNAPSHOT: std::cell::Cell<bool> = const { std::cell::Cell::new(false) };
43
44 static CURRENT_TENANT_ID: std::cell::RefCell<Option<String>> =
54 const { std::cell::RefCell::new(None) };
55
56 static CURRENT_CONFIG_RESOLVER: std::cell::RefCell<Option<ConfigResolver>> =
60 const { std::cell::RefCell::new(None) };
61
62 static CURRENT_SECRET_RESOLVER: std::cell::RefCell<Option<SecretResolver>> =
66 const { std::cell::RefCell::new(None) };
67}
68
69fn record_column_f64(
74 rec: &crate::storage::query::unified::UnifiedRecord,
75 column: &str,
76) -> Option<f64> {
77 let value = rec
78 .get(column)
79 .or_else(|| rec.get(&column.to_lowercase()))?;
80 match value {
81 Value::Integer(n) => Some(*n as f64),
82 Value::UnsignedInteger(n) => Some(*n as f64),
83 Value::Float(n) => Some(*n),
84 Value::Timestamp(n) | Value::Duration(n) => Some(*n as f64),
85 _ => None,
86 }
87}
88
89fn secret_sql_value_to_string(value: &Value) -> RedDBResult<String> {
90 match value {
91 Value::Text(s) => Ok(s.to_string()),
92 Value::Integer(n) => Ok(n.to_string()),
93 Value::UnsignedInteger(n) => Ok(n.to_string()),
94 Value::Float(n) => Ok(n.to_string()),
95 Value::Boolean(b) => Ok(b.to_string()),
96 Value::Null => Err(RedDBError::Query(
97 "SET SECRET key = NULL deletes the secret; use DELETE SECRET for explicit deletes"
98 .to_string(),
99 )),
100 Value::Password(_) | Value::Secret(_) => Err(RedDBError::Query(
101 "SET SECRET accepts plain scalar literals; PASSWORD() and SECRET() are for typed columns"
102 .to_string(),
103 )),
104 _ => Err(RedDBError::Query(format!(
105 "SET SECRET does not support value type {:?} yet",
106 value.data_type()
107 ))),
108 }
109}
110
111#[derive(Clone)]
112struct QueryControlEventSpec {
113 kind: crate::runtime::control_events::EventKind,
114 action: &'static str,
115 resource: Option<String>,
116 fields: Vec<(String, crate::runtime::control_events::Sensitivity)>,
117}
118
119#[derive(Clone)]
120struct QueryAuditPlan {
121 statement_kind: &'static str,
122 collections: Vec<String>,
123}
124
125fn query_audit_plan(expr: &QueryExpr) -> Option<QueryAuditPlan> {
126 let mut collections = Vec::new();
127 let statement_kind = match expr {
128 QueryExpr::Table(table) => {
129 push_query_audit_collection(&mut collections, &table.table);
130 "select"
131 }
132 QueryExpr::Join(join) => {
133 collect_query_audit_collections(&join.left, &mut collections);
134 collect_query_audit_collections(&join.right, &mut collections);
135 "select"
136 }
137 QueryExpr::Insert(insert) => {
138 push_query_audit_collection(&mut collections, &insert.table);
139 "insert"
140 }
141 QueryExpr::Update(update) => {
142 push_query_audit_collection(&mut collections, &update.table);
143 "update"
144 }
145 QueryExpr::Delete(delete) => {
146 push_query_audit_collection(&mut collections, &delete.table);
147 "delete"
148 }
149 _ => return None,
150 };
151 if collections.is_empty() {
152 None
153 } else {
154 Some(QueryAuditPlan {
155 statement_kind,
156 collections,
157 })
158 }
159}
160
161fn collect_query_audit_collections(expr: &QueryExpr, collections: &mut Vec<String>) {
162 match expr {
163 QueryExpr::Table(table) => push_query_audit_collection(collections, &table.table),
164 QueryExpr::Join(join) => {
165 collect_query_audit_collections(&join.left, collections);
166 collect_query_audit_collections(&join.right, collections);
167 }
168 _ => {}
169 }
170}
171
172fn push_query_audit_collection(collections: &mut Vec<String>, name: &str) {
173 if name == "red" || name.starts_with("red.") || name.starts_with("__red_schema_") {
174 return;
175 }
176 if !collections.iter().any(|existing| existing == name) {
177 collections.push(name.to_string());
178 }
179}
180
181impl RedDBRuntime {
182 fn execute_create_metric(
183 &self,
184 raw_query: &str,
185 query: &crate::storage::query::ast::CreateMetricQuery,
186 ) -> RedDBResult<RuntimeQueryResult> {
187 self.check_write(crate::runtime::write_gate::WriteKind::Ddl)?;
188 let store = self.inner.db.store();
189 super::metric_descriptor_catalog::create(
190 store.as_ref(),
191 &query.path,
192 &query.kind,
193 &query.role,
194 super::metric_descriptor_catalog::DerivedSpec {
195 source: query.source.clone(),
196 query: query.query.clone(),
197 window_ms: query.window_ms,
198 time_field: query.time_field.clone(),
199 },
200 )?;
201 self.invalidate_result_cache();
202 Ok(RuntimeQueryResult::ok_message(
203 raw_query.to_string(),
204 &format!("metric descriptor '{}' created", query.path),
205 "create",
206 ))
207 }
208
209 fn execute_create_ranking(
215 &self,
216 raw_query: &str,
217 req: super::ranking_descriptor_catalog::CreateRankingRequest,
218 ) -> RedDBResult<RuntimeQueryResult> {
219 self.check_write(crate::runtime::write_gate::WriteKind::Ddl)?;
220 let store = self.inner.db.store();
221 let descriptor = super::ranking_descriptor_catalog::create(store.as_ref(), &req)?;
222 self.invalidate_result_cache();
223 Ok(RuntimeQueryResult::ok_message(
224 raw_query.to_string(),
225 &format!(
226 "ranking '{}' created on {}({})",
227 descriptor.name, descriptor.table, descriptor.column
228 ),
229 "create",
230 ))
231 }
232
233 fn execute_show_rankings(&self, raw_query: &str) -> RedDBResult<RuntimeQueryResult> {
237 let store = self.inner.db.store();
238 let entries = super::ranking_descriptor_catalog::list(store.as_ref());
239 let columns = vec![
240 "name".to_string(),
241 "table".to_string(),
242 "column".to_string(),
243 "direction".to_string(),
244 "top_k".to_string(),
245 ];
246 let rows = entries
247 .into_iter()
248 .map(|e| {
249 vec![
250 ("name".to_string(), Value::text(e.name)),
251 ("table".to_string(), Value::text(e.table)),
252 ("column".to_string(), Value::text(e.column)),
253 (
254 "direction".to_string(),
255 Value::text(if e.descending { "DESC" } else { "ASC" }.to_string()),
256 ),
257 ("top_k".to_string(), Value::UnsignedInteger(e.top_k)),
258 ]
259 })
260 .collect();
261 Ok(RuntimeQueryResult::ok_records(
262 raw_query.to_string(),
263 columns,
264 rows,
265 "select",
266 ))
267 }
268
269 fn execute_rank_of(
278 &self,
279 raw_query: &str,
280 req: super::ranking_descriptor_catalog::RankOfRequest,
281 ) -> RedDBResult<RuntimeQueryResult> {
282 let store = self.inner.db.store();
283 let descriptor = super::ranking_descriptor_catalog::get(store.as_ref(), &req.ranking)
284 .ok_or_else(|| {
285 RedDBError::Query(format!("ranking '{}' does not exist", req.ranking))
286 })?;
287 let rank = self.compute_exact_head_rank(&descriptor, req.entity_id)?;
288 let columns = vec!["rank".to_string()];
289 let rows = match rank {
290 Some(rank) => vec![vec![("rank".to_string(), Value::UnsignedInteger(rank))]],
291 None => Vec::new(),
292 };
293 Ok(RuntimeQueryResult::ok_records(
294 raw_query.to_string(),
295 columns,
296 rows,
297 "select",
298 ))
299 }
300
301 fn compute_exact_head_rank(
315 &self,
316 descriptor: &super::ranking_descriptor_catalog::RankingDescriptor,
317 target_id: u64,
318 ) -> RedDBResult<Option<u64>> {
319 let table = &descriptor.table;
320 let column = &descriptor.column;
321
322 let dir = if descriptor.descending { "DESC" } else { "ASC" };
329 let head_sql = format!(
330 "SELECT * FROM {table} ORDER BY {column} {dir} LIMIT {}",
331 descriptor.top_k
332 );
333 let head_result = self.execute_query_inner(&head_sql)?;
334 let head = &head_result.result.records;
335
336 let target_score = head.iter().find_map(|rec| {
340 let rid = match rec.get("rid") {
341 Some(Value::UnsignedInteger(n)) => *n,
342 Some(Value::Integer(n)) if *n >= 0 => *n as u64,
343 _ => return None,
344 };
345 (rid == target_id).then(|| record_column_f64(rec, column))?
346 });
347 let Some(target_score) = target_score else {
348 return Ok(None);
349 };
350
351 let mut strictly_better = 0u64;
354 for rec in head {
355 let Some(score) = record_column_f64(rec, column) else {
356 continue;
357 };
358 let better = if descriptor.descending {
359 score > target_score
360 } else {
361 score < target_score
362 };
363 if better {
364 strictly_better += 1;
365 }
366 }
367 Ok(Some(strictly_better + 1))
368 }
369
370 fn execute_alter_metric(
371 &self,
372 raw_query: &str,
373 query: &crate::storage::query::ast::AlterMetricQuery,
374 ) -> RedDBResult<RuntimeQueryResult> {
375 self.check_write(crate::runtime::write_gate::WriteKind::Ddl)?;
376 let store = self.inner.db.store();
377 super::metric_descriptor_catalog::update(
378 store.as_ref(),
379 &query.path,
380 query.set_role.as_deref(),
381 query.attempted_kind.as_deref(),
382 query.attempted_path.as_deref(),
383 )?;
384 self.invalidate_result_cache();
385 Ok(RuntimeQueryResult::ok_message(
386 raw_query.to_string(),
387 &format!("metric descriptor '{}' updated", query.path),
388 "alter",
389 ))
390 }
391
392 fn execute_create_slo(
393 &self,
394 raw_query: &str,
395 query: &crate::storage::query::ast::CreateSloQuery,
396 ) -> RedDBResult<RuntimeQueryResult> {
397 self.check_write(crate::runtime::write_gate::WriteKind::Ddl)?;
398 let store = self.inner.db.store();
399 super::slo_descriptor_catalog::create(
400 store.as_ref(),
401 &query.path,
402 &query.metric_path,
403 query.target,
404 query.window_ms,
405 )?;
406 self.invalidate_result_cache();
407 Ok(RuntimeQueryResult::ok_message(
408 raw_query.to_string(),
409 &format!("SLO descriptor '{}' created", query.path),
410 "create",
411 ))
412 }
413
414 fn execute_create_analytics_source(
415 &self,
416 raw_query: &str,
417 query: super::analytics_source_catalog::CreateAnalyticsSourceProfile,
418 ) -> RedDBResult<RuntimeQueryResult> {
419 self.check_write(crate::runtime::write_gate::WriteKind::Ddl)?;
420 let store = self.inner.db.store();
421 let profile = super::analytics_source_catalog::create(
422 store.as_ref(),
423 &self.inner.db.collection_contracts(),
424 query,
425 )?;
426 self.invalidate_result_cache();
427 Ok(RuntimeQueryResult::ok_message(
428 raw_query.to_string(),
429 &format!("analytics source '{}' created", profile.name),
430 "create",
431 ))
432 }
433}
434
435fn query_control_event_specs(expr: &QueryExpr) -> Vec<QueryControlEventSpec> {
436 use crate::runtime::control_events::{EventKind, Sensitivity};
437
438 let mut specs = Vec::new();
439 let mut schema = |action: &'static str, resource: Option<String>| {
440 specs.push(QueryControlEventSpec {
441 kind: EventKind::SchemaDdl,
442 action,
443 resource,
444 fields: Vec::new(),
445 });
446 };
447 match expr {
448 QueryExpr::CreateTable(q) => {
449 schema("create_table", Some(format!("table:{}", q.name)));
450 if let Some(column) = &q.tenant_by {
451 specs.push(QueryControlEventSpec {
452 kind: EventKind::TenantGovernance,
453 action: "create_table_tenant_by",
454 resource: Some(format!("table:{}", q.name)),
455 fields: vec![("tenant_column".to_string(), Sensitivity::raw(column))],
456 });
457 }
458 }
459 QueryExpr::CreateCollection(q) => {
460 schema("create_collection", Some(format!("collection:{}", q.name)));
461 }
462 QueryExpr::CreateVector(q) => schema("create_vector", Some(format!("vector:{}", q.name))),
463 QueryExpr::DropTable(q) => schema("drop_table", Some(format!("table:{}", q.name))),
464 QueryExpr::DropGraph(q) => schema("drop_graph", Some(format!("graph:{}", q.name))),
465 QueryExpr::DropVector(q) => schema("drop_vector", Some(format!("vector:{}", q.name))),
466 QueryExpr::DropDocument(q) => {
467 schema("drop_document", Some(format!("document:{}", q.name)));
468 }
469 QueryExpr::DropKv(q) => schema("drop_kv", Some(format!("kv:{}", q.name))),
470 QueryExpr::DropCollection(q) => {
471 schema("drop_collection", Some(format!("collection:{}", q.name)));
472 }
473 QueryExpr::Truncate(q) => schema("truncate", Some(format!("collection:{}", q.name))),
474 QueryExpr::AlterTable(q) => {
475 schema("alter_table", Some(format!("table:{}", q.name)));
476 for op in &q.operations {
477 match op {
478 crate::storage::query::ast::AlterOperation::EnableRowLevelSecurity => {
479 specs.push(QueryControlEventSpec {
480 kind: EventKind::RlsGovernance,
481 action: "enable_rls",
482 resource: Some(format!("table:{}", q.name)),
483 fields: Vec::new(),
484 });
485 }
486 crate::storage::query::ast::AlterOperation::DisableRowLevelSecurity => {
487 specs.push(QueryControlEventSpec {
488 kind: EventKind::RlsGovernance,
489 action: "disable_rls",
490 resource: Some(format!("table:{}", q.name)),
491 fields: Vec::new(),
492 });
493 }
494 crate::storage::query::ast::AlterOperation::EnableTenancy { column } => {
495 specs.push(QueryControlEventSpec {
496 kind: EventKind::TenantGovernance,
497 action: "enable_tenancy",
498 resource: Some(format!("table:{}", q.name)),
499 fields: vec![("tenant_column".to_string(), Sensitivity::raw(column))],
500 });
501 }
502 crate::storage::query::ast::AlterOperation::DisableTenancy => {
503 specs.push(QueryControlEventSpec {
504 kind: EventKind::TenantGovernance,
505 action: "disable_tenancy",
506 resource: Some(format!("table:{}", q.name)),
507 fields: Vec::new(),
508 });
509 }
510 _ => {}
511 }
512 }
513 }
514 QueryExpr::CreateIndex(q) => {
515 schema(
516 "create_index",
517 Some(format!("index:{}:{}", q.table, q.name)),
518 );
519 }
520 QueryExpr::DropIndex(q) => {
521 schema("drop_index", Some(format!("index:{}:{}", q.table, q.name)));
522 }
523 QueryExpr::CreateTimeSeries(q) => {
524 schema("create_timeseries", Some(format!("timeseries:{}", q.name)));
525 }
526 QueryExpr::CreateMetric(q) => {
527 schema("create_metric", Some(format!("metric:{}", q.path)));
528 }
529 QueryExpr::AlterMetric(q) => {
530 schema("alter_metric", Some(format!("metric:{}", q.path)));
531 }
532 QueryExpr::CreateSlo(q) => {
533 schema("create_slo", Some(format!("slo:{}", q.path)));
534 }
535 QueryExpr::DropTimeSeries(q) => {
536 schema("drop_timeseries", Some(format!("timeseries:{}", q.name)));
537 }
538 QueryExpr::CreateQueue(q) => schema("create_queue", Some(format!("queue:{}", q.name))),
539 QueryExpr::AlterQueue(q) => schema("alter_queue", Some(format!("queue:{}", q.name))),
540 QueryExpr::DropQueue(q) => schema("drop_queue", Some(format!("queue:{}", q.name))),
541 QueryExpr::CreateTree(q) => {
542 schema(
543 "create_tree",
544 Some(format!("tree:{}:{}", q.collection, q.name)),
545 );
546 }
547 QueryExpr::DropTree(q) => {
548 schema(
549 "drop_tree",
550 Some(format!("tree:{}:{}", q.collection, q.name)),
551 );
552 }
553 QueryExpr::CreateSchema(q) => schema("create_schema", Some(format!("schema:{}", q.name))),
554 QueryExpr::DropSchema(q) => schema("drop_schema", Some(format!("schema:{}", q.name))),
555 QueryExpr::CreateSequence(q) => {
556 schema("create_sequence", Some(format!("sequence:{}", q.name)));
557 }
558 QueryExpr::DropSequence(q) => schema("drop_sequence", Some(format!("sequence:{}", q.name))),
559 QueryExpr::CreateView(q) => schema("create_view", Some(format!("view:{}", q.name))),
560 QueryExpr::DropView(q) => schema("drop_view", Some(format!("view:{}", q.name))),
561 QueryExpr::RefreshMaterializedView(q) => {
562 schema(
563 "refresh_materialized_view",
564 Some(format!("view:{}", q.name)),
565 );
566 }
567 QueryExpr::CreatePolicy(q) => {
568 specs.push(QueryControlEventSpec {
569 kind: EventKind::RlsGovernance,
570 action: "create_policy",
571 resource: Some(format!("table:{}:policy:{}", q.table, q.name)),
572 fields: vec![(
573 "target_kind".to_string(),
574 Sensitivity::raw(q.target_kind.as_ident()),
575 )],
576 });
577 }
578 QueryExpr::DropPolicy(q) => {
579 specs.push(QueryControlEventSpec {
580 kind: EventKind::RlsGovernance,
581 action: "drop_policy",
582 resource: Some(format!("table:{}:policy:{}", q.table, q.name)),
583 fields: Vec::new(),
584 });
585 }
586 QueryExpr::SetTenant(value) => {
587 let mut fields = Vec::new();
588 if let Some(value) = value {
589 fields.push(("tenant".to_string(), Sensitivity::raw(value)));
590 }
591 specs.push(QueryControlEventSpec {
592 kind: EventKind::TenantGovernance,
593 action: "set_tenant",
594 resource: Some("tenant:session".to_string()),
595 fields,
596 });
597 }
598 QueryExpr::SetConfig { key, .. } => {
599 specs.push(QueryControlEventSpec {
600 kind: EventKind::ConfigWrite,
601 action: "config:write",
602 resource: Some(format!("config:{key}")),
603 fields: vec![("key".to_string(), Sensitivity::raw(key))],
604 });
605 }
606 QueryExpr::ConfigCommand(cmd) => match cmd {
607 crate::storage::query::ast::ConfigCommand::Put {
608 collection, key, ..
609 }
610 | crate::storage::query::ast::ConfigCommand::Rotate {
611 collection, key, ..
612 } => {
613 let target = format!("{collection}/{key}");
614 specs.push(QueryControlEventSpec {
615 kind: EventKind::ConfigWrite,
616 action: "config:write",
617 resource: Some(format!("config:{target}")),
618 fields: vec![
619 ("collection".to_string(), Sensitivity::raw(collection)),
620 ("key".to_string(), Sensitivity::raw(key)),
621 ],
622 });
623 }
624 crate::storage::query::ast::ConfigCommand::Delete { collection, key } => {
625 let target = format!("{collection}/{key}");
626 specs.push(QueryControlEventSpec {
627 kind: EventKind::ConfigDelete,
628 action: "config:write",
629 resource: Some(format!("config:{target}")),
630 fields: vec![
631 ("collection".to_string(), Sensitivity::raw(collection)),
632 ("key".to_string(), Sensitivity::raw(key)),
633 ],
634 });
635 }
636 _ => {}
637 },
638 QueryExpr::AlterUser(stmt) => {
639 let disables = stmt.attributes.iter().any(|attr| {
640 matches!(
641 attr,
642 crate::storage::query::ast::AlterUserAttribute::Disable
643 )
644 });
645 specs.push(QueryControlEventSpec {
646 kind: if disables {
647 EventKind::UserDisable
648 } else {
649 EventKind::UserUpdate
650 },
651 action: "alter_user",
652 resource: Some(format!("user:{}", stmt.username)),
653 fields: Vec::new(),
654 });
655 }
656 _ => {}
657 }
658 specs
659}
660
661fn control_event_outcome_for_error(err: &RedDBError) -> crate::runtime::control_events::Outcome {
662 match err {
663 RedDBError::ReadOnly(_) => crate::runtime::control_events::Outcome::Denied,
664 RedDBError::Query(msg)
665 if msg.contains("permission denied")
666 || msg.contains("cannot issue")
667 || msg.contains("lacks") =>
668 {
669 crate::runtime::control_events::Outcome::Denied
670 }
671 _ => crate::runtime::control_events::Outcome::Error,
672 }
673}
674
675fn view_records_to_entities(
684 table: &str,
685 records: &[crate::storage::query::unified::UnifiedRecord],
686) -> Vec<crate::storage::UnifiedEntity> {
687 use std::collections::HashMap;
688 let table_arc: std::sync::Arc<str> = std::sync::Arc::from(table);
689 let mut out = Vec::with_capacity(records.len());
690 for record in records {
691 let mut named: HashMap<String, crate::storage::schema::Value> = HashMap::new();
692 for (name, value) in record.iter_fields() {
693 named.insert(name.to_string(), value.clone());
694 }
695 let entity = crate::storage::UnifiedEntity::new(
696 crate::storage::EntityId::new(0),
697 crate::storage::EntityKind::TableRow {
698 table: std::sync::Arc::clone(&table_arc),
699 row_id: 0,
700 },
701 crate::storage::EntityData::Row(crate::storage::RowData {
702 columns: Vec::new(),
703 named: Some(named),
704 schema: None,
705 }),
706 );
707 out.push(entity);
708 }
709 out
710}
711
712fn system_keyed_collection_contract(
713 name: &str,
714 model: crate::catalog::CollectionModel,
715) -> crate::physical::CollectionContract {
716 let now = crate::utils::now_unix_millis() as u128;
717 crate::physical::CollectionContract {
718 name: name.to_string(),
719 declared_model: model,
720 schema_mode: crate::catalog::SchemaMode::Dynamic,
721 origin: crate::physical::ContractOrigin::Implicit,
722 version: 1,
723 created_at_unix_ms: now,
724 updated_at_unix_ms: now,
725 default_ttl_ms: None,
726 vector_dimension: None,
727 vector_metric: None,
728 context_index_fields: Vec::new(),
729 declared_columns: Vec::new(),
730 table_def: None,
731 timestamps_enabled: false,
732 context_index_enabled: false,
733 metrics_raw_retention_ms: None,
734 metrics_rollup_policies: Vec::new(),
735 metrics_tenant_identity: None,
736 metrics_namespace: None,
737 append_only: false,
738 subscriptions: Vec::new(),
739 analytics_config: Vec::new(),
740 session_key: None,
741 session_gap_ms: None,
742 retention_duration_ms: None,
743 analytical_storage: None,
744 }
745}
746
747#[derive(Clone)]
762pub struct SnapshotContext {
763 pub snapshot: crate::storage::transaction::snapshot::Snapshot,
764 pub manager: Arc<crate::storage::transaction::snapshot::SnapshotManager>,
765 pub own_xids: std::collections::HashSet<crate::storage::transaction::snapshot::Xid>,
766 pub requires_index_fallback: bool,
767}
768
769pub fn set_current_connection_id(id: u64) {
778 CURRENT_CONN_ID.with(|c| c.set(id));
779}
780
781pub fn clear_current_connection_id() {
783 CURRENT_CONN_ID.with(|c| c.set(0));
784}
785
786pub fn current_connection_id() -> u64 {
789 CURRENT_CONN_ID.with(|c| c.get())
790}
791
792pub fn set_current_auth_identity(username: String, role: crate::auth::Role) {
796 CURRENT_AUTH_IDENTITY.with(|cell| *cell.borrow_mut() = Some((username, role)));
797}
798
799pub fn clear_current_auth_identity() {
803 CURRENT_AUTH_IDENTITY.with(|cell| *cell.borrow_mut() = None);
804}
805
806pub(crate) fn current_auth_identity() -> Option<(String, crate::auth::Role)> {
809 CURRENT_AUTH_IDENTITY.with(|cell| cell.borrow().clone())
810}
811
812pub fn current_auth_identity_for_audit() -> Option<(String, crate::auth::Role)> {
816 current_auth_identity()
817}
818
819pub fn set_current_tenant(tenant_id: String) {
824 CURRENT_TENANT_ID.with(|cell| *cell.borrow_mut() = Some(tenant_id));
825}
826
827pub fn clear_current_tenant() {
830 CURRENT_TENANT_ID.with(|cell| *cell.borrow_mut() = None);
831}
832
833pub fn current_tenant() -> Option<String> {
844 let inherited = CURRENT_TENANT_ID.with(|cell| cell.borrow().clone());
845 if let Some(over) = current_scope_override() {
846 if over.tenant.is_active() {
847 return over.tenant.resolve(inherited);
848 }
849 }
850 if let Some(tx_local) = current_tx_local_tenant() {
851 return tx_local;
852 }
853 inherited
854}
855
856thread_local! {
857 static TX_LOCAL_TENANT: std::cell::RefCell<Option<Option<String>>> =
866 const { std::cell::RefCell::new(None) };
867}
868
869fn current_tx_local_tenant() -> Option<Option<String>> {
870 TX_LOCAL_TENANT.with(|cell| cell.borrow().clone())
871}
872
873fn parse_set_local_tenant(query: &str) -> RedDBResult<Option<Option<String>>> {
879 let mut tokens = query.split_ascii_whitespace();
880 let Some(w1) = tokens.next() else {
881 return Ok(None);
882 };
883 if !w1.eq_ignore_ascii_case("SET") {
884 return Ok(None);
885 }
886 let Some(w2) = tokens.next() else {
887 return Ok(None);
888 };
889 if !w2.eq_ignore_ascii_case("LOCAL") {
890 return Ok(None);
891 }
892 let Some(w3) = tokens.next() else {
893 return Ok(None);
894 };
895 if !w3.eq_ignore_ascii_case("TENANT") {
896 return Ok(None);
897 }
898 let rest: String = tokens.collect::<Vec<_>>().join(" ");
899 let rest = rest.trim().trim_end_matches(';').trim();
900 let value_str = rest.strip_prefix('=').map(|s| s.trim()).unwrap_or(rest);
901 if value_str.is_empty() {
902 return Err(RedDBError::Query(
903 "SET LOCAL TENANT expects a string literal or NULL".to_string(),
904 ));
905 }
906 if value_str.eq_ignore_ascii_case("NULL") {
907 return Ok(Some(None));
908 }
909 if value_str.starts_with('\'') && value_str.ends_with('\'') && value_str.len() >= 2 {
910 let inner = &value_str[1..value_str.len() - 1];
911 return Ok(Some(Some(inner.to_string())));
912 }
913 Err(RedDBError::Query(format!(
914 "SET LOCAL TENANT expects a string literal or NULL, got `{value_str}`"
915 )))
916}
917
918pub(crate) struct TxLocalTenantGuard;
919
920impl TxLocalTenantGuard {
921 pub fn install(value: Option<Option<String>>) -> Self {
922 TX_LOCAL_TENANT.with(|cell| *cell.borrow_mut() = value);
923 Self
924 }
925}
926
927impl Drop for TxLocalTenantGuard {
928 fn drop(&mut self) {
929 TX_LOCAL_TENANT.with(|cell| *cell.borrow_mut() = None);
930 }
931}
932
933thread_local! {
934 static SCOPE_OVERRIDES: std::cell::RefCell<Vec<crate::runtime::within_clause::ScopeOverride>> =
941 const { std::cell::RefCell::new(Vec::new()) };
942}
943
944pub(crate) fn push_scope_override(over: crate::runtime::within_clause::ScopeOverride) {
945 SCOPE_OVERRIDES.with(|cell| cell.borrow_mut().push(over));
946}
947
948pub(crate) fn pop_scope_override() {
949 SCOPE_OVERRIDES.with(|cell| {
950 cell.borrow_mut().pop();
951 });
952}
953
954pub(crate) fn current_scope_override() -> Option<crate::runtime::within_clause::ScopeOverride> {
955 SCOPE_OVERRIDES.with(|cell| cell.borrow().last().cloned())
956}
957
958pub(crate) fn has_scope_override_active() -> bool {
962 SCOPE_OVERRIDES.with(|cell| !cell.borrow().is_empty())
963}
964
965pub(crate) struct ScopeOverrideGuard;
969
970impl ScopeOverrideGuard {
971 pub fn install(over: crate::runtime::within_clause::ScopeOverride) -> Self {
972 push_scope_override(over);
973 Self
974 }
975}
976
977impl Drop for ScopeOverrideGuard {
978 fn drop(&mut self) {
979 pop_scope_override();
980 }
981}
982
983pub(crate) fn current_user_projected() -> Option<String> {
989 let inherited = current_auth_identity().map(|(u, _)| u);
990 if let Some(over) = current_scope_override() {
991 if over.user.is_active() {
992 return over.user.resolve(inherited);
993 }
994 }
995 inherited
996}
997
998pub(crate) fn current_role_projected() -> Option<String> {
999 let inherited = current_auth_identity().map(|(_, r)| format!("{r:?}").to_lowercase());
1000 if let Some(over) = current_scope_override() {
1001 if over.role.is_active() {
1002 return over.role.resolve(inherited);
1003 }
1004 }
1005 inherited
1006}
1007
1008pub(crate) fn current_secret_value(path: &str) -> Option<String> {
1009 let key = path.to_ascii_lowercase();
1010 CURRENT_SECRET_RESOLVER.with(|cell| {
1011 let mut resolver = cell.borrow_mut();
1012 let resolver = resolver.as_mut()?;
1013 if resolver.values.is_none() {
1014 resolver.values = resolver
1015 .store
1016 .as_ref()
1017 .map(|store| store.vault_kv_snapshot());
1018 }
1019 let values = resolver.values.as_ref()?;
1020 values.get(&key).cloned().or_else(|| {
1021 key.strip_prefix("red.vault/").and_then(|rest| {
1022 values
1023 .get(rest)
1024 .cloned()
1025 .or_else(|| values.get(&format!("red.secret.{rest}")).cloned())
1026 })
1027 })
1028 })
1029}
1030
1031struct SecretResolver {
1032 store: Option<Arc<crate::auth::store::AuthStore>>,
1033 values: Option<HashMap<String, String>>,
1034}
1035
1036pub(super) struct SecretStoreGuard {
1037 previous: Option<SecretResolver>,
1038}
1039
1040impl SecretStoreGuard {
1041 pub(super) fn install(store: Option<Arc<crate::auth::store::AuthStore>>) -> Self {
1042 let previous = CURRENT_SECRET_RESOLVER.with(|cell| {
1043 cell.replace(Some(SecretResolver {
1044 store,
1045 values: None,
1046 }))
1047 });
1048 Self { previous }
1049 }
1050}
1051
1052impl Drop for SecretStoreGuard {
1053 fn drop(&mut self) {
1054 let previous = self.previous.take();
1055 CURRENT_SECRET_RESOLVER.with(|cell| {
1056 cell.replace(previous);
1057 });
1058 }
1059}
1060
1061pub(crate) fn current_config_value(path: &str) -> Option<Value> {
1062 let key = path.to_ascii_lowercase();
1063 CURRENT_CONFIG_RESOLVER.with(|cell| {
1064 let mut resolver = cell.borrow_mut();
1065 let resolver = resolver.as_mut()?;
1066 if resolver.values.is_none() {
1067 resolver.values = Some(latest_config_snapshot(&resolver.db));
1068 }
1069 let values = resolver.values.as_ref()?;
1070 values.get(&key).cloned().or_else(|| {
1071 key.strip_prefix("red.config/")
1072 .and_then(|rest| values.get(&format!("red.config.{rest}")).cloned())
1073 })
1074 })
1075}
1076
1077fn update_current_config_value(path: &str, value: Value) {
1078 let key = path.to_ascii_lowercase();
1079 CURRENT_CONFIG_RESOLVER.with(|cell| {
1080 if let Some(resolver) = cell.borrow_mut().as_mut() {
1081 if let Some(values) = resolver.values.as_mut() {
1082 values.insert(key, value);
1083 }
1084 }
1085 });
1086}
1087
1088fn update_current_secret_value(path: &str, value: Option<String>) {
1089 let key = path.to_ascii_lowercase();
1090 CURRENT_SECRET_RESOLVER.with(|cell| {
1091 if let Some(resolver) = cell.borrow_mut().as_mut() {
1092 let Some(values) = resolver.values.as_mut() else {
1093 return;
1094 };
1095 match value {
1096 Some(value) => {
1097 values.insert(key, value);
1098 }
1099 None => {
1100 values.remove(&key);
1101 }
1102 }
1103 }
1104 });
1105}
1106
1107fn latest_config_snapshot(db: &RedDB) -> HashMap<String, Value> {
1108 let mut latest: HashMap<String, (u64, Value)> = HashMap::new();
1109
1110 if let Some(manager) = db.store().get_collection("red_config") {
1111 manager.for_each_entity(|entity| {
1112 let Some(row) = entity.data.as_row() else {
1113 return true;
1114 };
1115 let Some(Value::Text(key)) = row.get_field("key") else {
1116 return true;
1117 };
1118 let value = row.get_field("value").cloned().unwrap_or(Value::Null);
1119 let id = entity.id.raw();
1120 let key = key.to_ascii_lowercase();
1121 insert_latest_config_value(&mut latest, key.clone(), id, value.clone());
1122 if let Some(rest) = key.strip_prefix("red.config.") {
1123 insert_latest_config_value(&mut latest, format!("red.config/{rest}"), id, value);
1124 }
1125 true
1126 });
1127 }
1128
1129 if let Some(manager) = db.store().get_collection("red.config") {
1130 manager.for_each_entity(|entity| {
1131 let Some(row) = entity.data.as_row() else {
1132 return true;
1133 };
1134 if matches!(row.get_field("tombstone"), Some(Value::Boolean(true))) {
1135 return true;
1136 }
1137 let Some(Value::Text(key)) = row.get_field("key") else {
1138 return true;
1139 };
1140 let value = row.get_field("value").cloned().unwrap_or(Value::Null);
1141 insert_latest_config_value(
1142 &mut latest,
1143 format!("red.config/{}", key.to_ascii_lowercase()),
1144 entity.id.raw(),
1145 value,
1146 );
1147 true
1148 });
1149 }
1150
1151 latest
1152 .into_iter()
1153 .map(|(key, (_, value))| (key, value))
1154 .collect()
1155}
1156
1157fn insert_latest_config_value(
1158 latest: &mut HashMap<String, (u64, Value)>,
1159 key: String,
1160 id: u64,
1161 value: Value,
1162) {
1163 match latest.get(&key) {
1164 Some((prev_id, _)) if *prev_id > id => {}
1165 _ => {
1166 latest.insert(key, (id, value));
1167 }
1168 }
1169}
1170
1171struct ConfigResolver {
1172 db: Arc<RedDB>,
1173 values: Option<HashMap<String, Value>>,
1174}
1175
1176pub(super) struct ConfigSnapshotGuard {
1177 previous: Option<ConfigResolver>,
1178}
1179
1180impl ConfigSnapshotGuard {
1181 pub(super) fn install(db: Arc<RedDB>) -> Self {
1182 let previous = CURRENT_CONFIG_RESOLVER
1183 .with(|cell| cell.replace(Some(ConfigResolver { db, values: None })));
1184 Self { previous }
1185 }
1186}
1187
1188impl Drop for ConfigSnapshotGuard {
1189 fn drop(&mut self) {
1190 let previous = self.previous.take();
1191 CURRENT_CONFIG_RESOLVER.with(|cell| {
1192 cell.replace(previous);
1193 });
1194 }
1195}
1196
1197pub fn set_current_snapshot(ctx: SnapshotContext) {
1202 CURRENT_SNAPSHOT.with(|cell| *cell.borrow_mut() = Some(ctx));
1203 HAS_SNAPSHOT.with(|c| c.set(true));
1204}
1205
1206pub fn clear_current_snapshot() {
1207 CURRENT_SNAPSHOT.with(|cell| *cell.borrow_mut() = None);
1208 HAS_SNAPSHOT.with(|c| c.set(false));
1209}
1210
1211pub(crate) struct CurrentSnapshotGuard {
1217 previous: Option<SnapshotContext>,
1218}
1219
1220impl CurrentSnapshotGuard {
1221 pub(crate) fn install(ctx: SnapshotContext) -> Self {
1222 let previous = CURRENT_SNAPSHOT.with(|cell| cell.borrow().clone());
1223 set_current_snapshot(ctx);
1224 Self { previous }
1225 }
1226}
1227
1228impl Drop for CurrentSnapshotGuard {
1229 fn drop(&mut self) {
1230 let prev = self.previous.take();
1231 let has = prev.is_some();
1232 CURRENT_SNAPSHOT.with(|cell| *cell.borrow_mut() = prev);
1233 HAS_SNAPSHOT.with(|c| c.set(has));
1234 }
1235}
1236
1237#[inline]
1248pub fn entity_visible_under_current_snapshot(
1249 entity: &crate::storage::unified::entity::UnifiedEntity,
1250) -> bool {
1251 if !HAS_SNAPSHOT.with(|c| c.get()) {
1257 return entity.xmax == 0;
1258 }
1259 CURRENT_SNAPSHOT.with(|cell| {
1260 let guard = cell.borrow();
1261 let Some(ctx) = guard.as_ref() else {
1262 return true;
1263 };
1264 visibility_check(ctx, entity.xmin, entity.xmax)
1265 })
1266}
1267
1268#[inline]
1273pub(crate) fn xids_visible_under_current_snapshot(xmin: u64, xmax: u64) -> bool {
1274 if !HAS_SNAPSHOT.with(|c| c.get()) {
1275 return true;
1276 }
1277 CURRENT_SNAPSHOT.with(|cell| {
1278 let guard = cell.borrow();
1279 let Some(ctx) = guard.as_ref() else {
1280 return true;
1281 };
1282 visibility_check(ctx, xmin, xmax)
1283 })
1284}
1285
1286pub fn capture_current_snapshot() -> Option<SnapshotContext> {
1293 CURRENT_SNAPSHOT.with(|cell| cell.borrow().clone())
1294}
1295
1296pub(crate) fn current_snapshot_requires_index_fallback() -> bool {
1301 if !HAS_SNAPSHOT.with(|c| c.get()) {
1302 return false;
1303 }
1304 CURRENT_SNAPSHOT.with(|cell| {
1305 cell.borrow()
1306 .as_ref()
1307 .is_some_and(|ctx| ctx.requires_index_fallback)
1308 })
1309}
1310
1311#[derive(Clone, Default)]
1326pub struct SnapshotBundle {
1327 pub snapshot: Option<SnapshotContext>,
1328 pub auth: Option<(String, crate::auth::Role)>,
1329 pub tenant: Option<String>,
1330}
1331
1332pub fn snapshot_bundle() -> SnapshotBundle {
1335 SnapshotBundle {
1336 snapshot: capture_current_snapshot(),
1337 auth: current_auth_identity(),
1338 tenant: CURRENT_TENANT_ID.with(|cell| cell.borrow().clone()),
1339 }
1340}
1341
1342pub fn with_snapshot_bundle<R>(bundle: &SnapshotBundle, f: impl FnOnce() -> R) -> R {
1347 struct Guard {
1348 prev_snapshot: Option<SnapshotContext>,
1349 prev_auth: Option<(String, crate::auth::Role)>,
1350 prev_tenant: Option<String>,
1351 }
1352 impl Drop for Guard {
1353 fn drop(&mut self) {
1354 let snap = self.prev_snapshot.take();
1355 let has = snap.is_some();
1356 CURRENT_SNAPSHOT.with(|cell| *cell.borrow_mut() = snap);
1357 HAS_SNAPSHOT.with(|c| c.set(has));
1358 CURRENT_AUTH_IDENTITY.with(|cell| *cell.borrow_mut() = self.prev_auth.take());
1359 CURRENT_TENANT_ID.with(|cell| *cell.borrow_mut() = self.prev_tenant.take());
1360 }
1361 }
1362
1363 let _guard = {
1364 let prev_snapshot = CURRENT_SNAPSHOT.with(|cell| cell.borrow().clone());
1365 let prev_auth = CURRENT_AUTH_IDENTITY.with(|cell| cell.borrow().clone());
1366 let prev_tenant = CURRENT_TENANT_ID.with(|cell| cell.borrow().clone());
1367
1368 match bundle.snapshot.clone() {
1369 Some(ctx) => set_current_snapshot(ctx),
1370 None => clear_current_snapshot(),
1371 }
1372 CURRENT_AUTH_IDENTITY.with(|cell| *cell.borrow_mut() = bundle.auth.clone());
1373 CURRENT_TENANT_ID.with(|cell| *cell.borrow_mut() = bundle.tenant.clone());
1374
1375 Guard {
1376 prev_snapshot,
1377 prev_auth,
1378 prev_tenant,
1379 }
1380 };
1381 f()
1382}
1383
1384#[inline]
1388pub fn entity_visible_with_context(
1389 ctx: Option<&SnapshotContext>,
1390 entity: &crate::storage::unified::entity::UnifiedEntity,
1391) -> bool {
1392 match ctx {
1393 Some(ctx) => visibility_check(ctx, entity.xmin, entity.xmax),
1394 None => true,
1395 }
1396}
1397
1398fn table_row_index_fields(
1399 entity: &crate::storage::unified::entity::UnifiedEntity,
1400) -> Vec<(String, crate::storage::schema::Value)> {
1401 let crate::storage::EntityData::Row(row) = &entity.data else {
1402 return Vec::new();
1403 };
1404 if let Some(named) = &row.named {
1405 return named
1406 .iter()
1407 .map(|(name, value)| (name.clone(), value.clone()))
1408 .collect();
1409 }
1410 if let Some(schema) = &row.schema {
1411 return schema
1412 .iter()
1413 .zip(row.columns.iter())
1414 .map(|(name, value)| (name.clone(), value.clone()))
1415 .collect();
1416 }
1417 Vec::new()
1418}
1419
1420#[inline]
1421fn visibility_check(ctx: &SnapshotContext, xmin: u64, xmax: u64) -> bool {
1422 if xmin != 0 && ctx.manager.is_aborted(xmin) {
1426 return false;
1427 }
1428 let effective_xmax = if xmax != 0 && ctx.manager.is_aborted(xmax) {
1430 0
1431 } else {
1432 xmax
1433 };
1434 let own_xmin = xmin != 0 && ctx.own_xids.contains(&xmin);
1438 let own_xmax = effective_xmax != 0 && ctx.own_xids.contains(&effective_xmax);
1439 if own_xmax {
1440 return false;
1442 }
1443 if own_xmin {
1444 return true;
1445 }
1446 ctx.snapshot.sees(xmin, effective_xmax)
1447}
1448
1449fn runtime_pool_lock(runtime: &RedDBRuntime) -> std::sync::MutexGuard<'_, PoolState> {
1450 runtime
1451 .inner
1452 .pool
1453 .lock()
1454 .unwrap_or_else(|poisoned| poisoned.into_inner())
1455}
1456
1457fn is_graph_tvf_name(name: &str) -> bool {
1461 name.eq_ignore_ascii_case("components")
1462 || name.eq_ignore_ascii_case("louvain")
1463 || name.eq_ignore_ascii_case("degree_centrality")
1464 || name.eq_ignore_ascii_case("shortest_path")
1465 || name.eq_ignore_ascii_case("betweenness")
1466 || name.eq_ignore_ascii_case("eigenvector")
1467 || name.eq_ignore_ascii_case("pagerank")
1468}
1469
1470fn analytics_view_algorithm(
1477 graph: &str,
1478 view: &crate::catalog::AnalyticsViewDescriptor,
1479) -> RedDBResult<(String, Vec<(String, f64)>)> {
1480 use crate::catalog::AnalyticsOutput;
1481
1482 let mut named_args: Vec<(String, f64)> = Vec::new();
1483 let algorithm = match view.output {
1484 AnalyticsOutput::Communities => {
1485 let algo = view.algorithm.as_deref().unwrap_or("louvain");
1486 if !algo.eq_ignore_ascii_case("louvain") {
1487 return Err(RedDBError::Query(format!(
1488 "analytics output 'communities' on graph '{graph}' has unsupported algorithm '{algo}' (expected louvain)"
1489 )));
1490 }
1491 if let Some(resolution) = view.resolution {
1492 named_args.push(("resolution".to_string(), resolution));
1493 }
1494 "louvain".to_string()
1495 }
1496 AnalyticsOutput::Components => {
1497 if let Some(algo) = view.algorithm.as_deref() {
1498 if !algo.eq_ignore_ascii_case("components")
1499 && !algo.eq_ignore_ascii_case("connected_components")
1500 {
1501 return Err(RedDBError::Query(format!(
1502 "analytics output 'components' on graph '{graph}' has unsupported algorithm '{algo}' (expected connected_components)"
1503 )));
1504 }
1505 }
1506 "components".to_string()
1507 }
1508 AnalyticsOutput::Centrality => {
1509 let algo = view
1510 .algorithm
1511 .as_deref()
1512 .unwrap_or("pagerank")
1513 .to_ascii_lowercase();
1514 match algo.as_str() {
1515 "pagerank" => {
1516 if let Some(max_iterations) = view.max_iterations {
1517 named_args.push(("max_iterations".to_string(), max_iterations as f64));
1518 }
1519 }
1520 "eigenvector" => {
1521 if let Some(max_iterations) = view.max_iterations {
1522 named_args.push(("max_iterations".to_string(), max_iterations as f64));
1523 }
1524 if let Some(tolerance) = view.tolerance {
1525 named_args.push(("tolerance".to_string(), tolerance));
1526 }
1527 }
1528 "betweenness" => {}
1529 other => {
1530 return Err(RedDBError::Query(format!(
1531 "analytics output 'centrality' on graph '{graph}' has unsupported algorithm '{other}' (expected pagerank, betweenness, or eigenvector)"
1532 )));
1533 }
1534 }
1535 algo
1536 }
1537 };
1538 Ok((algorithm, named_args))
1539}
1540
1541fn reject_named_args(name: &str, named_args: &[(String, f64)]) -> RedDBResult<()> {
1543 if let Some((key, _)) = named_args.first() {
1544 return Err(RedDBError::Query(format!(
1545 "table function '{name}' has no named argument '{key}'"
1546 )));
1547 }
1548 Ok(())
1549}
1550
1551fn louvain_resolution(named_args: &[(String, f64)]) -> RedDBResult<f64> {
1554 let mut resolution = 1.0_f64;
1555 for (key, value) in named_args {
1556 if key.eq_ignore_ascii_case("resolution") {
1557 if !value.is_finite() || *value <= 0.0 {
1558 return Err(RedDBError::Query(format!(
1559 "table function 'louvain' resolution must be > 0, got {value}"
1560 )));
1561 }
1562 resolution = *value;
1563 } else {
1564 return Err(RedDBError::Query(format!(
1565 "table function 'louvain' has no named argument '{key}' (expected 'resolution')"
1566 )));
1567 }
1568 }
1569 Ok(resolution)
1570}
1571
1572fn abstract_degree_centrality(
1577 nodes: &[String],
1578 edges: &[(
1579 String,
1580 String,
1581 crate::storage::engine::graph_algorithms::Weight,
1582 )],
1583) -> Vec<(String, usize)> {
1584 let mut degree: std::collections::BTreeMap<String, usize> = std::collections::BTreeMap::new();
1585 for n in nodes {
1586 degree.entry(n.clone()).or_insert(0);
1587 }
1588 for (a, b, _w) in edges {
1589 *degree.entry(a.clone()).or_insert(0) += 1;
1590 *degree.entry(b.clone()).or_insert(0) += 1;
1591 }
1592 degree.into_iter().collect()
1593}
1594
1595fn ordered_result_columns(result: &crate::storage::query::unified::UnifiedResult) -> Vec<String> {
1598 if !result.columns.is_empty() {
1599 return result.columns.clone();
1600 }
1601 result
1602 .records
1603 .first()
1604 .map(|record| {
1605 record
1606 .column_names()
1607 .iter()
1608 .map(|column| column.to_string())
1609 .collect()
1610 })
1611 .unwrap_or_default()
1612}
1613
1614fn value_to_node_id(value: &crate::storage::schema::Value) -> Option<String> {
1618 use crate::storage::schema::Value;
1619 match value {
1620 Value::Null => None,
1621 Value::Text(s) => Some(s.to_string()),
1622 Value::Integer(n) => Some(n.to_string()),
1623 Value::UnsignedInteger(n) => Some(n.to_string()),
1624 Value::NodeRef(s) => Some(s.clone()),
1625 other => Some(other.to_string()),
1626 }
1627}
1628
1629fn value_to_weight(value: &crate::storage::schema::Value) -> Option<f32> {
1631 use crate::storage::schema::Value;
1632 match value {
1633 Value::Float(f) => Some(*f as f32),
1634 Value::Integer(n) => Some(*n as f32),
1635 Value::UnsignedInteger(n) => Some(*n as f32),
1636 _ => None,
1637 }
1638}
1639
1640fn inline_node_ids(
1644 name: &str,
1645 result: &crate::storage::query::unified::UnifiedResult,
1646) -> RedDBResult<Vec<String>> {
1647 if result.records.is_empty() {
1648 return Ok(Vec::new());
1649 }
1650 let columns = ordered_result_columns(result);
1651 let Some(first_col) = columns.first() else {
1652 return Err(RedDBError::Query(format!(
1653 "table function '{name}' inline form: `nodes` subquery must project at least one column (the node id)"
1654 )));
1655 };
1656 let mut ids = Vec::with_capacity(result.records.len());
1657 for record in &result.records {
1658 if let Some(id) = record.get(first_col).and_then(value_to_node_id) {
1659 ids.push(id);
1660 }
1661 }
1662 Ok(ids)
1663}
1664
1665fn inline_edges(
1670 name: &str,
1671 result: &crate::storage::query::unified::UnifiedResult,
1672) -> RedDBResult<
1673 Vec<(
1674 String,
1675 String,
1676 crate::storage::engine::graph_algorithms::Weight,
1677 )>,
1678> {
1679 if result.records.is_empty() {
1680 return Ok(Vec::new());
1681 }
1682 let columns = ordered_result_columns(result);
1683 if columns.len() < 2 {
1684 return Err(RedDBError::Query(format!(
1685 "table function '{name}' inline form: `edges` subquery must project at least two columns (source, target), got {}",
1686 columns.len()
1687 )));
1688 }
1689 let src_col = &columns[0];
1690 let dst_col = &columns[1];
1691 let weight_col = columns.get(2);
1692 let mut edges = Vec::with_capacity(result.records.len());
1693 for record in &result.records {
1694 let (Some(src), Some(dst)) = (
1695 record.get(src_col).and_then(value_to_node_id),
1696 record.get(dst_col).and_then(value_to_node_id),
1697 ) else {
1698 continue;
1700 };
1701 let weight = match weight_col {
1702 Some(col) => match record.get(col) {
1703 None | Some(crate::storage::schema::Value::Null) => 1.0,
1704 Some(value) => value_to_weight(value).ok_or_else(|| {
1705 RedDBError::Query(format!(
1706 "table function '{name}' inline form: `edges` weight column must be numeric"
1707 ))
1708 })?,
1709 },
1710 None => 1.0,
1711 };
1712 edges.push((src, dst, weight));
1713 }
1714 Ok(edges)
1715}
1716
1717fn cache_scope_insert(scopes: &mut HashSet<String>, name: &str) {
1718 if name.is_empty() || name.starts_with("__subq_") || is_universal_query_source(name) {
1719 return;
1720 }
1721 scopes.insert(name.to_string());
1722}
1723
1724fn collect_table_source_scopes(scopes: &mut HashSet<String>, query: &TableQuery) {
1725 match query.source.as_ref() {
1726 Some(crate::storage::query::ast::TableSource::Name(name)) => {
1727 cache_scope_insert(scopes, name)
1728 }
1729 Some(crate::storage::query::ast::TableSource::Subquery(subquery)) => {
1730 collect_query_expr_result_cache_scopes(scopes, subquery);
1731 }
1732 Some(crate::storage::query::ast::TableSource::Function { name, args, .. }) => {
1739 if is_graph_tvf_name(name) {
1740 if let Some(graph) = args.first() {
1741 cache_scope_insert(scopes, graph);
1742 }
1743 }
1744 }
1745 Some(crate::storage::query::ast::TableSource::InlineGraphFunction {
1750 nodes, edges, ..
1751 }) => {
1752 collect_query_expr_result_cache_scopes(scopes, nodes);
1753 collect_query_expr_result_cache_scopes(scopes, edges);
1754 }
1755 None => cache_scope_insert(scopes, &query.table),
1756 }
1757}
1758
1759fn collect_vector_source_scopes(
1760 scopes: &mut HashSet<String>,
1761 source: &crate::storage::query::ast::VectorSource,
1762) {
1763 match source {
1764 crate::storage::query::ast::VectorSource::Reference { collection, .. } => {
1765 cache_scope_insert(scopes, collection);
1766 }
1767 crate::storage::query::ast::VectorSource::Subquery(subquery) => {
1768 collect_query_expr_result_cache_scopes(scopes, subquery);
1769 }
1770 crate::storage::query::ast::VectorSource::Literal(_)
1771 | crate::storage::query::ast::VectorSource::Text(_) => {}
1772 }
1773}
1774
1775fn collect_path_selector_scopes(
1776 scopes: &mut HashSet<String>,
1777 selector: &crate::storage::query::ast::NodeSelector,
1778) {
1779 if let crate::storage::query::ast::NodeSelector::ByRow { table, .. } = selector {
1780 cache_scope_insert(scopes, table);
1781 }
1782}
1783
1784fn collect_query_expr_result_cache_scopes(scopes: &mut HashSet<String>, expr: &QueryExpr) {
1785 match expr {
1786 QueryExpr::Table(query) => collect_table_source_scopes(scopes, query),
1787 QueryExpr::Join(query) => {
1788 collect_query_expr_result_cache_scopes(scopes, &query.left);
1789 collect_query_expr_result_cache_scopes(scopes, &query.right);
1790 }
1791 QueryExpr::Path(query) => {
1792 collect_path_selector_scopes(scopes, &query.from);
1793 collect_path_selector_scopes(scopes, &query.to);
1794 }
1795 QueryExpr::Vector(query) => {
1796 cache_scope_insert(scopes, &query.collection);
1797 collect_vector_source_scopes(scopes, &query.query_vector);
1798 }
1799 QueryExpr::Hybrid(query) => {
1800 collect_query_expr_result_cache_scopes(scopes, &query.structured);
1801 cache_scope_insert(scopes, &query.vector.collection);
1802 collect_vector_source_scopes(scopes, &query.vector.query_vector);
1803 }
1804 QueryExpr::Insert(query) => cache_scope_insert(scopes, &query.table),
1805 QueryExpr::Update(query) => cache_scope_insert(scopes, &query.table),
1806 QueryExpr::Delete(query) => cache_scope_insert(scopes, &query.table),
1807 QueryExpr::CreateTable(query) => cache_scope_insert(scopes, &query.name),
1808 QueryExpr::CreateCollection(query) => cache_scope_insert(scopes, &query.name),
1809 QueryExpr::CreateVector(query) => cache_scope_insert(scopes, &query.name),
1810 QueryExpr::DropTable(query) => cache_scope_insert(scopes, &query.name),
1811 QueryExpr::DropGraph(query) => cache_scope_insert(scopes, &query.name),
1812 QueryExpr::DropVector(query) => cache_scope_insert(scopes, &query.name),
1813 QueryExpr::DropDocument(query) => cache_scope_insert(scopes, &query.name),
1814 QueryExpr::DropKv(query) => cache_scope_insert(scopes, &query.name),
1815 QueryExpr::DropCollection(query) => cache_scope_insert(scopes, &query.name),
1816 QueryExpr::Truncate(query) => cache_scope_insert(scopes, &query.name),
1817 QueryExpr::AlterTable(query) => cache_scope_insert(scopes, &query.name),
1818 QueryExpr::CreateIndex(query) => cache_scope_insert(scopes, &query.table),
1819 QueryExpr::DropIndex(query) => cache_scope_insert(scopes, &query.table),
1820 QueryExpr::CreateTimeSeries(query) => cache_scope_insert(scopes, &query.name),
1821 QueryExpr::CreateMetric(query) => cache_scope_insert(scopes, &query.path),
1822 QueryExpr::AlterMetric(query) => cache_scope_insert(scopes, &query.path),
1823 QueryExpr::CreateSlo(query) => cache_scope_insert(scopes, &query.path),
1824 QueryExpr::DropTimeSeries(query) => cache_scope_insert(scopes, &query.name),
1825 QueryExpr::CreateQueue(query) => cache_scope_insert(scopes, &query.name),
1826 QueryExpr::AlterQueue(query) => cache_scope_insert(scopes, &query.name),
1827 QueryExpr::DropQueue(query) => cache_scope_insert(scopes, &query.name),
1828 QueryExpr::QueueSelect(query) => cache_scope_insert(scopes, &query.queue),
1829 QueryExpr::QueueCommand(query) => match query {
1830 QueueCommand::Push { queue, .. }
1831 | QueueCommand::Pop { queue, .. }
1832 | QueueCommand::Peek { queue, .. }
1833 | QueueCommand::Len { queue }
1834 | QueueCommand::Purge { queue }
1835 | QueueCommand::GroupCreate { queue, .. }
1836 | QueueCommand::GroupRead { queue, .. }
1837 | QueueCommand::Pending { queue, .. }
1838 | QueueCommand::Claim { queue, .. }
1839 | QueueCommand::Ack { queue, .. }
1840 | QueueCommand::Nack { queue, .. } => cache_scope_insert(scopes, queue),
1841 QueueCommand::Move {
1842 source,
1843 destination,
1844 ..
1845 } => {
1846 cache_scope_insert(scopes, source);
1847 cache_scope_insert(scopes, destination);
1848 }
1849 },
1850 QueryExpr::EventsBackfill(query) => {
1851 cache_scope_insert(scopes, &query.collection);
1852 cache_scope_insert(scopes, &query.target_queue);
1853 }
1854 QueryExpr::CreateTree(query) => cache_scope_insert(scopes, &query.collection),
1855 QueryExpr::DropTree(query) => cache_scope_insert(scopes, &query.collection),
1856 QueryExpr::TreeCommand(query) => match query {
1857 TreeCommand::Insert { collection, .. }
1858 | TreeCommand::Move { collection, .. }
1859 | TreeCommand::Delete { collection, .. }
1860 | TreeCommand::Validate { collection, .. }
1861 | TreeCommand::Rebalance { collection, .. } => cache_scope_insert(scopes, collection),
1862 },
1863 QueryExpr::SearchCommand(query) => match query {
1864 SearchCommand::Similar { collection, .. }
1865 | SearchCommand::Hybrid { collection, .. }
1866 | SearchCommand::SpatialRadius { collection, .. }
1867 | SearchCommand::SpatialBbox { collection, .. }
1868 | SearchCommand::SpatialNearest { collection, .. } => {
1869 cache_scope_insert(scopes, collection);
1870 }
1871 SearchCommand::Text { collection, .. }
1872 | SearchCommand::Multimodal { collection, .. }
1873 | SearchCommand::Index { collection, .. }
1874 | SearchCommand::Context { collection, .. } => {
1875 if let Some(collection) = collection.as_deref() {
1876 cache_scope_insert(scopes, collection);
1877 }
1878 }
1879 },
1880 QueryExpr::Ask(query) => {
1881 if let Some(collection) = query.collection.as_deref() {
1882 cache_scope_insert(scopes, collection);
1883 }
1884 }
1885 QueryExpr::ExplainAlter(query) => cache_scope_insert(scopes, &query.target.name),
1886 QueryExpr::MaintenanceCommand(cmd) => match cmd {
1887 crate::storage::query::ast::MaintenanceCommand::Vacuum { target, .. }
1888 | crate::storage::query::ast::MaintenanceCommand::Analyze { target } => {
1889 if let Some(t) = target {
1890 cache_scope_insert(scopes, t);
1891 }
1892 }
1893 },
1894 QueryExpr::CopyFrom(cmd) => cache_scope_insert(scopes, &cmd.table),
1895 QueryExpr::CreateView(cmd) => {
1896 cache_scope_insert(scopes, &cmd.name);
1897 collect_query_expr_result_cache_scopes(scopes, &cmd.query);
1899 }
1900 QueryExpr::DropView(cmd) => cache_scope_insert(scopes, &cmd.name),
1901 QueryExpr::RefreshMaterializedView(cmd) => cache_scope_insert(scopes, &cmd.name),
1902 QueryExpr::CreatePolicy(cmd) => cache_scope_insert(scopes, &cmd.table),
1903 QueryExpr::DropPolicy(cmd) => cache_scope_insert(scopes, &cmd.table),
1904 QueryExpr::CreateServer(_) | QueryExpr::DropServer(_) => {}
1905 QueryExpr::CreateForeignTable(cmd) => cache_scope_insert(scopes, &cmd.name),
1906 QueryExpr::DropForeignTable(cmd) => cache_scope_insert(scopes, &cmd.name),
1907 QueryExpr::Graph(_)
1908 | QueryExpr::GraphCommand(_)
1909 | QueryExpr::ProbabilisticCommand(_)
1910 | QueryExpr::SetConfig { .. }
1911 | QueryExpr::ShowConfig { .. }
1912 | QueryExpr::SetSecret { .. }
1913 | QueryExpr::DeleteSecret { .. }
1914 | QueryExpr::ShowSecrets { .. }
1915 | QueryExpr::SetTenant(_)
1916 | QueryExpr::ShowTenant
1917 | QueryExpr::TransactionControl(_)
1918 | QueryExpr::CreateSchema(_)
1919 | QueryExpr::DropSchema(_)
1920 | QueryExpr::CreateSequence(_)
1921 | QueryExpr::DropSequence(_)
1922 | QueryExpr::Grant(_)
1923 | QueryExpr::Revoke(_)
1924 | QueryExpr::AlterUser(_)
1925 | QueryExpr::CreateIamPolicy { .. }
1926 | QueryExpr::DropIamPolicy { .. }
1927 | QueryExpr::AttachPolicy { .. }
1928 | QueryExpr::DetachPolicy { .. }
1929 | QueryExpr::ShowPolicies { .. }
1930 | QueryExpr::ShowEffectivePermissions { .. }
1931 | QueryExpr::SimulatePolicy { .. }
1932 | QueryExpr::LintPolicy { .. }
1933 | QueryExpr::MigratePolicyMode { .. }
1934 | QueryExpr::CreateMigration(_)
1935 | QueryExpr::ApplyMigration(_)
1936 | QueryExpr::RollbackMigration(_)
1937 | QueryExpr::ExplainMigration(_)
1938 | QueryExpr::EventsBackfillStatus { .. } => {}
1939 QueryExpr::KvCommand(cmd) => {
1940 use crate::storage::query::ast::KvCommand;
1941 match cmd {
1942 KvCommand::Put { collection, .. }
1943 | KvCommand::InvalidateTags { collection, .. }
1944 | KvCommand::Get { collection, .. }
1945 | KvCommand::Unseal { collection, .. }
1946 | KvCommand::Rotate { collection, .. }
1947 | KvCommand::History { collection, .. }
1948 | KvCommand::List { collection, .. }
1949 | KvCommand::Purge { collection, .. }
1950 | KvCommand::Watch { collection, .. }
1951 | KvCommand::Delete { collection, .. }
1952 | KvCommand::Incr { collection, .. }
1953 | KvCommand::Cas { collection, .. } => cache_scope_insert(scopes, collection),
1954 }
1955 }
1956 QueryExpr::ConfigCommand(cmd) => {
1957 use crate::storage::query::ast::ConfigCommand;
1958 match cmd {
1959 ConfigCommand::Put { collection, .. }
1960 | ConfigCommand::Get { collection, .. }
1961 | ConfigCommand::Resolve { collection, .. }
1962 | ConfigCommand::Rotate { collection, .. }
1963 | ConfigCommand::Delete { collection, .. }
1964 | ConfigCommand::History { collection, .. }
1965 | ConfigCommand::List { collection, .. }
1966 | ConfigCommand::Watch { collection, .. }
1967 | ConfigCommand::InvalidVolatileOperation { collection, .. } => {
1968 cache_scope_insert(scopes, collection)
1969 }
1970 }
1971 }
1972 }
1973}
1974
1975pub(crate) fn rls_policy_filter(
1983 runtime: &RedDBRuntime,
1984 table: &str,
1985 action: crate::storage::query::ast::PolicyAction,
1986) -> Option<crate::storage::query::ast::Filter> {
1987 rls_policy_filter_for_kind(
1988 runtime,
1989 table,
1990 action,
1991 crate::storage::query::ast::PolicyTargetKind::Table,
1992 )
1993}
1994
1995pub(crate) fn rls_policy_filter_for_kind(
2001 runtime: &RedDBRuntime,
2002 table: &str,
2003 action: crate::storage::query::ast::PolicyAction,
2004 kind: crate::storage::query::ast::PolicyTargetKind,
2005) -> Option<crate::storage::query::ast::Filter> {
2006 use crate::storage::query::ast::Filter;
2007
2008 if !runtime.inner.rls_enabled_tables.read().contains(table) {
2009 return None;
2010 }
2011 let role = current_auth_identity().map(|(_, role)| role);
2012 let role_str = role.map(|r| r.as_str().to_string());
2013 let policies = runtime.matching_rls_policies_for_kind(table, role_str.as_deref(), action, kind);
2014 if policies.is_empty() {
2015 return None;
2016 }
2017 policies
2018 .into_iter()
2019 .reduce(|acc, f| Filter::Or(Box::new(acc), Box::new(f)))
2020}
2021
2022pub(crate) fn rls_is_enabled(runtime: &RedDBRuntime, table: &str) -> bool {
2026 runtime.inner.rls_enabled_tables.read().contains(table)
2027}
2028
2029fn node_passes_rls(
2036 runtime: &RedDBRuntime,
2037 collection: &str,
2038 role: Option<&str>,
2039 cache: &mut std::collections::HashMap<String, Option<crate::storage::query::ast::Filter>>,
2040 entity: &crate::storage::unified::entity::UnifiedEntity,
2041) -> bool {
2042 use crate::storage::query::ast::{Filter, PolicyAction, PolicyTargetKind};
2043
2044 if !runtime.inner.rls_enabled_tables.read().contains(collection) {
2045 return true;
2046 }
2047 let filter = cache.entry(collection.to_string()).or_insert_with(|| {
2048 let policies = runtime.matching_rls_policies_for_kind(
2049 collection,
2050 role,
2051 PolicyAction::Select,
2052 PolicyTargetKind::Nodes,
2053 );
2054 if policies.is_empty() {
2055 None
2056 } else {
2057 policies
2058 .into_iter()
2059 .reduce(|acc, f| Filter::Or(Box::new(acc), Box::new(f)))
2060 }
2061 });
2062 let Some(filter) = filter else {
2063 return false;
2064 };
2065 crate::runtime::query_exec::evaluate_entity_filter_with_db(
2066 Some(&runtime.inner.db),
2067 entity,
2068 filter,
2069 collection,
2070 collection,
2071 )
2072}
2073
2074fn edge_passes_rls(
2077 runtime: &RedDBRuntime,
2078 collection: &str,
2079 role: Option<&str>,
2080 cache: &mut std::collections::HashMap<String, Option<crate::storage::query::ast::Filter>>,
2081 entity: &crate::storage::unified::entity::UnifiedEntity,
2082) -> bool {
2083 use crate::storage::query::ast::{Filter, PolicyAction, PolicyTargetKind};
2084
2085 if !runtime.inner.rls_enabled_tables.read().contains(collection) {
2086 return true;
2087 }
2088 let filter = cache.entry(collection.to_string()).or_insert_with(|| {
2089 let policies = runtime.matching_rls_policies_for_kind(
2090 collection,
2091 role,
2092 PolicyAction::Select,
2093 PolicyTargetKind::Edges,
2094 );
2095 if policies.is_empty() {
2096 None
2097 } else {
2098 policies
2099 .into_iter()
2100 .reduce(|acc, f| Filter::Or(Box::new(acc), Box::new(f)))
2101 }
2102 });
2103 let Some(filter) = filter else {
2104 return false;
2105 };
2106 crate::runtime::query_exec::evaluate_entity_filter_with_db(
2107 Some(&runtime.inner.db),
2108 entity,
2109 filter,
2110 collection,
2111 collection,
2112 )
2113}
2114
2115fn inject_rls_filters(
2136 runtime: &RedDBRuntime,
2137 frame: &dyn super::statement_frame::ReadFrame,
2138 mut table: crate::storage::query::ast::TableQuery,
2139) -> Option<crate::storage::query::ast::TableQuery> {
2140 use crate::storage::query::ast::{Filter, PolicyAction};
2141
2142 let role = frame.identity().map(|(_, role)| role);
2144 let role_str = role.map(|r| r.as_str().to_string());
2145 let policies =
2146 runtime.matching_rls_policies(&table.table, role_str.as_deref(), PolicyAction::Select);
2147
2148 if policies.is_empty() {
2149 return None;
2152 }
2153
2154 let combined = policies
2156 .into_iter()
2157 .reduce(|acc, f| Filter::Or(Box::new(acc), Box::new(f)))
2158 .expect("policies non-empty");
2159
2160 use crate::storage::query::sql_lowering::{expr_to_filter, filter_to_expr};
2169 let had_where_expr = table.where_expr.is_some();
2170 let existing = table
2171 .filter
2172 .take()
2173 .or_else(|| table.where_expr.as_ref().map(expr_to_filter));
2174 let new_filter = match existing {
2175 Some(existing) => Filter::And(Box::new(existing), Box::new(combined)),
2176 None => combined,
2177 };
2178 if had_where_expr {
2181 table.where_expr = Some(filter_to_expr(&new_filter));
2182 }
2183 table.filter = Some(new_filter);
2184 Some(table)
2185}
2186
2187fn inject_rls_into_join(
2197 runtime: &RedDBRuntime,
2198 frame: &dyn super::statement_frame::ReadFrame,
2199 mut join: crate::storage::query::ast::JoinQuery,
2200) -> Option<crate::storage::query::ast::JoinQuery> {
2201 use crate::storage::query::ast::Filter;
2202
2203 let mut policy_filters: Vec<Filter> = Vec::new();
2204 if !collect_join_side_policy(runtime, frame, join.left.as_ref(), &mut policy_filters) {
2205 return None;
2206 }
2207 if !collect_join_side_policy(runtime, frame, join.right.as_ref(), &mut policy_filters) {
2208 return None;
2209 }
2210
2211 if policy_filters.is_empty() {
2212 return Some(join);
2213 }
2214
2215 let combined = policy_filters
2216 .into_iter()
2217 .reduce(|acc, f| Filter::And(Box::new(acc), Box::new(f)))
2218 .expect("policy_filters non-empty");
2219
2220 join.filter = Some(match join.filter.take() {
2221 Some(existing) => Filter::And(Box::new(existing), Box::new(combined)),
2222 None => combined,
2223 });
2224
2225 Some(join)
2226}
2227
2228fn collect_join_side_policy(
2233 runtime: &RedDBRuntime,
2234 frame: &dyn super::statement_frame::ReadFrame,
2235 expr: &crate::storage::query::ast::QueryExpr,
2236 out: &mut Vec<crate::storage::query::ast::Filter>,
2237) -> bool {
2238 use crate::storage::query::ast::{Filter, PolicyAction, QueryExpr};
2239 match expr {
2240 QueryExpr::Table(t) => {
2241 if !runtime.inner.rls_enabled_tables.read().contains(&t.table) {
2242 return true;
2243 }
2244 let role = frame.identity().map(|(_, role)| role);
2245 let role_str = role.map(|r| r.as_str().to_string());
2246 let policies =
2247 runtime.matching_rls_policies(&t.table, role_str.as_deref(), PolicyAction::Select);
2248 if policies.is_empty() {
2249 return false;
2250 }
2251 let combined = policies
2252 .into_iter()
2253 .reduce(|acc, f| Filter::Or(Box::new(acc), Box::new(f)))
2254 .expect("policies non-empty");
2255 out.push(combined);
2256 true
2257 }
2258 QueryExpr::Join(inner) => {
2259 collect_join_side_policy(runtime, frame, inner.left.as_ref(), out)
2260 && collect_join_side_policy(runtime, frame, inner.right.as_ref(), out)
2261 }
2262 _ => true,
2263 }
2264}
2265
2266fn apply_foreign_table_filters(
2277 records: Vec<crate::storage::query::unified::UnifiedRecord>,
2278 query: &crate::storage::query::ast::TableQuery,
2279) -> crate::storage::query::unified::UnifiedResult {
2280 use crate::storage::query::sql_lowering::{
2281 effective_table_filter, effective_table_projections,
2282 };
2283 use crate::storage::query::unified::UnifiedResult;
2284
2285 let filter = effective_table_filter(query);
2286 let projections = effective_table_projections(query);
2287
2288 let mut filtered: Vec<_> = records
2291 .into_iter()
2292 .filter(|record| match &filter {
2293 Some(f) => {
2294 super::join_filter::evaluate_runtime_filter_with_db(None, record, f, None, None)
2295 }
2296 None => true,
2297 })
2298 .collect();
2299
2300 if let Some(offset) = query.offset {
2302 let offset = offset as usize;
2303 if offset >= filtered.len() {
2304 filtered.clear();
2305 } else {
2306 filtered.drain(0..offset);
2307 }
2308 }
2309 if let Some(limit) = query.limit {
2310 filtered.truncate(limit as usize);
2311 }
2312
2313 let columns: Vec<String> = if projections.is_empty() {
2316 filtered
2317 .first()
2318 .map(|r| r.column_names().iter().map(|k| k.to_string()).collect())
2319 .unwrap_or_default()
2320 } else {
2321 projections
2322 .iter()
2323 .map(super::join_filter::projection_name)
2324 .collect()
2325 };
2326
2327 let mut result = UnifiedResult::empty();
2328 result.columns = columns;
2329 result.records = filtered;
2330 result
2331}
2332
2333pub(crate) fn collect_table_refs(expr: &QueryExpr) -> Vec<String> {
2340 let mut scopes: HashSet<String> = HashSet::new();
2341 collect_query_expr_result_cache_scopes(&mut scopes, expr);
2342 scopes.into_iter().collect()
2343}
2344
2345fn query_expr_result_cache_scopes(expr: &QueryExpr) -> HashSet<String> {
2346 let mut scopes = HashSet::new();
2347 collect_query_expr_result_cache_scopes(&mut scopes, expr);
2348 scopes
2349}
2350
2351const RESULT_CACHE_BACKEND_KEY: &str = "runtime.result_cache.backend";
2352const RESULT_CACHE_DEFAULT_BACKEND: &str = "legacy";
2353const RESULT_CACHE_BLOB_NAMESPACE: &str = "runtime.result_cache";
2354const RESULT_CACHE_TTL_SECS: u64 = 30;
2358const RESULT_CACHE_MAX_ENTRIES: usize = 1000;
2359const RESULT_CACHE_ENABLED_KEY: &str = "runtime.result_cache.enabled";
2360const RESULT_CACHE_TTL_KEY: &str = "runtime.result_cache.ttl_seconds";
2361const RESULT_CACHE_CAPACITY_KEY: &str = "runtime.result_cache.capacity_entries";
2362const RESULT_CACHE_PAYLOAD_MAGIC: &[u8; 8] = b"RDRC0001";
2363
2364#[derive(Clone, Copy, Debug, PartialEq, Eq)]
2365enum RuntimeResultCacheBackend {
2366 Legacy,
2367 BlobCache,
2368 Shadow,
2369}
2370
2371fn trim_result_cache(
2375 map: &mut HashMap<String, RuntimeResultCacheEntry>,
2376 order: &mut std::collections::VecDeque<String>,
2377 max_entries: usize,
2378) -> u64 {
2379 let mut evicted = 0u64;
2380 while map.len() > max_entries {
2381 if let Some(oldest) = order.pop_front() {
2382 if map.remove(&oldest).is_some() {
2383 evicted += 1;
2384 }
2385 } else {
2386 break;
2387 }
2388 }
2389 evicted
2390}
2391
2392fn result_cache_fingerprint(result: &RuntimeQueryResult) -> String {
2393 format!(
2394 "{:?}|{}|{}|{}|{}|{:?}",
2395 result.result,
2396 result.query,
2397 result.statement,
2398 result.engine,
2399 result.affected_rows,
2400 result.statement_type
2401 )
2402}
2403
2404fn mode_to_byte(mode: crate::storage::query::modes::QueryMode) -> u8 {
2405 match mode {
2406 crate::storage::query::modes::QueryMode::Sql => 0,
2407 crate::storage::query::modes::QueryMode::Gremlin => 1,
2408 crate::storage::query::modes::QueryMode::Cypher => 2,
2409 crate::storage::query::modes::QueryMode::Sparql => 3,
2410 crate::storage::query::modes::QueryMode::Path => 4,
2411 crate::storage::query::modes::QueryMode::Natural => 5,
2412 crate::storage::query::modes::QueryMode::Unknown => 255,
2413 }
2414}
2415
2416fn mode_from_byte(byte: u8) -> Option<crate::storage::query::modes::QueryMode> {
2417 match byte {
2418 0 => Some(crate::storage::query::modes::QueryMode::Sql),
2419 1 => Some(crate::storage::query::modes::QueryMode::Gremlin),
2420 2 => Some(crate::storage::query::modes::QueryMode::Cypher),
2421 3 => Some(crate::storage::query::modes::QueryMode::Sparql),
2422 4 => Some(crate::storage::query::modes::QueryMode::Path),
2423 5 => Some(crate::storage::query::modes::QueryMode::Natural),
2424 255 => Some(crate::storage::query::modes::QueryMode::Unknown),
2425 _ => None,
2426 }
2427}
2428
2429fn result_cache_static_str(value: &str) -> Option<&'static str> {
2430 match value {
2431 "select" => Some("select"),
2432 "materialized-graph" => Some("materialized-graph"),
2433 "runtime-red-schema" => Some("runtime-red-schema"),
2434 "runtime-fdw" => Some("runtime-fdw"),
2435 "runtime-table-rls" => Some("runtime-table-rls"),
2436 "runtime-table" => Some("runtime-table"),
2437 "runtime-join-rls" => Some("runtime-join-rls"),
2438 "runtime-join" => Some("runtime-join"),
2439 "runtime-vector" => Some("runtime-vector"),
2440 "runtime-hybrid" => Some("runtime-hybrid"),
2441 "runtime-secret" => Some("runtime-secret"),
2442 "runtime-config" => Some("runtime-config"),
2443 "runtime-tenant" => Some("runtime-tenant"),
2444 "runtime-explain" => Some("runtime-explain"),
2445 "runtime-tree" => Some("runtime-tree"),
2446 "runtime-kv" => Some("runtime-kv"),
2447 "runtime-queue" => Some("runtime-queue"),
2448 _ => None,
2449 }
2450}
2451
2452fn write_u32(out: &mut Vec<u8>, value: usize) -> Option<()> {
2453 let value = u32::try_from(value).ok()?;
2454 out.extend_from_slice(&value.to_le_bytes());
2455 Some(())
2456}
2457
2458fn write_string(out: &mut Vec<u8>, value: &str) -> Option<()> {
2459 write_u32(out, value.len())?;
2460 out.extend_from_slice(value.as_bytes());
2461 Some(())
2462}
2463
2464fn write_bytes(out: &mut Vec<u8>, value: &[u8]) -> Option<()> {
2465 write_u32(out, value.len())?;
2466 out.extend_from_slice(value);
2467 Some(())
2468}
2469
2470fn read_u8(input: &mut &[u8]) -> Option<u8> {
2471 let (&value, rest) = input.split_first()?;
2472 *input = rest;
2473 Some(value)
2474}
2475
2476fn read_u32(input: &mut &[u8]) -> Option<usize> {
2477 if input.len() < 4 {
2478 return None;
2479 }
2480 let value = u32::from_le_bytes(input[..4].try_into().ok()?) as usize;
2481 *input = &input[4..];
2482 Some(value)
2483}
2484
2485fn read_u64(input: &mut &[u8]) -> Option<u64> {
2486 if input.len() < 8 {
2487 return None;
2488 }
2489 let value = u64::from_le_bytes(input[..8].try_into().ok()?);
2490 *input = &input[8..];
2491 Some(value)
2492}
2493
2494fn read_string(input: &mut &[u8]) -> Option<String> {
2495 let len = read_u32(input)?;
2496 if input.len() < len {
2497 return None;
2498 }
2499 let value = String::from_utf8(input[..len].to_vec()).ok()?;
2500 *input = &input[len..];
2501 Some(value)
2502}
2503
2504fn read_bytes<'a>(input: &mut &'a [u8]) -> Option<&'a [u8]> {
2505 let len = read_u32(input)?;
2506 if input.len() < len {
2507 return None;
2508 }
2509 let value = &input[..len];
2510 *input = &input[len..];
2511 Some(value)
2512}
2513
2514fn encode_result_cache_payload(entry: &RuntimeResultCacheEntry) -> Option<Vec<u8>> {
2515 let result = &entry.result;
2516 if result.result.pre_serialized_json.is_some()
2517 || result_cache_static_str(result.statement).is_none()
2518 || result_cache_static_str(result.engine).is_none()
2519 || result_cache_static_str(result.statement_type).is_none()
2520 || result.result.records.iter().any(|record| {
2521 !record.nodes.is_empty()
2522 || !record.edges.is_empty()
2523 || !record.paths.is_empty()
2524 || !record.vector_results.is_empty()
2525 })
2526 {
2527 return None;
2528 }
2529
2530 let mut out = Vec::new();
2531 out.extend_from_slice(RESULT_CACHE_PAYLOAD_MAGIC);
2532 write_string(&mut out, &result.query)?;
2533 out.push(mode_to_byte(result.mode));
2534 write_string(&mut out, result.statement)?;
2535 write_string(&mut out, result.engine)?;
2536 out.extend_from_slice(&result.affected_rows.to_le_bytes());
2537 write_string(&mut out, result.statement_type)?;
2538
2539 write_u32(&mut out, result.result.columns.len())?;
2540 for column in &result.result.columns {
2541 write_string(&mut out, column)?;
2542 }
2543 out.extend_from_slice(&result.result.stats.nodes_scanned.to_le_bytes());
2544 out.extend_from_slice(&result.result.stats.edges_scanned.to_le_bytes());
2545 out.extend_from_slice(&result.result.stats.rows_scanned.to_le_bytes());
2546 out.extend_from_slice(&result.result.stats.exec_time_us.to_le_bytes());
2547
2548 write_u32(&mut out, result.result.records.len())?;
2549 for record in &result.result.records {
2550 let fields = record.iter_fields().collect::<Vec<_>>();
2551 write_u32(&mut out, fields.len())?;
2552 for (name, value) in fields {
2553 write_string(&mut out, name)?;
2554 let mut encoded = Vec::new();
2555 crate::storage::schema::value_codec::encode(value, &mut encoded);
2556 write_bytes(&mut out, &encoded)?;
2557 }
2558 }
2559
2560 write_u32(&mut out, entry.scopes.len())?;
2561 for scope in &entry.scopes {
2562 write_string(&mut out, scope)?;
2563 }
2564 Some(out)
2565}
2566
2567fn decode_result_cache_payload(mut input: &[u8]) -> Option<(RuntimeQueryResult, HashSet<String>)> {
2568 if input.len() < RESULT_CACHE_PAYLOAD_MAGIC.len()
2569 || &input[..RESULT_CACHE_PAYLOAD_MAGIC.len()] != RESULT_CACHE_PAYLOAD_MAGIC
2570 {
2571 return None;
2572 }
2573 input = &input[RESULT_CACHE_PAYLOAD_MAGIC.len()..];
2574
2575 let query = read_string(&mut input)?;
2576 let mode = mode_from_byte(read_u8(&mut input)?)?;
2577 let statement = result_cache_static_str(&read_string(&mut input)?)?;
2578 let engine = result_cache_static_str(&read_string(&mut input)?)?;
2579 let affected_rows = read_u64(&mut input)?;
2580 let statement_type = result_cache_static_str(&read_string(&mut input)?)?;
2581
2582 let mut columns = Vec::new();
2583 for _ in 0..read_u32(&mut input)? {
2584 columns.push(read_string(&mut input)?);
2585 }
2586 let stats = crate::storage::query::unified::QueryStats {
2587 nodes_scanned: read_u64(&mut input)?,
2588 edges_scanned: read_u64(&mut input)?,
2589 rows_scanned: read_u64(&mut input)?,
2590 exec_time_us: read_u64(&mut input)?,
2591 };
2592
2593 let mut records = Vec::new();
2594 for _ in 0..read_u32(&mut input)? {
2595 let mut record = crate::storage::query::unified::UnifiedRecord::new();
2596 for _ in 0..read_u32(&mut input)? {
2597 let name = read_string(&mut input)?;
2598 let bytes = read_bytes(&mut input)?;
2599 let (value, used) = crate::storage::schema::value_codec::decode(bytes).ok()?;
2600 if used != bytes.len() {
2601 return None;
2602 }
2603 record.set_owned(name, value);
2604 }
2605 records.push(record);
2606 }
2607
2608 let mut scopes = HashSet::new();
2609 for _ in 0..read_u32(&mut input)? {
2610 scopes.insert(read_string(&mut input)?);
2611 }
2612 if !input.is_empty() {
2613 return None;
2614 }
2615
2616 Some((
2617 RuntimeQueryResult {
2618 query,
2619 mode,
2620 statement,
2621 engine,
2622 result: crate::storage::query::unified::UnifiedResult {
2623 columns,
2624 records,
2625 stats,
2626 pre_serialized_json: None,
2627 },
2628 affected_rows,
2629 statement_type,
2630 bookmark: None,
2631 },
2632 scopes,
2633 ))
2634}
2635
2636fn strip_explain_prefix(sql: &str) -> Option<&str> {
2650 let trimmed = sql.trim_start();
2651 let (head, rest) = trimmed.split_at(
2652 trimmed
2653 .find(|c: char| c.is_whitespace())
2654 .unwrap_or(trimmed.len()),
2655 );
2656 if !head.eq_ignore_ascii_case("EXPLAIN") {
2657 return None;
2658 }
2659 let rest = rest.trim_start();
2660 if rest.is_empty() {
2661 return None;
2662 }
2663 let next_head_end = rest.find(|c: char| c.is_whitespace()).unwrap_or(rest.len());
2667 if rest[..next_head_end].eq_ignore_ascii_case("ALTER")
2668 || rest[..next_head_end].eq_ignore_ascii_case("ASK")
2669 {
2670 return None;
2671 }
2672 Some(rest)
2673}
2674
2675pub(super) fn has_with_prefix(sql: &str) -> bool {
2680 let trimmed = sql.trim_start();
2681 let head_end = trimmed
2682 .find(|c: char| c.is_whitespace() || c == '(')
2683 .unwrap_or(trimmed.len());
2684 trimmed[..head_end].eq_ignore_ascii_case("WITH")
2685}
2686
2687fn peek_top_level_as_of(sql: &str) -> Option<crate::application::vcs::AsOfSpec> {
2695 peek_top_level_as_of_with_table(sql).map(|(spec, _)| spec)
2696}
2697
2698pub(super) fn peek_top_level_as_of_with_table(
2703 sql: &str,
2704) -> Option<(crate::application::vcs::AsOfSpec, Option<String>)> {
2705 if !sql
2706 .as_bytes()
2707 .windows(5)
2708 .any(|w| w.eq_ignore_ascii_case(b"as of"))
2709 {
2710 return None;
2711 }
2712 let parsed = crate::storage::query::parser::parse(sql).ok()?;
2713 let crate::storage::query::ast::QueryExpr::Table(table) = parsed.query else {
2714 return None;
2715 };
2716 let clause = table.as_of?;
2717 let table_name = if table.table.is_empty() || table.table == "any" {
2718 None
2719 } else {
2720 Some(table.table.clone())
2721 };
2722 let spec = match clause {
2723 crate::storage::query::ast::AsOfClause::Commit(h) => {
2724 crate::application::vcs::AsOfSpec::Commit(h)
2725 }
2726 crate::storage::query::ast::AsOfClause::Branch(b) => {
2727 crate::application::vcs::AsOfSpec::Branch(b)
2728 }
2729 crate::storage::query::ast::AsOfClause::Tag(t) => crate::application::vcs::AsOfSpec::Tag(t),
2730 crate::storage::query::ast::AsOfClause::TimestampMs(ts) => {
2731 crate::application::vcs::AsOfSpec::TimestampMs(ts)
2732 }
2733 crate::storage::query::ast::AsOfClause::Snapshot(x) => {
2734 crate::application::vcs::AsOfSpec::Snapshot(x)
2735 }
2736 };
2737 Some((spec, table_name))
2738}
2739
2740pub(super) fn query_has_volatile_builtin(sql: &str) -> bool {
2741 const VOLATILE_TOKENS: &[&str] = &[
2745 "pg_advisory_lock",
2746 "pg_try_advisory_lock",
2747 "pg_advisory_unlock",
2748 "random()",
2749 ];
2754 let lowered = sql.to_ascii_lowercase();
2755 VOLATILE_TOKENS.iter().any(|t| lowered.contains(t))
2756}
2757
2758pub(super) fn query_is_ask_statement(sql: &str) -> bool {
2759 let trimmed = sql.trim_start();
2760 let head_end = trimmed
2761 .find(|c: char| c.is_whitespace() || c == '(' || c == ';')
2762 .unwrap_or(trimmed.len());
2763 trimmed[..head_end].eq_ignore_ascii_case("ASK")
2764}
2765
2766pub(super) fn intent_lock_modes_for(
2776 expr: &QueryExpr,
2777) -> Option<(
2778 crate::storage::transaction::lock::LockMode,
2779 crate::storage::transaction::lock::LockMode,
2780)> {
2781 use crate::storage::transaction::lock::LockMode::{Exclusive, IntentExclusive, IntentShared};
2782
2783 match expr {
2784 QueryExpr::Table(_)
2786 | QueryExpr::Join(_)
2787 | QueryExpr::Vector(_)
2788 | QueryExpr::Hybrid(_)
2789 | QueryExpr::Graph(_)
2790 | QueryExpr::Path(_)
2791 | QueryExpr::Ask(_)
2792 | QueryExpr::SearchCommand(_)
2793 | QueryExpr::GraphCommand(_)
2794 | QueryExpr::QueueSelect(_) => Some((IntentShared, IntentShared)),
2795
2796 QueryExpr::Insert(_)
2804 | QueryExpr::Update(_)
2805 | QueryExpr::Delete(_)
2806 | QueryExpr::QueueCommand(QueueCommand::Move { .. }) => {
2807 Some((IntentExclusive, IntentExclusive))
2808 }
2809 QueryExpr::QueueCommand(_) => Some((IntentShared, IntentShared)),
2810
2811 QueryExpr::CreateTable(_)
2815 | QueryExpr::CreateCollection(_)
2816 | QueryExpr::CreateVector(_)
2817 | QueryExpr::DropTable(_)
2818 | QueryExpr::DropGraph(_)
2819 | QueryExpr::DropVector(_)
2820 | QueryExpr::DropDocument(_)
2821 | QueryExpr::DropKv(_)
2822 | QueryExpr::DropCollection(_)
2823 | QueryExpr::Truncate(_)
2824 | QueryExpr::AlterTable(_)
2825 | QueryExpr::CreateIndex(_)
2826 | QueryExpr::DropIndex(_)
2827 | QueryExpr::CreateTimeSeries(_)
2828 | QueryExpr::CreateMetric(_)
2829 | QueryExpr::AlterMetric(_)
2830 | QueryExpr::CreateSlo(_)
2831 | QueryExpr::DropTimeSeries(_)
2832 | QueryExpr::CreateQueue(_)
2833 | QueryExpr::AlterQueue(_)
2834 | QueryExpr::DropQueue(_)
2835 | QueryExpr::CreateTree(_)
2836 | QueryExpr::DropTree(_)
2837 | QueryExpr::CreatePolicy(_)
2838 | QueryExpr::DropPolicy(_)
2839 | QueryExpr::CreateView(_)
2840 | QueryExpr::DropView(_)
2841 | QueryExpr::RefreshMaterializedView(_)
2842 | QueryExpr::CreateSchema(_)
2843 | QueryExpr::DropSchema(_)
2844 | QueryExpr::CreateSequence(_)
2845 | QueryExpr::DropSequence(_)
2846 | QueryExpr::CreateServer(_)
2847 | QueryExpr::DropServer(_)
2848 | QueryExpr::CreateForeignTable(_)
2849 | QueryExpr::DropForeignTable(_) => Some((IntentExclusive, Exclusive)),
2850
2851 _ => None,
2857 }
2858}
2859
2860pub(super) fn collections_referenced(expr: &QueryExpr) -> Vec<String> {
2865 let mut out = Vec::new();
2866 walk_collections(expr, &mut out);
2867 out.sort();
2868 out.dedup();
2869 out
2870}
2871
2872fn walk_collections(expr: &QueryExpr, out: &mut Vec<String>) {
2873 match expr {
2874 QueryExpr::Table(t) => out.push(t.table.clone()),
2875 QueryExpr::Join(j) => {
2876 walk_collections(&j.left, out);
2877 walk_collections(&j.right, out);
2878 }
2879 QueryExpr::Insert(i) => out.push(i.table.clone()),
2880 QueryExpr::Update(u) => out.push(u.table.clone()),
2881 QueryExpr::Delete(d) => out.push(d.table.clone()),
2882 QueryExpr::QueueSelect(q) => out.push(q.queue.clone()),
2883
2884 QueryExpr::CreateTable(q) => out.push(q.name.clone()),
2889 QueryExpr::CreateCollection(q) => out.push(q.name.clone()),
2890 QueryExpr::CreateVector(q) => out.push(q.name.clone()),
2891 QueryExpr::DropTable(q) => out.push(q.name.clone()),
2892 QueryExpr::DropGraph(q) => out.push(q.name.clone()),
2893 QueryExpr::DropVector(q) => out.push(q.name.clone()),
2894 QueryExpr::DropDocument(q) => out.push(q.name.clone()),
2895 QueryExpr::DropKv(q) => out.push(q.name.clone()),
2896 QueryExpr::DropCollection(q) => out.push(q.name.clone()),
2897 QueryExpr::Truncate(q) => out.push(q.name.clone()),
2898 QueryExpr::AlterTable(q) => out.push(q.name.clone()),
2899 QueryExpr::CreateIndex(q) => out.push(q.table.clone()),
2900 QueryExpr::DropIndex(q) => out.push(q.table.clone()),
2901 QueryExpr::CreateTimeSeries(q) => out.push(q.name.clone()),
2902 QueryExpr::CreateMetric(q) => out.push(q.path.clone()),
2903 QueryExpr::AlterMetric(q) => out.push(q.path.clone()),
2904 QueryExpr::CreateSlo(q) => out.push(q.path.clone()),
2905 QueryExpr::DropTimeSeries(q) => out.push(q.name.clone()),
2906 QueryExpr::CreateQueue(q) => out.push(q.name.clone()),
2907 QueryExpr::AlterQueue(q) => out.push(q.name.clone()),
2908 QueryExpr::DropQueue(q) => out.push(q.name.clone()),
2909 QueryExpr::QueueCommand(QueueCommand::Move {
2910 source,
2911 destination,
2912 ..
2913 }) => {
2914 out.push(source.clone());
2915 out.push(destination.clone());
2916 }
2917 QueryExpr::CreatePolicy(q) => out.push(q.table.clone()),
2918 QueryExpr::CreateView(q) => out.push(q.name.clone()),
2919 QueryExpr::DropView(q) => out.push(q.name.clone()),
2920 QueryExpr::RefreshMaterializedView(q) => out.push(q.name.clone()),
2921
2922 _ => {}
2928 }
2929}
2930
2931impl RedDBRuntime {
2932 pub fn in_memory() -> RedDBResult<Self> {
2933 Self::with_options(RedDBOptions::in_memory())
2934 }
2935
2936 pub fn lock_manager(&self) -> std::sync::Arc<crate::storage::transaction::lock::LockManager> {
2940 self.inner.lock_manager.clone()
2941 }
2942
2943 pub fn config_registry(&self) -> std::sync::Arc<crate::auth::registry::ConfigRegistry> {
2945 self.inner.config_registry.clone()
2946 }
2947
2948 pub fn query_audit(&self) -> std::sync::Arc<crate::runtime::query_audit::QueryAuditStream> {
2949 self.inner.query_audit.clone()
2950 }
2951
2952 pub fn control_events_require_persistence(&self) -> bool {
2953 self.inner.control_event_config.require_persistence()
2954 }
2955
2956 pub fn control_event_config(&self) -> crate::runtime::control_events::ControlEventConfig {
2957 self.inner.control_event_config
2958 }
2959
2960 pub fn control_event_ledger(
2961 &self,
2962 ) -> Arc<dyn crate::runtime::control_events::ControlEventLedger> {
2963 self.inner.control_event_ledger.read().clone()
2964 }
2965
2966 #[doc(hidden)]
2967 pub fn replace_control_event_ledger_for_tests(
2968 &self,
2969 ledger: Arc<dyn crate::runtime::control_events::ControlEventLedger>,
2970 ) {
2971 *self.inner.control_event_ledger.write() = ledger;
2972 }
2973
2974 #[inline(never)]
2975 pub fn with_options(options: RedDBOptions) -> RedDBResult<Self> {
2976 Self::with_pool(options, ConnectionPoolConfig::default())
2977 }
2978
2979 pub fn with_pool(
2980 options: RedDBOptions,
2981 pool_config: ConnectionPoolConfig,
2982 ) -> RedDBResult<Self> {
2983 let boot_open_start_ms = std::time::SystemTime::now()
2991 .duration_since(std::time::UNIX_EPOCH)
2992 .map(|d| d.as_millis() as u64)
2993 .unwrap_or(0);
2994 let db = Arc::new(
2995 RedDB::open_with_options(&options)
2996 .map_err(|err| RedDBError::Internal(err.to_string()))?,
2997 );
2998 let result_blob_cache = crate::storage::cache::BlobCache::open_with_l2(
2999 crate::storage::cache::BlobCacheConfig::default().with_l2_path(
3000 options
3001 .resolved_path("data.rdb")
3002 .with_extension("result-cache.l2"),
3003 ),
3004 )
3005 .map_err(|err| {
3006 RedDBError::Internal(format!("open result Blob Cache L2 failed: {err:?}"))
3007 })?;
3008 let storage_ready_ms = std::time::SystemTime::now()
3009 .duration_since(std::time::UNIX_EPOCH)
3010 .map(|d| d.as_millis() as u64)
3011 .unwrap_or(0);
3012
3013 let runtime = Self {
3014 inner: Arc::new(RuntimeInner {
3015 db: db.clone(),
3016 layout: PhysicalLayout::from_options(&options),
3017 indices: IndexCatalog::register_default_vector_graph(
3018 options.has_capability(crate::api::Capability::Table),
3019 options.has_capability(crate::api::Capability::Graph),
3020 ),
3021 pool_config,
3022 pool: Mutex::new(PoolState::default()),
3023 started_at_unix_ms: SystemTime::now()
3024 .duration_since(UNIX_EPOCH)
3025 .unwrap_or_default()
3026 .as_millis(),
3027 probabilistic: super::probabilistic_store::ProbabilisticStore::new(),
3028 index_store: super::index_store::IndexStore::new(),
3029 cdc: crate::replication::cdc::CdcBuffer::new(100_000),
3030 backup_scheduler: crate::replication::scheduler::BackupScheduler::new(3600),
3031 query_cache: parking_lot::RwLock::new(
3032 crate::storage::query::planner::cache::PlanCache::new(1000),
3033 ),
3034 result_cache: parking_lot::RwLock::new((
3035 HashMap::new(),
3036 std::collections::VecDeque::new(),
3037 )),
3038 result_blob_cache,
3039 result_blob_entries: parking_lot::RwLock::new((
3040 HashMap::new(),
3041 std::collections::VecDeque::new(),
3042 )),
3043 ask_answer_cache_entries: parking_lot::RwLock::new((
3044 HashSet::new(),
3045 std::collections::VecDeque::new(),
3046 )),
3047 result_cache_shadow_divergences: std::sync::atomic::AtomicU64::new(0),
3048 result_cache_hits: std::sync::atomic::AtomicU64::new(0),
3049 result_cache_misses: std::sync::atomic::AtomicU64::new(0),
3050 result_cache_evictions: std::sync::atomic::AtomicU64::new(0),
3051 ask_daily_spend: parking_lot::RwLock::new(HashMap::new()),
3052 queue_message_locks: parking_lot::RwLock::new(HashMap::new()),
3053 rmw_locks: RmwLockTable::new(),
3054 planner_dirty_tables: parking_lot::RwLock::new(HashSet::new()),
3055 ec_registry: Arc::new(crate::ec::config::EcRegistry::new()),
3056 config_registry: Arc::new(crate::auth::registry::ConfigRegistry::new()),
3057 ec_worker: crate::ec::worker::EcWorker::new(),
3058 auth_store: parking_lot::RwLock::new(None),
3059 oauth_validator: parking_lot::RwLock::new(None),
3060 views: parking_lot::RwLock::new(HashMap::new()),
3061 materialized_views: parking_lot::RwLock::new(
3062 crate::storage::cache::result::MaterializedViewCache::new(),
3063 ),
3064 retention_sweeper: parking_lot::RwLock::new(
3065 crate::runtime::retention_sweeper::RetentionSweeperState::new(),
3066 ),
3067 snapshot_manager: Arc::new(
3068 crate::storage::transaction::snapshot::SnapshotManager::new(),
3069 ),
3070 tx_contexts: parking_lot::RwLock::new(HashMap::new()),
3071 tx_local_tenants: parking_lot::RwLock::new(HashMap::new()),
3072 env_config_overrides: crate::runtime::config_overlay::collect_env_overrides(),
3073 lock_manager: Arc::new({
3074 let env = crate::runtime::config_overlay::collect_env_overrides();
3079 let timeout_ms = env
3080 .get("concurrency.locking.deadlock_timeout_ms")
3081 .and_then(|raw| raw.parse::<u64>().ok())
3082 .unwrap_or_else(|| {
3083 match crate::runtime::config_matrix::default_for(
3084 "concurrency.locking.deadlock_timeout_ms",
3085 ) {
3086 Some(crate::serde_json::Value::Number(n)) => n as u64,
3087 _ => 5000,
3088 }
3089 });
3090 let cfg = crate::storage::transaction::lock::LockConfig {
3091 default_timeout: std::time::Duration::from_millis(timeout_ms),
3092 ..Default::default()
3093 };
3094 crate::storage::transaction::lock::LockManager::new(cfg)
3095 }),
3096 rls_policies: parking_lot::RwLock::new(HashMap::new()),
3097 rls_enabled_tables: parking_lot::RwLock::new(HashSet::new()),
3098 foreign_tables: Arc::new(crate::storage::fdw::ForeignTableRegistry::with_builtins()),
3099 pending_tombstones: parking_lot::RwLock::new(HashMap::new()),
3100 pending_versioned_updates: parking_lot::RwLock::new(HashMap::new()),
3101 pending_kv_watch_events: parking_lot::RwLock::new(HashMap::new()),
3102 pending_store_wal_actions: parking_lot::RwLock::new(HashMap::new()),
3103 queue_wait_registry: std::sync::Arc::new(
3104 crate::runtime::queue_wait_registry::QueueWaitRegistry::new(),
3105 ),
3106 pending_queue_wakes: parking_lot::RwLock::new(HashMap::new()),
3107 tenant_tables: parking_lot::RwLock::new(HashMap::new()),
3108 ddl_epoch: std::sync::atomic::AtomicU64::new(0),
3109 write_gate: Arc::new(crate::runtime::write_gate::WriteGate::from_options(
3110 &options,
3111 )),
3112 lifecycle: crate::runtime::lifecycle::Lifecycle::new(),
3113 resource_limits: crate::runtime::resource_limits::ResourceLimits::from_env(),
3114 audit_log: {
3115 let data_path = options
3125 .data_path
3126 .clone()
3127 .unwrap_or_else(|| std::env::temp_dir().join("reddb"));
3128 let (audit_dest, _) = crate::api::tier_wiring::current_log_destinations();
3129 Arc::new(crate::runtime::audit_log::AuditLogger::for_destination(
3130 &audit_dest,
3131 &data_path,
3132 ))
3133 },
3134 control_event_ledger: parking_lot::RwLock::new(Arc::new(
3135 crate::runtime::control_events::RuntimeLedger::new(db.store()),
3136 )),
3137 control_event_config: options.control_events,
3138 query_audit: Arc::new(crate::runtime::query_audit::QueryAuditStream::new(
3139 db.store(),
3140 options.query_audit.clone(),
3141 )),
3142 lease_lifecycle: std::sync::OnceLock::new(),
3143 replica_apply_metrics: std::sync::Arc::new(
3144 crate::replication::logical::ReplicaApplyMetrics::default(),
3145 ),
3146 quota_bucket: crate::runtime::quota_bucket::QuotaBucket::from_env(),
3147 schema_vocabulary: parking_lot::RwLock::new(
3148 crate::runtime::schema_vocabulary::SchemaVocabulary::new(),
3149 ),
3150 slow_query_logger: {
3151 let fallback_dir = options
3164 .data_path
3165 .as_ref()
3166 .and_then(|p| p.parent().map(std::path::PathBuf::from))
3167 .unwrap_or_else(|| std::env::temp_dir().join("reddb"));
3168 let threshold_ms = std::env::var("RED_SLOW_QUERY_THRESHOLD_MS")
3169 .ok()
3170 .and_then(|s| s.parse::<u64>().ok())
3171 .unwrap_or(1000);
3172 let sample_pct = std::env::var("RED_SLOW_QUERY_SAMPLE_PCT")
3173 .ok()
3174 .and_then(|s| s.parse::<u8>().ok())
3175 .unwrap_or(100);
3176 let (_, slow_dest) = crate::api::tier_wiring::current_log_destinations();
3177 crate::telemetry::slow_query_logger::SlowQueryLogger::for_destination(
3178 &slow_dest,
3179 &fallback_dir,
3180 threshold_ms,
3181 sample_pct,
3182 )
3183 },
3184 kv_stats: crate::runtime::KvStatsCounters::default(),
3185 metrics_ingest_stats: crate::runtime::MetricsIngestCounters::default(),
3186 metrics_tenant_activity_stats:
3187 crate::runtime::MetricsTenantActivityCounters::default(),
3188 queue_telemetry: Arc::new(
3189 crate::runtime::queue_telemetry::QueueTelemetryCounters::default(),
3190 ),
3191 queue_presence: Arc::new(
3192 crate::storage::queue::presence::ConsumerPresenceRegistry::new(),
3193 ),
3194 vector_introspection: Arc::new(
3195 crate::storage::vector::introspection::VectorIntrospectionRegistry::new(),
3196 ),
3197 kv_tag_index: crate::runtime::KvTagIndex::default(),
3198 chain_tip_cache: parking_lot::Mutex::new(HashMap::new()),
3199 chain_integrity_broken: parking_lot::Mutex::new(HashMap::new()),
3200 integrity_tombstones: parking_lot::Mutex::new(Vec::new()),
3201 integrity_tombstones_state: std::sync::atomic::AtomicU8::new(0),
3202 }),
3203 };
3204
3205 crate::telemetry::operator_event::install_global_audit_sink(Arc::clone(
3211 &runtime.inner.audit_log,
3212 ));
3213
3214 runtime
3222 .inner
3223 .lifecycle
3224 .set_restore_started_at_ms(boot_open_start_ms);
3225 runtime
3226 .inner
3227 .lifecycle
3228 .set_restore_ready_at_ms(storage_ready_ms);
3229 runtime
3230 .inner
3231 .lifecycle
3232 .set_wal_replay_started_at_ms(boot_open_start_ms);
3233 runtime
3234 .inner
3235 .lifecycle
3236 .set_wal_replay_ready_at_ms(storage_ready_ms);
3237
3238 let restored_cdc_lsn = runtime
3239 .inner
3240 .db
3241 .replication
3242 .as_ref()
3243 .map(|repl| {
3244 repl.logical_wal_spool
3245 .as_ref()
3246 .map(|spool| spool.current_lsn())
3247 .unwrap_or(0)
3248 })
3249 .unwrap_or(0)
3250 .max(runtime.config_u64("red.config.timeline.last_archived_lsn", 0));
3251 runtime.inner.cdc.set_current_lsn(restored_cdc_lsn);
3252 runtime.rehydrate_snapshot_xid_floor();
3253 runtime.bootstrap_system_keyed_collections()?;
3254 runtime.rehydrate_declared_column_schemas();
3255 runtime.load_probabilistic_state()?;
3256
3257 runtime.rehydrate_tenant_tables();
3261 runtime.rehydrate_materialized_view_descriptors();
3266 if let Some(repl) = &runtime.inner.db.replication {
3267 repl.wal_buffer.set_current_lsn(restored_cdc_lsn);
3268 }
3269
3270 {
3272 let sys = SystemInfo::collect();
3273 runtime.inner.db.store().set_config_tree(
3274 "red.system",
3275 &crate::serde_json::json!({
3276 "pid": sys.pid,
3277 "cpu_cores": sys.cpu_cores,
3278 "total_memory_bytes": sys.total_memory_bytes,
3279 "available_memory_bytes": sys.available_memory_bytes,
3280 "os": sys.os,
3281 "arch": sys.arch,
3282 "hostname": sys.hostname,
3283 "started_at": SystemTime::now()
3284 .duration_since(UNIX_EPOCH)
3285 .unwrap_or_default()
3286 .as_millis() as u64
3287 }),
3288 );
3289
3290 let store = runtime.inner.db.store();
3292 if store
3293 .get_collection("red_config")
3294 .map(|m| m.query_all(|_| true).len())
3295 .unwrap_or(0)
3296 <= 10
3297 {
3298 store.set_config_tree("red.ai", &crate::json!({
3299 "default": crate::json!({
3300 "provider": "openai",
3301 "model": crate::ai::DEFAULT_OPENAI_PROMPT_MODEL
3302 }),
3303 "max_embedding_inputs": 256,
3304 "max_prompt_batch": 256,
3305 "timeout": crate::json!({ "connect_secs": 10, "read_secs": 90, "write_secs": 30 })
3306 }));
3307 store.set_config_tree(
3308 "red.server",
3309 &crate::json!({
3310 "max_scan_limit": 1000,
3311 "max_body_size": 1048576,
3312 "read_timeout_ms": 5000,
3313 "write_timeout_ms": 5000
3314 }),
3315 );
3316 store.set_config_tree(
3317 "red.storage",
3318 &crate::json!({
3319 "page_size": 4096,
3320 "page_cache_capacity": 100000,
3321 "auto_checkpoint_pages": 1000,
3322 "snapshot_retention": 16,
3323 "verify_checksums": true,
3324 "segment": crate::json!({
3325 "max_entities": 100000,
3326 "max_bytes": 268435456_u64,
3327 "compression_level": 6
3328 }),
3329 "hnsw": crate::json!({ "m": 16, "ef_construction": 100, "ef_search": 50 }),
3330 "ivf": crate::json!({ "n_lists": 100, "n_probes": 10 }),
3331 "bm25": crate::json!({ "k1": 1.2, "b": 0.75 })
3332 }),
3333 );
3334 store.set_config_tree(
3335 "red.search",
3336 &crate::json!({
3337 "rag": crate::json!({
3338 "max_chunks_per_source": 10,
3339 "max_total_chunks": 25,
3340 "similarity_threshold": 0.8,
3341 "graph_depth": 2,
3342 "min_relevance": 0.3
3343 }),
3344 "fusion": crate::json!({
3345 "vector_weight": 0.5,
3346 "graph_weight": 0.3,
3347 "table_weight": 0.2,
3348 "dedup_threshold": 0.85
3349 })
3350 }),
3351 );
3352 store.set_config_tree(
3353 "red.auth",
3354 &crate::json!({
3355 "enabled": false,
3356 "session_ttl_secs": 3600,
3357 "require_auth": false
3358 }),
3359 );
3360 store.set_config_tree(
3361 "red.query",
3362 &crate::json!({
3363 "connection_pool": crate::json!({ "max_connections": 64, "max_idle": 16 }),
3364 "max_recursion_depth": 1000
3365 }),
3366 );
3367 store.set_config_tree(
3368 "red.indexes",
3369 &crate::json!({
3370 "auto_select": true,
3371 "bloom_filter": crate::json!({
3372 "enabled": true,
3373 "false_positive_rate": 0.01,
3374 "prune_on_scan": true
3375 }),
3376 "hash": crate::json!({ "enabled": true }),
3377 "bitmap": crate::json!({ "enabled": true, "max_cardinality": 1000 }),
3378 "spatial": crate::json!({ "enabled": true })
3379 }),
3380 );
3381 store.set_config_tree(
3382 "red.memtable",
3383 &crate::json!({
3384 "enabled": true,
3385 "max_bytes": 67108864_u64,
3386 "flush_threshold": 0.75
3387 }),
3388 );
3389 store.set_config_tree(
3390 "red.probabilistic",
3391 &crate::json!({
3392 "hll_registers": 16384,
3393 "sketch_default_width": 1000,
3394 "sketch_default_depth": 5,
3395 "filter_default_capacity": 100000
3396 }),
3397 );
3398 store.set_config_tree(
3399 "red.timeseries",
3400 &crate::json!({
3401 "default_chunk_size": 1024,
3402 "compression": crate::json!({
3403 "timestamps": "delta_of_delta",
3404 "values": "gorilla_xor"
3405 }),
3406 "default_retention_days": 0
3407 }),
3408 );
3409 store.set_config_tree(
3410 "red.queue",
3411 &crate::json!({
3412 "default_max_size": 0,
3413 "default_max_attempts": 3,
3414 "visibility_timeout_ms": 30000,
3415 "consumer_idle_timeout_ms": 60000
3416 }),
3417 );
3418 store.set_config_tree(
3419 "red.backup",
3420 &crate::json!({
3421 "enabled": false,
3422 "interval_secs": 3600,
3423 "retention_count": 24,
3424 "upload": false,
3425 "backend": "local"
3426 }),
3427 );
3428 store.set_config_tree(
3429 "red.wal",
3430 &crate::json!({
3431 "archive": crate::json!({
3432 "enabled": false,
3433 "retention_hours": 168,
3434 "prefix": "wal/"
3435 })
3436 }),
3437 );
3438 store.set_config_tree(
3439 "red.cdc",
3440 &crate::json!({
3441 "enabled": true,
3442 "buffer_size": 100000
3443 }),
3444 );
3445 store.set_config_tree(
3446 "red.config.secret",
3447 &crate::json!({
3448 "auto_encrypt": true,
3449 "auto_decrypt": true
3450 }),
3451 );
3452 }
3453
3454 crate::runtime::config_matrix::heal_critical_keys(store.as_ref());
3461
3462 let lehman_yao = runtime.config_bool("storage.btree.lehman_yao", true);
3469 crate::storage::engine::btree::lehman_yao::set_enabled(lehman_yao);
3470 if lehman_yao {
3471 tracing::info!(
3472 "storage.btree.lehman_yao=true — lock-free concurrent descent enabled"
3473 );
3474 }
3475
3476 let overlay_path = crate::runtime::config_overlay::config_file_path();
3481 let _ =
3482 crate::runtime::config_overlay::apply_config_file(store.as_ref(), &overlay_path);
3483 }
3484
3485 {
3489 let store = runtime.inner.db.store();
3490 for name in crate::application::vcs_collections::ALL {
3491 let _ = store.get_or_create_collection(*name);
3492 }
3493 store.set_config_tree(
3496 crate::application::vcs_collections::CONFIG_NAMESPACE,
3497 &crate::json!({
3498 "default_branch": "main",
3499 "author": crate::json!({
3500 "name": "reddb",
3501 "email": "reddb@localhost"
3502 }),
3503 "protected_branches": crate::json!(["main"]),
3504 "closure": crate::json!({
3505 "enabled": true,
3506 "lazy": true
3507 }),
3508 "merge": crate::json!({
3509 "default_strategy": "auto",
3510 "fast_forward": true
3511 })
3512 }),
3513 );
3514 }
3515
3516 {
3519 let store = runtime.inner.db.store();
3520 for name in crate::application::migration_collections::ALL {
3521 let _ = store.get_or_create_collection(*name);
3522 }
3523 }
3524
3525 let _ = crate::application::topology_collections::ensure(&runtime);
3529
3530 {
3545 let weak = Arc::downgrade(&runtime.inner);
3546 std::thread::Builder::new()
3547 .name("reddb-maintenance".into())
3548 .spawn(move || {
3549 let tick = std::time::Duration::from_millis(200);
3550 let work_interval = std::time::Duration::from_secs(60);
3551 let mut last_work = std::time::Instant::now();
3552 loop {
3553 std::thread::sleep(tick);
3554 let Some(inner) = weak.upgrade() else {
3555 break;
3558 };
3559 if last_work.elapsed() >= work_interval {
3560 let _stats = inner.db.store().context_index().stats();
3561 last_work = std::time::Instant::now();
3562 }
3563 }
3564 })
3565 .ok();
3566 }
3567
3568 {
3570 let store = runtime.inner.db.store();
3571 let mut backup_enabled = false;
3572 let mut backup_interval = 3600u64;
3573
3574 if let Some(manager) = store.get_collection("red_config") {
3575 manager.for_each_entity(|entity| {
3576 if let Some(row) = entity.data.as_row() {
3577 let key = row.get_field("key").and_then(|v| match v {
3578 crate::storage::schema::Value::Text(s) => Some(s.as_ref()),
3579 _ => None,
3580 });
3581 let val = row.get_field("value");
3582 if key == Some("red.config.backup.enabled") {
3583 backup_enabled = match val {
3584 Some(crate::storage::schema::Value::Boolean(true)) => true,
3585 Some(crate::storage::schema::Value::Text(s)) => &**s == "true",
3586 _ => false,
3587 };
3588 } else if key == Some("red.config.backup.interval_secs") {
3589 if let Some(crate::storage::schema::Value::Integer(n)) = val {
3590 backup_interval = *n as u64;
3591 }
3592 }
3593 }
3594 true
3595 });
3596 }
3597
3598 if backup_enabled {
3599 runtime.inner.backup_scheduler.set_interval(backup_interval);
3600 let rt = runtime.clone();
3601 runtime
3602 .inner
3603 .backup_scheduler
3604 .start(move || rt.trigger_backup().map_err(|e| format!("{}", e)));
3605 }
3606 }
3607
3608 {
3610 runtime
3611 .inner
3612 .ec_registry
3613 .load_from_config_store(runtime.inner.db.store().as_ref());
3614 if !runtime.inner.ec_registry.async_configs().is_empty() {
3615 runtime.inner.ec_worker.start(
3616 Arc::clone(&runtime.inner.ec_registry),
3617 Arc::clone(&runtime.inner.db.store()),
3618 );
3619 }
3620 }
3621
3622 if let crate::replication::ReplicationRole::Replica { primary_addr } =
3623 runtime.inner.db.options().replication.role.clone()
3624 {
3625 let rt = runtime.clone();
3626 std::thread::Builder::new()
3627 .name("reddb-replica".into())
3628 .spawn(move || rt.run_replica_loop(primary_addr))
3629 .ok();
3630 }
3631
3632 runtime.inner.lifecycle.mark_ready();
3637
3638 {
3647 let weak_inner = Arc::downgrade(&runtime.inner);
3648 std::thread::Builder::new()
3649 .name("reddb-mv-scheduler".into())
3650 .spawn(move || loop {
3651 std::thread::sleep(std::time::Duration::from_millis(50));
3652 let Some(inner) = weak_inner.upgrade() else {
3653 break;
3654 };
3655 let rt = RedDBRuntime { inner };
3656 rt.refresh_due_materialized_views();
3657 })
3658 .ok();
3659 }
3660
3661 if !runtime.write_gate().is_read_only() {
3671 let weak_inner = Arc::downgrade(&runtime.inner);
3672 std::thread::Builder::new()
3673 .name("reddb-retention-sweeper".into())
3674 .spawn(move || loop {
3675 std::thread::sleep(std::time::Duration::from_millis(500));
3676 let Some(inner) = weak_inner.upgrade() else {
3677 break;
3678 };
3679 let rt = RedDBRuntime { inner };
3680 rt.sweep_retention_tick(
3681 crate::runtime::retention_sweeper::DEFAULT_SWEEPER_BATCH,
3682 );
3683 })
3684 .ok();
3685 }
3686
3687 Ok(runtime)
3688 }
3689
3690 fn rehydrate_snapshot_xid_floor(&self) {
3691 let store = self.inner.db.store();
3692 for collection in store.list_collections() {
3693 let Some(manager) = store.get_collection(&collection) else {
3694 continue;
3695 };
3696 for entity in manager.query_all(|_| true) {
3697 self.inner
3698 .snapshot_manager
3699 .observe_committed_xid(entity.xmin);
3700 self.inner
3701 .snapshot_manager
3702 .observe_committed_xid(entity.xmax);
3703 }
3704 }
3705 }
3706
3707 pub(crate) fn ensure_materialized_view_backing(&self, name: &str) -> RedDBResult<()> {
3720 let store = self.inner.db.store();
3721 let mut changed = false;
3722 if store.get_collection(name).is_none() {
3723 store.get_or_create_collection(name);
3724 changed = true;
3725 }
3726 if self.inner.db.collection_contract(name).is_none() {
3727 self.inner
3728 .db
3729 .save_collection_contract(system_keyed_collection_contract(
3730 name,
3731 crate::catalog::CollectionModel::Table,
3732 ))
3733 .map_err(|err| RedDBError::Internal(err.to_string()))?;
3734 changed = true;
3735 }
3736 if changed {
3737 self.inner
3738 .db
3739 .persist_metadata()
3740 .map_err(|err| RedDBError::Internal(err.to_string()))?;
3741 }
3742 Ok(())
3743 }
3744
3745 pub(crate) fn drop_materialized_view_backing(&self, name: &str) -> RedDBResult<()> {
3750 let store = self.inner.db.store();
3751 if store.get_collection(name).is_none() {
3752 return Ok(());
3753 }
3754 store
3755 .drop_collection(name)
3756 .map_err(|err| RedDBError::Internal(err.to_string()))?;
3757 if self.inner.db.collection_contract(name).is_some() {
3760 self.inner
3761 .db
3762 .remove_collection_contract(name)
3763 .map_err(|err| RedDBError::Internal(err.to_string()))?;
3764 }
3765 self.invalidate_result_cache();
3766 self.inner
3767 .db
3768 .persist_metadata()
3769 .map_err(|err| RedDBError::Internal(err.to_string()))?;
3770 Ok(())
3771 }
3772
3773 fn bootstrap_system_keyed_collections(&self) -> RedDBResult<()> {
3774 let mut changed = false;
3775 for (name, model) in [
3776 ("red.config", crate::catalog::CollectionModel::Config),
3777 ("red.vault", crate::catalog::CollectionModel::Vault),
3778 (
3782 crate::runtime::continuous_materialized_view::CATALOG_COLLECTION,
3783 crate::catalog::CollectionModel::Config,
3784 ),
3785 ] {
3786 if self.inner.db.store().get_collection(name).is_none() {
3787 self.inner.db.store().get_or_create_collection(name);
3788 changed = true;
3789 }
3790 if self.inner.db.collection_contract(name).is_none() {
3791 self.inner
3792 .db
3793 .save_collection_contract(system_keyed_collection_contract(name, model))
3794 .map_err(|err| RedDBError::Internal(err.to_string()))?;
3795 changed = true;
3796 }
3797 }
3798 if changed {
3799 self.inner
3800 .db
3801 .persist_metadata()
3802 .map_err(|err| RedDBError::Internal(err.to_string()))?;
3803 }
3804 Ok(())
3805 }
3806
3807 pub fn db(&self) -> Arc<RedDB> {
3808 Arc::clone(&self.inner.db)
3809 }
3810
3811 pub fn index_store_ref(&self) -> &super::index_store::IndexStore {
3816 &self.inner.index_store
3817 }
3818
3819 pub(crate) fn schema_vocabulary_apply(
3824 &self,
3825 event: crate::runtime::schema_vocabulary::DdlEvent,
3826 ) {
3827 self.inner.schema_vocabulary.write().on_ddl(event);
3828 }
3829
3830 pub fn schema_vocabulary_lookup(
3835 &self,
3836 token: &str,
3837 ) -> Vec<crate::runtime::schema_vocabulary::VocabHit> {
3838 self.inner.schema_vocabulary.read().lookup(token).to_vec()
3839 }
3840
3841 pub fn set_auth_store(&self, store: Arc<crate::auth::store::AuthStore>) {
3845 *self.inner.auth_store.write() = Some(store);
3846 }
3847
3848 pub fn auth_store(&self) -> Option<Arc<crate::auth::store::AuthStore>> {
3851 self.inner.auth_store.read().clone()
3852 }
3853
3854 pub fn vault_kv_get(&self, key: &str) -> Option<String> {
3856 self.inner
3857 .auth_store
3858 .read()
3859 .as_ref()
3860 .and_then(|store| store.vault_kv_get(key))
3861 }
3862
3863 pub fn vault_kv_try_set(&self, key: String, value: String) -> RedDBResult<()> {
3866 let store = self.inner.auth_store.read().clone().ok_or_else(|| {
3867 RedDBError::Query("secret storage requires an enabled, unsealed vault".to_string())
3868 })?;
3869 store
3870 .vault_kv_try_set(key, value)
3871 .map_err(|err| RedDBError::Query(err.to_string()))
3872 }
3873
3874 pub fn set_oauth_validator(&self, validator: Option<Arc<crate::auth::oauth::OAuthValidator>>) {
3878 *self.inner.oauth_validator.write() = validator;
3879 }
3880
3881 pub fn oauth_validator(&self) -> Option<Arc<crate::auth::oauth::OAuthValidator>> {
3885 self.inner.oauth_validator.read().clone()
3886 }
3887
3888 pub(crate) fn secret_aes_key(&self) -> Option<[u8; 32]> {
3892 let guard = self.inner.auth_store.read();
3893 guard.as_ref().and_then(|s| s.vault_secret_key())
3894 }
3895
3896 pub(crate) fn config_bool(&self, key: &str, default: bool) -> bool {
3902 if let Some(raw) = self.inner.env_config_overrides.get(key) {
3903 if let Some(crate::storage::schema::Value::Boolean(b)) =
3904 crate::runtime::config_overlay::coerce_env_value(key, raw)
3905 {
3906 return b;
3907 }
3908 }
3909 let store = self.inner.db.store();
3910 let Some(manager) = store.get_collection("red_config") else {
3911 return default;
3912 };
3913 let mut result = default;
3914 let mut latest_id: u64 = 0;
3915 manager.for_each_entity(|entity| {
3916 if let Some(row) = entity.data.as_row() {
3917 let entry_key = row.get_field("key").and_then(|v| match v {
3918 crate::storage::schema::Value::Text(s) => Some(s.as_ref()),
3919 _ => None,
3920 });
3921 if entry_key == Some(key) {
3922 let id = entity.id.raw();
3923 if id >= latest_id {
3924 latest_id = id;
3925 result = match row.get_field("value") {
3926 Some(crate::storage::schema::Value::Boolean(b)) => *b,
3927 Some(crate::storage::schema::Value::Text(s)) => {
3928 matches!(s.as_ref(), "true" | "TRUE" | "True" | "1")
3929 }
3930 Some(crate::storage::schema::Value::Integer(n)) => *n != 0,
3931 _ => default,
3932 };
3933 }
3934 }
3935 }
3936 true
3937 });
3938 result
3939 }
3940
3941 pub(crate) fn config_u64(&self, key: &str, default: u64) -> u64 {
3942 if let Some(raw) = self.inner.env_config_overrides.get(key) {
3943 if let Some(crate::storage::schema::Value::UnsignedInteger(n)) =
3944 crate::runtime::config_overlay::coerce_env_value(key, raw)
3945 {
3946 return n;
3947 }
3948 }
3949 let store = self.inner.db.store();
3950 let Some(manager) = store.get_collection("red_config") else {
3951 return default;
3952 };
3953 let mut result = default;
3954 let mut latest_id: u64 = 0;
3955 manager.for_each_entity(|entity| {
3956 if let Some(row) = entity.data.as_row() {
3957 let entry_key = row.get_field("key").and_then(|v| match v {
3958 crate::storage::schema::Value::Text(s) => Some(s.as_ref()),
3959 _ => None,
3960 });
3961 if entry_key == Some(key) {
3962 let id = entity.id.raw();
3963 if id >= latest_id {
3964 latest_id = id;
3965 result = match row.get_field("value") {
3966 Some(crate::storage::schema::Value::Integer(n)) => *n as u64,
3967 Some(crate::storage::schema::Value::UnsignedInteger(n)) => *n,
3968 Some(crate::storage::schema::Value::Text(s)) => {
3969 s.parse::<u64>().unwrap_or(default)
3970 }
3971 _ => default,
3972 };
3973 }
3974 }
3975 }
3976 true
3977 });
3978 result
3979 }
3980
3981 pub(crate) fn config_f64(&self, key: &str, default: f64) -> f64 {
3982 if let Some(raw) = self.inner.env_config_overrides.get(key) {
3983 if let Ok(n) = raw.parse::<f64>() {
3984 return n;
3985 }
3986 }
3987 let store = self.inner.db.store();
3988 let Some(manager) = store.get_collection("red_config") else {
3989 return default;
3990 };
3991 let mut result = default;
3992 let mut latest_id: u64 = 0;
3993 manager.for_each_entity(|entity| {
3994 if let Some(row) = entity.data.as_row() {
3995 let entry_key = row.get_field("key").and_then(|v| match v {
3996 crate::storage::schema::Value::Text(s) => Some(s.as_ref()),
3997 _ => None,
3998 });
3999 if entry_key == Some(key) {
4000 let id = entity.id.raw();
4001 if id >= latest_id {
4002 latest_id = id;
4003 result = match row.get_field("value") {
4004 Some(crate::storage::schema::Value::Float(n)) => *n,
4005 Some(crate::storage::schema::Value::Integer(n)) => *n as f64,
4006 Some(crate::storage::schema::Value::UnsignedInteger(n)) => *n as f64,
4007 Some(crate::storage::schema::Value::Text(s)) => {
4008 s.parse::<f64>().unwrap_or(default)
4009 }
4010 _ => default,
4011 };
4012 }
4013 }
4014 }
4015 true
4016 });
4017 result
4018 }
4019
4020 pub(crate) fn config_string(&self, key: &str, default: &str) -> String {
4021 if let Some(raw) = self.inner.env_config_overrides.get(key) {
4022 return raw.clone();
4023 }
4024 let store = self.inner.db.store();
4025 let Some(manager) = store.get_collection("red_config") else {
4026 return default.to_string();
4027 };
4028 let mut result = default.to_string();
4029 let mut latest_id: u64 = 0;
4030 manager.for_each_entity(|entity| {
4031 if let Some(row) = entity.data.as_row() {
4032 let entry_key = row.get_field("key").and_then(|v| match v {
4033 crate::storage::schema::Value::Text(s) => Some(s.as_ref()),
4034 _ => None,
4035 });
4036 if entry_key == Some(key) {
4037 let id = entity.id.raw();
4038 if id >= latest_id {
4039 latest_id = id;
4040 if let Some(crate::storage::schema::Value::Text(value)) =
4041 row.get_field("value")
4042 {
4043 result = value.to_string();
4044 }
4045 }
4046 }
4047 }
4048 true
4049 });
4050 result
4051 }
4052
4053 fn latest_metadata_for(
4054 &self,
4055 collection: &str,
4056 entity_id: u64,
4057 ) -> Option<crate::serde_json::Value> {
4058 self.inner
4059 .db
4060 .store()
4061 .get_metadata(collection, EntityId::new(entity_id))
4062 .map(|metadata| metadata_to_json(&metadata))
4063 }
4064
4065 fn persist_replica_lsn(&self, lsn: u64) {
4066 self.inner.db.store().set_config_tree(
4067 "red.replication",
4068 &crate::json!({
4069 "last_applied_lsn": lsn
4070 }),
4071 );
4072 }
4073
4074 fn resolve_replica_id(&self) -> String {
4081 let configured = self.config_string("red.replication.replica_id", "");
4082 if !configured.is_empty() {
4083 return configured;
4084 }
4085 let generated = crate::crypto::uuid::Uuid::new_v4().to_string();
4086 self.inner.db.store().set_config_tree(
4087 "red.replication",
4088 &crate::json!({
4089 "replica_id": generated.clone()
4090 }),
4091 );
4092 generated
4093 }
4094
4095 fn persist_replication_health(
4096 &self,
4097 state: &str,
4098 last_error: &str,
4099 primary_lsn: Option<u64>,
4100 oldest_available_lsn: Option<u64>,
4101 ) {
4102 self.inner.db.store().set_config_tree(
4103 "red.replication",
4104 &crate::json!({
4105 "state": state,
4106 "last_error": last_error,
4107 "last_seen_primary_lsn": primary_lsn.unwrap_or(0),
4108 "last_seen_oldest_lsn": oldest_available_lsn.unwrap_or(0),
4109 "updated_at_unix_ms": SystemTime::now()
4110 .duration_since(UNIX_EPOCH)
4111 .unwrap_or_default()
4112 .as_millis() as u64
4113 }),
4114 );
4115 }
4116
4117 pub(crate) fn secret_auto_encrypt(&self) -> bool {
4120 self.config_bool("red.config.secret.auto_encrypt", true)
4121 }
4122
4123 pub(crate) fn secret_auto_decrypt(&self) -> bool {
4128 self.config_bool("red.config.secret.auto_decrypt", true)
4129 }
4130
4131 pub(crate) fn apply_secret_decryption(&self, result: &mut RuntimeQueryResult) {
4138 if !self.secret_auto_decrypt() {
4139 return;
4140 }
4141 let Some(key) = self.secret_aes_key() else {
4142 return;
4143 };
4144 for record in result.result.records.iter_mut() {
4145 for value in record.values_mut() {
4146 if let Value::Secret(ref bytes) = value {
4147 if let Some(plain) =
4148 super::impl_dml::decrypt_secret_payload(&key, bytes.as_slice())
4149 {
4150 if let Ok(text) = String::from_utf8(plain) {
4151 *value = Value::text(text);
4152 }
4153 }
4154 }
4155 }
4156 }
4157 }
4158
4159 pub(crate) fn mutation_engine(&self) -> crate::runtime::mutation::MutationEngine<'_> {
4167 crate::runtime::mutation::MutationEngine::new(self)
4168 }
4169
4170 pub fn check_write(&self, kind: crate::runtime::write_gate::WriteKind) -> RedDBResult<()> {
4181 self.inner.write_gate.check(kind)
4182 }
4183
4184 pub fn write_gate(&self) -> &crate::runtime::write_gate::WriteGate {
4188 &self.inner.write_gate
4189 }
4190
4191 pub fn lifecycle(&self) -> &crate::runtime::lifecycle::Lifecycle {
4195 &self.inner.lifecycle
4196 }
4197
4198 pub fn resource_limits(&self) -> &crate::runtime::resource_limits::ResourceLimits {
4200 &self.inner.resource_limits
4201 }
4202
4203 pub fn audit_log(&self) -> &crate::runtime::audit_log::AuditLogger {
4205 &self.inner.audit_log
4206 }
4207
4208 pub fn audit_log_arc(&self) -> Arc<crate::runtime::audit_log::AuditLogger> {
4212 Arc::clone(&self.inner.audit_log)
4213 }
4214
4215 pub(crate) fn emit_control_event(
4216 &self,
4217 kind: crate::runtime::control_events::EventKind,
4218 outcome: crate::runtime::control_events::Outcome,
4219 action: &'static str,
4220 resource: Option<String>,
4221 reason: Option<String>,
4222 extra_fields: Vec<(String, crate::runtime::control_events::Sensitivity)>,
4223 ) -> RedDBResult<()> {
4224 use crate::runtime::control_events::{
4225 ActorRef, ControlEvent, ControlEventCtx, ControlEventLedger, Sensitivity,
4226 };
4227
4228 let tenant = current_tenant();
4229 let principal = current_auth_identity();
4230 let actor_user = principal
4231 .as_ref()
4232 .map(|(principal, _)| UserId::from_parts(tenant.as_deref(), principal));
4233 let actor = actor_user
4234 .as_ref()
4235 .map(ActorRef::User)
4236 .unwrap_or(ActorRef::Anonymous);
4237 let ctx = ControlEventCtx {
4238 actor,
4239 scope: tenant
4240 .as_ref()
4241 .map(|scope| std::borrow::Cow::Borrowed(scope.as_str())),
4242 request_id: Some(std::borrow::Cow::Owned(format!(
4243 "conn-{}",
4244 current_connection_id()
4245 ))),
4246 trace_id: None,
4247 };
4248 let mut fields = std::collections::HashMap::new();
4249 fields.insert(
4250 "connection_id".to_string(),
4251 Sensitivity::raw(current_connection_id().to_string()),
4252 );
4253 if let Some((_, role)) = principal {
4254 fields.insert("actor_role".to_string(), Sensitivity::raw(role.as_str()));
4255 }
4256 for (key, value) in extra_fields {
4257 fields.insert(key, value);
4258 }
4259 let event = ControlEvent {
4260 kind,
4261 outcome,
4262 action: std::borrow::Cow::Borrowed(action),
4263 resource,
4264 reason,
4265 matched_policy_id: None,
4266 fields,
4267 };
4268 let ledger = self.inner.control_event_ledger.read();
4269 match ledger.emit(&ctx, event) {
4270 Ok(_) => Ok(()),
4271 Err(err) if self.inner.control_event_config.require_persistence() => {
4272 Err(RedDBError::Internal(err.to_string()))
4273 }
4274 Err(_) => Ok(()),
4275 }
4276 }
4277
4278 fn policy_mutation_control_ctx<'a>(
4279 &self,
4280 actor: &'a crate::auth::UserId,
4281 tenant: Option<&'a str>,
4282 ) -> crate::runtime::control_events::ControlEventCtx<'a> {
4283 crate::runtime::control_events::ControlEventCtx {
4284 actor: crate::runtime::control_events::ActorRef::User(actor),
4285 scope: tenant.map(std::borrow::Cow::Borrowed),
4286 request_id: Some(std::borrow::Cow::Owned(format!(
4287 "conn-{}",
4288 current_connection_id()
4289 ))),
4290 trace_id: None,
4291 }
4292 }
4293
4294 fn emit_query_audit(
4295 &self,
4296 query: &str,
4297 plan: &QueryAuditPlan,
4298 duration_ms: u64,
4299 result: &RuntimeQueryResult,
4300 ) {
4301 if !self.inner.query_audit.has_rules() {
4302 return;
4303 }
4304 let actor = current_auth_identity().map(|(principal, _)| principal);
4305 let tenant = current_tenant();
4306 let row_count = if result.statement_type == "select" {
4307 result.result.records.len() as u64
4308 } else {
4309 result.affected_rows
4310 };
4311 self.inner
4312 .query_audit
4313 .emit(crate::runtime::query_audit::QueryAuditEvent {
4314 actor,
4315 tenant,
4316 statement_kind: plan.statement_kind,
4317 touched_collections: plan.collections.clone(),
4318 duration_ms,
4319 row_count,
4320 request_id: Some(crate::crypto::uuid::Uuid::new_v7().to_string()),
4321 query_hash: Some(blake3::hash(query.as_bytes()).to_hex().to_string()),
4322 });
4323 }
4324
4325 pub(crate) fn queue_telemetry(
4329 &self,
4330 ) -> &crate::runtime::queue_telemetry::QueueTelemetryCounters {
4331 &self.inner.queue_telemetry
4332 }
4333
4334 pub fn queue_telemetry_snapshot(
4337 &self,
4338 ) -> crate::runtime::queue_telemetry::QueueTelemetrySnapshot {
4339 crate::runtime::queue_telemetry::QueueTelemetrySnapshot {
4340 delivered: self.inner.queue_telemetry.delivered_snapshot(),
4341 acked: self.inner.queue_telemetry.acked_snapshot(),
4342 nacked: self.inner.queue_telemetry.nacked_snapshot(),
4343 wait_started: self.inner.queue_telemetry.wait_started_snapshot(),
4344 wait_woken: self.inner.queue_telemetry.wait_woken_snapshot(),
4345 wait_timed_out: self.inner.queue_telemetry.wait_timed_out_snapshot(),
4346 wait_cancelled: self.inner.queue_telemetry.wait_cancelled_snapshot(),
4347 wait_duration: self.inner.queue_telemetry.wait_duration_snapshot(),
4348 }
4349 }
4350
4351 pub(crate) fn queue_presence(
4356 &self,
4357 ) -> &std::sync::Arc<crate::storage::queue::presence::ConsumerPresenceRegistry> {
4358 &self.inner.queue_presence
4359 }
4360
4361 pub fn queue_consumer_presence_snapshot(
4366 &self,
4367 ttl_ms: u64,
4368 ) -> Vec<crate::storage::queue::presence::ConsumerPresence> {
4369 let now_ns = std::time::SystemTime::now()
4370 .duration_since(std::time::UNIX_EPOCH)
4371 .map(|d| d.as_nanos() as u64)
4372 .unwrap_or(0);
4373 self.inner.queue_presence.snapshot(now_ns, ttl_ms)
4374 }
4375
4376 pub fn queue_active_consumer_counts(
4380 &self,
4381 ttl_ms: u64,
4382 ) -> std::collections::HashMap<(String, String), u32> {
4383 let now_ns = std::time::SystemTime::now()
4384 .duration_since(std::time::UNIX_EPOCH)
4385 .map(|d| d.as_nanos() as u64)
4386 .unwrap_or(0);
4387 self.inner
4388 .queue_presence
4389 .count_active_by_group(now_ns, ttl_ms)
4390 }
4391
4392 pub(crate) fn vector_introspection_registry(
4398 &self,
4399 ) -> &std::sync::Arc<crate::storage::vector::introspection::VectorIntrospectionRegistry> {
4400 &self.inner.vector_introspection
4401 }
4402
4403 pub fn vector_introspection_snapshot(
4408 &self,
4409 ) -> Vec<crate::storage::vector::introspection::VectorIntrospection> {
4410 self.inner.vector_introspection.snapshot()
4411 }
4412
4413 pub fn vector_introspection_get(
4417 &self,
4418 collection: &str,
4419 ) -> Option<crate::storage::vector::introspection::VectorIntrospection> {
4420 self.inner.vector_introspection.get(collection)
4421 }
4422
4423 pub fn queue_pending_counts(&self) -> Vec<((String, String), u64)> {
4428 let store = self.inner.db.store();
4429 crate::runtime::impl_queue::pending_counts_by_group(store.as_ref())
4430 .into_iter()
4431 .collect()
4432 }
4433
4434 pub fn write_gate_arc(&self) -> Arc<crate::runtime::write_gate::WriteGate> {
4439 Arc::clone(&self.inner.write_gate)
4440 }
4441
4442 pub fn lease_lifecycle(&self) -> Option<&Arc<crate::runtime::lease_lifecycle::LeaseLifecycle>> {
4445 self.inner.lease_lifecycle.get()
4446 }
4447
4448 pub fn set_lease_lifecycle(
4451 &self,
4452 lifecycle: Arc<crate::runtime::lease_lifecycle::LeaseLifecycle>,
4453 ) -> Result<(), Arc<crate::runtime::lease_lifecycle::LeaseLifecycle>> {
4454 self.inner.lease_lifecycle.set(lifecycle)
4455 }
4456
4457 pub fn check_batch_size(&self, requested: usize) -> RedDBResult<()> {
4462 if self.inner.resource_limits.batch_size_exceeded(requested) {
4463 let max = self.inner.resource_limits.max_batch_size.unwrap_or(0);
4464 return Err(RedDBError::QuotaExceeded(format!(
4465 "max_batch_size:{requested}:{max}"
4466 )));
4467 }
4468 Ok(())
4469 }
4470
4471 pub fn check_db_size(&self) -> RedDBResult<()> {
4477 let Some(limit) = self.inner.resource_limits.max_db_size_bytes else {
4478 return Ok(());
4479 };
4480 if limit == 0 {
4481 return Ok(());
4482 }
4483 let Some(path) = self.inner.db.path() else {
4484 return Ok(());
4485 };
4486 let current = std::fs::metadata(path).map(|m| m.len()).unwrap_or(0);
4487 if current > limit {
4488 return Err(RedDBError::QuotaExceeded(format!(
4489 "max_db_size_bytes:{current}:{limit}"
4490 )));
4491 }
4492 Ok(())
4493 }
4494
4495 pub fn graceful_shutdown(
4513 &self,
4514 backup_on_shutdown: bool,
4515 ) -> RedDBResult<crate::runtime::lifecycle::ShutdownReport> {
4516 if !self.inner.lifecycle.begin_shutdown() {
4517 return Ok(self.inner.lifecycle.shutdown_report().unwrap_or_default());
4521 }
4522
4523 let started_ms = std::time::SystemTime::now()
4524 .duration_since(std::time::UNIX_EPOCH)
4525 .map(|d| d.as_millis() as u64)
4526 .unwrap_or(0);
4527 let mut report = crate::runtime::lifecycle::ShutdownReport {
4528 started_at_ms: started_ms,
4529 ..Default::default()
4530 };
4531
4532 let flush_res = self.inner.db.flush_local_only();
4538 report.flushed_wal = flush_res.is_ok();
4539 report.final_checkpoint = flush_res.is_ok();
4540 if let Err(err) = &flush_res {
4541 tracing::error!(
4542 target: "reddb::lifecycle",
4543 error = %err,
4544 "graceful_shutdown: local flush failed"
4545 );
4546 } else if let Err(lease_err) =
4547 self.assert_remote_write_allowed("shutdown/checkpoint_upload")
4548 {
4549 tracing::warn!(
4550 target: "reddb::serverless::lease",
4551 error = %lease_err,
4552 "graceful_shutdown: remote upload skipped — lease not held"
4553 );
4554 } else if let Err(err) = self.inner.db.upload_to_remote_backend() {
4555 tracing::error!(
4556 target: "reddb::lifecycle",
4557 error = %err,
4558 "graceful_shutdown: remote upload failed"
4559 );
4560 }
4561
4562 if backup_on_shutdown && self.inner.db.remote_backend.is_some() {
4567 match self.trigger_backup() {
4573 Ok(result) => {
4574 report.backup_uploaded = result.uploaded;
4575 }
4576 Err(err) => {
4577 tracing::warn!(
4578 target: "reddb::lifecycle",
4579 error = %err,
4580 "graceful_shutdown: final backup skipped"
4581 );
4582 }
4583 }
4584 }
4585
4586 let completed_ms = std::time::SystemTime::now()
4587 .duration_since(std::time::UNIX_EPOCH)
4588 .map(|d| d.as_millis() as u64)
4589 .unwrap_or(started_ms);
4590 report.completed_at_ms = completed_ms;
4591 report.duration_ms = completed_ms.saturating_sub(started_ms);
4592
4593 self.inner.lifecycle.finish_shutdown(report.clone());
4594 Ok(report)
4595 }
4596
4597 pub(crate) fn cdc_emit_no_cache_invalidate(
4603 &self,
4604 operation: crate::replication::cdc::ChangeOperation,
4605 collection: &str,
4606 entity_id: u64,
4607 entity_kind: &str,
4608 ) -> u64 {
4609 let lsn = self
4610 .inner
4611 .cdc
4612 .emit(operation, collection, entity_id, entity_kind);
4613
4614 if let Some(ref primary) = self.inner.db.replication {
4616 let store = self.inner.db.store();
4617 let entity = if operation == crate::replication::cdc::ChangeOperation::Delete {
4618 None
4619 } else {
4620 store.get(collection, EntityId::new(entity_id))
4621 };
4622 let record = ChangeRecord {
4623 term: self.current_replication_term(),
4624 lsn,
4625 timestamp: SystemTime::now()
4626 .duration_since(UNIX_EPOCH)
4627 .unwrap_or_default()
4628 .as_millis() as u64,
4629 operation,
4630 collection: collection.to_string(),
4631 entity_id,
4632 entity_kind: entity_kind.to_string(),
4633 entity_bytes: entity
4634 .as_ref()
4635 .map(|e| UnifiedStore::serialize_entity(e, store.format_version())),
4636 metadata: self.latest_metadata_for(collection, entity_id),
4637 refresh_records: None,
4638 };
4639 let encoded = record.encode();
4640 primary.append_logical_record(record.lsn, encoded);
4641 }
4642 lsn
4643 }
4644
4645 pub(crate) fn cdc_emit_insert_batch_no_cache_invalidate(
4646 &self,
4647 collection: &str,
4648 ids: &[EntityId],
4649 entity_kind: &str,
4650 ) -> Vec<u64> {
4651 if ids.is_empty() {
4652 return Vec::new();
4653 }
4654
4655 if self.inner.db.replication.is_none() {
4659 return self.inner.cdc.emit_batch_same_collection(
4660 crate::replication::cdc::ChangeOperation::Insert,
4661 collection,
4662 entity_kind,
4663 ids.iter().map(|id| id.raw()),
4664 );
4665 }
4666
4667 ids.iter()
4670 .map(|id| {
4671 self.cdc_emit_no_cache_invalidate(
4672 crate::replication::cdc::ChangeOperation::Insert,
4673 collection,
4674 id.raw(),
4675 entity_kind,
4676 )
4677 })
4678 .collect()
4679 }
4680
4681 pub fn cdc_emit(
4682 &self,
4683 operation: crate::replication::cdc::ChangeOperation,
4684 collection: &str,
4685 entity_id: u64,
4686 entity_kind: &str,
4687 ) -> u64 {
4688 let lsn = self
4689 .inner
4690 .cdc
4691 .emit(operation, collection, entity_id, entity_kind);
4692 self.invalidate_result_cache_for_table(collection);
4698
4699 if let Some(ref primary) = self.inner.db.replication {
4701 let store = self.inner.db.store();
4702 let entity = if operation == crate::replication::cdc::ChangeOperation::Delete {
4703 None
4704 } else {
4705 store.get(collection, EntityId::new(entity_id))
4706 };
4707 let record = ChangeRecord {
4708 term: self.current_replication_term(),
4709 lsn,
4710 timestamp: SystemTime::now()
4711 .duration_since(UNIX_EPOCH)
4712 .unwrap_or_default()
4713 .as_millis() as u64,
4714 operation,
4715 collection: collection.to_string(),
4716 entity_id,
4717 entity_kind: entity_kind.to_string(),
4718 entity_bytes: entity
4719 .as_ref()
4720 .map(|entity| UnifiedStore::serialize_entity(entity, store.format_version())),
4721 metadata: self.latest_metadata_for(collection, entity_id),
4722 refresh_records: None,
4723 };
4724 let encoded = record.encode();
4725 primary.append_logical_record(record.lsn, encoded);
4726 }
4727 lsn
4728 }
4729
4730 pub(crate) fn cdc_emit_kv(
4731 &self,
4732 operation: crate::replication::cdc::ChangeOperation,
4733 collection: &str,
4734 key: &str,
4735 entity_id: u64,
4736 before: Option<crate::json::Value>,
4737 after: Option<crate::json::Value>,
4738 ) -> u64 {
4739 let lsn = self
4740 .inner
4741 .cdc
4742 .emit_kv(operation, collection, key, entity_id, before, after);
4743 self.inner.kv_stats.incr_watch_events_emitted();
4744 self.invalidate_result_cache_for_table(collection);
4745 lsn
4746 }
4747
4748 pub(crate) fn record_kv_watch_event(
4749 &self,
4750 operation: crate::replication::cdc::ChangeOperation,
4751 collection: &str,
4752 key: &str,
4753 entity_id: u64,
4754 before: Option<crate::json::Value>,
4755 after: Option<crate::json::Value>,
4756 ) {
4757 if self.current_xid().is_some() {
4758 let conn_id = current_connection_id();
4759 let event = crate::replication::cdc::KvWatchEvent {
4760 collection: collection.to_string(),
4761 key: key.to_string(),
4762 op: operation,
4763 before,
4764 after,
4765 lsn: 0,
4766 committed_at: 0,
4767 dropped_event_count: 0,
4768 };
4769 self.inner
4770 .pending_kv_watch_events
4771 .write()
4772 .entry(conn_id)
4773 .or_default()
4774 .push(event);
4775 return;
4776 }
4777
4778 self.cdc_emit_kv(operation, collection, key, entity_id, before, after);
4779 }
4780
4781 pub(crate) fn cdc_emit_prebuilt(
4782 &self,
4783 operation: crate::replication::cdc::ChangeOperation,
4784 collection: &str,
4785 entity: &UnifiedEntity,
4786 entity_kind: &str,
4787 metadata: Option<&crate::storage::Metadata>,
4788 invalidate_cache: bool,
4789 ) -> u64 {
4790 self.cdc_emit_prebuilt_with_columns(
4791 operation,
4792 collection,
4793 entity,
4794 entity_kind,
4795 metadata,
4796 invalidate_cache,
4797 None,
4798 )
4799 }
4800
4801 pub(crate) fn cdc_emit_prebuilt_with_columns(
4808 &self,
4809 operation: crate::replication::cdc::ChangeOperation,
4810 collection: &str,
4811 entity: &UnifiedEntity,
4812 entity_kind: &str,
4813 metadata: Option<&crate::storage::Metadata>,
4814 invalidate_cache: bool,
4815 changed_columns: Option<Vec<String>>,
4816 ) -> u64 {
4817 if invalidate_cache {
4818 self.invalidate_result_cache();
4819 }
4820
4821 let public_id = entity.logical_id().raw();
4822 let lsn = self.inner.cdc.emit_with_columns(
4823 operation,
4824 collection,
4825 public_id,
4826 entity_kind,
4827 changed_columns,
4828 );
4829
4830 if let Some(ref primary) = self.inner.db.replication {
4831 let store = self.inner.db.store();
4832 let record = ChangeRecord {
4833 term: self.current_replication_term(),
4834 lsn,
4835 timestamp: SystemTime::now()
4836 .duration_since(UNIX_EPOCH)
4837 .unwrap_or_default()
4838 .as_millis() as u64,
4839 operation,
4840 collection: collection.to_string(),
4841 entity_id: entity.id.raw(),
4842 entity_kind: entity_kind.to_string(),
4843 entity_bytes: Some(UnifiedStore::serialize_entity(
4844 entity,
4845 store.format_version(),
4846 )),
4847 metadata: metadata
4848 .map(metadata_to_json)
4849 .or_else(|| self.latest_metadata_for(collection, entity.id.raw())),
4850 refresh_records: None,
4851 };
4852 let encoded = record.encode();
4853 primary.append_logical_record(record.lsn, encoded);
4854 }
4855
4856 lsn
4857 }
4858
4859 pub(crate) fn current_replication_term(&self) -> u64 {
4860 self.inner.db.options().replication.term
4861 }
4862
4863 pub(crate) fn cdc_emit_prebuilt_batch<'a, I>(
4864 &self,
4865 operation: crate::replication::cdc::ChangeOperation,
4866 entity_kind: &str,
4867 items: I,
4868 invalidate_cache: bool,
4869 ) where
4870 I: IntoIterator<
4871 Item = (
4872 &'a str,
4873 &'a UnifiedEntity,
4874 Option<&'a crate::storage::Metadata>,
4875 ),
4876 >,
4877 {
4878 let items: Vec<(&str, &UnifiedEntity, Option<&crate::storage::Metadata>)> =
4879 items.into_iter().collect();
4880 if items.is_empty() {
4881 return;
4882 }
4883
4884 if invalidate_cache {
4885 self.invalidate_result_cache();
4886 }
4887
4888 for (collection, entity, metadata) in items {
4889 self.cdc_emit_prebuilt(operation, collection, entity, entity_kind, metadata, false);
4890 }
4891 }
4892
4893 fn run_replica_loop(&self, primary_addr: String) {
4894 let endpoint = if primary_addr.starts_with("http") {
4895 primary_addr
4896 } else {
4897 format!("http://{primary_addr}")
4898 };
4899 let poll_ms = self.inner.db.options().replication.poll_interval_ms;
4900 let max_count = self.inner.db.options().replication.max_batch_size;
4901 let mut since_lsn = self.config_u64("red.replication.last_applied_lsn", 0);
4902 let replica_id = self.resolve_replica_id();
4905
4906 let runtime = match tokio::runtime::Builder::new_current_thread()
4907 .enable_all()
4908 .build()
4909 {
4910 Ok(runtime) => runtime,
4911 Err(_) => return,
4912 };
4913
4914 runtime.block_on(async move {
4915 use crate::grpc::proto::red_db_client::RedDbClient;
4916 use crate::grpc::proto::JsonPayloadRequest;
4917
4918 let mut client = loop {
4919 match RedDbClient::connect(endpoint.clone()).await {
4920 Ok(client) => {
4921 self.persist_replication_health("connecting", "", None, None);
4922 break client;
4923 }
4924 Err(_) => {
4925 self.persist_replication_health(
4926 "connecting",
4927 "waiting for primary connection",
4928 None,
4929 None,
4930 );
4931 std::thread::sleep(std::time::Duration::from_millis(poll_ms.max(250)))
4932 }
4933 }
4934 };
4935
4936 let applier = crate::replication::logical::LogicalChangeApplier::with_metrics(
4941 since_lsn,
4942 self.inner.replica_apply_metrics.clone(),
4943 );
4944
4945 loop {
4946 let payload = crate::json!({
4947 "since_lsn": since_lsn,
4948 "max_count": max_count,
4949 "replica_id": replica_id,
4950 "await_data": true,
4951 "await_timeout_ms": 30_000
4952 });
4953 let request = tonic::Request::new(JsonPayloadRequest {
4954 payload_json: crate::json::to_string(&payload)
4955 .unwrap_or_else(|_| "{}".to_string()),
4956 });
4957
4958 if let Ok(response) = client.pull_wal_records(request).await {
4959 if let Ok(value) =
4960 crate::json::from_str::<crate::json::Value>(&response.into_inner().payload)
4961 {
4962 let current_lsn =
4963 value.get("current_lsn").and_then(crate::json::Value::as_u64);
4964 let oldest_available_lsn = value
4965 .get("oldest_available_lsn")
4966 .and_then(crate::json::Value::as_u64);
4967 if value
4968 .get("needs_rebootstrap")
4969 .and_then(crate::json::Value::as_bool)
4970 .unwrap_or(false)
4971 {
4972 let reason = value
4973 .get("invalidation_reason")
4974 .and_then(crate::json::Value::as_str)
4975 .unwrap_or("unknown");
4976 self.persist_replication_health(
4977 "rebootstrap_required",
4978 &format!("replication slot invalidated ({reason}); re-bootstrap required"),
4979 current_lsn,
4980 oldest_available_lsn,
4981 );
4982 std::thread::sleep(std::time::Duration::from_millis(poll_ms.max(250)));
4983 continue;
4984 }
4985 if since_lsn > 0
4986 && oldest_available_lsn
4987 .map(|oldest| oldest > since_lsn.saturating_add(1))
4988 .unwrap_or(false)
4989 {
4990 self.persist_replication_health(
4991 "rebootstrap_required",
4992 "replica is behind the oldest logical WAL available on primary; re-bootstrap required",
4993 current_lsn,
4994 oldest_available_lsn,
4995 );
4996 std::thread::sleep(std::time::Duration::from_millis(poll_ms.max(250)));
4997 continue;
4998 }
4999 if let Some(records) =
5000 value.get("records").and_then(crate::json::Value::as_array)
5001 {
5002 let mut batch_applied_lsn = None;
5003 let mut ack_failed = false;
5004 for record in records {
5005 let Some(data_hex) =
5006 record.get("data").and_then(crate::json::Value::as_str)
5007 else {
5008 continue;
5009 };
5010 let Ok(data) = hex::decode(data_hex) else {
5011 self.inner.replica_apply_metrics.record(
5012 crate::replication::logical::ApplyErrorKind::Decode,
5013 );
5014 self.persist_replication_health(
5015 "apply_error",
5016 "failed to decode WAL record hex payload",
5017 current_lsn,
5018 oldest_available_lsn,
5019 );
5020 continue;
5021 };
5022 let Ok(change) = ChangeRecord::decode(&data) else {
5023 self.inner.replica_apply_metrics.record(
5024 crate::replication::logical::ApplyErrorKind::Decode,
5025 );
5026 self.persist_replication_health(
5027 "apply_error",
5028 "failed to decode logical WAL record",
5029 current_lsn,
5030 oldest_available_lsn,
5031 );
5032 continue;
5033 };
5034 match applier.apply(
5035 self.inner.db.as_ref(),
5036 &change,
5037 ApplyMode::Replica,
5038 ) {
5039 Ok(crate::replication::logical::ApplyOutcome::Applied) => {
5040 self.invalidate_result_cache_for_table(&change.collection);
5041 since_lsn = since_lsn.max(change.lsn);
5042 self.persist_replica_lsn(since_lsn);
5043 batch_applied_lsn = Some(since_lsn);
5044 }
5045 Ok(_) => {
5046 }
5048 Err(err) => {
5049 self.inner.replica_apply_metrics.record(err.kind());
5050 match &err {
5059 crate::replication::logical::LogicalApplyError::Divergence { lsn, expected: _, got: _, .. } => {
5060 crate::telemetry::operator_event::OperatorEvent::Divergence {
5061 peer: "primary".to_string(),
5062 leader_lsn: *lsn,
5063 follower_lsn: since_lsn,
5064 }
5065 .emit_global();
5066 }
5067 crate::replication::logical::LogicalApplyError::Gap { last, next } => {
5068 crate::telemetry::operator_event::OperatorEvent::ReplicationBroken {
5069 peer: "primary".to_string(),
5070 reason: format!("stalled gap last={last} next={next}"),
5071 }
5072 .emit_global();
5073 }
5074 _ => {}
5075 }
5076 let kind = match &err {
5077 crate::replication::logical::LogicalApplyError::Gap { .. } => "stalled_gap",
5078 crate::replication::logical::LogicalApplyError::Divergence { .. } => "divergence",
5079 crate::replication::logical::LogicalApplyError::StaleTermFenced { .. } => "stale_term_fenced",
5085 _ => "apply_error",
5086 };
5087 self.persist_replication_health(
5088 kind,
5089 &format!("replica apply rejected: {err}"),
5090 current_lsn,
5091 oldest_available_lsn,
5092 );
5093 break;
5104 }
5105 }
5106 }
5107 if let Some(applied_lsn) = batch_applied_lsn {
5108 let apply_errors = self.replica_apply_error_counts();
5109 let apply_errors_total =
5110 apply_errors.iter().map(|(_, count)| *count).sum::<u64>();
5111 let divergence_total = apply_errors
5112 .iter()
5113 .find(|(kind, _)| {
5114 matches!(
5115 kind,
5116 crate::replication::logical::ApplyErrorKind::Divergence
5117 )
5118 })
5119 .map(|(_, count)| *count)
5120 .unwrap_or(0);
5121 let ack_payload = crate::json!({
5122 "replica_id": replica_id.clone(),
5123 "applied_lsn": applied_lsn,
5124 "durable_lsn": applied_lsn,
5125 "apply_errors_total": apply_errors_total,
5126 "divergence_total": divergence_total
5127 });
5128 let ack_request = tonic::Request::new(JsonPayloadRequest {
5129 payload_json: crate::json::to_string(&ack_payload)
5130 .unwrap_or_else(|_| "{}".to_string()),
5131 });
5132 if client.ack_replica_lsn(ack_request).await.is_err() {
5133 ack_failed = true;
5134 self.persist_replication_health(
5135 "ack_error",
5136 "primary ack_replica_lsn request failed",
5137 current_lsn,
5138 oldest_available_lsn,
5139 );
5140 }
5141 }
5142 if ack_failed {
5143 std::thread::sleep(std::time::Duration::from_millis(poll_ms));
5144 continue;
5145 }
5146 }
5147 self.persist_replication_health(
5148 "healthy",
5149 "",
5150 current_lsn,
5151 oldest_available_lsn,
5152 );
5153 } else {
5154 self.persist_replication_health(
5155 "apply_error",
5156 "failed to parse pull_wal_records response",
5157 None,
5158 None,
5159 );
5160 }
5161 } else {
5162 self.persist_replication_health(
5163 "connecting",
5164 "primary pull_wal_records request failed",
5165 None,
5166 None,
5167 );
5168 std::thread::sleep(std::time::Duration::from_millis(poll_ms.max(250)));
5169 }
5170 }
5171 });
5172 }
5173
5174 pub fn cdc_poll(
5176 &self,
5177 since_lsn: u64,
5178 max_count: usize,
5179 ) -> Vec<crate::replication::cdc::ChangeEvent> {
5180 self.inner.cdc.poll(since_lsn, max_count)
5181 }
5182
5183 pub fn cdc_current_lsn(&self) -> u64 {
5187 self.inner.cdc.current_lsn()
5188 }
5189
5190 pub fn kv_watch_events_since(
5191 &self,
5192 collection: &str,
5193 key: &str,
5194 since_lsn: u64,
5195 max_count: usize,
5196 ) -> Vec<crate::replication::cdc::KvWatchEvent> {
5197 self.inner
5198 .cdc
5199 .poll(since_lsn, max_count)
5200 .into_iter()
5201 .filter_map(|event| event.kv)
5202 .filter(|event| event.collection == collection && event.key == key)
5203 .collect()
5204 }
5205
5206 pub fn kv_watch_events_since_prefix(
5207 &self,
5208 collection: &str,
5209 prefix: &str,
5210 since_lsn: u64,
5211 max_count: usize,
5212 ) -> Vec<crate::replication::cdc::KvWatchEvent> {
5213 self.inner
5214 .cdc
5215 .poll(since_lsn, max_count)
5216 .into_iter()
5217 .filter_map(|event| event.kv)
5218 .filter(|event| event.collection == collection && event.key.starts_with(prefix))
5219 .collect()
5220 }
5221
5222 pub(crate) fn kv_watch_subscribe<'a>(
5223 &'a self,
5224 collection: impl Into<String>,
5225 key: impl Into<String>,
5226 from_lsn: Option<u64>,
5227 ) -> crate::runtime::kv_watch::KvWatchStream<'a> {
5228 crate::runtime::kv_watch::KvWatchStream::subscribe(
5229 &self.inner.cdc,
5230 &self.inner.kv_stats,
5231 collection,
5232 key,
5233 from_lsn,
5234 self.kv_watch_idle_timeout_ms(),
5235 )
5236 }
5237
5238 pub(crate) fn kv_watch_subscribe_prefix<'a>(
5239 &'a self,
5240 collection: impl Into<String>,
5241 prefix: impl Into<String>,
5242 from_lsn: Option<u64>,
5243 ) -> crate::runtime::kv_watch::KvWatchStream<'a> {
5244 crate::runtime::kv_watch::KvWatchStream::subscribe_prefix(
5245 &self.inner.cdc,
5246 &self.inner.kv_stats,
5247 collection,
5248 prefix,
5249 from_lsn,
5250 self.kv_watch_idle_timeout_ms(),
5251 )
5252 }
5253
5254 pub(crate) fn kv_watch_idle_timeout_ms(&self) -> u64 {
5255 self.config_u64("red.config.kv.watch.idle_timeout_ms", 60_000)
5256 }
5257
5258 pub fn backup_status(&self) -> crate::replication::scheduler::BackupStatus {
5260 self.inner.backup_scheduler.status()
5261 }
5262
5263 pub fn result_blob_cache(&self) -> &crate::storage::cache::BlobCache {
5273 &self.inner.result_blob_cache
5274 }
5275
5276 pub fn primary_replica_snapshots(&self) -> Vec<crate::replication::primary::ReplicaState> {
5280 self.inner
5281 .db
5282 .replication
5283 .as_ref()
5284 .map(|repl| repl.replica_snapshots())
5285 .unwrap_or_default()
5286 }
5287
5288 pub fn primary_logical_head_lsn(&self) -> u64 {
5292 self.inner
5293 .db
5294 .replication
5295 .as_ref()
5296 .map(|repl| repl.current_logical_lsn())
5297 .unwrap_or(0)
5298 }
5299
5300 pub fn replication_full_resync_count(&self) -> u64 {
5304 self.inner
5305 .db
5306 .replication
5307 .as_ref()
5308 .map(|repl| repl.full_resync_count())
5309 .unwrap_or(0)
5310 }
5311
5312 pub fn replication_partial_resync_count(&self) -> u64 {
5315 self.inner
5316 .db
5317 .replication
5318 .as_ref()
5319 .map(|repl| repl.partial_resync_count())
5320 .unwrap_or(0)
5321 }
5322
5323 pub fn node_id(&self) -> String {
5328 self.resolve_replica_id()
5329 }
5330
5331 pub fn refresh_replication_flow_control(&self) -> bool {
5342 let flow = self.inner.write_gate.flow_control();
5343 if !flow.is_enabled() {
5344 return false;
5345 }
5346 let Some(repl) = self.inner.db.replication.as_ref() else {
5347 return false;
5348 };
5349 let primary_lsn = repl.current_logical_lsn();
5350 let replicas = repl.replica_snapshots();
5351 flow.observe(&replicas, primary_lsn)
5352 }
5353
5354 pub fn commit_policy(&self) -> crate::replication::CommitPolicy {
5359 crate::replication::CommitPolicy::from_env()
5360 }
5361
5362 pub fn replica_apply_error_counts(
5368 &self,
5369 ) -> [(crate::replication::logical::ApplyErrorKind, u64); 6] {
5370 self.inner.replica_apply_metrics.snapshot()
5371 }
5372
5373 pub fn quota_bucket(&self) -> &crate::runtime::quota_bucket::QuotaBucket {
5376 &self.inner.quota_bucket
5377 }
5378
5379 pub fn commit_waiter_snapshot(&self) -> Vec<(String, u64)> {
5383 self.inner
5384 .db
5385 .replication
5386 .as_ref()
5387 .map(|repl| repl.commit_waiter.snapshot())
5388 .unwrap_or_default()
5389 }
5390
5391 pub fn commit_waiter_metrics_snapshot(&self) -> (u64, u64, u64, u64) {
5394 self.inner
5395 .db
5396 .replication
5397 .as_ref()
5398 .map(|repl| repl.commit_waiter.metrics_snapshot())
5399 .unwrap_or((0, 0, 0, 0))
5400 }
5401
5402 pub fn commit_watermark(&self) -> u64 {
5406 match self.commit_policy() {
5407 crate::replication::CommitPolicy::AckN(n) if n > 0 => self
5408 .inner
5409 .db
5410 .replication
5411 .as_ref()
5412 .map(|repl| repl.commit_waiter.commit_watermark(n))
5413 .unwrap_or(0),
5414 crate::replication::CommitPolicy::Quorum => self
5415 .inner
5416 .db
5417 .quorum
5418 .as_ref()
5419 .map(|q| q.commit_watermark())
5420 .unwrap_or(0),
5421 _ => 0,
5422 }
5423 }
5424
5425 pub fn await_replica_acks(
5434 &self,
5435 target_lsn: u64,
5436 count: u32,
5437 timeout: std::time::Duration,
5438 ) -> crate::replication::AwaitOutcome {
5439 match &self.inner.db.replication {
5440 Some(repl) => repl.commit_waiter.await_acks(target_lsn, count, timeout),
5441 None => {
5442 crate::replication::AwaitOutcome::NotRequired
5446 }
5447 }
5448 }
5449
5450 pub fn enforce_commit_policy(
5463 &self,
5464 post_lsn: u64,
5465 ) -> RedDBResult<crate::replication::AwaitOutcome> {
5466 let policy = self.commit_policy();
5467 if matches!(policy, crate::replication::CommitPolicy::Quorum) {
5468 return match self.inner.db.wait_for_replication_quorum(post_lsn) {
5469 Ok(()) => Ok(crate::replication::AwaitOutcome::Reached(0)),
5470 Err(err) => {
5471 tracing::warn!(
5472 target: "reddb::commit",
5473 post_lsn,
5474 error = %err,
5475 "quorum: timed out waiting for commit watermark"
5476 );
5477 let fail = std::env::var("RED_COMMIT_FAIL_ON_TIMEOUT")
5478 .ok()
5479 .map(|v| {
5480 let t = v.trim();
5481 t.eq_ignore_ascii_case("true")
5482 || t == "1"
5483 || t.eq_ignore_ascii_case("yes")
5484 })
5485 .unwrap_or(false);
5486 if fail {
5487 return Err(RedDBError::ReadOnly(format!(
5488 "commit policy timed out at lsn {post_lsn}: {err} (RED_COMMIT_FAIL_ON_TIMEOUT=true)"
5489 )));
5490 }
5491 Ok(crate::replication::AwaitOutcome::TimedOut {
5492 observed: 0,
5493 required: 1,
5494 })
5495 }
5496 };
5497 }
5498
5499 let n = match policy {
5500 crate::replication::CommitPolicy::AckN(n) if n > 0 => n,
5501 _ => return Ok(crate::replication::AwaitOutcome::NotRequired),
5502 };
5503 let timeout_ms = std::env::var("RED_REPLICATION_ACK_TIMEOUT_MS")
5504 .ok()
5505 .and_then(|v| v.parse::<u64>().ok())
5506 .unwrap_or(5_000);
5507 let outcome =
5508 self.await_replica_acks(post_lsn, n, std::time::Duration::from_millis(timeout_ms));
5509 {
5510 use crate::runtime::control_events::{EventKind, Outcome, Sensitivity};
5511 let (event_outcome, fields) = match &outcome {
5512 crate::replication::AwaitOutcome::Reached(count) => (
5513 Outcome::Allowed,
5514 vec![
5515 (
5516 "post_lsn".to_string(),
5517 Sensitivity::raw(post_lsn.to_string()),
5518 ),
5519 ("required".to_string(), Sensitivity::raw(n.to_string())),
5520 ("observed".to_string(), Sensitivity::raw(count.to_string())),
5521 (
5522 "timeout_ms".to_string(),
5523 Sensitivity::raw(timeout_ms.to_string()),
5524 ),
5525 ],
5526 ),
5527 crate::replication::AwaitOutcome::TimedOut { observed, required } => (
5528 Outcome::Error,
5529 vec![
5530 (
5531 "post_lsn".to_string(),
5532 Sensitivity::raw(post_lsn.to_string()),
5533 ),
5534 (
5535 "required".to_string(),
5536 Sensitivity::raw(required.to_string()),
5537 ),
5538 (
5539 "observed".to_string(),
5540 Sensitivity::raw(observed.to_string()),
5541 ),
5542 (
5543 "timeout_ms".to_string(),
5544 Sensitivity::raw(timeout_ms.to_string()),
5545 ),
5546 ],
5547 ),
5548 crate::replication::AwaitOutcome::NotRequired => (Outcome::Allowed, Vec::new()),
5549 };
5550 if !fields.is_empty() {
5551 self.emit_control_event(
5552 EventKind::ReplicationSafety,
5553 event_outcome,
5554 "replication_commit_policy",
5555 Some(format!("replication:lsn:{post_lsn}")),
5556 None,
5557 fields,
5558 )?;
5559 }
5560 }
5561 if let crate::replication::AwaitOutcome::TimedOut { observed, required } = &outcome {
5562 tracing::warn!(
5563 target: "reddb::commit",
5564 post_lsn,
5565 observed = *observed,
5566 required = *required,
5567 timeout_ms,
5568 "ack_n: timed out waiting for replicas"
5569 );
5570 let fail = std::env::var("RED_COMMIT_FAIL_ON_TIMEOUT")
5571 .ok()
5572 .map(|v| {
5573 let t = v.trim();
5574 t.eq_ignore_ascii_case("true") || t == "1" || t.eq_ignore_ascii_case("yes")
5575 })
5576 .unwrap_or(false);
5577 if fail {
5578 return Err(RedDBError::ReadOnly(format!(
5579 "commit policy timed out at lsn {post_lsn}: observed={observed} required={required} (RED_COMMIT_FAIL_ON_TIMEOUT=true)"
5580 )));
5581 }
5582 }
5583 Ok(outcome)
5584 }
5585
5586 pub fn encryption_at_rest_status(&self) -> (&'static str, Option<String>) {
5594 match crate::crypto::page_encryption::key_from_env() {
5595 Ok(Some(_)) => ("enabled", None),
5596 Ok(None) => ("disabled", None),
5597 Err(err) => ("error", Some(err)),
5598 }
5599 }
5600
5601 pub fn replica_apply_health(&self) -> Option<String> {
5607 let state = self.config_string("red.replication.state", "");
5608 if state.is_empty() {
5609 None
5610 } else {
5611 Some(state)
5612 }
5613 }
5614
5615 pub fn wal_archive_progress(&self) -> (u64, u64) {
5620 let current_lsn = self
5621 .inner
5622 .db
5623 .replication
5624 .as_ref()
5625 .map(|repl| {
5626 repl.logical_wal_spool
5627 .as_ref()
5628 .map(|spool| spool.current_lsn())
5629 .unwrap_or_else(|| repl.wal_buffer.current_lsn())
5630 })
5631 .unwrap_or_else(|| self.inner.cdc.current_lsn());
5632 let last_archived_lsn = self.config_u64("red.config.timeline.last_archived_lsn", 0);
5633 (current_lsn, last_archived_lsn)
5634 }
5635
5636 pub fn trigger_backup(&self) -> RedDBResult<crate::replication::scheduler::BackupResult> {
5638 let result = (|| {
5639 self.check_write(crate::runtime::write_gate::WriteKind::Backup)?;
5640 self.assert_remote_write_allowed("admin/backup")?;
5645 let started = std::time::Instant::now();
5646 let snapshot = self.create_snapshot()?;
5647 let mut uploaded = false;
5648
5649 if let (Some(backend), Some(path)) =
5650 (&self.inner.db.remote_backend, self.inner.db.path())
5651 {
5652 let default_snapshot_prefix = self.inner.db.options().default_snapshot_prefix();
5653 let default_wal_prefix = self.inner.db.options().default_wal_archive_prefix();
5654 let default_head_key = self.inner.db.options().default_backup_head_key();
5655 let snapshot_prefix = self.config_string(
5656 "red.config.backup.snapshot_prefix",
5657 &default_snapshot_prefix,
5658 );
5659 let wal_prefix =
5660 self.config_string("red.config.wal.archive.prefix", &default_wal_prefix);
5661 let head_key = self.config_string("red.config.backup.head_key", &default_head_key);
5662 let timeline_id = self.config_string("red.config.timeline.id", "main");
5663 let snapshot_key = crate::storage::wal::archive_snapshot(
5664 backend.as_ref(),
5665 path,
5666 snapshot.snapshot_id,
5667 &snapshot_prefix,
5668 )
5669 .map_err(|err| RedDBError::Internal(err.to_string()))?;
5670 let current_lsn = self
5671 .inner
5672 .db
5673 .replication
5674 .as_ref()
5675 .map(|repl| {
5676 repl.logical_wal_spool
5677 .as_ref()
5678 .map(|spool| spool.current_lsn())
5679 .unwrap_or_else(|| repl.wal_buffer.current_lsn())
5680 })
5681 .unwrap_or_else(|| self.inner.cdc.current_lsn());
5682 let last_archived_lsn = self.config_u64("red.config.timeline.last_archived_lsn", 0);
5683 let snapshot_sha256 =
5689 crate::storage::wal::SnapshotManifest::compute_snapshot_sha256(path)
5690 .map_err(|err| {
5691 tracing::warn!(
5692 target: "reddb::backup",
5693 error = %err,
5694 snapshot_id = snapshot.snapshot_id,
5695 "snapshot hash failed; manifest will lack checksum"
5696 );
5697 })
5698 .ok();
5699 let manifest = crate::storage::wal::SnapshotManifest {
5700 timeline_id: timeline_id.clone(),
5701 snapshot_key: snapshot_key.clone(),
5702 snapshot_id: snapshot.snapshot_id,
5703 snapshot_time: snapshot.created_at_unix_ms as u64,
5704 base_lsn: current_lsn,
5705 schema_version: crate::api::REDDB_FORMAT_VERSION,
5706 format_version: crate::api::REDDB_FORMAT_VERSION,
5707 snapshot_sha256,
5708 };
5709 crate::storage::wal::publish_snapshot_manifest(backend.as_ref(), &manifest)
5710 .map_err(|err| RedDBError::Internal(err.to_string()))?;
5711
5712 let prev_segment_hash =
5719 self.config_string("red.config.timeline.last_segment_hash", "");
5720 let prev_hash_arg = if prev_segment_hash.is_empty() {
5721 None
5722 } else {
5723 Some(prev_segment_hash)
5724 };
5725
5726 let archived_lsn = if let Some(primary) = &self.inner.db.replication {
5727 let oldest = primary
5728 .logical_wal_spool
5729 .as_ref()
5730 .and_then(|spool| spool.oldest_lsn().ok().flatten())
5731 .or_else(|| primary.wal_buffer.oldest_lsn())
5732 .unwrap_or(last_archived_lsn);
5733 if last_archived_lsn > 0 && last_archived_lsn < oldest.saturating_sub(1) {
5734 return Err(RedDBError::Internal(format!(
5735 "logical WAL gap detected: last_archived_lsn={last_archived_lsn}, oldest_available_lsn={oldest}"
5736 )));
5737 }
5738 let records = if let Some(spool) = &primary.logical_wal_spool {
5739 spool
5740 .read_since(last_archived_lsn, usize::MAX)
5741 .map_err(|err| RedDBError::Internal(err.to_string()))?
5742 } else {
5743 primary.wal_buffer.read_since(last_archived_lsn, usize::MAX)
5744 };
5745 if let Some(meta) = crate::storage::wal::archive_change_records(
5746 backend.as_ref(),
5747 &wal_prefix,
5748 &records,
5749 prev_hash_arg,
5750 )
5751 .map_err(|err| RedDBError::Internal(err.to_string()))?
5752 {
5753 let _ = primary.prune_retained_wal_through(meta.lsn_end);
5754 if let Some(sha) = &meta.sha256 {
5760 self.inner.db.store().set_config_tree(
5761 "red.config.timeline",
5762 &crate::json!({ "last_segment_hash": sha }),
5763 );
5764 }
5765 meta.lsn_end
5766 } else {
5767 last_archived_lsn
5768 }
5769 } else {
5770 last_archived_lsn
5771 };
5772
5773 let head = crate::storage::wal::BackupHead {
5774 timeline_id,
5775 snapshot_key,
5776 snapshot_id: snapshot.snapshot_id,
5777 snapshot_time: snapshot.created_at_unix_ms as u64,
5778 current_lsn,
5779 last_archived_lsn: archived_lsn,
5780 wal_prefix,
5781 };
5782 crate::storage::wal::publish_backup_head(backend.as_ref(), &head_key, &head)
5783 .map_err(|err| RedDBError::Internal(err.to_string()))?;
5784 self.inner.db.store().set_config_tree(
5785 "red.config.timeline",
5786 &crate::json!({
5787 "last_archived_lsn": archived_lsn,
5788 "id": head.timeline_id
5789 }),
5790 );
5791
5792 if let Err(err) = crate::storage::wal::publish_unified_manifest_for_prefix(
5800 backend.as_ref(),
5801 &snapshot_prefix,
5802 ) {
5803 tracing::warn!(
5804 target: "reddb::backup",
5805 error = %err,
5806 snapshot_prefix = %snapshot_prefix,
5807 "unified MANIFEST.json refresh failed; per-artifact sidecars unaffected"
5808 );
5809 }
5810
5811 match self.commit_policy() {
5823 crate::replication::CommitPolicy::AckN(n) if n > 0 => {
5824 let timeout = std::env::var("RED_REPLICATION_ACK_TIMEOUT_MS")
5825 .ok()
5826 .and_then(|v| v.parse::<u64>().ok())
5827 .unwrap_or(5_000);
5828 let outcome = self.await_replica_acks(
5829 archived_lsn,
5830 n,
5831 std::time::Duration::from_millis(timeout),
5832 );
5833 match outcome {
5834 crate::replication::AwaitOutcome::Reached(count) => {
5835 tracing::debug!(
5836 target: "reddb::backup",
5837 archived_lsn,
5838 n,
5839 count,
5840 "ack_n: replicas synced before backup return"
5841 );
5842 }
5843 crate::replication::AwaitOutcome::TimedOut { observed, required } => {
5844 tracing::warn!(
5845 target: "reddb::backup",
5846 archived_lsn,
5847 observed,
5848 required,
5849 timeout_ms = timeout,
5850 "ack_n: timed out waiting for replicas; backup uploaded but DR posture degraded"
5851 );
5852 }
5853 crate::replication::AwaitOutcome::NotRequired => {}
5854 }
5855 }
5856 _ => {} }
5858
5859 if self.config_bool("red.config.backup.include_blob_cache", false) {
5871 let blob_cache_prefix = self.config_string(
5872 "red.config.backup.blob_cache_prefix",
5873 &format!("{snapshot_prefix}blob_cache/"),
5874 );
5875 if let Some(l2_path) = self.inner.result_blob_cache.l2_path() {
5876 match crate::storage::cache::archive_blob_cache_l2(
5877 backend.as_ref(),
5878 l2_path,
5879 &blob_cache_prefix,
5880 ) {
5881 Ok(count) => {
5882 tracing::info!(
5883 target: "reddb::backup",
5884 files_uploaded = count,
5885 blob_cache_prefix = %blob_cache_prefix,
5886 "include_blob_cache: archived L2 directory"
5887 );
5888 }
5889 Err(err) => {
5890 tracing::warn!(
5891 target: "reddb::backup",
5892 error = %err,
5893 blob_cache_prefix = %blob_cache_prefix,
5894 "include_blob_cache: L2 archive failed; backup proceeding (cache is derived state)"
5895 );
5896 }
5897 }
5898 } else {
5899 tracing::debug!(
5900 target: "reddb::backup",
5901 "include_blob_cache=true but no L2 path configured; nothing to archive"
5902 );
5903 }
5904 }
5905
5906 uploaded = true;
5907 }
5908
5909 Ok(crate::replication::scheduler::BackupResult {
5910 snapshot_id: snapshot.snapshot_id,
5911 uploaded,
5912 duration_ms: started.elapsed().as_millis() as u64,
5913 timestamp: snapshot.created_at_unix_ms as u64,
5914 })
5915 })();
5916
5917 use crate::runtime::control_events::{EventKind, Outcome, Sensitivity};
5918 let (current_lsn, last_archived_lsn) = self.wal_archive_progress();
5919 let mut fields = vec![
5920 (
5921 "current_lsn".to_string(),
5922 Sensitivity::raw(current_lsn.to_string()),
5923 ),
5924 (
5925 "last_archived_lsn".to_string(),
5926 Sensitivity::raw(last_archived_lsn.to_string()),
5927 ),
5928 ];
5929 if let Ok(backup) = &result {
5930 fields.push((
5931 "snapshot_id".to_string(),
5932 Sensitivity::raw(backup.snapshot_id.to_string()),
5933 ));
5934 fields.push((
5935 "uploaded".to_string(),
5936 Sensitivity::raw(backup.uploaded.to_string()),
5937 ));
5938 fields.push((
5939 "duration_ms".to_string(),
5940 Sensitivity::raw(backup.duration_ms.to_string()),
5941 ));
5942 fields.push((
5943 "snapshot_time".to_string(),
5944 Sensitivity::raw(backup.timestamp.to_string()),
5945 ));
5946 }
5947 let outcome = match &result {
5948 Ok(_) => Outcome::Allowed,
5949 Err(err) => control_event_outcome_for_error(err),
5950 };
5951 let reason = result.as_ref().err().map(|err| err.to_string());
5952 self.emit_control_event(
5953 EventKind::BackupRun,
5954 outcome,
5955 "backup_trigger",
5956 Some("backup:trigger".to_string()),
5957 reason,
5958 fields,
5959 )?;
5960 result
5961 }
5962
5963 pub fn acquire(&self) -> RedDBResult<RuntimeConnection> {
5964 let mut pool = self
5965 .inner
5966 .pool
5967 .lock()
5968 .map_err(|e| RedDBError::Internal(format!("connection pool lock poisoned: {e}")))?;
5969 if pool.active >= self.inner.pool_config.max_connections {
5970 return Err(RedDBError::Internal(
5971 "connection pool exhausted".to_string(),
5972 ));
5973 }
5974
5975 let id = if let Some(id) = pool.idle.pop() {
5976 id
5977 } else {
5978 let id = pool.next_id;
5979 pool.next_id += 1;
5980 id
5981 };
5982 pool.active += 1;
5983 pool.total_checkouts += 1;
5984 drop(pool);
5985
5986 Ok(RuntimeConnection {
5987 id,
5988 inner: Arc::clone(&self.inner),
5989 })
5990 }
5991
5992 pub fn checkpoint(&self) -> RedDBResult<()> {
5993 self.inner.db.flush_local_only().map_err(|err| {
5998 let msg = err.to_string();
6003 crate::telemetry::operator_event::OperatorEvent::CheckpointFailed {
6004 lsn: 0,
6005 error: msg.clone(),
6006 }
6007 .emit_global();
6008 crate::telemetry::operator_event::OperatorEvent::WalFsyncFailed {
6009 path: "<flush_local_only>".to_string(),
6010 error: msg.clone(),
6011 }
6012 .emit_global();
6013 RedDBError::Engine(msg)
6014 })?;
6015 if let Err(err) = self.assert_remote_write_allowed("checkpoint") {
6016 tracing::warn!(
6017 target: "reddb::serverless::lease",
6018 error = %err,
6019 "checkpoint: skipping remote upload — lease not held"
6020 );
6021 return Ok(());
6022 }
6023 self.inner
6024 .db
6025 .upload_to_remote_backend()
6026 .map_err(|err| RedDBError::Engine(err.to_string()))
6027 }
6028
6029 pub(crate) fn assert_remote_write_allowed(&self, action: &str) -> RedDBResult<()> {
6036 if self.inner.db.remote_backend.is_none() {
6037 return Ok(());
6038 }
6039 match self.inner.write_gate.lease_state() {
6040 crate::runtime::write_gate::LeaseGateState::NotHeld => {
6041 self.inner.audit_log.record(
6042 action,
6043 "system",
6044 "remote_backend",
6045 "err: writer lease not held",
6046 crate::json::Value::Null,
6047 );
6048 Err(RedDBError::ReadOnly(format!(
6049 "writer lease not held — {action} blocked (serverless fence)"
6050 )))
6051 }
6052 _ => Ok(()),
6053 }
6054 }
6055
6056 pub fn run_maintenance(&self) -> RedDBResult<()> {
6057 self.inner
6058 .db
6059 .run_maintenance()
6060 .map_err(|err| RedDBError::Internal(err.to_string()))
6061 }
6062
6063 pub fn scan_collection(
6064 &self,
6065 collection: &str,
6066 cursor: Option<ScanCursor>,
6067 limit: usize,
6068 ) -> RedDBResult<ScanPage> {
6069 let store = self.inner.db.store();
6070 let manager = store
6071 .get_collection(collection)
6072 .ok_or_else(|| RedDBError::NotFound(collection.to_string()))?;
6073
6074 let mut entities = manager.query_all(|_| true);
6075 entities.sort_by_key(|entity| entity.id.raw());
6076
6077 let offset = cursor.map(|cursor| cursor.offset).unwrap_or(0);
6078 let total = entities.len();
6079 let end = total.min(offset.saturating_add(limit.max(1)));
6080 let items = if offset >= total {
6081 Vec::new()
6082 } else {
6083 entities[offset..end].to_vec()
6084 };
6085 let next = (end < total).then_some(ScanCursor { offset: end });
6086
6087 Ok(ScanPage {
6088 collection: collection.to_string(),
6089 items,
6090 next,
6091 total,
6092 })
6093 }
6094
6095 pub fn catalog(&self) -> CatalogModelSnapshot {
6096 self.inner.db.catalog_model_snapshot()
6097 }
6098
6099 pub fn catalog_consistency_report(&self) -> crate::catalog::CatalogConsistencyReport {
6100 self.inner.db.catalog_consistency_report()
6101 }
6102
6103 pub fn catalog_attention_summary(&self) -> CatalogAttentionSummary {
6104 crate::catalog::attention_summary(&self.catalog())
6105 }
6106
6107 pub fn collection_attention(&self) -> Vec<CollectionDescriptor> {
6108 crate::catalog::collection_attention(&self.catalog())
6109 }
6110
6111 pub fn index_attention(&self) -> Vec<CatalogIndexStatus> {
6112 crate::catalog::index_attention(&self.catalog())
6113 }
6114
6115 pub fn graph_projection_attention(&self) -> Vec<CatalogGraphProjectionStatus> {
6116 crate::catalog::graph_projection_attention(&self.catalog())
6117 }
6118
6119 pub fn analytics_job_attention(&self) -> Vec<CatalogAnalyticsJobStatus> {
6120 crate::catalog::analytics_job_attention(&self.catalog())
6121 }
6122
6123 pub fn stats(&self) -> RuntimeStats {
6124 let pool = runtime_pool_lock(self);
6125 RuntimeStats {
6126 active_connections: pool.active,
6127 idle_connections: pool.idle.len(),
6128 total_checkouts: pool.total_checkouts,
6129 paged_mode: self.inner.db.is_paged(),
6130 started_at_unix_ms: self.inner.started_at_unix_ms,
6131 store: self.inner.db.stats(),
6132 system: SystemInfo::collect(),
6133 result_blob_cache: self.inner.result_blob_cache.stats(),
6134 kv: self.inner.kv_stats.snapshot(),
6135 metrics_ingest: self.inner.metrics_ingest_stats.snapshot(),
6136 }
6137 }
6138
6139 pub(crate) fn record_metrics_ingest(
6140 &self,
6141 accepted_samples: u64,
6142 accepted_series: u64,
6143 rejected_samples: u64,
6144 rejected_series: u64,
6145 ) {
6146 self.inner.metrics_ingest_stats.record(
6147 accepted_samples,
6148 accepted_series,
6149 rejected_samples,
6150 rejected_series,
6151 );
6152 }
6153
6154 pub(crate) fn record_metrics_cardinality_budget_rejections(&self, rejected_series: u64) {
6155 self.inner
6156 .metrics_ingest_stats
6157 .record_cardinality_budget_rejections(rejected_series);
6158 }
6159
6160 pub(crate) fn record_metrics_tenant_activity(
6161 &self,
6162 tenant: &str,
6163 namespace: &str,
6164 operation: &str,
6165 ) {
6166 self.inner
6167 .metrics_tenant_activity_stats
6168 .record(tenant, namespace, operation);
6169 }
6170
6171 pub(crate) fn metrics_tenant_activity_snapshot(
6172 &self,
6173 ) -> Vec<crate::runtime::MetricsTenantActivityStats> {
6174 self.inner.metrics_tenant_activity_stats.snapshot()
6175 }
6176
6177 pub fn execute_query_with_scope(
6191 &self,
6192 query: &str,
6193 scope: crate::runtime::within_clause::ScopeOverride,
6194 ) -> RedDBResult<RuntimeQueryResult> {
6195 if scope.is_empty() {
6196 return self.execute_query(query);
6197 }
6198 let _scope_guard = ScopeOverrideGuard::install(scope);
6199 self.execute_query(query)
6200 }
6201
6202 pub fn execute_query(&self, query: &str) -> RedDBResult<RuntimeQueryResult> {
6211 let started = std::time::Instant::now();
6212 let mut result = self.execute_query_inner(query);
6213 if let Ok(ref mut query_result) = result {
6218 if query_result.statement_type == "select" {
6219 self.filter_integrity_tombstoned(&mut query_result.result);
6220 }
6221 }
6222 let elapsed_ms = started.elapsed().as_millis() as u64;
6223
6224 let scope = self.ai_scope();
6229 let kind = match result
6230 .as_ref()
6231 .map(|r| r.statement_type)
6232 .unwrap_or("select")
6233 {
6234 "select" => crate::telemetry::slow_query_logger::QueryKind::Select,
6235 "insert" => crate::telemetry::slow_query_logger::QueryKind::Insert,
6236 "update" => crate::telemetry::slow_query_logger::QueryKind::Update,
6237 "delete" => crate::telemetry::slow_query_logger::QueryKind::Delete,
6238 _ => crate::telemetry::slow_query_logger::QueryKind::Internal,
6239 };
6240 self.inner
6246 .slow_query_logger
6247 .record(kind, elapsed_ms, query.to_string(), &scope);
6248
6249 if let Ok(ref mut query_result) = result {
6250 if matches!(query_result.statement_type, "insert" | "update" | "delete") {
6251 let bookmark = crate::replication::CausalBookmark::new(
6252 self.current_replication_term(),
6253 self.cdc_current_lsn(),
6254 );
6255 query_result.bookmark = Some(bookmark.encode());
6256 }
6257 }
6258
6259 result
6260 }
6261
6262 pub fn causal_session(&self) -> crate::runtime::CausalSession {
6263 crate::runtime::CausalSession {
6264 runtime: self.clone(),
6265 bookmark: None,
6266 wait_timeout: std::time::Duration::from_secs(5),
6267 }
6268 }
6269
6270 pub fn wait_for_bookmark(
6271 &self,
6272 bookmark: &crate::replication::CausalBookmark,
6273 timeout: std::time::Duration,
6274 ) -> RedDBResult<()> {
6275 let deadline = std::time::Instant::now() + timeout;
6276 loop {
6277 let applied_lsn = self.local_contiguous_applied_lsn();
6278 if applied_lsn >= bookmark.commit_lsn() {
6279 return Ok(());
6280 }
6281 let now = std::time::Instant::now();
6282 if now >= deadline {
6283 return Err(RedDBError::InvalidOperation(format!(
6284 "timed out waiting for causal bookmark lsn {}; applied={}",
6285 bookmark.commit_lsn(),
6286 applied_lsn
6287 )));
6288 }
6289 let remaining = deadline.saturating_duration_since(now);
6290 std::thread::sleep(remaining.min(std::time::Duration::from_millis(5)));
6291 }
6292 }
6293
6294 fn local_contiguous_applied_lsn(&self) -> u64 {
6295 match self.inner.db.options().replication.role {
6296 crate::replication::ReplicationRole::Replica { .. } => {
6297 self.config_u64("red.replication.last_applied_lsn", 0)
6298 }
6299 _ => self.cdc_current_lsn(),
6300 }
6301 }
6302
6303 #[inline(never)]
6304 fn execute_query_inner(&self, query: &str) -> RedDBResult<RuntimeQueryResult> {
6305 if !has_scope_override_active()
6316 && !query.trim_start().starts_with("WITHIN")
6317 && !query.trim_start().starts_with("within")
6318 && !self.inner.query_audit.has_rules()
6319 && !self
6320 .inner
6321 .tx_contexts
6322 .read()
6323 .contains_key(¤t_connection_id())
6324 {
6325 if let Some(result) = self.try_fast_entity_lookup(query) {
6326 return result;
6327 }
6328 }
6329
6330 match crate::runtime::within_clause::try_strip_within_prefix(query) {
6337 Ok(Some((scope, inner))) => {
6338 let _scope_guard = ScopeOverrideGuard::install(scope);
6339 return self.execute_query_inner(inner);
6344 }
6345 Ok(None) => {}
6346 Err(msg) => return Err(RedDBError::Query(msg)),
6347 }
6348
6349 if let Some(inner) = strip_explain_prefix(query) {
6356 return self.explain_as_rows(query, inner);
6357 }
6358
6359 if let Some(value) = parse_set_local_tenant(query)? {
6364 let conn_id = current_connection_id();
6365 if !self.inner.tx_contexts.read().contains_key(&conn_id) {
6366 return Err(RedDBError::Query(
6367 "SET LOCAL TENANT requires an active transaction".to_string(),
6368 ));
6369 }
6370 self.inner
6371 .tx_local_tenants
6372 .write()
6373 .insert(conn_id, value.clone());
6374 return Ok(RuntimeQueryResult::ok_message(
6375 query.to_string(),
6376 &match &value {
6377 Some(id) => format!("local tenant set: {id}"),
6378 None => "local tenant cleared".to_string(),
6379 },
6380 "set_local_tenant",
6381 ));
6382 }
6383
6384 if super::red_schema::is_system_schema_write(query) {
6385 return Err(RedDBError::Query(
6386 super::red_schema::READ_ONLY_ERROR.to_string(),
6387 ));
6388 }
6389
6390 if let Some(create_source) = super::analytics_source_catalog::parse_create_statement(query)?
6391 {
6392 return self.execute_create_analytics_source(query, create_source);
6393 }
6394
6395 if let Some(path) = super::metric_descriptor_catalog::parse_read_metric_statement(query) {
6401 return Err(super::metric_descriptor_catalog::read_output_unsupported(
6402 &path,
6403 ));
6404 }
6405
6406 if let Some(parsed) = super::ranking_descriptor_catalog::parse_create_ranking(query) {
6413 return self.execute_create_ranking(query, parsed?);
6414 }
6415 if super::ranking_descriptor_catalog::parse_show_rankings(query) {
6416 return self.execute_show_rankings(query);
6417 }
6418 if let Some(parsed) = super::ranking_descriptor_catalog::parse_rank_of(query) {
6419 return self.execute_rank_of(query, parsed?);
6420 }
6421
6422 let rewritten_query = super::red_schema::rewrite_virtual_names(query);
6423 let execution_query = rewritten_query.as_deref().unwrap_or(query);
6424
6425 let frame = super::statement_frame::StatementExecutionFrame::build(self, execution_query)?;
6426 let _frame_guards = frame.install(self);
6427
6428 let _log_span = crate::telemetry::span::query_span(query).entered();
6435
6436 if let Some(rewritten) = frame.prepare_cte(execution_query)? {
6438 return self.execute_query_expr(rewritten);
6439 }
6440
6441 if !self.inner.query_audit.has_rules() {
6443 if let Some(result) = self.try_fast_entity_lookup(execution_query) {
6444 return result;
6445 }
6446 }
6447
6448 if !self.inner.query_audit.has_rules() {
6450 if let Some(result) = frame.read_result_cache(self) {
6451 return Ok(result);
6452 }
6453 }
6454
6455 let prepared = frame.prepare_statement(self, execution_query)?;
6456 let mode = prepared.mode;
6457 let expr = prepared.expr;
6458
6459 let statement = query_expr_name(&expr);
6460 let result_cache_scopes = query_expr_result_cache_scopes(&expr);
6461 let control_event_specs = query_control_event_specs(&expr);
6462 let query_audit_plan = query_audit_plan(&expr);
6463
6464 let _lock_guard = match frame.prepare_dispatch(self, &expr) {
6465 Ok(guard) => guard,
6466 Err(err) => {
6467 let outcome = control_event_outcome_for_error(&err);
6468 for spec in &control_event_specs {
6469 self.emit_control_event(
6470 spec.kind,
6471 outcome,
6472 spec.action,
6473 spec.resource.clone(),
6474 Some(err.to_string()),
6475 spec.fields.clone(),
6476 )?;
6477 }
6478 return Err(err);
6479 }
6480 };
6481 let frame_iface: &dyn super::statement_frame::ReadFrame = &frame;
6482 let query_audit_started = std::time::Instant::now();
6483
6484 let query_result = match expr {
6485 QueryExpr::Graph(_) | QueryExpr::Path(_) => {
6486 let (graph, node_properties, edge_properties) =
6494 self.materialize_graph_with_rls()?;
6495 let result =
6496 crate::storage::query::unified::UnifiedExecutor::execute_on_with_graph_properties(
6497 &graph,
6498 &expr,
6499 node_properties,
6500 edge_properties,
6501 )
6502 .map_err(|err| RedDBError::Query(err.to_string()))?;
6503
6504 Ok(RuntimeQueryResult {
6505 query: query.to_string(),
6506 mode,
6507 statement,
6508 engine: "materialized-graph",
6509 result,
6510 affected_rows: 0,
6511 statement_type: "select",
6512 bookmark: None,
6513 })
6514 }
6515 QueryExpr::Table(table) => {
6516 let table = self.resolve_table_expr_subqueries(
6517 table,
6518 &frame as &dyn super::statement_frame::ReadFrame,
6519 )?;
6520 if let Some(TableSource::Function {
6524 name,
6525 args,
6526 named_args,
6527 }) = table.source.clone()
6528 {
6529 let tvf_result = RuntimeQueryResult {
6537 query: query.to_string(),
6538 mode,
6539 statement,
6540 engine: "runtime-graph-tvf",
6541 result: self.execute_table_function(&name, &args, &named_args)?,
6542 affected_rows: 0,
6543 statement_type: "select",
6544 bookmark: None,
6545 };
6546 frame.write_result_cache(self, &tvf_result, result_cache_scopes.clone());
6547 return Ok(tvf_result);
6548 }
6549 if let Some(TableSource::InlineGraphFunction {
6557 name,
6558 nodes,
6559 edges,
6560 named_args,
6561 }) = table.source.clone()
6562 {
6563 let inline_result = RuntimeQueryResult {
6564 query: query.to_string(),
6565 mode,
6566 statement,
6567 engine: "runtime-graph-tvf-inline",
6568 result: self.execute_inline_graph_function(
6569 &name,
6570 &nodes,
6571 &edges,
6572 &named_args,
6573 )?,
6574 affected_rows: 0,
6575 statement_type: "select",
6576 bookmark: None,
6577 };
6578 frame.write_result_cache(self, &inline_result, result_cache_scopes);
6579 return Ok(inline_result);
6580 }
6581 if super::red_schema::is_virtual_table(&table.table) {
6582 return Ok(RuntimeQueryResult {
6583 query: query.to_string(),
6584 mode,
6585 statement,
6586 engine: "runtime-red-schema",
6587 result: super::red_schema::red_query(
6588 self,
6589 &table.table,
6590 &table,
6591 &frame as &dyn super::statement_frame::ReadFrame,
6592 )?,
6593 affected_rows: 0,
6594 statement_type: "select",
6595 bookmark: None,
6596 });
6597 }
6598
6599 if let Some(view_result) = self.try_resolve_analytics_view(
6603 &table,
6604 &frame as &dyn super::statement_frame::ReadFrame,
6605 )? {
6606 return Ok(RuntimeQueryResult {
6607 query: query.to_string(),
6608 mode,
6609 statement,
6610 engine: "runtime-graph-analytics-view",
6611 result: view_result,
6612 affected_rows: 0,
6613 statement_type: "select",
6614 bookmark: None,
6615 });
6616 }
6617
6618 if let Some(result) = self.execute_probabilistic_select(&table)? {
6619 return Ok(RuntimeQueryResult {
6620 query: query.to_string(),
6621 mode,
6622 statement,
6623 engine: "runtime-probabilistic",
6624 result,
6625 affected_rows: 0,
6626 statement_type: "select",
6627 bookmark: None,
6628 });
6629 }
6630
6631 if self.inner.foreign_tables.is_foreign_table(&table.table) {
6639 let records = self
6640 .inner
6641 .foreign_tables
6642 .scan(&table.table)
6643 .map_err(|e| RedDBError::Internal(e.to_string()))?;
6644 let result = apply_foreign_table_filters(records, &table);
6645 return Ok(RuntimeQueryResult {
6646 query: query.to_string(),
6647 mode,
6648 statement,
6649 engine: "runtime-fdw",
6650 result,
6651 affected_rows: 0,
6652 statement_type: "select",
6653 bookmark: None,
6654 });
6655 }
6656
6657 let Some(table_with_rls) = self.authorize_relational_table_select(
6674 table,
6675 &frame as &dyn super::statement_frame::ReadFrame,
6676 )?
6677 else {
6678 let empty = crate::storage::query::unified::UnifiedResult::empty();
6679 return Ok(RuntimeQueryResult {
6680 query: query.to_string(),
6681 mode,
6682 statement,
6683 engine: "runtime-table-rls",
6684 result: empty,
6685 affected_rows: 0,
6686 statement_type: "select",
6687 bookmark: None,
6688 });
6689 };
6690 Ok(RuntimeQueryResult {
6691 query: query.to_string(),
6692 mode,
6693 statement,
6694 engine: "runtime-table",
6695 result: execute_runtime_table_query_in(
6702 &self.inner.db,
6703 &table_with_rls,
6704 Some(&self.inner.index_store),
6705 Some(frame.row_arena()),
6706 )?,
6707 affected_rows: 0,
6708 statement_type: "select",
6709 bookmark: None,
6710 })
6711 }
6712 QueryExpr::Join(join) => {
6713 let join_with_rls = match self.authorize_relational_join_select(
6722 join,
6723 &frame as &dyn super::statement_frame::ReadFrame,
6724 )? {
6725 Some(j) => j,
6726 None => {
6727 return Ok(RuntimeQueryResult {
6728 query: query.to_string(),
6729 mode,
6730 statement,
6731 engine: "runtime-join-rls",
6732 result: crate::storage::query::unified::UnifiedResult::empty(),
6733 affected_rows: 0,
6734 statement_type: "select",
6735 bookmark: None,
6736 });
6737 }
6738 };
6739 Ok(RuntimeQueryResult {
6740 query: query.to_string(),
6741 mode,
6742 statement,
6743 engine: "runtime-join",
6744 result: execute_runtime_join_query(&self.inner.db, &join_with_rls)?,
6745 affected_rows: 0,
6746 statement_type: "select",
6747 bookmark: None,
6748 })
6749 }
6750 QueryExpr::Vector(vector) => Ok(RuntimeQueryResult {
6751 query: query.to_string(),
6752 mode,
6753 statement,
6754 engine: "runtime-vector",
6755 result: execute_runtime_vector_query(&self.inner.db, &vector)?,
6756 affected_rows: 0,
6757 statement_type: "select",
6758 bookmark: None,
6759 }),
6760 QueryExpr::Hybrid(hybrid) => Ok(RuntimeQueryResult {
6761 query: query.to_string(),
6762 mode,
6763 statement,
6764 engine: "runtime-hybrid",
6765 result: execute_runtime_hybrid_query(&self.inner.db, &hybrid)?,
6766 affected_rows: 0,
6767 statement_type: "select",
6768 bookmark: None,
6769 }),
6770 QueryExpr::Insert(ref insert) if super::red_schema::is_virtual_table(&insert.table) => {
6772 Err(RedDBError::Query(
6773 super::red_schema::READ_ONLY_ERROR.to_string(),
6774 ))
6775 }
6776 QueryExpr::Update(ref update) if super::red_schema::is_virtual_table(&update.table) => {
6777 Err(RedDBError::Query(
6778 super::red_schema::READ_ONLY_ERROR.to_string(),
6779 ))
6780 }
6781 QueryExpr::Delete(ref delete) if super::red_schema::is_virtual_table(&delete.table) => {
6782 Err(RedDBError::Query(
6783 super::red_schema::READ_ONLY_ERROR.to_string(),
6784 ))
6785 }
6786 QueryExpr::Insert(ref insert) => self
6787 .with_deferred_store_wal_for_dml(self.insert_may_emit_events(insert), || {
6788 self.execute_insert(query, insert)
6789 }),
6790 QueryExpr::Update(ref update) => self
6791 .with_deferred_store_wal_for_dml(self.update_may_emit_events(update), || {
6792 self.execute_update(query, update)
6793 }),
6794 QueryExpr::Delete(ref delete) => self
6795 .with_deferred_store_wal_for_dml(self.delete_may_emit_events(delete), || {
6796 self.execute_delete(query, delete)
6797 }),
6798 QueryExpr::CreateTable(ref create) => self.execute_create_table(query, create),
6800 QueryExpr::CreateCollection(ref create) => {
6801 self.execute_create_collection(query, create)
6802 }
6803 QueryExpr::CreateVector(ref create) => self.execute_create_vector(query, create),
6804 QueryExpr::DropTable(ref drop_tbl) => self.execute_drop_table(query, drop_tbl),
6805 QueryExpr::DropGraph(ref drop_graph) => self.execute_drop_graph(query, drop_graph),
6806 QueryExpr::DropVector(ref drop_vector) => self.execute_drop_vector(query, drop_vector),
6807 QueryExpr::DropDocument(ref drop_document) => {
6808 self.execute_drop_document(query, drop_document)
6809 }
6810 QueryExpr::DropKv(ref drop_kv) => self.execute_drop_kv(query, drop_kv),
6811 QueryExpr::DropCollection(ref drop_collection) => {
6812 self.execute_drop_collection(query, drop_collection)
6813 }
6814 QueryExpr::Truncate(ref truncate) => self.execute_truncate(query, truncate),
6815 QueryExpr::AlterTable(ref alter) => self.execute_alter_table(query, alter),
6816 QueryExpr::ExplainAlter(ref explain) => self.execute_explain_alter(query, explain),
6817 QueryExpr::GraphCommand(ref cmd) => self.execute_graph_command(query, cmd),
6819 QueryExpr::SearchCommand(ref cmd) => self.execute_search_command(query, cmd),
6821 QueryExpr::Ask(ref ask) => self.execute_ask(query, ask),
6823 QueryExpr::CreateIndex(ref create_idx) => self.execute_create_index(query, create_idx),
6824 QueryExpr::DropIndex(ref drop_idx) => self.execute_drop_index(query, drop_idx),
6825 QueryExpr::ProbabilisticCommand(ref cmd) => {
6826 self.execute_probabilistic_command(query, cmd)
6827 }
6828 QueryExpr::CreateTimeSeries(ref ts) => self.execute_create_timeseries(query, ts),
6830 QueryExpr::CreateMetric(ref metric) => self.execute_create_metric(query, metric),
6831 QueryExpr::AlterMetric(ref alter) => self.execute_alter_metric(query, alter),
6832 QueryExpr::CreateSlo(ref slo) => self.execute_create_slo(query, slo),
6833 QueryExpr::DropTimeSeries(ref ts) => self.execute_drop_timeseries(query, ts),
6834 QueryExpr::CreateQueue(ref q) => self.execute_create_queue(query, q),
6836 QueryExpr::AlterQueue(ref q) => self.execute_alter_queue(query, q),
6837 QueryExpr::DropQueue(ref q) => self.execute_drop_queue(query, q),
6838 QueryExpr::QueueSelect(ref q) => self.execute_queue_select(query, q),
6839 QueryExpr::QueueCommand(ref cmd) => self.execute_queue_command(query, cmd),
6840 QueryExpr::EventsBackfill(ref backfill) => {
6841 self.execute_events_backfill(query, backfill)
6842 }
6843 QueryExpr::EventsBackfillStatus { ref collection } => Err(RedDBError::Query(format!(
6844 "EVENTS BACKFILL STATUS for '{collection}' is not implemented in this slice"
6845 ))),
6846 QueryExpr::KvCommand(ref cmd) => self.execute_kv_command(query, cmd),
6847 QueryExpr::ConfigCommand(ref cmd) => self.execute_config_command(query, cmd),
6848 QueryExpr::CreateTree(ref tree) => self.execute_create_tree(query, tree),
6849 QueryExpr::DropTree(ref tree) => self.execute_drop_tree(query, tree),
6850 QueryExpr::TreeCommand(ref cmd) => self.execute_tree_command(query, cmd),
6851 QueryExpr::SetConfig { ref key, ref value } => {
6853 if key.starts_with("red.secret.") {
6854 return Err(RedDBError::Query(
6855 "red.secret.* is reserved for vault secrets; use SET SECRET".to_string(),
6856 ));
6857 }
6858 match self.check_managed_config_write_for_set_config(key) {
6859 Err(err) => Err(err),
6860 Ok(()) => {
6861 let store = self.inner.db.store();
6862 let json_val = match value {
6863 Value::Text(s) => crate::serde_json::Value::String(s.to_string()),
6864 Value::Integer(n) => crate::serde_json::Value::Number(*n as f64),
6865 Value::Float(n) => crate::serde_json::Value::Number(*n),
6866 Value::Boolean(b) => crate::serde_json::Value::Bool(*b),
6867 _ => crate::serde_json::Value::String(value.to_string()),
6868 };
6869 store.set_config_tree(key, &json_val);
6870 update_current_config_value(key, value.clone());
6871 self.invalidate_result_cache();
6876 Ok(RuntimeQueryResult::ok_message(
6877 query.to_string(),
6878 &format!("config set: {key}"),
6879 "set",
6880 ))
6881 }
6882 }
6883 }
6884 QueryExpr::SetSecret { ref key, ref value } => {
6886 if key.starts_with("red.config.") {
6887 return Err(RedDBError::Query(
6888 "red.config.* is reserved for config; use SET CONFIG".to_string(),
6889 ));
6890 }
6891 let auth_store = self.inner.auth_store.read().clone().ok_or_else(|| {
6892 RedDBError::Query("SET SECRET requires an enabled, unsealed vault".to_string())
6893 })?;
6894 if matches!(value, Value::Null) {
6895 auth_store
6896 .vault_kv_try_delete(key)
6897 .map_err(|err| RedDBError::Query(err.to_string()))?;
6898 update_current_secret_value(key, None);
6899 self.invalidate_result_cache();
6900 return Ok(RuntimeQueryResult::ok_message(
6901 query.to_string(),
6902 &format!("secret deleted: {key}"),
6903 "delete_secret",
6904 ));
6905 }
6906 let value = secret_sql_value_to_string(value)?;
6907 auth_store
6908 .vault_kv_try_set(key.clone(), value.clone())
6909 .map_err(|err| RedDBError::Query(err.to_string()))?;
6910 update_current_secret_value(key, Some(value));
6911 self.invalidate_result_cache();
6912 Ok(RuntimeQueryResult::ok_message(
6913 query.to_string(),
6914 &format!("secret set: {key}"),
6915 "set_secret",
6916 ))
6917 }
6918 QueryExpr::DeleteSecret { ref key } => {
6920 let auth_store = self.inner.auth_store.read().clone().ok_or_else(|| {
6921 RedDBError::Query(
6922 "DELETE SECRET requires an enabled, unsealed vault".to_string(),
6923 )
6924 })?;
6925 let deleted = auth_store
6926 .vault_kv_try_delete(key)
6927 .map_err(|err| RedDBError::Query(err.to_string()))?;
6928 if deleted {
6929 update_current_secret_value(key, None);
6930 }
6931 self.invalidate_result_cache();
6932 Ok(RuntimeQueryResult::ok_message(
6933 query.to_string(),
6934 &format!("secret deleted: {key}"),
6935 if deleted {
6936 "delete_secret"
6937 } else {
6938 "delete_secret_not_found"
6939 },
6940 ))
6941 }
6942 QueryExpr::ShowSecrets { ref prefix } => {
6944 let auth_store = self.inner.auth_store.read().clone().ok_or_else(|| {
6945 RedDBError::Query("SHOW SECRET requires an enabled, unsealed vault".to_string())
6946 })?;
6947 if !auth_store.is_vault_backed() {
6948 return Err(RedDBError::Query(
6949 "SHOW SECRET requires an enabled, unsealed vault".to_string(),
6950 ));
6951 }
6952 let mut keys = auth_store.vault_kv_keys();
6953 keys.sort();
6954 let mut result = UnifiedResult::with_columns(vec![
6955 "key".into(),
6956 "value".into(),
6957 "status".into(),
6958 ]);
6959 for key in keys {
6960 if let Some(ref pfx) = prefix {
6961 if !key.starts_with(pfx) {
6962 continue;
6963 }
6964 }
6965 let mut record = UnifiedRecord::new();
6966 record.set("key", Value::text(key));
6967 record.set("value", Value::text("***"));
6968 record.set("status", Value::text("active"));
6969 result.push(record);
6970 }
6971 Ok(RuntimeQueryResult {
6972 query: query.to_string(),
6973 mode,
6974 statement: "show_secrets",
6975 engine: "runtime-secret",
6976 result,
6977 affected_rows: 0,
6978 statement_type: "select",
6979 bookmark: None,
6980 })
6981 }
6982 QueryExpr::ShowConfig { ref prefix } => {
6984 let store = self.inner.db.store();
6985 let all_collections = store.list_collections();
6986 if !all_collections.contains(&"red_config".to_string()) {
6987 let result = UnifiedResult::with_columns(vec!["key".into(), "value".into()]);
6988 return Ok(RuntimeQueryResult {
6989 query: query.to_string(),
6990 mode,
6991 statement: "show_config",
6992 engine: "runtime-config",
6993 result,
6994 affected_rows: 0,
6995 statement_type: "select",
6996 bookmark: None,
6997 });
6998 }
6999 let manager = store
7000 .get_collection("red_config")
7001 .ok_or_else(|| RedDBError::NotFound("red_config".to_string()))?;
7002 let entities = manager.query_all(|_| true);
7003 let mut latest = std::collections::BTreeMap::<String, (u64, Value, Value)>::new();
7004 for entity in entities {
7005 if let EntityData::Row(ref row) = entity.data {
7006 if let Some(ref named) = row.named {
7007 let key_val = named.get("key").cloned().unwrap_or(Value::Null);
7008 let val = named.get("value").cloned().unwrap_or(Value::Null);
7009 let key_str = match &key_val {
7010 Value::Text(s) => s.as_ref(),
7011 _ => continue,
7012 };
7013 if let Some(ref pfx) = prefix {
7014 if !key_str.starts_with(pfx.as_str()) {
7015 continue;
7016 }
7017 }
7018 let entity_id = entity.id.raw();
7019 match latest.get(key_str) {
7020 Some((prev_id, _, _)) if *prev_id > entity_id => {}
7021 _ => {
7022 latest.insert(key_str.to_string(), (entity_id, key_val, val));
7023 }
7024 }
7025 }
7026 }
7027 }
7028 let mut result = UnifiedResult::with_columns(vec!["key".into(), "value".into()]);
7029 for (_, key_val, val) in latest.into_values() {
7030 let mut record = UnifiedRecord::new();
7031 record.set("key", key_val);
7032 record.set("value", val);
7033 result.push(record);
7034 }
7035 Ok(RuntimeQueryResult {
7036 query: query.to_string(),
7037 mode,
7038 statement: "show_config",
7039 engine: "runtime-config",
7040 result,
7041 affected_rows: 0,
7042 statement_type: "select",
7043 bookmark: None,
7044 })
7045 }
7046 QueryExpr::SetTenant(ref value) => {
7052 match value {
7053 Some(id) => set_current_tenant(id.clone()),
7054 None => clear_current_tenant(),
7055 }
7056 Ok(RuntimeQueryResult::ok_message(
7057 query.to_string(),
7058 &match value {
7059 Some(id) => format!("tenant set: {id}"),
7060 None => "tenant cleared".to_string(),
7061 },
7062 "set_tenant",
7063 ))
7064 }
7065 QueryExpr::ShowTenant => {
7066 let mut result = UnifiedResult::with_columns(vec!["tenant".into()]);
7067 let mut record = UnifiedRecord::new();
7068 record.set(
7069 "tenant",
7070 current_tenant().map(Value::text).unwrap_or(Value::Null),
7071 );
7072 result.push(record);
7073 Ok(RuntimeQueryResult {
7074 query: query.to_string(),
7075 mode,
7076 statement: "show_tenant",
7077 engine: "runtime-tenant",
7078 result,
7079 affected_rows: 0,
7080 statement_type: "select",
7081 bookmark: None,
7082 })
7083 }
7084 QueryExpr::TransactionControl(ref ctl) => {
7096 use crate::storage::query::ast::TxnControl;
7097 use crate::storage::transaction::snapshot::{TxnContext, Xid};
7098 use crate::storage::transaction::IsolationLevel;
7099
7100 let conn_id = current_connection_id();
7105
7106 let (kind, msg) = match ctl {
7107 TxnControl::Begin => {
7108 let mgr = Arc::clone(&self.inner.snapshot_manager);
7109 let xid = mgr.begin();
7110 let snapshot = mgr.snapshot(xid);
7111 let ctx = TxnContext {
7112 xid,
7113 isolation: IsolationLevel::SnapshotIsolation,
7114 snapshot,
7115 savepoints: Vec::new(),
7116 released_sub_xids: Vec::new(),
7117 };
7118 self.inner.tx_contexts.write().insert(conn_id, ctx);
7119 ("begin", format!("BEGIN — xid={xid} (snapshot isolation)"))
7120 }
7121 TxnControl::Commit => {
7122 self.inner.tx_local_tenants.write().remove(&conn_id);
7124 let ctx = self.inner.tx_contexts.write().remove(&conn_id);
7125 match ctx {
7126 Some(ctx) => {
7127 let mut own_xids = std::collections::HashSet::new();
7128 own_xids.insert(ctx.xid);
7129 for (_, sub) in &ctx.savepoints {
7130 own_xids.insert(*sub);
7131 }
7132 for sub in &ctx.released_sub_xids {
7133 own_xids.insert(*sub);
7134 }
7135 if let Err(err) = self.check_table_row_write_conflicts(
7136 conn_id,
7137 &ctx.snapshot,
7138 &own_xids,
7139 ) {
7140 for (_, sub) in &ctx.savepoints {
7141 self.inner.snapshot_manager.rollback(*sub);
7142 }
7143 for sub in &ctx.released_sub_xids {
7144 self.inner.snapshot_manager.rollback(*sub);
7145 }
7146 self.inner.snapshot_manager.rollback(ctx.xid);
7147 self.revive_pending_versioned_updates(conn_id);
7148 self.revive_pending_tombstones(conn_id);
7149 self.discard_pending_kv_watch_events(conn_id);
7150 self.discard_pending_queue_wakes(conn_id);
7151 self.discard_pending_store_wal_actions(conn_id);
7152 return Err(err);
7153 }
7154 self.restore_pending_write_stamps(conn_id);
7155 if let Err(err) = self.flush_pending_store_wal_actions(conn_id) {
7156 for (_, sub) in &ctx.savepoints {
7157 self.inner.snapshot_manager.rollback(*sub);
7158 }
7159 for sub in &ctx.released_sub_xids {
7160 self.inner.snapshot_manager.rollback(*sub);
7161 }
7162 self.inner.snapshot_manager.rollback(ctx.xid);
7163 self.revive_pending_versioned_updates(conn_id);
7164 self.revive_pending_tombstones(conn_id);
7165 self.discard_pending_kv_watch_events(conn_id);
7166 return Err(err);
7167 }
7168 for (_, sub) in &ctx.savepoints {
7174 self.inner.snapshot_manager.commit(*sub);
7175 }
7176 for sub in &ctx.released_sub_xids {
7177 self.inner.snapshot_manager.commit(*sub);
7178 }
7179 self.inner.snapshot_manager.commit(ctx.xid);
7180 self.finalize_pending_versioned_updates(conn_id);
7181 self.finalize_pending_tombstones(conn_id);
7182 self.finalize_pending_kv_watch_events(conn_id);
7183 self.finalize_pending_queue_wakes(conn_id);
7184 ("commit", format!("COMMIT — xid={} committed", ctx.xid))
7185 }
7186 None => (
7187 "commit",
7188 "COMMIT outside transaction — no-op (autocommit)".to_string(),
7189 ),
7190 }
7191 }
7192 TxnControl::Rollback => {
7193 self.inner.tx_local_tenants.write().remove(&conn_id);
7194 let ctx = self.inner.tx_contexts.write().remove(&conn_id);
7195 match ctx {
7196 Some(ctx) => {
7197 for (_, sub) in &ctx.savepoints {
7200 self.inner.snapshot_manager.rollback(*sub);
7201 }
7202 for sub in &ctx.released_sub_xids {
7203 self.inner.snapshot_manager.rollback(*sub);
7204 }
7205 self.inner.snapshot_manager.rollback(ctx.xid);
7206 self.revive_pending_versioned_updates(conn_id);
7210 self.revive_pending_tombstones(conn_id);
7211 self.discard_pending_kv_watch_events(conn_id);
7212 self.discard_pending_queue_wakes(conn_id);
7213 self.discard_pending_store_wal_actions(conn_id);
7214 ("rollback", format!("ROLLBACK — xid={} aborted", ctx.xid))
7215 }
7216 None => (
7217 "rollback",
7218 "ROLLBACK outside transaction — no-op (autocommit)".to_string(),
7219 ),
7220 }
7221 }
7222 TxnControl::Savepoint(name) => {
7229 let mgr = Arc::clone(&self.inner.snapshot_manager);
7230 let mut guard = self.inner.tx_contexts.write();
7231 match guard.get_mut(&conn_id) {
7232 Some(ctx) => {
7233 let sub = mgr.begin();
7234 ctx.savepoints.push((name.clone(), sub));
7235 ("savepoint", format!("SAVEPOINT {name} — sub_xid={sub}"))
7236 }
7237 None => (
7238 "savepoint",
7239 "SAVEPOINT outside transaction — no-op".to_string(),
7240 ),
7241 }
7242 }
7243 TxnControl::ReleaseSavepoint(name) => {
7244 let mut guard = self.inner.tx_contexts.write();
7245 match guard.get_mut(&conn_id) {
7246 Some(ctx) => {
7247 let pos = ctx
7248 .savepoints
7249 .iter()
7250 .position(|(n, _)| n == name)
7251 .ok_or_else(|| {
7252 RedDBError::Internal(format!(
7253 "savepoint {name} does not exist"
7254 ))
7255 })?;
7256 let released = ctx.savepoints.len() - pos;
7264 let popped: Vec<Xid> = ctx
7265 .savepoints
7266 .split_off(pos)
7267 .into_iter()
7268 .map(|(_, x)| x)
7269 .collect();
7270 ctx.released_sub_xids.extend(popped);
7271 (
7272 "release_savepoint",
7273 format!("RELEASE SAVEPOINT {name} — {released} level(s)"),
7274 )
7275 }
7276 None => (
7277 "release_savepoint",
7278 "RELEASE outside transaction — no-op".to_string(),
7279 ),
7280 }
7281 }
7282 TxnControl::RollbackToSavepoint(name) => {
7283 let mgr = Arc::clone(&self.inner.snapshot_manager);
7284 let drop_result: Option<(Xid, Vec<Xid>)> = {
7289 let mut guard = self.inner.tx_contexts.write();
7290 if let Some(ctx) = guard.get_mut(&conn_id) {
7291 let pos = ctx
7292 .savepoints
7293 .iter()
7294 .position(|(n, _)| n == name)
7295 .ok_or_else(|| {
7296 RedDBError::Internal(format!(
7297 "savepoint {name} does not exist"
7298 ))
7299 })?;
7300 let savepoint_xid = ctx.savepoints[pos].1;
7301 let aborted: Vec<Xid> = ctx
7302 .savepoints
7303 .split_off(pos)
7304 .into_iter()
7305 .map(|(_, x)| x)
7306 .collect();
7307 Some((savepoint_xid, aborted))
7308 } else {
7309 None
7310 }
7311 };
7312
7313 match drop_result {
7314 Some((savepoint_xid, aborted)) => {
7315 for x in &aborted {
7316 mgr.rollback(*x);
7317 }
7318 let reverted_updates =
7319 self.revive_versioned_updates_since(conn_id, savepoint_xid);
7320 let revived = self.revive_tombstones_since(conn_id, savepoint_xid);
7321 (
7322 "rollback_to_savepoint",
7323 format!(
7324 "ROLLBACK TO SAVEPOINT {name} — aborted {} sub_xid(s), reverted {reverted_updates} update(s), revived {revived} tombstone(s)",
7325 aborted.len(),
7326 ),
7327 )
7328 }
7329 None => (
7330 "rollback_to_savepoint",
7331 "ROLLBACK TO outside transaction — no-op".to_string(),
7332 ),
7333 }
7334 }
7335 };
7336 Ok(RuntimeQueryResult::ok_message(
7337 query.to_string(),
7338 &msg,
7339 kind,
7340 ))
7341 }
7342 QueryExpr::CreateSchema(ref q) => {
7355 let store = self.inner.db.store();
7356 let key = format!("schema.{}", q.name);
7357 if store.get_config(&key).is_some() {
7358 if q.if_not_exists {
7359 return Ok(RuntimeQueryResult::ok_message(
7360 query.to_string(),
7361 &format!("schema {} already exists — skipped", q.name),
7362 "create_schema",
7363 ));
7364 }
7365 return Err(RedDBError::Internal(format!(
7366 "schema {} already exists",
7367 q.name
7368 )));
7369 }
7370 store.set_config_tree(&key, &crate::serde_json::Value::Bool(true));
7371 Ok(RuntimeQueryResult::ok_message(
7372 query.to_string(),
7373 &format!("schema {} created", q.name),
7374 "create_schema",
7375 ))
7376 }
7377 QueryExpr::DropSchema(ref q) => {
7378 let store = self.inner.db.store();
7379 let key = format!("schema.{}", q.name);
7380 let existed = store.get_config(&key).is_some();
7381 if !existed && !q.if_exists {
7382 return Err(RedDBError::Internal(format!(
7383 "schema {} does not exist",
7384 q.name
7385 )));
7386 }
7387 store.set_config_tree(&key, &crate::serde_json::Value::Null);
7389 let suffix = if q.cascade {
7390 " (CASCADE accepted — tables untouched)"
7391 } else {
7392 ""
7393 };
7394 Ok(RuntimeQueryResult::ok_message(
7395 query.to_string(),
7396 &format!("schema {} dropped{}", q.name, suffix),
7397 "drop_schema",
7398 ))
7399 }
7400 QueryExpr::CreateSequence(ref q) => {
7401 let store = self.inner.db.store();
7402 let base = format!("sequence.{}", q.name);
7403 let start_key = format!("{base}.start");
7404 let incr_key = format!("{base}.increment");
7405 let curr_key = format!("{base}.current");
7406 if store.get_config(&start_key).is_some() {
7407 if q.if_not_exists {
7408 return Ok(RuntimeQueryResult::ok_message(
7409 query.to_string(),
7410 &format!("sequence {} already exists — skipped", q.name),
7411 "create_sequence",
7412 ));
7413 }
7414 return Err(RedDBError::Internal(format!(
7415 "sequence {} already exists",
7416 q.name
7417 )));
7418 }
7419 let initial_current = q.start - q.increment;
7422 store.set_config_tree(
7423 &start_key,
7424 &crate::serde_json::Value::Number(q.start as f64),
7425 );
7426 store.set_config_tree(
7427 &incr_key,
7428 &crate::serde_json::Value::Number(q.increment as f64),
7429 );
7430 store.set_config_tree(
7431 &curr_key,
7432 &crate::serde_json::Value::Number(initial_current as f64),
7433 );
7434 Ok(RuntimeQueryResult::ok_message(
7435 query.to_string(),
7436 &format!(
7437 "sequence {} created (start={}, increment={})",
7438 q.name, q.start, q.increment
7439 ),
7440 "create_sequence",
7441 ))
7442 }
7443 QueryExpr::DropSequence(ref q) => {
7444 let store = self.inner.db.store();
7445 let base = format!("sequence.{}", q.name);
7446 let existed = store.get_config(&format!("{base}.start")).is_some();
7447 if !existed && !q.if_exists {
7448 return Err(RedDBError::Internal(format!(
7449 "sequence {} does not exist",
7450 q.name
7451 )));
7452 }
7453 for k in ["start", "increment", "current"] {
7454 store.set_config_tree(&format!("{base}.{k}"), &crate::serde_json::Value::Null);
7455 }
7456 Ok(RuntimeQueryResult::ok_message(
7457 query.to_string(),
7458 &format!("sequence {} dropped", q.name),
7459 "drop_sequence",
7460 ))
7461 }
7462 QueryExpr::CreateView(ref q) => {
7472 let mut views = self.inner.views.write();
7473 if views.contains_key(&q.name) && !q.or_replace {
7474 if q.if_not_exists {
7475 return Ok(RuntimeQueryResult::ok_message(
7476 query.to_string(),
7477 &format!("view {} already exists — skipped", q.name),
7478 "create_view",
7479 ));
7480 }
7481 return Err(RedDBError::Internal(format!(
7482 "view {} already exists",
7483 q.name
7484 )));
7485 }
7486 views.insert(q.name.clone(), Arc::new(q.clone()));
7487 drop(views);
7488
7489 if q.materialized {
7491 use crate::storage::cache::result::{MaterializedViewDef, RefreshPolicy};
7492 let refresh = match q.refresh_every_ms {
7493 Some(ms) => RefreshPolicy::Periodic(std::time::Duration::from_millis(ms)),
7494 None => RefreshPolicy::Manual,
7495 };
7496 let dependencies = collect_table_refs(&q.query);
7497 let def = MaterializedViewDef {
7498 name: q.name.clone(),
7499 query: format!("<parsed view {}>", q.name),
7500 dependencies: dependencies.clone(),
7501 refresh,
7502 retention_duration_ms: q.retention_duration_ms,
7503 };
7504 self.inner.materialized_views.write().register(def);
7505
7506 let descriptor =
7512 crate::runtime::continuous_materialized_view::MaterializedViewDescriptor {
7513 name: q.name.clone(),
7514 source_sql: query.to_string(),
7515 source_collections: dependencies,
7516 refresh_every_ms: q.refresh_every_ms,
7517 retention_duration_ms: q.retention_duration_ms,
7518 };
7519 let store = self.inner.db.store();
7520 crate::runtime::continuous_materialized_view::persist_descriptor(
7521 store.as_ref(),
7522 &descriptor,
7523 )?;
7524
7525 self.ensure_materialized_view_backing(&q.name)?;
7532 }
7533 self.invalidate_plan_cache();
7538 self.invalidate_result_cache();
7539
7540 Ok(RuntimeQueryResult::ok_message(
7541 query.to_string(),
7542 &format!(
7543 "{}view {} created",
7544 if q.materialized { "materialized " } else { "" },
7545 q.name
7546 ),
7547 "create_view",
7548 ))
7549 }
7550 QueryExpr::DropView(ref q) => {
7551 let mut views = self.inner.views.write();
7552 let removed = views.remove(&q.name);
7553 let existed = removed.is_some();
7554 let removed_materialized =
7555 removed.as_ref().map(|v| v.materialized).unwrap_or(false);
7556 drop(views);
7557 if q.materialized || existed {
7558 self.inner.materialized_views.write().remove(&q.name);
7560 let store = self.inner.db.store();
7564 crate::runtime::continuous_materialized_view::remove_by_name(
7565 store.as_ref(),
7566 &q.name,
7567 )?;
7568 }
7569 if removed_materialized || q.materialized {
7573 self.drop_materialized_view_backing(&q.name)?;
7574 }
7575 self.invalidate_plan_cache();
7578 self.invalidate_result_cache();
7579 if !existed && !q.if_exists {
7580 return Err(RedDBError::Internal(format!(
7581 "view {} does not exist",
7582 q.name
7583 )));
7584 }
7585 self.invalidate_plan_cache();
7586 Ok(RuntimeQueryResult::ok_message(
7587 query.to_string(),
7588 &format!("view {} dropped", q.name),
7589 "drop_view",
7590 ))
7591 }
7592 QueryExpr::RefreshMaterializedView(ref q) => {
7593 let view = {
7596 let views = self.inner.views.read();
7597 views.get(&q.name).cloned()
7598 };
7599 let view = match view {
7600 Some(v) => v,
7601 None => {
7602 return Err(RedDBError::Internal(format!(
7603 "view {} does not exist",
7604 q.name
7605 )))
7606 }
7607 };
7608 if !view.materialized {
7609 return Err(RedDBError::Internal(format!(
7610 "view {} is not materialized — REFRESH requires \
7611 CREATE MATERIALIZED VIEW",
7612 q.name
7613 )));
7614 }
7615 let started = std::time::Instant::now();
7617 let now_ms = std::time::SystemTime::now()
7618 .duration_since(std::time::UNIX_EPOCH)
7619 .map(|d| d.as_millis() as u64)
7620 .unwrap_or(0);
7621 match self.execute_query_expr((*view.query).clone()) {
7622 Ok(inner_result) => {
7623 let entities =
7630 view_records_to_entities(&q.name, &inner_result.result.records);
7631 let row_count = entities.len() as u64;
7632 let store = self.inner.db.store();
7633 let serialized_records = match store.refresh_collection(&q.name, entities) {
7634 Ok(records) => records,
7635 Err(err) => {
7636 let duration_ms = started.elapsed().as_millis() as u64;
7637 let msg = err.to_string();
7638 self.inner
7639 .materialized_views
7640 .write()
7641 .record_refresh_failure(
7642 &q.name,
7643 msg.clone(),
7644 duration_ms,
7645 now_ms,
7646 );
7647 return Err(RedDBError::Internal(format!(
7648 "REFRESH MATERIALIZED VIEW {}: {msg}",
7649 q.name
7650 )));
7651 }
7652 };
7653
7654 if let Some(ref primary) = self.inner.db.replication {
7660 let lsn = self.inner.cdc.emit(
7661 crate::replication::cdc::ChangeOperation::Refresh,
7662 &q.name,
7663 0,
7664 "refresh",
7665 );
7666 self.invalidate_result_cache_for_table(&q.name);
7667 let timestamp = std::time::SystemTime::now()
7668 .duration_since(std::time::UNIX_EPOCH)
7669 .unwrap_or_default()
7670 .as_millis() as u64;
7671 let record = ChangeRecord::for_refresh(
7672 lsn,
7673 timestamp,
7674 q.name.clone(),
7675 serialized_records,
7676 )
7677 .with_term(self.current_replication_term());
7678 let encoded = record.encode();
7679 primary.append_logical_record(record.lsn, encoded);
7680 }
7681
7682 let duration_ms = started.elapsed().as_millis() as u64;
7683 let serialized = format!("{:?}", inner_result.result);
7684 self.inner
7685 .materialized_views
7686 .write()
7687 .record_refresh_success(
7688 &q.name,
7689 serialized.into_bytes(),
7690 row_count,
7691 duration_ms,
7692 now_ms,
7693 );
7694 self.invalidate_result_cache();
7699 Ok(RuntimeQueryResult::ok_message(
7700 query.to_string(),
7701 &format!("materialized view {} refreshed", q.name),
7702 "refresh_materialized_view",
7703 ))
7704 }
7705 Err(err) => {
7706 let duration_ms = started.elapsed().as_millis() as u64;
7707 let msg = err.to_string();
7708 self.inner
7709 .materialized_views
7710 .write()
7711 .record_refresh_failure(&q.name, msg.clone(), duration_ms, now_ms);
7712 Err(err)
7713 }
7714 }
7715 }
7716 QueryExpr::CreatePolicy(ref q) => {
7723 let key = (q.table.clone(), q.name.clone());
7724 self.inner
7725 .rls_policies
7726 .write()
7727 .insert(key, Arc::new(q.clone()));
7728 self.invalidate_plan_cache();
7729 self.schema_vocabulary_apply(
7733 crate::runtime::schema_vocabulary::DdlEvent::CreatePolicy {
7734 collection: q.table.clone(),
7735 policy: q.name.clone(),
7736 },
7737 );
7738 Ok(RuntimeQueryResult::ok_message(
7739 query.to_string(),
7740 &format!("policy {} on {} created", q.name, q.table),
7741 "create_policy",
7742 ))
7743 }
7744 QueryExpr::DropPolicy(ref q) => {
7745 let removed = self
7746 .inner
7747 .rls_policies
7748 .write()
7749 .remove(&(q.table.clone(), q.name.clone()))
7750 .is_some();
7751 if !removed && !q.if_exists {
7752 return Err(RedDBError::Internal(format!(
7753 "policy {} on {} does not exist",
7754 q.name, q.table
7755 )));
7756 }
7757 self.invalidate_plan_cache();
7758 self.schema_vocabulary_apply(
7761 crate::runtime::schema_vocabulary::DdlEvent::DropPolicy {
7762 collection: q.table.clone(),
7763 policy: q.name.clone(),
7764 },
7765 );
7766 Ok(RuntimeQueryResult::ok_message(
7767 query.to_string(),
7768 &format!("policy {} on {} dropped", q.name, q.table),
7769 "drop_policy",
7770 ))
7771 }
7772 QueryExpr::CreateServer(ref q) => {
7783 use crate::storage::fdw::FdwOptions;
7784 let registry = Arc::clone(&self.inner.foreign_tables);
7785 if registry.server(&q.name).is_some() {
7786 if q.if_not_exists {
7787 return Ok(RuntimeQueryResult::ok_message(
7788 query.to_string(),
7789 &format!("server {} already exists — skipped", q.name),
7790 "create_server",
7791 ));
7792 }
7793 return Err(RedDBError::Internal(format!(
7794 "server {} already exists",
7795 q.name
7796 )));
7797 }
7798 let mut opts = FdwOptions::new();
7799 for (k, v) in &q.options {
7800 opts.values.insert(k.clone(), v.clone());
7801 }
7802 registry
7803 .create_server(&q.name, &q.wrapper, opts)
7804 .map_err(|e| RedDBError::Internal(e.to_string()))?;
7805 Ok(RuntimeQueryResult::ok_message(
7806 query.to_string(),
7807 &format!("server {} created (wrapper {})", q.name, q.wrapper),
7808 "create_server",
7809 ))
7810 }
7811 QueryExpr::DropServer(ref q) => {
7812 let existed = self.inner.foreign_tables.drop_server(&q.name);
7813 if !existed && !q.if_exists {
7814 return Err(RedDBError::Internal(format!(
7815 "server {} does not exist",
7816 q.name
7817 )));
7818 }
7819 Ok(RuntimeQueryResult::ok_message(
7820 query.to_string(),
7821 &format!(
7822 "server {} dropped{}",
7823 q.name,
7824 if q.cascade { " (cascade)" } else { "" }
7825 ),
7826 "drop_server",
7827 ))
7828 }
7829 QueryExpr::CreateForeignTable(ref q) => {
7830 use crate::storage::fdw::{FdwOptions, ForeignColumn, ForeignTable};
7831 let registry = Arc::clone(&self.inner.foreign_tables);
7832 if registry.foreign_table(&q.name).is_some() {
7833 if q.if_not_exists {
7834 return Ok(RuntimeQueryResult::ok_message(
7835 query.to_string(),
7836 &format!("foreign table {} already exists — skipped", q.name),
7837 "create_foreign_table",
7838 ));
7839 }
7840 return Err(RedDBError::Internal(format!(
7841 "foreign table {} already exists",
7842 q.name
7843 )));
7844 }
7845 let mut opts = FdwOptions::new();
7846 for (k, v) in &q.options {
7847 opts.values.insert(k.clone(), v.clone());
7848 }
7849 let columns: Vec<ForeignColumn> = q
7850 .columns
7851 .iter()
7852 .map(|c| ForeignColumn {
7853 name: c.name.clone(),
7854 data_type: c.data_type.clone(),
7855 not_null: c.not_null,
7856 })
7857 .collect();
7858 registry
7859 .create_foreign_table(ForeignTable {
7860 name: q.name.clone(),
7861 server_name: q.server.clone(),
7862 columns,
7863 options: opts,
7864 })
7865 .map_err(|e| RedDBError::Internal(e.to_string()))?;
7866 self.invalidate_plan_cache();
7867 Ok(RuntimeQueryResult::ok_message(
7868 query.to_string(),
7869 &format!("foreign table {} created (server {})", q.name, q.server),
7870 "create_foreign_table",
7871 ))
7872 }
7873 QueryExpr::DropForeignTable(ref q) => {
7874 let existed = self.inner.foreign_tables.drop_foreign_table(&q.name);
7875 if !existed && !q.if_exists {
7876 return Err(RedDBError::Internal(format!(
7877 "foreign table {} does not exist",
7878 q.name
7879 )));
7880 }
7881 self.invalidate_plan_cache();
7882 Ok(RuntimeQueryResult::ok_message(
7883 query.to_string(),
7884 &format!("foreign table {} dropped", q.name),
7885 "drop_foreign_table",
7886 ))
7887 }
7888 QueryExpr::CopyFrom(ref q) => {
7894 use crate::storage::import::{CsvConfig, CsvImporter};
7895 let store = self.inner.db.store();
7896 let cfg = CsvConfig {
7897 collection: q.table.clone(),
7898 has_header: q.has_header,
7899 delimiter: q.delimiter.map(|c| c as u8).unwrap_or(b','),
7900 ..CsvConfig::default()
7901 };
7902 let importer = CsvImporter::new(cfg);
7903 let stats = importer
7904 .import_file(&q.path, store.as_ref())
7905 .map_err(|e| RedDBError::Internal(format!("COPY failed: {e}")))?;
7906 self.note_table_write(&q.table);
7908 Ok(RuntimeQueryResult::ok_message(
7909 query.to_string(),
7910 &format!(
7911 "COPY imported {} rows into {} ({} errors skipped, {}ms)",
7912 stats.records_imported, q.table, stats.errors_skipped, stats.duration_ms
7913 ),
7914 "copy_from",
7915 ))
7916 }
7917 QueryExpr::MaintenanceCommand(ref cmd) => {
7933 use crate::storage::query::ast::MaintenanceCommand as Mc;
7934 let store = self.inner.db.store();
7935 let (kind, msg) = match cmd {
7936 Mc::Analyze { target } => {
7937 let targets: Vec<String> = match target {
7938 Some(t) => vec![t.clone()],
7939 None => store.list_collections(),
7940 };
7941 for t in &targets {
7942 self.refresh_table_planner_stats(t);
7943 }
7944 (
7945 "analyze",
7946 format!("ANALYZE refreshed stats for {} table(s)", targets.len()),
7947 )
7948 }
7949 Mc::Vacuum { target, full } => {
7950 let targets: Vec<String> = match target {
7951 Some(t) => vec![t.clone()],
7952 None => store.list_collections(),
7953 };
7954 let cutoff_xid = self.mvcc_vacuum_cutoff_xid();
7955 let mut vacuum_stats =
7956 crate::storage::unified::store::MvccVacuumStats::default();
7957 for t in &targets {
7958 let stats = store.vacuum_mvcc_history(t, cutoff_xid).map_err(|e| {
7959 RedDBError::Internal(format!(
7960 "VACUUM MVCC history failed for {t}: {e}"
7961 ))
7962 })?;
7963 if stats.reclaimed_versions > 0 {
7964 self.rebuild_runtime_indexes_for_table(t)?;
7965 }
7966 vacuum_stats.add(&stats);
7967 }
7968 self.inner.snapshot_manager.prune_aborted(cutoff_xid);
7969 for t in &targets {
7971 self.refresh_table_planner_stats(t);
7972 }
7973 let persisted = if *full {
7977 match store.persist() {
7978 Ok(()) => true,
7979 Err(e) => {
7980 return Err(RedDBError::Internal(format!(
7981 "VACUUM FULL persist failed: {e:?}"
7982 )));
7983 }
7984 }
7985 } else {
7986 false
7987 };
7988 self.invalidate_result_cache();
7990 (
7991 "vacuum",
7992 format!(
7993 "VACUUM{} processed {} table(s): scanned_versions={}, retained_versions={}, reclaimed_versions={}, retained_history_versions={}, reclaimed_history_versions={}, retained_tombstones={}, reclaimed_tombstones={}{}",
7994 if *full { " FULL" } else { "" },
7995 targets.len(),
7996 vacuum_stats.scanned_versions,
7997 vacuum_stats.retained_versions,
7998 vacuum_stats.reclaimed_versions,
7999 vacuum_stats.retained_history_versions,
8000 vacuum_stats.reclaimed_history_versions,
8001 vacuum_stats.retained_tombstones,
8002 vacuum_stats.reclaimed_tombstones,
8003 if persisted {
8004 " (pages flushed to disk)"
8005 } else {
8006 ""
8007 }
8008 ),
8009 )
8010 }
8011 };
8012 Ok(RuntimeQueryResult::ok_message(
8013 query.to_string(),
8014 &msg,
8015 kind,
8016 ))
8017 }
8018 QueryExpr::Grant(ref g) => self.execute_grant_statement(query, g),
8025 QueryExpr::Revoke(ref r) => self.execute_revoke_statement(query, r),
8026 QueryExpr::AlterUser(ref a) => self.execute_alter_user_statement(query, a),
8027 QueryExpr::CreateIamPolicy { ref id, ref json } => {
8028 self.execute_create_iam_policy(query, id, json)
8029 }
8030 QueryExpr::DropIamPolicy { ref id } => self.execute_drop_iam_policy(query, id),
8031 QueryExpr::AttachPolicy {
8032 ref policy_id,
8033 ref principal,
8034 } => self.execute_attach_policy(query, policy_id, principal),
8035 QueryExpr::DetachPolicy {
8036 ref policy_id,
8037 ref principal,
8038 } => self.execute_detach_policy(query, policy_id, principal),
8039 QueryExpr::ShowPolicies { ref filter } => {
8040 self.execute_show_policies(query, filter.as_ref())
8041 }
8042 QueryExpr::ShowEffectivePermissions {
8043 ref user,
8044 ref resource,
8045 } => self.execute_show_effective_permissions(query, user, resource.as_ref()),
8046 QueryExpr::SimulatePolicy {
8047 ref user,
8048 ref action,
8049 ref resource,
8050 } => self.execute_simulate_policy(query, user, action, resource),
8051 QueryExpr::LintPolicy { ref source } => self.execute_lint_policy(query, source),
8052 QueryExpr::MigratePolicyMode {
8053 ref target,
8054 dry_run,
8055 } => self.execute_migrate_policy_mode(query, target, dry_run),
8056 QueryExpr::CreateMigration(ref q) => self.execute_create_migration(query, q),
8057 QueryExpr::ApplyMigration(ref q) => self.execute_apply_migration(query, q),
8058 QueryExpr::RollbackMigration(ref q) => self.execute_rollback_migration(query, q),
8059 QueryExpr::ExplainMigration(ref q) => self.execute_explain_migration(query, q),
8060 };
8061
8062 if !control_event_specs.is_empty() {
8063 let (outcome, reason) = match &query_result {
8064 Ok(_) => (crate::runtime::control_events::Outcome::Allowed, None),
8065 Err(err) => (control_event_outcome_for_error(err), Some(err.to_string())),
8066 };
8067 for spec in &control_event_specs {
8068 self.emit_control_event(
8069 spec.kind,
8070 outcome,
8071 spec.action,
8072 spec.resource.clone(),
8073 reason.clone(),
8074 spec.fields.clone(),
8075 )?;
8076 }
8077 }
8078
8079 if let (Some(plan), Ok(result)) = (&query_audit_plan, &query_result) {
8080 self.emit_query_audit(
8081 query,
8082 plan,
8083 query_audit_started.elapsed().as_millis() as u64,
8084 result,
8085 );
8086 }
8087
8088 let mut query_result = query_result;
8092 if let Ok(ref mut result) = query_result {
8093 if result.statement_type == "select" {
8094 self.apply_secret_decryption(result);
8095 }
8096 }
8097
8098 if let Ok(ref result) = query_result {
8105 frame.write_result_cache(self, result, result_cache_scopes);
8106 }
8107
8108 query_result
8109 }
8110
8111 pub fn materialized_view_metadata(
8115 &self,
8116 ) -> Vec<crate::storage::cache::result::MaterializedViewMetadata> {
8117 let store = self.inner.db.store();
8124 let mut entries = self.inner.materialized_views.read().metadata();
8125 for entry in &mut entries {
8126 if let Some(manager) = store.get_collection(&entry.name) {
8127 entry.current_row_count = manager.count() as u64;
8128 }
8129 }
8130 entries
8131 }
8132
8133 pub(crate) fn retention_sweeper_snapshot(
8144 &self,
8145 ) -> Vec<(String, crate::runtime::retention_sweeper::SweeperState)> {
8146 self.inner.retention_sweeper.read().snapshot()
8147 }
8148
8149 pub fn sweep_retention_tick(&self, batch_size: usize) {
8171 if batch_size == 0 {
8172 return;
8173 }
8174 let now_ms = std::time::SystemTime::now()
8175 .duration_since(std::time::UNIX_EPOCH)
8176 .map(|d| d.as_millis() as u64)
8177 .unwrap_or(0);
8178
8179 let store = self.inner.db.store();
8180 let collections = store.list_collections();
8181 for name in collections {
8182 let Some(contract) = self.inner.db.collection_contract(&name) else {
8183 continue;
8184 };
8185 let Some(retention_ms) = contract.retention_duration_ms else {
8186 continue;
8187 };
8188 let Some(ts_column) =
8189 crate::runtime::retention_filter::resolve_timestamp_column(&contract)
8190 else {
8191 continue;
8192 };
8193 let Some(manager) = store.get_collection(&name) else {
8194 continue;
8195 };
8196 let cutoff = (now_ms as i64).saturating_sub(retention_ms as i64);
8197
8198 let mut expired_ts: Vec<i64> = Vec::new();
8206 manager.for_each_entity(|entity| {
8207 let ts = match ts_column.as_str() {
8208 "created_at" => Some(entity.created_at as i64),
8209 "updated_at" => Some(entity.updated_at as i64),
8210 other => entity
8211 .data
8212 .as_row()
8213 .and_then(|row| row.get_field(other))
8214 .and_then(|v| match v {
8215 crate::storage::schema::Value::TimestampMs(t) => Some(*t),
8216 crate::storage::schema::Value::Timestamp(t) => {
8217 Some(t.saturating_mul(1_000))
8218 }
8219 crate::storage::schema::Value::BigInt(t) => Some(*t),
8220 crate::storage::schema::Value::UnsignedInteger(t) => {
8221 i64::try_from(*t).ok()
8222 }
8223 crate::storage::schema::Value::Integer(t) => Some(*t),
8224 _ => None,
8225 }),
8226 };
8227 if let Some(t) = ts {
8228 if t < cutoff {
8229 expired_ts.push(t);
8230 }
8231 }
8232 true
8233 });
8234
8235 let total_expired = expired_ts.len() as u64;
8236 if total_expired == 0 {
8237 self.inner
8238 .retention_sweeper
8239 .write()
8240 .record_tick(&name, 0, 0, now_ms);
8241 continue;
8242 }
8243
8244 let (effective_cutoff, pending) = if (total_expired as usize) <= batch_size {
8245 (cutoff, 0u64)
8246 } else {
8247 expired_ts.sort_unstable();
8251 let nth = expired_ts[batch_size - 1];
8252 (
8253 nth.saturating_add(1),
8254 total_expired.saturating_sub(batch_size as u64),
8255 )
8256 };
8257
8258 let stmt = format!(
8259 "DELETE FROM {} WHERE {} < {}",
8260 name, ts_column, effective_cutoff
8261 );
8262 let deleted = match self.execute_query(&stmt) {
8263 Ok(r) => r.affected_rows,
8264 Err(_) => 0,
8265 };
8266
8267 self.inner
8268 .retention_sweeper
8269 .write()
8270 .record_tick(&name, deleted, pending, now_ms);
8271 }
8272 }
8273
8274 pub fn refresh_due_materialized_views(&self) {
8275 let due = {
8276 let mut cache = self.inner.materialized_views.write();
8277 cache.claim_due_at(std::time::Instant::now())
8278 };
8279 for name in due {
8280 let stmt = format!("REFRESH MATERIALIZED VIEW {}", name);
8287 let _ = self.execute_query(&stmt);
8288 }
8289 }
8290
8291 pub fn execute_query_expr(&self, expr: QueryExpr) -> RedDBResult<RuntimeQueryResult> {
8297 let _config_snapshot_guard = ConfigSnapshotGuard::install(Arc::clone(&self.inner.db));
8298 let _secret_store_guard = SecretStoreGuard::install(self.inner.auth_store.read().clone());
8299 let expr = self.rewrite_view_refs(expr);
8303
8304 self.validate_model_operations_before_auth(&expr)?;
8305 if let Err(err) = self.check_query_privilege(&expr) {
8309 return Err(RedDBError::Query(format!("permission denied: {err}")));
8310 }
8311
8312 let statement = query_expr_name(&expr);
8313 let mode = detect_mode(statement);
8314 let query_str = statement;
8315
8316 let result = self.dispatch_expr(expr, query_str, mode)?;
8317 let mut r = result;
8318 if r.statement_type == "select" {
8319 self.apply_secret_decryption(&mut r);
8320 }
8321 Ok(r)
8322 }
8323
8324 pub(super) fn validate_model_operations_before_auth(
8325 &self,
8326 expr: &QueryExpr,
8327 ) -> RedDBResult<()> {
8328 use crate::catalog::CollectionModel;
8329 use crate::runtime::ddl::polymorphic_resolver;
8330 use crate::storage::query::ast::KvCommand;
8331
8332 let system_schema_target = match expr {
8333 QueryExpr::DropTable(q) => Some(q.name.as_str()),
8334 QueryExpr::DropGraph(q) => Some(q.name.as_str()),
8335 QueryExpr::DropVector(q) => Some(q.name.as_str()),
8336 QueryExpr::DropDocument(q) => Some(q.name.as_str()),
8337 QueryExpr::DropKv(q) => Some(q.name.as_str()),
8338 QueryExpr::DropCollection(q) => Some(q.name.as_str()),
8339 QueryExpr::Truncate(q) => Some(q.name.as_str()),
8340 _ => None,
8341 };
8342 if system_schema_target.is_some_and(crate::runtime::impl_ddl::is_system_schema_name) {
8343 return Err(RedDBError::Query("system schema is read-only".to_string()));
8344 }
8345
8346 let expected = match expr {
8347 QueryExpr::DropTable(q) => Some((q.name.as_str(), CollectionModel::Table)),
8348 QueryExpr::DropGraph(q) => Some((q.name.as_str(), CollectionModel::Graph)),
8349 QueryExpr::DropVector(q) => Some((q.name.as_str(), CollectionModel::Vector)),
8350 QueryExpr::DropDocument(q) => Some((q.name.as_str(), CollectionModel::Document)),
8351 QueryExpr::DropKv(q) => Some((q.name.as_str(), q.model)),
8352 QueryExpr::DropCollection(q) => q.model.map(|model| (q.name.as_str(), model)),
8353 QueryExpr::Truncate(q) => q.model.map(|model| (q.name.as_str(), model)),
8354 QueryExpr::KvCommand(cmd) => {
8355 let (collection, model) = match cmd {
8356 KvCommand::Put {
8357 collection, model, ..
8358 }
8359 | KvCommand::Get {
8360 collection, model, ..
8361 }
8362 | KvCommand::Incr {
8363 collection, model, ..
8364 }
8365 | KvCommand::Cas {
8366 collection, model, ..
8367 }
8368 | KvCommand::Delete {
8369 collection, model, ..
8370 } => (collection.as_str(), *model),
8371 KvCommand::Rotate { collection, .. }
8372 | KvCommand::History { collection, .. }
8373 | KvCommand::List { collection, .. }
8374 | KvCommand::Purge { collection, .. } => {
8375 (collection.as_str(), CollectionModel::Vault)
8376 }
8377 KvCommand::InvalidateTags { collection, .. } => {
8378 (collection.as_str(), CollectionModel::Kv)
8379 }
8380 KvCommand::Watch {
8381 collection, model, ..
8382 } => (collection.as_str(), *model),
8383 KvCommand::Unseal { collection, .. } => {
8384 (collection.as_str(), CollectionModel::Vault)
8385 }
8386 };
8387 Some((collection, model))
8388 }
8389 QueryExpr::ConfigCommand(cmd) => {
8390 self.validate_config_command_before_auth(cmd)?;
8391 None
8392 }
8393 _ => None,
8394 };
8395
8396 let Some((name, expected_model)) = expected else {
8397 return Ok(());
8398 };
8399 let snapshot = self.inner.db.catalog_model_snapshot();
8400 let Some(actual_model) = snapshot
8401 .collections
8402 .iter()
8403 .find(|collection| collection.name == name)
8404 .map(|collection| collection.declared_model.unwrap_or(collection.model))
8405 else {
8406 return Ok(());
8407 };
8408 polymorphic_resolver::ensure_model_match(expected_model, actual_model)
8409 }
8410
8411 pub(super) fn rewrite_view_refs(&self, expr: QueryExpr) -> QueryExpr {
8416 if self.inner.views.read().is_empty() {
8418 return expr;
8419 }
8420 self.rewrite_view_refs_inner(expr)
8421 }
8422
8423 fn rewrite_view_refs_inner(&self, expr: QueryExpr) -> QueryExpr {
8424 use crate::storage::query::ast::{Filter, TableSource};
8425 match expr {
8426 QueryExpr::Table(mut tq) => {
8427 if let Some(TableSource::Subquery(body)) = tq.source.take() {
8433 tq.source = Some(TableSource::Subquery(Box::new(
8434 self.rewrite_view_refs_inner(*body),
8435 )));
8436 return QueryExpr::Table(tq);
8437 }
8438
8439 let maybe_view = {
8443 let views = self.inner.views.read();
8444 views.get(&tq.table).cloned()
8445 };
8446 let Some(view) = maybe_view else {
8447 return QueryExpr::Table(tq);
8448 };
8449
8450 if view.materialized {
8456 return QueryExpr::Table(tq);
8457 }
8458
8459 let inner_expr = self.rewrite_view_refs_inner((*view.query).clone());
8463
8464 match inner_expr {
8472 QueryExpr::Table(mut inner_tq) => {
8473 if let Some(outer_filter) = tq.filter.take() {
8474 inner_tq.filter = Some(match inner_tq.filter.take() {
8475 Some(existing) => {
8476 Filter::And(Box::new(existing), Box::new(outer_filter))
8477 }
8478 None => outer_filter,
8479 });
8480 inner_tq.where_expr = inner_tq
8488 .filter
8489 .as_ref()
8490 .map(crate::storage::query::sql_lowering::filter_to_expr);
8491 }
8492 if let Some(outer_limit) = tq.limit {
8493 inner_tq.limit = Some(match inner_tq.limit {
8494 Some(existing) => existing.min(outer_limit),
8495 None => outer_limit,
8496 });
8497 }
8498 if let Some(outer_offset) = tq.offset {
8499 inner_tq.offset = Some(match inner_tq.offset {
8500 Some(existing) => existing + outer_offset,
8501 None => outer_offset,
8502 });
8503 }
8504 QueryExpr::Table(inner_tq)
8505 }
8506 other => other,
8507 }
8508 }
8509 QueryExpr::Join(mut jq) => {
8510 jq.left = Box::new(self.rewrite_view_refs_inner(*jq.left));
8511 jq.right = Box::new(self.rewrite_view_refs_inner(*jq.right));
8512 QueryExpr::Join(jq)
8513 }
8514 other => other,
8517 }
8518 }
8519
8520 fn authorize_relational_table_select(
8524 &self,
8525 mut table: TableQuery,
8526 frame: &dyn super::statement_frame::ReadFrame,
8527 ) -> RedDBResult<Option<TableQuery>> {
8528 if let Some(TableSource::Subquery(inner)) = table.source.take() {
8529 let authorized_inner = self.authorize_relational_select_expr(*inner, frame)?;
8530 table.source = Some(TableSource::Subquery(Box::new(authorized_inner)));
8531 return Ok(Some(table));
8532 }
8533
8534 self.check_table_column_projection_authz(&table, frame)?;
8535
8536 if self.inner.rls_enabled_tables.read().contains(&table.table) {
8537 return Ok(inject_rls_filters(self, frame, table));
8538 }
8539
8540 Ok(Some(table))
8541 }
8542
8543 fn authorize_relational_join_select(
8544 &self,
8545 mut join: JoinQuery,
8546 frame: &dyn super::statement_frame::ReadFrame,
8547 ) -> RedDBResult<Option<JoinQuery>> {
8548 self.check_join_column_projection_authz(&join, frame)?;
8549 join.left = Box::new(self.authorize_relational_join_child(*join.left, frame)?);
8550 join.right = Box::new(self.authorize_relational_join_child(*join.right, frame)?);
8551 Ok(inject_rls_into_join(self, frame, join))
8552 }
8553
8554 fn authorize_relational_join_child(
8555 &self,
8556 expr: QueryExpr,
8557 frame: &dyn super::statement_frame::ReadFrame,
8558 ) -> RedDBResult<QueryExpr> {
8559 match expr {
8560 QueryExpr::Table(mut table) => {
8561 if let Some(TableSource::Subquery(inner)) = table.source.take() {
8562 let authorized_inner = self.authorize_relational_select_expr(*inner, frame)?;
8563 table.source = Some(TableSource::Subquery(Box::new(authorized_inner)));
8564 }
8565 Ok(QueryExpr::Table(table))
8566 }
8567 QueryExpr::Join(join) => self
8568 .authorize_relational_join_select(join, frame)?
8569 .map(QueryExpr::Join)
8570 .ok_or_else(|| {
8571 RedDBError::Query("permission denied: RLS denied relational subquery".into())
8572 }),
8573 other => Ok(other),
8574 }
8575 }
8576
8577 fn authorize_relational_select_expr(
8578 &self,
8579 expr: QueryExpr,
8580 frame: &dyn super::statement_frame::ReadFrame,
8581 ) -> RedDBResult<QueryExpr> {
8582 match expr {
8583 QueryExpr::Table(table) => self
8584 .authorize_relational_table_select(table, frame)?
8585 .map(QueryExpr::Table)
8586 .ok_or_else(|| {
8587 RedDBError::Query("permission denied: RLS denied relational subquery".into())
8588 }),
8589 QueryExpr::Join(join) => self
8590 .authorize_relational_join_select(join, frame)?
8591 .map(QueryExpr::Join)
8592 .ok_or_else(|| {
8593 RedDBError::Query("permission denied: RLS denied relational subquery".into())
8594 }),
8595 other => Ok(other),
8596 }
8597 }
8598
8599 fn check_table_column_projection_authz(
8600 &self,
8601 table: &TableQuery,
8602 frame: &dyn super::statement_frame::ReadFrame,
8603 ) -> RedDBResult<()> {
8604 let Some((username, role)) = frame.identity() else {
8605 return Ok(());
8606 };
8607 let Some(auth_store) = self.inner.auth_store.read().clone() else {
8608 return Ok(());
8609 };
8610
8611 let columns = self.resolved_table_projection_columns(table)?;
8612 let request = ColumnAccessRequest::select(table.table.clone(), columns);
8613 let principal = UserId::from_parts(frame.effective_scope(), username);
8614 let ctx = runtime_iam_context(
8615 role,
8616 frame.effective_scope(),
8617 auth_store.principal_is_system_owned(&principal),
8618 );
8619 let outcome = auth_store.check_column_projection_authz(&principal, &request, &ctx);
8620 if outcome.allowed() {
8621 return Ok(());
8622 }
8623
8624 if let Some(denied) = outcome.first_denied_column() {
8625 return Err(RedDBError::Query(format!(
8626 "permission denied: principal=`{username}` cannot select column `{}`",
8627 denied.resource.name
8628 )));
8629 }
8630 Err(RedDBError::Query(format!(
8631 "permission denied: principal=`{username}` cannot select table `{}`",
8632 table.table
8633 )))
8634 }
8635
8636 fn check_join_column_projection_authz(
8637 &self,
8638 join: &JoinQuery,
8639 frame: &dyn super::statement_frame::ReadFrame,
8640 ) -> RedDBResult<()> {
8641 let mut by_table: HashMap<String, BTreeSet<String>> = HashMap::new();
8642 let projections = crate::storage::query::sql_lowering::effective_join_projections(join);
8643 self.collect_join_projection_columns(join, &projections, &mut by_table)?;
8644
8645 for (table, columns) in by_table {
8646 let query = TableQuery {
8647 table,
8648 source: None,
8649 alias: None,
8650 select_items: Vec::new(),
8651 columns: columns.into_iter().map(Projection::Column).collect(),
8652 where_expr: None,
8653 filter: None,
8654 group_by_exprs: Vec::new(),
8655 group_by: Vec::new(),
8656 having_expr: None,
8657 having: None,
8658 order_by: Vec::new(),
8659 limit: None,
8660 limit_param: None,
8661 offset: None,
8662 offset_param: None,
8663 expand: None,
8664 as_of: None,
8665 sessionize: None,
8666 };
8667 self.check_table_column_projection_authz(&query, frame)?;
8668 }
8669 Ok(())
8670 }
8671
8672 fn collect_join_projection_columns(
8673 &self,
8674 join: &JoinQuery,
8675 projections: &[Projection],
8676 out: &mut HashMap<String, BTreeSet<String>>,
8677 ) -> RedDBResult<()> {
8678 let left = table_side_context(join.left.as_ref());
8679 let right = table_side_context(join.right.as_ref());
8680
8681 if projections
8682 .iter()
8683 .any(|projection| matches!(projection, Projection::All))
8684 {
8685 for side in [left.as_ref(), right.as_ref()].into_iter().flatten() {
8686 out.entry(side.table.clone())
8687 .or_default()
8688 .extend(self.table_all_projection_columns(&side.table)?);
8689 }
8690 return Ok(());
8691 }
8692
8693 for projection in projections {
8694 collect_projection_columns_for_join_side(
8695 projection,
8696 left.as_ref(),
8697 right.as_ref(),
8698 out,
8699 )?;
8700 }
8701 Ok(())
8702 }
8703
8704 fn resolved_table_projection_columns(&self, table: &TableQuery) -> RedDBResult<Vec<String>> {
8705 let projections = crate::storage::query::sql_lowering::effective_table_projections(table);
8706 if projections
8707 .iter()
8708 .any(|projection| matches!(projection, Projection::All))
8709 {
8710 return self.table_all_projection_columns(&table.table);
8711 }
8712
8713 let mut columns = BTreeSet::new();
8714 for projection in &projections {
8715 collect_projection_columns_for_table(
8716 projection,
8717 &table.table,
8718 table.alias.as_deref(),
8719 &mut columns,
8720 );
8721 }
8722 Ok(columns.into_iter().collect())
8723 }
8724
8725 fn table_all_projection_columns(&self, table: &str) -> RedDBResult<Vec<String>> {
8726 if let Some(contract) = self.inner.db.collection_contract_arc(table) {
8727 let columns: Vec<String> = contract
8728 .declared_columns
8729 .iter()
8730 .map(|column| column.name.clone())
8731 .collect();
8732 if !columns.is_empty() {
8733 return Ok(columns);
8734 }
8735 }
8736
8737 let records = scan_runtime_table_source_records_limited(&self.inner.db, table, Some(1))?;
8738 Ok(records
8739 .first()
8740 .map(|record| {
8741 record
8742 .column_names()
8743 .into_iter()
8744 .map(|column| column.to_string())
8745 .collect()
8746 })
8747 .unwrap_or_default())
8748 }
8749
8750 fn resolve_table_expr_subqueries(
8751 &self,
8752 mut table: TableQuery,
8753 frame: &dyn super::statement_frame::ReadFrame,
8754 ) -> RedDBResult<TableQuery> {
8755 match table.source.take() {
8762 Some(TableSource::Subquery(inner)) => {
8763 let inner = self.resolve_select_expr_subqueries(*inner, frame)?;
8764 table.source = Some(TableSource::Subquery(Box::new(inner)));
8765 }
8766 other => table.source = other,
8767 }
8768
8769 let outer_scopes = relation_scopes_for_query(&QueryExpr::Table(table.clone()));
8770 for item in &mut table.select_items {
8771 if let crate::storage::query::ast::SelectItem::Expr { expr, .. } = item {
8772 *expr = self.resolve_expr_subqueries(expr.clone(), &outer_scopes, frame)?;
8773 }
8774 }
8775 if let Some(where_expr) = table.where_expr.take() {
8776 table.where_expr =
8777 Some(self.resolve_expr_subqueries(where_expr, &outer_scopes, frame)?);
8778 table.filter = None;
8779 }
8780 if let Some(having_expr) = table.having_expr.take() {
8781 table.having_expr =
8782 Some(self.resolve_expr_subqueries(having_expr, &outer_scopes, frame)?);
8783 table.having = None;
8784 }
8785 for expr in &mut table.group_by_exprs {
8786 *expr = self.resolve_expr_subqueries(expr.clone(), &outer_scopes, frame)?;
8787 }
8788 for clause in &mut table.order_by {
8789 if let Some(expr) = clause.expr.take() {
8790 clause.expr = Some(self.resolve_expr_subqueries(expr, &outer_scopes, frame)?);
8791 }
8792 }
8793 Ok(table)
8794 }
8795
8796 fn resolve_select_expr_subqueries(
8797 &self,
8798 expr: QueryExpr,
8799 frame: &dyn super::statement_frame::ReadFrame,
8800 ) -> RedDBResult<QueryExpr> {
8801 match expr {
8802 QueryExpr::Table(table) => self
8803 .resolve_table_expr_subqueries(table, frame)
8804 .map(QueryExpr::Table),
8805 QueryExpr::Join(mut join) => {
8806 join.left = Box::new(self.resolve_select_expr_subqueries(*join.left, frame)?);
8807 join.right = Box::new(self.resolve_select_expr_subqueries(*join.right, frame)?);
8808 Ok(QueryExpr::Join(join))
8809 }
8810 other => Ok(other),
8811 }
8812 }
8813
8814 fn resolve_expr_subqueries(
8815 &self,
8816 expr: crate::storage::query::ast::Expr,
8817 outer_scopes: &[String],
8818 frame: &dyn super::statement_frame::ReadFrame,
8819 ) -> RedDBResult<crate::storage::query::ast::Expr> {
8820 use crate::storage::query::ast::Expr;
8821
8822 match expr {
8823 Expr::Subquery { query, span } => {
8824 let values = self.execute_expr_subquery_values(query, outer_scopes, frame)?;
8825 if values.len() > 1 {
8826 return Err(RedDBError::Query(
8827 "scalar subquery returned more than one row".to_string(),
8828 ));
8829 }
8830 Ok(Expr::Literal {
8831 value: values.into_iter().next().unwrap_or(Value::Null),
8832 span,
8833 })
8834 }
8835 Expr::BinaryOp { op, lhs, rhs, span } => Ok(Expr::BinaryOp {
8836 op,
8837 lhs: Box::new(self.resolve_expr_subqueries(*lhs, outer_scopes, frame)?),
8838 rhs: Box::new(self.resolve_expr_subqueries(*rhs, outer_scopes, frame)?),
8839 span,
8840 }),
8841 Expr::UnaryOp { op, operand, span } => Ok(Expr::UnaryOp {
8842 op,
8843 operand: Box::new(self.resolve_expr_subqueries(*operand, outer_scopes, frame)?),
8844 span,
8845 }),
8846 Expr::Cast {
8847 inner,
8848 target,
8849 span,
8850 } => Ok(Expr::Cast {
8851 inner: Box::new(self.resolve_expr_subqueries(*inner, outer_scopes, frame)?),
8852 target,
8853 span,
8854 }),
8855 Expr::FunctionCall { name, args, span } => {
8856 let args = args
8857 .into_iter()
8858 .map(|arg| self.resolve_expr_subqueries(arg, outer_scopes, frame))
8859 .collect::<RedDBResult<Vec<_>>>()?;
8860 Ok(Expr::FunctionCall { name, args, span })
8861 }
8862 Expr::Case {
8863 branches,
8864 else_,
8865 span,
8866 } => {
8867 let branches = branches
8868 .into_iter()
8869 .map(|(cond, value)| {
8870 Ok((
8871 self.resolve_expr_subqueries(cond, outer_scopes, frame)?,
8872 self.resolve_expr_subqueries(value, outer_scopes, frame)?,
8873 ))
8874 })
8875 .collect::<RedDBResult<Vec<_>>>()?;
8876 let else_ = else_
8877 .map(|expr| self.resolve_expr_subqueries(*expr, outer_scopes, frame))
8878 .transpose()?
8879 .map(Box::new);
8880 Ok(Expr::Case {
8881 branches,
8882 else_,
8883 span,
8884 })
8885 }
8886 Expr::IsNull {
8887 operand,
8888 negated,
8889 span,
8890 } => Ok(Expr::IsNull {
8891 operand: Box::new(self.resolve_expr_subqueries(*operand, outer_scopes, frame)?),
8892 negated,
8893 span,
8894 }),
8895 Expr::InList {
8896 target,
8897 values,
8898 negated,
8899 span,
8900 } => {
8901 let target =
8902 Box::new(self.resolve_expr_subqueries(*target, outer_scopes, frame)?);
8903 let mut resolved = Vec::new();
8904 for value in values {
8905 if let Expr::Subquery { query, .. } = value {
8906 resolved.extend(
8907 self.execute_expr_subquery_values(query, outer_scopes, frame)?
8908 .into_iter()
8909 .map(Expr::lit),
8910 );
8911 } else {
8912 resolved.push(self.resolve_expr_subqueries(value, outer_scopes, frame)?);
8913 }
8914 }
8915 Ok(Expr::InList {
8916 target,
8917 values: resolved,
8918 negated,
8919 span,
8920 })
8921 }
8922 Expr::Between {
8923 target,
8924 low,
8925 high,
8926 negated,
8927 span,
8928 } => Ok(Expr::Between {
8929 target: Box::new(self.resolve_expr_subqueries(*target, outer_scopes, frame)?),
8930 low: Box::new(self.resolve_expr_subqueries(*low, outer_scopes, frame)?),
8931 high: Box::new(self.resolve_expr_subqueries(*high, outer_scopes, frame)?),
8932 negated,
8933 span,
8934 }),
8935 other => Ok(other),
8936 }
8937 }
8938
8939 fn execute_expr_subquery_values(
8940 &self,
8941 subquery: crate::storage::query::ast::ExprSubquery,
8942 outer_scopes: &[String],
8943 frame: &dyn super::statement_frame::ReadFrame,
8944 ) -> RedDBResult<Vec<Value>> {
8945 let query = *subquery.query;
8946 if query_references_outer_scope(&query, outer_scopes) {
8947 return Err(RedDBError::Query(
8948 "NOT_YET_SUPPORTED: correlated subqueries are not supported yet; track follow-up issue #470-correlated-subqueries".to_string(),
8949 ));
8950 }
8951 let query = self.rewrite_view_refs(query);
8952 let query = self.resolve_select_expr_subqueries(query, frame)?;
8953 let query = self.authorize_relational_select_expr(query, frame)?;
8954 let result = match query {
8955 QueryExpr::Table(table) => {
8956 execute_runtime_table_query(&self.inner.db, &table, Some(&self.inner.index_store))?
8957 }
8958 QueryExpr::Join(join) => execute_runtime_join_query(&self.inner.db, &join)?,
8959 other => {
8960 return Err(RedDBError::Query(format!(
8961 "expression subquery must be a SELECT query, got {}",
8962 query_expr_name(&other)
8963 )))
8964 }
8965 };
8966 first_column_values(result)
8967 }
8968
8969 fn dispatch_expr(
8970 &self,
8971 expr: QueryExpr,
8972 query_str: &str,
8973 mode: QueryMode,
8974 ) -> RedDBResult<RuntimeQueryResult> {
8975 let statement = query_expr_name(&expr);
8976 match expr {
8977 QueryExpr::Graph(_) | QueryExpr::Path(_) => {
8978 Err(RedDBError::Query(
8980 "graph queries cannot be used as prepared statements".to_string(),
8981 ))
8982 }
8983 QueryExpr::Table(table) => {
8984 let scope = self.ai_scope();
8985 let table = self.resolve_table_expr_subqueries(
8986 table,
8987 &scope as &dyn super::statement_frame::ReadFrame,
8988 )?;
8989 if let Some(TableSource::Function {
8993 name,
8994 args,
8995 named_args,
8996 }) = table.source.clone()
8997 {
8998 return Ok(RuntimeQueryResult {
8999 query: query_str.to_string(),
9000 mode,
9001 statement,
9002 engine: "runtime-graph-tvf",
9003 result: self.execute_table_function(&name, &args, &named_args)?,
9004 affected_rows: 0,
9005 statement_type: "select",
9006 bookmark: None,
9007 });
9008 }
9009 if let Some(TableSource::InlineGraphFunction {
9013 name,
9014 nodes,
9015 edges,
9016 named_args,
9017 }) = table.source.clone()
9018 {
9019 return Ok(RuntimeQueryResult {
9020 query: query_str.to_string(),
9021 mode,
9022 statement,
9023 engine: "runtime-graph-tvf-inline",
9024 result: self.execute_inline_graph_function(
9025 &name,
9026 &nodes,
9027 &edges,
9028 &named_args,
9029 )?,
9030 affected_rows: 0,
9031 statement_type: "select",
9032 bookmark: None,
9033 });
9034 }
9035 if super::red_schema::is_virtual_table(&table.table) {
9036 return Ok(RuntimeQueryResult {
9037 query: query_str.to_string(),
9038 mode,
9039 statement,
9040 engine: "runtime-red-schema",
9041 result: super::red_schema::red_query(
9042 self,
9043 &table.table,
9044 &table,
9045 &scope as &dyn super::statement_frame::ReadFrame,
9046 )?,
9047 affected_rows: 0,
9048 statement_type: "select",
9049 bookmark: None,
9050 });
9051 }
9052 if let Some(view_result) = self.try_resolve_analytics_view(
9054 &table,
9055 &scope as &dyn super::statement_frame::ReadFrame,
9056 )? {
9057 return Ok(RuntimeQueryResult {
9058 query: query_str.to_string(),
9059 mode,
9060 statement,
9061 engine: "runtime-graph-analytics-view",
9062 result: view_result,
9063 affected_rows: 0,
9064 statement_type: "select",
9065 bookmark: None,
9066 });
9067 }
9068 let Some(table_with_rls) = self.authorize_relational_table_select(
9069 table,
9070 &scope as &dyn super::statement_frame::ReadFrame,
9071 )?
9072 else {
9073 return Ok(RuntimeQueryResult {
9074 query: query_str.to_string(),
9075 mode,
9076 statement,
9077 engine: "runtime-table-rls",
9078 result: crate::storage::query::unified::UnifiedResult::empty(),
9079 affected_rows: 0,
9080 statement_type: "select",
9081 bookmark: None,
9082 });
9083 };
9084 Ok(RuntimeQueryResult {
9085 query: query_str.to_string(),
9086 mode,
9087 statement,
9088 engine: "runtime-table",
9089 result: execute_runtime_table_query(
9090 &self.inner.db,
9091 &table_with_rls,
9092 Some(&self.inner.index_store),
9093 )?,
9094 affected_rows: 0,
9095 statement_type: "select",
9096 bookmark: None,
9097 })
9098 }
9099 QueryExpr::Join(join) => {
9100 let scope = self.ai_scope();
9101 let Some(join_with_rls) = self.authorize_relational_join_select(
9102 join,
9103 &scope as &dyn super::statement_frame::ReadFrame,
9104 )?
9105 else {
9106 return Ok(RuntimeQueryResult {
9107 query: query_str.to_string(),
9108 mode,
9109 statement,
9110 engine: "runtime-join-rls",
9111 result: crate::storage::query::unified::UnifiedResult::empty(),
9112 affected_rows: 0,
9113 statement_type: "select",
9114 bookmark: None,
9115 });
9116 };
9117 Ok(RuntimeQueryResult {
9118 query: query_str.to_string(),
9119 mode,
9120 statement,
9121 engine: "runtime-join",
9122 result: execute_runtime_join_query(&self.inner.db, &join_with_rls)?,
9123 affected_rows: 0,
9124 statement_type: "select",
9125 bookmark: None,
9126 })
9127 }
9128 QueryExpr::Vector(vector) => Ok(RuntimeQueryResult {
9129 query: query_str.to_string(),
9130 mode,
9131 statement,
9132 engine: "runtime-vector",
9133 result: execute_runtime_vector_query(&self.inner.db, &vector)?,
9134 affected_rows: 0,
9135 statement_type: "select",
9136 bookmark: None,
9137 }),
9138 QueryExpr::Hybrid(hybrid) => Ok(RuntimeQueryResult {
9139 query: query_str.to_string(),
9140 mode,
9141 statement,
9142 engine: "runtime-hybrid",
9143 result: execute_runtime_hybrid_query(&self.inner.db, &hybrid)?,
9144 affected_rows: 0,
9145 statement_type: "select",
9146 bookmark: None,
9147 }),
9148 QueryExpr::Insert(ref insert) if super::red_schema::is_virtual_table(&insert.table) => {
9149 Err(RedDBError::Query(
9150 super::red_schema::READ_ONLY_ERROR.to_string(),
9151 ))
9152 }
9153 QueryExpr::Update(ref update) if super::red_schema::is_virtual_table(&update.table) => {
9154 Err(RedDBError::Query(
9155 super::red_schema::READ_ONLY_ERROR.to_string(),
9156 ))
9157 }
9158 QueryExpr::Delete(ref delete) if super::red_schema::is_virtual_table(&delete.table) => {
9159 Err(RedDBError::Query(
9160 super::red_schema::READ_ONLY_ERROR.to_string(),
9161 ))
9162 }
9163 QueryExpr::Insert(ref insert) => self
9164 .with_deferred_store_wal_for_dml(self.insert_may_emit_events(insert), || {
9165 self.execute_insert(query_str, insert)
9166 }),
9167 QueryExpr::Update(ref update) => self
9168 .with_deferred_store_wal_for_dml(self.update_may_emit_events(update), || {
9169 self.execute_update(query_str, update)
9170 }),
9171 QueryExpr::Delete(ref delete) => self
9172 .with_deferred_store_wal_for_dml(self.delete_may_emit_events(delete), || {
9173 self.execute_delete(query_str, delete)
9174 }),
9175 QueryExpr::SearchCommand(ref cmd) => self.execute_search_command(query_str, cmd),
9176 QueryExpr::Ask(ref ask) => self.execute_ask(query_str, ask),
9177 _ => Err(RedDBError::Query(format!(
9178 "prepared-statement execution does not support {statement} statements"
9179 ))),
9180 }
9181 }
9182
9183 fn execute_table_function(
9190 &self,
9191 name: &str,
9192 args: &[String],
9193 named_args: &[(String, f64)],
9194 ) -> RedDBResult<crate::storage::query::unified::UnifiedResult> {
9195 if !is_graph_tvf_name(name) {
9196 return Err(RedDBError::Query(format!("unknown table function: {name}")));
9197 }
9198 if args.len() != 1 {
9200 return Err(RedDBError::Query(format!(
9201 "table function '{name}' takes exactly 1 graph argument, got {}",
9202 args.len()
9203 )));
9204 }
9205
9206 let (nodes, edges) = self.materialize_whole_graph_abstract()?;
9211 self.dispatch_graph_algorithm(name, nodes, edges, named_args)
9212 }
9213
9214 fn execute_inline_graph_function(
9224 &self,
9225 name: &str,
9226 nodes_query: &QueryExpr,
9227 edges_query: &QueryExpr,
9228 named_args: &[(String, f64)],
9229 ) -> RedDBResult<crate::storage::query::unified::UnifiedResult> {
9230 if !is_graph_tvf_name(name) {
9231 return Err(RedDBError::Query(format!("unknown table function: {name}")));
9232 }
9233
9234 let node_result = self.execute_query_expr(nodes_query.clone())?.result;
9235 let nodes = inline_node_ids(name, &node_result)?;
9236
9237 let edge_result = self.execute_query_expr(edges_query.clone())?.result;
9238 let edges = inline_edges(name, &edge_result)?;
9239
9240 self.dispatch_graph_algorithm(name, nodes, edges, named_args)
9241 }
9242
9243 fn materialize_whole_graph_abstract(
9246 &self,
9247 ) -> RedDBResult<(
9248 Vec<String>,
9249 Vec<(
9250 String,
9251 String,
9252 crate::storage::engine::graph_algorithms::Weight,
9253 )>,
9254 )> {
9255 use crate::storage::engine::graph_algorithms;
9256
9257 let graph = super::graph_dsl::materialize_graph_with_projection(
9258 self.inner.db.store().as_ref(),
9259 None,
9260 )?;
9261 let nodes: Vec<String> = graph.iter_nodes().map(|n| n.id.clone()).collect();
9262 let edges: Vec<(String, String, graph_algorithms::Weight)> = graph
9263 .iter_all_edges()
9264 .into_iter()
9265 .map(|e| (e.source_id, e.target_id, e.weight))
9266 .collect();
9267 Ok((nodes, edges))
9268 }
9269
9270 fn try_resolve_analytics_view(
9285 &self,
9286 table: &TableQuery,
9287 frame: &dyn super::statement_frame::ReadFrame,
9288 ) -> RedDBResult<Option<crate::storage::query::unified::UnifiedResult>> {
9289 let full = table.table.as_str();
9290 let Some(dot) = full.rfind('.') else {
9291 return Ok(None);
9292 };
9293 if self.inner.db.store().get_collection(full).is_some() {
9295 return Ok(None);
9296 }
9297 let graph_name = &full[..dot];
9298 let output_name = &full[dot + 1..];
9299 let Some(output) = crate::catalog::AnalyticsOutput::from_str(output_name) else {
9300 return Ok(None);
9301 };
9302
9303 let contracts = self.inner.db.collection_contracts();
9304 let Some(contract) = contracts.iter().find(|c| c.name == graph_name) else {
9305 return Ok(None);
9306 };
9307 if contract.declared_model != crate::catalog::CollectionModel::Graph {
9308 return Ok(None);
9309 }
9310 let Some(view) = contract
9311 .analytics_config
9312 .iter()
9313 .find(|view| view.output == output)
9314 else {
9315 return Err(RedDBError::Query(format!(
9318 "analytics output '{output_name}' is not enabled on graph '{graph_name}'; declare it with WITH ANALYTICS (...)"
9319 )));
9320 };
9321
9322 let parent_query = TableQuery::new(graph_name);
9326 if self
9327 .authorize_relational_table_select(parent_query, frame)?
9328 .is_none()
9329 {
9330 return Err(RedDBError::Query(format!(
9331 "permission denied: policy on graph '{graph_name}' denies analytics view '{output_name}'"
9332 )));
9333 }
9334
9335 let (algorithm, named_args) = analytics_view_algorithm(graph_name, view)?;
9336 let (nodes, edges) = self.materialize_whole_graph_abstract()?;
9337 let result = self.dispatch_graph_algorithm(&algorithm, nodes, edges, &named_args)?;
9338 Ok(Some(result))
9339 }
9340
9341 fn dispatch_graph_algorithm(
9348 &self,
9349 name: &str,
9350 nodes: Vec<String>,
9351 edges: Vec<(
9352 String,
9353 String,
9354 crate::storage::engine::graph_algorithms::Weight,
9355 )>,
9356 named_args: &[(String, f64)],
9357 ) -> RedDBResult<crate::storage::query::unified::UnifiedResult> {
9358 use crate::storage::engine::graph_algorithms;
9359 use crate::storage::query::unified::UnifiedResult;
9360 use crate::storage::schema::Value;
9361
9362 if name.eq_ignore_ascii_case("components") {
9363 reject_named_args(name, named_args)?;
9364 let assignment = graph_algorithms::connected_components(&nodes, &edges);
9365 let mut result =
9366 UnifiedResult::with_columns(vec!["node_id".into(), "island_id".into()]);
9367 for (node_id, island_id) in assignment {
9368 let mut record = UnifiedRecord::new();
9369 record.set("node_id", Value::text(node_id));
9370 record.set("island_id", Value::Integer(island_id as i64));
9371 result.push(record);
9372 }
9373 return Ok(result);
9374 }
9375
9376 if name.eq_ignore_ascii_case("louvain") {
9377 let resolution = louvain_resolution(named_args)?;
9382 let assignment = graph_algorithms::louvain(&nodes, &edges, resolution);
9383 let mut result =
9384 UnifiedResult::with_columns(vec!["node_id".into(), "community_id".into()]);
9385 for (node_id, community_id) in assignment {
9386 let mut record = UnifiedRecord::new();
9387 record.set("node_id", Value::text(node_id));
9388 record.set("community_id", Value::Integer(community_id as i64));
9389 result.push(record);
9390 }
9391 return Ok(result);
9392 }
9393
9394 if name.eq_ignore_ascii_case("degree_centrality") {
9395 reject_named_args(name, named_args)?;
9396 let assignment = abstract_degree_centrality(&nodes, &edges);
9397 let mut result = UnifiedResult::with_columns(vec!["node_id".into(), "degree".into()]);
9398 for (node_id, degree) in assignment {
9399 let mut record = UnifiedRecord::new();
9400 record.set("node_id", Value::text(node_id));
9401 record.set("degree", Value::Integer(degree as i64));
9402 result.push(record);
9403 }
9404 return Ok(result);
9405 }
9406
9407 if name.eq_ignore_ascii_case("shortest_path") {
9408 let mut src: Option<String> = None;
9414 let mut dst: Option<String> = None;
9415 let mut max_hops: Option<usize> = None;
9416 let as_node_id = |key: &str, value: f64| -> RedDBResult<String> {
9417 if !value.is_finite() || value < 0.0 || value.fract() != 0.0 {
9418 return Err(RedDBError::Query(format!(
9419 "table function 'shortest_path' argument '{key}' must be a non-negative integer node id, got {value}"
9420 )));
9421 }
9422 Ok((value as i64).to_string())
9423 };
9424 for (key, value) in named_args {
9425 if key.eq_ignore_ascii_case("src") {
9426 src = Some(as_node_id("src", *value)?);
9427 } else if key.eq_ignore_ascii_case("dst") {
9428 dst = Some(as_node_id("dst", *value)?);
9429 } else if key.eq_ignore_ascii_case("max_hops") {
9430 if !value.is_finite() || *value < 0.0 || value.fract() != 0.0 {
9431 return Err(RedDBError::Query(format!(
9432 "table function 'shortest_path' max_hops must be a non-negative integer, got {value}"
9433 )));
9434 }
9435 max_hops = Some(*value as usize);
9436 } else {
9437 return Err(RedDBError::Query(format!(
9438 "table function 'shortest_path' has no named argument '{key}' (expected 'src', 'dst', 'max_hops')"
9439 )));
9440 }
9441 }
9442 let src = src.ok_or_else(|| {
9443 RedDBError::Query(
9444 "table function 'shortest_path' requires named argument 'src'".to_string(),
9445 )
9446 })?;
9447 let dst = dst.ok_or_else(|| {
9448 RedDBError::Query(
9449 "table function 'shortest_path' requires named argument 'dst'".to_string(),
9450 )
9451 })?;
9452
9453 let mut result = UnifiedResult::with_columns(vec![
9460 "hop".into(),
9461 "node_id".into(),
9462 "cumulative_weight".into(),
9463 ]);
9464 if let Some(path) =
9465 graph_algorithms::shortest_path(&nodes, &edges, &src, &dst, max_hops)
9466 {
9467 for (hop, (node_id, cumulative_weight)) in path.into_iter().enumerate() {
9468 let mut record = UnifiedRecord::new();
9469 record.set("hop", Value::Integer(hop as i64));
9470 record.set("node_id", Value::text(node_id));
9471 record.set("cumulative_weight", Value::Float(cumulative_weight));
9472 result.push(record);
9473 }
9474 }
9475 return Ok(result);
9476 }
9477 if name.eq_ignore_ascii_case("betweenness") {
9482 reject_named_args(name, named_args)?;
9483 return Ok(Self::centrality_result(graph_algorithms::betweenness(
9484 &nodes, &edges,
9485 )));
9486 }
9487 if name.eq_ignore_ascii_case("eigenvector") {
9488 let mut max_iterations = 100_usize;
9491 let mut tolerance = 1e-6_f64;
9492 for (key, value) in named_args {
9493 if key.eq_ignore_ascii_case("max_iterations") {
9494 max_iterations = parse_positive_iterations("eigenvector", value)?;
9495 } else if key.eq_ignore_ascii_case("tolerance") {
9496 if !value.is_finite() || *value <= 0.0 {
9497 return Err(RedDBError::Query(format!(
9498 "table function 'eigenvector' tolerance must be > 0, got {value}"
9499 )));
9500 }
9501 tolerance = *value;
9502 } else {
9503 return Err(RedDBError::Query(format!(
9504 "table function 'eigenvector' has no named argument '{key}' (expected 'max_iterations' or 'tolerance')"
9505 )));
9506 }
9507 }
9508 return Ok(Self::centrality_result(graph_algorithms::eigenvector(
9509 &nodes,
9510 &edges,
9511 max_iterations,
9512 tolerance,
9513 )));
9514 }
9515 if name.eq_ignore_ascii_case("pagerank") {
9516 let mut damping = 0.85_f64;
9519 let mut max_iterations = 100_usize;
9520 for (key, value) in named_args {
9521 if key.eq_ignore_ascii_case("damping") {
9522 if !value.is_finite() || *value <= 0.0 || *value >= 1.0 {
9523 return Err(RedDBError::Query(format!(
9524 "table function 'pagerank' damping must be in (0, 1), got {value}"
9525 )));
9526 }
9527 damping = *value;
9528 } else if key.eq_ignore_ascii_case("max_iterations") {
9529 max_iterations = parse_positive_iterations("pagerank", value)?;
9530 } else {
9531 return Err(RedDBError::Query(format!(
9532 "table function 'pagerank' has no named argument '{key}' (expected 'damping' or 'max_iterations')"
9533 )));
9534 }
9535 }
9536 return Ok(Self::centrality_result(graph_algorithms::pagerank(
9537 &nodes,
9538 &edges,
9539 damping,
9540 max_iterations,
9541 )));
9542 }
9543 Err(RedDBError::Query(format!("unknown table function: {name}")))
9544 }
9545
9546 fn execute_components_tvf(
9553 &self,
9554 _collection: &str,
9555 ) -> RedDBResult<crate::storage::query::unified::UnifiedResult> {
9556 use crate::storage::engine::graph_algorithms;
9557 use crate::storage::query::unified::UnifiedResult;
9558 use crate::storage::schema::Value;
9559
9560 let graph = super::graph_dsl::materialize_graph_with_projection(
9566 self.inner.db.store().as_ref(),
9567 None,
9568 )?;
9569
9570 let nodes: Vec<String> = graph.iter_nodes().map(|n| n.id.clone()).collect();
9572 let edges: Vec<(String, String, graph_algorithms::Weight)> = graph
9573 .iter_all_edges()
9574 .into_iter()
9575 .map(|e| (e.source_id, e.target_id, e.weight))
9576 .collect();
9577
9578 let assignment = graph_algorithms::connected_components(&nodes, &edges);
9579
9580 let mut result = UnifiedResult::with_columns(vec!["node_id".into(), "island_id".into()]);
9582 for (node_id, island_id) in assignment {
9583 let mut record = UnifiedRecord::new();
9584 record.set("node_id", Value::text(node_id));
9585 record.set("island_id", Value::Integer(island_id as i64));
9586 result.push(record);
9587 }
9588 Ok(result)
9589 }
9590
9591 fn execute_louvain_tvf(
9601 &self,
9602 _collection: &str,
9603 resolution: f64,
9604 ) -> RedDBResult<crate::storage::query::unified::UnifiedResult> {
9605 use crate::storage::engine::graph_algorithms;
9606 use crate::storage::query::unified::UnifiedResult;
9607 use crate::storage::schema::Value;
9608
9609 let graph = super::graph_dsl::materialize_graph_with_projection(
9610 self.inner.db.store().as_ref(),
9611 None,
9612 )?;
9613
9614 let nodes: Vec<String> = graph.iter_nodes().map(|n| n.id.clone()).collect();
9615 let edges: Vec<(String, String, graph_algorithms::Weight)> = graph
9616 .iter_all_edges()
9617 .into_iter()
9618 .map(|e| (e.source_id, e.target_id, e.weight))
9619 .collect();
9620
9621 let assignment = graph_algorithms::louvain(&nodes, &edges, resolution);
9622
9623 let mut result = UnifiedResult::with_columns(vec!["node_id".into(), "community_id".into()]);
9625 for (node_id, community_id) in assignment {
9626 let mut record = UnifiedRecord::new();
9627 record.set("node_id", Value::text(node_id));
9628 record.set("community_id", Value::Integer(community_id as i64));
9629 result.push(record);
9630 }
9631 Ok(result)
9632 }
9633
9634 fn centrality_result(
9637 rows: Vec<(String, f64)>,
9638 ) -> crate::storage::query::unified::UnifiedResult {
9639 use crate::storage::query::unified::UnifiedResult;
9640 use crate::storage::schema::Value;
9641 let mut result = UnifiedResult::with_columns(vec!["node_id".into(), "score".into()]);
9642 for (node_id, score) in rows {
9643 let mut record = UnifiedRecord::new();
9644 record.set("node_id", Value::text(node_id));
9645 record.set("score", Value::Float(score));
9646 result.push(record);
9647 }
9648 result
9649 }
9650
9651 fn try_fast_entity_lookup(&self, query: &str) -> Option<RedDBResult<RuntimeQueryResult>> {
9654 let q = query.trim();
9657 if !q.starts_with("SELECT") && !q.starts_with("select") {
9658 return None;
9659 }
9660
9661 let where_pos = q
9663 .find("WHERE _entity_id")
9664 .or_else(|| q.find("where _entity_id"))?;
9665 let after_field = &q[where_pos + 16..].trim_start(); let after_eq = after_field.strip_prefix('=')?.trim_start();
9667
9668 let id_str = after_eq.trim();
9670 let entity_id: u64 = id_str.parse().ok()?;
9671
9672 let from_pos = q.find("FROM ").or_else(|| q.find("from "))? + 5;
9674 let table = q[from_pos..where_pos].trim();
9675 if table.is_empty()
9676 || table.contains(' ') && !table.contains(" AS ") && !table.contains(" as ")
9677 {
9678 return None; }
9680 let table_name = table.split_whitespace().next()?;
9681
9682 let store = self.inner.db.store();
9688 let entity = store
9689 .get(
9690 table_name,
9691 crate::storage::unified::EntityId::new(entity_id),
9692 )
9693 .filter(entity_visible_under_current_snapshot)
9694 .filter(|entity| {
9695 self.inner
9696 .db
9697 .replica_allows_entity_at_read(table_name, entity)
9698 });
9699
9700 let count = if entity.is_some() { 1u64 } else { 0 };
9701
9702 let records: Vec<crate::storage::query::unified::UnifiedRecord> = entity
9708 .as_ref()
9709 .and_then(|e| runtime_table_record_from_entity(e.clone()))
9710 .into_iter()
9711 .collect();
9712
9713 let json = match entity {
9714 Some(ref e) => execute_runtime_serialize_single_entity(e),
9715 None => r#"{"columns":[],"record_count":0,"selection":{"scope":"any"},"records":[]}"#
9716 .to_string(),
9717 };
9718
9719 Some(Ok(RuntimeQueryResult {
9720 query: query.to_string(),
9721 mode: crate::storage::query::modes::QueryMode::Sql,
9722 statement: "select",
9723 engine: "fast-entity-lookup",
9724 result: crate::storage::query::unified::UnifiedResult {
9725 columns: Vec::new(),
9726 records,
9727 stats: crate::storage::query::unified::QueryStats {
9728 rows_scanned: count,
9729 ..Default::default()
9730 },
9731 pre_serialized_json: Some(json),
9732 },
9733 affected_rows: 0,
9734 statement_type: "select",
9735 bookmark: None,
9736 }))
9737 }
9738
9739 fn result_cache_backend(&self) -> RuntimeResultCacheBackend {
9740 match self
9741 .config_string(RESULT_CACHE_BACKEND_KEY, RESULT_CACHE_DEFAULT_BACKEND)
9742 .as_str()
9743 {
9744 "blob_cache" => RuntimeResultCacheBackend::BlobCache,
9745 "shadow" => RuntimeResultCacheBackend::Shadow,
9746 _ => RuntimeResultCacheBackend::Legacy,
9747 }
9748 }
9749
9750 fn result_cache_enabled(&self) -> bool {
9754 self.config_bool(RESULT_CACHE_ENABLED_KEY, true)
9755 }
9756
9757 fn result_cache_ttl_secs(&self) -> u64 {
9760 self.config_u64(RESULT_CACHE_TTL_KEY, RESULT_CACHE_TTL_SECS)
9761 }
9762
9763 fn result_cache_capacity(&self) -> usize {
9767 self.config_u64(RESULT_CACHE_CAPACITY_KEY, RESULT_CACHE_MAX_ENTRIES as u64) as usize
9768 }
9769
9770 pub fn result_cache_metrics(&self) -> (u64, u64, u64) {
9773 use std::sync::atomic::Ordering::Relaxed;
9774 (
9775 self.inner.result_cache_hits.load(Relaxed),
9776 self.inner.result_cache_misses.load(Relaxed),
9777 self.inner.result_cache_evictions.load(Relaxed),
9778 )
9779 }
9780
9781 fn record_result_cache_evictions(&self, evicted: u64) {
9782 if evicted > 0 {
9783 self.inner
9784 .result_cache_evictions
9785 .fetch_add(evicted, std::sync::atomic::Ordering::Relaxed);
9786 }
9787 }
9788
9789 pub(super) fn get_result_cache_entry(&self, key: &str) -> Option<RuntimeQueryResult> {
9790 if !self.result_cache_enabled() {
9791 return None;
9792 }
9793 let hit = self.get_result_cache_entry_inner(key);
9794 let counter = if hit.is_some() {
9795 &self.inner.result_cache_hits
9796 } else {
9797 &self.inner.result_cache_misses
9798 };
9799 counter.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
9800 hit
9801 }
9802
9803 fn get_result_cache_entry_inner(&self, key: &str) -> Option<RuntimeQueryResult> {
9804 match self.result_cache_backend() {
9805 RuntimeResultCacheBackend::Legacy => self.get_legacy_result_cache_entry(key),
9806 RuntimeResultCacheBackend::BlobCache => self.get_blob_result_cache_entry(key),
9807 RuntimeResultCacheBackend::Shadow => {
9808 let legacy = self.get_legacy_result_cache_entry(key);
9809 let blob = self.get_blob_result_cache_entry(key);
9810 if let (Some(ref legacy), Some(ref blob)) = (&legacy, &blob) {
9811 if result_cache_fingerprint(legacy) != result_cache_fingerprint(blob) {
9812 self.inner
9813 .result_cache_shadow_divergences
9814 .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
9815 tracing::warn!(
9816 key,
9817 metric = crate::runtime::METRIC_CACHE_SHADOW_DIVERGENCE_TOTAL,
9818 "result cache shadow backend diverged from legacy"
9819 );
9820 }
9821 }
9822 legacy
9823 }
9824 }
9825 }
9826
9827 fn get_legacy_result_cache_entry(&self, key: &str) -> Option<RuntimeQueryResult> {
9828 let ttl = self.result_cache_ttl_secs();
9829 let cache = self.inner.result_cache.read();
9830 cache.0.get(key).and_then(|entry| {
9831 if entry.cached_at.elapsed().as_secs() < ttl {
9832 Some(entry.result.clone())
9833 } else {
9834 None
9835 }
9836 })
9837 }
9838
9839 fn get_blob_result_cache_entry(&self, key: &str) -> Option<RuntimeQueryResult> {
9840 let hit = self
9841 .inner
9842 .result_blob_cache
9843 .get(RESULT_CACHE_BLOB_NAMESPACE, key)?;
9844 {
9845 let cache = self.inner.result_blob_entries.read();
9846 if let Some(entry) = cache.0.get(key) {
9847 return Some(entry.result.clone());
9848 }
9849 }
9850
9851 let (result, scopes) = decode_result_cache_payload(hit.value())?;
9852 let mut cache = self.inner.result_blob_entries.write();
9853 let (ref mut map, ref mut order) = *cache;
9854 if !map.contains_key(key) {
9855 order.push_back(key.to_string());
9856 }
9857 map.insert(
9858 key.to_string(),
9859 RuntimeResultCacheEntry {
9860 result: result.clone(),
9861 cached_at: std::time::Instant::now(),
9862 scopes,
9863 },
9864 );
9865 let evicted = trim_result_cache(map, order, self.result_cache_capacity());
9866 drop(cache);
9867 self.record_result_cache_evictions(evicted);
9868 Some(result)
9869 }
9870
9871 pub(super) fn put_result_cache_entry(&self, key: &str, entry: RuntimeResultCacheEntry) {
9872 if !self.result_cache_enabled() {
9873 return;
9874 }
9875 match self.result_cache_backend() {
9876 RuntimeResultCacheBackend::Legacy => self.put_legacy_result_cache_entry(key, entry),
9877 RuntimeResultCacheBackend::BlobCache => self.put_blob_result_cache_entry(key, entry),
9878 RuntimeResultCacheBackend::Shadow => {
9879 self.put_legacy_result_cache_entry(key, entry.clone());
9880 self.put_blob_result_cache_entry(key, entry);
9881 }
9882 }
9883 }
9884
9885 fn put_legacy_result_cache_entry(&self, key: &str, entry: RuntimeResultCacheEntry) {
9886 let capacity = self.result_cache_capacity();
9887 let mut cache = self.inner.result_cache.write();
9888 let (ref mut map, ref mut order) = *cache;
9889 if !map.contains_key(key) {
9890 order.push_back(key.to_string());
9891 }
9892 map.insert(key.to_string(), entry);
9893 let evicted = trim_result_cache(map, order, capacity);
9894 drop(cache);
9895 self.record_result_cache_evictions(evicted);
9896 }
9897
9898 fn put_blob_result_cache_entry(&self, key: &str, entry: RuntimeResultCacheEntry) {
9899 let policy = crate::storage::cache::BlobCachePolicy::default()
9900 .ttl_ms(self.result_cache_ttl_secs() * 1000)
9901 .priority(200);
9902 let dependencies = entry.scopes.iter().cloned().collect::<Vec<_>>();
9903 let bytes = encode_result_cache_payload(&entry)
9904 .unwrap_or_else(|| result_cache_fingerprint(&entry.result).into_bytes());
9905 let put = crate::storage::cache::BlobCachePut::new(bytes)
9906 .with_dependencies(dependencies)
9907 .with_policy(policy);
9908 if self
9909 .inner
9910 .result_blob_cache
9911 .put(RESULT_CACHE_BLOB_NAMESPACE, key, put)
9912 .is_err()
9913 {
9914 return;
9915 }
9916
9917 let capacity = self.result_cache_capacity();
9918 let mut cache = self.inner.result_blob_entries.write();
9919 let (ref mut map, ref mut order) = *cache;
9920 if !map.contains_key(key) {
9921 order.push_back(key.to_string());
9922 }
9923 map.insert(key.to_string(), entry);
9924 let evicted = trim_result_cache(map, order, capacity);
9925 drop(cache);
9926 self.record_result_cache_evictions(evicted);
9927 }
9928
9929 pub fn result_cache_shadow_divergences(&self) -> u64 {
9930 self.inner
9931 .result_cache_shadow_divergences
9932 .load(std::sync::atomic::Ordering::Relaxed)
9933 }
9934
9935 pub fn invalidate_result_cache(&self) {
9938 let mut cache = self.inner.result_cache.write();
9939 cache.0.clear();
9940 cache.1.clear();
9941 let mut blob_entries = self.inner.result_blob_entries.write();
9942 blob_entries.0.clear();
9943 blob_entries.1.clear();
9944 self.inner
9945 .result_blob_cache
9946 .invalidate_namespace(RESULT_CACHE_BLOB_NAMESPACE);
9947 let mut ask_entries = self.inner.ask_answer_cache_entries.write();
9948 ask_entries.0.clear();
9949 ask_entries.1.clear();
9950 self.inner
9951 .result_blob_cache
9952 .invalidate_namespace(ASK_ANSWER_CACHE_NAMESPACE);
9953 }
9954
9955 pub(crate) fn invalidate_result_cache_for_table(&self, table: &str) {
9958 let legacy_has_match = {
9961 let cache = self.inner.result_cache.read();
9962 let (ref map, _) = *cache;
9963 !map.is_empty() && map.values().any(|entry| entry.scopes.contains(table))
9964 };
9965 let blob_has_match = {
9966 let cache = self.inner.result_blob_entries.read();
9967 let (ref map, _) = *cache;
9968 !map.is_empty() && map.values().any(|entry| entry.scopes.contains(table))
9969 };
9970 if legacy_has_match {
9971 let mut cache = self.inner.result_cache.write();
9972 let (ref mut map, ref mut order) = *cache;
9973 map.retain(|_, entry| !entry.scopes.contains(table));
9974 order.retain(|key| map.contains_key(key));
9975 }
9976
9977 if matches!(
9978 self.result_cache_backend(),
9979 RuntimeResultCacheBackend::BlobCache | RuntimeResultCacheBackend::Shadow
9980 ) {
9981 let mut blob_entries = self.inner.result_blob_entries.write();
9982 let (ref mut blob_map, ref mut blob_order) = *blob_entries;
9983 blob_map.clear();
9984 blob_order.clear();
9985 self.inner
9986 .result_blob_cache
9987 .invalidate_namespace(RESULT_CACHE_BLOB_NAMESPACE);
9988 } else if blob_has_match {
9989 let mut blob_entries = self.inner.result_blob_entries.write();
9990 let (ref mut blob_map, ref mut blob_order) = *blob_entries;
9991 blob_map.retain(|_, entry| !entry.scopes.contains(table));
9992 blob_order.retain(|key| blob_map.contains_key(key));
9993 }
9994 let mut ask_entries = self.inner.ask_answer_cache_entries.write();
9995 ask_entries.0.clear();
9996 ask_entries.1.clear();
9997 self.inner
9998 .result_blob_cache
9999 .invalidate_namespace(ASK_ANSWER_CACHE_NAMESPACE);
10000 }
10001
10002 pub(crate) fn invalidate_plan_cache(&self) {
10003 self.inner.query_cache.write().clear();
10004 self.inner
10005 .ddl_epoch
10006 .fetch_add(1, std::sync::atomic::Ordering::Release);
10007 }
10008
10009 pub fn ddl_epoch(&self) -> u64 {
10013 self.inner
10014 .ddl_epoch
10015 .load(std::sync::atomic::Ordering::Acquire)
10016 }
10017
10018 pub(crate) fn clear_table_planner_stats(&self, table: &str) {
10019 let store = self.inner.db.store();
10020 crate::storage::query::planner::stats_catalog::clear_table_stats(store.as_ref(), table);
10021 self.invalidate_plan_cache();
10022 }
10023
10024 pub(crate) fn rehydrate_tenant_tables(&self) {
10033 let store = self.inner.db.store();
10034 let Some(manager) = store.get_collection("red_config") else {
10035 return;
10036 };
10037 for entity in manager.query_all(|_| true) {
10042 let crate::storage::unified::entity::EntityData::Row(row) = &entity.data else {
10043 continue;
10044 };
10045 let Some(named) = &row.named else { continue };
10046 let Some(crate::storage::schema::Value::Text(key)) = named.get("key") else {
10047 continue;
10048 };
10049 let Some(rest) = key.strip_prefix("tenant_tables.") else {
10051 continue;
10052 };
10053 let Some((table, suffix)) = rest.rsplit_once('.') else {
10054 crate::telemetry::operator_event::OperatorEvent::SchemaCorruption {
10060 collection: "red_config".to_string(),
10061 detail: format!("malformed tenant_tables key: {key}"),
10062 }
10063 .emit_global();
10064 continue;
10065 };
10066 if suffix != "column" {
10067 crate::telemetry::operator_event::OperatorEvent::SchemaCorruption {
10068 collection: "red_config".to_string(),
10069 detail: format!("unexpected tenant_tables suffix: {key}"),
10070 }
10071 .emit_global();
10072 continue;
10073 }
10074 match named.get("value") {
10075 Some(crate::storage::schema::Value::Text(column)) => {
10076 self.register_tenant_table(table, column);
10077 }
10078 Some(crate::storage::schema::Value::Null) | None => {
10080 self.unregister_tenant_table(table);
10081 }
10082 _ => {}
10083 }
10084 }
10085 }
10086
10087 pub(crate) fn rehydrate_materialized_view_descriptors(&self) {
10099 let store = self.inner.db.store();
10100 let descriptors = crate::runtime::continuous_materialized_view::load_all(store.as_ref());
10101 for descriptor in descriptors {
10102 let parsed = match crate::storage::query::parser::parse(&descriptor.source_sql) {
10103 Ok(qc) => qc,
10104 Err(err) => {
10105 crate::telemetry::operator_event::OperatorEvent::SchemaCorruption {
10106 collection:
10107 crate::runtime::continuous_materialized_view::CATALOG_COLLECTION
10108 .to_string(),
10109 detail: format!(
10110 "failed to re-parse materialized-view source for {}: {err}",
10111 descriptor.name
10112 ),
10113 }
10114 .emit_global();
10115 continue;
10116 }
10117 };
10118 let crate::storage::query::ast::QueryExpr::CreateView(create) = parsed.query else {
10119 crate::telemetry::operator_event::OperatorEvent::SchemaCorruption {
10120 collection: crate::runtime::continuous_materialized_view::CATALOG_COLLECTION
10121 .to_string(),
10122 detail: format!(
10123 "materialized-view source for {} did not re-parse as CREATE VIEW",
10124 descriptor.name
10125 ),
10126 }
10127 .emit_global();
10128 continue;
10129 };
10130 let view_name = create.name.clone();
10132 self.inner
10133 .views
10134 .write()
10135 .insert(view_name.clone(), Arc::new(create));
10136 use crate::storage::cache::result::{MaterializedViewDef, RefreshPolicy};
10138 let refresh = match descriptor.refresh_every_ms {
10139 Some(ms) => RefreshPolicy::Periodic(std::time::Duration::from_millis(ms)),
10140 None => RefreshPolicy::Manual,
10141 };
10142 let def = MaterializedViewDef {
10143 name: view_name.clone(),
10144 query: format!("<parsed view {}>", view_name),
10145 dependencies: descriptor.source_collections.clone(),
10146 refresh,
10147 retention_duration_ms: descriptor.retention_duration_ms,
10148 };
10149 self.inner.materialized_views.write().register(def);
10150 }
10151 self.invalidate_plan_cache();
10154 }
10155
10156 pub(crate) fn rehydrate_declared_column_schemas(&self) {
10157 let store = self.inner.db.store();
10158 for contract in self.inner.db.collection_contracts() {
10159 let columns: Vec<String> = contract
10160 .declared_columns
10161 .iter()
10162 .map(|column| column.name.clone())
10163 .collect();
10164 let Some(manager) = store.get_collection(&contract.name) else {
10165 continue;
10166 };
10167 manager.set_column_schema_if_empty(columns);
10168 }
10169 }
10170
10171 pub fn register_tenant_table(&self, table: &str, column: &str) {
10176 use crate::storage::query::ast::{
10177 CompareOp, CreatePolicyQuery, Expr, FieldRef, Filter, Span,
10178 };
10179 self.inner
10180 .tenant_tables
10181 .write()
10182 .insert(table.to_string(), column.to_string());
10183
10184 let lhs = Expr::Column {
10190 field: FieldRef::TableColumn {
10191 table: table.to_string(),
10192 column: column.to_string(),
10193 },
10194 span: Span::synthetic(),
10195 };
10196 let rhs = Expr::FunctionCall {
10197 name: "CURRENT_TENANT".to_string(),
10198 args: Vec::new(),
10199 span: Span::synthetic(),
10200 };
10201 let policy_filter = Filter::CompareExpr {
10202 lhs,
10203 op: CompareOp::Eq,
10204 rhs,
10205 };
10206
10207 let policy = CreatePolicyQuery {
10208 name: "__tenant_iso".to_string(),
10209 table: table.to_string(),
10210 action: None, role: None, using: Box::new(policy_filter),
10213 target_kind: crate::storage::query::ast::PolicyTargetKind::Table,
10220 };
10221
10222 self.inner.rls_policies.write().insert(
10224 (table.to_string(), "__tenant_iso".to_string()),
10225 Arc::new(policy),
10226 );
10227 self.inner
10228 .rls_enabled_tables
10229 .write()
10230 .insert(table.to_string());
10231
10232 self.ensure_tenant_index(table, column);
10238 }
10239
10240 fn ensure_tenant_index(&self, table: &str, column: &str) {
10248 if column.contains('.') {
10249 return;
10250 }
10251 let index_name = format!("__tenant_idx_{table}");
10252 let registry = self.inner.index_store.list_indices(table);
10253 if registry.iter().any(|idx| idx.name == index_name) {
10254 return;
10255 }
10256 if registry
10257 .iter()
10258 .any(|idx| idx.columns.first().map(|c| c.as_str()) == Some(column))
10259 {
10260 return;
10261 }
10262
10263 let store = self.inner.db.store();
10264 let Some(manager) = store.get_collection(table) else {
10265 return;
10266 };
10267 let entities = manager.query_all(|_| true);
10268 let entity_fields: Vec<(
10269 crate::storage::unified::EntityId,
10270 Vec<(String, crate::storage::schema::Value)>,
10271 )> = entities
10272 .iter()
10273 .map(|e| {
10274 let fields = match &e.data {
10275 crate::storage::EntityData::Row(row) => {
10276 if let Some(ref named) = row.named {
10277 named.iter().map(|(k, v)| (k.clone(), v.clone())).collect()
10278 } else if let Some(ref schema) = row.schema {
10279 schema
10280 .iter()
10281 .zip(row.columns.iter())
10282 .map(|(k, v)| (k.clone(), v.clone()))
10283 .collect()
10284 } else {
10285 Vec::new()
10286 }
10287 }
10288 crate::storage::EntityData::Node(node) => node
10289 .properties
10290 .iter()
10291 .map(|(k, v)| (k.clone(), v.clone()))
10292 .collect(),
10293 _ => Vec::new(),
10294 };
10295 (e.id, fields)
10296 })
10297 .collect();
10298
10299 let columns = vec![column.to_string()];
10300 if self
10301 .inner
10302 .index_store
10303 .create_index(
10304 &index_name,
10305 table,
10306 &columns,
10307 super::index_store::IndexMethodKind::Hash,
10308 false,
10309 &entity_fields,
10310 )
10311 .is_err()
10312 {
10313 return;
10314 }
10315 self.inner
10316 .index_store
10317 .register(super::index_store::RegisteredIndex {
10318 name: index_name,
10319 collection: table.to_string(),
10320 columns,
10321 method: super::index_store::IndexMethodKind::Hash,
10322 unique: false,
10323 });
10324 self.invalidate_plan_cache();
10325 }
10326
10327 fn drop_tenant_index(&self, table: &str) {
10330 let index_name = format!("__tenant_idx_{table}");
10331 self.inner.index_store.drop_index(&index_name, table);
10332 }
10333
10334 pub fn tenant_column(&self, table: &str) -> Option<String> {
10338 self.inner.tenant_tables.read().get(table).cloned()
10339 }
10340
10341 pub fn unregister_tenant_table(&self, table: &str) {
10345 self.inner.tenant_tables.write().remove(table);
10346 self.inner
10347 .rls_policies
10348 .write()
10349 .remove(&(table.to_string(), "__tenant_iso".to_string()));
10350 self.drop_tenant_index(table);
10351 let has_other_policies = self
10353 .inner
10354 .rls_policies
10355 .read()
10356 .keys()
10357 .any(|(t, _)| t == table);
10358 if !has_other_policies {
10359 self.inner.rls_enabled_tables.write().remove(table);
10360 }
10361 }
10362
10363 pub(crate) fn record_pending_tombstone(
10369 &self,
10370 conn_id: u64,
10371 collection: &str,
10372 id: crate::storage::unified::entity::EntityId,
10373 stamper_xid: crate::storage::transaction::snapshot::Xid,
10374 previous_xmax: crate::storage::transaction::snapshot::Xid,
10375 ) {
10376 self.inner
10377 .pending_tombstones
10378 .write()
10379 .entry(conn_id)
10380 .or_default()
10381 .push((collection.to_string(), id, stamper_xid, previous_xmax));
10382 }
10383
10384 pub(crate) fn record_pending_versioned_update(
10385 &self,
10386 conn_id: u64,
10387 collection: &str,
10388 old_id: crate::storage::unified::entity::EntityId,
10389 new_id: crate::storage::unified::entity::EntityId,
10390 stamper_xid: crate::storage::transaction::snapshot::Xid,
10391 previous_xmax: crate::storage::transaction::snapshot::Xid,
10392 ) {
10393 self.inner
10394 .pending_versioned_updates
10395 .write()
10396 .entry(conn_id)
10397 .or_default()
10398 .push((
10399 collection.to_string(),
10400 old_id,
10401 new_id,
10402 stamper_xid,
10403 previous_xmax,
10404 ));
10405 }
10406
10407 fn with_deferred_store_wal_if_transaction<T>(
10408 &self,
10409 f: impl FnOnce() -> RedDBResult<T>,
10410 ) -> RedDBResult<T> {
10411 let conn_id = current_connection_id();
10412 if !self.inner.tx_contexts.read().contains_key(&conn_id) {
10413 return f();
10414 }
10415
10416 crate::storage::UnifiedStore::begin_deferred_store_wal_capture();
10417 let result = f();
10418 let captured = crate::storage::UnifiedStore::take_deferred_store_wal_capture();
10419 match result {
10420 Ok(value) => {
10421 self.record_pending_store_wal_actions(conn_id, captured);
10422 Ok(value)
10423 }
10424 Err(err) => Err(err),
10425 }
10426 }
10427
10428 fn with_deferred_store_wal_for_dml<T>(
10429 &self,
10430 capture_autocommit_events: bool,
10431 f: impl FnOnce() -> RedDBResult<T>,
10432 ) -> RedDBResult<T> {
10433 let conn_id = current_connection_id();
10434 if self.inner.tx_contexts.read().contains_key(&conn_id) {
10435 return self.with_deferred_store_wal_if_transaction(f);
10436 }
10437 if !capture_autocommit_events {
10438 return f();
10439 }
10440
10441 crate::storage::UnifiedStore::begin_deferred_store_wal_capture();
10442 let result = f();
10443 let captured = crate::storage::UnifiedStore::take_deferred_store_wal_capture();
10444 self.inner
10445 .db
10446 .store()
10447 .append_deferred_store_wal_actions(captured)
10448 .map_err(|err| RedDBError::Internal(err.to_string()))?;
10449 result
10450 }
10451
10452 fn insert_may_emit_events(&self, query: &InsertQuery) -> bool {
10453 !query.suppress_events
10454 && self.collection_has_event_subscriptions_for_operation(
10455 &query.table,
10456 crate::catalog::SubscriptionOperation::Insert,
10457 )
10458 }
10459
10460 fn update_may_emit_events(&self, query: &UpdateQuery) -> bool {
10461 !query.suppress_events
10462 && self.collection_has_event_subscriptions_for_operation(
10463 &query.table,
10464 crate::catalog::SubscriptionOperation::Update,
10465 )
10466 }
10467
10468 fn delete_may_emit_events(&self, query: &DeleteQuery) -> bool {
10469 !query.suppress_events
10470 && self.collection_has_event_subscriptions_for_operation(
10471 &query.table,
10472 crate::catalog::SubscriptionOperation::Delete,
10473 )
10474 }
10475
10476 fn collection_has_event_subscriptions_for_operation(
10477 &self,
10478 collection: &str,
10479 operation: crate::catalog::SubscriptionOperation,
10480 ) -> bool {
10481 let Some(contract) = self.db().collection_contract_arc(collection) else {
10482 return false;
10483 };
10484 contract.subscriptions.iter().any(|subscription| {
10485 subscription.enabled
10486 && (subscription.ops_filter.is_empty()
10487 || subscription.ops_filter.contains(&operation))
10488 })
10489 }
10490
10491 fn record_pending_store_wal_actions(
10492 &self,
10493 conn_id: u64,
10494 actions: crate::storage::unified::DeferredStoreWalActions,
10495 ) {
10496 if actions.is_empty() {
10497 return;
10498 }
10499 let mut guard = self.inner.pending_store_wal_actions.write();
10500 guard.entry(conn_id).or_default().extend(actions);
10501 }
10502
10503 fn flush_pending_store_wal_actions(&self, conn_id: u64) -> RedDBResult<()> {
10504 let Some(actions) = self
10505 .inner
10506 .pending_store_wal_actions
10507 .write()
10508 .remove(&conn_id)
10509 else {
10510 return Ok(());
10511 };
10512 self.inner
10513 .db
10514 .store()
10515 .append_deferred_store_wal_actions(actions)
10516 .map_err(|err| RedDBError::Internal(err.to_string()))
10517 }
10518
10519 fn discard_pending_store_wal_actions(&self, conn_id: u64) {
10520 self.inner
10521 .pending_store_wal_actions
10522 .write()
10523 .remove(&conn_id);
10524 }
10525
10526 fn xid_conflicts_with_snapshot(
10527 &self,
10528 xid: crate::storage::transaction::snapshot::Xid,
10529 snapshot: &crate::storage::transaction::snapshot::Snapshot,
10530 own_xids: &std::collections::HashSet<crate::storage::transaction::snapshot::Xid>,
10531 ) -> bool {
10532 xid != 0
10533 && !own_xids.contains(&xid)
10534 && !self.inner.snapshot_manager.is_aborted(xid)
10535 && !self.inner.snapshot_manager.is_active(xid)
10536 && (xid > snapshot.xid || snapshot.in_progress.contains(&xid))
10537 }
10538
10539 fn conflict_error(
10540 collection: &str,
10541 logical_id: crate::storage::unified::entity::EntityId,
10542 xid: crate::storage::transaction::snapshot::Xid,
10543 ) -> RedDBError {
10544 RedDBError::Query(format!(
10545 "serialization conflict: table row {collection}/{} was modified by concurrent transaction {xid}",
10546 logical_id.raw()
10547 ))
10548 }
10549
10550 fn check_logical_row_conflict(
10551 &self,
10552 collection: &str,
10553 logical_id: crate::storage::unified::entity::EntityId,
10554 excluded_ids: &[crate::storage::unified::entity::EntityId],
10555 snapshot: &crate::storage::transaction::snapshot::Snapshot,
10556 own_xids: &std::collections::HashSet<crate::storage::transaction::snapshot::Xid>,
10557 ) -> RedDBResult<()> {
10558 let store = self.inner.db.store();
10559 let Some(manager) = store.get_collection(collection) else {
10560 return Ok(());
10561 };
10562
10563 for candidate in manager.query_all(|_| true) {
10564 if excluded_ids.contains(&candidate.id) || candidate.logical_id() != logical_id {
10565 continue;
10566 }
10567 if self.xid_conflicts_with_snapshot(candidate.xmin, snapshot, own_xids) {
10568 return Err(Self::conflict_error(collection, logical_id, candidate.xmin));
10569 }
10570 if self.xid_conflicts_with_snapshot(candidate.xmax, snapshot, own_xids) {
10571 return Err(Self::conflict_error(collection, logical_id, candidate.xmax));
10572 }
10573 }
10574 Ok(())
10575 }
10576
10577 pub(crate) fn check_table_row_write_conflicts(
10578 &self,
10579 conn_id: u64,
10580 snapshot: &crate::storage::transaction::snapshot::Snapshot,
10581 own_xids: &std::collections::HashSet<crate::storage::transaction::snapshot::Xid>,
10582 ) -> RedDBResult<()> {
10583 let versioned_updates = self
10584 .inner
10585 .pending_versioned_updates
10586 .read()
10587 .get(&conn_id)
10588 .cloned()
10589 .unwrap_or_default();
10590 let tombstones = self
10591 .inner
10592 .pending_tombstones
10593 .read()
10594 .get(&conn_id)
10595 .cloned()
10596 .unwrap_or_default();
10597
10598 let store = self.inner.db.store();
10599 for (collection, old_id, new_id, xid, previous_xmax) in versioned_updates {
10600 let Some(manager) = store.get_collection(&collection) else {
10601 continue;
10602 };
10603 let Some(old) = manager.get(old_id) else {
10604 continue;
10605 };
10606 let logical_id = old.logical_id();
10607 if self.xid_conflicts_with_snapshot(previous_xmax, snapshot, own_xids) {
10608 return Err(Self::conflict_error(&collection, logical_id, previous_xmax));
10609 }
10610 if old.xmax != xid && self.xid_conflicts_with_snapshot(old.xmax, snapshot, own_xids) {
10611 return Err(Self::conflict_error(&collection, logical_id, old.xmax));
10612 }
10613 self.check_logical_row_conflict(
10614 &collection,
10615 logical_id,
10616 &[old_id, new_id],
10617 snapshot,
10618 own_xids,
10619 )?;
10620 }
10621
10622 for (collection, id, xid, previous_xmax) in tombstones {
10623 let Some(manager) = store.get_collection(&collection) else {
10624 continue;
10625 };
10626 let Some(entity) = manager.get(id) else {
10627 continue;
10628 };
10629 let logical_id = entity.logical_id();
10630 if self.xid_conflicts_with_snapshot(previous_xmax, snapshot, own_xids) {
10631 return Err(Self::conflict_error(&collection, logical_id, previous_xmax));
10632 }
10633 if entity.xmax != xid
10634 && self.xid_conflicts_with_snapshot(entity.xmax, snapshot, own_xids)
10635 {
10636 return Err(Self::conflict_error(&collection, logical_id, entity.xmax));
10637 }
10638 self.check_logical_row_conflict(&collection, logical_id, &[id], snapshot, own_xids)?;
10639 }
10640
10641 Ok(())
10642 }
10643
10644 pub(crate) fn restore_pending_write_stamps(&self, conn_id: u64) {
10645 let versioned_updates = self
10646 .inner
10647 .pending_versioned_updates
10648 .read()
10649 .get(&conn_id)
10650 .cloned()
10651 .unwrap_or_default();
10652 let tombstones = self
10653 .inner
10654 .pending_tombstones
10655 .read()
10656 .get(&conn_id)
10657 .cloned()
10658 .unwrap_or_default();
10659
10660 let store = self.inner.db.store();
10661 for (collection, old_id, _new_id, xid, _previous_xmax) in versioned_updates {
10662 if let Some(manager) = store.get_collection(&collection) {
10663 if let Some(mut entity) = manager.get(old_id) {
10664 entity.set_xmax(xid);
10665 let _ = manager.update(entity);
10666 }
10667 }
10668 }
10669 for (collection, id, xid, _previous_xmax) in tombstones {
10670 if let Some(manager) = store.get_collection(&collection) {
10671 if let Some(mut entity) = manager.get(id) {
10672 entity.set_xmax(xid);
10673 let _ = manager.update(entity);
10674 }
10675 }
10676 }
10677 }
10678
10679 pub(crate) fn finalize_pending_versioned_updates(&self, conn_id: u64) {
10680 self.inner
10681 .pending_versioned_updates
10682 .write()
10683 .remove(&conn_id);
10684 }
10685
10686 pub(crate) fn revive_pending_versioned_updates(&self, conn_id: u64) {
10687 let Some(pending) = self
10688 .inner
10689 .pending_versioned_updates
10690 .write()
10691 .remove(&conn_id)
10692 else {
10693 return;
10694 };
10695
10696 let store = self.inner.db.store();
10697 for (collection, old_id, new_id, xid, previous_xmax) in pending {
10698 if let Some(manager) = store.get_collection(&collection) {
10699 if let Some(mut old) = manager.get(old_id) {
10700 if old.xmax == xid {
10701 old.set_xmax(previous_xmax);
10702 let _ = manager.update(old);
10703 }
10704 }
10705 }
10706 let _ = store.delete_batch(&collection, &[new_id]);
10707 }
10708 }
10709
10710 pub(crate) fn revive_versioned_updates_since(&self, conn_id: u64, stamper_xid: u64) -> usize {
10711 let mut guard = self.inner.pending_versioned_updates.write();
10712 let Some(pending) = guard.get_mut(&conn_id) else {
10713 return 0;
10714 };
10715
10716 let store = self.inner.db.store();
10717 let mut reverted = 0usize;
10718 pending.retain(|(collection, old_id, new_id, xid, previous_xmax)| {
10719 if *xid < stamper_xid {
10720 return true;
10721 }
10722 if let Some(manager) = store.get_collection(collection) {
10723 if let Some(mut old) = manager.get(*old_id) {
10724 if old.xmax == *xid {
10725 old.set_xmax(*previous_xmax);
10726 let _ = manager.update(old);
10727 }
10728 }
10729 }
10730 let _ = store.delete_batch(collection, &[*new_id]);
10731 reverted += 1;
10732 false
10733 });
10734 if pending.is_empty() {
10735 guard.remove(&conn_id);
10736 }
10737 reverted
10738 }
10739
10740 pub(crate) fn finalize_pending_tombstones(&self, conn_id: u64) {
10745 let Some(pending) = self.inner.pending_tombstones.write().remove(&conn_id) else {
10746 return;
10747 };
10748 if pending.is_empty() {
10749 return;
10750 }
10751
10752 let store = self.inner.db.store();
10753 for (collection, id, _xid, _previous_xmax) in pending {
10754 store.context_index().remove_entity(id);
10755 self.cdc_emit(
10756 crate::replication::cdc::ChangeOperation::Delete,
10757 &collection,
10758 id.raw(),
10759 "entity",
10760 );
10761 }
10762 }
10763
10764 pub(crate) fn revive_pending_tombstones(&self, conn_id: u64) {
10771 let Some(pending) = self.inner.pending_tombstones.write().remove(&conn_id) else {
10772 return;
10773 };
10774
10775 let store = self.inner.db.store();
10776 for (collection, id, xid, previous_xmax) in pending {
10777 let Some(manager) = store.get_collection(&collection) else {
10778 continue;
10779 };
10780 if let Some(mut entity) = manager.get(id) {
10781 if entity.xmax == xid {
10782 entity.set_xmax(previous_xmax);
10783 let _ = manager.update(entity);
10784 }
10785 }
10786 }
10787 }
10788
10789 pub fn queue_wait_registry(
10791 &self,
10792 ) -> std::sync::Arc<crate::runtime::queue_wait_registry::QueueWaitRegistry> {
10793 self.inner.queue_wait_registry.clone()
10794 }
10795
10796 pub(crate) fn record_queue_wake(&self, scope: &str, queue: &str) {
10801 if self.current_xid().is_some() {
10802 let conn_id = current_connection_id();
10803 self.inner
10804 .pending_queue_wakes
10805 .write()
10806 .entry(conn_id)
10807 .or_default()
10808 .push((scope.to_string(), queue.to_string()));
10809 return;
10810 }
10811 self.inner.queue_wait_registry.notify(scope, queue);
10812 }
10813
10814 pub(crate) fn finalize_pending_queue_wakes(&self, conn_id: u64) {
10815 let Some(pending) = self.inner.pending_queue_wakes.write().remove(&conn_id) else {
10816 return;
10817 };
10818 for (scope, queue) in pending {
10819 self.inner.queue_wait_registry.notify(&scope, &queue);
10820 }
10821 }
10822
10823 pub(crate) fn discard_pending_queue_wakes(&self, conn_id: u64) {
10824 self.inner.pending_queue_wakes.write().remove(&conn_id);
10825 }
10826
10827 pub(crate) fn finalize_pending_kv_watch_events(&self, conn_id: u64) {
10828 let Some(pending) = self.inner.pending_kv_watch_events.write().remove(&conn_id) else {
10829 return;
10830 };
10831 for event in pending {
10832 self.cdc_emit_kv(
10833 event.op,
10834 &event.collection,
10835 &event.key,
10836 0,
10837 event.before,
10838 event.after,
10839 );
10840 }
10841 }
10842
10843 pub(crate) fn discard_pending_kv_watch_events(&self, conn_id: u64) {
10844 self.inner.pending_kv_watch_events.write().remove(&conn_id);
10845 }
10846
10847 fn materialize_graph_with_rls(
10856 &self,
10857 ) -> RedDBResult<(
10858 crate::storage::engine::GraphStore,
10859 std::collections::HashMap<
10860 String,
10861 std::collections::HashMap<String, crate::storage::schema::Value>,
10862 >,
10863 crate::storage::query::unified::EdgeProperties,
10864 )> {
10865 use crate::storage::engine::GraphStore;
10866 use crate::storage::query::ast::{PolicyAction, PolicyTargetKind};
10867 use crate::storage::unified::entity::{EntityData, EntityKind};
10868 use std::collections::{HashMap, HashSet};
10869
10870 let store = self.inner.db.store();
10871 let snap_ctx = capture_current_snapshot();
10872 let role = current_auth_identity().map(|(_, r)| r.as_str().to_string());
10873
10874 let graph = GraphStore::new();
10875 let mut node_properties: HashMap<String, HashMap<String, crate::storage::schema::Value>> =
10876 HashMap::new();
10877 let mut edge_properties: crate::storage::query::unified::EdgeProperties = HashMap::new();
10878 let mut allowed_nodes: HashSet<String> = HashSet::new();
10879
10880 let mut node_rls: HashMap<String, Option<crate::storage::query::ast::Filter>> =
10884 HashMap::new();
10885 let mut edge_rls: HashMap<String, Option<crate::storage::query::ast::Filter>> =
10886 HashMap::new();
10887
10888 let collections = store.list_collections();
10889
10890 for collection in &collections {
10892 let Some(manager) = store.get_collection(collection) else {
10893 continue;
10894 };
10895 let entities = manager.query_all(|_| true);
10896 for entity in entities {
10897 if !entity_visible_with_context(snap_ctx.as_ref(), &entity) {
10898 continue;
10899 }
10900 let EntityKind::GraphNode(ref node) = entity.kind else {
10901 continue;
10902 };
10903 if !node_passes_rls(self, collection, role.as_deref(), &mut node_rls, &entity) {
10904 continue;
10905 }
10906 let id_str = entity.id.raw().to_string();
10907 graph
10908 .add_node_with_label(
10909 &id_str,
10910 &node.label,
10911 &super::graph_node_label(&node.node_type),
10912 )
10913 .map_err(|err| RedDBError::Query(err.to_string()))?;
10914 allowed_nodes.insert(id_str.clone());
10915 if let EntityData::Node(node_data) = &entity.data {
10916 node_properties.insert(id_str, node_data.properties.clone());
10917 }
10918 }
10919 }
10920
10921 for collection in &collections {
10925 let Some(manager) = store.get_collection(collection) else {
10926 continue;
10927 };
10928 let entities = manager.query_all(|_| true);
10929 for entity in entities {
10930 if !entity_visible_with_context(snap_ctx.as_ref(), &entity) {
10931 continue;
10932 }
10933 let EntityKind::GraphEdge(ref edge) = entity.kind else {
10934 continue;
10935 };
10936 if !allowed_nodes.contains(&edge.from_node)
10937 || !allowed_nodes.contains(&edge.to_node)
10938 {
10939 continue;
10940 }
10941 if !edge_passes_rls(self, collection, role.as_deref(), &mut edge_rls, &entity) {
10942 continue;
10943 }
10944 let weight = match &entity.data {
10945 EntityData::Edge(e) => e.weight,
10946 _ => edge.weight as f32 / 1000.0,
10947 };
10948 let edge_label = super::graph_edge_label(&edge.label);
10949 graph
10950 .add_edge_with_label(&edge.from_node, &edge.to_node, &edge_label, weight)
10951 .map_err(|err| RedDBError::Query(err.to_string()))?;
10952 if let EntityData::Edge(edge_data) = &entity.data {
10953 edge_properties.insert(
10954 (edge.from_node.clone(), edge_label, edge.to_node.clone()),
10955 edge_data.properties.clone(),
10956 );
10957 }
10958 }
10959 }
10960
10961 let _ = (PolicyAction::Select, PolicyTargetKind::Nodes);
10965
10966 Ok((graph, node_properties, edge_properties))
10967 }
10968
10969 pub(crate) fn stamp_xmin_if_in_txn(
10984 &self,
10985 collection: &str,
10986 id: crate::storage::unified::entity::EntityId,
10987 ) {
10988 let Some(xid) = self.current_xid() else {
10989 return;
10990 };
10991 let store = self.inner.db.store();
10992 let Some(manager) = store.get_collection(collection) else {
10993 return;
10994 };
10995 if let Some(mut entity) = manager.get(id) {
10996 entity.set_xmin(xid);
10997 let _ = manager.update(entity);
10998 }
10999 }
11000
11001 pub(crate) fn revive_tombstones_since(&self, conn_id: u64, stamper_xid: u64) -> usize {
11009 let mut guard = self.inner.pending_tombstones.write();
11010 let Some(pending) = guard.get_mut(&conn_id) else {
11011 return 0;
11012 };
11013
11014 let store = self.inner.db.store();
11015 let mut revived = 0usize;
11016 pending.retain(|(collection, id, xid, previous_xmax)| {
11017 if *xid < stamper_xid {
11018 return true;
11020 }
11021 if let Some(manager) = store.get_collection(collection) {
11022 if let Some(mut entity) = manager.get(*id) {
11023 if entity.xmax == *xid {
11024 entity.set_xmax(*previous_xmax);
11025 let _ = manager.update(entity);
11026 revived += 1;
11027 }
11028 }
11029 }
11030 false
11031 });
11032 if pending.is_empty() {
11033 guard.remove(&conn_id);
11034 }
11035 revived
11036 }
11037
11038 pub fn current_snapshot(&self) -> crate::storage::transaction::snapshot::Snapshot {
11047 let conn_id = current_connection_id();
11048 if let Some(ctx) = self.inner.tx_contexts.read().get(&conn_id).cloned() {
11049 return ctx.snapshot;
11050 }
11051 let high_water = self.inner.snapshot_manager.peek_next_xid();
11057 self.inner.snapshot_manager.snapshot(high_water)
11058 }
11059
11060 pub fn current_xid(&self) -> Option<crate::storage::transaction::snapshot::Xid> {
11070 let conn_id = current_connection_id();
11071 self.inner
11072 .tx_contexts
11073 .read()
11074 .get(&conn_id)
11075 .map(|ctx| ctx.writer_xid())
11076 }
11077
11078 pub fn connection_in_transaction(&self, conn_id: u64) -> bool {
11085 self.inner.tx_contexts.read().contains_key(&conn_id)
11086 }
11087
11088 pub fn snapshot_manager(&self) -> Arc<crate::storage::transaction::snapshot::SnapshotManager> {
11091 Arc::clone(&self.inner.snapshot_manager)
11092 }
11093
11094 fn mvcc_vacuum_cutoff_xid(&self) -> crate::storage::transaction::snapshot::Xid {
11095 let manager = &self.inner.snapshot_manager;
11096 let next_xid = manager.peek_next_xid();
11097 let mut cutoff = next_xid;
11098 if let Some(oldest_active) = manager.oldest_active_xid() {
11099 cutoff = cutoff.min(oldest_active);
11100 }
11101 if let Some(oldest_pinned) = manager.oldest_pinned_xid() {
11102 cutoff = cutoff.min(oldest_pinned);
11103 }
11104 let retention_xids = self.config_u64("runtime.mvcc.vacuum_retention_xids", 0);
11105 if retention_xids > 0 {
11106 cutoff = cutoff.min(next_xid.saturating_sub(retention_xids));
11107 }
11108 cutoff
11109 }
11110
11111 fn rebuild_runtime_indexes_for_table(&self, table: &str) -> RedDBResult<()> {
11112 let registered = self.inner.index_store.list_indices(table);
11113 if registered.is_empty() {
11114 return Ok(());
11115 }
11116 let store = self.inner.db.store();
11117 let Some(manager) = store.get_collection(table) else {
11118 return Ok(());
11119 };
11120 let entity_fields = manager
11121 .query_all(|entity| matches!(entity.kind, crate::storage::EntityKind::TableRow { .. }))
11122 .into_iter()
11123 .map(|entity| (entity.id, table_row_index_fields(&entity)))
11124 .collect::<Vec<_>>();
11125
11126 for index in registered {
11127 self.inner.index_store.drop_index(&index.name, table);
11128 self.inner
11129 .index_store
11130 .create_index(
11131 &index.name,
11132 table,
11133 &index.columns,
11134 index.method,
11135 index.unique,
11136 &entity_fields,
11137 )
11138 .map_err(RedDBError::Internal)?;
11139 self.inner.index_store.register(index);
11140 }
11141 self.invalidate_plan_cache();
11142 Ok(())
11143 }
11144
11145 pub fn current_txn_own_xids(
11150 &self,
11151 ) -> std::collections::HashSet<crate::storage::transaction::snapshot::Xid> {
11152 let mut set = std::collections::HashSet::new();
11153 if let Some(ctx) = self.inner.tx_contexts.read().get(¤t_connection_id()) {
11154 set.insert(ctx.xid);
11155 for (_, sub) in &ctx.savepoints {
11156 set.insert(*sub);
11157 }
11158 for sub in &ctx.released_sub_xids {
11159 set.insert(*sub);
11160 }
11161 }
11162 set
11163 }
11164
11165 pub fn foreign_tables(&self) -> Arc<crate::storage::fdw::ForeignTableRegistry> {
11172 Arc::clone(&self.inner.foreign_tables)
11173 }
11174
11175 pub fn is_rls_enabled(&self, table: &str) -> bool {
11177 self.inner.rls_enabled_tables.read().contains(table)
11178 }
11179
11180 pub fn matching_rls_policies(
11187 &self,
11188 table: &str,
11189 role: Option<&str>,
11190 action: crate::storage::query::ast::PolicyAction,
11191 ) -> Vec<crate::storage::query::ast::Filter> {
11192 self.matching_rls_policies_for_kind(
11197 table,
11198 role,
11199 action,
11200 crate::storage::query::ast::PolicyTargetKind::Table,
11201 )
11202 }
11203
11204 pub fn matching_rls_policies_for_kind(
11212 &self,
11213 table: &str,
11214 role: Option<&str>,
11215 action: crate::storage::query::ast::PolicyAction,
11216 kind: crate::storage::query::ast::PolicyTargetKind,
11217 ) -> Vec<crate::storage::query::ast::Filter> {
11218 if !self.is_rls_enabled(table) {
11219 return Vec::new();
11220 }
11221 let policies = self.inner.rls_policies.read();
11222 policies
11223 .iter()
11224 .filter_map(|((t, _), p)| {
11225 if t != table {
11226 return None;
11227 }
11228 if p.target_kind != kind
11237 && p.target_kind != crate::storage::query::ast::PolicyTargetKind::Table
11238 {
11239 return None;
11240 }
11241 if let Some(a) = p.action {
11243 if a != action {
11244 return None;
11245 }
11246 }
11247 if let Some(p_role) = p.role.as_deref() {
11249 match role {
11250 Some(r) if r == p_role => {}
11251 _ => return None,
11252 }
11253 }
11254 Some((*p.using).clone())
11255 })
11256 .collect()
11257 }
11258
11259 pub(crate) fn refresh_table_planner_stats(&self, table: &str) {
11260 let store = self.inner.db.store();
11261 if let Some(stats) =
11262 crate::storage::query::planner::stats_catalog::analyze_collection(store.as_ref(), table)
11263 {
11264 crate::storage::query::planner::stats_catalog::persist_table_stats(
11265 store.as_ref(),
11266 &stats,
11267 );
11268 } else {
11269 crate::storage::query::planner::stats_catalog::clear_table_stats(store.as_ref(), table);
11270 }
11271 self.invalidate_plan_cache();
11272 }
11273
11274 pub(crate) fn note_table_write(&self, table: &str) {
11275 let already_dirty = self.inner.planner_dirty_tables.read().contains(table);
11280 if !already_dirty {
11281 self.inner
11282 .planner_dirty_tables
11283 .write()
11284 .insert(table.to_string());
11285 }
11286 self.invalidate_result_cache_for_table(table);
11287 }
11288
11289 fn explain_as_rows(&self, raw_query: &str, inner_sql: &str) -> RedDBResult<RuntimeQueryResult> {
11297 let explain = self.explain_query(inner_sql)?;
11298
11299 let columns = vec![
11300 "op".to_string(),
11301 "source".to_string(),
11302 "est_rows".to_string(),
11303 "est_cost".to_string(),
11304 "depth".to_string(),
11305 ];
11306
11307 let mut records: Vec<crate::storage::query::unified::UnifiedRecord> = Vec::new();
11308
11309 for name in &explain.cte_materializations {
11315 use std::sync::Arc;
11316 let mut rec = crate::storage::query::unified::UnifiedRecord::default();
11317 rec.set_arc(Arc::from("op"), Value::text("CteScan".to_string()));
11318 rec.set_arc(Arc::from("source"), Value::text(name.clone()));
11319 rec.set_arc(Arc::from("est_rows"), Value::Float(0.0));
11320 rec.set_arc(Arc::from("est_cost"), Value::Float(0.0));
11321 rec.set_arc(Arc::from("depth"), Value::Integer(0));
11322 records.push(rec);
11323 }
11324
11325 walk_plan_node(&explain.logical_plan.root, 0, &mut records);
11326
11327 let result = crate::storage::query::unified::UnifiedResult {
11328 columns,
11329 records,
11330 stats: Default::default(),
11331 pre_serialized_json: None,
11332 };
11333
11334 Ok(RuntimeQueryResult {
11335 query: raw_query.to_string(),
11336 mode: explain.mode,
11337 statement: "explain",
11338 engine: "runtime-explain",
11339 result,
11340 affected_rows: 0,
11341 statement_type: "select",
11342 bookmark: None,
11343 })
11344 }
11345
11346 pub(crate) fn check_query_privilege(
11354 &self,
11355 expr: &crate::storage::query::ast::QueryExpr,
11356 ) -> Result<(), String> {
11357 use crate::auth::privileges::{Action, AuthzContext, Resource};
11358 use crate::auth::UserId;
11359 use crate::storage::query::ast::QueryExpr;
11360
11361 let auth_store = match self.inner.auth_store.read().clone() {
11366 Some(s) => s,
11367 None => return Ok(()),
11368 };
11369
11370 let (username, role) = match current_auth_identity() {
11376 Some(p) => p,
11377 None => return Ok(()),
11378 };
11379 let tenant = current_tenant();
11380
11381 let ctx = AuthzContext {
11382 principal: &username,
11383 effective_role: role,
11384 tenant: tenant.as_deref(),
11385 };
11386 let principal_id = UserId::from_parts(tenant.as_deref(), &username);
11387
11388 let (action, resource) = match expr {
11390 QueryExpr::Table(t) => (Action::Select, Resource::table_from_name(&t.table)),
11391 QueryExpr::QueueSelect(q) => {
11392 return self.check_queue_op_privilege(
11393 &auth_store,
11394 &principal_id,
11395 role,
11396 tenant.as_deref(),
11397 "queue:peek",
11398 &q.queue,
11399 );
11400 }
11401 QueryExpr::QueueCommand(cmd) => {
11402 use crate::storage::query::ast::QueueCommand;
11403 let (queue, action_verb) = match cmd {
11404 QueueCommand::Push { queue, .. } => (queue.as_str(), "queue:enqueue"),
11405 QueueCommand::Pop { queue, .. }
11406 | QueueCommand::GroupRead { queue, .. }
11407 | QueueCommand::Claim { queue, .. } => (queue.as_str(), "queue:read"),
11408 QueueCommand::Peek { queue, .. }
11409 | QueueCommand::Len { queue }
11410 | QueueCommand::Pending { queue, .. } => (queue.as_str(), "queue:peek"),
11411 QueueCommand::Ack { queue, .. } => (queue.as_str(), "queue:ack"),
11412 QueueCommand::Nack {
11413 queue, delay_ms, ..
11414 } => {
11415 let verb = if delay_ms.is_some() {
11421 "queue:retry"
11422 } else {
11423 "queue:nack"
11424 };
11425 (queue.as_str(), verb)
11426 }
11427 QueueCommand::Purge { queue } => (queue.as_str(), "queue:purge"),
11428 QueueCommand::GroupCreate { queue, .. } => (queue.as_str(), "queue:read"),
11431 QueueCommand::Move { source, .. } => (source.as_str(), "queue:dlq:move"),
11432 };
11433 return self.check_queue_op_privilege(
11434 &auth_store,
11435 &principal_id,
11436 role,
11437 tenant.as_deref(),
11438 action_verb,
11439 queue,
11440 );
11441 }
11442 QueryExpr::Graph(g) => {
11443 self.check_graph_op_privilege(
11446 &auth_store,
11447 &principal_id,
11448 role,
11449 tenant.as_deref(),
11450 "graph:traverse",
11451 )?;
11452 if auth_store.iam_authorization_enabled() {
11453 self.check_graph_property_projection_privilege(
11454 &auth_store,
11455 &principal_id,
11456 role,
11457 tenant.as_deref(),
11458 g,
11459 )?;
11460 return Ok(());
11461 }
11462 return Ok(());
11463 }
11464 QueryExpr::Path(_) => {
11465 return self.check_graph_op_privilege(
11469 &auth_store,
11470 &principal_id,
11471 role,
11472 tenant.as_deref(),
11473 "graph:traverse",
11474 );
11475 }
11476 QueryExpr::GraphCommand(cmd) => {
11477 use crate::storage::query::ast::GraphCommand;
11478 let action_verb = match cmd {
11479 GraphCommand::Properties { .. } => "graph:read",
11481 GraphCommand::Neighborhood { .. }
11483 | GraphCommand::Traverse { .. }
11484 | GraphCommand::ShortestPath { .. } => "graph:traverse",
11485 GraphCommand::Centrality { .. }
11489 | GraphCommand::Community { .. }
11490 | GraphCommand::Components { .. }
11491 | GraphCommand::Cycles { .. }
11492 | GraphCommand::Clustering
11493 | GraphCommand::TopologicalSort => "graph:algorithm:run",
11494 };
11495 return self.check_graph_op_privilege(
11496 &auth_store,
11497 &principal_id,
11498 role,
11499 tenant.as_deref(),
11500 action_verb,
11501 );
11502 }
11503 QueryExpr::Vector(v) => {
11504 if auth_store.iam_authorization_enabled() {
11505 self.check_vector_op_privilege(
11506 &auth_store,
11507 &principal_id,
11508 role,
11509 tenant.as_deref(),
11510 "vector:search",
11511 &v.collection,
11512 )?;
11513 self.check_table_like_column_projection_privilege(
11514 &auth_store,
11515 &principal_id,
11516 role,
11517 tenant.as_deref(),
11518 &v.collection,
11519 &["content".to_string()],
11520 )?;
11521 return Ok(());
11522 }
11523 return Ok(());
11524 }
11525 QueryExpr::SearchCommand(cmd) => {
11526 use crate::storage::query::ast::SearchCommand;
11527 if auth_store.iam_authorization_enabled() {
11528 let collection = match cmd {
11534 SearchCommand::Similar { collection, .. }
11535 | SearchCommand::Hybrid { collection, .. } => Some(collection.as_str()),
11536 _ => None,
11537 };
11538 if let Some(c) = collection {
11539 self.check_vector_op_privilege(
11540 &auth_store,
11541 &principal_id,
11542 role,
11543 tenant.as_deref(),
11544 "vector:search",
11545 c,
11546 )?;
11547 return Ok(());
11548 }
11549 }
11550 return Ok(());
11551 }
11552 QueryExpr::Hybrid(h) => {
11553 if auth_store.iam_authorization_enabled() {
11554 self.check_vector_op_privilege(
11562 &auth_store,
11563 &principal_id,
11564 role,
11565 tenant.as_deref(),
11566 "vector:search",
11567 &h.vector.collection,
11568 )?;
11569 return Ok(());
11570 }
11571 return Ok(());
11572 }
11573 QueryExpr::Insert(i) => (Action::Insert, Resource::table_from_name(&i.table)),
11574 QueryExpr::Update(u) => (Action::Update, Resource::table_from_name(&u.table)),
11575 QueryExpr::Delete(d) => (Action::Delete, Resource::table_from_name(&d.table)),
11576 QueryExpr::Join(_) => (Action::Select, Resource::Database),
11580 QueryExpr::Grant(_) | QueryExpr::Revoke(_) | QueryExpr::AlterUser(_) => {
11583 return if role == crate::auth::Role::Admin {
11584 Ok(())
11585 } else {
11586 Err(format!(
11587 "principal=`{}` role=`{:?}` cannot issue ACL/auth DDL",
11588 username, role
11589 ))
11590 };
11591 }
11592 QueryExpr::CreateIamPolicy { id, .. } => {
11593 return self.check_policy_management_privilege(
11594 &auth_store,
11595 &principal_id,
11596 role,
11597 tenant.as_deref(),
11598 "policy:put",
11599 "policy",
11600 id,
11601 );
11602 }
11603 QueryExpr::DropIamPolicy { id } => {
11604 return self.check_policy_management_privilege(
11605 &auth_store,
11606 &principal_id,
11607 role,
11608 tenant.as_deref(),
11609 "policy:drop",
11610 "policy",
11611 id,
11612 );
11613 }
11614 QueryExpr::AttachPolicy { policy_id, .. } => {
11615 return self.check_policy_management_privilege(
11616 &auth_store,
11617 &principal_id,
11618 role,
11619 tenant.as_deref(),
11620 "policy:attach",
11621 "policy",
11622 policy_id,
11623 );
11624 }
11625 QueryExpr::DetachPolicy { policy_id, .. } => {
11626 return self.check_policy_management_privilege(
11627 &auth_store,
11628 &principal_id,
11629 role,
11630 tenant.as_deref(),
11631 "policy:detach",
11632 "policy",
11633 policy_id,
11634 );
11635 }
11636 QueryExpr::ShowPolicies { .. } | QueryExpr::ShowEffectivePermissions { .. } => {
11637 return Ok(());
11638 }
11639 QueryExpr::SimulatePolicy { .. } => {
11640 return self.check_policy_management_privilege(
11641 &auth_store,
11642 &principal_id,
11643 role,
11644 tenant.as_deref(),
11645 "policy:simulate",
11646 "policy",
11647 "*",
11648 );
11649 }
11650 QueryExpr::LintPolicy { .. } => {
11651 return self.check_policy_management_privilege(
11654 &auth_store,
11655 &principal_id,
11656 role,
11657 tenant.as_deref(),
11658 "policy:simulate",
11659 "policy",
11660 "*",
11661 );
11662 }
11663 QueryExpr::MigratePolicyMode { dry_run, .. } => {
11664 let action = if *dry_run {
11669 "policy:simulate"
11670 } else {
11671 "policy:put"
11672 };
11673 return self.check_policy_management_privilege(
11674 &auth_store,
11675 &principal_id,
11676 role,
11677 tenant.as_deref(),
11678 action,
11679 "policy",
11680 "*",
11681 );
11682 }
11683 QueryExpr::DropTable(q) => {
11686 return self.check_ddl_collection_privilege(
11687 &auth_store,
11688 &principal_id,
11689 role,
11690 tenant.as_deref(),
11691 &username,
11692 "drop",
11693 &q.name,
11694 );
11695 }
11696 QueryExpr::DropGraph(q) => {
11697 return self.check_ddl_collection_privilege(
11698 &auth_store,
11699 &principal_id,
11700 role,
11701 tenant.as_deref(),
11702 &username,
11703 "drop",
11704 &q.name,
11705 );
11706 }
11707 QueryExpr::DropVector(q) => {
11708 return self.check_ddl_collection_privilege(
11709 &auth_store,
11710 &principal_id,
11711 role,
11712 tenant.as_deref(),
11713 &username,
11714 "drop",
11715 &q.name,
11716 );
11717 }
11718 QueryExpr::DropDocument(q) => {
11719 return self.check_ddl_collection_privilege(
11720 &auth_store,
11721 &principal_id,
11722 role,
11723 tenant.as_deref(),
11724 &username,
11725 "drop",
11726 &q.name,
11727 );
11728 }
11729 QueryExpr::DropKv(q) => {
11730 return self.check_ddl_collection_privilege(
11731 &auth_store,
11732 &principal_id,
11733 role,
11734 tenant.as_deref(),
11735 &username,
11736 "drop",
11737 &q.name,
11738 );
11739 }
11740 QueryExpr::DropCollection(q) => {
11741 return self.check_ddl_collection_privilege(
11742 &auth_store,
11743 &principal_id,
11744 role,
11745 tenant.as_deref(),
11746 &username,
11747 "drop",
11748 &q.name,
11749 );
11750 }
11751 QueryExpr::Truncate(q) => {
11752 return self.check_ddl_collection_privilege(
11753 &auth_store,
11754 &principal_id,
11755 role,
11756 tenant.as_deref(),
11757 &username,
11758 "truncate",
11759 &q.name,
11760 );
11761 }
11762 QueryExpr::CreateTable(q) => {
11774 return self.check_ddl_object_privilege(
11775 &auth_store,
11776 &principal_id,
11777 role,
11778 tenant.as_deref(),
11779 &username,
11780 "create",
11781 "collection",
11782 &q.name,
11783 crate::auth::Role::Write,
11784 );
11785 }
11786 QueryExpr::CreateCollection(q) => {
11787 return self.check_ddl_object_privilege(
11788 &auth_store,
11789 &principal_id,
11790 role,
11791 tenant.as_deref(),
11792 &username,
11793 "create",
11794 "collection",
11795 &q.name,
11796 crate::auth::Role::Write,
11797 );
11798 }
11799 QueryExpr::CreateVector(q) => {
11800 return self.check_ddl_object_privilege(
11801 &auth_store,
11802 &principal_id,
11803 role,
11804 tenant.as_deref(),
11805 &username,
11806 "create",
11807 "collection",
11808 &q.name,
11809 crate::auth::Role::Write,
11810 );
11811 }
11812 QueryExpr::AlterTable(q) => {
11813 return self.check_ddl_object_privilege(
11814 &auth_store,
11815 &principal_id,
11816 role,
11817 tenant.as_deref(),
11818 &username,
11819 "alter",
11820 "collection",
11821 &q.name,
11822 crate::auth::Role::Write,
11823 );
11824 }
11825 QueryExpr::CreateIndex(q) => {
11826 return self.check_ddl_object_privilege(
11827 &auth_store,
11828 &principal_id,
11829 role,
11830 tenant.as_deref(),
11831 &username,
11832 "create",
11833 "collection",
11834 &q.table,
11835 crate::auth::Role::Write,
11836 );
11837 }
11838 QueryExpr::DropIndex(q) => {
11839 return self.check_ddl_object_privilege(
11840 &auth_store,
11841 &principal_id,
11842 role,
11843 tenant.as_deref(),
11844 &username,
11845 "drop",
11846 "collection",
11847 &q.table,
11848 crate::auth::Role::Write,
11849 );
11850 }
11851 QueryExpr::CreateSchema(q) => {
11852 return self.check_ddl_object_privilege(
11853 &auth_store,
11854 &principal_id,
11855 role,
11856 tenant.as_deref(),
11857 &username,
11858 "schema:admin",
11859 "schema",
11860 &q.name,
11861 crate::auth::Role::Admin,
11862 );
11863 }
11864 QueryExpr::DropSchema(q) => {
11865 return self.check_ddl_object_privilege(
11866 &auth_store,
11867 &principal_id,
11868 role,
11869 tenant.as_deref(),
11870 &username,
11871 "schema:admin",
11872 "schema",
11873 &q.name,
11874 crate::auth::Role::Admin,
11875 );
11876 }
11877 QueryExpr::CreateSequence(q) => {
11878 return self.check_ddl_object_privilege(
11879 &auth_store,
11880 &principal_id,
11881 role,
11882 tenant.as_deref(),
11883 &username,
11884 "create",
11885 "collection",
11886 &q.name,
11887 crate::auth::Role::Write,
11888 );
11889 }
11890 QueryExpr::DropSequence(q) => {
11891 return self.check_ddl_object_privilege(
11892 &auth_store,
11893 &principal_id,
11894 role,
11895 tenant.as_deref(),
11896 &username,
11897 "drop",
11898 "collection",
11899 &q.name,
11900 crate::auth::Role::Write,
11901 );
11902 }
11903 QueryExpr::CreateView(q) => {
11904 return self.check_ddl_object_privilege(
11905 &auth_store,
11906 &principal_id,
11907 role,
11908 tenant.as_deref(),
11909 &username,
11910 "create",
11911 "collection",
11912 &q.name,
11913 crate::auth::Role::Write,
11914 );
11915 }
11916 QueryExpr::DropView(q) => {
11917 return self.check_ddl_object_privilege(
11918 &auth_store,
11919 &principal_id,
11920 role,
11921 tenant.as_deref(),
11922 &username,
11923 "drop",
11924 "collection",
11925 &q.name,
11926 crate::auth::Role::Write,
11927 );
11928 }
11929 QueryExpr::RefreshMaterializedView(q) => {
11930 return self.check_ddl_object_privilege(
11931 &auth_store,
11932 &principal_id,
11933 role,
11934 tenant.as_deref(),
11935 &username,
11936 "alter",
11937 "collection",
11938 &q.name,
11939 crate::auth::Role::Write,
11940 );
11941 }
11942 QueryExpr::CreatePolicy(q) => {
11943 return self.check_ddl_object_privilege(
11944 &auth_store,
11945 &principal_id,
11946 role,
11947 tenant.as_deref(),
11948 &username,
11949 "create",
11950 "collection",
11951 &q.table,
11952 crate::auth::Role::Write,
11953 );
11954 }
11955 QueryExpr::DropPolicy(q) => {
11956 return self.check_ddl_object_privilege(
11957 &auth_store,
11958 &principal_id,
11959 role,
11960 tenant.as_deref(),
11961 &username,
11962 "drop",
11963 "collection",
11964 &q.table,
11965 crate::auth::Role::Write,
11966 );
11967 }
11968 QueryExpr::CreateServer(q) => {
11969 return self.check_ddl_object_privilege(
11970 &auth_store,
11971 &principal_id,
11972 role,
11973 tenant.as_deref(),
11974 &username,
11975 "schema:admin",
11976 "schema",
11977 &q.name,
11978 crate::auth::Role::Admin,
11979 );
11980 }
11981 QueryExpr::DropServer(q) => {
11982 return self.check_ddl_object_privilege(
11983 &auth_store,
11984 &principal_id,
11985 role,
11986 tenant.as_deref(),
11987 &username,
11988 "schema:admin",
11989 "schema",
11990 &q.name,
11991 crate::auth::Role::Admin,
11992 );
11993 }
11994 QueryExpr::CreateForeignTable(q) => {
11995 return self.check_ddl_object_privilege(
11996 &auth_store,
11997 &principal_id,
11998 role,
11999 tenant.as_deref(),
12000 &username,
12001 "schema:write",
12002 "schema",
12003 &q.name,
12004 crate::auth::Role::Write,
12005 );
12006 }
12007 QueryExpr::DropForeignTable(q) => {
12008 return self.check_ddl_object_privilege(
12009 &auth_store,
12010 &principal_id,
12011 role,
12012 tenant.as_deref(),
12013 &username,
12014 "schema:write",
12015 "schema",
12016 &q.name,
12017 crate::auth::Role::Write,
12018 );
12019 }
12020 QueryExpr::CreateTimeSeries(q) => {
12021 return self.check_ddl_object_privilege(
12022 &auth_store,
12023 &principal_id,
12024 role,
12025 tenant.as_deref(),
12026 &username,
12027 "create",
12028 "collection",
12029 &q.name,
12030 crate::auth::Role::Write,
12031 );
12032 }
12033 QueryExpr::CreateMetric(q) => {
12034 return self.check_ddl_object_privilege(
12035 &auth_store,
12036 &principal_id,
12037 role,
12038 tenant.as_deref(),
12039 &username,
12040 "create",
12041 "collection",
12042 &q.path,
12043 crate::auth::Role::Write,
12044 );
12045 }
12046 QueryExpr::AlterMetric(q) => {
12047 return self.check_ddl_object_privilege(
12048 &auth_store,
12049 &principal_id,
12050 role,
12051 tenant.as_deref(),
12052 &username,
12053 "alter",
12054 "collection",
12055 &q.path,
12056 crate::auth::Role::Write,
12057 );
12058 }
12059 QueryExpr::CreateSlo(q) => {
12060 return self.check_ddl_object_privilege(
12061 &auth_store,
12062 &principal_id,
12063 role,
12064 tenant.as_deref(),
12065 &username,
12066 "create",
12067 "collection",
12068 &q.path,
12069 crate::auth::Role::Write,
12070 );
12071 }
12072 QueryExpr::DropTimeSeries(q) => {
12073 return self.check_ddl_object_privilege(
12074 &auth_store,
12075 &principal_id,
12076 role,
12077 tenant.as_deref(),
12078 &username,
12079 "drop",
12080 "collection",
12081 &q.name,
12082 crate::auth::Role::Write,
12083 );
12084 }
12085 QueryExpr::CreateQueue(q) => {
12086 return self.check_ddl_object_privilege(
12087 &auth_store,
12088 &principal_id,
12089 role,
12090 tenant.as_deref(),
12091 &username,
12092 "create",
12093 "collection",
12094 &q.name,
12095 crate::auth::Role::Write,
12096 );
12097 }
12098 QueryExpr::AlterQueue(q) => {
12099 return self.check_ddl_object_privilege(
12100 &auth_store,
12101 &principal_id,
12102 role,
12103 tenant.as_deref(),
12104 &username,
12105 "alter",
12106 "collection",
12107 &q.name,
12108 crate::auth::Role::Write,
12109 );
12110 }
12111 QueryExpr::DropQueue(q) => {
12112 return self.check_ddl_object_privilege(
12113 &auth_store,
12114 &principal_id,
12115 role,
12116 tenant.as_deref(),
12117 &username,
12118 "drop",
12119 "collection",
12120 &q.name,
12121 crate::auth::Role::Write,
12122 );
12123 }
12124 QueryExpr::CreateTree(q) => {
12125 return self.check_ddl_object_privilege(
12126 &auth_store,
12127 &principal_id,
12128 role,
12129 tenant.as_deref(),
12130 &username,
12131 "create",
12132 "collection",
12133 &q.collection,
12134 crate::auth::Role::Write,
12135 );
12136 }
12137 QueryExpr::DropTree(q) => {
12138 return self.check_ddl_object_privilege(
12139 &auth_store,
12140 &principal_id,
12141 role,
12142 tenant.as_deref(),
12143 &username,
12144 "drop",
12145 "collection",
12146 &q.collection,
12147 crate::auth::Role::Write,
12148 );
12149 }
12150 QueryExpr::CreateMigration(q) => {
12154 return self.check_ddl_object_privilege(
12155 &auth_store,
12156 &principal_id,
12157 role,
12158 tenant.as_deref(),
12159 &username,
12160 "schema:write",
12161 "schema",
12162 &q.name,
12163 crate::auth::Role::Write,
12164 );
12165 }
12166 QueryExpr::ApplyMigration(_) | QueryExpr::RollbackMigration(_) => {
12168 return if role == crate::auth::Role::Admin {
12169 Ok(())
12170 } else {
12171 Err(format!(
12172 "principal=`{}` role=`{:?}` cannot issue APPLY/ROLLBACK MIGRATION",
12173 username, role
12174 ))
12175 };
12176 }
12177 QueryExpr::ExplainMigration(_) => return Ok(()),
12179 _ => return Ok(()),
12183 };
12184
12185 if auth_store.iam_authorization_enabled() {
12186 let iam_action = legacy_action_to_iam(action);
12187 let iam_resource = legacy_resource_to_iam(&resource, tenant.as_deref());
12188 let iam_ctx = runtime_iam_context(
12189 role,
12190 tenant.as_deref(),
12191 auth_store.principal_is_system_owned(&principal_id),
12192 );
12193 if !auth_store.check_policy_authz_with_role(
12194 &principal_id,
12195 iam_action,
12196 &iam_resource,
12197 &iam_ctx,
12198 role,
12199 ) {
12200 return Err(format!(
12201 "principal=`{}` action=`{}` resource=`{}:{}` denied by IAM policy",
12202 username, iam_action, iam_resource.kind, iam_resource.name
12203 ));
12204 }
12205
12206 if let QueryExpr::Table(table) = expr {
12207 self.check_table_column_projection_privilege(
12208 &auth_store,
12209 &principal_id,
12210 &iam_ctx,
12211 table,
12212 )?;
12213 }
12214
12215 if let QueryExpr::Update(update) = expr {
12216 let columns = update_set_target_columns(update);
12217 if !columns.is_empty() {
12218 let request = column_access_request_for_table_update(&update.table, columns);
12219 let outcome =
12220 auth_store.check_column_projection_authz(&principal_id, &request, &iam_ctx);
12221 if let Some(denied) = outcome.first_denied_column() {
12222 return Err(format!(
12223 "principal=`{}` action=`{}` resource=`{}:{}` denied by IAM column policy",
12224 username, iam_action, denied.resource.kind, denied.resource.name
12225 ));
12226 }
12227 if !outcome.allowed() {
12228 return Err(format!(
12229 "principal=`{}` action=`{}` resource=`{}:{}` denied by IAM policy",
12230 username,
12231 iam_action,
12232 outcome.table_resource.kind,
12233 outcome.table_resource.name
12234 ));
12235 }
12236 }
12237
12238 if let Some(columns) = update_returning_columns_for_policy(self, update) {
12239 let request = column_access_request_for_table_select(&update.table, columns);
12240 let outcome =
12241 auth_store.check_column_projection_authz(&principal_id, &request, &iam_ctx);
12242 if let Some(denied) = outcome.first_denied_column() {
12243 return Err(format!(
12244 "principal=`{}` action=`select` resource=`{}:{}` denied by IAM column policy",
12245 username, denied.resource.kind, denied.resource.name
12246 ));
12247 }
12248 if !outcome.allowed() {
12249 return Err(format!(
12250 "principal=`{}` action=`select` resource=`{}:{}` denied by IAM policy",
12251 username, outcome.table_resource.kind, outcome.table_resource.name
12252 ));
12253 }
12254 }
12255 }
12256
12257 Ok(())
12258 } else {
12259 auth_store
12260 .check_grant(&ctx, action, &resource)
12261 .map_err(|e| e.to_string())
12262 }
12263 }
12264
12265 fn check_table_column_projection_privilege(
12266 &self,
12267 auth_store: &Arc<crate::auth::store::AuthStore>,
12268 principal: &crate::auth::UserId,
12269 ctx: &crate::auth::policies::EvalContext,
12270 table: &crate::storage::query::ast::TableQuery,
12271 ) -> Result<(), String> {
12272 use crate::auth::{ColumnAccessRequest, ColumnDecisionEffect};
12273
12274 let columns = requested_table_columns_for_policy(table);
12275 if columns.is_empty() {
12276 return Ok(());
12277 }
12278
12279 let request = ColumnAccessRequest::select(table.table.clone(), columns);
12280 let outcome = auth_store.check_column_projection_authz(principal, &request, ctx);
12281 if outcome.allowed() {
12282 return Ok(());
12283 }
12284
12285 if !matches!(
12286 outcome.table_decision,
12287 crate::auth::policies::Decision::Allow { .. }
12288 | crate::auth::policies::Decision::AdminBypass
12289 ) {
12290 return Err(format!(
12291 "principal=`{}` action=`select` resource=`{}:{}` denied by IAM policy",
12292 principal, outcome.table_resource.kind, outcome.table_resource.name
12293 ));
12294 }
12295
12296 let denied = outcome
12297 .first_denied_column()
12298 .filter(|decision| decision.effective == ColumnDecisionEffect::Denied);
12299 match denied {
12300 Some(decision) => Err(format!(
12301 "principal=`{}` action=`select` resource=`{}:{}` denied by IAM policy",
12302 principal, decision.resource.kind, decision.resource.name
12303 )),
12304 None => Ok(()),
12305 }
12306 }
12307
12308 fn check_graph_property_projection_privilege(
12309 &self,
12310 auth_store: &Arc<crate::auth::store::AuthStore>,
12311 principal: &crate::auth::UserId,
12312 role: crate::auth::Role,
12313 tenant: Option<&str>,
12314 query: &crate::storage::query::ast::GraphQuery,
12315 ) -> Result<(), String> {
12316 let columns = explicit_graph_projection_properties(query);
12317 if columns.is_empty() {
12318 return Ok(());
12319 }
12320 self.check_table_like_column_projection_privilege(
12321 auth_store, principal, role, tenant, "graph", &columns,
12322 )
12323 }
12324
12325 fn check_table_like_column_projection_privilege(
12326 &self,
12327 auth_store: &Arc<crate::auth::store::AuthStore>,
12328 principal: &crate::auth::UserId,
12329 role: crate::auth::Role,
12330 tenant: Option<&str>,
12331 table: &str,
12332 columns: &[String],
12333 ) -> Result<(), String> {
12334 let iam_ctx = runtime_iam_context(
12335 role,
12336 tenant,
12337 auth_store.principal_is_system_owned(principal),
12338 );
12339 let request =
12340 crate::auth::ColumnAccessRequest::select(table.to_string(), columns.iter().cloned());
12341 let outcome = auth_store.check_column_projection_authz(principal, &request, &iam_ctx);
12342 if outcome.allowed() {
12343 return Ok(());
12344 }
12345 let denied = outcome
12346 .first_denied_column()
12347 .map(|d| d.resource.name.clone())
12348 .unwrap_or_else(|| format!("{table}.<unknown>"));
12349 Err(format!(
12350 "principal=`{}` action=`select` resource=`column:{}` denied by IAM policy",
12351 principal, denied
12352 ))
12353 }
12354
12355 fn check_policy_management_privilege(
12356 &self,
12357 auth_store: &Arc<crate::auth::store::AuthStore>,
12358 principal: &crate::auth::UserId,
12359 role: crate::auth::Role,
12360 tenant: Option<&str>,
12361 action: &str,
12362 resource_kind: &str,
12363 resource_name: &str,
12364 ) -> Result<(), String> {
12365 let ctx = runtime_iam_context(
12366 role,
12367 tenant,
12368 auth_store.principal_is_system_owned(principal),
12369 );
12370
12371 if !auth_store.iam_authorization_enabled() {
12372 return if role == crate::auth::Role::Admin {
12373 Ok(())
12374 } else {
12375 Err(format!(
12376 "principal=`{}` role=`{:?}` cannot issue ACL/auth DDL",
12377 principal, role
12378 ))
12379 };
12380 }
12381
12382 let mut resource = crate::auth::policies::ResourceRef::new(
12383 resource_kind.to_string(),
12384 resource_name.to_string(),
12385 );
12386 if let Some(t) = tenant {
12387 resource = resource.with_tenant(t.to_string());
12388 }
12389 if auth_store.check_policy_authz_with_role(principal, action, &resource, &ctx, role) {
12390 Ok(())
12391 } else {
12392 Err(format!(
12393 "principal=`{}` action=`{}` resource=`{}:{}` denied by IAM policy",
12394 principal, action, resource.kind, resource.name
12395 ))
12396 }
12397 }
12398
12399 fn check_managed_config_write_for_set_config(&self, key: &str) -> RedDBResult<()> {
12400 let Some(auth_store) = self.inner.auth_store.read().clone() else {
12401 return Ok(());
12402 };
12403 let (username, role) = current_auth_identity()
12404 .unwrap_or_else(|| ("anonymous".to_string(), crate::auth::Role::Read));
12405 let tenant = current_tenant();
12406 let principal = crate::auth::UserId::from_parts(tenant.as_deref(), &username);
12407 let ctx = runtime_iam_context(
12408 role,
12409 tenant.as_deref(),
12410 auth_store.principal_is_system_owned(&principal),
12411 );
12412 let gate = crate::auth::managed_config::ManagedConfigGate::new(
12413 self.inner.config_registry.as_ref(),
12414 );
12415 match gate.check_write(&auth_store, &principal, &ctx, key) {
12416 crate::auth::managed_config::ManagedConfigDecision::PassThrough { .. }
12417 | crate::auth::managed_config::ManagedConfigDecision::Allow { .. } => Ok(()),
12418 crate::auth::managed_config::ManagedConfigDecision::Deny { reason, .. } => {
12419 Err(RedDBError::Query(format!(
12420 "permission denied: managed config mutation blocked for `{key}`: {reason}"
12421 )))
12422 }
12423 }
12424 }
12425
12426 fn check_queue_op_privilege(
12442 &self,
12443 auth_store: &Arc<crate::auth::store::AuthStore>,
12444 principal: &crate::auth::UserId,
12445 role: crate::auth::Role,
12446 tenant: Option<&str>,
12447 action: &str,
12448 queue: &str,
12449 ) -> Result<(), String> {
12450 if !auth_store.iam_authorization_enabled() {
12451 return Ok(());
12452 }
12453 let mut resource =
12454 crate::auth::policies::ResourceRef::new("queue".to_string(), queue.to_string());
12455 if let Some(t) = tenant {
12456 resource = resource.with_tenant(t.to_string());
12457 }
12458 let ctx = runtime_iam_context(
12459 role,
12460 tenant,
12461 auth_store.principal_is_system_owned(principal),
12462 );
12463 if auth_store.check_policy_authz_with_role(principal, action, &resource, &ctx, role) {
12464 Ok(())
12465 } else {
12466 Err(format!(
12467 "principal=`{}` action=`{}` resource=`queue:{}` denied by IAM policy",
12468 principal, action, queue
12469 ))
12470 }
12471 }
12472
12473 fn check_graph_op_privilege(
12493 &self,
12494 auth_store: &Arc<crate::auth::store::AuthStore>,
12495 principal: &crate::auth::UserId,
12496 role: crate::auth::Role,
12497 tenant: Option<&str>,
12498 action: &str,
12499 ) -> Result<(), String> {
12500 if !auth_store.iam_authorization_enabled() {
12501 return Ok(());
12502 }
12503 let mut resource =
12504 crate::auth::policies::ResourceRef::new("graph".to_string(), "*".to_string());
12505 if let Some(t) = tenant {
12506 resource = resource.with_tenant(t.to_string());
12507 }
12508 let ctx = runtime_iam_context(
12509 role,
12510 tenant,
12511 auth_store.principal_is_system_owned(principal),
12512 );
12513 if auth_store.check_policy_authz_with_role(principal, action, &resource, &ctx, role) {
12514 Ok(())
12515 } else {
12516 Err(format!(
12517 "principal=`{}` action=`{}` resource=`graph:*` denied by IAM policy",
12518 principal, action
12519 ))
12520 }
12521 }
12522
12523 fn check_vector_op_privilege(
12538 &self,
12539 auth_store: &Arc<crate::auth::store::AuthStore>,
12540 principal: &crate::auth::UserId,
12541 role: crate::auth::Role,
12542 tenant: Option<&str>,
12543 action: &str,
12544 collection: &str,
12545 ) -> Result<(), String> {
12546 if !auth_store.iam_authorization_enabled() {
12547 return Ok(());
12548 }
12549 let mut resource =
12550 crate::auth::policies::ResourceRef::new("vector".to_string(), collection.to_string());
12551 if let Some(t) = tenant {
12552 resource = resource.with_tenant(t.to_string());
12553 }
12554 let ctx = runtime_iam_context(
12555 role,
12556 tenant,
12557 auth_store.principal_is_system_owned(principal),
12558 );
12559 if auth_store.check_policy_authz_with_role(principal, action, &resource, &ctx, role) {
12560 Ok(())
12561 } else {
12562 Err(format!(
12563 "principal=`{}` action=`{}` resource=`vector:{}` denied by IAM policy",
12564 principal, action, collection
12565 ))
12566 }
12567 }
12568
12569 fn check_ddl_collection_privilege(
12575 &self,
12576 auth_store: &Arc<crate::auth::store::AuthStore>,
12577 principal: &crate::auth::UserId,
12578 role: crate::auth::Role,
12579 tenant: Option<&str>,
12580 username: &str,
12581 action: &str,
12582 collection: &str,
12583 ) -> Result<(), String> {
12584 self.check_ddl_object_privilege(
12585 auth_store,
12586 principal,
12587 role,
12588 tenant,
12589 username,
12590 action,
12591 "collection",
12592 collection,
12593 crate::auth::Role::Write,
12594 )
12595 }
12596
12597 #[allow(clippy::too_many_arguments)]
12615 fn check_ddl_object_privilege(
12616 &self,
12617 auth_store: &Arc<crate::auth::store::AuthStore>,
12618 principal: &crate::auth::UserId,
12619 role: crate::auth::Role,
12620 tenant: Option<&str>,
12621 username: &str,
12622 action: &str,
12623 resource_kind: &str,
12624 resource_name: &str,
12625 min_role: crate::auth::Role,
12626 ) -> Result<(), String> {
12627 if role < min_role {
12628 let msg = format!(
12629 "principal=`{}` role=`{:?}` cannot issue DDL action=`{}` resource=`{}:{}`",
12630 username, role, action, resource_kind, resource_name
12631 );
12632 self.inner.audit_log.record(
12633 action,
12634 username,
12635 resource_name,
12636 "denied",
12637 crate::json::Value::Null,
12638 );
12639 return Err(msg);
12640 }
12641
12642 if !auth_store.iam_authorization_enabled() {
12643 self.inner.audit_log.record(
12644 action,
12645 username,
12646 resource_name,
12647 "ok",
12648 crate::json::Value::Null,
12649 );
12650 return Ok(());
12651 }
12652
12653 let mut resource = crate::auth::policies::ResourceRef::new(
12654 resource_kind.to_string(),
12655 resource_name.to_string(),
12656 );
12657 if let Some(t) = tenant {
12658 resource = resource.with_tenant(t.to_string());
12659 }
12660 let ctx = runtime_iam_context(
12661 role,
12662 tenant,
12663 auth_store.principal_is_system_owned(principal),
12664 );
12665 if auth_store.check_policy_authz_with_role(principal, action, &resource, &ctx, role) {
12666 self.inner.audit_log.record(
12667 action,
12668 username,
12669 resource_name,
12670 "ok",
12671 crate::json::Value::Null,
12672 );
12673 Ok(())
12674 } else {
12675 self.inner.audit_log.record(
12676 action,
12677 username,
12678 resource_name,
12679 "denied",
12680 crate::json::Value::Null,
12681 );
12682 Err(format!(
12683 "principal=`{}` action=`{}` resource=`{}:{}` denied by IAM policy",
12684 username, action, resource_kind, resource_name
12685 ))
12686 }
12687 }
12688
12689 fn execute_grant_statement(
12691 &self,
12692 query: &str,
12693 stmt: &crate::storage::query::ast::GrantStmt,
12694 ) -> RedDBResult<RuntimeQueryResult> {
12695 use crate::auth::privileges::{Action, GrantPrincipal, Resource};
12696 use crate::auth::UserId;
12697 use crate::storage::query::ast::{GrantObjectKind, GrantPrincipalRef};
12698
12699 let auth_store = self
12700 .inner
12701 .auth_store
12702 .read()
12703 .clone()
12704 .ok_or_else(|| RedDBError::Query("auth store not configured".to_string()))?;
12705
12706 let (gname, grole) = current_auth_identity().ok_or_else(|| {
12708 RedDBError::Query("GRANT requires an authenticated principal".to_string())
12709 })?;
12710 let granter = UserId::from_parts(current_tenant().as_deref(), &gname);
12711 let granter_role = grole;
12712
12713 let mut actions: Vec<Action> = Vec::new();
12715 if stmt.all {
12716 actions.push(Action::All);
12717 } else {
12718 for kw in &stmt.actions {
12719 let a = Action::from_keyword(kw).ok_or_else(|| {
12720 RedDBError::Query(format!("unknown privilege keyword `{}`", kw))
12721 })?;
12722 actions.push(a);
12723 }
12724 }
12725
12726 let mut applied = 0usize;
12728 for obj in &stmt.objects {
12729 let resource = match stmt.object_kind {
12730 GrantObjectKind::Table => Resource::Table {
12731 schema: obj.schema.clone(),
12732 table: obj.name.clone(),
12733 },
12734 GrantObjectKind::Schema => Resource::Schema(obj.name.clone()),
12735 GrantObjectKind::Database => Resource::Database,
12736 GrantObjectKind::Function => Resource::Function {
12737 schema: obj.schema.clone(),
12738 name: obj.name.clone(),
12739 },
12740 };
12741 for principal in &stmt.principals {
12742 let p = match principal {
12743 GrantPrincipalRef::Public => GrantPrincipal::Public,
12744 GrantPrincipalRef::Group(g) => GrantPrincipal::Group(g.clone()),
12745 GrantPrincipalRef::User { tenant, name } => {
12746 GrantPrincipal::User(UserId::from_parts(tenant.as_deref(), name))
12747 }
12748 };
12749 let tenant = granter.tenant.clone();
12752 auth_store
12753 .grant(
12754 &granter,
12755 granter_role,
12756 p.clone(),
12757 resource.clone(),
12758 actions.clone(),
12759 stmt.with_grant_option,
12760 tenant.clone(),
12761 )
12762 .map_err(|e| RedDBError::Query(e.to_string()))?;
12763
12764 if let Some(policy) =
12768 grant_to_iam_policy(&p, &resource, &actions, tenant.as_deref())
12769 {
12770 let pid = policy.id.clone();
12771 auth_store
12772 .put_policy_internal(policy)
12773 .map_err(|e| RedDBError::Query(e.to_string()))?;
12774 let attachment = match &p {
12775 GrantPrincipal::User(uid) => {
12776 crate::auth::store::PrincipalRef::User(uid.clone())
12777 }
12778 GrantPrincipal::Group(group) => {
12779 crate::auth::store::PrincipalRef::Group(group.clone())
12780 }
12781 GrantPrincipal::Public => crate::auth::store::PrincipalRef::Group(
12782 crate::auth::store::PUBLIC_IAM_GROUP.to_string(),
12783 ),
12784 };
12785 auth_store
12786 .attach_policy(attachment, &pid)
12787 .map_err(|e| RedDBError::Query(e.to_string()))?;
12788 }
12789 applied += 1;
12790 tracing::info!(
12791 target: "audit",
12792 principal = %granter,
12793 action = "grant",
12794 "GRANT applied"
12795 );
12796 }
12797 }
12798
12799 self.invalidate_result_cache();
12800 Ok(RuntimeQueryResult::ok_message(
12801 query.to_string(),
12802 &format!("GRANT applied to {} target(s)", applied),
12803 "grant",
12804 ))
12805 }
12806
12807 fn execute_revoke_statement(
12809 &self,
12810 query: &str,
12811 stmt: &crate::storage::query::ast::RevokeStmt,
12812 ) -> RedDBResult<RuntimeQueryResult> {
12813 use crate::auth::privileges::{Action, GrantPrincipal, Resource};
12814 use crate::auth::UserId;
12815 use crate::storage::query::ast::{GrantObjectKind, GrantPrincipalRef};
12816
12817 let auth_store = self
12818 .inner
12819 .auth_store
12820 .read()
12821 .clone()
12822 .ok_or_else(|| RedDBError::Query("auth store not configured".to_string()))?;
12823
12824 let (_gname, grole) = current_auth_identity().ok_or_else(|| {
12825 RedDBError::Query("REVOKE requires an authenticated principal".to_string())
12826 })?;
12827 let granter_role = grole;
12828
12829 let actions: Vec<Action> = if stmt.all {
12830 vec![Action::All]
12831 } else {
12832 stmt.actions
12833 .iter()
12834 .map(|kw| Action::from_keyword(kw).unwrap_or(Action::Select))
12835 .collect()
12836 };
12837
12838 let mut total_removed = 0usize;
12839 for obj in &stmt.objects {
12840 let resource = match stmt.object_kind {
12841 GrantObjectKind::Table => Resource::Table {
12842 schema: obj.schema.clone(),
12843 table: obj.name.clone(),
12844 },
12845 GrantObjectKind::Schema => Resource::Schema(obj.name.clone()),
12846 GrantObjectKind::Database => Resource::Database,
12847 GrantObjectKind::Function => Resource::Function {
12848 schema: obj.schema.clone(),
12849 name: obj.name.clone(),
12850 },
12851 };
12852 for principal in &stmt.principals {
12853 let p = match principal {
12854 GrantPrincipalRef::Public => GrantPrincipal::Public,
12855 GrantPrincipalRef::Group(g) => GrantPrincipal::Group(g.clone()),
12856 GrantPrincipalRef::User { tenant, name } => {
12857 GrantPrincipal::User(UserId::from_parts(tenant.as_deref(), name))
12858 }
12859 };
12860 let removed = auth_store
12861 .revoke(granter_role, &p, &resource, &actions)
12862 .map_err(|e| RedDBError::Query(e.to_string()))?;
12863 let _removed_policies =
12864 auth_store.delete_synthetic_grant_policies(&p, &resource, &actions);
12865 total_removed += removed;
12866 }
12867 }
12868
12869 self.invalidate_result_cache();
12870 Ok(RuntimeQueryResult::ok_message(
12871 query.to_string(),
12872 &format!("REVOKE removed {} grant(s)", total_removed),
12873 "revoke",
12874 ))
12875 }
12876
12877 fn execute_alter_user_statement(
12879 &self,
12880 query: &str,
12881 stmt: &crate::storage::query::ast::AlterUserStmt,
12882 ) -> RedDBResult<RuntimeQueryResult> {
12883 use crate::auth::privileges::UserAttributes;
12884 use crate::auth::UserId;
12885 use crate::storage::query::ast::AlterUserAttribute;
12886
12887 let auth_store = self
12888 .inner
12889 .auth_store
12890 .read()
12891 .clone()
12892 .ok_or_else(|| RedDBError::Query("auth store not configured".to_string()))?;
12893
12894 let (_gname, grole) = current_auth_identity().ok_or_else(|| {
12895 RedDBError::Query("ALTER USER requires an authenticated principal".to_string())
12896 })?;
12897 if grole != crate::auth::Role::Admin {
12898 return Err(RedDBError::Query(
12899 "ALTER USER requires Admin role".to_string(),
12900 ));
12901 }
12902
12903 let target = UserId::from_parts(stmt.tenant.as_deref(), &stmt.username);
12904
12905 let mut attrs = auth_store.user_attributes(&target);
12908 let mut enable_change: Option<bool> = None;
12909
12910 for a in &stmt.attributes {
12911 match a {
12912 AlterUserAttribute::ValidUntil(ts) => {
12913 let ms = parse_timestamp_to_ms(ts).ok_or_else(|| {
12917 RedDBError::Query(format!("invalid VALID UNTIL timestamp `{ts}`"))
12918 })?;
12919 attrs.valid_until = Some(ms);
12920 }
12921 AlterUserAttribute::ConnectionLimit(n) => {
12922 if *n < 0 {
12923 return Err(RedDBError::Query(
12924 "CONNECTION LIMIT must be non-negative".to_string(),
12925 ));
12926 }
12927 attrs.connection_limit = Some(*n as u32);
12928 }
12929 AlterUserAttribute::SetSearchPath(p) => {
12930 attrs.search_path = Some(p.clone());
12931 }
12932 AlterUserAttribute::AddGroup(g) => {
12933 if !attrs.groups.iter().any(|existing| existing == g) {
12934 attrs.groups.push(g.clone());
12935 attrs.groups.sort();
12936 }
12937 }
12938 AlterUserAttribute::DropGroup(g) => {
12939 attrs.groups.retain(|existing| existing != g);
12940 }
12941 AlterUserAttribute::Enable => enable_change = Some(true),
12942 AlterUserAttribute::Disable => enable_change = Some(false),
12943 AlterUserAttribute::Password(_) => {
12944 }
12948 }
12949 }
12950
12951 auth_store
12952 .set_user_attributes(&target, attrs)
12953 .map_err(|e| RedDBError::Query(e.to_string()))?;
12954 if let Some(en) = enable_change {
12955 auth_store
12956 .set_user_enabled(&target, en)
12957 .map_err(|e| RedDBError::Query(e.to_string()))?;
12958 }
12959 self.invalidate_result_cache();
12960 tracing::info!(
12961 target: "audit",
12962 principal = %target,
12963 action = "alter_user",
12964 "ALTER USER applied"
12965 );
12966
12967 Ok(RuntimeQueryResult::ok_message(
12968 query.to_string(),
12969 &format!("ALTER USER {} applied", target),
12970 "alter_user",
12971 ))
12972 }
12973
12974 fn execute_create_iam_policy(
12979 &self,
12980 query: &str,
12981 id: &str,
12982 json: &str,
12983 ) -> RedDBResult<RuntimeQueryResult> {
12984 use crate::auth::policies::Policy;
12985
12986 let auth_store = self
12987 .inner
12988 .auth_store
12989 .read()
12990 .clone()
12991 .ok_or_else(|| RedDBError::Query("auth store not configured".to_string()))?;
12992
12993 let mut policy = Policy::from_json_str(json)
12998 .map_err(|e| RedDBError::Query(format!("policy parse: {e}")))?;
12999 if policy.id != id {
13000 policy.id = id.to_string();
13001 }
13002 let pid = policy.id.clone();
13003 let tenant = current_tenant();
13004 let (actor_name, actor_role) = current_auth_identity()
13005 .unwrap_or_else(|| ("anonymous".to_string(), crate::auth::Role::Read));
13006 let actor = crate::auth::UserId::from_parts(tenant.as_deref(), &actor_name);
13007 let eval_ctx = runtime_iam_context(
13008 actor_role,
13009 tenant.as_deref(),
13010 auth_store.principal_is_system_owned(&actor),
13011 );
13012 let event_ctx = self.policy_mutation_control_ctx(&actor, tenant.as_deref());
13013 let ledger = self.inner.control_event_ledger.read();
13014 let control = crate::auth::store::PolicyMutationControl {
13015 ctx: &event_ctx,
13016 ledger: ledger.as_ref(),
13017 config: self.inner.control_event_config,
13018 registry: Some(self.inner.config_registry.as_ref()),
13019 actor: &actor,
13020 eval_ctx: &eval_ctx,
13021 };
13022 auth_store
13023 .put_policy_with_control_events(policy, &control)
13024 .map_err(|e| RedDBError::Query(e.to_string()))?;
13025
13026 let principal = actor_name;
13027 tracing::info!(
13028 target: "audit",
13029 principal = %principal,
13030 action = "iam:policy.put",
13031 matched_policy_id = %pid,
13032 "CREATE POLICY applied"
13033 );
13034 self.inner.audit_log.record(
13035 "iam/policy.put",
13036 &principal,
13037 &pid,
13038 "ok",
13039 crate::json::Value::Null,
13040 );
13041
13042 self.invalidate_result_cache();
13043 Ok(RuntimeQueryResult::ok_message(
13044 query.to_string(),
13045 &format!("policy `{pid}` stored"),
13046 "create_iam_policy",
13047 ))
13048 }
13049
13050 fn execute_drop_iam_policy(&self, query: &str, id: &str) -> RedDBResult<RuntimeQueryResult> {
13051 let auth_store = self
13052 .inner
13053 .auth_store
13054 .read()
13055 .clone()
13056 .ok_or_else(|| RedDBError::Query("auth store not configured".to_string()))?;
13057 let tenant = current_tenant();
13058 let (actor_name, actor_role) = current_auth_identity()
13059 .unwrap_or_else(|| ("anonymous".to_string(), crate::auth::Role::Read));
13060 let actor = crate::auth::UserId::from_parts(tenant.as_deref(), &actor_name);
13061 let eval_ctx = runtime_iam_context(
13062 actor_role,
13063 tenant.as_deref(),
13064 auth_store.principal_is_system_owned(&actor),
13065 );
13066 let event_ctx = self.policy_mutation_control_ctx(&actor, tenant.as_deref());
13067 let ledger = self.inner.control_event_ledger.read();
13068 let control = crate::auth::store::PolicyMutationControl {
13069 ctx: &event_ctx,
13070 ledger: ledger.as_ref(),
13071 config: self.inner.control_event_config,
13072 registry: Some(self.inner.config_registry.as_ref()),
13073 actor: &actor,
13074 eval_ctx: &eval_ctx,
13075 };
13076 auth_store
13077 .delete_policy_with_control_events(id, &control)
13078 .map_err(|e| RedDBError::Query(e.to_string()))?;
13079
13080 let principal = actor_name;
13081 tracing::info!(
13082 target: "audit",
13083 principal = %principal,
13084 action = "iam:policy.drop",
13085 matched_policy_id = %id,
13086 "DROP POLICY applied"
13087 );
13088 self.inner.audit_log.record(
13089 "iam/policy.drop",
13090 &principal,
13091 id,
13092 "ok",
13093 crate::json::Value::Null,
13094 );
13095
13096 self.invalidate_result_cache();
13097 Ok(RuntimeQueryResult::ok_message(
13098 query.to_string(),
13099 &format!("policy `{id}` dropped"),
13100 "drop_iam_policy",
13101 ))
13102 }
13103
13104 fn execute_attach_policy(
13105 &self,
13106 query: &str,
13107 policy_id: &str,
13108 principal: &crate::storage::query::ast::PolicyPrincipalRef,
13109 ) -> RedDBResult<RuntimeQueryResult> {
13110 use crate::auth::store::PrincipalRef;
13111 use crate::auth::UserId;
13112 use crate::storage::query::ast::PolicyPrincipalRef;
13113
13114 let auth_store = self
13115 .inner
13116 .auth_store
13117 .read()
13118 .clone()
13119 .ok_or_else(|| RedDBError::Query("auth store not configured".to_string()))?;
13120 let p = match principal {
13121 PolicyPrincipalRef::User(u) => {
13122 PrincipalRef::User(UserId::from_parts(u.tenant.as_deref(), &u.username))
13123 }
13124 PolicyPrincipalRef::Group(g) => PrincipalRef::Group(g.clone()),
13125 };
13126 let pretty_target = principal_label(principal);
13127 let tenant = current_tenant();
13128 let (actor_name, actor_role) = current_auth_identity()
13129 .unwrap_or_else(|| ("anonymous".to_string(), crate::auth::Role::Read));
13130 let actor = crate::auth::UserId::from_parts(tenant.as_deref(), &actor_name);
13131 let eval_ctx = runtime_iam_context(
13132 actor_role,
13133 tenant.as_deref(),
13134 auth_store.principal_is_system_owned(&actor),
13135 );
13136 let event_ctx = self.policy_mutation_control_ctx(&actor, tenant.as_deref());
13137 let ledger = self.inner.control_event_ledger.read();
13138 let control = crate::auth::store::PolicyMutationControl {
13139 ctx: &event_ctx,
13140 ledger: ledger.as_ref(),
13141 config: self.inner.control_event_config,
13142 registry: Some(self.inner.config_registry.as_ref()),
13143 actor: &actor,
13144 eval_ctx: &eval_ctx,
13145 };
13146 auth_store
13147 .attach_policy_with_control_events(p, policy_id, &control)
13148 .map_err(|e| RedDBError::Query(e.to_string()))?;
13149
13150 let principal_str = actor_name;
13151 tracing::info!(
13152 target: "audit",
13153 principal = %principal_str,
13154 action = "iam:policy.attach",
13155 matched_policy_id = %policy_id,
13156 target = %pretty_target,
13157 "ATTACH POLICY applied"
13158 );
13159 self.inner.audit_log.record(
13160 "iam/policy.attach",
13161 &principal_str,
13162 &pretty_target,
13163 "ok",
13164 crate::json::Value::Null,
13165 );
13166
13167 self.invalidate_result_cache();
13168 Ok(RuntimeQueryResult::ok_message(
13169 query.to_string(),
13170 &format!("policy `{policy_id}` attached to {pretty_target}"),
13171 "attach_policy",
13172 ))
13173 }
13174
13175 fn execute_detach_policy(
13176 &self,
13177 query: &str,
13178 policy_id: &str,
13179 principal: &crate::storage::query::ast::PolicyPrincipalRef,
13180 ) -> RedDBResult<RuntimeQueryResult> {
13181 use crate::auth::store::PrincipalRef;
13182 use crate::auth::UserId;
13183 use crate::storage::query::ast::PolicyPrincipalRef;
13184
13185 let auth_store = self
13186 .inner
13187 .auth_store
13188 .read()
13189 .clone()
13190 .ok_or_else(|| RedDBError::Query("auth store not configured".to_string()))?;
13191 let p = match principal {
13192 PolicyPrincipalRef::User(u) => {
13193 PrincipalRef::User(UserId::from_parts(u.tenant.as_deref(), &u.username))
13194 }
13195 PolicyPrincipalRef::Group(g) => PrincipalRef::Group(g.clone()),
13196 };
13197 let pretty_target = principal_label(principal);
13198 let tenant = current_tenant();
13199 let (actor_name, actor_role) = current_auth_identity()
13200 .unwrap_or_else(|| ("anonymous".to_string(), crate::auth::Role::Read));
13201 let actor = crate::auth::UserId::from_parts(tenant.as_deref(), &actor_name);
13202 let eval_ctx = runtime_iam_context(
13203 actor_role,
13204 tenant.as_deref(),
13205 auth_store.principal_is_system_owned(&actor),
13206 );
13207 let event_ctx = self.policy_mutation_control_ctx(&actor, tenant.as_deref());
13208 let ledger = self.inner.control_event_ledger.read();
13209 let control = crate::auth::store::PolicyMutationControl {
13210 ctx: &event_ctx,
13211 ledger: ledger.as_ref(),
13212 config: self.inner.control_event_config,
13213 registry: Some(self.inner.config_registry.as_ref()),
13214 actor: &actor,
13215 eval_ctx: &eval_ctx,
13216 };
13217 auth_store
13218 .detach_policy_with_control_events(p, policy_id, &control)
13219 .map_err(|e| RedDBError::Query(e.to_string()))?;
13220
13221 let principal_str = actor_name;
13222 tracing::info!(
13223 target: "audit",
13224 principal = %principal_str,
13225 action = "iam:policy.detach",
13226 matched_policy_id = %policy_id,
13227 target = %pretty_target,
13228 "DETACH POLICY applied"
13229 );
13230 self.inner.audit_log.record(
13231 "iam/policy.detach",
13232 &principal_str,
13233 &pretty_target,
13234 "ok",
13235 crate::json::Value::Null,
13236 );
13237
13238 self.invalidate_result_cache();
13239 Ok(RuntimeQueryResult::ok_message(
13240 query.to_string(),
13241 &format!("policy `{policy_id}` detached from {pretty_target}"),
13242 "detach_policy",
13243 ))
13244 }
13245
13246 fn execute_show_policies(
13247 &self,
13248 query: &str,
13249 filter: Option<&crate::storage::query::ast::PolicyPrincipalRef>,
13250 ) -> RedDBResult<RuntimeQueryResult> {
13251 use crate::auth::UserId;
13252 use crate::storage::query::ast::PolicyPrincipalRef;
13253 use crate::storage::query::unified::UnifiedRecord;
13254 use crate::storage::schema::Value as SchemaValue;
13255 use std::sync::Arc;
13256
13257 let auth_store = self
13258 .inner
13259 .auth_store
13260 .read()
13261 .clone()
13262 .ok_or_else(|| RedDBError::Query("auth store not configured".to_string()))?;
13263
13264 let pols = match filter {
13265 None => auth_store.list_policies(),
13266 Some(PolicyPrincipalRef::User(u)) => {
13267 let id = UserId::from_parts(u.tenant.as_deref(), &u.username);
13268 auth_store.effective_policies(&id)
13269 }
13270 Some(PolicyPrincipalRef::Group(g)) => auth_store.group_policies(g),
13271 };
13272
13273 let mut records = Vec::with_capacity(pols.len() + 1);
13274
13275 let mode = auth_store.enforcement_mode();
13280 let mut header = UnifiedRecord::default();
13281 header.set_arc(
13282 Arc::from("id"),
13283 SchemaValue::text("<enforcement_mode>".to_string()),
13284 );
13285 header.set_arc(Arc::from("statements"), SchemaValue::Integer(0));
13286 header.set_arc(Arc::from("tenant"), SchemaValue::Null);
13287 let header_json = format!(
13288 r#"{{"enforcement_mode":"{}","policy_only_hard_version":"{}"}}"#,
13289 mode.as_str(),
13290 crate::auth::enforcement_mode::POLICY_ONLY_HARD_VERSION
13291 );
13292 header.set_arc(Arc::from("json"), SchemaValue::text(header_json));
13293 records.push(header);
13294
13295 for p in pols.iter() {
13296 let mut rec = UnifiedRecord::default();
13297 rec.set_arc(Arc::from("id"), SchemaValue::text(p.id.clone()));
13298 rec.set_arc(
13299 Arc::from("statements"),
13300 SchemaValue::Integer(p.statements.len() as i64),
13301 );
13302 rec.set_arc(
13303 Arc::from("tenant"),
13304 p.tenant
13305 .as_deref()
13306 .map(|t| SchemaValue::text(t.to_string()))
13307 .unwrap_or(SchemaValue::Null),
13308 );
13309 rec.set_arc(Arc::from("json"), SchemaValue::text(p.to_json_string()));
13310 records.push(rec);
13311 }
13312 let mut result = crate::storage::query::unified::UnifiedResult::empty();
13313 result.records = records;
13314 Ok(RuntimeQueryResult {
13315 query: query.to_string(),
13316 mode: crate::storage::query::modes::QueryMode::Sql,
13317 statement: "show_policies",
13318 engine: "iam-policies",
13319 result,
13320 affected_rows: 0,
13321 statement_type: "select",
13322 bookmark: None,
13323 })
13324 }
13325
13326 fn execute_show_effective_permissions(
13327 &self,
13328 query: &str,
13329 user: &crate::storage::query::ast::PolicyUserRef,
13330 resource: Option<&crate::storage::query::ast::PolicyResourceRef>,
13331 ) -> RedDBResult<RuntimeQueryResult> {
13332 use crate::auth::UserId;
13333 use crate::storage::query::unified::UnifiedRecord;
13334 use crate::storage::schema::Value as SchemaValue;
13335 use std::sync::Arc;
13336
13337 let auth_store = self
13338 .inner
13339 .auth_store
13340 .read()
13341 .clone()
13342 .ok_or_else(|| RedDBError::Query("auth store not configured".to_string()))?;
13343 let id = UserId::from_parts(user.tenant.as_deref(), &user.username);
13344 let pols = auth_store.effective_policies(&id);
13345
13346 let mut records = Vec::new();
13349 for p in pols.iter() {
13350 for (idx, st) in p.statements.iter().enumerate() {
13351 if let Some(_r) = resource {
13352 }
13356 let mut rec = UnifiedRecord::default();
13357 rec.set_arc(Arc::from("policy_id"), SchemaValue::text(p.id.clone()));
13358 rec.set_arc(
13359 Arc::from("statement_index"),
13360 SchemaValue::Integer(idx as i64),
13361 );
13362 rec.set_arc(
13363 Arc::from("sid"),
13364 st.sid
13365 .as_deref()
13366 .map(|s| SchemaValue::text(s.to_string()))
13367 .unwrap_or(SchemaValue::Null),
13368 );
13369 rec.set_arc(
13370 Arc::from("effect"),
13371 SchemaValue::text(match st.effect {
13372 crate::auth::policies::Effect::Allow => "allow",
13373 crate::auth::policies::Effect::Deny => "deny",
13374 }),
13375 );
13376 rec.set_arc(
13377 Arc::from("actions"),
13378 SchemaValue::Integer(st.actions.len() as i64),
13379 );
13380 rec.set_arc(
13381 Arc::from("resources"),
13382 SchemaValue::Integer(st.resources.len() as i64),
13383 );
13384 records.push(rec);
13385 }
13386 }
13387 let mut result = crate::storage::query::unified::UnifiedResult::empty();
13388 result.records = records;
13389 Ok(RuntimeQueryResult {
13390 query: query.to_string(),
13391 mode: crate::storage::query::modes::QueryMode::Sql,
13392 statement: "show_effective_permissions",
13393 engine: "iam-policies",
13394 result,
13395 affected_rows: 0,
13396 statement_type: "select",
13397 bookmark: None,
13398 })
13399 }
13400
13401 fn execute_lint_policy(
13402 &self,
13403 query: &str,
13404 source: &crate::storage::query::ast::LintPolicySource,
13405 ) -> RedDBResult<RuntimeQueryResult> {
13406 use crate::auth::policy_linter::lint;
13407 use crate::storage::query::ast::LintPolicySource;
13408 use crate::storage::query::unified::UnifiedRecord;
13409 use crate::storage::schema::Value as SchemaValue;
13410 use std::sync::Arc;
13411
13412 let policy_text = match source {
13417 LintPolicySource::Json(text) => text.clone(),
13418 LintPolicySource::Id(id) => {
13419 let auth_store =
13420 self.inner.auth_store.read().clone().ok_or_else(|| {
13421 RedDBError::Query("auth store not configured".to_string())
13422 })?;
13423 let policy = auth_store
13424 .get_policy(id)
13425 .ok_or_else(|| RedDBError::Query(format!("policy `{id}` not found")))?;
13426 policy.to_json_string()
13427 }
13428 };
13429 let diagnostics = lint(&policy_text);
13430
13431 let principal_str = current_auth_identity()
13432 .map(|(u, _)| u)
13433 .unwrap_or_else(|| "anonymous".into());
13434 tracing::info!(
13435 target: "audit",
13436 principal = %principal_str,
13437 action = "iam:policy.lint",
13438 diagnostic_count = diagnostics.len(),
13439 "LINT POLICY issued"
13440 );
13441 self.inner.audit_log.record(
13442 "iam/policy.lint",
13443 &principal_str,
13444 match source {
13445 LintPolicySource::Id(id) => id.as_str(),
13446 LintPolicySource::Json(_) => "<json>",
13447 },
13448 "ok",
13449 crate::json::Value::Null,
13450 );
13451
13452 const COLUMNS: [&str; 5] = ["severity", "code", "message", "suggested_fix", "location"];
13455 let schema = Arc::new(
13456 COLUMNS
13457 .iter()
13458 .map(|name| Arc::<str>::from(*name))
13459 .collect::<Vec<_>>(),
13460 );
13461 let records: Vec<UnifiedRecord> = diagnostics
13462 .iter()
13463 .map(|d| {
13464 UnifiedRecord::with_schema(
13465 Arc::clone(&schema),
13466 vec![
13467 SchemaValue::text(d.severity.as_str()),
13468 SchemaValue::text(d.code.as_str()),
13469 SchemaValue::text(d.message.clone()),
13470 d.suggested_fix
13471 .as_deref()
13472 .map(SchemaValue::text)
13473 .unwrap_or(SchemaValue::Null),
13474 d.location
13475 .as_deref()
13476 .map(SchemaValue::text)
13477 .unwrap_or(SchemaValue::Null),
13478 ],
13479 )
13480 })
13481 .collect();
13482 let mut result = crate::storage::query::unified::UnifiedResult::with_columns(
13483 COLUMNS.iter().map(|c| c.to_string()).collect(),
13484 );
13485 result.records = records;
13486 Ok(RuntimeQueryResult {
13487 query: query.to_string(),
13488 mode: crate::storage::query::modes::QueryMode::Sql,
13489 statement: "lint_policy",
13490 engine: "iam-policies",
13491 result,
13492 affected_rows: 0,
13493 statement_type: "select",
13494 bookmark: None,
13495 })
13496 }
13497
13498 fn execute_migrate_policy_mode(
13503 &self,
13504 query: &str,
13505 target: &str,
13506 dry_run: bool,
13507 ) -> RedDBResult<RuntimeQueryResult> {
13508 use crate::auth::enforcement_mode::PolicyEnforcementMode;
13509 use crate::auth::migrate_policy_mode::{
13510 principal_label, simulate_migration_delta, MigratePolicyDelta,
13511 };
13512 use crate::auth::policies::ResourceRef;
13513 use crate::storage::query::unified::UnifiedRecord;
13514 use crate::storage::schema::Value as SchemaValue;
13515 use std::sync::Arc;
13516
13517 let parsed = PolicyEnforcementMode::parse(target).ok_or_else(|| {
13522 RedDBError::Query(format!(
13523 "MIGRATE POLICY MODE: invalid target `{target}` (expected `policy_only`)"
13524 ))
13525 })?;
13526 if parsed != PolicyEnforcementMode::PolicyOnly {
13527 return Err(RedDBError::Query(format!(
13528 "MIGRATE POLICY MODE: target `{target}` is not supported — only `policy_only` may be migrated to via this command"
13529 )));
13530 }
13531
13532 let auth_store = self
13533 .inner
13534 .auth_store
13535 .read()
13536 .clone()
13537 .ok_or_else(|| RedDBError::Query("auth store not configured".to_string()))?;
13538
13539 let snapshot = self.inner.db.catalog_model_snapshot();
13547 let resources: Vec<ResourceRef> = snapshot
13548 .collections
13549 .iter()
13550 .map(|c| ResourceRef::new("table", c.name.clone()))
13551 .collect();
13552
13553 let now_ms = crate::utils::now_unix_millis() as u128;
13554 let deltas: Vec<MigratePolicyDelta> =
13555 simulate_migration_delta(auth_store.as_ref(), &resources, now_ms);
13556
13557 let principal_str = current_auth_identity()
13558 .map(|(u, _)| u)
13559 .unwrap_or_else(|| "anonymous".into());
13560
13561 let outcome_str = if dry_run {
13565 "dry_run"
13566 } else if deltas.is_empty() {
13567 "applied"
13568 } else {
13569 "refused"
13570 };
13571 tracing::info!(
13572 target: "audit",
13573 principal = %principal_str,
13574 action = "iam:policy.migrate_mode",
13575 target = %target,
13576 dry_run,
13577 delta_count = deltas.len(),
13578 outcome = outcome_str,
13579 "MIGRATE POLICY MODE issued"
13580 );
13581 self.inner.audit_log.record(
13582 "iam/policy.migrate_mode",
13583 &principal_str,
13584 target,
13585 outcome_str,
13586 crate::json::Value::Null,
13587 );
13588
13589 if !dry_run && !deltas.is_empty() {
13593 let summary = deltas
13594 .iter()
13595 .take(5)
13596 .map(|d| {
13597 format!(
13598 "{}:{}/{}:{}",
13599 principal_label(&d.principal),
13600 d.action,
13601 d.resource_kind,
13602 d.resource_name
13603 )
13604 })
13605 .collect::<Vec<_>>()
13606 .join(", ");
13607 let more = if deltas.len() > 5 {
13608 format!(" (and {} more)", deltas.len() - 5)
13609 } else {
13610 String::new()
13611 };
13612 return Err(RedDBError::Query(format!(
13613 "MIGRATE POLICY MODE refused: {n} principal/action/resource pair(s) would lose access under `policy_only`. Run `MIGRATE POLICY MODE TO '{target}' DRY RUN` to inspect. Sample: {summary}{more}",
13614 n = deltas.len(),
13615 )));
13616 }
13617
13618 if !dry_run {
13622 auth_store.set_enforcement_mode(parsed);
13623 }
13624
13625 const COLUMNS: [&str; 5] = [
13626 "principal",
13627 "role",
13628 "action",
13629 "resource_kind",
13630 "resource_name",
13631 ];
13632 let schema = Arc::new(
13633 COLUMNS
13634 .iter()
13635 .map(|name| Arc::<str>::from(*name))
13636 .collect::<Vec<_>>(),
13637 );
13638 let records: Vec<UnifiedRecord> = deltas
13639 .iter()
13640 .map(|d| {
13641 UnifiedRecord::with_schema(
13642 Arc::clone(&schema),
13643 vec![
13644 SchemaValue::text(principal_label(&d.principal)),
13645 SchemaValue::text(d.role.as_str()),
13646 SchemaValue::text(d.action.clone()),
13647 SchemaValue::text(d.resource_kind.clone()),
13648 SchemaValue::text(d.resource_name.clone()),
13649 ],
13650 )
13651 })
13652 .collect();
13653 let mut result = crate::storage::query::unified::UnifiedResult::with_columns(
13654 COLUMNS.iter().map(|c| c.to_string()).collect(),
13655 );
13656 result.records = records;
13657 Ok(RuntimeQueryResult {
13658 query: query.to_string(),
13659 mode: crate::storage::query::modes::QueryMode::Sql,
13660 statement: "migrate_policy_mode",
13661 engine: "iam-policies",
13662 result,
13663 affected_rows: 0,
13664 statement_type: "select",
13665 bookmark: None,
13666 })
13667 }
13668
13669 fn execute_simulate_policy(
13670 &self,
13671 query: &str,
13672 user: &crate::storage::query::ast::PolicyUserRef,
13673 action: &str,
13674 resource: &crate::storage::query::ast::PolicyResourceRef,
13675 ) -> RedDBResult<RuntimeQueryResult> {
13676 use crate::auth::policies::ResourceRef;
13677 use crate::auth::store::SimCtx;
13678 use crate::auth::UserId;
13679 use crate::storage::query::unified::UnifiedRecord;
13680 use crate::storage::schema::Value as SchemaValue;
13681 use std::sync::Arc;
13682
13683 let auth_store = self
13684 .inner
13685 .auth_store
13686 .read()
13687 .clone()
13688 .ok_or_else(|| RedDBError::Query("auth store not configured".to_string()))?;
13689 let id = UserId::from_parts(user.tenant.as_deref(), &user.username);
13690 let r = ResourceRef::new(resource.kind.clone(), resource.name.clone());
13691 let outcome = auth_store.simulate(&id, action, &r, SimCtx::default());
13692
13693 let principal_str = current_auth_identity()
13694 .map(|(u, _)| u)
13695 .unwrap_or_else(|| "anonymous".into());
13696 let (decision_str, matched_pid, matched_sid) = decision_to_strings(&outcome.decision);
13697 tracing::info!(
13698 target: "audit",
13699 principal = %principal_str,
13700 action = "iam:policy.simulate",
13701 decision = %decision_str,
13702 matched_policy_id = ?matched_pid,
13703 matched_sid = ?matched_sid,
13704 "SIMULATE issued"
13705 );
13706 self.inner.audit_log.record(
13707 "iam/policy.simulate",
13708 &principal_str,
13709 &id.to_string(),
13710 "ok",
13711 crate::json::Value::Null,
13712 );
13713
13714 let mut rec = UnifiedRecord::default();
13715 rec.set_arc(Arc::from("decision"), SchemaValue::text(decision_str));
13716 rec.set_arc(
13717 Arc::from("matched_policy_id"),
13718 matched_pid
13719 .map(SchemaValue::text)
13720 .unwrap_or(SchemaValue::Null),
13721 );
13722 rec.set_arc(
13723 Arc::from("matched_sid"),
13724 matched_sid
13725 .map(SchemaValue::text)
13726 .unwrap_or(SchemaValue::Null),
13727 );
13728 rec.set_arc(Arc::from("reason"), SchemaValue::text(outcome.reason));
13729 rec.set_arc(
13730 Arc::from("trail_len"),
13731 SchemaValue::Integer(outcome.trail.len() as i64),
13732 );
13733 let mut result = crate::storage::query::unified::UnifiedResult::empty();
13734 result.records = vec![rec];
13735 Ok(RuntimeQueryResult {
13736 query: query.to_string(),
13737 mode: crate::storage::query::modes::QueryMode::Sql,
13738 statement: "simulate_policy",
13739 engine: "iam-policies",
13740 result,
13741 affected_rows: 0,
13742 statement_type: "select",
13743 bookmark: None,
13744 })
13745 }
13746}
13747
13748fn grant_to_iam_policy(
13753 principal: &crate::auth::privileges::GrantPrincipal,
13754 resource: &crate::auth::privileges::Resource,
13755 actions: &[crate::auth::privileges::Action],
13756 tenant: Option<&str>,
13757) -> Option<crate::auth::policies::Policy> {
13758 use crate::auth::policies::{
13759 compile_action, ActionPattern, Effect, Policy, ResourcePattern, Statement,
13760 };
13761 use crate::auth::privileges::{Action, GrantPrincipal, Resource};
13762
13763 if matches!(principal, GrantPrincipal::Group(_)) {
13764 return None;
13765 }
13766
13767 let now = crate::auth::now_ms();
13768 let id = format!("_grant_{:x}_{:x}", now, std::process::id());
13769
13770 let resource_str = match resource {
13771 Resource::Database => "table:*".to_string(),
13772 Resource::Schema(s) => format!("table:{s}.*"),
13773 Resource::Table { schema, table } => match schema {
13774 Some(s) => format!("table:{s}.{table}"),
13775 None => format!("table:{table}"),
13776 },
13777 Resource::Function { schema, name } => match schema {
13778 Some(s) => format!("function:{s}.{name}"),
13779 None => format!("function:{name}"),
13780 },
13781 };
13782
13783 let action_patterns: Vec<ActionPattern> = if actions.contains(&Action::All) {
13787 vec![ActionPattern::Wildcard]
13788 } else {
13789 actions
13790 .iter()
13791 .map(|a| compile_action(&a.as_str().to_ascii_lowercase()))
13792 .collect()
13793 };
13794 if action_patterns.is_empty() {
13795 return None;
13796 }
13797
13798 let resource_patterns = if resource_str == "*" {
13803 vec![ResourcePattern::Wildcard]
13804 } else if resource_str.contains('*') {
13805 vec![ResourcePattern::Glob(resource_str.clone())]
13806 } else if let Some((kind, name)) = resource_str.split_once(':') {
13807 vec![ResourcePattern::Exact {
13808 kind: kind.to_string(),
13809 name: name.to_string(),
13810 }]
13811 } else {
13812 vec![ResourcePattern::Wildcard]
13813 };
13814
13815 let policy = Policy {
13816 id,
13817 version: 1,
13818 tenant: tenant.map(|t| t.to_string()),
13819 created_at: now,
13820 updated_at: now,
13821 statements: vec![Statement {
13822 sid: None,
13823 effect: Effect::Allow,
13824 actions: action_patterns,
13825 resources: resource_patterns,
13826 condition: None,
13827 }],
13828 };
13829 if policy.validate().is_err() {
13830 return None;
13831 }
13832 Some(policy)
13833}
13834
13835fn parse_positive_iterations(func: &str, value: &f64) -> RedDBResult<usize> {
13841 if !value.is_finite() || *value < 1.0 || value.fract() != 0.0 {
13842 return Err(RedDBError::Query(format!(
13843 "table function '{func}' max_iterations must be a positive integer, got {value}"
13844 )));
13845 }
13846 Ok(*value as usize)
13847}
13848
13849fn legacy_action_to_iam(action: crate::auth::privileges::Action) -> &'static str {
13850 use crate::auth::privileges::Action;
13851 match action {
13852 Action::Select => "select",
13853 Action::Insert => "insert",
13854 Action::Update => "update",
13855 Action::Delete => "delete",
13856 Action::Truncate => "truncate",
13857 Action::References => "references",
13858 Action::Execute => "execute",
13859 Action::Usage => "usage",
13860 Action::All => "*",
13861 }
13862}
13863
13864fn update_set_target_columns(query: &crate::storage::query::ast::UpdateQuery) -> Vec<String> {
13865 let mut columns = Vec::new();
13866 for (column, _) in &query.assignment_exprs {
13867 if !columns.iter().any(|seen| seen == column) {
13868 columns.push(column.clone());
13869 }
13870 }
13871 columns
13872}
13873
13874fn column_access_request_for_table_update(
13875 table_name: &str,
13876 columns: Vec<String>,
13877) -> crate::auth::ColumnAccessRequest {
13878 match table_name.split_once('.') {
13879 Some((schema, table)) => {
13880 crate::auth::ColumnAccessRequest::update(table.to_string(), columns)
13881 .with_schema(schema.to_string())
13882 }
13883 None => crate::auth::ColumnAccessRequest::update(table_name.to_string(), columns),
13884 }
13885}
13886
13887fn column_access_request_for_table_select(
13888 table_name: &str,
13889 columns: Vec<String>,
13890) -> crate::auth::ColumnAccessRequest {
13891 match table_name.split_once('.') {
13892 Some((schema, table)) => {
13893 crate::auth::ColumnAccessRequest::select(table.to_string(), columns)
13894 .with_schema(schema.to_string())
13895 }
13896 None => crate::auth::ColumnAccessRequest::select(table_name.to_string(), columns),
13897 }
13898}
13899
13900fn update_returning_columns_for_policy(
13901 runtime: &RedDBRuntime,
13902 query: &crate::storage::query::ast::UpdateQuery,
13903) -> Option<Vec<String>> {
13904 let items = query.returning.as_ref()?;
13905 let mut columns = Vec::new();
13906 let project_all = items
13907 .iter()
13908 .any(|item| matches!(item, crate::storage::query::ast::ReturningItem::All));
13909 if project_all {
13910 collect_returning_star_columns(runtime, query, &mut columns);
13911 } else {
13912 for item in items {
13913 let crate::storage::query::ast::ReturningItem::Column(column) = item else {
13914 continue;
13915 };
13916 push_returning_policy_column(&mut columns, column);
13917 }
13918 }
13919 (!columns.is_empty()).then_some(columns)
13920}
13921
13922fn collect_returning_star_columns(
13923 runtime: &RedDBRuntime,
13924 query: &crate::storage::query::ast::UpdateQuery,
13925 columns: &mut Vec<String>,
13926) {
13927 let store = runtime.db().store();
13928 let Some(manager) = store.get_collection(&query.table) else {
13929 return;
13930 };
13931 if let Some(schema) = manager.column_schema() {
13932 for column in schema.iter() {
13933 push_returning_policy_column(columns, column);
13934 }
13935 }
13936 for entity in manager.query_all(|_| true) {
13937 if !returning_entity_matches_update_target(&entity, query.target) {
13938 continue;
13939 }
13940 match &entity.data {
13941 crate::storage::EntityData::Row(row) => {
13942 for (column, _) in row.iter_fields() {
13943 push_returning_policy_column(columns, column);
13944 }
13945 }
13946 crate::storage::EntityData::Node(node) => {
13947 push_returning_policy_column(columns, "label");
13948 push_returning_policy_column(columns, "node_type");
13949 for column in node.properties.keys() {
13950 push_returning_policy_column(columns, column);
13951 }
13952 }
13953 crate::storage::EntityData::Edge(edge) => {
13954 push_returning_policy_column(columns, "label");
13955 push_returning_policy_column(columns, "from_rid");
13956 push_returning_policy_column(columns, "to_rid");
13957 push_returning_policy_column(columns, "weight");
13958 for column in edge.properties.keys() {
13959 push_returning_policy_column(columns, column);
13960 }
13961 }
13962 _ => {}
13963 }
13964 }
13965}
13966
13967fn push_returning_policy_column(columns: &mut Vec<String>, column: &str) {
13968 if returning_public_envelope_column(column) {
13969 return;
13970 }
13971 if !columns.iter().any(|seen| seen == column) {
13972 columns.push(column.to_string());
13973 }
13974}
13975
13976fn returning_public_envelope_column(column: &str) -> bool {
13977 matches!(
13978 column.to_ascii_lowercase().as_str(),
13979 "rid" | "collection" | "kind" | "tenant" | "created_at" | "updated_at" | "red_entity_id"
13980 )
13981}
13982
13983fn returning_entity_matches_update_target(
13984 entity: &crate::storage::UnifiedEntity,
13985 target: crate::storage::query::ast::UpdateTarget,
13986) -> bool {
13987 use crate::storage::query::ast::UpdateTarget;
13988 match target {
13989 UpdateTarget::Rows => {
13990 matches!(returning_row_item_kind(entity), Some(ReturningRowKind::Row))
13991 }
13992 UpdateTarget::Documents => {
13993 matches!(
13994 returning_row_item_kind(entity),
13995 Some(ReturningRowKind::Document)
13996 )
13997 }
13998 UpdateTarget::Kv => matches!(returning_row_item_kind(entity), Some(ReturningRowKind::Kv)),
13999 UpdateTarget::Nodes => matches!(
14000 (&entity.kind, &entity.data),
14001 (
14002 crate::storage::EntityKind::GraphNode(_),
14003 crate::storage::EntityData::Node(_)
14004 )
14005 ),
14006 UpdateTarget::Edges => matches!(
14007 (&entity.kind, &entity.data),
14008 (
14009 crate::storage::EntityKind::GraphEdge(_),
14010 crate::storage::EntityData::Edge(_)
14011 )
14012 ),
14013 }
14014}
14015
14016#[derive(Debug, Clone, Copy, PartialEq, Eq)]
14017enum ReturningRowKind {
14018 Row,
14019 Document,
14020 Kv,
14021}
14022
14023fn returning_row_item_kind(entity: &crate::storage::UnifiedEntity) -> Option<ReturningRowKind> {
14024 let row = entity.data.as_row()?;
14025 let is_kv = row.iter_fields().all(|(column, _)| {
14026 column.eq_ignore_ascii_case("key") || column.eq_ignore_ascii_case("value")
14027 });
14028 if is_kv {
14029 return Some(ReturningRowKind::Kv);
14030 }
14031 let is_document = row
14032 .iter_fields()
14033 .any(|(_, value)| matches!(value, crate::storage::schema::Value::Json(_)));
14034 if is_document {
14035 Some(ReturningRowKind::Document)
14036 } else {
14037 Some(ReturningRowKind::Row)
14038 }
14039}
14040
14041fn requested_table_columns_for_policy(
14042 table: &crate::storage::query::ast::TableQuery,
14043) -> Vec<String> {
14044 use crate::storage::query::sql_lowering::{
14045 effective_table_filter, effective_table_group_by_exprs, effective_table_having_filter,
14046 effective_table_projections,
14047 };
14048
14049 let table_name = table.table.as_str();
14050 let table_alias = table.alias.as_deref();
14051 let mut columns = std::collections::BTreeSet::new();
14052
14053 for projection in effective_table_projections(table) {
14054 collect_projection_columns(&projection, table_name, table_alias, &mut columns);
14055 }
14056 if let Some(filter) = effective_table_filter(table) {
14057 collect_filter_columns(&filter, table_name, table_alias, &mut columns);
14058 }
14059 for expr in effective_table_group_by_exprs(table) {
14060 collect_expr_columns(&expr, table_name, table_alias, &mut columns);
14061 }
14062 if let Some(filter) = effective_table_having_filter(table) {
14063 collect_filter_columns(&filter, table_name, table_alias, &mut columns);
14064 }
14065 for order in &table.order_by {
14066 if let Some(expr) = order.expr.as_ref() {
14067 collect_expr_columns(expr, table_name, table_alias, &mut columns);
14068 } else {
14069 collect_field_ref_column(&order.field, table_name, table_alias, &mut columns);
14070 }
14071 }
14072
14073 columns.into_iter().collect()
14074}
14075
14076fn collect_projection_columns(
14077 projection: &crate::storage::query::ast::Projection,
14078 table_name: &str,
14079 table_alias: Option<&str>,
14080 columns: &mut std::collections::BTreeSet<String>,
14081) {
14082 use crate::storage::query::ast::Projection;
14083 match projection {
14084 Projection::All => {
14085 columns.insert("*".to_string());
14086 }
14087 Projection::Column(column) | Projection::Alias(column, _) => {
14088 if column != "*" {
14089 columns.insert(column.clone());
14090 }
14091 }
14092 Projection::Function(_, args) => {
14093 for arg in args {
14094 collect_projection_columns(arg, table_name, table_alias, columns);
14095 }
14096 }
14097 Projection::Expression(filter, _) => {
14098 collect_filter_columns(filter, table_name, table_alias, columns);
14099 }
14100 Projection::Field(field, _) => {
14101 collect_field_ref_column(field, table_name, table_alias, columns);
14102 }
14103 Projection::Window { args, .. } => {
14107 for arg in args {
14108 collect_projection_columns(arg, table_name, table_alias, columns);
14109 }
14110 }
14111 }
14112}
14113
14114fn collect_filter_columns(
14115 filter: &crate::storage::query::ast::Filter,
14116 table_name: &str,
14117 table_alias: Option<&str>,
14118 columns: &mut std::collections::BTreeSet<String>,
14119) {
14120 use crate::storage::query::ast::Filter;
14121 match filter {
14122 Filter::Compare { field, .. }
14123 | Filter::IsNull(field)
14124 | Filter::IsNotNull(field)
14125 | Filter::In { field, .. }
14126 | Filter::Between { field, .. }
14127 | Filter::Like { field, .. }
14128 | Filter::StartsWith { field, .. }
14129 | Filter::EndsWith { field, .. }
14130 | Filter::Contains { field, .. } => {
14131 collect_field_ref_column(field, table_name, table_alias, columns);
14132 }
14133 Filter::CompareFields { left, right, .. } => {
14134 collect_field_ref_column(left, table_name, table_alias, columns);
14135 collect_field_ref_column(right, table_name, table_alias, columns);
14136 }
14137 Filter::CompareExpr { lhs, rhs, .. } => {
14138 collect_expr_columns(lhs, table_name, table_alias, columns);
14139 collect_expr_columns(rhs, table_name, table_alias, columns);
14140 }
14141 Filter::And(left, right) | Filter::Or(left, right) => {
14142 collect_filter_columns(left, table_name, table_alias, columns);
14143 collect_filter_columns(right, table_name, table_alias, columns);
14144 }
14145 Filter::Not(inner) => collect_filter_columns(inner, table_name, table_alias, columns),
14146 }
14147}
14148
14149fn collect_expr_columns(
14150 expr: &crate::storage::query::ast::Expr,
14151 table_name: &str,
14152 table_alias: Option<&str>,
14153 columns: &mut std::collections::BTreeSet<String>,
14154) {
14155 use crate::storage::query::ast::Expr;
14156 match expr {
14157 Expr::Column { field, .. } => {
14158 collect_field_ref_column(field, table_name, table_alias, columns);
14159 }
14160 Expr::Literal { .. } | Expr::Parameter { .. } => {}
14161 Expr::UnaryOp { operand, .. } | Expr::Cast { inner: operand, .. } => {
14162 collect_expr_columns(operand, table_name, table_alias, columns);
14163 }
14164 Expr::BinaryOp { lhs, rhs, .. } => {
14165 collect_expr_columns(lhs, table_name, table_alias, columns);
14166 collect_expr_columns(rhs, table_name, table_alias, columns);
14167 }
14168 Expr::FunctionCall { args, .. } => {
14169 for arg in args {
14170 collect_expr_columns(arg, table_name, table_alias, columns);
14171 }
14172 }
14173 Expr::Case {
14174 branches, else_, ..
14175 } => {
14176 for (condition, value) in branches {
14177 collect_expr_columns(condition, table_name, table_alias, columns);
14178 collect_expr_columns(value, table_name, table_alias, columns);
14179 }
14180 if let Some(value) = else_ {
14181 collect_expr_columns(value, table_name, table_alias, columns);
14182 }
14183 }
14184 Expr::IsNull { operand, .. } => {
14185 collect_expr_columns(operand, table_name, table_alias, columns);
14186 }
14187 Expr::InList { target, values, .. } => {
14188 collect_expr_columns(target, table_name, table_alias, columns);
14189 for value in values {
14190 collect_expr_columns(value, table_name, table_alias, columns);
14191 }
14192 }
14193 Expr::Between {
14194 target, low, high, ..
14195 } => {
14196 collect_expr_columns(target, table_name, table_alias, columns);
14197 collect_expr_columns(low, table_name, table_alias, columns);
14198 collect_expr_columns(high, table_name, table_alias, columns);
14199 }
14200 Expr::Subquery { .. } => {}
14201 Expr::WindowFunctionCall { args, window, .. } => {
14202 for arg in args {
14203 collect_expr_columns(arg, table_name, table_alias, columns);
14204 }
14205 for e in &window.partition_by {
14206 collect_expr_columns(e, table_name, table_alias, columns);
14207 }
14208 for o in &window.order_by {
14209 collect_expr_columns(&o.expr, table_name, table_alias, columns);
14210 }
14211 }
14212 }
14213}
14214
14215fn collect_field_ref_column(
14216 field: &crate::storage::query::ast::FieldRef,
14217 table_name: &str,
14218 table_alias: Option<&str>,
14219 columns: &mut std::collections::BTreeSet<String>,
14220) {
14221 if let Some(column) = policy_column_name_from_field_ref(field, table_name, table_alias) {
14222 if column != "*" {
14223 columns.insert(column);
14224 }
14225 }
14226}
14227
14228fn policy_column_name_from_field_ref(
14229 field: &crate::storage::query::ast::FieldRef,
14230 table_name: &str,
14231 table_alias: Option<&str>,
14232) -> Option<String> {
14233 match field {
14234 crate::storage::query::ast::FieldRef::TableColumn { table, column } => {
14235 if column == "*" {
14236 return Some("*".to_string());
14237 }
14238 if table.is_empty() || table == table_name || Some(table.as_str()) == table_alias {
14239 Some(column.clone())
14240 } else {
14241 Some(format!("{table}.{column}"))
14242 }
14243 }
14244 _ => None,
14245 }
14246}
14247
14248fn legacy_resource_to_iam(
14249 resource: &crate::auth::privileges::Resource,
14250 tenant: Option<&str>,
14251) -> crate::auth::policies::ResourceRef {
14252 use crate::auth::privileges::Resource;
14253
14254 let (kind, name) = match resource {
14255 Resource::Database => ("database".to_string(), "*".to_string()),
14256 Resource::Schema(s) => ("schema".to_string(), format!("{s}.*")),
14257 Resource::Table { schema, table } => (
14258 "table".to_string(),
14259 match schema {
14260 Some(s) => format!("{s}.{table}"),
14261 None => table.clone(),
14262 },
14263 ),
14264 Resource::Function { schema, name } => (
14265 "function".to_string(),
14266 match schema {
14267 Some(s) => format!("{s}.{name}"),
14268 None => name.clone(),
14269 },
14270 ),
14271 };
14272
14273 let mut out = crate::auth::policies::ResourceRef::new(kind, name);
14274 if let Some(t) = tenant {
14275 out = out.with_tenant(t.to_string());
14276 }
14277 out
14278}
14279
14280#[derive(Debug)]
14281struct JoinTableSide {
14282 table: String,
14283 alias: String,
14284}
14285
14286fn table_side_context(expr: &QueryExpr) -> Option<JoinTableSide> {
14287 match expr {
14288 QueryExpr::Table(table) => Some(JoinTableSide {
14289 table: table.table.clone(),
14290 alias: table.alias.clone().unwrap_or_else(|| table.table.clone()),
14291 }),
14292 _ => None,
14293 }
14294}
14295
14296fn collect_projection_columns_for_table(
14297 projection: &Projection,
14298 table: &str,
14299 alias: Option<&str>,
14300 out: &mut BTreeSet<String>,
14301) {
14302 match projection {
14303 Projection::Column(column) | Projection::Alias(column, _) => {
14304 match split_qualified_column(column) {
14305 Some((qualifier, column))
14306 if qualifier == table || alias.is_some_and(|alias| qualifier == alias) =>
14307 {
14308 push_policy_column(column, out);
14309 }
14310 Some(_) => {}
14311 None => push_policy_column(column, out),
14312 }
14313 }
14314 Projection::Field(
14315 FieldRef::TableColumn {
14316 table: qualifier,
14317 column,
14318 },
14319 _,
14320 ) => {
14321 if qualifier.is_empty()
14322 || qualifier == table
14323 || alias.is_some_and(|alias| qualifier == alias)
14324 {
14325 push_policy_column(column, out);
14326 }
14327 }
14328 Projection::Field(
14329 FieldRef::NodeProperty {
14330 alias: qualifier,
14331 property,
14332 },
14333 _,
14334 )
14335 | Projection::Field(
14336 FieldRef::EdgeProperty {
14337 alias: qualifier,
14338 property,
14339 },
14340 _,
14341 ) => {
14342 if qualifier == table || alias.is_some_and(|alias| qualifier == alias) {
14343 push_policy_column(property, out);
14344 }
14345 }
14346 Projection::Function(_, args) => {
14347 for arg in args {
14348 collect_projection_columns_for_table(arg, table, alias, out);
14349 }
14350 }
14351 Projection::Expression(_, _) | Projection::All | Projection::Field(_, _) => {}
14352 Projection::Window { args, .. } => {
14353 for arg in args {
14354 collect_projection_columns_for_table(arg, table, alias, out);
14355 }
14356 }
14357 }
14358}
14359
14360fn collect_projection_columns_for_join_side(
14361 projection: &Projection,
14362 left: Option<&JoinTableSide>,
14363 right: Option<&JoinTableSide>,
14364 out: &mut HashMap<String, BTreeSet<String>>,
14365) -> RedDBResult<()> {
14366 match projection {
14367 Projection::Column(column) | Projection::Alias(column, _) => {
14368 if let Some((qualifier, column)) = split_qualified_column(column) {
14369 push_qualified_join_column(qualifier, column, left, right, out);
14370 } else {
14371 push_unqualified_join_column(column, left, right, out);
14372 }
14373 }
14374 Projection::Field(FieldRef::TableColumn { table, column }, _) => {
14375 if table.is_empty() {
14376 push_unqualified_join_column(column, left, right, out);
14377 } else if let Some(side) = [left, right]
14378 .into_iter()
14379 .flatten()
14380 .find(|side| table == side.table.as_str() || table == side.alias.as_str())
14381 {
14382 push_join_column(&side.table, column, out);
14383 }
14384 }
14385 Projection::Field(FieldRef::NodeProperty { alias, property }, _)
14386 | Projection::Field(FieldRef::EdgeProperty { alias, property }, _) => {
14387 push_qualified_join_column(alias, property, left, right, out);
14388 }
14389 Projection::Function(_, args) => {
14390 for arg in args {
14391 collect_projection_columns_for_join_side(arg, left, right, out)?;
14392 }
14393 }
14394 Projection::Expression(_, _) | Projection::All | Projection::Field(_, _) => {}
14395 Projection::Window { args, .. } => {
14396 for arg in args {
14397 collect_projection_columns_for_join_side(arg, left, right, out)?;
14398 }
14399 }
14400 }
14401 Ok(())
14402}
14403
14404fn split_qualified_column(column: &str) -> Option<(&str, &str)> {
14405 let (qualifier, column) = column.split_once('.')?;
14406 if qualifier.is_empty() || column.is_empty() || column.contains('.') {
14407 return None;
14408 }
14409 Some((qualifier, column))
14410}
14411
14412fn push_qualified_join_column(
14413 qualifier: &str,
14414 column: &str,
14415 left: Option<&JoinTableSide>,
14416 right: Option<&JoinTableSide>,
14417 out: &mut HashMap<String, BTreeSet<String>>,
14418) {
14419 if let Some(side) = [left, right]
14420 .into_iter()
14421 .flatten()
14422 .find(|side| qualifier == side.table.as_str() || qualifier == side.alias.as_str())
14423 {
14424 push_join_column(&side.table, column, out);
14425 }
14426}
14427
14428fn push_unqualified_join_column(
14429 column: &str,
14430 left: Option<&JoinTableSide>,
14431 right: Option<&JoinTableSide>,
14432 out: &mut HashMap<String, BTreeSet<String>>,
14433) {
14434 for side in [left, right].into_iter().flatten() {
14435 push_join_column(&side.table, column, out);
14436 }
14437}
14438
14439fn push_join_column(table: &str, column: &str, out: &mut HashMap<String, BTreeSet<String>>) {
14440 if is_policy_column_name(column) {
14441 out.entry(table.to_string())
14442 .or_default()
14443 .insert(column.to_string());
14444 }
14445}
14446
14447fn push_policy_column(column: &str, out: &mut BTreeSet<String>) {
14448 if is_policy_column_name(column) {
14449 out.insert(column.to_string());
14450 }
14451}
14452
14453fn is_policy_column_name(column: &str) -> bool {
14454 !column.is_empty()
14455 && column != "*"
14456 && !column.starts_with("LIT:")
14457 && !column.starts_with("TYPE:")
14458}
14459
14460fn runtime_iam_context(
14461 role: crate::auth::Role,
14462 tenant: Option<&str>,
14463 principal_is_system_owned: bool,
14464) -> crate::auth::policies::EvalContext {
14465 crate::auth::policies::EvalContext {
14466 principal_tenant: tenant.map(|t| t.to_string()),
14467 current_tenant: tenant.map(|t| t.to_string()),
14468 peer_ip: None,
14469 mfa_present: false,
14470 now_ms: crate::auth::now_ms(),
14471 principal_is_admin_role: role == crate::auth::Role::Admin,
14472 principal_is_system_owned,
14473 principal_is_platform_scoped: tenant.is_none(),
14474 }
14475}
14476
14477fn explicit_table_projection_columns(
14478 query: &crate::storage::query::ast::TableQuery,
14479) -> Vec<String> {
14480 use crate::storage::query::ast::{FieldRef, Projection};
14481
14482 let mut columns = Vec::new();
14483 for projection in crate::storage::query::sql_lowering::effective_table_projections(query) {
14484 match projection {
14485 Projection::Column(column) | Projection::Alias(column, _) => {
14486 push_unique(&mut columns, column)
14487 }
14488 Projection::Field(FieldRef::TableColumn { column, .. }, _) => {
14489 push_unique(&mut columns, column)
14490 }
14491 _ => {}
14495 }
14496 }
14497 columns
14498}
14499
14500fn explicit_graph_projection_properties(
14501 query: &crate::storage::query::ast::GraphQuery,
14502) -> Vec<String> {
14503 use crate::storage::query::ast::{FieldRef, Projection};
14504
14505 let mut columns = Vec::new();
14506 for projection in &query.return_ {
14507 match projection {
14508 Projection::Field(FieldRef::NodeProperty { property, .. }, _)
14509 | Projection::Field(FieldRef::EdgeProperty { property, .. }, _) => {
14510 push_unique(&mut columns, property.clone())
14511 }
14512 _ => {}
14513 }
14514 }
14515 columns
14516}
14517
14518fn push_unique(columns: &mut Vec<String>, column: String) {
14519 if !columns.iter().any(|existing| existing == &column) {
14520 columns.push(column);
14521 }
14522}
14523
14524fn principal_label(p: &crate::storage::query::ast::PolicyPrincipalRef) -> String {
14525 use crate::storage::query::ast::PolicyPrincipalRef;
14526 match p {
14527 PolicyPrincipalRef::User(u) => match &u.tenant {
14528 Some(t) => format!("user:{t}/{}", u.username),
14529 None => format!("user:{}", u.username),
14530 },
14531 PolicyPrincipalRef::Group(g) => format!("group:{g}"),
14532 }
14533}
14534
14535pub(crate) fn decision_to_strings(
14538 d: &crate::auth::policies::Decision,
14539) -> (String, Option<String>, Option<String>) {
14540 use crate::auth::policies::Decision;
14541 match d {
14542 Decision::Allow {
14543 matched_policy_id,
14544 matched_sid,
14545 } => (
14546 "allow".into(),
14547 Some(matched_policy_id.clone()),
14548 matched_sid.clone(),
14549 ),
14550 Decision::Deny {
14551 matched_policy_id,
14552 matched_sid,
14553 } => (
14554 "deny".into(),
14555 Some(matched_policy_id.clone()),
14556 matched_sid.clone(),
14557 ),
14558 Decision::DefaultDeny => ("default_deny".into(), None, None),
14559 Decision::AdminBypass => ("admin_bypass".into(), None, None),
14560 }
14561}
14562
14563fn relation_scopes_for_query(query: &QueryExpr) -> Vec<String> {
14564 let mut scopes = Vec::new();
14565 collect_relation_scopes(query, &mut scopes);
14566 scopes.sort();
14567 scopes.dedup();
14568 scopes
14569}
14570
14571fn collect_relation_scopes(query: &QueryExpr, scopes: &mut Vec<String>) {
14572 match query {
14573 QueryExpr::Table(table) => {
14574 if !table.table.is_empty() {
14575 scopes.push(table.table.clone());
14576 }
14577 if let Some(alias) = &table.alias {
14578 scopes.push(alias.clone());
14579 }
14580 }
14581 QueryExpr::Join(join) => {
14582 collect_relation_scopes(&join.left, scopes);
14583 collect_relation_scopes(&join.right, scopes);
14584 }
14585 _ => {}
14586 }
14587}
14588
14589fn query_references_outer_scope(query: &QueryExpr, outer_scopes: &[String]) -> bool {
14590 let inner_scopes = relation_scopes_for_query(query);
14591 query_expr_references_outer_scope(query, outer_scopes, &inner_scopes)
14592}
14593
14594fn query_expr_references_outer_scope(
14595 query: &QueryExpr,
14596 outer_scopes: &[String],
14597 inner_scopes: &[String],
14598) -> bool {
14599 match query {
14600 QueryExpr::Table(table) => {
14601 table.select_items.iter().any(|item| match item {
14602 crate::storage::query::ast::SelectItem::Wildcard => false,
14603 crate::storage::query::ast::SelectItem::Expr { expr, .. } => {
14604 expr_references_outer_scope(expr, outer_scopes, inner_scopes)
14605 }
14606 }) || table
14607 .where_expr
14608 .as_ref()
14609 .is_some_and(|expr| expr_references_outer_scope(expr, outer_scopes, inner_scopes))
14610 || table.filter.as_ref().is_some_and(|filter| {
14611 filter_references_outer_scope(filter, outer_scopes, inner_scopes)
14612 })
14613 || table.having_expr.as_ref().is_some_and(|expr| {
14614 expr_references_outer_scope(expr, outer_scopes, inner_scopes)
14615 })
14616 || table.having.as_ref().is_some_and(|filter| {
14617 filter_references_outer_scope(filter, outer_scopes, inner_scopes)
14618 })
14619 || table
14620 .group_by_exprs
14621 .iter()
14622 .any(|expr| expr_references_outer_scope(expr, outer_scopes, inner_scopes))
14623 || table.order_by.iter().any(|clause| {
14624 clause.expr.as_ref().is_some_and(|expr| {
14625 expr_references_outer_scope(expr, outer_scopes, inner_scopes)
14626 })
14627 })
14628 }
14629 QueryExpr::Join(join) => {
14630 query_expr_references_outer_scope(&join.left, outer_scopes, inner_scopes)
14631 || query_expr_references_outer_scope(&join.right, outer_scopes, inner_scopes)
14632 || join.filter.as_ref().is_some_and(|filter| {
14633 filter_references_outer_scope(filter, outer_scopes, inner_scopes)
14634 })
14635 || join.return_items.iter().any(|item| match item {
14636 crate::storage::query::ast::SelectItem::Wildcard => false,
14637 crate::storage::query::ast::SelectItem::Expr { expr, .. } => {
14638 expr_references_outer_scope(expr, outer_scopes, inner_scopes)
14639 }
14640 })
14641 }
14642 _ => false,
14643 }
14644}
14645
14646fn filter_references_outer_scope(
14647 filter: &crate::storage::query::ast::Filter,
14648 outer_scopes: &[String],
14649 inner_scopes: &[String],
14650) -> bool {
14651 use crate::storage::query::ast::Filter;
14652 match filter {
14653 Filter::Compare { field, .. }
14654 | Filter::IsNull(field)
14655 | Filter::IsNotNull(field)
14656 | Filter::In { field, .. }
14657 | Filter::Between { field, .. }
14658 | Filter::Like { field, .. }
14659 | Filter::StartsWith { field, .. }
14660 | Filter::EndsWith { field, .. }
14661 | Filter::Contains { field, .. } => {
14662 field_ref_references_outer_scope(field, outer_scopes, inner_scopes)
14663 }
14664 Filter::CompareFields { left, right, .. } => {
14665 field_ref_references_outer_scope(left, outer_scopes, inner_scopes)
14666 || field_ref_references_outer_scope(right, outer_scopes, inner_scopes)
14667 }
14668 Filter::CompareExpr { lhs, rhs, .. } => {
14669 expr_references_outer_scope(lhs, outer_scopes, inner_scopes)
14670 || expr_references_outer_scope(rhs, outer_scopes, inner_scopes)
14671 }
14672 Filter::And(left, right) | Filter::Or(left, right) => {
14673 filter_references_outer_scope(left, outer_scopes, inner_scopes)
14674 || filter_references_outer_scope(right, outer_scopes, inner_scopes)
14675 }
14676 Filter::Not(inner) => filter_references_outer_scope(inner, outer_scopes, inner_scopes),
14677 }
14678}
14679
14680fn expr_references_outer_scope(
14681 expr: &crate::storage::query::ast::Expr,
14682 outer_scopes: &[String],
14683 inner_scopes: &[String],
14684) -> bool {
14685 use crate::storage::query::ast::Expr;
14686 match expr {
14687 Expr::Column { field, .. } => {
14688 field_ref_references_outer_scope(field, outer_scopes, inner_scopes)
14689 }
14690 Expr::BinaryOp { lhs, rhs, .. } => {
14691 expr_references_outer_scope(lhs, outer_scopes, inner_scopes)
14692 || expr_references_outer_scope(rhs, outer_scopes, inner_scopes)
14693 }
14694 Expr::UnaryOp { operand, .. }
14695 | Expr::Cast { inner: operand, .. }
14696 | Expr::IsNull { operand, .. } => {
14697 expr_references_outer_scope(operand, outer_scopes, inner_scopes)
14698 }
14699 Expr::FunctionCall { args, .. } => args
14700 .iter()
14701 .any(|arg| expr_references_outer_scope(arg, outer_scopes, inner_scopes)),
14702 Expr::Case {
14703 branches, else_, ..
14704 } => {
14705 branches.iter().any(|(cond, value)| {
14706 expr_references_outer_scope(cond, outer_scopes, inner_scopes)
14707 || expr_references_outer_scope(value, outer_scopes, inner_scopes)
14708 }) || else_
14709 .as_ref()
14710 .is_some_and(|expr| expr_references_outer_scope(expr, outer_scopes, inner_scopes))
14711 }
14712 Expr::InList { target, values, .. } => {
14713 expr_references_outer_scope(target, outer_scopes, inner_scopes)
14714 || values
14715 .iter()
14716 .any(|value| expr_references_outer_scope(value, outer_scopes, inner_scopes))
14717 }
14718 Expr::Between {
14719 target, low, high, ..
14720 } => {
14721 expr_references_outer_scope(target, outer_scopes, inner_scopes)
14722 || expr_references_outer_scope(low, outer_scopes, inner_scopes)
14723 || expr_references_outer_scope(high, outer_scopes, inner_scopes)
14724 }
14725 Expr::Subquery { query, .. } => query_references_outer_scope(&query.query, inner_scopes),
14726 Expr::Literal { .. } | Expr::Parameter { .. } => false,
14727 Expr::WindowFunctionCall { args, window, .. } => {
14728 args.iter()
14729 .any(|arg| expr_references_outer_scope(arg, outer_scopes, inner_scopes))
14730 || window
14731 .partition_by
14732 .iter()
14733 .any(|e| expr_references_outer_scope(e, outer_scopes, inner_scopes))
14734 || window
14735 .order_by
14736 .iter()
14737 .any(|o| expr_references_outer_scope(&o.expr, outer_scopes, inner_scopes))
14738 }
14739 }
14740}
14741
14742fn field_ref_references_outer_scope(
14743 field: &crate::storage::query::ast::FieldRef,
14744 outer_scopes: &[String],
14745 inner_scopes: &[String],
14746) -> bool {
14747 match field {
14748 crate::storage::query::ast::FieldRef::TableColumn { table, .. } if !table.is_empty() => {
14749 outer_scopes.iter().any(|scope| scope == table)
14750 && !inner_scopes.iter().any(|scope| scope == table)
14751 }
14752 _ => false,
14753 }
14754}
14755
14756fn first_column_values(
14757 result: crate::storage::query::unified::UnifiedResult,
14758) -> RedDBResult<Vec<Value>> {
14759 if result.columns.len() > 1 {
14760 return Err(RedDBError::Query(
14761 "expression subquery must return exactly one column".to_string(),
14762 ));
14763 }
14764 let fallback_column = result
14765 .records
14766 .first()
14767 .and_then(|record| record.column_names().into_iter().next())
14768 .map(|name| name.to_string());
14769 let column = result.columns.first().cloned().or(fallback_column);
14770 let Some(column) = column else {
14771 return Ok(Vec::new());
14772 };
14773 Ok(result
14774 .records
14775 .iter()
14776 .map(|record| record.get(column.as_str()).cloned().unwrap_or(Value::Null))
14777 .collect())
14778}
14779
14780fn parse_timestamp_to_ms(s: &str) -> Option<u128> {
14781 if let Ok(n) = s.parse::<u128>() {
14783 return Some(n);
14784 }
14785 if let Some(date) = s.split_whitespace().next() {
14789 let parts: Vec<&str> = date.split('-').collect();
14790 if parts.len() == 3 {
14791 let (y, m, d) = (parts[0], parts[1], parts[2]);
14792 if let (Ok(y), Ok(m), Ok(d)) = (y.parse::<i64>(), m.parse::<u32>(), d.parse::<u32>()) {
14793 let days_in = days_from_civil(y, m, d);
14797 return Some((days_in as u128) * 86_400_000u128);
14798 }
14799 }
14800 }
14801 None
14802}
14803
14804fn days_from_civil(y: i64, m: u32, d: u32) -> i64 {
14807 let y = if m <= 2 { y - 1 } else { y };
14808 let era = if y >= 0 { y } else { y - 399 } / 400;
14809 let yoe = (y - era * 400) as u64; let doy = (153 * (if m > 2 { m - 3 } else { m + 9 }) as u64 + 2) / 5 + d as u64 - 1;
14811 let doe = yoe * 365 + yoe / 4 - yoe / 100 + doy;
14812 era * 146097 + doe as i64 - 719468
14813}
14814
14815fn walk_plan_node(
14816 node: &crate::storage::query::planner::CanonicalLogicalNode,
14817 depth: usize,
14818 out: &mut Vec<crate::storage::query::unified::UnifiedRecord>,
14819) {
14820 use std::sync::Arc;
14821 let mut rec = crate::storage::query::unified::UnifiedRecord::default();
14822 rec.set_arc(Arc::from("op"), Value::text(node.operator.clone()));
14823 rec.set_arc(
14824 Arc::from("source"),
14825 node.source.clone().map(Value::text).unwrap_or(Value::Null),
14826 );
14827 rec.set_arc(Arc::from("est_rows"), Value::Float(node.estimated_rows));
14828 rec.set_arc(Arc::from("est_cost"), Value::Float(node.operator_cost));
14829 rec.set_arc(Arc::from("depth"), Value::Integer(depth as i64));
14830 out.push(rec);
14831 for child in &node.children {
14832 walk_plan_node(child, depth + 1, out);
14833 }
14834}
14835
14836#[cfg(test)]
14837mod inline_graph_tvf_tests {
14838 use super::*;
14839
14840 fn scopes_for(sql: &str) -> HashSet<String> {
14841 let expr = crate::storage::query::parser::parse(sql)
14842 .expect("parse")
14843 .query;
14844 query_expr_result_cache_scopes(&expr)
14845 }
14846
14847 #[test]
14848 fn inline_tvf_cache_scopes_include_source_collections() {
14849 let scopes = scopes_for(
14853 "SELECT * FROM components(nodes => (SELECT id FROM hosts), edges => (SELECT src, dst FROM links))",
14854 );
14855 assert!(scopes.contains("hosts"), "nodes source scoped: {scopes:?}");
14856 assert!(scopes.contains("links"), "edges source scoped: {scopes:?}");
14857 }
14858
14859 #[test]
14860 fn graph_collection_tvf_has_no_cache_scope() {
14861 let scopes = scopes_for("SELECT * FROM components(g)");
14864 assert!(scopes.is_empty(), "collection form unscoped: {scopes:?}");
14865 }
14866
14867 #[test]
14868 fn abstract_degree_centrality_counts_undirected_endpoints() {
14869 let nodes = vec!["a".to_string(), "b".to_string(), "c".to_string()];
14870 let edges = vec![
14871 ("a".to_string(), "b".to_string(), 1.0_f32),
14872 ("b".to_string(), "c".to_string(), 1.0_f32),
14873 ];
14874 let degrees = abstract_degree_centrality(&nodes, &edges);
14875 assert_eq!(
14876 degrees,
14877 vec![
14878 ("a".to_string(), 1),
14879 ("b".to_string(), 2),
14880 ("c".to_string(), 1),
14881 ]
14882 );
14883 }
14884}