1use super::*;
2use crate::auth::column_policy_gate::ColumnAccessRequest;
3use crate::auth::UserId;
4use crate::replication::cdc::ChangeRecord;
5use crate::storage::query::ast::TableSource;
6
7fn record_column_f64(
12 rec: &crate::storage::query::unified::UnifiedRecord,
13 column: &str,
14) -> Option<f64> {
15 let value = rec
16 .get(column)
17 .or_else(|| rec.get(&column.to_lowercase()))?;
18 match value {
19 Value::Integer(n) => Some(*n as f64),
20 Value::UnsignedInteger(n) => Some(*n as f64),
21 Value::Float(n) => Some(*n),
22 Value::Timestamp(n) | Value::Duration(n) => Some(*n as f64),
23 _ => None,
24 }
25}
26
27fn record_rid_u64(rec: &crate::storage::query::unified::UnifiedRecord) -> Option<u64> {
28 match rec.get("rid") {
29 Some(Value::UnsignedInteger(n)) => Some(*n),
30 Some(Value::Integer(n)) if *n >= 0 => Some(*n as u64),
31 _ => None,
32 }
33}
34
35fn seed_storage_deploy_config(
36 store: &crate::storage::UnifiedStore,
37 selection: crate::storage::StorageProfileSelection,
38) {
39 store.set_config_tree(
40 "storage.deploy",
41 &crate::json!({
42 "profile": selection.deploy_profile.as_str(),
43 "packaging": selection.packaging.as_str(),
44 "preset": selection.preset_name(),
45 "replica_count": selection.replica_count,
46 "managed_backup": selection.managed_backup,
47 "wal_retention": selection.wal_retention,
48 }),
49 );
50}
51
52struct RankedHeadEntry {
53 rank: u64,
54 record: crate::storage::query::unified::UnifiedRecord,
55}
56
57fn secret_sql_value_to_string(value: &Value) -> RedDBResult<String> {
58 match value {
59 Value::Text(s) => Ok(s.to_string()),
60 Value::Integer(n) => Ok(n.to_string()),
61 Value::UnsignedInteger(n) => Ok(n.to_string()),
62 Value::Float(n) => Ok(n.to_string()),
63 Value::Boolean(b) => Ok(b.to_string()),
64 Value::Null => Err(RedDBError::Query(
65 "SET SECRET key = NULL deletes the secret; use DELETE SECRET for explicit deletes"
66 .to_string(),
67 )),
68 Value::Password(_) | Value::Secret(_) => Err(RedDBError::Query(
69 "SET SECRET accepts plain scalar literals; PASSWORD() and SECRET() are for typed columns"
70 .to_string(),
71 )),
72 _ => Err(RedDBError::Query(format!(
73 "SET SECRET does not support value type {:?} yet",
74 value.data_type()
75 ))),
76 }
77}
78
79fn insert_config_json_path(
80 root: &mut crate::serde_json::Value,
81 path: &str,
82 value: crate::serde_json::Value,
83) {
84 let segments: Vec<&str> = path
85 .split('.')
86 .filter(|segment| !segment.is_empty())
87 .collect();
88 insert_config_json_segments(root, &segments, value);
89}
90
91fn insert_config_json_segments(
92 root: &mut crate::serde_json::Value,
93 segments: &[&str],
94 value: crate::serde_json::Value,
95) {
96 if segments.is_empty() {
97 *root = value;
98 return;
99 }
100
101 if !matches!(root, crate::serde_json::Value::Object(_)) {
102 *root = crate::serde_json::Value::Object(crate::serde_json::Map::new());
103 }
104
105 let crate::serde_json::Value::Object(map) = root else {
106 return;
107 };
108 if segments.len() == 1 {
109 map.insert(segments[0].to_string(), value);
110 return;
111 }
112 let entry = map
113 .entry(segments[0].to_string())
114 .or_insert_with(|| crate::serde_json::Value::Object(crate::serde_json::Map::new()));
115 insert_config_json_segments(entry, &segments[1..], value);
116}
117
118fn show_config_json_result(
119 query: &str,
120 mode: crate::storage::query::modes::QueryMode,
121 prefix: &Option<String>,
122 value: crate::serde_json::Value,
123) -> RuntimeQueryResult {
124 let mut result = UnifiedResult::with_columns(vec!["key".into(), "value".into()]);
125 let mut record = UnifiedRecord::new();
126 record.set(
127 "key",
128 prefix
129 .as_ref()
130 .map(|key| Value::text(key.clone()))
131 .unwrap_or(Value::Null),
132 );
133 record.set("value", Value::Json(value.to_string_compact().into_bytes()));
134 result.push(record);
135 RuntimeQueryResult {
136 query: query.to_string(),
137 mode,
138 statement: "show_config_json",
139 engine: "runtime-config",
140 result,
141 affected_rows: 0,
142 statement_type: "select",
143 bookmark: None,
144 }
145}
146
147#[derive(Clone)]
148struct QueryControlEventSpec {
149 kind: crate::runtime::control_events::EventKind,
150 action: &'static str,
151 resource: Option<String>,
152 fields: Vec<(String, crate::runtime::control_events::Sensitivity)>,
153}
154
155#[derive(Clone)]
156struct QueryAuditPlan {
157 statement_kind: &'static str,
158 collections: Vec<String>,
159}
160
161fn query_audit_plan(expr: &QueryExpr) -> Option<QueryAuditPlan> {
162 let mut collections = Vec::new();
163 let statement_kind = match expr {
164 QueryExpr::Table(table) => {
165 push_query_audit_collection(&mut collections, &table.table);
166 "select"
167 }
168 QueryExpr::Join(join) => {
169 collect_query_audit_collections(&join.left, &mut collections);
170 collect_query_audit_collections(&join.right, &mut collections);
171 "select"
172 }
173 QueryExpr::Insert(insert) => {
174 push_query_audit_collection(&mut collections, &insert.table);
175 "insert"
176 }
177 QueryExpr::Update(update) => {
178 push_query_audit_collection(&mut collections, &update.table);
179 "update"
180 }
181 QueryExpr::Delete(delete) => {
182 push_query_audit_collection(&mut collections, &delete.table);
183 "delete"
184 }
185 _ => return None,
186 };
187 if collections.is_empty() {
188 None
189 } else {
190 Some(QueryAuditPlan {
191 statement_kind,
192 collections,
193 })
194 }
195}
196
197fn collect_query_audit_collections(expr: &QueryExpr, collections: &mut Vec<String>) {
198 match expr {
199 QueryExpr::Table(table) => push_query_audit_collection(collections, &table.table),
200 QueryExpr::Join(join) => {
201 collect_query_audit_collections(&join.left, collections);
202 collect_query_audit_collections(&join.right, collections);
203 }
204 _ => {}
205 }
206}
207
208fn push_query_audit_collection(collections: &mut Vec<String>, name: &str) {
209 if name == "red" || name.starts_with("red.") || name.starts_with("__red_schema_") {
210 return;
211 }
212 if !collections.iter().any(|existing| existing == name) {
213 collections.push(name.to_string());
214 }
215}
216
217const RUNTIME_INDEX_REGISTRY_COLLECTION: &str = "red_index_registry";
218
219impl RedDBRuntime {
220 fn execute_create_metric(
221 &self,
222 raw_query: &str,
223 query: &crate::storage::query::ast::CreateMetricQuery,
224 ) -> RedDBResult<RuntimeQueryResult> {
225 self.check_write(crate::runtime::write_gate::WriteKind::Ddl)?;
226 let store = self.inner.db.store();
227 super::metric_descriptor_catalog::create(
228 store.as_ref(),
229 &query.path,
230 &query.kind,
231 &query.role,
232 super::metric_descriptor_catalog::DerivedSpec {
233 source: query.source.clone(),
234 query: query.query.clone(),
235 window_ms: query.window_ms,
236 time_field: query.time_field.clone(),
237 },
238 )?;
239 self.invalidate_result_cache();
240 Ok(RuntimeQueryResult::ok_message(
241 raw_query.to_string(),
242 &format!("metric descriptor '{}' created", query.path),
243 "create",
244 ))
245 }
246
247 fn execute_create_ranking(
253 &self,
254 raw_query: &str,
255 req: super::ranking_descriptor_catalog::CreateRankingRequest,
256 ) -> RedDBResult<RuntimeQueryResult> {
257 self.check_write(crate::runtime::write_gate::WriteKind::Ddl)?;
258 let store = self.inner.db.store();
259 let descriptor = super::ranking_descriptor_catalog::create(store.as_ref(), &req)?;
260 self.invalidate_result_cache();
261 Ok(RuntimeQueryResult::ok_message(
262 raw_query.to_string(),
263 &format!(
264 "ranking '{}' created on {}({})",
265 descriptor.name, descriptor.table, descriptor.column
266 ),
267 "create",
268 ))
269 }
270
271 fn execute_show_rankings(&self, raw_query: &str) -> RedDBResult<RuntimeQueryResult> {
275 let store = self.inner.db.store();
276 let entries = super::ranking_descriptor_catalog::list(store.as_ref());
277 let columns = vec![
278 "name".to_string(),
279 "table".to_string(),
280 "column".to_string(),
281 "direction".to_string(),
282 "top_k".to_string(),
283 ];
284 let rows = entries
285 .into_iter()
286 .map(|e| {
287 vec![
288 ("name".to_string(), Value::text(e.name)),
289 ("table".to_string(), Value::text(e.table)),
290 ("column".to_string(), Value::text(e.column)),
291 (
292 "direction".to_string(),
293 Value::text(if e.descending { "DESC" } else { "ASC" }.to_string()),
294 ),
295 ("top_k".to_string(), Value::UnsignedInteger(e.top_k)),
296 ]
297 })
298 .collect();
299 let mut result =
300 RuntimeQueryResult::ok_records(raw_query.to_string(), columns, rows, "select");
301 result.statement = "rank_of";
302 result.engine = "runtime-rank";
303 Ok(result)
304 }
305
306 fn execute_rank_of(
315 &self,
316 raw_query: &str,
317 req: &crate::storage::query::ast::RankOfQuery,
318 ) -> RedDBResult<RuntimeQueryResult> {
319 let store = self.inner.db.store();
320 let descriptor = super::ranking_descriptor_catalog::get(store.as_ref(), &req.ranking)
321 .ok_or_else(|| {
322 RedDBError::Query(format!("ranking '{}' does not exist", req.ranking))
323 })?;
324 let rank = self.compute_exact_head_rank(&descriptor, req.entity_id)?;
325 let columns = vec!["rank".to_string()];
326 let rows = match rank {
327 Some(rank) => vec![vec![("rank".to_string(), Value::UnsignedInteger(rank))]],
328 None => Vec::new(),
329 };
330 let mut result =
331 RuntimeQueryResult::ok_records(raw_query.to_string(), columns, rows, "select");
332 result.statement = "rank_range";
333 result.engine = "runtime-rank";
334 Ok(result)
335 }
336
337 fn execute_rank_range(
343 &self,
344 raw_query: &str,
345 req: &crate::storage::query::ast::RankRangeQuery,
346 ) -> RedDBResult<RuntimeQueryResult> {
347 let store = self.inner.db.store();
348 let descriptor = super::ranking_descriptor_catalog::get(store.as_ref(), &req.ranking)
349 .ok_or_else(|| {
350 RedDBError::Query(format!("ranking '{}' does not exist", req.ranking))
351 })?;
352 let (head_columns, entries) = self.compute_ranked_head_entries(&descriptor)?;
353
354 let mut columns = Vec::with_capacity(head_columns.len() + 1);
355 columns.push("rank".to_string());
356 for column in &head_columns {
357 if column != "rank" {
358 columns.push(column.clone());
359 }
360 }
361
362 let rows = entries
363 .into_iter()
364 .filter(|entry| entry.rank >= req.lo && entry.rank <= req.hi)
365 .map(|entry| {
366 let mut row = Vec::with_capacity(columns.len());
367 row.push(("rank".to_string(), Value::UnsignedInteger(entry.rank)));
368 for column in &head_columns {
369 if column == "rank" {
370 continue;
371 }
372 if let Some(value) = entry.record.get(column) {
373 row.push((column.clone(), value.clone()));
374 }
375 }
376 row
377 })
378 .collect();
379 let mut result =
380 RuntimeQueryResult::ok_records(raw_query.to_string(), columns, rows, "select");
381 result.statement = "approx_rank_of";
382 result.engine = "runtime-rank";
383 Ok(result)
384 }
385
386 fn compute_exact_head_rank(
400 &self,
401 descriptor: &super::ranking_descriptor_catalog::RankingDescriptor,
402 target_id: u64,
403 ) -> RedDBResult<Option<u64>> {
404 let (_columns, entries) = self.compute_ranked_head_entries(descriptor)?;
405 Ok(entries
406 .into_iter()
407 .find(|entry| record_rid_u64(&entry.record) == Some(target_id))
408 .map(|entry| entry.rank))
409 }
410
411 fn compute_ranked_head_entries(
413 &self,
414 descriptor: &super::ranking_descriptor_catalog::RankingDescriptor,
415 ) -> RedDBResult<(Vec<String>, Vec<RankedHeadEntry>)> {
416 let table = &descriptor.table;
417 let column = &descriptor.column;
418
419 let dir = if descriptor.descending { "DESC" } else { "ASC" };
426 let head_sql = format!(
427 "SELECT * FROM {table} ORDER BY {column} {dir}, rid ASC LIMIT {}",
428 descriptor.top_k
429 );
430 let head_result = self.execute_query_inner(&head_sql)?;
431
432 let mut entries = Vec::with_capacity(head_result.result.records.len());
433 let mut row_position = 0u64;
434 let mut current_rank = 0u64;
435 let mut previous_score: Option<f64> = None;
436 for rec in &head_result.result.records {
437 let Some(score) = record_column_f64(rec, column) else {
438 continue;
439 };
440 row_position += 1;
441 current_rank = if previous_score == Some(score) {
442 current_rank
443 } else {
444 row_position
445 };
446 previous_score = Some(score);
447 entries.push(RankedHeadEntry {
448 rank: current_rank,
449 record: rec.clone(),
450 });
451 }
452 Ok((head_result.result.columns, entries))
453 }
454
455 fn execute_approx_rank_of(
466 &self,
467 raw_query: &str,
468 req: &crate::storage::query::ast::RankOfQuery,
469 ) -> RedDBResult<RuntimeQueryResult> {
470 let store = self.inner.db.store();
471 let descriptor = super::ranking_descriptor_catalog::get(store.as_ref(), &req.ranking)
472 .ok_or_else(|| {
473 RedDBError::Query(format!("ranking '{}' does not exist", req.ranking))
474 })?;
475
476 let approx = self.compute_approx_rank(&descriptor, req.entity_id)?;
477 let columns = vec![
478 "rank".to_string(),
479 "percentile".to_string(),
480 "approximate".to_string(),
481 ];
482 let rows = match approx {
483 Some(approx) => vec![vec![
484 ("rank".to_string(), Value::UnsignedInteger(approx.rank)),
485 ("percentile".to_string(), Value::Float(approx.percentile)),
486 ("approximate".to_string(), Value::Boolean(true)),
487 ]],
488 None => Vec::new(),
489 };
490 let mut result =
491 RuntimeQueryResult::ok_records(raw_query.to_string(), columns, rows, "select");
492 result.statement = "approx_rank_of";
493 result.engine = "runtime-rank";
500 Ok(result)
501 }
502
503 fn compute_approx_rank(
518 &self,
519 descriptor: &super::ranking_descriptor_catalog::RankingDescriptor,
520 target_id: u64,
521 ) -> RedDBResult<Option<super::score_sketch::ApproxRank>> {
522 let table = &descriptor.table;
523 let column = &descriptor.column;
524
525 let scan_sql = format!("SELECT * FROM {table}");
528 let scan = self.execute_query_inner(&scan_sql)?;
529 let records = &scan.result.records;
530
531 let mut scores: Vec<f64> = Vec::with_capacity(records.len());
532 let mut target_score: Option<f64> = None;
533 for rec in records {
534 let Some(score) = record_column_f64(rec, column) else {
535 continue;
536 };
537 scores.push(score);
538 let rid = match rec.get("rid") {
539 Some(Value::UnsignedInteger(n)) => Some(*n),
540 Some(Value::Integer(n)) if *n >= 0 => Some(*n as u64),
541 _ => None,
542 };
543 if rid == Some(target_id) {
544 target_score = Some(score);
545 }
546 }
547
548 let sketch = super::score_sketch::ScoreSketch::from_scores(&scores);
549 super::ranking_descriptor_catalog::save_sketch(
551 self.inner.db.store().as_ref(),
552 table,
553 column,
554 &sketch,
555 );
556
557 let Some(target_score) = target_score else {
558 return Ok(None);
560 };
561 Ok(sketch.approx_rank(target_score, descriptor.descending))
562 }
563
564 fn execute_alter_metric(
565 &self,
566 raw_query: &str,
567 query: &crate::storage::query::ast::AlterMetricQuery,
568 ) -> RedDBResult<RuntimeQueryResult> {
569 self.check_write(crate::runtime::write_gate::WriteKind::Ddl)?;
570 let store = self.inner.db.store();
571 super::metric_descriptor_catalog::update(
572 store.as_ref(),
573 &query.path,
574 query.set_role.as_deref(),
575 query.attempted_kind.as_deref(),
576 query.attempted_path.as_deref(),
577 )?;
578 self.invalidate_result_cache();
579 Ok(RuntimeQueryResult::ok_message(
580 raw_query.to_string(),
581 &format!("metric descriptor '{}' updated", query.path),
582 "alter",
583 ))
584 }
585
586 fn execute_create_slo(
587 &self,
588 raw_query: &str,
589 query: &crate::storage::query::ast::CreateSloQuery,
590 ) -> RedDBResult<RuntimeQueryResult> {
591 self.check_write(crate::runtime::write_gate::WriteKind::Ddl)?;
592 let store = self.inner.db.store();
593 super::slo_descriptor_catalog::create(
594 store.as_ref(),
595 &query.path,
596 &query.metric_path,
597 query.target,
598 query.window_ms,
599 )?;
600 self.invalidate_result_cache();
601 Ok(RuntimeQueryResult::ok_message(
602 raw_query.to_string(),
603 &format!("SLO descriptor '{}' created", query.path),
604 "create",
605 ))
606 }
607
608 fn execute_create_analytics_source(
609 &self,
610 raw_query: &str,
611 query: super::analytics_source_catalog::CreateAnalyticsSourceProfile,
612 ) -> RedDBResult<RuntimeQueryResult> {
613 self.check_write(crate::runtime::write_gate::WriteKind::Ddl)?;
614 let store = self.inner.db.store();
615 let profile = super::analytics_source_catalog::create(
616 store.as_ref(),
617 &self.inner.db.collection_contracts(),
618 query,
619 )?;
620 self.invalidate_result_cache();
621 Ok(RuntimeQueryResult::ok_message(
622 raw_query.to_string(),
623 &format!("analytics source '{}' created", profile.name),
624 "create",
625 ))
626 }
627}
628
629fn query_control_event_specs(expr: &QueryExpr) -> Vec<QueryControlEventSpec> {
630 use crate::runtime::control_events::{EventKind, Sensitivity};
631
632 let mut specs = Vec::new();
633 let mut schema = |action: &'static str, resource: Option<String>| {
634 specs.push(QueryControlEventSpec {
635 kind: EventKind::SchemaDdl,
636 action,
637 resource,
638 fields: Vec::new(),
639 });
640 };
641 match expr {
642 QueryExpr::CreateTable(q) => {
643 schema("create_table", Some(format!("table:{}", q.name)));
644 if let Some(column) = &q.tenant_by {
645 specs.push(QueryControlEventSpec {
646 kind: EventKind::TenantGovernance,
647 action: "create_table_tenant_by",
648 resource: Some(format!("table:{}", q.name)),
649 fields: vec![("tenant_column".to_string(), Sensitivity::raw(column))],
650 });
651 }
652 }
653 QueryExpr::CreateCollection(q) => {
654 schema("create_collection", Some(format!("collection:{}", q.name)));
655 }
656 QueryExpr::CreateVector(q) => schema("create_vector", Some(format!("vector:{}", q.name))),
657 QueryExpr::DropTable(q) => schema("drop_table", Some(format!("table:{}", q.name))),
658 QueryExpr::DropGraph(q) => schema("drop_graph", Some(format!("graph:{}", q.name))),
659 QueryExpr::DropVector(q) => schema("drop_vector", Some(format!("vector:{}", q.name))),
660 QueryExpr::DropDocument(q) => {
661 schema("drop_document", Some(format!("document:{}", q.name)));
662 }
663 QueryExpr::DropKv(q) => schema("drop_kv", Some(format!("kv:{}", q.name))),
664 QueryExpr::DropCollection(q) => {
665 schema("drop_collection", Some(format!("collection:{}", q.name)));
666 }
667 QueryExpr::Truncate(q) => schema("truncate", Some(format!("collection:{}", q.name))),
668 QueryExpr::AlterTable(q) => {
669 schema("alter_table", Some(format!("table:{}", q.name)));
670 for op in &q.operations {
671 match op {
672 crate::storage::query::ast::AlterOperation::EnableRowLevelSecurity => {
673 specs.push(QueryControlEventSpec {
674 kind: EventKind::RlsGovernance,
675 action: "enable_rls",
676 resource: Some(format!("table:{}", q.name)),
677 fields: Vec::new(),
678 });
679 }
680 crate::storage::query::ast::AlterOperation::DisableRowLevelSecurity => {
681 specs.push(QueryControlEventSpec {
682 kind: EventKind::RlsGovernance,
683 action: "disable_rls",
684 resource: Some(format!("table:{}", q.name)),
685 fields: Vec::new(),
686 });
687 }
688 crate::storage::query::ast::AlterOperation::EnableTenancy { column } => {
689 specs.push(QueryControlEventSpec {
690 kind: EventKind::TenantGovernance,
691 action: "enable_tenancy",
692 resource: Some(format!("table:{}", q.name)),
693 fields: vec![("tenant_column".to_string(), Sensitivity::raw(column))],
694 });
695 }
696 crate::storage::query::ast::AlterOperation::DisableTenancy => {
697 specs.push(QueryControlEventSpec {
698 kind: EventKind::TenantGovernance,
699 action: "disable_tenancy",
700 resource: Some(format!("table:{}", q.name)),
701 fields: Vec::new(),
702 });
703 }
704 _ => {}
705 }
706 }
707 }
708 QueryExpr::CreateIndex(q) => {
709 schema(
710 "create_index",
711 Some(format!("index:{}:{}", q.table, q.name)),
712 );
713 }
714 QueryExpr::DropIndex(q) => {
715 schema("drop_index", Some(format!("index:{}:{}", q.table, q.name)));
716 }
717 QueryExpr::CreateTimeSeries(q) => {
718 schema("create_timeseries", Some(format!("timeseries:{}", q.name)));
719 }
720 QueryExpr::CreateMetric(q) => {
721 schema("create_metric", Some(format!("metric:{}", q.path)));
722 }
723 QueryExpr::AlterMetric(q) => {
724 schema("alter_metric", Some(format!("metric:{}", q.path)));
725 }
726 QueryExpr::CreateSlo(q) => {
727 schema("create_slo", Some(format!("slo:{}", q.path)));
728 }
729 QueryExpr::DropTimeSeries(q) => {
730 schema("drop_timeseries", Some(format!("timeseries:{}", q.name)));
731 }
732 QueryExpr::CreateQueue(q) => schema("create_queue", Some(format!("queue:{}", q.name))),
733 QueryExpr::AlterQueue(q) => schema("alter_queue", Some(format!("queue:{}", q.name))),
734 QueryExpr::DropQueue(q) => schema("drop_queue", Some(format!("queue:{}", q.name))),
735 QueryExpr::CreateTree(q) => {
736 schema(
737 "create_tree",
738 Some(format!("tree:{}:{}", q.collection, q.name)),
739 );
740 }
741 QueryExpr::DropTree(q) => {
742 schema(
743 "drop_tree",
744 Some(format!("tree:{}:{}", q.collection, q.name)),
745 );
746 }
747 QueryExpr::CreateSchema(q) => schema("create_schema", Some(format!("schema:{}", q.name))),
748 QueryExpr::DropSchema(q) => schema("drop_schema", Some(format!("schema:{}", q.name))),
749 QueryExpr::CreateSequence(q) => {
750 schema("create_sequence", Some(format!("sequence:{}", q.name)));
751 }
752 QueryExpr::DropSequence(q) => schema("drop_sequence", Some(format!("sequence:{}", q.name))),
753 QueryExpr::CreateView(q) => schema("create_view", Some(format!("view:{}", q.name))),
754 QueryExpr::DropView(q) => schema("drop_view", Some(format!("view:{}", q.name))),
755 QueryExpr::RefreshMaterializedView(q) => {
756 schema(
757 "refresh_materialized_view",
758 Some(format!("view:{}", q.name)),
759 );
760 }
761 QueryExpr::CreatePolicy(q) => {
762 specs.push(QueryControlEventSpec {
763 kind: EventKind::RlsGovernance,
764 action: "create_policy",
765 resource: Some(format!("table:{}:policy:{}", q.table, q.name)),
766 fields: vec![(
767 "target_kind".to_string(),
768 Sensitivity::raw(q.target_kind.as_ident()),
769 )],
770 });
771 }
772 QueryExpr::DropPolicy(q) => {
773 specs.push(QueryControlEventSpec {
774 kind: EventKind::RlsGovernance,
775 action: "drop_policy",
776 resource: Some(format!("table:{}:policy:{}", q.table, q.name)),
777 fields: Vec::new(),
778 });
779 }
780 QueryExpr::SetTenant(value) => {
781 let mut fields = Vec::new();
782 if let Some(value) = value {
783 fields.push(("tenant".to_string(), Sensitivity::raw(value)));
784 }
785 specs.push(QueryControlEventSpec {
786 kind: EventKind::TenantGovernance,
787 action: "set_tenant",
788 resource: Some("tenant:session".to_string()),
789 fields,
790 });
791 }
792 QueryExpr::SetConfig { key, .. } => {
793 specs.push(QueryControlEventSpec {
794 kind: EventKind::ConfigWrite,
795 action: "config:write",
796 resource: Some(format!("config:{key}")),
797 fields: vec![("key".to_string(), Sensitivity::raw(key))],
798 });
799 }
800 QueryExpr::ConfigCommand(cmd) => match cmd {
801 crate::storage::query::ast::ConfigCommand::Put {
802 collection, key, ..
803 }
804 | crate::storage::query::ast::ConfigCommand::Rotate {
805 collection, key, ..
806 } => {
807 let target = format!("{collection}/{key}");
808 specs.push(QueryControlEventSpec {
809 kind: EventKind::ConfigWrite,
810 action: "config:write",
811 resource: Some(format!("config:{target}")),
812 fields: vec![
813 ("collection".to_string(), Sensitivity::raw(collection)),
814 ("key".to_string(), Sensitivity::raw(key)),
815 ],
816 });
817 }
818 crate::storage::query::ast::ConfigCommand::Delete { collection, key } => {
819 let target = format!("{collection}/{key}");
820 specs.push(QueryControlEventSpec {
821 kind: EventKind::ConfigDelete,
822 action: "config:write",
823 resource: Some(format!("config:{target}")),
824 fields: vec![
825 ("collection".to_string(), Sensitivity::raw(collection)),
826 ("key".to_string(), Sensitivity::raw(key)),
827 ],
828 });
829 }
830 _ => {}
831 },
832 QueryExpr::AlterUser(stmt) => {
833 let disables = stmt.attributes.iter().any(|attr| {
834 matches!(
835 attr,
836 crate::storage::query::ast::AlterUserAttribute::Disable
837 )
838 });
839 specs.push(QueryControlEventSpec {
840 kind: if disables {
841 EventKind::UserDisable
842 } else {
843 EventKind::UserUpdate
844 },
845 action: "alter_user",
846 resource: Some(format!("user:{}", stmt.username)),
847 fields: Vec::new(),
848 });
849 }
850 QueryExpr::CreateUser(stmt) => {
851 specs.push(QueryControlEventSpec {
852 kind: EventKind::UserCreate,
853 action: "create_user",
854 resource: Some(format!("user:{}", stmt.username)),
855 fields: Vec::new(),
856 });
857 }
858 _ => {}
859 }
860 specs
861}
862
863pub(crate) fn control_event_outcome_for_error(
864 err: &RedDBError,
865) -> crate::runtime::control_events::Outcome {
866 match err {
867 RedDBError::ReadOnly(_) => crate::runtime::control_events::Outcome::Denied,
868 RedDBError::Query(msg)
869 if msg.contains("permission denied")
870 || msg.contains("cannot issue")
871 || msg.contains("lacks") =>
872 {
873 crate::runtime::control_events::Outcome::Denied
874 }
875 _ => crate::runtime::control_events::Outcome::Error,
876 }
877}
878
879fn view_records_to_entities(
888 table: &str,
889 records: &[crate::storage::query::unified::UnifiedRecord],
890) -> Vec<crate::storage::UnifiedEntity> {
891 use std::collections::HashMap;
892 let table_arc: std::sync::Arc<str> = std::sync::Arc::from(table);
893 let mut out = Vec::with_capacity(records.len());
894 for record in records {
895 let mut named: HashMap<String, crate::storage::schema::Value> = HashMap::new();
896 for (name, value) in record.iter_fields() {
897 named.insert(name.to_string(), value.clone());
898 }
899 let entity = crate::storage::UnifiedEntity::new(
900 crate::storage::EntityId::new(0),
901 crate::storage::EntityKind::TableRow {
902 table: std::sync::Arc::clone(&table_arc),
903 row_id: 0,
904 },
905 crate::storage::EntityData::Row(crate::storage::RowData {
906 columns: Vec::new(),
907 named: Some(named),
908 schema: None,
909 }),
910 );
911 out.push(entity);
912 }
913 out
914}
915
916fn system_keyed_collection_contract(
917 name: &str,
918 model: crate::catalog::CollectionModel,
919) -> crate::physical::CollectionContract {
920 let now = crate::utils::now_unix_millis() as u128;
921 crate::physical::CollectionContract {
922 name: name.to_string(),
923 declared_model: model,
924 schema_mode: crate::catalog::SchemaMode::Dynamic,
925 origin: crate::physical::ContractOrigin::Implicit,
926 version: 1,
927 created_at_unix_ms: now,
928 updated_at_unix_ms: now,
929 default_ttl_ms: None,
930 vector_dimension: None,
931 vector_metric: None,
932 context_index_fields: Vec::new(),
933 declared_columns: Vec::new(),
934 table_def: None,
935 timestamps_enabled: false,
936 context_index_enabled: false,
937 metrics_raw_retention_ms: None,
938 metrics_rollup_policies: Vec::new(),
939 metrics_tenant_identity: None,
940 metrics_namespace: None,
941 append_only: false,
942 subscriptions: Vec::new(),
943 analytics_config: Vec::new(),
944 session_key: None,
945 session_gap_ms: None,
946 retention_duration_ms: None,
947 analytical_storage: None,
948
949 ai_policy: None,
950 }
951}
952
953pub use super::execution_context::{
954 capture_current_snapshot, clear_current_auth_identity, clear_current_connection_id,
955 clear_current_snapshot, clear_current_tenant, current_auth_identity_for_audit,
956 current_connection_id, current_tenant, entity_visible_under_current_snapshot,
957 entity_visible_with_context, set_current_auth_identity, set_current_connection_id,
958 set_current_snapshot, set_current_tenant, snapshot_bundle, with_snapshot_bundle,
959 SnapshotBundle, SnapshotContext,
960};
961pub(crate) use super::execution_context::{
962 current_auth_identity, current_config_value, current_role_projected, current_scope_override,
963 current_secret_value, current_snapshot_requires_index_fallback, current_user_projected,
964 has_scope_override_active, parse_set_local_tenant, update_current_config_value,
965 update_current_secret_value, xids_visible_under_current_snapshot, ConfigSnapshotGuard,
966 CurrentSnapshotGuard, ScopeOverrideGuard, SecretStoreGuard, TxLocalTenantGuard,
967};
968
969fn table_row_index_fields(
970 entity: &crate::storage::unified::entity::UnifiedEntity,
971) -> Vec<(String, crate::storage::schema::Value)> {
972 let crate::storage::EntityData::Row(row) = &entity.data else {
973 return Vec::new();
974 };
975 if let Some(named) = &row.named {
976 return named
977 .iter()
978 .map(|(name, value)| (name.clone(), value.clone()))
979 .collect();
980 }
981 if let Some(schema) = &row.schema {
982 return schema
983 .iter()
984 .zip(row.columns.iter())
985 .map(|(name, value)| (name.clone(), value.clone()))
986 .collect();
987 }
988 Vec::new()
989}
990
991fn named_text(
992 named: &std::collections::HashMap<String, crate::storage::schema::Value>,
993 key: &str,
994) -> Option<String> {
995 match named.get(key) {
996 Some(crate::storage::schema::Value::Text(value)) => Some(value.to_string()),
997 _ => None,
998 }
999}
1000
1001fn named_bool(
1002 named: &std::collections::HashMap<String, crate::storage::schema::Value>,
1003 key: &str,
1004) -> Option<bool> {
1005 match named.get(key) {
1006 Some(crate::storage::schema::Value::Boolean(value)) => Some(*value),
1007 _ => None,
1008 }
1009}
1010
1011fn index_method_kind_as_str(method: super::index_store::IndexMethodKind) -> &'static str {
1012 match method {
1013 super::index_store::IndexMethodKind::Hash => "hash",
1014 super::index_store::IndexMethodKind::Bitmap => "bitmap",
1015 super::index_store::IndexMethodKind::Spatial => "spatial",
1016 super::index_store::IndexMethodKind::BTree => "btree",
1017 }
1018}
1019
1020fn index_method_kind_from_str(raw: &str) -> Option<super::index_store::IndexMethodKind> {
1021 match raw {
1022 "hash" => Some(super::index_store::IndexMethodKind::Hash),
1023 "bitmap" => Some(super::index_store::IndexMethodKind::Bitmap),
1024 "spatial" | "rtree" => Some(super::index_store::IndexMethodKind::Spatial),
1025 "btree" => Some(super::index_store::IndexMethodKind::BTree),
1026 _ => None,
1027 }
1028}
1029
1030fn runtime_pool_lock(runtime: &RedDBRuntime) -> std::sync::MutexGuard<'_, PoolState> {
1031 runtime
1032 .inner
1033 .pool
1034 .lock()
1035 .unwrap_or_else(|poisoned| poisoned.into_inner())
1036}
1037
1038fn is_graph_tvf_name(name: &str) -> bool {
1042 name.eq_ignore_ascii_case("components")
1043 || name.eq_ignore_ascii_case("louvain")
1044 || name.eq_ignore_ascii_case("degree_centrality")
1045 || name.eq_ignore_ascii_case("shortest_path")
1046 || name.eq_ignore_ascii_case("betweenness")
1047 || name.eq_ignore_ascii_case("eigenvector")
1048 || name.eq_ignore_ascii_case("pagerank")
1049}
1050
1051fn analytics_view_algorithm(
1058 graph: &str,
1059 view: &crate::catalog::AnalyticsViewDescriptor,
1060) -> RedDBResult<(String, Vec<(String, f64)>)> {
1061 use crate::catalog::AnalyticsOutput;
1062
1063 let mut named_args: Vec<(String, f64)> = Vec::new();
1064 let algorithm = match view.output {
1065 AnalyticsOutput::Communities => {
1066 let algo = view.algorithm.as_deref().unwrap_or("louvain");
1067 if !algo.eq_ignore_ascii_case("louvain") {
1068 return Err(RedDBError::Query(format!(
1069 "analytics output 'communities' on graph '{graph}' has unsupported algorithm '{algo}' (expected louvain)"
1070 )));
1071 }
1072 if let Some(resolution) = view.resolution {
1073 named_args.push(("resolution".to_string(), resolution));
1074 }
1075 "louvain".to_string()
1076 }
1077 AnalyticsOutput::Components => {
1078 if let Some(algo) = view.algorithm.as_deref() {
1079 if !algo.eq_ignore_ascii_case("components")
1080 && !algo.eq_ignore_ascii_case("connected_components")
1081 {
1082 return Err(RedDBError::Query(format!(
1083 "analytics output 'components' on graph '{graph}' has unsupported algorithm '{algo}' (expected connected_components)"
1084 )));
1085 }
1086 }
1087 "components".to_string()
1088 }
1089 AnalyticsOutput::Centrality => {
1090 let algo = view
1091 .algorithm
1092 .as_deref()
1093 .unwrap_or("pagerank")
1094 .to_ascii_lowercase();
1095 match algo.as_str() {
1096 "pagerank" => {
1097 if let Some(max_iterations) = view.max_iterations {
1098 named_args.push(("max_iterations".to_string(), max_iterations as f64));
1099 }
1100 }
1101 "eigenvector" => {
1102 if let Some(max_iterations) = view.max_iterations {
1103 named_args.push(("max_iterations".to_string(), max_iterations as f64));
1104 }
1105 if let Some(tolerance) = view.tolerance {
1106 named_args.push(("tolerance".to_string(), tolerance));
1107 }
1108 }
1109 "betweenness" => {}
1110 other => {
1111 return Err(RedDBError::Query(format!(
1112 "analytics output 'centrality' on graph '{graph}' has unsupported algorithm '{other}' (expected pagerank, betweenness, or eigenvector)"
1113 )));
1114 }
1115 }
1116 algo
1117 }
1118 };
1119 Ok((algorithm, named_args))
1120}
1121
1122fn reject_named_args(name: &str, named_args: &[(String, f64)]) -> RedDBResult<()> {
1124 if let Some((key, _)) = named_args.first() {
1125 return Err(RedDBError::Query(format!(
1126 "table function '{name}' has no named argument '{key}'"
1127 )));
1128 }
1129 Ok(())
1130}
1131
1132fn louvain_resolution(named_args: &[(String, f64)]) -> RedDBResult<f64> {
1135 let mut resolution = 1.0_f64;
1136 for (key, value) in named_args {
1137 if key.eq_ignore_ascii_case("resolution") {
1138 if !value.is_finite() || *value <= 0.0 {
1139 return Err(RedDBError::Query(format!(
1140 "table function 'louvain' resolution must be > 0, got {value}"
1141 )));
1142 }
1143 resolution = *value;
1144 } else {
1145 return Err(RedDBError::Query(format!(
1146 "table function 'louvain' has no named argument '{key}' (expected 'resolution')"
1147 )));
1148 }
1149 }
1150 Ok(resolution)
1151}
1152
1153fn abstract_degree_centrality(
1158 nodes: &[String],
1159 edges: &[(
1160 String,
1161 String,
1162 crate::storage::engine::graph_algorithms::Weight,
1163 )],
1164) -> Vec<(String, usize)> {
1165 let mut degree: std::collections::BTreeMap<String, usize> = std::collections::BTreeMap::new();
1166 for n in nodes {
1167 degree.entry(n.clone()).or_insert(0);
1168 }
1169 for (a, b, _w) in edges {
1170 *degree.entry(a.clone()).or_insert(0) += 1;
1171 *degree.entry(b.clone()).or_insert(0) += 1;
1172 }
1173 degree.into_iter().collect()
1174}
1175
1176fn ordered_result_columns(result: &crate::storage::query::unified::UnifiedResult) -> Vec<String> {
1179 if !result.columns.is_empty() {
1180 return result.columns.clone();
1181 }
1182 result
1183 .records
1184 .first()
1185 .map(|record| {
1186 record
1187 .column_names()
1188 .iter()
1189 .map(|column| column.to_string())
1190 .collect()
1191 })
1192 .unwrap_or_default()
1193}
1194
1195fn value_to_node_id(value: &crate::storage::schema::Value) -> Option<String> {
1199 use crate::storage::schema::Value;
1200 match value {
1201 Value::Null => None,
1202 Value::Text(s) => Some(s.to_string()),
1203 Value::Integer(n) => Some(n.to_string()),
1204 Value::UnsignedInteger(n) => Some(n.to_string()),
1205 Value::NodeRef(s) => Some(s.clone()),
1206 other => Some(other.to_string()),
1207 }
1208}
1209
1210fn value_to_weight(value: &crate::storage::schema::Value) -> Option<f32> {
1212 use crate::storage::schema::Value;
1213 match value {
1214 Value::Float(f) => Some(*f as f32),
1215 Value::Integer(n) => Some(*n as f32),
1216 Value::UnsignedInteger(n) => Some(*n as f32),
1217 _ => None,
1218 }
1219}
1220
1221fn inline_node_ids(
1225 name: &str,
1226 result: &crate::storage::query::unified::UnifiedResult,
1227) -> RedDBResult<Vec<String>> {
1228 if result.records.is_empty() {
1229 return Ok(Vec::new());
1230 }
1231 let columns = ordered_result_columns(result);
1232 let Some(first_col) = columns.first() else {
1233 return Err(RedDBError::Query(format!(
1234 "table function '{name}' inline form: `nodes` subquery must project at least one column (the node id)"
1235 )));
1236 };
1237 let mut ids = Vec::with_capacity(result.records.len());
1238 for record in &result.records {
1239 if let Some(id) = record.get(first_col).and_then(value_to_node_id) {
1240 ids.push(id);
1241 }
1242 }
1243 Ok(ids)
1244}
1245
1246fn inline_edges(
1251 name: &str,
1252 result: &crate::storage::query::unified::UnifiedResult,
1253) -> RedDBResult<
1254 Vec<(
1255 String,
1256 String,
1257 crate::storage::engine::graph_algorithms::Weight,
1258 )>,
1259> {
1260 if result.records.is_empty() {
1261 return Ok(Vec::new());
1262 }
1263 let columns = ordered_result_columns(result);
1264 if columns.len() < 2 {
1265 return Err(RedDBError::Query(format!(
1266 "table function '{name}' inline form: `edges` subquery must project at least two columns (source, target), got {}",
1267 columns.len()
1268 )));
1269 }
1270 let src_col = &columns[0];
1271 let dst_col = &columns[1];
1272 let weight_col = columns.get(2);
1273 let mut edges = Vec::with_capacity(result.records.len());
1274 for record in &result.records {
1275 let (Some(src), Some(dst)) = (
1276 record.get(src_col).and_then(value_to_node_id),
1277 record.get(dst_col).and_then(value_to_node_id),
1278 ) else {
1279 continue;
1281 };
1282 let weight = match weight_col {
1283 Some(col) => match record.get(col) {
1284 None | Some(crate::storage::schema::Value::Null) => 1.0,
1285 Some(value) => value_to_weight(value).ok_or_else(|| {
1286 RedDBError::Query(format!(
1287 "table function '{name}' inline form: `edges` weight column must be numeric"
1288 ))
1289 })?,
1290 },
1291 None => 1.0,
1292 };
1293 edges.push((src, dst, weight));
1294 }
1295 Ok(edges)
1296}
1297
1298fn cache_scope_insert(scopes: &mut HashSet<String>, name: &str) {
1299 if name.is_empty() || name.starts_with("__subq_") || is_universal_query_source(name) {
1300 return;
1301 }
1302 scopes.insert(name.to_string());
1303}
1304
1305fn collect_table_source_scopes(scopes: &mut HashSet<String>, query: &TableQuery) {
1306 match query.source.as_ref() {
1307 Some(crate::storage::query::ast::TableSource::Name(name)) => {
1308 cache_scope_insert(scopes, name)
1309 }
1310 Some(crate::storage::query::ast::TableSource::Subquery(subquery)) => {
1311 collect_query_expr_result_cache_scopes(scopes, subquery);
1312 }
1313 Some(crate::storage::query::ast::TableSource::Function { name, args, .. }) => {
1320 if is_graph_tvf_name(name) {
1321 if let Some(graph) = args.first() {
1322 cache_scope_insert(scopes, graph);
1323 }
1324 }
1325 }
1326 Some(crate::storage::query::ast::TableSource::InlineGraphFunction {
1331 nodes, edges, ..
1332 }) => {
1333 collect_query_expr_result_cache_scopes(scopes, nodes);
1334 collect_query_expr_result_cache_scopes(scopes, edges);
1335 }
1336 None => cache_scope_insert(scopes, &query.table),
1337 }
1338}
1339
1340fn collect_vector_source_scopes(
1341 scopes: &mut HashSet<String>,
1342 source: &crate::storage::query::ast::VectorSource,
1343) {
1344 match source {
1345 crate::storage::query::ast::VectorSource::Reference { collection, .. } => {
1346 cache_scope_insert(scopes, collection);
1347 }
1348 crate::storage::query::ast::VectorSource::Subquery(subquery) => {
1349 collect_query_expr_result_cache_scopes(scopes, subquery);
1350 }
1351 crate::storage::query::ast::VectorSource::Literal(_)
1352 | crate::storage::query::ast::VectorSource::Text(_) => {}
1353 }
1354}
1355
1356fn collect_path_selector_scopes(
1357 scopes: &mut HashSet<String>,
1358 selector: &crate::storage::query::ast::NodeSelector,
1359) {
1360 if let crate::storage::query::ast::NodeSelector::ByRow { table, .. } = selector {
1361 cache_scope_insert(scopes, table);
1362 }
1363}
1364
1365fn collect_query_expr_result_cache_scopes(scopes: &mut HashSet<String>, expr: &QueryExpr) {
1366 match expr {
1367 QueryExpr::Table(query) => collect_table_source_scopes(scopes, query),
1368 QueryExpr::Join(query) => {
1369 collect_query_expr_result_cache_scopes(scopes, &query.left);
1370 collect_query_expr_result_cache_scopes(scopes, &query.right);
1371 }
1372 QueryExpr::Path(query) => {
1373 collect_path_selector_scopes(scopes, &query.from);
1374 collect_path_selector_scopes(scopes, &query.to);
1375 }
1376 QueryExpr::Vector(query) => {
1377 cache_scope_insert(scopes, &query.collection);
1378 collect_vector_source_scopes(scopes, &query.query_vector);
1379 }
1380 QueryExpr::Hybrid(query) => {
1381 collect_query_expr_result_cache_scopes(scopes, &query.structured);
1382 cache_scope_insert(scopes, &query.vector.collection);
1383 collect_vector_source_scopes(scopes, &query.vector.query_vector);
1384 }
1385 QueryExpr::Insert(query) => cache_scope_insert(scopes, &query.table),
1386 QueryExpr::Update(query) => cache_scope_insert(scopes, &query.table),
1387 QueryExpr::Delete(query) => cache_scope_insert(scopes, &query.table),
1388 QueryExpr::CreateTable(query) => cache_scope_insert(scopes, &query.name),
1389 QueryExpr::CreateCollection(query) => cache_scope_insert(scopes, &query.name),
1390 QueryExpr::CreateVector(query) => cache_scope_insert(scopes, &query.name),
1391 QueryExpr::DropTable(query) => cache_scope_insert(scopes, &query.name),
1392 QueryExpr::DropGraph(query) => cache_scope_insert(scopes, &query.name),
1393 QueryExpr::DropVector(query) => cache_scope_insert(scopes, &query.name),
1394 QueryExpr::DropDocument(query) => cache_scope_insert(scopes, &query.name),
1395 QueryExpr::DropKv(query) => cache_scope_insert(scopes, &query.name),
1396 QueryExpr::DropCollection(query) => cache_scope_insert(scopes, &query.name),
1397 QueryExpr::Truncate(query) => cache_scope_insert(scopes, &query.name),
1398 QueryExpr::AlterTable(query) => cache_scope_insert(scopes, &query.name),
1399 QueryExpr::CreateIndex(query) => cache_scope_insert(scopes, &query.table),
1400 QueryExpr::DropIndex(query) => cache_scope_insert(scopes, &query.table),
1401 QueryExpr::CreateTimeSeries(query) => cache_scope_insert(scopes, &query.name),
1402 QueryExpr::CreateMetric(query) => cache_scope_insert(scopes, &query.path),
1403 QueryExpr::AlterMetric(query) => cache_scope_insert(scopes, &query.path),
1404 QueryExpr::CreateSlo(query) => cache_scope_insert(scopes, &query.path),
1405 QueryExpr::DropTimeSeries(query) => cache_scope_insert(scopes, &query.name),
1406 QueryExpr::CreateQueue(query) => cache_scope_insert(scopes, &query.name),
1407 QueryExpr::AlterQueue(query) => cache_scope_insert(scopes, &query.name),
1408 QueryExpr::DropQueue(query) => cache_scope_insert(scopes, &query.name),
1409 QueryExpr::QueueSelect(query) => cache_scope_insert(scopes, &query.queue),
1410 QueryExpr::QueueCommand(query) => match query {
1411 QueueCommand::Push { queue, .. }
1412 | QueueCommand::Pop { queue, .. }
1413 | QueueCommand::Peek { queue, .. }
1414 | QueueCommand::Len { queue }
1415 | QueueCommand::Purge { queue }
1416 | QueueCommand::GroupCreate { queue, .. }
1417 | QueueCommand::GroupRead { queue, .. }
1418 | QueueCommand::Pending { queue, .. }
1419 | QueueCommand::Claim { queue, .. }
1420 | QueueCommand::Ack { queue, .. }
1421 | QueueCommand::Nack { queue, .. } => cache_scope_insert(scopes, queue),
1422 QueueCommand::Move {
1423 source,
1424 destination,
1425 ..
1426 } => {
1427 cache_scope_insert(scopes, source);
1428 cache_scope_insert(scopes, destination);
1429 }
1430 },
1431 QueryExpr::EventsBackfill(query) => {
1432 cache_scope_insert(scopes, &query.collection);
1433 cache_scope_insert(scopes, &query.target_queue);
1434 }
1435 QueryExpr::CreateTree(query) => cache_scope_insert(scopes, &query.collection),
1436 QueryExpr::DropTree(query) => cache_scope_insert(scopes, &query.collection),
1437 QueryExpr::TreeCommand(query) => match query {
1438 TreeCommand::Insert { collection, .. }
1439 | TreeCommand::Move { collection, .. }
1440 | TreeCommand::Delete { collection, .. }
1441 | TreeCommand::Validate { collection, .. }
1442 | TreeCommand::Rebalance { collection, .. } => cache_scope_insert(scopes, collection),
1443 },
1444 QueryExpr::SearchCommand(query) => match query {
1445 SearchCommand::Similar { collection, .. }
1446 | SearchCommand::Hybrid { collection, .. }
1447 | SearchCommand::SpatialRadius { collection, .. }
1448 | SearchCommand::SpatialBbox { collection, .. }
1449 | SearchCommand::SpatialNearest { collection, .. } => {
1450 cache_scope_insert(scopes, collection);
1451 }
1452 SearchCommand::Text { collection, .. }
1453 | SearchCommand::Multimodal { collection, .. }
1454 | SearchCommand::Index { collection, .. }
1455 | SearchCommand::Context { collection, .. } => {
1456 if let Some(collection) = collection.as_deref() {
1457 cache_scope_insert(scopes, collection);
1458 }
1459 }
1460 },
1461 QueryExpr::Ask(query) => {
1462 if let Some(collection) = query.collection.as_deref() {
1463 cache_scope_insert(scopes, collection);
1464 }
1465 }
1466 QueryExpr::ExplainAlter(query) => cache_scope_insert(scopes, &query.target.name),
1467 QueryExpr::MaintenanceCommand(cmd) => match cmd {
1468 crate::storage::query::ast::MaintenanceCommand::Vacuum { target, .. }
1469 | crate::storage::query::ast::MaintenanceCommand::Analyze { target } => {
1470 if let Some(t) = target {
1471 cache_scope_insert(scopes, t);
1472 }
1473 }
1474 },
1475 QueryExpr::CopyFrom(cmd) => cache_scope_insert(scopes, &cmd.table),
1476 QueryExpr::CreateView(cmd) => {
1477 cache_scope_insert(scopes, &cmd.name);
1478 collect_query_expr_result_cache_scopes(scopes, &cmd.query);
1480 }
1481 QueryExpr::DropView(cmd) => cache_scope_insert(scopes, &cmd.name),
1482 QueryExpr::RefreshMaterializedView(cmd) => cache_scope_insert(scopes, &cmd.name),
1483 QueryExpr::CreatePolicy(cmd) => cache_scope_insert(scopes, &cmd.table),
1484 QueryExpr::DropPolicy(cmd) => cache_scope_insert(scopes, &cmd.table),
1485 QueryExpr::CreateServer(_) | QueryExpr::DropServer(_) => {}
1486 QueryExpr::CreateForeignTable(cmd) => cache_scope_insert(scopes, &cmd.name),
1487 QueryExpr::DropForeignTable(cmd) => cache_scope_insert(scopes, &cmd.name),
1488 QueryExpr::Graph(_)
1489 | QueryExpr::GraphCommand(_)
1490 | QueryExpr::ProbabilisticCommand(_)
1491 | QueryExpr::SetConfig { .. }
1492 | QueryExpr::ShowConfig { .. }
1493 | QueryExpr::SetSecret { .. }
1494 | QueryExpr::DeleteSecret { .. }
1495 | QueryExpr::ShowSecrets { .. }
1496 | QueryExpr::SetTenant(_)
1497 | QueryExpr::ShowTenant
1498 | QueryExpr::TransactionControl(_)
1499 | QueryExpr::CreateSchema(_)
1500 | QueryExpr::DropSchema(_)
1501 | QueryExpr::CreateSequence(_)
1502 | QueryExpr::DropSequence(_)
1503 | QueryExpr::Grant(_)
1504 | QueryExpr::Revoke(_)
1505 | QueryExpr::AlterUser(_)
1506 | QueryExpr::CreateUser(_)
1507 | QueryExpr::CreateIamPolicy { .. }
1508 | QueryExpr::DropIamPolicy { .. }
1509 | QueryExpr::AttachPolicy { .. }
1510 | QueryExpr::DetachPolicy { .. }
1511 | QueryExpr::ShowPolicies { .. }
1512 | QueryExpr::ShowEffectivePermissions { .. }
1513 | QueryExpr::RankOf(_)
1514 | QueryExpr::ApproxRankOf(_)
1515 | QueryExpr::RankRange(_)
1516 | QueryExpr::SimulatePolicy { .. }
1517 | QueryExpr::LintPolicy { .. }
1518 | QueryExpr::MigratePolicyMode { .. }
1519 | QueryExpr::CreateMigration(_)
1520 | QueryExpr::ApplyMigration(_)
1521 | QueryExpr::RollbackMigration(_)
1522 | QueryExpr::ExplainMigration(_)
1523 | QueryExpr::EventsBackfillStatus { .. } => {}
1524 QueryExpr::KvCommand(cmd) => {
1525 use crate::storage::query::ast::KvCommand;
1526 match cmd {
1527 KvCommand::Put { collection, .. }
1528 | KvCommand::InvalidateTags { collection, .. }
1529 | KvCommand::Get { collection, .. }
1530 | KvCommand::Unseal { collection, .. }
1531 | KvCommand::Rotate { collection, .. }
1532 | KvCommand::History { collection, .. }
1533 | KvCommand::List { collection, .. }
1534 | KvCommand::Purge { collection, .. }
1535 | KvCommand::Watch { collection, .. }
1536 | KvCommand::Delete { collection, .. }
1537 | KvCommand::Incr { collection, .. }
1538 | KvCommand::Cas { collection, .. } => cache_scope_insert(scopes, collection),
1539 }
1540 }
1541 QueryExpr::ConfigCommand(cmd) => {
1542 use crate::storage::query::ast::ConfigCommand;
1543 match cmd {
1544 ConfigCommand::Put { collection, .. }
1545 | ConfigCommand::Get { collection, .. }
1546 | ConfigCommand::Resolve { collection, .. }
1547 | ConfigCommand::Rotate { collection, .. }
1548 | ConfigCommand::Delete { collection, .. }
1549 | ConfigCommand::History { collection, .. }
1550 | ConfigCommand::List { collection, .. }
1551 | ConfigCommand::Watch { collection, .. }
1552 | ConfigCommand::InvalidVolatileOperation { collection, .. } => {
1553 cache_scope_insert(scopes, collection)
1554 }
1555 }
1556 }
1557 }
1558}
1559
1560pub(crate) fn rls_policy_filter(
1568 runtime: &RedDBRuntime,
1569 table: &str,
1570 action: crate::storage::query::ast::PolicyAction,
1571) -> Option<crate::storage::query::ast::Filter> {
1572 rls_policy_filter_for_kind(
1573 runtime,
1574 table,
1575 action,
1576 crate::storage::query::ast::PolicyTargetKind::Table,
1577 )
1578}
1579
1580pub(crate) fn rls_policy_filter_for_kind(
1586 runtime: &RedDBRuntime,
1587 table: &str,
1588 action: crate::storage::query::ast::PolicyAction,
1589 kind: crate::storage::query::ast::PolicyTargetKind,
1590) -> Option<crate::storage::query::ast::Filter> {
1591 use crate::storage::query::ast::Filter;
1592
1593 if !runtime.inner.rls_enabled_tables.read().contains(table) {
1594 return None;
1595 }
1596 let role = current_auth_identity().map(|(_, role)| role);
1597 let role_str = role.map(|r| r.as_str().to_string());
1598 let policies = runtime.matching_rls_policies_for_kind(table, role_str.as_deref(), action, kind);
1599 if policies.is_empty() {
1600 return None;
1601 }
1602 policies
1603 .into_iter()
1604 .reduce(|acc, f| Filter::Or(Box::new(acc), Box::new(f)))
1605}
1606
1607pub(crate) fn rls_is_enabled(runtime: &RedDBRuntime, table: &str) -> bool {
1611 runtime.inner.rls_enabled_tables.read().contains(table)
1612}
1613
1614fn node_passes_rls(
1621 runtime: &RedDBRuntime,
1622 collection: &str,
1623 role: Option<&str>,
1624 cache: &mut std::collections::HashMap<String, Option<crate::storage::query::ast::Filter>>,
1625 entity: &crate::storage::unified::entity::UnifiedEntity,
1626) -> bool {
1627 use crate::storage::query::ast::{Filter, PolicyAction, PolicyTargetKind};
1628
1629 if !runtime.inner.rls_enabled_tables.read().contains(collection) {
1630 return true;
1631 }
1632 let filter = cache.entry(collection.to_string()).or_insert_with(|| {
1633 let policies = runtime.matching_rls_policies_for_kind(
1634 collection,
1635 role,
1636 PolicyAction::Select,
1637 PolicyTargetKind::Nodes,
1638 );
1639 if policies.is_empty() {
1640 None
1641 } else {
1642 policies
1643 .into_iter()
1644 .reduce(|acc, f| Filter::Or(Box::new(acc), Box::new(f)))
1645 }
1646 });
1647 let Some(filter) = filter else {
1648 return false;
1649 };
1650 crate::runtime::query_exec::evaluate_entity_filter_with_db(
1651 Some(&runtime.inner.db),
1652 entity,
1653 filter,
1654 collection,
1655 collection,
1656 )
1657}
1658
1659fn edge_passes_rls(
1662 runtime: &RedDBRuntime,
1663 collection: &str,
1664 role: Option<&str>,
1665 cache: &mut std::collections::HashMap<String, Option<crate::storage::query::ast::Filter>>,
1666 entity: &crate::storage::unified::entity::UnifiedEntity,
1667) -> bool {
1668 use crate::storage::query::ast::{Filter, PolicyAction, PolicyTargetKind};
1669
1670 if !runtime.inner.rls_enabled_tables.read().contains(collection) {
1671 return true;
1672 }
1673 let filter = cache.entry(collection.to_string()).or_insert_with(|| {
1674 let policies = runtime.matching_rls_policies_for_kind(
1675 collection,
1676 role,
1677 PolicyAction::Select,
1678 PolicyTargetKind::Edges,
1679 );
1680 if policies.is_empty() {
1681 None
1682 } else {
1683 policies
1684 .into_iter()
1685 .reduce(|acc, f| Filter::Or(Box::new(acc), Box::new(f)))
1686 }
1687 });
1688 let Some(filter) = filter else {
1689 return false;
1690 };
1691 crate::runtime::query_exec::evaluate_entity_filter_with_db(
1692 Some(&runtime.inner.db),
1693 entity,
1694 filter,
1695 collection,
1696 collection,
1697 )
1698}
1699
1700fn inject_rls_filters(
1721 runtime: &RedDBRuntime,
1722 frame: &dyn super::statement_frame::ReadFrame,
1723 mut table: crate::storage::query::ast::TableQuery,
1724) -> Option<crate::storage::query::ast::TableQuery> {
1725 use crate::storage::query::ast::{Filter, PolicyAction};
1726
1727 let role = frame.identity().map(|(_, role)| role);
1729 let role_str = role.map(|r| r.as_str().to_string());
1730 let policies =
1731 runtime.matching_rls_policies(&table.table, role_str.as_deref(), PolicyAction::Select);
1732
1733 if policies.is_empty() {
1734 return None;
1737 }
1738
1739 let combined = policies
1741 .into_iter()
1742 .reduce(|acc, f| Filter::Or(Box::new(acc), Box::new(f)))
1743 .expect("policies non-empty");
1744
1745 use crate::storage::query::sql_lowering::{expr_to_filter, filter_to_expr};
1754 let had_where_expr = table.where_expr.is_some();
1755 let existing = table
1756 .filter
1757 .take()
1758 .or_else(|| table.where_expr.as_ref().map(expr_to_filter));
1759 let new_filter = match existing {
1760 Some(existing) => Filter::And(Box::new(existing), Box::new(combined)),
1761 None => combined,
1762 };
1763 if had_where_expr {
1766 table.where_expr = Some(filter_to_expr(&new_filter));
1767 }
1768 table.filter = Some(new_filter);
1769 Some(table)
1770}
1771
1772fn inject_rls_into_join(
1782 runtime: &RedDBRuntime,
1783 frame: &dyn super::statement_frame::ReadFrame,
1784 mut join: crate::storage::query::ast::JoinQuery,
1785) -> Option<crate::storage::query::ast::JoinQuery> {
1786 use crate::storage::query::ast::Filter;
1787
1788 let mut policy_filters: Vec<Filter> = Vec::new();
1789 if !collect_join_side_policy(runtime, frame, join.left.as_ref(), &mut policy_filters) {
1790 return None;
1791 }
1792 if !collect_join_side_policy(runtime, frame, join.right.as_ref(), &mut policy_filters) {
1793 return None;
1794 }
1795
1796 if policy_filters.is_empty() {
1797 return Some(join);
1798 }
1799
1800 let combined = policy_filters
1801 .into_iter()
1802 .reduce(|acc, f| Filter::And(Box::new(acc), Box::new(f)))
1803 .expect("policy_filters non-empty");
1804
1805 join.filter = Some(match join.filter.take() {
1806 Some(existing) => Filter::And(Box::new(existing), Box::new(combined)),
1807 None => combined,
1808 });
1809
1810 Some(join)
1811}
1812
1813fn collect_join_side_policy(
1818 runtime: &RedDBRuntime,
1819 frame: &dyn super::statement_frame::ReadFrame,
1820 expr: &crate::storage::query::ast::QueryExpr,
1821 out: &mut Vec<crate::storage::query::ast::Filter>,
1822) -> bool {
1823 use crate::storage::query::ast::{Filter, PolicyAction, QueryExpr};
1824 match expr {
1825 QueryExpr::Table(t) => {
1826 if !runtime.inner.rls_enabled_tables.read().contains(&t.table) {
1827 return true;
1828 }
1829 let role = frame.identity().map(|(_, role)| role);
1830 let role_str = role.map(|r| r.as_str().to_string());
1831 let policies =
1832 runtime.matching_rls_policies(&t.table, role_str.as_deref(), PolicyAction::Select);
1833 if policies.is_empty() {
1834 return false;
1835 }
1836 let combined = policies
1837 .into_iter()
1838 .reduce(|acc, f| Filter::Or(Box::new(acc), Box::new(f)))
1839 .expect("policies non-empty");
1840 out.push(combined);
1841 true
1842 }
1843 QueryExpr::Join(inner) => {
1844 collect_join_side_policy(runtime, frame, inner.left.as_ref(), out)
1845 && collect_join_side_policy(runtime, frame, inner.right.as_ref(), out)
1846 }
1847 _ => true,
1848 }
1849}
1850
1851fn apply_foreign_table_filters(
1862 records: Vec<crate::storage::query::unified::UnifiedRecord>,
1863 query: &crate::storage::query::ast::TableQuery,
1864) -> crate::storage::query::unified::UnifiedResult {
1865 use crate::storage::query::sql_lowering::{
1866 effective_table_filter, effective_table_projections,
1867 };
1868 use crate::storage::query::unified::UnifiedResult;
1869
1870 let filter = effective_table_filter(query);
1871 let projections = effective_table_projections(query);
1872
1873 let mut filtered: Vec<_> = records
1876 .into_iter()
1877 .filter(|record| match &filter {
1878 Some(f) => {
1879 super::join_filter::evaluate_runtime_filter_with_db(None, record, f, None, None)
1880 }
1881 None => true,
1882 })
1883 .collect();
1884
1885 if let Some(offset) = query.offset {
1887 let offset = offset as usize;
1888 if offset >= filtered.len() {
1889 filtered.clear();
1890 } else {
1891 filtered.drain(0..offset);
1892 }
1893 }
1894 if let Some(limit) = query.limit {
1895 filtered.truncate(limit as usize);
1896 }
1897
1898 let columns: Vec<String> = if projections.is_empty() {
1901 filtered
1902 .first()
1903 .map(|r| r.column_names().iter().map(|k| k.to_string()).collect())
1904 .unwrap_or_default()
1905 } else {
1906 projections
1907 .iter()
1908 .map(super::join_filter::projection_name)
1909 .collect()
1910 };
1911
1912 let mut result = UnifiedResult::empty();
1913 result.columns = columns;
1914 result.records = filtered;
1915 result
1916}
1917
1918pub(crate) fn collect_table_refs(expr: &QueryExpr) -> Vec<String> {
1925 let mut scopes: HashSet<String> = HashSet::new();
1926 collect_query_expr_result_cache_scopes(&mut scopes, expr);
1927 scopes.into_iter().collect()
1928}
1929
1930fn query_expr_result_cache_scopes(expr: &QueryExpr) -> HashSet<String> {
1931 let mut scopes = HashSet::new();
1932 collect_query_expr_result_cache_scopes(&mut scopes, expr);
1933 scopes
1934}
1935
1936fn strip_explain_prefix(sql: &str) -> Option<&str> {
1951 let trimmed = sql.trim_start();
1952 let (head, rest) = trimmed.split_at(
1953 trimmed
1954 .find(|c: char| c.is_whitespace())
1955 .unwrap_or(trimmed.len()),
1956 );
1957 if !head.eq_ignore_ascii_case("EXPLAIN") {
1958 return None;
1959 }
1960 let rest = rest.trim_start();
1961 if rest.is_empty() {
1962 return None;
1963 }
1964 let next_head_end = rest.find(|c: char| c.is_whitespace()).unwrap_or(rest.len());
1967 if rest[..next_head_end].eq_ignore_ascii_case("ALTER")
1968 || rest[..next_head_end].eq_ignore_ascii_case("ASK")
1969 || rest[..next_head_end].eq_ignore_ascii_case("MIGRATION")
1970 {
1971 return None;
1972 }
1973 Some(rest)
1974}
1975
1976pub(super) fn has_with_prefix(sql: &str) -> bool {
1981 let trimmed = sql.trim_start();
1982 let head_end = trimmed
1983 .find(|c: char| c.is_whitespace() || c == '(')
1984 .unwrap_or(trimmed.len());
1985 trimmed[..head_end].eq_ignore_ascii_case("WITH")
1986}
1987
1988fn peek_top_level_as_of(sql: &str) -> Option<crate::application::vcs::AsOfSpec> {
1996 peek_top_level_as_of_with_table(sql).map(|(spec, _)| spec)
1997}
1998
1999pub(super) fn peek_top_level_as_of_with_table(
2004 sql: &str,
2005) -> Option<(crate::application::vcs::AsOfSpec, Option<String>)> {
2006 if !sql
2007 .as_bytes()
2008 .windows(5)
2009 .any(|w| w.eq_ignore_ascii_case(b"as of"))
2010 {
2011 return None;
2012 }
2013 let parsed = crate::storage::query::parser::parse(sql).ok()?;
2014 let crate::storage::query::ast::QueryExpr::Table(table) = parsed.query else {
2015 return None;
2016 };
2017 let clause = table.as_of?;
2018 let table_name = if table.table.is_empty() || table.table == "any" {
2019 None
2020 } else {
2021 Some(table.table.clone())
2022 };
2023 let spec = match clause {
2024 crate::storage::query::ast::AsOfClause::Commit(h) => {
2025 crate::application::vcs::AsOfSpec::Commit(h)
2026 }
2027 crate::storage::query::ast::AsOfClause::Branch(b) => {
2028 crate::application::vcs::AsOfSpec::Branch(b)
2029 }
2030 crate::storage::query::ast::AsOfClause::Tag(t) => crate::application::vcs::AsOfSpec::Tag(t),
2031 crate::storage::query::ast::AsOfClause::TimestampMs(ts) => {
2032 crate::application::vcs::AsOfSpec::TimestampMs(ts)
2033 }
2034 crate::storage::query::ast::AsOfClause::Snapshot(x) => {
2035 crate::application::vcs::AsOfSpec::Snapshot(x)
2036 }
2037 };
2038 Some((spec, table_name))
2039}
2040
2041pub(super) fn query_has_volatile_builtin(sql: &str) -> bool {
2042 const VOLATILE_TOKENS: &[&str] = &[
2046 "pg_advisory_lock",
2047 "pg_try_advisory_lock",
2048 "pg_advisory_unlock",
2049 "random()",
2050 "$config",
2055 "$secret",
2056 ];
2061 let lowered = sql.to_ascii_lowercase();
2062 VOLATILE_TOKENS.iter().any(|t| lowered.contains(t))
2063}
2064
2065pub(super) fn query_is_ask_statement(sql: &str) -> bool {
2066 let trimmed = sql.trim_start();
2067 let head_end = trimmed
2068 .find(|c: char| c.is_whitespace() || c == '(' || c == ';')
2069 .unwrap_or(trimmed.len());
2070 trimmed[..head_end].eq_ignore_ascii_case("ASK")
2071}
2072
2073pub(super) fn intent_lock_modes_for(
2083 expr: &QueryExpr,
2084) -> Option<(
2085 crate::storage::transaction::lock::LockMode,
2086 crate::storage::transaction::lock::LockMode,
2087)> {
2088 use crate::storage::transaction::lock::LockMode::{Exclusive, IntentExclusive, IntentShared};
2089
2090 match expr {
2091 QueryExpr::Table(_)
2093 | QueryExpr::Join(_)
2094 | QueryExpr::Vector(_)
2095 | QueryExpr::Hybrid(_)
2096 | QueryExpr::Graph(_)
2097 | QueryExpr::Path(_)
2098 | QueryExpr::Ask(_)
2099 | QueryExpr::SearchCommand(_)
2100 | QueryExpr::GraphCommand(_)
2101 | QueryExpr::RankOf(_)
2102 | QueryExpr::ApproxRankOf(_)
2103 | QueryExpr::RankRange(_)
2104 | QueryExpr::QueueSelect(_) => Some((IntentShared, IntentShared)),
2105
2106 QueryExpr::Insert(_)
2114 | QueryExpr::Update(_)
2115 | QueryExpr::Delete(_)
2116 | QueryExpr::QueueCommand(QueueCommand::Move { .. }) => {
2117 Some((IntentExclusive, IntentExclusive))
2118 }
2119 QueryExpr::QueueCommand(_) => Some((IntentShared, IntentShared)),
2120
2121 QueryExpr::CreateTable(_)
2125 | QueryExpr::CreateCollection(_)
2126 | QueryExpr::CreateVector(_)
2127 | QueryExpr::DropTable(_)
2128 | QueryExpr::DropGraph(_)
2129 | QueryExpr::DropVector(_)
2130 | QueryExpr::DropDocument(_)
2131 | QueryExpr::DropKv(_)
2132 | QueryExpr::DropCollection(_)
2133 | QueryExpr::Truncate(_)
2134 | QueryExpr::AlterTable(_)
2135 | QueryExpr::CreateIndex(_)
2136 | QueryExpr::DropIndex(_)
2137 | QueryExpr::CreateTimeSeries(_)
2138 | QueryExpr::CreateMetric(_)
2139 | QueryExpr::AlterMetric(_)
2140 | QueryExpr::CreateSlo(_)
2141 | QueryExpr::DropTimeSeries(_)
2142 | QueryExpr::CreateQueue(_)
2143 | QueryExpr::AlterQueue(_)
2144 | QueryExpr::DropQueue(_)
2145 | QueryExpr::CreateTree(_)
2146 | QueryExpr::DropTree(_)
2147 | QueryExpr::CreatePolicy(_)
2148 | QueryExpr::DropPolicy(_)
2149 | QueryExpr::CreateView(_)
2150 | QueryExpr::DropView(_)
2151 | QueryExpr::RefreshMaterializedView(_)
2152 | QueryExpr::CreateSchema(_)
2153 | QueryExpr::DropSchema(_)
2154 | QueryExpr::CreateSequence(_)
2155 | QueryExpr::DropSequence(_)
2156 | QueryExpr::CreateServer(_)
2157 | QueryExpr::DropServer(_)
2158 | QueryExpr::CreateForeignTable(_)
2159 | QueryExpr::DropForeignTable(_) => Some((IntentExclusive, Exclusive)),
2160
2161 _ => None,
2167 }
2168}
2169
2170pub(super) fn collections_referenced(expr: &QueryExpr) -> Vec<String> {
2175 let mut out = Vec::new();
2176 walk_collections(expr, &mut out);
2177 out.sort();
2178 out.dedup();
2179 out
2180}
2181
2182fn walk_collections(expr: &QueryExpr, out: &mut Vec<String>) {
2183 match expr {
2184 QueryExpr::Table(t) => out.push(t.table.clone()),
2185 QueryExpr::Join(j) => {
2186 walk_collections(&j.left, out);
2187 walk_collections(&j.right, out);
2188 }
2189 QueryExpr::Insert(i) => out.push(i.table.clone()),
2190 QueryExpr::Update(u) => out.push(u.table.clone()),
2191 QueryExpr::Delete(d) => out.push(d.table.clone()),
2192 QueryExpr::QueueSelect(q) => out.push(q.queue.clone()),
2193
2194 QueryExpr::CreateTable(q) => out.push(q.name.clone()),
2199 QueryExpr::CreateCollection(q) => out.push(q.name.clone()),
2200 QueryExpr::CreateVector(q) => out.push(q.name.clone()),
2201 QueryExpr::DropTable(q) => out.push(q.name.clone()),
2202 QueryExpr::DropGraph(q) => out.push(q.name.clone()),
2203 QueryExpr::DropVector(q) => out.push(q.name.clone()),
2204 QueryExpr::DropDocument(q) => out.push(q.name.clone()),
2205 QueryExpr::DropKv(q) => out.push(q.name.clone()),
2206 QueryExpr::DropCollection(q) => out.push(q.name.clone()),
2207 QueryExpr::Truncate(q) => out.push(q.name.clone()),
2208 QueryExpr::AlterTable(q) => out.push(q.name.clone()),
2209 QueryExpr::CreateIndex(q) => out.push(q.table.clone()),
2210 QueryExpr::DropIndex(q) => out.push(q.table.clone()),
2211 QueryExpr::CreateTimeSeries(q) => out.push(q.name.clone()),
2212 QueryExpr::CreateMetric(q) => out.push(q.path.clone()),
2213 QueryExpr::AlterMetric(q) => out.push(q.path.clone()),
2214 QueryExpr::CreateSlo(q) => out.push(q.path.clone()),
2215 QueryExpr::DropTimeSeries(q) => out.push(q.name.clone()),
2216 QueryExpr::CreateQueue(q) => out.push(q.name.clone()),
2217 QueryExpr::AlterQueue(q) => out.push(q.name.clone()),
2218 QueryExpr::DropQueue(q) => out.push(q.name.clone()),
2219 QueryExpr::QueueCommand(QueueCommand::Move {
2220 source,
2221 destination,
2222 ..
2223 }) => {
2224 out.push(source.clone());
2225 out.push(destination.clone());
2226 }
2227 QueryExpr::CreatePolicy(q) => out.push(q.table.clone()),
2228 QueryExpr::CreateView(q) => out.push(q.name.clone()),
2229 QueryExpr::DropView(q) => out.push(q.name.clone()),
2230 QueryExpr::RefreshMaterializedView(q) => out.push(q.name.clone()),
2231
2232 _ => {}
2238 }
2239}
2240
2241impl RedDBRuntime {
2242 pub fn in_memory() -> RedDBResult<Self> {
2243 Self::with_options(RedDBOptions::in_memory())
2244 }
2245
2246 pub fn flush(&self) -> RedDBResult<()> {
2247 self.inner
2248 .db
2249 .flush()
2250 .map_err(|err| RedDBError::Internal(err.to_string()))
2251 }
2252
2253 pub fn lock_manager(&self) -> std::sync::Arc<crate::storage::transaction::lock::LockManager> {
2257 self.inner.lock_manager.clone()
2258 }
2259
2260 pub fn config_registry(&self) -> std::sync::Arc<crate::auth::registry::ConfigRegistry> {
2262 self.inner.config_registry.clone()
2263 }
2264
2265 pub fn query_audit(&self) -> std::sync::Arc<crate::runtime::query_audit::QueryAuditStream> {
2266 self.inner.query_audit.clone()
2267 }
2268
2269 pub fn control_events_require_persistence(&self) -> bool {
2270 self.inner.control_event_config.require_persistence()
2271 }
2272
2273 pub fn control_event_config(&self) -> crate::runtime::control_events::ControlEventConfig {
2274 self.inner.control_event_config
2275 }
2276
2277 pub fn control_event_ledger(
2278 &self,
2279 ) -> Arc<dyn crate::runtime::control_events::ControlEventLedger> {
2280 self.inner.control_event_ledger.read().clone()
2281 }
2282
2283 #[doc(hidden)]
2284 pub fn replace_control_event_ledger_for_tests(
2285 &self,
2286 ledger: Arc<dyn crate::runtime::control_events::ControlEventLedger>,
2287 ) {
2288 *self.inner.control_event_ledger.write() = ledger;
2289 }
2290
2291 #[inline(never)]
2292 pub fn with_options(options: RedDBOptions) -> RedDBResult<Self> {
2293 Self::with_pool(options, ConnectionPoolConfig::default())
2294 }
2295
2296 pub fn with_pool(
2297 options: RedDBOptions,
2298 pool_config: ConnectionPoolConfig,
2299 ) -> RedDBResult<Self> {
2300 let boot_open_start_ms = std::time::SystemTime::now()
2308 .duration_since(std::time::UNIX_EPOCH)
2309 .map(|d| d.as_millis() as u64)
2310 .unwrap_or(0);
2311 let embedded_single_file = options.storage_profile.deploy_profile
2312 == crate::storage::DeployProfile::Embedded
2313 && options.storage_profile.packaging == crate::storage::StoragePackaging::SingleFile;
2314 let db = Arc::new(
2315 RedDB::open_with_options(&options)
2316 .map_err(|err| RedDBError::Internal(err.to_string()))?,
2317 );
2318 let result_blob_cache_config = if embedded_single_file {
2319 crate::storage::cache::BlobCacheConfig::default()
2320 } else {
2321 crate::storage::cache::BlobCacheConfig::default().with_l2_path(
2322 reddb_file::layout::result_cache_l2_path(
2323 &options.resolved_path(reddb_file::default_database_path()),
2324 ),
2325 )
2326 };
2327 let result_blob_cache =
2328 crate::storage::cache::BlobCache::open_with_l2(result_blob_cache_config).map_err(
2329 |err| RedDBError::Internal(format!("open result Blob Cache L2 failed: {err:?}")),
2330 )?;
2331 let storage_ready_ms = std::time::SystemTime::now()
2332 .duration_since(std::time::UNIX_EPOCH)
2333 .map(|d| d.as_millis() as u64)
2334 .unwrap_or(0);
2335
2336 let runtime = Self {
2337 inner: Arc::new(RuntimeInner {
2338 db: db.clone(),
2339 layout: PhysicalLayout::from_options(&options),
2340 embedded_single_file,
2341 indices: IndexCatalog::register_default_vector_graph(
2342 options.has_capability(crate::api::Capability::Table),
2343 options.has_capability(crate::api::Capability::Graph),
2344 ),
2345 pool_config,
2346 pool: Mutex::new(PoolState::default()),
2347 started_at_unix_ms: SystemTime::now()
2348 .duration_since(UNIX_EPOCH)
2349 .unwrap_or_default()
2350 .as_millis(),
2351 probabilistic: super::probabilistic_store::ProbabilisticStore::new(),
2352 index_store: super::index_store::IndexStore::new(),
2353 cdc: crate::replication::cdc::CdcBuffer::new(100_000),
2354 backup_scheduler: crate::replication::scheduler::BackupScheduler::new(3600),
2355 query_cache: parking_lot::RwLock::new(
2356 crate::storage::query::planner::cache::PlanCache::new(1000),
2357 ),
2358 result_cache: parking_lot::RwLock::new((
2359 HashMap::new(),
2360 std::collections::VecDeque::new(),
2361 )),
2362 result_blob_cache,
2363 result_blob_entries: parking_lot::RwLock::new((
2364 HashMap::new(),
2365 std::collections::VecDeque::new(),
2366 )),
2367 ask_answer_cache_entries: parking_lot::RwLock::new((
2368 HashSet::new(),
2369 std::collections::VecDeque::new(),
2370 )),
2371 result_cache_shadow_divergences: std::sync::atomic::AtomicU64::new(0),
2372 result_cache_hits: std::sync::atomic::AtomicU64::new(0),
2373 result_cache_misses: std::sync::atomic::AtomicU64::new(0),
2374 result_cache_evictions: std::sync::atomic::AtomicU64::new(0),
2375 ask_daily_spend: parking_lot::RwLock::new(HashMap::new()),
2376 queue_message_locks: parking_lot::RwLock::new(HashMap::new()),
2377 rmw_locks: RmwLockTable::new(),
2378 planner_dirty_tables: parking_lot::RwLock::new(HashSet::new()),
2379 ec_registry: Arc::new(crate::ec::config::EcRegistry::new()),
2380 config_registry: Arc::new(crate::auth::registry::ConfigRegistry::new()),
2381 ec_worker: crate::ec::worker::EcWorker::new(),
2382 auth_store: parking_lot::RwLock::new(None),
2383 oauth_validator: parking_lot::RwLock::new(None),
2384 browser_token_authority: parking_lot::RwLock::new(None),
2385 views: parking_lot::RwLock::new(HashMap::new()),
2386 materialized_views: parking_lot::RwLock::new(
2387 crate::storage::cache::result::MaterializedViewCache::new(),
2388 ),
2389 retention_sweeper: parking_lot::RwLock::new(
2390 crate::runtime::retention_sweeper::RetentionSweeperState::new(),
2391 ),
2392 snapshot_manager: Arc::new(
2393 crate::storage::transaction::snapshot::SnapshotManager::new(),
2394 ),
2395 tx_contexts: parking_lot::RwLock::new(HashMap::new()),
2396 tx_local_tenants: parking_lot::RwLock::new(HashMap::new()),
2397 env_config_overrides: crate::runtime::config_overlay::collect_env_overrides(),
2398 lock_manager: Arc::new({
2399 let env = crate::runtime::config_overlay::collect_env_overrides();
2404 let timeout_ms = env
2405 .get("concurrency.locking.deadlock_timeout_ms")
2406 .and_then(|raw| raw.parse::<u64>().ok())
2407 .unwrap_or_else(|| {
2408 match crate::runtime::config_matrix::default_for(
2409 "concurrency.locking.deadlock_timeout_ms",
2410 ) {
2411 Some(crate::serde_json::Value::Number(n)) => n as u64,
2412 _ => 5000,
2413 }
2414 });
2415 let cfg = crate::storage::transaction::lock::LockConfig {
2416 default_timeout: std::time::Duration::from_millis(timeout_ms),
2417 ..Default::default()
2418 };
2419 crate::storage::transaction::lock::LockManager::new(cfg)
2420 }),
2421 rls_policies: parking_lot::RwLock::new(HashMap::new()),
2422 rls_enabled_tables: parking_lot::RwLock::new(HashSet::new()),
2423 foreign_tables: Arc::new(crate::storage::fdw::ForeignTableRegistry::with_builtins()),
2424 pending_tombstones: parking_lot::RwLock::new(HashMap::new()),
2425 pending_versioned_updates: parking_lot::RwLock::new(HashMap::new()),
2426 pending_kv_watch_events: parking_lot::RwLock::new(HashMap::new()),
2427 pending_store_wal_actions: parking_lot::RwLock::new(HashMap::new()),
2428 pending_claim_locks: parking_lot::RwLock::new(HashMap::new()),
2429 queue_wait_registry: std::sync::Arc::new(
2430 crate::runtime::queue_wait_registry::QueueWaitRegistry::new(),
2431 ),
2432 pending_queue_wakes: parking_lot::RwLock::new(HashMap::new()),
2433 tenant_tables: parking_lot::RwLock::new(HashMap::new()),
2434 ddl_epoch: std::sync::atomic::AtomicU64::new(0),
2435 write_gate: Arc::new(crate::runtime::write_gate::WriteGate::from_options(
2436 &options,
2437 )),
2438 lifecycle: crate::runtime::lifecycle::Lifecycle::new(),
2439 resource_limits: crate::runtime::resource_limits::ResourceLimits::from_env(),
2440 audit_log: {
2441 let data_path = if embedded_single_file {
2457 std::env::temp_dir()
2458 .join("reddb-embedded-runtime")
2459 .join(format!("audit-{}", std::process::id()))
2460 } else {
2461 options
2462 .data_path
2463 .clone()
2464 .unwrap_or_else(|| std::env::temp_dir().join("reddb"))
2465 };
2466 let (audit_dest, _) = crate::api::tier_wiring::current_log_destinations();
2467 if !matches!(audit_dest, crate::storage::layout::LogDestination::File(_))
2468 && (embedded_single_file
2469 || options
2470 .metadata
2471 .contains_key(crate::api::EPHEMERAL_RUNTIME_METADATA_KEY))
2472 {
2473 let audit_path = reddb_file::layout::sibling_path(
2483 &data_path,
2484 &reddb_file::layout::sidecar_file_name(&data_path, "audit.log"),
2485 );
2486 Arc::new(crate::runtime::audit_log::AuditLogger::with_path(
2487 audit_path,
2488 ))
2489 } else {
2490 Arc::new(crate::runtime::audit_log::AuditLogger::for_destination(
2491 &audit_dest,
2492 &data_path,
2493 ))
2494 }
2495 },
2496 control_event_ledger: parking_lot::RwLock::new(Arc::new(
2497 crate::runtime::control_events::RuntimeLedger::new(db.store()),
2498 )),
2499 control_event_config: options.control_events,
2500 query_audit: Arc::new(crate::runtime::query_audit::QueryAuditStream::new(
2501 db.store(),
2502 options.query_audit.clone(),
2503 )),
2504 lease_lifecycle: std::sync::OnceLock::new(),
2505 replica_apply_metrics: std::sync::Arc::new(
2506 crate::replication::logical::ReplicaApplyMetrics::default(),
2507 ),
2508 replica_link_metrics: std::sync::Arc::new(
2509 crate::replication::reconnect::ReplicaLinkMetrics::default(),
2510 ),
2511 quota_bucket: crate::runtime::quota_bucket::QuotaBucket::from_env(),
2512 schema_vocabulary: parking_lot::RwLock::new(
2513 crate::runtime::schema_vocabulary::SchemaVocabulary::new(),
2514 ),
2515 slow_query_logger: {
2516 let fallback_dir = if embedded_single_file {
2533 std::env::temp_dir()
2534 .join("reddb-embedded-runtime")
2535 .join(format!("slow-{}", std::process::id()))
2536 } else {
2537 options
2538 .data_path
2539 .as_ref()
2540 .and_then(|p| p.parent().map(std::path::PathBuf::from))
2541 .unwrap_or_else(|| std::env::temp_dir().join("reddb"))
2542 };
2543 let threshold_ms = std::env::var("RED_SLOW_QUERY_THRESHOLD_MS")
2544 .ok()
2545 .and_then(|s| s.parse::<u64>().ok())
2546 .unwrap_or(1000);
2547 let sample_pct = std::env::var("RED_SLOW_QUERY_SAMPLE_PCT")
2548 .ok()
2549 .and_then(|s| s.parse::<u8>().ok())
2550 .unwrap_or(100);
2551 let (_, slow_dest) = crate::api::tier_wiring::current_log_destinations();
2552 crate::telemetry::slow_query_logger::SlowQueryLogger::for_destination(
2553 &slow_dest,
2554 &fallback_dir,
2555 threshold_ms,
2556 sample_pct,
2557 )
2558 },
2559 slow_query_store: crate::telemetry::slow_query_store::SlowQueryStore::new(
2560 crate::telemetry::slow_query_store::DEFAULT_CAP,
2561 ),
2562 kv_stats: crate::runtime::KvStatsCounters::default(),
2563 metrics_ingest_stats: crate::runtime::MetricsIngestCounters::default(),
2564 metrics_tenant_activity_stats:
2565 crate::runtime::MetricsTenantActivityCounters::default(),
2566 claim_telemetry: Arc::new(
2567 crate::runtime::claim_telemetry::ClaimTelemetryCounters::default(),
2568 ),
2569 queue_telemetry: Arc::new(
2570 crate::runtime::queue_telemetry::QueueTelemetryCounters::default(),
2571 ),
2572 query_latency_telemetry: Arc::new(
2573 crate::runtime::query_latency_telemetry::QueryLatencyTelemetry::default(),
2574 ),
2575 occupancy_sampler: Arc::new(
2576 crate::runtime::occupancy_sampler::OccupancySampler::new(),
2577 ),
2578 node_load_telemetry: Arc::new(
2579 crate::runtime::node_load_telemetry::NodeLoadTelemetry::default(),
2580 ),
2581 queue_presence: Arc::new(
2582 crate::storage::queue::presence::ConsumerPresenceRegistry::new(),
2583 ),
2584 vector_introspection: Arc::new(
2585 crate::storage::vector::introspection::VectorIntrospectionRegistry::new(),
2586 ),
2587 kv_tag_index: crate::runtime::KvTagIndex::default(),
2588 chain_tip_cache: parking_lot::Mutex::new(HashMap::new()),
2589 chain_integrity_broken: parking_lot::Mutex::new(HashMap::new()),
2590 integrity_tombstones: parking_lot::Mutex::new(Vec::new()),
2591 integrity_tombstones_state: std::sync::atomic::AtomicU8::new(0),
2592 }),
2593 };
2594
2595 crate::telemetry::operator_event::install_global_audit_sink(Arc::clone(
2601 &runtime.inner.audit_log,
2602 ));
2603
2604 runtime
2607 .inner
2608 .slow_query_logger
2609 .attach_store(Arc::clone(&runtime.inner.slow_query_store));
2610
2611 runtime
2619 .inner
2620 .lifecycle
2621 .set_restore_started_at_ms(boot_open_start_ms);
2622 runtime
2623 .inner
2624 .lifecycle
2625 .set_restore_ready_at_ms(storage_ready_ms);
2626 runtime
2627 .inner
2628 .lifecycle
2629 .set_wal_replay_started_at_ms(boot_open_start_ms);
2630 runtime
2631 .inner
2632 .lifecycle
2633 .set_wal_replay_ready_at_ms(storage_ready_ms);
2634
2635 let restored_cdc_lsn = runtime
2636 .inner
2637 .db
2638 .replication
2639 .as_ref()
2640 .map(|repl| {
2641 repl.logical_wal_spool
2642 .as_ref()
2643 .map(|spool| spool.current_lsn())
2644 .unwrap_or(0)
2645 })
2646 .unwrap_or(0)
2647 .max(runtime.config_u64("red.config.timeline.last_archived_lsn", 0));
2648 runtime.inner.cdc.set_current_lsn(restored_cdc_lsn);
2649 runtime.rehydrate_snapshot_xid_floor();
2650 runtime
2651 .bootstrap_system_keyed_collections()
2652 .map_err(|err| RedDBError::Internal(format!("bootstrap system collections: {err}")))?;
2653 runtime.rehydrate_declared_column_schemas();
2654 runtime.rehydrate_runtime_index_registry()?;
2655 runtime
2656 .load_probabilistic_state()
2657 .map_err(|err| RedDBError::Internal(format!("load probabilistic state: {err}")))?;
2658
2659 runtime.rehydrate_tenant_tables();
2663 runtime.rehydrate_materialized_view_descriptors();
2668 if let Some(repl) = &runtime.inner.db.replication {
2669 repl.wal_buffer.set_current_lsn(restored_cdc_lsn);
2670 }
2671
2672 {
2674 let sys = SystemInfo::collect();
2675 runtime.inner.db.store().set_config_tree(
2676 "red.system",
2677 &crate::serde_json::json!({
2678 "pid": sys.pid,
2679 "cpu_cores": sys.cpu_cores,
2680 "total_memory_bytes": sys.total_memory_bytes,
2681 "available_memory_bytes": sys.available_memory_bytes,
2682 "os": sys.os,
2683 "arch": sys.arch,
2684 "hostname": sys.hostname,
2685 "started_at": SystemTime::now()
2686 .duration_since(UNIX_EPOCH)
2687 .unwrap_or_default()
2688 .as_millis() as u64
2689 }),
2690 );
2691
2692 let store = runtime.inner.db.store();
2694 if store
2695 .get_collection("red_config")
2696 .map(|m| m.query_all(|_| true).len())
2697 .unwrap_or(0)
2698 <= 10
2699 {
2700 store.set_config_tree("red.ai", &crate::json!({
2701 "default": crate::json!({
2702 "provider": "openai",
2703 "model": crate::ai::DEFAULT_OPENAI_PROMPT_MODEL
2704 }),
2705 "max_embedding_inputs": 256,
2706 "max_prompt_batch": 256,
2707 "timeout": crate::json!({ "connect_secs": 10, "read_secs": 90, "write_secs": 30 })
2708 }));
2709 store.set_config_tree(
2710 "red.server",
2711 &crate::json!({
2712 "max_scan_limit": 1000,
2713 "max_body_size": 1048576,
2714 "read_timeout_ms": 5000,
2715 "write_timeout_ms": 5000
2716 }),
2717 );
2718 store.set_config_tree(
2719 "red.storage",
2720 &crate::json!({
2721 "page_size": 4096,
2722 "page_cache_capacity": 100000,
2723 "auto_checkpoint_pages": 1000,
2724 "snapshot_retention": 16,
2725 "verify_checksums": true,
2726 "segment": crate::json!({
2727 "max_entities": 100000,
2728 "max_bytes": 268435456_u64,
2729 "compression_level": 6
2730 }),
2731 "hnsw": crate::json!({ "m": 16, "ef_construction": 100, "ef_search": 50 }),
2732 "ivf": crate::json!({ "n_lists": 100, "n_probes": 10 }),
2733 "bm25": crate::json!({ "k1": 1.2, "b": 0.75 })
2734 }),
2735 );
2736 store.set_config_tree(
2737 "red.search",
2738 &crate::json!({
2739 "rag": crate::json!({
2740 "max_chunks_per_source": 10,
2741 "max_total_chunks": 25,
2742 "similarity_threshold": 0.8,
2743 "graph_depth": 2,
2744 "min_relevance": 0.3
2745 }),
2746 "fusion": crate::json!({
2747 "vector_weight": 0.5,
2748 "graph_weight": 0.3,
2749 "table_weight": 0.2,
2750 "dedup_threshold": 0.85
2751 })
2752 }),
2753 );
2754 store.set_config_tree(
2755 "red.auth",
2756 &crate::json!({
2757 "enabled": false,
2758 "session_ttl_secs": 3600,
2759 "require_auth": false
2760 }),
2761 );
2762 store.set_config_tree(
2763 "red.query",
2764 &crate::json!({
2765 "connection_pool": crate::json!({ "max_connections": 64, "max_idle": 16 }),
2766 "max_recursion_depth": 1000
2767 }),
2768 );
2769 store.set_config_tree(
2770 "red.indexes",
2771 &crate::json!({
2772 "auto_select": true,
2773 "bloom_filter": crate::json!({
2774 "enabled": true,
2775 "false_positive_rate": 0.01,
2776 "prune_on_scan": true
2777 }),
2778 "hash": crate::json!({ "enabled": true }),
2779 "bitmap": crate::json!({ "enabled": true, "max_cardinality": 1000 }),
2780 "spatial": crate::json!({ "enabled": true })
2781 }),
2782 );
2783 store.set_config_tree(
2784 "red.memtable",
2785 &crate::json!({
2786 "enabled": true,
2787 "max_bytes": 67108864_u64,
2788 "flush_threshold": 0.75
2789 }),
2790 );
2791 store.set_config_tree(
2792 "red.probabilistic",
2793 &crate::json!({
2794 "hll_registers": 16384,
2795 "sketch_default_width": 1000,
2796 "sketch_default_depth": 5,
2797 "filter_default_capacity": 100000
2798 }),
2799 );
2800 store.set_config_tree(
2801 "red.timeseries",
2802 &crate::json!({
2803 "default_chunk_size": 1024,
2804 "compression": crate::json!({
2805 "timestamps": "delta_of_delta",
2806 "values": "gorilla_xor"
2807 }),
2808 "default_retention_days": 0
2809 }),
2810 );
2811 store.set_config_tree(
2812 "red.queue",
2813 &crate::json!({
2814 "default_max_size": 0,
2815 "default_max_attempts": 3,
2816 "visibility_timeout_ms": 30000,
2817 "consumer_idle_timeout_ms": 60000
2818 }),
2819 );
2820 store.set_config_tree(
2821 "red.backup",
2822 &crate::json!({
2823 "enabled": false,
2824 "interval_secs": 3600,
2825 "retention_count": 24,
2826 "upload": false,
2827 "backend": "local"
2828 }),
2829 );
2830 store.set_config_tree(
2831 "red.wal",
2832 &crate::json!({
2833 "archive": crate::json!({
2834 "enabled": false,
2835 "retention_hours": 168,
2836 "prefix": reddb_file::backup_wal_prefix("")
2837 })
2838 }),
2839 );
2840 store.set_config_tree(
2841 "red.cdc",
2842 &crate::json!({
2843 "enabled": true,
2844 "buffer_size": 100000
2845 }),
2846 );
2847 store.set_config_tree(
2848 "red.config.secret",
2849 &crate::json!({
2850 "auto_encrypt": true,
2851 "auto_decrypt": true
2852 }),
2853 );
2854 }
2855
2856 crate::runtime::config_matrix::heal_critical_keys(store.as_ref());
2863 seed_storage_deploy_config(store.as_ref(), options.storage_profile);
2864
2865 let lehman_yao = runtime.config_bool("storage.btree.lehman_yao", true);
2872 crate::storage::engine::btree::lehman_yao::set_enabled(lehman_yao);
2873 if lehman_yao {
2874 tracing::info!(
2875 "storage.btree.lehman_yao=true — lock-free concurrent descent enabled"
2876 );
2877 }
2878
2879 let overlay_path = crate::runtime::config_overlay::config_file_path();
2884 let _ =
2885 crate::runtime::config_overlay::apply_config_file(store.as_ref(), &overlay_path);
2886 }
2887
2888 {
2892 let store = runtime.inner.db.store();
2893 for name in crate::application::vcs_collections::ALL {
2894 let _ = store.get_or_create_collection(*name);
2895 }
2896 store.set_config_tree(
2899 crate::application::vcs_collections::CONFIG_NAMESPACE,
2900 &crate::json!({
2901 "default_branch": "main",
2902 "author": crate::json!({
2903 "name": "reddb",
2904 "email": "reddb@localhost"
2905 }),
2906 "protected_branches": crate::json!(["main"]),
2907 "closure": crate::json!({
2908 "enabled": true,
2909 "lazy": true
2910 }),
2911 "merge": crate::json!({
2912 "default_strategy": "auto",
2913 "fast_forward": true
2914 })
2915 }),
2916 );
2917 }
2918
2919 {
2922 let store = runtime.inner.db.store();
2923 for name in crate::application::migration_collections::ALL {
2924 let _ = store.get_or_create_collection(*name);
2925 }
2926 }
2927
2928 let _ = crate::application::topology_collections::ensure(&runtime);
2932
2933 runtime
2941 .inner
2942 .db
2943 .store()
2944 .register_entity_id(crate::storage::EntityId::new(
2945 crate::storage::FIRST_USER_ENTITY_ID - 1,
2946 ));
2947
2948 {
2963 let weak = Arc::downgrade(&runtime.inner);
2964 std::thread::Builder::new()
2965 .name("reddb-maintenance".into())
2966 .spawn(move || {
2967 let tick = std::time::Duration::from_millis(200);
2968 let work_interval = std::time::Duration::from_secs(60);
2969 let mut last_work = std::time::Instant::now();
2970 loop {
2971 std::thread::sleep(tick);
2972 let Some(inner) = weak.upgrade() else {
2973 break;
2976 };
2977 if last_work.elapsed() >= work_interval {
2978 let _stats = inner.db.store().context_index().stats();
2979 last_work = std::time::Instant::now();
2980 }
2981 }
2982 })
2983 .ok();
2984 }
2985
2986 {
2988 let store = runtime.inner.db.store();
2989 let mut backup_enabled = false;
2990 let mut backup_interval = 3600u64;
2991
2992 if let Some(manager) = store.get_collection("red_config") {
2993 manager.for_each_entity(|entity| {
2994 if let Some(row) = entity.data.as_row() {
2995 let key = row.get_field("key").and_then(|v| match v {
2996 crate::storage::schema::Value::Text(s) => Some(s.as_ref()),
2997 _ => None,
2998 });
2999 let val = row.get_field("value");
3000 if key == Some("red.config.backup.enabled") {
3001 backup_enabled = match val {
3002 Some(crate::storage::schema::Value::Boolean(true)) => true,
3003 Some(crate::storage::schema::Value::Text(s)) => &**s == "true",
3004 _ => false,
3005 };
3006 } else if key == Some("red.config.backup.interval_secs") {
3007 if let Some(crate::storage::schema::Value::Integer(n)) = val {
3008 backup_interval = *n as u64;
3009 }
3010 }
3011 }
3012 true
3013 });
3014 }
3015
3016 if backup_enabled {
3017 runtime.inner.backup_scheduler.set_interval(backup_interval);
3018 let rt = runtime.clone();
3019 runtime
3020 .inner
3021 .backup_scheduler
3022 .start(move || rt.trigger_backup().map_err(|e| format!("{}", e)));
3023 }
3024 }
3025
3026 {
3028 runtime
3029 .inner
3030 .ec_registry
3031 .load_from_config_store(runtime.inner.db.store().as_ref());
3032 if !runtime.inner.ec_registry.async_configs().is_empty() {
3033 runtime.inner.ec_worker.start(
3034 Arc::clone(&runtime.inner.ec_registry),
3035 Arc::clone(&runtime.inner.db.store()),
3036 );
3037 }
3038 }
3039
3040 if let crate::replication::ReplicationRole::Replica { primary_addr } =
3041 runtime.inner.db.options().replication.role.clone()
3042 {
3043 let rt = runtime.clone();
3044 std::thread::Builder::new()
3045 .name("reddb-replica".into())
3046 .spawn(move || rt.run_replica_loop(primary_addr))
3047 .ok();
3048 }
3049
3050 runtime.inner.lifecycle.mark_ready();
3055
3056 {
3065 let weak_inner = Arc::downgrade(&runtime.inner);
3066 std::thread::Builder::new()
3067 .name("reddb-mv-scheduler".into())
3068 .spawn(move || loop {
3069 std::thread::sleep(std::time::Duration::from_millis(50));
3070 let Some(inner) = weak_inner.upgrade() else {
3071 break;
3072 };
3073 let rt = RedDBRuntime { inner };
3074 rt.refresh_due_materialized_views();
3075 })
3076 .ok();
3077 }
3078
3079 if !runtime.write_gate().is_read_only() {
3089 let weak_inner = Arc::downgrade(&runtime.inner);
3090 std::thread::Builder::new()
3091 .name("reddb-retention-sweeper".into())
3092 .spawn(move || loop {
3093 std::thread::sleep(std::time::Duration::from_millis(500));
3094 let Some(inner) = weak_inner.upgrade() else {
3095 break;
3096 };
3097 let rt = RedDBRuntime { inner };
3098 rt.sweep_retention_tick(
3099 crate::runtime::retention_sweeper::DEFAULT_SWEEPER_BATCH,
3100 );
3101 })
3102 .ok();
3103 }
3104
3105 Ok(runtime)
3106 }
3107
3108 fn rehydrate_snapshot_xid_floor(&self) {
3109 let store = self.inner.db.store();
3110 for collection in store.list_collections() {
3111 let Some(manager) = store.get_collection(&collection) else {
3112 continue;
3113 };
3114 for entity in manager.query_all(|_| true) {
3115 self.inner
3116 .snapshot_manager
3117 .observe_committed_xid(entity.xmin);
3118 self.inner
3119 .snapshot_manager
3120 .observe_committed_xid(entity.xmax);
3121 }
3122 }
3123 }
3124
3125 pub(crate) fn ensure_materialized_view_backing(&self, name: &str) -> RedDBResult<()> {
3138 let store = self.inner.db.store();
3139 let mut changed = false;
3140 if store.get_collection(name).is_none() {
3141 store.get_or_create_collection(name);
3142 changed = true;
3143 }
3144 if self.inner.db.collection_contract(name).is_none() {
3145 self.inner
3146 .db
3147 .save_collection_contract(system_keyed_collection_contract(
3148 name,
3149 crate::catalog::CollectionModel::Table,
3150 ))
3151 .map_err(|err| RedDBError::Internal(err.to_string()))?;
3152 changed = true;
3153 }
3154 if changed {
3155 self.inner
3156 .db
3157 .persist_metadata()
3158 .map_err(|err| RedDBError::Internal(err.to_string()))?;
3159 }
3160 Ok(())
3161 }
3162
3163 pub(crate) fn drop_materialized_view_backing(&self, name: &str) -> RedDBResult<()> {
3168 let store = self.inner.db.store();
3169 if store.get_collection(name).is_none() {
3170 return Ok(());
3171 }
3172 store
3173 .drop_collection(name)
3174 .map_err(|err| RedDBError::Internal(err.to_string()))?;
3175 if self.inner.db.collection_contract(name).is_some() {
3178 self.inner
3179 .db
3180 .remove_collection_contract(name)
3181 .map_err(|err| RedDBError::Internal(err.to_string()))?;
3182 }
3183 self.invalidate_result_cache();
3184 self.inner
3185 .db
3186 .persist_metadata()
3187 .map_err(|err| RedDBError::Internal(err.to_string()))?;
3188 Ok(())
3189 }
3190
3191 fn bootstrap_system_keyed_collections(&self) -> RedDBResult<()> {
3192 let mut changed = false;
3193 for (name, model) in [
3194 ("red.config", crate::catalog::CollectionModel::Config),
3195 ("red.vault", crate::catalog::CollectionModel::Vault),
3196 (
3200 crate::runtime::continuous_materialized_view::CATALOG_COLLECTION,
3201 crate::catalog::CollectionModel::Config,
3202 ),
3203 ] {
3204 if self.inner.db.store().get_collection(name).is_none() {
3205 self.inner.db.store().get_or_create_collection(name);
3206 changed = true;
3207 }
3208 if self.inner.db.collection_contract(name).is_none() {
3209 self.inner
3210 .db
3211 .save_collection_contract(system_keyed_collection_contract(name, model))
3212 .map_err(|err| RedDBError::Internal(err.to_string()))?;
3213 changed = true;
3214 }
3215 }
3216 if changed {
3217 self.inner
3218 .db
3219 .persist_metadata()
3220 .map_err(|err| RedDBError::Internal(err.to_string()))?;
3221 }
3222 Ok(())
3223 }
3224
3225 pub fn db(&self) -> Arc<RedDB> {
3226 Arc::clone(&self.inner.db)
3227 }
3228
3229 pub fn index_store_ref(&self) -> &super::index_store::IndexStore {
3234 &self.inner.index_store
3235 }
3236
3237 pub(crate) fn schema_vocabulary_apply(
3242 &self,
3243 event: crate::runtime::schema_vocabulary::DdlEvent,
3244 ) {
3245 self.inner.schema_vocabulary.write().on_ddl(event);
3246 }
3247
3248 pub fn schema_vocabulary_lookup(
3253 &self,
3254 token: &str,
3255 ) -> Vec<crate::runtime::schema_vocabulary::VocabHit> {
3256 self.inner.schema_vocabulary.read().lookup(token).to_vec()
3257 }
3258
3259 pub fn set_auth_store(&self, store: Arc<crate::auth::store::AuthStore>) {
3263 *self.inner.auth_store.write() = Some(store);
3264 }
3265
3266 pub fn auth_store(&self) -> Option<Arc<crate::auth::store::AuthStore>> {
3269 self.inner.auth_store.read().clone()
3270 }
3271
3272 pub fn vault_kv_get(&self, key: &str) -> Option<String> {
3274 self.inner
3275 .auth_store
3276 .read()
3277 .as_ref()
3278 .and_then(|store| store.vault_kv_get(key))
3279 }
3280
3281 pub fn vault_kv_try_set(&self, key: String, value: String) -> RedDBResult<()> {
3284 let store = self.inner.auth_store.read().clone().ok_or_else(|| {
3285 RedDBError::Query("secret storage requires an enabled, unsealed vault".to_string())
3286 })?;
3287 store
3288 .vault_kv_try_set(key, value)
3289 .map_err(|err| RedDBError::Query(err.to_string()))
3290 }
3291
3292 pub fn set_oauth_validator(&self, validator: Option<Arc<crate::auth::oauth::OAuthValidator>>) {
3296 *self.inner.oauth_validator.write() = validator;
3297 }
3298
3299 pub fn oauth_validator(&self) -> Option<Arc<crate::auth::oauth::OAuthValidator>> {
3303 self.inner.oauth_validator.read().clone()
3304 }
3305
3306 pub fn set_browser_token_authority(
3312 &self,
3313 authority: Option<Arc<crate::auth::browser_token::BrowserTokenAuthority>>,
3314 ) {
3315 *self.inner.browser_token_authority.write() = authority;
3316 }
3317
3318 pub fn browser_token_authority(
3322 &self,
3323 ) -> Option<Arc<crate::auth::browser_token::BrowserTokenAuthority>> {
3324 self.inner.browser_token_authority.read().clone()
3325 }
3326
3327 pub(crate) fn secret_aes_key(&self) -> Option<[u8; 32]> {
3331 let guard = self.inner.auth_store.read();
3332 guard.as_ref().and_then(|s| s.vault_secret_key())
3333 }
3334
3335 pub(crate) fn config_bool(&self, key: &str, default: bool) -> bool {
3341 if let Some(raw) = self.inner.env_config_overrides.get(key) {
3342 if let Some(crate::storage::schema::Value::Boolean(b)) =
3343 crate::runtime::config_overlay::coerce_env_value(key, raw)
3344 {
3345 return b;
3346 }
3347 }
3348 let store = self.inner.db.store();
3349 let Some(manager) = store.get_collection("red_config") else {
3350 return default;
3351 };
3352 let mut result = default;
3353 let mut latest_id: u64 = 0;
3354 manager.for_each_entity(|entity| {
3355 if let Some(row) = entity.data.as_row() {
3356 let entry_key = row.get_field("key").and_then(|v| match v {
3357 crate::storage::schema::Value::Text(s) => Some(s.as_ref()),
3358 _ => None,
3359 });
3360 if entry_key == Some(key) {
3361 let id = entity.id.raw();
3362 if id >= latest_id {
3363 latest_id = id;
3364 result = match row.get_field("value") {
3365 Some(crate::storage::schema::Value::Boolean(b)) => *b,
3366 Some(crate::storage::schema::Value::Text(s)) => {
3367 matches!(s.as_ref(), "true" | "TRUE" | "True" | "1")
3368 }
3369 Some(crate::storage::schema::Value::Integer(n)) => *n != 0,
3370 _ => default,
3371 };
3372 }
3373 }
3374 }
3375 true
3376 });
3377 result
3378 }
3379
3380 pub(crate) fn binary_document_body_enabled(&self) -> bool {
3386 self.config_bool("storage.binary_document_body", false)
3387 }
3388
3389 pub(crate) fn config_u64(&self, key: &str, default: u64) -> u64 {
3390 if let Some(raw) = self.inner.env_config_overrides.get(key) {
3391 if let Some(crate::storage::schema::Value::UnsignedInteger(n)) =
3392 crate::runtime::config_overlay::coerce_env_value(key, raw)
3393 {
3394 return n;
3395 }
3396 }
3397 let store = self.inner.db.store();
3398 let Some(manager) = store.get_collection("red_config") else {
3399 return default;
3400 };
3401 let mut result = default;
3402 let mut latest_id: u64 = 0;
3403 manager.for_each_entity(|entity| {
3404 if let Some(row) = entity.data.as_row() {
3405 let entry_key = row.get_field("key").and_then(|v| match v {
3406 crate::storage::schema::Value::Text(s) => Some(s.as_ref()),
3407 _ => None,
3408 });
3409 if entry_key == Some(key) {
3410 let id = entity.id.raw();
3411 if id >= latest_id {
3412 latest_id = id;
3413 result = match row.get_field("value") {
3414 Some(crate::storage::schema::Value::Integer(n)) => *n as u64,
3415 Some(crate::storage::schema::Value::UnsignedInteger(n)) => *n,
3416 Some(crate::storage::schema::Value::Text(s)) => {
3417 s.parse::<u64>().unwrap_or(default)
3418 }
3419 _ => default,
3420 };
3421 }
3422 }
3423 }
3424 true
3425 });
3426 result
3427 }
3428
3429 pub(crate) fn config_f64(&self, key: &str, default: f64) -> f64 {
3430 if let Some(raw) = self.inner.env_config_overrides.get(key) {
3431 if let Ok(n) = raw.parse::<f64>() {
3432 return n;
3433 }
3434 }
3435 let store = self.inner.db.store();
3436 let Some(manager) = store.get_collection("red_config") else {
3437 return default;
3438 };
3439 let mut result = default;
3440 let mut latest_id: u64 = 0;
3441 manager.for_each_entity(|entity| {
3442 if let Some(row) = entity.data.as_row() {
3443 let entry_key = row.get_field("key").and_then(|v| match v {
3444 crate::storage::schema::Value::Text(s) => Some(s.as_ref()),
3445 _ => None,
3446 });
3447 if entry_key == Some(key) {
3448 let id = entity.id.raw();
3449 if id >= latest_id {
3450 latest_id = id;
3451 result = match row.get_field("value") {
3452 Some(crate::storage::schema::Value::Float(n)) => *n,
3453 Some(crate::storage::schema::Value::Integer(n)) => *n as f64,
3454 Some(crate::storage::schema::Value::UnsignedInteger(n)) => *n as f64,
3455 Some(crate::storage::schema::Value::Text(s)) => {
3456 s.parse::<f64>().unwrap_or(default)
3457 }
3458 _ => default,
3459 };
3460 }
3461 }
3462 }
3463 true
3464 });
3465 result
3466 }
3467
3468 pub(crate) fn config_string(&self, key: &str, default: &str) -> String {
3469 if let Some(raw) = self.inner.env_config_overrides.get(key) {
3470 return raw.clone();
3471 }
3472 let store = self.inner.db.store();
3473 let Some(manager) = store.get_collection("red_config") else {
3474 return default.to_string();
3475 };
3476 let mut result = default.to_string();
3477 let mut latest_id: u64 = 0;
3478 manager.for_each_entity(|entity| {
3479 if let Some(row) = entity.data.as_row() {
3480 let entry_key = row.get_field("key").and_then(|v| match v {
3481 crate::storage::schema::Value::Text(s) => Some(s.as_ref()),
3482 _ => None,
3483 });
3484 if entry_key == Some(key) {
3485 let id = entity.id.raw();
3486 if id >= latest_id {
3487 latest_id = id;
3488 if let Some(crate::storage::schema::Value::Text(value)) =
3489 row.get_field("value")
3490 {
3491 result = value.to_string();
3492 }
3493 }
3494 }
3495 }
3496 true
3497 });
3498 result
3499 }
3500
3501 pub(crate) fn secret_auto_encrypt(&self) -> bool {
3504 self.config_bool("red.config.secret.auto_encrypt", true)
3505 }
3506
3507 pub(crate) fn secret_auto_decrypt(&self) -> bool {
3512 self.config_bool("red.config.secret.auto_decrypt", true)
3513 }
3514
3515 pub(crate) fn apply_secret_decryption(&self, result: &mut RuntimeQueryResult) {
3522 if !self.secret_auto_decrypt() {
3523 return;
3524 }
3525 let Some(key) = self.secret_aes_key() else {
3526 return;
3527 };
3528 for record in result.result.records.iter_mut() {
3529 for value in record.values_mut() {
3530 if let Value::Secret(ref bytes) = value {
3531 if let Some(plain) =
3532 super::impl_dml::decrypt_secret_payload(&key, bytes.as_slice())
3533 {
3534 if let Ok(text) = String::from_utf8(plain) {
3535 *value = Value::text(text);
3536 }
3537 }
3538 }
3539 }
3540 }
3541 }
3542
3543 pub(crate) fn mutation_engine(&self) -> crate::runtime::mutation::MutationEngine<'_> {
3551 crate::runtime::mutation::MutationEngine::new(self)
3552 }
3553
3554 pub fn check_write(&self, kind: crate::runtime::write_gate::WriteKind) -> RedDBResult<()> {
3565 self.inner.write_gate.check(kind)
3566 }
3567
3568 pub fn write_gate(&self) -> &crate::runtime::write_gate::WriteGate {
3572 &self.inner.write_gate
3573 }
3574
3575 pub fn lifecycle(&self) -> &crate::runtime::lifecycle::Lifecycle {
3579 &self.inner.lifecycle
3580 }
3581
3582 pub fn resource_limits(&self) -> &crate::runtime::resource_limits::ResourceLimits {
3584 &self.inner.resource_limits
3585 }
3586
3587 pub fn audit_log(&self) -> &crate::runtime::audit_log::AuditLogger {
3589 &self.inner.audit_log
3590 }
3591
3592 pub fn audit_log_arc(&self) -> Arc<crate::runtime::audit_log::AuditLogger> {
3596 Arc::clone(&self.inner.audit_log)
3597 }
3598
3599 pub(crate) fn emit_control_event(
3600 &self,
3601 kind: crate::runtime::control_events::EventKind,
3602 outcome: crate::runtime::control_events::Outcome,
3603 action: &'static str,
3604 resource: Option<String>,
3605 reason: Option<String>,
3606 extra_fields: Vec<(String, crate::runtime::control_events::Sensitivity)>,
3607 ) -> RedDBResult<()> {
3608 use crate::runtime::control_events::{
3609 ActorRef, ControlEvent, ControlEventCtx, ControlEventLedger, Sensitivity,
3610 };
3611
3612 let tenant = current_tenant();
3613 let principal = current_auth_identity();
3614 let actor_user = principal
3615 .as_ref()
3616 .map(|(principal, _)| UserId::from_parts(tenant.as_deref(), principal));
3617 let actor = actor_user
3618 .as_ref()
3619 .map(ActorRef::User)
3620 .unwrap_or(ActorRef::Anonymous);
3621 let ctx = ControlEventCtx {
3622 actor,
3623 scope: tenant
3624 .as_ref()
3625 .map(|scope| std::borrow::Cow::Borrowed(scope.as_str())),
3626 request_id: Some(std::borrow::Cow::Owned(format!(
3627 "conn-{}",
3628 current_connection_id()
3629 ))),
3630 trace_id: None,
3631 };
3632 let mut fields = std::collections::HashMap::new();
3633 fields.insert(
3634 "connection_id".to_string(),
3635 Sensitivity::raw(current_connection_id().to_string()),
3636 );
3637 if let Some((_, role)) = principal {
3638 fields.insert("actor_role".to_string(), Sensitivity::raw(role.as_str()));
3639 }
3640 for (key, value) in extra_fields {
3641 fields.insert(key, value);
3642 }
3643 let event = ControlEvent {
3644 kind,
3645 outcome,
3646 action: std::borrow::Cow::Borrowed(action),
3647 resource,
3648 reason,
3649 matched_policy_id: None,
3650 fields,
3651 };
3652 let ledger = self.inner.control_event_ledger.read();
3653 match ledger.emit(&ctx, event) {
3654 Ok(_) => Ok(()),
3655 Err(err) if self.inner.control_event_config.require_persistence() => {
3656 Err(RedDBError::Internal(err.to_string()))
3657 }
3658 Err(_) => Ok(()),
3659 }
3660 }
3661
3662 fn policy_mutation_control_ctx<'a>(
3663 &self,
3664 actor: &'a crate::auth::UserId,
3665 tenant: Option<&'a str>,
3666 ) -> crate::runtime::control_events::ControlEventCtx<'a> {
3667 crate::runtime::control_events::ControlEventCtx {
3668 actor: crate::runtime::control_events::ActorRef::User(actor),
3669 scope: tenant.map(std::borrow::Cow::Borrowed),
3670 request_id: Some(std::borrow::Cow::Owned(format!(
3671 "conn-{}",
3672 current_connection_id()
3673 ))),
3674 trace_id: None,
3675 }
3676 }
3677
3678 fn emit_query_audit(
3679 &self,
3680 query: &str,
3681 plan: &QueryAuditPlan,
3682 duration_ms: u64,
3683 result: &RuntimeQueryResult,
3684 ) {
3685 if !self.inner.query_audit.has_rules() {
3686 return;
3687 }
3688 let actor = current_auth_identity().map(|(principal, _)| principal);
3689 let tenant = current_tenant();
3690 let row_count = if result.statement_type == "select" {
3691 result.result.records.len() as u64
3692 } else {
3693 result.affected_rows
3694 };
3695 self.inner
3696 .query_audit
3697 .emit(crate::runtime::query_audit::QueryAuditEvent {
3698 actor,
3699 tenant,
3700 statement_kind: plan.statement_kind,
3701 touched_collections: plan.collections.clone(),
3702 duration_ms,
3703 row_count,
3704 request_id: Some(crate::crypto::uuid::Uuid::new_v7().to_string()),
3705 query_hash: Some(blake3::hash(query.as_bytes()).to_hex().to_string()),
3706 });
3707 }
3708
3709 pub(crate) fn queue_telemetry(
3711 &self,
3712 ) -> &crate::runtime::queue_telemetry::QueueTelemetryCounters {
3713 &self.inner.queue_telemetry
3714 }
3715
3716 pub fn queue_telemetry_snapshot(
3719 &self,
3720 ) -> crate::runtime::queue_telemetry::QueueTelemetrySnapshot {
3721 crate::runtime::queue_telemetry::QueueTelemetrySnapshot {
3722 delivered: self.inner.queue_telemetry.delivered_snapshot(),
3723 acked: self.inner.queue_telemetry.acked_snapshot(),
3724 nacked: self.inner.queue_telemetry.nacked_snapshot(),
3725 wait_started: self.inner.queue_telemetry.wait_started_snapshot(),
3726 wait_woken: self.inner.queue_telemetry.wait_woken_snapshot(),
3727 wait_timed_out: self.inner.queue_telemetry.wait_timed_out_snapshot(),
3728 wait_cancelled: self.inner.queue_telemetry.wait_cancelled_snapshot(),
3729 wait_duration: self.inner.queue_telemetry.wait_duration_snapshot(),
3730 }
3731 }
3732
3733 pub fn claim_telemetry_snapshot(&self) -> crate::runtime::ClaimTelemetrySnapshot {
3735 self.inner.claim_telemetry.snapshot()
3736 }
3737
3738 pub fn query_latency_snapshot(
3741 &self,
3742 ) -> Vec<crate::runtime::query_latency_telemetry::QueryLatencyHistogram> {
3743 self.inner.query_latency_telemetry.snapshot()
3744 }
3745
3746 pub fn query_latency_rollup(
3749 &self,
3750 ) -> crate::runtime::query_latency_telemetry::QueryLatencyHistogram {
3751 self.inner.query_latency_telemetry.rollup()
3752 }
3753
3754 pub fn sample_occupancy(&self) -> crate::runtime::occupancy_sampler::OccupancySample {
3759 self.inner.occupancy_sampler.sample()
3760 }
3761
3762 pub fn node_load_snapshot(&self) -> crate::runtime::node_load_telemetry::NodeLoadSnapshot {
3766 self.inner.node_load_telemetry.snapshot()
3767 }
3768
3769 pub(crate) fn queue_presence(
3774 &self,
3775 ) -> &std::sync::Arc<crate::storage::queue::presence::ConsumerPresenceRegistry> {
3776 &self.inner.queue_presence
3777 }
3778
3779 pub fn queue_consumer_presence_snapshot(
3784 &self,
3785 ttl_ms: u64,
3786 ) -> Vec<crate::storage::queue::presence::ConsumerPresence> {
3787 let now_ns = std::time::SystemTime::now()
3788 .duration_since(std::time::UNIX_EPOCH)
3789 .map(|d| d.as_nanos() as u64)
3790 .unwrap_or(0);
3791 self.inner.queue_presence.snapshot(now_ns, ttl_ms)
3792 }
3793
3794 pub fn queue_active_consumer_counts(
3798 &self,
3799 ttl_ms: u64,
3800 ) -> std::collections::HashMap<(String, String), u32> {
3801 let now_ns = std::time::SystemTime::now()
3802 .duration_since(std::time::UNIX_EPOCH)
3803 .map(|d| d.as_nanos() as u64)
3804 .unwrap_or(0);
3805 self.inner
3806 .queue_presence
3807 .count_active_by_group(now_ns, ttl_ms)
3808 }
3809
3810 pub(crate) fn vector_introspection_registry(
3816 &self,
3817 ) -> &std::sync::Arc<crate::storage::vector::introspection::VectorIntrospectionRegistry> {
3818 &self.inner.vector_introspection
3819 }
3820
3821 pub fn vector_introspection_snapshot(
3826 &self,
3827 ) -> Vec<crate::storage::vector::introspection::VectorIntrospection> {
3828 self.inner.vector_introspection.snapshot()
3829 }
3830
3831 pub fn vector_introspection_get(
3835 &self,
3836 collection: &str,
3837 ) -> Option<crate::storage::vector::introspection::VectorIntrospection> {
3838 self.inner.vector_introspection.get(collection)
3839 }
3840
3841 pub fn slow_query_store(&self) -> &Arc<crate::telemetry::slow_query_store::SlowQueryStore> {
3847 &self.inner.slow_query_store
3848 }
3849
3850 pub fn queue_pending_counts(&self) -> Vec<((String, String), u64)> {
3855 let store = self.inner.db.store();
3856 crate::runtime::impl_queue::pending_counts_by_group(store.as_ref())
3857 .into_iter()
3858 .collect()
3859 }
3860
3861 pub fn write_gate_arc(&self) -> Arc<crate::runtime::write_gate::WriteGate> {
3866 Arc::clone(&self.inner.write_gate)
3867 }
3868
3869 pub fn lease_lifecycle(&self) -> Option<&Arc<crate::runtime::lease_lifecycle::LeaseLifecycle>> {
3872 self.inner.lease_lifecycle.get()
3873 }
3874
3875 pub fn set_lease_lifecycle(
3878 &self,
3879 lifecycle: Arc<crate::runtime::lease_lifecycle::LeaseLifecycle>,
3880 ) -> Result<(), Arc<crate::runtime::lease_lifecycle::LeaseLifecycle>> {
3881 self.inner.lease_lifecycle.set(lifecycle)
3882 }
3883
3884 pub fn check_batch_size(&self, requested: usize) -> RedDBResult<()> {
3889 if self.inner.resource_limits.batch_size_exceeded(requested) {
3890 let max = self.inner.resource_limits.max_batch_size.unwrap_or(0);
3891 return Err(RedDBError::QuotaExceeded(format!(
3892 "max_batch_size:{requested}:{max}"
3893 )));
3894 }
3895 Ok(())
3896 }
3897
3898 pub fn check_db_size(&self) -> RedDBResult<()> {
3904 let Some(limit) = self.inner.resource_limits.max_db_size_bytes else {
3905 return Ok(());
3906 };
3907 if limit == 0 {
3908 return Ok(());
3909 }
3910 let Some(path) = self.inner.db.path() else {
3911 return Ok(());
3912 };
3913 let current = std::fs::metadata(path).map(|m| m.len()).unwrap_or(0);
3914 if current > limit {
3915 return Err(RedDBError::QuotaExceeded(format!(
3916 "max_db_size_bytes:{current}:{limit}"
3917 )));
3918 }
3919 Ok(())
3920 }
3921
3922 pub fn graceful_shutdown(
3940 &self,
3941 backup_on_shutdown: bool,
3942 ) -> RedDBResult<crate::runtime::lifecycle::ShutdownReport> {
3943 if !self.inner.lifecycle.begin_shutdown() {
3944 return Ok(self.inner.lifecycle.shutdown_report().unwrap_or_default());
3948 }
3949
3950 let started_ms = std::time::SystemTime::now()
3951 .duration_since(std::time::UNIX_EPOCH)
3952 .map(|d| d.as_millis() as u64)
3953 .unwrap_or(0);
3954 let mut report = crate::runtime::lifecycle::ShutdownReport {
3955 started_at_ms: started_ms,
3956 ..Default::default()
3957 };
3958
3959 let flush_res = self.inner.db.flush_local_only();
3965 report.flushed_wal = flush_res.is_ok();
3966 report.final_checkpoint = flush_res.is_ok();
3967 if let Err(err) = &flush_res {
3968 tracing::error!(
3969 target: "reddb::lifecycle",
3970 error = %err,
3971 "graceful_shutdown: local flush failed"
3972 );
3973 } else if let Err(lease_err) =
3974 self.assert_remote_write_allowed("shutdown/checkpoint_upload")
3975 {
3976 tracing::warn!(
3977 target: "reddb::serverless::lease",
3978 error = %lease_err,
3979 "graceful_shutdown: remote upload skipped — lease not held"
3980 );
3981 } else if let Err(err) = self.inner.db.upload_to_remote_backend() {
3982 tracing::error!(
3983 target: "reddb::lifecycle",
3984 error = %err,
3985 "graceful_shutdown: remote upload failed"
3986 );
3987 }
3988
3989 if backup_on_shutdown && self.inner.db.remote_backend.is_some() {
3994 match self.trigger_backup() {
4000 Ok(result) => {
4001 report.backup_uploaded = result.uploaded;
4002 }
4003 Err(err) => {
4004 tracing::warn!(
4005 target: "reddb::lifecycle",
4006 error = %err,
4007 "graceful_shutdown: final backup skipped"
4008 );
4009 }
4010 }
4011 }
4012
4013 let completed_ms = std::time::SystemTime::now()
4014 .duration_since(std::time::UNIX_EPOCH)
4015 .map(|d| d.as_millis() as u64)
4016 .unwrap_or(started_ms);
4017 report.completed_at_ms = completed_ms;
4018 report.duration_ms = completed_ms.saturating_sub(started_ms);
4019
4020 self.inner.lifecycle.finish_shutdown(report.clone());
4021 Ok(report)
4022 }
4023
4024 pub fn quota_bucket(&self) -> &crate::runtime::quota_bucket::QuotaBucket {
4027 &self.inner.quota_bucket
4028 }
4029
4030 pub fn encryption_at_rest_status(&self) -> (&'static str, Option<String>) {
4038 match crate::crypto::page_encryption::key_from_env() {
4039 Ok(Some(_)) => ("enabled", None),
4040 Ok(None) => ("disabled", None),
4041 Err(err) => ("error", Some(err)),
4042 }
4043 }
4044
4045 pub fn replica_apply_health(&self) -> Option<String> {
4051 let state = self.config_string("red.replication.state", "");
4052 if state.is_empty() {
4053 None
4054 } else {
4055 Some(state)
4056 }
4057 }
4058
4059 pub fn acquire(&self) -> RedDBResult<RuntimeConnection> {
4060 let mut pool = self
4061 .inner
4062 .pool
4063 .lock()
4064 .map_err(|e| RedDBError::Internal(format!("connection pool lock poisoned: {e}")))?;
4065 if pool.active >= self.inner.pool_config.max_connections {
4066 return Err(RedDBError::Internal(
4067 "connection pool exhausted".to_string(),
4068 ));
4069 }
4070
4071 let id = if let Some(id) = pool.idle.pop() {
4072 id
4073 } else {
4074 let id = pool.next_id;
4075 pool.next_id += 1;
4076 id
4077 };
4078 pool.active += 1;
4079 pool.total_checkouts += 1;
4080 drop(pool);
4081
4082 self.inner.node_load_telemetry.record_connect();
4085
4086 Ok(RuntimeConnection {
4087 id,
4088 inner: Arc::clone(&self.inner),
4089 })
4090 }
4091
4092 pub fn checkpoint(&self) -> RedDBResult<()> {
4093 self.inner.db.flush_local_only().map_err(|err| {
4098 let msg = err.to_string();
4103 crate::telemetry::operator_event::OperatorEvent::CheckpointFailed {
4104 lsn: 0,
4105 error: msg.clone(),
4106 }
4107 .emit_global();
4108 crate::telemetry::operator_event::OperatorEvent::WalFsyncFailed {
4109 path: "<flush_local_only>".to_string(),
4110 error: msg.clone(),
4111 }
4112 .emit_global();
4113 RedDBError::Engine(msg)
4114 })?;
4115 if let Err(err) = self.assert_remote_write_allowed("checkpoint") {
4116 tracing::warn!(
4117 target: "reddb::serverless::lease",
4118 error = %err,
4119 "checkpoint: skipping remote upload — lease not held"
4120 );
4121 return Ok(());
4122 }
4123 self.inner
4124 .db
4125 .upload_to_remote_backend()
4126 .map_err(|err| RedDBError::Engine(err.to_string()))
4127 }
4128
4129 pub(crate) fn assert_remote_write_allowed(&self, action: &str) -> RedDBResult<()> {
4136 if self.inner.db.remote_backend.is_none() {
4137 return Ok(());
4138 }
4139 match self.inner.write_gate.lease_state() {
4140 crate::runtime::write_gate::LeaseGateState::NotHeld => {
4141 self.inner.audit_log.record(
4142 action,
4143 "system",
4144 "remote_backend",
4145 "err: writer lease not held",
4146 crate::json::Value::Null,
4147 );
4148 Err(RedDBError::ReadOnly(format!(
4149 "writer lease not held — {action} blocked (serverless fence)"
4150 )))
4151 }
4152 _ => Ok(()),
4153 }
4154 }
4155
4156 pub fn run_maintenance(&self) -> RedDBResult<()> {
4157 self.inner
4158 .db
4159 .run_maintenance()
4160 .map_err(|err| RedDBError::Internal(err.to_string()))
4161 }
4162
4163 pub fn scan_collection(
4164 &self,
4165 collection: &str,
4166 cursor: Option<ScanCursor>,
4167 limit: usize,
4168 ) -> RedDBResult<ScanPage> {
4169 let store = self.inner.db.store();
4170 let manager = store
4171 .get_collection(collection)
4172 .ok_or_else(|| RedDBError::NotFound(collection.to_string()))?;
4173
4174 let mut entities = manager.query_all(|_| true);
4175 entities.sort_by_key(|entity| entity.id.raw());
4176
4177 let offset = cursor.map(|cursor| cursor.offset).unwrap_or(0);
4178 let total = entities.len();
4179 let end = total.min(offset.saturating_add(limit.max(1)));
4180 let items = if offset >= total {
4181 Vec::new()
4182 } else {
4183 entities[offset..end].to_vec()
4184 };
4185 let next = (end < total).then_some(ScanCursor { offset: end });
4186
4187 Ok(ScanPage {
4188 collection: collection.to_string(),
4189 items,
4190 next,
4191 total,
4192 })
4193 }
4194
4195 pub fn catalog(&self) -> CatalogModelSnapshot {
4196 self.inner.db.catalog_model_snapshot()
4197 }
4198
4199 pub fn catalog_consistency_report(&self) -> crate::catalog::CatalogConsistencyReport {
4200 self.inner.db.catalog_consistency_report()
4201 }
4202
4203 pub fn catalog_attention_summary(&self) -> CatalogAttentionSummary {
4204 crate::catalog::attention_summary(&self.catalog())
4205 }
4206
4207 pub fn collection_attention(&self) -> Vec<CollectionDescriptor> {
4208 crate::catalog::collection_attention(&self.catalog())
4209 }
4210
4211 pub fn index_attention(&self) -> Vec<CatalogIndexStatus> {
4212 crate::catalog::index_attention(&self.catalog())
4213 }
4214
4215 pub fn graph_projection_attention(&self) -> Vec<CatalogGraphProjectionStatus> {
4216 crate::catalog::graph_projection_attention(&self.catalog())
4217 }
4218
4219 pub fn analytics_job_attention(&self) -> Vec<CatalogAnalyticsJobStatus> {
4220 crate::catalog::analytics_job_attention(&self.catalog())
4221 }
4222
4223 pub fn stats(&self) -> RuntimeStats {
4224 let pool = runtime_pool_lock(self);
4225 RuntimeStats {
4226 active_connections: pool.active,
4227 idle_connections: pool.idle.len(),
4228 total_checkouts: pool.total_checkouts,
4229 paged_mode: self.inner.db.is_paged(),
4230 started_at_unix_ms: self.inner.started_at_unix_ms,
4231 store: self.inner.db.stats(),
4232 system: SystemInfo::collect(),
4233 result_blob_cache: self.inner.result_blob_cache.stats(),
4234 kv: self.inner.kv_stats.snapshot(),
4235 metrics_ingest: self.inner.metrics_ingest_stats.snapshot(),
4236 }
4237 }
4238
4239 pub(crate) fn record_metrics_ingest(
4240 &self,
4241 accepted_samples: u64,
4242 accepted_series: u64,
4243 rejected_samples: u64,
4244 rejected_series: u64,
4245 ) {
4246 self.inner.metrics_ingest_stats.record(
4247 accepted_samples,
4248 accepted_series,
4249 rejected_samples,
4250 rejected_series,
4251 );
4252 }
4253
4254 pub(crate) fn record_metrics_cardinality_budget_rejections(&self, rejected_series: u64) {
4255 self.inner
4256 .metrics_ingest_stats
4257 .record_cardinality_budget_rejections(rejected_series);
4258 }
4259
4260 pub(crate) fn record_metrics_tenant_activity(
4261 &self,
4262 tenant: &str,
4263 namespace: &str,
4264 operation: &str,
4265 ) {
4266 self.inner
4267 .metrics_tenant_activity_stats
4268 .record(tenant, namespace, operation);
4269 }
4270
4271 pub(crate) fn metrics_tenant_activity_snapshot(
4272 &self,
4273 ) -> Vec<crate::runtime::MetricsTenantActivityStats> {
4274 self.inner.metrics_tenant_activity_stats.snapshot()
4275 }
4276
4277 pub fn execute_query_with_scope(
4291 &self,
4292 query: &str,
4293 scope: crate::runtime::within_clause::ScopeOverride,
4294 ) -> RedDBResult<RuntimeQueryResult> {
4295 if scope.is_empty() {
4296 return self.execute_query(query);
4297 }
4298 let _scope_guard = ScopeOverrideGuard::install(scope);
4299 self.execute_query(query)
4300 }
4301
4302 pub fn execute_query(&self, query: &str) -> RedDBResult<RuntimeQueryResult> {
4311 let started = std::time::Instant::now();
4312 self.inner.node_load_telemetry.query_start();
4313 let result = self.execute_query_inner(query);
4314 self.finish_query_lifecycle(query, started, result)
4315 }
4316
4317 pub fn execute_query_with_params(
4325 &self,
4326 query: &str,
4327 params: &[Value],
4328 ) -> RedDBResult<RuntimeQueryResult> {
4329 if params.is_empty() {
4330 return self.execute_query(query);
4331 }
4332 let started = std::time::Instant::now();
4333 self.inner.node_load_telemetry.query_start();
4334 let result = self.execute_query_with_params_inner(query, params);
4335 self.finish_query_lifecycle(query, started, result)
4336 }
4337
4338 fn finish_query_lifecycle(
4339 &self,
4340 query: &str,
4341 started: std::time::Instant,
4342 mut result: RedDBResult<RuntimeQueryResult>,
4343 ) -> RedDBResult<RuntimeQueryResult> {
4344 if let Ok(ref mut query_result) = result {
4349 if query_result.statement_type == "select" {
4350 self.filter_integrity_tombstoned(&mut query_result.result);
4351 }
4352 }
4353 let elapsed_ms = started.elapsed().as_millis() as u64;
4354
4355 let scope = self.ai_scope();
4360 let kind = match result
4361 .as_ref()
4362 .map(|r| r.statement_type)
4363 .unwrap_or("select")
4364 {
4365 "select" => crate::telemetry::slow_query_logger::QueryKind::Select,
4366 "insert" => crate::telemetry::slow_query_logger::QueryKind::Insert,
4367 "update" => crate::telemetry::slow_query_logger::QueryKind::Update,
4368 "delete" => crate::telemetry::slow_query_logger::QueryKind::Delete,
4369 _ => crate::telemetry::slow_query_logger::QueryKind::Internal,
4370 };
4371 self.inner
4377 .slow_query_logger
4378 .record(kind, elapsed_ms, query.to_string(), &scope);
4379
4380 self.inner
4386 .query_latency_telemetry
4387 .observe(kind, started.elapsed().as_secs_f64());
4388
4389 self.inner.node_load_telemetry.query_finish();
4393
4394 if let Ok(ref mut query_result) = result {
4395 if matches!(query_result.statement_type, "insert" | "update" | "delete") {
4396 let bookmark = crate::replication::CausalBookmark::new(
4397 self.current_replication_term(),
4398 self.cdc_current_lsn(),
4399 );
4400 query_result.bookmark = Some(bookmark.encode());
4401 }
4402 }
4403
4404 result
4405 }
4406
4407 fn execute_query_with_params_inner(
4408 &self,
4409 query: &str,
4410 params: &[Value],
4411 ) -> RedDBResult<RuntimeQueryResult> {
4412 let parsed = parse_multi(query).map_err(|err| RedDBError::Query(err.to_string()))?;
4413 let bound = crate::storage::query::user_params::bind(&parsed, params).map_err(|err| {
4414 RedDBError::Validation {
4415 message: err.to_string(),
4416 validation: crate::json!({
4417 "code": "INVALID_PARAMS",
4418 "surface": "query.params",
4419 }),
4420 }
4421 })?;
4422 self.execute_bound_query_expr_in_frame(query, bound)
4423 }
4424
4425 fn execute_bound_query_expr_in_frame(
4426 &self,
4427 query: &str,
4428 expr: QueryExpr,
4429 ) -> RedDBResult<RuntimeQueryResult> {
4430 let rewritten_query = super::red_schema::rewrite_virtual_names(query);
4431 let execution_query = rewritten_query.as_deref().unwrap_or(query);
4432 let frame = super::statement_frame::StatementExecutionFrame::build(self, execution_query)?;
4433 let _frame_guards = frame.install(self);
4434 let _log_span = crate::telemetry::span::query_span(query).entered();
4435
4436 let expr = self.rewrite_view_refs(expr);
4437 let mode = detect_mode(execution_query);
4438 let control_event_specs = query_control_event_specs(&expr);
4439 let _lock_guard = match frame.prepare_dispatch(self, &expr) {
4440 Ok(guard) => guard,
4441 Err(err) => {
4442 let outcome = control_event_outcome_for_error(&err);
4443 for spec in &control_event_specs {
4444 self.emit_control_event(
4445 spec.kind,
4446 outcome,
4447 spec.action,
4448 spec.resource.clone(),
4449 Some(err.to_string()),
4450 spec.fields.clone(),
4451 )?;
4452 }
4453 return Err(err);
4454 }
4455 };
4456
4457 let mut result = self.dispatch_expr(expr, query, mode)?;
4458 if result.statement_type == "select" {
4459 self.apply_secret_decryption(&mut result);
4460 }
4461 Ok(result)
4462 }
4463
4464 pub fn causal_session(&self) -> crate::runtime::CausalSession {
4465 crate::runtime::CausalSession {
4466 runtime: self.clone(),
4467 bookmark: None,
4468 wait_timeout: std::time::Duration::from_secs(5),
4469 }
4470 }
4471
4472 pub fn wait_for_bookmark(
4473 &self,
4474 bookmark: &crate::replication::CausalBookmark,
4475 timeout: std::time::Duration,
4476 ) -> RedDBResult<()> {
4477 let deadline = std::time::Instant::now() + timeout;
4478 loop {
4479 let applied_lsn = self.local_contiguous_applied_lsn();
4480 if applied_lsn >= bookmark.commit_lsn() {
4481 return Ok(());
4482 }
4483 let now = std::time::Instant::now();
4484 if now >= deadline {
4485 return Err(RedDBError::InvalidOperation(format!(
4486 "timed out waiting for causal bookmark lsn {}; applied={}",
4487 bookmark.commit_lsn(),
4488 applied_lsn
4489 )));
4490 }
4491 let remaining = deadline.saturating_duration_since(now);
4492 std::thread::sleep(remaining.min(std::time::Duration::from_millis(5)));
4493 }
4494 }
4495
4496 fn local_contiguous_applied_lsn(&self) -> u64 {
4497 match self.inner.db.options().replication.role {
4498 crate::replication::ReplicationRole::Replica { .. } => {
4499 self.config_u64("red.replication.last_applied_lsn", 0)
4500 }
4501 _ => self.cdc_current_lsn(),
4502 }
4503 }
4504
4505 #[inline(never)]
4506 fn execute_query_inner(&self, query: &str) -> RedDBResult<RuntimeQueryResult> {
4507 if !has_scope_override_active()
4518 && !query.trim_start().starts_with("WITHIN")
4519 && !query.trim_start().starts_with("within")
4520 && !self.inner.query_audit.has_rules()
4521 && !self
4522 .inner
4523 .tx_contexts
4524 .read()
4525 .contains_key(¤t_connection_id())
4526 {
4527 if let Some(result) = self.try_fast_entity_lookup(query) {
4528 return result;
4529 }
4530 }
4531
4532 match crate::runtime::within_clause::try_strip_within_prefix(query) {
4539 Ok(Some((scope, inner))) => {
4540 let _scope_guard = ScopeOverrideGuard::install(scope);
4541 return self.execute_query_inner(inner);
4546 }
4547 Ok(None) => {}
4548 Err(msg) => return Err(RedDBError::Query(msg)),
4549 }
4550
4551 if let Some(inner) = strip_explain_prefix(query) {
4558 return self.explain_as_rows(query, inner);
4559 }
4560
4561 if let Some(value) = parse_set_local_tenant(query)? {
4566 let conn_id = current_connection_id();
4567 if !self.inner.tx_contexts.read().contains_key(&conn_id) {
4568 return Err(RedDBError::Query(
4569 "SET LOCAL TENANT requires an active transaction".to_string(),
4570 ));
4571 }
4572 self.inner
4573 .tx_local_tenants
4574 .write()
4575 .insert(conn_id, value.clone());
4576 return Ok(RuntimeQueryResult::ok_message(
4577 query.to_string(),
4578 &match &value {
4579 Some(id) => format!("local tenant set: {id}"),
4580 None => "local tenant cleared".to_string(),
4581 },
4582 "set_local_tenant",
4583 ));
4584 }
4585
4586 if super::red_schema::is_system_schema_write(query) {
4587 return Err(RedDBError::Query(
4588 super::red_schema::READ_ONLY_ERROR.to_string(),
4589 ));
4590 }
4591
4592 if let Some(create_source) = super::analytics_source_catalog::parse_create_statement(query)?
4593 {
4594 return self.execute_create_analytics_source(query, create_source);
4595 }
4596
4597 if let Some(path) = super::metric_descriptor_catalog::parse_read_metric_statement(query) {
4603 return Err(super::metric_descriptor_catalog::read_output_unsupported(
4604 &path,
4605 ));
4606 }
4607
4608 if let Some(parsed) = super::ranking_descriptor_catalog::parse_create_ranking(query) {
4613 return self.execute_create_ranking(query, parsed?);
4614 }
4615 if super::ranking_descriptor_catalog::parse_show_rankings(query) {
4616 return self.execute_show_rankings(query);
4617 }
4618
4619 let rewritten_query = super::red_schema::rewrite_virtual_names(query);
4620 let execution_query = rewritten_query.as_deref().unwrap_or(query);
4621
4622 let frame = super::statement_frame::StatementExecutionFrame::build(self, execution_query)?;
4623 let _frame_guards = frame.install(self);
4624
4625 let _log_span = crate::telemetry::span::query_span(query).entered();
4632
4633 if let Some(rewritten) = frame.prepare_cte(execution_query)? {
4635 return self.execute_query_expr(rewritten);
4636 }
4637
4638 if !self.inner.query_audit.has_rules() {
4640 if let Some(result) = self.try_fast_entity_lookup(execution_query) {
4641 return result;
4642 }
4643 }
4644
4645 if !self.inner.query_audit.has_rules() {
4647 if let Some(result) = frame.read_result_cache(self) {
4648 return Ok(result);
4649 }
4650 }
4651
4652 let prepared = frame.prepare_statement(self, execution_query)?;
4653 let mode = prepared.mode;
4654 let expr = prepared.expr;
4655
4656 let statement = query_expr_name(&expr);
4657 let result_cache_scopes = query_expr_result_cache_scopes(&expr);
4658 let control_event_specs = query_control_event_specs(&expr);
4659 let query_audit_plan = query_audit_plan(&expr);
4660
4661 let _lock_guard = match frame.prepare_dispatch(self, &expr) {
4662 Ok(guard) => guard,
4663 Err(err) => {
4664 let outcome = control_event_outcome_for_error(&err);
4665 for spec in &control_event_specs {
4666 self.emit_control_event(
4667 spec.kind,
4668 outcome,
4669 spec.action,
4670 spec.resource.clone(),
4671 Some(err.to_string()),
4672 spec.fields.clone(),
4673 )?;
4674 }
4675 return Err(err);
4676 }
4677 };
4678 let frame_iface: &dyn super::statement_frame::ReadFrame = &frame;
4679 let query_audit_started = std::time::Instant::now();
4680
4681 let query_result = match expr {
4682 QueryExpr::Graph(_) | QueryExpr::Path(_) => {
4683 let (graph, node_properties, edge_properties) =
4691 self.materialize_graph_with_rls()?;
4692 let result =
4693 crate::storage::query::unified::UnifiedExecutor::execute_on_with_graph_properties(
4694 &graph,
4695 &expr,
4696 node_properties,
4697 edge_properties,
4698 )
4699 .map_err(|err| RedDBError::Query(err.to_string()))?;
4700
4701 Ok(RuntimeQueryResult {
4702 query: query.to_string(),
4703 mode,
4704 statement,
4705 engine: "materialized-graph",
4706 result,
4707 affected_rows: 0,
4708 statement_type: "select",
4709 bookmark: None,
4710 })
4711 }
4712 QueryExpr::Table(table) => {
4713 let table = self.resolve_table_expr_subqueries(
4714 table,
4715 &frame as &dyn super::statement_frame::ReadFrame,
4716 )?;
4717 if let Some(TableSource::Function {
4721 name,
4722 args,
4723 named_args,
4724 }) = table.source.clone()
4725 {
4726 let tvf_result = RuntimeQueryResult {
4734 query: query.to_string(),
4735 mode,
4736 statement,
4737 engine: "runtime-graph-tvf",
4738 result: self.execute_table_function(&name, &args, &named_args)?,
4739 affected_rows: 0,
4740 statement_type: "select",
4741 bookmark: None,
4742 };
4743 frame.write_result_cache(self, &tvf_result, result_cache_scopes.clone());
4744 return Ok(tvf_result);
4745 }
4746 if let Some(TableSource::InlineGraphFunction {
4754 name,
4755 nodes,
4756 edges,
4757 named_args,
4758 }) = table.source.clone()
4759 {
4760 let inline_result = RuntimeQueryResult {
4761 query: query.to_string(),
4762 mode,
4763 statement,
4764 engine: "runtime-graph-tvf-inline",
4765 result: self.execute_inline_graph_function(
4766 &name,
4767 &nodes,
4768 &edges,
4769 &named_args,
4770 )?,
4771 affected_rows: 0,
4772 statement_type: "select",
4773 bookmark: None,
4774 };
4775 frame.write_result_cache(self, &inline_result, result_cache_scopes);
4776 return Ok(inline_result);
4777 }
4778 if super::red_schema::is_virtual_table(&table.table) {
4779 return Ok(RuntimeQueryResult {
4780 query: query.to_string(),
4781 mode,
4782 statement,
4783 engine: "runtime-red-schema",
4784 result: super::red_schema::red_query(
4785 self,
4786 &table.table,
4787 &table,
4788 &frame as &dyn super::statement_frame::ReadFrame,
4789 )?,
4790 affected_rows: 0,
4791 statement_type: "select",
4792 bookmark: None,
4793 });
4794 }
4795
4796 if let Some(view_result) = self.try_resolve_analytics_view(
4800 &table,
4801 &frame as &dyn super::statement_frame::ReadFrame,
4802 )? {
4803 return Ok(RuntimeQueryResult {
4804 query: query.to_string(),
4805 mode,
4806 statement,
4807 engine: "runtime-graph-analytics-view",
4808 result: view_result,
4809 affected_rows: 0,
4810 statement_type: "select",
4811 bookmark: None,
4812 });
4813 }
4814
4815 if let Some(result) = self.execute_probabilistic_select(&table)? {
4816 return Ok(RuntimeQueryResult {
4817 query: query.to_string(),
4818 mode,
4819 statement,
4820 engine: "runtime-probabilistic",
4821 result,
4822 affected_rows: 0,
4823 statement_type: "select",
4824 bookmark: None,
4825 });
4826 }
4827
4828 if self.inner.foreign_tables.is_foreign_table(&table.table) {
4836 let records = self
4837 .inner
4838 .foreign_tables
4839 .scan(&table.table)
4840 .map_err(|e| RedDBError::Internal(e.to_string()))?;
4841 let result = apply_foreign_table_filters(records, &table);
4842 return Ok(RuntimeQueryResult {
4843 query: query.to_string(),
4844 mode,
4845 statement,
4846 engine: "runtime-fdw",
4847 result,
4848 affected_rows: 0,
4849 statement_type: "select",
4850 bookmark: None,
4851 });
4852 }
4853
4854 let Some(table_with_rls) = self.authorize_relational_table_select(
4871 table,
4872 &frame as &dyn super::statement_frame::ReadFrame,
4873 )?
4874 else {
4875 let empty = crate::storage::query::unified::UnifiedResult::empty();
4876 return Ok(RuntimeQueryResult {
4877 query: query.to_string(),
4878 mode,
4879 statement,
4880 engine: "runtime-table-rls",
4881 result: empty,
4882 affected_rows: 0,
4883 statement_type: "select",
4884 bookmark: None,
4885 });
4886 };
4887 Ok(RuntimeQueryResult {
4888 query: query.to_string(),
4889 mode,
4890 statement,
4891 engine: "runtime-table",
4892 result: execute_runtime_table_query_in(
4899 &self.inner.db,
4900 &table_with_rls,
4901 Some(&self.inner.index_store),
4902 Some(frame.row_arena()),
4903 )?,
4904 affected_rows: 0,
4905 statement_type: "select",
4906 bookmark: None,
4907 })
4908 }
4909 QueryExpr::Join(join) => {
4910 let join_with_rls = match self.authorize_relational_join_select(
4919 join,
4920 &frame as &dyn super::statement_frame::ReadFrame,
4921 )? {
4922 Some(j) => j,
4923 None => {
4924 return Ok(RuntimeQueryResult {
4925 query: query.to_string(),
4926 mode,
4927 statement,
4928 engine: "runtime-join-rls",
4929 result: crate::storage::query::unified::UnifiedResult::empty(),
4930 affected_rows: 0,
4931 statement_type: "select",
4932 bookmark: None,
4933 });
4934 }
4935 };
4936 Ok(RuntimeQueryResult {
4937 query: query.to_string(),
4938 mode,
4939 statement,
4940 engine: "runtime-join",
4941 result: execute_runtime_join_query(&self.inner.db, &join_with_rls)?,
4942 affected_rows: 0,
4943 statement_type: "select",
4944 bookmark: None,
4945 })
4946 }
4947 QueryExpr::Vector(vector) => Ok(RuntimeQueryResult {
4948 query: query.to_string(),
4949 mode,
4950 statement,
4951 engine: "runtime-vector",
4952 result: execute_runtime_vector_query(&self.inner.db, &vector)?,
4953 affected_rows: 0,
4954 statement_type: "select",
4955 bookmark: None,
4956 }),
4957 QueryExpr::Hybrid(hybrid) => Ok(RuntimeQueryResult {
4958 query: query.to_string(),
4959 mode,
4960 statement,
4961 engine: "runtime-hybrid",
4962 result: execute_runtime_hybrid_query(&self.inner.db, &hybrid)?,
4963 affected_rows: 0,
4964 statement_type: "select",
4965 bookmark: None,
4966 }),
4967 QueryExpr::RankOf(ref rank) => self.execute_rank_of(query, rank),
4968 QueryExpr::ApproxRankOf(ref rank) => self.execute_approx_rank_of(query, rank),
4969 QueryExpr::RankRange(ref range) => self.execute_rank_range(query, range),
4970 QueryExpr::Insert(ref insert) if super::red_schema::is_virtual_table(&insert.table) => {
4972 Err(RedDBError::Query(
4973 super::red_schema::READ_ONLY_ERROR.to_string(),
4974 ))
4975 }
4976 QueryExpr::Update(ref update) if super::red_schema::is_virtual_table(&update.table) => {
4977 Err(RedDBError::Query(
4978 super::red_schema::READ_ONLY_ERROR.to_string(),
4979 ))
4980 }
4981 QueryExpr::Delete(ref delete) if super::red_schema::is_virtual_table(&delete.table) => {
4982 Err(RedDBError::Query(
4983 super::red_schema::READ_ONLY_ERROR.to_string(),
4984 ))
4985 }
4986 QueryExpr::Insert(ref insert) => self
4987 .with_deferred_store_wal_for_dml(self.insert_may_emit_events(insert), || {
4988 self.execute_insert(query, insert)
4989 }),
4990 QueryExpr::Update(ref update) => self
4991 .with_deferred_store_wal_for_dml(self.update_may_emit_events(update), || {
4992 self.execute_update(query, update)
4993 }),
4994 QueryExpr::Delete(ref delete) => self
4995 .with_deferred_store_wal_for_dml(self.delete_may_emit_events(delete), || {
4996 self.execute_delete(query, delete)
4997 }),
4998 QueryExpr::CreateTable(ref create) => self.execute_create_table(query, create),
5000 QueryExpr::CreateCollection(ref create) => {
5001 self.execute_create_collection(query, create)
5002 }
5003 QueryExpr::CreateVector(ref create) => self.execute_create_vector(query, create),
5004 QueryExpr::DropTable(ref drop_tbl) => self.execute_drop_table(query, drop_tbl),
5005 QueryExpr::DropGraph(ref drop_graph) => self.execute_drop_graph(query, drop_graph),
5006 QueryExpr::DropVector(ref drop_vector) => self.execute_drop_vector(query, drop_vector),
5007 QueryExpr::DropDocument(ref drop_document) => {
5008 self.execute_drop_document(query, drop_document)
5009 }
5010 QueryExpr::DropKv(ref drop_kv) => self.execute_drop_kv(query, drop_kv),
5011 QueryExpr::DropCollection(ref drop_collection) => {
5012 self.execute_drop_collection(query, drop_collection)
5013 }
5014 QueryExpr::Truncate(ref truncate) => self.execute_truncate(query, truncate),
5015 QueryExpr::AlterTable(ref alter) => self.execute_alter_table(query, alter),
5016 QueryExpr::ExplainAlter(ref explain) => self.execute_explain_alter(query, explain),
5017 QueryExpr::GraphCommand(ref cmd) => self.execute_graph_command(query, cmd),
5019 QueryExpr::SearchCommand(ref cmd) => self.execute_search_command(query, cmd),
5021 QueryExpr::Ask(ref ask) => self.execute_ask(query, ask),
5023 QueryExpr::CreateIndex(ref create_idx) => self.execute_create_index(query, create_idx),
5024 QueryExpr::DropIndex(ref drop_idx) => self.execute_drop_index(query, drop_idx),
5025 QueryExpr::ProbabilisticCommand(ref cmd) => {
5026 self.execute_probabilistic_command(query, cmd)
5027 }
5028 QueryExpr::CreateTimeSeries(ref ts) => self.execute_create_timeseries(query, ts),
5030 QueryExpr::CreateMetric(ref metric) => self.execute_create_metric(query, metric),
5031 QueryExpr::AlterMetric(ref alter) => self.execute_alter_metric(query, alter),
5032 QueryExpr::CreateSlo(ref slo) => self.execute_create_slo(query, slo),
5033 QueryExpr::DropTimeSeries(ref ts) => self.execute_drop_timeseries(query, ts),
5034 QueryExpr::CreateQueue(ref q) => self.execute_create_queue(query, q),
5036 QueryExpr::AlterQueue(ref q) => self.execute_alter_queue(query, q),
5037 QueryExpr::DropQueue(ref q) => self.execute_drop_queue(query, q),
5038 QueryExpr::QueueSelect(ref q) => self.execute_queue_select(query, q),
5039 QueryExpr::QueueCommand(ref cmd) => self.execute_queue_command(query, cmd),
5040 QueryExpr::EventsBackfill(ref backfill) => {
5041 self.execute_events_backfill(query, backfill)
5042 }
5043 QueryExpr::EventsBackfillStatus { ref collection } => Err(RedDBError::Query(format!(
5044 "EVENTS BACKFILL STATUS for '{collection}' is not implemented in this slice"
5045 ))),
5046 QueryExpr::KvCommand(ref cmd) => self.execute_kv_command(query, cmd),
5047 QueryExpr::ConfigCommand(ref cmd) => self.execute_config_command(query, cmd),
5048 QueryExpr::CreateTree(ref tree) => self.execute_create_tree(query, tree),
5049 QueryExpr::DropTree(ref tree) => self.execute_drop_tree(query, tree),
5050 QueryExpr::TreeCommand(ref cmd) => self.execute_tree_command(query, cmd),
5051 QueryExpr::SetConfig { ref key, ref value } => {
5053 if key.starts_with("red.secret.") {
5054 return Err(RedDBError::Query(
5055 "red.secret.* is reserved for vault secrets; use SET SECRET".to_string(),
5056 ));
5057 }
5058 if key.starts_with("red.secrets.") {
5059 return Err(RedDBError::Query(
5060 "red.secrets.* is reserved for vault secrets; use SET SECRET".to_string(),
5061 ));
5062 }
5063 match self.check_managed_config_write_for_set_config(key) {
5064 Err(err) => Err(err),
5065 Ok(()) => {
5066 let store = self.inner.db.store();
5067 let json_val = match value {
5068 Value::Text(s) => crate::serde_json::Value::String(s.to_string()),
5069 Value::Integer(n) => crate::serde_json::Value::Number(*n as f64),
5070 Value::Float(n) => crate::serde_json::Value::Number(*n),
5071 Value::Boolean(b) => crate::serde_json::Value::Bool(*b),
5072 _ => crate::serde_json::Value::String(value.to_string()),
5073 };
5074 store.set_config_tree(key, &json_val);
5075 update_current_config_value(key, value.clone());
5076 self.invalidate_result_cache();
5081 Ok(RuntimeQueryResult::ok_message(
5082 query.to_string(),
5083 &format!("config set: {key}"),
5084 "set",
5085 ))
5086 }
5087 }
5088 }
5089 QueryExpr::SetSecret { ref key, ref value } => {
5091 if key.starts_with("red.config.") {
5092 return Err(RedDBError::Query(
5093 "red.config.* is reserved for config; use SET CONFIG".to_string(),
5094 ));
5095 }
5096 let auth_store = self.inner.auth_store.read().clone().ok_or_else(|| {
5097 RedDBError::Query("SET SECRET requires an enabled, unsealed vault".to_string())
5098 })?;
5099 if matches!(value, Value::Null) {
5100 auth_store
5101 .vault_kv_try_delete(key)
5102 .map_err(|err| RedDBError::Query(err.to_string()))?;
5103 update_current_secret_value(key, None);
5104 self.invalidate_result_cache();
5105 return Ok(RuntimeQueryResult::ok_message(
5106 query.to_string(),
5107 &format!("secret deleted: {key}"),
5108 "delete_secret",
5109 ));
5110 }
5111 let value = secret_sql_value_to_string(value)?;
5112 auth_store
5113 .vault_kv_try_set(key.clone(), value.clone())
5114 .map_err(|err| RedDBError::Query(err.to_string()))?;
5115 update_current_secret_value(key, Some(value));
5116 self.invalidate_result_cache();
5117 Ok(RuntimeQueryResult::ok_message(
5118 query.to_string(),
5119 &format!("secret set: {key}"),
5120 "set_secret",
5121 ))
5122 }
5123 QueryExpr::DeleteSecret { ref key } => {
5125 let auth_store = self.inner.auth_store.read().clone().ok_or_else(|| {
5126 RedDBError::Query(
5127 "DELETE SECRET requires an enabled, unsealed vault".to_string(),
5128 )
5129 })?;
5130 let deleted = auth_store
5131 .vault_kv_try_delete(key)
5132 .map_err(|err| RedDBError::Query(err.to_string()))?;
5133 if deleted {
5134 update_current_secret_value(key, None);
5135 }
5136 self.invalidate_result_cache();
5137 Ok(RuntimeQueryResult::ok_message(
5138 query.to_string(),
5139 &format!("secret deleted: {key}"),
5140 if deleted {
5141 "delete_secret"
5142 } else {
5143 "delete_secret_not_found"
5144 },
5145 ))
5146 }
5147 QueryExpr::ShowSecrets { ref prefix } => {
5149 let auth_store = self.inner.auth_store.read().clone().ok_or_else(|| {
5150 RedDBError::Query("SHOW SECRET requires an enabled, unsealed vault".to_string())
5151 })?;
5152 if !auth_store.is_vault_backed() {
5153 return Err(RedDBError::Query(
5154 "SHOW SECRET requires an enabled, unsealed vault".to_string(),
5155 ));
5156 }
5157 let mut keys = auth_store.vault_kv_keys();
5158 keys.sort();
5159 let mut result = UnifiedResult::with_columns(vec![
5160 "key".into(),
5161 "value".into(),
5162 "status".into(),
5163 ]);
5164 for key in keys {
5165 if let Some(ref pfx) = prefix {
5166 if !key.starts_with(pfx) {
5167 continue;
5168 }
5169 }
5170 let mut record = UnifiedRecord::new();
5171 record.set("key", Value::text(key));
5172 record.set("value", Value::text("***"));
5173 record.set("status", Value::text("active"));
5174 result.push(record);
5175 }
5176 Ok(RuntimeQueryResult {
5177 query: query.to_string(),
5178 mode,
5179 statement: "show_secrets",
5180 engine: "runtime-secret",
5181 result,
5182 affected_rows: 0,
5183 statement_type: "select",
5184 bookmark: None,
5185 })
5186 }
5187 QueryExpr::ShowConfig {
5189 ref prefix,
5190 as_json,
5191 } => {
5192 let store = self.inner.db.store();
5193 let all_collections = store.list_collections();
5194 if !all_collections.contains(&"red_config".to_string()) {
5195 if as_json {
5196 return Ok(show_config_json_result(
5197 query,
5198 mode,
5199 prefix,
5200 crate::serde_json::Value::Object(crate::serde_json::Map::new()),
5201 ));
5202 }
5203 let result = UnifiedResult::with_columns(vec!["key".into(), "value".into()]);
5204 return Ok(RuntimeQueryResult {
5205 query: query.to_string(),
5206 mode,
5207 statement: "show_config",
5208 engine: "runtime-config",
5209 result,
5210 affected_rows: 0,
5211 statement_type: "select",
5212 bookmark: None,
5213 });
5214 }
5215 let manager = store
5216 .get_collection("red_config")
5217 .ok_or_else(|| RedDBError::NotFound("red_config".to_string()))?;
5218 let entities = manager.query_all(|_| true);
5219 let mut latest = std::collections::BTreeMap::<String, (u64, Value, Value)>::new();
5220 for entity in entities {
5221 if let EntityData::Row(ref row) = entity.data {
5222 if let Some(ref named) = row.named {
5223 let key_val = named.get("key").cloned().unwrap_or(Value::Null);
5224 let val = named.get("value").cloned().unwrap_or(Value::Null);
5225 let key_str = match &key_val {
5226 Value::Text(s) => s.as_ref(),
5227 _ => continue,
5228 };
5229 if let Some(ref pfx) = prefix {
5230 if !key_str.starts_with(pfx.as_str()) {
5231 continue;
5232 }
5233 }
5234 let entity_id = entity.id.raw();
5235 match latest.get(key_str) {
5236 Some((prev_id, _, _)) if *prev_id > entity_id => {}
5237 _ => {
5238 latest.insert(key_str.to_string(), (entity_id, key_val, val));
5239 }
5240 }
5241 }
5242 }
5243 }
5244 if as_json {
5245 let mut tree = crate::serde_json::Value::Object(crate::serde_json::Map::new());
5246 for (key, (_, _, val)) in latest {
5247 let relative = match prefix {
5248 Some(pfx) if key == *pfx => "",
5249 Some(pfx) => key
5250 .strip_prefix(pfx.as_str())
5251 .and_then(|tail| tail.strip_prefix('.'))
5252 .unwrap_or(key.as_str()),
5253 None => key.as_str(),
5254 };
5255 insert_config_json_path(
5256 &mut tree,
5257 relative,
5258 crate::presentation::entity_json::storage_value_to_json(&val),
5259 );
5260 }
5261 return Ok(show_config_json_result(query, mode, prefix, tree));
5262 }
5263 let mut result = UnifiedResult::with_columns(vec!["key".into(), "value".into()]);
5264 for (_, key_val, val) in latest.into_values() {
5265 let mut record = UnifiedRecord::new();
5266 record.set("key", key_val);
5267 record.set("value", val);
5268 result.push(record);
5269 }
5270 Ok(RuntimeQueryResult {
5271 query: query.to_string(),
5272 mode,
5273 statement: "show_config",
5274 engine: "runtime-config",
5275 result,
5276 affected_rows: 0,
5277 statement_type: "select",
5278 bookmark: None,
5279 })
5280 }
5281 QueryExpr::SetTenant(ref value) => {
5287 match value {
5288 Some(id) => set_current_tenant(id.clone()),
5289 None => clear_current_tenant(),
5290 }
5291 Ok(RuntimeQueryResult::ok_message(
5292 query.to_string(),
5293 &match value {
5294 Some(id) => format!("tenant set: {id}"),
5295 None => "tenant cleared".to_string(),
5296 },
5297 "set_tenant",
5298 ))
5299 }
5300 QueryExpr::ShowTenant => {
5301 let mut result = UnifiedResult::with_columns(vec!["tenant".into()]);
5302 let mut record = UnifiedRecord::new();
5303 record.set(
5304 "tenant",
5305 current_tenant().map(Value::text).unwrap_or(Value::Null),
5306 );
5307 result.push(record);
5308 Ok(RuntimeQueryResult {
5309 query: query.to_string(),
5310 mode,
5311 statement: "show_tenant",
5312 engine: "runtime-tenant",
5313 result,
5314 affected_rows: 0,
5315 statement_type: "select",
5316 bookmark: None,
5317 })
5318 }
5319 QueryExpr::TransactionControl(ref ctl) => {
5331 use crate::storage::query::ast::TxnControl;
5332 use crate::storage::transaction::snapshot::{TxnContext, Xid};
5333 use crate::storage::transaction::IsolationLevel;
5334
5335 let conn_id = current_connection_id();
5340
5341 let (kind, msg) = match ctl {
5342 TxnControl::Begin => {
5343 let mgr = Arc::clone(&self.inner.snapshot_manager);
5344 let xid = mgr.begin();
5345 let snapshot = mgr.snapshot(xid);
5346 let ctx = TxnContext {
5347 xid,
5348 isolation: IsolationLevel::SnapshotIsolation,
5349 snapshot,
5350 savepoints: Vec::new(),
5351 released_sub_xids: Vec::new(),
5352 };
5353 self.inner.tx_contexts.write().insert(conn_id, ctx);
5354 ("begin", format!("BEGIN — xid={xid} (snapshot isolation)"))
5355 }
5356 TxnControl::Commit => {
5357 self.inner.tx_local_tenants.write().remove(&conn_id);
5359 let ctx = self.inner.tx_contexts.write().remove(&conn_id);
5360 match ctx {
5361 Some(ctx) => {
5362 let mut own_xids = std::collections::HashSet::new();
5363 own_xids.insert(ctx.xid);
5364 for (_, sub) in &ctx.savepoints {
5365 own_xids.insert(*sub);
5366 }
5367 for sub in &ctx.released_sub_xids {
5368 own_xids.insert(*sub);
5369 }
5370 if let Err(err) = self.check_table_row_write_conflicts(
5371 conn_id,
5372 &ctx.snapshot,
5373 &own_xids,
5374 ) {
5375 for (_, sub) in &ctx.savepoints {
5376 self.inner.snapshot_manager.rollback(*sub);
5377 }
5378 for sub in &ctx.released_sub_xids {
5379 self.inner.snapshot_manager.rollback(*sub);
5380 }
5381 self.inner.snapshot_manager.rollback(ctx.xid);
5382 self.revive_pending_versioned_updates(conn_id);
5383 self.revive_pending_tombstones(conn_id);
5384 self.discard_pending_kv_watch_events(conn_id);
5385 self.discard_pending_queue_wakes(conn_id);
5386 self.discard_pending_store_wal_actions(conn_id);
5387 self.release_pending_claim_locks(conn_id);
5388 return Err(err);
5389 }
5390 self.restore_pending_write_stamps(conn_id);
5391 if let Err(err) = self.flush_pending_store_wal_actions(conn_id) {
5392 for (_, sub) in &ctx.savepoints {
5393 self.inner.snapshot_manager.rollback(*sub);
5394 }
5395 for sub in &ctx.released_sub_xids {
5396 self.inner.snapshot_manager.rollback(*sub);
5397 }
5398 self.inner.snapshot_manager.rollback(ctx.xid);
5399 self.revive_pending_versioned_updates(conn_id);
5400 self.revive_pending_tombstones(conn_id);
5401 self.discard_pending_kv_watch_events(conn_id);
5402 self.discard_pending_queue_wakes(conn_id);
5403 self.release_pending_claim_locks(conn_id);
5404 return Err(err);
5405 }
5406 for (_, sub) in &ctx.savepoints {
5412 self.inner.snapshot_manager.commit(*sub);
5413 }
5414 for sub in &ctx.released_sub_xids {
5415 self.inner.snapshot_manager.commit(*sub);
5416 }
5417 self.inner.snapshot_manager.commit(ctx.xid);
5418 self.finalize_pending_versioned_updates(conn_id);
5419 self.finalize_pending_tombstones(conn_id);
5420 self.finalize_pending_kv_watch_events(conn_id);
5421 self.finalize_pending_queue_wakes(conn_id);
5422 self.release_pending_claim_locks(conn_id);
5423 ("commit", format!("COMMIT — xid={} committed", ctx.xid))
5424 }
5425 None => (
5426 "commit",
5427 "COMMIT outside transaction — no-op (autocommit)".to_string(),
5428 ),
5429 }
5430 }
5431 TxnControl::Rollback => {
5432 self.inner.tx_local_tenants.write().remove(&conn_id);
5433 let ctx = self.inner.tx_contexts.write().remove(&conn_id);
5434 match ctx {
5435 Some(ctx) => {
5436 for (_, sub) in &ctx.savepoints {
5439 self.inner.snapshot_manager.rollback(*sub);
5440 }
5441 for sub in &ctx.released_sub_xids {
5442 self.inner.snapshot_manager.rollback(*sub);
5443 }
5444 self.inner.snapshot_manager.rollback(ctx.xid);
5445 self.revive_pending_versioned_updates(conn_id);
5449 self.revive_pending_tombstones(conn_id);
5450 self.discard_pending_kv_watch_events(conn_id);
5451 self.discard_pending_queue_wakes(conn_id);
5452 self.discard_pending_store_wal_actions(conn_id);
5453 self.release_pending_claim_locks(conn_id);
5454 ("rollback", format!("ROLLBACK — xid={} aborted", ctx.xid))
5455 }
5456 None => (
5457 "rollback",
5458 "ROLLBACK outside transaction — no-op (autocommit)".to_string(),
5459 ),
5460 }
5461 }
5462 TxnControl::Savepoint(name) => {
5469 let mgr = Arc::clone(&self.inner.snapshot_manager);
5470 let mut guard = self.inner.tx_contexts.write();
5471 match guard.get_mut(&conn_id) {
5472 Some(ctx) => {
5473 let sub = mgr.begin();
5474 ctx.savepoints.push((name.clone(), sub));
5475 ("savepoint", format!("SAVEPOINT {name} — sub_xid={sub}"))
5476 }
5477 None => (
5478 "savepoint",
5479 "SAVEPOINT outside transaction — no-op".to_string(),
5480 ),
5481 }
5482 }
5483 TxnControl::ReleaseSavepoint(name) => {
5484 let mut guard = self.inner.tx_contexts.write();
5485 match guard.get_mut(&conn_id) {
5486 Some(ctx) => {
5487 let pos = ctx
5488 .savepoints
5489 .iter()
5490 .position(|(n, _)| n == name)
5491 .ok_or_else(|| {
5492 RedDBError::Internal(format!(
5493 "savepoint {name} does not exist"
5494 ))
5495 })?;
5496 let released = ctx.savepoints.len() - pos;
5504 let popped: Vec<Xid> = ctx
5505 .savepoints
5506 .split_off(pos)
5507 .into_iter()
5508 .map(|(_, x)| x)
5509 .collect();
5510 ctx.released_sub_xids.extend(popped);
5511 (
5512 "release_savepoint",
5513 format!("RELEASE SAVEPOINT {name} — {released} level(s)"),
5514 )
5515 }
5516 None => (
5517 "release_savepoint",
5518 "RELEASE outside transaction — no-op".to_string(),
5519 ),
5520 }
5521 }
5522 TxnControl::RollbackToSavepoint(name) => {
5523 let mgr = Arc::clone(&self.inner.snapshot_manager);
5524 let drop_result: Option<(Xid, Vec<Xid>)> = {
5529 let mut guard = self.inner.tx_contexts.write();
5530 if let Some(ctx) = guard.get_mut(&conn_id) {
5531 let pos = ctx
5532 .savepoints
5533 .iter()
5534 .position(|(n, _)| n == name)
5535 .ok_or_else(|| {
5536 RedDBError::Internal(format!(
5537 "savepoint {name} does not exist"
5538 ))
5539 })?;
5540 let savepoint_xid = ctx.savepoints[pos].1;
5541 let aborted: Vec<Xid> = ctx
5542 .savepoints
5543 .split_off(pos)
5544 .into_iter()
5545 .map(|(_, x)| x)
5546 .collect();
5547 Some((savepoint_xid, aborted))
5548 } else {
5549 None
5550 }
5551 };
5552
5553 match drop_result {
5554 Some((savepoint_xid, aborted)) => {
5555 for x in &aborted {
5556 mgr.rollback(*x);
5557 }
5558 let reverted_updates =
5559 self.revive_versioned_updates_since(conn_id, savepoint_xid);
5560 let revived = self.revive_tombstones_since(conn_id, savepoint_xid);
5561 (
5562 "rollback_to_savepoint",
5563 format!(
5564 "ROLLBACK TO SAVEPOINT {name} — aborted {} sub_xid(s), reverted {reverted_updates} update(s), revived {revived} tombstone(s)",
5565 aborted.len(),
5566 ),
5567 )
5568 }
5569 None => (
5570 "rollback_to_savepoint",
5571 "ROLLBACK TO outside transaction — no-op".to_string(),
5572 ),
5573 }
5574 }
5575 };
5576 Ok(RuntimeQueryResult::ok_message(
5577 query.to_string(),
5578 &msg,
5579 kind,
5580 ))
5581 }
5582 QueryExpr::CreateSchema(ref q) => {
5595 let store = self.inner.db.store();
5596 let key = format!("schema.{}", q.name);
5597 if store.get_config(&key).is_some() {
5598 if q.if_not_exists {
5599 return Ok(RuntimeQueryResult::ok_message(
5600 query.to_string(),
5601 &format!("schema {} already exists — skipped", q.name),
5602 "create_schema",
5603 ));
5604 }
5605 return Err(RedDBError::Internal(format!(
5606 "schema {} already exists",
5607 q.name
5608 )));
5609 }
5610 store.set_config_tree(&key, &crate::serde_json::Value::Bool(true));
5611 Ok(RuntimeQueryResult::ok_message(
5612 query.to_string(),
5613 &format!("schema {} created", q.name),
5614 "create_schema",
5615 ))
5616 }
5617 QueryExpr::DropSchema(ref q) => {
5618 let store = self.inner.db.store();
5619 let key = format!("schema.{}", q.name);
5620 let existed = store.get_config(&key).is_some();
5621 if !existed && !q.if_exists {
5622 return Err(RedDBError::Internal(format!(
5623 "schema {} does not exist",
5624 q.name
5625 )));
5626 }
5627 store.set_config_tree(&key, &crate::serde_json::Value::Null);
5629 let suffix = if q.cascade {
5630 " (CASCADE accepted — tables untouched)"
5631 } else {
5632 ""
5633 };
5634 Ok(RuntimeQueryResult::ok_message(
5635 query.to_string(),
5636 &format!("schema {} dropped{}", q.name, suffix),
5637 "drop_schema",
5638 ))
5639 }
5640 QueryExpr::CreateSequence(ref q) => {
5641 let store = self.inner.db.store();
5642 let base = format!("sequence.{}", q.name);
5643 let start_key = format!("{base}.start");
5644 let incr_key = format!("{base}.increment");
5645 let curr_key = format!("{base}.current");
5646 if store.get_config(&start_key).is_some() {
5647 if q.if_not_exists {
5648 return Ok(RuntimeQueryResult::ok_message(
5649 query.to_string(),
5650 &format!("sequence {} already exists — skipped", q.name),
5651 "create_sequence",
5652 ));
5653 }
5654 return Err(RedDBError::Internal(format!(
5655 "sequence {} already exists",
5656 q.name
5657 )));
5658 }
5659 let initial_current = q.start - q.increment;
5662 store.set_config_tree(
5663 &start_key,
5664 &crate::serde_json::Value::Number(q.start as f64),
5665 );
5666 store.set_config_tree(
5667 &incr_key,
5668 &crate::serde_json::Value::Number(q.increment as f64),
5669 );
5670 store.set_config_tree(
5671 &curr_key,
5672 &crate::serde_json::Value::Number(initial_current as f64),
5673 );
5674 Ok(RuntimeQueryResult::ok_message(
5675 query.to_string(),
5676 &format!(
5677 "sequence {} created (start={}, increment={})",
5678 q.name, q.start, q.increment
5679 ),
5680 "create_sequence",
5681 ))
5682 }
5683 QueryExpr::DropSequence(ref q) => {
5684 let store = self.inner.db.store();
5685 let base = format!("sequence.{}", q.name);
5686 let existed = store.get_config(&format!("{base}.start")).is_some();
5687 if !existed && !q.if_exists {
5688 return Err(RedDBError::Internal(format!(
5689 "sequence {} does not exist",
5690 q.name
5691 )));
5692 }
5693 for k in ["start", "increment", "current"] {
5694 store.set_config_tree(&format!("{base}.{k}"), &crate::serde_json::Value::Null);
5695 }
5696 Ok(RuntimeQueryResult::ok_message(
5697 query.to_string(),
5698 &format!("sequence {} dropped", q.name),
5699 "drop_sequence",
5700 ))
5701 }
5702 QueryExpr::CreateView(ref q) => {
5712 let mut views = self.inner.views.write();
5713 if views.contains_key(&q.name) && !q.or_replace {
5714 if q.if_not_exists {
5715 return Ok(RuntimeQueryResult::ok_message(
5716 query.to_string(),
5717 &format!("view {} already exists — skipped", q.name),
5718 "create_view",
5719 ));
5720 }
5721 return Err(RedDBError::Internal(format!(
5722 "view {} already exists",
5723 q.name
5724 )));
5725 }
5726 views.insert(q.name.clone(), Arc::new(q.clone()));
5727 drop(views);
5728
5729 if q.materialized {
5731 use crate::storage::cache::result::{MaterializedViewDef, RefreshPolicy};
5732 let refresh = match q.refresh_every_ms {
5733 Some(ms) => RefreshPolicy::Periodic(std::time::Duration::from_millis(ms)),
5734 None => RefreshPolicy::Manual,
5735 };
5736 let dependencies = collect_table_refs(&q.query);
5737 let def = MaterializedViewDef {
5738 name: q.name.clone(),
5739 query: format!("<parsed view {}>", q.name),
5740 dependencies: dependencies.clone(),
5741 refresh,
5742 retention_duration_ms: q.retention_duration_ms,
5743 };
5744 self.inner.materialized_views.write().register(def);
5745
5746 let descriptor =
5752 crate::runtime::continuous_materialized_view::MaterializedViewDescriptor {
5753 name: q.name.clone(),
5754 source_sql: query.to_string(),
5755 source_collections: dependencies,
5756 refresh_every_ms: q.refresh_every_ms,
5757 retention_duration_ms: q.retention_duration_ms,
5758 };
5759 let store = self.inner.db.store();
5760 crate::runtime::continuous_materialized_view::persist_descriptor(
5761 store.as_ref(),
5762 &descriptor,
5763 )?;
5764
5765 self.ensure_materialized_view_backing(&q.name)?;
5772 }
5773 self.invalidate_plan_cache();
5778 self.invalidate_result_cache();
5779
5780 Ok(RuntimeQueryResult::ok_message(
5781 query.to_string(),
5782 &format!(
5783 "{}view {} created",
5784 if q.materialized { "materialized " } else { "" },
5785 q.name
5786 ),
5787 "create_view",
5788 ))
5789 }
5790 QueryExpr::DropView(ref q) => {
5791 let mut views = self.inner.views.write();
5792 let removed = views.remove(&q.name);
5793 let existed = removed.is_some();
5794 let removed_materialized =
5795 removed.as_ref().map(|v| v.materialized).unwrap_or(false);
5796 drop(views);
5797 if q.materialized || existed {
5798 self.inner.materialized_views.write().remove(&q.name);
5800 let store = self.inner.db.store();
5804 crate::runtime::continuous_materialized_view::remove_by_name(
5805 store.as_ref(),
5806 &q.name,
5807 )?;
5808 }
5809 if removed_materialized || q.materialized {
5813 self.drop_materialized_view_backing(&q.name)?;
5814 }
5815 self.invalidate_plan_cache();
5818 self.invalidate_result_cache();
5819 if !existed && !q.if_exists {
5820 return Err(RedDBError::Internal(format!(
5821 "view {} does not exist",
5822 q.name
5823 )));
5824 }
5825 self.invalidate_plan_cache();
5826 Ok(RuntimeQueryResult::ok_message(
5827 query.to_string(),
5828 &format!("view {} dropped", q.name),
5829 "drop_view",
5830 ))
5831 }
5832 QueryExpr::RefreshMaterializedView(ref q) => {
5833 let view = {
5836 let views = self.inner.views.read();
5837 views.get(&q.name).cloned()
5838 };
5839 let view = match view {
5840 Some(v) => v,
5841 None => {
5842 return Err(RedDBError::Internal(format!(
5843 "view {} does not exist",
5844 q.name
5845 )))
5846 }
5847 };
5848 if !view.materialized {
5849 return Err(RedDBError::Internal(format!(
5850 "view {} is not materialized — REFRESH requires \
5851 CREATE MATERIALIZED VIEW",
5852 q.name
5853 )));
5854 }
5855 let started = std::time::Instant::now();
5857 let now_ms = std::time::SystemTime::now()
5858 .duration_since(std::time::UNIX_EPOCH)
5859 .map(|d| d.as_millis() as u64)
5860 .unwrap_or(0);
5861 match self.execute_query_expr((*view.query).clone()) {
5862 Ok(inner_result) => {
5863 let entities =
5870 view_records_to_entities(&q.name, &inner_result.result.records);
5871 let row_count = entities.len() as u64;
5872 let store = self.inner.db.store();
5873 let serialized_records = match store.refresh_collection(&q.name, entities) {
5874 Ok(records) => records,
5875 Err(err) => {
5876 let duration_ms = started.elapsed().as_millis() as u64;
5877 let msg = err.to_string();
5878 self.inner
5879 .materialized_views
5880 .write()
5881 .record_refresh_failure(
5882 &q.name,
5883 msg.clone(),
5884 duration_ms,
5885 now_ms,
5886 );
5887 return Err(RedDBError::Internal(format!(
5888 "REFRESH MATERIALIZED VIEW {}: {msg}",
5889 q.name
5890 )));
5891 }
5892 };
5893
5894 if let Some(ref primary) = self.inner.db.replication {
5900 let lsn = self.inner.cdc.emit(
5901 crate::replication::cdc::ChangeOperation::Refresh,
5902 &q.name,
5903 0,
5904 "refresh",
5905 );
5906 self.invalidate_result_cache_for_table(&q.name);
5907 let timestamp = std::time::SystemTime::now()
5908 .duration_since(std::time::UNIX_EPOCH)
5909 .unwrap_or_default()
5910 .as_millis() as u64;
5911 let record = ChangeRecord::for_refresh(
5912 lsn,
5913 timestamp,
5914 q.name.clone(),
5915 serialized_records,
5916 )
5917 .with_term(self.current_replication_term());
5918 let encoded = record.encode();
5919 primary.append_logical_record(record.lsn, encoded);
5920 }
5921
5922 let duration_ms = started.elapsed().as_millis() as u64;
5923 let serialized = format!("{:?}", inner_result.result);
5924 self.inner
5925 .materialized_views
5926 .write()
5927 .record_refresh_success(
5928 &q.name,
5929 serialized.into_bytes(),
5930 row_count,
5931 duration_ms,
5932 now_ms,
5933 );
5934 self.invalidate_result_cache();
5939 Ok(RuntimeQueryResult::ok_message(
5940 query.to_string(),
5941 &format!("materialized view {} refreshed", q.name),
5942 "refresh_materialized_view",
5943 ))
5944 }
5945 Err(err) => {
5946 let duration_ms = started.elapsed().as_millis() as u64;
5947 let msg = err.to_string();
5948 self.inner
5949 .materialized_views
5950 .write()
5951 .record_refresh_failure(&q.name, msg.clone(), duration_ms, now_ms);
5952 Err(err)
5953 }
5954 }
5955 }
5956 QueryExpr::CreatePolicy(ref q) => {
5963 let key = (q.table.clone(), q.name.clone());
5964 self.inner
5965 .rls_policies
5966 .write()
5967 .insert(key, Arc::new(q.clone()));
5968 self.invalidate_plan_cache();
5969 self.schema_vocabulary_apply(
5973 crate::runtime::schema_vocabulary::DdlEvent::CreatePolicy {
5974 collection: q.table.clone(),
5975 policy: q.name.clone(),
5976 },
5977 );
5978 Ok(RuntimeQueryResult::ok_message(
5979 query.to_string(),
5980 &format!("policy {} on {} created", q.name, q.table),
5981 "create_policy",
5982 ))
5983 }
5984 QueryExpr::DropPolicy(ref q) => {
5985 let removed = self
5986 .inner
5987 .rls_policies
5988 .write()
5989 .remove(&(q.table.clone(), q.name.clone()))
5990 .is_some();
5991 if !removed && !q.if_exists {
5992 return Err(RedDBError::Internal(format!(
5993 "policy {} on {} does not exist",
5994 q.name, q.table
5995 )));
5996 }
5997 self.invalidate_plan_cache();
5998 self.schema_vocabulary_apply(
6001 crate::runtime::schema_vocabulary::DdlEvent::DropPolicy {
6002 collection: q.table.clone(),
6003 policy: q.name.clone(),
6004 },
6005 );
6006 Ok(RuntimeQueryResult::ok_message(
6007 query.to_string(),
6008 &format!("policy {} on {} dropped", q.name, q.table),
6009 "drop_policy",
6010 ))
6011 }
6012 QueryExpr::CreateServer(ref q) => {
6023 use crate::storage::fdw::FdwOptions;
6024 let registry = Arc::clone(&self.inner.foreign_tables);
6025 if registry.server(&q.name).is_some() {
6026 if q.if_not_exists {
6027 return Ok(RuntimeQueryResult::ok_message(
6028 query.to_string(),
6029 &format!("server {} already exists — skipped", q.name),
6030 "create_server",
6031 ));
6032 }
6033 return Err(RedDBError::Internal(format!(
6034 "server {} already exists",
6035 q.name
6036 )));
6037 }
6038 let mut opts = FdwOptions::new();
6039 for (k, v) in &q.options {
6040 opts.values.insert(k.clone(), v.clone());
6041 }
6042 registry
6043 .create_server(&q.name, &q.wrapper, opts)
6044 .map_err(|e| RedDBError::Internal(e.to_string()))?;
6045 Ok(RuntimeQueryResult::ok_message(
6046 query.to_string(),
6047 &format!("server {} created (wrapper {})", q.name, q.wrapper),
6048 "create_server",
6049 ))
6050 }
6051 QueryExpr::DropServer(ref q) => {
6052 let existed = self.inner.foreign_tables.drop_server(&q.name);
6053 if !existed && !q.if_exists {
6054 return Err(RedDBError::Internal(format!(
6055 "server {} does not exist",
6056 q.name
6057 )));
6058 }
6059 Ok(RuntimeQueryResult::ok_message(
6060 query.to_string(),
6061 &format!(
6062 "server {} dropped{}",
6063 q.name,
6064 if q.cascade { " (cascade)" } else { "" }
6065 ),
6066 "drop_server",
6067 ))
6068 }
6069 QueryExpr::CreateForeignTable(ref q) => {
6070 use crate::storage::fdw::{FdwOptions, ForeignColumn, ForeignTable};
6071 let registry = Arc::clone(&self.inner.foreign_tables);
6072 if registry.foreign_table(&q.name).is_some() {
6073 if q.if_not_exists {
6074 return Ok(RuntimeQueryResult::ok_message(
6075 query.to_string(),
6076 &format!("foreign table {} already exists — skipped", q.name),
6077 "create_foreign_table",
6078 ));
6079 }
6080 return Err(RedDBError::Internal(format!(
6081 "foreign table {} already exists",
6082 q.name
6083 )));
6084 }
6085 let mut opts = FdwOptions::new();
6086 for (k, v) in &q.options {
6087 opts.values.insert(k.clone(), v.clone());
6088 }
6089 let columns: Vec<ForeignColumn> = q
6090 .columns
6091 .iter()
6092 .map(|c| ForeignColumn {
6093 name: c.name.clone(),
6094 data_type: c.data_type.clone(),
6095 not_null: c.not_null,
6096 })
6097 .collect();
6098 registry
6099 .create_foreign_table(ForeignTable {
6100 name: q.name.clone(),
6101 server_name: q.server.clone(),
6102 columns,
6103 options: opts,
6104 })
6105 .map_err(|e| RedDBError::Internal(e.to_string()))?;
6106 self.invalidate_plan_cache();
6107 Ok(RuntimeQueryResult::ok_message(
6108 query.to_string(),
6109 &format!("foreign table {} created (server {})", q.name, q.server),
6110 "create_foreign_table",
6111 ))
6112 }
6113 QueryExpr::DropForeignTable(ref q) => {
6114 let existed = self.inner.foreign_tables.drop_foreign_table(&q.name);
6115 if !existed && !q.if_exists {
6116 return Err(RedDBError::Internal(format!(
6117 "foreign table {} does not exist",
6118 q.name
6119 )));
6120 }
6121 self.invalidate_plan_cache();
6122 Ok(RuntimeQueryResult::ok_message(
6123 query.to_string(),
6124 &format!("foreign table {} dropped", q.name),
6125 "drop_foreign_table",
6126 ))
6127 }
6128 QueryExpr::CopyFrom(ref q) => {
6134 use crate::storage::import::{CsvConfig, CsvImporter};
6135 let store = self.inner.db.store();
6136 let cfg = CsvConfig {
6137 collection: q.table.clone(),
6138 has_header: q.has_header,
6139 delimiter: q.delimiter.map(|c| c as u8).unwrap_or(b','),
6140 ..CsvConfig::default()
6141 };
6142 let importer = CsvImporter::new(cfg);
6143 let stats = importer
6144 .import_file(&q.path, store.as_ref())
6145 .map_err(|e| RedDBError::Internal(format!("COPY failed: {e}")))?;
6146 self.note_table_write(&q.table);
6148 Ok(RuntimeQueryResult::ok_message(
6149 query.to_string(),
6150 &format!(
6151 "COPY imported {} rows into {} ({} errors skipped, {}ms)",
6152 stats.records_imported, q.table, stats.errors_skipped, stats.duration_ms
6153 ),
6154 "copy_from",
6155 ))
6156 }
6157 QueryExpr::MaintenanceCommand(ref cmd) => {
6173 use crate::storage::query::ast::MaintenanceCommand as Mc;
6174 let store = self.inner.db.store();
6175 let (kind, msg) = match cmd {
6176 Mc::Analyze { target } => {
6177 let targets: Vec<String> = match target {
6178 Some(t) => vec![t.clone()],
6179 None => store.list_collections(),
6180 };
6181 for t in &targets {
6182 self.refresh_table_planner_stats(t);
6183 }
6184 (
6185 "analyze",
6186 format!("ANALYZE refreshed stats for {} table(s)", targets.len()),
6187 )
6188 }
6189 Mc::Vacuum { target, full } => {
6190 let targets: Vec<String> = match target {
6191 Some(t) => vec![t.clone()],
6192 None => store.list_collections(),
6193 };
6194 let cutoff_xid = self.mvcc_vacuum_cutoff_xid();
6195 let mut vacuum_stats =
6196 crate::storage::unified::store::MvccVacuumStats::default();
6197 for t in &targets {
6198 let stats = store.vacuum_mvcc_history(t, cutoff_xid).map_err(|e| {
6199 RedDBError::Internal(format!(
6200 "VACUUM MVCC history failed for {t}: {e}"
6201 ))
6202 })?;
6203 if stats.reclaimed_versions > 0 {
6204 self.rebuild_runtime_indexes_for_table(t)?;
6205 }
6206 vacuum_stats.add(&stats);
6207 }
6208 self.inner.snapshot_manager.prune_aborted(cutoff_xid);
6209 for t in &targets {
6211 self.refresh_table_planner_stats(t);
6212 }
6213 let persisted = if *full {
6217 match store.persist() {
6218 Ok(()) => true,
6219 Err(e) => {
6220 return Err(RedDBError::Internal(format!(
6221 "VACUUM FULL persist failed: {e:?}"
6222 )));
6223 }
6224 }
6225 } else {
6226 false
6227 };
6228 self.invalidate_result_cache();
6230 (
6231 "vacuum",
6232 format!(
6233 "VACUUM{} processed {} table(s): scanned_versions={}, retained_versions={}, reclaimed_versions={}, retained_history_versions={}, reclaimed_history_versions={}, retained_tombstones={}, reclaimed_tombstones={}{}",
6234 if *full { " FULL" } else { "" },
6235 targets.len(),
6236 vacuum_stats.scanned_versions,
6237 vacuum_stats.retained_versions,
6238 vacuum_stats.reclaimed_versions,
6239 vacuum_stats.retained_history_versions,
6240 vacuum_stats.reclaimed_history_versions,
6241 vacuum_stats.retained_tombstones,
6242 vacuum_stats.reclaimed_tombstones,
6243 if persisted {
6244 " (pages flushed to disk)"
6245 } else {
6246 ""
6247 }
6248 ),
6249 )
6250 }
6251 };
6252 Ok(RuntimeQueryResult::ok_message(
6253 query.to_string(),
6254 &msg,
6255 kind,
6256 ))
6257 }
6258 QueryExpr::Grant(ref g) => self.execute_grant_statement(query, g),
6265 QueryExpr::Revoke(ref r) => self.execute_revoke_statement(query, r),
6266 QueryExpr::AlterUser(ref a) => self.execute_alter_user_statement(query, a),
6267 QueryExpr::CreateUser(ref u) => self.execute_create_user_statement(query, u),
6268 QueryExpr::CreateIamPolicy { ref id, ref json } => {
6269 self.execute_create_iam_policy(query, id, json)
6270 }
6271 QueryExpr::DropIamPolicy { ref id } => self.execute_drop_iam_policy(query, id),
6272 QueryExpr::AttachPolicy {
6273 ref policy_id,
6274 ref principal,
6275 } => self.execute_attach_policy(query, policy_id, principal),
6276 QueryExpr::DetachPolicy {
6277 ref policy_id,
6278 ref principal,
6279 } => self.execute_detach_policy(query, policy_id, principal),
6280 QueryExpr::ShowPolicies { ref filter } => {
6281 self.execute_show_policies(query, filter.as_ref())
6282 }
6283 QueryExpr::ShowEffectivePermissions {
6284 ref user,
6285 ref resource,
6286 } => self.execute_show_effective_permissions(query, user, resource.as_ref()),
6287 QueryExpr::SimulatePolicy {
6288 ref user,
6289 ref action,
6290 ref resource,
6291 } => self.execute_simulate_policy(query, user, action, resource),
6292 QueryExpr::LintPolicy { ref source } => self.execute_lint_policy(query, source),
6293 QueryExpr::MigratePolicyMode {
6294 ref target,
6295 dry_run,
6296 } => self.execute_migrate_policy_mode(query, target, dry_run),
6297 QueryExpr::CreateMigration(ref q) => self.execute_create_migration(query, q),
6298 QueryExpr::ApplyMigration(ref q) => self.execute_apply_migration(query, q),
6299 QueryExpr::RollbackMigration(ref q) => self.execute_rollback_migration(query, q),
6300 QueryExpr::ExplainMigration(ref q) => self.execute_explain_migration(query, q),
6301 };
6302
6303 if !control_event_specs.is_empty() {
6304 let (outcome, reason) = match &query_result {
6305 Ok(_) => (crate::runtime::control_events::Outcome::Allowed, None),
6306 Err(err) => (control_event_outcome_for_error(err), Some(err.to_string())),
6307 };
6308 for spec in &control_event_specs {
6309 self.emit_control_event(
6310 spec.kind,
6311 outcome,
6312 spec.action,
6313 spec.resource.clone(),
6314 reason.clone(),
6315 spec.fields.clone(),
6316 )?;
6317 }
6318 }
6319
6320 if let (Some(plan), Ok(result)) = (&query_audit_plan, &query_result) {
6321 self.emit_query_audit(
6322 query,
6323 plan,
6324 query_audit_started.elapsed().as_millis() as u64,
6325 result,
6326 );
6327 }
6328
6329 let mut query_result = query_result;
6333 if let Ok(ref mut result) = query_result {
6334 if result.statement_type == "select" {
6335 self.apply_secret_decryption(result);
6336 }
6337 }
6338
6339 if let Ok(ref result) = query_result {
6346 frame.write_result_cache(self, result, result_cache_scopes);
6347 }
6348
6349 query_result
6350 }
6351
6352 pub fn materialized_view_metadata(
6356 &self,
6357 ) -> Vec<crate::storage::cache::result::MaterializedViewMetadata> {
6358 let store = self.inner.db.store();
6365 let mut entries = self.inner.materialized_views.read().metadata();
6366 for entry in &mut entries {
6367 if let Some(manager) = store.get_collection(&entry.name) {
6368 entry.current_row_count = manager.count() as u64;
6369 }
6370 }
6371 entries
6372 }
6373
6374 pub(crate) fn retention_sweeper_snapshot(
6385 &self,
6386 ) -> Vec<(String, crate::runtime::retention_sweeper::SweeperState)> {
6387 self.inner.retention_sweeper.read().snapshot()
6388 }
6389
6390 pub fn sweep_retention_tick(&self, batch_size: usize) {
6412 if batch_size == 0 {
6413 return;
6414 }
6415 let now_ms = std::time::SystemTime::now()
6416 .duration_since(std::time::UNIX_EPOCH)
6417 .map(|d| d.as_millis() as u64)
6418 .unwrap_or(0);
6419
6420 let store = self.inner.db.store();
6421 let collections = store.list_collections();
6422 for name in collections {
6423 let Some(contract) = self.inner.db.collection_contract(&name) else {
6424 continue;
6425 };
6426 let Some(retention_ms) = contract.retention_duration_ms else {
6427 continue;
6428 };
6429 let Some(ts_column) =
6430 crate::runtime::retention_filter::resolve_timestamp_column(&contract)
6431 else {
6432 continue;
6433 };
6434 let Some(manager) = store.get_collection(&name) else {
6435 continue;
6436 };
6437 let cutoff = (now_ms as i64).saturating_sub(retention_ms as i64);
6438
6439 let mut expired_ts: Vec<i64> = Vec::new();
6447 manager.for_each_entity(|entity| {
6448 let ts = match ts_column.as_str() {
6449 "created_at" => Some(entity.created_at as i64),
6450 "updated_at" => Some(entity.updated_at as i64),
6451 other => entity
6452 .data
6453 .as_row()
6454 .and_then(|row| row.get_field(other))
6455 .and_then(|v| match v {
6456 crate::storage::schema::Value::TimestampMs(t) => Some(*t),
6457 crate::storage::schema::Value::Timestamp(t) => {
6458 Some(t.saturating_mul(1_000))
6459 }
6460 crate::storage::schema::Value::BigInt(t) => Some(*t),
6461 crate::storage::schema::Value::UnsignedInteger(t) => {
6462 i64::try_from(*t).ok()
6463 }
6464 crate::storage::schema::Value::Integer(t) => Some(*t),
6465 _ => None,
6466 }),
6467 };
6468 if let Some(t) = ts {
6469 if t < cutoff {
6470 expired_ts.push(t);
6471 }
6472 }
6473 true
6474 });
6475
6476 let total_expired = expired_ts.len() as u64;
6477 if total_expired == 0 {
6478 self.inner
6479 .retention_sweeper
6480 .write()
6481 .record_tick(&name, 0, 0, now_ms);
6482 continue;
6483 }
6484
6485 let (effective_cutoff, pending) = if (total_expired as usize) <= batch_size {
6486 (cutoff, 0u64)
6487 } else {
6488 expired_ts.sort_unstable();
6492 let nth = expired_ts[batch_size - 1];
6493 (
6494 nth.saturating_add(1),
6495 total_expired.saturating_sub(batch_size as u64),
6496 )
6497 };
6498
6499 let stmt = format!(
6500 "DELETE FROM {} WHERE {} < {}",
6501 name, ts_column, effective_cutoff
6502 );
6503 let deleted = match self.execute_query(&stmt) {
6504 Ok(r) => r.affected_rows,
6505 Err(_) => 0,
6506 };
6507
6508 self.inner
6509 .retention_sweeper
6510 .write()
6511 .record_tick(&name, deleted, pending, now_ms);
6512 }
6513 }
6514
6515 pub fn refresh_due_materialized_views(&self) {
6516 let due = {
6517 let mut cache = self.inner.materialized_views.write();
6518 cache.claim_due_at(std::time::Instant::now())
6519 };
6520 for name in due {
6521 let stmt = format!("REFRESH MATERIALIZED VIEW {}", name);
6528 let _ = self.execute_query(&stmt);
6529 }
6530 }
6531
6532 pub fn execute_query_expr(&self, expr: QueryExpr) -> RedDBResult<RuntimeQueryResult> {
6538 let _config_snapshot_guard = ConfigSnapshotGuard::install(Arc::clone(&self.inner.db));
6539 let _secret_store_guard = SecretStoreGuard::install(self.inner.auth_store.read().clone());
6540 let expr = self.rewrite_view_refs(expr);
6544
6545 self.validate_model_operations_before_auth(&expr)?;
6546 if let Err(err) = self.check_query_privilege(&expr) {
6550 return Err(RedDBError::Query(format!("permission denied: {err}")));
6551 }
6552
6553 let statement = query_expr_name(&expr);
6554 let mode = detect_mode(statement);
6555 let query_str = statement;
6556
6557 let result = self.dispatch_expr(expr, query_str, mode)?;
6558 let mut r = result;
6559 if r.statement_type == "select" {
6560 self.apply_secret_decryption(&mut r);
6561 }
6562 Ok(r)
6563 }
6564
6565 pub(super) fn validate_model_operations_before_auth(
6566 &self,
6567 expr: &QueryExpr,
6568 ) -> RedDBResult<()> {
6569 use crate::catalog::CollectionModel;
6570 use crate::runtime::ddl::polymorphic_resolver;
6571 use crate::storage::query::ast::KvCommand;
6572
6573 let system_schema_target = match expr {
6574 QueryExpr::DropTable(q) => Some(q.name.as_str()),
6575 QueryExpr::DropGraph(q) => Some(q.name.as_str()),
6576 QueryExpr::DropVector(q) => Some(q.name.as_str()),
6577 QueryExpr::DropDocument(q) => Some(q.name.as_str()),
6578 QueryExpr::DropKv(q) => Some(q.name.as_str()),
6579 QueryExpr::DropCollection(q) => Some(q.name.as_str()),
6580 QueryExpr::Truncate(q) => Some(q.name.as_str()),
6581 _ => None,
6582 };
6583 if system_schema_target.is_some_and(crate::runtime::impl_ddl::is_system_schema_name) {
6584 return Err(RedDBError::Query("system schema is read-only".to_string()));
6585 }
6586
6587 let expected = match expr {
6588 QueryExpr::DropTable(q) => Some((q.name.as_str(), CollectionModel::Table)),
6589 QueryExpr::DropGraph(q) => Some((q.name.as_str(), CollectionModel::Graph)),
6590 QueryExpr::DropVector(q) => Some((q.name.as_str(), CollectionModel::Vector)),
6591 QueryExpr::DropDocument(q) => Some((q.name.as_str(), CollectionModel::Document)),
6592 QueryExpr::DropKv(q) => Some((q.name.as_str(), q.model)),
6593 QueryExpr::DropCollection(q) => q.model.map(|model| (q.name.as_str(), model)),
6594 QueryExpr::Truncate(q) => q.model.map(|model| (q.name.as_str(), model)),
6595 QueryExpr::KvCommand(cmd) => {
6596 let (collection, model) = match cmd {
6597 KvCommand::Put {
6598 collection, model, ..
6599 }
6600 | KvCommand::Get {
6601 collection, model, ..
6602 }
6603 | KvCommand::Incr {
6604 collection, model, ..
6605 }
6606 | KvCommand::Cas {
6607 collection, model, ..
6608 }
6609 | KvCommand::List {
6610 collection, model, ..
6611 }
6612 | KvCommand::Delete {
6613 collection, model, ..
6614 } => (collection.as_str(), *model),
6615 KvCommand::Rotate { collection, .. }
6616 | KvCommand::History { collection, .. }
6617 | KvCommand::Purge { collection, .. } => {
6618 (collection.as_str(), CollectionModel::Vault)
6619 }
6620 KvCommand::InvalidateTags { collection, .. } => {
6621 (collection.as_str(), CollectionModel::Kv)
6622 }
6623 KvCommand::Watch {
6624 collection, model, ..
6625 } => (collection.as_str(), *model),
6626 KvCommand::Unseal { collection, .. } => {
6627 (collection.as_str(), CollectionModel::Vault)
6628 }
6629 };
6630 Some((collection, model))
6631 }
6632 QueryExpr::ConfigCommand(cmd) => {
6633 self.validate_config_command_before_auth(cmd)?;
6634 None
6635 }
6636 _ => None,
6637 };
6638
6639 let Some((name, expected_model)) = expected else {
6640 return Ok(());
6641 };
6642 let snapshot = self.inner.db.catalog_model_snapshot();
6643 let Some(actual_model) = snapshot
6644 .collections
6645 .iter()
6646 .find(|collection| collection.name == name)
6647 .map(|collection| collection.declared_model.unwrap_or(collection.model))
6648 else {
6649 return Ok(());
6650 };
6651 polymorphic_resolver::ensure_model_match(expected_model, actual_model)
6652 }
6653
6654 pub(super) fn rewrite_view_refs(&self, expr: QueryExpr) -> QueryExpr {
6659 if self.inner.views.read().is_empty() {
6661 return expr;
6662 }
6663 self.rewrite_view_refs_inner(expr)
6664 }
6665
6666 fn rewrite_view_refs_inner(&self, expr: QueryExpr) -> QueryExpr {
6667 use crate::storage::query::ast::{Filter, TableSource};
6668 match expr {
6669 QueryExpr::Table(mut tq) => {
6670 if let Some(TableSource::Subquery(body)) = tq.source.take() {
6676 tq.source = Some(TableSource::Subquery(Box::new(
6677 self.rewrite_view_refs_inner(*body),
6678 )));
6679 return QueryExpr::Table(tq);
6680 }
6681
6682 let maybe_view = {
6686 let views = self.inner.views.read();
6687 views.get(&tq.table).cloned()
6688 };
6689 let Some(view) = maybe_view else {
6690 return QueryExpr::Table(tq);
6691 };
6692
6693 if view.materialized {
6699 return QueryExpr::Table(tq);
6700 }
6701
6702 let inner_expr = self.rewrite_view_refs_inner((*view.query).clone());
6706
6707 match inner_expr {
6715 QueryExpr::Table(mut inner_tq) => {
6716 if let Some(outer_filter) = tq.filter.take() {
6717 inner_tq.filter = Some(match inner_tq.filter.take() {
6718 Some(existing) => {
6719 Filter::And(Box::new(existing), Box::new(outer_filter))
6720 }
6721 None => outer_filter,
6722 });
6723 inner_tq.where_expr = inner_tq
6731 .filter
6732 .as_ref()
6733 .map(crate::storage::query::sql_lowering::filter_to_expr);
6734 }
6735 if let Some(outer_limit) = tq.limit {
6736 inner_tq.limit = Some(match inner_tq.limit {
6737 Some(existing) => existing.min(outer_limit),
6738 None => outer_limit,
6739 });
6740 }
6741 if let Some(outer_offset) = tq.offset {
6742 inner_tq.offset = Some(match inner_tq.offset {
6743 Some(existing) => existing + outer_offset,
6744 None => outer_offset,
6745 });
6746 }
6747 QueryExpr::Table(inner_tq)
6748 }
6749 other => other,
6750 }
6751 }
6752 QueryExpr::Join(mut jq) => {
6753 jq.left = Box::new(self.rewrite_view_refs_inner(*jq.left));
6754 jq.right = Box::new(self.rewrite_view_refs_inner(*jq.right));
6755 QueryExpr::Join(jq)
6756 }
6757 other => other,
6760 }
6761 }
6762
6763 fn authorize_relational_table_select(
6766 &self,
6767 mut table: TableQuery,
6768 frame: &dyn super::statement_frame::ReadFrame,
6769 ) -> RedDBResult<Option<TableQuery>> {
6770 if let Some(TableSource::Subquery(inner)) = table.source.take() {
6771 let authorized_inner = self.authorize_relational_select_expr(*inner, frame)?;
6772 table.source = Some(TableSource::Subquery(Box::new(authorized_inner)));
6773 return Ok(Some(table));
6774 }
6775
6776 self.check_table_column_projection_authz(&table, frame)?;
6777
6778 if self.inner.rls_enabled_tables.read().contains(&table.table) {
6779 return Ok(inject_rls_filters(self, frame, table));
6780 }
6781
6782 Ok(Some(table))
6783 }
6784
6785 fn authorize_relational_join_select(
6786 &self,
6787 mut join: JoinQuery,
6788 frame: &dyn super::statement_frame::ReadFrame,
6789 ) -> RedDBResult<Option<JoinQuery>> {
6790 self.check_join_column_projection_authz(&join, frame)?;
6791 join.left = Box::new(self.authorize_relational_join_child(*join.left, frame)?);
6792 join.right = Box::new(self.authorize_relational_join_child(*join.right, frame)?);
6793 Ok(inject_rls_into_join(self, frame, join))
6794 }
6795
6796 fn authorize_relational_join_child(
6797 &self,
6798 expr: QueryExpr,
6799 frame: &dyn super::statement_frame::ReadFrame,
6800 ) -> RedDBResult<QueryExpr> {
6801 match expr {
6802 QueryExpr::Table(mut table) => {
6803 if let Some(TableSource::Subquery(inner)) = table.source.take() {
6804 let authorized_inner = self.authorize_relational_select_expr(*inner, frame)?;
6805 table.source = Some(TableSource::Subquery(Box::new(authorized_inner)));
6806 }
6807 Ok(QueryExpr::Table(table))
6808 }
6809 QueryExpr::Join(join) => self
6810 .authorize_relational_join_select(join, frame)?
6811 .map(QueryExpr::Join)
6812 .ok_or_else(|| {
6813 RedDBError::Query("permission denied: RLS denied relational subquery".into())
6814 }),
6815 other => Ok(other),
6816 }
6817 }
6818
6819 fn authorize_relational_select_expr(
6820 &self,
6821 expr: QueryExpr,
6822 frame: &dyn super::statement_frame::ReadFrame,
6823 ) -> RedDBResult<QueryExpr> {
6824 match expr {
6825 QueryExpr::Table(table) => self
6826 .authorize_relational_table_select(table, frame)?
6827 .map(QueryExpr::Table)
6828 .ok_or_else(|| {
6829 RedDBError::Query("permission denied: RLS denied relational subquery".into())
6830 }),
6831 QueryExpr::Join(join) => self
6832 .authorize_relational_join_select(join, frame)?
6833 .map(QueryExpr::Join)
6834 .ok_or_else(|| {
6835 RedDBError::Query("permission denied: RLS denied relational subquery".into())
6836 }),
6837 other => Ok(other),
6838 }
6839 }
6840
6841 fn check_table_column_projection_authz(
6842 &self,
6843 table: &TableQuery,
6844 frame: &dyn super::statement_frame::ReadFrame,
6845 ) -> RedDBResult<()> {
6846 let Some((username, role)) = frame.identity() else {
6847 return Ok(());
6848 };
6849 let Some(auth_store) = self.inner.auth_store.read().clone() else {
6850 return Ok(());
6851 };
6852
6853 let columns = self.resolved_table_projection_columns(table)?;
6854 let request = ColumnAccessRequest::select(table.table.clone(), columns);
6855 let principal = UserId::from_parts(frame.effective_scope(), username);
6856 let ctx = runtime_iam_context(role, frame.effective_scope());
6857 let outcome = auth_store.check_column_projection_authz(&principal, &request, &ctx);
6858 if outcome.allowed() {
6859 return Ok(());
6860 }
6861
6862 if let Some(denied) = outcome.first_denied_column() {
6863 return Err(RedDBError::Query(format!(
6864 "permission denied: principal=`{username}` cannot select column `{}`",
6865 denied.resource.name
6866 )));
6867 }
6868 Err(RedDBError::Query(format!(
6869 "permission denied: principal=`{username}` cannot select table `{}`",
6870 table.table
6871 )))
6872 }
6873
6874 fn check_join_column_projection_authz(
6875 &self,
6876 join: &JoinQuery,
6877 frame: &dyn super::statement_frame::ReadFrame,
6878 ) -> RedDBResult<()> {
6879 let mut by_table: HashMap<String, BTreeSet<String>> = HashMap::new();
6880 let projections = crate::storage::query::sql_lowering::effective_join_projections(join);
6881 self.collect_join_projection_columns(join, &projections, &mut by_table)?;
6882
6883 for (table, columns) in by_table {
6884 let query = TableQuery {
6885 table,
6886 source: None,
6887 alias: None,
6888 select_items: Vec::new(),
6889 columns: columns.into_iter().map(Projection::Column).collect(),
6890 where_expr: None,
6891 filter: None,
6892 group_by_exprs: Vec::new(),
6893 group_by: Vec::new(),
6894 having_expr: None,
6895 having: None,
6896 order_by: Vec::new(),
6897 limit: None,
6898 limit_param: None,
6899 offset: None,
6900 offset_param: None,
6901 expand: None,
6902 as_of: None,
6903 sessionize: None,
6904 distinct: false,
6905 };
6906 self.check_table_column_projection_authz(&query, frame)?;
6907 }
6908 Ok(())
6909 }
6910
6911 fn collect_join_projection_columns(
6912 &self,
6913 join: &JoinQuery,
6914 projections: &[Projection],
6915 out: &mut HashMap<String, BTreeSet<String>>,
6916 ) -> RedDBResult<()> {
6917 let left = table_side_context(join.left.as_ref());
6918 let right = table_side_context(join.right.as_ref());
6919
6920 if projections
6921 .iter()
6922 .any(|projection| matches!(projection, Projection::All))
6923 {
6924 for side in [left.as_ref(), right.as_ref()].into_iter().flatten() {
6925 out.entry(side.table.clone())
6926 .or_default()
6927 .extend(self.table_all_projection_columns(&side.table)?);
6928 }
6929 return Ok(());
6930 }
6931
6932 for projection in projections {
6933 collect_projection_columns_for_join_side(
6934 projection,
6935 left.as_ref(),
6936 right.as_ref(),
6937 out,
6938 )?;
6939 }
6940 Ok(())
6941 }
6942
6943 fn resolved_table_projection_columns(&self, table: &TableQuery) -> RedDBResult<Vec<String>> {
6944 let projections = crate::storage::query::sql_lowering::effective_table_projections(table);
6945 if projections
6946 .iter()
6947 .any(|projection| matches!(projection, Projection::All))
6948 {
6949 return self.table_all_projection_columns(&table.table);
6950 }
6951
6952 let mut columns = BTreeSet::new();
6953 for projection in &projections {
6954 collect_projection_columns_for_table(
6955 projection,
6956 &table.table,
6957 table.alias.as_deref(),
6958 &mut columns,
6959 );
6960 }
6961 Ok(columns.into_iter().collect())
6962 }
6963
6964 fn table_all_projection_columns(&self, table: &str) -> RedDBResult<Vec<String>> {
6965 if let Some(contract) = self.inner.db.collection_contract_arc(table) {
6966 let columns: Vec<String> = contract
6967 .declared_columns
6968 .iter()
6969 .map(|column| column.name.clone())
6970 .collect();
6971 if !columns.is_empty() {
6972 return Ok(columns);
6973 }
6974 }
6975
6976 let records = scan_runtime_table_source_records_limited(&self.inner.db, table, Some(1))?;
6977 Ok(records
6978 .first()
6979 .map(|record| {
6980 record
6981 .column_names()
6982 .into_iter()
6983 .map(|column| column.to_string())
6984 .collect()
6985 })
6986 .unwrap_or_default())
6987 }
6988
6989 fn resolve_table_expr_subqueries(
6990 &self,
6991 mut table: TableQuery,
6992 frame: &dyn super::statement_frame::ReadFrame,
6993 ) -> RedDBResult<TableQuery> {
6994 match table.source.take() {
7001 Some(TableSource::Subquery(inner)) => {
7002 let inner = self.resolve_select_expr_subqueries(*inner, frame)?;
7003 table.source = Some(TableSource::Subquery(Box::new(inner)));
7004 }
7005 other => table.source = other,
7006 }
7007
7008 let outer_scopes = relation_scopes_for_query(&QueryExpr::Table(table.clone()));
7009 for item in &mut table.select_items {
7010 if let crate::storage::query::ast::SelectItem::Expr { expr, .. } = item {
7011 *expr = self.resolve_expr_subqueries(expr.clone(), &outer_scopes, frame)?;
7012 }
7013 }
7014 if let Some(where_expr) = table.where_expr.take() {
7015 table.where_expr =
7016 Some(self.resolve_expr_subqueries(where_expr, &outer_scopes, frame)?);
7017 table.filter = None;
7018 }
7019 if let Some(having_expr) = table.having_expr.take() {
7020 table.having_expr =
7021 Some(self.resolve_expr_subqueries(having_expr, &outer_scopes, frame)?);
7022 table.having = None;
7023 }
7024 for expr in &mut table.group_by_exprs {
7025 *expr = self.resolve_expr_subqueries(expr.clone(), &outer_scopes, frame)?;
7026 }
7027 for clause in &mut table.order_by {
7028 if let Some(expr) = clause.expr.take() {
7029 clause.expr = Some(self.resolve_expr_subqueries(expr, &outer_scopes, frame)?);
7030 }
7031 }
7032 Ok(table)
7033 }
7034
7035 fn resolve_select_expr_subqueries(
7036 &self,
7037 expr: QueryExpr,
7038 frame: &dyn super::statement_frame::ReadFrame,
7039 ) -> RedDBResult<QueryExpr> {
7040 match expr {
7041 QueryExpr::Table(table) => self
7042 .resolve_table_expr_subqueries(table, frame)
7043 .map(QueryExpr::Table),
7044 QueryExpr::Join(mut join) => {
7045 join.left = Box::new(self.resolve_select_expr_subqueries(*join.left, frame)?);
7046 join.right = Box::new(self.resolve_select_expr_subqueries(*join.right, frame)?);
7047 Ok(QueryExpr::Join(join))
7048 }
7049 other => Ok(other),
7050 }
7051 }
7052
7053 fn resolve_expr_subqueries(
7054 &self,
7055 expr: crate::storage::query::ast::Expr,
7056 outer_scopes: &[String],
7057 frame: &dyn super::statement_frame::ReadFrame,
7058 ) -> RedDBResult<crate::storage::query::ast::Expr> {
7059 use crate::storage::query::ast::Expr;
7060
7061 match expr {
7062 Expr::Subquery { query, span } => {
7063 let values = self.execute_expr_subquery_values(query, outer_scopes, frame)?;
7064 if values.len() > 1 {
7065 return Err(RedDBError::Query(
7066 "scalar subquery returned more than one row".to_string(),
7067 ));
7068 }
7069 Ok(Expr::Literal {
7070 value: values.into_iter().next().unwrap_or(Value::Null),
7071 span,
7072 })
7073 }
7074 Expr::BinaryOp { op, lhs, rhs, span } => Ok(Expr::BinaryOp {
7075 op,
7076 lhs: Box::new(self.resolve_expr_subqueries(*lhs, outer_scopes, frame)?),
7077 rhs: Box::new(self.resolve_expr_subqueries(*rhs, outer_scopes, frame)?),
7078 span,
7079 }),
7080 Expr::UnaryOp { op, operand, span } => Ok(Expr::UnaryOp {
7081 op,
7082 operand: Box::new(self.resolve_expr_subqueries(*operand, outer_scopes, frame)?),
7083 span,
7084 }),
7085 Expr::Cast {
7086 inner,
7087 target,
7088 span,
7089 } => Ok(Expr::Cast {
7090 inner: Box::new(self.resolve_expr_subqueries(*inner, outer_scopes, frame)?),
7091 target,
7092 span,
7093 }),
7094 Expr::FunctionCall { name, args, span } => {
7095 let args = args
7096 .into_iter()
7097 .map(|arg| self.resolve_expr_subqueries(arg, outer_scopes, frame))
7098 .collect::<RedDBResult<Vec<_>>>()?;
7099 Ok(Expr::FunctionCall { name, args, span })
7100 }
7101 Expr::Case {
7102 branches,
7103 else_,
7104 span,
7105 } => {
7106 let branches = branches
7107 .into_iter()
7108 .map(|(cond, value)| {
7109 Ok((
7110 self.resolve_expr_subqueries(cond, outer_scopes, frame)?,
7111 self.resolve_expr_subqueries(value, outer_scopes, frame)?,
7112 ))
7113 })
7114 .collect::<RedDBResult<Vec<_>>>()?;
7115 let else_ = else_
7116 .map(|expr| self.resolve_expr_subqueries(*expr, outer_scopes, frame))
7117 .transpose()?
7118 .map(Box::new);
7119 Ok(Expr::Case {
7120 branches,
7121 else_,
7122 span,
7123 })
7124 }
7125 Expr::IsNull {
7126 operand,
7127 negated,
7128 span,
7129 } => Ok(Expr::IsNull {
7130 operand: Box::new(self.resolve_expr_subqueries(*operand, outer_scopes, frame)?),
7131 negated,
7132 span,
7133 }),
7134 Expr::InList {
7135 target,
7136 values,
7137 negated,
7138 span,
7139 } => {
7140 let target =
7141 Box::new(self.resolve_expr_subqueries(*target, outer_scopes, frame)?);
7142 let mut resolved = Vec::new();
7143 for value in values {
7144 if let Expr::Subquery { query, .. } = value {
7145 resolved.extend(
7146 self.execute_expr_subquery_values(query, outer_scopes, frame)?
7147 .into_iter()
7148 .map(Expr::lit),
7149 );
7150 } else {
7151 resolved.push(self.resolve_expr_subqueries(value, outer_scopes, frame)?);
7152 }
7153 }
7154 Ok(Expr::InList {
7155 target,
7156 values: resolved,
7157 negated,
7158 span,
7159 })
7160 }
7161 Expr::Between {
7162 target,
7163 low,
7164 high,
7165 negated,
7166 span,
7167 } => Ok(Expr::Between {
7168 target: Box::new(self.resolve_expr_subqueries(*target, outer_scopes, frame)?),
7169 low: Box::new(self.resolve_expr_subqueries(*low, outer_scopes, frame)?),
7170 high: Box::new(self.resolve_expr_subqueries(*high, outer_scopes, frame)?),
7171 negated,
7172 span,
7173 }),
7174 other => Ok(other),
7175 }
7176 }
7177
7178 fn execute_expr_subquery_values(
7179 &self,
7180 subquery: crate::storage::query::ast::ExprSubquery,
7181 outer_scopes: &[String],
7182 frame: &dyn super::statement_frame::ReadFrame,
7183 ) -> RedDBResult<Vec<Value>> {
7184 let query = *subquery.query;
7185 if query_references_outer_scope(&query, outer_scopes) {
7186 return Err(RedDBError::Query(
7187 "NOT_YET_SUPPORTED: correlated subqueries are not supported yet; track follow-up issue #470-correlated-subqueries".to_string(),
7188 ));
7189 }
7190 let query = self.rewrite_view_refs(query);
7191 let query = self.resolve_select_expr_subqueries(query, frame)?;
7192 let query = self.authorize_relational_select_expr(query, frame)?;
7193 let result = match query {
7194 QueryExpr::Table(table) => {
7195 execute_runtime_table_query(&self.inner.db, &table, Some(&self.inner.index_store))?
7196 }
7197 QueryExpr::Join(join) => execute_runtime_join_query(&self.inner.db, &join)?,
7198 other => {
7199 return Err(RedDBError::Query(format!(
7200 "expression subquery must be a SELECT query, got {}",
7201 query_expr_name(&other)
7202 )))
7203 }
7204 };
7205 first_column_values(result)
7206 }
7207
7208 fn dispatch_expr(
7209 &self,
7210 expr: QueryExpr,
7211 query_str: &str,
7212 mode: QueryMode,
7213 ) -> RedDBResult<RuntimeQueryResult> {
7214 let statement = query_expr_name(&expr);
7215 match expr {
7216 QueryExpr::Graph(_) | QueryExpr::Path(_) => {
7217 Err(RedDBError::Query(
7219 "graph queries cannot be used as prepared statements".to_string(),
7220 ))
7221 }
7222 QueryExpr::Table(table) => {
7223 let scope = self.ai_scope();
7224 let table = self.resolve_table_expr_subqueries(
7225 table,
7226 &scope as &dyn super::statement_frame::ReadFrame,
7227 )?;
7228 if let Some(TableSource::Function {
7232 name,
7233 args,
7234 named_args,
7235 }) = table.source.clone()
7236 {
7237 return Ok(RuntimeQueryResult {
7238 query: query_str.to_string(),
7239 mode,
7240 statement,
7241 engine: "runtime-graph-tvf",
7242 result: self.execute_table_function(&name, &args, &named_args)?,
7243 affected_rows: 0,
7244 statement_type: "select",
7245 bookmark: None,
7246 });
7247 }
7248 if let Some(TableSource::InlineGraphFunction {
7252 name,
7253 nodes,
7254 edges,
7255 named_args,
7256 }) = table.source.clone()
7257 {
7258 return Ok(RuntimeQueryResult {
7259 query: query_str.to_string(),
7260 mode,
7261 statement,
7262 engine: "runtime-graph-tvf-inline",
7263 result: self.execute_inline_graph_function(
7264 &name,
7265 &nodes,
7266 &edges,
7267 &named_args,
7268 )?,
7269 affected_rows: 0,
7270 statement_type: "select",
7271 bookmark: None,
7272 });
7273 }
7274 if super::red_schema::is_virtual_table(&table.table) {
7275 return Ok(RuntimeQueryResult {
7276 query: query_str.to_string(),
7277 mode,
7278 statement,
7279 engine: "runtime-red-schema",
7280 result: super::red_schema::red_query(
7281 self,
7282 &table.table,
7283 &table,
7284 &scope as &dyn super::statement_frame::ReadFrame,
7285 )?,
7286 affected_rows: 0,
7287 statement_type: "select",
7288 bookmark: None,
7289 });
7290 }
7291 if let Some(view_result) = self.try_resolve_analytics_view(
7293 &table,
7294 &scope as &dyn super::statement_frame::ReadFrame,
7295 )? {
7296 return Ok(RuntimeQueryResult {
7297 query: query_str.to_string(),
7298 mode,
7299 statement,
7300 engine: "runtime-graph-analytics-view",
7301 result: view_result,
7302 affected_rows: 0,
7303 statement_type: "select",
7304 bookmark: None,
7305 });
7306 }
7307 let Some(table_with_rls) = self.authorize_relational_table_select(
7308 table,
7309 &scope as &dyn super::statement_frame::ReadFrame,
7310 )?
7311 else {
7312 return Ok(RuntimeQueryResult {
7313 query: query_str.to_string(),
7314 mode,
7315 statement,
7316 engine: "runtime-table-rls",
7317 result: crate::storage::query::unified::UnifiedResult::empty(),
7318 affected_rows: 0,
7319 statement_type: "select",
7320 bookmark: None,
7321 });
7322 };
7323 Ok(RuntimeQueryResult {
7324 query: query_str.to_string(),
7325 mode,
7326 statement,
7327 engine: "runtime-table",
7328 result: execute_runtime_table_query(
7329 &self.inner.db,
7330 &table_with_rls,
7331 Some(&self.inner.index_store),
7332 )?,
7333 affected_rows: 0,
7334 statement_type: "select",
7335 bookmark: None,
7336 })
7337 }
7338 QueryExpr::Join(join) => {
7339 let scope = self.ai_scope();
7340 let Some(join_with_rls) = self.authorize_relational_join_select(
7341 join,
7342 &scope as &dyn super::statement_frame::ReadFrame,
7343 )?
7344 else {
7345 return Ok(RuntimeQueryResult {
7346 query: query_str.to_string(),
7347 mode,
7348 statement,
7349 engine: "runtime-join-rls",
7350 result: crate::storage::query::unified::UnifiedResult::empty(),
7351 affected_rows: 0,
7352 statement_type: "select",
7353 bookmark: None,
7354 });
7355 };
7356 Ok(RuntimeQueryResult {
7357 query: query_str.to_string(),
7358 mode,
7359 statement,
7360 engine: "runtime-join",
7361 result: execute_runtime_join_query(&self.inner.db, &join_with_rls)?,
7362 affected_rows: 0,
7363 statement_type: "select",
7364 bookmark: None,
7365 })
7366 }
7367 QueryExpr::Vector(vector) => Ok(RuntimeQueryResult {
7368 query: query_str.to_string(),
7369 mode,
7370 statement,
7371 engine: "runtime-vector",
7372 result: execute_runtime_vector_query(&self.inner.db, &vector)?,
7373 affected_rows: 0,
7374 statement_type: "select",
7375 bookmark: None,
7376 }),
7377 QueryExpr::Hybrid(hybrid) => Ok(RuntimeQueryResult {
7378 query: query_str.to_string(),
7379 mode,
7380 statement,
7381 engine: "runtime-hybrid",
7382 result: execute_runtime_hybrid_query(&self.inner.db, &hybrid)?,
7383 affected_rows: 0,
7384 statement_type: "select",
7385 bookmark: None,
7386 }),
7387 QueryExpr::Insert(ref insert) if super::red_schema::is_virtual_table(&insert.table) => {
7388 Err(RedDBError::Query(
7389 super::red_schema::READ_ONLY_ERROR.to_string(),
7390 ))
7391 }
7392 QueryExpr::Update(ref update) if super::red_schema::is_virtual_table(&update.table) => {
7393 Err(RedDBError::Query(
7394 super::red_schema::READ_ONLY_ERROR.to_string(),
7395 ))
7396 }
7397 QueryExpr::Delete(ref delete) if super::red_schema::is_virtual_table(&delete.table) => {
7398 Err(RedDBError::Query(
7399 super::red_schema::READ_ONLY_ERROR.to_string(),
7400 ))
7401 }
7402 QueryExpr::Insert(ref insert) => self
7403 .with_deferred_store_wal_for_dml(self.insert_may_emit_events(insert), || {
7404 self.execute_insert(query_str, insert)
7405 }),
7406 QueryExpr::Update(ref update) => self
7407 .with_deferred_store_wal_for_dml(self.update_may_emit_events(update), || {
7408 self.execute_update(query_str, update)
7409 }),
7410 QueryExpr::Delete(ref delete) => self
7411 .with_deferred_store_wal_for_dml(self.delete_may_emit_events(delete), || {
7412 self.execute_delete(query_str, delete)
7413 }),
7414 QueryExpr::SearchCommand(ref cmd) => self.execute_search_command(query_str, cmd),
7415 QueryExpr::Ask(ref ask) => self.execute_ask(query_str, ask),
7416 _ => Err(RedDBError::Query(format!(
7417 "prepared-statement execution does not support {statement} statements"
7418 ))),
7419 }
7420 }
7421
7422 fn execute_table_function(
7429 &self,
7430 name: &str,
7431 args: &[String],
7432 named_args: &[(String, f64)],
7433 ) -> RedDBResult<crate::storage::query::unified::UnifiedResult> {
7434 if !is_graph_tvf_name(name) {
7435 return Err(RedDBError::Query(format!("unknown table function: {name}")));
7436 }
7437 if args.len() != 1 {
7439 return Err(RedDBError::Query(format!(
7440 "table function '{name}' takes exactly 1 graph argument, got {}",
7441 args.len()
7442 )));
7443 }
7444
7445 let (nodes, edges) = self.materialize_whole_graph_abstract()?;
7450 self.dispatch_graph_algorithm(name, nodes, edges, named_args)
7451 }
7452
7453 fn execute_inline_graph_function(
7463 &self,
7464 name: &str,
7465 nodes_query: &QueryExpr,
7466 edges_query: &QueryExpr,
7467 named_args: &[(String, f64)],
7468 ) -> RedDBResult<crate::storage::query::unified::UnifiedResult> {
7469 if !is_graph_tvf_name(name) {
7470 return Err(RedDBError::Query(format!("unknown table function: {name}")));
7471 }
7472
7473 let node_result = self.execute_query_expr(nodes_query.clone())?.result;
7474 let nodes = inline_node_ids(name, &node_result)?;
7475
7476 let edge_result = self.execute_query_expr(edges_query.clone())?.result;
7477 let edges = inline_edges(name, &edge_result)?;
7478
7479 self.dispatch_graph_algorithm(name, nodes, edges, named_args)
7480 }
7481
7482 fn materialize_whole_graph_abstract(
7485 &self,
7486 ) -> RedDBResult<(
7487 Vec<String>,
7488 Vec<(
7489 String,
7490 String,
7491 crate::storage::engine::graph_algorithms::Weight,
7492 )>,
7493 )> {
7494 use crate::storage::engine::graph_algorithms;
7495
7496 let graph = super::graph_dsl::materialize_graph_with_projection(
7497 self.inner.db.store().as_ref(),
7498 None,
7499 )?;
7500 let nodes: Vec<String> = graph.iter_nodes().map(|n| n.id.clone()).collect();
7501 let edges: Vec<(String, String, graph_algorithms::Weight)> = graph
7502 .iter_all_edges()
7503 .into_iter()
7504 .map(|e| (e.source_id, e.target_id, e.weight))
7505 .collect();
7506 Ok((nodes, edges))
7507 }
7508
7509 fn try_resolve_analytics_view(
7524 &self,
7525 table: &TableQuery,
7526 frame: &dyn super::statement_frame::ReadFrame,
7527 ) -> RedDBResult<Option<crate::storage::query::unified::UnifiedResult>> {
7528 let full = table.table.as_str();
7529 let Some(dot) = full.rfind('.') else {
7530 return Ok(None);
7531 };
7532 if self.inner.db.store().get_collection(full).is_some() {
7534 return Ok(None);
7535 }
7536 let graph_name = &full[..dot];
7537 let output_name = &full[dot + 1..];
7538 let Some(output) = crate::catalog::AnalyticsOutput::from_str(output_name) else {
7539 return Ok(None);
7540 };
7541
7542 let contracts = self.inner.db.collection_contracts();
7543 let Some(contract) = contracts.iter().find(|c| c.name == graph_name) else {
7544 return Ok(None);
7545 };
7546 if contract.declared_model != crate::catalog::CollectionModel::Graph {
7547 return Ok(None);
7548 }
7549 let Some(view) = contract
7550 .analytics_config
7551 .iter()
7552 .find(|view| view.output == output)
7553 else {
7554 return Err(RedDBError::Query(format!(
7557 "analytics output '{output_name}' is not enabled on graph '{graph_name}'; declare it with WITH ANALYTICS (...)"
7558 )));
7559 };
7560
7561 let parent_query = TableQuery::new(graph_name);
7565 if self
7566 .authorize_relational_table_select(parent_query, frame)?
7567 .is_none()
7568 {
7569 return Err(RedDBError::Query(format!(
7570 "permission denied: policy on graph '{graph_name}' denies analytics view '{output_name}'"
7571 )));
7572 }
7573
7574 let (algorithm, named_args) = analytics_view_algorithm(graph_name, view)?;
7575 let (nodes, edges) = self.materialize_whole_graph_abstract()?;
7576 let result = self.dispatch_graph_algorithm(&algorithm, nodes, edges, &named_args)?;
7577 Ok(Some(result))
7578 }
7579
7580 fn dispatch_graph_algorithm(
7587 &self,
7588 name: &str,
7589 nodes: Vec<String>,
7590 edges: Vec<(
7591 String,
7592 String,
7593 crate::storage::engine::graph_algorithms::Weight,
7594 )>,
7595 named_args: &[(String, f64)],
7596 ) -> RedDBResult<crate::storage::query::unified::UnifiedResult> {
7597 use crate::storage::engine::graph_algorithms;
7598 use crate::storage::query::unified::UnifiedResult;
7599 use crate::storage::schema::Value;
7600
7601 if name.eq_ignore_ascii_case("components") {
7602 reject_named_args(name, named_args)?;
7603 let assignment = graph_algorithms::connected_components(&nodes, &edges);
7604 let mut result =
7605 UnifiedResult::with_columns(vec!["node_id".into(), "island_id".into()]);
7606 for (node_id, island_id) in assignment {
7607 let mut record = UnifiedRecord::new();
7608 record.set("node_id", Value::text(node_id));
7609 record.set("island_id", Value::Integer(island_id as i64));
7610 result.push(record);
7611 }
7612 return Ok(result);
7613 }
7614
7615 if name.eq_ignore_ascii_case("louvain") {
7616 let resolution = louvain_resolution(named_args)?;
7621 let assignment = graph_algorithms::louvain(&nodes, &edges, resolution);
7622 let mut result =
7623 UnifiedResult::with_columns(vec!["node_id".into(), "community_id".into()]);
7624 for (node_id, community_id) in assignment {
7625 let mut record = UnifiedRecord::new();
7626 record.set("node_id", Value::text(node_id));
7627 record.set("community_id", Value::Integer(community_id as i64));
7628 result.push(record);
7629 }
7630 return Ok(result);
7631 }
7632
7633 if name.eq_ignore_ascii_case("degree_centrality") {
7634 reject_named_args(name, named_args)?;
7635 let assignment = abstract_degree_centrality(&nodes, &edges);
7636 let mut result = UnifiedResult::with_columns(vec!["node_id".into(), "degree".into()]);
7637 for (node_id, degree) in assignment {
7638 let mut record = UnifiedRecord::new();
7639 record.set("node_id", Value::text(node_id));
7640 record.set("degree", Value::Integer(degree as i64));
7641 result.push(record);
7642 }
7643 return Ok(result);
7644 }
7645
7646 if name.eq_ignore_ascii_case("shortest_path") {
7647 let mut src: Option<String> = None;
7653 let mut dst: Option<String> = None;
7654 let mut max_hops: Option<usize> = None;
7655 let as_node_id = |key: &str, value: f64| -> RedDBResult<String> {
7656 if !value.is_finite() || value < 0.0 || value.fract() != 0.0 {
7657 return Err(RedDBError::Query(format!(
7658 "table function 'shortest_path' argument '{key}' must be a non-negative integer node id, got {value}"
7659 )));
7660 }
7661 Ok((value as i64).to_string())
7662 };
7663 for (key, value) in named_args {
7664 if key.eq_ignore_ascii_case("src") {
7665 src = Some(as_node_id("src", *value)?);
7666 } else if key.eq_ignore_ascii_case("dst") {
7667 dst = Some(as_node_id("dst", *value)?);
7668 } else if key.eq_ignore_ascii_case("max_hops") {
7669 if !value.is_finite() || *value < 0.0 || value.fract() != 0.0 {
7670 return Err(RedDBError::Query(format!(
7671 "table function 'shortest_path' max_hops must be a non-negative integer, got {value}"
7672 )));
7673 }
7674 max_hops = Some(*value as usize);
7675 } else {
7676 return Err(RedDBError::Query(format!(
7677 "table function 'shortest_path' has no named argument '{key}' (expected 'src', 'dst', 'max_hops')"
7678 )));
7679 }
7680 }
7681 let src = src.ok_or_else(|| {
7682 RedDBError::Query(
7683 "table function 'shortest_path' requires named argument 'src'".to_string(),
7684 )
7685 })?;
7686 let dst = dst.ok_or_else(|| {
7687 RedDBError::Query(
7688 "table function 'shortest_path' requires named argument 'dst'".to_string(),
7689 )
7690 })?;
7691
7692 let mut result = UnifiedResult::with_columns(vec![
7699 "hop".into(),
7700 "node_id".into(),
7701 "cumulative_weight".into(),
7702 ]);
7703 if let Some(path) =
7704 graph_algorithms::shortest_path(&nodes, &edges, &src, &dst, max_hops)
7705 {
7706 for (hop, (node_id, cumulative_weight)) in path.into_iter().enumerate() {
7707 let mut record = UnifiedRecord::new();
7708 record.set("hop", Value::Integer(hop as i64));
7709 record.set("node_id", Value::text(node_id));
7710 record.set("cumulative_weight", Value::Float(cumulative_weight));
7711 result.push(record);
7712 }
7713 }
7714 return Ok(result);
7715 }
7716 if name.eq_ignore_ascii_case("betweenness") {
7721 reject_named_args(name, named_args)?;
7722 return Ok(Self::centrality_result(graph_algorithms::betweenness(
7723 &nodes, &edges,
7724 )));
7725 }
7726 if name.eq_ignore_ascii_case("eigenvector") {
7727 let mut max_iterations = 100_usize;
7730 let mut tolerance = 1e-6_f64;
7731 for (key, value) in named_args {
7732 if key.eq_ignore_ascii_case("max_iterations") {
7733 max_iterations = parse_positive_iterations("eigenvector", value)?;
7734 } else if key.eq_ignore_ascii_case("tolerance") {
7735 if !value.is_finite() || *value <= 0.0 {
7736 return Err(RedDBError::Query(format!(
7737 "table function 'eigenvector' tolerance must be > 0, got {value}"
7738 )));
7739 }
7740 tolerance = *value;
7741 } else {
7742 return Err(RedDBError::Query(format!(
7743 "table function 'eigenvector' has no named argument '{key}' (expected 'max_iterations' or 'tolerance')"
7744 )));
7745 }
7746 }
7747 return Ok(Self::centrality_result(graph_algorithms::eigenvector(
7748 &nodes,
7749 &edges,
7750 max_iterations,
7751 tolerance,
7752 )));
7753 }
7754 if name.eq_ignore_ascii_case("pagerank") {
7755 let mut damping = 0.85_f64;
7758 let mut max_iterations = 100_usize;
7759 for (key, value) in named_args {
7760 if key.eq_ignore_ascii_case("damping") {
7761 if !value.is_finite() || *value <= 0.0 || *value >= 1.0 {
7762 return Err(RedDBError::Query(format!(
7763 "table function 'pagerank' damping must be in (0, 1), got {value}"
7764 )));
7765 }
7766 damping = *value;
7767 } else if key.eq_ignore_ascii_case("max_iterations") {
7768 max_iterations = parse_positive_iterations("pagerank", value)?;
7769 } else {
7770 return Err(RedDBError::Query(format!(
7771 "table function 'pagerank' has no named argument '{key}' (expected 'damping' or 'max_iterations')"
7772 )));
7773 }
7774 }
7775 return Ok(Self::centrality_result(graph_algorithms::pagerank(
7776 &nodes,
7777 &edges,
7778 damping,
7779 max_iterations,
7780 )));
7781 }
7782 Err(RedDBError::Query(format!("unknown table function: {name}")))
7783 }
7784
7785 fn execute_components_tvf(
7792 &self,
7793 _collection: &str,
7794 ) -> RedDBResult<crate::storage::query::unified::UnifiedResult> {
7795 use crate::storage::engine::graph_algorithms;
7796 use crate::storage::query::unified::UnifiedResult;
7797 use crate::storage::schema::Value;
7798
7799 let graph = super::graph_dsl::materialize_graph_with_projection(
7805 self.inner.db.store().as_ref(),
7806 None,
7807 )?;
7808
7809 let nodes: Vec<String> = graph.iter_nodes().map(|n| n.id.clone()).collect();
7811 let edges: Vec<(String, String, graph_algorithms::Weight)> = graph
7812 .iter_all_edges()
7813 .into_iter()
7814 .map(|e| (e.source_id, e.target_id, e.weight))
7815 .collect();
7816
7817 let assignment = graph_algorithms::connected_components(&nodes, &edges);
7818
7819 let mut result = UnifiedResult::with_columns(vec!["node_id".into(), "island_id".into()]);
7821 for (node_id, island_id) in assignment {
7822 let mut record = UnifiedRecord::new();
7823 record.set("node_id", Value::text(node_id));
7824 record.set("island_id", Value::Integer(island_id as i64));
7825 result.push(record);
7826 }
7827 Ok(result)
7828 }
7829
7830 fn execute_louvain_tvf(
7840 &self,
7841 _collection: &str,
7842 resolution: f64,
7843 ) -> RedDBResult<crate::storage::query::unified::UnifiedResult> {
7844 use crate::storage::engine::graph_algorithms;
7845 use crate::storage::query::unified::UnifiedResult;
7846 use crate::storage::schema::Value;
7847
7848 let graph = super::graph_dsl::materialize_graph_with_projection(
7849 self.inner.db.store().as_ref(),
7850 None,
7851 )?;
7852
7853 let nodes: Vec<String> = graph.iter_nodes().map(|n| n.id.clone()).collect();
7854 let edges: Vec<(String, String, graph_algorithms::Weight)> = graph
7855 .iter_all_edges()
7856 .into_iter()
7857 .map(|e| (e.source_id, e.target_id, e.weight))
7858 .collect();
7859
7860 let assignment = graph_algorithms::louvain(&nodes, &edges, resolution);
7861
7862 let mut result = UnifiedResult::with_columns(vec!["node_id".into(), "community_id".into()]);
7864 for (node_id, community_id) in assignment {
7865 let mut record = UnifiedRecord::new();
7866 record.set("node_id", Value::text(node_id));
7867 record.set("community_id", Value::Integer(community_id as i64));
7868 result.push(record);
7869 }
7870 Ok(result)
7871 }
7872
7873 fn centrality_result(
7876 rows: Vec<(String, f64)>,
7877 ) -> crate::storage::query::unified::UnifiedResult {
7878 use crate::storage::query::unified::UnifiedResult;
7879 use crate::storage::schema::Value;
7880 let mut result = UnifiedResult::with_columns(vec!["node_id".into(), "score".into()]);
7881 for (node_id, score) in rows {
7882 let mut record = UnifiedRecord::new();
7883 record.set("node_id", Value::text(node_id));
7884 record.set("score", Value::Float(score));
7885 result.push(record);
7886 }
7887 result
7888 }
7889
7890 fn try_fast_entity_lookup(&self, query: &str) -> Option<RedDBResult<RuntimeQueryResult>> {
7893 let q = query.trim();
7896 if !q.starts_with("SELECT") && !q.starts_with("select") {
7897 return None;
7898 }
7899
7900 let where_pos = q
7902 .find("WHERE _entity_id")
7903 .or_else(|| q.find("where _entity_id"))?;
7904 let after_field = &q[where_pos + 16..].trim_start(); let after_eq = after_field.strip_prefix('=')?.trim_start();
7906
7907 let id_str = after_eq.trim();
7909 let entity_id: u64 = id_str.parse().ok()?;
7910
7911 let from_pos = q.find("FROM ").or_else(|| q.find("from "))? + 5;
7913 let table = q[from_pos..where_pos].trim();
7914 if table.is_empty()
7915 || table.contains(' ') && !table.contains(" AS ") && !table.contains(" as ")
7916 {
7917 return None; }
7919 let table_name = table.split_whitespace().next()?;
7920
7921 let store = self.inner.db.store();
7927 let entity = store
7928 .get(
7929 table_name,
7930 crate::storage::unified::EntityId::new(entity_id),
7931 )
7932 .filter(entity_visible_under_current_snapshot)
7933 .filter(|entity| {
7934 self.inner
7935 .db
7936 .replica_allows_entity_at_read(table_name, entity)
7937 });
7938
7939 let count = if entity.is_some() { 1u64 } else { 0 };
7940
7941 let records: Vec<crate::storage::query::unified::UnifiedRecord> = entity
7947 .as_ref()
7948 .and_then(|e| runtime_table_record_from_entity(e.clone()))
7949 .into_iter()
7950 .collect();
7951
7952 let json = match entity {
7953 Some(ref e) => execute_runtime_serialize_single_entity(e),
7954 None => r#"{"columns":[],"record_count":0,"selection":{"scope":"any"},"records":[]}"#
7955 .to_string(),
7956 };
7957
7958 Some(Ok(RuntimeQueryResult {
7959 query: query.to_string(),
7960 mode: crate::storage::query::modes::QueryMode::Sql,
7961 statement: "select",
7962 engine: "fast-entity-lookup",
7963 result: crate::storage::query::unified::UnifiedResult {
7964 columns: Vec::new(),
7965 records,
7966 stats: crate::storage::query::unified::QueryStats {
7967 rows_scanned: count,
7968 ..Default::default()
7969 },
7970 pre_serialized_json: Some(json),
7971 },
7972 affected_rows: 0,
7973 statement_type: "select",
7974 bookmark: None,
7975 }))
7976 }
7977
7978 pub(crate) fn invalidate_plan_cache(&self) {
7979 self.inner.query_cache.write().clear();
7980 self.inner
7981 .ddl_epoch
7982 .fetch_add(1, std::sync::atomic::Ordering::Release);
7983 }
7984
7985 pub fn ddl_epoch(&self) -> u64 {
7989 self.inner
7990 .ddl_epoch
7991 .load(std::sync::atomic::Ordering::Acquire)
7992 }
7993
7994 pub(crate) fn clear_table_planner_stats(&self, table: &str) {
7995 let store = self.inner.db.store();
7996 crate::storage::query::planner::stats_catalog::clear_table_stats(store.as_ref(), table);
7997 self.invalidate_plan_cache();
7998 }
7999
8000 pub(crate) fn rehydrate_tenant_tables(&self) {
8009 let store = self.inner.db.store();
8010 let Some(manager) = store.get_collection("red_config") else {
8011 return;
8012 };
8013 for entity in manager.query_all(|_| true) {
8018 let crate::storage::unified::entity::EntityData::Row(row) = &entity.data else {
8019 continue;
8020 };
8021 let Some(named) = &row.named else { continue };
8022 let Some(crate::storage::schema::Value::Text(key)) = named.get("key") else {
8023 continue;
8024 };
8025 let Some(rest) = key.strip_prefix("tenant_tables.") else {
8027 continue;
8028 };
8029 let Some((table, suffix)) = rest.rsplit_once('.') else {
8030 crate::telemetry::operator_event::OperatorEvent::SchemaCorruption {
8036 collection: "red_config".to_string(),
8037 detail: format!("malformed tenant_tables key: {key}"),
8038 }
8039 .emit_global();
8040 continue;
8041 };
8042 if suffix != "column" {
8043 crate::telemetry::operator_event::OperatorEvent::SchemaCorruption {
8044 collection: "red_config".to_string(),
8045 detail: format!("unexpected tenant_tables suffix: {key}"),
8046 }
8047 .emit_global();
8048 continue;
8049 }
8050 match named.get("value") {
8051 Some(crate::storage::schema::Value::Text(column)) => {
8052 self.register_tenant_table(table, column);
8053 }
8054 Some(crate::storage::schema::Value::Null) | None => {
8056 self.unregister_tenant_table(table);
8057 }
8058 _ => {}
8059 }
8060 }
8061 }
8062
8063 pub(crate) fn rehydrate_materialized_view_descriptors(&self) {
8075 let store = self.inner.db.store();
8076 let descriptors = crate::runtime::continuous_materialized_view::load_all(store.as_ref());
8077 for descriptor in descriptors {
8078 let parsed = match crate::storage::query::parser::parse(&descriptor.source_sql) {
8079 Ok(qc) => qc,
8080 Err(err) => {
8081 crate::telemetry::operator_event::OperatorEvent::SchemaCorruption {
8082 collection:
8083 crate::runtime::continuous_materialized_view::CATALOG_COLLECTION
8084 .to_string(),
8085 detail: format!(
8086 "failed to re-parse materialized-view source for {}: {err}",
8087 descriptor.name
8088 ),
8089 }
8090 .emit_global();
8091 continue;
8092 }
8093 };
8094 let crate::storage::query::ast::QueryExpr::CreateView(create) = parsed.query else {
8095 crate::telemetry::operator_event::OperatorEvent::SchemaCorruption {
8096 collection: crate::runtime::continuous_materialized_view::CATALOG_COLLECTION
8097 .to_string(),
8098 detail: format!(
8099 "materialized-view source for {} did not re-parse as CREATE VIEW",
8100 descriptor.name
8101 ),
8102 }
8103 .emit_global();
8104 continue;
8105 };
8106 let view_name = create.name.clone();
8108 self.inner
8109 .views
8110 .write()
8111 .insert(view_name.clone(), Arc::new(create));
8112 use crate::storage::cache::result::{MaterializedViewDef, RefreshPolicy};
8114 let refresh = match descriptor.refresh_every_ms {
8115 Some(ms) => RefreshPolicy::Periodic(std::time::Duration::from_millis(ms)),
8116 None => RefreshPolicy::Manual,
8117 };
8118 let def = MaterializedViewDef {
8119 name: view_name.clone(),
8120 query: format!("<parsed view {}>", view_name),
8121 dependencies: descriptor.source_collections.clone(),
8122 refresh,
8123 retention_duration_ms: descriptor.retention_duration_ms,
8124 };
8125 self.inner.materialized_views.write().register(def);
8126 if let Err(err) = self.ensure_materialized_view_backing(&view_name) {
8127 crate::telemetry::operator_event::OperatorEvent::SchemaCorruption {
8128 collection: crate::runtime::continuous_materialized_view::CATALOG_COLLECTION
8129 .to_string(),
8130 detail: format!(
8131 "failed to rehydrate backing collection for materialized view {view_name}: {err}"
8132 ),
8133 }
8134 .emit_global();
8135 }
8136 }
8137 self.invalidate_plan_cache();
8140 }
8141
8142 pub(crate) fn rehydrate_declared_column_schemas(&self) {
8143 let store = self.inner.db.store();
8144 for contract in self.inner.db.collection_contracts() {
8145 let columns: Vec<String> = contract
8146 .declared_columns
8147 .iter()
8148 .map(|column| column.name.clone())
8149 .collect();
8150 let Some(manager) = store.get_collection(&contract.name) else {
8151 continue;
8152 };
8153 manager.set_column_schema_if_empty(columns);
8154 }
8155 }
8156
8157 pub fn register_tenant_table(&self, table: &str, column: &str) {
8162 use crate::storage::query::ast::{
8163 CompareOp, CreatePolicyQuery, Expr, FieldRef, Filter, Span,
8164 };
8165 self.inner
8166 .tenant_tables
8167 .write()
8168 .insert(table.to_string(), column.to_string());
8169
8170 let lhs = Expr::Column {
8176 field: FieldRef::TableColumn {
8177 table: table.to_string(),
8178 column: column.to_string(),
8179 },
8180 span: Span::synthetic(),
8181 };
8182 let rhs = Expr::FunctionCall {
8183 name: "CURRENT_TENANT".to_string(),
8184 args: Vec::new(),
8185 span: Span::synthetic(),
8186 };
8187 let policy_filter = Filter::CompareExpr {
8188 lhs,
8189 op: CompareOp::Eq,
8190 rhs,
8191 };
8192
8193 let policy = CreatePolicyQuery {
8194 name: "__tenant_iso".to_string(),
8195 table: table.to_string(),
8196 action: None, role: None, using: Box::new(policy_filter),
8199 target_kind: crate::storage::query::ast::PolicyTargetKind::Table,
8206 };
8207
8208 self.inner.rls_policies.write().insert(
8210 (table.to_string(), "__tenant_iso".to_string()),
8211 Arc::new(policy),
8212 );
8213 self.inner
8214 .rls_enabled_tables
8215 .write()
8216 .insert(table.to_string());
8217
8218 self.ensure_tenant_index(table, column);
8224 }
8225
8226 fn ensure_tenant_index(&self, table: &str, column: &str) {
8234 if column.contains('.') {
8235 return;
8236 }
8237 let index_name = format!("__tenant_idx_{table}");
8238 let registry = self.inner.index_store.list_indices(table);
8239 if registry.iter().any(|idx| idx.name == index_name) {
8240 return;
8241 }
8242 if registry
8243 .iter()
8244 .any(|idx| idx.columns.first().map(|c| c.as_str()) == Some(column))
8245 {
8246 return;
8247 }
8248
8249 let store = self.inner.db.store();
8250 let Some(manager) = store.get_collection(table) else {
8251 return;
8252 };
8253 let entities = manager.query_all(|_| true);
8254 let entity_fields: Vec<(
8255 crate::storage::unified::EntityId,
8256 Vec<(String, crate::storage::schema::Value)>,
8257 )> = entities
8258 .iter()
8259 .map(|e| {
8260 let fields = match &e.data {
8261 crate::storage::EntityData::Row(row) => {
8262 if let Some(ref named) = row.named {
8263 named.iter().map(|(k, v)| (k.clone(), v.clone())).collect()
8264 } else if let Some(ref schema) = row.schema {
8265 schema
8266 .iter()
8267 .zip(row.columns.iter())
8268 .map(|(k, v)| (k.clone(), v.clone()))
8269 .collect()
8270 } else {
8271 Vec::new()
8272 }
8273 }
8274 crate::storage::EntityData::Node(node) => node
8275 .properties
8276 .iter()
8277 .map(|(k, v)| (k.clone(), v.clone()))
8278 .collect(),
8279 _ => Vec::new(),
8280 };
8281 (e.id, fields)
8282 })
8283 .collect();
8284
8285 let columns = vec![column.to_string()];
8286 if self
8287 .inner
8288 .index_store
8289 .create_index(
8290 &index_name,
8291 table,
8292 &columns,
8293 super::index_store::IndexMethodKind::Hash,
8294 false,
8295 &entity_fields,
8296 )
8297 .is_err()
8298 {
8299 return;
8300 }
8301 self.inner
8302 .index_store
8303 .register(super::index_store::RegisteredIndex {
8304 name: index_name,
8305 collection: table.to_string(),
8306 columns,
8307 method: super::index_store::IndexMethodKind::Hash,
8308 unique: false,
8309 });
8310 self.invalidate_plan_cache();
8311 }
8312
8313 fn drop_tenant_index(&self, table: &str) {
8316 let index_name = format!("__tenant_idx_{table}");
8317 self.inner.index_store.drop_index(&index_name, table);
8318 }
8319
8320 pub fn tenant_column(&self, table: &str) -> Option<String> {
8324 self.inner.tenant_tables.read().get(table).cloned()
8325 }
8326
8327 pub fn unregister_tenant_table(&self, table: &str) {
8331 self.inner.tenant_tables.write().remove(table);
8332 self.inner
8333 .rls_policies
8334 .write()
8335 .remove(&(table.to_string(), "__tenant_iso".to_string()));
8336 self.drop_tenant_index(table);
8337 let has_other_policies = self
8339 .inner
8340 .rls_policies
8341 .read()
8342 .keys()
8343 .any(|(t, _)| t == table);
8344 if !has_other_policies {
8345 self.inner.rls_enabled_tables.write().remove(table);
8346 }
8347 }
8348
8349 pub(crate) fn record_pending_tombstone(
8355 &self,
8356 conn_id: u64,
8357 collection: &str,
8358 id: crate::storage::unified::entity::EntityId,
8359 stamper_xid: crate::storage::transaction::snapshot::Xid,
8360 previous_xmax: crate::storage::transaction::snapshot::Xid,
8361 ) {
8362 self.inner
8363 .pending_tombstones
8364 .write()
8365 .entry(conn_id)
8366 .or_default()
8367 .push((collection.to_string(), id, stamper_xid, previous_xmax));
8368 }
8369
8370 pub(crate) fn record_pending_versioned_update(
8371 &self,
8372 conn_id: u64,
8373 collection: &str,
8374 old_id: crate::storage::unified::entity::EntityId,
8375 new_id: crate::storage::unified::entity::EntityId,
8376 stamper_xid: crate::storage::transaction::snapshot::Xid,
8377 previous_xmax: crate::storage::transaction::snapshot::Xid,
8378 ) {
8379 self.inner
8380 .pending_versioned_updates
8381 .write()
8382 .entry(conn_id)
8383 .or_default()
8384 .push((
8385 collection.to_string(),
8386 old_id,
8387 new_id,
8388 stamper_xid,
8389 previous_xmax,
8390 ));
8391 }
8392
8393 fn with_deferred_store_wal_if_transaction<T>(
8394 &self,
8395 f: impl FnOnce() -> RedDBResult<T>,
8396 ) -> RedDBResult<T> {
8397 let conn_id = current_connection_id();
8398 if !self.inner.tx_contexts.read().contains_key(&conn_id) {
8399 return f();
8400 }
8401
8402 crate::storage::UnifiedStore::begin_deferred_store_wal_capture();
8403 let result = f();
8404 let captured = crate::storage::UnifiedStore::take_deferred_store_wal_capture();
8405 match result {
8406 Ok(value) => {
8407 self.record_pending_store_wal_actions(conn_id, captured);
8408 Ok(value)
8409 }
8410 Err(err) => Err(err),
8411 }
8412 }
8413
8414 fn with_deferred_store_wal_for_dml<T>(
8415 &self,
8416 capture_autocommit_events: bool,
8417 f: impl FnOnce() -> RedDBResult<T>,
8418 ) -> RedDBResult<T> {
8419 let conn_id = current_connection_id();
8420 if self.inner.tx_contexts.read().contains_key(&conn_id) {
8421 return self.with_deferred_store_wal_if_transaction(f);
8422 }
8423 if !capture_autocommit_events {
8424 return f();
8425 }
8426
8427 crate::storage::UnifiedStore::begin_deferred_store_wal_capture();
8428 let result = f();
8429 let captured = crate::storage::UnifiedStore::take_deferred_store_wal_capture();
8430 self.inner
8431 .db
8432 .store()
8433 .append_deferred_store_wal_actions(captured)
8434 .map_err(|err| RedDBError::Internal(err.to_string()))?;
8435 result
8436 }
8437
8438 fn insert_may_emit_events(&self, query: &InsertQuery) -> bool {
8439 !query.suppress_events
8440 && self.collection_has_event_subscriptions_for_operation(
8441 &query.table,
8442 crate::catalog::SubscriptionOperation::Insert,
8443 )
8444 }
8445
8446 fn update_may_emit_events(&self, query: &UpdateQuery) -> bool {
8447 !query.suppress_events
8448 && self.collection_has_event_subscriptions_for_operation(
8449 &query.table,
8450 crate::catalog::SubscriptionOperation::Update,
8451 )
8452 }
8453
8454 fn delete_may_emit_events(&self, query: &DeleteQuery) -> bool {
8455 !query.suppress_events
8456 && self.collection_has_event_subscriptions_for_operation(
8457 &query.table,
8458 crate::catalog::SubscriptionOperation::Delete,
8459 )
8460 }
8461
8462 fn collection_has_event_subscriptions_for_operation(
8463 &self,
8464 collection: &str,
8465 operation: crate::catalog::SubscriptionOperation,
8466 ) -> bool {
8467 let Some(contract) = self.db().collection_contract_arc(collection) else {
8468 return false;
8469 };
8470 contract.subscriptions.iter().any(|subscription| {
8471 subscription.enabled
8472 && (subscription.ops_filter.is_empty()
8473 || subscription.ops_filter.contains(&operation))
8474 })
8475 }
8476
8477 fn record_pending_store_wal_actions(
8478 &self,
8479 conn_id: u64,
8480 actions: crate::storage::unified::DeferredStoreWalActions,
8481 ) {
8482 if actions.is_empty() {
8483 return;
8484 }
8485 let mut guard = self.inner.pending_store_wal_actions.write();
8486 guard.entry(conn_id).or_default().extend(actions);
8487 }
8488
8489 fn flush_pending_store_wal_actions(&self, conn_id: u64) -> RedDBResult<()> {
8490 let Some(actions) = self
8491 .inner
8492 .pending_store_wal_actions
8493 .write()
8494 .remove(&conn_id)
8495 else {
8496 return Ok(());
8497 };
8498 self.inner
8499 .db
8500 .store()
8501 .append_deferred_store_wal_actions(actions)
8502 .map_err(|err| RedDBError::Internal(err.to_string()))
8503 }
8504
8505 fn discard_pending_store_wal_actions(&self, conn_id: u64) {
8506 self.inner
8507 .pending_store_wal_actions
8508 .write()
8509 .remove(&conn_id);
8510 }
8511
8512 fn xid_conflicts_with_snapshot(
8513 &self,
8514 xid: crate::storage::transaction::snapshot::Xid,
8515 snapshot: &crate::storage::transaction::snapshot::Snapshot,
8516 own_xids: &std::collections::HashSet<crate::storage::transaction::snapshot::Xid>,
8517 ) -> bool {
8518 xid != 0
8519 && !own_xids.contains(&xid)
8520 && !self.inner.snapshot_manager.is_aborted(xid)
8521 && !self.inner.snapshot_manager.is_active(xid)
8522 && (xid > snapshot.xid || snapshot.in_progress.contains(&xid))
8523 }
8524
8525 fn conflict_error(
8526 collection: &str,
8527 logical_id: crate::storage::unified::entity::EntityId,
8528 xid: crate::storage::transaction::snapshot::Xid,
8529 ) -> RedDBError {
8530 RedDBError::Query(format!(
8531 "serialization conflict: table row {collection}/{} was modified by concurrent transaction {xid}",
8532 logical_id.raw()
8533 ))
8534 }
8535
8536 fn check_logical_row_conflict(
8537 &self,
8538 collection: &str,
8539 logical_id: crate::storage::unified::entity::EntityId,
8540 excluded_ids: &[crate::storage::unified::entity::EntityId],
8541 snapshot: &crate::storage::transaction::snapshot::Snapshot,
8542 own_xids: &std::collections::HashSet<crate::storage::transaction::snapshot::Xid>,
8543 ) -> RedDBResult<()> {
8544 let store = self.inner.db.store();
8545 let Some(manager) = store.get_collection(collection) else {
8546 return Ok(());
8547 };
8548
8549 for candidate in manager.query_all(|_| true) {
8550 if excluded_ids.contains(&candidate.id) || candidate.logical_id() != logical_id {
8551 continue;
8552 }
8553 if self.xid_conflicts_with_snapshot(candidate.xmin, snapshot, own_xids) {
8554 return Err(Self::conflict_error(collection, logical_id, candidate.xmin));
8555 }
8556 if self.xid_conflicts_with_snapshot(candidate.xmax, snapshot, own_xids) {
8557 return Err(Self::conflict_error(collection, logical_id, candidate.xmax));
8558 }
8559 }
8560 Ok(())
8561 }
8562
8563 pub(crate) fn check_table_row_write_conflicts(
8564 &self,
8565 conn_id: u64,
8566 snapshot: &crate::storage::transaction::snapshot::Snapshot,
8567 own_xids: &std::collections::HashSet<crate::storage::transaction::snapshot::Xid>,
8568 ) -> RedDBResult<()> {
8569 let versioned_updates = self
8570 .inner
8571 .pending_versioned_updates
8572 .read()
8573 .get(&conn_id)
8574 .cloned()
8575 .unwrap_or_default();
8576 let tombstones = self
8577 .inner
8578 .pending_tombstones
8579 .read()
8580 .get(&conn_id)
8581 .cloned()
8582 .unwrap_or_default();
8583
8584 let store = self.inner.db.store();
8585 for (collection, old_id, new_id, xid, previous_xmax) in versioned_updates {
8586 let Some(manager) = store.get_collection(&collection) else {
8587 continue;
8588 };
8589 let Some(old) = manager.get(old_id) else {
8590 continue;
8591 };
8592 let logical_id = old.logical_id();
8593 if self.xid_conflicts_with_snapshot(previous_xmax, snapshot, own_xids) {
8594 return Err(Self::conflict_error(&collection, logical_id, previous_xmax));
8595 }
8596 if old.xmax != xid && self.xid_conflicts_with_snapshot(old.xmax, snapshot, own_xids) {
8597 return Err(Self::conflict_error(&collection, logical_id, old.xmax));
8598 }
8599 self.check_logical_row_conflict(
8600 &collection,
8601 logical_id,
8602 &[old_id, new_id],
8603 snapshot,
8604 own_xids,
8605 )?;
8606 }
8607
8608 for (collection, id, xid, previous_xmax) in tombstones {
8609 let Some(manager) = store.get_collection(&collection) else {
8610 continue;
8611 };
8612 let Some(entity) = manager.get(id) else {
8613 continue;
8614 };
8615 let logical_id = entity.logical_id();
8616 if self.xid_conflicts_with_snapshot(previous_xmax, snapshot, own_xids) {
8617 return Err(Self::conflict_error(&collection, logical_id, previous_xmax));
8618 }
8619 if entity.xmax != xid
8620 && self.xid_conflicts_with_snapshot(entity.xmax, snapshot, own_xids)
8621 {
8622 return Err(Self::conflict_error(&collection, logical_id, entity.xmax));
8623 }
8624 self.check_logical_row_conflict(&collection, logical_id, &[id], snapshot, own_xids)?;
8625 }
8626
8627 Ok(())
8628 }
8629
8630 pub(crate) fn restore_pending_write_stamps(&self, conn_id: u64) {
8631 let versioned_updates = self
8632 .inner
8633 .pending_versioned_updates
8634 .read()
8635 .get(&conn_id)
8636 .cloned()
8637 .unwrap_or_default();
8638 let tombstones = self
8639 .inner
8640 .pending_tombstones
8641 .read()
8642 .get(&conn_id)
8643 .cloned()
8644 .unwrap_or_default();
8645
8646 let store = self.inner.db.store();
8647 for (collection, old_id, _new_id, xid, _previous_xmax) in versioned_updates {
8648 if let Some(manager) = store.get_collection(&collection) {
8649 if let Some(mut entity) = manager.get(old_id) {
8650 entity.set_xmax(xid);
8651 let _ = manager.update(entity);
8652 }
8653 }
8654 }
8655 for (collection, id, xid, _previous_xmax) in tombstones {
8656 if let Some(manager) = store.get_collection(&collection) {
8657 if let Some(mut entity) = manager.get(id) {
8658 entity.set_xmax(xid);
8659 let _ = manager.update(entity);
8660 }
8661 }
8662 }
8663 }
8664
8665 pub(crate) fn finalize_pending_versioned_updates(&self, conn_id: u64) {
8666 self.inner
8667 .pending_versioned_updates
8668 .write()
8669 .remove(&conn_id);
8670 }
8671
8672 pub(crate) fn revive_pending_versioned_updates(&self, conn_id: u64) {
8673 let Some(pending) = self
8674 .inner
8675 .pending_versioned_updates
8676 .write()
8677 .remove(&conn_id)
8678 else {
8679 return;
8680 };
8681
8682 let store = self.inner.db.store();
8683 for (collection, old_id, new_id, xid, previous_xmax) in pending {
8684 if let Some(manager) = store.get_collection(&collection) {
8685 if let Some(mut old) = manager.get(old_id) {
8686 if old.xmax == xid {
8687 old.set_xmax(previous_xmax);
8688 let _ = manager.update(old);
8689 }
8690 }
8691 }
8692 let _ = store.delete_batch(&collection, &[new_id]);
8693 }
8694 }
8695
8696 pub(crate) fn revive_versioned_updates_since(&self, conn_id: u64, stamper_xid: u64) -> usize {
8697 let mut guard = self.inner.pending_versioned_updates.write();
8698 let Some(pending) = guard.get_mut(&conn_id) else {
8699 return 0;
8700 };
8701
8702 let store = self.inner.db.store();
8703 let mut reverted = 0usize;
8704 pending.retain(|(collection, old_id, new_id, xid, previous_xmax)| {
8705 if *xid < stamper_xid {
8706 return true;
8707 }
8708 if let Some(manager) = store.get_collection(collection) {
8709 if let Some(mut old) = manager.get(*old_id) {
8710 if old.xmax == *xid {
8711 old.set_xmax(*previous_xmax);
8712 let _ = manager.update(old);
8713 }
8714 }
8715 }
8716 let _ = store.delete_batch(collection, &[*new_id]);
8717 reverted += 1;
8718 false
8719 });
8720 if pending.is_empty() {
8721 guard.remove(&conn_id);
8722 }
8723 reverted
8724 }
8725
8726 pub(crate) fn finalize_pending_tombstones(&self, conn_id: u64) {
8731 let Some(pending) = self.inner.pending_tombstones.write().remove(&conn_id) else {
8732 return;
8733 };
8734 if pending.is_empty() {
8735 return;
8736 }
8737
8738 let store = self.inner.db.store();
8739 for (collection, id, _xid, _previous_xmax) in pending {
8740 store.context_index().remove_entity(id);
8741 self.cdc_emit(
8742 crate::replication::cdc::ChangeOperation::Delete,
8743 &collection,
8744 id.raw(),
8745 "entity",
8746 );
8747 }
8748 }
8749
8750 pub(crate) fn revive_pending_tombstones(&self, conn_id: u64) {
8757 let Some(pending) = self.inner.pending_tombstones.write().remove(&conn_id) else {
8758 return;
8759 };
8760
8761 let store = self.inner.db.store();
8762 for (collection, id, xid, previous_xmax) in pending {
8763 let Some(manager) = store.get_collection(&collection) else {
8764 continue;
8765 };
8766 if let Some(mut entity) = manager.get(id) {
8767 if entity.xmax == xid {
8768 entity.set_xmax(previous_xmax);
8769 let _ = manager.update(entity);
8770 }
8771 }
8772 }
8773 }
8774
8775 pub fn queue_wait_registry(
8777 &self,
8778 ) -> std::sync::Arc<crate::runtime::queue_wait_registry::QueueWaitRegistry> {
8779 self.inner.queue_wait_registry.clone()
8780 }
8781
8782 pub(crate) fn record_queue_wake(&self, scope: &str, queue: &str) {
8787 if self.current_xid().is_some() {
8788 let conn_id = current_connection_id();
8789 self.inner
8790 .pending_queue_wakes
8791 .write()
8792 .entry(conn_id)
8793 .or_default()
8794 .push((scope.to_string(), queue.to_string()));
8795 return;
8796 }
8797 self.inner.queue_wait_registry.notify(scope, queue);
8798 }
8799
8800 pub(crate) fn finalize_pending_queue_wakes(&self, conn_id: u64) {
8801 let Some(pending) = self.inner.pending_queue_wakes.write().remove(&conn_id) else {
8802 return;
8803 };
8804 for (scope, queue) in pending {
8805 self.inner.queue_wait_registry.notify(&scope, &queue);
8806 }
8807 }
8808
8809 pub(crate) fn discard_pending_queue_wakes(&self, conn_id: u64) {
8810 self.inner.pending_queue_wakes.write().remove(&conn_id);
8811 }
8812
8813 pub(crate) fn release_pending_claim_locks(&self, conn_id: u64) {
8814 self.inner
8815 .pending_claim_locks
8816 .write()
8817 .retain(|_, owner| *owner != conn_id);
8818 }
8819
8820 pub(crate) fn finalize_pending_kv_watch_events(&self, conn_id: u64) {
8821 let Some(pending) = self.inner.pending_kv_watch_events.write().remove(&conn_id) else {
8822 return;
8823 };
8824 for event in pending {
8825 self.cdc_emit_kv(
8826 event.op,
8827 &event.collection,
8828 &event.key,
8829 0,
8830 event.before,
8831 event.after,
8832 );
8833 }
8834 }
8835
8836 pub(crate) fn discard_pending_kv_watch_events(&self, conn_id: u64) {
8837 self.inner.pending_kv_watch_events.write().remove(&conn_id);
8838 }
8839
8840 fn materialize_graph_with_rls(
8849 &self,
8850 ) -> RedDBResult<(
8851 crate::storage::engine::GraphStore,
8852 std::collections::HashMap<
8853 String,
8854 std::collections::HashMap<String, crate::storage::schema::Value>,
8855 >,
8856 crate::storage::query::unified::EdgeProperties,
8857 )> {
8858 use crate::storage::engine::GraphStore;
8859 use crate::storage::query::ast::{PolicyAction, PolicyTargetKind};
8860 use crate::storage::unified::entity::{EntityData, EntityKind};
8861 use std::collections::{HashMap, HashSet};
8862
8863 let store = self.inner.db.store();
8864 let snap_ctx = capture_current_snapshot();
8865 let role = current_auth_identity().map(|(_, r)| r.as_str().to_string());
8866
8867 let graph = GraphStore::new();
8868 let mut node_properties: HashMap<String, HashMap<String, crate::storage::schema::Value>> =
8869 HashMap::new();
8870 let mut edge_properties: crate::storage::query::unified::EdgeProperties = HashMap::new();
8871 let mut allowed_nodes: HashSet<String> = HashSet::new();
8872
8873 let mut node_rls: HashMap<String, Option<crate::storage::query::ast::Filter>> =
8877 HashMap::new();
8878 let mut edge_rls: HashMap<String, Option<crate::storage::query::ast::Filter>> =
8879 HashMap::new();
8880
8881 let collections = store.list_collections();
8882
8883 for collection in &collections {
8885 let Some(manager) = store.get_collection(collection) else {
8886 continue;
8887 };
8888 let entities = manager.query_all(|_| true);
8889 for entity in entities {
8890 if !entity_visible_with_context(snap_ctx.as_ref(), &entity) {
8891 continue;
8892 }
8893 let EntityKind::GraphNode(ref node) = entity.kind else {
8894 continue;
8895 };
8896 if !node_passes_rls(self, collection, role.as_deref(), &mut node_rls, &entity) {
8897 continue;
8898 }
8899 let id_str = entity.id.raw().to_string();
8900 graph
8901 .add_node_with_label(
8902 &id_str,
8903 &node.label,
8904 &super::graph_node_label(&node.node_type),
8905 )
8906 .map_err(|err| RedDBError::Query(err.to_string()))?;
8907 allowed_nodes.insert(id_str.clone());
8908 if let EntityData::Node(node_data) = &entity.data {
8909 node_properties.insert(id_str, node_data.properties.clone());
8910 }
8911 }
8912 }
8913
8914 for collection in &collections {
8918 let Some(manager) = store.get_collection(collection) else {
8919 continue;
8920 };
8921 let entities = manager.query_all(|_| true);
8922 for entity in entities {
8923 if !entity_visible_with_context(snap_ctx.as_ref(), &entity) {
8924 continue;
8925 }
8926 let EntityKind::GraphEdge(ref edge) = entity.kind else {
8927 continue;
8928 };
8929 if !allowed_nodes.contains(&edge.from_node)
8930 || !allowed_nodes.contains(&edge.to_node)
8931 {
8932 continue;
8933 }
8934 if !edge_passes_rls(self, collection, role.as_deref(), &mut edge_rls, &entity) {
8935 continue;
8936 }
8937 let weight = match &entity.data {
8938 EntityData::Edge(e) => e.weight,
8939 _ => edge.weight as f32 / 1000.0,
8940 };
8941 let edge_label = super::graph_edge_label(&edge.label);
8942 graph
8943 .add_edge_with_label(&edge.from_node, &edge.to_node, &edge_label, weight)
8944 .map_err(|err| RedDBError::Query(err.to_string()))?;
8945 if let EntityData::Edge(edge_data) = &entity.data {
8946 edge_properties.insert(
8947 (edge.from_node.clone(), edge_label, edge.to_node.clone()),
8948 edge_data.properties.clone(),
8949 );
8950 }
8951 }
8952 }
8953
8954 let _ = (PolicyAction::Select, PolicyTargetKind::Nodes);
8958
8959 Ok((graph, node_properties, edge_properties))
8960 }
8961
8962 pub(crate) fn stamp_xmin_if_in_txn(
8977 &self,
8978 collection: &str,
8979 id: crate::storage::unified::entity::EntityId,
8980 ) {
8981 let Some(xid) = self.current_xid() else {
8982 return;
8983 };
8984 let store = self.inner.db.store();
8985 let Some(manager) = store.get_collection(collection) else {
8986 return;
8987 };
8988 if let Some(mut entity) = manager.get(id) {
8989 entity.set_xmin(xid);
8990 let _ = manager.update(entity);
8991 }
8992 }
8993
8994 pub(crate) fn revive_tombstones_since(&self, conn_id: u64, stamper_xid: u64) -> usize {
9002 let mut guard = self.inner.pending_tombstones.write();
9003 let Some(pending) = guard.get_mut(&conn_id) else {
9004 return 0;
9005 };
9006
9007 let store = self.inner.db.store();
9008 let mut revived = 0usize;
9009 pending.retain(|(collection, id, xid, previous_xmax)| {
9010 if *xid < stamper_xid {
9011 return true;
9013 }
9014 if let Some(manager) = store.get_collection(collection) {
9015 if let Some(mut entity) = manager.get(*id) {
9016 if entity.xmax == *xid {
9017 entity.set_xmax(*previous_xmax);
9018 let _ = manager.update(entity);
9019 revived += 1;
9020 }
9021 }
9022 }
9023 false
9024 });
9025 if pending.is_empty() {
9026 guard.remove(&conn_id);
9027 }
9028 revived
9029 }
9030
9031 pub fn current_snapshot(&self) -> crate::storage::transaction::snapshot::Snapshot {
9040 let conn_id = current_connection_id();
9041 if let Some(ctx) = self.inner.tx_contexts.read().get(&conn_id).cloned() {
9042 return ctx.snapshot;
9043 }
9044 let high_water = self.inner.snapshot_manager.peek_next_xid();
9050 self.inner.snapshot_manager.snapshot(high_water)
9051 }
9052
9053 pub fn current_xid(&self) -> Option<crate::storage::transaction::snapshot::Xid> {
9063 let conn_id = current_connection_id();
9064 self.inner
9065 .tx_contexts
9066 .read()
9067 .get(&conn_id)
9068 .map(|ctx| ctx.writer_xid())
9069 }
9070
9071 pub fn connection_in_transaction(&self, conn_id: u64) -> bool {
9078 self.inner.tx_contexts.read().contains_key(&conn_id)
9079 }
9080
9081 pub fn snapshot_manager(&self) -> Arc<crate::storage::transaction::snapshot::SnapshotManager> {
9084 Arc::clone(&self.inner.snapshot_manager)
9085 }
9086
9087 fn mvcc_vacuum_cutoff_xid(&self) -> crate::storage::transaction::snapshot::Xid {
9088 let manager = &self.inner.snapshot_manager;
9089 let next_xid = manager.peek_next_xid();
9090 let mut cutoff = next_xid;
9091 if let Some(oldest_active) = manager.oldest_active_xid() {
9092 cutoff = cutoff.min(oldest_active);
9093 }
9094 if let Some(oldest_pinned) = manager.oldest_pinned_xid() {
9095 cutoff = cutoff.min(oldest_pinned);
9096 }
9097 let retention_xids = self.config_u64("runtime.mvcc.vacuum_retention_xids", 0);
9098 if retention_xids > 0 {
9099 cutoff = cutoff.min(next_xid.saturating_sub(retention_xids));
9100 }
9101 cutoff
9102 }
9103
9104 fn rebuild_runtime_indexes_for_table(&self, table: &str) -> RedDBResult<()> {
9105 let registered = self.inner.index_store.list_indices(table);
9106 if registered.is_empty() {
9107 return Ok(());
9108 }
9109 let store = self.inner.db.store();
9110 let Some(manager) = store.get_collection(table) else {
9111 return Ok(());
9112 };
9113 let entity_fields = manager
9114 .query_all(|entity| matches!(entity.kind, crate::storage::EntityKind::TableRow { .. }))
9115 .into_iter()
9116 .map(|entity| (entity.id, table_row_index_fields(&entity)))
9117 .collect::<Vec<_>>();
9118
9119 for index in registered {
9120 self.inner.index_store.drop_index(&index.name, table);
9121 self.inner
9122 .index_store
9123 .create_index(
9124 &index.name,
9125 table,
9126 &index.columns,
9127 index.method,
9128 index.unique,
9129 &entity_fields,
9130 )
9131 .map_err(RedDBError::Internal)?;
9132 self.inner.index_store.register(index);
9133 }
9134 self.invalidate_plan_cache();
9135 Ok(())
9136 }
9137
9138 pub(crate) fn persist_runtime_index_descriptor(
9139 &self,
9140 index: super::index_store::RegisteredIndex,
9141 ) -> RedDBResult<()> {
9142 let store = self.inner.db.store();
9143 let _ = store.get_or_create_collection(RUNTIME_INDEX_REGISTRY_COLLECTION);
9144 let entity = crate::storage::UnifiedEntity::new(
9145 crate::storage::EntityId::new(0),
9146 crate::storage::EntityKind::TableRow {
9147 table: std::sync::Arc::from(RUNTIME_INDEX_REGISTRY_COLLECTION),
9148 row_id: 0,
9149 },
9150 crate::storage::EntityData::Row(crate::storage::RowData {
9151 columns: Vec::new(),
9152 named: Some(
9153 [
9154 (
9155 "collection".to_string(),
9156 crate::storage::schema::Value::text(index.collection.clone()),
9157 ),
9158 (
9159 "name".to_string(),
9160 crate::storage::schema::Value::text(index.name.clone()),
9161 ),
9162 (
9163 "columns".to_string(),
9164 crate::storage::schema::Value::text(index.columns.join("\u{1f}")),
9165 ),
9166 (
9167 "method".to_string(),
9168 crate::storage::schema::Value::text(index_method_kind_as_str(
9169 index.method,
9170 )),
9171 ),
9172 (
9173 "unique".to_string(),
9174 crate::storage::schema::Value::Boolean(index.unique),
9175 ),
9176 (
9177 "dropped".to_string(),
9178 crate::storage::schema::Value::Boolean(false),
9179 ),
9180 ]
9181 .into_iter()
9182 .collect(),
9183 ),
9184 schema: None,
9185 }),
9186 );
9187 store
9188 .insert_auto(RUNTIME_INDEX_REGISTRY_COLLECTION, entity)
9189 .map(|_| ())
9190 .map_err(|err| RedDBError::Internal(format!("{err:?}")))
9191 }
9192
9193 pub(crate) fn persist_runtime_index_drop(
9194 &self,
9195 collection: &str,
9196 name: &str,
9197 ) -> RedDBResult<()> {
9198 let store = self.inner.db.store();
9199 let _ = store.get_or_create_collection(RUNTIME_INDEX_REGISTRY_COLLECTION);
9200 let entity = crate::storage::UnifiedEntity::new(
9201 crate::storage::EntityId::new(0),
9202 crate::storage::EntityKind::TableRow {
9203 table: std::sync::Arc::from(RUNTIME_INDEX_REGISTRY_COLLECTION),
9204 row_id: 0,
9205 },
9206 crate::storage::EntityData::Row(crate::storage::RowData {
9207 columns: Vec::new(),
9208 named: Some(
9209 [
9210 (
9211 "collection".to_string(),
9212 crate::storage::schema::Value::text(collection.to_string()),
9213 ),
9214 (
9215 "name".to_string(),
9216 crate::storage::schema::Value::text(name.to_string()),
9217 ),
9218 (
9219 "dropped".to_string(),
9220 crate::storage::schema::Value::Boolean(true),
9221 ),
9222 ]
9223 .into_iter()
9224 .collect(),
9225 ),
9226 schema: None,
9227 }),
9228 );
9229 store
9230 .insert_auto(RUNTIME_INDEX_REGISTRY_COLLECTION, entity)
9231 .map(|_| ())
9232 .map_err(|err| RedDBError::Internal(format!("{err:?}")))
9233 }
9234
9235 fn rehydrate_runtime_index_registry(&self) -> RedDBResult<()> {
9236 let store = self.inner.db.store();
9237 let Some(manager) = store.get_collection(RUNTIME_INDEX_REGISTRY_COLLECTION) else {
9238 return Ok(());
9239 };
9240 let mut rows = manager.query_all(|_| true);
9241 rows.sort_by_key(|entity| entity.id.raw());
9242
9243 let mut latest = std::collections::HashMap::<
9244 (String, String),
9245 Option<super::index_store::RegisteredIndex>,
9246 >::new();
9247 for entity in rows {
9248 let crate::storage::EntityData::Row(row) = &entity.data else {
9249 continue;
9250 };
9251 let Some(named) = &row.named else {
9252 continue;
9253 };
9254 let Some(collection) = named_text(named, "collection") else {
9255 continue;
9256 };
9257 let Some(name) = named_text(named, "name") else {
9258 continue;
9259 };
9260 let dropped = named_bool(named, "dropped").unwrap_or(false);
9261 let key = (collection.clone(), name.clone());
9262 if dropped {
9263 latest.insert(key, None);
9264 continue;
9265 }
9266 let columns = named_text(named, "columns")
9267 .map(|raw| {
9268 raw.split('\u{1f}')
9269 .filter(|part| !part.is_empty())
9270 .map(str::to_string)
9271 .collect::<Vec<_>>()
9272 })
9273 .unwrap_or_default();
9274 let Some(method) =
9275 named_text(named, "method").and_then(|raw| index_method_kind_from_str(&raw))
9276 else {
9277 continue;
9278 };
9279 latest.insert(
9280 key,
9281 Some(super::index_store::RegisteredIndex {
9282 name,
9283 collection,
9284 columns,
9285 method,
9286 unique: named_bool(named, "unique").unwrap_or(false),
9287 }),
9288 );
9289 }
9290
9291 for index in latest.into_values().flatten() {
9292 let Some(manager) = store.get_collection(&index.collection) else {
9293 continue;
9294 };
9295 let entity_fields = manager
9296 .query_all(|entity| {
9297 matches!(entity.kind, crate::storage::EntityKind::TableRow { .. })
9298 })
9299 .into_iter()
9300 .map(|entity| (entity.id, table_row_index_fields(&entity)))
9301 .collect::<Vec<_>>();
9302 self.inner
9303 .index_store
9304 .create_index(
9305 &index.name,
9306 &index.collection,
9307 &index.columns,
9308 index.method,
9309 index.unique,
9310 &entity_fields,
9311 )
9312 .map_err(RedDBError::Internal)?;
9313 self.inner.index_store.register(index);
9314 }
9315 self.invalidate_plan_cache();
9316 Ok(())
9317 }
9318
9319 pub fn current_txn_own_xids(
9324 &self,
9325 ) -> std::collections::HashSet<crate::storage::transaction::snapshot::Xid> {
9326 let mut set = std::collections::HashSet::new();
9327 if let Some(ctx) = self.inner.tx_contexts.read().get(¤t_connection_id()) {
9328 set.insert(ctx.xid);
9329 for (_, sub) in &ctx.savepoints {
9330 set.insert(*sub);
9331 }
9332 for sub in &ctx.released_sub_xids {
9333 set.insert(*sub);
9334 }
9335 }
9336 set
9337 }
9338
9339 pub fn foreign_tables(&self) -> Arc<crate::storage::fdw::ForeignTableRegistry> {
9346 Arc::clone(&self.inner.foreign_tables)
9347 }
9348
9349 pub fn is_rls_enabled(&self, table: &str) -> bool {
9351 self.inner.rls_enabled_tables.read().contains(table)
9352 }
9353
9354 pub fn matching_rls_policies(
9361 &self,
9362 table: &str,
9363 role: Option<&str>,
9364 action: crate::storage::query::ast::PolicyAction,
9365 ) -> Vec<crate::storage::query::ast::Filter> {
9366 self.matching_rls_policies_for_kind(
9371 table,
9372 role,
9373 action,
9374 crate::storage::query::ast::PolicyTargetKind::Table,
9375 )
9376 }
9377
9378 pub fn matching_rls_policies_for_kind(
9386 &self,
9387 table: &str,
9388 role: Option<&str>,
9389 action: crate::storage::query::ast::PolicyAction,
9390 kind: crate::storage::query::ast::PolicyTargetKind,
9391 ) -> Vec<crate::storage::query::ast::Filter> {
9392 if !self.is_rls_enabled(table) {
9393 return Vec::new();
9394 }
9395 let policies = self.inner.rls_policies.read();
9396 policies
9397 .iter()
9398 .filter_map(|((t, _), p)| {
9399 if t != table {
9400 return None;
9401 }
9402 if p.target_kind != kind
9411 && p.target_kind != crate::storage::query::ast::PolicyTargetKind::Table
9412 {
9413 return None;
9414 }
9415 if let Some(a) = p.action {
9417 if a != action {
9418 return None;
9419 }
9420 }
9421 if let Some(p_role) = p.role.as_deref() {
9423 match role {
9424 Some(r) if r == p_role => {}
9425 _ => return None,
9426 }
9427 }
9428 Some((*p.using).clone())
9429 })
9430 .collect()
9431 }
9432
9433 pub(crate) fn refresh_table_planner_stats(&self, table: &str) {
9434 let store = self.inner.db.store();
9435 if let Some(stats) =
9436 crate::storage::query::planner::stats_catalog::analyze_collection(store.as_ref(), table)
9437 {
9438 crate::storage::query::planner::stats_catalog::persist_table_stats(
9439 store.as_ref(),
9440 &stats,
9441 );
9442 } else {
9443 crate::storage::query::planner::stats_catalog::clear_table_stats(store.as_ref(), table);
9444 }
9445 self.invalidate_plan_cache();
9446 }
9447
9448 pub(crate) fn note_table_write(&self, table: &str) {
9449 let already_dirty = self.inner.planner_dirty_tables.read().contains(table);
9454 if !already_dirty {
9455 self.inner
9456 .planner_dirty_tables
9457 .write()
9458 .insert(table.to_string());
9459 }
9460 self.invalidate_result_cache_for_table(table);
9461 }
9462
9463 fn explain_as_rows(&self, raw_query: &str, inner_sql: &str) -> RedDBResult<RuntimeQueryResult> {
9471 let explain = self.explain_query(inner_sql)?;
9472
9473 let columns = vec![
9474 "op".to_string(),
9475 "source".to_string(),
9476 "est_rows".to_string(),
9477 "est_cost".to_string(),
9478 "depth".to_string(),
9479 ];
9480
9481 let mut records: Vec<crate::storage::query::unified::UnifiedRecord> = Vec::new();
9482
9483 for name in &explain.cte_materializations {
9489 use std::sync::Arc;
9490 let mut rec = crate::storage::query::unified::UnifiedRecord::default();
9491 rec.set_arc(Arc::from("op"), Value::text("CteScan".to_string()));
9492 rec.set_arc(Arc::from("source"), Value::text(name.clone()));
9493 rec.set_arc(Arc::from("est_rows"), Value::Float(0.0));
9494 rec.set_arc(Arc::from("est_cost"), Value::Float(0.0));
9495 rec.set_arc(Arc::from("depth"), Value::Integer(0));
9496 records.push(rec);
9497 }
9498
9499 walk_plan_node(&explain.logical_plan.root, 0, &mut records);
9500
9501 let result = crate::storage::query::unified::UnifiedResult {
9502 columns,
9503 records,
9504 stats: Default::default(),
9505 pre_serialized_json: None,
9506 };
9507
9508 Ok(RuntimeQueryResult {
9509 query: raw_query.to_string(),
9510 mode: explain.mode,
9511 statement: "explain",
9512 engine: "runtime-explain",
9513 result,
9514 affected_rows: 0,
9515 statement_type: "select",
9516 bookmark: None,
9517 })
9518 }
9519
9520 pub(crate) fn check_query_privilege(
9528 &self,
9529 expr: &crate::storage::query::ast::QueryExpr,
9530 ) -> Result<(), String> {
9531 use crate::auth::privileges::{Action, AuthzContext, Resource};
9532 use crate::auth::UserId;
9533 use crate::storage::query::ast::QueryExpr;
9534
9535 let auth_store = match self.inner.auth_store.read().clone() {
9540 Some(s) => s,
9541 None => return Ok(()),
9542 };
9543
9544 let (username, role) = match current_auth_identity() {
9550 Some(p) => p,
9551 None => return Ok(()),
9552 };
9553 let tenant = current_tenant();
9554
9555 let ctx = AuthzContext {
9556 principal: &username,
9557 effective_role: role,
9558 tenant: tenant.as_deref(),
9559 };
9560 let principal_id = UserId::from_parts(tenant.as_deref(), &username);
9561
9562 let (action, resource) = match expr {
9564 QueryExpr::Table(t) => (Action::Select, Resource::table_from_name(&t.table)),
9565 QueryExpr::RankOf(_) | QueryExpr::ApproxRankOf(_) | QueryExpr::RankRange(_) => {
9566 (Action::Select, Resource::Database)
9567 }
9568 QueryExpr::QueueSelect(q) => {
9569 return self.check_queue_op_privilege(
9570 &auth_store,
9571 &principal_id,
9572 role,
9573 tenant.as_deref(),
9574 "queue:peek",
9575 &q.queue,
9576 );
9577 }
9578 QueryExpr::QueueCommand(cmd) => {
9579 use crate::storage::query::ast::QueueCommand;
9580 let (queue, action_verb) = match cmd {
9581 QueueCommand::Push { queue, .. } => (queue.as_str(), "queue:enqueue"),
9582 QueueCommand::Pop { queue, .. }
9583 | QueueCommand::GroupRead { queue, .. }
9584 | QueueCommand::Claim { queue, .. } => (queue.as_str(), "queue:read"),
9585 QueueCommand::Peek { queue, .. }
9586 | QueueCommand::Len { queue }
9587 | QueueCommand::Pending { queue, .. } => (queue.as_str(), "queue:peek"),
9588 QueueCommand::Ack { queue, .. } => (queue.as_str(), "queue:ack"),
9589 QueueCommand::Nack {
9590 queue, delay_ms, ..
9591 } => {
9592 let verb = if delay_ms.is_some() {
9598 "queue:retry"
9599 } else {
9600 "queue:nack"
9601 };
9602 (queue.as_str(), verb)
9603 }
9604 QueueCommand::Purge { queue } => (queue.as_str(), "queue:purge"),
9605 QueueCommand::GroupCreate { queue, .. } => (queue.as_str(), "queue:read"),
9608 QueueCommand::Move { source, .. } => (source.as_str(), "queue:dlq:move"),
9609 };
9610 return self.check_queue_op_privilege(
9611 &auth_store,
9612 &principal_id,
9613 role,
9614 tenant.as_deref(),
9615 action_verb,
9616 queue,
9617 );
9618 }
9619 QueryExpr::Graph(g) => {
9620 self.check_graph_op_privilege(
9623 &auth_store,
9624 &principal_id,
9625 role,
9626 tenant.as_deref(),
9627 "graph:traverse",
9628 )?;
9629 if auth_store.iam_authorization_enabled() {
9630 self.check_graph_property_projection_privilege(
9631 &auth_store,
9632 &principal_id,
9633 role,
9634 tenant.as_deref(),
9635 g,
9636 )?;
9637 return Ok(());
9638 }
9639 return Ok(());
9640 }
9641 QueryExpr::Path(_) => {
9642 return self.check_graph_op_privilege(
9646 &auth_store,
9647 &principal_id,
9648 role,
9649 tenant.as_deref(),
9650 "graph:traverse",
9651 );
9652 }
9653 QueryExpr::GraphCommand(cmd) => {
9654 use crate::storage::query::ast::GraphCommand;
9655 let action_verb = match cmd {
9656 GraphCommand::Properties { .. } => "graph:read",
9658 GraphCommand::Neighborhood { .. }
9660 | GraphCommand::Traverse { .. }
9661 | GraphCommand::ShortestPath { .. } => "graph:traverse",
9662 GraphCommand::Centrality { .. }
9666 | GraphCommand::Community { .. }
9667 | GraphCommand::Components { .. }
9668 | GraphCommand::Cycles { .. }
9669 | GraphCommand::Clustering
9670 | GraphCommand::TopologicalSort => "graph:algorithm:run",
9671 };
9672 return self.check_graph_op_privilege(
9673 &auth_store,
9674 &principal_id,
9675 role,
9676 tenant.as_deref(),
9677 action_verb,
9678 );
9679 }
9680 QueryExpr::Vector(v) => {
9681 if auth_store.iam_authorization_enabled() {
9682 self.check_vector_op_privilege(
9683 &auth_store,
9684 &principal_id,
9685 role,
9686 tenant.as_deref(),
9687 "vector:search",
9688 &v.collection,
9689 )?;
9690 self.check_table_like_column_projection_privilege(
9691 &auth_store,
9692 &principal_id,
9693 role,
9694 tenant.as_deref(),
9695 &v.collection,
9696 &["content".to_string()],
9697 )?;
9698 return Ok(());
9699 }
9700 return Ok(());
9701 }
9702 QueryExpr::SearchCommand(cmd) => {
9703 use crate::storage::query::ast::SearchCommand;
9704 if auth_store.iam_authorization_enabled() {
9705 let collection = match cmd {
9711 SearchCommand::Similar { collection, .. }
9712 | SearchCommand::Hybrid { collection, .. } => Some(collection.as_str()),
9713 _ => None,
9714 };
9715 if let Some(c) = collection {
9716 self.check_vector_op_privilege(
9717 &auth_store,
9718 &principal_id,
9719 role,
9720 tenant.as_deref(),
9721 "vector:search",
9722 c,
9723 )?;
9724 return Ok(());
9725 }
9726 }
9727 return Ok(());
9728 }
9729 QueryExpr::Hybrid(h) => {
9730 if auth_store.iam_authorization_enabled() {
9731 self.check_vector_op_privilege(
9739 &auth_store,
9740 &principal_id,
9741 role,
9742 tenant.as_deref(),
9743 "vector:search",
9744 &h.vector.collection,
9745 )?;
9746 return Ok(());
9747 }
9748 return Ok(());
9749 }
9750 QueryExpr::Insert(i) => (Action::Insert, Resource::table_from_name(&i.table)),
9751 QueryExpr::Update(u) => (Action::Update, Resource::table_from_name(&u.table)),
9752 QueryExpr::Delete(d) => (Action::Delete, Resource::table_from_name(&d.table)),
9753 QueryExpr::Join(_) => (Action::Select, Resource::Database),
9757 QueryExpr::Grant(_)
9760 | QueryExpr::Revoke(_)
9761 | QueryExpr::AlterUser(_)
9762 | QueryExpr::CreateUser(_) => {
9763 return if role == crate::auth::Role::Admin {
9764 Ok(())
9765 } else {
9766 Err(format!(
9767 "principal=`{}` role=`{:?}` cannot issue ACL/auth DDL",
9768 username, role
9769 ))
9770 };
9771 }
9772 QueryExpr::CreateIamPolicy { id, .. } => {
9773 return self.check_policy_management_privilege(
9774 &auth_store,
9775 &principal_id,
9776 role,
9777 tenant.as_deref(),
9778 "policy:put",
9779 "policy",
9780 id,
9781 );
9782 }
9783 QueryExpr::DropIamPolicy { id } => {
9784 return self.check_policy_management_privilege(
9785 &auth_store,
9786 &principal_id,
9787 role,
9788 tenant.as_deref(),
9789 "policy:drop",
9790 "policy",
9791 id,
9792 );
9793 }
9794 QueryExpr::AttachPolicy { policy_id, .. } => {
9795 return self.check_policy_management_privilege(
9796 &auth_store,
9797 &principal_id,
9798 role,
9799 tenant.as_deref(),
9800 "policy:attach",
9801 "policy",
9802 policy_id,
9803 );
9804 }
9805 QueryExpr::DetachPolicy { policy_id, .. } => {
9806 return self.check_policy_management_privilege(
9807 &auth_store,
9808 &principal_id,
9809 role,
9810 tenant.as_deref(),
9811 "policy:detach",
9812 "policy",
9813 policy_id,
9814 );
9815 }
9816 QueryExpr::ShowPolicies { .. } | QueryExpr::ShowEffectivePermissions { .. } => {
9817 return Ok(());
9818 }
9819 QueryExpr::SimulatePolicy { .. } => {
9820 return self.check_policy_management_privilege(
9821 &auth_store,
9822 &principal_id,
9823 role,
9824 tenant.as_deref(),
9825 "policy:simulate",
9826 "policy",
9827 "*",
9828 );
9829 }
9830 QueryExpr::LintPolicy { .. } => {
9831 return self.check_policy_management_privilege(
9834 &auth_store,
9835 &principal_id,
9836 role,
9837 tenant.as_deref(),
9838 "policy:simulate",
9839 "policy",
9840 "*",
9841 );
9842 }
9843 QueryExpr::MigratePolicyMode { dry_run, .. } => {
9844 let action = if *dry_run {
9849 "policy:simulate"
9850 } else {
9851 "policy:put"
9852 };
9853 return self.check_policy_management_privilege(
9854 &auth_store,
9855 &principal_id,
9856 role,
9857 tenant.as_deref(),
9858 action,
9859 "policy",
9860 "*",
9861 );
9862 }
9863 QueryExpr::DropTable(q) => {
9866 return self.check_ddl_collection_privilege(
9867 &auth_store,
9868 &principal_id,
9869 role,
9870 tenant.as_deref(),
9871 &username,
9872 "drop",
9873 &q.name,
9874 );
9875 }
9876 QueryExpr::DropGraph(q) => {
9877 return self.check_ddl_collection_privilege(
9878 &auth_store,
9879 &principal_id,
9880 role,
9881 tenant.as_deref(),
9882 &username,
9883 "drop",
9884 &q.name,
9885 );
9886 }
9887 QueryExpr::DropVector(q) => {
9888 return self.check_ddl_collection_privilege(
9889 &auth_store,
9890 &principal_id,
9891 role,
9892 tenant.as_deref(),
9893 &username,
9894 "drop",
9895 &q.name,
9896 );
9897 }
9898 QueryExpr::DropDocument(q) => {
9899 return self.check_ddl_collection_privilege(
9900 &auth_store,
9901 &principal_id,
9902 role,
9903 tenant.as_deref(),
9904 &username,
9905 "drop",
9906 &q.name,
9907 );
9908 }
9909 QueryExpr::DropKv(q) => {
9910 return self.check_ddl_collection_privilege(
9911 &auth_store,
9912 &principal_id,
9913 role,
9914 tenant.as_deref(),
9915 &username,
9916 "drop",
9917 &q.name,
9918 );
9919 }
9920 QueryExpr::DropCollection(q) => {
9921 return self.check_ddl_collection_privilege(
9922 &auth_store,
9923 &principal_id,
9924 role,
9925 tenant.as_deref(),
9926 &username,
9927 "drop",
9928 &q.name,
9929 );
9930 }
9931 QueryExpr::Truncate(q) => {
9932 return self.check_ddl_collection_privilege(
9933 &auth_store,
9934 &principal_id,
9935 role,
9936 tenant.as_deref(),
9937 &username,
9938 "truncate",
9939 &q.name,
9940 );
9941 }
9942 QueryExpr::CreateTable(q) => {
9954 return self.check_ddl_object_privilege(
9955 &auth_store,
9956 &principal_id,
9957 role,
9958 tenant.as_deref(),
9959 &username,
9960 "create",
9961 "collection",
9962 &q.name,
9963 crate::auth::Role::Write,
9964 );
9965 }
9966 QueryExpr::CreateCollection(q) => {
9967 return self.check_ddl_object_privilege(
9968 &auth_store,
9969 &principal_id,
9970 role,
9971 tenant.as_deref(),
9972 &username,
9973 "create",
9974 "collection",
9975 &q.name,
9976 crate::auth::Role::Write,
9977 );
9978 }
9979 QueryExpr::CreateVector(q) => {
9980 return self.check_ddl_object_privilege(
9981 &auth_store,
9982 &principal_id,
9983 role,
9984 tenant.as_deref(),
9985 &username,
9986 "create",
9987 "collection",
9988 &q.name,
9989 crate::auth::Role::Write,
9990 );
9991 }
9992 QueryExpr::AlterTable(q) => {
9993 return self.check_ddl_object_privilege(
9994 &auth_store,
9995 &principal_id,
9996 role,
9997 tenant.as_deref(),
9998 &username,
9999 "alter",
10000 "collection",
10001 &q.name,
10002 crate::auth::Role::Write,
10003 );
10004 }
10005 QueryExpr::CreateIndex(q) => {
10006 return self.check_ddl_object_privilege(
10007 &auth_store,
10008 &principal_id,
10009 role,
10010 tenant.as_deref(),
10011 &username,
10012 "create",
10013 "collection",
10014 &q.table,
10015 crate::auth::Role::Write,
10016 );
10017 }
10018 QueryExpr::DropIndex(q) => {
10019 return self.check_ddl_object_privilege(
10020 &auth_store,
10021 &principal_id,
10022 role,
10023 tenant.as_deref(),
10024 &username,
10025 "drop",
10026 "collection",
10027 &q.table,
10028 crate::auth::Role::Write,
10029 );
10030 }
10031 QueryExpr::CreateSchema(q) => {
10032 return self.check_ddl_object_privilege(
10033 &auth_store,
10034 &principal_id,
10035 role,
10036 tenant.as_deref(),
10037 &username,
10038 "schema:admin",
10039 "schema",
10040 &q.name,
10041 crate::auth::Role::Admin,
10042 );
10043 }
10044 QueryExpr::DropSchema(q) => {
10045 return self.check_ddl_object_privilege(
10046 &auth_store,
10047 &principal_id,
10048 role,
10049 tenant.as_deref(),
10050 &username,
10051 "schema:admin",
10052 "schema",
10053 &q.name,
10054 crate::auth::Role::Admin,
10055 );
10056 }
10057 QueryExpr::CreateSequence(q) => {
10058 return self.check_ddl_object_privilege(
10059 &auth_store,
10060 &principal_id,
10061 role,
10062 tenant.as_deref(),
10063 &username,
10064 "create",
10065 "collection",
10066 &q.name,
10067 crate::auth::Role::Write,
10068 );
10069 }
10070 QueryExpr::DropSequence(q) => {
10071 return self.check_ddl_object_privilege(
10072 &auth_store,
10073 &principal_id,
10074 role,
10075 tenant.as_deref(),
10076 &username,
10077 "drop",
10078 "collection",
10079 &q.name,
10080 crate::auth::Role::Write,
10081 );
10082 }
10083 QueryExpr::CreateView(q) => {
10084 return self.check_ddl_object_privilege(
10085 &auth_store,
10086 &principal_id,
10087 role,
10088 tenant.as_deref(),
10089 &username,
10090 "create",
10091 "collection",
10092 &q.name,
10093 crate::auth::Role::Write,
10094 );
10095 }
10096 QueryExpr::DropView(q) => {
10097 return self.check_ddl_object_privilege(
10098 &auth_store,
10099 &principal_id,
10100 role,
10101 tenant.as_deref(),
10102 &username,
10103 "drop",
10104 "collection",
10105 &q.name,
10106 crate::auth::Role::Write,
10107 );
10108 }
10109 QueryExpr::RefreshMaterializedView(q) => {
10110 return self.check_ddl_object_privilege(
10111 &auth_store,
10112 &principal_id,
10113 role,
10114 tenant.as_deref(),
10115 &username,
10116 "alter",
10117 "collection",
10118 &q.name,
10119 crate::auth::Role::Write,
10120 );
10121 }
10122 QueryExpr::CreatePolicy(q) => {
10123 return self.check_ddl_object_privilege(
10124 &auth_store,
10125 &principal_id,
10126 role,
10127 tenant.as_deref(),
10128 &username,
10129 "create",
10130 "collection",
10131 &q.table,
10132 crate::auth::Role::Write,
10133 );
10134 }
10135 QueryExpr::DropPolicy(q) => {
10136 return self.check_ddl_object_privilege(
10137 &auth_store,
10138 &principal_id,
10139 role,
10140 tenant.as_deref(),
10141 &username,
10142 "drop",
10143 "collection",
10144 &q.table,
10145 crate::auth::Role::Write,
10146 );
10147 }
10148 QueryExpr::CreateServer(q) => {
10149 return self.check_ddl_object_privilege(
10150 &auth_store,
10151 &principal_id,
10152 role,
10153 tenant.as_deref(),
10154 &username,
10155 "schema:admin",
10156 "schema",
10157 &q.name,
10158 crate::auth::Role::Admin,
10159 );
10160 }
10161 QueryExpr::DropServer(q) => {
10162 return self.check_ddl_object_privilege(
10163 &auth_store,
10164 &principal_id,
10165 role,
10166 tenant.as_deref(),
10167 &username,
10168 "schema:admin",
10169 "schema",
10170 &q.name,
10171 crate::auth::Role::Admin,
10172 );
10173 }
10174 QueryExpr::CreateForeignTable(q) => {
10175 return self.check_ddl_object_privilege(
10176 &auth_store,
10177 &principal_id,
10178 role,
10179 tenant.as_deref(),
10180 &username,
10181 "schema:write",
10182 "schema",
10183 &q.name,
10184 crate::auth::Role::Write,
10185 );
10186 }
10187 QueryExpr::DropForeignTable(q) => {
10188 return self.check_ddl_object_privilege(
10189 &auth_store,
10190 &principal_id,
10191 role,
10192 tenant.as_deref(),
10193 &username,
10194 "schema:write",
10195 "schema",
10196 &q.name,
10197 crate::auth::Role::Write,
10198 );
10199 }
10200 QueryExpr::CreateTimeSeries(q) => {
10201 return self.check_ddl_object_privilege(
10202 &auth_store,
10203 &principal_id,
10204 role,
10205 tenant.as_deref(),
10206 &username,
10207 "create",
10208 "collection",
10209 &q.name,
10210 crate::auth::Role::Write,
10211 );
10212 }
10213 QueryExpr::CreateMetric(q) => {
10214 return self.check_ddl_object_privilege(
10215 &auth_store,
10216 &principal_id,
10217 role,
10218 tenant.as_deref(),
10219 &username,
10220 "create",
10221 "collection",
10222 &q.path,
10223 crate::auth::Role::Write,
10224 );
10225 }
10226 QueryExpr::AlterMetric(q) => {
10227 return self.check_ddl_object_privilege(
10228 &auth_store,
10229 &principal_id,
10230 role,
10231 tenant.as_deref(),
10232 &username,
10233 "alter",
10234 "collection",
10235 &q.path,
10236 crate::auth::Role::Write,
10237 );
10238 }
10239 QueryExpr::CreateSlo(q) => {
10240 return self.check_ddl_object_privilege(
10241 &auth_store,
10242 &principal_id,
10243 role,
10244 tenant.as_deref(),
10245 &username,
10246 "create",
10247 "collection",
10248 &q.path,
10249 crate::auth::Role::Write,
10250 );
10251 }
10252 QueryExpr::DropTimeSeries(q) => {
10253 return self.check_ddl_object_privilege(
10254 &auth_store,
10255 &principal_id,
10256 role,
10257 tenant.as_deref(),
10258 &username,
10259 "drop",
10260 "collection",
10261 &q.name,
10262 crate::auth::Role::Write,
10263 );
10264 }
10265 QueryExpr::CreateQueue(q) => {
10266 return self.check_ddl_object_privilege(
10267 &auth_store,
10268 &principal_id,
10269 role,
10270 tenant.as_deref(),
10271 &username,
10272 "create",
10273 "collection",
10274 &q.name,
10275 crate::auth::Role::Write,
10276 );
10277 }
10278 QueryExpr::AlterQueue(q) => {
10279 return self.check_ddl_object_privilege(
10280 &auth_store,
10281 &principal_id,
10282 role,
10283 tenant.as_deref(),
10284 &username,
10285 "alter",
10286 "collection",
10287 &q.name,
10288 crate::auth::Role::Write,
10289 );
10290 }
10291 QueryExpr::DropQueue(q) => {
10292 return self.check_ddl_object_privilege(
10293 &auth_store,
10294 &principal_id,
10295 role,
10296 tenant.as_deref(),
10297 &username,
10298 "drop",
10299 "collection",
10300 &q.name,
10301 crate::auth::Role::Write,
10302 );
10303 }
10304 QueryExpr::CreateTree(q) => {
10305 return self.check_ddl_object_privilege(
10306 &auth_store,
10307 &principal_id,
10308 role,
10309 tenant.as_deref(),
10310 &username,
10311 "create",
10312 "collection",
10313 &q.collection,
10314 crate::auth::Role::Write,
10315 );
10316 }
10317 QueryExpr::DropTree(q) => {
10318 return self.check_ddl_object_privilege(
10319 &auth_store,
10320 &principal_id,
10321 role,
10322 tenant.as_deref(),
10323 &username,
10324 "drop",
10325 "collection",
10326 &q.collection,
10327 crate::auth::Role::Write,
10328 );
10329 }
10330 QueryExpr::CreateMigration(q) => {
10334 return self.check_ddl_object_privilege(
10335 &auth_store,
10336 &principal_id,
10337 role,
10338 tenant.as_deref(),
10339 &username,
10340 "schema:write",
10341 "schema",
10342 &q.name,
10343 crate::auth::Role::Write,
10344 );
10345 }
10346 QueryExpr::ApplyMigration(_) | QueryExpr::RollbackMigration(_) => {
10348 return if role == crate::auth::Role::Admin {
10349 Ok(())
10350 } else {
10351 Err(format!(
10352 "principal=`{}` role=`{:?}` cannot issue APPLY/ROLLBACK MIGRATION",
10353 username, role
10354 ))
10355 };
10356 }
10357 QueryExpr::ExplainMigration(_) => return Ok(()),
10359 _ => return Ok(()),
10363 };
10364
10365 if auth_store.iam_authorization_enabled() {
10366 let iam_action = legacy_action_to_iam(action);
10367 let iam_resource = legacy_resource_to_iam(&resource, tenant.as_deref());
10368 let iam_ctx = runtime_iam_context(role, tenant.as_deref());
10369 if !auth_store.check_policy_authz_with_role(
10370 &principal_id,
10371 iam_action,
10372 &iam_resource,
10373 &iam_ctx,
10374 role,
10375 ) {
10376 return Err(format!(
10377 "principal=`{}` action=`{}` resource=`{}:{}` denied by IAM policy",
10378 username, iam_action, iam_resource.kind, iam_resource.name
10379 ));
10380 }
10381
10382 if let QueryExpr::Table(table) = expr {
10383 self.check_table_column_projection_privilege(
10384 &auth_store,
10385 &principal_id,
10386 &iam_ctx,
10387 table,
10388 )?;
10389 }
10390
10391 if let QueryExpr::Update(update) = expr {
10392 let columns = update_set_target_columns(update);
10393 if !columns.is_empty() {
10394 let request = column_access_request_for_table_update(&update.table, columns);
10395 let outcome =
10396 auth_store.check_column_projection_authz(&principal_id, &request, &iam_ctx);
10397 if let Some(denied) = outcome.first_denied_column() {
10398 return Err(format!(
10399 "principal=`{}` action=`{}` resource=`{}:{}` denied by IAM column policy",
10400 username, iam_action, denied.resource.kind, denied.resource.name
10401 ));
10402 }
10403 if !outcome.allowed() {
10404 return Err(format!(
10405 "principal=`{}` action=`{}` resource=`{}:{}` denied by IAM policy",
10406 username,
10407 iam_action,
10408 outcome.table_resource.kind,
10409 outcome.table_resource.name
10410 ));
10411 }
10412 }
10413
10414 if let Some(columns) = update_returning_columns_for_policy(self, update) {
10415 let request = column_access_request_for_table_select(&update.table, columns);
10416 let outcome =
10417 auth_store.check_column_projection_authz(&principal_id, &request, &iam_ctx);
10418 if let Some(denied) = outcome.first_denied_column() {
10419 return Err(format!(
10420 "principal=`{}` action=`select` resource=`{}:{}` denied by IAM column policy",
10421 username, denied.resource.kind, denied.resource.name
10422 ));
10423 }
10424 if !outcome.allowed() {
10425 return Err(format!(
10426 "principal=`{}` action=`select` resource=`{}:{}` denied by IAM policy",
10427 username, outcome.table_resource.kind, outcome.table_resource.name
10428 ));
10429 }
10430 }
10431 }
10432
10433 Ok(())
10434 } else {
10435 auth_store
10436 .check_grant(&ctx, action, &resource)
10437 .map_err(|e| e.to_string())
10438 }
10439 }
10440
10441 fn check_table_column_projection_privilege(
10442 &self,
10443 auth_store: &Arc<crate::auth::store::AuthStore>,
10444 principal: &crate::auth::UserId,
10445 ctx: &crate::auth::policies::EvalContext,
10446 table: &crate::storage::query::ast::TableQuery,
10447 ) -> Result<(), String> {
10448 use crate::auth::{ColumnAccessRequest, ColumnDecisionEffect};
10449
10450 let columns = requested_table_columns_for_policy(table);
10451 if columns.is_empty() {
10452 return Ok(());
10453 }
10454
10455 let request = ColumnAccessRequest::select(table.table.clone(), columns);
10456 let outcome = auth_store.check_column_projection_authz(principal, &request, ctx);
10457 if outcome.allowed() {
10458 return Ok(());
10459 }
10460
10461 if !matches!(
10462 outcome.table_decision,
10463 crate::auth::policies::Decision::Allow { .. }
10464 | crate::auth::policies::Decision::AdminBypass
10465 ) {
10466 return Err(format!(
10467 "principal=`{}` action=`select` resource=`{}:{}` denied by IAM policy",
10468 principal, outcome.table_resource.kind, outcome.table_resource.name
10469 ));
10470 }
10471
10472 let denied = outcome
10473 .first_denied_column()
10474 .filter(|decision| decision.effective == ColumnDecisionEffect::Denied);
10475 match denied {
10476 Some(decision) => Err(format!(
10477 "principal=`{}` action=`select` resource=`{}:{}` denied by IAM policy",
10478 principal, decision.resource.kind, decision.resource.name
10479 )),
10480 None => Ok(()),
10481 }
10482 }
10483
10484 fn check_graph_property_projection_privilege(
10485 &self,
10486 auth_store: &Arc<crate::auth::store::AuthStore>,
10487 principal: &crate::auth::UserId,
10488 role: crate::auth::Role,
10489 tenant: Option<&str>,
10490 query: &crate::storage::query::ast::GraphQuery,
10491 ) -> Result<(), String> {
10492 let columns = explicit_graph_projection_properties(query);
10493 if columns.is_empty() {
10494 return Ok(());
10495 }
10496 self.check_table_like_column_projection_privilege(
10497 auth_store, principal, role, tenant, "graph", &columns,
10498 )
10499 }
10500
10501 fn check_table_like_column_projection_privilege(
10502 &self,
10503 auth_store: &Arc<crate::auth::store::AuthStore>,
10504 principal: &crate::auth::UserId,
10505 role: crate::auth::Role,
10506 tenant: Option<&str>,
10507 table: &str,
10508 columns: &[String],
10509 ) -> Result<(), String> {
10510 let iam_ctx = runtime_iam_context(role, tenant);
10511 let request =
10512 crate::auth::ColumnAccessRequest::select(table.to_string(), columns.iter().cloned());
10513 let outcome = auth_store.check_column_projection_authz(principal, &request, &iam_ctx);
10514 if outcome.allowed() {
10515 return Ok(());
10516 }
10517 let denied = outcome
10518 .first_denied_column()
10519 .map(|d| d.resource.name.clone())
10520 .unwrap_or_else(|| format!("{table}.<unknown>"));
10521 Err(format!(
10522 "principal=`{}` action=`select` resource=`column:{}` denied by IAM policy",
10523 principal, denied
10524 ))
10525 }
10526
10527 fn check_policy_management_privilege(
10528 &self,
10529 auth_store: &Arc<crate::auth::store::AuthStore>,
10530 principal: &crate::auth::UserId,
10531 role: crate::auth::Role,
10532 tenant: Option<&str>,
10533 action: &str,
10534 resource_kind: &str,
10535 resource_name: &str,
10536 ) -> Result<(), String> {
10537 let ctx = runtime_iam_context(role, tenant);
10538
10539 if !auth_store.iam_authorization_enabled() {
10540 return if role == crate::auth::Role::Admin {
10541 Ok(())
10542 } else {
10543 Err(format!(
10544 "principal=`{}` role=`{:?}` cannot issue ACL/auth DDL",
10545 principal, role
10546 ))
10547 };
10548 }
10549
10550 if resource_kind == "policy"
10551 && matches!(
10552 action,
10553 "policy:put" | "policy:drop" | "policy:attach" | "policy:detach"
10554 )
10555 && self
10556 .inner
10557 .config_registry
10558 .get_active(resource_name)
10559 .map(|entry| entry.managed)
10560 .unwrap_or(false)
10561 {
10562 return Ok(());
10563 }
10564
10565 let mut resource = crate::auth::policies::ResourceRef::new(
10566 resource_kind.to_string(),
10567 resource_name.to_string(),
10568 );
10569 if let Some(t) = tenant {
10570 resource = resource.with_tenant(t.to_string());
10571 }
10572 if auth_store.check_policy_authz_with_role(principal, action, &resource, &ctx, role) {
10573 Ok(())
10574 } else {
10575 Err(format!(
10576 "principal=`{}` action=`{}` resource=`{}:{}` denied by IAM policy",
10577 principal, action, resource.kind, resource.name
10578 ))
10579 }
10580 }
10581
10582 fn check_managed_config_write_for_set_config(&self, key: &str) -> RedDBResult<()> {
10583 let Some(auth_store) = self.inner.auth_store.read().clone() else {
10584 return Ok(());
10585 };
10586 let (username, role) = current_auth_identity()
10587 .unwrap_or_else(|| ("anonymous".to_string(), crate::auth::Role::Read));
10588 let tenant = current_tenant();
10589 let principal = crate::auth::UserId::from_parts(tenant.as_deref(), &username);
10590 let ctx = runtime_iam_context(role, tenant.as_deref());
10591 let gate = crate::auth::managed_config::ManagedConfigGate::new(
10592 self.inner.config_registry.as_ref(),
10593 );
10594 match gate.check_write(&auth_store, &principal, &ctx, key) {
10595 crate::auth::managed_config::ManagedConfigDecision::PassThrough { .. }
10596 | crate::auth::managed_config::ManagedConfigDecision::Allow { .. } => Ok(()),
10597 crate::auth::managed_config::ManagedConfigDecision::Deny { reason, .. } => {
10598 Err(RedDBError::Query(format!(
10599 "permission denied: managed config mutation blocked for `{key}`: {reason}"
10600 )))
10601 }
10602 }
10603 }
10604
10605 fn check_queue_op_privilege(
10621 &self,
10622 auth_store: &Arc<crate::auth::store::AuthStore>,
10623 principal: &crate::auth::UserId,
10624 role: crate::auth::Role,
10625 tenant: Option<&str>,
10626 action: &str,
10627 queue: &str,
10628 ) -> Result<(), String> {
10629 if !auth_store.iam_authorization_enabled() {
10630 return Ok(());
10631 }
10632 let mut resource =
10633 crate::auth::policies::ResourceRef::new("queue".to_string(), queue.to_string());
10634 if let Some(t) = tenant {
10635 resource = resource.with_tenant(t.to_string());
10636 }
10637 let ctx = runtime_iam_context(role, tenant);
10638 if auth_store.check_policy_authz_with_role(principal, action, &resource, &ctx, role) {
10639 Ok(())
10640 } else {
10641 Err(format!(
10642 "principal=`{}` action=`{}` resource=`queue:{}` denied by IAM policy",
10643 principal, action, queue
10644 ))
10645 }
10646 }
10647
10648 fn check_graph_op_privilege(
10668 &self,
10669 auth_store: &Arc<crate::auth::store::AuthStore>,
10670 principal: &crate::auth::UserId,
10671 role: crate::auth::Role,
10672 tenant: Option<&str>,
10673 action: &str,
10674 ) -> Result<(), String> {
10675 if !auth_store.iam_authorization_enabled() {
10676 return Ok(());
10677 }
10678 let mut resource =
10679 crate::auth::policies::ResourceRef::new("graph".to_string(), "*".to_string());
10680 if let Some(t) = tenant {
10681 resource = resource.with_tenant(t.to_string());
10682 }
10683 let ctx = runtime_iam_context(role, tenant);
10684 if auth_store.check_policy_authz_with_role(principal, action, &resource, &ctx, role) {
10685 Ok(())
10686 } else {
10687 Err(format!(
10688 "principal=`{}` action=`{}` resource=`graph:*` denied by IAM policy",
10689 principal, action
10690 ))
10691 }
10692 }
10693
10694 fn check_vector_op_privilege(
10709 &self,
10710 auth_store: &Arc<crate::auth::store::AuthStore>,
10711 principal: &crate::auth::UserId,
10712 role: crate::auth::Role,
10713 tenant: Option<&str>,
10714 action: &str,
10715 collection: &str,
10716 ) -> Result<(), String> {
10717 if !auth_store.iam_authorization_enabled() {
10718 return Ok(());
10719 }
10720 let mut resource =
10721 crate::auth::policies::ResourceRef::new("vector".to_string(), collection.to_string());
10722 if let Some(t) = tenant {
10723 resource = resource.with_tenant(t.to_string());
10724 }
10725 let ctx = runtime_iam_context(role, tenant);
10726 if auth_store.check_policy_authz_with_role(principal, action, &resource, &ctx, role) {
10727 Ok(())
10728 } else {
10729 Err(format!(
10730 "principal=`{}` action=`{}` resource=`vector:{}` denied by IAM policy",
10731 principal, action, collection
10732 ))
10733 }
10734 }
10735
10736 fn check_ddl_collection_privilege(
10742 &self,
10743 auth_store: &Arc<crate::auth::store::AuthStore>,
10744 principal: &crate::auth::UserId,
10745 role: crate::auth::Role,
10746 tenant: Option<&str>,
10747 username: &str,
10748 action: &str,
10749 collection: &str,
10750 ) -> Result<(), String> {
10751 self.check_ddl_object_privilege(
10752 auth_store,
10753 principal,
10754 role,
10755 tenant,
10756 username,
10757 action,
10758 "collection",
10759 collection,
10760 crate::auth::Role::Write,
10761 )
10762 }
10763
10764 #[allow(clippy::too_many_arguments)]
10782 fn check_ddl_object_privilege(
10783 &self,
10784 auth_store: &Arc<crate::auth::store::AuthStore>,
10785 principal: &crate::auth::UserId,
10786 role: crate::auth::Role,
10787 tenant: Option<&str>,
10788 username: &str,
10789 action: &str,
10790 resource_kind: &str,
10791 resource_name: &str,
10792 min_role: crate::auth::Role,
10793 ) -> Result<(), String> {
10794 if role < min_role {
10795 let msg = format!(
10796 "principal=`{}` role=`{:?}` cannot issue DDL action=`{}` resource=`{}:{}`",
10797 username, role, action, resource_kind, resource_name
10798 );
10799 self.inner.audit_log.record(
10800 action,
10801 username,
10802 resource_name,
10803 "denied",
10804 crate::json::Value::Null,
10805 );
10806 return Err(msg);
10807 }
10808
10809 if !auth_store.iam_authorization_enabled() {
10810 self.inner.audit_log.record(
10811 action,
10812 username,
10813 resource_name,
10814 "ok",
10815 crate::json::Value::Null,
10816 );
10817 return Ok(());
10818 }
10819
10820 let mut resource = crate::auth::policies::ResourceRef::new(
10821 resource_kind.to_string(),
10822 resource_name.to_string(),
10823 );
10824 if let Some(t) = tenant {
10825 resource = resource.with_tenant(t.to_string());
10826 }
10827 let ctx = runtime_iam_context(role, tenant);
10828 if auth_store.check_policy_authz_with_role(principal, action, &resource, &ctx, role) {
10829 self.inner.audit_log.record(
10830 action,
10831 username,
10832 resource_name,
10833 "ok",
10834 crate::json::Value::Null,
10835 );
10836 Ok(())
10837 } else {
10838 self.inner.audit_log.record(
10839 action,
10840 username,
10841 resource_name,
10842 "denied",
10843 crate::json::Value::Null,
10844 );
10845 Err(format!(
10846 "principal=`{}` action=`{}` resource=`{}:{}` denied by IAM policy",
10847 username, action, resource_kind, resource_name
10848 ))
10849 }
10850 }
10851
10852 fn execute_grant_statement(
10854 &self,
10855 query: &str,
10856 stmt: &crate::storage::query::ast::GrantStmt,
10857 ) -> RedDBResult<RuntimeQueryResult> {
10858 use crate::auth::privileges::{Action, GrantPrincipal, Resource};
10859 use crate::auth::UserId;
10860 use crate::storage::query::ast::{GrantObjectKind, GrantPrincipalRef};
10861
10862 let auth_store = self
10863 .inner
10864 .auth_store
10865 .read()
10866 .clone()
10867 .ok_or_else(|| RedDBError::Query("auth store not configured".to_string()))?;
10868
10869 let (gname, grole) = current_auth_identity().ok_or_else(|| {
10871 RedDBError::Query("GRANT requires an authenticated principal".to_string())
10872 })?;
10873 let granter = UserId::from_parts(current_tenant().as_deref(), &gname);
10874 let granter_role = grole;
10875
10876 let mut actions: Vec<Action> = Vec::new();
10878 if stmt.all {
10879 actions.push(Action::All);
10880 } else {
10881 for kw in &stmt.actions {
10882 let a = Action::from_keyword(kw).ok_or_else(|| {
10883 RedDBError::Query(format!("unknown privilege keyword `{}`", kw))
10884 })?;
10885 actions.push(a);
10886 }
10887 }
10888
10889 let mut applied = 0usize;
10891 for obj in &stmt.objects {
10892 let resource = match stmt.object_kind {
10893 GrantObjectKind::Table => Resource::Table {
10894 schema: obj.schema.clone(),
10895 table: obj.name.clone(),
10896 },
10897 GrantObjectKind::Schema => Resource::Schema(obj.name.clone()),
10898 GrantObjectKind::Database => Resource::Database,
10899 GrantObjectKind::Function => Resource::Function {
10900 schema: obj.schema.clone(),
10901 name: obj.name.clone(),
10902 },
10903 };
10904 for principal in &stmt.principals {
10905 let p = match principal {
10906 GrantPrincipalRef::Public => GrantPrincipal::Public,
10907 GrantPrincipalRef::Group(g) => GrantPrincipal::Group(g.clone()),
10908 GrantPrincipalRef::User { tenant, name } => {
10909 GrantPrincipal::User(UserId::from_parts(tenant.as_deref(), name))
10910 }
10911 };
10912 let tenant = granter.tenant.clone();
10915 auth_store
10916 .grant(
10917 &granter,
10918 granter_role,
10919 p.clone(),
10920 resource.clone(),
10921 actions.clone(),
10922 stmt.with_grant_option,
10923 tenant.clone(),
10924 )
10925 .map_err(|e| RedDBError::Query(e.to_string()))?;
10926
10927 if let Some(policy) =
10931 grant_to_iam_policy(&p, &resource, &actions, tenant.as_deref())
10932 {
10933 let pid = policy.id.clone();
10934 auth_store
10935 .put_policy_internal(policy)
10936 .map_err(|e| RedDBError::Query(e.to_string()))?;
10937 let attachment = match &p {
10938 GrantPrincipal::User(uid) => {
10939 crate::auth::store::PrincipalRef::User(uid.clone())
10940 }
10941 GrantPrincipal::Group(group) => {
10942 crate::auth::store::PrincipalRef::Group(group.clone())
10943 }
10944 GrantPrincipal::Public => crate::auth::store::PrincipalRef::Group(
10945 crate::auth::store::PUBLIC_IAM_GROUP.to_string(),
10946 ),
10947 };
10948 auth_store
10949 .attach_policy(attachment, &pid)
10950 .map_err(|e| RedDBError::Query(e.to_string()))?;
10951 }
10952 applied += 1;
10953 tracing::info!(
10954 target: "audit",
10955 principal = %granter,
10956 action = "grant",
10957 "GRANT applied"
10958 );
10959 }
10960 }
10961
10962 self.invalidate_result_cache();
10963 Ok(RuntimeQueryResult::ok_message(
10964 query.to_string(),
10965 &format!("GRANT applied to {} target(s)", applied),
10966 "grant",
10967 ))
10968 }
10969
10970 fn execute_revoke_statement(
10972 &self,
10973 query: &str,
10974 stmt: &crate::storage::query::ast::RevokeStmt,
10975 ) -> RedDBResult<RuntimeQueryResult> {
10976 use crate::auth::privileges::{Action, GrantPrincipal, Resource};
10977 use crate::auth::UserId;
10978 use crate::storage::query::ast::{GrantObjectKind, GrantPrincipalRef};
10979
10980 let auth_store = self
10981 .inner
10982 .auth_store
10983 .read()
10984 .clone()
10985 .ok_or_else(|| RedDBError::Query("auth store not configured".to_string()))?;
10986
10987 let (_gname, grole) = current_auth_identity().ok_or_else(|| {
10988 RedDBError::Query("REVOKE requires an authenticated principal".to_string())
10989 })?;
10990 let granter_role = grole;
10991
10992 let actions: Vec<Action> = if stmt.all {
10993 vec![Action::All]
10994 } else {
10995 stmt.actions
10996 .iter()
10997 .map(|kw| Action::from_keyword(kw).unwrap_or(Action::Select))
10998 .collect()
10999 };
11000
11001 let mut total_removed = 0usize;
11002 for obj in &stmt.objects {
11003 let resource = match stmt.object_kind {
11004 GrantObjectKind::Table => Resource::Table {
11005 schema: obj.schema.clone(),
11006 table: obj.name.clone(),
11007 },
11008 GrantObjectKind::Schema => Resource::Schema(obj.name.clone()),
11009 GrantObjectKind::Database => Resource::Database,
11010 GrantObjectKind::Function => Resource::Function {
11011 schema: obj.schema.clone(),
11012 name: obj.name.clone(),
11013 },
11014 };
11015 for principal in &stmt.principals {
11016 let p = match principal {
11017 GrantPrincipalRef::Public => GrantPrincipal::Public,
11018 GrantPrincipalRef::Group(g) => GrantPrincipal::Group(g.clone()),
11019 GrantPrincipalRef::User { tenant, name } => {
11020 GrantPrincipal::User(UserId::from_parts(tenant.as_deref(), name))
11021 }
11022 };
11023 let removed = auth_store
11024 .revoke(granter_role, &p, &resource, &actions)
11025 .map_err(|e| RedDBError::Query(e.to_string()))?;
11026 let _removed_policies =
11027 auth_store.delete_synthetic_grant_policies(&p, &resource, &actions);
11028 total_removed += removed;
11029 }
11030 }
11031
11032 self.invalidate_result_cache();
11033 Ok(RuntimeQueryResult::ok_message(
11034 query.to_string(),
11035 &format!("REVOKE removed {} grant(s)", total_removed),
11036 "revoke",
11037 ))
11038 }
11039
11040 fn execute_create_user_statement(
11042 &self,
11043 query: &str,
11044 stmt: &crate::storage::query::ast::CreateUserStmt,
11045 ) -> RedDBResult<RuntimeQueryResult> {
11046 let auth_store = self
11047 .inner
11048 .auth_store
11049 .read()
11050 .clone()
11051 .ok_or_else(|| RedDBError::Query("auth store not configured".to_string()))?;
11052
11053 let (_gname, grole) = current_auth_identity().ok_or_else(|| {
11054 RedDBError::Query("CREATE USER requires an authenticated principal".to_string())
11055 })?;
11056 if grole != crate::auth::Role::Admin {
11057 return Err(RedDBError::Query(
11058 "CREATE USER requires Admin role".to_string(),
11059 ));
11060 }
11061
11062 let role = crate::auth::Role::from_str(&stmt.role)
11063 .ok_or_else(|| RedDBError::Query(format!("invalid role `{}`", stmt.role)))?;
11064 let user = auth_store
11065 .create_user_in_tenant(stmt.tenant.as_deref(), &stmt.username, &stmt.password, role)
11066 .map_err(|e| RedDBError::Query(e.to_string()))?;
11067
11068 self.invalidate_result_cache();
11069 let target = crate::auth::UserId::from_parts(user.tenant_id.as_deref(), &user.username);
11070 tracing::info!(
11071 target: "audit",
11072 principal = %target,
11073 role = %role,
11074 action = "create_user",
11075 "CREATE USER applied"
11076 );
11077
11078 Ok(RuntimeQueryResult::ok_message(
11079 query.to_string(),
11080 &format!("CREATE USER {} applied", target),
11081 "create_user",
11082 ))
11083 }
11084
11085 fn execute_alter_user_statement(
11087 &self,
11088 query: &str,
11089 stmt: &crate::storage::query::ast::AlterUserStmt,
11090 ) -> RedDBResult<RuntimeQueryResult> {
11091 use crate::auth::privileges::UserAttributes;
11092 use crate::auth::UserId;
11093 use crate::storage::query::ast::AlterUserAttribute;
11094
11095 let auth_store = self
11096 .inner
11097 .auth_store
11098 .read()
11099 .clone()
11100 .ok_or_else(|| RedDBError::Query("auth store not configured".to_string()))?;
11101
11102 let (_gname, grole) = current_auth_identity().ok_or_else(|| {
11103 RedDBError::Query("ALTER USER requires an authenticated principal".to_string())
11104 })?;
11105 if grole != crate::auth::Role::Admin {
11106 return Err(RedDBError::Query(
11107 "ALTER USER requires Admin role".to_string(),
11108 ));
11109 }
11110
11111 let target = UserId::from_parts(stmt.tenant.as_deref(), &stmt.username);
11112
11113 let mut attrs = auth_store.user_attributes(&target);
11116 let mut enable_change: Option<bool> = None;
11117
11118 for a in &stmt.attributes {
11119 match a {
11120 AlterUserAttribute::ValidUntil(ts) => {
11121 let ms = parse_timestamp_to_ms(ts).ok_or_else(|| {
11125 RedDBError::Query(format!("invalid VALID UNTIL timestamp `{ts}`"))
11126 })?;
11127 attrs.valid_until = Some(ms);
11128 }
11129 AlterUserAttribute::ConnectionLimit(n) => {
11130 if *n < 0 {
11131 return Err(RedDBError::Query(
11132 "CONNECTION LIMIT must be non-negative".to_string(),
11133 ));
11134 }
11135 attrs.connection_limit = Some(*n as u32);
11136 }
11137 AlterUserAttribute::SetSearchPath(p) => {
11138 attrs.search_path = Some(p.clone());
11139 }
11140 AlterUserAttribute::AddGroup(g) => {
11141 if !attrs.groups.iter().any(|existing| existing == g) {
11142 attrs.groups.push(g.clone());
11143 attrs.groups.sort();
11144 }
11145 }
11146 AlterUserAttribute::DropGroup(g) => {
11147 attrs.groups.retain(|existing| existing != g);
11148 }
11149 AlterUserAttribute::Enable => enable_change = Some(true),
11150 AlterUserAttribute::Disable => enable_change = Some(false),
11151 AlterUserAttribute::Password(_) => {
11152 }
11156 }
11157 }
11158
11159 auth_store
11160 .set_user_attributes(&target, attrs)
11161 .map_err(|e| RedDBError::Query(e.to_string()))?;
11162 if let Some(en) = enable_change {
11163 auth_store
11164 .set_user_enabled(&target, en)
11165 .map_err(|e| RedDBError::Query(e.to_string()))?;
11166 }
11167 self.invalidate_result_cache();
11168 tracing::info!(
11169 target: "audit",
11170 principal = %target,
11171 action = "alter_user",
11172 "ALTER USER applied"
11173 );
11174
11175 Ok(RuntimeQueryResult::ok_message(
11176 query.to_string(),
11177 &format!("ALTER USER {} applied", target),
11178 "alter_user",
11179 ))
11180 }
11181
11182 fn execute_create_iam_policy(
11187 &self,
11188 query: &str,
11189 id: &str,
11190 json: &str,
11191 ) -> RedDBResult<RuntimeQueryResult> {
11192 use crate::auth::policies::Policy;
11193
11194 let auth_store = self
11195 .inner
11196 .auth_store
11197 .read()
11198 .clone()
11199 .ok_or_else(|| RedDBError::Query("auth store not configured".to_string()))?;
11200
11201 let mut policy = Policy::from_json_str(json)
11206 .map_err(|e| RedDBError::Query(format!("policy parse: {e}")))?;
11207 if policy.id != id {
11208 policy.id = id.to_string();
11209 }
11210 let pid = policy.id.clone();
11211 let tenant = current_tenant();
11212 let (actor_name, actor_role) = current_auth_identity()
11213 .unwrap_or_else(|| ("anonymous".to_string(), crate::auth::Role::Read));
11214 let actor = crate::auth::UserId::from_parts(tenant.as_deref(), &actor_name);
11215 let eval_ctx = runtime_iam_context(actor_role, tenant.as_deref());
11216 let event_ctx = self.policy_mutation_control_ctx(&actor, tenant.as_deref());
11217 let ledger = self.inner.control_event_ledger.read();
11218 let control = crate::auth::store::PolicyMutationControl {
11219 ctx: &event_ctx,
11220 ledger: ledger.as_ref(),
11221 config: self.inner.control_event_config,
11222 registry: Some(self.inner.config_registry.as_ref()),
11223 actor: &actor,
11224 eval_ctx: &eval_ctx,
11225 };
11226 auth_store
11227 .put_policy_with_control_events(policy, &control)
11228 .map_err(|e| RedDBError::Query(e.to_string()))?;
11229
11230 let principal = actor_name;
11231 tracing::info!(
11232 target: "audit",
11233 principal = %principal,
11234 action = "iam:policy.put",
11235 matched_policy_id = %pid,
11236 "CREATE POLICY applied"
11237 );
11238 self.inner.audit_log.record(
11239 "iam/policy.put",
11240 &principal,
11241 &pid,
11242 "ok",
11243 crate::json::Value::Null,
11244 );
11245
11246 self.invalidate_result_cache();
11247 Ok(RuntimeQueryResult::ok_message(
11248 query.to_string(),
11249 &format!("policy `{pid}` stored"),
11250 "create_iam_policy",
11251 ))
11252 }
11253
11254 fn execute_drop_iam_policy(&self, query: &str, id: &str) -> RedDBResult<RuntimeQueryResult> {
11255 let auth_store = self
11256 .inner
11257 .auth_store
11258 .read()
11259 .clone()
11260 .ok_or_else(|| RedDBError::Query("auth store not configured".to_string()))?;
11261 let tenant = current_tenant();
11262 let (actor_name, actor_role) = current_auth_identity()
11263 .unwrap_or_else(|| ("anonymous".to_string(), crate::auth::Role::Read));
11264 let actor = crate::auth::UserId::from_parts(tenant.as_deref(), &actor_name);
11265 let eval_ctx = runtime_iam_context(actor_role, tenant.as_deref());
11266 let event_ctx = self.policy_mutation_control_ctx(&actor, tenant.as_deref());
11267 let ledger = self.inner.control_event_ledger.read();
11268 let control = crate::auth::store::PolicyMutationControl {
11269 ctx: &event_ctx,
11270 ledger: ledger.as_ref(),
11271 config: self.inner.control_event_config,
11272 registry: Some(self.inner.config_registry.as_ref()),
11273 actor: &actor,
11274 eval_ctx: &eval_ctx,
11275 };
11276 auth_store
11277 .delete_policy_with_control_events(id, &control)
11278 .map_err(|e| RedDBError::Query(e.to_string()))?;
11279
11280 let principal = actor_name;
11281 tracing::info!(
11282 target: "audit",
11283 principal = %principal,
11284 action = "iam:policy.drop",
11285 matched_policy_id = %id,
11286 "DROP POLICY applied"
11287 );
11288 self.inner.audit_log.record(
11289 "iam/policy.drop",
11290 &principal,
11291 id,
11292 "ok",
11293 crate::json::Value::Null,
11294 );
11295
11296 self.invalidate_result_cache();
11297 Ok(RuntimeQueryResult::ok_message(
11298 query.to_string(),
11299 &format!("policy `{id}` dropped"),
11300 "drop_iam_policy",
11301 ))
11302 }
11303
11304 fn execute_attach_policy(
11305 &self,
11306 query: &str,
11307 policy_id: &str,
11308 principal: &crate::storage::query::ast::PolicyPrincipalRef,
11309 ) -> RedDBResult<RuntimeQueryResult> {
11310 use crate::auth::store::PrincipalRef;
11311 use crate::auth::UserId;
11312 use crate::storage::query::ast::PolicyPrincipalRef;
11313
11314 let auth_store = self
11315 .inner
11316 .auth_store
11317 .read()
11318 .clone()
11319 .ok_or_else(|| RedDBError::Query("auth store not configured".to_string()))?;
11320 let p = match principal {
11321 PolicyPrincipalRef::User(u) => {
11322 PrincipalRef::User(UserId::from_parts(u.tenant.as_deref(), &u.username))
11323 }
11324 PolicyPrincipalRef::Group(g) => PrincipalRef::Group(g.clone()),
11325 };
11326 let pretty_target = principal_label(principal);
11327 let tenant = current_tenant();
11328 let (actor_name, actor_role) = current_auth_identity()
11329 .unwrap_or_else(|| ("anonymous".to_string(), crate::auth::Role::Read));
11330 let actor = crate::auth::UserId::from_parts(tenant.as_deref(), &actor_name);
11331 let eval_ctx = runtime_iam_context(actor_role, tenant.as_deref());
11332 let event_ctx = self.policy_mutation_control_ctx(&actor, tenant.as_deref());
11333 let ledger = self.inner.control_event_ledger.read();
11334 let control = crate::auth::store::PolicyMutationControl {
11335 ctx: &event_ctx,
11336 ledger: ledger.as_ref(),
11337 config: self.inner.control_event_config,
11338 registry: Some(self.inner.config_registry.as_ref()),
11339 actor: &actor,
11340 eval_ctx: &eval_ctx,
11341 };
11342 auth_store
11343 .attach_policy_with_control_events(p, policy_id, &control)
11344 .map_err(|e| RedDBError::Query(e.to_string()))?;
11345
11346 let principal_str = actor_name;
11347 tracing::info!(
11348 target: "audit",
11349 principal = %principal_str,
11350 action = "iam:policy.attach",
11351 matched_policy_id = %policy_id,
11352 target = %pretty_target,
11353 "ATTACH POLICY applied"
11354 );
11355 self.inner.audit_log.record(
11356 "iam/policy.attach",
11357 &principal_str,
11358 &pretty_target,
11359 "ok",
11360 crate::json::Value::Null,
11361 );
11362
11363 self.invalidate_result_cache();
11364 Ok(RuntimeQueryResult::ok_message(
11365 query.to_string(),
11366 &format!("policy `{policy_id}` attached to {pretty_target}"),
11367 "attach_policy",
11368 ))
11369 }
11370
11371 fn execute_detach_policy(
11372 &self,
11373 query: &str,
11374 policy_id: &str,
11375 principal: &crate::storage::query::ast::PolicyPrincipalRef,
11376 ) -> RedDBResult<RuntimeQueryResult> {
11377 use crate::auth::store::PrincipalRef;
11378 use crate::auth::UserId;
11379 use crate::storage::query::ast::PolicyPrincipalRef;
11380
11381 let auth_store = self
11382 .inner
11383 .auth_store
11384 .read()
11385 .clone()
11386 .ok_or_else(|| RedDBError::Query("auth store not configured".to_string()))?;
11387 let p = match principal {
11388 PolicyPrincipalRef::User(u) => {
11389 PrincipalRef::User(UserId::from_parts(u.tenant.as_deref(), &u.username))
11390 }
11391 PolicyPrincipalRef::Group(g) => PrincipalRef::Group(g.clone()),
11392 };
11393 let pretty_target = principal_label(principal);
11394 let tenant = current_tenant();
11395 let (actor_name, actor_role) = current_auth_identity()
11396 .unwrap_or_else(|| ("anonymous".to_string(), crate::auth::Role::Read));
11397 let actor = crate::auth::UserId::from_parts(tenant.as_deref(), &actor_name);
11398 let eval_ctx = runtime_iam_context(actor_role, tenant.as_deref());
11399 let event_ctx = self.policy_mutation_control_ctx(&actor, tenant.as_deref());
11400 let ledger = self.inner.control_event_ledger.read();
11401 let control = crate::auth::store::PolicyMutationControl {
11402 ctx: &event_ctx,
11403 ledger: ledger.as_ref(),
11404 config: self.inner.control_event_config,
11405 registry: Some(self.inner.config_registry.as_ref()),
11406 actor: &actor,
11407 eval_ctx: &eval_ctx,
11408 };
11409 auth_store
11410 .detach_policy_with_control_events(p, policy_id, &control)
11411 .map_err(|e| RedDBError::Query(e.to_string()))?;
11412
11413 let principal_str = actor_name;
11414 tracing::info!(
11415 target: "audit",
11416 principal = %principal_str,
11417 action = "iam:policy.detach",
11418 matched_policy_id = %policy_id,
11419 target = %pretty_target,
11420 "DETACH POLICY applied"
11421 );
11422 self.inner.audit_log.record(
11423 "iam/policy.detach",
11424 &principal_str,
11425 &pretty_target,
11426 "ok",
11427 crate::json::Value::Null,
11428 );
11429
11430 self.invalidate_result_cache();
11431 Ok(RuntimeQueryResult::ok_message(
11432 query.to_string(),
11433 &format!("policy `{policy_id}` detached from {pretty_target}"),
11434 "detach_policy",
11435 ))
11436 }
11437
11438 fn execute_show_policies(
11439 &self,
11440 query: &str,
11441 filter: Option<&crate::storage::query::ast::PolicyPrincipalRef>,
11442 ) -> RedDBResult<RuntimeQueryResult> {
11443 use crate::auth::UserId;
11444 use crate::storage::query::ast::PolicyPrincipalRef;
11445 use crate::storage::query::unified::UnifiedRecord;
11446 use crate::storage::schema::Value as SchemaValue;
11447 use std::sync::Arc;
11448
11449 let auth_store = self
11450 .inner
11451 .auth_store
11452 .read()
11453 .clone()
11454 .ok_or_else(|| RedDBError::Query("auth store not configured".to_string()))?;
11455
11456 let pols = match filter {
11457 None => auth_store.list_policies(),
11458 Some(PolicyPrincipalRef::User(u)) => {
11459 let id = UserId::from_parts(u.tenant.as_deref(), &u.username);
11460 auth_store.effective_policies(&id)
11461 }
11462 Some(PolicyPrincipalRef::Group(g)) => auth_store.group_policies(g),
11463 };
11464
11465 let mut records = Vec::with_capacity(pols.len() + 1);
11466
11467 let mode = auth_store.enforcement_mode();
11472 let mut header = UnifiedRecord::default();
11473 header.set_arc(
11474 Arc::from("id"),
11475 SchemaValue::text("<enforcement_mode>".to_string()),
11476 );
11477 header.set_arc(Arc::from("statements"), SchemaValue::Integer(0));
11478 header.set_arc(Arc::from("tenant"), SchemaValue::Null);
11479 let header_json = format!(
11480 r#"{{"enforcement_mode":"{}","policy_only_hard_version":"{}"}}"#,
11481 mode.as_str(),
11482 crate::auth::enforcement_mode::POLICY_ONLY_HARD_VERSION
11483 );
11484 header.set_arc(Arc::from("json"), SchemaValue::text(header_json));
11485 records.push(header);
11486
11487 for p in pols.iter() {
11488 let mut rec = UnifiedRecord::default();
11489 rec.set_arc(Arc::from("id"), SchemaValue::text(p.id.clone()));
11490 rec.set_arc(
11491 Arc::from("statements"),
11492 SchemaValue::Integer(p.statements.len() as i64),
11493 );
11494 rec.set_arc(
11495 Arc::from("tenant"),
11496 p.tenant
11497 .as_deref()
11498 .map(|t| SchemaValue::text(t.to_string()))
11499 .unwrap_or(SchemaValue::Null),
11500 );
11501 rec.set_arc(Arc::from("json"), SchemaValue::text(p.to_json_string()));
11502 records.push(rec);
11503 }
11504 let mut result = crate::storage::query::unified::UnifiedResult::empty();
11505 result.records = records;
11506 Ok(RuntimeQueryResult {
11507 query: query.to_string(),
11508 mode: crate::storage::query::modes::QueryMode::Sql,
11509 statement: "show_policies",
11510 engine: "iam-policies",
11511 result,
11512 affected_rows: 0,
11513 statement_type: "select",
11514 bookmark: None,
11515 })
11516 }
11517
11518 fn execute_show_effective_permissions(
11519 &self,
11520 query: &str,
11521 user: &crate::storage::query::ast::PolicyUserRef,
11522 resource: Option<&crate::storage::query::ast::PolicyResourceRef>,
11523 ) -> RedDBResult<RuntimeQueryResult> {
11524 use crate::auth::UserId;
11525 use crate::storage::query::unified::UnifiedRecord;
11526 use crate::storage::schema::Value as SchemaValue;
11527 use std::sync::Arc;
11528
11529 let auth_store = self
11530 .inner
11531 .auth_store
11532 .read()
11533 .clone()
11534 .ok_or_else(|| RedDBError::Query("auth store not configured".to_string()))?;
11535 let id = UserId::from_parts(user.tenant.as_deref(), &user.username);
11536 let pols = auth_store.effective_policies(&id);
11537
11538 let mut records = Vec::new();
11541 for p in pols.iter() {
11542 for (idx, st) in p.statements.iter().enumerate() {
11543 if let Some(_r) = resource {
11544 }
11548 let mut rec = UnifiedRecord::default();
11549 rec.set_arc(Arc::from("policy_id"), SchemaValue::text(p.id.clone()));
11550 rec.set_arc(
11551 Arc::from("statement_index"),
11552 SchemaValue::Integer(idx as i64),
11553 );
11554 rec.set_arc(
11555 Arc::from("sid"),
11556 st.sid
11557 .as_deref()
11558 .map(|s| SchemaValue::text(s.to_string()))
11559 .unwrap_or(SchemaValue::Null),
11560 );
11561 rec.set_arc(
11562 Arc::from("effect"),
11563 SchemaValue::text(match st.effect {
11564 crate::auth::policies::Effect::Allow => "allow",
11565 crate::auth::policies::Effect::Deny => "deny",
11566 }),
11567 );
11568 rec.set_arc(
11569 Arc::from("actions"),
11570 SchemaValue::Integer(st.actions.len() as i64),
11571 );
11572 rec.set_arc(
11573 Arc::from("resources"),
11574 SchemaValue::Integer(st.resources.len() as i64),
11575 );
11576 records.push(rec);
11577 }
11578 }
11579 let mut result = crate::storage::query::unified::UnifiedResult::empty();
11580 result.records = records;
11581 Ok(RuntimeQueryResult {
11582 query: query.to_string(),
11583 mode: crate::storage::query::modes::QueryMode::Sql,
11584 statement: "show_effective_permissions",
11585 engine: "iam-policies",
11586 result,
11587 affected_rows: 0,
11588 statement_type: "select",
11589 bookmark: None,
11590 })
11591 }
11592
11593 fn execute_lint_policy(
11594 &self,
11595 query: &str,
11596 source: &crate::storage::query::ast::LintPolicySource,
11597 ) -> RedDBResult<RuntimeQueryResult> {
11598 use crate::auth::policy_linter::lint;
11599 use crate::storage::query::ast::LintPolicySource;
11600 use crate::storage::query::unified::UnifiedRecord;
11601 use crate::storage::schema::Value as SchemaValue;
11602 use std::sync::Arc;
11603
11604 let policy_text = match source {
11609 LintPolicySource::Json(text) => text.clone(),
11610 LintPolicySource::Id(id) => {
11611 let auth_store =
11612 self.inner.auth_store.read().clone().ok_or_else(|| {
11613 RedDBError::Query("auth store not configured".to_string())
11614 })?;
11615 let policy = auth_store
11616 .get_policy(id)
11617 .ok_or_else(|| RedDBError::Query(format!("policy `{id}` not found")))?;
11618 policy.to_json_string()
11619 }
11620 };
11621 let diagnostics = lint(&policy_text);
11622
11623 let principal_str = current_auth_identity()
11624 .map(|(u, _)| u)
11625 .unwrap_or_else(|| "anonymous".into());
11626 tracing::info!(
11627 target: "audit",
11628 principal = %principal_str,
11629 action = "iam:policy.lint",
11630 diagnostic_count = diagnostics.len(),
11631 "LINT POLICY issued"
11632 );
11633 self.inner.audit_log.record(
11634 "iam/policy.lint",
11635 &principal_str,
11636 match source {
11637 LintPolicySource::Id(id) => id.as_str(),
11638 LintPolicySource::Json(_) => "<json>",
11639 },
11640 "ok",
11641 crate::json::Value::Null,
11642 );
11643
11644 const COLUMNS: [&str; 5] = ["severity", "code", "message", "suggested_fix", "location"];
11647 let schema = Arc::new(
11648 COLUMNS
11649 .iter()
11650 .map(|name| Arc::<str>::from(*name))
11651 .collect::<Vec<_>>(),
11652 );
11653 let records: Vec<UnifiedRecord> = diagnostics
11654 .iter()
11655 .map(|d| {
11656 UnifiedRecord::with_schema(
11657 Arc::clone(&schema),
11658 vec![
11659 SchemaValue::text(d.severity.as_str()),
11660 SchemaValue::text(d.code.as_str()),
11661 SchemaValue::text(d.message.clone()),
11662 d.suggested_fix
11663 .as_deref()
11664 .map(SchemaValue::text)
11665 .unwrap_or(SchemaValue::Null),
11666 d.location
11667 .as_deref()
11668 .map(SchemaValue::text)
11669 .unwrap_or(SchemaValue::Null),
11670 ],
11671 )
11672 })
11673 .collect();
11674 let mut result = crate::storage::query::unified::UnifiedResult::with_columns(
11675 COLUMNS.iter().map(|c| c.to_string()).collect(),
11676 );
11677 result.records = records;
11678 Ok(RuntimeQueryResult {
11679 query: query.to_string(),
11680 mode: crate::storage::query::modes::QueryMode::Sql,
11681 statement: "lint_policy",
11682 engine: "iam-policies",
11683 result,
11684 affected_rows: 0,
11685 statement_type: "select",
11686 bookmark: None,
11687 })
11688 }
11689
11690 fn execute_migrate_policy_mode(
11695 &self,
11696 query: &str,
11697 target: &str,
11698 dry_run: bool,
11699 ) -> RedDBResult<RuntimeQueryResult> {
11700 use crate::auth::enforcement_mode::PolicyEnforcementMode;
11701 use crate::auth::migrate_policy_mode::{
11702 principal_label, simulate_migration_delta, MigratePolicyDelta,
11703 };
11704 use crate::auth::policies::ResourceRef;
11705 use crate::storage::query::unified::UnifiedRecord;
11706 use crate::storage::schema::Value as SchemaValue;
11707 use std::sync::Arc;
11708
11709 let parsed = PolicyEnforcementMode::parse(target).ok_or_else(|| {
11714 RedDBError::Query(format!(
11715 "MIGRATE POLICY MODE: invalid target `{target}` (expected `policy_only`)"
11716 ))
11717 })?;
11718 if parsed != PolicyEnforcementMode::PolicyOnly {
11719 return Err(RedDBError::Query(format!(
11720 "MIGRATE POLICY MODE: target `{target}` is not supported — only `policy_only` may be migrated to via this command"
11721 )));
11722 }
11723
11724 let auth_store = self
11725 .inner
11726 .auth_store
11727 .read()
11728 .clone()
11729 .ok_or_else(|| RedDBError::Query("auth store not configured".to_string()))?;
11730
11731 let snapshot = self.inner.db.catalog_model_snapshot();
11739 let resources: Vec<ResourceRef> = snapshot
11740 .collections
11741 .iter()
11742 .map(|c| ResourceRef::new("table", c.name.clone()))
11743 .collect();
11744
11745 let now_ms = crate::utils::now_unix_millis() as u128;
11746 let deltas: Vec<MigratePolicyDelta> =
11747 simulate_migration_delta(auth_store.as_ref(), &resources, now_ms);
11748
11749 let principal_str = current_auth_identity()
11750 .map(|(u, _)| u)
11751 .unwrap_or_else(|| "anonymous".into());
11752
11753 let outcome_str = if dry_run {
11757 "dry_run"
11758 } else if deltas.is_empty() {
11759 "applied"
11760 } else {
11761 "refused"
11762 };
11763 tracing::info!(
11764 target: "audit",
11765 principal = %principal_str,
11766 action = "iam:policy.migrate_mode",
11767 target = %target,
11768 dry_run,
11769 delta_count = deltas.len(),
11770 outcome = outcome_str,
11771 "MIGRATE POLICY MODE issued"
11772 );
11773 self.inner.audit_log.record(
11774 "iam/policy.migrate_mode",
11775 &principal_str,
11776 target,
11777 outcome_str,
11778 crate::json::Value::Null,
11779 );
11780
11781 if !dry_run && !deltas.is_empty() {
11785 let summary = deltas
11786 .iter()
11787 .take(5)
11788 .map(|d| {
11789 format!(
11790 "{}:{}/{}:{}",
11791 principal_label(&d.principal),
11792 d.action,
11793 d.resource_kind,
11794 d.resource_name
11795 )
11796 })
11797 .collect::<Vec<_>>()
11798 .join(", ");
11799 let more = if deltas.len() > 5 {
11800 format!(" (and {} more)", deltas.len() - 5)
11801 } else {
11802 String::new()
11803 };
11804 return Err(RedDBError::Query(format!(
11805 "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}",
11806 n = deltas.len(),
11807 )));
11808 }
11809
11810 if !dry_run {
11814 auth_store.set_enforcement_mode(parsed);
11815 }
11816
11817 const COLUMNS: [&str; 5] = [
11818 "principal",
11819 "role",
11820 "action",
11821 "resource_kind",
11822 "resource_name",
11823 ];
11824 let schema = Arc::new(
11825 COLUMNS
11826 .iter()
11827 .map(|name| Arc::<str>::from(*name))
11828 .collect::<Vec<_>>(),
11829 );
11830 let records: Vec<UnifiedRecord> = deltas
11831 .iter()
11832 .map(|d| {
11833 UnifiedRecord::with_schema(
11834 Arc::clone(&schema),
11835 vec![
11836 SchemaValue::text(principal_label(&d.principal)),
11837 SchemaValue::text(d.role.as_str()),
11838 SchemaValue::text(d.action.clone()),
11839 SchemaValue::text(d.resource_kind.clone()),
11840 SchemaValue::text(d.resource_name.clone()),
11841 ],
11842 )
11843 })
11844 .collect();
11845 let mut result = crate::storage::query::unified::UnifiedResult::with_columns(
11846 COLUMNS.iter().map(|c| c.to_string()).collect(),
11847 );
11848 result.records = records;
11849 Ok(RuntimeQueryResult {
11850 query: query.to_string(),
11851 mode: crate::storage::query::modes::QueryMode::Sql,
11852 statement: "migrate_policy_mode",
11853 engine: "iam-policies",
11854 result,
11855 affected_rows: 0,
11856 statement_type: "select",
11857 bookmark: None,
11858 })
11859 }
11860
11861 fn execute_simulate_policy(
11862 &self,
11863 query: &str,
11864 user: &crate::storage::query::ast::PolicyUserRef,
11865 action: &str,
11866 resource: &crate::storage::query::ast::PolicyResourceRef,
11867 ) -> RedDBResult<RuntimeQueryResult> {
11868 use crate::auth::policies::ResourceRef;
11869 use crate::auth::store::SimCtx;
11870 use crate::auth::UserId;
11871 use crate::storage::query::unified::UnifiedRecord;
11872 use crate::storage::schema::Value as SchemaValue;
11873 use std::sync::Arc;
11874
11875 let auth_store = self
11876 .inner
11877 .auth_store
11878 .read()
11879 .clone()
11880 .ok_or_else(|| RedDBError::Query("auth store not configured".to_string()))?;
11881 let id = UserId::from_parts(user.tenant.as_deref(), &user.username);
11882 let r = ResourceRef::new(resource.kind.clone(), resource.name.clone());
11883 let outcome = auth_store.simulate(&id, action, &r, SimCtx::default());
11884
11885 let principal_str = current_auth_identity()
11886 .map(|(u, _)| u)
11887 .unwrap_or_else(|| "anonymous".into());
11888 let (decision_str, matched_pid, matched_sid) = decision_to_strings(&outcome.decision);
11889 tracing::info!(
11890 target: "audit",
11891 principal = %principal_str,
11892 action = "iam:policy.simulate",
11893 decision = %decision_str,
11894 matched_policy_id = ?matched_pid,
11895 matched_sid = ?matched_sid,
11896 "SIMULATE issued"
11897 );
11898 self.inner.audit_log.record(
11899 "iam/policy.simulate",
11900 &principal_str,
11901 &id.to_string(),
11902 "ok",
11903 crate::json::Value::Null,
11904 );
11905
11906 let mut rec = UnifiedRecord::default();
11907 rec.set_arc(Arc::from("decision"), SchemaValue::text(decision_str));
11908 rec.set_arc(
11909 Arc::from("matched_policy_id"),
11910 matched_pid
11911 .map(SchemaValue::text)
11912 .unwrap_or(SchemaValue::Null),
11913 );
11914 rec.set_arc(
11915 Arc::from("matched_sid"),
11916 matched_sid
11917 .map(SchemaValue::text)
11918 .unwrap_or(SchemaValue::Null),
11919 );
11920 rec.set_arc(Arc::from("reason"), SchemaValue::text(outcome.reason));
11921 rec.set_arc(
11922 Arc::from("trail_len"),
11923 SchemaValue::Integer(outcome.trail.len() as i64),
11924 );
11925 let mut result = crate::storage::query::unified::UnifiedResult::empty();
11926 result.records = vec![rec];
11927 Ok(RuntimeQueryResult {
11928 query: query.to_string(),
11929 mode: crate::storage::query::modes::QueryMode::Sql,
11930 statement: "simulate_policy",
11931 engine: "iam-policies",
11932 result,
11933 affected_rows: 0,
11934 statement_type: "select",
11935 bookmark: None,
11936 })
11937 }
11938}
11939
11940fn grant_to_iam_policy(
11945 principal: &crate::auth::privileges::GrantPrincipal,
11946 resource: &crate::auth::privileges::Resource,
11947 actions: &[crate::auth::privileges::Action],
11948 tenant: Option<&str>,
11949) -> Option<crate::auth::policies::Policy> {
11950 use crate::auth::policies::{
11951 compile_action, ActionPattern, Effect, Policy, ResourcePattern, Statement,
11952 };
11953 use crate::auth::privileges::{Action, GrantPrincipal, Resource};
11954
11955 if matches!(principal, GrantPrincipal::Group(_)) {
11956 return None;
11957 }
11958
11959 let now = crate::auth::now_ms();
11960 let id = format!("_grant_{:x}_{:x}", now, std::process::id());
11961
11962 let resource_str = match resource {
11963 Resource::Database => "table:*".to_string(),
11964 Resource::Schema(s) => format!("table:{s}.*"),
11965 Resource::Table { schema, table } => match schema {
11966 Some(s) => format!("table:{s}.{table}"),
11967 None => format!("table:{table}"),
11968 },
11969 Resource::Function { schema, name } => match schema {
11970 Some(s) => format!("function:{s}.{name}"),
11971 None => format!("function:{name}"),
11972 },
11973 };
11974
11975 let action_patterns: Vec<ActionPattern> = if actions.contains(&Action::All) {
11979 vec![ActionPattern::Wildcard]
11980 } else {
11981 actions
11982 .iter()
11983 .map(|a| compile_action(&a.as_str().to_ascii_lowercase()))
11984 .collect()
11985 };
11986 if action_patterns.is_empty() {
11987 return None;
11988 }
11989
11990 let resource_patterns = if resource_str == "*" {
11995 vec![ResourcePattern::Wildcard]
11996 } else if resource_str.contains('*') {
11997 vec![ResourcePattern::Glob(resource_str.clone())]
11998 } else if let Some((kind, name)) = resource_str.split_once(':') {
11999 vec![ResourcePattern::Exact {
12000 kind: kind.to_string(),
12001 name: name.to_string(),
12002 }]
12003 } else {
12004 vec![ResourcePattern::Wildcard]
12005 };
12006
12007 let policy = Policy {
12008 id,
12009 version: 1,
12010 tenant: tenant.map(|t| t.to_string()),
12011 created_at: now,
12012 updated_at: now,
12013 statements: vec![Statement {
12014 sid: None,
12015 effect: Effect::Allow,
12016 actions: action_patterns,
12017 resources: resource_patterns,
12018 condition: None,
12019 }],
12020 };
12021 if policy.validate().is_err() {
12022 return None;
12023 }
12024 Some(policy)
12025}
12026
12027fn parse_positive_iterations(func: &str, value: &f64) -> RedDBResult<usize> {
12033 if !value.is_finite() || *value < 1.0 || value.fract() != 0.0 {
12034 return Err(RedDBError::Query(format!(
12035 "table function '{func}' max_iterations must be a positive integer, got {value}"
12036 )));
12037 }
12038 Ok(*value as usize)
12039}
12040
12041fn legacy_action_to_iam(action: crate::auth::privileges::Action) -> &'static str {
12042 use crate::auth::privileges::Action;
12043 match action {
12044 Action::Select => "select",
12045 Action::Insert => "insert",
12046 Action::Update => "update",
12047 Action::Delete => "delete",
12048 Action::Truncate => "truncate",
12049 Action::References => "references",
12050 Action::Execute => "execute",
12051 Action::Usage => "usage",
12052 Action::All => "*",
12053 }
12054}
12055
12056fn update_set_target_columns(query: &crate::storage::query::ast::UpdateQuery) -> Vec<String> {
12057 let mut columns = Vec::new();
12058 for (column, _) in &query.assignment_exprs {
12059 if !columns.iter().any(|seen| seen == column) {
12060 columns.push(column.clone());
12061 }
12062 }
12063 columns
12064}
12065
12066fn column_access_request_for_table_update(
12067 table_name: &str,
12068 columns: Vec<String>,
12069) -> crate::auth::ColumnAccessRequest {
12070 match table_name.split_once('.') {
12071 Some((schema, table)) => {
12072 crate::auth::ColumnAccessRequest::update(table.to_string(), columns)
12073 .with_schema(schema.to_string())
12074 }
12075 None => crate::auth::ColumnAccessRequest::update(table_name.to_string(), columns),
12076 }
12077}
12078
12079fn column_access_request_for_table_select(
12080 table_name: &str,
12081 columns: Vec<String>,
12082) -> crate::auth::ColumnAccessRequest {
12083 match table_name.split_once('.') {
12084 Some((schema, table)) => {
12085 crate::auth::ColumnAccessRequest::select(table.to_string(), columns)
12086 .with_schema(schema.to_string())
12087 }
12088 None => crate::auth::ColumnAccessRequest::select(table_name.to_string(), columns),
12089 }
12090}
12091
12092fn update_returning_columns_for_policy(
12093 runtime: &RedDBRuntime,
12094 query: &crate::storage::query::ast::UpdateQuery,
12095) -> Option<Vec<String>> {
12096 let items = query.returning.as_ref()?;
12097 let mut columns = Vec::new();
12098 let project_all = items
12099 .iter()
12100 .any(|item| matches!(item, crate::storage::query::ast::ReturningItem::All));
12101 if project_all {
12102 collect_returning_star_columns(runtime, query, &mut columns);
12103 } else {
12104 for item in items {
12105 let crate::storage::query::ast::ReturningItem::Column(column) = item else {
12106 continue;
12107 };
12108 push_returning_policy_column(&mut columns, column);
12109 }
12110 }
12111 (!columns.is_empty()).then_some(columns)
12112}
12113
12114fn collect_returning_star_columns(
12115 runtime: &RedDBRuntime,
12116 query: &crate::storage::query::ast::UpdateQuery,
12117 columns: &mut Vec<String>,
12118) {
12119 let store = runtime.db().store();
12120 let Some(manager) = store.get_collection(&query.table) else {
12121 return;
12122 };
12123 if let Some(schema) = manager.column_schema() {
12124 for column in schema.iter() {
12125 push_returning_policy_column(columns, column);
12126 }
12127 }
12128 for entity in manager.query_all(|_| true) {
12129 if !returning_entity_matches_update_target(&entity, query.target) {
12130 continue;
12131 }
12132 match &entity.data {
12133 crate::storage::EntityData::Row(row) => {
12134 for (column, _) in row.iter_fields() {
12135 push_returning_policy_column(columns, column);
12136 }
12137 }
12138 crate::storage::EntityData::Node(node) => {
12139 push_returning_policy_column(columns, "label");
12140 push_returning_policy_column(columns, "node_type");
12141 for column in node.properties.keys() {
12142 push_returning_policy_column(columns, column);
12143 }
12144 }
12145 crate::storage::EntityData::Edge(edge) => {
12146 push_returning_policy_column(columns, "label");
12147 push_returning_policy_column(columns, "from_rid");
12148 push_returning_policy_column(columns, "to_rid");
12149 push_returning_policy_column(columns, "weight");
12150 for column in edge.properties.keys() {
12151 push_returning_policy_column(columns, column);
12152 }
12153 }
12154 _ => {}
12155 }
12156 }
12157}
12158
12159fn push_returning_policy_column(columns: &mut Vec<String>, column: &str) {
12160 if returning_public_envelope_column(column) {
12161 return;
12162 }
12163 if !columns.iter().any(|seen| seen == column) {
12164 columns.push(column.to_string());
12165 }
12166}
12167
12168fn returning_public_envelope_column(column: &str) -> bool {
12169 matches!(
12170 column.to_ascii_lowercase().as_str(),
12171 "rid" | "collection" | "kind" | "tenant" | "created_at" | "updated_at"
12172 )
12173}
12174
12175fn returning_entity_matches_update_target(
12176 entity: &crate::storage::UnifiedEntity,
12177 target: crate::storage::query::ast::UpdateTarget,
12178) -> bool {
12179 use crate::storage::query::ast::UpdateTarget;
12180 match target {
12181 UpdateTarget::Rows => {
12182 matches!(returning_row_item_kind(entity), Some(ReturningRowKind::Row))
12183 }
12184 UpdateTarget::Documents => {
12185 matches!(
12186 returning_row_item_kind(entity),
12187 Some(ReturningRowKind::Document)
12188 )
12189 }
12190 UpdateTarget::Kv => matches!(returning_row_item_kind(entity), Some(ReturningRowKind::Kv)),
12191 UpdateTarget::Nodes => matches!(
12192 (&entity.kind, &entity.data),
12193 (
12194 crate::storage::EntityKind::GraphNode(_),
12195 crate::storage::EntityData::Node(_)
12196 )
12197 ),
12198 UpdateTarget::Edges => matches!(
12199 (&entity.kind, &entity.data),
12200 (
12201 crate::storage::EntityKind::GraphEdge(_),
12202 crate::storage::EntityData::Edge(_)
12203 )
12204 ),
12205 }
12206}
12207
12208#[derive(Debug, Clone, Copy, PartialEq, Eq)]
12209enum ReturningRowKind {
12210 Row,
12211 Document,
12212 Kv,
12213}
12214
12215fn returning_row_item_kind(entity: &crate::storage::UnifiedEntity) -> Option<ReturningRowKind> {
12216 let row = entity.data.as_row()?;
12217 let is_kv = row.iter_fields().all(|(column, _)| {
12218 column.eq_ignore_ascii_case("key") || column.eq_ignore_ascii_case("value")
12219 });
12220 if is_kv {
12221 return Some(ReturningRowKind::Kv);
12222 }
12223 let is_document = row
12224 .iter_fields()
12225 .any(|(_, value)| matches!(value, crate::storage::schema::Value::Json(_)));
12226 if is_document {
12227 Some(ReturningRowKind::Document)
12228 } else {
12229 Some(ReturningRowKind::Row)
12230 }
12231}
12232
12233fn requested_table_columns_for_policy(
12234 table: &crate::storage::query::ast::TableQuery,
12235) -> Vec<String> {
12236 use crate::storage::query::sql_lowering::{
12237 effective_table_filter, effective_table_group_by_exprs, effective_table_having_filter,
12238 effective_table_projections,
12239 };
12240
12241 let table_name = table.table.as_str();
12242 let table_alias = table.alias.as_deref();
12243 let mut columns = std::collections::BTreeSet::new();
12244
12245 for projection in effective_table_projections(table) {
12246 collect_projection_columns(&projection, table_name, table_alias, &mut columns);
12247 }
12248 if let Some(filter) = effective_table_filter(table) {
12249 collect_filter_columns(&filter, table_name, table_alias, &mut columns);
12250 }
12251 for expr in effective_table_group_by_exprs(table) {
12252 collect_expr_columns(&expr, table_name, table_alias, &mut columns);
12253 }
12254 if let Some(filter) = effective_table_having_filter(table) {
12255 collect_filter_columns(&filter, table_name, table_alias, &mut columns);
12256 }
12257 for order in &table.order_by {
12258 if let Some(expr) = order.expr.as_ref() {
12259 collect_expr_columns(expr, table_name, table_alias, &mut columns);
12260 } else {
12261 collect_field_ref_column(&order.field, table_name, table_alias, &mut columns);
12262 }
12263 }
12264
12265 columns.into_iter().collect()
12266}
12267
12268fn collect_projection_columns(
12269 projection: &crate::storage::query::ast::Projection,
12270 table_name: &str,
12271 table_alias: Option<&str>,
12272 columns: &mut std::collections::BTreeSet<String>,
12273) {
12274 use crate::storage::query::ast::Projection;
12275 match projection {
12276 Projection::All => {
12277 columns.insert("*".to_string());
12278 }
12279 Projection::Column(column) | Projection::Alias(column, _) => {
12280 if column != "*" {
12281 columns.insert(column.clone());
12282 }
12283 }
12284 Projection::Function(_, args) => {
12285 for arg in args {
12286 collect_projection_columns(arg, table_name, table_alias, columns);
12287 }
12288 }
12289 Projection::Expression(filter, _) => {
12290 collect_filter_columns(filter, table_name, table_alias, columns);
12291 }
12292 Projection::Field(field, _) => {
12293 collect_field_ref_column(field, table_name, table_alias, columns);
12294 }
12295 Projection::Window { args, .. } => {
12299 for arg in args {
12300 collect_projection_columns(arg, table_name, table_alias, columns);
12301 }
12302 }
12303 }
12304}
12305
12306fn collect_filter_columns(
12307 filter: &crate::storage::query::ast::Filter,
12308 table_name: &str,
12309 table_alias: Option<&str>,
12310 columns: &mut std::collections::BTreeSet<String>,
12311) {
12312 use crate::storage::query::ast::Filter;
12313 match filter {
12314 Filter::Compare { field, .. }
12315 | Filter::IsNull(field)
12316 | Filter::IsNotNull(field)
12317 | Filter::In { field, .. }
12318 | Filter::Between { field, .. }
12319 | Filter::Like { field, .. }
12320 | Filter::StartsWith { field, .. }
12321 | Filter::EndsWith { field, .. }
12322 | Filter::Contains { field, .. } => {
12323 collect_field_ref_column(field, table_name, table_alias, columns);
12324 }
12325 Filter::CompareFields { left, right, .. } => {
12326 collect_field_ref_column(left, table_name, table_alias, columns);
12327 collect_field_ref_column(right, table_name, table_alias, columns);
12328 }
12329 Filter::CompareExpr { lhs, rhs, .. } => {
12330 collect_expr_columns(lhs, table_name, table_alias, columns);
12331 collect_expr_columns(rhs, table_name, table_alias, columns);
12332 }
12333 Filter::And(left, right) | Filter::Or(left, right) => {
12334 collect_filter_columns(left, table_name, table_alias, columns);
12335 collect_filter_columns(right, table_name, table_alias, columns);
12336 }
12337 Filter::Not(inner) => collect_filter_columns(inner, table_name, table_alias, columns),
12338 }
12339}
12340
12341fn collect_expr_columns(
12342 expr: &crate::storage::query::ast::Expr,
12343 table_name: &str,
12344 table_alias: Option<&str>,
12345 columns: &mut std::collections::BTreeSet<String>,
12346) {
12347 use crate::storage::query::ast::Expr;
12348 match expr {
12349 Expr::Column { field, .. } => {
12350 collect_field_ref_column(field, table_name, table_alias, columns);
12351 }
12352 Expr::Literal { .. } | Expr::Parameter { .. } => {}
12353 Expr::UnaryOp { operand, .. } | Expr::Cast { inner: operand, .. } => {
12354 collect_expr_columns(operand, table_name, table_alias, columns);
12355 }
12356 Expr::BinaryOp { lhs, rhs, .. } => {
12357 collect_expr_columns(lhs, table_name, table_alias, columns);
12358 collect_expr_columns(rhs, table_name, table_alias, columns);
12359 }
12360 Expr::FunctionCall { args, .. } => {
12361 for arg in args {
12362 collect_expr_columns(arg, table_name, table_alias, columns);
12363 }
12364 }
12365 Expr::Case {
12366 branches, else_, ..
12367 } => {
12368 for (condition, value) in branches {
12369 collect_expr_columns(condition, table_name, table_alias, columns);
12370 collect_expr_columns(value, table_name, table_alias, columns);
12371 }
12372 if let Some(value) = else_ {
12373 collect_expr_columns(value, table_name, table_alias, columns);
12374 }
12375 }
12376 Expr::IsNull { operand, .. } => {
12377 collect_expr_columns(operand, table_name, table_alias, columns);
12378 }
12379 Expr::InList { target, values, .. } => {
12380 collect_expr_columns(target, table_name, table_alias, columns);
12381 for value in values {
12382 collect_expr_columns(value, table_name, table_alias, columns);
12383 }
12384 }
12385 Expr::Between {
12386 target, low, high, ..
12387 } => {
12388 collect_expr_columns(target, table_name, table_alias, columns);
12389 collect_expr_columns(low, table_name, table_alias, columns);
12390 collect_expr_columns(high, table_name, table_alias, columns);
12391 }
12392 Expr::Subquery { .. } => {}
12393 Expr::WindowFunctionCall { args, window, .. } => {
12394 for arg in args {
12395 collect_expr_columns(arg, table_name, table_alias, columns);
12396 }
12397 for e in &window.partition_by {
12398 collect_expr_columns(e, table_name, table_alias, columns);
12399 }
12400 for o in &window.order_by {
12401 collect_expr_columns(&o.expr, table_name, table_alias, columns);
12402 }
12403 }
12404 }
12405}
12406
12407fn collect_field_ref_column(
12408 field: &crate::storage::query::ast::FieldRef,
12409 table_name: &str,
12410 table_alias: Option<&str>,
12411 columns: &mut std::collections::BTreeSet<String>,
12412) {
12413 if let Some(column) = policy_column_name_from_field_ref(field, table_name, table_alias) {
12414 if column != "*" {
12415 columns.insert(column);
12416 }
12417 }
12418}
12419
12420fn policy_column_name_from_field_ref(
12421 field: &crate::storage::query::ast::FieldRef,
12422 table_name: &str,
12423 table_alias: Option<&str>,
12424) -> Option<String> {
12425 match field {
12426 crate::storage::query::ast::FieldRef::TableColumn { table, column } => {
12427 if column == "*" {
12428 return Some("*".to_string());
12429 }
12430 if table.is_empty() || table == table_name || Some(table.as_str()) == table_alias {
12431 Some(column.clone())
12432 } else {
12433 Some(format!("{table}.{column}"))
12434 }
12435 }
12436 _ => None,
12437 }
12438}
12439
12440fn legacy_resource_to_iam(
12441 resource: &crate::auth::privileges::Resource,
12442 tenant: Option<&str>,
12443) -> crate::auth::policies::ResourceRef {
12444 use crate::auth::privileges::Resource;
12445
12446 let (kind, name) = match resource {
12447 Resource::Database => ("database".to_string(), "*".to_string()),
12448 Resource::Schema(s) => ("schema".to_string(), format!("{s}.*")),
12449 Resource::Table { schema, table } => (
12450 "table".to_string(),
12451 match schema {
12452 Some(s) => format!("{s}.{table}"),
12453 None => table.clone(),
12454 },
12455 ),
12456 Resource::Function { schema, name } => (
12457 "function".to_string(),
12458 match schema {
12459 Some(s) => format!("{s}.{name}"),
12460 None => name.clone(),
12461 },
12462 ),
12463 };
12464
12465 let mut out = crate::auth::policies::ResourceRef::new(kind, name);
12466 if let Some(t) = tenant {
12467 out = out.with_tenant(t.to_string());
12468 }
12469 out
12470}
12471
12472#[derive(Debug)]
12473struct JoinTableSide {
12474 table: String,
12475 alias: String,
12476}
12477
12478fn table_side_context(expr: &QueryExpr) -> Option<JoinTableSide> {
12479 match expr {
12480 QueryExpr::Table(table) => Some(JoinTableSide {
12481 table: table.table.clone(),
12482 alias: table.alias.clone().unwrap_or_else(|| table.table.clone()),
12483 }),
12484 _ => None,
12485 }
12486}
12487
12488fn collect_projection_columns_for_table(
12489 projection: &Projection,
12490 table: &str,
12491 alias: Option<&str>,
12492 out: &mut BTreeSet<String>,
12493) {
12494 match projection {
12495 Projection::Column(column) | Projection::Alias(column, _) => {
12496 match split_qualified_column(column) {
12497 Some((qualifier, column))
12498 if qualifier == table || alias.is_some_and(|alias| qualifier == alias) =>
12499 {
12500 push_policy_column(column, out);
12501 }
12502 Some(_) => {}
12503 None => push_policy_column(column, out),
12504 }
12505 }
12506 Projection::Field(
12507 FieldRef::TableColumn {
12508 table: qualifier,
12509 column,
12510 },
12511 _,
12512 ) => {
12513 if qualifier.is_empty()
12514 || qualifier == table
12515 || alias.is_some_and(|alias| qualifier == alias)
12516 {
12517 push_policy_column(column, out);
12518 }
12519 }
12520 Projection::Field(
12521 FieldRef::NodeProperty {
12522 alias: qualifier,
12523 property,
12524 },
12525 _,
12526 )
12527 | Projection::Field(
12528 FieldRef::EdgeProperty {
12529 alias: qualifier,
12530 property,
12531 },
12532 _,
12533 ) => {
12534 if qualifier == table || alias.is_some_and(|alias| qualifier == alias) {
12535 push_policy_column(property, out);
12536 }
12537 }
12538 Projection::Function(_, args) => {
12539 for arg in args {
12540 collect_projection_columns_for_table(arg, table, alias, out);
12541 }
12542 }
12543 Projection::Expression(_, _) | Projection::All | Projection::Field(_, _) => {}
12544 Projection::Window { args, .. } => {
12545 for arg in args {
12546 collect_projection_columns_for_table(arg, table, alias, out);
12547 }
12548 }
12549 }
12550}
12551
12552fn collect_projection_columns_for_join_side(
12553 projection: &Projection,
12554 left: Option<&JoinTableSide>,
12555 right: Option<&JoinTableSide>,
12556 out: &mut HashMap<String, BTreeSet<String>>,
12557) -> RedDBResult<()> {
12558 match projection {
12559 Projection::Column(column) | Projection::Alias(column, _) => {
12560 if let Some((qualifier, column)) = split_qualified_column(column) {
12561 push_qualified_join_column(qualifier, column, left, right, out);
12562 } else {
12563 push_unqualified_join_column(column, left, right, out);
12564 }
12565 }
12566 Projection::Field(FieldRef::TableColumn { table, column }, _) => {
12567 if table.is_empty() {
12568 push_unqualified_join_column(column, left, right, out);
12569 } else if let Some(side) = [left, right]
12570 .into_iter()
12571 .flatten()
12572 .find(|side| table == side.table.as_str() || table == side.alias.as_str())
12573 {
12574 push_join_column(&side.table, column, out);
12575 }
12576 }
12577 Projection::Field(FieldRef::NodeProperty { alias, property }, _)
12578 | Projection::Field(FieldRef::EdgeProperty { alias, property }, _) => {
12579 push_qualified_join_column(alias, property, left, right, out);
12580 }
12581 Projection::Function(_, args) => {
12582 for arg in args {
12583 collect_projection_columns_for_join_side(arg, left, right, out)?;
12584 }
12585 }
12586 Projection::Expression(_, _) | Projection::All | Projection::Field(_, _) => {}
12587 Projection::Window { args, .. } => {
12588 for arg in args {
12589 collect_projection_columns_for_join_side(arg, left, right, out)?;
12590 }
12591 }
12592 }
12593 Ok(())
12594}
12595
12596fn split_qualified_column(column: &str) -> Option<(&str, &str)> {
12597 let (qualifier, column) = column.split_once('.')?;
12598 if qualifier.is_empty() || column.is_empty() || column.contains('.') {
12599 return None;
12600 }
12601 Some((qualifier, column))
12602}
12603
12604fn push_qualified_join_column(
12605 qualifier: &str,
12606 column: &str,
12607 left: Option<&JoinTableSide>,
12608 right: Option<&JoinTableSide>,
12609 out: &mut HashMap<String, BTreeSet<String>>,
12610) {
12611 if let Some(side) = [left, right]
12612 .into_iter()
12613 .flatten()
12614 .find(|side| qualifier == side.table.as_str() || qualifier == side.alias.as_str())
12615 {
12616 push_join_column(&side.table, column, out);
12617 }
12618}
12619
12620fn push_unqualified_join_column(
12621 column: &str,
12622 left: Option<&JoinTableSide>,
12623 right: Option<&JoinTableSide>,
12624 out: &mut HashMap<String, BTreeSet<String>>,
12625) {
12626 for side in [left, right].into_iter().flatten() {
12627 push_join_column(&side.table, column, out);
12628 }
12629}
12630
12631fn push_join_column(table: &str, column: &str, out: &mut HashMap<String, BTreeSet<String>>) {
12632 if is_policy_column_name(column) {
12633 out.entry(table.to_string())
12634 .or_default()
12635 .insert(column.to_string());
12636 }
12637}
12638
12639fn push_policy_column(column: &str, out: &mut BTreeSet<String>) {
12640 if is_policy_column_name(column) {
12641 out.insert(column.to_string());
12642 }
12643}
12644
12645fn is_policy_column_name(column: &str) -> bool {
12646 !column.is_empty()
12647 && column != "*"
12648 && !column.starts_with("LIT:")
12649 && !column.starts_with("TYPE:")
12650}
12651
12652fn runtime_iam_context(
12653 role: crate::auth::Role,
12654 tenant: Option<&str>,
12655) -> crate::auth::policies::EvalContext {
12656 crate::auth::policies::EvalContext {
12657 principal_tenant: tenant.map(|t| t.to_string()),
12658 current_tenant: tenant.map(|t| t.to_string()),
12659 peer_ip: None,
12660 mfa_present: false,
12661 now_ms: crate::auth::now_ms(),
12662 principal_is_admin_role: role == crate::auth::Role::Admin,
12663 principal_is_platform_scoped: tenant.is_none(),
12664 }
12665}
12666
12667fn explicit_table_projection_columns(
12668 query: &crate::storage::query::ast::TableQuery,
12669) -> Vec<String> {
12670 use crate::storage::query::ast::{FieldRef, Projection};
12671
12672 let mut columns = Vec::new();
12673 for projection in crate::storage::query::sql_lowering::effective_table_projections(query) {
12674 match projection {
12675 Projection::Column(column) | Projection::Alias(column, _) => {
12676 push_unique(&mut columns, column)
12677 }
12678 Projection::Field(FieldRef::TableColumn { column, .. }, _) => {
12679 push_unique(&mut columns, column)
12680 }
12681 _ => {}
12685 }
12686 }
12687 columns
12688}
12689
12690fn explicit_graph_projection_properties(
12691 query: &crate::storage::query::ast::GraphQuery,
12692) -> Vec<String> {
12693 use crate::storage::query::ast::{FieldRef, Projection};
12694
12695 let mut columns = Vec::new();
12696 for projection in &query.return_ {
12697 match projection {
12698 Projection::Field(FieldRef::NodeProperty { property, .. }, _)
12699 | Projection::Field(FieldRef::EdgeProperty { property, .. }, _) => {
12700 push_unique(&mut columns, property.clone())
12701 }
12702 _ => {}
12703 }
12704 }
12705 columns
12706}
12707
12708fn push_unique(columns: &mut Vec<String>, column: String) {
12709 if !columns.iter().any(|existing| existing == &column) {
12710 columns.push(column);
12711 }
12712}
12713
12714fn principal_label(p: &crate::storage::query::ast::PolicyPrincipalRef) -> String {
12715 use crate::storage::query::ast::PolicyPrincipalRef;
12716 match p {
12717 PolicyPrincipalRef::User(u) => match &u.tenant {
12718 Some(t) => format!("user:{t}/{}", u.username),
12719 None => format!("user:{}", u.username),
12720 },
12721 PolicyPrincipalRef::Group(g) => format!("group:{g}"),
12722 }
12723}
12724
12725pub(crate) fn decision_to_strings(
12728 d: &crate::auth::policies::Decision,
12729) -> (String, Option<String>, Option<String>) {
12730 use crate::auth::policies::Decision;
12731 match d {
12732 Decision::Allow {
12733 matched_policy_id,
12734 matched_sid,
12735 } => (
12736 "allow".into(),
12737 Some(matched_policy_id.clone()),
12738 matched_sid.clone(),
12739 ),
12740 Decision::Deny {
12741 matched_policy_id,
12742 matched_sid,
12743 } => (
12744 "deny".into(),
12745 Some(matched_policy_id.clone()),
12746 matched_sid.clone(),
12747 ),
12748 Decision::DefaultDeny => ("default_deny".into(), None, None),
12749 Decision::AdminBypass => ("admin_bypass".into(), None, None),
12750 }
12751}
12752
12753fn relation_scopes_for_query(query: &QueryExpr) -> Vec<String> {
12754 let mut scopes = Vec::new();
12755 collect_relation_scopes(query, &mut scopes);
12756 scopes.sort();
12757 scopes.dedup();
12758 scopes
12759}
12760
12761fn collect_relation_scopes(query: &QueryExpr, scopes: &mut Vec<String>) {
12762 match query {
12763 QueryExpr::Table(table) => {
12764 if !table.table.is_empty() {
12765 scopes.push(table.table.clone());
12766 }
12767 if let Some(alias) = &table.alias {
12768 scopes.push(alias.clone());
12769 }
12770 }
12771 QueryExpr::Join(join) => {
12772 collect_relation_scopes(&join.left, scopes);
12773 collect_relation_scopes(&join.right, scopes);
12774 }
12775 _ => {}
12776 }
12777}
12778
12779fn query_references_outer_scope(query: &QueryExpr, outer_scopes: &[String]) -> bool {
12780 let inner_scopes = relation_scopes_for_query(query);
12781 query_expr_references_outer_scope(query, outer_scopes, &inner_scopes)
12782}
12783
12784fn query_expr_references_outer_scope(
12785 query: &QueryExpr,
12786 outer_scopes: &[String],
12787 inner_scopes: &[String],
12788) -> bool {
12789 match query {
12790 QueryExpr::Table(table) => {
12791 table.select_items.iter().any(|item| match item {
12792 crate::storage::query::ast::SelectItem::Wildcard => false,
12793 crate::storage::query::ast::SelectItem::Expr { expr, .. } => {
12794 expr_references_outer_scope(expr, outer_scopes, inner_scopes)
12795 }
12796 }) || table
12797 .where_expr
12798 .as_ref()
12799 .is_some_and(|expr| expr_references_outer_scope(expr, outer_scopes, inner_scopes))
12800 || table.filter.as_ref().is_some_and(|filter| {
12801 filter_references_outer_scope(filter, outer_scopes, inner_scopes)
12802 })
12803 || table.having_expr.as_ref().is_some_and(|expr| {
12804 expr_references_outer_scope(expr, outer_scopes, inner_scopes)
12805 })
12806 || table.having.as_ref().is_some_and(|filter| {
12807 filter_references_outer_scope(filter, outer_scopes, inner_scopes)
12808 })
12809 || table
12810 .group_by_exprs
12811 .iter()
12812 .any(|expr| expr_references_outer_scope(expr, outer_scopes, inner_scopes))
12813 || table.order_by.iter().any(|clause| {
12814 clause.expr.as_ref().is_some_and(|expr| {
12815 expr_references_outer_scope(expr, outer_scopes, inner_scopes)
12816 })
12817 })
12818 }
12819 QueryExpr::Join(join) => {
12820 query_expr_references_outer_scope(&join.left, outer_scopes, inner_scopes)
12821 || query_expr_references_outer_scope(&join.right, outer_scopes, inner_scopes)
12822 || join.filter.as_ref().is_some_and(|filter| {
12823 filter_references_outer_scope(filter, outer_scopes, inner_scopes)
12824 })
12825 || join.return_items.iter().any(|item| match item {
12826 crate::storage::query::ast::SelectItem::Wildcard => false,
12827 crate::storage::query::ast::SelectItem::Expr { expr, .. } => {
12828 expr_references_outer_scope(expr, outer_scopes, inner_scopes)
12829 }
12830 })
12831 }
12832 _ => false,
12833 }
12834}
12835
12836fn filter_references_outer_scope(
12837 filter: &crate::storage::query::ast::Filter,
12838 outer_scopes: &[String],
12839 inner_scopes: &[String],
12840) -> bool {
12841 use crate::storage::query::ast::Filter;
12842 match filter {
12843 Filter::Compare { field, .. }
12844 | Filter::IsNull(field)
12845 | Filter::IsNotNull(field)
12846 | Filter::In { field, .. }
12847 | Filter::Between { field, .. }
12848 | Filter::Like { field, .. }
12849 | Filter::StartsWith { field, .. }
12850 | Filter::EndsWith { field, .. }
12851 | Filter::Contains { field, .. } => {
12852 field_ref_references_outer_scope(field, outer_scopes, inner_scopes)
12853 }
12854 Filter::CompareFields { left, right, .. } => {
12855 field_ref_references_outer_scope(left, outer_scopes, inner_scopes)
12856 || field_ref_references_outer_scope(right, outer_scopes, inner_scopes)
12857 }
12858 Filter::CompareExpr { lhs, rhs, .. } => {
12859 expr_references_outer_scope(lhs, outer_scopes, inner_scopes)
12860 || expr_references_outer_scope(rhs, outer_scopes, inner_scopes)
12861 }
12862 Filter::And(left, right) | Filter::Or(left, right) => {
12863 filter_references_outer_scope(left, outer_scopes, inner_scopes)
12864 || filter_references_outer_scope(right, outer_scopes, inner_scopes)
12865 }
12866 Filter::Not(inner) => filter_references_outer_scope(inner, outer_scopes, inner_scopes),
12867 }
12868}
12869
12870fn expr_references_outer_scope(
12871 expr: &crate::storage::query::ast::Expr,
12872 outer_scopes: &[String],
12873 inner_scopes: &[String],
12874) -> bool {
12875 use crate::storage::query::ast::Expr;
12876 match expr {
12877 Expr::Column { field, .. } => {
12878 field_ref_references_outer_scope(field, outer_scopes, inner_scopes)
12879 }
12880 Expr::BinaryOp { lhs, rhs, .. } => {
12881 expr_references_outer_scope(lhs, outer_scopes, inner_scopes)
12882 || expr_references_outer_scope(rhs, outer_scopes, inner_scopes)
12883 }
12884 Expr::UnaryOp { operand, .. }
12885 | Expr::Cast { inner: operand, .. }
12886 | Expr::IsNull { operand, .. } => {
12887 expr_references_outer_scope(operand, outer_scopes, inner_scopes)
12888 }
12889 Expr::FunctionCall { args, .. } => args
12890 .iter()
12891 .any(|arg| expr_references_outer_scope(arg, outer_scopes, inner_scopes)),
12892 Expr::Case {
12893 branches, else_, ..
12894 } => {
12895 branches.iter().any(|(cond, value)| {
12896 expr_references_outer_scope(cond, outer_scopes, inner_scopes)
12897 || expr_references_outer_scope(value, outer_scopes, inner_scopes)
12898 }) || else_
12899 .as_ref()
12900 .is_some_and(|expr| expr_references_outer_scope(expr, outer_scopes, inner_scopes))
12901 }
12902 Expr::InList { target, values, .. } => {
12903 expr_references_outer_scope(target, outer_scopes, inner_scopes)
12904 || values
12905 .iter()
12906 .any(|value| expr_references_outer_scope(value, outer_scopes, inner_scopes))
12907 }
12908 Expr::Between {
12909 target, low, high, ..
12910 } => {
12911 expr_references_outer_scope(target, outer_scopes, inner_scopes)
12912 || expr_references_outer_scope(low, outer_scopes, inner_scopes)
12913 || expr_references_outer_scope(high, outer_scopes, inner_scopes)
12914 }
12915 Expr::Subquery { query, .. } => query_references_outer_scope(&query.query, inner_scopes),
12916 Expr::Literal { .. } | Expr::Parameter { .. } => false,
12917 Expr::WindowFunctionCall { args, window, .. } => {
12918 args.iter()
12919 .any(|arg| expr_references_outer_scope(arg, outer_scopes, inner_scopes))
12920 || window
12921 .partition_by
12922 .iter()
12923 .any(|e| expr_references_outer_scope(e, outer_scopes, inner_scopes))
12924 || window
12925 .order_by
12926 .iter()
12927 .any(|o| expr_references_outer_scope(&o.expr, outer_scopes, inner_scopes))
12928 }
12929 }
12930}
12931
12932fn field_ref_references_outer_scope(
12933 field: &crate::storage::query::ast::FieldRef,
12934 outer_scopes: &[String],
12935 inner_scopes: &[String],
12936) -> bool {
12937 match field {
12938 crate::storage::query::ast::FieldRef::TableColumn { table, .. } if !table.is_empty() => {
12939 outer_scopes.iter().any(|scope| scope == table)
12940 && !inner_scopes.iter().any(|scope| scope == table)
12941 }
12942 _ => false,
12943 }
12944}
12945
12946fn first_column_values(
12947 result: crate::storage::query::unified::UnifiedResult,
12948) -> RedDBResult<Vec<Value>> {
12949 if result.columns.len() > 1 {
12950 return Err(RedDBError::Query(
12951 "expression subquery must return exactly one column".to_string(),
12952 ));
12953 }
12954 let fallback_column = result
12955 .records
12956 .first()
12957 .and_then(|record| record.column_names().into_iter().next())
12958 .map(|name| name.to_string());
12959 let column = result.columns.first().cloned().or(fallback_column);
12960 let Some(column) = column else {
12961 return Ok(Vec::new());
12962 };
12963 Ok(result
12964 .records
12965 .iter()
12966 .map(|record| record.get(column.as_str()).cloned().unwrap_or(Value::Null))
12967 .collect())
12968}
12969
12970fn parse_timestamp_to_ms(s: &str) -> Option<u128> {
12971 if let Ok(n) = s.parse::<u128>() {
12973 return Some(n);
12974 }
12975 if let Some(date) = s.split_whitespace().next() {
12979 let parts: Vec<&str> = date.split('-').collect();
12980 if parts.len() == 3 {
12981 let (y, m, d) = (parts[0], parts[1], parts[2]);
12982 if let (Ok(y), Ok(m), Ok(d)) = (y.parse::<i64>(), m.parse::<u32>(), d.parse::<u32>()) {
12983 let days_in = days_from_civil(y, m, d);
12987 return Some((days_in as u128) * 86_400_000u128);
12988 }
12989 }
12990 }
12991 None
12992}
12993
12994fn days_from_civil(y: i64, m: u32, d: u32) -> i64 {
12997 let y = if m <= 2 { y - 1 } else { y };
12998 let era = if y >= 0 { y } else { y - 399 } / 400;
12999 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;
13001 let doe = yoe * 365 + yoe / 4 - yoe / 100 + doy;
13002 era * 146097 + doe as i64 - 719468
13003}
13004
13005fn walk_plan_node(
13006 node: &crate::storage::query::planner::CanonicalLogicalNode,
13007 depth: usize,
13008 out: &mut Vec<crate::storage::query::unified::UnifiedRecord>,
13009) {
13010 use std::sync::Arc;
13011 let mut rec = crate::storage::query::unified::UnifiedRecord::default();
13012 rec.set_arc(Arc::from("op"), Value::text(node.operator.clone()));
13013 rec.set_arc(
13014 Arc::from("source"),
13015 node.source.clone().map(Value::text).unwrap_or(Value::Null),
13016 );
13017 rec.set_arc(Arc::from("est_rows"), Value::Float(node.estimated_rows));
13018 rec.set_arc(Arc::from("est_cost"), Value::Float(node.operator_cost));
13019 rec.set_arc(Arc::from("depth"), Value::Integer(depth as i64));
13020 out.push(rec);
13021 for child in &node.children {
13022 walk_plan_node(child, depth + 1, out);
13023 }
13024}
13025
13026#[cfg(test)]
13027mod inline_graph_tvf_tests {
13028 use super::*;
13029
13030 fn scopes_for(sql: &str) -> HashSet<String> {
13031 let expr = crate::storage::query::parser::parse(sql)
13032 .expect("parse")
13033 .query;
13034 query_expr_result_cache_scopes(&expr)
13035 }
13036
13037 #[test]
13038 fn inline_tvf_cache_scopes_include_source_collections() {
13039 let scopes = scopes_for(
13043 "SELECT * FROM components(nodes => (SELECT id FROM hosts), edges => (SELECT src, dst FROM links))",
13044 );
13045 assert!(scopes.contains("hosts"), "nodes source scoped: {scopes:?}");
13046 assert!(scopes.contains("links"), "edges source scoped: {scopes:?}");
13047 }
13048
13049 #[test]
13050 fn graph_collection_tvf_cache_scope_is_graph_argument() {
13051 let scopes = scopes_for("SELECT * FROM components(g)");
13055 assert!(scopes.contains("g"), "collection form scoped: {scopes:?}");
13056 }
13057
13058 #[test]
13059 fn abstract_degree_centrality_counts_undirected_endpoints() {
13060 let nodes = vec!["a".to_string(), "b".to_string(), "c".to_string()];
13061 let edges = vec![
13062 ("a".to_string(), "b".to_string(), 1.0_f32),
13063 ("b".to_string(), "c".to_string(), 1.0_f32),
13064 ];
13065 let degrees = abstract_degree_centrality(&nodes, &edges);
13066 assert_eq!(
13067 degrees,
13068 vec![
13069 ("a".to_string(), 1),
13070 ("b".to_string(), 2),
13071 ("c".to_string(), 1),
13072 ]
13073 );
13074 }
13075}