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::CreateVcsRef(q) => {
709 let kind = match q.kind {
710 crate::storage::query::ast::VcsRefKind::Branch => "branch",
711 crate::storage::query::ast::VcsRefKind::Tag => "tag",
712 };
713 schema("create_vcs_ref", Some(format!("{kind}:{}", q.name)));
714 }
715 QueryExpr::DropVcsRef(q) => {
716 let kind = match q.kind {
717 crate::storage::query::ast::VcsRefKind::Branch => "branch",
718 crate::storage::query::ast::VcsRefKind::Tag => "tag",
719 };
720 schema("drop_vcs_ref", Some(format!("{kind}:{}", q.name)));
721 }
722 QueryExpr::CreateIndex(q) => {
723 schema(
724 "create_index",
725 Some(format!("index:{}:{}", q.table, q.name)),
726 );
727 }
728 QueryExpr::DropIndex(q) => {
729 schema("drop_index", Some(format!("index:{}:{}", q.table, q.name)));
730 }
731 QueryExpr::CreateTimeSeries(q) => {
732 schema("create_timeseries", Some(format!("timeseries:{}", q.name)));
733 }
734 QueryExpr::CreateMetric(q) => {
735 schema("create_metric", Some(format!("metric:{}", q.path)));
736 }
737 QueryExpr::AlterMetric(q) => {
738 schema("alter_metric", Some(format!("metric:{}", q.path)));
739 }
740 QueryExpr::CreateSlo(q) => {
741 schema("create_slo", Some(format!("slo:{}", q.path)));
742 }
743 QueryExpr::DropTimeSeries(q) => {
744 schema("drop_timeseries", Some(format!("timeseries:{}", q.name)));
745 }
746 QueryExpr::CreateQueue(q) => schema("create_queue", Some(format!("queue:{}", q.name))),
747 QueryExpr::AlterQueue(q) => schema("alter_queue", Some(format!("queue:{}", q.name))),
748 QueryExpr::DropQueue(q) => schema("drop_queue", Some(format!("queue:{}", q.name))),
749 QueryExpr::CreateTree(q) => {
750 schema(
751 "create_tree",
752 Some(format!("tree:{}:{}", q.collection, q.name)),
753 );
754 }
755 QueryExpr::DropTree(q) => {
756 schema(
757 "drop_tree",
758 Some(format!("tree:{}:{}", q.collection, q.name)),
759 );
760 }
761 QueryExpr::CreateSchema(q) => schema("create_schema", Some(format!("schema:{}", q.name))),
762 QueryExpr::DropSchema(q) => schema("drop_schema", Some(format!("schema:{}", q.name))),
763 QueryExpr::CreateSequence(q) => {
764 schema("create_sequence", Some(format!("sequence:{}", q.name)));
765 }
766 QueryExpr::DropSequence(q) => schema("drop_sequence", Some(format!("sequence:{}", q.name))),
767 QueryExpr::CreateView(q) => schema("create_view", Some(format!("view:{}", q.name))),
768 QueryExpr::DropView(q) => schema("drop_view", Some(format!("view:{}", q.name))),
769 QueryExpr::RefreshMaterializedView(q) => {
770 schema(
771 "refresh_materialized_view",
772 Some(format!("view:{}", q.name)),
773 );
774 }
775 QueryExpr::CreatePolicy(q) => {
776 specs.push(QueryControlEventSpec {
777 kind: EventKind::RlsGovernance,
778 action: "create_policy",
779 resource: Some(format!("table:{}:policy:{}", q.table, q.name)),
780 fields: vec![(
781 "target_kind".to_string(),
782 Sensitivity::raw(q.target_kind.as_ident()),
783 )],
784 });
785 }
786 QueryExpr::DropPolicy(q) => {
787 specs.push(QueryControlEventSpec {
788 kind: EventKind::RlsGovernance,
789 action: "drop_policy",
790 resource: Some(format!("table:{}:policy:{}", q.table, q.name)),
791 fields: Vec::new(),
792 });
793 }
794 QueryExpr::SetTenant(value) => {
795 let mut fields = Vec::new();
796 if let Some(value) = value {
797 fields.push(("tenant".to_string(), Sensitivity::raw(value)));
798 }
799 specs.push(QueryControlEventSpec {
800 kind: EventKind::TenantGovernance,
801 action: "set_tenant",
802 resource: Some("tenant:session".to_string()),
803 fields,
804 });
805 }
806 QueryExpr::SetConfig { key, .. } => {
807 specs.push(QueryControlEventSpec {
808 kind: EventKind::ConfigWrite,
809 action: "config:write",
810 resource: Some(format!("config:{key}")),
811 fields: vec![("key".to_string(), Sensitivity::raw(key))],
812 });
813 }
814 QueryExpr::ConfigCommand(cmd) => match cmd {
815 crate::storage::query::ast::ConfigCommand::Put {
816 collection, key, ..
817 }
818 | crate::storage::query::ast::ConfigCommand::Rotate {
819 collection, key, ..
820 } => {
821 let target = format!("{collection}/{key}");
822 specs.push(QueryControlEventSpec {
823 kind: EventKind::ConfigWrite,
824 action: "config:write",
825 resource: Some(format!("config:{target}")),
826 fields: vec![
827 ("collection".to_string(), Sensitivity::raw(collection)),
828 ("key".to_string(), Sensitivity::raw(key)),
829 ],
830 });
831 }
832 crate::storage::query::ast::ConfigCommand::Delete { collection, key } => {
833 let target = format!("{collection}/{key}");
834 specs.push(QueryControlEventSpec {
835 kind: EventKind::ConfigDelete,
836 action: "config:write",
837 resource: Some(format!("config:{target}")),
838 fields: vec![
839 ("collection".to_string(), Sensitivity::raw(collection)),
840 ("key".to_string(), Sensitivity::raw(key)),
841 ],
842 });
843 }
844 _ => {}
845 },
846 QueryExpr::AlterUser(stmt) => {
847 let disables = stmt.attributes.iter().any(|attr| {
848 matches!(
849 attr,
850 crate::storage::query::ast::AlterUserAttribute::Disable
851 )
852 });
853 specs.push(QueryControlEventSpec {
854 kind: if disables {
855 EventKind::UserDisable
856 } else {
857 EventKind::UserUpdate
858 },
859 action: "alter_user",
860 resource: Some(format!("user:{}", stmt.username)),
861 fields: Vec::new(),
862 });
863 }
864 QueryExpr::CreateUser(stmt) => {
865 specs.push(QueryControlEventSpec {
866 kind: EventKind::UserCreate,
867 action: "create_user",
868 resource: Some(format!("user:{}", stmt.username)),
869 fields: Vec::new(),
870 });
871 }
872 _ => {}
873 }
874 specs
875}
876
877pub(crate) fn control_event_outcome_for_error(
878 err: &RedDBError,
879) -> crate::runtime::control_events::Outcome {
880 match err {
881 RedDBError::ReadOnly(_) => crate::runtime::control_events::Outcome::Denied,
882 RedDBError::Query(msg)
883 if msg.contains("permission denied")
884 || msg.contains("cannot issue")
885 || msg.contains("lacks") =>
886 {
887 crate::runtime::control_events::Outcome::Denied
888 }
889 _ => crate::runtime::control_events::Outcome::Error,
890 }
891}
892
893fn view_records_to_entities(
902 table: &str,
903 records: &[crate::storage::query::unified::UnifiedRecord],
904) -> Vec<crate::storage::UnifiedEntity> {
905 use std::collections::HashMap;
906 let table_arc: std::sync::Arc<str> = std::sync::Arc::from(table);
907 let mut out = Vec::with_capacity(records.len());
908 for record in records {
909 let mut named: HashMap<String, crate::storage::schema::Value> = HashMap::new();
910 for (name, value) in record.iter_fields() {
911 named.insert(name.to_string(), value.clone());
912 }
913 let entity = crate::storage::UnifiedEntity::new(
914 crate::storage::EntityId::new(0),
915 crate::storage::EntityKind::TableRow {
916 table: std::sync::Arc::clone(&table_arc),
917 row_id: 0,
918 },
919 crate::storage::EntityData::Row(crate::storage::RowData {
920 columns: Vec::new(),
921 named: Some(named),
922 schema: None,
923 }),
924 );
925 out.push(entity);
926 }
927 out
928}
929
930fn system_keyed_collection_contract(
931 name: &str,
932 model: crate::catalog::CollectionModel,
933) -> crate::physical::CollectionContract {
934 let now = crate::utils::now_unix_millis() as u128;
935 crate::physical::CollectionContract {
936 name: name.to_string(),
937 declared_model: model,
938 schema_mode: crate::catalog::SchemaMode::Dynamic,
939 origin: crate::physical::ContractOrigin::Implicit,
940 version: 1,
941 created_at_unix_ms: now,
942 updated_at_unix_ms: now,
943 default_ttl_ms: None,
944 vector_dimension: None,
945 vector_metric: None,
946 context_index_fields: Vec::new(),
947 declared_columns: Vec::new(),
948 table_def: None,
949 timestamps_enabled: false,
950 context_index_enabled: false,
951 metrics_raw_retention_ms: None,
952 metrics_rollup_policies: Vec::new(),
953 metrics_tenant_identity: None,
954 metrics_namespace: None,
955 append_only: false,
956 subscriptions: Vec::new(),
957 analytics_config: Vec::new(),
958 session_key: None,
959 session_gap_ms: None,
960 retention_duration_ms: None,
961 analytical_storage: None,
962
963 ai_policy: None,
964 }
965}
966
967pub use super::execution_context::{
968 capture_current_snapshot, clear_current_auth_identity, clear_current_connection_id,
969 clear_current_snapshot, clear_current_tenant, current_auth_identity_for_audit,
970 current_connection_id, current_tenant, entity_visible_under_current_snapshot,
971 entity_visible_with_context, set_current_auth_identity, set_current_connection_id,
972 set_current_snapshot, set_current_tenant, snapshot_bundle, with_snapshot_bundle,
973 SnapshotBundle, SnapshotContext,
974};
975pub(crate) use super::execution_context::{
976 current_auth_identity, current_config_value, current_role_projected, current_scope_override,
977 current_secret_value, current_snapshot_requires_index_fallback, current_user_projected,
978 has_scope_override_active, parse_set_local_tenant, update_current_config_value,
979 update_current_secret_value, xids_visible_under_current_snapshot, ConfigSnapshotGuard,
980 CurrentSnapshotGuard, ScopeOverrideGuard, SecretStoreGuard, TxLocalTenantGuard,
981};
982
983fn table_row_index_fields(
984 entity: &crate::storage::unified::entity::UnifiedEntity,
985) -> Vec<(String, crate::storage::schema::Value)> {
986 let crate::storage::EntityData::Row(row) = &entity.data else {
987 return Vec::new();
988 };
989 if let Some(named) = &row.named {
990 return named
991 .iter()
992 .map(|(name, value)| (name.clone(), value.clone()))
993 .collect();
994 }
995 if let Some(schema) = &row.schema {
996 return schema
997 .iter()
998 .zip(row.columns.iter())
999 .map(|(name, value)| (name.clone(), value.clone()))
1000 .collect();
1001 }
1002 Vec::new()
1003}
1004
1005fn named_text(
1006 named: &std::collections::HashMap<String, crate::storage::schema::Value>,
1007 key: &str,
1008) -> Option<String> {
1009 match named.get(key) {
1010 Some(crate::storage::schema::Value::Text(value)) => Some(value.to_string()),
1011 _ => None,
1012 }
1013}
1014
1015fn named_bool(
1016 named: &std::collections::HashMap<String, crate::storage::schema::Value>,
1017 key: &str,
1018) -> Option<bool> {
1019 match named.get(key) {
1020 Some(crate::storage::schema::Value::Boolean(value)) => Some(*value),
1021 _ => None,
1022 }
1023}
1024
1025fn named_i64(
1026 named: &std::collections::HashMap<String, crate::storage::schema::Value>,
1027 key: &str,
1028) -> Option<i64> {
1029 match named.get(key) {
1030 Some(crate::storage::schema::Value::Integer(value)) => Some(*value),
1031 _ => None,
1032 }
1033}
1034
1035fn index_method_kind_as_str(method: super::index_store::IndexMethodKind) -> &'static str {
1036 match method {
1037 super::index_store::IndexMethodKind::Hash => "hash",
1038 super::index_store::IndexMethodKind::Bitmap => "bitmap",
1039 super::index_store::IndexMethodKind::Spatial => "spatial",
1040 super::index_store::IndexMethodKind::BTree => "btree",
1041 super::index_store::IndexMethodKind::H3 { .. } => "h3",
1044 }
1045}
1046
1047fn index_method_kind_resolution(method: super::index_store::IndexMethodKind) -> u8 {
1050 match method {
1051 super::index_store::IndexMethodKind::H3 { resolution } => resolution,
1052 _ => 0,
1053 }
1054}
1055
1056fn index_method_kind_from_str(
1057 raw: &str,
1058 resolution: u8,
1059) -> Option<super::index_store::IndexMethodKind> {
1060 match raw {
1061 "hash" => Some(super::index_store::IndexMethodKind::Hash),
1062 "bitmap" => Some(super::index_store::IndexMethodKind::Bitmap),
1063 "spatial" | "rtree" => Some(super::index_store::IndexMethodKind::Spatial),
1064 "btree" => Some(super::index_store::IndexMethodKind::BTree),
1065 "h3" => Some(super::index_store::IndexMethodKind::H3 { resolution }),
1066 _ => None,
1067 }
1068}
1069
1070fn runtime_pool_lock(runtime: &RedDBRuntime) -> std::sync::MutexGuard<'_, PoolState> {
1071 runtime
1072 .inner
1073 .pool
1074 .lock()
1075 .unwrap_or_else(|poisoned| poisoned.into_inner())
1076}
1077
1078fn json_runtime_value(value: crate::json::Value) -> Value {
1079 crate::json::to_vec(&value)
1080 .map(Value::Json)
1081 .unwrap_or(Value::Null)
1082}
1083
1084fn is_graph_tvf_name(name: &str) -> bool {
1088 name.eq_ignore_ascii_case("components")
1089 || name.eq_ignore_ascii_case("louvain")
1090 || name.eq_ignore_ascii_case("degree_centrality")
1091 || name.eq_ignore_ascii_case("shortest_path")
1092 || name.eq_ignore_ascii_case("betweenness")
1093 || name.eq_ignore_ascii_case("eigenvector")
1094 || name.eq_ignore_ascii_case("pagerank")
1095}
1096
1097fn analytics_view_algorithm(
1104 graph: &str,
1105 view: &crate::catalog::AnalyticsViewDescriptor,
1106) -> RedDBResult<(String, Vec<(String, f64)>)> {
1107 use crate::catalog::AnalyticsOutput;
1108
1109 let mut named_args: Vec<(String, f64)> = Vec::new();
1110 let algorithm = match view.output {
1111 AnalyticsOutput::Communities => {
1112 let algo = view.algorithm.as_deref().unwrap_or("louvain");
1113 if !algo.eq_ignore_ascii_case("louvain") {
1114 return Err(RedDBError::Query(format!(
1115 "analytics output 'communities' on graph '{graph}' has unsupported algorithm '{algo}' (expected louvain)"
1116 )));
1117 }
1118 if let Some(resolution) = view.resolution {
1119 named_args.push(("resolution".to_string(), resolution));
1120 }
1121 "louvain".to_string()
1122 }
1123 AnalyticsOutput::Components => {
1124 if let Some(algo) = view.algorithm.as_deref() {
1125 if !algo.eq_ignore_ascii_case("components")
1126 && !algo.eq_ignore_ascii_case("connected_components")
1127 {
1128 return Err(RedDBError::Query(format!(
1129 "analytics output 'components' on graph '{graph}' has unsupported algorithm '{algo}' (expected connected_components)"
1130 )));
1131 }
1132 }
1133 "components".to_string()
1134 }
1135 AnalyticsOutput::Centrality => {
1136 let algo = view
1137 .algorithm
1138 .as_deref()
1139 .unwrap_or("pagerank")
1140 .to_ascii_lowercase();
1141 match algo.as_str() {
1142 "pagerank" => {
1143 if let Some(max_iterations) = view.max_iterations {
1144 named_args.push(("max_iterations".to_string(), max_iterations as f64));
1145 }
1146 }
1147 "eigenvector" => {
1148 if let Some(max_iterations) = view.max_iterations {
1149 named_args.push(("max_iterations".to_string(), max_iterations as f64));
1150 }
1151 if let Some(tolerance) = view.tolerance {
1152 named_args.push(("tolerance".to_string(), tolerance));
1153 }
1154 }
1155 "betweenness" => {}
1156 other => {
1157 return Err(RedDBError::Query(format!(
1158 "analytics output 'centrality' on graph '{graph}' has unsupported algorithm '{other}' (expected pagerank, betweenness, or eigenvector)"
1159 )));
1160 }
1161 }
1162 algo
1163 }
1164 };
1165 Ok((algorithm, named_args))
1166}
1167
1168fn reject_named_args(name: &str, named_args: &[(String, f64)]) -> RedDBResult<()> {
1170 if let Some((key, _)) = named_args.first() {
1171 return Err(RedDBError::Query(format!(
1172 "table function '{name}' has no named argument '{key}'"
1173 )));
1174 }
1175 Ok(())
1176}
1177
1178fn louvain_resolution(named_args: &[(String, f64)]) -> RedDBResult<f64> {
1181 let mut resolution = 1.0_f64;
1182 for (key, value) in named_args {
1183 if key.eq_ignore_ascii_case("resolution") {
1184 if !value.is_finite() || *value <= 0.0 {
1185 return Err(RedDBError::Query(format!(
1186 "table function 'louvain' resolution must be > 0, got {value}"
1187 )));
1188 }
1189 resolution = *value;
1190 } else {
1191 return Err(RedDBError::Query(format!(
1192 "table function 'louvain' has no named argument '{key}' (expected 'resolution')"
1193 )));
1194 }
1195 }
1196 Ok(resolution)
1197}
1198
1199fn abstract_degree_centrality(
1204 nodes: &[String],
1205 edges: &[(
1206 String,
1207 String,
1208 crate::storage::engine::graph_algorithms::Weight,
1209 )],
1210) -> Vec<(String, usize)> {
1211 let mut degree: std::collections::BTreeMap<String, usize> = std::collections::BTreeMap::new();
1212 for n in nodes {
1213 degree.entry(n.clone()).or_insert(0);
1214 }
1215 for (a, b, _w) in edges {
1216 *degree.entry(a.clone()).or_insert(0) += 1;
1217 *degree.entry(b.clone()).or_insert(0) += 1;
1218 }
1219 degree.into_iter().collect()
1220}
1221
1222fn ordered_result_columns(result: &crate::storage::query::unified::UnifiedResult) -> Vec<String> {
1225 if !result.columns.is_empty() {
1226 return result.columns.clone();
1227 }
1228 result
1229 .records
1230 .first()
1231 .map(|record| {
1232 record
1233 .column_names()
1234 .iter()
1235 .map(|column| column.to_string())
1236 .collect()
1237 })
1238 .unwrap_or_default()
1239}
1240
1241fn value_to_node_id(value: &crate::storage::schema::Value) -> Option<String> {
1245 use crate::storage::schema::Value;
1246 match value {
1247 Value::Null => None,
1248 Value::Text(s) => Some(s.to_string()),
1249 Value::Integer(n) => Some(n.to_string()),
1250 Value::UnsignedInteger(n) => Some(n.to_string()),
1251 Value::NodeRef(s) => Some(s.clone()),
1252 other => Some(other.to_string()),
1253 }
1254}
1255
1256fn value_to_weight(value: &crate::storage::schema::Value) -> Option<f32> {
1258 use crate::storage::schema::Value;
1259 match value {
1260 Value::Float(f) => Some(*f as f32),
1261 Value::Integer(n) => Some(*n as f32),
1262 Value::UnsignedInteger(n) => Some(*n as f32),
1263 _ => None,
1264 }
1265}
1266
1267fn inline_node_ids(
1271 name: &str,
1272 result: &crate::storage::query::unified::UnifiedResult,
1273) -> RedDBResult<Vec<String>> {
1274 if result.records.is_empty() {
1275 return Ok(Vec::new());
1276 }
1277 let columns = ordered_result_columns(result);
1278 let Some(first_col) = columns.first() else {
1279 return Err(RedDBError::Query(format!(
1280 "table function '{name}' inline form: `nodes` subquery must project at least one column (the node id)"
1281 )));
1282 };
1283 let mut ids = Vec::with_capacity(result.records.len());
1284 for record in &result.records {
1285 if let Some(id) = record.get(first_col).and_then(value_to_node_id) {
1286 ids.push(id);
1287 }
1288 }
1289 Ok(ids)
1290}
1291
1292fn inline_edges(
1297 name: &str,
1298 result: &crate::storage::query::unified::UnifiedResult,
1299) -> RedDBResult<
1300 Vec<(
1301 String,
1302 String,
1303 crate::storage::engine::graph_algorithms::Weight,
1304 )>,
1305> {
1306 if result.records.is_empty() {
1307 return Ok(Vec::new());
1308 }
1309 let columns = ordered_result_columns(result);
1310 if columns.len() < 2 {
1311 return Err(RedDBError::Query(format!(
1312 "table function '{name}' inline form: `edges` subquery must project at least two columns (source, target), got {}",
1313 columns.len()
1314 )));
1315 }
1316 let src_col = &columns[0];
1317 let dst_col = &columns[1];
1318 let weight_col = columns.get(2);
1319 let mut edges = Vec::with_capacity(result.records.len());
1320 for record in &result.records {
1321 let (Some(src), Some(dst)) = (
1322 record.get(src_col).and_then(value_to_node_id),
1323 record.get(dst_col).and_then(value_to_node_id),
1324 ) else {
1325 continue;
1327 };
1328 let weight = match weight_col {
1329 Some(col) => match record.get(col) {
1330 None | Some(crate::storage::schema::Value::Null) => 1.0,
1331 Some(value) => value_to_weight(value).ok_or_else(|| {
1332 RedDBError::Query(format!(
1333 "table function '{name}' inline form: `edges` weight column must be numeric"
1334 ))
1335 })?,
1336 },
1337 None => 1.0,
1338 };
1339 edges.push((src, dst, weight));
1340 }
1341 Ok(edges)
1342}
1343
1344fn cache_scope_insert(scopes: &mut HashSet<String>, name: &str) {
1345 if name.is_empty() || name.starts_with("__subq_") || is_universal_query_source(name) {
1346 return;
1347 }
1348 scopes.insert(name.to_string());
1349}
1350
1351fn collect_table_source_scopes(scopes: &mut HashSet<String>, query: &TableQuery) {
1352 match query.source.as_ref() {
1353 Some(crate::storage::query::ast::TableSource::Name(name)) => {
1354 cache_scope_insert(scopes, name)
1355 }
1356 Some(crate::storage::query::ast::TableSource::Subquery(subquery)) => {
1357 collect_query_expr_result_cache_scopes(scopes, subquery);
1358 }
1359 Some(crate::storage::query::ast::TableSource::Function { name, args, .. }) => {
1366 if is_graph_tvf_name(name) {
1367 if let Some(graph) = args.first() {
1368 cache_scope_insert(scopes, graph);
1369 }
1370 }
1371 }
1372 Some(crate::storage::query::ast::TableSource::InlineGraphFunction {
1377 nodes, edges, ..
1378 }) => {
1379 collect_query_expr_result_cache_scopes(scopes, nodes);
1380 collect_query_expr_result_cache_scopes(scopes, edges);
1381 }
1382 None => cache_scope_insert(scopes, &query.table),
1383 }
1384}
1385
1386fn collect_vector_source_scopes(
1387 scopes: &mut HashSet<String>,
1388 source: &crate::storage::query::ast::VectorSource,
1389) {
1390 match source {
1391 crate::storage::query::ast::VectorSource::Reference { collection, .. } => {
1392 cache_scope_insert(scopes, collection);
1393 }
1394 crate::storage::query::ast::VectorSource::Subquery(subquery) => {
1395 collect_query_expr_result_cache_scopes(scopes, subquery);
1396 }
1397 crate::storage::query::ast::VectorSource::Literal(_)
1398 | crate::storage::query::ast::VectorSource::Text(_) => {}
1399 }
1400}
1401
1402fn collect_path_selector_scopes(
1403 scopes: &mut HashSet<String>,
1404 selector: &crate::storage::query::ast::NodeSelector,
1405) {
1406 if let crate::storage::query::ast::NodeSelector::ByRow { table, .. } = selector {
1407 cache_scope_insert(scopes, table);
1408 }
1409}
1410
1411fn collect_query_expr_result_cache_scopes(scopes: &mut HashSet<String>, expr: &QueryExpr) {
1412 match expr {
1413 QueryExpr::Table(query) => collect_table_source_scopes(scopes, query),
1414 QueryExpr::Join(query) => {
1415 collect_query_expr_result_cache_scopes(scopes, &query.left);
1416 collect_query_expr_result_cache_scopes(scopes, &query.right);
1417 }
1418 QueryExpr::Path(query) => {
1419 collect_path_selector_scopes(scopes, &query.from);
1420 collect_path_selector_scopes(scopes, &query.to);
1421 }
1422 QueryExpr::Vector(query) => {
1423 cache_scope_insert(scopes, &query.collection);
1424 collect_vector_source_scopes(scopes, &query.query_vector);
1425 }
1426 QueryExpr::Hybrid(query) => {
1427 collect_query_expr_result_cache_scopes(scopes, &query.structured);
1428 cache_scope_insert(scopes, &query.vector.collection);
1429 collect_vector_source_scopes(scopes, &query.vector.query_vector);
1430 }
1431 QueryExpr::Insert(query) => cache_scope_insert(scopes, &query.table),
1432 QueryExpr::Update(query) => cache_scope_insert(scopes, &query.table),
1433 QueryExpr::Delete(query) => cache_scope_insert(scopes, &query.table),
1434 QueryExpr::CreateTable(query) => cache_scope_insert(scopes, &query.name),
1435 QueryExpr::CreateCollection(query) => cache_scope_insert(scopes, &query.name),
1436 QueryExpr::CreateVector(query) => cache_scope_insert(scopes, &query.name),
1437 QueryExpr::DropTable(query) => cache_scope_insert(scopes, &query.name),
1438 QueryExpr::DropGraph(query) => cache_scope_insert(scopes, &query.name),
1439 QueryExpr::DropVector(query) => cache_scope_insert(scopes, &query.name),
1440 QueryExpr::DropDocument(query) => cache_scope_insert(scopes, &query.name),
1441 QueryExpr::DropKv(query) => cache_scope_insert(scopes, &query.name),
1442 QueryExpr::DropCollection(query) => cache_scope_insert(scopes, &query.name),
1443 QueryExpr::Truncate(query) => cache_scope_insert(scopes, &query.name),
1444 QueryExpr::AlterTable(query) => cache_scope_insert(scopes, &query.name),
1445 QueryExpr::CreateVcsRef(_) | QueryExpr::DropVcsRef(_) => {
1446 cache_scope_insert(scopes, crate::application::vcs_collections::REFS)
1447 }
1448 QueryExpr::CreateIndex(query) => cache_scope_insert(scopes, &query.table),
1449 QueryExpr::DropIndex(query) => cache_scope_insert(scopes, &query.table),
1450 QueryExpr::CreateTimeSeries(query) => cache_scope_insert(scopes, &query.name),
1451 QueryExpr::CreateMetric(query) => cache_scope_insert(scopes, &query.path),
1452 QueryExpr::AlterMetric(query) => cache_scope_insert(scopes, &query.path),
1453 QueryExpr::CreateSlo(query) => cache_scope_insert(scopes, &query.path),
1454 QueryExpr::DropTimeSeries(query) => cache_scope_insert(scopes, &query.name),
1455 QueryExpr::CreateQueue(query) => cache_scope_insert(scopes, &query.name),
1456 QueryExpr::AlterQueue(query) => cache_scope_insert(scopes, &query.name),
1457 QueryExpr::DropQueue(query) => cache_scope_insert(scopes, &query.name),
1458 QueryExpr::QueueSelect(query) => cache_scope_insert(scopes, &query.queue),
1459 QueryExpr::QueueCommand(query) => match query {
1460 QueueCommand::Push { queue, .. }
1461 | QueueCommand::Pop { queue, .. }
1462 | QueueCommand::Peek { queue, .. }
1463 | QueueCommand::Len { queue }
1464 | QueueCommand::Purge { queue }
1465 | QueueCommand::GroupCreate { queue, .. }
1466 | QueueCommand::GroupRead { queue, .. }
1467 | QueueCommand::Pending { queue, .. }
1468 | QueueCommand::Claim { queue, .. }
1469 | QueueCommand::Ack { queue, .. }
1470 | QueueCommand::Nack { queue, .. } => cache_scope_insert(scopes, queue),
1471 QueueCommand::Move {
1472 source,
1473 destination,
1474 ..
1475 } => {
1476 cache_scope_insert(scopes, source);
1477 cache_scope_insert(scopes, destination);
1478 }
1479 },
1480 QueryExpr::EventsBackfill(query) => {
1481 cache_scope_insert(scopes, &query.collection);
1482 cache_scope_insert(scopes, &query.target_queue);
1483 }
1484 QueryExpr::CreateTree(query) => cache_scope_insert(scopes, &query.collection),
1485 QueryExpr::DropTree(query) => cache_scope_insert(scopes, &query.collection),
1486 QueryExpr::TreeCommand(query) => match query {
1487 TreeCommand::Insert { collection, .. }
1488 | TreeCommand::Move { collection, .. }
1489 | TreeCommand::Delete { collection, .. }
1490 | TreeCommand::Validate { collection, .. }
1491 | TreeCommand::Rebalance { collection, .. } => cache_scope_insert(scopes, collection),
1492 },
1493 QueryExpr::SearchCommand(query) => match query {
1494 SearchCommand::Similar { collection, .. }
1495 | SearchCommand::Hybrid { collection, .. }
1496 | SearchCommand::SpatialRadius { collection, .. }
1497 | SearchCommand::SpatialBbox { collection, .. }
1498 | SearchCommand::SpatialNearest { collection, .. } => {
1499 cache_scope_insert(scopes, collection);
1500 }
1501 SearchCommand::Text { collection, .. }
1502 | SearchCommand::Multimodal { collection, .. }
1503 | SearchCommand::Index { collection, .. }
1504 | SearchCommand::Context { collection, .. } => {
1505 if let Some(collection) = collection.as_deref() {
1506 cache_scope_insert(scopes, collection);
1507 }
1508 }
1509 },
1510 QueryExpr::Ask(query) => {
1511 if let Some(collection) = query.collection.as_deref() {
1512 cache_scope_insert(scopes, collection);
1513 }
1514 }
1515 QueryExpr::ExplainAlter(query) => cache_scope_insert(scopes, &query.target.name),
1516 QueryExpr::MaintenanceCommand(cmd) => match cmd {
1517 crate::storage::query::ast::MaintenanceCommand::Vacuum { target, .. }
1518 | crate::storage::query::ast::MaintenanceCommand::Analyze { target } => {
1519 if let Some(t) = target {
1520 cache_scope_insert(scopes, t);
1521 }
1522 }
1523 },
1524 QueryExpr::CopyFrom(cmd) => cache_scope_insert(scopes, &cmd.table),
1525 QueryExpr::CreateView(cmd) => {
1526 cache_scope_insert(scopes, &cmd.name);
1527 collect_query_expr_result_cache_scopes(scopes, &cmd.query);
1529 }
1530 QueryExpr::DropView(cmd) => cache_scope_insert(scopes, &cmd.name),
1531 QueryExpr::RefreshMaterializedView(cmd) => cache_scope_insert(scopes, &cmd.name),
1532 QueryExpr::CreatePolicy(cmd) => cache_scope_insert(scopes, &cmd.table),
1533 QueryExpr::DropPolicy(cmd) => cache_scope_insert(scopes, &cmd.table),
1534 QueryExpr::CreateServer(_) | QueryExpr::DropServer(_) => {}
1535 QueryExpr::CreateForeignTable(cmd) => cache_scope_insert(scopes, &cmd.name),
1536 QueryExpr::DropForeignTable(cmd) => cache_scope_insert(scopes, &cmd.name),
1537 QueryExpr::Graph(_)
1538 | QueryExpr::GraphCommand(_)
1539 | QueryExpr::ProbabilisticCommand(_)
1540 | QueryExpr::SetConfig { .. }
1541 | QueryExpr::ShowConfig { .. }
1542 | QueryExpr::SetSecret { .. }
1543 | QueryExpr::DeleteSecret { .. }
1544 | QueryExpr::ShowSecrets { .. }
1545 | QueryExpr::SetTenant(_)
1546 | QueryExpr::ShowTenant
1547 | QueryExpr::TransactionControl(_)
1548 | QueryExpr::CreateSchema(_)
1549 | QueryExpr::DropSchema(_)
1550 | QueryExpr::CreateSequence(_)
1551 | QueryExpr::DropSequence(_)
1552 | QueryExpr::Grant(_)
1553 | QueryExpr::Revoke(_)
1554 | QueryExpr::AlterUser(_)
1555 | QueryExpr::CreateUser(_)
1556 | QueryExpr::CreateIamPolicy { .. }
1557 | QueryExpr::DropIamPolicy { .. }
1558 | QueryExpr::AttachPolicy { .. }
1559 | QueryExpr::DetachPolicy { .. }
1560 | QueryExpr::ShowPolicies { .. }
1561 | QueryExpr::ShowEffectivePermissions { .. }
1562 | QueryExpr::RankOf(_)
1563 | QueryExpr::ApproxRankOf(_)
1564 | QueryExpr::RankRange(_)
1565 | QueryExpr::SimulatePolicy { .. }
1566 | QueryExpr::LintPolicy { .. }
1567 | QueryExpr::MigratePolicyMode { .. }
1568 | QueryExpr::CreateMigration(_)
1569 | QueryExpr::ApplyMigration(_)
1570 | QueryExpr::RollbackMigration(_)
1571 | QueryExpr::ExplainMigration(_)
1572 | QueryExpr::EventsBackfillStatus { .. } => {}
1573 QueryExpr::KvCommand(cmd) => {
1574 use crate::storage::query::ast::KvCommand;
1575 match cmd {
1576 KvCommand::Put { collection, .. }
1577 | KvCommand::InvalidateTags { collection, .. }
1578 | KvCommand::Get { collection, .. }
1579 | KvCommand::Unseal { collection, .. }
1580 | KvCommand::Rotate { collection, .. }
1581 | KvCommand::History { collection, .. }
1582 | KvCommand::List { collection, .. }
1583 | KvCommand::Purge { collection, .. }
1584 | KvCommand::Watch { collection, .. }
1585 | KvCommand::Delete { collection, .. }
1586 | KvCommand::Incr { collection, .. }
1587 | KvCommand::Cas { collection, .. } => cache_scope_insert(scopes, collection),
1588 }
1589 }
1590 QueryExpr::ConfigCommand(cmd) => {
1591 use crate::storage::query::ast::ConfigCommand;
1592 match cmd {
1593 ConfigCommand::Put { collection, .. }
1594 | ConfigCommand::Get { collection, .. }
1595 | ConfigCommand::Resolve { collection, .. }
1596 | ConfigCommand::Rotate { collection, .. }
1597 | ConfigCommand::Delete { collection, .. }
1598 | ConfigCommand::History { collection, .. }
1599 | ConfigCommand::List { collection, .. }
1600 | ConfigCommand::Watch { collection, .. }
1601 | ConfigCommand::InvalidVolatileOperation { collection, .. } => {
1602 cache_scope_insert(scopes, collection)
1603 }
1604 }
1605 }
1606 _ => {}
1607 }
1608}
1609
1610pub(crate) fn rls_policy_filter(
1618 runtime: &RedDBRuntime,
1619 table: &str,
1620 action: crate::storage::query::ast::PolicyAction,
1621) -> Option<crate::storage::query::ast::Filter> {
1622 rls_policy_filter_for_kind(
1623 runtime,
1624 table,
1625 action,
1626 crate::storage::query::ast::PolicyTargetKind::Table,
1627 )
1628}
1629
1630pub(crate) fn rls_policy_filter_for_kind(
1636 runtime: &RedDBRuntime,
1637 table: &str,
1638 action: crate::storage::query::ast::PolicyAction,
1639 kind: crate::storage::query::ast::PolicyTargetKind,
1640) -> Option<crate::storage::query::ast::Filter> {
1641 use crate::storage::query::ast::Filter;
1642
1643 if !runtime.inner.rls_enabled_tables.read().contains(table) {
1644 return None;
1645 }
1646 let role = current_auth_identity().map(|(_, role)| role);
1647 let role_str = role.map(|r| r.as_str().to_string());
1648 let policies = runtime.matching_rls_policies_for_kind(table, role_str.as_deref(), action, kind);
1649 if policies.is_empty() {
1650 return None;
1651 }
1652 policies
1653 .into_iter()
1654 .reduce(|acc, f| Filter::Or(Box::new(acc), Box::new(f)))
1655}
1656
1657pub(crate) fn rls_is_enabled(runtime: &RedDBRuntime, table: &str) -> bool {
1661 runtime.inner.rls_enabled_tables.read().contains(table)
1662}
1663
1664fn node_passes_rls(
1671 runtime: &RedDBRuntime,
1672 collection: &str,
1673 role: Option<&str>,
1674 cache: &mut std::collections::HashMap<String, Option<crate::storage::query::ast::Filter>>,
1675 entity: &crate::storage::unified::entity::UnifiedEntity,
1676) -> bool {
1677 use crate::storage::query::ast::{Filter, PolicyAction, PolicyTargetKind};
1678
1679 if !runtime.inner.rls_enabled_tables.read().contains(collection) {
1680 return true;
1681 }
1682 let filter = cache.entry(collection.to_string()).or_insert_with(|| {
1683 let policies = runtime.matching_rls_policies_for_kind(
1684 collection,
1685 role,
1686 PolicyAction::Select,
1687 PolicyTargetKind::Nodes,
1688 );
1689 if policies.is_empty() {
1690 None
1691 } else {
1692 policies
1693 .into_iter()
1694 .reduce(|acc, f| Filter::Or(Box::new(acc), Box::new(f)))
1695 }
1696 });
1697 let Some(filter) = filter else {
1698 return false;
1699 };
1700 crate::runtime::query_exec::evaluate_entity_filter_with_db(
1701 Some(&runtime.inner.db),
1702 entity,
1703 filter,
1704 collection,
1705 collection,
1706 )
1707}
1708
1709fn edge_passes_rls(
1712 runtime: &RedDBRuntime,
1713 collection: &str,
1714 role: Option<&str>,
1715 cache: &mut std::collections::HashMap<String, Option<crate::storage::query::ast::Filter>>,
1716 entity: &crate::storage::unified::entity::UnifiedEntity,
1717) -> bool {
1718 use crate::storage::query::ast::{Filter, PolicyAction, PolicyTargetKind};
1719
1720 if !runtime.inner.rls_enabled_tables.read().contains(collection) {
1721 return true;
1722 }
1723 let filter = cache.entry(collection.to_string()).or_insert_with(|| {
1724 let policies = runtime.matching_rls_policies_for_kind(
1725 collection,
1726 role,
1727 PolicyAction::Select,
1728 PolicyTargetKind::Edges,
1729 );
1730 if policies.is_empty() {
1731 None
1732 } else {
1733 policies
1734 .into_iter()
1735 .reduce(|acc, f| Filter::Or(Box::new(acc), Box::new(f)))
1736 }
1737 });
1738 let Some(filter) = filter else {
1739 return false;
1740 };
1741 crate::runtime::query_exec::evaluate_entity_filter_with_db(
1742 Some(&runtime.inner.db),
1743 entity,
1744 filter,
1745 collection,
1746 collection,
1747 )
1748}
1749
1750fn inject_rls_filters(
1771 runtime: &RedDBRuntime,
1772 frame: &dyn super::statement_frame::ReadFrame,
1773 mut table: crate::storage::query::ast::TableQuery,
1774) -> Option<crate::storage::query::ast::TableQuery> {
1775 use crate::storage::query::ast::{Filter, PolicyAction};
1776
1777 let role = frame.identity().map(|(_, role)| role);
1779 let role_str = role.map(|r| r.as_str().to_string());
1780 let policies =
1781 runtime.matching_rls_policies(&table.table, role_str.as_deref(), PolicyAction::Select);
1782
1783 if policies.is_empty() {
1784 return None;
1787 }
1788
1789 let combined = policies
1791 .into_iter()
1792 .reduce(|acc, f| Filter::Or(Box::new(acc), Box::new(f)))
1793 .expect("policies non-empty");
1794
1795 use crate::storage::query::sql_lowering::{expr_to_filter, filter_to_expr};
1804 let had_where_expr = table.where_expr.is_some();
1805 let existing = table
1806 .filter
1807 .take()
1808 .or_else(|| table.where_expr.as_ref().map(expr_to_filter));
1809 let new_filter = match existing {
1810 Some(existing) => Filter::And(Box::new(existing), Box::new(combined)),
1811 None => combined,
1812 };
1813 if had_where_expr {
1816 table.where_expr = Some(filter_to_expr(&new_filter));
1817 }
1818 table.filter = Some(new_filter);
1819 Some(table)
1820}
1821
1822fn inject_rls_into_join(
1832 runtime: &RedDBRuntime,
1833 frame: &dyn super::statement_frame::ReadFrame,
1834 mut join: crate::storage::query::ast::JoinQuery,
1835) -> Option<crate::storage::query::ast::JoinQuery> {
1836 use crate::storage::query::ast::Filter;
1837
1838 let mut policy_filters: Vec<Filter> = Vec::new();
1839 if !collect_join_side_policy(runtime, frame, join.left.as_ref(), &mut policy_filters) {
1840 return None;
1841 }
1842 if !collect_join_side_policy(runtime, frame, join.right.as_ref(), &mut policy_filters) {
1843 return None;
1844 }
1845
1846 if policy_filters.is_empty() {
1847 return Some(join);
1848 }
1849
1850 let combined = policy_filters
1851 .into_iter()
1852 .reduce(|acc, f| Filter::And(Box::new(acc), Box::new(f)))
1853 .expect("policy_filters non-empty");
1854
1855 join.filter = Some(match join.filter.take() {
1856 Some(existing) => Filter::And(Box::new(existing), Box::new(combined)),
1857 None => combined,
1858 });
1859
1860 Some(join)
1861}
1862
1863fn collect_join_side_policy(
1868 runtime: &RedDBRuntime,
1869 frame: &dyn super::statement_frame::ReadFrame,
1870 expr: &crate::storage::query::ast::QueryExpr,
1871 out: &mut Vec<crate::storage::query::ast::Filter>,
1872) -> bool {
1873 use crate::storage::query::ast::{Filter, PolicyAction, QueryExpr};
1874 match expr {
1875 QueryExpr::Table(t) => {
1876 if !runtime.inner.rls_enabled_tables.read().contains(&t.table) {
1877 return true;
1878 }
1879 let role = frame.identity().map(|(_, role)| role);
1880 let role_str = role.map(|r| r.as_str().to_string());
1881 let policies =
1882 runtime.matching_rls_policies(&t.table, role_str.as_deref(), PolicyAction::Select);
1883 if policies.is_empty() {
1884 return false;
1885 }
1886 let combined = policies
1887 .into_iter()
1888 .reduce(|acc, f| Filter::Or(Box::new(acc), Box::new(f)))
1889 .expect("policies non-empty");
1890 out.push(combined);
1891 true
1892 }
1893 QueryExpr::Join(inner) => {
1894 collect_join_side_policy(runtime, frame, inner.left.as_ref(), out)
1895 && collect_join_side_policy(runtime, frame, inner.right.as_ref(), out)
1896 }
1897 _ => true,
1898 }
1899}
1900
1901fn apply_foreign_table_filters(
1912 records: Vec<crate::storage::query::unified::UnifiedRecord>,
1913 query: &crate::storage::query::ast::TableQuery,
1914) -> crate::storage::query::unified::UnifiedResult {
1915 use crate::storage::query::sql_lowering::{
1916 effective_table_filter, effective_table_projections,
1917 };
1918 use crate::storage::query::unified::UnifiedResult;
1919
1920 let filter = effective_table_filter(query);
1921 let projections = effective_table_projections(query);
1922
1923 let mut filtered: Vec<_> = records
1926 .into_iter()
1927 .filter(|record| match &filter {
1928 Some(f) => {
1929 super::join_filter::evaluate_runtime_filter_with_db(None, record, f, None, None)
1930 }
1931 None => true,
1932 })
1933 .collect();
1934
1935 if let Some(offset) = query.offset {
1937 let offset = offset as usize;
1938 if offset >= filtered.len() {
1939 filtered.clear();
1940 } else {
1941 filtered.drain(0..offset);
1942 }
1943 }
1944 if let Some(limit) = query.limit {
1945 filtered.truncate(limit as usize);
1946 }
1947
1948 let columns: Vec<String> = if projections.is_empty() {
1951 filtered
1952 .first()
1953 .map(|r| r.column_names().iter().map(|k| k.to_string()).collect())
1954 .unwrap_or_default()
1955 } else {
1956 projections
1957 .iter()
1958 .map(super::join_filter::projection_name)
1959 .collect()
1960 };
1961
1962 let mut result = UnifiedResult::empty();
1963 result.columns = columns;
1964 result.records = filtered;
1965 result
1966}
1967
1968pub(crate) fn collect_table_refs(expr: &QueryExpr) -> Vec<String> {
1975 let mut scopes: HashSet<String> = HashSet::new();
1976 collect_query_expr_result_cache_scopes(&mut scopes, expr);
1977 scopes.into_iter().collect()
1978}
1979
1980fn query_expr_result_cache_scopes(expr: &QueryExpr) -> HashSet<String> {
1981 let mut scopes = HashSet::new();
1982 collect_query_expr_result_cache_scopes(&mut scopes, expr);
1983 scopes
1984}
1985
1986fn strip_explain_prefix(sql: &str) -> Option<&str> {
2001 let trimmed = sql.trim_start();
2002 let (head, rest) = trimmed.split_at(
2003 trimmed
2004 .find(|c: char| c.is_whitespace())
2005 .unwrap_or(trimmed.len()),
2006 );
2007 if !head.eq_ignore_ascii_case("EXPLAIN") {
2008 return None;
2009 }
2010 let rest = rest.trim_start();
2011 if rest.is_empty() {
2012 return None;
2013 }
2014 let next_head_end = rest.find(|c: char| c.is_whitespace()).unwrap_or(rest.len());
2017 if rest[..next_head_end].eq_ignore_ascii_case("ALTER")
2018 || rest[..next_head_end].eq_ignore_ascii_case("ASK")
2019 || rest[..next_head_end].eq_ignore_ascii_case("MIGRATION")
2020 {
2021 return None;
2022 }
2023 Some(rest)
2024}
2025
2026fn parse_vcs_author(raw: &str) -> crate::application::vcs::Author {
2027 let trimmed = raw.trim();
2028 if let Some((name, rest)) = trimmed.rsplit_once('<') {
2029 if let Some(email) = rest.strip_suffix('>') {
2030 return crate::application::vcs::Author {
2031 name: name.trim().to_string(),
2032 email: email.trim().to_string(),
2033 };
2034 }
2035 }
2036 crate::application::vcs::Author {
2037 name: trimmed.to_string(),
2038 email: String::new(),
2039 }
2040}
2041
2042fn looks_like_commit_hash(value: &str) -> bool {
2043 value.len() >= 7 && value.bytes().all(|byte| byte.is_ascii_hexdigit())
2044}
2045
2046enum RuntimeVcsCommand {
2047 Checkpoint {
2048 message: String,
2049 author: Option<String>,
2050 },
2051 Checkout {
2052 target: String,
2053 },
2054 Reset {
2055 mode: RuntimeVcsResetMode,
2056 target: String,
2057 },
2058 Merge {
2059 branch: String,
2060 },
2061 CherryPick {
2062 commit: String,
2063 },
2064 Revert {
2065 commit: String,
2066 },
2067 ResolveConflict {
2068 key: String,
2069 resolution: RuntimeVcsConflictResolution,
2070 },
2071}
2072
2073enum RuntimeVcsResetMode {
2074 Hard,
2075 Soft,
2076 Mixed,
2077}
2078
2079enum RuntimeVcsConflictResolution {
2080 Ours,
2081 Theirs,
2082}
2083
2084fn strip_keyword_ci<'a>(input: &'a str, keyword: &str) -> Option<&'a str> {
2085 let trimmed = input.trim_start();
2086 if trimmed.len() < keyword.len() || !trimmed[..keyword.len()].eq_ignore_ascii_case(keyword) {
2087 return None;
2088 }
2089 let rest = &trimmed[keyword.len()..];
2090 if rest.is_empty() || rest.starts_with(char::is_whitespace) {
2091 Some(rest.trim_start())
2092 } else {
2093 None
2094 }
2095}
2096
2097fn parse_vcs_quoted(input: &str) -> RedDBResult<(String, &str)> {
2098 let trimmed = input.trim_start();
2099 let rest = trimmed
2100 .strip_prefix('\'')
2101 .ok_or_else(|| RedDBError::Query("expected quoted string".to_string()))?;
2102 let end = rest
2103 .find('\'')
2104 .ok_or_else(|| RedDBError::Query("unterminated quoted string".to_string()))?;
2105 Ok((rest[..end].to_string(), rest[end + 1..].trim_start()))
2106}
2107
2108fn parse_vcs_atom(input: &str) -> RedDBResult<(String, &str)> {
2109 let trimmed = input.trim_start();
2110 if trimmed.starts_with('\'') {
2111 return parse_vcs_quoted(trimmed);
2112 }
2113 let end = trimmed.find(char::is_whitespace).unwrap_or(trimmed.len());
2114 if end == 0 {
2115 return Err(RedDBError::Query("expected VCS argument".to_string()));
2116 }
2117 Ok((trimmed[..end].to_string(), trimmed[end..].trim_start()))
2118}
2119
2120fn expect_vcs_end(rest: &str) -> RedDBResult<()> {
2121 if rest.trim().is_empty() {
2122 Ok(())
2123 } else {
2124 Err(RedDBError::Query(format!(
2125 "unexpected token after VCS command: {}",
2126 rest.trim()
2127 )))
2128 }
2129}
2130
2131fn parse_runtime_vcs_command(query: &str) -> Option<RedDBResult<RuntimeVcsCommand>> {
2132 let trimmed = query.trim_start();
2133 if let Some(rest) = strip_keyword_ci(trimmed, "CHECKPOINT") {
2134 return Some((|| {
2135 let (message, rest) = parse_vcs_quoted(rest)?;
2136 let rest = rest.trim_start();
2137 let author = if let Some(author_rest) = strip_keyword_ci(rest, "AUTHOR") {
2138 let (author, rest) = parse_vcs_quoted(author_rest)?;
2139 expect_vcs_end(rest)?;
2140 Some(author)
2141 } else {
2142 expect_vcs_end(rest)?;
2143 None
2144 };
2145 Ok(RuntimeVcsCommand::Checkpoint { message, author })
2146 })());
2147 }
2148 if let Some(rest) = strip_keyword_ci(trimmed, "CHECKOUT") {
2149 return Some((|| {
2150 let (target, rest) = parse_vcs_atom(rest)?;
2151 expect_vcs_end(rest)?;
2152 Ok(RuntimeVcsCommand::Checkout { target })
2153 })());
2154 }
2155 if let Some(rest) = strip_keyword_ci(trimmed, "RESET") {
2156 if strip_keyword_ci(rest, "TENANT").is_some() {
2157 return None;
2158 }
2159 return Some((|| {
2160 let mut rest = rest.trim_start();
2161 let mode = if let Some(next) = strip_keyword_ci(rest, "HARD") {
2162 rest = next;
2163 RuntimeVcsResetMode::Hard
2164 } else if let Some(next) = strip_keyword_ci(rest, "SOFT") {
2165 rest = next;
2166 RuntimeVcsResetMode::Soft
2167 } else if let Some(next) = strip_keyword_ci(rest, "MIXED") {
2168 rest = next;
2169 RuntimeVcsResetMode::Mixed
2170 } else {
2171 RuntimeVcsResetMode::Mixed
2172 };
2173 let rest = strip_keyword_ci(rest, "TO")
2174 .ok_or_else(|| RedDBError::Query("expected TO in RESET".to_string()))?;
2175 let (target, rest) = parse_vcs_atom(rest)?;
2176 expect_vcs_end(rest)?;
2177 Ok(RuntimeVcsCommand::Reset { mode, target })
2178 })());
2179 }
2180 if let Some(rest) = strip_keyword_ci(trimmed, "MERGE") {
2181 return Some((|| {
2182 let (branch, rest) = parse_vcs_atom(rest)?;
2183 expect_vcs_end(rest)?;
2184 Ok(RuntimeVcsCommand::Merge { branch })
2185 })());
2186 }
2187 if let Some(rest) = strip_keyword_ci(trimmed, "CHERRY") {
2188 return Some((|| {
2189 let rest = strip_keyword_ci(rest, "PICK")
2190 .ok_or_else(|| RedDBError::Query("expected PICK in CHERRY PICK".to_string()))?;
2191 let (commit, rest) = parse_vcs_atom(rest)?;
2192 expect_vcs_end(rest)?;
2193 Ok(RuntimeVcsCommand::CherryPick { commit })
2194 })());
2195 }
2196 if let Some(rest) = strip_keyword_ci(trimmed, "REVERT") {
2197 return Some((|| {
2198 let (commit, rest) = parse_vcs_atom(rest)?;
2199 expect_vcs_end(rest)?;
2200 Ok(RuntimeVcsCommand::Revert { commit })
2201 })());
2202 }
2203 if let Some(rest) = strip_keyword_ci(trimmed, "RESOLVE") {
2204 let rest = strip_keyword_ci(rest, "CONFLICT")?;
2209 return Some((|| {
2210 let (key, rest) = parse_vcs_quoted(rest)?;
2211 let rest = strip_keyword_ci(rest, "USING")
2212 .ok_or_else(|| RedDBError::Query("expected USING in RESOLVE".to_string()))?;
2213 let (resolution, rest) = if let Some(rest) = strip_keyword_ci(rest, "OURS") {
2214 (RuntimeVcsConflictResolution::Ours, rest)
2215 } else if let Some(rest) = strip_keyword_ci(rest, "THEIRS") {
2216 (RuntimeVcsConflictResolution::Theirs, rest)
2217 } else {
2218 return Err(RedDBError::Query(
2219 "expected OURS or THEIRS in RESOLVE".to_string(),
2220 ));
2221 };
2222 expect_vcs_end(rest)?;
2223 Ok(RuntimeVcsCommand::ResolveConflict { key, resolution })
2224 })());
2225 }
2226 None
2227}
2228
2229pub(super) fn has_with_prefix(sql: &str) -> bool {
2234 let trimmed = sql.trim_start();
2235 let head_end = trimmed
2236 .find(|c: char| c.is_whitespace() || c == '(')
2237 .unwrap_or(trimmed.len());
2238 trimmed[..head_end].eq_ignore_ascii_case("WITH")
2239}
2240
2241fn peek_top_level_as_of(sql: &str) -> Option<crate::application::vcs::AsOfSpec> {
2249 peek_top_level_as_of_with_table(sql).map(|(spec, _)| spec)
2250}
2251
2252pub(super) fn peek_top_level_as_of_with_table(
2257 sql: &str,
2258) -> Option<(crate::application::vcs::AsOfSpec, Option<String>)> {
2259 if !sql
2260 .as_bytes()
2261 .windows(5)
2262 .any(|w| w.eq_ignore_ascii_case(b"as of"))
2263 {
2264 return None;
2265 }
2266 let parsed = crate::storage::query::parser::parse(sql).ok()?;
2267 let crate::storage::query::ast::QueryExpr::Table(table) = parsed.query else {
2268 return None;
2269 };
2270 let clause = table.as_of?;
2271 let table_name = if table.table.is_empty() || table.table == "any" {
2272 None
2273 } else {
2274 Some(table.table.clone())
2275 };
2276 let spec = match clause {
2277 crate::storage::query::ast::AsOfClause::Commit(h) => {
2278 crate::application::vcs::AsOfSpec::Commit(h)
2279 }
2280 crate::storage::query::ast::AsOfClause::Branch(b) => {
2281 crate::application::vcs::AsOfSpec::Branch(b)
2282 }
2283 crate::storage::query::ast::AsOfClause::Tag(t) => crate::application::vcs::AsOfSpec::Tag(t),
2284 crate::storage::query::ast::AsOfClause::TimestampMs(ts) => {
2285 crate::application::vcs::AsOfSpec::TimestampMs(ts)
2286 }
2287 crate::storage::query::ast::AsOfClause::Snapshot(x) => {
2288 crate::application::vcs::AsOfSpec::Snapshot(x)
2289 }
2290 };
2291 Some((spec, table_name))
2292}
2293
2294pub(super) fn query_has_volatile_builtin(sql: &str) -> bool {
2295 const VOLATILE_TOKENS: &[&str] = &[
2299 "pg_advisory_lock",
2300 "pg_try_advisory_lock",
2301 "pg_advisory_unlock",
2302 "random()",
2303 "$config",
2308 "$secret",
2309 ];
2314 let lowered = sql.to_ascii_lowercase();
2315 VOLATILE_TOKENS.iter().any(|t| lowered.contains(t))
2316}
2317
2318pub(super) fn query_is_ask_statement(sql: &str) -> bool {
2319 let trimmed = sql.trim_start();
2320 let head_end = trimmed
2321 .find(|c: char| c.is_whitespace() || c == '(' || c == ';')
2322 .unwrap_or(trimmed.len());
2323 trimmed[..head_end].eq_ignore_ascii_case("ASK")
2324}
2325
2326pub(super) fn intent_lock_modes_for(
2336 expr: &QueryExpr,
2337) -> Option<(
2338 crate::storage::transaction::lock::LockMode,
2339 crate::storage::transaction::lock::LockMode,
2340)> {
2341 use crate::storage::transaction::lock::LockMode::{Exclusive, IntentExclusive, IntentShared};
2342
2343 match expr {
2344 QueryExpr::Table(_)
2346 | QueryExpr::Join(_)
2347 | QueryExpr::Vector(_)
2348 | QueryExpr::Hybrid(_)
2349 | QueryExpr::Graph(_)
2350 | QueryExpr::Path(_)
2351 | QueryExpr::Ask(_)
2352 | QueryExpr::SearchCommand(_)
2353 | QueryExpr::GraphCommand(_)
2354 | QueryExpr::RankOf(_)
2355 | QueryExpr::ApproxRankOf(_)
2356 | QueryExpr::RankRange(_)
2357 | QueryExpr::QueueSelect(_) => Some((IntentShared, IntentShared)),
2358
2359 QueryExpr::Insert(_)
2367 | QueryExpr::Update(_)
2368 | QueryExpr::Delete(_)
2369 | QueryExpr::QueueCommand(QueueCommand::Move { .. }) => {
2370 Some((IntentExclusive, IntentExclusive))
2371 }
2372 QueryExpr::QueueCommand(_) => Some((IntentShared, IntentShared)),
2373
2374 QueryExpr::CreateTable(_)
2378 | QueryExpr::CreateCollection(_)
2379 | QueryExpr::CreateVector(_)
2380 | QueryExpr::DropTable(_)
2381 | QueryExpr::DropGraph(_)
2382 | QueryExpr::DropVector(_)
2383 | QueryExpr::DropDocument(_)
2384 | QueryExpr::DropKv(_)
2385 | QueryExpr::DropCollection(_)
2386 | QueryExpr::Truncate(_)
2387 | QueryExpr::AlterTable(_)
2388 | QueryExpr::CreateIndex(_)
2389 | QueryExpr::DropIndex(_)
2390 | QueryExpr::CreateTimeSeries(_)
2391 | QueryExpr::CreateMetric(_)
2392 | QueryExpr::AlterMetric(_)
2393 | QueryExpr::CreateSlo(_)
2394 | QueryExpr::DropTimeSeries(_)
2395 | QueryExpr::CreateQueue(_)
2396 | QueryExpr::AlterQueue(_)
2397 | QueryExpr::DropQueue(_)
2398 | QueryExpr::CreateTree(_)
2399 | QueryExpr::DropTree(_)
2400 | QueryExpr::CreatePolicy(_)
2401 | QueryExpr::DropPolicy(_)
2402 | QueryExpr::CreateView(_)
2403 | QueryExpr::DropView(_)
2404 | QueryExpr::RefreshMaterializedView(_)
2405 | QueryExpr::CreateSchema(_)
2406 | QueryExpr::DropSchema(_)
2407 | QueryExpr::CreateSequence(_)
2408 | QueryExpr::DropSequence(_)
2409 | QueryExpr::CreateServer(_)
2410 | QueryExpr::DropServer(_)
2411 | QueryExpr::CreateForeignTable(_)
2412 | QueryExpr::DropForeignTable(_) => Some((IntentExclusive, Exclusive)),
2413
2414 _ => None,
2420 }
2421}
2422
2423pub(super) fn collections_referenced(expr: &QueryExpr) -> Vec<String> {
2428 let mut out = Vec::new();
2429 walk_collections(expr, &mut out);
2430 out.sort();
2431 out.dedup();
2432 out
2433}
2434
2435fn walk_collections(expr: &QueryExpr, out: &mut Vec<String>) {
2436 match expr {
2437 QueryExpr::Table(t) => out.push(t.table.clone()),
2438 QueryExpr::Join(j) => {
2439 walk_collections(&j.left, out);
2440 walk_collections(&j.right, out);
2441 }
2442 QueryExpr::Insert(i) => out.push(i.table.clone()),
2443 QueryExpr::Update(u) => out.push(u.table.clone()),
2444 QueryExpr::Delete(d) => out.push(d.table.clone()),
2445 QueryExpr::QueueSelect(q) => out.push(q.queue.clone()),
2446
2447 QueryExpr::CreateTable(q) => out.push(q.name.clone()),
2452 QueryExpr::CreateCollection(q) => out.push(q.name.clone()),
2453 QueryExpr::CreateVector(q) => out.push(q.name.clone()),
2454 QueryExpr::DropTable(q) => out.push(q.name.clone()),
2455 QueryExpr::DropGraph(q) => out.push(q.name.clone()),
2456 QueryExpr::DropVector(q) => out.push(q.name.clone()),
2457 QueryExpr::DropDocument(q) => out.push(q.name.clone()),
2458 QueryExpr::DropKv(q) => out.push(q.name.clone()),
2459 QueryExpr::DropCollection(q) => out.push(q.name.clone()),
2460 QueryExpr::Truncate(q) => out.push(q.name.clone()),
2461 QueryExpr::AlterTable(q) => out.push(q.name.clone()),
2462 QueryExpr::CreateIndex(q) => out.push(q.table.clone()),
2463 QueryExpr::DropIndex(q) => out.push(q.table.clone()),
2464 QueryExpr::CreateTimeSeries(q) => out.push(q.name.clone()),
2465 QueryExpr::CreateMetric(q) => out.push(q.path.clone()),
2466 QueryExpr::AlterMetric(q) => out.push(q.path.clone()),
2467 QueryExpr::CreateSlo(q) => out.push(q.path.clone()),
2468 QueryExpr::DropTimeSeries(q) => out.push(q.name.clone()),
2469 QueryExpr::CreateQueue(q) => out.push(q.name.clone()),
2470 QueryExpr::AlterQueue(q) => out.push(q.name.clone()),
2471 QueryExpr::DropQueue(q) => out.push(q.name.clone()),
2472 QueryExpr::QueueCommand(QueueCommand::Move {
2473 source,
2474 destination,
2475 ..
2476 }) => {
2477 out.push(source.clone());
2478 out.push(destination.clone());
2479 }
2480 QueryExpr::CreatePolicy(q) => out.push(q.table.clone()),
2481 QueryExpr::CreateView(q) => out.push(q.name.clone()),
2482 QueryExpr::DropView(q) => out.push(q.name.clone()),
2483 QueryExpr::RefreshMaterializedView(q) => out.push(q.name.clone()),
2484
2485 _ => {}
2491 }
2492}
2493
2494impl RedDBRuntime {
2495 pub fn in_memory() -> RedDBResult<Self> {
2496 Self::with_options(RedDBOptions::in_memory())
2497 }
2498
2499 pub fn flush(&self) -> RedDBResult<()> {
2500 self.inner
2501 .db
2502 .flush()
2503 .map_err(|err| RedDBError::Internal(err.to_string()))
2504 }
2505
2506 pub fn lock_manager(&self) -> std::sync::Arc<crate::storage::transaction::lock::LockManager> {
2510 self.inner.lock_manager.clone()
2511 }
2512
2513 pub fn config_registry(&self) -> std::sync::Arc<crate::auth::registry::ConfigRegistry> {
2515 self.inner.config_registry.clone()
2516 }
2517
2518 pub fn query_audit(&self) -> std::sync::Arc<crate::runtime::query_audit::QueryAuditStream> {
2519 self.inner.query_audit.clone()
2520 }
2521
2522 pub fn control_events_require_persistence(&self) -> bool {
2523 self.inner.control_event_config.require_persistence()
2524 }
2525
2526 pub fn control_event_config(&self) -> crate::runtime::control_events::ControlEventConfig {
2527 self.inner.control_event_config
2528 }
2529
2530 pub fn control_event_ledger(
2531 &self,
2532 ) -> Arc<dyn crate::runtime::control_events::ControlEventLedger> {
2533 self.inner.control_event_ledger.read().clone()
2534 }
2535
2536 #[doc(hidden)]
2537 pub fn replace_control_event_ledger_for_tests(
2538 &self,
2539 ledger: Arc<dyn crate::runtime::control_events::ControlEventLedger>,
2540 ) {
2541 *self.inner.control_event_ledger.write() = ledger;
2542 }
2543
2544 #[inline(never)]
2545 pub fn with_options(options: RedDBOptions) -> RedDBResult<Self> {
2546 Self::with_pool(options, ConnectionPoolConfig::default())
2547 }
2548
2549 pub fn with_pool(
2550 options: RedDBOptions,
2551 pool_config: ConnectionPoolConfig,
2552 ) -> RedDBResult<Self> {
2553 let boot_open_start_ms = std::time::SystemTime::now()
2561 .duration_since(std::time::UNIX_EPOCH)
2562 .map(|d| d.as_millis() as u64)
2563 .unwrap_or(0);
2564 let embedded_single_file = options.storage_profile.deploy_profile
2565 == crate::storage::DeployProfile::Embedded
2566 && options.storage_profile.packaging == crate::storage::StoragePackaging::SingleFile;
2567 let db = Arc::new(
2568 RedDB::open_with_options(&options)
2569 .map_err(|err| RedDBError::Internal(err.to_string()))?,
2570 );
2571 let result_blob_cache_config = if embedded_single_file {
2572 crate::storage::cache::BlobCacheConfig::default()
2573 } else {
2574 crate::storage::cache::BlobCacheConfig::default().with_l2_path(
2575 reddb_file::layout::result_cache_l2_path(
2576 &options.resolved_path(reddb_file::default_database_path()),
2577 ),
2578 )
2579 };
2580 let result_blob_cache =
2581 crate::storage::cache::BlobCache::open_with_l2(result_blob_cache_config).map_err(
2582 |err| RedDBError::Internal(format!("open result Blob Cache L2 failed: {err:?}")),
2583 )?;
2584 let storage_ready_ms = std::time::SystemTime::now()
2585 .duration_since(std::time::UNIX_EPOCH)
2586 .map(|d| d.as_millis() as u64)
2587 .unwrap_or(0);
2588
2589 let runtime = Self {
2590 inner: Arc::new(RuntimeInner {
2591 db: db.clone(),
2592 layout: PhysicalLayout::from_options(&options),
2593 embedded_single_file,
2594 indices: IndexCatalog::register_default_vector_graph(
2595 options.has_capability(crate::api::Capability::Table),
2596 options.has_capability(crate::api::Capability::Graph),
2597 ),
2598 pool_config,
2599 pool: Mutex::new(PoolState::default()),
2600 started_at_unix_ms: SystemTime::now()
2601 .duration_since(UNIX_EPOCH)
2602 .unwrap_or_default()
2603 .as_millis(),
2604 probabilistic: super::probabilistic_store::ProbabilisticStore::new(),
2605 index_store: super::index_store::IndexStore::new(),
2606 cdc: crate::replication::cdc::CdcBuffer::new(100_000),
2607 backup_scheduler: crate::replication::scheduler::BackupScheduler::new(3600),
2608 query_cache: parking_lot::RwLock::new(
2609 crate::storage::query::planner::cache::PlanCache::new(1000),
2610 ),
2611 result_cache: parking_lot::RwLock::new((
2612 HashMap::new(),
2613 std::collections::VecDeque::new(),
2614 )),
2615 result_blob_cache,
2616 result_blob_entries: parking_lot::RwLock::new((
2617 HashMap::new(),
2618 std::collections::VecDeque::new(),
2619 )),
2620 ask_answer_cache_entries: parking_lot::RwLock::new((
2621 HashSet::new(),
2622 std::collections::VecDeque::new(),
2623 )),
2624 result_cache_shadow_divergences: std::sync::atomic::AtomicU64::new(0),
2625 result_cache_hits: std::sync::atomic::AtomicU64::new(0),
2626 result_cache_misses: std::sync::atomic::AtomicU64::new(0),
2627 result_cache_evictions: std::sync::atomic::AtomicU64::new(0),
2628 ask_daily_spend: parking_lot::RwLock::new(HashMap::new()),
2629 queue_message_locks: parking_lot::RwLock::new(HashMap::new()),
2630 rmw_locks: RmwLockTable::new(),
2631 planner_dirty_tables: parking_lot::RwLock::new(HashSet::new()),
2632 ec_registry: Arc::new(crate::ec::config::EcRegistry::new()),
2633 config_registry: Arc::new(crate::auth::registry::ConfigRegistry::new()),
2634 ec_worker: crate::ec::worker::EcWorker::new(),
2635 auth_store: parking_lot::RwLock::new(None),
2636 oauth_validator: parking_lot::RwLock::new(None),
2637 browser_token_authority: parking_lot::RwLock::new(None),
2638 views: parking_lot::RwLock::new(HashMap::new()),
2639 materialized_views: parking_lot::RwLock::new(
2640 crate::storage::cache::result::MaterializedViewCache::new(),
2641 ),
2642 retention_sweeper: parking_lot::RwLock::new(
2643 crate::runtime::retention_sweeper::RetentionSweeperState::new(),
2644 ),
2645 snapshot_manager: Arc::new(
2646 crate::storage::transaction::snapshot::SnapshotManager::new(),
2647 ),
2648 tx_contexts: parking_lot::RwLock::new(HashMap::new()),
2649 tx_local_tenants: parking_lot::RwLock::new(HashMap::new()),
2650 env_config_overrides: crate::runtime::config_overlay::collect_env_overrides(),
2651 lock_manager: Arc::new({
2652 let env = crate::runtime::config_overlay::collect_env_overrides();
2657 let timeout_ms = env
2658 .get("concurrency.locking.deadlock_timeout_ms")
2659 .and_then(|raw| raw.parse::<u64>().ok())
2660 .unwrap_or_else(|| {
2661 match crate::runtime::config_matrix::default_for(
2662 "concurrency.locking.deadlock_timeout_ms",
2663 ) {
2664 Some(crate::serde_json::Value::Number(n)) => n as u64,
2665 _ => 5000,
2666 }
2667 });
2668 let cfg = crate::storage::transaction::lock::LockConfig {
2669 default_timeout: std::time::Duration::from_millis(timeout_ms),
2670 ..Default::default()
2671 };
2672 crate::storage::transaction::lock::LockManager::new(cfg)
2673 }),
2674 rls_policies: parking_lot::RwLock::new(HashMap::new()),
2675 rls_enabled_tables: parking_lot::RwLock::new(HashSet::new()),
2676 foreign_tables: Arc::new(crate::storage::fdw::ForeignTableRegistry::with_builtins()),
2677 pending_tombstones: parking_lot::RwLock::new(HashMap::new()),
2678 pending_versioned_updates: parking_lot::RwLock::new(HashMap::new()),
2679 pending_kv_watch_events: parking_lot::RwLock::new(HashMap::new()),
2680 pending_store_wal_actions: parking_lot::RwLock::new(HashMap::new()),
2681 pending_claim_locks: parking_lot::RwLock::new(HashMap::new()),
2682 queue_wait_registry: std::sync::Arc::new(
2683 crate::runtime::queue_wait_registry::QueueWaitRegistry::new(),
2684 ),
2685 pending_queue_wakes: parking_lot::RwLock::new(HashMap::new()),
2686 tenant_tables: parking_lot::RwLock::new(HashMap::new()),
2687 ddl_epoch: std::sync::atomic::AtomicU64::new(0),
2688 write_gate: Arc::new(crate::runtime::write_gate::WriteGate::from_options(
2689 &options,
2690 )),
2691 lifecycle: crate::runtime::lifecycle::Lifecycle::new(),
2692 resource_limits: crate::runtime::resource_limits::ResourceLimits::from_env(),
2693 audit_log: {
2694 let data_path = if embedded_single_file {
2710 std::env::temp_dir()
2711 .join("reddb-embedded-runtime")
2712 .join(format!("audit-{}", std::process::id()))
2713 } else {
2714 options
2715 .data_path
2716 .clone()
2717 .unwrap_or_else(|| std::env::temp_dir().join("reddb"))
2718 };
2719 let (audit_dest, _) = crate::api::tier_wiring::current_log_destinations();
2720 if !matches!(audit_dest, crate::storage::layout::LogDestination::File(_))
2721 && (embedded_single_file
2722 || options
2723 .metadata
2724 .contains_key(crate::api::EPHEMERAL_RUNTIME_METADATA_KEY))
2725 {
2726 let audit_path = reddb_file::layout::sibling_path(
2736 &data_path,
2737 &reddb_file::layout::sidecar_file_name(&data_path, "audit.log"),
2738 );
2739 Arc::new(crate::runtime::audit_log::AuditLogger::with_path(
2740 audit_path,
2741 ))
2742 } else {
2743 Arc::new(crate::runtime::audit_log::AuditLogger::for_destination(
2744 &audit_dest,
2745 &data_path,
2746 ))
2747 }
2748 },
2749 control_event_ledger: parking_lot::RwLock::new(Arc::new(
2750 crate::runtime::control_events::RuntimeLedger::new(db.store()),
2751 )),
2752 control_event_config: options.control_events,
2753 query_audit: Arc::new(crate::runtime::query_audit::QueryAuditStream::new(
2754 db.store(),
2755 options.query_audit.clone(),
2756 )),
2757 lease_lifecycle: std::sync::OnceLock::new(),
2758 replica_apply_metrics: std::sync::Arc::new(
2759 crate::replication::logical::ReplicaApplyMetrics::default(),
2760 ),
2761 replica_link_metrics: std::sync::Arc::new(
2762 crate::replication::reconnect::ReplicaLinkMetrics::default(),
2763 ),
2764 quota_bucket: crate::runtime::quota_bucket::QuotaBucket::from_env(),
2765 schema_vocabulary: parking_lot::RwLock::new(
2766 crate::runtime::schema_vocabulary::SchemaVocabulary::new(),
2767 ),
2768 slow_query_logger: {
2769 let fallback_dir = if embedded_single_file {
2786 std::env::temp_dir()
2787 .join("reddb-embedded-runtime")
2788 .join(format!("slow-{}", std::process::id()))
2789 } else {
2790 options
2791 .data_path
2792 .as_ref()
2793 .and_then(|p| p.parent().map(std::path::PathBuf::from))
2794 .unwrap_or_else(|| std::env::temp_dir().join("reddb"))
2795 };
2796 let threshold_ms = std::env::var("RED_SLOW_QUERY_THRESHOLD_MS")
2797 .ok()
2798 .and_then(|s| s.parse::<u64>().ok())
2799 .unwrap_or(1000);
2800 let sample_pct = std::env::var("RED_SLOW_QUERY_SAMPLE_PCT")
2801 .ok()
2802 .and_then(|s| s.parse::<u8>().ok())
2803 .unwrap_or(100);
2804 let (_, slow_dest) = crate::api::tier_wiring::current_log_destinations();
2805 crate::telemetry::slow_query_logger::SlowQueryLogger::for_destination(
2806 &slow_dest,
2807 &fallback_dir,
2808 threshold_ms,
2809 sample_pct,
2810 )
2811 },
2812 slow_query_store: crate::telemetry::slow_query_store::SlowQueryStore::new(
2813 crate::telemetry::slow_query_store::DEFAULT_CAP,
2814 ),
2815 kv_stats: crate::runtime::KvStatsCounters::default(),
2816 metrics_ingest_stats: crate::runtime::MetricsIngestCounters::default(),
2817 metrics_tenant_activity_stats:
2818 crate::runtime::MetricsTenantActivityCounters::default(),
2819 claim_telemetry: Arc::new(
2820 crate::runtime::claim_telemetry::ClaimTelemetryCounters::default(),
2821 ),
2822 queue_telemetry: Arc::new(
2823 crate::runtime::queue_telemetry::QueueTelemetryCounters::default(),
2824 ),
2825 query_latency_telemetry: Arc::new(
2826 crate::runtime::query_latency_telemetry::QueryLatencyTelemetry::default(),
2827 ),
2828 occupancy_sampler: Arc::new(
2829 crate::runtime::occupancy_sampler::OccupancySampler::new(),
2830 ),
2831 node_load_telemetry: Arc::new(
2832 crate::runtime::node_load_telemetry::NodeLoadTelemetry::default(),
2833 ),
2834 queue_presence: Arc::new(
2835 crate::storage::queue::presence::ConsumerPresenceRegistry::new(),
2836 ),
2837 vector_introspection: Arc::new(
2838 crate::storage::vector::introspection::VectorIntrospectionRegistry::new(),
2839 ),
2840 kv_tag_index: crate::runtime::KvTagIndex::default(),
2841 chain_tip_cache: parking_lot::Mutex::new(HashMap::new()),
2842 chain_integrity_broken: parking_lot::Mutex::new(HashMap::new()),
2843 integrity_tombstones: parking_lot::Mutex::new(Vec::new()),
2844 integrity_tombstones_state: std::sync::atomic::AtomicU8::new(0),
2845 }),
2846 };
2847
2848 crate::telemetry::operator_event::install_global_audit_sink(Arc::clone(
2854 &runtime.inner.audit_log,
2855 ));
2856
2857 runtime
2860 .inner
2861 .slow_query_logger
2862 .attach_store(Arc::clone(&runtime.inner.slow_query_store));
2863
2864 runtime
2872 .inner
2873 .lifecycle
2874 .set_restore_started_at_ms(boot_open_start_ms);
2875 runtime
2876 .inner
2877 .lifecycle
2878 .set_restore_ready_at_ms(storage_ready_ms);
2879 runtime
2880 .inner
2881 .lifecycle
2882 .set_wal_replay_started_at_ms(boot_open_start_ms);
2883 runtime
2884 .inner
2885 .lifecycle
2886 .set_wal_replay_ready_at_ms(storage_ready_ms);
2887
2888 let restored_cdc_lsn = runtime
2889 .inner
2890 .db
2891 .replication
2892 .as_ref()
2893 .map(|repl| {
2894 repl.logical_wal_spool
2895 .as_ref()
2896 .map(|spool| spool.current_lsn())
2897 .unwrap_or(0)
2898 })
2899 .unwrap_or(0)
2900 .max(runtime.config_u64("red.config.timeline.last_archived_lsn", 0));
2901 runtime.inner.cdc.set_current_lsn(restored_cdc_lsn);
2902 runtime.rehydrate_snapshot_xid_floor();
2903 runtime
2904 .bootstrap_system_keyed_collections()
2905 .map_err(|err| RedDBError::Internal(format!("bootstrap system collections: {err}")))?;
2906 runtime.rehydrate_declared_column_schemas();
2907 runtime.rehydrate_runtime_index_registry()?;
2908 runtime
2909 .load_probabilistic_state()
2910 .map_err(|err| RedDBError::Internal(format!("load probabilistic state: {err}")))?;
2911
2912 runtime.rehydrate_tenant_tables();
2916 runtime.rehydrate_materialized_view_descriptors();
2921 if let Some(repl) = &runtime.inner.db.replication {
2922 repl.wal_buffer.set_current_lsn(restored_cdc_lsn);
2923 }
2924
2925 {
2927 let sys = SystemInfo::collect();
2928 runtime.inner.db.store().set_config_tree(
2929 "red.system",
2930 &crate::serde_json::json!({
2931 "pid": sys.pid,
2932 "cpu_cores": sys.cpu_cores,
2933 "total_memory_bytes": sys.total_memory_bytes,
2934 "available_memory_bytes": sys.available_memory_bytes,
2935 "os": sys.os,
2936 "arch": sys.arch,
2937 "hostname": sys.hostname,
2938 "started_at": SystemTime::now()
2939 .duration_since(UNIX_EPOCH)
2940 .unwrap_or_default()
2941 .as_millis() as u64
2942 }),
2943 );
2944
2945 let store = runtime.inner.db.store();
2947 if store
2948 .get_collection("red_config")
2949 .map(|m| m.query_all(|_| true).len())
2950 .unwrap_or(0)
2951 <= 10
2952 {
2953 store.set_config_tree("red.ai", &crate::json!({
2954 "default": crate::json!({
2955 "provider": "openai",
2956 "model": crate::ai::DEFAULT_OPENAI_PROMPT_MODEL
2957 }),
2958 "max_embedding_inputs": 256,
2959 "max_prompt_batch": 256,
2960 "timeout": crate::json!({ "connect_secs": 10, "read_secs": 90, "write_secs": 30 })
2961 }));
2962 store.set_config_tree(
2963 "red.server",
2964 &crate::json!({
2965 "max_scan_limit": 1000,
2966 "max_body_size": 1048576,
2967 "read_timeout_ms": 5000,
2968 "write_timeout_ms": 5000
2969 }),
2970 );
2971 store.set_config_tree(
2972 "red.storage",
2973 &crate::json!({
2974 "page_size": 4096,
2975 "page_cache_capacity": 100000,
2976 "auto_checkpoint_pages": 1000,
2977 "snapshot_retention": 16,
2978 "verify_checksums": true,
2979 "segment": crate::json!({
2980 "max_entities": 100000,
2981 "max_bytes": 268435456_u64,
2982 "compression_level": 6
2983 }),
2984 "hnsw": crate::json!({ "m": 16, "ef_construction": 100, "ef_search": 50 }),
2985 "ivf": crate::json!({ "n_lists": 100, "n_probes": 10 }),
2986 "bm25": crate::json!({ "k1": 1.2, "b": 0.75 })
2987 }),
2988 );
2989 store.set_config_tree(
2990 "red.search",
2991 &crate::json!({
2992 "rag": crate::json!({
2993 "max_chunks_per_source": 10,
2994 "max_total_chunks": 25,
2995 "similarity_threshold": 0.8,
2996 "graph_depth": 2,
2997 "min_relevance": 0.3
2998 }),
2999 "fusion": crate::json!({
3000 "vector_weight": 0.5,
3001 "graph_weight": 0.3,
3002 "table_weight": 0.2,
3003 "dedup_threshold": 0.85
3004 })
3005 }),
3006 );
3007 store.set_config_tree(
3008 "red.auth",
3009 &crate::json!({
3010 "enabled": false,
3011 "session_ttl_secs": 3600,
3012 "require_auth": false
3013 }),
3014 );
3015 store.set_config_tree(
3016 "red.query",
3017 &crate::json!({
3018 "connection_pool": crate::json!({ "max_connections": 64, "max_idle": 16 }),
3019 "max_recursion_depth": 1000
3020 }),
3021 );
3022 store.set_config_tree(
3023 "red.indexes",
3024 &crate::json!({
3025 "auto_select": true,
3026 "bloom_filter": crate::json!({
3027 "enabled": true,
3028 "false_positive_rate": 0.01,
3029 "prune_on_scan": true
3030 }),
3031 "hash": crate::json!({ "enabled": true }),
3032 "bitmap": crate::json!({ "enabled": true, "max_cardinality": 1000 }),
3033 "spatial": crate::json!({ "enabled": true })
3034 }),
3035 );
3036 store.set_config_tree(
3037 "red.memtable",
3038 &crate::json!({
3039 "enabled": true,
3040 "max_bytes": 67108864_u64,
3041 "flush_threshold": 0.75
3042 }),
3043 );
3044 store.set_config_tree(
3045 "red.probabilistic",
3046 &crate::json!({
3047 "hll_registers": 16384,
3048 "sketch_default_width": 1000,
3049 "sketch_default_depth": 5,
3050 "filter_default_capacity": 100000
3051 }),
3052 );
3053 store.set_config_tree(
3054 "red.timeseries",
3055 &crate::json!({
3056 "default_chunk_size": 1024,
3057 "compression": crate::json!({
3058 "timestamps": "delta_of_delta",
3059 "values": "gorilla_xor"
3060 }),
3061 "default_retention_days": 0
3062 }),
3063 );
3064 store.set_config_tree(
3065 "red.queue",
3066 &crate::json!({
3067 "default_max_size": 0,
3068 "default_max_attempts": 3,
3069 "visibility_timeout_ms": 30000,
3070 "consumer_idle_timeout_ms": 60000
3071 }),
3072 );
3073 store.set_config_tree(
3074 "red.backup",
3075 &crate::json!({
3076 "enabled": false,
3077 "interval_secs": 3600,
3078 "retention_count": 24,
3079 "upload": false,
3080 "backend": "local"
3081 }),
3082 );
3083 store.set_config_tree(
3084 "red.wal",
3085 &crate::json!({
3086 "archive": crate::json!({
3087 "enabled": false,
3088 "retention_hours": 168,
3089 "prefix": reddb_file::backup_wal_prefix("")
3090 })
3091 }),
3092 );
3093 store.set_config_tree(
3094 "red.cdc",
3095 &crate::json!({
3096 "enabled": true,
3097 "buffer_size": 100000
3098 }),
3099 );
3100 store.set_config_tree(
3101 "red.config.secret",
3102 &crate::json!({
3103 "auto_encrypt": true,
3104 "auto_decrypt": true
3105 }),
3106 );
3107 }
3108
3109 crate::runtime::config_matrix::heal_critical_keys(store.as_ref());
3116 seed_storage_deploy_config(store.as_ref(), options.storage_profile);
3117
3118 let lehman_yao = runtime.config_bool("storage.btree.lehman_yao", true);
3125 crate::storage::engine::btree::lehman_yao::set_enabled(lehman_yao);
3126 if lehman_yao {
3127 tracing::info!(
3128 "storage.btree.lehman_yao=true — lock-free concurrent descent enabled"
3129 );
3130 }
3131
3132 let overlay_path = crate::runtime::config_overlay::config_file_path();
3137 let _ =
3138 crate::runtime::config_overlay::apply_config_file(store.as_ref(), &overlay_path);
3139 }
3140
3141 {
3145 let store = runtime.inner.db.store();
3146 for name in crate::application::vcs_collections::ALL {
3147 let _ = store.get_or_create_collection(*name);
3148 }
3149 store.set_config_tree(
3152 crate::application::vcs_collections::CONFIG_NAMESPACE,
3153 &crate::json!({
3154 "default_branch": "main",
3155 "author": crate::json!({
3156 "name": "reddb",
3157 "email": "reddb@localhost"
3158 }),
3159 "protected_branches": crate::json!(["main"]),
3160 "closure": crate::json!({
3161 "enabled": true,
3162 "lazy": true
3163 }),
3164 "merge": crate::json!({
3165 "default_strategy": "auto",
3166 "fast_forward": true
3167 })
3168 }),
3169 );
3170 }
3171
3172 {
3175 let store = runtime.inner.db.store();
3176 for name in crate::application::migration_collections::ALL {
3177 let _ = store.get_or_create_collection(*name);
3178 }
3179 }
3180
3181 let _ = crate::application::topology_collections::ensure(&runtime);
3185
3186 runtime
3194 .inner
3195 .db
3196 .store()
3197 .register_entity_id(crate::storage::EntityId::new(
3198 crate::storage::FIRST_USER_ENTITY_ID - 1,
3199 ));
3200
3201 {
3216 let weak = Arc::downgrade(&runtime.inner);
3217 std::thread::Builder::new()
3218 .name("reddb-maintenance".into())
3219 .spawn(move || {
3220 let tick = std::time::Duration::from_millis(200);
3221 let work_interval = std::time::Duration::from_secs(60);
3222 let mut last_work = std::time::Instant::now();
3223 loop {
3224 std::thread::sleep(tick);
3225 let Some(inner) = weak.upgrade() else {
3226 break;
3229 };
3230 if last_work.elapsed() >= work_interval {
3231 let _stats = inner.db.store().context_index().stats();
3232 last_work = std::time::Instant::now();
3233 }
3234 }
3235 })
3236 .ok();
3237 }
3238
3239 {
3241 let store = runtime.inner.db.store();
3242 let mut backup_enabled = false;
3243 let mut backup_interval = 3600u64;
3244
3245 if let Some(manager) = store.get_collection("red_config") {
3246 manager.for_each_entity(|entity| {
3247 if let Some(row) = entity.data.as_row() {
3248 let key = row.get_field("key").and_then(|v| match v {
3249 crate::storage::schema::Value::Text(s) => Some(s.as_ref()),
3250 _ => None,
3251 });
3252 let val = row.get_field("value");
3253 if key == Some("red.config.backup.enabled") {
3254 backup_enabled = match val {
3255 Some(crate::storage::schema::Value::Boolean(true)) => true,
3256 Some(crate::storage::schema::Value::Text(s)) => &**s == "true",
3257 _ => false,
3258 };
3259 } else if key == Some("red.config.backup.interval_secs") {
3260 if let Some(crate::storage::schema::Value::Integer(n)) = val {
3261 backup_interval = *n as u64;
3262 }
3263 }
3264 }
3265 true
3266 });
3267 }
3268
3269 if backup_enabled {
3270 runtime.inner.backup_scheduler.set_interval(backup_interval);
3271 let rt = runtime.clone();
3272 runtime
3273 .inner
3274 .backup_scheduler
3275 .start(move || rt.trigger_backup().map_err(|e| format!("{}", e)));
3276 }
3277 }
3278
3279 {
3281 runtime
3282 .inner
3283 .ec_registry
3284 .load_from_config_store(runtime.inner.db.store().as_ref());
3285 if !runtime.inner.ec_registry.async_configs().is_empty() {
3286 runtime.inner.ec_worker.start(
3287 Arc::clone(&runtime.inner.ec_registry),
3288 Arc::clone(&runtime.inner.db.store()),
3289 );
3290 }
3291 }
3292
3293 if let crate::replication::ReplicationRole::Replica { primary_addr } =
3294 runtime.inner.db.options().replication.role.clone()
3295 {
3296 let rt = runtime.clone();
3297 std::thread::Builder::new()
3298 .name("reddb-replica".into())
3299 .spawn(move || rt.run_replica_loop(primary_addr))
3300 .ok();
3301 }
3302
3303 runtime.inner.lifecycle.mark_ready();
3308
3309 {
3318 let weak_inner = Arc::downgrade(&runtime.inner);
3319 std::thread::Builder::new()
3320 .name("reddb-mv-scheduler".into())
3321 .spawn(move || loop {
3322 std::thread::sleep(std::time::Duration::from_millis(50));
3323 let Some(inner) = weak_inner.upgrade() else {
3324 break;
3325 };
3326 let rt = RedDBRuntime { inner };
3327 rt.refresh_due_materialized_views();
3328 })
3329 .ok();
3330 }
3331
3332 if !runtime.write_gate().is_read_only() {
3342 let weak_inner = Arc::downgrade(&runtime.inner);
3343 std::thread::Builder::new()
3344 .name("reddb-retention-sweeper".into())
3345 .spawn(move || loop {
3346 std::thread::sleep(std::time::Duration::from_millis(500));
3347 let Some(inner) = weak_inner.upgrade() else {
3348 break;
3349 };
3350 let rt = RedDBRuntime { inner };
3351 rt.sweep_retention_tick(
3352 crate::runtime::retention_sweeper::DEFAULT_SWEEPER_BATCH,
3353 );
3354 })
3355 .ok();
3356 }
3357
3358 Ok(runtime)
3359 }
3360
3361 fn rehydrate_snapshot_xid_floor(&self) {
3362 let store = self.inner.db.store();
3363 for collection in store.list_collections() {
3364 let Some(manager) = store.get_collection(&collection) else {
3365 continue;
3366 };
3367 for entity in manager.query_all(|_| true) {
3368 self.inner
3369 .snapshot_manager
3370 .observe_committed_xid(entity.xmin);
3371 self.inner
3372 .snapshot_manager
3373 .observe_committed_xid(entity.xmax);
3374 }
3375 }
3376 }
3377
3378 pub(crate) fn ensure_materialized_view_backing(&self, name: &str) -> RedDBResult<()> {
3391 let store = self.inner.db.store();
3392 let mut changed = false;
3393 if store.get_collection(name).is_none() {
3394 store.get_or_create_collection(name);
3395 changed = true;
3396 }
3397 if self.inner.db.collection_contract(name).is_none() {
3398 self.inner
3399 .db
3400 .save_collection_contract(system_keyed_collection_contract(
3401 name,
3402 crate::catalog::CollectionModel::Table,
3403 ))
3404 .map_err(|err| RedDBError::Internal(err.to_string()))?;
3405 changed = true;
3406 }
3407 if changed {
3408 self.inner
3409 .db
3410 .persist_metadata()
3411 .map_err(|err| RedDBError::Internal(err.to_string()))?;
3412 }
3413 Ok(())
3414 }
3415
3416 pub(crate) fn drop_materialized_view_backing(&self, name: &str) -> RedDBResult<()> {
3421 let store = self.inner.db.store();
3422 if store.get_collection(name).is_none() {
3423 return Ok(());
3424 }
3425 store
3426 .drop_collection(name)
3427 .map_err(|err| RedDBError::Internal(err.to_string()))?;
3428 if self.inner.db.collection_contract(name).is_some() {
3431 self.inner
3432 .db
3433 .remove_collection_contract(name)
3434 .map_err(|err| RedDBError::Internal(err.to_string()))?;
3435 }
3436 self.invalidate_result_cache();
3437 self.inner
3438 .db
3439 .persist_metadata()
3440 .map_err(|err| RedDBError::Internal(err.to_string()))?;
3441 Ok(())
3442 }
3443
3444 fn bootstrap_system_keyed_collections(&self) -> RedDBResult<()> {
3445 let mut changed = false;
3446 for (name, model) in [
3447 ("red.config", crate::catalog::CollectionModel::Config),
3448 ("red.vault", crate::catalog::CollectionModel::Vault),
3449 (
3453 crate::runtime::continuous_materialized_view::CATALOG_COLLECTION,
3454 crate::catalog::CollectionModel::Config,
3455 ),
3456 ] {
3457 if self.inner.db.store().get_collection(name).is_none() {
3458 self.inner.db.store().get_or_create_collection(name);
3459 changed = true;
3460 }
3461 if self.inner.db.collection_contract(name).is_none() {
3462 self.inner
3463 .db
3464 .save_collection_contract(system_keyed_collection_contract(name, model))
3465 .map_err(|err| RedDBError::Internal(err.to_string()))?;
3466 changed = true;
3467 }
3468 }
3469 if changed {
3470 self.inner
3471 .db
3472 .persist_metadata()
3473 .map_err(|err| RedDBError::Internal(err.to_string()))?;
3474 }
3475 Ok(())
3476 }
3477
3478 pub fn db(&self) -> Arc<RedDB> {
3479 Arc::clone(&self.inner.db)
3480 }
3481
3482 pub fn index_store_ref(&self) -> &super::index_store::IndexStore {
3487 &self.inner.index_store
3488 }
3489
3490 pub(crate) fn schema_vocabulary_apply(
3495 &self,
3496 event: crate::runtime::schema_vocabulary::DdlEvent,
3497 ) {
3498 self.inner.schema_vocabulary.write().on_ddl(event);
3499 }
3500
3501 pub fn schema_vocabulary_lookup(
3506 &self,
3507 token: &str,
3508 ) -> Vec<crate::runtime::schema_vocabulary::VocabHit> {
3509 self.inner.schema_vocabulary.read().lookup(token).to_vec()
3510 }
3511
3512 pub fn set_auth_store(&self, store: Arc<crate::auth::store::AuthStore>) {
3516 *self.inner.auth_store.write() = Some(store);
3517 }
3518
3519 pub fn auth_store(&self) -> Option<Arc<crate::auth::store::AuthStore>> {
3522 self.inner.auth_store.read().clone()
3523 }
3524
3525 pub fn vault_kv_get(&self, key: &str) -> Option<String> {
3527 self.inner
3528 .auth_store
3529 .read()
3530 .as_ref()
3531 .and_then(|store| store.vault_kv_get(key))
3532 }
3533
3534 pub fn vault_kv_try_set(&self, key: String, value: String) -> RedDBResult<()> {
3537 let store = self.inner.auth_store.read().clone().ok_or_else(|| {
3538 RedDBError::Query("secret storage requires an enabled, unsealed vault".to_string())
3539 })?;
3540 store
3541 .vault_kv_try_set(key, value)
3542 .map_err(|err| RedDBError::Query(err.to_string()))
3543 }
3544
3545 pub fn set_oauth_validator(&self, validator: Option<Arc<crate::auth::oauth::OAuthValidator>>) {
3549 *self.inner.oauth_validator.write() = validator;
3550 }
3551
3552 pub fn oauth_validator(&self) -> Option<Arc<crate::auth::oauth::OAuthValidator>> {
3556 self.inner.oauth_validator.read().clone()
3557 }
3558
3559 pub fn set_browser_token_authority(
3565 &self,
3566 authority: Option<Arc<crate::auth::browser_token::BrowserTokenAuthority>>,
3567 ) {
3568 *self.inner.browser_token_authority.write() = authority;
3569 }
3570
3571 pub fn browser_token_authority(
3575 &self,
3576 ) -> Option<Arc<crate::auth::browser_token::BrowserTokenAuthority>> {
3577 self.inner.browser_token_authority.read().clone()
3578 }
3579
3580 pub(crate) fn secret_aes_key(&self) -> Option<[u8; 32]> {
3584 let guard = self.inner.auth_store.read();
3585 guard.as_ref().and_then(|s| s.vault_secret_key())
3586 }
3587
3588 pub(crate) fn config_bool(&self, key: &str, default: bool) -> bool {
3594 if let Some(raw) = self.inner.env_config_overrides.get(key) {
3595 if let Some(crate::storage::schema::Value::Boolean(b)) =
3596 crate::runtime::config_overlay::coerce_env_value(key, raw)
3597 {
3598 return b;
3599 }
3600 }
3601 let store = self.inner.db.store();
3602 let Some(manager) = store.get_collection("red_config") else {
3603 return default;
3604 };
3605 let mut result = default;
3606 let mut latest_id: u64 = 0;
3607 manager.for_each_entity(|entity| {
3608 if let Some(row) = entity.data.as_row() {
3609 let entry_key = row.get_field("key").and_then(|v| match v {
3610 crate::storage::schema::Value::Text(s) => Some(s.as_ref()),
3611 _ => None,
3612 });
3613 if entry_key == Some(key) {
3614 let id = entity.id.raw();
3615 if id >= latest_id {
3616 latest_id = id;
3617 result = match row.get_field("value") {
3618 Some(crate::storage::schema::Value::Boolean(b)) => *b,
3619 Some(crate::storage::schema::Value::Text(s)) => {
3620 matches!(s.as_ref(), "true" | "TRUE" | "True" | "1")
3621 }
3622 Some(crate::storage::schema::Value::Integer(n)) => *n != 0,
3623 _ => default,
3624 };
3625 }
3626 }
3627 }
3628 true
3629 });
3630 result
3631 }
3632
3633 pub(crate) fn binary_document_body_enabled(&self) -> bool {
3638 self.config_bool("storage.binary_document_body", true)
3639 }
3640
3641 pub(crate) fn config_u64(&self, key: &str, default: u64) -> u64 {
3642 if let Some(raw) = self.inner.env_config_overrides.get(key) {
3643 if let Some(crate::storage::schema::Value::UnsignedInteger(n)) =
3644 crate::runtime::config_overlay::coerce_env_value(key, raw)
3645 {
3646 return n;
3647 }
3648 }
3649 let store = self.inner.db.store();
3650 let Some(manager) = store.get_collection("red_config") else {
3651 return default;
3652 };
3653 let mut result = default;
3654 let mut latest_id: u64 = 0;
3655 manager.for_each_entity(|entity| {
3656 if let Some(row) = entity.data.as_row() {
3657 let entry_key = row.get_field("key").and_then(|v| match v {
3658 crate::storage::schema::Value::Text(s) => Some(s.as_ref()),
3659 _ => None,
3660 });
3661 if entry_key == Some(key) {
3662 let id = entity.id.raw();
3663 if id >= latest_id {
3664 latest_id = id;
3665 result = match row.get_field("value") {
3666 Some(crate::storage::schema::Value::Integer(n)) => *n as u64,
3667 Some(crate::storage::schema::Value::UnsignedInteger(n)) => *n,
3668 Some(crate::storage::schema::Value::Text(s)) => {
3669 s.parse::<u64>().unwrap_or(default)
3670 }
3671 _ => default,
3672 };
3673 }
3674 }
3675 }
3676 true
3677 });
3678 result
3679 }
3680
3681 pub(crate) fn config_f64(&self, key: &str, default: f64) -> f64 {
3682 if let Some(raw) = self.inner.env_config_overrides.get(key) {
3683 if let Ok(n) = raw.parse::<f64>() {
3684 return n;
3685 }
3686 }
3687 let store = self.inner.db.store();
3688 let Some(manager) = store.get_collection("red_config") else {
3689 return default;
3690 };
3691 let mut result = default;
3692 let mut latest_id: u64 = 0;
3693 manager.for_each_entity(|entity| {
3694 if let Some(row) = entity.data.as_row() {
3695 let entry_key = row.get_field("key").and_then(|v| match v {
3696 crate::storage::schema::Value::Text(s) => Some(s.as_ref()),
3697 _ => None,
3698 });
3699 if entry_key == Some(key) {
3700 let id = entity.id.raw();
3701 if id >= latest_id {
3702 latest_id = id;
3703 result = match row.get_field("value") {
3704 Some(crate::storage::schema::Value::Float(n)) => *n,
3705 Some(crate::storage::schema::Value::Integer(n)) => *n as f64,
3706 Some(crate::storage::schema::Value::UnsignedInteger(n)) => *n as f64,
3707 Some(crate::storage::schema::Value::Text(s)) => {
3708 s.parse::<f64>().unwrap_or(default)
3709 }
3710 _ => default,
3711 };
3712 }
3713 }
3714 }
3715 true
3716 });
3717 result
3718 }
3719
3720 pub(crate) fn config_string(&self, key: &str, default: &str) -> String {
3721 if let Some(raw) = self.inner.env_config_overrides.get(key) {
3722 return raw.clone();
3723 }
3724 let store = self.inner.db.store();
3725 let Some(manager) = store.get_collection("red_config") else {
3726 return default.to_string();
3727 };
3728 let mut result = default.to_string();
3729 let mut latest_id: u64 = 0;
3730 manager.for_each_entity(|entity| {
3731 if let Some(row) = entity.data.as_row() {
3732 let entry_key = row.get_field("key").and_then(|v| match v {
3733 crate::storage::schema::Value::Text(s) => Some(s.as_ref()),
3734 _ => None,
3735 });
3736 if entry_key == Some(key) {
3737 let id = entity.id.raw();
3738 if id >= latest_id {
3739 latest_id = id;
3740 if let Some(crate::storage::schema::Value::Text(value)) =
3741 row.get_field("value")
3742 {
3743 result = value.to_string();
3744 }
3745 }
3746 }
3747 }
3748 true
3749 });
3750 result
3751 }
3752
3753 pub(crate) fn secret_auto_encrypt(&self) -> bool {
3756 self.config_bool("red.config.secret.auto_encrypt", true)
3757 }
3758
3759 pub(crate) fn secret_auto_decrypt(&self) -> bool {
3764 self.config_bool("red.config.secret.auto_decrypt", true)
3765 }
3766
3767 pub(crate) fn apply_secret_decryption(&self, result: &mut RuntimeQueryResult) {
3774 if !self.secret_auto_decrypt() {
3775 return;
3776 }
3777 let Some(key) = self.secret_aes_key() else {
3778 return;
3779 };
3780 for record in result.result.records.iter_mut() {
3781 for value in record.values_mut() {
3782 if let Value::Secret(ref bytes) = value {
3783 if let Some(plain) =
3784 super::impl_dml::decrypt_secret_payload(&key, bytes.as_slice())
3785 {
3786 if let Ok(text) = String::from_utf8(plain) {
3787 *value = Value::text(text);
3788 }
3789 }
3790 }
3791 }
3792 }
3793 }
3794
3795 pub(crate) fn mutation_engine(&self) -> crate::runtime::mutation::MutationEngine<'_> {
3803 crate::runtime::mutation::MutationEngine::new(self)
3804 }
3805
3806 pub fn check_write(&self, kind: crate::runtime::write_gate::WriteKind) -> RedDBResult<()> {
3817 self.inner.write_gate.check(kind)
3818 }
3819
3820 pub fn write_gate(&self) -> &crate::runtime::write_gate::WriteGate {
3824 &self.inner.write_gate
3825 }
3826
3827 pub fn lifecycle(&self) -> &crate::runtime::lifecycle::Lifecycle {
3831 &self.inner.lifecycle
3832 }
3833
3834 pub fn resource_limits(&self) -> &crate::runtime::resource_limits::ResourceLimits {
3836 &self.inner.resource_limits
3837 }
3838
3839 pub fn audit_log(&self) -> &crate::runtime::audit_log::AuditLogger {
3841 &self.inner.audit_log
3842 }
3843
3844 pub fn audit_log_arc(&self) -> Arc<crate::runtime::audit_log::AuditLogger> {
3848 Arc::clone(&self.inner.audit_log)
3849 }
3850
3851 pub(crate) fn emit_control_event(
3852 &self,
3853 kind: crate::runtime::control_events::EventKind,
3854 outcome: crate::runtime::control_events::Outcome,
3855 action: &'static str,
3856 resource: Option<String>,
3857 reason: Option<String>,
3858 extra_fields: Vec<(String, crate::runtime::control_events::Sensitivity)>,
3859 ) -> RedDBResult<()> {
3860 use crate::runtime::control_events::{
3861 ActorRef, ControlEvent, ControlEventCtx, ControlEventLedger, Sensitivity,
3862 };
3863
3864 let tenant = current_tenant();
3865 let principal = current_auth_identity();
3866 let actor_user = principal
3867 .as_ref()
3868 .map(|(principal, _)| UserId::from_parts(tenant.as_deref(), principal));
3869 let actor = actor_user
3870 .as_ref()
3871 .map(ActorRef::User)
3872 .unwrap_or(ActorRef::Anonymous);
3873 let ctx = ControlEventCtx {
3874 actor,
3875 scope: tenant
3876 .as_ref()
3877 .map(|scope| std::borrow::Cow::Borrowed(scope.as_str())),
3878 request_id: Some(std::borrow::Cow::Owned(format!(
3879 "conn-{}",
3880 current_connection_id()
3881 ))),
3882 trace_id: None,
3883 };
3884 let mut fields = std::collections::HashMap::new();
3885 fields.insert(
3886 "connection_id".to_string(),
3887 Sensitivity::raw(current_connection_id().to_string()),
3888 );
3889 if let Some((_, role)) = principal {
3890 fields.insert("actor_role".to_string(), Sensitivity::raw(role.as_str()));
3891 }
3892 for (key, value) in extra_fields {
3893 fields.insert(key, value);
3894 }
3895 let event = ControlEvent {
3896 kind,
3897 outcome,
3898 action: std::borrow::Cow::Borrowed(action),
3899 resource,
3900 reason,
3901 matched_policy_id: None,
3902 fields,
3903 };
3904 let ledger = self.inner.control_event_ledger.read();
3905 match ledger.emit(&ctx, event) {
3906 Ok(_) => Ok(()),
3907 Err(err) if self.inner.control_event_config.require_persistence() => {
3908 Err(RedDBError::Internal(err.to_string()))
3909 }
3910 Err(_) => Ok(()),
3911 }
3912 }
3913
3914 fn policy_mutation_control_ctx<'a>(
3915 &self,
3916 actor: &'a crate::auth::UserId,
3917 tenant: Option<&'a str>,
3918 ) -> crate::runtime::control_events::ControlEventCtx<'a> {
3919 crate::runtime::control_events::ControlEventCtx {
3920 actor: crate::runtime::control_events::ActorRef::User(actor),
3921 scope: tenant.map(std::borrow::Cow::Borrowed),
3922 request_id: Some(std::borrow::Cow::Owned(format!(
3923 "conn-{}",
3924 current_connection_id()
3925 ))),
3926 trace_id: None,
3927 }
3928 }
3929
3930 fn emit_query_audit(
3931 &self,
3932 query: &str,
3933 plan: &QueryAuditPlan,
3934 duration_ms: u64,
3935 result: &RuntimeQueryResult,
3936 ) {
3937 if !self.inner.query_audit.has_rules() {
3938 return;
3939 }
3940 let actor = current_auth_identity().map(|(principal, _)| principal);
3941 let tenant = current_tenant();
3942 let row_count = if result.statement_type == "select" {
3943 result.result.records.len() as u64
3944 } else {
3945 result.affected_rows
3946 };
3947 self.inner
3948 .query_audit
3949 .emit(crate::runtime::query_audit::QueryAuditEvent {
3950 actor,
3951 tenant,
3952 statement_kind: plan.statement_kind,
3953 touched_collections: plan.collections.clone(),
3954 duration_ms,
3955 row_count,
3956 request_id: Some(crate::crypto::uuid::Uuid::new_v7().to_string()),
3957 query_hash: Some(blake3::hash(query.as_bytes()).to_hex().to_string()),
3958 });
3959 }
3960
3961 pub(crate) fn queue_telemetry(
3963 &self,
3964 ) -> &crate::runtime::queue_telemetry::QueueTelemetryCounters {
3965 &self.inner.queue_telemetry
3966 }
3967
3968 pub fn queue_telemetry_snapshot(
3971 &self,
3972 ) -> crate::runtime::queue_telemetry::QueueTelemetrySnapshot {
3973 crate::runtime::queue_telemetry::QueueTelemetrySnapshot {
3974 delivered: self.inner.queue_telemetry.delivered_snapshot(),
3975 acked: self.inner.queue_telemetry.acked_snapshot(),
3976 nacked: self.inner.queue_telemetry.nacked_snapshot(),
3977 wait_started: self.inner.queue_telemetry.wait_started_snapshot(),
3978 wait_woken: self.inner.queue_telemetry.wait_woken_snapshot(),
3979 wait_timed_out: self.inner.queue_telemetry.wait_timed_out_snapshot(),
3980 wait_cancelled: self.inner.queue_telemetry.wait_cancelled_snapshot(),
3981 wait_duration: self.inner.queue_telemetry.wait_duration_snapshot(),
3982 }
3983 }
3984
3985 pub fn claim_telemetry_snapshot(&self) -> crate::runtime::ClaimTelemetrySnapshot {
3987 self.inner.claim_telemetry.snapshot()
3988 }
3989
3990 pub fn query_latency_snapshot(
3993 &self,
3994 ) -> Vec<crate::runtime::query_latency_telemetry::QueryLatencyHistogram> {
3995 self.inner.query_latency_telemetry.snapshot()
3996 }
3997
3998 pub fn query_latency_rollup(
4001 &self,
4002 ) -> crate::runtime::query_latency_telemetry::QueryLatencyHistogram {
4003 self.inner.query_latency_telemetry.rollup()
4004 }
4005
4006 pub fn sample_occupancy(&self) -> crate::runtime::occupancy_sampler::OccupancySample {
4011 self.inner.occupancy_sampler.sample()
4012 }
4013
4014 pub fn node_load_snapshot(&self) -> crate::runtime::node_load_telemetry::NodeLoadSnapshot {
4018 self.inner.node_load_telemetry.snapshot()
4019 }
4020
4021 pub(crate) fn queue_presence(
4026 &self,
4027 ) -> &std::sync::Arc<crate::storage::queue::presence::ConsumerPresenceRegistry> {
4028 &self.inner.queue_presence
4029 }
4030
4031 pub fn queue_consumer_presence_snapshot(
4036 &self,
4037 ttl_ms: u64,
4038 ) -> Vec<crate::storage::queue::presence::ConsumerPresence> {
4039 let now_ns = std::time::SystemTime::now()
4040 .duration_since(std::time::UNIX_EPOCH)
4041 .map(|d| d.as_nanos() as u64)
4042 .unwrap_or(0);
4043 self.inner.queue_presence.snapshot(now_ns, ttl_ms)
4044 }
4045
4046 pub fn queue_active_consumer_counts(
4050 &self,
4051 ttl_ms: u64,
4052 ) -> std::collections::HashMap<(String, String), u32> {
4053 let now_ns = std::time::SystemTime::now()
4054 .duration_since(std::time::UNIX_EPOCH)
4055 .map(|d| d.as_nanos() as u64)
4056 .unwrap_or(0);
4057 self.inner
4058 .queue_presence
4059 .count_active_by_group(now_ns, ttl_ms)
4060 }
4061
4062 pub(crate) fn vector_introspection_registry(
4068 &self,
4069 ) -> &std::sync::Arc<crate::storage::vector::introspection::VectorIntrospectionRegistry> {
4070 &self.inner.vector_introspection
4071 }
4072
4073 pub fn vector_introspection_snapshot(
4078 &self,
4079 ) -> Vec<crate::storage::vector::introspection::VectorIntrospection> {
4080 self.inner.vector_introspection.snapshot()
4081 }
4082
4083 pub fn vector_introspection_get(
4087 &self,
4088 collection: &str,
4089 ) -> Option<crate::storage::vector::introspection::VectorIntrospection> {
4090 self.inner.vector_introspection.get(collection)
4091 }
4092
4093 pub fn slow_query_store(&self) -> &Arc<crate::telemetry::slow_query_store::SlowQueryStore> {
4099 &self.inner.slow_query_store
4100 }
4101
4102 pub fn queue_pending_counts(&self) -> Vec<((String, String), u64)> {
4107 let store = self.inner.db.store();
4108 crate::runtime::impl_queue::pending_counts_by_group(store.as_ref())
4109 .into_iter()
4110 .collect()
4111 }
4112
4113 pub fn write_gate_arc(&self) -> Arc<crate::runtime::write_gate::WriteGate> {
4118 Arc::clone(&self.inner.write_gate)
4119 }
4120
4121 pub fn lease_lifecycle(&self) -> Option<&Arc<crate::runtime::lease_lifecycle::LeaseLifecycle>> {
4124 self.inner.lease_lifecycle.get()
4125 }
4126
4127 pub fn set_lease_lifecycle(
4130 &self,
4131 lifecycle: Arc<crate::runtime::lease_lifecycle::LeaseLifecycle>,
4132 ) -> Result<(), Arc<crate::runtime::lease_lifecycle::LeaseLifecycle>> {
4133 self.inner.lease_lifecycle.set(lifecycle)
4134 }
4135
4136 pub fn check_batch_size(&self, requested: usize) -> RedDBResult<()> {
4141 if self.inner.resource_limits.batch_size_exceeded(requested) {
4142 let max = self.inner.resource_limits.max_batch_size.unwrap_or(0);
4143 return Err(RedDBError::QuotaExceeded(format!(
4144 "max_batch_size:{requested}:{max}"
4145 )));
4146 }
4147 Ok(())
4148 }
4149
4150 pub fn check_db_size(&self) -> RedDBResult<()> {
4156 let Some(limit) = self.inner.resource_limits.max_db_size_bytes else {
4157 return Ok(());
4158 };
4159 if limit == 0 {
4160 return Ok(());
4161 }
4162 let Some(path) = self.inner.db.path() else {
4163 return Ok(());
4164 };
4165 let current = std::fs::metadata(path).map(|m| m.len()).unwrap_or(0);
4166 if current > limit {
4167 return Err(RedDBError::QuotaExceeded(format!(
4168 "max_db_size_bytes:{current}:{limit}"
4169 )));
4170 }
4171 Ok(())
4172 }
4173
4174 pub fn graceful_shutdown(
4192 &self,
4193 backup_on_shutdown: bool,
4194 ) -> RedDBResult<crate::runtime::lifecycle::ShutdownReport> {
4195 if !self.inner.lifecycle.begin_shutdown() {
4196 return Ok(self.inner.lifecycle.shutdown_report().unwrap_or_default());
4200 }
4201
4202 let started_ms = std::time::SystemTime::now()
4203 .duration_since(std::time::UNIX_EPOCH)
4204 .map(|d| d.as_millis() as u64)
4205 .unwrap_or(0);
4206 let mut report = crate::runtime::lifecycle::ShutdownReport {
4207 started_at_ms: started_ms,
4208 ..Default::default()
4209 };
4210
4211 let flush_res = self.inner.db.flush_local_only();
4217 report.flushed_wal = flush_res.is_ok();
4218 report.final_checkpoint = flush_res.is_ok();
4219 if let Err(err) = &flush_res {
4220 tracing::error!(
4221 target: "reddb::lifecycle",
4222 error = %err,
4223 "graceful_shutdown: local flush failed"
4224 );
4225 } else if let Err(lease_err) =
4226 self.assert_remote_write_allowed("shutdown/checkpoint_upload")
4227 {
4228 tracing::warn!(
4229 target: "reddb::serverless::lease",
4230 error = %lease_err,
4231 "graceful_shutdown: remote upload skipped — lease not held"
4232 );
4233 } else if let Err(err) = self.inner.db.upload_to_remote_backend() {
4234 tracing::error!(
4235 target: "reddb::lifecycle",
4236 error = %err,
4237 "graceful_shutdown: remote upload failed"
4238 );
4239 }
4240
4241 if backup_on_shutdown && self.inner.db.remote_backend.is_some() {
4246 match self.trigger_backup() {
4252 Ok(result) => {
4253 report.backup_uploaded = result.uploaded;
4254 }
4255 Err(err) => {
4256 tracing::warn!(
4257 target: "reddb::lifecycle",
4258 error = %err,
4259 "graceful_shutdown: final backup skipped"
4260 );
4261 }
4262 }
4263 }
4264
4265 let completed_ms = std::time::SystemTime::now()
4266 .duration_since(std::time::UNIX_EPOCH)
4267 .map(|d| d.as_millis() as u64)
4268 .unwrap_or(started_ms);
4269 report.completed_at_ms = completed_ms;
4270 report.duration_ms = completed_ms.saturating_sub(started_ms);
4271
4272 self.inner.lifecycle.finish_shutdown(report.clone());
4273 Ok(report)
4274 }
4275
4276 pub fn quota_bucket(&self) -> &crate::runtime::quota_bucket::QuotaBucket {
4279 &self.inner.quota_bucket
4280 }
4281
4282 pub fn encryption_at_rest_status(&self) -> (&'static str, Option<String>) {
4290 match crate::crypto::page_encryption::key_from_env() {
4291 Ok(Some(_)) => ("enabled", None),
4292 Ok(None) => ("disabled", None),
4293 Err(err) => ("error", Some(err)),
4294 }
4295 }
4296
4297 pub fn replica_apply_health(&self) -> Option<String> {
4303 let state = self.config_string("red.replication.state", "");
4304 if state.is_empty() {
4305 None
4306 } else {
4307 Some(state)
4308 }
4309 }
4310
4311 pub fn acquire(&self) -> RedDBResult<RuntimeConnection> {
4312 let mut pool = self
4313 .inner
4314 .pool
4315 .lock()
4316 .map_err(|e| RedDBError::Internal(format!("connection pool lock poisoned: {e}")))?;
4317 if pool.active >= self.inner.pool_config.max_connections {
4318 return Err(RedDBError::Internal(
4319 "connection pool exhausted".to_string(),
4320 ));
4321 }
4322
4323 let id = if let Some(id) = pool.idle.pop() {
4324 id
4325 } else {
4326 let id = pool.next_id;
4327 pool.next_id += 1;
4328 id
4329 };
4330 pool.active += 1;
4331 pool.total_checkouts += 1;
4332 drop(pool);
4333
4334 self.inner.node_load_telemetry.record_connect();
4337
4338 Ok(RuntimeConnection {
4339 id,
4340 inner: Arc::clone(&self.inner),
4341 })
4342 }
4343
4344 pub fn checkpoint(&self) -> RedDBResult<()> {
4345 self.inner.db.flush_local_only().map_err(|err| {
4350 let msg = err.to_string();
4355 crate::telemetry::operator_event::OperatorEvent::CheckpointFailed {
4356 lsn: 0,
4357 error: msg.clone(),
4358 }
4359 .emit_global();
4360 crate::telemetry::operator_event::OperatorEvent::WalFsyncFailed {
4361 path: "<flush_local_only>".to_string(),
4362 error: msg.clone(),
4363 }
4364 .emit_global();
4365 RedDBError::Engine(msg)
4366 })?;
4367 if let Err(err) = self.assert_remote_write_allowed("checkpoint") {
4368 tracing::warn!(
4369 target: "reddb::serverless::lease",
4370 error = %err,
4371 "checkpoint: skipping remote upload — lease not held"
4372 );
4373 return Ok(());
4374 }
4375 self.inner
4376 .db
4377 .upload_to_remote_backend()
4378 .map_err(|err| RedDBError::Engine(err.to_string()))
4379 }
4380
4381 pub(crate) fn assert_remote_write_allowed(&self, action: &str) -> RedDBResult<()> {
4388 if self.inner.db.remote_backend.is_none() {
4389 return Ok(());
4390 }
4391 match self.inner.write_gate.lease_state() {
4392 crate::runtime::write_gate::LeaseGateState::NotHeld => {
4393 self.inner.audit_log.record(
4394 action,
4395 "system",
4396 "remote_backend",
4397 "err: writer lease not held",
4398 crate::json::Value::Null,
4399 );
4400 Err(RedDBError::ReadOnly(format!(
4401 "writer lease not held — {action} blocked (serverless fence)"
4402 )))
4403 }
4404 _ => Ok(()),
4405 }
4406 }
4407
4408 pub fn run_maintenance(&self) -> RedDBResult<()> {
4409 self.inner
4410 .db
4411 .run_maintenance()
4412 .map_err(|err| RedDBError::Internal(err.to_string()))
4413 }
4414
4415 pub fn scan_collection(
4416 &self,
4417 collection: &str,
4418 cursor: Option<ScanCursor>,
4419 limit: usize,
4420 ) -> RedDBResult<ScanPage> {
4421 let store = self.inner.db.store();
4422 let manager = store
4423 .get_collection(collection)
4424 .ok_or_else(|| RedDBError::NotFound(collection.to_string()))?;
4425
4426 let mut entities = manager.query_all(|_| true);
4427 entities.sort_by_key(|entity| entity.id.raw());
4428
4429 let offset = cursor.map(|cursor| cursor.offset).unwrap_or(0);
4430 let total = entities.len();
4431 let end = total.min(offset.saturating_add(limit.max(1)));
4432 let items = if offset >= total {
4433 Vec::new()
4434 } else {
4435 entities[offset..end].to_vec()
4436 };
4437 let next = (end < total).then_some(ScanCursor { offset: end });
4438
4439 Ok(ScanPage {
4440 collection: collection.to_string(),
4441 items,
4442 next,
4443 total,
4444 })
4445 }
4446
4447 pub fn catalog(&self) -> CatalogModelSnapshot {
4448 self.inner.db.catalog_model_snapshot()
4449 }
4450
4451 pub fn catalog_consistency_report(&self) -> crate::catalog::CatalogConsistencyReport {
4452 self.inner.db.catalog_consistency_report()
4453 }
4454
4455 pub fn catalog_attention_summary(&self) -> CatalogAttentionSummary {
4456 crate::catalog::attention_summary(&self.catalog())
4457 }
4458
4459 pub fn collection_attention(&self) -> Vec<CollectionDescriptor> {
4460 crate::catalog::collection_attention(&self.catalog())
4461 }
4462
4463 pub fn index_attention(&self) -> Vec<CatalogIndexStatus> {
4464 crate::catalog::index_attention(&self.catalog())
4465 }
4466
4467 pub fn graph_projection_attention(&self) -> Vec<CatalogGraphProjectionStatus> {
4468 crate::catalog::graph_projection_attention(&self.catalog())
4469 }
4470
4471 pub fn analytics_job_attention(&self) -> Vec<CatalogAnalyticsJobStatus> {
4472 crate::catalog::analytics_job_attention(&self.catalog())
4473 }
4474
4475 pub fn stats(&self) -> RuntimeStats {
4476 let pool = runtime_pool_lock(self);
4477 RuntimeStats {
4478 active_connections: pool.active,
4479 idle_connections: pool.idle.len(),
4480 total_checkouts: pool.total_checkouts,
4481 paged_mode: self.inner.db.is_paged(),
4482 started_at_unix_ms: self.inner.started_at_unix_ms,
4483 store: self.inner.db.stats(),
4484 system: SystemInfo::collect(),
4485 result_blob_cache: self.inner.result_blob_cache.stats(),
4486 kv: self.inner.kv_stats.snapshot(),
4487 metrics_ingest: self.inner.metrics_ingest_stats.snapshot(),
4488 }
4489 }
4490
4491 pub(crate) fn record_metrics_ingest(
4492 &self,
4493 accepted_samples: u64,
4494 accepted_series: u64,
4495 rejected_samples: u64,
4496 rejected_series: u64,
4497 ) {
4498 self.inner.metrics_ingest_stats.record(
4499 accepted_samples,
4500 accepted_series,
4501 rejected_samples,
4502 rejected_series,
4503 );
4504 }
4505
4506 pub(crate) fn record_metrics_cardinality_budget_rejections(&self, rejected_series: u64) {
4507 self.inner
4508 .metrics_ingest_stats
4509 .record_cardinality_budget_rejections(rejected_series);
4510 }
4511
4512 pub(crate) fn record_metrics_tenant_activity(
4513 &self,
4514 tenant: &str,
4515 namespace: &str,
4516 operation: &str,
4517 ) {
4518 self.inner
4519 .metrics_tenant_activity_stats
4520 .record(tenant, namespace, operation);
4521 }
4522
4523 pub(crate) fn metrics_tenant_activity_snapshot(
4524 &self,
4525 ) -> Vec<crate::runtime::MetricsTenantActivityStats> {
4526 self.inner.metrics_tenant_activity_stats.snapshot()
4527 }
4528
4529 pub fn execute_query_with_scope(
4543 &self,
4544 query: &str,
4545 scope: crate::runtime::within_clause::ScopeOverride,
4546 ) -> RedDBResult<RuntimeQueryResult> {
4547 if scope.is_empty() {
4548 return self.execute_query(query);
4549 }
4550 let _scope_guard = ScopeOverrideGuard::install(scope);
4551 self.execute_query(query)
4552 }
4553
4554 pub fn execute_query(&self, query: &str) -> RedDBResult<RuntimeQueryResult> {
4563 let started = std::time::Instant::now();
4564 self.inner.node_load_telemetry.query_start();
4565 let result = self.execute_query_inner(query);
4566 self.finish_query_lifecycle(query, started, result)
4567 }
4568
4569 pub fn execute_query_with_params(
4577 &self,
4578 query: &str,
4579 params: &[Value],
4580 ) -> RedDBResult<RuntimeQueryResult> {
4581 if params.is_empty() {
4582 return self.execute_query(query);
4583 }
4584 let started = std::time::Instant::now();
4585 self.inner.node_load_telemetry.query_start();
4586 let result = self.execute_query_with_params_inner(query, params);
4587 self.finish_query_lifecycle(query, started, result)
4588 }
4589
4590 fn finish_query_lifecycle(
4591 &self,
4592 query: &str,
4593 started: std::time::Instant,
4594 mut result: RedDBResult<RuntimeQueryResult>,
4595 ) -> RedDBResult<RuntimeQueryResult> {
4596 if let Ok(ref mut query_result) = result {
4601 if query_result.statement_type == "select" {
4602 self.filter_integrity_tombstoned(&mut query_result.result);
4603 }
4604 }
4605 let elapsed_ms = started.elapsed().as_millis() as u64;
4606
4607 let scope = self.ai_scope();
4612 let kind = match result
4613 .as_ref()
4614 .map(|r| r.statement_type)
4615 .unwrap_or("select")
4616 {
4617 "select" => crate::telemetry::slow_query_logger::QueryKind::Select,
4618 "insert" => crate::telemetry::slow_query_logger::QueryKind::Insert,
4619 "update" => crate::telemetry::slow_query_logger::QueryKind::Update,
4620 "delete" => crate::telemetry::slow_query_logger::QueryKind::Delete,
4621 _ => crate::telemetry::slow_query_logger::QueryKind::Internal,
4622 };
4623 self.inner
4629 .slow_query_logger
4630 .record(kind, elapsed_ms, query.to_string(), &scope);
4631
4632 self.inner
4638 .query_latency_telemetry
4639 .observe(kind, started.elapsed().as_secs_f64());
4640
4641 self.inner.node_load_telemetry.query_finish();
4645
4646 if let Ok(ref mut query_result) = result {
4647 if matches!(query_result.statement_type, "insert" | "update" | "delete") {
4648 let bookmark = crate::replication::CausalBookmark::new(
4649 self.current_replication_term(),
4650 self.cdc_current_lsn(),
4651 );
4652 query_result.bookmark = Some(bookmark.encode());
4653 }
4654 }
4655
4656 result
4657 }
4658
4659 fn execute_query_with_params_inner(
4660 &self,
4661 query: &str,
4662 params: &[Value],
4663 ) -> RedDBResult<RuntimeQueryResult> {
4664 let parsed = parse_multi(query).map_err(|err| RedDBError::Query(err.to_string()))?;
4665 let bound = crate::storage::query::user_params::bind(&parsed, params).map_err(|err| {
4666 RedDBError::Validation {
4667 message: err.to_string(),
4668 validation: crate::json!({
4669 "code": "INVALID_PARAMS",
4670 "surface": "query.params",
4671 }),
4672 }
4673 })?;
4674 self.execute_bound_query_expr_in_frame(query, bound)
4675 }
4676
4677 fn execute_bound_query_expr_in_frame(
4678 &self,
4679 query: &str,
4680 expr: QueryExpr,
4681 ) -> RedDBResult<RuntimeQueryResult> {
4682 let rewritten_query = super::red_schema::rewrite_virtual_names(query);
4683 let execution_query = rewritten_query.as_deref().unwrap_or(query);
4684 let frame = super::statement_frame::StatementExecutionFrame::build(self, execution_query)?;
4685 let _frame_guards = frame.install(self);
4686 let _log_span = crate::telemetry::span::query_span(query).entered();
4687
4688 let expr = self.rewrite_view_refs(expr);
4689 let mode = detect_mode(execution_query);
4690 let control_event_specs = query_control_event_specs(&expr);
4691 let _lock_guard = match frame.prepare_dispatch(self, &expr) {
4692 Ok(guard) => guard,
4693 Err(err) => {
4694 let outcome = control_event_outcome_for_error(&err);
4695 for spec in &control_event_specs {
4696 self.emit_control_event(
4697 spec.kind,
4698 outcome,
4699 spec.action,
4700 spec.resource.clone(),
4701 Some(err.to_string()),
4702 spec.fields.clone(),
4703 )?;
4704 }
4705 return Err(err);
4706 }
4707 };
4708
4709 let mut result = self.dispatch_expr(expr, query, mode)?;
4710 if result.statement_type == "select" {
4711 self.apply_secret_decryption(&mut result);
4712 }
4713 Ok(result)
4714 }
4715
4716 pub fn causal_session(&self) -> crate::runtime::CausalSession {
4717 crate::runtime::CausalSession {
4718 runtime: self.clone(),
4719 bookmark: None,
4720 wait_timeout: std::time::Duration::from_secs(5),
4721 }
4722 }
4723
4724 pub fn wait_for_bookmark(
4725 &self,
4726 bookmark: &crate::replication::CausalBookmark,
4727 timeout: std::time::Duration,
4728 ) -> RedDBResult<()> {
4729 let deadline = std::time::Instant::now() + timeout;
4730 loop {
4731 let applied_lsn = self.local_contiguous_applied_lsn();
4732 if applied_lsn >= bookmark.commit_lsn() {
4733 return Ok(());
4734 }
4735 let now = std::time::Instant::now();
4736 if now >= deadline {
4737 return Err(RedDBError::InvalidOperation(format!(
4738 "timed out waiting for causal bookmark lsn {}; applied={}",
4739 bookmark.commit_lsn(),
4740 applied_lsn
4741 )));
4742 }
4743 let remaining = deadline.saturating_duration_since(now);
4744 std::thread::sleep(remaining.min(std::time::Duration::from_millis(5)));
4745 }
4746 }
4747
4748 fn local_contiguous_applied_lsn(&self) -> u64 {
4749 match self.inner.db.options().replication.role {
4750 crate::replication::ReplicationRole::Replica { .. } => {
4751 self.config_u64("red.replication.last_applied_lsn", 0)
4752 }
4753 _ => self.cdc_current_lsn(),
4754 }
4755 }
4756
4757 fn execute_vcs_command(
4758 &self,
4759 query: &str,
4760 mode: QueryMode,
4761 cmd: RuntimeVcsCommand,
4762 ) -> RedDBResult<RuntimeQueryResult> {
4763 use crate::application::vcs::{
4764 Author, CheckoutInput, CheckoutTarget, CreateCommitInput, MergeInput, MergeOpts,
4765 ResetInput, ResetMode,
4766 };
4767 use crate::application::VcsUseCases;
4768
4769 let conn_id = current_connection_id();
4770 let vcs = VcsUseCases::new(self);
4771 let default_author = || Author {
4772 name: "rql".to_string(),
4773 email: "rql@reddb.io".to_string(),
4774 };
4775
4776 match cmd {
4777 RuntimeVcsCommand::Checkpoint { message, author } => {
4778 let author = author
4779 .as_deref()
4780 .map(parse_vcs_author)
4781 .unwrap_or_else(default_author);
4782 let commit = vcs.commit(CreateCommitInput {
4783 connection_id: conn_id,
4784 message: message.clone(),
4785 author,
4786 committer: None,
4787 amend: false,
4788 allow_empty: true,
4789 })?;
4790 Ok(RuntimeQueryResult::ok_message(
4791 query.to_string(),
4792 &format!("checkpoint {}", commit.hash),
4793 "vcs_checkpoint",
4794 ))
4795 }
4796 RuntimeVcsCommand::Checkout { target } => {
4797 let result = if target.starts_with("refs/tags/") {
4798 vcs.checkout(CheckoutInput {
4799 connection_id: conn_id,
4800 target: CheckoutTarget::Tag(target.clone()),
4801 force: false,
4802 })
4803 } else if looks_like_commit_hash(&target) {
4804 vcs.checkout(CheckoutInput {
4805 connection_id: conn_id,
4806 target: CheckoutTarget::Commit(target.clone()),
4807 force: false,
4808 })
4809 } else {
4810 vcs.checkout(CheckoutInput {
4811 connection_id: conn_id,
4812 target: CheckoutTarget::Branch(target.clone()),
4813 force: false,
4814 })
4815 .or_else(|_| {
4816 vcs.checkout(CheckoutInput {
4817 connection_id: conn_id,
4818 target: CheckoutTarget::Commit(target.clone()),
4819 force: false,
4820 })
4821 })
4822 }?;
4823 Ok(RuntimeQueryResult::ok_message(
4824 query.to_string(),
4825 &format!("checked out {}", result.name),
4826 "vcs_checkout",
4827 ))
4828 }
4829 RuntimeVcsCommand::Reset {
4830 mode: reset_mode,
4831 target,
4832 } => {
4833 let mode = match reset_mode {
4834 RuntimeVcsResetMode::Hard => ResetMode::Hard,
4835 RuntimeVcsResetMode::Soft => ResetMode::Soft,
4836 RuntimeVcsResetMode::Mixed => ResetMode::Mixed,
4837 };
4838 vcs.reset(ResetInput {
4839 connection_id: conn_id,
4840 target: target.clone(),
4841 mode,
4842 })?;
4843 Ok(RuntimeQueryResult::ok_message(
4844 query.to_string(),
4845 "reset complete",
4846 "vcs_reset",
4847 ))
4848 }
4849 RuntimeVcsCommand::Merge { branch } => {
4850 let outcome = vcs.merge(MergeInput {
4851 connection_id: conn_id,
4852 from: branch.clone(),
4853 opts: MergeOpts::default(),
4854 author: default_author(),
4855 })?;
4856 Ok(RuntimeQueryResult::ok_message(
4857 query.to_string(),
4858 &format!("merge complete; conflicts={}", outcome.conflicts.len()),
4859 "vcs_merge",
4860 ))
4861 }
4862 RuntimeVcsCommand::CherryPick { commit } => {
4863 let outcome = vcs.cherry_pick(conn_id, &commit, default_author())?;
4864 Ok(RuntimeQueryResult::ok_message(
4865 query.to_string(),
4866 &format!(
4867 "cherry-pick complete; conflicts={}",
4868 outcome.conflicts.len()
4869 ),
4870 "vcs_cherry_pick",
4871 ))
4872 }
4873 RuntimeVcsCommand::Revert { commit } => {
4874 let reverted = vcs.revert(conn_id, &commit, default_author())?;
4875 Ok(RuntimeQueryResult::ok_message(
4876 query.to_string(),
4877 &format!("revert {}", reverted.hash),
4878 "vcs_revert",
4879 ))
4880 }
4881 RuntimeVcsCommand::ResolveConflict { key, resolution } => {
4882 let value = match resolution {
4883 RuntimeVcsConflictResolution::Ours => {
4884 crate::json::Value::String("ours".to_string())
4885 }
4886 RuntimeVcsConflictResolution::Theirs => {
4887 crate::json::Value::String("theirs".to_string())
4888 }
4889 };
4890 vcs.conflict_resolve(&key, value)?;
4891 Ok(RuntimeQueryResult::ok_message(
4892 query.to_string(),
4893 "conflict resolved",
4894 "vcs_resolve_conflict",
4895 ))
4896 }
4897 }
4898 }
4899
4900 #[inline(never)]
4901 fn execute_query_inner(&self, query: &str) -> RedDBResult<RuntimeQueryResult> {
4902 if !has_scope_override_active()
4913 && !query.trim_start().starts_with("WITHIN")
4914 && !query.trim_start().starts_with("within")
4915 && !self.inner.query_audit.has_rules()
4916 && !self
4917 .inner
4918 .tx_contexts
4919 .read()
4920 .contains_key(¤t_connection_id())
4921 {
4922 if let Some(result) = self.try_fast_entity_lookup(query) {
4923 return result;
4924 }
4925 }
4926
4927 match crate::runtime::within_clause::try_strip_within_prefix(query) {
4934 Ok(Some((scope, inner))) => {
4935 let _scope_guard = ScopeOverrideGuard::install(scope);
4936 return self.execute_query_inner(inner);
4941 }
4942 Ok(None) => {}
4943 Err(msg) => return Err(RedDBError::Query(msg)),
4944 }
4945
4946 if let Some(inner) = strip_explain_prefix(query) {
4953 return self.explain_as_rows(query, inner);
4954 }
4955
4956 if let Some(value) = parse_set_local_tenant(query)? {
4961 let conn_id = current_connection_id();
4962 if !self.inner.tx_contexts.read().contains_key(&conn_id) {
4963 return Err(RedDBError::Query(
4964 "SET LOCAL TENANT requires an active transaction".to_string(),
4965 ));
4966 }
4967 self.inner
4968 .tx_local_tenants
4969 .write()
4970 .insert(conn_id, value.clone());
4971 return Ok(RuntimeQueryResult::ok_message(
4972 query.to_string(),
4973 &match &value {
4974 Some(id) => format!("local tenant set: {id}"),
4975 None => "local tenant cleared".to_string(),
4976 },
4977 "set_local_tenant",
4978 ));
4979 }
4980
4981 if super::red_schema::is_system_schema_write(query) {
4982 return Err(RedDBError::Query(
4983 super::red_schema::READ_ONLY_ERROR.to_string(),
4984 ));
4985 }
4986
4987 if let Some(command) = parse_runtime_vcs_command(query) {
4988 return self.execute_vcs_command(query, detect_mode(query), command?);
4989 }
4990
4991 if let Some(create_source) = super::analytics_source_catalog::parse_create_statement(query)?
4992 {
4993 return self.execute_create_analytics_source(query, create_source);
4994 }
4995
4996 if let Some(path) = super::metric_descriptor_catalog::parse_read_metric_statement(query) {
5002 return Err(super::metric_descriptor_catalog::read_output_unsupported(
5003 &path,
5004 ));
5005 }
5006
5007 if let Some(parsed) = super::ranking_descriptor_catalog::parse_create_ranking(query) {
5012 return self.execute_create_ranking(query, parsed?);
5013 }
5014 if super::ranking_descriptor_catalog::parse_show_rankings(query) {
5015 return self.execute_show_rankings(query);
5016 }
5017
5018 let rewritten_query = super::red_schema::rewrite_virtual_names(query);
5019 let execution_query = rewritten_query.as_deref().unwrap_or(query);
5020
5021 let frame = super::statement_frame::StatementExecutionFrame::build(self, execution_query)?;
5022 let _frame_guards = frame.install(self);
5023
5024 let _log_span = crate::telemetry::span::query_span(query).entered();
5031
5032 if let Some(rewritten) = frame.prepare_cte(execution_query)? {
5034 return self.execute_query_expr(rewritten);
5035 }
5036
5037 if !self.inner.query_audit.has_rules() {
5039 if let Some(result) = self.try_fast_entity_lookup(execution_query) {
5040 return result;
5041 }
5042 }
5043
5044 if !self.inner.query_audit.has_rules() {
5046 if let Some(result) = frame.read_result_cache(self) {
5047 return Ok(result);
5048 }
5049 }
5050
5051 let prepared = frame.prepare_statement(self, execution_query)?;
5052 let mode = prepared.mode;
5053 let expr = prepared.expr;
5054
5055 let statement = query_expr_name(&expr);
5056 let result_cache_scopes = query_expr_result_cache_scopes(&expr);
5057 let control_event_specs = query_control_event_specs(&expr);
5058 let query_audit_plan = query_audit_plan(&expr);
5059
5060 let _lock_guard = match frame.prepare_dispatch(self, &expr) {
5061 Ok(guard) => guard,
5062 Err(err) => {
5063 let outcome = control_event_outcome_for_error(&err);
5064 for spec in &control_event_specs {
5065 self.emit_control_event(
5066 spec.kind,
5067 outcome,
5068 spec.action,
5069 spec.resource.clone(),
5070 Some(err.to_string()),
5071 spec.fields.clone(),
5072 )?;
5073 }
5074 return Err(err);
5075 }
5076 };
5077 let frame_iface: &dyn super::statement_frame::ReadFrame = &frame;
5078 let query_audit_started = std::time::Instant::now();
5079
5080 let query_result = match expr {
5081 QueryExpr::Graph(_) | QueryExpr::Path(_) => {
5082 let (graph, node_properties, edge_properties) =
5090 self.materialize_graph_with_rls()?;
5091 let result =
5092 crate::storage::query::unified::UnifiedExecutor::execute_on_with_graph_properties(
5093 &graph,
5094 &expr,
5095 node_properties,
5096 edge_properties,
5097 )
5098 .map_err(|err| RedDBError::Query(err.to_string()))?;
5099
5100 Ok(RuntimeQueryResult {
5101 query: query.to_string(),
5102 mode,
5103 statement,
5104 engine: "materialized-graph",
5105 result,
5106 affected_rows: 0,
5107 statement_type: "select",
5108 bookmark: None,
5109 })
5110 }
5111 QueryExpr::Table(table) => {
5112 let table = self.resolve_table_expr_subqueries(
5113 table,
5114 &frame as &dyn super::statement_frame::ReadFrame,
5115 )?;
5116 if let Some(TableSource::Function {
5120 name,
5121 args,
5122 named_args,
5123 }) = table.source.clone()
5124 {
5125 let tvf_result = RuntimeQueryResult {
5133 query: query.to_string(),
5134 mode,
5135 statement,
5136 engine: "runtime-graph-tvf",
5137 result: self.execute_table_function(&name, &args, &named_args)?,
5138 affected_rows: 0,
5139 statement_type: "select",
5140 bookmark: None,
5141 };
5142 frame.write_result_cache(self, &tvf_result, result_cache_scopes.clone());
5143 return Ok(tvf_result);
5144 }
5145 if let Some(TableSource::InlineGraphFunction {
5153 name,
5154 nodes,
5155 edges,
5156 named_args,
5157 }) = table.source.clone()
5158 {
5159 let inline_result = RuntimeQueryResult {
5160 query: query.to_string(),
5161 mode,
5162 statement,
5163 engine: "runtime-graph-tvf-inline",
5164 result: self.execute_inline_graph_function(
5165 &name,
5166 &nodes,
5167 &edges,
5168 &named_args,
5169 )?,
5170 affected_rows: 0,
5171 statement_type: "select",
5172 bookmark: None,
5173 };
5174 frame.write_result_cache(self, &inline_result, result_cache_scopes);
5175 return Ok(inline_result);
5176 }
5177 if super::red_schema::is_virtual_table(&table.table) {
5178 return Ok(RuntimeQueryResult {
5179 query: query.to_string(),
5180 mode,
5181 statement,
5182 engine: "runtime-red-schema",
5183 result: super::red_schema::red_query(
5184 self,
5185 &table.table,
5186 &table,
5187 &frame as &dyn super::statement_frame::ReadFrame,
5188 )?,
5189 affected_rows: 0,
5190 statement_type: "select",
5191 bookmark: None,
5192 });
5193 }
5194
5195 if let Some(view_result) = self.try_resolve_analytics_view(
5199 &table,
5200 &frame as &dyn super::statement_frame::ReadFrame,
5201 )? {
5202 return Ok(RuntimeQueryResult {
5203 query: query.to_string(),
5204 mode,
5205 statement,
5206 engine: "runtime-graph-analytics-view",
5207 result: view_result,
5208 affected_rows: 0,
5209 statement_type: "select",
5210 bookmark: None,
5211 });
5212 }
5213
5214 if let Some(result) = self.execute_probabilistic_select(&table)? {
5215 return Ok(RuntimeQueryResult {
5216 query: query.to_string(),
5217 mode,
5218 statement,
5219 engine: "runtime-probabilistic",
5220 result,
5221 affected_rows: 0,
5222 statement_type: "select",
5223 bookmark: None,
5224 });
5225 }
5226
5227 if self.inner.foreign_tables.is_foreign_table(&table.table) {
5235 let records = self
5236 .inner
5237 .foreign_tables
5238 .scan(&table.table)
5239 .map_err(|e| RedDBError::Internal(e.to_string()))?;
5240 let result = apply_foreign_table_filters(records, &table);
5241 return Ok(RuntimeQueryResult {
5242 query: query.to_string(),
5243 mode,
5244 statement,
5245 engine: "runtime-fdw",
5246 result,
5247 affected_rows: 0,
5248 statement_type: "select",
5249 bookmark: None,
5250 });
5251 }
5252
5253 let Some(table_with_rls) = self.authorize_relational_table_select(
5270 table,
5271 &frame as &dyn super::statement_frame::ReadFrame,
5272 )?
5273 else {
5274 let empty = crate::storage::query::unified::UnifiedResult::empty();
5275 return Ok(RuntimeQueryResult {
5276 query: query.to_string(),
5277 mode,
5278 statement,
5279 engine: "runtime-table-rls",
5280 result: empty,
5281 affected_rows: 0,
5282 statement_type: "select",
5283 bookmark: None,
5284 });
5285 };
5286 Ok(RuntimeQueryResult {
5287 query: query.to_string(),
5288 mode,
5289 statement,
5290 engine: "runtime-table",
5291 result: execute_runtime_table_query_in(
5298 &self.inner.db,
5299 &table_with_rls,
5300 Some(&self.inner.index_store),
5301 Some(frame.row_arena()),
5302 )?,
5303 affected_rows: 0,
5304 statement_type: "select",
5305 bookmark: None,
5306 })
5307 }
5308 QueryExpr::Join(join) => {
5309 let join_with_rls = match self.authorize_relational_join_select(
5318 join,
5319 &frame as &dyn super::statement_frame::ReadFrame,
5320 )? {
5321 Some(j) => j,
5322 None => {
5323 return Ok(RuntimeQueryResult {
5324 query: query.to_string(),
5325 mode,
5326 statement,
5327 engine: "runtime-join-rls",
5328 result: crate::storage::query::unified::UnifiedResult::empty(),
5329 affected_rows: 0,
5330 statement_type: "select",
5331 bookmark: None,
5332 });
5333 }
5334 };
5335 Ok(RuntimeQueryResult {
5336 query: query.to_string(),
5337 mode,
5338 statement,
5339 engine: "runtime-join",
5340 result: execute_runtime_join_query(&self.inner.db, &join_with_rls)?,
5341 affected_rows: 0,
5342 statement_type: "select",
5343 bookmark: None,
5344 })
5345 }
5346 QueryExpr::Vector(vector) => Ok(RuntimeQueryResult {
5347 query: query.to_string(),
5348 mode,
5349 statement,
5350 engine: "runtime-vector",
5351 result: execute_runtime_vector_query(&self.inner.db, &vector)?,
5352 affected_rows: 0,
5353 statement_type: "select",
5354 bookmark: None,
5355 }),
5356 QueryExpr::Hybrid(hybrid) => Ok(RuntimeQueryResult {
5357 query: query.to_string(),
5358 mode,
5359 statement,
5360 engine: "runtime-hybrid",
5361 result: execute_runtime_hybrid_query(&self.inner.db, &hybrid)?,
5362 affected_rows: 0,
5363 statement_type: "select",
5364 bookmark: None,
5365 }),
5366 QueryExpr::RankOf(ref rank) => self.execute_rank_of(query, rank),
5367 QueryExpr::ApproxRankOf(ref rank) => self.execute_approx_rank_of(query, rank),
5368 QueryExpr::RankRange(ref range) => self.execute_rank_range(query, range),
5369 QueryExpr::Insert(ref insert) if super::red_schema::is_virtual_table(&insert.table) => {
5371 Err(RedDBError::Query(
5372 super::red_schema::READ_ONLY_ERROR.to_string(),
5373 ))
5374 }
5375 QueryExpr::Update(ref update) if super::red_schema::is_virtual_table(&update.table) => {
5376 Err(RedDBError::Query(
5377 super::red_schema::READ_ONLY_ERROR.to_string(),
5378 ))
5379 }
5380 QueryExpr::Delete(ref delete) if super::red_schema::is_virtual_table(&delete.table) => {
5381 Err(RedDBError::Query(
5382 super::red_schema::READ_ONLY_ERROR.to_string(),
5383 ))
5384 }
5385 QueryExpr::Insert(ref insert) => self
5386 .with_deferred_store_wal_for_dml(self.insert_may_emit_events(insert), || {
5387 self.execute_insert(query, insert)
5388 }),
5389 QueryExpr::Update(ref update) => self
5390 .with_deferred_store_wal_for_dml(self.update_may_emit_events(update), || {
5391 self.execute_update(query, update)
5392 }),
5393 QueryExpr::Delete(ref delete) => self
5394 .with_deferred_store_wal_for_dml(self.delete_may_emit_events(delete), || {
5395 self.execute_delete(query, delete)
5396 }),
5397 QueryExpr::CreateTable(ref create) => self.execute_create_table(query, create),
5399 QueryExpr::CreateCollection(ref create) => {
5400 self.execute_create_collection(query, create)
5401 }
5402 QueryExpr::CreateVector(ref create) => self.execute_create_vector(query, create),
5403 QueryExpr::DropTable(ref drop_tbl) => self.execute_drop_table(query, drop_tbl),
5404 QueryExpr::DropGraph(ref drop_graph) => self.execute_drop_graph(query, drop_graph),
5405 QueryExpr::DropVector(ref drop_vector) => self.execute_drop_vector(query, drop_vector),
5406 QueryExpr::DropDocument(ref drop_document) => {
5407 self.execute_drop_document(query, drop_document)
5408 }
5409 QueryExpr::DropKv(ref drop_kv) => self.execute_drop_kv(query, drop_kv),
5410 QueryExpr::DropCollection(ref drop_collection) => {
5411 self.execute_drop_collection(query, drop_collection)
5412 }
5413 QueryExpr::Truncate(ref truncate) => self.execute_truncate(query, truncate),
5414 QueryExpr::AlterTable(ref alter) => self.execute_alter_table(query, alter),
5415 QueryExpr::CreateVcsRef(ref create) => self.execute_create_vcs_ref(query, create),
5416 QueryExpr::DropVcsRef(ref drop_ref) => self.execute_drop_vcs_ref(query, drop_ref),
5417 QueryExpr::ExplainAlter(ref explain) => self.execute_explain_alter(query, explain),
5418 QueryExpr::GraphCommand(ref cmd) => self.execute_graph_command(query, cmd),
5420 QueryExpr::SearchCommand(ref cmd) => self.execute_search_command(query, cmd),
5422 QueryExpr::Ask(ref ask) => self.execute_ask(query, ask),
5424 QueryExpr::CreateIndex(ref create_idx) => self.execute_create_index(query, create_idx),
5425 QueryExpr::DropIndex(ref drop_idx) => self.execute_drop_index(query, drop_idx),
5426 QueryExpr::ProbabilisticCommand(ref cmd) => {
5427 self.execute_probabilistic_command(query, cmd)
5428 }
5429 QueryExpr::CreateTimeSeries(ref ts) => self.execute_create_timeseries(query, ts),
5431 QueryExpr::CreateMetric(ref metric) => self.execute_create_metric(query, metric),
5432 QueryExpr::AlterMetric(ref alter) => self.execute_alter_metric(query, alter),
5433 QueryExpr::CreateSlo(ref slo) => self.execute_create_slo(query, slo),
5434 QueryExpr::DropTimeSeries(ref ts) => self.execute_drop_timeseries(query, ts),
5435 QueryExpr::CreateQueue(ref q) => self.execute_create_queue(query, q),
5437 QueryExpr::AlterQueue(ref q) => self.execute_alter_queue(query, q),
5438 QueryExpr::DropQueue(ref q) => self.execute_drop_queue(query, q),
5439 QueryExpr::QueueSelect(ref q) => self.execute_queue_select(query, q),
5440 QueryExpr::QueueCommand(ref cmd) => self
5441 .with_deferred_store_wal_if_transaction(|| self.execute_queue_command(query, cmd)),
5442 QueryExpr::EventsBackfill(ref backfill) => {
5443 self.execute_events_backfill(query, backfill)
5444 }
5445 QueryExpr::EventsBackfillStatus { ref collection } => Err(RedDBError::Query(format!(
5446 "EVENTS BACKFILL STATUS for '{collection}' is not implemented in this slice"
5447 ))),
5448 QueryExpr::KvCommand(ref cmd) => self.execute_kv_command(query, cmd),
5449 QueryExpr::ConfigCommand(ref cmd) => self.execute_config_command(query, cmd),
5450 QueryExpr::CreateTree(ref tree) => self.execute_create_tree(query, tree),
5451 QueryExpr::DropTree(ref tree) => self.execute_drop_tree(query, tree),
5452 QueryExpr::TreeCommand(ref cmd) => self.execute_tree_command(query, cmd),
5453 QueryExpr::SetConfig { ref key, ref value } => {
5455 if key.starts_with("red.secret.") {
5456 return Err(RedDBError::Query(
5457 "red.secret.* is reserved for vault secrets; use SET SECRET".to_string(),
5458 ));
5459 }
5460 if key.starts_with("red.secrets.") {
5461 return Err(RedDBError::Query(
5462 "red.secrets.* is reserved for vault secrets; use SET SECRET".to_string(),
5463 ));
5464 }
5465 match self.check_managed_config_write_for_set_config(key) {
5466 Err(err) => Err(err),
5467 Ok(()) => {
5468 let store = self.inner.db.store();
5469 let json_val = match value {
5470 Value::Text(s) => crate::serde_json::Value::String(s.to_string()),
5471 Value::Integer(n) => crate::serde_json::Value::Number(*n as f64),
5472 Value::Float(n) => crate::serde_json::Value::Number(*n),
5473 Value::Boolean(b) => crate::serde_json::Value::Bool(*b),
5474 _ => crate::serde_json::Value::String(value.to_string()),
5475 };
5476 store.set_config_tree(key, &json_val);
5477 update_current_config_value(key, value.clone());
5478 self.invalidate_result_cache();
5483 Ok(RuntimeQueryResult::ok_message(
5484 query.to_string(),
5485 &format!("config set: {key}"),
5486 "set",
5487 ))
5488 }
5489 }
5490 }
5491 QueryExpr::SetSecret { ref key, ref value } => {
5493 if key.starts_with("red.config.") {
5494 return Err(RedDBError::Query(
5495 "red.config.* is reserved for config; use SET CONFIG".to_string(),
5496 ));
5497 }
5498 let auth_store = self.inner.auth_store.read().clone().ok_or_else(|| {
5499 RedDBError::Query("SET SECRET requires an enabled, unsealed vault".to_string())
5500 })?;
5501 if matches!(value, Value::Null) {
5502 auth_store
5503 .vault_kv_try_delete(key)
5504 .map_err(|err| RedDBError::Query(err.to_string()))?;
5505 update_current_secret_value(key, None);
5506 self.invalidate_result_cache();
5507 return Ok(RuntimeQueryResult::ok_message(
5508 query.to_string(),
5509 &format!("secret deleted: {key}"),
5510 "delete_secret",
5511 ));
5512 }
5513 let value = secret_sql_value_to_string(value)?;
5514 auth_store
5515 .vault_kv_try_set(key.clone(), value.clone())
5516 .map_err(|err| RedDBError::Query(err.to_string()))?;
5517 update_current_secret_value(key, Some(value));
5518 self.invalidate_result_cache();
5519 Ok(RuntimeQueryResult::ok_message(
5520 query.to_string(),
5521 &format!("secret set: {key}"),
5522 "set_secret",
5523 ))
5524 }
5525 QueryExpr::DeleteSecret { ref key } => {
5527 let auth_store = self.inner.auth_store.read().clone().ok_or_else(|| {
5528 RedDBError::Query(
5529 "DELETE SECRET requires an enabled, unsealed vault".to_string(),
5530 )
5531 })?;
5532 let deleted = auth_store
5533 .vault_kv_try_delete(key)
5534 .map_err(|err| RedDBError::Query(err.to_string()))?;
5535 if deleted {
5536 update_current_secret_value(key, None);
5537 }
5538 self.invalidate_result_cache();
5539 Ok(RuntimeQueryResult::ok_message(
5540 query.to_string(),
5541 &format!("secret deleted: {key}"),
5542 if deleted {
5543 "delete_secret"
5544 } else {
5545 "delete_secret_not_found"
5546 },
5547 ))
5548 }
5549 QueryExpr::ShowSecrets { ref prefix } => {
5551 let auth_store = self.inner.auth_store.read().clone().ok_or_else(|| {
5552 RedDBError::Query("SHOW SECRET requires an enabled, unsealed vault".to_string())
5553 })?;
5554 if !auth_store.is_vault_backed() {
5555 return Err(RedDBError::Query(
5556 "SHOW SECRET requires an enabled, unsealed vault".to_string(),
5557 ));
5558 }
5559 let mut keys = auth_store.vault_kv_keys();
5560 keys.sort();
5561 let mut result = UnifiedResult::with_columns(vec![
5562 "key".into(),
5563 "value".into(),
5564 "status".into(),
5565 ]);
5566 for key in keys {
5567 if let Some(ref pfx) = prefix {
5568 if !key.starts_with(pfx) {
5569 continue;
5570 }
5571 }
5572 let mut record = UnifiedRecord::new();
5573 record.set("key", Value::text(key));
5574 record.set("value", Value::text("***"));
5575 record.set("status", Value::text("active"));
5576 result.push(record);
5577 }
5578 Ok(RuntimeQueryResult {
5579 query: query.to_string(),
5580 mode,
5581 statement: "show_secrets",
5582 engine: "runtime-secret",
5583 result,
5584 affected_rows: 0,
5585 statement_type: "select",
5586 bookmark: None,
5587 })
5588 }
5589 QueryExpr::ShowConfig {
5591 ref prefix,
5592 as_json,
5593 } => {
5594 let store = self.inner.db.store();
5595 let all_collections = store.list_collections();
5596 if !all_collections.contains(&"red_config".to_string()) {
5597 if as_json {
5598 return Ok(show_config_json_result(
5599 query,
5600 mode,
5601 prefix,
5602 crate::serde_json::Value::Object(crate::serde_json::Map::new()),
5603 ));
5604 }
5605 let result = UnifiedResult::with_columns(vec!["key".into(), "value".into()]);
5606 return Ok(RuntimeQueryResult {
5607 query: query.to_string(),
5608 mode,
5609 statement: "show_config",
5610 engine: "runtime-config",
5611 result,
5612 affected_rows: 0,
5613 statement_type: "select",
5614 bookmark: None,
5615 });
5616 }
5617 let manager = store
5618 .get_collection("red_config")
5619 .ok_or_else(|| RedDBError::NotFound("red_config".to_string()))?;
5620 let entities = manager.query_all(|_| true);
5621 let mut latest = std::collections::BTreeMap::<String, (u64, Value, Value)>::new();
5622 for entity in entities {
5623 if let EntityData::Row(ref row) = entity.data {
5624 if let Some(ref named) = row.named {
5625 let key_val = named.get("key").cloned().unwrap_or(Value::Null);
5626 let val = named.get("value").cloned().unwrap_or(Value::Null);
5627 let key_str = match &key_val {
5628 Value::Text(s) => s.as_ref(),
5629 _ => continue,
5630 };
5631 if let Some(ref pfx) = prefix {
5632 if !key_str.starts_with(pfx.as_str()) {
5633 continue;
5634 }
5635 }
5636 let entity_id = entity.id.raw();
5637 match latest.get(key_str) {
5638 Some((prev_id, _, _)) if *prev_id > entity_id => {}
5639 _ => {
5640 latest.insert(key_str.to_string(), (entity_id, key_val, val));
5641 }
5642 }
5643 }
5644 }
5645 }
5646 if as_json {
5647 let mut tree = crate::serde_json::Value::Object(crate::serde_json::Map::new());
5648 for (key, (_, _, val)) in latest {
5649 let relative = match prefix {
5650 Some(pfx) if key == *pfx => "",
5651 Some(pfx) => key
5652 .strip_prefix(pfx.as_str())
5653 .and_then(|tail| tail.strip_prefix('.'))
5654 .unwrap_or(key.as_str()),
5655 None => key.as_str(),
5656 };
5657 insert_config_json_path(
5658 &mut tree,
5659 relative,
5660 crate::presentation::entity_json::storage_value_to_json(&val),
5661 );
5662 }
5663 return Ok(show_config_json_result(query, mode, prefix, tree));
5664 }
5665 let mut result = UnifiedResult::with_columns(vec!["key".into(), "value".into()]);
5666 for (_, key_val, val) in latest.into_values() {
5667 let mut record = UnifiedRecord::new();
5668 record.set("key", key_val);
5669 record.set("value", val);
5670 result.push(record);
5671 }
5672 Ok(RuntimeQueryResult {
5673 query: query.to_string(),
5674 mode,
5675 statement: "show_config",
5676 engine: "runtime-config",
5677 result,
5678 affected_rows: 0,
5679 statement_type: "select",
5680 bookmark: None,
5681 })
5682 }
5683 QueryExpr::SetTenant(ref value) => {
5689 match value {
5690 Some(id) => set_current_tenant(id.clone()),
5691 None => clear_current_tenant(),
5692 }
5693 Ok(RuntimeQueryResult::ok_message(
5694 query.to_string(),
5695 &match value {
5696 Some(id) => format!("tenant set: {id}"),
5697 None => "tenant cleared".to_string(),
5698 },
5699 "set_tenant",
5700 ))
5701 }
5702 QueryExpr::ShowTenant => {
5703 let mut result = UnifiedResult::with_columns(vec!["tenant".into()]);
5704 let mut record = UnifiedRecord::new();
5705 record.set(
5706 "tenant",
5707 current_tenant().map(Value::text).unwrap_or(Value::Null),
5708 );
5709 result.push(record);
5710 Ok(RuntimeQueryResult {
5711 query: query.to_string(),
5712 mode,
5713 statement: "show_tenant",
5714 engine: "runtime-tenant",
5715 result,
5716 affected_rows: 0,
5717 statement_type: "select",
5718 bookmark: None,
5719 })
5720 }
5721 QueryExpr::TransactionControl(ref ctl) => {
5733 use crate::storage::query::ast::TxnControl;
5734 use crate::storage::transaction::snapshot::{TxnContext, Xid};
5735 use crate::storage::transaction::IsolationLevel;
5736
5737 let conn_id = current_connection_id();
5742
5743 let (kind, msg) = match ctl {
5744 TxnControl::Begin => {
5745 let mgr = Arc::clone(&self.inner.snapshot_manager);
5746 let xid = mgr.begin();
5747 let snapshot = mgr.snapshot(xid);
5748 let ctx = TxnContext {
5749 xid,
5750 isolation: IsolationLevel::SnapshotIsolation,
5751 snapshot,
5752 savepoints: Vec::new(),
5753 released_sub_xids: Vec::new(),
5754 };
5755 self.inner.tx_contexts.write().insert(conn_id, ctx);
5756 ("begin", format!("BEGIN — xid={xid} (snapshot isolation)"))
5757 }
5758 TxnControl::Commit => {
5759 self.inner.tx_local_tenants.write().remove(&conn_id);
5761 let ctx = self.inner.tx_contexts.write().remove(&conn_id);
5762 match ctx {
5763 Some(ctx) => {
5764 let mut own_xids = std::collections::HashSet::new();
5765 own_xids.insert(ctx.xid);
5766 for (_, sub) in &ctx.savepoints {
5767 own_xids.insert(*sub);
5768 }
5769 for sub in &ctx.released_sub_xids {
5770 own_xids.insert(*sub);
5771 }
5772 if let Err(err) = self.check_table_row_write_conflicts(
5773 conn_id,
5774 &ctx.snapshot,
5775 &own_xids,
5776 ) {
5777 for (_, sub) in &ctx.savepoints {
5778 self.inner.snapshot_manager.rollback(*sub);
5779 }
5780 for sub in &ctx.released_sub_xids {
5781 self.inner.snapshot_manager.rollback(*sub);
5782 }
5783 self.inner.snapshot_manager.rollback(ctx.xid);
5784 self.revive_pending_versioned_updates(conn_id);
5785 self.revive_pending_tombstones(conn_id);
5786 self.discard_pending_kv_watch_events(conn_id);
5787 self.discard_pending_queue_wakes(conn_id);
5788 self.discard_pending_store_wal_actions(conn_id);
5789 self.release_pending_claim_locks(conn_id);
5790 return Err(err);
5791 }
5792 self.restore_pending_write_stamps(conn_id);
5793 if let Err(err) = self.flush_pending_store_wal_actions(conn_id) {
5794 for (_, sub) in &ctx.savepoints {
5795 self.inner.snapshot_manager.rollback(*sub);
5796 }
5797 for sub in &ctx.released_sub_xids {
5798 self.inner.snapshot_manager.rollback(*sub);
5799 }
5800 self.inner.snapshot_manager.rollback(ctx.xid);
5801 self.revive_pending_versioned_updates(conn_id);
5802 self.revive_pending_tombstones(conn_id);
5803 self.discard_pending_kv_watch_events(conn_id);
5804 self.discard_pending_queue_wakes(conn_id);
5805 self.release_pending_claim_locks(conn_id);
5806 return Err(err);
5807 }
5808 for (_, sub) in &ctx.savepoints {
5814 self.inner.snapshot_manager.commit(*sub);
5815 }
5816 for sub in &ctx.released_sub_xids {
5817 self.inner.snapshot_manager.commit(*sub);
5818 }
5819 self.inner.snapshot_manager.commit(ctx.xid);
5820 self.finalize_pending_versioned_updates(conn_id);
5821 self.finalize_pending_tombstones(conn_id);
5822 self.finalize_pending_kv_watch_events(conn_id);
5823 self.finalize_pending_queue_wakes(conn_id);
5824 self.release_pending_claim_locks(conn_id);
5825 ("commit", format!("COMMIT — xid={} committed", ctx.xid))
5826 }
5827 None => (
5828 "commit",
5829 "COMMIT outside transaction — no-op (autocommit)".to_string(),
5830 ),
5831 }
5832 }
5833 TxnControl::Rollback => {
5834 self.inner.tx_local_tenants.write().remove(&conn_id);
5835 let ctx = self.inner.tx_contexts.write().remove(&conn_id);
5836 match ctx {
5837 Some(ctx) => {
5838 for (_, sub) in &ctx.savepoints {
5841 self.inner.snapshot_manager.rollback(*sub);
5842 }
5843 for sub in &ctx.released_sub_xids {
5844 self.inner.snapshot_manager.rollback(*sub);
5845 }
5846 self.inner.snapshot_manager.rollback(ctx.xid);
5847 self.revive_pending_versioned_updates(conn_id);
5851 self.revive_pending_tombstones(conn_id);
5852 self.discard_pending_kv_watch_events(conn_id);
5853 self.discard_pending_queue_wakes(conn_id);
5854 self.discard_pending_store_wal_actions(conn_id);
5855 self.release_pending_claim_locks(conn_id);
5856 ("rollback", format!("ROLLBACK — xid={} aborted", ctx.xid))
5857 }
5858 None => (
5859 "rollback",
5860 "ROLLBACK outside transaction — no-op (autocommit)".to_string(),
5861 ),
5862 }
5863 }
5864 TxnControl::Savepoint(name) => {
5871 let mgr = Arc::clone(&self.inner.snapshot_manager);
5872 let mut guard = self.inner.tx_contexts.write();
5873 match guard.get_mut(&conn_id) {
5874 Some(ctx) => {
5875 let sub = mgr.begin();
5876 ctx.savepoints.push((name.clone(), sub));
5877 ("savepoint", format!("SAVEPOINT {name} — sub_xid={sub}"))
5878 }
5879 None => (
5880 "savepoint",
5881 "SAVEPOINT outside transaction — no-op".to_string(),
5882 ),
5883 }
5884 }
5885 TxnControl::ReleaseSavepoint(name) => {
5886 let mut guard = self.inner.tx_contexts.write();
5887 match guard.get_mut(&conn_id) {
5888 Some(ctx) => {
5889 let pos = ctx
5890 .savepoints
5891 .iter()
5892 .position(|(n, _)| n == name)
5893 .ok_or_else(|| {
5894 RedDBError::Internal(format!(
5895 "savepoint {name} does not exist"
5896 ))
5897 })?;
5898 let released = ctx.savepoints.len() - pos;
5906 let popped: Vec<Xid> = ctx
5907 .savepoints
5908 .split_off(pos)
5909 .into_iter()
5910 .map(|(_, x)| x)
5911 .collect();
5912 ctx.released_sub_xids.extend(popped);
5913 (
5914 "release_savepoint",
5915 format!("RELEASE SAVEPOINT {name} — {released} level(s)"),
5916 )
5917 }
5918 None => (
5919 "release_savepoint",
5920 "RELEASE outside transaction — no-op".to_string(),
5921 ),
5922 }
5923 }
5924 TxnControl::RollbackToSavepoint(name) => {
5925 let mgr = Arc::clone(&self.inner.snapshot_manager);
5926 let drop_result: Option<(Xid, Vec<Xid>)> = {
5931 let mut guard = self.inner.tx_contexts.write();
5932 if let Some(ctx) = guard.get_mut(&conn_id) {
5933 let pos = ctx
5934 .savepoints
5935 .iter()
5936 .position(|(n, _)| n == name)
5937 .ok_or_else(|| {
5938 RedDBError::Internal(format!(
5939 "savepoint {name} does not exist"
5940 ))
5941 })?;
5942 let savepoint_xid = ctx.savepoints[pos].1;
5943 let aborted: Vec<Xid> = ctx
5944 .savepoints
5945 .split_off(pos)
5946 .into_iter()
5947 .map(|(_, x)| x)
5948 .collect();
5949 Some((savepoint_xid, aborted))
5950 } else {
5951 None
5952 }
5953 };
5954
5955 match drop_result {
5956 Some((savepoint_xid, aborted)) => {
5957 for x in &aborted {
5958 mgr.rollback(*x);
5959 }
5960 let reverted_updates =
5961 self.revive_versioned_updates_since(conn_id, savepoint_xid);
5962 let revived = self.revive_tombstones_since(conn_id, savepoint_xid);
5963 (
5964 "rollback_to_savepoint",
5965 format!(
5966 "ROLLBACK TO SAVEPOINT {name} — aborted {} sub_xid(s), reverted {reverted_updates} update(s), revived {revived} tombstone(s)",
5967 aborted.len(),
5968 ),
5969 )
5970 }
5971 None => (
5972 "rollback_to_savepoint",
5973 "ROLLBACK TO outside transaction — no-op".to_string(),
5974 ),
5975 }
5976 }
5977 };
5978 Ok(RuntimeQueryResult::ok_message(
5979 query.to_string(),
5980 &msg,
5981 kind,
5982 ))
5983 }
5984 QueryExpr::CreateSchema(ref q) => {
5997 let store = self.inner.db.store();
5998 let key = format!("schema.{}", q.name);
5999 if store.get_config(&key).is_some() {
6000 if q.if_not_exists {
6001 return Ok(RuntimeQueryResult::ok_message(
6002 query.to_string(),
6003 &format!("schema {} already exists — skipped", q.name),
6004 "create_schema",
6005 ));
6006 }
6007 return Err(RedDBError::Internal(format!(
6008 "schema {} already exists",
6009 q.name
6010 )));
6011 }
6012 store.set_config_tree(&key, &crate::serde_json::Value::Bool(true));
6013 Ok(RuntimeQueryResult::ok_message(
6014 query.to_string(),
6015 &format!("schema {} created", q.name),
6016 "create_schema",
6017 ))
6018 }
6019 QueryExpr::DropSchema(ref q) => {
6020 let store = self.inner.db.store();
6021 let key = format!("schema.{}", q.name);
6022 let existed = store.get_config(&key).is_some();
6023 if !existed && !q.if_exists {
6024 return Err(RedDBError::Internal(format!(
6025 "schema {} does not exist",
6026 q.name
6027 )));
6028 }
6029 store.set_config_tree(&key, &crate::serde_json::Value::Null);
6031 let suffix = if q.cascade {
6032 " (CASCADE accepted — tables untouched)"
6033 } else {
6034 ""
6035 };
6036 Ok(RuntimeQueryResult::ok_message(
6037 query.to_string(),
6038 &format!("schema {} dropped{}", q.name, suffix),
6039 "drop_schema",
6040 ))
6041 }
6042 QueryExpr::CreateSequence(ref q) => {
6043 let store = self.inner.db.store();
6044 let base = format!("sequence.{}", q.name);
6045 let start_key = format!("{base}.start");
6046 let incr_key = format!("{base}.increment");
6047 let curr_key = format!("{base}.current");
6048 if store.get_config(&start_key).is_some() {
6049 if q.if_not_exists {
6050 return Ok(RuntimeQueryResult::ok_message(
6051 query.to_string(),
6052 &format!("sequence {} already exists — skipped", q.name),
6053 "create_sequence",
6054 ));
6055 }
6056 return Err(RedDBError::Internal(format!(
6057 "sequence {} already exists",
6058 q.name
6059 )));
6060 }
6061 let initial_current = q.start - q.increment;
6064 store.set_config_tree(
6065 &start_key,
6066 &crate::serde_json::Value::Number(q.start as f64),
6067 );
6068 store.set_config_tree(
6069 &incr_key,
6070 &crate::serde_json::Value::Number(q.increment as f64),
6071 );
6072 store.set_config_tree(
6073 &curr_key,
6074 &crate::serde_json::Value::Number(initial_current as f64),
6075 );
6076 Ok(RuntimeQueryResult::ok_message(
6077 query.to_string(),
6078 &format!(
6079 "sequence {} created (start={}, increment={})",
6080 q.name, q.start, q.increment
6081 ),
6082 "create_sequence",
6083 ))
6084 }
6085 QueryExpr::DropSequence(ref q) => {
6086 let store = self.inner.db.store();
6087 let base = format!("sequence.{}", q.name);
6088 let existed = store.get_config(&format!("{base}.start")).is_some();
6089 if !existed && !q.if_exists {
6090 return Err(RedDBError::Internal(format!(
6091 "sequence {} does not exist",
6092 q.name
6093 )));
6094 }
6095 for k in ["start", "increment", "current"] {
6096 store.set_config_tree(&format!("{base}.{k}"), &crate::serde_json::Value::Null);
6097 }
6098 Ok(RuntimeQueryResult::ok_message(
6099 query.to_string(),
6100 &format!("sequence {} dropped", q.name),
6101 "drop_sequence",
6102 ))
6103 }
6104 QueryExpr::CreateView(ref q) => {
6114 let mut views = self.inner.views.write();
6115 if views.contains_key(&q.name) && !q.or_replace {
6116 if q.if_not_exists {
6117 return Ok(RuntimeQueryResult::ok_message(
6118 query.to_string(),
6119 &format!("view {} already exists — skipped", q.name),
6120 "create_view",
6121 ));
6122 }
6123 return Err(RedDBError::Internal(format!(
6124 "view {} already exists",
6125 q.name
6126 )));
6127 }
6128 views.insert(q.name.clone(), Arc::new(q.clone()));
6129 drop(views);
6130
6131 if q.materialized {
6133 use crate::storage::cache::result::{MaterializedViewDef, RefreshPolicy};
6134 let refresh = match q.refresh_every_ms {
6135 Some(ms) => RefreshPolicy::Periodic(std::time::Duration::from_millis(ms)),
6136 None => RefreshPolicy::Manual,
6137 };
6138 let dependencies = collect_table_refs(&q.query);
6139 let def = MaterializedViewDef {
6140 name: q.name.clone(),
6141 query: format!("<parsed view {}>", q.name),
6142 dependencies: dependencies.clone(),
6143 refresh,
6144 retention_duration_ms: q.retention_duration_ms,
6145 };
6146 self.inner.materialized_views.write().register(def);
6147
6148 let descriptor =
6154 crate::runtime::continuous_materialized_view::MaterializedViewDescriptor {
6155 name: q.name.clone(),
6156 source_sql: query.to_string(),
6157 source_collections: dependencies,
6158 refresh_every_ms: q.refresh_every_ms,
6159 retention_duration_ms: q.retention_duration_ms,
6160 };
6161 let store = self.inner.db.store();
6162 crate::runtime::continuous_materialized_view::persist_descriptor(
6163 store.as_ref(),
6164 &descriptor,
6165 )?;
6166
6167 self.ensure_materialized_view_backing(&q.name)?;
6174 }
6175 self.invalidate_plan_cache();
6180 self.invalidate_result_cache();
6181
6182 Ok(RuntimeQueryResult::ok_message(
6183 query.to_string(),
6184 &format!(
6185 "{}view {} created",
6186 if q.materialized { "materialized " } else { "" },
6187 q.name
6188 ),
6189 "create_view",
6190 ))
6191 }
6192 QueryExpr::DropView(ref q) => {
6193 let mut views = self.inner.views.write();
6194 let removed = views.remove(&q.name);
6195 let existed = removed.is_some();
6196 let removed_materialized =
6197 removed.as_ref().map(|v| v.materialized).unwrap_or(false);
6198 drop(views);
6199 if q.materialized || existed {
6200 self.inner.materialized_views.write().remove(&q.name);
6202 let store = self.inner.db.store();
6206 crate::runtime::continuous_materialized_view::remove_by_name(
6207 store.as_ref(),
6208 &q.name,
6209 )?;
6210 }
6211 if removed_materialized || q.materialized {
6215 self.drop_materialized_view_backing(&q.name)?;
6216 }
6217 self.invalidate_plan_cache();
6220 self.invalidate_result_cache();
6221 if !existed && !q.if_exists {
6222 return Err(RedDBError::Internal(format!(
6223 "view {} does not exist",
6224 q.name
6225 )));
6226 }
6227 self.invalidate_plan_cache();
6228 Ok(RuntimeQueryResult::ok_message(
6229 query.to_string(),
6230 &format!("view {} dropped", q.name),
6231 "drop_view",
6232 ))
6233 }
6234 QueryExpr::RefreshMaterializedView(ref q) => {
6235 let view = {
6238 let views = self.inner.views.read();
6239 views.get(&q.name).cloned()
6240 };
6241 let view = match view {
6242 Some(v) => v,
6243 None => {
6244 return Err(RedDBError::Internal(format!(
6245 "view {} does not exist",
6246 q.name
6247 )))
6248 }
6249 };
6250 if !view.materialized {
6251 return Err(RedDBError::Internal(format!(
6252 "view {} is not materialized — REFRESH requires \
6253 CREATE MATERIALIZED VIEW",
6254 q.name
6255 )));
6256 }
6257 let started = std::time::Instant::now();
6259 let now_ms = std::time::SystemTime::now()
6260 .duration_since(std::time::UNIX_EPOCH)
6261 .map(|d| d.as_millis() as u64)
6262 .unwrap_or(0);
6263 match self.execute_query_expr((*view.query).clone()) {
6264 Ok(inner_result) => {
6265 let entities =
6272 view_records_to_entities(&q.name, &inner_result.result.records);
6273 let row_count = entities.len() as u64;
6274 let store = self.inner.db.store();
6275 let serialized_records = match store.refresh_collection(&q.name, entities) {
6276 Ok(records) => records,
6277 Err(err) => {
6278 let duration_ms = started.elapsed().as_millis() as u64;
6279 let msg = err.to_string();
6280 self.inner
6281 .materialized_views
6282 .write()
6283 .record_refresh_failure(
6284 &q.name,
6285 msg.clone(),
6286 duration_ms,
6287 now_ms,
6288 );
6289 return Err(RedDBError::Internal(format!(
6290 "REFRESH MATERIALIZED VIEW {}: {msg}",
6291 q.name
6292 )));
6293 }
6294 };
6295
6296 if let Some(ref primary) = self.inner.db.replication {
6302 let lsn = self.inner.cdc.emit(
6303 crate::replication::cdc::ChangeOperation::Refresh,
6304 &q.name,
6305 0,
6306 "refresh",
6307 );
6308 self.invalidate_result_cache_for_table(&q.name);
6309 let timestamp = std::time::SystemTime::now()
6310 .duration_since(std::time::UNIX_EPOCH)
6311 .unwrap_or_default()
6312 .as_millis() as u64;
6313 let record = ChangeRecord::for_refresh(
6314 lsn,
6315 timestamp,
6316 q.name.clone(),
6317 serialized_records,
6318 )
6319 .with_term(self.current_replication_term());
6320 let encoded = record.encode();
6321 primary.append_logical_record(record.lsn, encoded);
6322 }
6323
6324 let duration_ms = started.elapsed().as_millis() as u64;
6325 let serialized = format!("{:?}", inner_result.result);
6326 self.inner
6327 .materialized_views
6328 .write()
6329 .record_refresh_success(
6330 &q.name,
6331 serialized.into_bytes(),
6332 row_count,
6333 duration_ms,
6334 now_ms,
6335 );
6336 self.invalidate_result_cache();
6341 Ok(RuntimeQueryResult::ok_message(
6342 query.to_string(),
6343 &format!("materialized view {} refreshed", q.name),
6344 "refresh_materialized_view",
6345 ))
6346 }
6347 Err(err) => {
6348 let duration_ms = started.elapsed().as_millis() as u64;
6349 let msg = err.to_string();
6350 self.inner
6351 .materialized_views
6352 .write()
6353 .record_refresh_failure(&q.name, msg.clone(), duration_ms, now_ms);
6354 Err(err)
6355 }
6356 }
6357 }
6358 QueryExpr::CreatePolicy(ref q) => {
6365 let key = (q.table.clone(), q.name.clone());
6366 self.inner
6367 .rls_policies
6368 .write()
6369 .insert(key, Arc::new(q.clone()));
6370 self.invalidate_plan_cache();
6371 self.schema_vocabulary_apply(
6375 crate::runtime::schema_vocabulary::DdlEvent::CreatePolicy {
6376 collection: q.table.clone(),
6377 policy: q.name.clone(),
6378 },
6379 );
6380 Ok(RuntimeQueryResult::ok_message(
6381 query.to_string(),
6382 &format!("policy {} on {} created", q.name, q.table),
6383 "create_policy",
6384 ))
6385 }
6386 QueryExpr::DropPolicy(ref q) => {
6387 let removed = self
6388 .inner
6389 .rls_policies
6390 .write()
6391 .remove(&(q.table.clone(), q.name.clone()))
6392 .is_some();
6393 if !removed && !q.if_exists {
6394 return Err(RedDBError::Internal(format!(
6395 "policy {} on {} does not exist",
6396 q.name, q.table
6397 )));
6398 }
6399 self.invalidate_plan_cache();
6400 self.schema_vocabulary_apply(
6403 crate::runtime::schema_vocabulary::DdlEvent::DropPolicy {
6404 collection: q.table.clone(),
6405 policy: q.name.clone(),
6406 },
6407 );
6408 Ok(RuntimeQueryResult::ok_message(
6409 query.to_string(),
6410 &format!("policy {} on {} dropped", q.name, q.table),
6411 "drop_policy",
6412 ))
6413 }
6414 QueryExpr::CreateServer(ref q) => {
6425 use crate::storage::fdw::FdwOptions;
6426 let registry = Arc::clone(&self.inner.foreign_tables);
6427 if registry.server(&q.name).is_some() {
6428 if q.if_not_exists {
6429 return Ok(RuntimeQueryResult::ok_message(
6430 query.to_string(),
6431 &format!("server {} already exists — skipped", q.name),
6432 "create_server",
6433 ));
6434 }
6435 return Err(RedDBError::Internal(format!(
6436 "server {} already exists",
6437 q.name
6438 )));
6439 }
6440 let mut opts = FdwOptions::new();
6441 for (k, v) in &q.options {
6442 opts.values.insert(k.clone(), v.clone());
6443 }
6444 registry
6445 .create_server(&q.name, &q.wrapper, opts)
6446 .map_err(|e| RedDBError::Internal(e.to_string()))?;
6447 Ok(RuntimeQueryResult::ok_message(
6448 query.to_string(),
6449 &format!("server {} created (wrapper {})", q.name, q.wrapper),
6450 "create_server",
6451 ))
6452 }
6453 QueryExpr::DropServer(ref q) => {
6454 let existed = self.inner.foreign_tables.drop_server(&q.name);
6455 if !existed && !q.if_exists {
6456 return Err(RedDBError::Internal(format!(
6457 "server {} does not exist",
6458 q.name
6459 )));
6460 }
6461 Ok(RuntimeQueryResult::ok_message(
6462 query.to_string(),
6463 &format!(
6464 "server {} dropped{}",
6465 q.name,
6466 if q.cascade { " (cascade)" } else { "" }
6467 ),
6468 "drop_server",
6469 ))
6470 }
6471 QueryExpr::CreateForeignTable(ref q) => {
6472 use crate::storage::fdw::{FdwOptions, ForeignColumn, ForeignTable};
6473 let registry = Arc::clone(&self.inner.foreign_tables);
6474 if registry.foreign_table(&q.name).is_some() {
6475 if q.if_not_exists {
6476 return Ok(RuntimeQueryResult::ok_message(
6477 query.to_string(),
6478 &format!("foreign table {} already exists — skipped", q.name),
6479 "create_foreign_table",
6480 ));
6481 }
6482 return Err(RedDBError::Internal(format!(
6483 "foreign table {} already exists",
6484 q.name
6485 )));
6486 }
6487 let mut opts = FdwOptions::new();
6488 for (k, v) in &q.options {
6489 opts.values.insert(k.clone(), v.clone());
6490 }
6491 let columns: Vec<ForeignColumn> = q
6492 .columns
6493 .iter()
6494 .map(|c| ForeignColumn {
6495 name: c.name.clone(),
6496 data_type: c.data_type.clone(),
6497 not_null: c.not_null,
6498 })
6499 .collect();
6500 registry
6501 .create_foreign_table(ForeignTable {
6502 name: q.name.clone(),
6503 server_name: q.server.clone(),
6504 columns,
6505 options: opts,
6506 })
6507 .map_err(|e| RedDBError::Internal(e.to_string()))?;
6508 self.invalidate_plan_cache();
6509 Ok(RuntimeQueryResult::ok_message(
6510 query.to_string(),
6511 &format!("foreign table {} created (server {})", q.name, q.server),
6512 "create_foreign_table",
6513 ))
6514 }
6515 QueryExpr::DropForeignTable(ref q) => {
6516 let existed = self.inner.foreign_tables.drop_foreign_table(&q.name);
6517 if !existed && !q.if_exists {
6518 return Err(RedDBError::Internal(format!(
6519 "foreign table {} does not exist",
6520 q.name
6521 )));
6522 }
6523 self.invalidate_plan_cache();
6524 Ok(RuntimeQueryResult::ok_message(
6525 query.to_string(),
6526 &format!("foreign table {} dropped", q.name),
6527 "drop_foreign_table",
6528 ))
6529 }
6530 QueryExpr::CopyFrom(ref q) => {
6536 use crate::storage::import::{CsvConfig, CsvImporter};
6537 let store = self.inner.db.store();
6538 let cfg = CsvConfig {
6539 collection: q.table.clone(),
6540 has_header: q.has_header,
6541 delimiter: q.delimiter.map(|c| c as u8).unwrap_or(b','),
6542 ..CsvConfig::default()
6543 };
6544 let importer = CsvImporter::new(cfg);
6545 let stats = importer
6546 .import_file(&q.path, store.as_ref())
6547 .map_err(|e| RedDBError::Internal(format!("COPY failed: {e}")))?;
6548 self.note_table_write(&q.table);
6550 Ok(RuntimeQueryResult::ok_message(
6551 query.to_string(),
6552 &format!(
6553 "COPY imported {} rows into {} ({} errors skipped, {}ms)",
6554 stats.records_imported, q.table, stats.errors_skipped, stats.duration_ms
6555 ),
6556 "copy_from",
6557 ))
6558 }
6559 QueryExpr::MaintenanceCommand(ref cmd) => {
6575 use crate::storage::query::ast::MaintenanceCommand as Mc;
6576 let store = self.inner.db.store();
6577 let (kind, msg) = match cmd {
6578 Mc::Analyze { target } => {
6579 let targets: Vec<String> = match target {
6580 Some(t) => vec![t.clone()],
6581 None => store.list_collections(),
6582 };
6583 for t in &targets {
6584 self.refresh_table_planner_stats(t);
6585 }
6586 (
6587 "analyze",
6588 format!("ANALYZE refreshed stats for {} table(s)", targets.len()),
6589 )
6590 }
6591 Mc::Vacuum { target, full } => {
6592 let targets: Vec<String> = match target {
6593 Some(t) => vec![t.clone()],
6594 None => store.list_collections(),
6595 };
6596 let cutoff_xid = self.mvcc_vacuum_cutoff_xid();
6597 let mut vacuum_stats =
6598 crate::storage::unified::store::MvccVacuumStats::default();
6599 for t in &targets {
6600 let stats = store.vacuum_mvcc_history(t, cutoff_xid).map_err(|e| {
6601 RedDBError::Internal(format!(
6602 "VACUUM MVCC history failed for {t}: {e}"
6603 ))
6604 })?;
6605 if stats.reclaimed_versions > 0 {
6606 self.rebuild_runtime_indexes_for_table(t)?;
6607 }
6608 vacuum_stats.add(&stats);
6609 }
6610 self.inner.snapshot_manager.prune_aborted(cutoff_xid);
6611 for t in &targets {
6613 self.refresh_table_planner_stats(t);
6614 }
6615 let persisted = if *full {
6619 match store.persist() {
6620 Ok(()) => true,
6621 Err(e) => {
6622 return Err(RedDBError::Internal(format!(
6623 "VACUUM FULL persist failed: {e:?}"
6624 )));
6625 }
6626 }
6627 } else {
6628 false
6629 };
6630 self.invalidate_result_cache();
6632 (
6633 "vacuum",
6634 format!(
6635 "VACUUM{} processed {} table(s): scanned_versions={}, retained_versions={}, reclaimed_versions={}, retained_history_versions={}, reclaimed_history_versions={}, retained_tombstones={}, reclaimed_tombstones={}{}",
6636 if *full { " FULL" } else { "" },
6637 targets.len(),
6638 vacuum_stats.scanned_versions,
6639 vacuum_stats.retained_versions,
6640 vacuum_stats.reclaimed_versions,
6641 vacuum_stats.retained_history_versions,
6642 vacuum_stats.reclaimed_history_versions,
6643 vacuum_stats.retained_tombstones,
6644 vacuum_stats.reclaimed_tombstones,
6645 if persisted {
6646 " (pages flushed to disk)"
6647 } else {
6648 ""
6649 }
6650 ),
6651 )
6652 }
6653 };
6654 Ok(RuntimeQueryResult::ok_message(
6655 query.to_string(),
6656 &msg,
6657 kind,
6658 ))
6659 }
6660 QueryExpr::Grant(ref g) => self.execute_grant_statement(query, g),
6667 QueryExpr::Revoke(ref r) => self.execute_revoke_statement(query, r),
6668 QueryExpr::AlterUser(ref a) => self.execute_alter_user_statement(query, a),
6669 QueryExpr::CreateUser(ref u) => self.execute_create_user_statement(query, u),
6670 QueryExpr::CreateIamPolicy { ref id, ref json } => {
6671 self.execute_create_iam_policy(query, id, json)
6672 }
6673 QueryExpr::DropIamPolicy { ref id } => self.execute_drop_iam_policy(query, id),
6674 QueryExpr::AttachPolicy {
6675 ref policy_id,
6676 ref principal,
6677 } => self.execute_attach_policy(query, policy_id, principal),
6678 QueryExpr::DetachPolicy {
6679 ref policy_id,
6680 ref principal,
6681 } => self.execute_detach_policy(query, policy_id, principal),
6682 QueryExpr::ShowPolicies { ref filter } => {
6683 self.execute_show_policies(query, filter.as_ref())
6684 }
6685 QueryExpr::ShowEffectivePermissions {
6686 ref user,
6687 ref resource,
6688 } => self.execute_show_effective_permissions(query, user, resource.as_ref()),
6689 QueryExpr::SimulatePolicy {
6690 ref user,
6691 ref action,
6692 ref resource,
6693 } => self.execute_simulate_policy(query, user, action, resource),
6694 QueryExpr::LintPolicy { ref source } => self.execute_lint_policy(query, source),
6695 QueryExpr::MigratePolicyMode {
6696 ref target,
6697 dry_run,
6698 } => self.execute_migrate_policy_mode(query, target, dry_run),
6699 QueryExpr::CreateMigration(ref q) => self.execute_create_migration(query, q),
6700 QueryExpr::ApplyMigration(ref q) => self.execute_apply_migration(query, q),
6701 QueryExpr::RollbackMigration(ref q) => self.execute_rollback_migration(query, q),
6702 QueryExpr::ExplainMigration(ref q) => self.execute_explain_migration(query, q),
6703 _ => Err(RedDBError::Query(
6704 "unsupported command in runtime dispatcher".to_string(),
6705 )),
6706 };
6707
6708 if !control_event_specs.is_empty() {
6709 let (outcome, reason) = match &query_result {
6710 Ok(_) => (crate::runtime::control_events::Outcome::Allowed, None),
6711 Err(err) => (control_event_outcome_for_error(err), Some(err.to_string())),
6712 };
6713 for spec in &control_event_specs {
6714 self.emit_control_event(
6715 spec.kind,
6716 outcome,
6717 spec.action,
6718 spec.resource.clone(),
6719 reason.clone(),
6720 spec.fields.clone(),
6721 )?;
6722 }
6723 }
6724
6725 if let (Some(plan), Ok(result)) = (&query_audit_plan, &query_result) {
6726 self.emit_query_audit(
6727 query,
6728 plan,
6729 query_audit_started.elapsed().as_millis() as u64,
6730 result,
6731 );
6732 }
6733
6734 let mut query_result = query_result;
6738 if let Ok(ref mut result) = query_result {
6739 if result.statement_type == "select" {
6740 self.apply_secret_decryption(result);
6741 }
6742 }
6743
6744 if let Ok(ref result) = query_result {
6751 frame.write_result_cache(self, result, result_cache_scopes);
6752 }
6753
6754 query_result
6755 }
6756
6757 pub fn materialized_view_metadata(
6761 &self,
6762 ) -> Vec<crate::storage::cache::result::MaterializedViewMetadata> {
6763 let store = self.inner.db.store();
6770 let mut entries = self.inner.materialized_views.read().metadata();
6771 for entry in &mut entries {
6772 if let Some(manager) = store.get_collection(&entry.name) {
6773 entry.current_row_count = manager.count() as u64;
6774 }
6775 }
6776 entries
6777 }
6778
6779 pub(crate) fn retention_sweeper_snapshot(
6790 &self,
6791 ) -> Vec<(String, crate::runtime::retention_sweeper::SweeperState)> {
6792 self.inner.retention_sweeper.read().snapshot()
6793 }
6794
6795 pub fn sweep_retention_tick(&self, batch_size: usize) {
6817 if batch_size == 0 {
6818 return;
6819 }
6820 let now_ms = std::time::SystemTime::now()
6821 .duration_since(std::time::UNIX_EPOCH)
6822 .map(|d| d.as_millis() as u64)
6823 .unwrap_or(0);
6824
6825 let store = self.inner.db.store();
6826 let collections = store.list_collections();
6827 for name in collections {
6828 let Some(contract) = self.inner.db.collection_contract(&name) else {
6829 continue;
6830 };
6831 let Some(retention_ms) = contract.retention_duration_ms else {
6832 continue;
6833 };
6834 let Some(ts_column) =
6835 crate::runtime::retention_filter::resolve_timestamp_column(&contract)
6836 else {
6837 continue;
6838 };
6839 let Some(manager) = store.get_collection(&name) else {
6840 continue;
6841 };
6842 let cutoff = (now_ms as i64).saturating_sub(retention_ms as i64);
6843
6844 let mut expired_ts: Vec<i64> = Vec::new();
6852 manager.for_each_entity(|entity| {
6853 let ts = match ts_column.as_str() {
6854 "created_at" => Some(entity.created_at as i64),
6855 "updated_at" => Some(entity.updated_at as i64),
6856 other => entity
6857 .data
6858 .as_row()
6859 .and_then(|row| row.get_field(other))
6860 .and_then(|v| match v {
6861 crate::storage::schema::Value::TimestampMs(t) => Some(*t),
6862 crate::storage::schema::Value::Timestamp(t) => {
6863 Some(t.saturating_mul(1_000))
6864 }
6865 crate::storage::schema::Value::BigInt(t) => Some(*t),
6866 crate::storage::schema::Value::UnsignedInteger(t) => {
6867 i64::try_from(*t).ok()
6868 }
6869 crate::storage::schema::Value::Integer(t) => Some(*t),
6870 _ => None,
6871 }),
6872 };
6873 if let Some(t) = ts {
6874 if t < cutoff {
6875 expired_ts.push(t);
6876 }
6877 }
6878 true
6879 });
6880
6881 let total_expired = expired_ts.len() as u64;
6882 if total_expired == 0 {
6883 self.inner
6884 .retention_sweeper
6885 .write()
6886 .record_tick(&name, 0, 0, now_ms);
6887 continue;
6888 }
6889
6890 let (effective_cutoff, pending) = if (total_expired as usize) <= batch_size {
6891 (cutoff, 0u64)
6892 } else {
6893 expired_ts.sort_unstable();
6897 let nth = expired_ts[batch_size - 1];
6898 (
6899 nth.saturating_add(1),
6900 total_expired.saturating_sub(batch_size as u64),
6901 )
6902 };
6903
6904 let stmt = format!(
6905 "DELETE FROM {} WHERE {} < {}",
6906 name, ts_column, effective_cutoff
6907 );
6908 let deleted = match self.execute_query(&stmt) {
6909 Ok(r) => r.affected_rows,
6910 Err(_) => 0,
6911 };
6912
6913 self.inner
6914 .retention_sweeper
6915 .write()
6916 .record_tick(&name, deleted, pending, now_ms);
6917 }
6918 }
6919
6920 pub fn refresh_due_materialized_views(&self) {
6921 let due = {
6922 let mut cache = self.inner.materialized_views.write();
6923 cache.claim_due_at(std::time::Instant::now())
6924 };
6925 for name in due {
6926 let stmt = format!("REFRESH MATERIALIZED VIEW {}", name);
6933 let _ = self.execute_query(&stmt);
6934 }
6935 }
6936
6937 pub fn execute_query_expr(&self, expr: QueryExpr) -> RedDBResult<RuntimeQueryResult> {
6943 let _config_snapshot_guard = ConfigSnapshotGuard::install(Arc::clone(&self.inner.db));
6944 let _secret_store_guard = SecretStoreGuard::install(self.inner.auth_store.read().clone());
6945 let expr = self.rewrite_view_refs(expr);
6949
6950 self.validate_model_operations_before_auth(&expr)?;
6951 if let Err(err) = self.check_query_privilege(&expr) {
6955 return Err(RedDBError::Query(format!("permission denied: {err}")));
6956 }
6957
6958 let statement = query_expr_name(&expr);
6959 let mode = detect_mode(statement);
6960 let query_str = statement;
6961
6962 let result = self.dispatch_expr(expr, query_str, mode)?;
6963 let mut r = result;
6964 if r.statement_type == "select" {
6965 self.apply_secret_decryption(&mut r);
6966 }
6967 Ok(r)
6968 }
6969
6970 pub(super) fn validate_model_operations_before_auth(
6971 &self,
6972 expr: &QueryExpr,
6973 ) -> RedDBResult<()> {
6974 use crate::catalog::CollectionModel;
6975 use crate::runtime::ddl::polymorphic_resolver;
6976 use crate::storage::query::ast::KvCommand;
6977
6978 let system_schema_target = match expr {
6979 QueryExpr::DropTable(q) => Some(q.name.as_str()),
6980 QueryExpr::DropGraph(q) => Some(q.name.as_str()),
6981 QueryExpr::DropVector(q) => Some(q.name.as_str()),
6982 QueryExpr::DropDocument(q) => Some(q.name.as_str()),
6983 QueryExpr::DropKv(q) => Some(q.name.as_str()),
6984 QueryExpr::DropCollection(q) => Some(q.name.as_str()),
6985 QueryExpr::Truncate(q) => Some(q.name.as_str()),
6986 _ => None,
6987 };
6988 if system_schema_target.is_some_and(crate::runtime::impl_ddl::is_system_schema_name) {
6989 return Err(RedDBError::Query("system schema is read-only".to_string()));
6990 }
6991
6992 let expected = match expr {
6993 QueryExpr::DropTable(q) => Some((q.name.as_str(), CollectionModel::Table)),
6994 QueryExpr::DropGraph(q) => Some((q.name.as_str(), CollectionModel::Graph)),
6995 QueryExpr::DropVector(q) => Some((q.name.as_str(), CollectionModel::Vector)),
6996 QueryExpr::DropDocument(q) => Some((q.name.as_str(), CollectionModel::Document)),
6997 QueryExpr::DropKv(q) => Some((q.name.as_str(), q.model)),
6998 QueryExpr::DropCollection(q) => q.model.map(|model| (q.name.as_str(), model)),
6999 QueryExpr::Truncate(q) => q.model.map(|model| (q.name.as_str(), model)),
7000 QueryExpr::KvCommand(cmd) => {
7001 let (collection, model) = match cmd {
7002 KvCommand::Put {
7003 collection, model, ..
7004 }
7005 | KvCommand::Get {
7006 collection, model, ..
7007 }
7008 | KvCommand::Incr {
7009 collection, model, ..
7010 }
7011 | KvCommand::Cas {
7012 collection, model, ..
7013 }
7014 | KvCommand::List {
7015 collection, model, ..
7016 }
7017 | KvCommand::Delete {
7018 collection, model, ..
7019 } => (collection.as_str(), *model),
7020 KvCommand::Rotate { collection, .. }
7021 | KvCommand::History { collection, .. }
7022 | KvCommand::Purge { collection, .. } => {
7023 (collection.as_str(), CollectionModel::Vault)
7024 }
7025 KvCommand::InvalidateTags { collection, .. } => {
7026 (collection.as_str(), CollectionModel::Kv)
7027 }
7028 KvCommand::Watch {
7029 collection, model, ..
7030 } => (collection.as_str(), *model),
7031 KvCommand::Unseal { collection, .. } => {
7032 (collection.as_str(), CollectionModel::Vault)
7033 }
7034 };
7035 Some((collection, model))
7036 }
7037 QueryExpr::ConfigCommand(cmd) => {
7038 self.validate_config_command_before_auth(cmd)?;
7039 None
7040 }
7041 _ => None,
7042 };
7043
7044 let Some((name, expected_model)) = expected else {
7045 return Ok(());
7046 };
7047 let snapshot = self.inner.db.catalog_model_snapshot();
7048 let Some(actual_model) = snapshot
7049 .collections
7050 .iter()
7051 .find(|collection| collection.name == name)
7052 .map(|collection| collection.declared_model.unwrap_or(collection.model))
7053 else {
7054 return Ok(());
7055 };
7056 polymorphic_resolver::ensure_model_match(expected_model, actual_model)
7057 }
7058
7059 pub(super) fn rewrite_view_refs(&self, expr: QueryExpr) -> QueryExpr {
7064 if self.inner.views.read().is_empty() {
7066 return expr;
7067 }
7068 self.rewrite_view_refs_inner(expr)
7069 }
7070
7071 fn rewrite_view_refs_inner(&self, expr: QueryExpr) -> QueryExpr {
7072 use crate::storage::query::ast::{Filter, TableSource};
7073 match expr {
7074 QueryExpr::Table(mut tq) => {
7075 if let Some(TableSource::Subquery(body)) = tq.source.take() {
7081 tq.source = Some(TableSource::Subquery(Box::new(
7082 self.rewrite_view_refs_inner(*body),
7083 )));
7084 return QueryExpr::Table(tq);
7085 }
7086
7087 let maybe_view = {
7091 let views = self.inner.views.read();
7092 views.get(&tq.table).cloned()
7093 };
7094 let Some(view) = maybe_view else {
7095 return QueryExpr::Table(tq);
7096 };
7097
7098 if view.materialized {
7104 return QueryExpr::Table(tq);
7105 }
7106
7107 let inner_expr = self.rewrite_view_refs_inner((*view.query).clone());
7111
7112 match inner_expr {
7120 QueryExpr::Table(mut inner_tq) => {
7121 if let Some(outer_filter) = tq.filter.take() {
7122 inner_tq.filter = Some(match inner_tq.filter.take() {
7123 Some(existing) => {
7124 Filter::And(Box::new(existing), Box::new(outer_filter))
7125 }
7126 None => outer_filter,
7127 });
7128 inner_tq.where_expr = inner_tq
7136 .filter
7137 .as_ref()
7138 .map(crate::storage::query::sql_lowering::filter_to_expr);
7139 }
7140 if let Some(outer_limit) = tq.limit {
7141 inner_tq.limit = Some(match inner_tq.limit {
7142 Some(existing) => existing.min(outer_limit),
7143 None => outer_limit,
7144 });
7145 }
7146 if let Some(outer_offset) = tq.offset {
7147 inner_tq.offset = Some(match inner_tq.offset {
7148 Some(existing) => existing + outer_offset,
7149 None => outer_offset,
7150 });
7151 }
7152 QueryExpr::Table(inner_tq)
7153 }
7154 other => other,
7155 }
7156 }
7157 QueryExpr::Join(mut jq) => {
7158 jq.left = Box::new(self.rewrite_view_refs_inner(*jq.left));
7159 jq.right = Box::new(self.rewrite_view_refs_inner(*jq.right));
7160 QueryExpr::Join(jq)
7161 }
7162 other => other,
7165 }
7166 }
7167
7168 fn authorize_relational_table_select(
7171 &self,
7172 mut table: TableQuery,
7173 frame: &dyn super::statement_frame::ReadFrame,
7174 ) -> RedDBResult<Option<TableQuery>> {
7175 if let Some(TableSource::Subquery(inner)) = table.source.take() {
7176 let authorized_inner = self.authorize_relational_select_expr(*inner, frame)?;
7177 table.source = Some(TableSource::Subquery(Box::new(authorized_inner)));
7178 return Ok(Some(table));
7179 }
7180
7181 self.check_table_column_projection_authz(&table, frame)?;
7182
7183 if self.inner.rls_enabled_tables.read().contains(&table.table) {
7184 return Ok(inject_rls_filters(self, frame, table));
7185 }
7186
7187 Ok(Some(table))
7188 }
7189
7190 fn authorize_relational_join_select(
7191 &self,
7192 mut join: JoinQuery,
7193 frame: &dyn super::statement_frame::ReadFrame,
7194 ) -> RedDBResult<Option<JoinQuery>> {
7195 self.check_join_column_projection_authz(&join, frame)?;
7196 join.left = Box::new(self.authorize_relational_join_child(*join.left, frame)?);
7197 join.right = Box::new(self.authorize_relational_join_child(*join.right, frame)?);
7198 Ok(inject_rls_into_join(self, frame, join))
7199 }
7200
7201 fn authorize_relational_join_child(
7202 &self,
7203 expr: QueryExpr,
7204 frame: &dyn super::statement_frame::ReadFrame,
7205 ) -> RedDBResult<QueryExpr> {
7206 match expr {
7207 QueryExpr::Table(mut table) => {
7208 if let Some(TableSource::Subquery(inner)) = table.source.take() {
7209 let authorized_inner = self.authorize_relational_select_expr(*inner, frame)?;
7210 table.source = Some(TableSource::Subquery(Box::new(authorized_inner)));
7211 }
7212 Ok(QueryExpr::Table(table))
7213 }
7214 QueryExpr::Join(join) => self
7215 .authorize_relational_join_select(join, frame)?
7216 .map(QueryExpr::Join)
7217 .ok_or_else(|| {
7218 RedDBError::Query("permission denied: RLS denied relational subquery".into())
7219 }),
7220 other => Ok(other),
7221 }
7222 }
7223
7224 fn authorize_relational_select_expr(
7225 &self,
7226 expr: QueryExpr,
7227 frame: &dyn super::statement_frame::ReadFrame,
7228 ) -> RedDBResult<QueryExpr> {
7229 match expr {
7230 QueryExpr::Table(table) => self
7231 .authorize_relational_table_select(table, frame)?
7232 .map(QueryExpr::Table)
7233 .ok_or_else(|| {
7234 RedDBError::Query("permission denied: RLS denied relational subquery".into())
7235 }),
7236 QueryExpr::Join(join) => self
7237 .authorize_relational_join_select(join, frame)?
7238 .map(QueryExpr::Join)
7239 .ok_or_else(|| {
7240 RedDBError::Query("permission denied: RLS denied relational subquery".into())
7241 }),
7242 other => Ok(other),
7243 }
7244 }
7245
7246 fn check_table_column_projection_authz(
7247 &self,
7248 table: &TableQuery,
7249 frame: &dyn super::statement_frame::ReadFrame,
7250 ) -> RedDBResult<()> {
7251 let Some((username, role)) = frame.identity() else {
7252 return Ok(());
7253 };
7254 let Some(auth_store) = self.inner.auth_store.read().clone() else {
7255 return Ok(());
7256 };
7257
7258 let columns = self.resolved_table_projection_columns(table)?;
7259 let request = ColumnAccessRequest::select(table.table.clone(), columns);
7260 let principal = UserId::from_parts(frame.effective_scope(), username);
7261 let ctx = runtime_iam_context(role, frame.effective_scope());
7262 let outcome = auth_store.check_column_projection_authz(&principal, &request, &ctx);
7263 if outcome.allowed() {
7264 return Ok(());
7265 }
7266
7267 if let Some(denied) = outcome.first_denied_column() {
7268 return Err(RedDBError::Query(format!(
7269 "permission denied: principal=`{username}` cannot select column `{}`",
7270 denied.resource.name
7271 )));
7272 }
7273 Err(RedDBError::Query(format!(
7274 "permission denied: principal=`{username}` cannot select table `{}`",
7275 table.table
7276 )))
7277 }
7278
7279 fn check_join_column_projection_authz(
7280 &self,
7281 join: &JoinQuery,
7282 frame: &dyn super::statement_frame::ReadFrame,
7283 ) -> RedDBResult<()> {
7284 let mut by_table: HashMap<String, BTreeSet<String>> = HashMap::new();
7285 let projections = crate::storage::query::sql_lowering::effective_join_projections(join);
7286 self.collect_join_projection_columns(join, &projections, &mut by_table)?;
7287
7288 for (table, columns) in by_table {
7289 let query = TableQuery {
7290 table,
7291 source: None,
7292 alias: None,
7293 select_items: Vec::new(),
7294 columns: columns.into_iter().map(Projection::Column).collect(),
7295 where_expr: None,
7296 filter: None,
7297 group_by_exprs: Vec::new(),
7298 group_by: Vec::new(),
7299 having_expr: None,
7300 having: None,
7301 order_by: Vec::new(),
7302 limit: None,
7303 limit_param: None,
7304 offset: None,
7305 offset_param: None,
7306 expand: None,
7307 as_of: None,
7308 sessionize: None,
7309 distinct: false,
7310 };
7311 self.check_table_column_projection_authz(&query, frame)?;
7312 }
7313 Ok(())
7314 }
7315
7316 fn collect_join_projection_columns(
7317 &self,
7318 join: &JoinQuery,
7319 projections: &[Projection],
7320 out: &mut HashMap<String, BTreeSet<String>>,
7321 ) -> RedDBResult<()> {
7322 let left = table_side_context(join.left.as_ref());
7323 let right = table_side_context(join.right.as_ref());
7324
7325 if projections
7326 .iter()
7327 .any(|projection| matches!(projection, Projection::All))
7328 {
7329 for side in [left.as_ref(), right.as_ref()].into_iter().flatten() {
7330 out.entry(side.table.clone())
7331 .or_default()
7332 .extend(self.table_all_projection_columns(&side.table)?);
7333 }
7334 return Ok(());
7335 }
7336
7337 for projection in projections {
7338 collect_projection_columns_for_join_side(
7339 projection,
7340 left.as_ref(),
7341 right.as_ref(),
7342 out,
7343 )?;
7344 }
7345 Ok(())
7346 }
7347
7348 fn resolved_table_projection_columns(&self, table: &TableQuery) -> RedDBResult<Vec<String>> {
7349 let projections = crate::storage::query::sql_lowering::effective_table_projections(table);
7350 if projections
7351 .iter()
7352 .any(|projection| matches!(projection, Projection::All))
7353 {
7354 return self.table_all_projection_columns(&table.table);
7355 }
7356
7357 let mut columns = BTreeSet::new();
7358 for projection in &projections {
7359 collect_projection_columns_for_table(
7360 projection,
7361 &table.table,
7362 table.alias.as_deref(),
7363 &mut columns,
7364 );
7365 }
7366 Ok(columns.into_iter().collect())
7367 }
7368
7369 fn table_all_projection_columns(&self, table: &str) -> RedDBResult<Vec<String>> {
7370 if let Some(contract) = self.inner.db.collection_contract_arc(table) {
7371 let columns: Vec<String> = contract
7372 .declared_columns
7373 .iter()
7374 .map(|column| column.name.clone())
7375 .collect();
7376 if !columns.is_empty() {
7377 return Ok(columns);
7378 }
7379 }
7380
7381 let records = scan_runtime_table_source_records_limited(&self.inner.db, table, Some(1))?;
7382 Ok(records
7383 .first()
7384 .map(|record| {
7385 record
7386 .column_names()
7387 .into_iter()
7388 .map(|column| column.to_string())
7389 .collect()
7390 })
7391 .unwrap_or_default())
7392 }
7393
7394 fn resolve_table_expr_subqueries(
7395 &self,
7396 mut table: TableQuery,
7397 frame: &dyn super::statement_frame::ReadFrame,
7398 ) -> RedDBResult<TableQuery> {
7399 match table.source.take() {
7406 Some(TableSource::Subquery(inner)) => {
7407 let inner = self.resolve_select_expr_subqueries(*inner, frame)?;
7408 table.source = Some(TableSource::Subquery(Box::new(inner)));
7409 }
7410 other => table.source = other,
7411 }
7412
7413 let outer_scopes = relation_scopes_for_query(&QueryExpr::Table(table.clone()));
7414 for item in &mut table.select_items {
7415 if let crate::storage::query::ast::SelectItem::Expr { expr, .. } = item {
7416 *expr = self.resolve_expr_subqueries(expr.clone(), &outer_scopes, frame)?;
7417 }
7418 }
7419 if let Some(where_expr) = table.where_expr.take() {
7420 table.where_expr =
7421 Some(self.resolve_expr_subqueries(where_expr, &outer_scopes, frame)?);
7422 table.filter = None;
7423 }
7424 if let Some(having_expr) = table.having_expr.take() {
7425 table.having_expr =
7426 Some(self.resolve_expr_subqueries(having_expr, &outer_scopes, frame)?);
7427 table.having = None;
7428 }
7429 for expr in &mut table.group_by_exprs {
7430 *expr = self.resolve_expr_subqueries(expr.clone(), &outer_scopes, frame)?;
7431 }
7432 for clause in &mut table.order_by {
7433 if let Some(expr) = clause.expr.take() {
7434 clause.expr = Some(self.resolve_expr_subqueries(expr, &outer_scopes, frame)?);
7435 }
7436 }
7437 Ok(table)
7438 }
7439
7440 fn resolve_select_expr_subqueries(
7441 &self,
7442 expr: QueryExpr,
7443 frame: &dyn super::statement_frame::ReadFrame,
7444 ) -> RedDBResult<QueryExpr> {
7445 match expr {
7446 QueryExpr::Table(table) => self
7447 .resolve_table_expr_subqueries(table, frame)
7448 .map(QueryExpr::Table),
7449 QueryExpr::Join(mut join) => {
7450 join.left = Box::new(self.resolve_select_expr_subqueries(*join.left, frame)?);
7451 join.right = Box::new(self.resolve_select_expr_subqueries(*join.right, frame)?);
7452 Ok(QueryExpr::Join(join))
7453 }
7454 other => Ok(other),
7455 }
7456 }
7457
7458 fn resolve_expr_subqueries(
7459 &self,
7460 expr: crate::storage::query::ast::Expr,
7461 outer_scopes: &[String],
7462 frame: &dyn super::statement_frame::ReadFrame,
7463 ) -> RedDBResult<crate::storage::query::ast::Expr> {
7464 use crate::storage::query::ast::Expr;
7465
7466 match expr {
7467 Expr::Subquery { query, span } => {
7468 let values = self.execute_expr_subquery_values(query, outer_scopes, frame)?;
7469 if values.len() > 1 {
7470 return Err(RedDBError::Query(
7471 "scalar subquery returned more than one row".to_string(),
7472 ));
7473 }
7474 Ok(Expr::Literal {
7475 value: values.into_iter().next().unwrap_or(Value::Null),
7476 span,
7477 })
7478 }
7479 Expr::BinaryOp { op, lhs, rhs, span } => Ok(Expr::BinaryOp {
7480 op,
7481 lhs: Box::new(self.resolve_expr_subqueries(*lhs, outer_scopes, frame)?),
7482 rhs: Box::new(self.resolve_expr_subqueries(*rhs, outer_scopes, frame)?),
7483 span,
7484 }),
7485 Expr::UnaryOp { op, operand, span } => Ok(Expr::UnaryOp {
7486 op,
7487 operand: Box::new(self.resolve_expr_subqueries(*operand, outer_scopes, frame)?),
7488 span,
7489 }),
7490 Expr::Cast {
7491 inner,
7492 target,
7493 span,
7494 } => Ok(Expr::Cast {
7495 inner: Box::new(self.resolve_expr_subqueries(*inner, outer_scopes, frame)?),
7496 target,
7497 span,
7498 }),
7499 Expr::FunctionCall { name, args, span } => {
7500 let args = args
7501 .into_iter()
7502 .map(|arg| self.resolve_expr_subqueries(arg, outer_scopes, frame))
7503 .collect::<RedDBResult<Vec<_>>>()?;
7504 Ok(Expr::FunctionCall { name, args, span })
7505 }
7506 Expr::Case {
7507 branches,
7508 else_,
7509 span,
7510 } => {
7511 let branches = branches
7512 .into_iter()
7513 .map(|(cond, value)| {
7514 Ok((
7515 self.resolve_expr_subqueries(cond, outer_scopes, frame)?,
7516 self.resolve_expr_subqueries(value, outer_scopes, frame)?,
7517 ))
7518 })
7519 .collect::<RedDBResult<Vec<_>>>()?;
7520 let else_ = else_
7521 .map(|expr| self.resolve_expr_subqueries(*expr, outer_scopes, frame))
7522 .transpose()?
7523 .map(Box::new);
7524 Ok(Expr::Case {
7525 branches,
7526 else_,
7527 span,
7528 })
7529 }
7530 Expr::IsNull {
7531 operand,
7532 negated,
7533 span,
7534 } => Ok(Expr::IsNull {
7535 operand: Box::new(self.resolve_expr_subqueries(*operand, outer_scopes, frame)?),
7536 negated,
7537 span,
7538 }),
7539 Expr::InList {
7540 target,
7541 values,
7542 negated,
7543 span,
7544 } => {
7545 let target =
7546 Box::new(self.resolve_expr_subqueries(*target, outer_scopes, frame)?);
7547 let mut resolved = Vec::new();
7548 for value in values {
7549 if let Expr::Subquery { query, .. } = value {
7550 resolved.extend(
7551 self.execute_expr_subquery_values(query, outer_scopes, frame)?
7552 .into_iter()
7553 .map(Expr::lit),
7554 );
7555 } else {
7556 resolved.push(self.resolve_expr_subqueries(value, outer_scopes, frame)?);
7557 }
7558 }
7559 Ok(Expr::InList {
7560 target,
7561 values: resolved,
7562 negated,
7563 span,
7564 })
7565 }
7566 Expr::Between {
7567 target,
7568 low,
7569 high,
7570 negated,
7571 span,
7572 } => Ok(Expr::Between {
7573 target: Box::new(self.resolve_expr_subqueries(*target, outer_scopes, frame)?),
7574 low: Box::new(self.resolve_expr_subqueries(*low, outer_scopes, frame)?),
7575 high: Box::new(self.resolve_expr_subqueries(*high, outer_scopes, frame)?),
7576 negated,
7577 span,
7578 }),
7579 other => Ok(other),
7580 }
7581 }
7582
7583 fn execute_expr_subquery_values(
7584 &self,
7585 subquery: crate::storage::query::ast::ExprSubquery,
7586 outer_scopes: &[String],
7587 frame: &dyn super::statement_frame::ReadFrame,
7588 ) -> RedDBResult<Vec<Value>> {
7589 let query = *subquery.query;
7590 if query_references_outer_scope(&query, outer_scopes) {
7591 return Err(RedDBError::Query(
7592 "NOT_YET_SUPPORTED: correlated subqueries are not supported yet; track follow-up issue #470-correlated-subqueries".to_string(),
7593 ));
7594 }
7595 let query = self.rewrite_view_refs(query);
7596 let query = self.resolve_select_expr_subqueries(query, frame)?;
7597 let query = self.authorize_relational_select_expr(query, frame)?;
7598 let result = match query {
7599 QueryExpr::Table(table) => {
7600 execute_runtime_table_query(&self.inner.db, &table, Some(&self.inner.index_store))?
7601 }
7602 QueryExpr::Join(join) => execute_runtime_join_query(&self.inner.db, &join)?,
7603 other => {
7604 return Err(RedDBError::Query(format!(
7605 "expression subquery must be a SELECT query, got {}",
7606 query_expr_name(&other)
7607 )))
7608 }
7609 };
7610 first_column_values(result)
7611 }
7612
7613 fn dispatch_expr(
7614 &self,
7615 expr: QueryExpr,
7616 query_str: &str,
7617 mode: QueryMode,
7618 ) -> RedDBResult<RuntimeQueryResult> {
7619 let statement = query_expr_name(&expr);
7620 match expr {
7621 QueryExpr::Graph(_) | QueryExpr::Path(_) => {
7622 Err(RedDBError::Query(
7624 "graph queries cannot be used as prepared statements".to_string(),
7625 ))
7626 }
7627 QueryExpr::Table(table) => {
7628 let scope = self.ai_scope();
7629 let table = self.resolve_table_expr_subqueries(
7630 table,
7631 &scope as &dyn super::statement_frame::ReadFrame,
7632 )?;
7633 if let Some(TableSource::Function {
7637 name,
7638 args,
7639 named_args,
7640 }) = table.source.clone()
7641 {
7642 return Ok(RuntimeQueryResult {
7643 query: query_str.to_string(),
7644 mode,
7645 statement,
7646 engine: "runtime-graph-tvf",
7647 result: self.execute_table_function(&name, &args, &named_args)?,
7648 affected_rows: 0,
7649 statement_type: "select",
7650 bookmark: None,
7651 });
7652 }
7653 if let Some(TableSource::InlineGraphFunction {
7657 name,
7658 nodes,
7659 edges,
7660 named_args,
7661 }) = table.source.clone()
7662 {
7663 return Ok(RuntimeQueryResult {
7664 query: query_str.to_string(),
7665 mode,
7666 statement,
7667 engine: "runtime-graph-tvf-inline",
7668 result: self.execute_inline_graph_function(
7669 &name,
7670 &nodes,
7671 &edges,
7672 &named_args,
7673 )?,
7674 affected_rows: 0,
7675 statement_type: "select",
7676 bookmark: None,
7677 });
7678 }
7679 if super::red_schema::is_virtual_table(&table.table) {
7680 return Ok(RuntimeQueryResult {
7681 query: query_str.to_string(),
7682 mode,
7683 statement,
7684 engine: "runtime-red-schema",
7685 result: super::red_schema::red_query(
7686 self,
7687 &table.table,
7688 &table,
7689 &scope as &dyn super::statement_frame::ReadFrame,
7690 )?,
7691 affected_rows: 0,
7692 statement_type: "select",
7693 bookmark: None,
7694 });
7695 }
7696 if let Some(view_result) = self.try_resolve_analytics_view(
7698 &table,
7699 &scope as &dyn super::statement_frame::ReadFrame,
7700 )? {
7701 return Ok(RuntimeQueryResult {
7702 query: query_str.to_string(),
7703 mode,
7704 statement,
7705 engine: "runtime-graph-analytics-view",
7706 result: view_result,
7707 affected_rows: 0,
7708 statement_type: "select",
7709 bookmark: None,
7710 });
7711 }
7712 let Some(table_with_rls) = self.authorize_relational_table_select(
7713 table,
7714 &scope as &dyn super::statement_frame::ReadFrame,
7715 )?
7716 else {
7717 return Ok(RuntimeQueryResult {
7718 query: query_str.to_string(),
7719 mode,
7720 statement,
7721 engine: "runtime-table-rls",
7722 result: crate::storage::query::unified::UnifiedResult::empty(),
7723 affected_rows: 0,
7724 statement_type: "select",
7725 bookmark: None,
7726 });
7727 };
7728 Ok(RuntimeQueryResult {
7729 query: query_str.to_string(),
7730 mode,
7731 statement,
7732 engine: "runtime-table",
7733 result: execute_runtime_table_query(
7734 &self.inner.db,
7735 &table_with_rls,
7736 Some(&self.inner.index_store),
7737 )?,
7738 affected_rows: 0,
7739 statement_type: "select",
7740 bookmark: None,
7741 })
7742 }
7743 QueryExpr::Join(join) => {
7744 let scope = self.ai_scope();
7745 let Some(join_with_rls) = self.authorize_relational_join_select(
7746 join,
7747 &scope as &dyn super::statement_frame::ReadFrame,
7748 )?
7749 else {
7750 return Ok(RuntimeQueryResult {
7751 query: query_str.to_string(),
7752 mode,
7753 statement,
7754 engine: "runtime-join-rls",
7755 result: crate::storage::query::unified::UnifiedResult::empty(),
7756 affected_rows: 0,
7757 statement_type: "select",
7758 bookmark: None,
7759 });
7760 };
7761 Ok(RuntimeQueryResult {
7762 query: query_str.to_string(),
7763 mode,
7764 statement,
7765 engine: "runtime-join",
7766 result: execute_runtime_join_query(&self.inner.db, &join_with_rls)?,
7767 affected_rows: 0,
7768 statement_type: "select",
7769 bookmark: None,
7770 })
7771 }
7772 QueryExpr::Vector(vector) => Ok(RuntimeQueryResult {
7773 query: query_str.to_string(),
7774 mode,
7775 statement,
7776 engine: "runtime-vector",
7777 result: execute_runtime_vector_query(&self.inner.db, &vector)?,
7778 affected_rows: 0,
7779 statement_type: "select",
7780 bookmark: None,
7781 }),
7782 QueryExpr::Hybrid(hybrid) => Ok(RuntimeQueryResult {
7783 query: query_str.to_string(),
7784 mode,
7785 statement,
7786 engine: "runtime-hybrid",
7787 result: execute_runtime_hybrid_query(&self.inner.db, &hybrid)?,
7788 affected_rows: 0,
7789 statement_type: "select",
7790 bookmark: None,
7791 }),
7792 QueryExpr::Insert(ref insert) if super::red_schema::is_virtual_table(&insert.table) => {
7793 Err(RedDBError::Query(
7794 super::red_schema::READ_ONLY_ERROR.to_string(),
7795 ))
7796 }
7797 QueryExpr::Update(ref update) if super::red_schema::is_virtual_table(&update.table) => {
7798 Err(RedDBError::Query(
7799 super::red_schema::READ_ONLY_ERROR.to_string(),
7800 ))
7801 }
7802 QueryExpr::Delete(ref delete) if super::red_schema::is_virtual_table(&delete.table) => {
7803 Err(RedDBError::Query(
7804 super::red_schema::READ_ONLY_ERROR.to_string(),
7805 ))
7806 }
7807 QueryExpr::Insert(ref insert) => self
7808 .with_deferred_store_wal_for_dml(self.insert_may_emit_events(insert), || {
7809 self.execute_insert(query_str, insert)
7810 }),
7811 QueryExpr::Update(ref update) => self
7812 .with_deferred_store_wal_for_dml(self.update_may_emit_events(update), || {
7813 self.execute_update(query_str, update)
7814 }),
7815 QueryExpr::Delete(ref delete) => self
7816 .with_deferred_store_wal_for_dml(self.delete_may_emit_events(delete), || {
7817 self.execute_delete(query_str, delete)
7818 }),
7819 QueryExpr::SearchCommand(ref cmd) => self.execute_search_command(query_str, cmd),
7820 QueryExpr::Ask(ref ask) => self.execute_ask(query_str, ask),
7821 _ => Err(RedDBError::Query(format!(
7822 "prepared-statement execution does not support {statement} statements"
7823 ))),
7824 }
7825 }
7826
7827 fn execute_table_function(
7834 &self,
7835 name: &str,
7836 args: &[String],
7837 named_args: &[(String, f64)],
7838 ) -> RedDBResult<crate::storage::query::unified::UnifiedResult> {
7839 if name.eq_ignore_ascii_case("red.diff") {
7840 return self.execute_vcs_diff_tvf(args, named_args);
7841 }
7842 if !is_graph_tvf_name(name) {
7843 return Err(RedDBError::Query(format!("unknown table function: {name}")));
7844 }
7845 if args.len() != 1 {
7847 return Err(RedDBError::Query(format!(
7848 "table function '{name}' takes exactly 1 graph argument, got {}",
7849 args.len()
7850 )));
7851 }
7852
7853 let (nodes, edges) = self.materialize_whole_graph_abstract()?;
7858 self.dispatch_graph_algorithm(name, nodes, edges, named_args)
7859 }
7860
7861 fn execute_vcs_diff_tvf(
7862 &self,
7863 args: &[String],
7864 named_args: &[(String, f64)],
7865 ) -> RedDBResult<crate::storage::query::unified::UnifiedResult> {
7866 use crate::application::vcs::{DiffChange, DiffInput};
7867 use crate::storage::query::unified::{UnifiedRecord, UnifiedResult};
7868 use crate::storage::schema::Value;
7869
7870 if !named_args.is_empty() {
7871 return Err(RedDBError::Query(
7872 "red.diff takes only positional commit arguments".to_string(),
7873 ));
7874 }
7875 if args.len() != 2 {
7876 return Err(RedDBError::Query(format!(
7877 "red.diff takes exactly 2 commit arguments, got {}",
7878 args.len()
7879 )));
7880 }
7881
7882 let diff = self.vcs_diff(DiffInput {
7883 from: args[0].clone(),
7884 to: args[1].clone(),
7885 collection: None,
7886 summary_only: false,
7887 })?;
7888 let mut result = UnifiedResult::with_columns(vec![
7889 "from".into(),
7890 "to".into(),
7891 "collection".into(),
7892 "entity_id".into(),
7893 "change".into(),
7894 "before".into(),
7895 "after".into(),
7896 ]);
7897 for entry in diff.entries {
7898 let (change, before, after) = match entry.change {
7899 DiffChange::Added { after } => ("added", Value::Null, json_runtime_value(after)),
7900 DiffChange::Removed { before } => {
7901 ("removed", json_runtime_value(before), Value::Null)
7902 }
7903 DiffChange::Modified { before, after } => (
7904 "modified",
7905 json_runtime_value(before),
7906 json_runtime_value(after),
7907 ),
7908 };
7909 let mut record = UnifiedRecord::new();
7910 record.set("from", Value::text(diff.from.clone()));
7911 record.set("to", Value::text(diff.to.clone()));
7912 record.set("collection", Value::text(entry.collection));
7913 record.set("entity_id", Value::text(entry.entity_id));
7914 record.set("change", Value::text(change));
7915 record.set("before", before);
7916 record.set("after", after);
7917 result.push(record);
7918 }
7919 Ok(result)
7920 }
7921
7922 fn execute_inline_graph_function(
7932 &self,
7933 name: &str,
7934 nodes_query: &QueryExpr,
7935 edges_query: &QueryExpr,
7936 named_args: &[(String, f64)],
7937 ) -> RedDBResult<crate::storage::query::unified::UnifiedResult> {
7938 if !is_graph_tvf_name(name) {
7939 return Err(RedDBError::Query(format!("unknown table function: {name}")));
7940 }
7941
7942 let node_result = self.execute_query_expr(nodes_query.clone())?.result;
7943 let nodes = inline_node_ids(name, &node_result)?;
7944
7945 let edge_result = self.execute_query_expr(edges_query.clone())?.result;
7946 let edges = inline_edges(name, &edge_result)?;
7947
7948 self.dispatch_graph_algorithm(name, nodes, edges, named_args)
7949 }
7950
7951 fn materialize_whole_graph_abstract(
7954 &self,
7955 ) -> RedDBResult<(
7956 Vec<String>,
7957 Vec<(
7958 String,
7959 String,
7960 crate::storage::engine::graph_algorithms::Weight,
7961 )>,
7962 )> {
7963 use crate::storage::engine::graph_algorithms;
7964
7965 let graph = super::graph_dsl::materialize_graph_with_projection(
7966 self.inner.db.store().as_ref(),
7967 None,
7968 )?;
7969 let nodes: Vec<String> = graph.iter_nodes().map(|n| n.id.clone()).collect();
7970 let edges: Vec<(String, String, graph_algorithms::Weight)> = graph
7971 .iter_all_edges()
7972 .into_iter()
7973 .map(|e| (e.source_id, e.target_id, e.weight))
7974 .collect();
7975 Ok((nodes, edges))
7976 }
7977
7978 fn try_resolve_analytics_view(
7993 &self,
7994 table: &TableQuery,
7995 frame: &dyn super::statement_frame::ReadFrame,
7996 ) -> RedDBResult<Option<crate::storage::query::unified::UnifiedResult>> {
7997 let full = table.table.as_str();
7998 let Some(dot) = full.rfind('.') else {
7999 return Ok(None);
8000 };
8001 if self.inner.db.store().get_collection(full).is_some() {
8003 return Ok(None);
8004 }
8005 let graph_name = &full[..dot];
8006 let output_name = &full[dot + 1..];
8007 let Some(output) = crate::catalog::AnalyticsOutput::from_str(output_name) else {
8008 return Ok(None);
8009 };
8010
8011 let contracts = self.inner.db.collection_contracts();
8012 let Some(contract) = contracts.iter().find(|c| c.name == graph_name) else {
8013 return Ok(None);
8014 };
8015 if contract.declared_model != crate::catalog::CollectionModel::Graph {
8016 return Ok(None);
8017 }
8018 let Some(view) = contract
8019 .analytics_config
8020 .iter()
8021 .find(|view| view.output == output)
8022 else {
8023 return Err(RedDBError::Query(format!(
8026 "analytics output '{output_name}' is not enabled on graph '{graph_name}'; declare it with WITH ANALYTICS (...)"
8027 )));
8028 };
8029
8030 let parent_query = TableQuery::new(graph_name);
8034 if self
8035 .authorize_relational_table_select(parent_query, frame)?
8036 .is_none()
8037 {
8038 return Err(RedDBError::Query(format!(
8039 "permission denied: policy on graph '{graph_name}' denies analytics view '{output_name}'"
8040 )));
8041 }
8042
8043 let (algorithm, named_args) = analytics_view_algorithm(graph_name, view)?;
8044 let (nodes, edges) = self.materialize_whole_graph_abstract()?;
8045 let result = self.dispatch_graph_algorithm(&algorithm, nodes, edges, &named_args)?;
8046 Ok(Some(result))
8047 }
8048
8049 fn dispatch_graph_algorithm(
8056 &self,
8057 name: &str,
8058 nodes: Vec<String>,
8059 edges: Vec<(
8060 String,
8061 String,
8062 crate::storage::engine::graph_algorithms::Weight,
8063 )>,
8064 named_args: &[(String, f64)],
8065 ) -> RedDBResult<crate::storage::query::unified::UnifiedResult> {
8066 use crate::storage::engine::graph_algorithms;
8067 use crate::storage::query::unified::UnifiedResult;
8068 use crate::storage::schema::Value;
8069
8070 if name.eq_ignore_ascii_case("components") {
8071 reject_named_args(name, named_args)?;
8072 let assignment = graph_algorithms::connected_components(&nodes, &edges);
8073 let mut result =
8074 UnifiedResult::with_columns(vec!["node_id".into(), "island_id".into()]);
8075 for (node_id, island_id) in assignment {
8076 let mut record = UnifiedRecord::new();
8077 record.set("node_id", Value::text(node_id));
8078 record.set("island_id", Value::Integer(island_id as i64));
8079 result.push(record);
8080 }
8081 return Ok(result);
8082 }
8083
8084 if name.eq_ignore_ascii_case("louvain") {
8085 let resolution = louvain_resolution(named_args)?;
8090 let assignment = graph_algorithms::louvain(&nodes, &edges, resolution);
8091 let mut result =
8092 UnifiedResult::with_columns(vec!["node_id".into(), "community_id".into()]);
8093 for (node_id, community_id) in assignment {
8094 let mut record = UnifiedRecord::new();
8095 record.set("node_id", Value::text(node_id));
8096 record.set("community_id", Value::Integer(community_id as i64));
8097 result.push(record);
8098 }
8099 return Ok(result);
8100 }
8101
8102 if name.eq_ignore_ascii_case("degree_centrality") {
8103 reject_named_args(name, named_args)?;
8104 let assignment = abstract_degree_centrality(&nodes, &edges);
8105 let mut result = UnifiedResult::with_columns(vec!["node_id".into(), "degree".into()]);
8106 for (node_id, degree) in assignment {
8107 let mut record = UnifiedRecord::new();
8108 record.set("node_id", Value::text(node_id));
8109 record.set("degree", Value::Integer(degree as i64));
8110 result.push(record);
8111 }
8112 return Ok(result);
8113 }
8114
8115 if name.eq_ignore_ascii_case("shortest_path") {
8116 let mut src: Option<String> = None;
8122 let mut dst: Option<String> = None;
8123 let mut max_hops: Option<usize> = None;
8124 let as_node_id = |key: &str, value: f64| -> RedDBResult<String> {
8125 if !value.is_finite() || value < 0.0 || value.fract() != 0.0 {
8126 return Err(RedDBError::Query(format!(
8127 "table function 'shortest_path' argument '{key}' must be a non-negative integer node id, got {value}"
8128 )));
8129 }
8130 Ok((value as i64).to_string())
8131 };
8132 for (key, value) in named_args {
8133 if key.eq_ignore_ascii_case("src") {
8134 src = Some(as_node_id("src", *value)?);
8135 } else if key.eq_ignore_ascii_case("dst") {
8136 dst = Some(as_node_id("dst", *value)?);
8137 } else if key.eq_ignore_ascii_case("max_hops") {
8138 if !value.is_finite() || *value < 0.0 || value.fract() != 0.0 {
8139 return Err(RedDBError::Query(format!(
8140 "table function 'shortest_path' max_hops must be a non-negative integer, got {value}"
8141 )));
8142 }
8143 max_hops = Some(*value as usize);
8144 } else {
8145 return Err(RedDBError::Query(format!(
8146 "table function 'shortest_path' has no named argument '{key}' (expected 'src', 'dst', 'max_hops')"
8147 )));
8148 }
8149 }
8150 let src = src.ok_or_else(|| {
8151 RedDBError::Query(
8152 "table function 'shortest_path' requires named argument 'src'".to_string(),
8153 )
8154 })?;
8155 let dst = dst.ok_or_else(|| {
8156 RedDBError::Query(
8157 "table function 'shortest_path' requires named argument 'dst'".to_string(),
8158 )
8159 })?;
8160
8161 let mut result = UnifiedResult::with_columns(vec![
8168 "hop".into(),
8169 "node_id".into(),
8170 "cumulative_weight".into(),
8171 ]);
8172 if let Some(path) =
8173 graph_algorithms::shortest_path(&nodes, &edges, &src, &dst, max_hops)
8174 {
8175 for (hop, (node_id, cumulative_weight)) in path.into_iter().enumerate() {
8176 let mut record = UnifiedRecord::new();
8177 record.set("hop", Value::Integer(hop as i64));
8178 record.set("node_id", Value::text(node_id));
8179 record.set("cumulative_weight", Value::Float(cumulative_weight));
8180 result.push(record);
8181 }
8182 }
8183 return Ok(result);
8184 }
8185 if name.eq_ignore_ascii_case("betweenness") {
8190 reject_named_args(name, named_args)?;
8191 return Ok(Self::centrality_result(graph_algorithms::betweenness(
8192 &nodes, &edges,
8193 )));
8194 }
8195 if name.eq_ignore_ascii_case("eigenvector") {
8196 let mut max_iterations = 100_usize;
8199 let mut tolerance = 1e-6_f64;
8200 for (key, value) in named_args {
8201 if key.eq_ignore_ascii_case("max_iterations") {
8202 max_iterations = parse_positive_iterations("eigenvector", value)?;
8203 } else if key.eq_ignore_ascii_case("tolerance") {
8204 if !value.is_finite() || *value <= 0.0 {
8205 return Err(RedDBError::Query(format!(
8206 "table function 'eigenvector' tolerance must be > 0, got {value}"
8207 )));
8208 }
8209 tolerance = *value;
8210 } else {
8211 return Err(RedDBError::Query(format!(
8212 "table function 'eigenvector' has no named argument '{key}' (expected 'max_iterations' or 'tolerance')"
8213 )));
8214 }
8215 }
8216 return Ok(Self::centrality_result(graph_algorithms::eigenvector(
8217 &nodes,
8218 &edges,
8219 max_iterations,
8220 tolerance,
8221 )));
8222 }
8223 if name.eq_ignore_ascii_case("pagerank") {
8224 let mut damping = 0.85_f64;
8227 let mut max_iterations = 100_usize;
8228 for (key, value) in named_args {
8229 if key.eq_ignore_ascii_case("damping") {
8230 if !value.is_finite() || *value <= 0.0 || *value >= 1.0 {
8231 return Err(RedDBError::Query(format!(
8232 "table function 'pagerank' damping must be in (0, 1), got {value}"
8233 )));
8234 }
8235 damping = *value;
8236 } else if key.eq_ignore_ascii_case("max_iterations") {
8237 max_iterations = parse_positive_iterations("pagerank", value)?;
8238 } else {
8239 return Err(RedDBError::Query(format!(
8240 "table function 'pagerank' has no named argument '{key}' (expected 'damping' or 'max_iterations')"
8241 )));
8242 }
8243 }
8244 return Ok(Self::centrality_result(graph_algorithms::pagerank(
8245 &nodes,
8246 &edges,
8247 damping,
8248 max_iterations,
8249 )));
8250 }
8251 Err(RedDBError::Query(format!("unknown table function: {name}")))
8252 }
8253
8254 fn execute_components_tvf(
8261 &self,
8262 _collection: &str,
8263 ) -> RedDBResult<crate::storage::query::unified::UnifiedResult> {
8264 use crate::storage::engine::graph_algorithms;
8265 use crate::storage::query::unified::UnifiedResult;
8266 use crate::storage::schema::Value;
8267
8268 let graph = super::graph_dsl::materialize_graph_with_projection(
8274 self.inner.db.store().as_ref(),
8275 None,
8276 )?;
8277
8278 let nodes: Vec<String> = graph.iter_nodes().map(|n| n.id.clone()).collect();
8280 let edges: Vec<(String, String, graph_algorithms::Weight)> = graph
8281 .iter_all_edges()
8282 .into_iter()
8283 .map(|e| (e.source_id, e.target_id, e.weight))
8284 .collect();
8285
8286 let assignment = graph_algorithms::connected_components(&nodes, &edges);
8287
8288 let mut result = UnifiedResult::with_columns(vec!["node_id".into(), "island_id".into()]);
8290 for (node_id, island_id) in assignment {
8291 let mut record = UnifiedRecord::new();
8292 record.set("node_id", Value::text(node_id));
8293 record.set("island_id", Value::Integer(island_id as i64));
8294 result.push(record);
8295 }
8296 Ok(result)
8297 }
8298
8299 fn execute_louvain_tvf(
8309 &self,
8310 _collection: &str,
8311 resolution: f64,
8312 ) -> RedDBResult<crate::storage::query::unified::UnifiedResult> {
8313 use crate::storage::engine::graph_algorithms;
8314 use crate::storage::query::unified::UnifiedResult;
8315 use crate::storage::schema::Value;
8316
8317 let graph = super::graph_dsl::materialize_graph_with_projection(
8318 self.inner.db.store().as_ref(),
8319 None,
8320 )?;
8321
8322 let nodes: Vec<String> = graph.iter_nodes().map(|n| n.id.clone()).collect();
8323 let edges: Vec<(String, String, graph_algorithms::Weight)> = graph
8324 .iter_all_edges()
8325 .into_iter()
8326 .map(|e| (e.source_id, e.target_id, e.weight))
8327 .collect();
8328
8329 let assignment = graph_algorithms::louvain(&nodes, &edges, resolution);
8330
8331 let mut result = UnifiedResult::with_columns(vec!["node_id".into(), "community_id".into()]);
8333 for (node_id, community_id) in assignment {
8334 let mut record = UnifiedRecord::new();
8335 record.set("node_id", Value::text(node_id));
8336 record.set("community_id", Value::Integer(community_id as i64));
8337 result.push(record);
8338 }
8339 Ok(result)
8340 }
8341
8342 fn centrality_result(
8345 rows: Vec<(String, f64)>,
8346 ) -> crate::storage::query::unified::UnifiedResult {
8347 use crate::storage::query::unified::UnifiedResult;
8348 use crate::storage::schema::Value;
8349 let mut result = UnifiedResult::with_columns(vec!["node_id".into(), "score".into()]);
8350 for (node_id, score) in rows {
8351 let mut record = UnifiedRecord::new();
8352 record.set("node_id", Value::text(node_id));
8353 record.set("score", Value::Float(score));
8354 result.push(record);
8355 }
8356 result
8357 }
8358
8359 fn try_fast_entity_lookup(&self, query: &str) -> Option<RedDBResult<RuntimeQueryResult>> {
8362 let q = query.trim();
8365 if !q.starts_with("SELECT") && !q.starts_with("select") {
8366 return None;
8367 }
8368
8369 let where_pos = q
8371 .find("WHERE _entity_id")
8372 .or_else(|| q.find("where _entity_id"))?;
8373 let after_field = &q[where_pos + 16..].trim_start(); let after_eq = after_field.strip_prefix('=')?.trim_start();
8375
8376 let id_str = after_eq.trim();
8378 let entity_id: u64 = id_str.parse().ok()?;
8379
8380 let from_pos = q.find("FROM ").or_else(|| q.find("from "))? + 5;
8382 let table = q[from_pos..where_pos].trim();
8383 if table.is_empty()
8384 || table.contains(' ') && !table.contains(" AS ") && !table.contains(" as ")
8385 {
8386 return None; }
8388 let table_name = table.split_whitespace().next()?;
8389
8390 let store = self.inner.db.store();
8396 let entity = store
8397 .get(
8398 table_name,
8399 crate::storage::unified::EntityId::new(entity_id),
8400 )
8401 .filter(entity_visible_under_current_snapshot)
8402 .filter(|entity| {
8403 self.inner
8404 .db
8405 .replica_allows_entity_at_read(table_name, entity)
8406 });
8407
8408 let count = if entity.is_some() { 1u64 } else { 0 };
8409
8410 let records: Vec<crate::storage::query::unified::UnifiedRecord> = entity
8416 .as_ref()
8417 .and_then(|e| runtime_table_record_from_entity(e.clone()))
8418 .into_iter()
8419 .collect();
8420
8421 let json = match entity {
8422 Some(ref e) => execute_runtime_serialize_single_entity(e),
8423 None => r#"{"columns":[],"record_count":0,"selection":{"scope":"any"},"records":[]}"#
8424 .to_string(),
8425 };
8426
8427 Some(Ok(RuntimeQueryResult {
8428 query: query.to_string(),
8429 mode: crate::storage::query::modes::QueryMode::Sql,
8430 statement: "select",
8431 engine: "fast-entity-lookup",
8432 result: crate::storage::query::unified::UnifiedResult {
8433 columns: Vec::new(),
8434 records,
8435 stats: crate::storage::query::unified::QueryStats {
8436 rows_scanned: count,
8437 ..Default::default()
8438 },
8439 pre_serialized_json: Some(json),
8440 },
8441 affected_rows: 0,
8442 statement_type: "select",
8443 bookmark: None,
8444 }))
8445 }
8446
8447 pub(crate) fn invalidate_plan_cache(&self) {
8448 self.inner.query_cache.write().clear();
8449 self.inner
8450 .ddl_epoch
8451 .fetch_add(1, std::sync::atomic::Ordering::Release);
8452 }
8453
8454 pub fn ddl_epoch(&self) -> u64 {
8458 self.inner
8459 .ddl_epoch
8460 .load(std::sync::atomic::Ordering::Acquire)
8461 }
8462
8463 pub(crate) fn clear_table_planner_stats(&self, table: &str) {
8464 let store = self.inner.db.store();
8465 crate::storage::query::planner::stats_catalog::clear_table_stats(store.as_ref(), table);
8466 self.invalidate_plan_cache();
8467 }
8468
8469 pub(crate) fn rehydrate_tenant_tables(&self) {
8478 let store = self.inner.db.store();
8479 let Some(manager) = store.get_collection("red_config") else {
8480 return;
8481 };
8482 for entity in manager.query_all(|_| true) {
8487 let crate::storage::unified::entity::EntityData::Row(row) = &entity.data else {
8488 continue;
8489 };
8490 let Some(named) = &row.named else { continue };
8491 let Some(crate::storage::schema::Value::Text(key)) = named.get("key") else {
8492 continue;
8493 };
8494 let Some(rest) = key.strip_prefix("tenant_tables.") else {
8496 continue;
8497 };
8498 let Some((table, suffix)) = rest.rsplit_once('.') else {
8499 crate::telemetry::operator_event::OperatorEvent::SchemaCorruption {
8505 collection: "red_config".to_string(),
8506 detail: format!("malformed tenant_tables key: {key}"),
8507 }
8508 .emit_global();
8509 continue;
8510 };
8511 if suffix != "column" {
8512 crate::telemetry::operator_event::OperatorEvent::SchemaCorruption {
8513 collection: "red_config".to_string(),
8514 detail: format!("unexpected tenant_tables suffix: {key}"),
8515 }
8516 .emit_global();
8517 continue;
8518 }
8519 match named.get("value") {
8520 Some(crate::storage::schema::Value::Text(column)) => {
8521 self.register_tenant_table(table, column);
8522 }
8523 Some(crate::storage::schema::Value::Null) | None => {
8525 self.unregister_tenant_table(table);
8526 }
8527 _ => {}
8528 }
8529 }
8530 }
8531
8532 pub(crate) fn rehydrate_materialized_view_descriptors(&self) {
8544 let store = self.inner.db.store();
8545 let descriptors = crate::runtime::continuous_materialized_view::load_all(store.as_ref());
8546 for descriptor in descriptors {
8547 let parsed = match crate::storage::query::parser::parse(&descriptor.source_sql) {
8548 Ok(qc) => qc,
8549 Err(err) => {
8550 crate::telemetry::operator_event::OperatorEvent::SchemaCorruption {
8551 collection:
8552 crate::runtime::continuous_materialized_view::CATALOG_COLLECTION
8553 .to_string(),
8554 detail: format!(
8555 "failed to re-parse materialized-view source for {}: {err}",
8556 descriptor.name
8557 ),
8558 }
8559 .emit_global();
8560 continue;
8561 }
8562 };
8563 let crate::storage::query::ast::QueryExpr::CreateView(create) = parsed.query else {
8564 crate::telemetry::operator_event::OperatorEvent::SchemaCorruption {
8565 collection: crate::runtime::continuous_materialized_view::CATALOG_COLLECTION
8566 .to_string(),
8567 detail: format!(
8568 "materialized-view source for {} did not re-parse as CREATE VIEW",
8569 descriptor.name
8570 ),
8571 }
8572 .emit_global();
8573 continue;
8574 };
8575 let view_name = create.name.clone();
8577 self.inner
8578 .views
8579 .write()
8580 .insert(view_name.clone(), Arc::new(create));
8581 use crate::storage::cache::result::{MaterializedViewDef, RefreshPolicy};
8583 let refresh = match descriptor.refresh_every_ms {
8584 Some(ms) => RefreshPolicy::Periodic(std::time::Duration::from_millis(ms)),
8585 None => RefreshPolicy::Manual,
8586 };
8587 let def = MaterializedViewDef {
8588 name: view_name.clone(),
8589 query: format!("<parsed view {}>", view_name),
8590 dependencies: descriptor.source_collections.clone(),
8591 refresh,
8592 retention_duration_ms: descriptor.retention_duration_ms,
8593 };
8594 self.inner.materialized_views.write().register(def);
8595 if let Err(err) = self.ensure_materialized_view_backing(&view_name) {
8596 crate::telemetry::operator_event::OperatorEvent::SchemaCorruption {
8597 collection: crate::runtime::continuous_materialized_view::CATALOG_COLLECTION
8598 .to_string(),
8599 detail: format!(
8600 "failed to rehydrate backing collection for materialized view {view_name}: {err}"
8601 ),
8602 }
8603 .emit_global();
8604 }
8605 }
8606 self.invalidate_plan_cache();
8609 }
8610
8611 pub(crate) fn rehydrate_declared_column_schemas(&self) {
8612 let store = self.inner.db.store();
8613 for contract in self.inner.db.collection_contracts() {
8614 let columns: Vec<String> = contract
8615 .declared_columns
8616 .iter()
8617 .map(|column| column.name.clone())
8618 .collect();
8619 let Some(manager) = store.get_collection(&contract.name) else {
8620 continue;
8621 };
8622 manager.set_column_schema_if_empty(columns);
8623 }
8624 }
8625
8626 pub fn register_tenant_table(&self, table: &str, column: &str) {
8631 use crate::storage::query::ast::{
8632 CompareOp, CreatePolicyQuery, Expr, FieldRef, Filter, Span,
8633 };
8634 self.inner
8635 .tenant_tables
8636 .write()
8637 .insert(table.to_string(), column.to_string());
8638
8639 let lhs = Expr::Column {
8645 field: FieldRef::TableColumn {
8646 table: table.to_string(),
8647 column: column.to_string(),
8648 },
8649 span: Span::synthetic(),
8650 };
8651 let rhs = Expr::FunctionCall {
8652 name: "CURRENT_TENANT".to_string(),
8653 args: Vec::new(),
8654 span: Span::synthetic(),
8655 };
8656 let policy_filter = Filter::CompareExpr {
8657 lhs,
8658 op: CompareOp::Eq,
8659 rhs,
8660 };
8661
8662 let policy = CreatePolicyQuery {
8663 name: "__tenant_iso".to_string(),
8664 table: table.to_string(),
8665 action: None, role: None, using: Box::new(policy_filter),
8668 target_kind: crate::storage::query::ast::PolicyTargetKind::Table,
8675 };
8676
8677 self.inner.rls_policies.write().insert(
8679 (table.to_string(), "__tenant_iso".to_string()),
8680 Arc::new(policy),
8681 );
8682 self.inner
8683 .rls_enabled_tables
8684 .write()
8685 .insert(table.to_string());
8686
8687 self.ensure_tenant_index(table, column);
8693 }
8694
8695 fn ensure_tenant_index(&self, table: &str, column: &str) {
8703 if column.contains('.') {
8704 return;
8705 }
8706 let index_name = format!("__tenant_idx_{table}");
8707 let registry = self.inner.index_store.list_indices(table);
8708 if registry.iter().any(|idx| idx.name == index_name) {
8709 return;
8710 }
8711 if registry
8712 .iter()
8713 .any(|idx| idx.columns.first().map(|c| c.as_str()) == Some(column))
8714 {
8715 return;
8716 }
8717
8718 let store = self.inner.db.store();
8719 let Some(manager) = store.get_collection(table) else {
8720 return;
8721 };
8722 let entities = manager.query_all(|_| true);
8723 let entity_fields: Vec<(
8724 crate::storage::unified::EntityId,
8725 Vec<(String, crate::storage::schema::Value)>,
8726 )> = entities
8727 .iter()
8728 .map(|e| {
8729 let fields = match &e.data {
8730 crate::storage::EntityData::Row(row) => {
8731 if let Some(ref named) = row.named {
8732 named.iter().map(|(k, v)| (k.clone(), v.clone())).collect()
8733 } else if let Some(ref schema) = row.schema {
8734 schema
8735 .iter()
8736 .zip(row.columns.iter())
8737 .map(|(k, v)| (k.clone(), v.clone()))
8738 .collect()
8739 } else {
8740 Vec::new()
8741 }
8742 }
8743 crate::storage::EntityData::Node(node) => node
8744 .properties
8745 .iter()
8746 .map(|(k, v)| (k.clone(), v.clone()))
8747 .collect(),
8748 _ => Vec::new(),
8749 };
8750 (e.id, fields)
8751 })
8752 .collect();
8753
8754 let columns = vec![column.to_string()];
8755 if self
8756 .inner
8757 .index_store
8758 .create_index(
8759 &index_name,
8760 table,
8761 &columns,
8762 super::index_store::IndexMethodKind::Hash,
8763 false,
8764 &entity_fields,
8765 )
8766 .is_err()
8767 {
8768 return;
8769 }
8770 self.inner
8771 .index_store
8772 .register(super::index_store::RegisteredIndex {
8773 name: index_name,
8774 collection: table.to_string(),
8775 columns,
8776 method: super::index_store::IndexMethodKind::Hash,
8777 unique: false,
8778 });
8779 self.invalidate_plan_cache();
8780 }
8781
8782 fn drop_tenant_index(&self, table: &str) {
8785 let index_name = format!("__tenant_idx_{table}");
8786 self.inner.index_store.drop_index(&index_name, table);
8787 }
8788
8789 pub fn tenant_column(&self, table: &str) -> Option<String> {
8793 self.inner.tenant_tables.read().get(table).cloned()
8794 }
8795
8796 pub fn unregister_tenant_table(&self, table: &str) {
8800 self.inner.tenant_tables.write().remove(table);
8801 self.inner
8802 .rls_policies
8803 .write()
8804 .remove(&(table.to_string(), "__tenant_iso".to_string()));
8805 self.drop_tenant_index(table);
8806 let has_other_policies = self
8808 .inner
8809 .rls_policies
8810 .read()
8811 .keys()
8812 .any(|(t, _)| t == table);
8813 if !has_other_policies {
8814 self.inner.rls_enabled_tables.write().remove(table);
8815 }
8816 }
8817
8818 pub(crate) fn record_pending_tombstone(
8824 &self,
8825 conn_id: u64,
8826 collection: &str,
8827 id: crate::storage::unified::entity::EntityId,
8828 stamper_xid: crate::storage::transaction::snapshot::Xid,
8829 previous_xmax: crate::storage::transaction::snapshot::Xid,
8830 ) {
8831 self.inner
8832 .pending_tombstones
8833 .write()
8834 .entry(conn_id)
8835 .or_default()
8836 .push((collection.to_string(), id, stamper_xid, previous_xmax));
8837 }
8838
8839 pub(crate) fn record_pending_versioned_update(
8840 &self,
8841 conn_id: u64,
8842 collection: &str,
8843 old_id: crate::storage::unified::entity::EntityId,
8844 new_id: crate::storage::unified::entity::EntityId,
8845 stamper_xid: crate::storage::transaction::snapshot::Xid,
8846 previous_xmax: crate::storage::transaction::snapshot::Xid,
8847 ) {
8848 self.inner
8849 .pending_versioned_updates
8850 .write()
8851 .entry(conn_id)
8852 .or_default()
8853 .push((
8854 collection.to_string(),
8855 old_id,
8856 new_id,
8857 stamper_xid,
8858 previous_xmax,
8859 ));
8860 }
8861
8862 fn with_deferred_store_wal_if_transaction<T>(
8863 &self,
8864 f: impl FnOnce() -> RedDBResult<T>,
8865 ) -> RedDBResult<T> {
8866 let conn_id = current_connection_id();
8867 if !self.inner.tx_contexts.read().contains_key(&conn_id) {
8868 return f();
8869 }
8870
8871 crate::storage::UnifiedStore::begin_deferred_store_wal_capture();
8872 let result = f();
8873 let captured = crate::storage::UnifiedStore::take_deferred_store_wal_capture();
8874 match result {
8875 Ok(value) => {
8876 self.record_pending_store_wal_actions(conn_id, captured);
8877 Ok(value)
8878 }
8879 Err(err) => Err(err),
8880 }
8881 }
8882
8883 fn with_deferred_store_wal_for_dml<T>(
8884 &self,
8885 capture_autocommit_events: bool,
8886 f: impl FnOnce() -> RedDBResult<T>,
8887 ) -> RedDBResult<T> {
8888 let conn_id = current_connection_id();
8889 if self.inner.tx_contexts.read().contains_key(&conn_id) {
8890 return self.with_deferred_store_wal_if_transaction(f);
8891 }
8892 if !capture_autocommit_events {
8893 return f();
8894 }
8895
8896 crate::storage::UnifiedStore::begin_deferred_store_wal_capture();
8897 let result = f();
8898 let captured = crate::storage::UnifiedStore::take_deferred_store_wal_capture();
8899 self.inner
8900 .db
8901 .store()
8902 .append_deferred_store_wal_actions(captured)
8903 .map_err(|err| RedDBError::Internal(err.to_string()))?;
8904 result
8905 }
8906
8907 fn insert_may_emit_events(&self, query: &InsertQuery) -> bool {
8908 !query.suppress_events
8909 && self.collection_has_event_subscriptions_for_operation(
8910 &query.table,
8911 crate::catalog::SubscriptionOperation::Insert,
8912 )
8913 }
8914
8915 fn update_may_emit_events(&self, query: &UpdateQuery) -> bool {
8916 !query.suppress_events
8917 && self.collection_has_event_subscriptions_for_operation(
8918 &query.table,
8919 crate::catalog::SubscriptionOperation::Update,
8920 )
8921 }
8922
8923 fn delete_may_emit_events(&self, query: &DeleteQuery) -> bool {
8924 !query.suppress_events
8925 && self.collection_has_event_subscriptions_for_operation(
8926 &query.table,
8927 crate::catalog::SubscriptionOperation::Delete,
8928 )
8929 }
8930
8931 fn collection_has_event_subscriptions_for_operation(
8932 &self,
8933 collection: &str,
8934 operation: crate::catalog::SubscriptionOperation,
8935 ) -> bool {
8936 let Some(contract) = self.db().collection_contract_arc(collection) else {
8937 return false;
8938 };
8939 contract.subscriptions.iter().any(|subscription| {
8940 subscription.enabled
8941 && (subscription.ops_filter.is_empty()
8942 || subscription.ops_filter.contains(&operation))
8943 })
8944 }
8945
8946 fn record_pending_store_wal_actions(
8947 &self,
8948 conn_id: u64,
8949 actions: crate::storage::unified::DeferredStoreWalActions,
8950 ) {
8951 if actions.is_empty() {
8952 return;
8953 }
8954 let mut guard = self.inner.pending_store_wal_actions.write();
8955 guard.entry(conn_id).or_default().extend(actions);
8956 }
8957
8958 fn flush_pending_store_wal_actions(&self, conn_id: u64) -> RedDBResult<()> {
8959 let Some(actions) = self
8960 .inner
8961 .pending_store_wal_actions
8962 .write()
8963 .remove(&conn_id)
8964 else {
8965 return Ok(());
8966 };
8967 self.inner
8968 .db
8969 .store()
8970 .append_deferred_store_wal_actions(actions)
8971 .map_err(|err| RedDBError::Internal(err.to_string()))
8972 }
8973
8974 fn discard_pending_store_wal_actions(&self, conn_id: u64) {
8975 self.inner
8976 .pending_store_wal_actions
8977 .write()
8978 .remove(&conn_id);
8979 }
8980
8981 fn xid_conflicts_with_snapshot(
8982 &self,
8983 xid: crate::storage::transaction::snapshot::Xid,
8984 snapshot: &crate::storage::transaction::snapshot::Snapshot,
8985 own_xids: &std::collections::HashSet<crate::storage::transaction::snapshot::Xid>,
8986 ) -> bool {
8987 xid != 0
8988 && !own_xids.contains(&xid)
8989 && !self.inner.snapshot_manager.is_aborted(xid)
8990 && !self.inner.snapshot_manager.is_active(xid)
8991 && (xid > snapshot.xid || snapshot.in_progress.contains(&xid))
8992 }
8993
8994 fn conflict_error(
8995 collection: &str,
8996 logical_id: crate::storage::unified::entity::EntityId,
8997 xid: crate::storage::transaction::snapshot::Xid,
8998 ) -> RedDBError {
8999 RedDBError::Query(format!(
9000 "serialization conflict: table row {collection}/{} was modified by concurrent transaction {xid}",
9001 logical_id.raw()
9002 ))
9003 }
9004
9005 fn check_logical_row_conflict(
9006 &self,
9007 collection: &str,
9008 logical_id: crate::storage::unified::entity::EntityId,
9009 excluded_ids: &[crate::storage::unified::entity::EntityId],
9010 snapshot: &crate::storage::transaction::snapshot::Snapshot,
9011 own_xids: &std::collections::HashSet<crate::storage::transaction::snapshot::Xid>,
9012 ) -> RedDBResult<()> {
9013 let store = self.inner.db.store();
9014 let Some(manager) = store.get_collection(collection) else {
9015 return Ok(());
9016 };
9017
9018 for candidate in manager.query_all(|_| true) {
9019 if excluded_ids.contains(&candidate.id) || candidate.logical_id() != logical_id {
9020 continue;
9021 }
9022 if self.xid_conflicts_with_snapshot(candidate.xmin, snapshot, own_xids) {
9023 return Err(Self::conflict_error(collection, logical_id, candidate.xmin));
9024 }
9025 if self.xid_conflicts_with_snapshot(candidate.xmax, snapshot, own_xids) {
9026 return Err(Self::conflict_error(collection, logical_id, candidate.xmax));
9027 }
9028 }
9029 Ok(())
9030 }
9031
9032 pub(crate) fn check_table_row_write_conflicts(
9033 &self,
9034 conn_id: u64,
9035 snapshot: &crate::storage::transaction::snapshot::Snapshot,
9036 own_xids: &std::collections::HashSet<crate::storage::transaction::snapshot::Xid>,
9037 ) -> RedDBResult<()> {
9038 let versioned_updates = self
9039 .inner
9040 .pending_versioned_updates
9041 .read()
9042 .get(&conn_id)
9043 .cloned()
9044 .unwrap_or_default();
9045 let tombstones = self
9046 .inner
9047 .pending_tombstones
9048 .read()
9049 .get(&conn_id)
9050 .cloned()
9051 .unwrap_or_default();
9052
9053 let store = self.inner.db.store();
9054 for (collection, old_id, new_id, xid, previous_xmax) in versioned_updates {
9055 let Some(manager) = store.get_collection(&collection) else {
9056 continue;
9057 };
9058 let Some(old) = manager.get(old_id) else {
9059 continue;
9060 };
9061 let logical_id = old.logical_id();
9062 if self.xid_conflicts_with_snapshot(previous_xmax, snapshot, own_xids) {
9063 return Err(Self::conflict_error(&collection, logical_id, previous_xmax));
9064 }
9065 if old.xmax != xid && self.xid_conflicts_with_snapshot(old.xmax, snapshot, own_xids) {
9066 return Err(Self::conflict_error(&collection, logical_id, old.xmax));
9067 }
9068 self.check_logical_row_conflict(
9069 &collection,
9070 logical_id,
9071 &[old_id, new_id],
9072 snapshot,
9073 own_xids,
9074 )?;
9075 }
9076
9077 for (collection, id, xid, previous_xmax) in tombstones {
9078 let Some(manager) = store.get_collection(&collection) else {
9079 continue;
9080 };
9081 let Some(entity) = manager.get(id) else {
9082 continue;
9083 };
9084 let logical_id = entity.logical_id();
9085 if self.xid_conflicts_with_snapshot(previous_xmax, snapshot, own_xids) {
9086 return Err(Self::conflict_error(&collection, logical_id, previous_xmax));
9087 }
9088 if entity.xmax != xid
9089 && self.xid_conflicts_with_snapshot(entity.xmax, snapshot, own_xids)
9090 {
9091 return Err(Self::conflict_error(&collection, logical_id, entity.xmax));
9092 }
9093 self.check_logical_row_conflict(&collection, logical_id, &[id], snapshot, own_xids)?;
9094 }
9095
9096 Ok(())
9097 }
9098
9099 pub(crate) fn restore_pending_write_stamps(&self, conn_id: u64) {
9100 let versioned_updates = self
9101 .inner
9102 .pending_versioned_updates
9103 .read()
9104 .get(&conn_id)
9105 .cloned()
9106 .unwrap_or_default();
9107 let tombstones = self
9108 .inner
9109 .pending_tombstones
9110 .read()
9111 .get(&conn_id)
9112 .cloned()
9113 .unwrap_or_default();
9114
9115 let store = self.inner.db.store();
9116 for (collection, old_id, _new_id, xid, _previous_xmax) in versioned_updates {
9117 if let Some(manager) = store.get_collection(&collection) {
9118 if let Some(mut entity) = manager.get(old_id) {
9119 entity.set_xmax(xid);
9120 let _ = manager.update(entity);
9121 }
9122 }
9123 }
9124 for (collection, id, xid, _previous_xmax) in tombstones {
9125 if let Some(manager) = store.get_collection(&collection) {
9126 if let Some(mut entity) = manager.get(id) {
9127 entity.set_xmax(xid);
9128 let _ = manager.update(entity);
9129 }
9130 }
9131 }
9132 }
9133
9134 pub(crate) fn finalize_pending_versioned_updates(&self, conn_id: u64) {
9135 self.inner
9136 .pending_versioned_updates
9137 .write()
9138 .remove(&conn_id);
9139 }
9140
9141 pub(crate) fn revive_pending_versioned_updates(&self, conn_id: u64) {
9142 let Some(pending) = self
9143 .inner
9144 .pending_versioned_updates
9145 .write()
9146 .remove(&conn_id)
9147 else {
9148 return;
9149 };
9150
9151 let store = self.inner.db.store();
9152 for (collection, old_id, new_id, xid, previous_xmax) in pending {
9153 if let Some(manager) = store.get_collection(&collection) {
9154 if let Some(mut old) = manager.get(old_id) {
9155 if old.xmax == xid {
9156 old.set_xmax(previous_xmax);
9157 let _ = manager.update(old);
9158 }
9159 }
9160 }
9161 let _ = store.delete_batch(&collection, &[new_id]);
9162 }
9163 }
9164
9165 pub(crate) fn revive_versioned_updates_since(&self, conn_id: u64, stamper_xid: u64) -> usize {
9166 let mut guard = self.inner.pending_versioned_updates.write();
9167 let Some(pending) = guard.get_mut(&conn_id) else {
9168 return 0;
9169 };
9170
9171 let store = self.inner.db.store();
9172 let mut reverted = 0usize;
9173 pending.retain(|(collection, old_id, new_id, xid, previous_xmax)| {
9174 if *xid < stamper_xid {
9175 return true;
9176 }
9177 if let Some(manager) = store.get_collection(collection) {
9178 if let Some(mut old) = manager.get(*old_id) {
9179 if old.xmax == *xid {
9180 old.set_xmax(*previous_xmax);
9181 let _ = manager.update(old);
9182 }
9183 }
9184 }
9185 let _ = store.delete_batch(collection, &[*new_id]);
9186 reverted += 1;
9187 false
9188 });
9189 if pending.is_empty() {
9190 guard.remove(&conn_id);
9191 }
9192 reverted
9193 }
9194
9195 pub(crate) fn finalize_pending_tombstones(&self, conn_id: u64) {
9200 let Some(pending) = self.inner.pending_tombstones.write().remove(&conn_id) else {
9201 return;
9202 };
9203 if pending.is_empty() {
9204 return;
9205 }
9206
9207 let store = self.inner.db.store();
9208 for (collection, id, _xid, _previous_xmax) in pending {
9209 store.context_index().remove_entity(id);
9210 self.cdc_emit(
9211 crate::replication::cdc::ChangeOperation::Delete,
9212 &collection,
9213 id.raw(),
9214 "entity",
9215 );
9216 }
9217 }
9218
9219 pub(crate) fn revive_pending_tombstones(&self, conn_id: u64) {
9226 let Some(pending) = self.inner.pending_tombstones.write().remove(&conn_id) else {
9227 return;
9228 };
9229
9230 let store = self.inner.db.store();
9231 for (collection, id, xid, previous_xmax) in pending {
9232 let Some(manager) = store.get_collection(&collection) else {
9233 continue;
9234 };
9235 if let Some(mut entity) = manager.get(id) {
9236 if entity.xmax == xid {
9237 entity.set_xmax(previous_xmax);
9238 let _ = manager.update(entity);
9239 }
9240 }
9241 }
9242 }
9243
9244 pub fn queue_wait_registry(
9246 &self,
9247 ) -> std::sync::Arc<crate::runtime::queue_wait_registry::QueueWaitRegistry> {
9248 self.inner.queue_wait_registry.clone()
9249 }
9250
9251 pub(crate) fn record_queue_wake(&self, scope: &str, queue: &str) {
9256 if self.current_xid().is_some() {
9257 let conn_id = current_connection_id();
9258 self.inner
9259 .pending_queue_wakes
9260 .write()
9261 .entry(conn_id)
9262 .or_default()
9263 .push((scope.to_string(), queue.to_string()));
9264 return;
9265 }
9266 self.inner.queue_wait_registry.notify(scope, queue);
9267 }
9268
9269 pub(crate) fn finalize_pending_queue_wakes(&self, conn_id: u64) {
9270 let Some(pending) = self.inner.pending_queue_wakes.write().remove(&conn_id) else {
9271 return;
9272 };
9273 for (scope, queue) in pending {
9274 self.inner.queue_wait_registry.notify(&scope, &queue);
9275 }
9276 }
9277
9278 pub(crate) fn discard_pending_queue_wakes(&self, conn_id: u64) {
9279 self.inner.pending_queue_wakes.write().remove(&conn_id);
9280 }
9281
9282 pub(crate) fn release_pending_claim_locks(&self, conn_id: u64) {
9283 self.inner
9284 .pending_claim_locks
9285 .write()
9286 .retain(|_, owner| *owner != conn_id);
9287 }
9288
9289 pub(crate) fn finalize_pending_kv_watch_events(&self, conn_id: u64) {
9290 let Some(pending) = self.inner.pending_kv_watch_events.write().remove(&conn_id) else {
9291 return;
9292 };
9293 for event in pending {
9294 self.cdc_emit_kv(
9295 event.op,
9296 &event.collection,
9297 &event.key,
9298 0,
9299 event.before,
9300 event.after,
9301 );
9302 }
9303 }
9304
9305 pub(crate) fn discard_pending_kv_watch_events(&self, conn_id: u64) {
9306 self.inner.pending_kv_watch_events.write().remove(&conn_id);
9307 }
9308
9309 fn materialize_graph_with_rls(
9318 &self,
9319 ) -> RedDBResult<(
9320 crate::storage::engine::GraphStore,
9321 std::collections::HashMap<
9322 String,
9323 std::collections::HashMap<String, crate::storage::schema::Value>,
9324 >,
9325 crate::storage::query::unified::EdgeProperties,
9326 )> {
9327 use crate::storage::engine::GraphStore;
9328 use crate::storage::query::ast::{PolicyAction, PolicyTargetKind};
9329 use crate::storage::unified::entity::{EntityData, EntityKind};
9330 use std::collections::{HashMap, HashSet};
9331
9332 let store = self.inner.db.store();
9333 let snap_ctx = capture_current_snapshot();
9334 let role = current_auth_identity().map(|(_, r)| r.as_str().to_string());
9335
9336 let graph = GraphStore::new();
9337 let mut node_properties: HashMap<String, HashMap<String, crate::storage::schema::Value>> =
9338 HashMap::new();
9339 let mut edge_properties: crate::storage::query::unified::EdgeProperties = HashMap::new();
9340 let mut allowed_nodes: HashSet<String> = HashSet::new();
9341
9342 let mut node_rls: HashMap<String, Option<crate::storage::query::ast::Filter>> =
9346 HashMap::new();
9347 let mut edge_rls: HashMap<String, Option<crate::storage::query::ast::Filter>> =
9348 HashMap::new();
9349
9350 let collections = store.list_collections();
9351
9352 for collection in &collections {
9354 let Some(manager) = store.get_collection(collection) else {
9355 continue;
9356 };
9357 let entities = manager.query_all(|_| true);
9358 for entity in entities {
9359 if !entity_visible_with_context(snap_ctx.as_ref(), &entity) {
9360 continue;
9361 }
9362 let EntityKind::GraphNode(ref node) = entity.kind else {
9363 continue;
9364 };
9365 if !node_passes_rls(self, collection, role.as_deref(), &mut node_rls, &entity) {
9366 continue;
9367 }
9368 let id_str = entity.id.raw().to_string();
9369 graph
9370 .add_node_with_label(
9371 &id_str,
9372 &node.label,
9373 &super::graph_node_label(&node.node_type),
9374 )
9375 .map_err(|err| RedDBError::Query(err.to_string()))?;
9376 allowed_nodes.insert(id_str.clone());
9377 if let EntityData::Node(node_data) = &entity.data {
9378 node_properties.insert(id_str, node_data.properties.clone());
9379 }
9380 }
9381 }
9382
9383 for collection in &collections {
9387 let Some(manager) = store.get_collection(collection) else {
9388 continue;
9389 };
9390 let entities = manager.query_all(|_| true);
9391 for entity in entities {
9392 if !entity_visible_with_context(snap_ctx.as_ref(), &entity) {
9393 continue;
9394 }
9395 let EntityKind::GraphEdge(ref edge) = entity.kind else {
9396 continue;
9397 };
9398 if !allowed_nodes.contains(&edge.from_node)
9399 || !allowed_nodes.contains(&edge.to_node)
9400 {
9401 continue;
9402 }
9403 if !edge_passes_rls(self, collection, role.as_deref(), &mut edge_rls, &entity) {
9404 continue;
9405 }
9406 let weight = match &entity.data {
9407 EntityData::Edge(e) => e.weight,
9408 _ => edge.weight as f32 / 1000.0,
9409 };
9410 let edge_label = super::graph_edge_label(&edge.label);
9411 graph
9412 .add_edge_with_label(&edge.from_node, &edge.to_node, &edge_label, weight)
9413 .map_err(|err| RedDBError::Query(err.to_string()))?;
9414 if let EntityData::Edge(edge_data) = &entity.data {
9415 edge_properties.insert(
9416 (edge.from_node.clone(), edge_label, edge.to_node.clone()),
9417 edge_data.properties.clone(),
9418 );
9419 }
9420 }
9421 }
9422
9423 let _ = (PolicyAction::Select, PolicyTargetKind::Nodes);
9427
9428 Ok((graph, node_properties, edge_properties))
9429 }
9430
9431 pub(crate) fn stamp_xmin_if_in_txn(
9446 &self,
9447 collection: &str,
9448 id: crate::storage::unified::entity::EntityId,
9449 ) {
9450 let Some(xid) = self.current_xid() else {
9451 return;
9452 };
9453 let store = self.inner.db.store();
9454 let Some(manager) = store.get_collection(collection) else {
9455 return;
9456 };
9457 if let Some(mut entity) = manager.get(id) {
9458 entity.set_xmin(xid);
9459 let _ = manager.update(entity);
9460 }
9461 }
9462
9463 pub(crate) fn revive_tombstones_since(&self, conn_id: u64, stamper_xid: u64) -> usize {
9471 let mut guard = self.inner.pending_tombstones.write();
9472 let Some(pending) = guard.get_mut(&conn_id) else {
9473 return 0;
9474 };
9475
9476 let store = self.inner.db.store();
9477 let mut revived = 0usize;
9478 pending.retain(|(collection, id, xid, previous_xmax)| {
9479 if *xid < stamper_xid {
9480 return true;
9482 }
9483 if let Some(manager) = store.get_collection(collection) {
9484 if let Some(mut entity) = manager.get(*id) {
9485 if entity.xmax == *xid {
9486 entity.set_xmax(*previous_xmax);
9487 let _ = manager.update(entity);
9488 revived += 1;
9489 }
9490 }
9491 }
9492 false
9493 });
9494 if pending.is_empty() {
9495 guard.remove(&conn_id);
9496 }
9497 revived
9498 }
9499
9500 pub fn current_snapshot(&self) -> crate::storage::transaction::snapshot::Snapshot {
9509 let conn_id = current_connection_id();
9510 if let Some(ctx) = self.inner.tx_contexts.read().get(&conn_id).cloned() {
9511 return ctx.snapshot;
9512 }
9513 let high_water = self.inner.snapshot_manager.peek_next_xid();
9519 self.inner.snapshot_manager.snapshot(high_water)
9520 }
9521
9522 pub fn current_xid(&self) -> Option<crate::storage::transaction::snapshot::Xid> {
9532 let conn_id = current_connection_id();
9533 self.inner
9534 .tx_contexts
9535 .read()
9536 .get(&conn_id)
9537 .map(|ctx| ctx.writer_xid())
9538 }
9539
9540 pub fn connection_in_transaction(&self, conn_id: u64) -> bool {
9547 self.inner.tx_contexts.read().contains_key(&conn_id)
9548 }
9549
9550 pub fn snapshot_manager(&self) -> Arc<crate::storage::transaction::snapshot::SnapshotManager> {
9553 Arc::clone(&self.inner.snapshot_manager)
9554 }
9555
9556 fn mvcc_vacuum_cutoff_xid(&self) -> crate::storage::transaction::snapshot::Xid {
9557 let manager = &self.inner.snapshot_manager;
9558 let next_xid = manager.peek_next_xid();
9559 let mut cutoff = next_xid;
9560 if let Some(oldest_active) = manager.oldest_active_xid() {
9561 cutoff = cutoff.min(oldest_active);
9562 }
9563 if let Some(oldest_pinned) = manager.oldest_pinned_xid() {
9564 cutoff = cutoff.min(oldest_pinned);
9565 }
9566 let retention_xids = self.config_u64("runtime.mvcc.vacuum_retention_xids", 0);
9567 if retention_xids > 0 {
9568 cutoff = cutoff.min(next_xid.saturating_sub(retention_xids));
9569 }
9570 cutoff
9571 }
9572
9573 fn rebuild_runtime_indexes_for_table(&self, table: &str) -> RedDBResult<()> {
9574 let registered = self.inner.index_store.list_indices(table);
9575 if registered.is_empty() {
9576 return Ok(());
9577 }
9578 let store = self.inner.db.store();
9579 let Some(manager) = store.get_collection(table) else {
9580 return Ok(());
9581 };
9582 let entity_fields = manager
9583 .query_all(|entity| matches!(entity.kind, crate::storage::EntityKind::TableRow { .. }))
9584 .into_iter()
9585 .map(|entity| (entity.id, table_row_index_fields(&entity)))
9586 .collect::<Vec<_>>();
9587
9588 for index in registered {
9589 self.inner.index_store.drop_index(&index.name, table);
9590 self.inner
9591 .index_store
9592 .create_index(
9593 &index.name,
9594 table,
9595 &index.columns,
9596 index.method,
9597 index.unique,
9598 &entity_fields,
9599 )
9600 .map_err(RedDBError::Internal)?;
9601 self.inner.index_store.register(index);
9602 }
9603 self.invalidate_plan_cache();
9604 Ok(())
9605 }
9606
9607 pub(crate) fn persist_runtime_index_descriptor(
9608 &self,
9609 index: super::index_store::RegisteredIndex,
9610 ) -> RedDBResult<()> {
9611 let store = self.inner.db.store();
9612 let _ = store.get_or_create_collection(RUNTIME_INDEX_REGISTRY_COLLECTION);
9613 let entity = crate::storage::UnifiedEntity::new(
9614 crate::storage::EntityId::new(0),
9615 crate::storage::EntityKind::TableRow {
9616 table: std::sync::Arc::from(RUNTIME_INDEX_REGISTRY_COLLECTION),
9617 row_id: 0,
9618 },
9619 crate::storage::EntityData::Row(crate::storage::RowData {
9620 columns: Vec::new(),
9621 named: Some(
9622 [
9623 (
9624 "collection".to_string(),
9625 crate::storage::schema::Value::text(index.collection.clone()),
9626 ),
9627 (
9628 "name".to_string(),
9629 crate::storage::schema::Value::text(index.name.clone()),
9630 ),
9631 (
9632 "columns".to_string(),
9633 crate::storage::schema::Value::text(index.columns.join("\u{1f}")),
9634 ),
9635 (
9636 "method".to_string(),
9637 crate::storage::schema::Value::text(index_method_kind_as_str(
9638 index.method,
9639 )),
9640 ),
9641 (
9642 "resolution".to_string(),
9643 crate::storage::schema::Value::Integer(i64::from(
9644 index_method_kind_resolution(index.method),
9645 )),
9646 ),
9647 (
9648 "unique".to_string(),
9649 crate::storage::schema::Value::Boolean(index.unique),
9650 ),
9651 (
9652 "dropped".to_string(),
9653 crate::storage::schema::Value::Boolean(false),
9654 ),
9655 ]
9656 .into_iter()
9657 .collect(),
9658 ),
9659 schema: None,
9660 }),
9661 );
9662 store
9663 .insert_auto(RUNTIME_INDEX_REGISTRY_COLLECTION, entity)
9664 .map(|_| ())
9665 .map_err(|err| RedDBError::Internal(format!("{err:?}")))
9666 }
9667
9668 pub(crate) fn persist_runtime_index_drop(
9669 &self,
9670 collection: &str,
9671 name: &str,
9672 ) -> RedDBResult<()> {
9673 let store = self.inner.db.store();
9674 let _ = store.get_or_create_collection(RUNTIME_INDEX_REGISTRY_COLLECTION);
9675 let entity = crate::storage::UnifiedEntity::new(
9676 crate::storage::EntityId::new(0),
9677 crate::storage::EntityKind::TableRow {
9678 table: std::sync::Arc::from(RUNTIME_INDEX_REGISTRY_COLLECTION),
9679 row_id: 0,
9680 },
9681 crate::storage::EntityData::Row(crate::storage::RowData {
9682 columns: Vec::new(),
9683 named: Some(
9684 [
9685 (
9686 "collection".to_string(),
9687 crate::storage::schema::Value::text(collection.to_string()),
9688 ),
9689 (
9690 "name".to_string(),
9691 crate::storage::schema::Value::text(name.to_string()),
9692 ),
9693 (
9694 "dropped".to_string(),
9695 crate::storage::schema::Value::Boolean(true),
9696 ),
9697 ]
9698 .into_iter()
9699 .collect(),
9700 ),
9701 schema: None,
9702 }),
9703 );
9704 store
9705 .insert_auto(RUNTIME_INDEX_REGISTRY_COLLECTION, entity)
9706 .map(|_| ())
9707 .map_err(|err| RedDBError::Internal(format!("{err:?}")))
9708 }
9709
9710 fn rehydrate_runtime_index_registry(&self) -> RedDBResult<()> {
9711 let store = self.inner.db.store();
9712 let Some(manager) = store.get_collection(RUNTIME_INDEX_REGISTRY_COLLECTION) else {
9713 return Ok(());
9714 };
9715 let mut rows = manager.query_all(|_| true);
9716 rows.sort_by_key(|entity| entity.id.raw());
9717
9718 let mut latest = std::collections::HashMap::<
9719 (String, String),
9720 Option<super::index_store::RegisteredIndex>,
9721 >::new();
9722 for entity in rows {
9723 let crate::storage::EntityData::Row(row) = &entity.data else {
9724 continue;
9725 };
9726 let Some(named) = &row.named else {
9727 continue;
9728 };
9729 let Some(collection) = named_text(named, "collection") else {
9730 continue;
9731 };
9732 let Some(name) = named_text(named, "name") else {
9733 continue;
9734 };
9735 let dropped = named_bool(named, "dropped").unwrap_or(false);
9736 let key = (collection.clone(), name.clone());
9737 if dropped {
9738 latest.insert(key, None);
9739 continue;
9740 }
9741 let columns = named_text(named, "columns")
9742 .map(|raw| {
9743 raw.split('\u{1f}')
9744 .filter(|part| !part.is_empty())
9745 .map(str::to_string)
9746 .collect::<Vec<_>>()
9747 })
9748 .unwrap_or_default();
9749 let resolution = named_i64(named, "resolution")
9750 .and_then(|v| u8::try_from(v).ok())
9751 .unwrap_or(0);
9752 let Some(method) = named_text(named, "method")
9753 .and_then(|raw| index_method_kind_from_str(&raw, resolution))
9754 else {
9755 continue;
9756 };
9757 latest.insert(
9758 key,
9759 Some(super::index_store::RegisteredIndex {
9760 name,
9761 collection,
9762 columns,
9763 method,
9764 unique: named_bool(named, "unique").unwrap_or(false),
9765 }),
9766 );
9767 }
9768
9769 for index in latest.into_values().flatten() {
9770 let Some(manager) = store.get_collection(&index.collection) else {
9771 continue;
9772 };
9773 let entity_fields = manager
9774 .query_all(|entity| {
9775 matches!(entity.kind, crate::storage::EntityKind::TableRow { .. })
9776 })
9777 .into_iter()
9778 .map(|entity| (entity.id, table_row_index_fields(&entity)))
9779 .collect::<Vec<_>>();
9780 self.inner
9781 .index_store
9782 .create_index(
9783 &index.name,
9784 &index.collection,
9785 &index.columns,
9786 index.method,
9787 index.unique,
9788 &entity_fields,
9789 )
9790 .map_err(RedDBError::Internal)?;
9791 self.inner.index_store.register(index);
9792 }
9793 self.invalidate_plan_cache();
9794 Ok(())
9795 }
9796
9797 pub fn current_txn_own_xids(
9802 &self,
9803 ) -> std::collections::HashSet<crate::storage::transaction::snapshot::Xid> {
9804 let mut set = std::collections::HashSet::new();
9805 if let Some(ctx) = self.inner.tx_contexts.read().get(¤t_connection_id()) {
9806 set.insert(ctx.xid);
9807 for (_, sub) in &ctx.savepoints {
9808 set.insert(*sub);
9809 }
9810 for sub in &ctx.released_sub_xids {
9811 set.insert(*sub);
9812 }
9813 }
9814 set
9815 }
9816
9817 pub fn foreign_tables(&self) -> Arc<crate::storage::fdw::ForeignTableRegistry> {
9824 Arc::clone(&self.inner.foreign_tables)
9825 }
9826
9827 pub fn is_rls_enabled(&self, table: &str) -> bool {
9829 self.inner.rls_enabled_tables.read().contains(table)
9830 }
9831
9832 pub fn matching_rls_policies(
9839 &self,
9840 table: &str,
9841 role: Option<&str>,
9842 action: crate::storage::query::ast::PolicyAction,
9843 ) -> Vec<crate::storage::query::ast::Filter> {
9844 self.matching_rls_policies_for_kind(
9849 table,
9850 role,
9851 action,
9852 crate::storage::query::ast::PolicyTargetKind::Table,
9853 )
9854 }
9855
9856 pub fn matching_rls_policies_for_kind(
9864 &self,
9865 table: &str,
9866 role: Option<&str>,
9867 action: crate::storage::query::ast::PolicyAction,
9868 kind: crate::storage::query::ast::PolicyTargetKind,
9869 ) -> Vec<crate::storage::query::ast::Filter> {
9870 if !self.is_rls_enabled(table) {
9871 return Vec::new();
9872 }
9873 let policies = self.inner.rls_policies.read();
9874 policies
9875 .iter()
9876 .filter_map(|((t, _), p)| {
9877 if t != table {
9878 return None;
9879 }
9880 if p.target_kind != kind
9889 && p.target_kind != crate::storage::query::ast::PolicyTargetKind::Table
9890 {
9891 return None;
9892 }
9893 if let Some(a) = p.action {
9895 if a != action {
9896 return None;
9897 }
9898 }
9899 if let Some(p_role) = p.role.as_deref() {
9901 match role {
9902 Some(r) if r == p_role => {}
9903 _ => return None,
9904 }
9905 }
9906 Some((*p.using).clone())
9907 })
9908 .collect()
9909 }
9910
9911 pub(crate) fn refresh_table_planner_stats(&self, table: &str) {
9912 let store = self.inner.db.store();
9913 if let Some(stats) =
9914 crate::storage::query::planner::stats_catalog::analyze_collection(store.as_ref(), table)
9915 {
9916 crate::storage::query::planner::stats_catalog::persist_table_stats(
9917 store.as_ref(),
9918 &stats,
9919 );
9920 } else {
9921 crate::storage::query::planner::stats_catalog::clear_table_stats(store.as_ref(), table);
9922 }
9923 self.invalidate_plan_cache();
9924 }
9925
9926 pub(crate) fn note_table_write(&self, table: &str) {
9927 let already_dirty = self.inner.planner_dirty_tables.read().contains(table);
9932 if !already_dirty {
9933 self.inner
9934 .planner_dirty_tables
9935 .write()
9936 .insert(table.to_string());
9937 }
9938 self.invalidate_result_cache_for_table(table);
9939 }
9940
9941 fn explain_as_rows(&self, raw_query: &str, inner_sql: &str) -> RedDBResult<RuntimeQueryResult> {
9949 let explain = self.explain_query(inner_sql)?;
9950
9951 let columns = vec![
9952 "op".to_string(),
9953 "source".to_string(),
9954 "est_rows".to_string(),
9955 "est_cost".to_string(),
9956 "depth".to_string(),
9957 ];
9958
9959 let mut records: Vec<crate::storage::query::unified::UnifiedRecord> = Vec::new();
9960
9961 for name in &explain.cte_materializations {
9967 use std::sync::Arc;
9968 let mut rec = crate::storage::query::unified::UnifiedRecord::default();
9969 rec.set_arc(Arc::from("op"), Value::text("CteScan".to_string()));
9970 rec.set_arc(Arc::from("source"), Value::text(name.clone()));
9971 rec.set_arc(Arc::from("est_rows"), Value::Float(0.0));
9972 rec.set_arc(Arc::from("est_cost"), Value::Float(0.0));
9973 rec.set_arc(Arc::from("depth"), Value::Integer(0));
9974 records.push(rec);
9975 }
9976
9977 walk_plan_node(&explain.logical_plan.root, 0, &mut records);
9978
9979 let result = crate::storage::query::unified::UnifiedResult {
9980 columns,
9981 records,
9982 stats: Default::default(),
9983 pre_serialized_json: None,
9984 };
9985
9986 Ok(RuntimeQueryResult {
9987 query: raw_query.to_string(),
9988 mode: explain.mode,
9989 statement: "explain",
9990 engine: "runtime-explain",
9991 result,
9992 affected_rows: 0,
9993 statement_type: "select",
9994 bookmark: None,
9995 })
9996 }
9997
9998 pub(crate) fn check_query_privilege(
10006 &self,
10007 expr: &crate::storage::query::ast::QueryExpr,
10008 ) -> Result<(), String> {
10009 use crate::auth::privileges::{Action, AuthzContext, Resource};
10010 use crate::auth::UserId;
10011 use crate::storage::query::ast::QueryExpr;
10012
10013 let auth_store = match self.inner.auth_store.read().clone() {
10018 Some(s) => s,
10019 None => return Ok(()),
10020 };
10021
10022 let (username, role) = match current_auth_identity() {
10028 Some(p) => p,
10029 None => return Ok(()),
10030 };
10031 let tenant = current_tenant();
10032
10033 let ctx = AuthzContext {
10034 principal: &username,
10035 effective_role: role,
10036 tenant: tenant.as_deref(),
10037 };
10038 let principal_id = UserId::from_parts(tenant.as_deref(), &username);
10039
10040 let (action, resource) = match expr {
10042 QueryExpr::Table(t) => (Action::Select, Resource::table_from_name(&t.table)),
10043 QueryExpr::RankOf(_) | QueryExpr::ApproxRankOf(_) | QueryExpr::RankRange(_) => {
10044 (Action::Select, Resource::Database)
10045 }
10046 QueryExpr::QueueSelect(q) => {
10047 return self.check_queue_op_privilege(
10048 &auth_store,
10049 &principal_id,
10050 role,
10051 tenant.as_deref(),
10052 "queue:peek",
10053 &q.queue,
10054 );
10055 }
10056 QueryExpr::QueueCommand(cmd) => {
10057 use crate::storage::query::ast::QueueCommand;
10058 let (queue, action_verb) = match cmd {
10059 QueueCommand::Push { queue, .. } => (queue.as_str(), "queue:enqueue"),
10060 QueueCommand::Pop { queue, .. }
10061 | QueueCommand::GroupRead { queue, .. }
10062 | QueueCommand::Claim { queue, .. } => (queue.as_str(), "queue:read"),
10063 QueueCommand::Peek { queue, .. }
10064 | QueueCommand::Len { queue }
10065 | QueueCommand::Pending { queue, .. } => (queue.as_str(), "queue:peek"),
10066 QueueCommand::Ack { queue, .. } => (queue.as_str(), "queue:ack"),
10067 QueueCommand::Nack {
10068 queue, delay_ms, ..
10069 } => {
10070 let verb = if delay_ms.is_some() {
10076 "queue:retry"
10077 } else {
10078 "queue:nack"
10079 };
10080 (queue.as_str(), verb)
10081 }
10082 QueueCommand::Purge { queue } => (queue.as_str(), "queue:purge"),
10083 QueueCommand::GroupCreate { queue, .. } => (queue.as_str(), "queue:read"),
10086 QueueCommand::Move { source, .. } => (source.as_str(), "queue:dlq:move"),
10087 };
10088 return self.check_queue_op_privilege(
10089 &auth_store,
10090 &principal_id,
10091 role,
10092 tenant.as_deref(),
10093 action_verb,
10094 queue,
10095 );
10096 }
10097 QueryExpr::Graph(g) => {
10098 self.check_graph_op_privilege(
10101 &auth_store,
10102 &principal_id,
10103 role,
10104 tenant.as_deref(),
10105 "graph:traverse",
10106 )?;
10107 if auth_store.iam_authorization_enabled() {
10108 self.check_graph_property_projection_privilege(
10109 &auth_store,
10110 &principal_id,
10111 role,
10112 tenant.as_deref(),
10113 g,
10114 )?;
10115 return Ok(());
10116 }
10117 return Ok(());
10118 }
10119 QueryExpr::Path(_) => {
10120 return self.check_graph_op_privilege(
10124 &auth_store,
10125 &principal_id,
10126 role,
10127 tenant.as_deref(),
10128 "graph:traverse",
10129 );
10130 }
10131 QueryExpr::GraphCommand(cmd) => {
10132 use crate::storage::query::ast::GraphCommand;
10133 let action_verb = match cmd {
10134 GraphCommand::Properties { .. } => "graph:read",
10136 GraphCommand::Neighborhood { .. }
10138 | GraphCommand::Traverse { .. }
10139 | GraphCommand::ShortestPath { .. } => "graph:traverse",
10140 GraphCommand::Centrality { .. }
10144 | GraphCommand::Community { .. }
10145 | GraphCommand::Components { .. }
10146 | GraphCommand::Cycles { .. }
10147 | GraphCommand::Clustering
10148 | GraphCommand::TopologicalSort => "graph:algorithm:run",
10149 };
10150 return self.check_graph_op_privilege(
10151 &auth_store,
10152 &principal_id,
10153 role,
10154 tenant.as_deref(),
10155 action_verb,
10156 );
10157 }
10158 QueryExpr::Vector(v) => {
10159 if auth_store.iam_authorization_enabled() {
10160 self.check_vector_op_privilege(
10161 &auth_store,
10162 &principal_id,
10163 role,
10164 tenant.as_deref(),
10165 "vector:search",
10166 &v.collection,
10167 )?;
10168 self.check_table_like_column_projection_privilege(
10169 &auth_store,
10170 &principal_id,
10171 role,
10172 tenant.as_deref(),
10173 &v.collection,
10174 &["content".to_string()],
10175 )?;
10176 return Ok(());
10177 }
10178 return Ok(());
10179 }
10180 QueryExpr::SearchCommand(cmd) => {
10181 use crate::storage::query::ast::SearchCommand;
10182 if auth_store.iam_authorization_enabled() {
10183 let collection = match cmd {
10189 SearchCommand::Similar { collection, .. }
10190 | SearchCommand::Hybrid { collection, .. } => Some(collection.as_str()),
10191 _ => None,
10192 };
10193 if let Some(c) = collection {
10194 self.check_vector_op_privilege(
10195 &auth_store,
10196 &principal_id,
10197 role,
10198 tenant.as_deref(),
10199 "vector:search",
10200 c,
10201 )?;
10202 return Ok(());
10203 }
10204 }
10205 return Ok(());
10206 }
10207 QueryExpr::Hybrid(h) => {
10208 if auth_store.iam_authorization_enabled() {
10209 self.check_vector_op_privilege(
10217 &auth_store,
10218 &principal_id,
10219 role,
10220 tenant.as_deref(),
10221 "vector:search",
10222 &h.vector.collection,
10223 )?;
10224 return Ok(());
10225 }
10226 return Ok(());
10227 }
10228 QueryExpr::Insert(i) => (Action::Insert, Resource::table_from_name(&i.table)),
10229 QueryExpr::Update(u) => (Action::Update, Resource::table_from_name(&u.table)),
10230 QueryExpr::Delete(d) => (Action::Delete, Resource::table_from_name(&d.table)),
10231 QueryExpr::Join(_) => (Action::Select, Resource::Database),
10235 QueryExpr::Grant(_)
10238 | QueryExpr::Revoke(_)
10239 | QueryExpr::AlterUser(_)
10240 | QueryExpr::CreateUser(_) => {
10241 return if role == crate::auth::Role::Admin {
10242 Ok(())
10243 } else {
10244 Err(format!(
10245 "principal=`{}` role=`{:?}` cannot issue ACL/auth DDL",
10246 username, role
10247 ))
10248 };
10249 }
10250 QueryExpr::CreateIamPolicy { id, .. } => {
10251 return self.check_policy_management_privilege(
10252 &auth_store,
10253 &principal_id,
10254 role,
10255 tenant.as_deref(),
10256 "policy:put",
10257 "policy",
10258 id,
10259 );
10260 }
10261 QueryExpr::DropIamPolicy { id } => {
10262 return self.check_policy_management_privilege(
10263 &auth_store,
10264 &principal_id,
10265 role,
10266 tenant.as_deref(),
10267 "policy:drop",
10268 "policy",
10269 id,
10270 );
10271 }
10272 QueryExpr::AttachPolicy { policy_id, .. } => {
10273 return self.check_policy_management_privilege(
10274 &auth_store,
10275 &principal_id,
10276 role,
10277 tenant.as_deref(),
10278 "policy:attach",
10279 "policy",
10280 policy_id,
10281 );
10282 }
10283 QueryExpr::DetachPolicy { policy_id, .. } => {
10284 return self.check_policy_management_privilege(
10285 &auth_store,
10286 &principal_id,
10287 role,
10288 tenant.as_deref(),
10289 "policy:detach",
10290 "policy",
10291 policy_id,
10292 );
10293 }
10294 QueryExpr::ShowPolicies { .. } | QueryExpr::ShowEffectivePermissions { .. } => {
10295 return Ok(());
10296 }
10297 QueryExpr::SimulatePolicy { .. } => {
10298 return self.check_policy_management_privilege(
10299 &auth_store,
10300 &principal_id,
10301 role,
10302 tenant.as_deref(),
10303 "policy:simulate",
10304 "policy",
10305 "*",
10306 );
10307 }
10308 QueryExpr::LintPolicy { .. } => {
10309 return self.check_policy_management_privilege(
10312 &auth_store,
10313 &principal_id,
10314 role,
10315 tenant.as_deref(),
10316 "policy:simulate",
10317 "policy",
10318 "*",
10319 );
10320 }
10321 QueryExpr::MigratePolicyMode { dry_run, .. } => {
10322 let action = if *dry_run {
10327 "policy:simulate"
10328 } else {
10329 "policy:put"
10330 };
10331 return self.check_policy_management_privilege(
10332 &auth_store,
10333 &principal_id,
10334 role,
10335 tenant.as_deref(),
10336 action,
10337 "policy",
10338 "*",
10339 );
10340 }
10341 QueryExpr::DropTable(q) => {
10344 return self.check_ddl_collection_privilege(
10345 &auth_store,
10346 &principal_id,
10347 role,
10348 tenant.as_deref(),
10349 &username,
10350 "drop",
10351 &q.name,
10352 );
10353 }
10354 QueryExpr::DropGraph(q) => {
10355 return self.check_ddl_collection_privilege(
10356 &auth_store,
10357 &principal_id,
10358 role,
10359 tenant.as_deref(),
10360 &username,
10361 "drop",
10362 &q.name,
10363 );
10364 }
10365 QueryExpr::DropVector(q) => {
10366 return self.check_ddl_collection_privilege(
10367 &auth_store,
10368 &principal_id,
10369 role,
10370 tenant.as_deref(),
10371 &username,
10372 "drop",
10373 &q.name,
10374 );
10375 }
10376 QueryExpr::DropDocument(q) => {
10377 return self.check_ddl_collection_privilege(
10378 &auth_store,
10379 &principal_id,
10380 role,
10381 tenant.as_deref(),
10382 &username,
10383 "drop",
10384 &q.name,
10385 );
10386 }
10387 QueryExpr::DropKv(q) => {
10388 return self.check_ddl_collection_privilege(
10389 &auth_store,
10390 &principal_id,
10391 role,
10392 tenant.as_deref(),
10393 &username,
10394 "drop",
10395 &q.name,
10396 );
10397 }
10398 QueryExpr::DropCollection(q) => {
10399 return self.check_ddl_collection_privilege(
10400 &auth_store,
10401 &principal_id,
10402 role,
10403 tenant.as_deref(),
10404 &username,
10405 "drop",
10406 &q.name,
10407 );
10408 }
10409 QueryExpr::Truncate(q) => {
10410 return self.check_ddl_collection_privilege(
10411 &auth_store,
10412 &principal_id,
10413 role,
10414 tenant.as_deref(),
10415 &username,
10416 "truncate",
10417 &q.name,
10418 );
10419 }
10420 QueryExpr::CreateTable(q) => {
10432 return self.check_ddl_object_privilege(
10433 &auth_store,
10434 &principal_id,
10435 role,
10436 tenant.as_deref(),
10437 &username,
10438 "create",
10439 "collection",
10440 &q.name,
10441 crate::auth::Role::Write,
10442 );
10443 }
10444 QueryExpr::CreateCollection(q) => {
10445 return self.check_ddl_object_privilege(
10446 &auth_store,
10447 &principal_id,
10448 role,
10449 tenant.as_deref(),
10450 &username,
10451 "create",
10452 "collection",
10453 &q.name,
10454 crate::auth::Role::Write,
10455 );
10456 }
10457 QueryExpr::CreateVector(q) => {
10458 return self.check_ddl_object_privilege(
10459 &auth_store,
10460 &principal_id,
10461 role,
10462 tenant.as_deref(),
10463 &username,
10464 "create",
10465 "collection",
10466 &q.name,
10467 crate::auth::Role::Write,
10468 );
10469 }
10470 QueryExpr::AlterTable(q) => {
10471 return self.check_ddl_object_privilege(
10472 &auth_store,
10473 &principal_id,
10474 role,
10475 tenant.as_deref(),
10476 &username,
10477 "alter",
10478 "collection",
10479 &q.name,
10480 crate::auth::Role::Write,
10481 );
10482 }
10483 QueryExpr::CreateIndex(q) => {
10484 return self.check_ddl_object_privilege(
10485 &auth_store,
10486 &principal_id,
10487 role,
10488 tenant.as_deref(),
10489 &username,
10490 "create",
10491 "collection",
10492 &q.table,
10493 crate::auth::Role::Write,
10494 );
10495 }
10496 QueryExpr::DropIndex(q) => {
10497 return self.check_ddl_object_privilege(
10498 &auth_store,
10499 &principal_id,
10500 role,
10501 tenant.as_deref(),
10502 &username,
10503 "drop",
10504 "collection",
10505 &q.table,
10506 crate::auth::Role::Write,
10507 );
10508 }
10509 QueryExpr::CreateSchema(q) => {
10510 return self.check_ddl_object_privilege(
10511 &auth_store,
10512 &principal_id,
10513 role,
10514 tenant.as_deref(),
10515 &username,
10516 "schema:admin",
10517 "schema",
10518 &q.name,
10519 crate::auth::Role::Admin,
10520 );
10521 }
10522 QueryExpr::DropSchema(q) => {
10523 return self.check_ddl_object_privilege(
10524 &auth_store,
10525 &principal_id,
10526 role,
10527 tenant.as_deref(),
10528 &username,
10529 "schema:admin",
10530 "schema",
10531 &q.name,
10532 crate::auth::Role::Admin,
10533 );
10534 }
10535 QueryExpr::CreateSequence(q) => {
10536 return self.check_ddl_object_privilege(
10537 &auth_store,
10538 &principal_id,
10539 role,
10540 tenant.as_deref(),
10541 &username,
10542 "create",
10543 "collection",
10544 &q.name,
10545 crate::auth::Role::Write,
10546 );
10547 }
10548 QueryExpr::DropSequence(q) => {
10549 return self.check_ddl_object_privilege(
10550 &auth_store,
10551 &principal_id,
10552 role,
10553 tenant.as_deref(),
10554 &username,
10555 "drop",
10556 "collection",
10557 &q.name,
10558 crate::auth::Role::Write,
10559 );
10560 }
10561 QueryExpr::CreateView(q) => {
10562 return self.check_ddl_object_privilege(
10563 &auth_store,
10564 &principal_id,
10565 role,
10566 tenant.as_deref(),
10567 &username,
10568 "create",
10569 "collection",
10570 &q.name,
10571 crate::auth::Role::Write,
10572 );
10573 }
10574 QueryExpr::DropView(q) => {
10575 return self.check_ddl_object_privilege(
10576 &auth_store,
10577 &principal_id,
10578 role,
10579 tenant.as_deref(),
10580 &username,
10581 "drop",
10582 "collection",
10583 &q.name,
10584 crate::auth::Role::Write,
10585 );
10586 }
10587 QueryExpr::RefreshMaterializedView(q) => {
10588 return self.check_ddl_object_privilege(
10589 &auth_store,
10590 &principal_id,
10591 role,
10592 tenant.as_deref(),
10593 &username,
10594 "alter",
10595 "collection",
10596 &q.name,
10597 crate::auth::Role::Write,
10598 );
10599 }
10600 QueryExpr::CreatePolicy(q) => {
10601 return self.check_ddl_object_privilege(
10602 &auth_store,
10603 &principal_id,
10604 role,
10605 tenant.as_deref(),
10606 &username,
10607 "create",
10608 "collection",
10609 &q.table,
10610 crate::auth::Role::Write,
10611 );
10612 }
10613 QueryExpr::DropPolicy(q) => {
10614 return self.check_ddl_object_privilege(
10615 &auth_store,
10616 &principal_id,
10617 role,
10618 tenant.as_deref(),
10619 &username,
10620 "drop",
10621 "collection",
10622 &q.table,
10623 crate::auth::Role::Write,
10624 );
10625 }
10626 QueryExpr::CreateServer(q) => {
10627 return self.check_ddl_object_privilege(
10628 &auth_store,
10629 &principal_id,
10630 role,
10631 tenant.as_deref(),
10632 &username,
10633 "schema:admin",
10634 "schema",
10635 &q.name,
10636 crate::auth::Role::Admin,
10637 );
10638 }
10639 QueryExpr::DropServer(q) => {
10640 return self.check_ddl_object_privilege(
10641 &auth_store,
10642 &principal_id,
10643 role,
10644 tenant.as_deref(),
10645 &username,
10646 "schema:admin",
10647 "schema",
10648 &q.name,
10649 crate::auth::Role::Admin,
10650 );
10651 }
10652 QueryExpr::CreateForeignTable(q) => {
10653 return self.check_ddl_object_privilege(
10654 &auth_store,
10655 &principal_id,
10656 role,
10657 tenant.as_deref(),
10658 &username,
10659 "schema:write",
10660 "schema",
10661 &q.name,
10662 crate::auth::Role::Write,
10663 );
10664 }
10665 QueryExpr::DropForeignTable(q) => {
10666 return self.check_ddl_object_privilege(
10667 &auth_store,
10668 &principal_id,
10669 role,
10670 tenant.as_deref(),
10671 &username,
10672 "schema:write",
10673 "schema",
10674 &q.name,
10675 crate::auth::Role::Write,
10676 );
10677 }
10678 QueryExpr::CreateTimeSeries(q) => {
10679 return self.check_ddl_object_privilege(
10680 &auth_store,
10681 &principal_id,
10682 role,
10683 tenant.as_deref(),
10684 &username,
10685 "create",
10686 "collection",
10687 &q.name,
10688 crate::auth::Role::Write,
10689 );
10690 }
10691 QueryExpr::CreateMetric(q) => {
10692 return self.check_ddl_object_privilege(
10693 &auth_store,
10694 &principal_id,
10695 role,
10696 tenant.as_deref(),
10697 &username,
10698 "create",
10699 "collection",
10700 &q.path,
10701 crate::auth::Role::Write,
10702 );
10703 }
10704 QueryExpr::AlterMetric(q) => {
10705 return self.check_ddl_object_privilege(
10706 &auth_store,
10707 &principal_id,
10708 role,
10709 tenant.as_deref(),
10710 &username,
10711 "alter",
10712 "collection",
10713 &q.path,
10714 crate::auth::Role::Write,
10715 );
10716 }
10717 QueryExpr::CreateSlo(q) => {
10718 return self.check_ddl_object_privilege(
10719 &auth_store,
10720 &principal_id,
10721 role,
10722 tenant.as_deref(),
10723 &username,
10724 "create",
10725 "collection",
10726 &q.path,
10727 crate::auth::Role::Write,
10728 );
10729 }
10730 QueryExpr::DropTimeSeries(q) => {
10731 return self.check_ddl_object_privilege(
10732 &auth_store,
10733 &principal_id,
10734 role,
10735 tenant.as_deref(),
10736 &username,
10737 "drop",
10738 "collection",
10739 &q.name,
10740 crate::auth::Role::Write,
10741 );
10742 }
10743 QueryExpr::CreateQueue(q) => {
10744 return self.check_ddl_object_privilege(
10745 &auth_store,
10746 &principal_id,
10747 role,
10748 tenant.as_deref(),
10749 &username,
10750 "create",
10751 "collection",
10752 &q.name,
10753 crate::auth::Role::Write,
10754 );
10755 }
10756 QueryExpr::AlterQueue(q) => {
10757 return self.check_ddl_object_privilege(
10758 &auth_store,
10759 &principal_id,
10760 role,
10761 tenant.as_deref(),
10762 &username,
10763 "alter",
10764 "collection",
10765 &q.name,
10766 crate::auth::Role::Write,
10767 );
10768 }
10769 QueryExpr::DropQueue(q) => {
10770 return self.check_ddl_object_privilege(
10771 &auth_store,
10772 &principal_id,
10773 role,
10774 tenant.as_deref(),
10775 &username,
10776 "drop",
10777 "collection",
10778 &q.name,
10779 crate::auth::Role::Write,
10780 );
10781 }
10782 QueryExpr::CreateTree(q) => {
10783 return self.check_ddl_object_privilege(
10784 &auth_store,
10785 &principal_id,
10786 role,
10787 tenant.as_deref(),
10788 &username,
10789 "create",
10790 "collection",
10791 &q.collection,
10792 crate::auth::Role::Write,
10793 );
10794 }
10795 QueryExpr::DropTree(q) => {
10796 return self.check_ddl_object_privilege(
10797 &auth_store,
10798 &principal_id,
10799 role,
10800 tenant.as_deref(),
10801 &username,
10802 "drop",
10803 "collection",
10804 &q.collection,
10805 crate::auth::Role::Write,
10806 );
10807 }
10808 QueryExpr::CreateMigration(q) => {
10812 return self.check_ddl_object_privilege(
10813 &auth_store,
10814 &principal_id,
10815 role,
10816 tenant.as_deref(),
10817 &username,
10818 "schema:write",
10819 "schema",
10820 &q.name,
10821 crate::auth::Role::Write,
10822 );
10823 }
10824 QueryExpr::ApplyMigration(_) | QueryExpr::RollbackMigration(_) => {
10826 return if role == crate::auth::Role::Admin {
10827 Ok(())
10828 } else {
10829 Err(format!(
10830 "principal=`{}` role=`{:?}` cannot issue APPLY/ROLLBACK MIGRATION",
10831 username, role
10832 ))
10833 };
10834 }
10835 QueryExpr::ExplainMigration(_) => return Ok(()),
10837 _ => return Ok(()),
10841 };
10842
10843 if auth_store.iam_authorization_enabled() {
10844 let iam_action = legacy_action_to_iam(action);
10845 let iam_resource = legacy_resource_to_iam(&resource, tenant.as_deref());
10846 let iam_ctx = runtime_iam_context(role, tenant.as_deref());
10847 if !auth_store.check_policy_authz_with_role(
10848 &principal_id,
10849 iam_action,
10850 &iam_resource,
10851 &iam_ctx,
10852 role,
10853 ) {
10854 return Err(format!(
10855 "principal=`{}` action=`{}` resource=`{}:{}` denied by IAM policy",
10856 username, iam_action, iam_resource.kind, iam_resource.name
10857 ));
10858 }
10859
10860 if let QueryExpr::Table(table) = expr {
10861 self.check_table_column_projection_privilege(
10862 &auth_store,
10863 &principal_id,
10864 &iam_ctx,
10865 table,
10866 )?;
10867 }
10868
10869 if let QueryExpr::Update(update) = expr {
10870 let columns = update_set_target_columns(update);
10871 if !columns.is_empty() {
10872 let request = column_access_request_for_table_update(&update.table, columns);
10873 let outcome =
10874 auth_store.check_column_projection_authz(&principal_id, &request, &iam_ctx);
10875 if let Some(denied) = outcome.first_denied_column() {
10876 return Err(format!(
10877 "principal=`{}` action=`{}` resource=`{}:{}` denied by IAM column policy",
10878 username, iam_action, denied.resource.kind, denied.resource.name
10879 ));
10880 }
10881 if !outcome.allowed() {
10882 return Err(format!(
10883 "principal=`{}` action=`{}` resource=`{}:{}` denied by IAM policy",
10884 username,
10885 iam_action,
10886 outcome.table_resource.kind,
10887 outcome.table_resource.name
10888 ));
10889 }
10890 }
10891
10892 if let Some(columns) = update_returning_columns_for_policy(self, update) {
10893 let request = column_access_request_for_table_select(&update.table, columns);
10894 let outcome =
10895 auth_store.check_column_projection_authz(&principal_id, &request, &iam_ctx);
10896 if let Some(denied) = outcome.first_denied_column() {
10897 return Err(format!(
10898 "principal=`{}` action=`select` resource=`{}:{}` denied by IAM column policy",
10899 username, denied.resource.kind, denied.resource.name
10900 ));
10901 }
10902 if !outcome.allowed() {
10903 return Err(format!(
10904 "principal=`{}` action=`select` resource=`{}:{}` denied by IAM policy",
10905 username, outcome.table_resource.kind, outcome.table_resource.name
10906 ));
10907 }
10908 }
10909 }
10910
10911 Ok(())
10912 } else {
10913 auth_store
10914 .check_grant(&ctx, action, &resource)
10915 .map_err(|e| e.to_string())
10916 }
10917 }
10918
10919 fn check_table_column_projection_privilege(
10920 &self,
10921 auth_store: &Arc<crate::auth::store::AuthStore>,
10922 principal: &crate::auth::UserId,
10923 ctx: &crate::auth::policies::EvalContext,
10924 table: &crate::storage::query::ast::TableQuery,
10925 ) -> Result<(), String> {
10926 use crate::auth::{ColumnAccessRequest, ColumnDecisionEffect};
10927
10928 let columns = requested_table_columns_for_policy(table);
10929 if columns.is_empty() {
10930 return Ok(());
10931 }
10932
10933 let request = ColumnAccessRequest::select(table.table.clone(), columns);
10934 let outcome = auth_store.check_column_projection_authz(principal, &request, ctx);
10935 if outcome.allowed() {
10936 return Ok(());
10937 }
10938
10939 if !matches!(
10940 outcome.table_decision,
10941 crate::auth::policies::Decision::Allow { .. }
10942 | crate::auth::policies::Decision::AdminBypass
10943 ) {
10944 return Err(format!(
10945 "principal=`{}` action=`select` resource=`{}:{}` denied by IAM policy",
10946 principal, outcome.table_resource.kind, outcome.table_resource.name
10947 ));
10948 }
10949
10950 let denied = outcome
10951 .first_denied_column()
10952 .filter(|decision| decision.effective == ColumnDecisionEffect::Denied);
10953 match denied {
10954 Some(decision) => Err(format!(
10955 "principal=`{}` action=`select` resource=`{}:{}` denied by IAM policy",
10956 principal, decision.resource.kind, decision.resource.name
10957 )),
10958 None => Ok(()),
10959 }
10960 }
10961
10962 fn check_graph_property_projection_privilege(
10963 &self,
10964 auth_store: &Arc<crate::auth::store::AuthStore>,
10965 principal: &crate::auth::UserId,
10966 role: crate::auth::Role,
10967 tenant: Option<&str>,
10968 query: &crate::storage::query::ast::GraphQuery,
10969 ) -> Result<(), String> {
10970 let columns = explicit_graph_projection_properties(query);
10971 if columns.is_empty() {
10972 return Ok(());
10973 }
10974 self.check_table_like_column_projection_privilege(
10975 auth_store, principal, role, tenant, "graph", &columns,
10976 )
10977 }
10978
10979 fn check_table_like_column_projection_privilege(
10980 &self,
10981 auth_store: &Arc<crate::auth::store::AuthStore>,
10982 principal: &crate::auth::UserId,
10983 role: crate::auth::Role,
10984 tenant: Option<&str>,
10985 table: &str,
10986 columns: &[String],
10987 ) -> Result<(), String> {
10988 let iam_ctx = runtime_iam_context(role, tenant);
10989 let request =
10990 crate::auth::ColumnAccessRequest::select(table.to_string(), columns.iter().cloned());
10991 let outcome = auth_store.check_column_projection_authz(principal, &request, &iam_ctx);
10992 if outcome.allowed() {
10993 return Ok(());
10994 }
10995 let denied = outcome
10996 .first_denied_column()
10997 .map(|d| d.resource.name.clone())
10998 .unwrap_or_else(|| format!("{table}.<unknown>"));
10999 Err(format!(
11000 "principal=`{}` action=`select` resource=`column:{}` denied by IAM policy",
11001 principal, denied
11002 ))
11003 }
11004
11005 fn check_policy_management_privilege(
11006 &self,
11007 auth_store: &Arc<crate::auth::store::AuthStore>,
11008 principal: &crate::auth::UserId,
11009 role: crate::auth::Role,
11010 tenant: Option<&str>,
11011 action: &str,
11012 resource_kind: &str,
11013 resource_name: &str,
11014 ) -> Result<(), String> {
11015 let ctx = runtime_iam_context(role, tenant);
11016
11017 if !auth_store.iam_authorization_enabled() {
11018 return if role == crate::auth::Role::Admin {
11019 Ok(())
11020 } else {
11021 Err(format!(
11022 "principal=`{}` role=`{:?}` cannot issue ACL/auth DDL",
11023 principal, role
11024 ))
11025 };
11026 }
11027
11028 if resource_kind == "policy"
11029 && matches!(
11030 action,
11031 "policy:put" | "policy:drop" | "policy:attach" | "policy:detach"
11032 )
11033 && self
11034 .inner
11035 .config_registry
11036 .get_active(resource_name)
11037 .map(|entry| entry.managed)
11038 .unwrap_or(false)
11039 {
11040 return Ok(());
11041 }
11042
11043 let mut resource = crate::auth::policies::ResourceRef::new(
11044 resource_kind.to_string(),
11045 resource_name.to_string(),
11046 );
11047 if let Some(t) = tenant {
11048 resource = resource.with_tenant(t.to_string());
11049 }
11050 if auth_store.check_policy_authz_with_role(principal, action, &resource, &ctx, role) {
11051 Ok(())
11052 } else {
11053 Err(format!(
11054 "principal=`{}` action=`{}` resource=`{}:{}` denied by IAM policy",
11055 principal, action, resource.kind, resource.name
11056 ))
11057 }
11058 }
11059
11060 fn check_managed_config_write_for_set_config(&self, key: &str) -> RedDBResult<()> {
11061 let Some(auth_store) = self.inner.auth_store.read().clone() else {
11062 return Ok(());
11063 };
11064 let (username, role) = current_auth_identity()
11065 .unwrap_or_else(|| ("anonymous".to_string(), crate::auth::Role::Read));
11066 let tenant = current_tenant();
11067 let principal = crate::auth::UserId::from_parts(tenant.as_deref(), &username);
11068 let ctx = runtime_iam_context(role, tenant.as_deref());
11069 let gate = crate::auth::managed_config::ManagedConfigGate::new(
11070 self.inner.config_registry.as_ref(),
11071 );
11072 match gate.check_write(&auth_store, &principal, &ctx, key) {
11073 crate::auth::managed_config::ManagedConfigDecision::PassThrough { .. }
11074 | crate::auth::managed_config::ManagedConfigDecision::Allow { .. } => Ok(()),
11075 crate::auth::managed_config::ManagedConfigDecision::Deny { reason, .. } => {
11076 Err(RedDBError::Query(format!(
11077 "permission denied: managed config mutation blocked for `{key}`: {reason}"
11078 )))
11079 }
11080 }
11081 }
11082
11083 fn check_queue_op_privilege(
11099 &self,
11100 auth_store: &Arc<crate::auth::store::AuthStore>,
11101 principal: &crate::auth::UserId,
11102 role: crate::auth::Role,
11103 tenant: Option<&str>,
11104 action: &str,
11105 queue: &str,
11106 ) -> Result<(), String> {
11107 if !auth_store.iam_authorization_enabled() {
11108 return Ok(());
11109 }
11110 let mut resource =
11111 crate::auth::policies::ResourceRef::new("queue".to_string(), queue.to_string());
11112 if let Some(t) = tenant {
11113 resource = resource.with_tenant(t.to_string());
11114 }
11115 let ctx = runtime_iam_context(role, tenant);
11116 if auth_store.check_policy_authz_with_role(principal, action, &resource, &ctx, role) {
11117 Ok(())
11118 } else {
11119 Err(format!(
11120 "principal=`{}` action=`{}` resource=`queue:{}` denied by IAM policy",
11121 principal, action, queue
11122 ))
11123 }
11124 }
11125
11126 fn check_graph_op_privilege(
11146 &self,
11147 auth_store: &Arc<crate::auth::store::AuthStore>,
11148 principal: &crate::auth::UserId,
11149 role: crate::auth::Role,
11150 tenant: Option<&str>,
11151 action: &str,
11152 ) -> Result<(), String> {
11153 if !auth_store.iam_authorization_enabled() {
11154 return Ok(());
11155 }
11156 let mut resource =
11157 crate::auth::policies::ResourceRef::new("graph".to_string(), "*".to_string());
11158 if let Some(t) = tenant {
11159 resource = resource.with_tenant(t.to_string());
11160 }
11161 let ctx = runtime_iam_context(role, tenant);
11162 if auth_store.check_policy_authz_with_role(principal, action, &resource, &ctx, role) {
11163 Ok(())
11164 } else {
11165 Err(format!(
11166 "principal=`{}` action=`{}` resource=`graph:*` denied by IAM policy",
11167 principal, action
11168 ))
11169 }
11170 }
11171
11172 fn check_vector_op_privilege(
11187 &self,
11188 auth_store: &Arc<crate::auth::store::AuthStore>,
11189 principal: &crate::auth::UserId,
11190 role: crate::auth::Role,
11191 tenant: Option<&str>,
11192 action: &str,
11193 collection: &str,
11194 ) -> Result<(), String> {
11195 if !auth_store.iam_authorization_enabled() {
11196 return Ok(());
11197 }
11198 let mut resource =
11199 crate::auth::policies::ResourceRef::new("vector".to_string(), collection.to_string());
11200 if let Some(t) = tenant {
11201 resource = resource.with_tenant(t.to_string());
11202 }
11203 let ctx = runtime_iam_context(role, tenant);
11204 if auth_store.check_policy_authz_with_role(principal, action, &resource, &ctx, role) {
11205 Ok(())
11206 } else {
11207 Err(format!(
11208 "principal=`{}` action=`{}` resource=`vector:{}` denied by IAM policy",
11209 principal, action, collection
11210 ))
11211 }
11212 }
11213
11214 fn check_ddl_collection_privilege(
11220 &self,
11221 auth_store: &Arc<crate::auth::store::AuthStore>,
11222 principal: &crate::auth::UserId,
11223 role: crate::auth::Role,
11224 tenant: Option<&str>,
11225 username: &str,
11226 action: &str,
11227 collection: &str,
11228 ) -> Result<(), String> {
11229 self.check_ddl_object_privilege(
11230 auth_store,
11231 principal,
11232 role,
11233 tenant,
11234 username,
11235 action,
11236 "collection",
11237 collection,
11238 crate::auth::Role::Write,
11239 )
11240 }
11241
11242 #[allow(clippy::too_many_arguments)]
11260 fn check_ddl_object_privilege(
11261 &self,
11262 auth_store: &Arc<crate::auth::store::AuthStore>,
11263 principal: &crate::auth::UserId,
11264 role: crate::auth::Role,
11265 tenant: Option<&str>,
11266 username: &str,
11267 action: &str,
11268 resource_kind: &str,
11269 resource_name: &str,
11270 min_role: crate::auth::Role,
11271 ) -> Result<(), String> {
11272 if role < min_role {
11273 let msg = format!(
11274 "principal=`{}` role=`{:?}` cannot issue DDL action=`{}` resource=`{}:{}`",
11275 username, role, action, resource_kind, resource_name
11276 );
11277 self.inner.audit_log.record(
11278 action,
11279 username,
11280 resource_name,
11281 "denied",
11282 crate::json::Value::Null,
11283 );
11284 return Err(msg);
11285 }
11286
11287 if !auth_store.iam_authorization_enabled() {
11288 self.inner.audit_log.record(
11289 action,
11290 username,
11291 resource_name,
11292 "ok",
11293 crate::json::Value::Null,
11294 );
11295 return Ok(());
11296 }
11297
11298 let mut resource = crate::auth::policies::ResourceRef::new(
11299 resource_kind.to_string(),
11300 resource_name.to_string(),
11301 );
11302 if let Some(t) = tenant {
11303 resource = resource.with_tenant(t.to_string());
11304 }
11305 let ctx = runtime_iam_context(role, tenant);
11306 if auth_store.check_policy_authz_with_role(principal, action, &resource, &ctx, role) {
11307 self.inner.audit_log.record(
11308 action,
11309 username,
11310 resource_name,
11311 "ok",
11312 crate::json::Value::Null,
11313 );
11314 Ok(())
11315 } else {
11316 self.inner.audit_log.record(
11317 action,
11318 username,
11319 resource_name,
11320 "denied",
11321 crate::json::Value::Null,
11322 );
11323 Err(format!(
11324 "principal=`{}` action=`{}` resource=`{}:{}` denied by IAM policy",
11325 username, action, resource_kind, resource_name
11326 ))
11327 }
11328 }
11329
11330 fn execute_grant_statement(
11332 &self,
11333 query: &str,
11334 stmt: &crate::storage::query::ast::GrantStmt,
11335 ) -> RedDBResult<RuntimeQueryResult> {
11336 use crate::auth::privileges::{Action, GrantPrincipal, Resource};
11337 use crate::auth::UserId;
11338 use crate::storage::query::ast::{GrantObjectKind, GrantPrincipalRef};
11339
11340 let auth_store = self
11341 .inner
11342 .auth_store
11343 .read()
11344 .clone()
11345 .ok_or_else(|| RedDBError::Query("auth store not configured".to_string()))?;
11346
11347 let (gname, grole) = current_auth_identity().ok_or_else(|| {
11349 RedDBError::Query("GRANT requires an authenticated principal".to_string())
11350 })?;
11351 let granter = UserId::from_parts(current_tenant().as_deref(), &gname);
11352 let granter_role = grole;
11353
11354 let mut actions: Vec<Action> = Vec::new();
11356 if stmt.all {
11357 actions.push(Action::All);
11358 } else {
11359 for kw in &stmt.actions {
11360 let a = Action::from_keyword(kw).ok_or_else(|| {
11361 RedDBError::Query(format!("unknown privilege keyword `{}`", kw))
11362 })?;
11363 actions.push(a);
11364 }
11365 }
11366
11367 let mut applied = 0usize;
11369 for obj in &stmt.objects {
11370 let resource = match stmt.object_kind {
11371 GrantObjectKind::Table => Resource::Table {
11372 schema: obj.schema.clone(),
11373 table: obj.name.clone(),
11374 },
11375 GrantObjectKind::Schema => Resource::Schema(obj.name.clone()),
11376 GrantObjectKind::Database => Resource::Database,
11377 GrantObjectKind::Function => Resource::Function {
11378 schema: obj.schema.clone(),
11379 name: obj.name.clone(),
11380 },
11381 };
11382 for principal in &stmt.principals {
11383 let p = match principal {
11384 GrantPrincipalRef::Public => GrantPrincipal::Public,
11385 GrantPrincipalRef::Group(g) => GrantPrincipal::Group(g.clone()),
11386 GrantPrincipalRef::User { tenant, name } => {
11387 GrantPrincipal::User(UserId::from_parts(tenant.as_deref(), name))
11388 }
11389 };
11390 let tenant = granter.tenant.clone();
11393 auth_store
11394 .grant(
11395 &granter,
11396 granter_role,
11397 p.clone(),
11398 resource.clone(),
11399 actions.clone(),
11400 stmt.with_grant_option,
11401 tenant.clone(),
11402 )
11403 .map_err(|e| RedDBError::Query(e.to_string()))?;
11404
11405 if let Some(policy) =
11409 grant_to_iam_policy(&p, &resource, &actions, tenant.as_deref())
11410 {
11411 let pid = policy.id.clone();
11412 auth_store
11413 .put_policy_internal(policy)
11414 .map_err(|e| RedDBError::Query(e.to_string()))?;
11415 let attachment = match &p {
11416 GrantPrincipal::User(uid) => {
11417 crate::auth::store::PrincipalRef::User(uid.clone())
11418 }
11419 GrantPrincipal::Group(group) => {
11420 crate::auth::store::PrincipalRef::Group(group.clone())
11421 }
11422 GrantPrincipal::Public => crate::auth::store::PrincipalRef::Group(
11423 crate::auth::store::PUBLIC_IAM_GROUP.to_string(),
11424 ),
11425 };
11426 auth_store
11427 .attach_policy(attachment, &pid)
11428 .map_err(|e| RedDBError::Query(e.to_string()))?;
11429 }
11430 applied += 1;
11431 tracing::info!(
11432 target: "audit",
11433 principal = %granter,
11434 action = "grant",
11435 "GRANT applied"
11436 );
11437 }
11438 }
11439
11440 self.invalidate_result_cache();
11441 Ok(RuntimeQueryResult::ok_message(
11442 query.to_string(),
11443 &format!("GRANT applied to {} target(s)", applied),
11444 "grant",
11445 ))
11446 }
11447
11448 fn execute_revoke_statement(
11450 &self,
11451 query: &str,
11452 stmt: &crate::storage::query::ast::RevokeStmt,
11453 ) -> RedDBResult<RuntimeQueryResult> {
11454 use crate::auth::privileges::{Action, GrantPrincipal, Resource};
11455 use crate::auth::UserId;
11456 use crate::storage::query::ast::{GrantObjectKind, GrantPrincipalRef};
11457
11458 let auth_store = self
11459 .inner
11460 .auth_store
11461 .read()
11462 .clone()
11463 .ok_or_else(|| RedDBError::Query("auth store not configured".to_string()))?;
11464
11465 let (_gname, grole) = current_auth_identity().ok_or_else(|| {
11466 RedDBError::Query("REVOKE requires an authenticated principal".to_string())
11467 })?;
11468 let granter_role = grole;
11469
11470 let actions: Vec<Action> = if stmt.all {
11471 vec![Action::All]
11472 } else {
11473 stmt.actions
11474 .iter()
11475 .map(|kw| Action::from_keyword(kw).unwrap_or(Action::Select))
11476 .collect()
11477 };
11478
11479 let mut total_removed = 0usize;
11480 for obj in &stmt.objects {
11481 let resource = match stmt.object_kind {
11482 GrantObjectKind::Table => Resource::Table {
11483 schema: obj.schema.clone(),
11484 table: obj.name.clone(),
11485 },
11486 GrantObjectKind::Schema => Resource::Schema(obj.name.clone()),
11487 GrantObjectKind::Database => Resource::Database,
11488 GrantObjectKind::Function => Resource::Function {
11489 schema: obj.schema.clone(),
11490 name: obj.name.clone(),
11491 },
11492 };
11493 for principal in &stmt.principals {
11494 let p = match principal {
11495 GrantPrincipalRef::Public => GrantPrincipal::Public,
11496 GrantPrincipalRef::Group(g) => GrantPrincipal::Group(g.clone()),
11497 GrantPrincipalRef::User { tenant, name } => {
11498 GrantPrincipal::User(UserId::from_parts(tenant.as_deref(), name))
11499 }
11500 };
11501 let removed = auth_store
11502 .revoke(granter_role, &p, &resource, &actions)
11503 .map_err(|e| RedDBError::Query(e.to_string()))?;
11504 let _removed_policies =
11505 auth_store.delete_synthetic_grant_policies(&p, &resource, &actions);
11506 total_removed += removed;
11507 }
11508 }
11509
11510 self.invalidate_result_cache();
11511 Ok(RuntimeQueryResult::ok_message(
11512 query.to_string(),
11513 &format!("REVOKE removed {} grant(s)", total_removed),
11514 "revoke",
11515 ))
11516 }
11517
11518 fn execute_create_user_statement(
11520 &self,
11521 query: &str,
11522 stmt: &crate::storage::query::ast::CreateUserStmt,
11523 ) -> RedDBResult<RuntimeQueryResult> {
11524 let auth_store = self
11525 .inner
11526 .auth_store
11527 .read()
11528 .clone()
11529 .ok_or_else(|| RedDBError::Query("auth store not configured".to_string()))?;
11530
11531 let (_gname, grole) = current_auth_identity().ok_or_else(|| {
11532 RedDBError::Query("CREATE USER requires an authenticated principal".to_string())
11533 })?;
11534 if grole != crate::auth::Role::Admin {
11535 return Err(RedDBError::Query(
11536 "CREATE USER requires Admin role".to_string(),
11537 ));
11538 }
11539
11540 let role = crate::auth::Role::from_str(&stmt.role)
11541 .ok_or_else(|| RedDBError::Query(format!("invalid role `{}`", stmt.role)))?;
11542 let user = auth_store
11543 .create_user_in_tenant(stmt.tenant.as_deref(), &stmt.username, &stmt.password, role)
11544 .map_err(|e| RedDBError::Query(e.to_string()))?;
11545
11546 self.invalidate_result_cache();
11547 let target = crate::auth::UserId::from_parts(user.tenant_id.as_deref(), &user.username);
11548 tracing::info!(
11549 target: "audit",
11550 principal = %target,
11551 role = %role,
11552 action = "create_user",
11553 "CREATE USER applied"
11554 );
11555
11556 Ok(RuntimeQueryResult::ok_message(
11557 query.to_string(),
11558 &format!("CREATE USER {} applied", target),
11559 "create_user",
11560 ))
11561 }
11562
11563 fn execute_alter_user_statement(
11565 &self,
11566 query: &str,
11567 stmt: &crate::storage::query::ast::AlterUserStmt,
11568 ) -> RedDBResult<RuntimeQueryResult> {
11569 use crate::auth::privileges::UserAttributes;
11570 use crate::auth::UserId;
11571 use crate::storage::query::ast::AlterUserAttribute;
11572
11573 let auth_store = self
11574 .inner
11575 .auth_store
11576 .read()
11577 .clone()
11578 .ok_or_else(|| RedDBError::Query("auth store not configured".to_string()))?;
11579
11580 let (_gname, grole) = current_auth_identity().ok_or_else(|| {
11581 RedDBError::Query("ALTER USER requires an authenticated principal".to_string())
11582 })?;
11583 if grole != crate::auth::Role::Admin {
11584 return Err(RedDBError::Query(
11585 "ALTER USER requires Admin role".to_string(),
11586 ));
11587 }
11588
11589 let target = UserId::from_parts(stmt.tenant.as_deref(), &stmt.username);
11590
11591 let mut attrs = auth_store.user_attributes(&target);
11594 let mut enable_change: Option<bool> = None;
11595
11596 for a in &stmt.attributes {
11597 match a {
11598 AlterUserAttribute::ValidUntil(ts) => {
11599 let ms = parse_timestamp_to_ms(ts).ok_or_else(|| {
11603 RedDBError::Query(format!("invalid VALID UNTIL timestamp `{ts}`"))
11604 })?;
11605 attrs.valid_until = Some(ms);
11606 }
11607 AlterUserAttribute::ConnectionLimit(n) => {
11608 if *n < 0 {
11609 return Err(RedDBError::Query(
11610 "CONNECTION LIMIT must be non-negative".to_string(),
11611 ));
11612 }
11613 attrs.connection_limit = Some(*n as u32);
11614 }
11615 AlterUserAttribute::SetSearchPath(p) => {
11616 attrs.search_path = Some(p.clone());
11617 }
11618 AlterUserAttribute::AddGroup(g) => {
11619 if !attrs.groups.iter().any(|existing| existing == g) {
11620 attrs.groups.push(g.clone());
11621 attrs.groups.sort();
11622 }
11623 }
11624 AlterUserAttribute::DropGroup(g) => {
11625 attrs.groups.retain(|existing| existing != g);
11626 }
11627 AlterUserAttribute::Enable => enable_change = Some(true),
11628 AlterUserAttribute::Disable => enable_change = Some(false),
11629 AlterUserAttribute::Password(_) => {
11630 }
11634 }
11635 }
11636
11637 auth_store
11638 .set_user_attributes(&target, attrs)
11639 .map_err(|e| RedDBError::Query(e.to_string()))?;
11640 if let Some(en) = enable_change {
11641 auth_store
11642 .set_user_enabled(&target, en)
11643 .map_err(|e| RedDBError::Query(e.to_string()))?;
11644 }
11645 self.invalidate_result_cache();
11646 tracing::info!(
11647 target: "audit",
11648 principal = %target,
11649 action = "alter_user",
11650 "ALTER USER applied"
11651 );
11652
11653 Ok(RuntimeQueryResult::ok_message(
11654 query.to_string(),
11655 &format!("ALTER USER {} applied", target),
11656 "alter_user",
11657 ))
11658 }
11659
11660 fn execute_create_iam_policy(
11665 &self,
11666 query: &str,
11667 id: &str,
11668 json: &str,
11669 ) -> RedDBResult<RuntimeQueryResult> {
11670 use crate::auth::policies::Policy;
11671
11672 let auth_store = self
11673 .inner
11674 .auth_store
11675 .read()
11676 .clone()
11677 .ok_or_else(|| RedDBError::Query("auth store not configured".to_string()))?;
11678
11679 let mut policy = Policy::from_json_str(json)
11684 .map_err(|e| RedDBError::Query(format!("policy parse: {e}")))?;
11685 if policy.id != id {
11686 policy.id = id.to_string();
11687 }
11688 let pid = policy.id.clone();
11689 let tenant = current_tenant();
11690 let (actor_name, actor_role) = current_auth_identity()
11691 .unwrap_or_else(|| ("anonymous".to_string(), crate::auth::Role::Read));
11692 let actor = crate::auth::UserId::from_parts(tenant.as_deref(), &actor_name);
11693 let eval_ctx = runtime_iam_context(actor_role, tenant.as_deref());
11694 let event_ctx = self.policy_mutation_control_ctx(&actor, tenant.as_deref());
11695 let ledger = self.inner.control_event_ledger.read();
11696 let control = crate::auth::store::PolicyMutationControl {
11697 ctx: &event_ctx,
11698 ledger: ledger.as_ref(),
11699 config: self.inner.control_event_config,
11700 registry: Some(self.inner.config_registry.as_ref()),
11701 actor: &actor,
11702 eval_ctx: &eval_ctx,
11703 };
11704 auth_store
11705 .put_policy_with_control_events(policy, &control)
11706 .map_err(|e| RedDBError::Query(e.to_string()))?;
11707
11708 let principal = actor_name;
11709 tracing::info!(
11710 target: "audit",
11711 principal = %principal,
11712 action = "iam:policy.put",
11713 matched_policy_id = %pid,
11714 "CREATE POLICY applied"
11715 );
11716 self.inner.audit_log.record(
11717 "iam/policy.put",
11718 &principal,
11719 &pid,
11720 "ok",
11721 crate::json::Value::Null,
11722 );
11723
11724 self.invalidate_result_cache();
11725 Ok(RuntimeQueryResult::ok_message(
11726 query.to_string(),
11727 &format!("policy `{pid}` stored"),
11728 "create_iam_policy",
11729 ))
11730 }
11731
11732 fn execute_drop_iam_policy(&self, query: &str, id: &str) -> RedDBResult<RuntimeQueryResult> {
11733 let auth_store = self
11734 .inner
11735 .auth_store
11736 .read()
11737 .clone()
11738 .ok_or_else(|| RedDBError::Query("auth store not configured".to_string()))?;
11739 let tenant = current_tenant();
11740 let (actor_name, actor_role) = current_auth_identity()
11741 .unwrap_or_else(|| ("anonymous".to_string(), crate::auth::Role::Read));
11742 let actor = crate::auth::UserId::from_parts(tenant.as_deref(), &actor_name);
11743 let eval_ctx = runtime_iam_context(actor_role, tenant.as_deref());
11744 let event_ctx = self.policy_mutation_control_ctx(&actor, tenant.as_deref());
11745 let ledger = self.inner.control_event_ledger.read();
11746 let control = crate::auth::store::PolicyMutationControl {
11747 ctx: &event_ctx,
11748 ledger: ledger.as_ref(),
11749 config: self.inner.control_event_config,
11750 registry: Some(self.inner.config_registry.as_ref()),
11751 actor: &actor,
11752 eval_ctx: &eval_ctx,
11753 };
11754 auth_store
11755 .delete_policy_with_control_events(id, &control)
11756 .map_err(|e| RedDBError::Query(e.to_string()))?;
11757
11758 let principal = actor_name;
11759 tracing::info!(
11760 target: "audit",
11761 principal = %principal,
11762 action = "iam:policy.drop",
11763 matched_policy_id = %id,
11764 "DROP POLICY applied"
11765 );
11766 self.inner.audit_log.record(
11767 "iam/policy.drop",
11768 &principal,
11769 id,
11770 "ok",
11771 crate::json::Value::Null,
11772 );
11773
11774 self.invalidate_result_cache();
11775 Ok(RuntimeQueryResult::ok_message(
11776 query.to_string(),
11777 &format!("policy `{id}` dropped"),
11778 "drop_iam_policy",
11779 ))
11780 }
11781
11782 fn execute_attach_policy(
11783 &self,
11784 query: &str,
11785 policy_id: &str,
11786 principal: &crate::storage::query::ast::PolicyPrincipalRef,
11787 ) -> RedDBResult<RuntimeQueryResult> {
11788 use crate::auth::store::PrincipalRef;
11789 use crate::auth::UserId;
11790 use crate::storage::query::ast::PolicyPrincipalRef;
11791
11792 let auth_store = self
11793 .inner
11794 .auth_store
11795 .read()
11796 .clone()
11797 .ok_or_else(|| RedDBError::Query("auth store not configured".to_string()))?;
11798 let p = match principal {
11799 PolicyPrincipalRef::User(u) => {
11800 PrincipalRef::User(UserId::from_parts(u.tenant.as_deref(), &u.username))
11801 }
11802 PolicyPrincipalRef::Group(g) => PrincipalRef::Group(g.clone()),
11803 };
11804 let pretty_target = principal_label(principal);
11805 let tenant = current_tenant();
11806 let (actor_name, actor_role) = current_auth_identity()
11807 .unwrap_or_else(|| ("anonymous".to_string(), crate::auth::Role::Read));
11808 let actor = crate::auth::UserId::from_parts(tenant.as_deref(), &actor_name);
11809 let eval_ctx = runtime_iam_context(actor_role, tenant.as_deref());
11810 let event_ctx = self.policy_mutation_control_ctx(&actor, tenant.as_deref());
11811 let ledger = self.inner.control_event_ledger.read();
11812 let control = crate::auth::store::PolicyMutationControl {
11813 ctx: &event_ctx,
11814 ledger: ledger.as_ref(),
11815 config: self.inner.control_event_config,
11816 registry: Some(self.inner.config_registry.as_ref()),
11817 actor: &actor,
11818 eval_ctx: &eval_ctx,
11819 };
11820 auth_store
11821 .attach_policy_with_control_events(p, policy_id, &control)
11822 .map_err(|e| RedDBError::Query(e.to_string()))?;
11823
11824 let principal_str = actor_name;
11825 tracing::info!(
11826 target: "audit",
11827 principal = %principal_str,
11828 action = "iam:policy.attach",
11829 matched_policy_id = %policy_id,
11830 target = %pretty_target,
11831 "ATTACH POLICY applied"
11832 );
11833 self.inner.audit_log.record(
11834 "iam/policy.attach",
11835 &principal_str,
11836 &pretty_target,
11837 "ok",
11838 crate::json::Value::Null,
11839 );
11840
11841 self.invalidate_result_cache();
11842 Ok(RuntimeQueryResult::ok_message(
11843 query.to_string(),
11844 &format!("policy `{policy_id}` attached to {pretty_target}"),
11845 "attach_policy",
11846 ))
11847 }
11848
11849 fn execute_detach_policy(
11850 &self,
11851 query: &str,
11852 policy_id: &str,
11853 principal: &crate::storage::query::ast::PolicyPrincipalRef,
11854 ) -> RedDBResult<RuntimeQueryResult> {
11855 use crate::auth::store::PrincipalRef;
11856 use crate::auth::UserId;
11857 use crate::storage::query::ast::PolicyPrincipalRef;
11858
11859 let auth_store = self
11860 .inner
11861 .auth_store
11862 .read()
11863 .clone()
11864 .ok_or_else(|| RedDBError::Query("auth store not configured".to_string()))?;
11865 let p = match principal {
11866 PolicyPrincipalRef::User(u) => {
11867 PrincipalRef::User(UserId::from_parts(u.tenant.as_deref(), &u.username))
11868 }
11869 PolicyPrincipalRef::Group(g) => PrincipalRef::Group(g.clone()),
11870 };
11871 let pretty_target = principal_label(principal);
11872 let tenant = current_tenant();
11873 let (actor_name, actor_role) = current_auth_identity()
11874 .unwrap_or_else(|| ("anonymous".to_string(), crate::auth::Role::Read));
11875 let actor = crate::auth::UserId::from_parts(tenant.as_deref(), &actor_name);
11876 let eval_ctx = runtime_iam_context(actor_role, tenant.as_deref());
11877 let event_ctx = self.policy_mutation_control_ctx(&actor, tenant.as_deref());
11878 let ledger = self.inner.control_event_ledger.read();
11879 let control = crate::auth::store::PolicyMutationControl {
11880 ctx: &event_ctx,
11881 ledger: ledger.as_ref(),
11882 config: self.inner.control_event_config,
11883 registry: Some(self.inner.config_registry.as_ref()),
11884 actor: &actor,
11885 eval_ctx: &eval_ctx,
11886 };
11887 auth_store
11888 .detach_policy_with_control_events(p, policy_id, &control)
11889 .map_err(|e| RedDBError::Query(e.to_string()))?;
11890
11891 let principal_str = actor_name;
11892 tracing::info!(
11893 target: "audit",
11894 principal = %principal_str,
11895 action = "iam:policy.detach",
11896 matched_policy_id = %policy_id,
11897 target = %pretty_target,
11898 "DETACH POLICY applied"
11899 );
11900 self.inner.audit_log.record(
11901 "iam/policy.detach",
11902 &principal_str,
11903 &pretty_target,
11904 "ok",
11905 crate::json::Value::Null,
11906 );
11907
11908 self.invalidate_result_cache();
11909 Ok(RuntimeQueryResult::ok_message(
11910 query.to_string(),
11911 &format!("policy `{policy_id}` detached from {pretty_target}"),
11912 "detach_policy",
11913 ))
11914 }
11915
11916 fn execute_show_policies(
11917 &self,
11918 query: &str,
11919 filter: Option<&crate::storage::query::ast::PolicyPrincipalRef>,
11920 ) -> RedDBResult<RuntimeQueryResult> {
11921 use crate::auth::UserId;
11922 use crate::storage::query::ast::PolicyPrincipalRef;
11923 use crate::storage::query::unified::UnifiedRecord;
11924 use crate::storage::schema::Value as SchemaValue;
11925 use std::sync::Arc;
11926
11927 let auth_store = self
11928 .inner
11929 .auth_store
11930 .read()
11931 .clone()
11932 .ok_or_else(|| RedDBError::Query("auth store not configured".to_string()))?;
11933
11934 let pols = match filter {
11935 None => auth_store.list_policies(),
11936 Some(PolicyPrincipalRef::User(u)) => {
11937 let id = UserId::from_parts(u.tenant.as_deref(), &u.username);
11938 auth_store.effective_policies(&id)
11939 }
11940 Some(PolicyPrincipalRef::Group(g)) => auth_store.group_policies(g),
11941 };
11942
11943 let mut records = Vec::with_capacity(pols.len() + 1);
11944
11945 let mode = auth_store.enforcement_mode();
11950 let mut header = UnifiedRecord::default();
11951 header.set_arc(
11952 Arc::from("id"),
11953 SchemaValue::text("<enforcement_mode>".to_string()),
11954 );
11955 header.set_arc(Arc::from("statements"), SchemaValue::Integer(0));
11956 header.set_arc(Arc::from("tenant"), SchemaValue::Null);
11957 let header_json = format!(
11958 r#"{{"enforcement_mode":"{}","policy_only_hard_version":"{}"}}"#,
11959 mode.as_str(),
11960 crate::auth::enforcement_mode::POLICY_ONLY_HARD_VERSION
11961 );
11962 header.set_arc(Arc::from("json"), SchemaValue::text(header_json));
11963 records.push(header);
11964
11965 for p in pols.iter() {
11966 let mut rec = UnifiedRecord::default();
11967 rec.set_arc(Arc::from("id"), SchemaValue::text(p.id.clone()));
11968 rec.set_arc(
11969 Arc::from("statements"),
11970 SchemaValue::Integer(p.statements.len() as i64),
11971 );
11972 rec.set_arc(
11973 Arc::from("tenant"),
11974 p.tenant
11975 .as_deref()
11976 .map(|t| SchemaValue::text(t.to_string()))
11977 .unwrap_or(SchemaValue::Null),
11978 );
11979 rec.set_arc(Arc::from("json"), SchemaValue::text(p.to_json_string()));
11980 records.push(rec);
11981 }
11982 let mut result = crate::storage::query::unified::UnifiedResult::empty();
11983 result.records = records;
11984 Ok(RuntimeQueryResult {
11985 query: query.to_string(),
11986 mode: crate::storage::query::modes::QueryMode::Sql,
11987 statement: "show_policies",
11988 engine: "iam-policies",
11989 result,
11990 affected_rows: 0,
11991 statement_type: "select",
11992 bookmark: None,
11993 })
11994 }
11995
11996 fn execute_show_effective_permissions(
11997 &self,
11998 query: &str,
11999 user: &crate::storage::query::ast::PolicyUserRef,
12000 resource: Option<&crate::storage::query::ast::PolicyResourceRef>,
12001 ) -> RedDBResult<RuntimeQueryResult> {
12002 use crate::auth::UserId;
12003 use crate::storage::query::unified::UnifiedRecord;
12004 use crate::storage::schema::Value as SchemaValue;
12005 use std::sync::Arc;
12006
12007 let auth_store = self
12008 .inner
12009 .auth_store
12010 .read()
12011 .clone()
12012 .ok_or_else(|| RedDBError::Query("auth store not configured".to_string()))?;
12013 let id = UserId::from_parts(user.tenant.as_deref(), &user.username);
12014 let pols = auth_store.effective_policies(&id);
12015
12016 let mut records = Vec::new();
12019 for p in pols.iter() {
12020 for (idx, st) in p.statements.iter().enumerate() {
12021 if let Some(_r) = resource {
12022 }
12026 let mut rec = UnifiedRecord::default();
12027 rec.set_arc(Arc::from("policy_id"), SchemaValue::text(p.id.clone()));
12028 rec.set_arc(
12029 Arc::from("statement_index"),
12030 SchemaValue::Integer(idx as i64),
12031 );
12032 rec.set_arc(
12033 Arc::from("sid"),
12034 st.sid
12035 .as_deref()
12036 .map(|s| SchemaValue::text(s.to_string()))
12037 .unwrap_or(SchemaValue::Null),
12038 );
12039 rec.set_arc(
12040 Arc::from("effect"),
12041 SchemaValue::text(match st.effect {
12042 crate::auth::policies::Effect::Allow => "allow",
12043 crate::auth::policies::Effect::Deny => "deny",
12044 }),
12045 );
12046 rec.set_arc(
12047 Arc::from("actions"),
12048 SchemaValue::Integer(st.actions.len() as i64),
12049 );
12050 rec.set_arc(
12051 Arc::from("resources"),
12052 SchemaValue::Integer(st.resources.len() as i64),
12053 );
12054 records.push(rec);
12055 }
12056 }
12057 let mut result = crate::storage::query::unified::UnifiedResult::empty();
12058 result.records = records;
12059 Ok(RuntimeQueryResult {
12060 query: query.to_string(),
12061 mode: crate::storage::query::modes::QueryMode::Sql,
12062 statement: "show_effective_permissions",
12063 engine: "iam-policies",
12064 result,
12065 affected_rows: 0,
12066 statement_type: "select",
12067 bookmark: None,
12068 })
12069 }
12070
12071 fn execute_lint_policy(
12072 &self,
12073 query: &str,
12074 source: &crate::storage::query::ast::LintPolicySource,
12075 ) -> RedDBResult<RuntimeQueryResult> {
12076 use crate::auth::policy_linter::lint;
12077 use crate::storage::query::ast::LintPolicySource;
12078 use crate::storage::query::unified::UnifiedRecord;
12079 use crate::storage::schema::Value as SchemaValue;
12080 use std::sync::Arc;
12081
12082 let policy_text = match source {
12087 LintPolicySource::Json(text) => text.clone(),
12088 LintPolicySource::Id(id) => {
12089 let auth_store =
12090 self.inner.auth_store.read().clone().ok_or_else(|| {
12091 RedDBError::Query("auth store not configured".to_string())
12092 })?;
12093 let policy = auth_store
12094 .get_policy(id)
12095 .ok_or_else(|| RedDBError::Query(format!("policy `{id}` not found")))?;
12096 policy.to_json_string()
12097 }
12098 };
12099 let diagnostics = lint(&policy_text);
12100
12101 let principal_str = current_auth_identity()
12102 .map(|(u, _)| u)
12103 .unwrap_or_else(|| "anonymous".into());
12104 tracing::info!(
12105 target: "audit",
12106 principal = %principal_str,
12107 action = "iam:policy.lint",
12108 diagnostic_count = diagnostics.len(),
12109 "LINT POLICY issued"
12110 );
12111 self.inner.audit_log.record(
12112 "iam/policy.lint",
12113 &principal_str,
12114 match source {
12115 LintPolicySource::Id(id) => id.as_str(),
12116 LintPolicySource::Json(_) => "<json>",
12117 },
12118 "ok",
12119 crate::json::Value::Null,
12120 );
12121
12122 const COLUMNS: [&str; 5] = ["severity", "code", "message", "suggested_fix", "location"];
12125 let schema = Arc::new(
12126 COLUMNS
12127 .iter()
12128 .map(|name| Arc::<str>::from(*name))
12129 .collect::<Vec<_>>(),
12130 );
12131 let records: Vec<UnifiedRecord> = diagnostics
12132 .iter()
12133 .map(|d| {
12134 UnifiedRecord::with_schema(
12135 Arc::clone(&schema),
12136 vec![
12137 SchemaValue::text(d.severity.as_str()),
12138 SchemaValue::text(d.code.as_str()),
12139 SchemaValue::text(d.message.clone()),
12140 d.suggested_fix
12141 .as_deref()
12142 .map(SchemaValue::text)
12143 .unwrap_or(SchemaValue::Null),
12144 d.location
12145 .as_deref()
12146 .map(SchemaValue::text)
12147 .unwrap_or(SchemaValue::Null),
12148 ],
12149 )
12150 })
12151 .collect();
12152 let mut result = crate::storage::query::unified::UnifiedResult::with_columns(
12153 COLUMNS.iter().map(|c| c.to_string()).collect(),
12154 );
12155 result.records = records;
12156 Ok(RuntimeQueryResult {
12157 query: query.to_string(),
12158 mode: crate::storage::query::modes::QueryMode::Sql,
12159 statement: "lint_policy",
12160 engine: "iam-policies",
12161 result,
12162 affected_rows: 0,
12163 statement_type: "select",
12164 bookmark: None,
12165 })
12166 }
12167
12168 fn execute_migrate_policy_mode(
12173 &self,
12174 query: &str,
12175 target: &str,
12176 dry_run: bool,
12177 ) -> RedDBResult<RuntimeQueryResult> {
12178 use crate::auth::enforcement_mode::PolicyEnforcementMode;
12179 use crate::auth::migrate_policy_mode::{
12180 principal_label, simulate_migration_delta, MigratePolicyDelta,
12181 };
12182 use crate::auth::policies::ResourceRef;
12183 use crate::storage::query::unified::UnifiedRecord;
12184 use crate::storage::schema::Value as SchemaValue;
12185 use std::sync::Arc;
12186
12187 let parsed = PolicyEnforcementMode::parse(target).ok_or_else(|| {
12192 RedDBError::Query(format!(
12193 "MIGRATE POLICY MODE: invalid target `{target}` (expected `policy_only`)"
12194 ))
12195 })?;
12196 if parsed != PolicyEnforcementMode::PolicyOnly {
12197 return Err(RedDBError::Query(format!(
12198 "MIGRATE POLICY MODE: target `{target}` is not supported — only `policy_only` may be migrated to via this command"
12199 )));
12200 }
12201
12202 let auth_store = self
12203 .inner
12204 .auth_store
12205 .read()
12206 .clone()
12207 .ok_or_else(|| RedDBError::Query("auth store not configured".to_string()))?;
12208
12209 let snapshot = self.inner.db.catalog_model_snapshot();
12217 let resources: Vec<ResourceRef> = snapshot
12218 .collections
12219 .iter()
12220 .map(|c| ResourceRef::new("table", c.name.clone()))
12221 .collect();
12222
12223 let now_ms = crate::utils::now_unix_millis() as u128;
12224 let deltas: Vec<MigratePolicyDelta> =
12225 simulate_migration_delta(auth_store.as_ref(), &resources, now_ms);
12226
12227 let principal_str = current_auth_identity()
12228 .map(|(u, _)| u)
12229 .unwrap_or_else(|| "anonymous".into());
12230
12231 let outcome_str = if dry_run {
12235 "dry_run"
12236 } else if deltas.is_empty() {
12237 "applied"
12238 } else {
12239 "refused"
12240 };
12241 tracing::info!(
12242 target: "audit",
12243 principal = %principal_str,
12244 action = "iam:policy.migrate_mode",
12245 target = %target,
12246 dry_run,
12247 delta_count = deltas.len(),
12248 outcome = outcome_str,
12249 "MIGRATE POLICY MODE issued"
12250 );
12251 self.inner.audit_log.record(
12252 "iam/policy.migrate_mode",
12253 &principal_str,
12254 target,
12255 outcome_str,
12256 crate::json::Value::Null,
12257 );
12258
12259 if !dry_run && !deltas.is_empty() {
12263 let summary = deltas
12264 .iter()
12265 .take(5)
12266 .map(|d| {
12267 format!(
12268 "{}:{}/{}:{}",
12269 principal_label(&d.principal),
12270 d.action,
12271 d.resource_kind,
12272 d.resource_name
12273 )
12274 })
12275 .collect::<Vec<_>>()
12276 .join(", ");
12277 let more = if deltas.len() > 5 {
12278 format!(" (and {} more)", deltas.len() - 5)
12279 } else {
12280 String::new()
12281 };
12282 return Err(RedDBError::Query(format!(
12283 "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}",
12284 n = deltas.len(),
12285 )));
12286 }
12287
12288 if !dry_run {
12292 auth_store.set_enforcement_mode(parsed);
12293 }
12294
12295 const COLUMNS: [&str; 5] = [
12296 "principal",
12297 "role",
12298 "action",
12299 "resource_kind",
12300 "resource_name",
12301 ];
12302 let schema = Arc::new(
12303 COLUMNS
12304 .iter()
12305 .map(|name| Arc::<str>::from(*name))
12306 .collect::<Vec<_>>(),
12307 );
12308 let records: Vec<UnifiedRecord> = deltas
12309 .iter()
12310 .map(|d| {
12311 UnifiedRecord::with_schema(
12312 Arc::clone(&schema),
12313 vec![
12314 SchemaValue::text(principal_label(&d.principal)),
12315 SchemaValue::text(d.role.as_str()),
12316 SchemaValue::text(d.action.clone()),
12317 SchemaValue::text(d.resource_kind.clone()),
12318 SchemaValue::text(d.resource_name.clone()),
12319 ],
12320 )
12321 })
12322 .collect();
12323 let mut result = crate::storage::query::unified::UnifiedResult::with_columns(
12324 COLUMNS.iter().map(|c| c.to_string()).collect(),
12325 );
12326 result.records = records;
12327 Ok(RuntimeQueryResult {
12328 query: query.to_string(),
12329 mode: crate::storage::query::modes::QueryMode::Sql,
12330 statement: "migrate_policy_mode",
12331 engine: "iam-policies",
12332 result,
12333 affected_rows: 0,
12334 statement_type: "select",
12335 bookmark: None,
12336 })
12337 }
12338
12339 fn execute_simulate_policy(
12340 &self,
12341 query: &str,
12342 user: &crate::storage::query::ast::PolicyUserRef,
12343 action: &str,
12344 resource: &crate::storage::query::ast::PolicyResourceRef,
12345 ) -> RedDBResult<RuntimeQueryResult> {
12346 use crate::auth::policies::ResourceRef;
12347 use crate::auth::store::SimCtx;
12348 use crate::auth::UserId;
12349 use crate::storage::query::unified::UnifiedRecord;
12350 use crate::storage::schema::Value as SchemaValue;
12351 use std::sync::Arc;
12352
12353 let auth_store = self
12354 .inner
12355 .auth_store
12356 .read()
12357 .clone()
12358 .ok_or_else(|| RedDBError::Query("auth store not configured".to_string()))?;
12359 let id = UserId::from_parts(user.tenant.as_deref(), &user.username);
12360 let r = ResourceRef::new(resource.kind.clone(), resource.name.clone());
12361 let outcome = auth_store.simulate(&id, action, &r, SimCtx::default());
12362
12363 let principal_str = current_auth_identity()
12364 .map(|(u, _)| u)
12365 .unwrap_or_else(|| "anonymous".into());
12366 let (decision_str, matched_pid, matched_sid) = decision_to_strings(&outcome.decision);
12367 tracing::info!(
12368 target: "audit",
12369 principal = %principal_str,
12370 action = "iam:policy.simulate",
12371 decision = %decision_str,
12372 matched_policy_id = ?matched_pid,
12373 matched_sid = ?matched_sid,
12374 "SIMULATE issued"
12375 );
12376 self.inner.audit_log.record(
12377 "iam/policy.simulate",
12378 &principal_str,
12379 &id.to_string(),
12380 "ok",
12381 crate::json::Value::Null,
12382 );
12383
12384 let mut rec = UnifiedRecord::default();
12385 rec.set_arc(Arc::from("decision"), SchemaValue::text(decision_str));
12386 rec.set_arc(
12387 Arc::from("matched_policy_id"),
12388 matched_pid
12389 .map(SchemaValue::text)
12390 .unwrap_or(SchemaValue::Null),
12391 );
12392 rec.set_arc(
12393 Arc::from("matched_sid"),
12394 matched_sid
12395 .map(SchemaValue::text)
12396 .unwrap_or(SchemaValue::Null),
12397 );
12398 rec.set_arc(Arc::from("reason"), SchemaValue::text(outcome.reason));
12399 rec.set_arc(
12400 Arc::from("trail_len"),
12401 SchemaValue::Integer(outcome.trail.len() as i64),
12402 );
12403 let mut result = crate::storage::query::unified::UnifiedResult::empty();
12404 result.records = vec![rec];
12405 Ok(RuntimeQueryResult {
12406 query: query.to_string(),
12407 mode: crate::storage::query::modes::QueryMode::Sql,
12408 statement: "simulate_policy",
12409 engine: "iam-policies",
12410 result,
12411 affected_rows: 0,
12412 statement_type: "select",
12413 bookmark: None,
12414 })
12415 }
12416}
12417
12418fn grant_to_iam_policy(
12423 principal: &crate::auth::privileges::GrantPrincipal,
12424 resource: &crate::auth::privileges::Resource,
12425 actions: &[crate::auth::privileges::Action],
12426 tenant: Option<&str>,
12427) -> Option<crate::auth::policies::Policy> {
12428 use crate::auth::policies::{
12429 compile_action, ActionPattern, Effect, Policy, ResourcePattern, Statement,
12430 };
12431 use crate::auth::privileges::{Action, GrantPrincipal, Resource};
12432
12433 if matches!(principal, GrantPrincipal::Group(_)) {
12434 return None;
12435 }
12436
12437 let now = crate::auth::now_ms();
12438 let id = format!("_grant_{:x}_{:x}", now, std::process::id());
12439
12440 let resource_str = match resource {
12441 Resource::Database => "table:*".to_string(),
12442 Resource::Schema(s) => format!("table:{s}.*"),
12443 Resource::Table { schema, table } => match schema {
12444 Some(s) => format!("table:{s}.{table}"),
12445 None => format!("table:{table}"),
12446 },
12447 Resource::Function { schema, name } => match schema {
12448 Some(s) => format!("function:{s}.{name}"),
12449 None => format!("function:{name}"),
12450 },
12451 };
12452
12453 let action_patterns: Vec<ActionPattern> = if actions.contains(&Action::All) {
12457 vec![ActionPattern::Wildcard]
12458 } else {
12459 actions
12460 .iter()
12461 .map(|a| compile_action(&a.as_str().to_ascii_lowercase()))
12462 .collect()
12463 };
12464 if action_patterns.is_empty() {
12465 return None;
12466 }
12467
12468 let resource_patterns = if resource_str == "*" {
12473 vec![ResourcePattern::Wildcard]
12474 } else if resource_str.contains('*') {
12475 vec![ResourcePattern::Glob(resource_str.clone())]
12476 } else if let Some((kind, name)) = resource_str.split_once(':') {
12477 vec![ResourcePattern::Exact {
12478 kind: kind.to_string(),
12479 name: name.to_string(),
12480 }]
12481 } else {
12482 vec![ResourcePattern::Wildcard]
12483 };
12484
12485 let policy = Policy {
12486 id,
12487 version: 1,
12488 tenant: tenant.map(|t| t.to_string()),
12489 created_at: now,
12490 updated_at: now,
12491 statements: vec![Statement {
12492 sid: None,
12493 effect: Effect::Allow,
12494 actions: action_patterns,
12495 resources: resource_patterns,
12496 condition: None,
12497 }],
12498 };
12499 if policy.validate().is_err() {
12500 return None;
12501 }
12502 Some(policy)
12503}
12504
12505fn parse_positive_iterations(func: &str, value: &f64) -> RedDBResult<usize> {
12511 if !value.is_finite() || *value < 1.0 || value.fract() != 0.0 {
12512 return Err(RedDBError::Query(format!(
12513 "table function '{func}' max_iterations must be a positive integer, got {value}"
12514 )));
12515 }
12516 Ok(*value as usize)
12517}
12518
12519fn legacy_action_to_iam(action: crate::auth::privileges::Action) -> &'static str {
12520 use crate::auth::privileges::Action;
12521 match action {
12522 Action::Select => "select",
12523 Action::Insert => "insert",
12524 Action::Update => "update",
12525 Action::Delete => "delete",
12526 Action::Truncate => "truncate",
12527 Action::References => "references",
12528 Action::Execute => "execute",
12529 Action::Usage => "usage",
12530 Action::All => "*",
12531 }
12532}
12533
12534fn update_set_target_columns(query: &crate::storage::query::ast::UpdateQuery) -> Vec<String> {
12535 let mut columns = Vec::new();
12536 for (column, _) in &query.assignment_exprs {
12537 if !columns.iter().any(|seen| seen == column) {
12538 columns.push(column.clone());
12539 }
12540 }
12541 columns
12542}
12543
12544fn column_access_request_for_table_update(
12545 table_name: &str,
12546 columns: Vec<String>,
12547) -> crate::auth::ColumnAccessRequest {
12548 match table_name.split_once('.') {
12549 Some((schema, table)) => {
12550 crate::auth::ColumnAccessRequest::update(table.to_string(), columns)
12551 .with_schema(schema.to_string())
12552 }
12553 None => crate::auth::ColumnAccessRequest::update(table_name.to_string(), columns),
12554 }
12555}
12556
12557fn column_access_request_for_table_select(
12558 table_name: &str,
12559 columns: Vec<String>,
12560) -> crate::auth::ColumnAccessRequest {
12561 match table_name.split_once('.') {
12562 Some((schema, table)) => {
12563 crate::auth::ColumnAccessRequest::select(table.to_string(), columns)
12564 .with_schema(schema.to_string())
12565 }
12566 None => crate::auth::ColumnAccessRequest::select(table_name.to_string(), columns),
12567 }
12568}
12569
12570fn update_returning_columns_for_policy(
12571 runtime: &RedDBRuntime,
12572 query: &crate::storage::query::ast::UpdateQuery,
12573) -> Option<Vec<String>> {
12574 let items = query.returning.as_ref()?;
12575 let mut columns = Vec::new();
12576 let project_all = items
12577 .iter()
12578 .any(|item| matches!(item, crate::storage::query::ast::ReturningItem::All));
12579 if project_all {
12580 collect_returning_star_columns(runtime, query, &mut columns);
12581 } else {
12582 for item in items {
12583 let crate::storage::query::ast::ReturningItem::Column(column) = item else {
12584 continue;
12585 };
12586 push_returning_policy_column(&mut columns, column);
12587 }
12588 }
12589 (!columns.is_empty()).then_some(columns)
12590}
12591
12592fn collect_returning_star_columns(
12593 runtime: &RedDBRuntime,
12594 query: &crate::storage::query::ast::UpdateQuery,
12595 columns: &mut Vec<String>,
12596) {
12597 let store = runtime.db().store();
12598 let Some(manager) = store.get_collection(&query.table) else {
12599 return;
12600 };
12601 if let Some(schema) = manager.column_schema() {
12602 for column in schema.iter() {
12603 push_returning_policy_column(columns, column);
12604 }
12605 }
12606 for entity in manager.query_all(|_| true) {
12607 if !returning_entity_matches_update_target(&entity, query.target) {
12608 continue;
12609 }
12610 match &entity.data {
12611 crate::storage::EntityData::Row(row) => {
12612 for (column, _) in row.iter_fields() {
12613 push_returning_policy_column(columns, column);
12614 }
12615 }
12616 crate::storage::EntityData::Node(node) => {
12617 push_returning_policy_column(columns, "label");
12618 push_returning_policy_column(columns, "node_type");
12619 for column in node.properties.keys() {
12620 push_returning_policy_column(columns, column);
12621 }
12622 }
12623 crate::storage::EntityData::Edge(edge) => {
12624 push_returning_policy_column(columns, "label");
12625 push_returning_policy_column(columns, "from_rid");
12626 push_returning_policy_column(columns, "to_rid");
12627 push_returning_policy_column(columns, "weight");
12628 for column in edge.properties.keys() {
12629 push_returning_policy_column(columns, column);
12630 }
12631 }
12632 _ => {}
12633 }
12634 }
12635}
12636
12637fn push_returning_policy_column(columns: &mut Vec<String>, column: &str) {
12638 if returning_public_envelope_column(column) {
12639 return;
12640 }
12641 if !columns.iter().any(|seen| seen == column) {
12642 columns.push(column.to_string());
12643 }
12644}
12645
12646fn returning_public_envelope_column(column: &str) -> bool {
12647 matches!(
12648 column.to_ascii_lowercase().as_str(),
12649 "rid" | "collection" | "kind" | "tenant" | "created_at" | "updated_at"
12650 )
12651}
12652
12653fn returning_entity_matches_update_target(
12654 entity: &crate::storage::UnifiedEntity,
12655 target: crate::storage::query::ast::UpdateTarget,
12656) -> bool {
12657 use crate::storage::query::ast::UpdateTarget;
12658 match target {
12659 UpdateTarget::Rows => {
12660 matches!(returning_row_item_kind(entity), Some(ReturningRowKind::Row))
12661 }
12662 UpdateTarget::Documents => {
12663 matches!(
12664 returning_row_item_kind(entity),
12665 Some(ReturningRowKind::Document)
12666 )
12667 }
12668 UpdateTarget::Kv => matches!(returning_row_item_kind(entity), Some(ReturningRowKind::Kv)),
12669 UpdateTarget::Nodes => matches!(
12670 (&entity.kind, &entity.data),
12671 (
12672 crate::storage::EntityKind::GraphNode(_),
12673 crate::storage::EntityData::Node(_)
12674 )
12675 ),
12676 UpdateTarget::Edges => matches!(
12677 (&entity.kind, &entity.data),
12678 (
12679 crate::storage::EntityKind::GraphEdge(_),
12680 crate::storage::EntityData::Edge(_)
12681 )
12682 ),
12683 }
12684}
12685
12686#[derive(Debug, Clone, Copy, PartialEq, Eq)]
12687enum ReturningRowKind {
12688 Row,
12689 Document,
12690 Kv,
12691}
12692
12693fn returning_row_item_kind(entity: &crate::storage::UnifiedEntity) -> Option<ReturningRowKind> {
12694 let row = entity.data.as_row()?;
12695 let is_kv = row.iter_fields().all(|(column, _)| {
12696 column.eq_ignore_ascii_case("key") || column.eq_ignore_ascii_case("value")
12697 });
12698 if is_kv {
12699 return Some(ReturningRowKind::Kv);
12700 }
12701 let is_document = row
12702 .iter_fields()
12703 .any(|(_, value)| matches!(value, crate::storage::schema::Value::Json(_)));
12704 if is_document {
12705 Some(ReturningRowKind::Document)
12706 } else {
12707 Some(ReturningRowKind::Row)
12708 }
12709}
12710
12711fn requested_table_columns_for_policy(
12712 table: &crate::storage::query::ast::TableQuery,
12713) -> Vec<String> {
12714 use crate::storage::query::sql_lowering::{
12715 effective_table_filter, effective_table_group_by_exprs, effective_table_having_filter,
12716 effective_table_projections,
12717 };
12718
12719 let table_name = table.table.as_str();
12720 let table_alias = table.alias.as_deref();
12721 let mut columns = std::collections::BTreeSet::new();
12722
12723 for projection in effective_table_projections(table) {
12724 collect_projection_columns(&projection, table_name, table_alias, &mut columns);
12725 }
12726 if let Some(filter) = effective_table_filter(table) {
12727 collect_filter_columns(&filter, table_name, table_alias, &mut columns);
12728 }
12729 for expr in effective_table_group_by_exprs(table) {
12730 collect_expr_columns(&expr, table_name, table_alias, &mut columns);
12731 }
12732 if let Some(filter) = effective_table_having_filter(table) {
12733 collect_filter_columns(&filter, table_name, table_alias, &mut columns);
12734 }
12735 for order in &table.order_by {
12736 if let Some(expr) = order.expr.as_ref() {
12737 collect_expr_columns(expr, table_name, table_alias, &mut columns);
12738 } else {
12739 collect_field_ref_column(&order.field, table_name, table_alias, &mut columns);
12740 }
12741 }
12742
12743 columns.into_iter().collect()
12744}
12745
12746fn collect_projection_columns(
12747 projection: &crate::storage::query::ast::Projection,
12748 table_name: &str,
12749 table_alias: Option<&str>,
12750 columns: &mut std::collections::BTreeSet<String>,
12751) {
12752 use crate::storage::query::ast::Projection;
12753 match projection {
12754 Projection::All => {
12755 columns.insert("*".to_string());
12756 }
12757 Projection::Column(column) | Projection::Alias(column, _) => {
12758 if column != "*" {
12759 columns.insert(column.clone());
12760 }
12761 }
12762 Projection::Function(_, args) => {
12763 for arg in args {
12764 collect_projection_columns(arg, table_name, table_alias, columns);
12765 }
12766 }
12767 Projection::Expression(filter, _) => {
12768 collect_filter_columns(filter, table_name, table_alias, columns);
12769 }
12770 Projection::Field(field, _) => {
12771 collect_field_ref_column(field, table_name, table_alias, columns);
12772 }
12773 Projection::Window { args, .. } => {
12777 for arg in args {
12778 collect_projection_columns(arg, table_name, table_alias, columns);
12779 }
12780 }
12781 }
12782}
12783
12784fn collect_filter_columns(
12785 filter: &crate::storage::query::ast::Filter,
12786 table_name: &str,
12787 table_alias: Option<&str>,
12788 columns: &mut std::collections::BTreeSet<String>,
12789) {
12790 use crate::storage::query::ast::Filter;
12791 match filter {
12792 Filter::Compare { field, .. }
12793 | Filter::IsNull(field)
12794 | Filter::IsNotNull(field)
12795 | Filter::In { field, .. }
12796 | Filter::Between { field, .. }
12797 | Filter::Like { field, .. }
12798 | Filter::StartsWith { field, .. }
12799 | Filter::EndsWith { field, .. }
12800 | Filter::Contains { field, .. } => {
12801 collect_field_ref_column(field, table_name, table_alias, columns);
12802 }
12803 Filter::CompareFields { left, right, .. } => {
12804 collect_field_ref_column(left, table_name, table_alias, columns);
12805 collect_field_ref_column(right, table_name, table_alias, columns);
12806 }
12807 Filter::CompareExpr { lhs, rhs, .. } => {
12808 collect_expr_columns(lhs, table_name, table_alias, columns);
12809 collect_expr_columns(rhs, table_name, table_alias, columns);
12810 }
12811 Filter::And(left, right) | Filter::Or(left, right) => {
12812 collect_filter_columns(left, table_name, table_alias, columns);
12813 collect_filter_columns(right, table_name, table_alias, columns);
12814 }
12815 Filter::Not(inner) => collect_filter_columns(inner, table_name, table_alias, columns),
12816 }
12817}
12818
12819fn collect_expr_columns(
12820 expr: &crate::storage::query::ast::Expr,
12821 table_name: &str,
12822 table_alias: Option<&str>,
12823 columns: &mut std::collections::BTreeSet<String>,
12824) {
12825 use crate::storage::query::ast::Expr;
12826 match expr {
12827 Expr::Column { field, .. } => {
12828 collect_field_ref_column(field, table_name, table_alias, columns);
12829 }
12830 Expr::Literal { .. } | Expr::Parameter { .. } => {}
12831 Expr::UnaryOp { operand, .. } | Expr::Cast { inner: operand, .. } => {
12832 collect_expr_columns(operand, table_name, table_alias, columns);
12833 }
12834 Expr::BinaryOp { lhs, rhs, .. } => {
12835 collect_expr_columns(lhs, table_name, table_alias, columns);
12836 collect_expr_columns(rhs, table_name, table_alias, columns);
12837 }
12838 Expr::FunctionCall { args, .. } => {
12839 for arg in args {
12840 collect_expr_columns(arg, table_name, table_alias, columns);
12841 }
12842 }
12843 Expr::Case {
12844 branches, else_, ..
12845 } => {
12846 for (condition, value) in branches {
12847 collect_expr_columns(condition, table_name, table_alias, columns);
12848 collect_expr_columns(value, table_name, table_alias, columns);
12849 }
12850 if let Some(value) = else_ {
12851 collect_expr_columns(value, table_name, table_alias, columns);
12852 }
12853 }
12854 Expr::IsNull { operand, .. } => {
12855 collect_expr_columns(operand, table_name, table_alias, columns);
12856 }
12857 Expr::InList { target, values, .. } => {
12858 collect_expr_columns(target, table_name, table_alias, columns);
12859 for value in values {
12860 collect_expr_columns(value, table_name, table_alias, columns);
12861 }
12862 }
12863 Expr::Between {
12864 target, low, high, ..
12865 } => {
12866 collect_expr_columns(target, table_name, table_alias, columns);
12867 collect_expr_columns(low, table_name, table_alias, columns);
12868 collect_expr_columns(high, table_name, table_alias, columns);
12869 }
12870 Expr::Subquery { .. } => {}
12871 Expr::WindowFunctionCall { args, window, .. } => {
12872 for arg in args {
12873 collect_expr_columns(arg, table_name, table_alias, columns);
12874 }
12875 for e in &window.partition_by {
12876 collect_expr_columns(e, table_name, table_alias, columns);
12877 }
12878 for o in &window.order_by {
12879 collect_expr_columns(&o.expr, table_name, table_alias, columns);
12880 }
12881 }
12882 }
12883}
12884
12885fn collect_field_ref_column(
12886 field: &crate::storage::query::ast::FieldRef,
12887 table_name: &str,
12888 table_alias: Option<&str>,
12889 columns: &mut std::collections::BTreeSet<String>,
12890) {
12891 if let Some(column) = policy_column_name_from_field_ref(field, table_name, table_alias) {
12892 if column != "*" {
12893 columns.insert(column);
12894 }
12895 }
12896}
12897
12898fn policy_column_name_from_field_ref(
12899 field: &crate::storage::query::ast::FieldRef,
12900 table_name: &str,
12901 table_alias: Option<&str>,
12902) -> Option<String> {
12903 match field {
12904 crate::storage::query::ast::FieldRef::TableColumn { table, column } => {
12905 if column == "*" {
12906 return Some("*".to_string());
12907 }
12908 if table.is_empty() || table == table_name || Some(table.as_str()) == table_alias {
12909 Some(column.clone())
12910 } else {
12911 Some(format!("{table}.{column}"))
12912 }
12913 }
12914 _ => None,
12915 }
12916}
12917
12918fn legacy_resource_to_iam(
12919 resource: &crate::auth::privileges::Resource,
12920 tenant: Option<&str>,
12921) -> crate::auth::policies::ResourceRef {
12922 use crate::auth::privileges::Resource;
12923
12924 let (kind, name) = match resource {
12925 Resource::Database => ("database".to_string(), "*".to_string()),
12926 Resource::Schema(s) => ("schema".to_string(), format!("{s}.*")),
12927 Resource::Table { schema, table } => (
12928 "table".to_string(),
12929 match schema {
12930 Some(s) => format!("{s}.{table}"),
12931 None => table.clone(),
12932 },
12933 ),
12934 Resource::Function { schema, name } => (
12935 "function".to_string(),
12936 match schema {
12937 Some(s) => format!("{s}.{name}"),
12938 None => name.clone(),
12939 },
12940 ),
12941 };
12942
12943 let mut out = crate::auth::policies::ResourceRef::new(kind, name);
12944 if let Some(t) = tenant {
12945 out = out.with_tenant(t.to_string());
12946 }
12947 out
12948}
12949
12950#[derive(Debug)]
12951struct JoinTableSide {
12952 table: String,
12953 alias: String,
12954}
12955
12956fn table_side_context(expr: &QueryExpr) -> Option<JoinTableSide> {
12957 match expr {
12958 QueryExpr::Table(table) => Some(JoinTableSide {
12959 table: table.table.clone(),
12960 alias: table.alias.clone().unwrap_or_else(|| table.table.clone()),
12961 }),
12962 _ => None,
12963 }
12964}
12965
12966fn collect_projection_columns_for_table(
12967 projection: &Projection,
12968 table: &str,
12969 alias: Option<&str>,
12970 out: &mut BTreeSet<String>,
12971) {
12972 match projection {
12973 Projection::Column(column) | Projection::Alias(column, _) => {
12974 match split_qualified_column(column) {
12975 Some((qualifier, column))
12976 if qualifier == table || alias.is_some_and(|alias| qualifier == alias) =>
12977 {
12978 push_policy_column(column, out);
12979 }
12980 Some(_) => {}
12981 None => push_policy_column(column, out),
12982 }
12983 }
12984 Projection::Field(
12985 FieldRef::TableColumn {
12986 table: qualifier,
12987 column,
12988 },
12989 _,
12990 ) => {
12991 if qualifier.is_empty()
12992 || qualifier == table
12993 || alias.is_some_and(|alias| qualifier == alias)
12994 {
12995 push_policy_column(column, out);
12996 }
12997 }
12998 Projection::Field(
12999 FieldRef::NodeProperty {
13000 alias: qualifier,
13001 property,
13002 },
13003 _,
13004 )
13005 | Projection::Field(
13006 FieldRef::EdgeProperty {
13007 alias: qualifier,
13008 property,
13009 },
13010 _,
13011 ) => {
13012 if qualifier == table || alias.is_some_and(|alias| qualifier == alias) {
13013 push_policy_column(property, out);
13014 }
13015 }
13016 Projection::Function(_, args) => {
13017 for arg in args {
13018 collect_projection_columns_for_table(arg, table, alias, out);
13019 }
13020 }
13021 Projection::Expression(_, _) | Projection::All | Projection::Field(_, _) => {}
13022 Projection::Window { args, .. } => {
13023 for arg in args {
13024 collect_projection_columns_for_table(arg, table, alias, out);
13025 }
13026 }
13027 }
13028}
13029
13030fn collect_projection_columns_for_join_side(
13031 projection: &Projection,
13032 left: Option<&JoinTableSide>,
13033 right: Option<&JoinTableSide>,
13034 out: &mut HashMap<String, BTreeSet<String>>,
13035) -> RedDBResult<()> {
13036 match projection {
13037 Projection::Column(column) | Projection::Alias(column, _) => {
13038 if let Some((qualifier, column)) = split_qualified_column(column) {
13039 push_qualified_join_column(qualifier, column, left, right, out);
13040 } else {
13041 push_unqualified_join_column(column, left, right, out);
13042 }
13043 }
13044 Projection::Field(FieldRef::TableColumn { table, column }, _) => {
13045 if table.is_empty() {
13046 push_unqualified_join_column(column, left, right, out);
13047 } else if let Some(side) = [left, right]
13048 .into_iter()
13049 .flatten()
13050 .find(|side| table == side.table.as_str() || table == side.alias.as_str())
13051 {
13052 push_join_column(&side.table, column, out);
13053 }
13054 }
13055 Projection::Field(FieldRef::NodeProperty { alias, property }, _)
13056 | Projection::Field(FieldRef::EdgeProperty { alias, property }, _) => {
13057 push_qualified_join_column(alias, property, left, right, out);
13058 }
13059 Projection::Function(_, args) => {
13060 for arg in args {
13061 collect_projection_columns_for_join_side(arg, left, right, out)?;
13062 }
13063 }
13064 Projection::Expression(_, _) | Projection::All | Projection::Field(_, _) => {}
13065 Projection::Window { args, .. } => {
13066 for arg in args {
13067 collect_projection_columns_for_join_side(arg, left, right, out)?;
13068 }
13069 }
13070 }
13071 Ok(())
13072}
13073
13074fn split_qualified_column(column: &str) -> Option<(&str, &str)> {
13075 let (qualifier, column) = column.split_once('.')?;
13076 if qualifier.is_empty() || column.is_empty() || column.contains('.') {
13077 return None;
13078 }
13079 Some((qualifier, column))
13080}
13081
13082fn push_qualified_join_column(
13083 qualifier: &str,
13084 column: &str,
13085 left: Option<&JoinTableSide>,
13086 right: Option<&JoinTableSide>,
13087 out: &mut HashMap<String, BTreeSet<String>>,
13088) {
13089 if let Some(side) = [left, right]
13090 .into_iter()
13091 .flatten()
13092 .find(|side| qualifier == side.table.as_str() || qualifier == side.alias.as_str())
13093 {
13094 push_join_column(&side.table, column, out);
13095 }
13096}
13097
13098fn push_unqualified_join_column(
13099 column: &str,
13100 left: Option<&JoinTableSide>,
13101 right: Option<&JoinTableSide>,
13102 out: &mut HashMap<String, BTreeSet<String>>,
13103) {
13104 for side in [left, right].into_iter().flatten() {
13105 push_join_column(&side.table, column, out);
13106 }
13107}
13108
13109fn push_join_column(table: &str, column: &str, out: &mut HashMap<String, BTreeSet<String>>) {
13110 if is_policy_column_name(column) {
13111 out.entry(table.to_string())
13112 .or_default()
13113 .insert(column.to_string());
13114 }
13115}
13116
13117fn push_policy_column(column: &str, out: &mut BTreeSet<String>) {
13118 if is_policy_column_name(column) {
13119 out.insert(column.to_string());
13120 }
13121}
13122
13123fn is_policy_column_name(column: &str) -> bool {
13124 !column.is_empty()
13125 && column != "*"
13126 && !column.starts_with("LIT:")
13127 && !column.starts_with("TYPE:")
13128}
13129
13130fn runtime_iam_context(
13131 role: crate::auth::Role,
13132 tenant: Option<&str>,
13133) -> crate::auth::policies::EvalContext {
13134 crate::auth::policies::EvalContext {
13135 principal_tenant: tenant.map(|t| t.to_string()),
13136 current_tenant: tenant.map(|t| t.to_string()),
13137 peer_ip: None,
13138 mfa_present: false,
13139 now_ms: crate::auth::now_ms(),
13140 principal_is_admin_role: role == crate::auth::Role::Admin,
13141 principal_is_platform_scoped: tenant.is_none(),
13142 }
13143}
13144
13145fn explicit_table_projection_columns(
13146 query: &crate::storage::query::ast::TableQuery,
13147) -> Vec<String> {
13148 use crate::storage::query::ast::{FieldRef, Projection};
13149
13150 let mut columns = Vec::new();
13151 for projection in crate::storage::query::sql_lowering::effective_table_projections(query) {
13152 match projection {
13153 Projection::Column(column) | Projection::Alias(column, _) => {
13154 push_unique(&mut columns, column)
13155 }
13156 Projection::Field(FieldRef::TableColumn { column, .. }, _) => {
13157 push_unique(&mut columns, column)
13158 }
13159 _ => {}
13163 }
13164 }
13165 columns
13166}
13167
13168fn explicit_graph_projection_properties(
13169 query: &crate::storage::query::ast::GraphQuery,
13170) -> Vec<String> {
13171 use crate::storage::query::ast::{FieldRef, Projection};
13172
13173 let mut columns = Vec::new();
13174 for projection in &query.return_ {
13175 match projection {
13176 Projection::Field(FieldRef::NodeProperty { property, .. }, _)
13177 | Projection::Field(FieldRef::EdgeProperty { property, .. }, _) => {
13178 push_unique(&mut columns, property.clone())
13179 }
13180 _ => {}
13181 }
13182 }
13183 columns
13184}
13185
13186fn push_unique(columns: &mut Vec<String>, column: String) {
13187 if !columns.iter().any(|existing| existing == &column) {
13188 columns.push(column);
13189 }
13190}
13191
13192fn principal_label(p: &crate::storage::query::ast::PolicyPrincipalRef) -> String {
13193 use crate::storage::query::ast::PolicyPrincipalRef;
13194 match p {
13195 PolicyPrincipalRef::User(u) => match &u.tenant {
13196 Some(t) => format!("user:{t}/{}", u.username),
13197 None => format!("user:{}", u.username),
13198 },
13199 PolicyPrincipalRef::Group(g) => format!("group:{g}"),
13200 }
13201}
13202
13203pub(crate) fn decision_to_strings(
13206 d: &crate::auth::policies::Decision,
13207) -> (String, Option<String>, Option<String>) {
13208 use crate::auth::policies::Decision;
13209 match d {
13210 Decision::Allow {
13211 matched_policy_id,
13212 matched_sid,
13213 } => (
13214 "allow".into(),
13215 Some(matched_policy_id.clone()),
13216 matched_sid.clone(),
13217 ),
13218 Decision::Deny {
13219 matched_policy_id,
13220 matched_sid,
13221 } => (
13222 "deny".into(),
13223 Some(matched_policy_id.clone()),
13224 matched_sid.clone(),
13225 ),
13226 Decision::DefaultDeny => ("default_deny".into(), None, None),
13227 Decision::AdminBypass => ("admin_bypass".into(), None, None),
13228 }
13229}
13230
13231fn relation_scopes_for_query(query: &QueryExpr) -> Vec<String> {
13232 let mut scopes = Vec::new();
13233 collect_relation_scopes(query, &mut scopes);
13234 scopes.sort();
13235 scopes.dedup();
13236 scopes
13237}
13238
13239fn collect_relation_scopes(query: &QueryExpr, scopes: &mut Vec<String>) {
13240 match query {
13241 QueryExpr::Table(table) => {
13242 if !table.table.is_empty() {
13243 scopes.push(table.table.clone());
13244 }
13245 if let Some(alias) = &table.alias {
13246 scopes.push(alias.clone());
13247 }
13248 }
13249 QueryExpr::Join(join) => {
13250 collect_relation_scopes(&join.left, scopes);
13251 collect_relation_scopes(&join.right, scopes);
13252 }
13253 _ => {}
13254 }
13255}
13256
13257fn query_references_outer_scope(query: &QueryExpr, outer_scopes: &[String]) -> bool {
13258 let inner_scopes = relation_scopes_for_query(query);
13259 query_expr_references_outer_scope(query, outer_scopes, &inner_scopes)
13260}
13261
13262fn query_expr_references_outer_scope(
13263 query: &QueryExpr,
13264 outer_scopes: &[String],
13265 inner_scopes: &[String],
13266) -> bool {
13267 match query {
13268 QueryExpr::Table(table) => {
13269 table.select_items.iter().any(|item| match item {
13270 crate::storage::query::ast::SelectItem::Wildcard => false,
13271 crate::storage::query::ast::SelectItem::Expr { expr, .. } => {
13272 expr_references_outer_scope(expr, outer_scopes, inner_scopes)
13273 }
13274 }) || table
13275 .where_expr
13276 .as_ref()
13277 .is_some_and(|expr| expr_references_outer_scope(expr, outer_scopes, inner_scopes))
13278 || table.filter.as_ref().is_some_and(|filter| {
13279 filter_references_outer_scope(filter, outer_scopes, inner_scopes)
13280 })
13281 || table.having_expr.as_ref().is_some_and(|expr| {
13282 expr_references_outer_scope(expr, outer_scopes, inner_scopes)
13283 })
13284 || table.having.as_ref().is_some_and(|filter| {
13285 filter_references_outer_scope(filter, outer_scopes, inner_scopes)
13286 })
13287 || table
13288 .group_by_exprs
13289 .iter()
13290 .any(|expr| expr_references_outer_scope(expr, outer_scopes, inner_scopes))
13291 || table.order_by.iter().any(|clause| {
13292 clause.expr.as_ref().is_some_and(|expr| {
13293 expr_references_outer_scope(expr, outer_scopes, inner_scopes)
13294 })
13295 })
13296 }
13297 QueryExpr::Join(join) => {
13298 query_expr_references_outer_scope(&join.left, outer_scopes, inner_scopes)
13299 || query_expr_references_outer_scope(&join.right, outer_scopes, inner_scopes)
13300 || join.filter.as_ref().is_some_and(|filter| {
13301 filter_references_outer_scope(filter, outer_scopes, inner_scopes)
13302 })
13303 || join.return_items.iter().any(|item| match item {
13304 crate::storage::query::ast::SelectItem::Wildcard => false,
13305 crate::storage::query::ast::SelectItem::Expr { expr, .. } => {
13306 expr_references_outer_scope(expr, outer_scopes, inner_scopes)
13307 }
13308 })
13309 }
13310 _ => false,
13311 }
13312}
13313
13314fn filter_references_outer_scope(
13315 filter: &crate::storage::query::ast::Filter,
13316 outer_scopes: &[String],
13317 inner_scopes: &[String],
13318) -> bool {
13319 use crate::storage::query::ast::Filter;
13320 match filter {
13321 Filter::Compare { field, .. }
13322 | Filter::IsNull(field)
13323 | Filter::IsNotNull(field)
13324 | Filter::In { field, .. }
13325 | Filter::Between { field, .. }
13326 | Filter::Like { field, .. }
13327 | Filter::StartsWith { field, .. }
13328 | Filter::EndsWith { field, .. }
13329 | Filter::Contains { field, .. } => {
13330 field_ref_references_outer_scope(field, outer_scopes, inner_scopes)
13331 }
13332 Filter::CompareFields { left, right, .. } => {
13333 field_ref_references_outer_scope(left, outer_scopes, inner_scopes)
13334 || field_ref_references_outer_scope(right, outer_scopes, inner_scopes)
13335 }
13336 Filter::CompareExpr { lhs, rhs, .. } => {
13337 expr_references_outer_scope(lhs, outer_scopes, inner_scopes)
13338 || expr_references_outer_scope(rhs, outer_scopes, inner_scopes)
13339 }
13340 Filter::And(left, right) | Filter::Or(left, right) => {
13341 filter_references_outer_scope(left, outer_scopes, inner_scopes)
13342 || filter_references_outer_scope(right, outer_scopes, inner_scopes)
13343 }
13344 Filter::Not(inner) => filter_references_outer_scope(inner, outer_scopes, inner_scopes),
13345 }
13346}
13347
13348fn expr_references_outer_scope(
13349 expr: &crate::storage::query::ast::Expr,
13350 outer_scopes: &[String],
13351 inner_scopes: &[String],
13352) -> bool {
13353 use crate::storage::query::ast::Expr;
13354 match expr {
13355 Expr::Column { field, .. } => {
13356 field_ref_references_outer_scope(field, outer_scopes, inner_scopes)
13357 }
13358 Expr::BinaryOp { lhs, rhs, .. } => {
13359 expr_references_outer_scope(lhs, outer_scopes, inner_scopes)
13360 || expr_references_outer_scope(rhs, outer_scopes, inner_scopes)
13361 }
13362 Expr::UnaryOp { operand, .. }
13363 | Expr::Cast { inner: operand, .. }
13364 | Expr::IsNull { operand, .. } => {
13365 expr_references_outer_scope(operand, outer_scopes, inner_scopes)
13366 }
13367 Expr::FunctionCall { args, .. } => args
13368 .iter()
13369 .any(|arg| expr_references_outer_scope(arg, outer_scopes, inner_scopes)),
13370 Expr::Case {
13371 branches, else_, ..
13372 } => {
13373 branches.iter().any(|(cond, value)| {
13374 expr_references_outer_scope(cond, outer_scopes, inner_scopes)
13375 || expr_references_outer_scope(value, outer_scopes, inner_scopes)
13376 }) || else_
13377 .as_ref()
13378 .is_some_and(|expr| expr_references_outer_scope(expr, outer_scopes, inner_scopes))
13379 }
13380 Expr::InList { target, values, .. } => {
13381 expr_references_outer_scope(target, outer_scopes, inner_scopes)
13382 || values
13383 .iter()
13384 .any(|value| expr_references_outer_scope(value, outer_scopes, inner_scopes))
13385 }
13386 Expr::Between {
13387 target, low, high, ..
13388 } => {
13389 expr_references_outer_scope(target, outer_scopes, inner_scopes)
13390 || expr_references_outer_scope(low, outer_scopes, inner_scopes)
13391 || expr_references_outer_scope(high, outer_scopes, inner_scopes)
13392 }
13393 Expr::Subquery { query, .. } => query_references_outer_scope(&query.query, inner_scopes),
13394 Expr::Literal { .. } | Expr::Parameter { .. } => false,
13395 Expr::WindowFunctionCall { args, window, .. } => {
13396 args.iter()
13397 .any(|arg| expr_references_outer_scope(arg, outer_scopes, inner_scopes))
13398 || window
13399 .partition_by
13400 .iter()
13401 .any(|e| expr_references_outer_scope(e, outer_scopes, inner_scopes))
13402 || window
13403 .order_by
13404 .iter()
13405 .any(|o| expr_references_outer_scope(&o.expr, outer_scopes, inner_scopes))
13406 }
13407 }
13408}
13409
13410fn field_ref_references_outer_scope(
13411 field: &crate::storage::query::ast::FieldRef,
13412 outer_scopes: &[String],
13413 inner_scopes: &[String],
13414) -> bool {
13415 match field {
13416 crate::storage::query::ast::FieldRef::TableColumn { table, .. } if !table.is_empty() => {
13417 outer_scopes.iter().any(|scope| scope == table)
13418 && !inner_scopes.iter().any(|scope| scope == table)
13419 }
13420 _ => false,
13421 }
13422}
13423
13424fn first_column_values(
13425 result: crate::storage::query::unified::UnifiedResult,
13426) -> RedDBResult<Vec<Value>> {
13427 if result.columns.len() > 1 {
13428 return Err(RedDBError::Query(
13429 "expression subquery must return exactly one column".to_string(),
13430 ));
13431 }
13432 let fallback_column = result
13433 .records
13434 .first()
13435 .and_then(|record| record.column_names().into_iter().next())
13436 .map(|name| name.to_string());
13437 let column = result.columns.first().cloned().or(fallback_column);
13438 let Some(column) = column else {
13439 return Ok(Vec::new());
13440 };
13441 Ok(result
13442 .records
13443 .iter()
13444 .map(|record| record.get(column.as_str()).cloned().unwrap_or(Value::Null))
13445 .collect())
13446}
13447
13448fn parse_timestamp_to_ms(s: &str) -> Option<u128> {
13449 if let Ok(n) = s.parse::<u128>() {
13451 return Some(n);
13452 }
13453 if let Some(date) = s.split_whitespace().next() {
13457 let parts: Vec<&str> = date.split('-').collect();
13458 if parts.len() == 3 {
13459 let (y, m, d) = (parts[0], parts[1], parts[2]);
13460 if let (Ok(y), Ok(m), Ok(d)) = (y.parse::<i64>(), m.parse::<u32>(), d.parse::<u32>()) {
13461 let days_in = days_from_civil(y, m, d);
13465 return Some((days_in as u128) * 86_400_000u128);
13466 }
13467 }
13468 }
13469 None
13470}
13471
13472fn days_from_civil(y: i64, m: u32, d: u32) -> i64 {
13475 let y = if m <= 2 { y - 1 } else { y };
13476 let era = if y >= 0 { y } else { y - 399 } / 400;
13477 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;
13479 let doe = yoe * 365 + yoe / 4 - yoe / 100 + doy;
13480 era * 146097 + doe as i64 - 719468
13481}
13482
13483fn walk_plan_node(
13484 node: &crate::storage::query::planner::CanonicalLogicalNode,
13485 depth: usize,
13486 out: &mut Vec<crate::storage::query::unified::UnifiedRecord>,
13487) {
13488 use std::sync::Arc;
13489 let mut rec = crate::storage::query::unified::UnifiedRecord::default();
13490 rec.set_arc(Arc::from("op"), Value::text(node.operator.clone()));
13491 rec.set_arc(
13492 Arc::from("source"),
13493 node.source.clone().map(Value::text).unwrap_or(Value::Null),
13494 );
13495 rec.set_arc(Arc::from("est_rows"), Value::Float(node.estimated_rows));
13496 rec.set_arc(Arc::from("est_cost"), Value::Float(node.operator_cost));
13497 rec.set_arc(Arc::from("depth"), Value::Integer(depth as i64));
13498 out.push(rec);
13499 for child in &node.children {
13500 walk_plan_node(child, depth + 1, out);
13501 }
13502}
13503
13504#[cfg(test)]
13505mod inline_graph_tvf_tests {
13506 use super::*;
13507
13508 fn scopes_for(sql: &str) -> HashSet<String> {
13509 let expr = crate::storage::query::parser::parse(sql)
13510 .expect("parse")
13511 .query;
13512 query_expr_result_cache_scopes(&expr)
13513 }
13514
13515 #[test]
13516 fn inline_tvf_cache_scopes_include_source_collections() {
13517 let scopes = scopes_for(
13521 "SELECT * FROM components(nodes => (SELECT id FROM hosts), edges => (SELECT src, dst FROM links))",
13522 );
13523 assert!(scopes.contains("hosts"), "nodes source scoped: {scopes:?}");
13524 assert!(scopes.contains("links"), "edges source scoped: {scopes:?}");
13525 }
13526
13527 #[test]
13528 fn graph_collection_tvf_cache_scope_is_graph_argument() {
13529 let scopes = scopes_for("SELECT * FROM components(g)");
13533 assert!(scopes.contains("g"), "collection form scoped: {scopes:?}");
13534 }
13535
13536 #[test]
13537 fn abstract_degree_centrality_counts_undirected_endpoints() {
13538 let nodes = vec!["a".to_string(), "b".to_string(), "c".to_string()];
13539 let edges = vec![
13540 ("a".to_string(), "b".to_string(), 1.0_f32),
13541 ("b".to_string(), "c".to_string(), 1.0_f32),
13542 ];
13543 let degrees = abstract_degree_centrality(&nodes, &edges);
13544 assert_eq!(
13545 degrees,
13546 vec![
13547 ("a".to_string(), 1),
13548 ("b".to_string(), 2),
13549 ("c".to_string(), 1),
13550 ]
13551 );
13552 }
13553}