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 show_secrets_allows_key(key: &str) -> bool {
58 !key.starts_with("red.secret.") && !key.starts_with("red.config.")
59}
60
61fn secret_sql_value_to_string(value: &Value) -> RedDBResult<String> {
62 match value {
63 Value::Text(s) => Ok(s.to_string()),
64 Value::Integer(n) => Ok(n.to_string()),
65 Value::UnsignedInteger(n) => Ok(n.to_string()),
66 Value::Float(n) => Ok(n.to_string()),
67 Value::Boolean(b) => Ok(b.to_string()),
68 Value::Null => Err(RedDBError::Query(
69 "SET SECRET key = NULL deletes the secret; use DELETE SECRET for explicit deletes"
70 .to_string(),
71 )),
72 Value::Password(_) | Value::Secret(_) => Err(RedDBError::Query(
73 "SET SECRET accepts plain scalar literals; PASSWORD() and SECRET() are for typed columns"
74 .to_string(),
75 )),
76 _ => Err(RedDBError::Query(format!(
77 "SET SECRET does not support value type {:?} yet",
78 value.data_type()
79 ))),
80 }
81}
82
83#[cfg(test)]
84mod tests {
85 use super::*;
86
87 #[test]
88 fn show_secrets_allows_only_user_managed_keys() {
89 assert!(!show_secrets_allows_key("red.secret.aes_key"));
90 assert!(!show_secrets_allows_key(
91 "red.secret.ai.anthropic.default.api_key"
92 ));
93 assert!(!show_secrets_allows_key("red.config.ai.default.provider"));
94 assert!(show_secrets_allows_key("acme.key"));
95 }
96}
97
98fn insert_config_json_path(
99 root: &mut crate::serde_json::Value,
100 path: &str,
101 value: crate::serde_json::Value,
102) {
103 let segments: Vec<&str> = path
104 .split('.')
105 .filter(|segment| !segment.is_empty())
106 .collect();
107 insert_config_json_segments(root, &segments, value);
108}
109
110fn insert_config_json_segments(
111 root: &mut crate::serde_json::Value,
112 segments: &[&str],
113 value: crate::serde_json::Value,
114) {
115 if segments.is_empty() {
116 *root = value;
117 return;
118 }
119
120 if !matches!(root, crate::serde_json::Value::Object(_)) {
121 *root = crate::serde_json::Value::Object(crate::serde_json::Map::new());
122 }
123
124 let crate::serde_json::Value::Object(map) = root else {
125 return;
126 };
127 if segments.len() == 1 {
128 map.insert(segments[0].to_string(), value);
129 return;
130 }
131 let entry = map
132 .entry(segments[0].to_string())
133 .or_insert_with(|| crate::serde_json::Value::Object(crate::serde_json::Map::new()));
134 insert_config_json_segments(entry, &segments[1..], value);
135}
136
137fn show_config_json_result(
138 query: &str,
139 mode: crate::storage::query::modes::QueryMode,
140 prefix: &Option<String>,
141 value: crate::serde_json::Value,
142) -> RuntimeQueryResult {
143 let mut result = UnifiedResult::with_columns(vec!["key".into(), "value".into()]);
144 let mut record = UnifiedRecord::new();
145 record.set(
146 "key",
147 prefix
148 .as_ref()
149 .map(|key| Value::text(key.clone()))
150 .unwrap_or(Value::Null),
151 );
152 record.set("value", Value::Json(value.to_string_compact().into_bytes()));
153 result.push(record);
154 RuntimeQueryResult {
155 query: query.to_string(),
156 mode,
157 statement: "show_config_json",
158 engine: "runtime-config",
159 result,
160 affected_rows: 0,
161 statement_type: "select",
162 bookmark: None,
163 }
164}
165
166#[derive(Clone)]
167struct QueryControlEventSpec {
168 kind: crate::runtime::control_events::EventKind,
169 action: &'static str,
170 resource: Option<String>,
171 fields: Vec<(String, crate::runtime::control_events::Sensitivity)>,
172}
173
174#[derive(Clone)]
175struct QueryAuditPlan {
176 statement_kind: &'static str,
177 collections: Vec<String>,
178}
179
180fn query_audit_plan(expr: &QueryExpr) -> Option<QueryAuditPlan> {
181 let mut collections = Vec::new();
182 let statement_kind = match expr {
183 QueryExpr::Table(table) => {
184 push_query_audit_collection(&mut collections, &table.table);
185 "select"
186 }
187 QueryExpr::Join(join) => {
188 collect_query_audit_collections(&join.left, &mut collections);
189 collect_query_audit_collections(&join.right, &mut collections);
190 "select"
191 }
192 QueryExpr::Insert(insert) => {
193 push_query_audit_collection(&mut collections, &insert.table);
194 "insert"
195 }
196 QueryExpr::Update(update) => {
197 push_query_audit_collection(&mut collections, &update.table);
198 "update"
199 }
200 QueryExpr::Delete(delete) => {
201 push_query_audit_collection(&mut collections, &delete.table);
202 "delete"
203 }
204 _ => return None,
205 };
206 if collections.is_empty() {
207 None
208 } else {
209 Some(QueryAuditPlan {
210 statement_kind,
211 collections,
212 })
213 }
214}
215
216fn collect_query_audit_collections(expr: &QueryExpr, collections: &mut Vec<String>) {
217 match expr {
218 QueryExpr::Table(table) => push_query_audit_collection(collections, &table.table),
219 QueryExpr::Join(join) => {
220 collect_query_audit_collections(&join.left, collections);
221 collect_query_audit_collections(&join.right, collections);
222 }
223 _ => {}
224 }
225}
226
227fn push_query_audit_collection(collections: &mut Vec<String>, name: &str) {
228 if name == "red" || name.starts_with("red.") || name.starts_with("__red_schema_") {
229 return;
230 }
231 if !collections.iter().any(|existing| existing == name) {
232 collections.push(name.to_string());
233 }
234}
235
236const RUNTIME_INDEX_REGISTRY_COLLECTION: &str = "red_index_registry";
237
238impl RedDBRuntime {
239 fn execute_create_metric(
240 &self,
241 raw_query: &str,
242 query: &crate::storage::query::ast::CreateMetricQuery,
243 ) -> RedDBResult<RuntimeQueryResult> {
244 self.check_write(crate::runtime::write_gate::WriteKind::Ddl)?;
245 let store = self.inner.db.store();
246 super::metric_descriptor_catalog::create(
247 store.as_ref(),
248 &query.path,
249 &query.kind,
250 &query.role,
251 super::metric_descriptor_catalog::DerivedSpec {
252 source: query.source.clone(),
253 query: query.query.clone(),
254 window_ms: query.window_ms,
255 time_field: query.time_field.clone(),
256 },
257 )?;
258 self.invalidate_result_cache();
259 Ok(RuntimeQueryResult::ok_message(
260 raw_query.to_string(),
261 &format!("metric descriptor '{}' created", query.path),
262 "create",
263 ))
264 }
265
266 fn execute_create_ranking(
272 &self,
273 raw_query: &str,
274 req: super::ranking_descriptor_catalog::CreateRankingRequest,
275 ) -> RedDBResult<RuntimeQueryResult> {
276 self.check_write(crate::runtime::write_gate::WriteKind::Ddl)?;
277 let store = self.inner.db.store();
278 let descriptor = super::ranking_descriptor_catalog::create(store.as_ref(), &req)?;
279 self.invalidate_result_cache();
280 Ok(RuntimeQueryResult::ok_message(
281 raw_query.to_string(),
282 &format!(
283 "ranking '{}' created on {}({})",
284 descriptor.name, descriptor.table, descriptor.column
285 ),
286 "create",
287 ))
288 }
289
290 fn execute_show_rankings(&self, raw_query: &str) -> RedDBResult<RuntimeQueryResult> {
294 let store = self.inner.db.store();
295 let entries = super::ranking_descriptor_catalog::list(store.as_ref());
296 let columns = vec![
297 "name".to_string(),
298 "table".to_string(),
299 "column".to_string(),
300 "direction".to_string(),
301 "top_k".to_string(),
302 ];
303 let rows = entries
304 .into_iter()
305 .map(|e| {
306 vec![
307 ("name".to_string(), Value::text(e.name)),
308 ("table".to_string(), Value::text(e.table)),
309 ("column".to_string(), Value::text(e.column)),
310 (
311 "direction".to_string(),
312 Value::text(if e.descending { "DESC" } else { "ASC" }.to_string()),
313 ),
314 ("top_k".to_string(), Value::UnsignedInteger(e.top_k)),
315 ]
316 })
317 .collect();
318 let mut result =
319 RuntimeQueryResult::ok_records(raw_query.to_string(), columns, rows, "select");
320 result.statement = "rank_of";
321 result.engine = "runtime-rank";
322 Ok(result)
323 }
324
325 fn execute_rank_of(
334 &self,
335 raw_query: &str,
336 req: &crate::storage::query::ast::RankOfQuery,
337 ) -> RedDBResult<RuntimeQueryResult> {
338 let store = self.inner.db.store();
339 let descriptor = super::ranking_descriptor_catalog::get(store.as_ref(), &req.ranking)
340 .ok_or_else(|| {
341 RedDBError::Query(format!("ranking '{}' does not exist", req.ranking))
342 })?;
343 let rank = self.compute_exact_head_rank(&descriptor, req.entity_id)?;
344 let columns = vec!["rank".to_string()];
345 let rows = match rank {
346 Some(rank) => vec![vec![("rank".to_string(), Value::UnsignedInteger(rank))]],
347 None => Vec::new(),
348 };
349 let mut result =
350 RuntimeQueryResult::ok_records(raw_query.to_string(), columns, rows, "select");
351 result.statement = "rank_range";
352 result.engine = "runtime-rank";
353 Ok(result)
354 }
355
356 fn execute_rank_range(
362 &self,
363 raw_query: &str,
364 req: &crate::storage::query::ast::RankRangeQuery,
365 ) -> RedDBResult<RuntimeQueryResult> {
366 let store = self.inner.db.store();
367 let descriptor = super::ranking_descriptor_catalog::get(store.as_ref(), &req.ranking)
368 .ok_or_else(|| {
369 RedDBError::Query(format!("ranking '{}' does not exist", req.ranking))
370 })?;
371 let (head_columns, entries) = self.compute_ranked_head_entries(&descriptor)?;
372
373 let mut columns = Vec::with_capacity(head_columns.len() + 1);
374 columns.push("rank".to_string());
375 for column in &head_columns {
376 if column != "rank" {
377 columns.push(column.clone());
378 }
379 }
380
381 let rows = entries
382 .into_iter()
383 .filter(|entry| entry.rank >= req.lo && entry.rank <= req.hi)
384 .map(|entry| {
385 let mut row = Vec::with_capacity(columns.len());
386 row.push(("rank".to_string(), Value::UnsignedInteger(entry.rank)));
387 for column in &head_columns {
388 if column == "rank" {
389 continue;
390 }
391 if let Some(value) = entry.record.get(column) {
392 row.push((column.clone(), value.clone()));
393 }
394 }
395 row
396 })
397 .collect();
398 let mut result =
399 RuntimeQueryResult::ok_records(raw_query.to_string(), columns, rows, "select");
400 result.statement = "approx_rank_of";
401 result.engine = "runtime-rank";
402 Ok(result)
403 }
404
405 fn compute_exact_head_rank(
419 &self,
420 descriptor: &super::ranking_descriptor_catalog::RankingDescriptor,
421 target_id: u64,
422 ) -> RedDBResult<Option<u64>> {
423 let (_columns, entries) = self.compute_ranked_head_entries(descriptor)?;
424 Ok(entries
425 .into_iter()
426 .find(|entry| record_rid_u64(&entry.record) == Some(target_id))
427 .map(|entry| entry.rank))
428 }
429
430 fn compute_ranked_head_entries(
432 &self,
433 descriptor: &super::ranking_descriptor_catalog::RankingDescriptor,
434 ) -> RedDBResult<(Vec<String>, Vec<RankedHeadEntry>)> {
435 let table = &descriptor.table;
436 let column = &descriptor.column;
437
438 let dir = if descriptor.descending { "DESC" } else { "ASC" };
445 let head_sql = format!(
446 "SELECT * FROM {table} ORDER BY {column} {dir}, rid ASC LIMIT {}",
447 descriptor.top_k
448 );
449 let head_result = self.execute_query_inner(&head_sql)?;
450
451 let mut entries = Vec::with_capacity(head_result.result.records.len());
452 let mut row_position = 0u64;
453 let mut current_rank = 0u64;
454 let mut previous_score: Option<f64> = None;
455 for rec in &head_result.result.records {
456 let Some(score) = record_column_f64(rec, column) else {
457 continue;
458 };
459 row_position += 1;
460 current_rank = if previous_score == Some(score) {
461 current_rank
462 } else {
463 row_position
464 };
465 previous_score = Some(score);
466 entries.push(RankedHeadEntry {
467 rank: current_rank,
468 record: rec.clone(),
469 });
470 }
471 Ok((head_result.result.columns, entries))
472 }
473
474 fn execute_approx_rank_of(
485 &self,
486 raw_query: &str,
487 req: &crate::storage::query::ast::RankOfQuery,
488 ) -> RedDBResult<RuntimeQueryResult> {
489 let store = self.inner.db.store();
490 let descriptor = super::ranking_descriptor_catalog::get(store.as_ref(), &req.ranking)
491 .ok_or_else(|| {
492 RedDBError::Query(format!("ranking '{}' does not exist", req.ranking))
493 })?;
494
495 let approx = self.compute_approx_rank(&descriptor, req.entity_id)?;
496 let columns = vec![
497 "rank".to_string(),
498 "percentile".to_string(),
499 "approximate".to_string(),
500 ];
501 let rows = match approx {
502 Some(approx) => vec![vec![
503 ("rank".to_string(), Value::UnsignedInteger(approx.rank)),
504 ("percentile".to_string(), Value::Float(approx.percentile)),
505 ("approximate".to_string(), Value::Boolean(true)),
506 ]],
507 None => Vec::new(),
508 };
509 let mut result =
510 RuntimeQueryResult::ok_records(raw_query.to_string(), columns, rows, "select");
511 result.statement = "approx_rank_of";
512 result.engine = "runtime-rank";
519 Ok(result)
520 }
521
522 fn compute_approx_rank(
537 &self,
538 descriptor: &super::ranking_descriptor_catalog::RankingDescriptor,
539 target_id: u64,
540 ) -> RedDBResult<Option<super::score_sketch::ApproxRank>> {
541 let table = &descriptor.table;
542 let column = &descriptor.column;
543
544 let scan_sql = format!("SELECT * FROM {table}");
547 let scan = self.execute_query_inner(&scan_sql)?;
548 let records = &scan.result.records;
549
550 let mut scores: Vec<f64> = Vec::with_capacity(records.len());
551 let mut target_score: Option<f64> = None;
552 for rec in records {
553 let Some(score) = record_column_f64(rec, column) else {
554 continue;
555 };
556 scores.push(score);
557 let rid = match rec.get("rid") {
558 Some(Value::UnsignedInteger(n)) => Some(*n),
559 Some(Value::Integer(n)) if *n >= 0 => Some(*n as u64),
560 _ => None,
561 };
562 if rid == Some(target_id) {
563 target_score = Some(score);
564 }
565 }
566
567 let sketch = super::score_sketch::ScoreSketch::from_scores(&scores);
568 super::ranking_descriptor_catalog::save_sketch(
570 self.inner.db.store().as_ref(),
571 table,
572 column,
573 &sketch,
574 );
575
576 let Some(target_score) = target_score else {
577 return Ok(None);
579 };
580 Ok(sketch.approx_rank(target_score, descriptor.descending))
581 }
582
583 fn execute_alter_metric(
584 &self,
585 raw_query: &str,
586 query: &crate::storage::query::ast::AlterMetricQuery,
587 ) -> RedDBResult<RuntimeQueryResult> {
588 self.check_write(crate::runtime::write_gate::WriteKind::Ddl)?;
589 let store = self.inner.db.store();
590 super::metric_descriptor_catalog::update(
591 store.as_ref(),
592 &query.path,
593 query.set_role.as_deref(),
594 query.attempted_kind.as_deref(),
595 query.attempted_path.as_deref(),
596 )?;
597 self.invalidate_result_cache();
598 Ok(RuntimeQueryResult::ok_message(
599 raw_query.to_string(),
600 &format!("metric descriptor '{}' updated", query.path),
601 "alter",
602 ))
603 }
604
605 fn execute_create_slo(
606 &self,
607 raw_query: &str,
608 query: &crate::storage::query::ast::CreateSloQuery,
609 ) -> RedDBResult<RuntimeQueryResult> {
610 self.check_write(crate::runtime::write_gate::WriteKind::Ddl)?;
611 let store = self.inner.db.store();
612 super::slo_descriptor_catalog::create(
613 store.as_ref(),
614 &query.path,
615 &query.metric_path,
616 query.target,
617 query.window_ms,
618 )?;
619 self.invalidate_result_cache();
620 Ok(RuntimeQueryResult::ok_message(
621 raw_query.to_string(),
622 &format!("SLO descriptor '{}' created", query.path),
623 "create",
624 ))
625 }
626
627 fn execute_create_analytics_source(
628 &self,
629 raw_query: &str,
630 query: super::analytics_source_catalog::CreateAnalyticsSourceProfile,
631 ) -> RedDBResult<RuntimeQueryResult> {
632 self.check_write(crate::runtime::write_gate::WriteKind::Ddl)?;
633 let store = self.inner.db.store();
634 let profile = super::analytics_source_catalog::create(
635 store.as_ref(),
636 &self.inner.db.collection_contracts(),
637 query,
638 )?;
639 self.invalidate_result_cache();
640 Ok(RuntimeQueryResult::ok_message(
641 raw_query.to_string(),
642 &format!("analytics source '{}' created", profile.name),
643 "create",
644 ))
645 }
646}
647
648fn query_control_event_specs(expr: &QueryExpr) -> Vec<QueryControlEventSpec> {
649 use crate::runtime::control_events::{EventKind, Sensitivity};
650
651 let mut specs = Vec::new();
652 let mut schema = |action: &'static str, resource: Option<String>| {
653 specs.push(QueryControlEventSpec {
654 kind: EventKind::SchemaDdl,
655 action,
656 resource,
657 fields: Vec::new(),
658 });
659 };
660 match expr {
661 QueryExpr::CreateTable(q) => {
662 schema("create_table", Some(format!("table:{}", q.name)));
663 if let Some(column) = &q.tenant_by {
664 specs.push(QueryControlEventSpec {
665 kind: EventKind::TenantGovernance,
666 action: "create_table_tenant_by",
667 resource: Some(format!("table:{}", q.name)),
668 fields: vec![("tenant_column".to_string(), Sensitivity::raw(column))],
669 });
670 }
671 }
672 QueryExpr::CreateCollection(q) => {
673 schema("create_collection", Some(format!("collection:{}", q.name)));
674 }
675 QueryExpr::CreateVector(q) => schema("create_vector", Some(format!("vector:{}", q.name))),
676 QueryExpr::DropTable(q) => schema("drop_table", Some(format!("table:{}", q.name))),
677 QueryExpr::DropGraph(q) => schema("drop_graph", Some(format!("graph:{}", q.name))),
678 QueryExpr::DropVector(q) => schema("drop_vector", Some(format!("vector:{}", q.name))),
679 QueryExpr::DropDocument(q) => {
680 schema("drop_document", Some(format!("document:{}", q.name)));
681 }
682 QueryExpr::DropKv(q) => schema("drop_kv", Some(format!("kv:{}", q.name))),
683 QueryExpr::DropCollection(q) => {
684 schema("drop_collection", Some(format!("collection:{}", q.name)));
685 }
686 QueryExpr::Truncate(q) => schema("truncate", Some(format!("collection:{}", q.name))),
687 QueryExpr::AlterTable(q) => {
688 schema("alter_table", Some(format!("table:{}", q.name)));
689 for op in &q.operations {
690 match op {
691 crate::storage::query::ast::AlterOperation::EnableRowLevelSecurity => {
692 specs.push(QueryControlEventSpec {
693 kind: EventKind::RlsGovernance,
694 action: "enable_rls",
695 resource: Some(format!("table:{}", q.name)),
696 fields: Vec::new(),
697 });
698 }
699 crate::storage::query::ast::AlterOperation::DisableRowLevelSecurity => {
700 specs.push(QueryControlEventSpec {
701 kind: EventKind::RlsGovernance,
702 action: "disable_rls",
703 resource: Some(format!("table:{}", q.name)),
704 fields: Vec::new(),
705 });
706 }
707 crate::storage::query::ast::AlterOperation::EnableTenancy { column } => {
708 specs.push(QueryControlEventSpec {
709 kind: EventKind::TenantGovernance,
710 action: "enable_tenancy",
711 resource: Some(format!("table:{}", q.name)),
712 fields: vec![("tenant_column".to_string(), Sensitivity::raw(column))],
713 });
714 }
715 crate::storage::query::ast::AlterOperation::DisableTenancy => {
716 specs.push(QueryControlEventSpec {
717 kind: EventKind::TenantGovernance,
718 action: "disable_tenancy",
719 resource: Some(format!("table:{}", q.name)),
720 fields: Vec::new(),
721 });
722 }
723 _ => {}
724 }
725 }
726 }
727 QueryExpr::CreateVcsRef(q) => {
728 let kind = match q.kind {
729 crate::storage::query::ast::VcsRefKind::Branch => "branch",
730 crate::storage::query::ast::VcsRefKind::Tag => "tag",
731 };
732 schema("create_vcs_ref", Some(format!("{kind}:{}", q.name)));
733 }
734 QueryExpr::DropVcsRef(q) => {
735 let kind = match q.kind {
736 crate::storage::query::ast::VcsRefKind::Branch => "branch",
737 crate::storage::query::ast::VcsRefKind::Tag => "tag",
738 };
739 schema("drop_vcs_ref", Some(format!("{kind}:{}", q.name)));
740 }
741 QueryExpr::CreateIndex(q) => {
742 schema(
743 "create_index",
744 Some(format!("index:{}:{}", q.table, q.name)),
745 );
746 }
747 QueryExpr::DropIndex(q) => {
748 schema("drop_index", Some(format!("index:{}:{}", q.table, q.name)));
749 }
750 QueryExpr::CreateTimeSeries(q) => {
751 schema("create_timeseries", Some(format!("timeseries:{}", q.name)));
752 }
753 QueryExpr::CreateMetric(q) => {
754 schema("create_metric", Some(format!("metric:{}", q.path)));
755 }
756 QueryExpr::AlterMetric(q) => {
757 schema("alter_metric", Some(format!("metric:{}", q.path)));
758 }
759 QueryExpr::CreateSlo(q) => {
760 schema("create_slo", Some(format!("slo:{}", q.path)));
761 }
762 QueryExpr::DropTimeSeries(q) => {
763 schema("drop_timeseries", Some(format!("timeseries:{}", q.name)));
764 }
765 QueryExpr::CreateQueue(q) => schema("create_queue", Some(format!("queue:{}", q.name))),
766 QueryExpr::AlterQueue(q) => schema("alter_queue", Some(format!("queue:{}", q.name))),
767 QueryExpr::DropQueue(q) => schema("drop_queue", Some(format!("queue:{}", q.name))),
768 QueryExpr::CreateTree(q) => {
769 schema(
770 "create_tree",
771 Some(format!("tree:{}:{}", q.collection, q.name)),
772 );
773 }
774 QueryExpr::DropTree(q) => {
775 schema(
776 "drop_tree",
777 Some(format!("tree:{}:{}", q.collection, q.name)),
778 );
779 }
780 QueryExpr::CreateSchema(q) => schema("create_schema", Some(format!("schema:{}", q.name))),
781 QueryExpr::DropSchema(q) => schema("drop_schema", Some(format!("schema:{}", q.name))),
782 QueryExpr::CreateSequence(q) => {
783 schema("create_sequence", Some(format!("sequence:{}", q.name)));
784 }
785 QueryExpr::DropSequence(q) => schema("drop_sequence", Some(format!("sequence:{}", q.name))),
786 QueryExpr::CreateView(q) => schema("create_view", Some(format!("view:{}", q.name))),
787 QueryExpr::DropView(q) => schema("drop_view", Some(format!("view:{}", q.name))),
788 QueryExpr::RefreshMaterializedView(q) => {
789 schema(
790 "refresh_materialized_view",
791 Some(format!("view:{}", q.name)),
792 );
793 }
794 QueryExpr::CreatePolicy(q) => {
795 specs.push(QueryControlEventSpec {
796 kind: EventKind::RlsGovernance,
797 action: "create_policy",
798 resource: Some(format!("table:{}:policy:{}", q.table, q.name)),
799 fields: vec![(
800 "target_kind".to_string(),
801 Sensitivity::raw(q.target_kind.as_ident()),
802 )],
803 });
804 }
805 QueryExpr::DropPolicy(q) => {
806 specs.push(QueryControlEventSpec {
807 kind: EventKind::RlsGovernance,
808 action: "drop_policy",
809 resource: Some(format!("table:{}:policy:{}", q.table, q.name)),
810 fields: Vec::new(),
811 });
812 }
813 QueryExpr::SetTenant(value) => {
814 let mut fields = Vec::new();
815 if let Some(value) = value {
816 fields.push(("tenant".to_string(), Sensitivity::raw(value)));
817 }
818 specs.push(QueryControlEventSpec {
819 kind: EventKind::TenantGovernance,
820 action: "set_tenant",
821 resource: Some("tenant:session".to_string()),
822 fields,
823 });
824 }
825 QueryExpr::SetConfig { key, .. } => {
826 specs.push(QueryControlEventSpec {
827 kind: EventKind::ConfigWrite,
828 action: "config:write",
829 resource: Some(format!("config:{key}")),
830 fields: vec![("key".to_string(), Sensitivity::raw(key))],
831 });
832 }
833 QueryExpr::ConfigCommand(cmd) => match cmd {
834 crate::storage::query::ast::ConfigCommand::Put {
835 collection, key, ..
836 }
837 | crate::storage::query::ast::ConfigCommand::Rotate {
838 collection, key, ..
839 } => {
840 let target = format!("{collection}/{key}");
841 specs.push(QueryControlEventSpec {
842 kind: EventKind::ConfigWrite,
843 action: "config:write",
844 resource: Some(format!("config:{target}")),
845 fields: vec![
846 ("collection".to_string(), Sensitivity::raw(collection)),
847 ("key".to_string(), Sensitivity::raw(key)),
848 ],
849 });
850 }
851 crate::storage::query::ast::ConfigCommand::Delete { collection, key } => {
852 let target = format!("{collection}/{key}");
853 specs.push(QueryControlEventSpec {
854 kind: EventKind::ConfigDelete,
855 action: "config:write",
856 resource: Some(format!("config:{target}")),
857 fields: vec![
858 ("collection".to_string(), Sensitivity::raw(collection)),
859 ("key".to_string(), Sensitivity::raw(key)),
860 ],
861 });
862 }
863 _ => {}
864 },
865 QueryExpr::AlterUser(stmt) => {
866 let disables = stmt.attributes.iter().any(|attr| {
867 matches!(
868 attr,
869 crate::storage::query::ast::AlterUserAttribute::Disable
870 )
871 });
872 specs.push(QueryControlEventSpec {
873 kind: if disables {
874 EventKind::UserDisable
875 } else {
876 EventKind::UserUpdate
877 },
878 action: "alter_user",
879 resource: Some(format!("user:{}", stmt.username)),
880 fields: Vec::new(),
881 });
882 }
883 QueryExpr::CreateUser(stmt) => {
884 specs.push(QueryControlEventSpec {
885 kind: EventKind::UserCreate,
886 action: "create_user",
887 resource: Some(format!("user:{}", stmt.username)),
888 fields: Vec::new(),
889 });
890 }
891 _ => {}
892 }
893 specs
894}
895
896pub(crate) fn control_event_outcome_for_error(
897 err: &RedDBError,
898) -> crate::runtime::control_events::Outcome {
899 match err {
900 RedDBError::ReadOnly(_) => crate::runtime::control_events::Outcome::Denied,
901 RedDBError::Query(msg)
902 if msg.contains("permission denied")
903 || msg.contains("cannot issue")
904 || msg.contains("lacks") =>
905 {
906 crate::runtime::control_events::Outcome::Denied
907 }
908 _ => crate::runtime::control_events::Outcome::Error,
909 }
910}
911
912fn view_records_to_entities(
921 table: &str,
922 records: &[crate::storage::query::unified::UnifiedRecord],
923) -> Vec<crate::storage::UnifiedEntity> {
924 use std::collections::HashMap;
925 let table_arc: std::sync::Arc<str> = std::sync::Arc::from(table);
926 let mut out = Vec::with_capacity(records.len());
927 for record in records {
928 let mut named: HashMap<String, crate::storage::schema::Value> = HashMap::new();
929 for (name, value) in record.iter_fields() {
930 named.insert(name.to_string(), value.clone());
931 }
932 let entity = crate::storage::UnifiedEntity::new(
933 crate::storage::EntityId::new(0),
934 crate::storage::EntityKind::TableRow {
935 table: std::sync::Arc::clone(&table_arc),
936 row_id: 0,
937 },
938 crate::storage::EntityData::Row(crate::storage::RowData {
939 columns: Vec::new(),
940 named: Some(named),
941 schema: None,
942 }),
943 );
944 out.push(entity);
945 }
946 out
947}
948
949fn system_keyed_collection_contract(
950 name: &str,
951 model: crate::catalog::CollectionModel,
952) -> crate::physical::CollectionContract {
953 let now = crate::utils::now_unix_millis() as u128;
954 crate::physical::CollectionContract {
955 name: name.to_string(),
956 declared_model: model,
957 schema_mode: crate::catalog::SchemaMode::Dynamic,
958 origin: crate::physical::ContractOrigin::Implicit,
959 version: 1,
960 created_at_unix_ms: now,
961 updated_at_unix_ms: now,
962 default_ttl_ms: None,
963 vector_dimension: None,
964 vector_metric: None,
965 context_index_fields: Vec::new(),
966 declared_columns: Vec::new(),
967 table_def: None,
968 timestamps_enabled: false,
969 context_index_enabled: false,
970 metrics_raw_retention_ms: None,
971 metrics_rollup_policies: Vec::new(),
972 metrics_tenant_identity: None,
973 metrics_namespace: None,
974 append_only: false,
975 subscriptions: Vec::new(),
976 analytics_config: Vec::new(),
977 session_key: None,
978 session_gap_ms: None,
979 retention_duration_ms: None,
980 analytical_storage: None,
981
982 ai_policy: None,
983 }
984}
985
986pub use super::execution_context::{
987 capture_current_snapshot, clear_current_auth_identity, clear_current_connection_id,
988 clear_current_snapshot, clear_current_tenant, current_auth_identity_for_audit,
989 current_connection_id, current_tenant, entity_visible_under_current_snapshot,
990 entity_visible_with_context, set_current_auth_identity, set_current_connection_id,
991 set_current_snapshot, set_current_tenant, snapshot_bundle, with_snapshot_bundle,
992 SnapshotBundle, SnapshotContext,
993};
994pub(crate) use super::execution_context::{
995 current_auth_identity, current_config_value, current_kv_value, current_role_projected,
996 current_scope_override, current_secret_value, current_snapshot_requires_index_fallback,
997 current_user_projected, has_scope_override_active, parse_set_local_tenant,
998 update_current_config_value, update_current_kv_value, update_current_secret_value,
999 xids_visible_under_current_snapshot, ConfigSnapshotGuard, CurrentSnapshotGuard, KvStoreGuard,
1000 ScopeOverrideGuard, SecretStoreGuard, TxLocalTenantGuard,
1001};
1002
1003fn table_row_index_fields(
1004 entity: &crate::storage::unified::entity::UnifiedEntity,
1005) -> Vec<(String, crate::storage::schema::Value)> {
1006 let crate::storage::EntityData::Row(row) = &entity.data else {
1007 return Vec::new();
1008 };
1009 if let Some(named) = &row.named {
1010 return named
1011 .iter()
1012 .map(|(name, value)| (name.clone(), value.clone()))
1013 .collect();
1014 }
1015 if let Some(schema) = &row.schema {
1016 return schema
1017 .iter()
1018 .zip(row.columns.iter())
1019 .map(|(name, value)| (name.clone(), value.clone()))
1020 .collect();
1021 }
1022 Vec::new()
1023}
1024
1025fn named_text(
1026 named: &std::collections::HashMap<String, crate::storage::schema::Value>,
1027 key: &str,
1028) -> Option<String> {
1029 match named.get(key) {
1030 Some(crate::storage::schema::Value::Text(value)) => Some(value.to_string()),
1031 _ => None,
1032 }
1033}
1034
1035fn named_bool(
1036 named: &std::collections::HashMap<String, crate::storage::schema::Value>,
1037 key: &str,
1038) -> Option<bool> {
1039 match named.get(key) {
1040 Some(crate::storage::schema::Value::Boolean(value)) => Some(*value),
1041 _ => None,
1042 }
1043}
1044
1045fn named_i64(
1046 named: &std::collections::HashMap<String, crate::storage::schema::Value>,
1047 key: &str,
1048) -> Option<i64> {
1049 match named.get(key) {
1050 Some(crate::storage::schema::Value::Integer(value)) => Some(*value),
1051 _ => None,
1052 }
1053}
1054
1055fn index_method_kind_as_str(method: super::index_store::IndexMethodKind) -> &'static str {
1056 match method {
1057 super::index_store::IndexMethodKind::Hash => "hash",
1058 super::index_store::IndexMethodKind::Bitmap => "bitmap",
1059 super::index_store::IndexMethodKind::Spatial => "spatial",
1060 super::index_store::IndexMethodKind::BTree => "btree",
1061 super::index_store::IndexMethodKind::H3 { .. } => "h3",
1064 }
1065}
1066
1067fn index_method_kind_resolution(method: super::index_store::IndexMethodKind) -> u8 {
1070 match method {
1071 super::index_store::IndexMethodKind::H3 { resolution } => resolution,
1072 _ => 0,
1073 }
1074}
1075
1076fn index_method_kind_from_str(
1077 raw: &str,
1078 resolution: u8,
1079) -> Option<super::index_store::IndexMethodKind> {
1080 match raw {
1081 "hash" => Some(super::index_store::IndexMethodKind::Hash),
1082 "bitmap" => Some(super::index_store::IndexMethodKind::Bitmap),
1083 "spatial" | "rtree" => Some(super::index_store::IndexMethodKind::Spatial),
1084 "btree" => Some(super::index_store::IndexMethodKind::BTree),
1085 "h3" => Some(super::index_store::IndexMethodKind::H3 { resolution }),
1086 _ => None,
1087 }
1088}
1089
1090fn runtime_pool_lock(runtime: &RedDBRuntime) -> std::sync::MutexGuard<'_, PoolState> {
1091 runtime
1092 .inner
1093 .pool
1094 .lock()
1095 .unwrap_or_else(|poisoned| poisoned.into_inner())
1096}
1097
1098fn json_runtime_value(value: crate::json::Value) -> Value {
1099 crate::json::to_vec(&value)
1100 .map(Value::Json)
1101 .unwrap_or(Value::Null)
1102}
1103
1104fn is_graph_tvf_name(name: &str) -> bool {
1108 name.eq_ignore_ascii_case("components")
1109 || name.eq_ignore_ascii_case("louvain")
1110 || name.eq_ignore_ascii_case("degree_centrality")
1111 || name.eq_ignore_ascii_case("shortest_path")
1112 || name.eq_ignore_ascii_case("betweenness")
1113 || name.eq_ignore_ascii_case("eigenvector")
1114 || name.eq_ignore_ascii_case("pagerank")
1115}
1116
1117fn analytics_view_algorithm(
1124 graph: &str,
1125 view: &crate::catalog::AnalyticsViewDescriptor,
1126) -> RedDBResult<(String, Vec<(String, f64)>)> {
1127 use crate::catalog::AnalyticsOutput;
1128
1129 let mut named_args: Vec<(String, f64)> = Vec::new();
1130 let algorithm = match view.output {
1131 AnalyticsOutput::Communities => {
1132 let algo = view.algorithm.as_deref().unwrap_or("louvain");
1133 if !algo.eq_ignore_ascii_case("louvain") {
1134 return Err(RedDBError::Query(format!(
1135 "analytics output 'communities' on graph '{graph}' has unsupported algorithm '{algo}' (expected louvain)"
1136 )));
1137 }
1138 if let Some(resolution) = view.resolution {
1139 named_args.push(("resolution".to_string(), resolution));
1140 }
1141 "louvain".to_string()
1142 }
1143 AnalyticsOutput::Components => {
1144 if let Some(algo) = view.algorithm.as_deref() {
1145 if !algo.eq_ignore_ascii_case("components")
1146 && !algo.eq_ignore_ascii_case("connected_components")
1147 {
1148 return Err(RedDBError::Query(format!(
1149 "analytics output 'components' on graph '{graph}' has unsupported algorithm '{algo}' (expected connected_components)"
1150 )));
1151 }
1152 }
1153 "components".to_string()
1154 }
1155 AnalyticsOutput::Centrality => {
1156 let algo = view
1157 .algorithm
1158 .as_deref()
1159 .unwrap_or("pagerank")
1160 .to_ascii_lowercase();
1161 match algo.as_str() {
1162 "pagerank" => {
1163 if let Some(max_iterations) = view.max_iterations {
1164 named_args.push(("max_iterations".to_string(), max_iterations as f64));
1165 }
1166 }
1167 "eigenvector" => {
1168 if let Some(max_iterations) = view.max_iterations {
1169 named_args.push(("max_iterations".to_string(), max_iterations as f64));
1170 }
1171 if let Some(tolerance) = view.tolerance {
1172 named_args.push(("tolerance".to_string(), tolerance));
1173 }
1174 }
1175 "betweenness" => {}
1176 other => {
1177 return Err(RedDBError::Query(format!(
1178 "analytics output 'centrality' on graph '{graph}' has unsupported algorithm '{other}' (expected pagerank, betweenness, or eigenvector)"
1179 )));
1180 }
1181 }
1182 algo
1183 }
1184 };
1185 Ok((algorithm, named_args))
1186}
1187
1188fn reject_named_args(name: &str, named_args: &[(String, f64)]) -> RedDBResult<()> {
1190 if let Some((key, _)) = named_args.first() {
1191 return Err(RedDBError::Query(format!(
1192 "table function '{name}' has no named argument '{key}'"
1193 )));
1194 }
1195 Ok(())
1196}
1197
1198fn louvain_resolution(named_args: &[(String, f64)]) -> RedDBResult<f64> {
1201 let mut resolution = 1.0_f64;
1202 for (key, value) in named_args {
1203 if key.eq_ignore_ascii_case("resolution") {
1204 if !value.is_finite() || *value <= 0.0 {
1205 return Err(RedDBError::Query(format!(
1206 "table function 'louvain' resolution must be > 0, got {value}"
1207 )));
1208 }
1209 resolution = *value;
1210 } else {
1211 return Err(RedDBError::Query(format!(
1212 "table function 'louvain' has no named argument '{key}' (expected 'resolution')"
1213 )));
1214 }
1215 }
1216 Ok(resolution)
1217}
1218
1219fn abstract_degree_centrality(
1224 nodes: &[String],
1225 edges: &[(
1226 String,
1227 String,
1228 crate::storage::engine::graph_algorithms::Weight,
1229 )],
1230) -> Vec<(String, usize)> {
1231 let mut degree: std::collections::BTreeMap<String, usize> = std::collections::BTreeMap::new();
1232 for n in nodes {
1233 degree.entry(n.clone()).or_insert(0);
1234 }
1235 for (a, b, _w) in edges {
1236 *degree.entry(a.clone()).or_insert(0) += 1;
1237 *degree.entry(b.clone()).or_insert(0) += 1;
1238 }
1239 degree.into_iter().collect()
1240}
1241
1242fn ordered_result_columns(result: &crate::storage::query::unified::UnifiedResult) -> Vec<String> {
1245 if !result.columns.is_empty() {
1246 return result.columns.clone();
1247 }
1248 result
1249 .records
1250 .first()
1251 .map(|record| {
1252 record
1253 .column_names()
1254 .iter()
1255 .map(|column| column.to_string())
1256 .collect()
1257 })
1258 .unwrap_or_default()
1259}
1260
1261fn value_to_node_id(value: &crate::storage::schema::Value) -> Option<String> {
1265 use crate::storage::schema::Value;
1266 match value {
1267 Value::Null => None,
1268 Value::Text(s) => Some(s.to_string()),
1269 Value::Integer(n) => Some(n.to_string()),
1270 Value::UnsignedInteger(n) => Some(n.to_string()),
1271 Value::NodeRef(s) => Some(s.clone()),
1272 other => Some(other.to_string()),
1273 }
1274}
1275
1276fn value_to_weight(value: &crate::storage::schema::Value) -> Option<f32> {
1278 use crate::storage::schema::Value;
1279 match value {
1280 Value::Float(f) => Some(*f as f32),
1281 Value::Integer(n) => Some(*n as f32),
1282 Value::UnsignedInteger(n) => Some(*n as f32),
1283 _ => None,
1284 }
1285}
1286
1287fn inline_node_ids(
1291 name: &str,
1292 result: &crate::storage::query::unified::UnifiedResult,
1293) -> RedDBResult<Vec<String>> {
1294 if result.records.is_empty() {
1295 return Ok(Vec::new());
1296 }
1297 let columns = ordered_result_columns(result);
1298 let Some(first_col) = columns.first() else {
1299 return Err(RedDBError::Query(format!(
1300 "table function '{name}' inline form: `nodes` subquery must project at least one column (the node id)"
1301 )));
1302 };
1303 let mut ids = Vec::with_capacity(result.records.len());
1304 for record in &result.records {
1305 if let Some(id) = record.get(first_col).and_then(value_to_node_id) {
1306 ids.push(id);
1307 }
1308 }
1309 Ok(ids)
1310}
1311
1312fn inline_edges(
1317 name: &str,
1318 result: &crate::storage::query::unified::UnifiedResult,
1319) -> RedDBResult<
1320 Vec<(
1321 String,
1322 String,
1323 crate::storage::engine::graph_algorithms::Weight,
1324 )>,
1325> {
1326 if result.records.is_empty() {
1327 return Ok(Vec::new());
1328 }
1329 let columns = ordered_result_columns(result);
1330 if columns.len() < 2 {
1331 return Err(RedDBError::Query(format!(
1332 "table function '{name}' inline form: `edges` subquery must project at least two columns (source, target), got {}",
1333 columns.len()
1334 )));
1335 }
1336 let src_col = &columns[0];
1337 let dst_col = &columns[1];
1338 let weight_col = columns.get(2);
1339 let mut edges = Vec::with_capacity(result.records.len());
1340 for record in &result.records {
1341 let (Some(src), Some(dst)) = (
1342 record.get(src_col).and_then(value_to_node_id),
1343 record.get(dst_col).and_then(value_to_node_id),
1344 ) else {
1345 continue;
1347 };
1348 let weight = match weight_col {
1349 Some(col) => match record.get(col) {
1350 None | Some(crate::storage::schema::Value::Null) => 1.0,
1351 Some(value) => value_to_weight(value).ok_or_else(|| {
1352 RedDBError::Query(format!(
1353 "table function '{name}' inline form: `edges` weight column must be numeric"
1354 ))
1355 })?,
1356 },
1357 None => 1.0,
1358 };
1359 edges.push((src, dst, weight));
1360 }
1361 Ok(edges)
1362}
1363
1364fn cache_scope_insert(scopes: &mut HashSet<String>, name: &str) {
1365 if name.is_empty() || name.starts_with("__subq_") || is_universal_query_source(name) {
1366 return;
1367 }
1368 scopes.insert(name.to_string());
1369}
1370
1371fn collect_table_source_scopes(scopes: &mut HashSet<String>, query: &TableQuery) {
1372 match query.source.as_ref() {
1373 Some(crate::storage::query::ast::TableSource::Name(name)) => {
1374 cache_scope_insert(scopes, name)
1375 }
1376 Some(crate::storage::query::ast::TableSource::Subquery(subquery)) => {
1377 collect_query_expr_result_cache_scopes(scopes, subquery);
1378 }
1379 Some(crate::storage::query::ast::TableSource::Function { name, args, .. }) => {
1386 if is_graph_tvf_name(name) {
1387 if let Some(graph) = args.first() {
1388 cache_scope_insert(scopes, graph);
1389 }
1390 }
1391 }
1392 Some(crate::storage::query::ast::TableSource::InlineGraphFunction {
1397 nodes, edges, ..
1398 }) => {
1399 collect_query_expr_result_cache_scopes(scopes, nodes);
1400 collect_query_expr_result_cache_scopes(scopes, edges);
1401 }
1402 None => cache_scope_insert(scopes, &query.table),
1403 }
1404}
1405
1406fn collect_vector_source_scopes(
1407 scopes: &mut HashSet<String>,
1408 source: &crate::storage::query::ast::VectorSource,
1409) {
1410 match source {
1411 crate::storage::query::ast::VectorSource::Reference { collection, .. } => {
1412 cache_scope_insert(scopes, collection);
1413 }
1414 crate::storage::query::ast::VectorSource::Subquery(subquery) => {
1415 collect_query_expr_result_cache_scopes(scopes, subquery);
1416 }
1417 crate::storage::query::ast::VectorSource::Literal(_)
1418 | crate::storage::query::ast::VectorSource::Text(_) => {}
1419 }
1420}
1421
1422fn collect_path_selector_scopes(
1423 scopes: &mut HashSet<String>,
1424 selector: &crate::storage::query::ast::NodeSelector,
1425) {
1426 if let crate::storage::query::ast::NodeSelector::ByRow { table, .. } = selector {
1427 cache_scope_insert(scopes, table);
1428 }
1429}
1430
1431fn collect_query_expr_result_cache_scopes(scopes: &mut HashSet<String>, expr: &QueryExpr) {
1432 match expr {
1433 QueryExpr::Table(query) => collect_table_source_scopes(scopes, query),
1434 QueryExpr::Join(query) => {
1435 collect_query_expr_result_cache_scopes(scopes, &query.left);
1436 collect_query_expr_result_cache_scopes(scopes, &query.right);
1437 }
1438 QueryExpr::Path(query) => {
1439 collect_path_selector_scopes(scopes, &query.from);
1440 collect_path_selector_scopes(scopes, &query.to);
1441 }
1442 QueryExpr::Vector(query) => {
1443 cache_scope_insert(scopes, &query.collection);
1444 collect_vector_source_scopes(scopes, &query.query_vector);
1445 }
1446 QueryExpr::Hybrid(query) => {
1447 collect_query_expr_result_cache_scopes(scopes, &query.structured);
1448 cache_scope_insert(scopes, &query.vector.collection);
1449 collect_vector_source_scopes(scopes, &query.vector.query_vector);
1450 }
1451 QueryExpr::Insert(query) => cache_scope_insert(scopes, &query.table),
1452 QueryExpr::Update(query) => cache_scope_insert(scopes, &query.table),
1453 QueryExpr::Delete(query) => cache_scope_insert(scopes, &query.table),
1454 QueryExpr::CreateTable(query) => cache_scope_insert(scopes, &query.name),
1455 QueryExpr::CreateCollection(query) => cache_scope_insert(scopes, &query.name),
1456 QueryExpr::CreateVector(query) => cache_scope_insert(scopes, &query.name),
1457 QueryExpr::DropTable(query) => cache_scope_insert(scopes, &query.name),
1458 QueryExpr::DropGraph(query) => cache_scope_insert(scopes, &query.name),
1459 QueryExpr::DropVector(query) => cache_scope_insert(scopes, &query.name),
1460 QueryExpr::DropDocument(query) => cache_scope_insert(scopes, &query.name),
1461 QueryExpr::DropKv(query) => cache_scope_insert(scopes, &query.name),
1462 QueryExpr::DropCollection(query) => cache_scope_insert(scopes, &query.name),
1463 QueryExpr::Truncate(query) => cache_scope_insert(scopes, &query.name),
1464 QueryExpr::AlterTable(query) => cache_scope_insert(scopes, &query.name),
1465 QueryExpr::CreateVcsRef(_) | QueryExpr::DropVcsRef(_) => {
1466 cache_scope_insert(scopes, crate::application::vcs_collections::REFS)
1467 }
1468 QueryExpr::CreateIndex(query) => cache_scope_insert(scopes, &query.table),
1469 QueryExpr::DropIndex(query) => cache_scope_insert(scopes, &query.table),
1470 QueryExpr::CreateTimeSeries(query) => cache_scope_insert(scopes, &query.name),
1471 QueryExpr::CreateMetric(query) => cache_scope_insert(scopes, &query.path),
1472 QueryExpr::AlterMetric(query) => cache_scope_insert(scopes, &query.path),
1473 QueryExpr::CreateSlo(query) => cache_scope_insert(scopes, &query.path),
1474 QueryExpr::DropTimeSeries(query) => cache_scope_insert(scopes, &query.name),
1475 QueryExpr::CreateQueue(query) => cache_scope_insert(scopes, &query.name),
1476 QueryExpr::AlterQueue(query) => cache_scope_insert(scopes, &query.name),
1477 QueryExpr::DropQueue(query) => cache_scope_insert(scopes, &query.name),
1478 QueryExpr::QueueSelect(query) => cache_scope_insert(scopes, &query.queue),
1479 QueryExpr::QueueCommand(query) => match query {
1480 QueueCommand::Push { queue, .. }
1481 | QueueCommand::Pop { queue, .. }
1482 | QueueCommand::Peek { queue, .. }
1483 | QueueCommand::Len { queue }
1484 | QueueCommand::Purge { queue }
1485 | QueueCommand::GroupCreate { queue, .. }
1486 | QueueCommand::GroupRead { queue, .. }
1487 | QueueCommand::Pending { queue, .. }
1488 | QueueCommand::Claim { queue, .. }
1489 | QueueCommand::Ack { queue, .. }
1490 | QueueCommand::Nack { queue, .. } => cache_scope_insert(scopes, queue),
1491 QueueCommand::Move {
1492 source,
1493 destination,
1494 ..
1495 } => {
1496 cache_scope_insert(scopes, source);
1497 cache_scope_insert(scopes, destination);
1498 }
1499 },
1500 QueryExpr::EventsBackfill(query) => {
1501 cache_scope_insert(scopes, &query.collection);
1502 cache_scope_insert(scopes, &query.target_queue);
1503 }
1504 QueryExpr::CreateTree(query) => cache_scope_insert(scopes, &query.collection),
1505 QueryExpr::DropTree(query) => cache_scope_insert(scopes, &query.collection),
1506 QueryExpr::TreeCommand(query) => match query {
1507 TreeCommand::Insert { collection, .. }
1508 | TreeCommand::Move { collection, .. }
1509 | TreeCommand::Delete { collection, .. }
1510 | TreeCommand::Validate { collection, .. }
1511 | TreeCommand::Rebalance { collection, .. } => cache_scope_insert(scopes, collection),
1512 },
1513 QueryExpr::SearchCommand(query) => match query {
1514 SearchCommand::Similar { collection, .. }
1515 | SearchCommand::Hybrid { collection, .. }
1516 | SearchCommand::SpatialRadius { collection, .. }
1517 | SearchCommand::SpatialBbox { collection, .. }
1518 | SearchCommand::SpatialNearest { collection, .. } => {
1519 cache_scope_insert(scopes, collection);
1520 }
1521 SearchCommand::Text { collection, .. }
1522 | SearchCommand::Multimodal { collection, .. }
1523 | SearchCommand::Index { collection, .. }
1524 | SearchCommand::Context { collection, .. } => {
1525 if let Some(collection) = collection.as_deref() {
1526 cache_scope_insert(scopes, collection);
1527 }
1528 }
1529 },
1530 QueryExpr::Ask(query) => {
1531 if let Some(collection) = query.collection.as_deref() {
1532 cache_scope_insert(scopes, collection);
1533 }
1534 }
1535 QueryExpr::ExplainAlter(query) => cache_scope_insert(scopes, &query.target.name),
1536 QueryExpr::MaintenanceCommand(cmd) => match cmd {
1537 crate::storage::query::ast::MaintenanceCommand::Vacuum { target, .. }
1538 | crate::storage::query::ast::MaintenanceCommand::Analyze { target } => {
1539 if let Some(t) = target {
1540 cache_scope_insert(scopes, t);
1541 }
1542 }
1543 },
1544 QueryExpr::CopyFrom(cmd) => cache_scope_insert(scopes, &cmd.table),
1545 QueryExpr::CreateView(cmd) => {
1546 cache_scope_insert(scopes, &cmd.name);
1547 collect_query_expr_result_cache_scopes(scopes, &cmd.query);
1549 }
1550 QueryExpr::DropView(cmd) => cache_scope_insert(scopes, &cmd.name),
1551 QueryExpr::RefreshMaterializedView(cmd) => cache_scope_insert(scopes, &cmd.name),
1552 QueryExpr::CreatePolicy(cmd) => cache_scope_insert(scopes, &cmd.table),
1553 QueryExpr::DropPolicy(cmd) => cache_scope_insert(scopes, &cmd.table),
1554 QueryExpr::CreateServer(_) | QueryExpr::DropServer(_) => {}
1555 QueryExpr::CreateForeignTable(cmd) => cache_scope_insert(scopes, &cmd.name),
1556 QueryExpr::DropForeignTable(cmd) => cache_scope_insert(scopes, &cmd.name),
1557 QueryExpr::Graph(_)
1558 | QueryExpr::GraphCommand(_)
1559 | QueryExpr::ProbabilisticCommand(_)
1560 | QueryExpr::SetConfig { .. }
1561 | QueryExpr::ShowConfig { .. }
1562 | QueryExpr::SetSecret { .. }
1563 | QueryExpr::DeleteSecret { .. }
1564 | QueryExpr::ShowSecrets { .. }
1565 | QueryExpr::SetKv { .. }
1566 | QueryExpr::DeleteKv { .. }
1567 | QueryExpr::SetTenant(_)
1568 | QueryExpr::ShowTenant
1569 | QueryExpr::TransactionControl(_)
1570 | QueryExpr::CreateSchema(_)
1571 | QueryExpr::DropSchema(_)
1572 | QueryExpr::CreateSequence(_)
1573 | QueryExpr::DropSequence(_)
1574 | QueryExpr::Grant(_)
1575 | QueryExpr::Revoke(_)
1576 | QueryExpr::AlterUser(_)
1577 | QueryExpr::CreateUser(_)
1578 | QueryExpr::CreateIamPolicy { .. }
1579 | QueryExpr::DropIamPolicy { .. }
1580 | QueryExpr::AttachPolicy { .. }
1581 | QueryExpr::DetachPolicy { .. }
1582 | QueryExpr::ShowPolicies { .. }
1583 | QueryExpr::ShowEffectivePermissions { .. }
1584 | QueryExpr::RankOf(_)
1585 | QueryExpr::ApproxRankOf(_)
1586 | QueryExpr::RankRange(_)
1587 | QueryExpr::SimulatePolicy { .. }
1588 | QueryExpr::LintPolicy { .. }
1589 | QueryExpr::MigratePolicyMode { .. }
1590 | QueryExpr::CreateMigration(_)
1591 | QueryExpr::ApplyMigration(_)
1592 | QueryExpr::RollbackMigration(_)
1593 | QueryExpr::ExplainMigration(_)
1594 | QueryExpr::EventsBackfillStatus { .. } => {}
1595 QueryExpr::KvCommand(cmd) => {
1596 use crate::storage::query::ast::KvCommand;
1597 match cmd {
1598 KvCommand::Put { collection, .. }
1599 | KvCommand::InvalidateTags { collection, .. }
1600 | KvCommand::Get { collection, .. }
1601 | KvCommand::Unseal { collection, .. }
1602 | KvCommand::Rotate { collection, .. }
1603 | KvCommand::History { collection, .. }
1604 | KvCommand::List { collection, .. }
1605 | KvCommand::Purge { collection, .. }
1606 | KvCommand::Watch { collection, .. }
1607 | KvCommand::Delete { collection, .. }
1608 | KvCommand::Incr { collection, .. }
1609 | KvCommand::Cas { collection, .. } => cache_scope_insert(scopes, collection),
1610 }
1611 }
1612 QueryExpr::ConfigCommand(cmd) => {
1613 use crate::storage::query::ast::ConfigCommand;
1614 match cmd {
1615 ConfigCommand::Put { collection, .. }
1616 | ConfigCommand::Get { collection, .. }
1617 | ConfigCommand::Resolve { collection, .. }
1618 | ConfigCommand::Rotate { collection, .. }
1619 | ConfigCommand::Delete { collection, .. }
1620 | ConfigCommand::History { collection, .. }
1621 | ConfigCommand::List { collection, .. }
1622 | ConfigCommand::Watch { collection, .. }
1623 | ConfigCommand::InvalidVolatileOperation { collection, .. } => {
1624 cache_scope_insert(scopes, collection)
1625 }
1626 }
1627 }
1628 _ => {}
1629 }
1630}
1631
1632pub(crate) fn rls_policy_filter(
1640 runtime: &RedDBRuntime,
1641 table: &str,
1642 action: crate::storage::query::ast::PolicyAction,
1643) -> Option<crate::storage::query::ast::Filter> {
1644 rls_policy_filter_for_kind(
1645 runtime,
1646 table,
1647 action,
1648 crate::storage::query::ast::PolicyTargetKind::Table,
1649 )
1650}
1651
1652pub(crate) fn rls_policy_filter_for_kind(
1658 runtime: &RedDBRuntime,
1659 table: &str,
1660 action: crate::storage::query::ast::PolicyAction,
1661 kind: crate::storage::query::ast::PolicyTargetKind,
1662) -> Option<crate::storage::query::ast::Filter> {
1663 use crate::storage::query::ast::Filter;
1664
1665 if !runtime.inner.rls_enabled_tables.read().contains(table) {
1666 return None;
1667 }
1668 let role = current_auth_identity().map(|(_, role)| role);
1669 let role_str = role.map(|r| r.as_str().to_string());
1670 let policies = runtime.matching_rls_policies_for_kind(table, role_str.as_deref(), action, kind);
1671 if policies.is_empty() {
1672 return None;
1673 }
1674 policies
1675 .into_iter()
1676 .reduce(|acc, f| Filter::Or(Box::new(acc), Box::new(f)))
1677}
1678
1679pub(crate) fn rls_is_enabled(runtime: &RedDBRuntime, table: &str) -> bool {
1683 runtime.inner.rls_enabled_tables.read().contains(table)
1684}
1685
1686fn node_passes_rls(
1693 runtime: &RedDBRuntime,
1694 collection: &str,
1695 role: Option<&str>,
1696 cache: &mut std::collections::HashMap<String, Option<crate::storage::query::ast::Filter>>,
1697 entity: &crate::storage::unified::entity::UnifiedEntity,
1698) -> bool {
1699 use crate::storage::query::ast::{Filter, PolicyAction, PolicyTargetKind};
1700
1701 if !runtime.inner.rls_enabled_tables.read().contains(collection) {
1702 return true;
1703 }
1704 let filter = cache.entry(collection.to_string()).or_insert_with(|| {
1705 let policies = runtime.matching_rls_policies_for_kind(
1706 collection,
1707 role,
1708 PolicyAction::Select,
1709 PolicyTargetKind::Nodes,
1710 );
1711 if policies.is_empty() {
1712 None
1713 } else {
1714 policies
1715 .into_iter()
1716 .reduce(|acc, f| Filter::Or(Box::new(acc), Box::new(f)))
1717 }
1718 });
1719 let Some(filter) = filter else {
1720 return false;
1721 };
1722 crate::runtime::query_exec::evaluate_entity_filter_with_db(
1723 Some(&runtime.inner.db),
1724 entity,
1725 filter,
1726 collection,
1727 collection,
1728 )
1729}
1730
1731fn edge_passes_rls(
1734 runtime: &RedDBRuntime,
1735 collection: &str,
1736 role: Option<&str>,
1737 cache: &mut std::collections::HashMap<String, Option<crate::storage::query::ast::Filter>>,
1738 entity: &crate::storage::unified::entity::UnifiedEntity,
1739) -> bool {
1740 use crate::storage::query::ast::{Filter, PolicyAction, PolicyTargetKind};
1741
1742 if !runtime.inner.rls_enabled_tables.read().contains(collection) {
1743 return true;
1744 }
1745 let filter = cache.entry(collection.to_string()).or_insert_with(|| {
1746 let policies = runtime.matching_rls_policies_for_kind(
1747 collection,
1748 role,
1749 PolicyAction::Select,
1750 PolicyTargetKind::Edges,
1751 );
1752 if policies.is_empty() {
1753 None
1754 } else {
1755 policies
1756 .into_iter()
1757 .reduce(|acc, f| Filter::Or(Box::new(acc), Box::new(f)))
1758 }
1759 });
1760 let Some(filter) = filter else {
1761 return false;
1762 };
1763 crate::runtime::query_exec::evaluate_entity_filter_with_db(
1764 Some(&runtime.inner.db),
1765 entity,
1766 filter,
1767 collection,
1768 collection,
1769 )
1770}
1771
1772fn inject_rls_filters(
1793 runtime: &RedDBRuntime,
1794 frame: &dyn super::statement_frame::ReadFrame,
1795 mut table: crate::storage::query::ast::TableQuery,
1796) -> Option<crate::storage::query::ast::TableQuery> {
1797 use crate::storage::query::ast::{Filter, PolicyAction};
1798
1799 let role = frame.identity().map(|(_, role)| role);
1801 let role_str = role.map(|r| r.as_str().to_string());
1802 let policies =
1803 runtime.matching_rls_policies(&table.table, role_str.as_deref(), PolicyAction::Select);
1804
1805 if policies.is_empty() {
1806 return None;
1809 }
1810
1811 let combined = policies
1813 .into_iter()
1814 .reduce(|acc, f| Filter::Or(Box::new(acc), Box::new(f)))
1815 .expect("policies non-empty");
1816
1817 use crate::storage::query::sql_lowering::{expr_to_filter, filter_to_expr};
1826 let had_where_expr = table.where_expr.is_some();
1827 let existing = table
1828 .filter
1829 .take()
1830 .or_else(|| table.where_expr.as_ref().map(expr_to_filter));
1831 let new_filter = match existing {
1832 Some(existing) => Filter::And(Box::new(existing), Box::new(combined)),
1833 None => combined,
1834 };
1835 if had_where_expr {
1838 table.where_expr = Some(filter_to_expr(&new_filter));
1839 }
1840 table.filter = Some(new_filter);
1841 Some(table)
1842}
1843
1844fn inject_rls_into_join(
1854 runtime: &RedDBRuntime,
1855 frame: &dyn super::statement_frame::ReadFrame,
1856 mut join: crate::storage::query::ast::JoinQuery,
1857) -> Option<crate::storage::query::ast::JoinQuery> {
1858 use crate::storage::query::ast::Filter;
1859
1860 let mut policy_filters: Vec<Filter> = Vec::new();
1861 if !collect_join_side_policy(runtime, frame, join.left.as_ref(), &mut policy_filters) {
1862 return None;
1863 }
1864 if !collect_join_side_policy(runtime, frame, join.right.as_ref(), &mut policy_filters) {
1865 return None;
1866 }
1867
1868 if policy_filters.is_empty() {
1869 return Some(join);
1870 }
1871
1872 let combined = policy_filters
1873 .into_iter()
1874 .reduce(|acc, f| Filter::And(Box::new(acc), Box::new(f)))
1875 .expect("policy_filters non-empty");
1876
1877 join.filter = Some(match join.filter.take() {
1878 Some(existing) => Filter::And(Box::new(existing), Box::new(combined)),
1879 None => combined,
1880 });
1881
1882 Some(join)
1883}
1884
1885fn collect_join_side_policy(
1890 runtime: &RedDBRuntime,
1891 frame: &dyn super::statement_frame::ReadFrame,
1892 expr: &crate::storage::query::ast::QueryExpr,
1893 out: &mut Vec<crate::storage::query::ast::Filter>,
1894) -> bool {
1895 use crate::storage::query::ast::{Filter, PolicyAction, QueryExpr};
1896 match expr {
1897 QueryExpr::Table(t) => {
1898 if !runtime.inner.rls_enabled_tables.read().contains(&t.table) {
1899 return true;
1900 }
1901 let role = frame.identity().map(|(_, role)| role);
1902 let role_str = role.map(|r| r.as_str().to_string());
1903 let policies =
1904 runtime.matching_rls_policies(&t.table, role_str.as_deref(), PolicyAction::Select);
1905 if policies.is_empty() {
1906 return false;
1907 }
1908 let combined = policies
1909 .into_iter()
1910 .reduce(|acc, f| Filter::Or(Box::new(acc), Box::new(f)))
1911 .expect("policies non-empty");
1912 out.push(combined);
1913 true
1914 }
1915 QueryExpr::Join(inner) => {
1916 collect_join_side_policy(runtime, frame, inner.left.as_ref(), out)
1917 && collect_join_side_policy(runtime, frame, inner.right.as_ref(), out)
1918 }
1919 _ => true,
1920 }
1921}
1922
1923fn apply_foreign_table_filters(
1934 records: Vec<crate::storage::query::unified::UnifiedRecord>,
1935 query: &crate::storage::query::ast::TableQuery,
1936) -> crate::storage::query::unified::UnifiedResult {
1937 use crate::storage::query::sql_lowering::{
1938 effective_table_filter, effective_table_projections,
1939 };
1940 use crate::storage::query::unified::UnifiedResult;
1941
1942 let filter = effective_table_filter(query);
1943 let projections = effective_table_projections(query);
1944
1945 let mut filtered: Vec<_> = records
1948 .into_iter()
1949 .filter(|record| match &filter {
1950 Some(f) => {
1951 super::join_filter::evaluate_runtime_filter_with_db(None, record, f, None, None)
1952 }
1953 None => true,
1954 })
1955 .collect();
1956
1957 if let Some(offset) = query.offset {
1959 let offset = offset as usize;
1960 if offset >= filtered.len() {
1961 filtered.clear();
1962 } else {
1963 filtered.drain(0..offset);
1964 }
1965 }
1966 if let Some(limit) = query.limit {
1967 filtered.truncate(limit as usize);
1968 }
1969
1970 let columns: Vec<String> = if projections.is_empty() {
1973 filtered
1974 .first()
1975 .map(|r| r.column_names().iter().map(|k| k.to_string()).collect())
1976 .unwrap_or_default()
1977 } else {
1978 projections
1979 .iter()
1980 .map(super::join_filter::projection_name)
1981 .collect()
1982 };
1983
1984 let mut result = UnifiedResult::empty();
1985 result.columns = columns;
1986 result.records = filtered;
1987 result
1988}
1989
1990pub(crate) fn collect_table_refs(expr: &QueryExpr) -> Vec<String> {
1997 let mut scopes: HashSet<String> = HashSet::new();
1998 collect_query_expr_result_cache_scopes(&mut scopes, expr);
1999 scopes.into_iter().collect()
2000}
2001
2002fn query_expr_result_cache_scopes(expr: &QueryExpr) -> HashSet<String> {
2003 let mut scopes = HashSet::new();
2004 collect_query_expr_result_cache_scopes(&mut scopes, expr);
2005 scopes
2006}
2007
2008fn strip_explain_prefix(sql: &str) -> Option<&str> {
2023 let trimmed = sql.trim_start();
2024 let (head, rest) = trimmed.split_at(
2025 trimmed
2026 .find(|c: char| c.is_whitespace())
2027 .unwrap_or(trimmed.len()),
2028 );
2029 if !head.eq_ignore_ascii_case("EXPLAIN") {
2030 return None;
2031 }
2032 let rest = rest.trim_start();
2033 if rest.is_empty() {
2034 return None;
2035 }
2036 let next_head_end = rest.find(|c: char| c.is_whitespace()).unwrap_or(rest.len());
2039 if rest[..next_head_end].eq_ignore_ascii_case("ALTER")
2040 || rest[..next_head_end].eq_ignore_ascii_case("ASK")
2041 || rest[..next_head_end].eq_ignore_ascii_case("MIGRATION")
2042 {
2043 return None;
2044 }
2045 Some(rest)
2046}
2047
2048fn parse_vcs_author(raw: &str) -> crate::application::vcs::Author {
2049 let trimmed = raw.trim();
2050 if let Some((name, rest)) = trimmed.rsplit_once('<') {
2051 if let Some(email) = rest.strip_suffix('>') {
2052 return crate::application::vcs::Author {
2053 name: name.trim().to_string(),
2054 email: email.trim().to_string(),
2055 };
2056 }
2057 }
2058 crate::application::vcs::Author {
2059 name: trimmed.to_string(),
2060 email: String::new(),
2061 }
2062}
2063
2064fn looks_like_commit_hash(value: &str) -> bool {
2065 value.len() >= 7 && value.bytes().all(|byte| byte.is_ascii_hexdigit())
2066}
2067
2068enum RuntimeVcsCommand {
2069 Checkpoint {
2070 message: String,
2071 author: Option<String>,
2072 },
2073 Checkout {
2074 target: String,
2075 },
2076 Reset {
2077 mode: RuntimeVcsResetMode,
2078 target: String,
2079 },
2080 Merge {
2081 branch: String,
2082 },
2083 CherryPick {
2084 commit: String,
2085 },
2086 Revert {
2087 commit: String,
2088 },
2089 ResolveConflict {
2090 key: String,
2091 resolution: RuntimeVcsConflictResolution,
2092 },
2093}
2094
2095enum RuntimeVcsResetMode {
2096 Hard,
2097 Soft,
2098 Mixed,
2099}
2100
2101enum RuntimeVcsConflictResolution {
2102 Ours,
2103 Theirs,
2104}
2105
2106fn strip_keyword_ci<'a>(input: &'a str, keyword: &str) -> Option<&'a str> {
2107 let trimmed = input.trim_start();
2108 if trimmed.len() < keyword.len() || !trimmed[..keyword.len()].eq_ignore_ascii_case(keyword) {
2109 return None;
2110 }
2111 let rest = &trimmed[keyword.len()..];
2112 if rest.is_empty() || rest.starts_with(char::is_whitespace) {
2113 Some(rest.trim_start())
2114 } else {
2115 None
2116 }
2117}
2118
2119fn parse_vcs_quoted(input: &str) -> RedDBResult<(String, &str)> {
2120 let trimmed = input.trim_start();
2121 let rest = trimmed
2122 .strip_prefix('\'')
2123 .ok_or_else(|| RedDBError::Query("expected quoted string".to_string()))?;
2124 let end = rest
2125 .find('\'')
2126 .ok_or_else(|| RedDBError::Query("unterminated quoted string".to_string()))?;
2127 Ok((rest[..end].to_string(), rest[end + 1..].trim_start()))
2128}
2129
2130fn parse_vcs_atom(input: &str) -> RedDBResult<(String, &str)> {
2131 let trimmed = input.trim_start();
2132 if trimmed.starts_with('\'') {
2133 return parse_vcs_quoted(trimmed);
2134 }
2135 let end = trimmed.find(char::is_whitespace).unwrap_or(trimmed.len());
2136 if end == 0 {
2137 return Err(RedDBError::Query("expected VCS argument".to_string()));
2138 }
2139 Ok((trimmed[..end].to_string(), trimmed[end..].trim_start()))
2140}
2141
2142fn expect_vcs_end(rest: &str) -> RedDBResult<()> {
2143 if rest.trim().is_empty() {
2144 Ok(())
2145 } else {
2146 Err(RedDBError::Query(format!(
2147 "unexpected token after VCS command: {}",
2148 rest.trim()
2149 )))
2150 }
2151}
2152
2153fn parse_runtime_vcs_command(query: &str) -> Option<RedDBResult<RuntimeVcsCommand>> {
2154 let trimmed = query.trim_start();
2155 if let Some(rest) = strip_keyword_ci(trimmed, "CHECKPOINT") {
2156 return Some((|| {
2157 let (message, rest) = parse_vcs_quoted(rest)?;
2158 let rest = rest.trim_start();
2159 let author = if let Some(author_rest) = strip_keyword_ci(rest, "AUTHOR") {
2160 let (author, rest) = parse_vcs_quoted(author_rest)?;
2161 expect_vcs_end(rest)?;
2162 Some(author)
2163 } else {
2164 expect_vcs_end(rest)?;
2165 None
2166 };
2167 Ok(RuntimeVcsCommand::Checkpoint { message, author })
2168 })());
2169 }
2170 if let Some(rest) = strip_keyword_ci(trimmed, "CHECKOUT") {
2171 return Some((|| {
2172 let (target, rest) = parse_vcs_atom(rest)?;
2173 expect_vcs_end(rest)?;
2174 Ok(RuntimeVcsCommand::Checkout { target })
2175 })());
2176 }
2177 if let Some(rest) = strip_keyword_ci(trimmed, "RESET") {
2178 if strip_keyword_ci(rest, "TENANT").is_some() {
2179 return None;
2180 }
2181 return Some((|| {
2182 let mut rest = rest.trim_start();
2183 let mode = if let Some(next) = strip_keyword_ci(rest, "HARD") {
2184 rest = next;
2185 RuntimeVcsResetMode::Hard
2186 } else if let Some(next) = strip_keyword_ci(rest, "SOFT") {
2187 rest = next;
2188 RuntimeVcsResetMode::Soft
2189 } else if let Some(next) = strip_keyword_ci(rest, "MIXED") {
2190 rest = next;
2191 RuntimeVcsResetMode::Mixed
2192 } else {
2193 RuntimeVcsResetMode::Mixed
2194 };
2195 let rest = strip_keyword_ci(rest, "TO")
2196 .ok_or_else(|| RedDBError::Query("expected TO in RESET".to_string()))?;
2197 let (target, rest) = parse_vcs_atom(rest)?;
2198 expect_vcs_end(rest)?;
2199 Ok(RuntimeVcsCommand::Reset { mode, target })
2200 })());
2201 }
2202 if let Some(rest) = strip_keyword_ci(trimmed, "MERGE") {
2203 return Some((|| {
2204 let (branch, rest) = parse_vcs_atom(rest)?;
2205 expect_vcs_end(rest)?;
2206 Ok(RuntimeVcsCommand::Merge { branch })
2207 })());
2208 }
2209 if let Some(rest) = strip_keyword_ci(trimmed, "CHERRY") {
2210 return Some((|| {
2211 let rest = strip_keyword_ci(rest, "PICK")
2212 .ok_or_else(|| RedDBError::Query("expected PICK in CHERRY PICK".to_string()))?;
2213 let (commit, rest) = parse_vcs_atom(rest)?;
2214 expect_vcs_end(rest)?;
2215 Ok(RuntimeVcsCommand::CherryPick { commit })
2216 })());
2217 }
2218 if let Some(rest) = strip_keyword_ci(trimmed, "REVERT") {
2219 return Some((|| {
2220 let (commit, rest) = parse_vcs_atom(rest)?;
2221 expect_vcs_end(rest)?;
2222 Ok(RuntimeVcsCommand::Revert { commit })
2223 })());
2224 }
2225 if let Some(rest) = strip_keyword_ci(trimmed, "RESOLVE") {
2226 let rest = strip_keyword_ci(rest, "CONFLICT")?;
2231 return Some((|| {
2232 let (key, rest) = parse_vcs_quoted(rest)?;
2233 let rest = strip_keyword_ci(rest, "USING")
2234 .ok_or_else(|| RedDBError::Query("expected USING in RESOLVE".to_string()))?;
2235 let (resolution, rest) = if let Some(rest) = strip_keyword_ci(rest, "OURS") {
2236 (RuntimeVcsConflictResolution::Ours, rest)
2237 } else if let Some(rest) = strip_keyword_ci(rest, "THEIRS") {
2238 (RuntimeVcsConflictResolution::Theirs, rest)
2239 } else {
2240 return Err(RedDBError::Query(
2241 "expected OURS or THEIRS in RESOLVE".to_string(),
2242 ));
2243 };
2244 expect_vcs_end(rest)?;
2245 Ok(RuntimeVcsCommand::ResolveConflict { key, resolution })
2246 })());
2247 }
2248 None
2249}
2250
2251pub(super) fn has_with_prefix(sql: &str) -> bool {
2256 let trimmed = sql.trim_start();
2257 let head_end = trimmed
2258 .find(|c: char| c.is_whitespace() || c == '(')
2259 .unwrap_or(trimmed.len());
2260 trimmed[..head_end].eq_ignore_ascii_case("WITH")
2261}
2262
2263fn peek_top_level_as_of(sql: &str) -> Option<crate::application::vcs::AsOfSpec> {
2271 peek_top_level_as_of_with_table(sql).map(|(spec, _)| spec)
2272}
2273
2274pub(super) fn peek_top_level_as_of_with_table(
2279 sql: &str,
2280) -> Option<(crate::application::vcs::AsOfSpec, Option<String>)> {
2281 if !sql
2282 .as_bytes()
2283 .windows(5)
2284 .any(|w| w.eq_ignore_ascii_case(b"as of"))
2285 {
2286 return None;
2287 }
2288 let parsed = crate::storage::query::parser::parse(sql).ok()?;
2289 let crate::storage::query::ast::QueryExpr::Table(table) = parsed.query else {
2290 return None;
2291 };
2292 let clause = table.as_of?;
2293 let table_name = if table.table.is_empty() || table.table == "any" {
2294 None
2295 } else {
2296 Some(table.table.clone())
2297 };
2298 let spec = match clause {
2299 crate::storage::query::ast::AsOfClause::Commit(h) => {
2300 crate::application::vcs::AsOfSpec::Commit(h)
2301 }
2302 crate::storage::query::ast::AsOfClause::Branch(b) => {
2303 crate::application::vcs::AsOfSpec::Branch(b)
2304 }
2305 crate::storage::query::ast::AsOfClause::Tag(t) => crate::application::vcs::AsOfSpec::Tag(t),
2306 crate::storage::query::ast::AsOfClause::TimestampMs(ts) => {
2307 crate::application::vcs::AsOfSpec::TimestampMs(ts)
2308 }
2309 crate::storage::query::ast::AsOfClause::Snapshot(x) => {
2310 crate::application::vcs::AsOfSpec::Snapshot(x)
2311 }
2312 };
2313 Some((spec, table_name))
2314}
2315
2316pub(super) fn query_has_volatile_builtin(sql: &str) -> bool {
2317 const VOLATILE_TOKENS: &[&str] = &[
2321 "pg_advisory_lock",
2322 "pg_try_advisory_lock",
2323 "pg_advisory_unlock",
2324 "random()",
2325 "$config",
2330 "$secret",
2331 "$kv",
2332 ];
2337 let lowered = sql.to_ascii_lowercase();
2338 VOLATILE_TOKENS.iter().any(|t| lowered.contains(t))
2339}
2340
2341pub(super) fn query_is_ask_statement(sql: &str) -> bool {
2342 let trimmed = sql.trim_start();
2343 let head_end = trimmed
2344 .find(|c: char| c.is_whitespace() || c == '(' || c == ';')
2345 .unwrap_or(trimmed.len());
2346 trimmed[..head_end].eq_ignore_ascii_case("ASK")
2347}
2348
2349pub(super) fn intent_lock_modes_for(
2359 expr: &QueryExpr,
2360) -> Option<(
2361 crate::storage::transaction::lock::LockMode,
2362 crate::storage::transaction::lock::LockMode,
2363)> {
2364 use crate::storage::transaction::lock::LockMode::{Exclusive, IntentExclusive, IntentShared};
2365
2366 match expr {
2367 QueryExpr::Table(_)
2369 | QueryExpr::Join(_)
2370 | QueryExpr::Vector(_)
2371 | QueryExpr::Hybrid(_)
2372 | QueryExpr::Graph(_)
2373 | QueryExpr::Path(_)
2374 | QueryExpr::Ask(_)
2375 | QueryExpr::SearchCommand(_)
2376 | QueryExpr::GraphCommand(_)
2377 | QueryExpr::RankOf(_)
2378 | QueryExpr::ApproxRankOf(_)
2379 | QueryExpr::RankRange(_)
2380 | QueryExpr::QueueSelect(_) => Some((IntentShared, IntentShared)),
2381
2382 QueryExpr::Insert(_)
2390 | QueryExpr::Update(_)
2391 | QueryExpr::Delete(_)
2392 | QueryExpr::QueueCommand(QueueCommand::Move { .. }) => {
2393 Some((IntentExclusive, IntentExclusive))
2394 }
2395 QueryExpr::QueueCommand(_) => Some((IntentShared, IntentShared)),
2396
2397 QueryExpr::CreateTable(_)
2401 | QueryExpr::CreateCollection(_)
2402 | QueryExpr::CreateVector(_)
2403 | QueryExpr::DropTable(_)
2404 | QueryExpr::DropGraph(_)
2405 | QueryExpr::DropVector(_)
2406 | QueryExpr::DropDocument(_)
2407 | QueryExpr::DropKv(_)
2408 | QueryExpr::DropCollection(_)
2409 | QueryExpr::Truncate(_)
2410 | QueryExpr::AlterTable(_)
2411 | QueryExpr::CreateIndex(_)
2412 | QueryExpr::DropIndex(_)
2413 | QueryExpr::CreateTimeSeries(_)
2414 | QueryExpr::CreateMetric(_)
2415 | QueryExpr::AlterMetric(_)
2416 | QueryExpr::CreateSlo(_)
2417 | QueryExpr::DropTimeSeries(_)
2418 | QueryExpr::CreateQueue(_)
2419 | QueryExpr::AlterQueue(_)
2420 | QueryExpr::DropQueue(_)
2421 | QueryExpr::CreateTree(_)
2422 | QueryExpr::DropTree(_)
2423 | QueryExpr::CreatePolicy(_)
2424 | QueryExpr::DropPolicy(_)
2425 | QueryExpr::CreateView(_)
2426 | QueryExpr::DropView(_)
2427 | QueryExpr::RefreshMaterializedView(_)
2428 | QueryExpr::CreateSchema(_)
2429 | QueryExpr::DropSchema(_)
2430 | QueryExpr::CreateSequence(_)
2431 | QueryExpr::DropSequence(_)
2432 | QueryExpr::CreateServer(_)
2433 | QueryExpr::DropServer(_)
2434 | QueryExpr::CreateForeignTable(_)
2435 | QueryExpr::DropForeignTable(_) => Some((IntentExclusive, Exclusive)),
2436
2437 _ => None,
2443 }
2444}
2445
2446pub(super) fn collections_referenced(expr: &QueryExpr) -> Vec<String> {
2451 let mut out = Vec::new();
2452 walk_collections(expr, &mut out);
2453 out.sort();
2454 out.dedup();
2455 out
2456}
2457
2458fn walk_collections(expr: &QueryExpr, out: &mut Vec<String>) {
2459 match expr {
2460 QueryExpr::Table(t) => out.push(t.table.clone()),
2461 QueryExpr::Join(j) => {
2462 walk_collections(&j.left, out);
2463 walk_collections(&j.right, out);
2464 }
2465 QueryExpr::Insert(i) => out.push(i.table.clone()),
2466 QueryExpr::Update(u) => out.push(u.table.clone()),
2467 QueryExpr::Delete(d) => out.push(d.table.clone()),
2468 QueryExpr::QueueSelect(q) => out.push(q.queue.clone()),
2469
2470 QueryExpr::CreateTable(q) => out.push(q.name.clone()),
2475 QueryExpr::CreateCollection(q) => out.push(q.name.clone()),
2476 QueryExpr::CreateVector(q) => out.push(q.name.clone()),
2477 QueryExpr::DropTable(q) => out.push(q.name.clone()),
2478 QueryExpr::DropGraph(q) => out.push(q.name.clone()),
2479 QueryExpr::DropVector(q) => out.push(q.name.clone()),
2480 QueryExpr::DropDocument(q) => out.push(q.name.clone()),
2481 QueryExpr::DropKv(q) => out.push(q.name.clone()),
2482 QueryExpr::DropCollection(q) => out.push(q.name.clone()),
2483 QueryExpr::Truncate(q) => out.push(q.name.clone()),
2484 QueryExpr::AlterTable(q) => out.push(q.name.clone()),
2485 QueryExpr::CreateIndex(q) => out.push(q.table.clone()),
2486 QueryExpr::DropIndex(q) => out.push(q.table.clone()),
2487 QueryExpr::CreateTimeSeries(q) => out.push(q.name.clone()),
2488 QueryExpr::CreateMetric(q) => out.push(q.path.clone()),
2489 QueryExpr::AlterMetric(q) => out.push(q.path.clone()),
2490 QueryExpr::CreateSlo(q) => out.push(q.path.clone()),
2491 QueryExpr::DropTimeSeries(q) => out.push(q.name.clone()),
2492 QueryExpr::CreateQueue(q) => out.push(q.name.clone()),
2493 QueryExpr::AlterQueue(q) => out.push(q.name.clone()),
2494 QueryExpr::DropQueue(q) => out.push(q.name.clone()),
2495 QueryExpr::QueueCommand(QueueCommand::Move {
2496 source,
2497 destination,
2498 ..
2499 }) => {
2500 out.push(source.clone());
2501 out.push(destination.clone());
2502 }
2503 QueryExpr::CreatePolicy(q) => out.push(q.table.clone()),
2504 QueryExpr::CreateView(q) => out.push(q.name.clone()),
2505 QueryExpr::DropView(q) => out.push(q.name.clone()),
2506 QueryExpr::RefreshMaterializedView(q) => out.push(q.name.clone()),
2507
2508 _ => {}
2514 }
2515}
2516
2517impl RedDBRuntime {
2518 pub fn in_memory() -> RedDBResult<Self> {
2519 Self::with_options(RedDBOptions::in_memory())
2520 }
2521
2522 pub fn flush(&self) -> RedDBResult<()> {
2523 self.inner
2524 .db
2525 .flush()
2526 .map_err(|err| RedDBError::Internal(err.to_string()))
2527 }
2528
2529 pub fn lock_manager(&self) -> std::sync::Arc<crate::storage::transaction::lock::LockManager> {
2533 self.inner.lock_manager.clone()
2534 }
2535
2536 pub fn config_registry(&self) -> std::sync::Arc<crate::auth::registry::ConfigRegistry> {
2538 self.inner.config_registry.clone()
2539 }
2540
2541 pub fn query_audit(&self) -> std::sync::Arc<crate::runtime::query_audit::QueryAuditStream> {
2542 self.inner.query_audit.clone()
2543 }
2544
2545 pub fn control_events_require_persistence(&self) -> bool {
2546 self.inner.control_event_config.require_persistence()
2547 }
2548
2549 pub fn control_event_config(&self) -> crate::runtime::control_events::ControlEventConfig {
2550 self.inner.control_event_config
2551 }
2552
2553 pub fn control_event_ledger(
2554 &self,
2555 ) -> Arc<dyn crate::runtime::control_events::ControlEventLedger> {
2556 self.inner.control_event_ledger.read().clone()
2557 }
2558
2559 #[doc(hidden)]
2560 pub fn replace_control_event_ledger_for_tests(
2561 &self,
2562 ledger: Arc<dyn crate::runtime::control_events::ControlEventLedger>,
2563 ) {
2564 *self.inner.control_event_ledger.write() = ledger;
2565 }
2566
2567 #[inline(never)]
2568 pub fn with_options(options: RedDBOptions) -> RedDBResult<Self> {
2569 Self::with_pool(options, ConnectionPoolConfig::default())
2570 }
2571
2572 pub fn with_pool(
2573 options: RedDBOptions,
2574 pool_config: ConnectionPoolConfig,
2575 ) -> RedDBResult<Self> {
2576 let boot_open_start_ms = std::time::SystemTime::now()
2584 .duration_since(std::time::UNIX_EPOCH)
2585 .map(|d| d.as_millis() as u64)
2586 .unwrap_or(0);
2587 let embedded_single_file = options.storage_profile.deploy_profile
2588 == crate::storage::DeployProfile::Embedded
2589 && options.storage_profile.packaging == crate::storage::StoragePackaging::SingleFile;
2590 let db = Arc::new(
2591 RedDB::open_with_options(&options)
2592 .map_err(|err| RedDBError::Internal(err.to_string()))?,
2593 );
2594 let result_blob_cache_config = if embedded_single_file {
2595 crate::storage::cache::BlobCacheConfig::default()
2596 } else {
2597 crate::storage::cache::BlobCacheConfig::default().with_l2_path(
2598 reddb_file::layout::result_cache_l2_path(
2599 &options.resolved_path(reddb_file::default_database_path()),
2600 ),
2601 )
2602 };
2603 let result_blob_cache =
2604 crate::storage::cache::BlobCache::open_with_l2(result_blob_cache_config).map_err(
2605 |err| RedDBError::Internal(format!("open result Blob Cache L2 failed: {err:?}")),
2606 )?;
2607 let storage_ready_ms = std::time::SystemTime::now()
2608 .duration_since(std::time::UNIX_EPOCH)
2609 .map(|d| d.as_millis() as u64)
2610 .unwrap_or(0);
2611
2612 let runtime = Self {
2613 inner: Arc::new(RuntimeInner {
2614 db: db.clone(),
2615 layout: PhysicalLayout::from_options(&options),
2616 embedded_single_file,
2617 indices: IndexCatalog::register_default_vector_graph(
2618 options.has_capability(crate::api::Capability::Table),
2619 options.has_capability(crate::api::Capability::Graph),
2620 ),
2621 pool_config,
2622 pool: Mutex::new(PoolState::default()),
2623 started_at_unix_ms: SystemTime::now()
2624 .duration_since(UNIX_EPOCH)
2625 .unwrap_or_default()
2626 .as_millis(),
2627 probabilistic: super::probabilistic_store::ProbabilisticStore::new(),
2628 index_store: super::index_store::IndexStore::new(),
2629 cdc: crate::replication::cdc::CdcBuffer::new(100_000),
2630 backup_scheduler: crate::replication::scheduler::BackupScheduler::new(3600),
2631 query_cache: parking_lot::RwLock::new(
2632 crate::storage::query::planner::cache::PlanCache::new(1000),
2633 ),
2634 result_cache: parking_lot::RwLock::new((
2635 HashMap::new(),
2636 std::collections::VecDeque::new(),
2637 )),
2638 result_blob_cache,
2639 result_blob_entries: parking_lot::RwLock::new((
2640 HashMap::new(),
2641 std::collections::VecDeque::new(),
2642 )),
2643 ask_answer_cache_entries: parking_lot::RwLock::new((
2644 HashSet::new(),
2645 std::collections::VecDeque::new(),
2646 )),
2647 result_cache_shadow_divergences: std::sync::atomic::AtomicU64::new(0),
2648 result_cache_hits: std::sync::atomic::AtomicU64::new(0),
2649 result_cache_misses: std::sync::atomic::AtomicU64::new(0),
2650 result_cache_evictions: std::sync::atomic::AtomicU64::new(0),
2651 ask_daily_spend: parking_lot::RwLock::new(HashMap::new()),
2652 queue_message_locks: parking_lot::RwLock::new(HashMap::new()),
2653 rmw_locks: RmwLockTable::new(),
2654 planner_dirty_tables: parking_lot::RwLock::new(HashSet::new()),
2655 ec_registry: Arc::new(crate::ec::config::EcRegistry::new()),
2656 config_registry: Arc::new(crate::auth::registry::ConfigRegistry::new()),
2657 ec_worker: crate::ec::worker::EcWorker::new(),
2658 auth_store: parking_lot::RwLock::new(None),
2659 oauth_validator: parking_lot::RwLock::new(None),
2660 browser_token_authority: parking_lot::RwLock::new(None),
2661 views: parking_lot::RwLock::new(HashMap::new()),
2662 materialized_views: parking_lot::RwLock::new(
2663 crate::storage::cache::result::MaterializedViewCache::new(),
2664 ),
2665 retention_sweeper: parking_lot::RwLock::new(
2666 crate::runtime::retention_sweeper::RetentionSweeperState::new(),
2667 ),
2668 snapshot_manager: Arc::new(
2669 crate::storage::transaction::snapshot::SnapshotManager::new(),
2670 ),
2671 tx_contexts: parking_lot::RwLock::new(HashMap::new()),
2672 tx_local_tenants: parking_lot::RwLock::new(HashMap::new()),
2673 env_config_overrides: crate::runtime::config_overlay::collect_env_overrides(),
2674 lock_manager: Arc::new({
2675 let env = crate::runtime::config_overlay::collect_env_overrides();
2680 let timeout_ms = env
2681 .get("concurrency.locking.deadlock_timeout_ms")
2682 .and_then(|raw| raw.parse::<u64>().ok())
2683 .unwrap_or_else(|| {
2684 match crate::runtime::config_matrix::default_for(
2685 "concurrency.locking.deadlock_timeout_ms",
2686 ) {
2687 Some(crate::serde_json::Value::Number(n)) => n as u64,
2688 _ => 5000,
2689 }
2690 });
2691 let cfg = crate::storage::transaction::lock::LockConfig {
2692 default_timeout: std::time::Duration::from_millis(timeout_ms),
2693 ..Default::default()
2694 };
2695 crate::storage::transaction::lock::LockManager::new(cfg)
2696 }),
2697 rls_policies: parking_lot::RwLock::new(HashMap::new()),
2698 rls_enabled_tables: parking_lot::RwLock::new(HashSet::new()),
2699 foreign_tables: Arc::new(crate::storage::fdw::ForeignTableRegistry::with_builtins()),
2700 pending_tombstones: parking_lot::RwLock::new(HashMap::new()),
2701 pending_versioned_updates: parking_lot::RwLock::new(HashMap::new()),
2702 pending_kv_watch_events: parking_lot::RwLock::new(HashMap::new()),
2703 pending_store_wal_actions: parking_lot::RwLock::new(HashMap::new()),
2704 pending_claim_locks: parking_lot::RwLock::new(HashMap::new()),
2705 queue_wait_registry: std::sync::Arc::new(
2706 crate::runtime::queue_wait_registry::QueueWaitRegistry::new(),
2707 ),
2708 pending_queue_wakes: parking_lot::RwLock::new(HashMap::new()),
2709 tenant_tables: parking_lot::RwLock::new(HashMap::new()),
2710 ddl_epoch: std::sync::atomic::AtomicU64::new(0),
2711 write_gate: Arc::new(crate::runtime::write_gate::WriteGate::from_options(
2712 &options,
2713 )),
2714 lifecycle: crate::runtime::lifecycle::Lifecycle::new(),
2715 resource_limits: crate::runtime::resource_limits::ResourceLimits::from_env(),
2716 audit_log: {
2717 let data_path = if embedded_single_file {
2733 std::env::temp_dir()
2734 .join("reddb-embedded-runtime")
2735 .join(format!("audit-{}", std::process::id()))
2736 } else {
2737 options
2738 .data_path
2739 .clone()
2740 .unwrap_or_else(|| std::env::temp_dir().join("reddb"))
2741 };
2742 let (audit_dest, _) = crate::api::tier_wiring::current_log_destinations();
2743 if !matches!(audit_dest, crate::storage::layout::LogDestination::File(_))
2744 && (embedded_single_file
2745 || options
2746 .metadata
2747 .contains_key(crate::api::EPHEMERAL_RUNTIME_METADATA_KEY))
2748 {
2749 let audit_path = reddb_file::layout::sibling_path(
2759 &data_path,
2760 &reddb_file::layout::sidecar_file_name(&data_path, "audit.log"),
2761 );
2762 Arc::new(crate::runtime::audit_log::AuditLogger::with_path(
2763 audit_path,
2764 ))
2765 } else {
2766 Arc::new(crate::runtime::audit_log::AuditLogger::for_destination(
2767 &audit_dest,
2768 &data_path,
2769 ))
2770 }
2771 },
2772 control_event_ledger: parking_lot::RwLock::new(Arc::new(
2773 crate::runtime::control_events::RuntimeLedger::new(db.store()),
2774 )),
2775 control_event_config: options.control_events,
2776 query_audit: Arc::new(crate::runtime::query_audit::QueryAuditStream::new(
2777 db.store(),
2778 options.query_audit.clone(),
2779 )),
2780 lease_lifecycle: std::sync::OnceLock::new(),
2781 replica_apply_metrics: std::sync::Arc::new(
2782 crate::replication::logical::ReplicaApplyMetrics::default(),
2783 ),
2784 replica_link_metrics: std::sync::Arc::new(
2785 crate::replication::reconnect::ReplicaLinkMetrics::default(),
2786 ),
2787 quota_bucket: crate::runtime::quota_bucket::QuotaBucket::from_env(),
2788 schema_vocabulary: parking_lot::RwLock::new(
2789 crate::runtime::schema_vocabulary::SchemaVocabulary::new(),
2790 ),
2791 slow_query_logger: {
2792 let fallback_dir = if embedded_single_file {
2809 std::env::temp_dir()
2810 .join("reddb-embedded-runtime")
2811 .join(format!("slow-{}", std::process::id()))
2812 } else {
2813 options
2814 .data_path
2815 .as_ref()
2816 .and_then(|p| p.parent().map(std::path::PathBuf::from))
2817 .unwrap_or_else(|| std::env::temp_dir().join("reddb"))
2818 };
2819 let threshold_ms = std::env::var("RED_SLOW_QUERY_THRESHOLD_MS")
2820 .ok()
2821 .and_then(|s| s.parse::<u64>().ok())
2822 .unwrap_or(1000);
2823 let sample_pct = std::env::var("RED_SLOW_QUERY_SAMPLE_PCT")
2824 .ok()
2825 .and_then(|s| s.parse::<u8>().ok())
2826 .unwrap_or(100);
2827 let (_, slow_dest) = crate::api::tier_wiring::current_log_destinations();
2828 crate::telemetry::slow_query_logger::SlowQueryLogger::for_destination(
2829 &slow_dest,
2830 &fallback_dir,
2831 threshold_ms,
2832 sample_pct,
2833 )
2834 },
2835 slow_query_store: crate::telemetry::slow_query_store::SlowQueryStore::new(
2836 crate::telemetry::slow_query_store::DEFAULT_CAP,
2837 ),
2838 kv_stats: crate::runtime::KvStatsCounters::default(),
2839 metrics_ingest_stats: crate::runtime::MetricsIngestCounters::default(),
2840 metrics_tenant_activity_stats:
2841 crate::runtime::MetricsTenantActivityCounters::default(),
2842 claim_telemetry: Arc::new(
2843 crate::runtime::claim_telemetry::ClaimTelemetryCounters::default(),
2844 ),
2845 queue_telemetry: Arc::new(
2846 crate::runtime::queue_telemetry::QueueTelemetryCounters::default(),
2847 ),
2848 query_latency_telemetry: Arc::new(
2849 crate::runtime::query_latency_telemetry::QueryLatencyTelemetry::default(),
2850 ),
2851 occupancy_sampler: Arc::new(
2852 crate::runtime::occupancy_sampler::OccupancySampler::new(),
2853 ),
2854 node_load_telemetry: Arc::new(
2855 crate::runtime::node_load_telemetry::NodeLoadTelemetry::default(),
2856 ),
2857 queue_presence: Arc::new(
2858 crate::storage::queue::presence::ConsumerPresenceRegistry::new(),
2859 ),
2860 vector_introspection: Arc::new(
2861 crate::storage::vector::introspection::VectorIntrospectionRegistry::new(),
2862 ),
2863 kv_tag_index: crate::runtime::KvTagIndex::default(),
2864 chain_tip_cache: parking_lot::Mutex::new(HashMap::new()),
2865 chain_integrity_broken: parking_lot::Mutex::new(HashMap::new()),
2866 integrity_tombstones: parking_lot::Mutex::new(Vec::new()),
2867 integrity_tombstones_state: std::sync::atomic::AtomicU8::new(0),
2868 }),
2869 };
2870
2871 crate::telemetry::operator_event::install_global_audit_sink(Arc::clone(
2877 &runtime.inner.audit_log,
2878 ));
2879
2880 runtime
2883 .inner
2884 .slow_query_logger
2885 .attach_store(Arc::clone(&runtime.inner.slow_query_store));
2886
2887 runtime
2895 .inner
2896 .lifecycle
2897 .set_restore_started_at_ms(boot_open_start_ms);
2898 runtime
2899 .inner
2900 .lifecycle
2901 .set_restore_ready_at_ms(storage_ready_ms);
2902 runtime
2903 .inner
2904 .lifecycle
2905 .set_wal_replay_started_at_ms(boot_open_start_ms);
2906 runtime
2907 .inner
2908 .lifecycle
2909 .set_wal_replay_ready_at_ms(storage_ready_ms);
2910
2911 let restored_cdc_lsn = runtime
2912 .inner
2913 .db
2914 .replication
2915 .as_ref()
2916 .map(|repl| {
2917 repl.logical_wal_spool
2918 .as_ref()
2919 .map(|spool| spool.current_lsn())
2920 .unwrap_or(0)
2921 })
2922 .unwrap_or(0)
2923 .max(runtime.config_u64("red.config.timeline.last_archived_lsn", 0));
2924 runtime.inner.cdc.set_current_lsn(restored_cdc_lsn);
2925 runtime.rehydrate_snapshot_xid_floor();
2926 runtime
2927 .bootstrap_system_keyed_collections()
2928 .map_err(|err| RedDBError::Internal(format!("bootstrap system collections: {err}")))?;
2929 runtime.rehydrate_declared_column_schemas();
2930 runtime.rehydrate_runtime_index_registry()?;
2931 runtime
2932 .load_probabilistic_state()
2933 .map_err(|err| RedDBError::Internal(format!("load probabilistic state: {err}")))?;
2934
2935 runtime.rehydrate_tenant_tables();
2939 runtime.rehydrate_materialized_view_descriptors();
2944 if let Some(repl) = &runtime.inner.db.replication {
2945 repl.wal_buffer.set_current_lsn(restored_cdc_lsn);
2946 }
2947
2948 {
2950 let sys = SystemInfo::collect();
2951 runtime.inner.db.store().set_config_tree(
2952 "red.system",
2953 &crate::serde_json::json!({
2954 "pid": sys.pid,
2955 "cpu_cores": sys.cpu_cores,
2956 "total_memory_bytes": sys.total_memory_bytes,
2957 "available_memory_bytes": sys.available_memory_bytes,
2958 "os": sys.os,
2959 "arch": sys.arch,
2960 "hostname": sys.hostname,
2961 "started_at": SystemTime::now()
2962 .duration_since(UNIX_EPOCH)
2963 .unwrap_or_default()
2964 .as_millis() as u64
2965 }),
2966 );
2967
2968 let store = runtime.inner.db.store();
2970 if store
2971 .get_collection("red_config")
2972 .map(|m| m.query_all(|_| true).len())
2973 .unwrap_or(0)
2974 <= 10
2975 {
2976 store.set_config_tree("red.ai", &crate::json!({
2977 "default": crate::json!({
2978 "provider": "openai",
2979 "model": crate::ai::DEFAULT_OPENAI_PROMPT_MODEL
2980 }),
2981 "max_embedding_inputs": 256,
2982 "max_prompt_batch": 256,
2983 "timeout": crate::json!({ "connect_secs": 10, "read_secs": 90, "write_secs": 30 })
2984 }));
2985 store.set_config_tree(
2986 "red.server",
2987 &crate::json!({
2988 "max_scan_limit": 1000,
2989 "max_body_size": 1048576,
2990 "read_timeout_ms": 5000,
2991 "write_timeout_ms": 5000
2992 }),
2993 );
2994 store.set_config_tree(
2995 "red.storage",
2996 &crate::json!({
2997 "page_size": 4096,
2998 "page_cache_capacity": 100000,
2999 "auto_checkpoint_pages": 1000,
3000 "snapshot_retention": 16,
3001 "verify_checksums": true,
3002 "segment": crate::json!({
3003 "max_entities": 100000,
3004 "max_bytes": 268435456_u64,
3005 "compression_level": 6
3006 }),
3007 "hnsw": crate::json!({ "m": 16, "ef_construction": 100, "ef_search": 50 }),
3008 "ivf": crate::json!({ "n_lists": 100, "n_probes": 10 }),
3009 "bm25": crate::json!({ "k1": 1.2, "b": 0.75 })
3010 }),
3011 );
3012 store.set_config_tree(
3013 "red.search",
3014 &crate::json!({
3015 "rag": crate::json!({
3016 "max_chunks_per_source": 10,
3017 "max_total_chunks": 25,
3018 "similarity_threshold": 0.8,
3019 "graph_depth": 2,
3020 "min_relevance": 0.3
3021 }),
3022 "fusion": crate::json!({
3023 "vector_weight": 0.5,
3024 "graph_weight": 0.3,
3025 "table_weight": 0.2,
3026 "dedup_threshold": 0.85
3027 })
3028 }),
3029 );
3030 store.set_config_tree(
3031 "red.auth",
3032 &crate::json!({
3033 "enabled": false,
3034 "session_ttl_secs": 3600,
3035 "require_auth": false
3036 }),
3037 );
3038 store.set_config_tree(
3039 "red.query",
3040 &crate::json!({
3041 "connection_pool": crate::json!({ "max_connections": 64, "max_idle": 16 }),
3042 "max_recursion_depth": 1000
3043 }),
3044 );
3045 store.set_config_tree(
3046 "red.indexes",
3047 &crate::json!({
3048 "auto_select": true,
3049 "bloom_filter": crate::json!({
3050 "enabled": true,
3051 "false_positive_rate": 0.01,
3052 "prune_on_scan": true
3053 }),
3054 "hash": crate::json!({ "enabled": true }),
3055 "bitmap": crate::json!({ "enabled": true, "max_cardinality": 1000 }),
3056 "spatial": crate::json!({ "enabled": true })
3057 }),
3058 );
3059 store.set_config_tree(
3060 "red.memtable",
3061 &crate::json!({
3062 "enabled": true,
3063 "max_bytes": 67108864_u64,
3064 "flush_threshold": 0.75
3065 }),
3066 );
3067 store.set_config_tree(
3068 "red.probabilistic",
3069 &crate::json!({
3070 "hll_registers": 16384,
3071 "sketch_default_width": 1000,
3072 "sketch_default_depth": 5,
3073 "filter_default_capacity": 100000
3074 }),
3075 );
3076 store.set_config_tree(
3077 "red.timeseries",
3078 &crate::json!({
3079 "default_chunk_size": 1024,
3080 "compression": crate::json!({
3081 "timestamps": "delta_of_delta",
3082 "values": "gorilla_xor"
3083 }),
3084 "default_retention_days": 0
3085 }),
3086 );
3087 store.set_config_tree(
3088 "red.queue",
3089 &crate::json!({
3090 "default_max_size": 0,
3091 "default_max_attempts": 3,
3092 "visibility_timeout_ms": 30000,
3093 "consumer_idle_timeout_ms": 60000
3094 }),
3095 );
3096 store.set_config_tree(
3097 "red.backup",
3098 &crate::json!({
3099 "enabled": false,
3100 "interval_secs": 3600,
3101 "retention_count": 24,
3102 "upload": false,
3103 "backend": "local"
3104 }),
3105 );
3106 store.set_config_tree(
3107 "red.wal",
3108 &crate::json!({
3109 "archive": crate::json!({
3110 "enabled": false,
3111 "retention_hours": 168,
3112 "prefix": reddb_file::backup_wal_prefix("")
3113 })
3114 }),
3115 );
3116 store.set_config_tree(
3117 "red.cdc",
3118 &crate::json!({
3119 "enabled": true,
3120 "buffer_size": 100000
3121 }),
3122 );
3123 store.set_config_tree(
3124 "red.config.secret",
3125 &crate::json!({
3126 "auto_encrypt": true,
3127 "auto_decrypt": true
3128 }),
3129 );
3130 }
3131
3132 crate::runtime::config_matrix::heal_critical_keys(store.as_ref());
3139 seed_storage_deploy_config(store.as_ref(), options.storage_profile);
3140
3141 let lehman_yao = runtime.config_bool("storage.btree.lehman_yao", true);
3148 crate::storage::engine::btree::lehman_yao::set_enabled(lehman_yao);
3149 if lehman_yao {
3150 tracing::info!(
3151 "storage.btree.lehman_yao=true — lock-free concurrent descent enabled"
3152 );
3153 }
3154
3155 let overlay_path = crate::runtime::config_overlay::config_file_path();
3160 let _ =
3161 crate::runtime::config_overlay::apply_config_file(store.as_ref(), &overlay_path);
3162 }
3163
3164 {
3168 let store = runtime.inner.db.store();
3169 for name in crate::application::vcs_collections::ALL {
3170 let _ = store.get_or_create_collection(*name);
3171 }
3172 store.set_config_tree(
3175 crate::application::vcs_collections::CONFIG_NAMESPACE,
3176 &crate::json!({
3177 "default_branch": "main",
3178 "author": crate::json!({
3179 "name": "reddb",
3180 "email": "reddb@localhost"
3181 }),
3182 "protected_branches": crate::json!(["main"]),
3183 "closure": crate::json!({
3184 "enabled": true,
3185 "lazy": true
3186 }),
3187 "merge": crate::json!({
3188 "default_strategy": "auto",
3189 "fast_forward": true
3190 })
3191 }),
3192 );
3193 }
3194
3195 {
3198 let store = runtime.inner.db.store();
3199 for name in crate::application::migration_collections::ALL {
3200 let _ = store.get_or_create_collection(*name);
3201 }
3202 }
3203
3204 let _ = crate::application::topology_collections::ensure(&runtime);
3208
3209 runtime
3217 .inner
3218 .db
3219 .store()
3220 .register_entity_id(crate::storage::EntityId::new(
3221 crate::storage::FIRST_USER_ENTITY_ID - 1,
3222 ));
3223
3224 {
3239 let weak = Arc::downgrade(&runtime.inner);
3240 std::thread::Builder::new()
3241 .name("reddb-maintenance".into())
3242 .spawn(move || {
3243 let tick = std::time::Duration::from_millis(200);
3244 let work_interval = std::time::Duration::from_secs(60);
3245 let mut last_work = std::time::Instant::now();
3246 loop {
3247 std::thread::sleep(tick);
3248 let Some(inner) = weak.upgrade() else {
3249 break;
3252 };
3253 if last_work.elapsed() >= work_interval {
3254 let _stats = inner.db.store().context_index().stats();
3255 last_work = std::time::Instant::now();
3256 }
3257 }
3258 })
3259 .ok();
3260 }
3261
3262 {
3264 let store = runtime.inner.db.store();
3265 let mut backup_enabled = false;
3266 let mut backup_interval = 3600u64;
3267
3268 if let Some(manager) = store.get_collection("red_config") {
3269 manager.for_each_entity(|entity| {
3270 if let Some(row) = entity.data.as_row() {
3271 let key = row.get_field("key").and_then(|v| match v {
3272 crate::storage::schema::Value::Text(s) => Some(s.as_ref()),
3273 _ => None,
3274 });
3275 let val = row.get_field("value");
3276 if key == Some("red.config.backup.enabled") {
3277 backup_enabled = match val {
3278 Some(crate::storage::schema::Value::Boolean(true)) => true,
3279 Some(crate::storage::schema::Value::Text(s)) => &**s == "true",
3280 _ => false,
3281 };
3282 } else if key == Some("red.config.backup.interval_secs") {
3283 if let Some(crate::storage::schema::Value::Integer(n)) = val {
3284 backup_interval = *n as u64;
3285 }
3286 }
3287 }
3288 true
3289 });
3290 }
3291
3292 if backup_enabled {
3293 runtime.inner.backup_scheduler.set_interval(backup_interval);
3294 let rt = runtime.clone();
3295 runtime
3296 .inner
3297 .backup_scheduler
3298 .start(move || rt.trigger_backup().map_err(|e| format!("{}", e)));
3299 }
3300 }
3301
3302 {
3304 runtime
3305 .inner
3306 .ec_registry
3307 .load_from_config_store(runtime.inner.db.store().as_ref());
3308 if !runtime.inner.ec_registry.async_configs().is_empty() {
3309 runtime.inner.ec_worker.start(
3310 Arc::clone(&runtime.inner.ec_registry),
3311 Arc::clone(&runtime.inner.db.store()),
3312 );
3313 }
3314 }
3315
3316 if let crate::replication::ReplicationRole::Replica { primary_addr } =
3317 runtime.inner.db.options().replication.role.clone()
3318 {
3319 let rt = runtime.clone();
3320 std::thread::Builder::new()
3321 .name("reddb-replica".into())
3322 .spawn(move || rt.run_replica_loop(primary_addr))
3323 .ok();
3324 }
3325
3326 runtime.inner.lifecycle.mark_ready();
3331
3332 {
3341 let weak_inner = Arc::downgrade(&runtime.inner);
3342 std::thread::Builder::new()
3343 .name("reddb-mv-scheduler".into())
3344 .stack_size(8 * 1024 * 1024)
3349 .spawn(move || loop {
3350 std::thread::sleep(std::time::Duration::from_millis(50));
3351 let Some(inner) = weak_inner.upgrade() else {
3352 break;
3353 };
3354 let rt = RedDBRuntime { inner };
3355 rt.refresh_due_materialized_views();
3356 })
3357 .ok();
3358 }
3359
3360 if !runtime.write_gate().is_read_only() {
3370 let weak_inner = Arc::downgrade(&runtime.inner);
3371 std::thread::Builder::new()
3372 .name("reddb-retention-sweeper".into())
3373 .spawn(move || loop {
3374 std::thread::sleep(std::time::Duration::from_millis(500));
3375 let Some(inner) = weak_inner.upgrade() else {
3376 break;
3377 };
3378 let rt = RedDBRuntime { inner };
3379 rt.sweep_retention_tick(
3380 crate::runtime::retention_sweeper::DEFAULT_SWEEPER_BATCH,
3381 );
3382 })
3383 .ok();
3384 }
3385
3386 Ok(runtime)
3387 }
3388
3389 fn rehydrate_snapshot_xid_floor(&self) {
3390 let store = self.inner.db.store();
3391 for collection in store.list_collections() {
3392 let Some(manager) = store.get_collection(&collection) else {
3393 continue;
3394 };
3395 for entity in manager.query_all(|_| true) {
3396 self.inner
3397 .snapshot_manager
3398 .observe_committed_xid(entity.xmin);
3399 self.inner
3400 .snapshot_manager
3401 .observe_committed_xid(entity.xmax);
3402 }
3403 }
3404 }
3405
3406 pub(crate) fn ensure_materialized_view_backing(&self, name: &str) -> RedDBResult<()> {
3419 let store = self.inner.db.store();
3420 let mut changed = false;
3421 if store.get_collection(name).is_none() {
3422 store.get_or_create_collection(name);
3423 changed = true;
3424 }
3425 if self.inner.db.collection_contract(name).is_none() {
3426 self.inner
3427 .db
3428 .save_collection_contract(system_keyed_collection_contract(
3429 name,
3430 crate::catalog::CollectionModel::Table,
3431 ))
3432 .map_err(|err| RedDBError::Internal(err.to_string()))?;
3433 changed = true;
3434 }
3435 if changed {
3436 self.inner
3437 .db
3438 .persist_metadata()
3439 .map_err(|err| RedDBError::Internal(err.to_string()))?;
3440 }
3441 Ok(())
3442 }
3443
3444 pub(crate) fn drop_materialized_view_backing(&self, name: &str) -> RedDBResult<()> {
3449 let store = self.inner.db.store();
3450 if store.get_collection(name).is_none() {
3451 return Ok(());
3452 }
3453 store
3454 .drop_collection(name)
3455 .map_err(|err| RedDBError::Internal(err.to_string()))?;
3456 if self.inner.db.collection_contract(name).is_some() {
3459 self.inner
3460 .db
3461 .remove_collection_contract(name)
3462 .map_err(|err| RedDBError::Internal(err.to_string()))?;
3463 }
3464 self.invalidate_result_cache();
3465 self.inner
3466 .db
3467 .persist_metadata()
3468 .map_err(|err| RedDBError::Internal(err.to_string()))?;
3469 Ok(())
3470 }
3471
3472 fn bootstrap_system_keyed_collections(&self) -> RedDBResult<()> {
3473 let mut changed = false;
3474 for (name, model) in [
3475 ("red.config", crate::catalog::CollectionModel::Config),
3476 ("red.vault", crate::catalog::CollectionModel::Vault),
3477 (
3481 crate::runtime::continuous_materialized_view::CATALOG_COLLECTION,
3482 crate::catalog::CollectionModel::Config,
3483 ),
3484 ] {
3485 if self.inner.db.store().get_collection(name).is_none() {
3486 self.inner.db.store().get_or_create_collection(name);
3487 changed = true;
3488 }
3489 if self.inner.db.collection_contract(name).is_none() {
3490 self.inner
3491 .db
3492 .save_collection_contract(system_keyed_collection_contract(name, model))
3493 .map_err(|err| RedDBError::Internal(err.to_string()))?;
3494 changed = true;
3495 }
3496 }
3497 if changed {
3498 self.inner
3499 .db
3500 .persist_metadata()
3501 .map_err(|err| RedDBError::Internal(err.to_string()))?;
3502 }
3503 Ok(())
3504 }
3505
3506 pub fn db(&self) -> Arc<RedDB> {
3507 Arc::clone(&self.inner.db)
3508 }
3509
3510 pub fn index_store_ref(&self) -> &super::index_store::IndexStore {
3515 &self.inner.index_store
3516 }
3517
3518 pub(crate) fn schema_vocabulary_apply(
3523 &self,
3524 event: crate::runtime::schema_vocabulary::DdlEvent,
3525 ) {
3526 self.inner.schema_vocabulary.write().on_ddl(event);
3527 }
3528
3529 pub fn schema_vocabulary_lookup(
3534 &self,
3535 token: &str,
3536 ) -> Vec<crate::runtime::schema_vocabulary::VocabHit> {
3537 self.inner.schema_vocabulary.read().lookup(token).to_vec()
3538 }
3539
3540 pub fn set_auth_store(&self, store: Arc<crate::auth::store::AuthStore>) {
3544 *self.inner.auth_store.write() = Some(store);
3545 }
3546
3547 pub fn auth_store(&self) -> Option<Arc<crate::auth::store::AuthStore>> {
3550 self.inner.auth_store.read().clone()
3551 }
3552
3553 pub fn vault_kv_get(&self, key: &str) -> Option<String> {
3555 self.inner
3556 .auth_store
3557 .read()
3558 .as_ref()
3559 .and_then(|store| store.vault_kv_get(key))
3560 }
3561
3562 pub fn vault_kv_try_set(&self, key: String, value: String) -> RedDBResult<()> {
3565 let store = self.inner.auth_store.read().clone().ok_or_else(|| {
3566 RedDBError::Query("secret storage requires an enabled, unsealed vault".to_string())
3567 })?;
3568 store
3569 .vault_kv_try_set(key, value)
3570 .map_err(|err| RedDBError::Query(err.to_string()))
3571 }
3572
3573 pub fn set_oauth_validator(&self, validator: Option<Arc<crate::auth::oauth::OAuthValidator>>) {
3577 *self.inner.oauth_validator.write() = validator;
3578 }
3579
3580 pub fn oauth_validator(&self) -> Option<Arc<crate::auth::oauth::OAuthValidator>> {
3584 self.inner.oauth_validator.read().clone()
3585 }
3586
3587 pub fn set_browser_token_authority(
3593 &self,
3594 authority: Option<Arc<crate::auth::browser_token::BrowserTokenAuthority>>,
3595 ) {
3596 *self.inner.browser_token_authority.write() = authority;
3597 }
3598
3599 pub fn browser_token_authority(
3603 &self,
3604 ) -> Option<Arc<crate::auth::browser_token::BrowserTokenAuthority>> {
3605 self.inner.browser_token_authority.read().clone()
3606 }
3607
3608 pub(crate) fn secret_aes_key(&self) -> Option<[u8; 32]> {
3612 let guard = self.inner.auth_store.read();
3613 guard.as_ref().and_then(|s| s.vault_secret_key())
3614 }
3615
3616 pub(crate) fn config_bool(&self, key: &str, default: bool) -> bool {
3622 if let Some(raw) = self.inner.env_config_overrides.get(key) {
3623 if let Some(crate::storage::schema::Value::Boolean(b)) =
3624 crate::runtime::config_overlay::coerce_env_value(key, raw)
3625 {
3626 return b;
3627 }
3628 }
3629 let store = self.inner.db.store();
3630 let Some(manager) = store.get_collection("red_config") else {
3631 return default;
3632 };
3633 let mut result = default;
3634 let mut latest_id: u64 = 0;
3635 manager.for_each_entity(|entity| {
3636 if let Some(row) = entity.data.as_row() {
3637 let entry_key = row.get_field("key").and_then(|v| match v {
3638 crate::storage::schema::Value::Text(s) => Some(s.as_ref()),
3639 _ => None,
3640 });
3641 if entry_key == Some(key) {
3642 let id = entity.id.raw();
3643 if id >= latest_id {
3644 latest_id = id;
3645 result = match row.get_field("value") {
3646 Some(crate::storage::schema::Value::Boolean(b)) => *b,
3647 Some(crate::storage::schema::Value::Text(s)) => {
3648 matches!(s.as_ref(), "true" | "TRUE" | "True" | "1")
3649 }
3650 Some(crate::storage::schema::Value::Integer(n)) => *n != 0,
3651 _ => default,
3652 };
3653 }
3654 }
3655 }
3656 true
3657 });
3658 result
3659 }
3660
3661 pub(crate) fn binary_document_body_enabled(&self) -> bool {
3666 self.config_bool("storage.binary_document_body", true)
3667 }
3668
3669 pub(crate) fn config_u64(&self, key: &str, default: u64) -> u64 {
3670 if let Some(raw) = self.inner.env_config_overrides.get(key) {
3671 if let Some(crate::storage::schema::Value::UnsignedInteger(n)) =
3672 crate::runtime::config_overlay::coerce_env_value(key, raw)
3673 {
3674 return n;
3675 }
3676 }
3677 let store = self.inner.db.store();
3678 let Some(manager) = store.get_collection("red_config") else {
3679 return default;
3680 };
3681 let mut result = default;
3682 let mut latest_id: u64 = 0;
3683 manager.for_each_entity(|entity| {
3684 if let Some(row) = entity.data.as_row() {
3685 let entry_key = row.get_field("key").and_then(|v| match v {
3686 crate::storage::schema::Value::Text(s) => Some(s.as_ref()),
3687 _ => None,
3688 });
3689 if entry_key == Some(key) {
3690 let id = entity.id.raw();
3691 if id >= latest_id {
3692 latest_id = id;
3693 result = match row.get_field("value") {
3694 Some(crate::storage::schema::Value::Integer(n)) => *n as u64,
3695 Some(crate::storage::schema::Value::UnsignedInteger(n)) => *n,
3696 Some(crate::storage::schema::Value::Text(s)) => {
3697 s.parse::<u64>().unwrap_or(default)
3698 }
3699 _ => default,
3700 };
3701 }
3702 }
3703 }
3704 true
3705 });
3706 result
3707 }
3708
3709 pub(crate) fn config_f64(&self, key: &str, default: f64) -> f64 {
3710 if let Some(raw) = self.inner.env_config_overrides.get(key) {
3711 if let Ok(n) = raw.parse::<f64>() {
3712 return n;
3713 }
3714 }
3715 let store = self.inner.db.store();
3716 let Some(manager) = store.get_collection("red_config") else {
3717 return default;
3718 };
3719 let mut result = default;
3720 let mut latest_id: u64 = 0;
3721 manager.for_each_entity(|entity| {
3722 if let Some(row) = entity.data.as_row() {
3723 let entry_key = row.get_field("key").and_then(|v| match v {
3724 crate::storage::schema::Value::Text(s) => Some(s.as_ref()),
3725 _ => None,
3726 });
3727 if entry_key == Some(key) {
3728 let id = entity.id.raw();
3729 if id >= latest_id {
3730 latest_id = id;
3731 result = match row.get_field("value") {
3732 Some(crate::storage::schema::Value::Float(n)) => *n,
3733 Some(crate::storage::schema::Value::Integer(n)) => *n as f64,
3734 Some(crate::storage::schema::Value::UnsignedInteger(n)) => *n as f64,
3735 Some(crate::storage::schema::Value::Text(s)) => {
3736 s.parse::<f64>().unwrap_or(default)
3737 }
3738 _ => default,
3739 };
3740 }
3741 }
3742 }
3743 true
3744 });
3745 result
3746 }
3747
3748 pub(crate) fn config_string(&self, key: &str, default: &str) -> String {
3749 if let Some(raw) = self.inner.env_config_overrides.get(key) {
3750 return raw.clone();
3751 }
3752 let store = self.inner.db.store();
3753 let Some(manager) = store.get_collection("red_config") else {
3754 return default.to_string();
3755 };
3756 let mut result = default.to_string();
3757 let mut latest_id: u64 = 0;
3758 manager.for_each_entity(|entity| {
3759 if let Some(row) = entity.data.as_row() {
3760 let entry_key = row.get_field("key").and_then(|v| match v {
3761 crate::storage::schema::Value::Text(s) => Some(s.as_ref()),
3762 _ => None,
3763 });
3764 if entry_key == Some(key) {
3765 let id = entity.id.raw();
3766 if id >= latest_id {
3767 latest_id = id;
3768 if let Some(crate::storage::schema::Value::Text(value)) =
3769 row.get_field("value")
3770 {
3771 result = value.to_string();
3772 }
3773 }
3774 }
3775 }
3776 true
3777 });
3778 result
3779 }
3780
3781 pub(crate) fn secret_auto_encrypt(&self) -> bool {
3784 self.config_bool("red.config.secret.auto_encrypt", true)
3785 }
3786
3787 pub(crate) fn secret_auto_decrypt(&self) -> bool {
3792 self.config_bool("red.config.secret.auto_decrypt", true)
3793 }
3794
3795 pub(crate) fn apply_secret_decryption(&self, result: &mut RuntimeQueryResult) {
3802 if !self.secret_auto_decrypt() {
3803 return;
3804 }
3805 let Some(key) = self.secret_aes_key() else {
3806 return;
3807 };
3808 for record in result.result.records.iter_mut() {
3809 for value in record.values_mut() {
3810 if let Value::Secret(ref bytes) = value {
3811 if let Some(plain) =
3812 super::impl_dml::decrypt_secret_payload(&key, bytes.as_slice())
3813 {
3814 if let Ok(text) = String::from_utf8(plain) {
3815 *value = Value::text(text);
3816 }
3817 }
3818 }
3819 }
3820 }
3821 }
3822
3823 pub(crate) fn mutation_engine(&self) -> crate::runtime::mutation::MutationEngine<'_> {
3831 crate::runtime::mutation::MutationEngine::new(self)
3832 }
3833
3834 pub fn check_write(&self, kind: crate::runtime::write_gate::WriteKind) -> RedDBResult<()> {
3845 self.inner.write_gate.check(kind)
3846 }
3847
3848 pub fn write_gate(&self) -> &crate::runtime::write_gate::WriteGate {
3852 &self.inner.write_gate
3853 }
3854
3855 pub fn lifecycle(&self) -> &crate::runtime::lifecycle::Lifecycle {
3859 &self.inner.lifecycle
3860 }
3861
3862 pub fn resource_limits(&self) -> &crate::runtime::resource_limits::ResourceLimits {
3864 &self.inner.resource_limits
3865 }
3866
3867 pub fn audit_log(&self) -> &crate::runtime::audit_log::AuditLogger {
3869 &self.inner.audit_log
3870 }
3871
3872 pub fn audit_log_arc(&self) -> Arc<crate::runtime::audit_log::AuditLogger> {
3876 Arc::clone(&self.inner.audit_log)
3877 }
3878
3879 pub(crate) fn emit_control_event(
3880 &self,
3881 kind: crate::runtime::control_events::EventKind,
3882 outcome: crate::runtime::control_events::Outcome,
3883 action: &'static str,
3884 resource: Option<String>,
3885 reason: Option<String>,
3886 extra_fields: Vec<(String, crate::runtime::control_events::Sensitivity)>,
3887 ) -> RedDBResult<()> {
3888 use crate::runtime::control_events::{
3889 ActorRef, ControlEvent, ControlEventCtx, ControlEventLedger, Sensitivity,
3890 };
3891
3892 let tenant = current_tenant();
3893 let principal = current_auth_identity();
3894 let actor_user = principal
3895 .as_ref()
3896 .map(|(principal, _)| UserId::from_parts(tenant.as_deref(), principal));
3897 let actor = actor_user
3898 .as_ref()
3899 .map(ActorRef::User)
3900 .unwrap_or(ActorRef::Anonymous);
3901 let ctx = ControlEventCtx {
3902 actor,
3903 scope: tenant
3904 .as_ref()
3905 .map(|scope| std::borrow::Cow::Borrowed(scope.as_str())),
3906 request_id: Some(std::borrow::Cow::Owned(format!(
3907 "conn-{}",
3908 current_connection_id()
3909 ))),
3910 trace_id: None,
3911 };
3912 let mut fields = std::collections::HashMap::new();
3913 fields.insert(
3914 "connection_id".to_string(),
3915 Sensitivity::raw(current_connection_id().to_string()),
3916 );
3917 if let Some((_, role)) = principal {
3918 fields.insert("actor_role".to_string(), Sensitivity::raw(role.as_str()));
3919 }
3920 for (key, value) in extra_fields {
3921 fields.insert(key, value);
3922 }
3923 let event = ControlEvent {
3924 kind,
3925 outcome,
3926 action: std::borrow::Cow::Borrowed(action),
3927 resource,
3928 reason,
3929 matched_policy_id: None,
3930 fields,
3931 };
3932 let ledger = self.inner.control_event_ledger.read();
3933 match ledger.emit(&ctx, event) {
3934 Ok(_) => Ok(()),
3935 Err(err) if self.inner.control_event_config.require_persistence() => {
3936 Err(RedDBError::Internal(err.to_string()))
3937 }
3938 Err(_) => Ok(()),
3939 }
3940 }
3941
3942 fn policy_mutation_control_ctx<'a>(
3943 &self,
3944 actor: &'a crate::auth::UserId,
3945 tenant: Option<&'a str>,
3946 ) -> crate::runtime::control_events::ControlEventCtx<'a> {
3947 crate::runtime::control_events::ControlEventCtx {
3948 actor: crate::runtime::control_events::ActorRef::User(actor),
3949 scope: tenant.map(std::borrow::Cow::Borrowed),
3950 request_id: Some(std::borrow::Cow::Owned(format!(
3951 "conn-{}",
3952 current_connection_id()
3953 ))),
3954 trace_id: None,
3955 }
3956 }
3957
3958 fn emit_query_audit(
3959 &self,
3960 query: &str,
3961 plan: &QueryAuditPlan,
3962 duration_ms: u64,
3963 result: &RuntimeQueryResult,
3964 ) {
3965 if !self.inner.query_audit.has_rules() {
3966 return;
3967 }
3968 let actor = current_auth_identity().map(|(principal, _)| principal);
3969 let tenant = current_tenant();
3970 let row_count = if result.statement_type == "select" {
3971 result.result.records.len() as u64
3972 } else {
3973 result.affected_rows
3974 };
3975 self.inner
3976 .query_audit
3977 .emit(crate::runtime::query_audit::QueryAuditEvent {
3978 actor,
3979 tenant,
3980 statement_kind: plan.statement_kind,
3981 touched_collections: plan.collections.clone(),
3982 duration_ms,
3983 row_count,
3984 request_id: Some(crate::crypto::uuid::Uuid::new_v7().to_string()),
3985 query_hash: Some(blake3::hash(query.as_bytes()).to_hex().to_string()),
3986 });
3987 }
3988
3989 pub(crate) fn queue_telemetry(
3991 &self,
3992 ) -> &crate::runtime::queue_telemetry::QueueTelemetryCounters {
3993 &self.inner.queue_telemetry
3994 }
3995
3996 pub fn queue_telemetry_snapshot(
3999 &self,
4000 ) -> crate::runtime::queue_telemetry::QueueTelemetrySnapshot {
4001 crate::runtime::queue_telemetry::QueueTelemetrySnapshot {
4002 delivered: self.inner.queue_telemetry.delivered_snapshot(),
4003 acked: self.inner.queue_telemetry.acked_snapshot(),
4004 nacked: self.inner.queue_telemetry.nacked_snapshot(),
4005 wait_started: self.inner.queue_telemetry.wait_started_snapshot(),
4006 wait_woken: self.inner.queue_telemetry.wait_woken_snapshot(),
4007 wait_timed_out: self.inner.queue_telemetry.wait_timed_out_snapshot(),
4008 wait_cancelled: self.inner.queue_telemetry.wait_cancelled_snapshot(),
4009 wait_duration: self.inner.queue_telemetry.wait_duration_snapshot(),
4010 }
4011 }
4012
4013 pub fn claim_telemetry_snapshot(&self) -> crate::runtime::ClaimTelemetrySnapshot {
4015 self.inner.claim_telemetry.snapshot()
4016 }
4017
4018 pub fn query_latency_snapshot(
4021 &self,
4022 ) -> Vec<crate::runtime::query_latency_telemetry::QueryLatencyHistogram> {
4023 self.inner.query_latency_telemetry.snapshot()
4024 }
4025
4026 pub fn query_latency_rollup(
4029 &self,
4030 ) -> crate::runtime::query_latency_telemetry::QueryLatencyHistogram {
4031 self.inner.query_latency_telemetry.rollup()
4032 }
4033
4034 pub fn sample_occupancy(&self) -> crate::runtime::occupancy_sampler::OccupancySample {
4039 self.inner.occupancy_sampler.sample()
4040 }
4041
4042 pub fn node_load_snapshot(&self) -> crate::runtime::node_load_telemetry::NodeLoadSnapshot {
4046 self.inner.node_load_telemetry.snapshot()
4047 }
4048
4049 pub(crate) fn queue_presence(
4054 &self,
4055 ) -> &std::sync::Arc<crate::storage::queue::presence::ConsumerPresenceRegistry> {
4056 &self.inner.queue_presence
4057 }
4058
4059 pub fn queue_consumer_presence_snapshot(
4064 &self,
4065 ttl_ms: u64,
4066 ) -> Vec<crate::storage::queue::presence::ConsumerPresence> {
4067 let now_ns = std::time::SystemTime::now()
4068 .duration_since(std::time::UNIX_EPOCH)
4069 .map(|d| d.as_nanos() as u64)
4070 .unwrap_or(0);
4071 self.inner.queue_presence.snapshot(now_ns, ttl_ms)
4072 }
4073
4074 pub fn queue_active_consumer_counts(
4078 &self,
4079 ttl_ms: u64,
4080 ) -> std::collections::HashMap<(String, String), u32> {
4081 let now_ns = std::time::SystemTime::now()
4082 .duration_since(std::time::UNIX_EPOCH)
4083 .map(|d| d.as_nanos() as u64)
4084 .unwrap_or(0);
4085 self.inner
4086 .queue_presence
4087 .count_active_by_group(now_ns, ttl_ms)
4088 }
4089
4090 pub(crate) fn vector_introspection_registry(
4096 &self,
4097 ) -> &std::sync::Arc<crate::storage::vector::introspection::VectorIntrospectionRegistry> {
4098 &self.inner.vector_introspection
4099 }
4100
4101 pub fn vector_introspection_snapshot(
4106 &self,
4107 ) -> Vec<crate::storage::vector::introspection::VectorIntrospection> {
4108 self.inner.vector_introspection.snapshot()
4109 }
4110
4111 pub fn vector_introspection_get(
4115 &self,
4116 collection: &str,
4117 ) -> Option<crate::storage::vector::introspection::VectorIntrospection> {
4118 self.inner.vector_introspection.get(collection)
4119 }
4120
4121 pub fn slow_query_store(&self) -> &Arc<crate::telemetry::slow_query_store::SlowQueryStore> {
4127 &self.inner.slow_query_store
4128 }
4129
4130 pub fn queue_pending_counts(&self) -> Vec<((String, String), u64)> {
4135 let store = self.inner.db.store();
4136 crate::runtime::impl_queue::pending_counts_by_group(store.as_ref())
4137 .into_iter()
4138 .collect()
4139 }
4140
4141 pub fn write_gate_arc(&self) -> Arc<crate::runtime::write_gate::WriteGate> {
4146 Arc::clone(&self.inner.write_gate)
4147 }
4148
4149 pub fn lease_lifecycle(&self) -> Option<&Arc<crate::runtime::lease_lifecycle::LeaseLifecycle>> {
4152 self.inner.lease_lifecycle.get()
4153 }
4154
4155 pub fn set_lease_lifecycle(
4158 &self,
4159 lifecycle: Arc<crate::runtime::lease_lifecycle::LeaseLifecycle>,
4160 ) -> Result<(), Arc<crate::runtime::lease_lifecycle::LeaseLifecycle>> {
4161 self.inner.lease_lifecycle.set(lifecycle)
4162 }
4163
4164 pub fn check_batch_size(&self, requested: usize) -> RedDBResult<()> {
4169 if self.inner.resource_limits.batch_size_exceeded(requested) {
4170 let max = self.inner.resource_limits.max_batch_size.unwrap_or(0);
4171 return Err(RedDBError::QuotaExceeded(format!(
4172 "max_batch_size:{requested}:{max}"
4173 )));
4174 }
4175 Ok(())
4176 }
4177
4178 pub fn check_db_size(&self) -> RedDBResult<()> {
4184 let Some(limit) = self.inner.resource_limits.max_db_size_bytes else {
4185 return Ok(());
4186 };
4187 if limit == 0 {
4188 return Ok(());
4189 }
4190 let Some(path) = self.inner.db.path() else {
4191 return Ok(());
4192 };
4193 let current = std::fs::metadata(path).map(|m| m.len()).unwrap_or(0);
4194 if current > limit {
4195 return Err(RedDBError::QuotaExceeded(format!(
4196 "max_db_size_bytes:{current}:{limit}"
4197 )));
4198 }
4199 Ok(())
4200 }
4201
4202 pub fn graceful_shutdown(
4220 &self,
4221 backup_on_shutdown: bool,
4222 ) -> RedDBResult<crate::runtime::lifecycle::ShutdownReport> {
4223 if !self.inner.lifecycle.begin_shutdown() {
4224 return Ok(self.inner.lifecycle.shutdown_report().unwrap_or_default());
4228 }
4229
4230 let started_ms = std::time::SystemTime::now()
4231 .duration_since(std::time::UNIX_EPOCH)
4232 .map(|d| d.as_millis() as u64)
4233 .unwrap_or(0);
4234 let mut report = crate::runtime::lifecycle::ShutdownReport {
4235 started_at_ms: started_ms,
4236 ..Default::default()
4237 };
4238
4239 let flush_res = self.inner.db.flush_local_only();
4245 report.flushed_wal = flush_res.is_ok();
4246 report.final_checkpoint = flush_res.is_ok();
4247 if let Err(err) = &flush_res {
4248 tracing::error!(
4249 target: "reddb::lifecycle",
4250 error = %err,
4251 "graceful_shutdown: local flush failed"
4252 );
4253 } else if let Err(lease_err) =
4254 self.assert_remote_write_allowed("shutdown/checkpoint_upload")
4255 {
4256 tracing::warn!(
4257 target: "reddb::serverless::lease",
4258 error = %lease_err,
4259 "graceful_shutdown: remote upload skipped — lease not held"
4260 );
4261 } else if let Err(err) = self.inner.db.upload_to_remote_backend() {
4262 tracing::error!(
4263 target: "reddb::lifecycle",
4264 error = %err,
4265 "graceful_shutdown: remote upload failed"
4266 );
4267 }
4268
4269 if backup_on_shutdown && self.inner.db.remote_backend.is_some() {
4274 match self.trigger_backup() {
4280 Ok(result) => {
4281 report.backup_uploaded = result.uploaded;
4282 }
4283 Err(err) => {
4284 tracing::warn!(
4285 target: "reddb::lifecycle",
4286 error = %err,
4287 "graceful_shutdown: final backup skipped"
4288 );
4289 }
4290 }
4291 }
4292
4293 let completed_ms = std::time::SystemTime::now()
4294 .duration_since(std::time::UNIX_EPOCH)
4295 .map(|d| d.as_millis() as u64)
4296 .unwrap_or(started_ms);
4297 report.completed_at_ms = completed_ms;
4298 report.duration_ms = completed_ms.saturating_sub(started_ms);
4299
4300 self.inner.lifecycle.finish_shutdown(report.clone());
4301 Ok(report)
4302 }
4303
4304 pub fn quota_bucket(&self) -> &crate::runtime::quota_bucket::QuotaBucket {
4307 &self.inner.quota_bucket
4308 }
4309
4310 pub fn encryption_at_rest_status(&self) -> (&'static str, Option<String>) {
4318 match crate::crypto::page_encryption::key_from_env() {
4319 Ok(Some(_)) => ("enabled", None),
4320 Ok(None) => ("disabled", None),
4321 Err(err) => ("error", Some(err)),
4322 }
4323 }
4324
4325 pub fn replica_apply_health(&self) -> Option<String> {
4331 let state = self.config_string("red.replication.state", "");
4332 if state.is_empty() {
4333 None
4334 } else {
4335 Some(state)
4336 }
4337 }
4338
4339 pub fn acquire(&self) -> RedDBResult<RuntimeConnection> {
4340 let mut pool = self
4341 .inner
4342 .pool
4343 .lock()
4344 .map_err(|e| RedDBError::Internal(format!("connection pool lock poisoned: {e}")))?;
4345 if pool.active >= self.inner.pool_config.max_connections {
4346 return Err(RedDBError::Internal(
4347 "connection pool exhausted".to_string(),
4348 ));
4349 }
4350
4351 let id = if let Some(id) = pool.idle.pop() {
4352 id
4353 } else {
4354 let id = pool.next_id;
4355 pool.next_id += 1;
4356 id
4357 };
4358 pool.active += 1;
4359 pool.total_checkouts += 1;
4360 drop(pool);
4361
4362 self.inner.node_load_telemetry.record_connect();
4365
4366 Ok(RuntimeConnection {
4367 id,
4368 inner: Arc::clone(&self.inner),
4369 })
4370 }
4371
4372 pub fn checkpoint(&self) -> RedDBResult<()> {
4373 self.inner.db.flush_local_only().map_err(|err| {
4378 let msg = err.to_string();
4383 crate::telemetry::operator_event::OperatorEvent::CheckpointFailed {
4384 lsn: 0,
4385 error: msg.clone(),
4386 }
4387 .emit_global();
4388 crate::telemetry::operator_event::OperatorEvent::WalFsyncFailed {
4389 path: "<flush_local_only>".to_string(),
4390 error: msg.clone(),
4391 }
4392 .emit_global();
4393 RedDBError::Engine(msg)
4394 })?;
4395 if let Err(err) = self.assert_remote_write_allowed("checkpoint") {
4396 tracing::warn!(
4397 target: "reddb::serverless::lease",
4398 error = %err,
4399 "checkpoint: skipping remote upload — lease not held"
4400 );
4401 return Ok(());
4402 }
4403 self.inner
4404 .db
4405 .upload_to_remote_backend()
4406 .map_err(|err| RedDBError::Engine(err.to_string()))
4407 }
4408
4409 pub(crate) fn assert_remote_write_allowed(&self, action: &str) -> RedDBResult<()> {
4416 if self.inner.db.remote_backend.is_none() {
4417 return Ok(());
4418 }
4419 match self.inner.write_gate.lease_state() {
4420 crate::runtime::write_gate::LeaseGateState::NotHeld => {
4421 self.inner.audit_log.record(
4422 action,
4423 "system",
4424 "remote_backend",
4425 "err: writer lease not held",
4426 crate::json::Value::Null,
4427 );
4428 Err(RedDBError::ReadOnly(format!(
4429 "writer lease not held — {action} blocked (serverless fence)"
4430 )))
4431 }
4432 _ => Ok(()),
4433 }
4434 }
4435
4436 pub fn run_maintenance(&self) -> RedDBResult<()> {
4437 self.inner
4438 .db
4439 .run_maintenance()
4440 .map_err(|err| RedDBError::Internal(err.to_string()))
4441 }
4442
4443 pub fn scan_collection(
4444 &self,
4445 collection: &str,
4446 cursor: Option<ScanCursor>,
4447 limit: usize,
4448 ) -> RedDBResult<ScanPage> {
4449 let store = self.inner.db.store();
4450 let manager = store
4451 .get_collection(collection)
4452 .ok_or_else(|| RedDBError::NotFound(collection.to_string()))?;
4453
4454 let mut entities = manager.query_all(|_| true);
4455 entities.sort_by_key(|entity| entity.id.raw());
4456
4457 let offset = cursor.map(|cursor| cursor.offset).unwrap_or(0);
4458 let total = entities.len();
4459 let end = total.min(offset.saturating_add(limit.max(1)));
4460 let items = if offset >= total {
4461 Vec::new()
4462 } else {
4463 entities[offset..end].to_vec()
4464 };
4465 let next = (end < total).then_some(ScanCursor { offset: end });
4466
4467 Ok(ScanPage {
4468 collection: collection.to_string(),
4469 items,
4470 next,
4471 total,
4472 })
4473 }
4474
4475 pub fn catalog(&self) -> CatalogModelSnapshot {
4476 self.inner.db.catalog_model_snapshot()
4477 }
4478
4479 pub fn catalog_consistency_report(&self) -> crate::catalog::CatalogConsistencyReport {
4480 self.inner.db.catalog_consistency_report()
4481 }
4482
4483 pub fn catalog_attention_summary(&self) -> CatalogAttentionSummary {
4484 crate::catalog::attention_summary(&self.catalog())
4485 }
4486
4487 pub fn collection_attention(&self) -> Vec<CollectionDescriptor> {
4488 crate::catalog::collection_attention(&self.catalog())
4489 }
4490
4491 pub fn index_attention(&self) -> Vec<CatalogIndexStatus> {
4492 crate::catalog::index_attention(&self.catalog())
4493 }
4494
4495 pub fn graph_projection_attention(&self) -> Vec<CatalogGraphProjectionStatus> {
4496 crate::catalog::graph_projection_attention(&self.catalog())
4497 }
4498
4499 pub fn analytics_job_attention(&self) -> Vec<CatalogAnalyticsJobStatus> {
4500 crate::catalog::analytics_job_attention(&self.catalog())
4501 }
4502
4503 pub fn stats(&self) -> RuntimeStats {
4504 let pool = runtime_pool_lock(self);
4505 RuntimeStats {
4506 active_connections: pool.active,
4507 idle_connections: pool.idle.len(),
4508 total_checkouts: pool.total_checkouts,
4509 paged_mode: self.inner.db.is_paged(),
4510 started_at_unix_ms: self.inner.started_at_unix_ms,
4511 store: self.inner.db.stats(),
4512 system: SystemInfo::collect(),
4513 result_blob_cache: self.inner.result_blob_cache.stats(),
4514 kv: self.inner.kv_stats.snapshot(),
4515 metrics_ingest: self.inner.metrics_ingest_stats.snapshot(),
4516 }
4517 }
4518
4519 pub(crate) fn record_metrics_ingest(
4520 &self,
4521 accepted_samples: u64,
4522 accepted_series: u64,
4523 rejected_samples: u64,
4524 rejected_series: u64,
4525 ) {
4526 self.inner.metrics_ingest_stats.record(
4527 accepted_samples,
4528 accepted_series,
4529 rejected_samples,
4530 rejected_series,
4531 );
4532 }
4533
4534 pub(crate) fn record_metrics_cardinality_budget_rejections(&self, rejected_series: u64) {
4535 self.inner
4536 .metrics_ingest_stats
4537 .record_cardinality_budget_rejections(rejected_series);
4538 }
4539
4540 pub(crate) fn record_metrics_tenant_activity(
4541 &self,
4542 tenant: &str,
4543 namespace: &str,
4544 operation: &str,
4545 ) {
4546 self.inner
4547 .metrics_tenant_activity_stats
4548 .record(tenant, namespace, operation);
4549 }
4550
4551 pub(crate) fn metrics_tenant_activity_snapshot(
4552 &self,
4553 ) -> Vec<crate::runtime::MetricsTenantActivityStats> {
4554 self.inner.metrics_tenant_activity_stats.snapshot()
4555 }
4556
4557 pub fn execute_query_with_scope(
4571 &self,
4572 query: &str,
4573 scope: crate::runtime::within_clause::ScopeOverride,
4574 ) -> RedDBResult<RuntimeQueryResult> {
4575 if scope.is_empty() {
4576 return self.execute_query(query);
4577 }
4578 let _scope_guard = ScopeOverrideGuard::install(scope);
4579 self.execute_query(query)
4580 }
4581
4582 pub fn execute_query(&self, query: &str) -> RedDBResult<RuntimeQueryResult> {
4591 let started = std::time::Instant::now();
4592 self.inner.node_load_telemetry.query_start();
4593 let result = self.execute_query_inner(query);
4594 self.finish_query_lifecycle(query, started, result)
4595 }
4596
4597 pub fn execute_query_with_params(
4605 &self,
4606 query: &str,
4607 params: &[Value],
4608 ) -> RedDBResult<RuntimeQueryResult> {
4609 if params.is_empty() {
4610 return self.execute_query(query);
4611 }
4612 let started = std::time::Instant::now();
4613 self.inner.node_load_telemetry.query_start();
4614 let result = self.execute_query_with_params_inner(query, params);
4615 self.finish_query_lifecycle(query, started, result)
4616 }
4617
4618 fn finish_query_lifecycle(
4619 &self,
4620 query: &str,
4621 started: std::time::Instant,
4622 mut result: RedDBResult<RuntimeQueryResult>,
4623 ) -> RedDBResult<RuntimeQueryResult> {
4624 if let Ok(ref mut query_result) = result {
4629 if query_result.statement_type == "select" {
4630 self.filter_integrity_tombstoned(&mut query_result.result);
4631 }
4632 }
4633 let elapsed_ms = started.elapsed().as_millis() as u64;
4634
4635 let scope = self.ai_scope();
4640 let kind = match result
4641 .as_ref()
4642 .map(|r| r.statement_type)
4643 .unwrap_or("select")
4644 {
4645 "select" => crate::telemetry::slow_query_logger::QueryKind::Select,
4646 "insert" => crate::telemetry::slow_query_logger::QueryKind::Insert,
4647 "update" => crate::telemetry::slow_query_logger::QueryKind::Update,
4648 "delete" => crate::telemetry::slow_query_logger::QueryKind::Delete,
4649 _ => crate::telemetry::slow_query_logger::QueryKind::Internal,
4650 };
4651 self.inner
4657 .slow_query_logger
4658 .record(kind, elapsed_ms, query.to_string(), &scope);
4659
4660 self.inner
4666 .query_latency_telemetry
4667 .observe(kind, started.elapsed().as_secs_f64());
4668
4669 self.inner.node_load_telemetry.query_finish();
4673
4674 if let Ok(ref mut query_result) = result {
4675 if matches!(query_result.statement_type, "insert" | "update" | "delete") {
4676 let bookmark = crate::replication::CausalBookmark::new(
4677 self.current_replication_term(),
4678 self.cdc_current_lsn(),
4679 );
4680 query_result.bookmark = Some(bookmark.encode());
4681 }
4682 }
4683
4684 result
4685 }
4686
4687 fn execute_query_with_params_inner(
4688 &self,
4689 query: &str,
4690 params: &[Value],
4691 ) -> RedDBResult<RuntimeQueryResult> {
4692 let parsed = parse_multi(query).map_err(|err| RedDBError::Query(err.to_string()))?;
4693 let bound = crate::storage::query::user_params::bind(&parsed, params).map_err(|err| {
4694 RedDBError::Validation {
4695 message: err.to_string(),
4696 validation: crate::json!({
4697 "code": "INVALID_PARAMS",
4698 "surface": "query.params",
4699 }),
4700 }
4701 })?;
4702 self.execute_bound_query_expr_in_frame(query, bound)
4703 }
4704
4705 fn execute_bound_query_expr_in_frame(
4706 &self,
4707 query: &str,
4708 expr: QueryExpr,
4709 ) -> RedDBResult<RuntimeQueryResult> {
4710 let rewritten_query = super::red_schema::rewrite_virtual_names(query);
4711 let execution_query = rewritten_query.as_deref().unwrap_or(query);
4712 let frame = super::statement_frame::StatementExecutionFrame::build(self, execution_query)?;
4713 let _frame_guards = Box::new(frame.install(self));
4717 let _log_span = crate::telemetry::span::query_span(query).entered();
4718
4719 let expr = self.rewrite_view_refs(expr);
4720 let mode = detect_mode(execution_query);
4721 let control_event_specs = query_control_event_specs(&expr);
4722 let _lock_guard = match frame.prepare_dispatch(self, &expr) {
4723 Ok(guard) => guard,
4724 Err(err) => {
4725 let outcome = control_event_outcome_for_error(&err);
4726 for spec in &control_event_specs {
4727 self.emit_control_event(
4728 spec.kind,
4729 outcome,
4730 spec.action,
4731 spec.resource.clone(),
4732 Some(err.to_string()),
4733 spec.fields.clone(),
4734 )?;
4735 }
4736 return Err(err);
4737 }
4738 };
4739
4740 let mut result = self.dispatch_expr(expr, query, mode)?;
4741 if result.statement_type == "select" {
4742 self.apply_secret_decryption(&mut result);
4743 }
4744 Ok(result)
4745 }
4746
4747 pub fn causal_session(&self) -> crate::runtime::CausalSession {
4748 crate::runtime::CausalSession {
4749 runtime: self.clone(),
4750 bookmark: None,
4751 wait_timeout: std::time::Duration::from_secs(5),
4752 }
4753 }
4754
4755 pub fn wait_for_bookmark(
4756 &self,
4757 bookmark: &crate::replication::CausalBookmark,
4758 timeout: std::time::Duration,
4759 ) -> RedDBResult<()> {
4760 let deadline = std::time::Instant::now() + timeout;
4761 loop {
4762 let applied_lsn = self.local_contiguous_applied_lsn();
4763 if applied_lsn >= bookmark.commit_lsn() {
4764 return Ok(());
4765 }
4766 let now = std::time::Instant::now();
4767 if now >= deadline {
4768 return Err(RedDBError::InvalidOperation(format!(
4769 "timed out waiting for causal bookmark lsn {}; applied={}",
4770 bookmark.commit_lsn(),
4771 applied_lsn
4772 )));
4773 }
4774 let remaining = deadline.saturating_duration_since(now);
4775 std::thread::sleep(remaining.min(std::time::Duration::from_millis(5)));
4776 }
4777 }
4778
4779 fn local_contiguous_applied_lsn(&self) -> u64 {
4780 match self.inner.db.options().replication.role {
4781 crate::replication::ReplicationRole::Replica { .. } => {
4782 self.config_u64("red.replication.last_applied_lsn", 0)
4783 }
4784 _ => self.cdc_current_lsn(),
4785 }
4786 }
4787
4788 fn execute_vcs_command(
4789 &self,
4790 query: &str,
4791 mode: QueryMode,
4792 cmd: RuntimeVcsCommand,
4793 ) -> RedDBResult<RuntimeQueryResult> {
4794 use crate::application::vcs::{
4795 Author, CheckoutInput, CheckoutTarget, CreateCommitInput, MergeInput, MergeOpts,
4796 ResetInput, ResetMode,
4797 };
4798 use crate::application::VcsUseCases;
4799
4800 let conn_id = current_connection_id();
4801 let vcs = VcsUseCases::new(self);
4802 let default_author = || Author {
4803 name: "rql".to_string(),
4804 email: "rql@reddb.io".to_string(),
4805 };
4806
4807 match cmd {
4808 RuntimeVcsCommand::Checkpoint { message, author } => {
4809 let author = author
4810 .as_deref()
4811 .map(parse_vcs_author)
4812 .unwrap_or_else(default_author);
4813 let commit = vcs.commit(CreateCommitInput {
4814 connection_id: conn_id,
4815 message: message.clone(),
4816 author,
4817 committer: None,
4818 amend: false,
4819 allow_empty: true,
4820 })?;
4821 Ok(RuntimeQueryResult::ok_message(
4822 query.to_string(),
4823 &format!("checkpoint {}", commit.hash),
4824 "vcs_checkpoint",
4825 ))
4826 }
4827 RuntimeVcsCommand::Checkout { target } => {
4828 let result = if target.starts_with("refs/tags/") {
4829 vcs.checkout(CheckoutInput {
4830 connection_id: conn_id,
4831 target: CheckoutTarget::Tag(target.clone()),
4832 force: false,
4833 })
4834 } else if looks_like_commit_hash(&target) {
4835 vcs.checkout(CheckoutInput {
4836 connection_id: conn_id,
4837 target: CheckoutTarget::Commit(target.clone()),
4838 force: false,
4839 })
4840 } else {
4841 vcs.checkout(CheckoutInput {
4842 connection_id: conn_id,
4843 target: CheckoutTarget::Branch(target.clone()),
4844 force: false,
4845 })
4846 .or_else(|_| {
4847 vcs.checkout(CheckoutInput {
4848 connection_id: conn_id,
4849 target: CheckoutTarget::Commit(target.clone()),
4850 force: false,
4851 })
4852 })
4853 }?;
4854 Ok(RuntimeQueryResult::ok_message(
4855 query.to_string(),
4856 &format!("checked out {}", result.name),
4857 "vcs_checkout",
4858 ))
4859 }
4860 RuntimeVcsCommand::Reset {
4861 mode: reset_mode,
4862 target,
4863 } => {
4864 let mode = match reset_mode {
4865 RuntimeVcsResetMode::Hard => ResetMode::Hard,
4866 RuntimeVcsResetMode::Soft => ResetMode::Soft,
4867 RuntimeVcsResetMode::Mixed => ResetMode::Mixed,
4868 };
4869 vcs.reset(ResetInput {
4870 connection_id: conn_id,
4871 target: target.clone(),
4872 mode,
4873 })?;
4874 Ok(RuntimeQueryResult::ok_message(
4875 query.to_string(),
4876 "reset complete",
4877 "vcs_reset",
4878 ))
4879 }
4880 RuntimeVcsCommand::Merge { branch } => {
4881 let outcome = vcs.merge(MergeInput {
4882 connection_id: conn_id,
4883 from: branch.clone(),
4884 opts: MergeOpts::default(),
4885 author: default_author(),
4886 })?;
4887 Ok(RuntimeQueryResult::ok_message(
4888 query.to_string(),
4889 &format!("merge complete; conflicts={}", outcome.conflicts.len()),
4890 "vcs_merge",
4891 ))
4892 }
4893 RuntimeVcsCommand::CherryPick { commit } => {
4894 let outcome = vcs.cherry_pick(conn_id, &commit, default_author())?;
4895 Ok(RuntimeQueryResult::ok_message(
4896 query.to_string(),
4897 &format!(
4898 "cherry-pick complete; conflicts={}",
4899 outcome.conflicts.len()
4900 ),
4901 "vcs_cherry_pick",
4902 ))
4903 }
4904 RuntimeVcsCommand::Revert { commit } => {
4905 let reverted = vcs.revert(conn_id, &commit, default_author())?;
4906 Ok(RuntimeQueryResult::ok_message(
4907 query.to_string(),
4908 &format!("revert {}", reverted.hash),
4909 "vcs_revert",
4910 ))
4911 }
4912 RuntimeVcsCommand::ResolveConflict { key, resolution } => {
4913 let value = match resolution {
4914 RuntimeVcsConflictResolution::Ours => {
4915 crate::json::Value::String("ours".to_string())
4916 }
4917 RuntimeVcsConflictResolution::Theirs => {
4918 crate::json::Value::String("theirs".to_string())
4919 }
4920 };
4921 vcs.conflict_resolve(&key, value)?;
4922 Ok(RuntimeQueryResult::ok_message(
4923 query.to_string(),
4924 "conflict resolved",
4925 "vcs_resolve_conflict",
4926 ))
4927 }
4928 }
4929 }
4930
4931 #[inline(never)]
4932 fn execute_query_inner(&self, query: &str) -> RedDBResult<RuntimeQueryResult> {
4933 if !has_scope_override_active()
4944 && !query.trim_start().starts_with("WITHIN")
4945 && !query.trim_start().starts_with("within")
4946 && !self.inner.query_audit.has_rules()
4947 && !self
4948 .inner
4949 .tx_contexts
4950 .read()
4951 .contains_key(¤t_connection_id())
4952 {
4953 if let Some(result) = self.try_fast_entity_lookup(query) {
4954 return result;
4955 }
4956 }
4957
4958 match crate::runtime::within_clause::try_strip_within_prefix(query) {
4965 Ok(Some((scope, inner))) => {
4966 let _scope_guard = ScopeOverrideGuard::install(scope);
4967 return self.execute_query_inner(inner);
4972 }
4973 Ok(None) => {}
4974 Err(msg) => return Err(RedDBError::Query(msg)),
4975 }
4976
4977 if let Some(inner) = strip_explain_prefix(query) {
4984 return self.explain_as_rows(query, inner);
4985 }
4986
4987 if let Some(value) = parse_set_local_tenant(query)? {
4992 let conn_id = current_connection_id();
4993 if !self.inner.tx_contexts.read().contains_key(&conn_id) {
4994 return Err(RedDBError::Query(
4995 "SET LOCAL TENANT requires an active transaction".to_string(),
4996 ));
4997 }
4998 self.inner
4999 .tx_local_tenants
5000 .write()
5001 .insert(conn_id, value.clone());
5002 return Ok(RuntimeQueryResult::ok_message(
5003 query.to_string(),
5004 &match &value {
5005 Some(id) => format!("local tenant set: {id}"),
5006 None => "local tenant cleared".to_string(),
5007 },
5008 "set_local_tenant",
5009 ));
5010 }
5011
5012 if super::red_schema::is_system_schema_write(query) {
5013 return Err(RedDBError::Query(
5014 super::red_schema::READ_ONLY_ERROR.to_string(),
5015 ));
5016 }
5017
5018 if let Some(command) = parse_runtime_vcs_command(query) {
5019 return self.execute_vcs_command(query, detect_mode(query), command?);
5020 }
5021
5022 if let Some(create_source) = super::analytics_source_catalog::parse_create_statement(query)?
5023 {
5024 return self.execute_create_analytics_source(query, create_source);
5025 }
5026
5027 if let Some(path) = super::metric_descriptor_catalog::parse_read_metric_statement(query) {
5033 return Err(super::metric_descriptor_catalog::read_output_unsupported(
5034 &path,
5035 ));
5036 }
5037
5038 if let Some(parsed) = super::ranking_descriptor_catalog::parse_create_ranking(query) {
5043 return self.execute_create_ranking(query, parsed?);
5044 }
5045 if super::ranking_descriptor_catalog::parse_show_rankings(query) {
5046 return self.execute_show_rankings(query);
5047 }
5048
5049 let rewritten_query = super::red_schema::rewrite_virtual_names(query);
5050 let execution_query = rewritten_query.as_deref().unwrap_or(query);
5051
5052 let frame = super::statement_frame::StatementExecutionFrame::build(self, execution_query)?;
5053 let _frame_guards = Box::new(frame.install(self));
5054
5055 let _log_span = crate::telemetry::span::query_span(query).entered();
5062
5063 if let Some(rewritten) = frame.prepare_cte(execution_query)? {
5065 return self.execute_query_expr(rewritten);
5066 }
5067
5068 if !self.inner.query_audit.has_rules() {
5070 if let Some(result) = self.try_fast_entity_lookup(execution_query) {
5071 return result;
5072 }
5073 }
5074
5075 if !self.inner.query_audit.has_rules() {
5077 if let Some(result) = frame.read_result_cache(self) {
5078 return Ok(result);
5079 }
5080 }
5081
5082 let prepared = frame.prepare_statement(self, execution_query)?;
5083 let mode = prepared.mode;
5084 let expr = prepared.expr;
5085
5086 let statement = query_expr_name(&expr);
5087 let result_cache_scopes = query_expr_result_cache_scopes(&expr);
5088 let control_event_specs = query_control_event_specs(&expr);
5089 let query_audit_plan = query_audit_plan(&expr);
5090
5091 let _lock_guard = match frame.prepare_dispatch(self, &expr) {
5092 Ok(guard) => guard,
5093 Err(err) => {
5094 let outcome = control_event_outcome_for_error(&err);
5095 for spec in &control_event_specs {
5096 self.emit_control_event(
5097 spec.kind,
5098 outcome,
5099 spec.action,
5100 spec.resource.clone(),
5101 Some(err.to_string()),
5102 spec.fields.clone(),
5103 )?;
5104 }
5105 return Err(err);
5106 }
5107 };
5108 let frame_iface: &dyn super::statement_frame::ReadFrame = &frame;
5109 let query_audit_started = std::time::Instant::now();
5110
5111 let query_result = match expr {
5112 QueryExpr::Graph(_) | QueryExpr::Path(_) => {
5113 let (graph, node_properties, edge_properties) =
5121 self.materialize_graph_with_rls()?;
5122 let result =
5123 crate::storage::query::unified::UnifiedExecutor::execute_on_with_graph_properties(
5124 &graph,
5125 &expr,
5126 node_properties,
5127 edge_properties,
5128 )
5129 .map_err(|err| RedDBError::Query(err.to_string()))?;
5130
5131 Ok(RuntimeQueryResult {
5132 query: query.to_string(),
5133 mode,
5134 statement,
5135 engine: "materialized-graph",
5136 result,
5137 affected_rows: 0,
5138 statement_type: "select",
5139 bookmark: None,
5140 })
5141 }
5142 QueryExpr::Table(table) => {
5143 let table = self.resolve_table_expr_subqueries(
5144 table,
5145 &frame as &dyn super::statement_frame::ReadFrame,
5146 )?;
5147 if let Some(TableSource::Function {
5151 name,
5152 args,
5153 named_args,
5154 }) = table.source.clone()
5155 {
5156 let tvf_result = RuntimeQueryResult {
5164 query: query.to_string(),
5165 mode,
5166 statement,
5167 engine: "runtime-graph-tvf",
5168 result: self.execute_table_function(&name, &args, &named_args)?,
5169 affected_rows: 0,
5170 statement_type: "select",
5171 bookmark: None,
5172 };
5173 frame.write_result_cache(self, &tvf_result, result_cache_scopes.clone());
5174 return Ok(tvf_result);
5175 }
5176 if let Some(TableSource::InlineGraphFunction {
5184 name,
5185 nodes,
5186 edges,
5187 named_args,
5188 }) = table.source.clone()
5189 {
5190 let inline_result = RuntimeQueryResult {
5191 query: query.to_string(),
5192 mode,
5193 statement,
5194 engine: "runtime-graph-tvf-inline",
5195 result: self.execute_inline_graph_function(
5196 &name,
5197 &nodes,
5198 &edges,
5199 &named_args,
5200 )?,
5201 affected_rows: 0,
5202 statement_type: "select",
5203 bookmark: None,
5204 };
5205 frame.write_result_cache(self, &inline_result, result_cache_scopes);
5206 return Ok(inline_result);
5207 }
5208 if super::red_schema::is_virtual_table(&table.table) {
5209 return Ok(RuntimeQueryResult {
5210 query: query.to_string(),
5211 mode,
5212 statement,
5213 engine: "runtime-red-schema",
5214 result: super::red_schema::red_query(
5215 self,
5216 &table.table,
5217 &table,
5218 &frame as &dyn super::statement_frame::ReadFrame,
5219 )?,
5220 affected_rows: 0,
5221 statement_type: "select",
5222 bookmark: None,
5223 });
5224 }
5225
5226 if let Some(view_result) = self.try_resolve_analytics_view(
5230 &table,
5231 &frame as &dyn super::statement_frame::ReadFrame,
5232 )? {
5233 return Ok(RuntimeQueryResult {
5234 query: query.to_string(),
5235 mode,
5236 statement,
5237 engine: "runtime-graph-analytics-view",
5238 result: view_result,
5239 affected_rows: 0,
5240 statement_type: "select",
5241 bookmark: None,
5242 });
5243 }
5244
5245 if let Some(result) = self.execute_probabilistic_select(&table)? {
5246 return Ok(RuntimeQueryResult {
5247 query: query.to_string(),
5248 mode,
5249 statement,
5250 engine: "runtime-probabilistic",
5251 result,
5252 affected_rows: 0,
5253 statement_type: "select",
5254 bookmark: None,
5255 });
5256 }
5257
5258 if self.inner.foreign_tables.is_foreign_table(&table.table) {
5266 let records = self
5267 .inner
5268 .foreign_tables
5269 .scan(&table.table)
5270 .map_err(|e| RedDBError::Internal(e.to_string()))?;
5271 let result = apply_foreign_table_filters(records, &table);
5272 return Ok(RuntimeQueryResult {
5273 query: query.to_string(),
5274 mode,
5275 statement,
5276 engine: "runtime-fdw",
5277 result,
5278 affected_rows: 0,
5279 statement_type: "select",
5280 bookmark: None,
5281 });
5282 }
5283
5284 let Some(table_with_rls) = self.authorize_relational_table_select(
5301 table,
5302 &frame as &dyn super::statement_frame::ReadFrame,
5303 )?
5304 else {
5305 let empty = crate::storage::query::unified::UnifiedResult::empty();
5306 return Ok(RuntimeQueryResult {
5307 query: query.to_string(),
5308 mode,
5309 statement,
5310 engine: "runtime-table-rls",
5311 result: empty,
5312 affected_rows: 0,
5313 statement_type: "select",
5314 bookmark: None,
5315 });
5316 };
5317 Ok(RuntimeQueryResult {
5318 query: query.to_string(),
5319 mode,
5320 statement,
5321 engine: "runtime-table",
5322 result: execute_runtime_table_query_in(
5329 &self.inner.db,
5330 &table_with_rls,
5331 Some(&self.inner.index_store),
5332 Some(frame.row_arena()),
5333 )?,
5334 affected_rows: 0,
5335 statement_type: "select",
5336 bookmark: None,
5337 })
5338 }
5339 QueryExpr::Join(join) => {
5340 let join_with_rls = match self.authorize_relational_join_select(
5349 join,
5350 &frame as &dyn super::statement_frame::ReadFrame,
5351 )? {
5352 Some(j) => j,
5353 None => {
5354 return Ok(RuntimeQueryResult {
5355 query: query.to_string(),
5356 mode,
5357 statement,
5358 engine: "runtime-join-rls",
5359 result: crate::storage::query::unified::UnifiedResult::empty(),
5360 affected_rows: 0,
5361 statement_type: "select",
5362 bookmark: None,
5363 });
5364 }
5365 };
5366 Ok(RuntimeQueryResult {
5367 query: query.to_string(),
5368 mode,
5369 statement,
5370 engine: "runtime-join",
5371 result: execute_runtime_join_query(&self.inner.db, &join_with_rls)?,
5372 affected_rows: 0,
5373 statement_type: "select",
5374 bookmark: None,
5375 })
5376 }
5377 QueryExpr::Vector(vector) => Ok(RuntimeQueryResult {
5378 query: query.to_string(),
5379 mode,
5380 statement,
5381 engine: "runtime-vector",
5382 result: execute_runtime_vector_query(&self.inner.db, &vector)?,
5383 affected_rows: 0,
5384 statement_type: "select",
5385 bookmark: None,
5386 }),
5387 QueryExpr::Hybrid(hybrid) => Ok(RuntimeQueryResult {
5388 query: query.to_string(),
5389 mode,
5390 statement,
5391 engine: "runtime-hybrid",
5392 result: execute_runtime_hybrid_query(&self.inner.db, &hybrid)?,
5393 affected_rows: 0,
5394 statement_type: "select",
5395 bookmark: None,
5396 }),
5397 QueryExpr::RankOf(ref rank) => self.execute_rank_of(query, rank),
5398 QueryExpr::ApproxRankOf(ref rank) => self.execute_approx_rank_of(query, rank),
5399 QueryExpr::RankRange(ref range) => self.execute_rank_range(query, range),
5400 QueryExpr::Insert(ref insert) if super::red_schema::is_virtual_table(&insert.table) => {
5402 Err(RedDBError::Query(
5403 super::red_schema::READ_ONLY_ERROR.to_string(),
5404 ))
5405 }
5406 QueryExpr::Update(ref update) if super::red_schema::is_virtual_table(&update.table) => {
5407 Err(RedDBError::Query(
5408 super::red_schema::READ_ONLY_ERROR.to_string(),
5409 ))
5410 }
5411 QueryExpr::Delete(ref delete) if super::red_schema::is_virtual_table(&delete.table) => {
5412 Err(RedDBError::Query(
5413 super::red_schema::READ_ONLY_ERROR.to_string(),
5414 ))
5415 }
5416 QueryExpr::Insert(ref insert) => self
5417 .with_deferred_store_wal_for_dml(self.insert_may_emit_events(insert), || {
5418 self.execute_insert(query, insert)
5419 }),
5420 QueryExpr::Update(ref update) => self
5421 .with_deferred_store_wal_for_dml(self.update_may_emit_events(update), || {
5422 self.execute_update(query, update)
5423 }),
5424 QueryExpr::Delete(ref delete) => self
5425 .with_deferred_store_wal_for_dml(self.delete_may_emit_events(delete), || {
5426 self.execute_delete(query, delete)
5427 }),
5428 QueryExpr::CreateTable(ref create) => self.execute_create_table(query, create),
5430 QueryExpr::CreateCollection(ref create) => {
5431 self.execute_create_collection(query, create)
5432 }
5433 QueryExpr::CreateVector(ref create) => self.execute_create_vector(query, create),
5434 QueryExpr::DropTable(ref drop_tbl) => self.execute_drop_table(query, drop_tbl),
5435 QueryExpr::DropGraph(ref drop_graph) => self.execute_drop_graph(query, drop_graph),
5436 QueryExpr::DropVector(ref drop_vector) => self.execute_drop_vector(query, drop_vector),
5437 QueryExpr::DropDocument(ref drop_document) => {
5438 self.execute_drop_document(query, drop_document)
5439 }
5440 QueryExpr::DropKv(ref drop_kv) => self.execute_drop_kv(query, drop_kv),
5441 QueryExpr::DropCollection(ref drop_collection) => {
5442 self.execute_drop_collection(query, drop_collection)
5443 }
5444 QueryExpr::Truncate(ref truncate) => self.execute_truncate(query, truncate),
5445 QueryExpr::AlterTable(ref alter) => self.execute_alter_table(query, alter),
5446 QueryExpr::CreateVcsRef(ref create) => self.execute_create_vcs_ref(query, create),
5447 QueryExpr::DropVcsRef(ref drop_ref) => self.execute_drop_vcs_ref(query, drop_ref),
5448 QueryExpr::ExplainAlter(ref explain) => self.execute_explain_alter(query, explain),
5449 QueryExpr::GraphCommand(ref cmd) => self.execute_graph_command(query, cmd),
5451 QueryExpr::SearchCommand(ref cmd) => self.execute_search_command(query, cmd),
5453 QueryExpr::Ask(ref ask) => self.execute_ask(query, ask),
5455 QueryExpr::CreateIndex(ref create_idx) => self.execute_create_index(query, create_idx),
5456 QueryExpr::DropIndex(ref drop_idx) => self.execute_drop_index(query, drop_idx),
5457 QueryExpr::ProbabilisticCommand(ref cmd) => {
5458 self.execute_probabilistic_command(query, cmd)
5459 }
5460 QueryExpr::CreateTimeSeries(ref ts) => self.execute_create_timeseries(query, ts),
5462 QueryExpr::CreateMetric(ref metric) => self.execute_create_metric(query, metric),
5463 QueryExpr::AlterMetric(ref alter) => self.execute_alter_metric(query, alter),
5464 QueryExpr::CreateSlo(ref slo) => self.execute_create_slo(query, slo),
5465 QueryExpr::DropTimeSeries(ref ts) => self.execute_drop_timeseries(query, ts),
5466 QueryExpr::CreateQueue(ref q) => self.execute_create_queue(query, q),
5468 QueryExpr::AlterQueue(ref q) => self.execute_alter_queue(query, q),
5469 QueryExpr::DropQueue(ref q) => self.execute_drop_queue(query, q),
5470 QueryExpr::QueueSelect(ref q) => self.execute_queue_select(query, q),
5471 QueryExpr::QueueCommand(ref cmd) => self
5472 .with_deferred_store_wal_if_transaction(|| self.execute_queue_command(query, cmd)),
5473 QueryExpr::EventsBackfill(ref backfill) => {
5474 self.execute_events_backfill(query, backfill)
5475 }
5476 QueryExpr::EventsBackfillStatus { ref collection } => Err(RedDBError::Query(format!(
5477 "EVENTS BACKFILL STATUS for '{collection}' is not implemented in this slice"
5478 ))),
5479 QueryExpr::KvCommand(ref cmd) => self.execute_kv_command(query, cmd),
5480 QueryExpr::ConfigCommand(ref cmd) => self.execute_config_command(query, cmd),
5481 QueryExpr::CreateTree(ref tree) => self.execute_create_tree(query, tree),
5482 QueryExpr::DropTree(ref tree) => self.execute_drop_tree(query, tree),
5483 QueryExpr::TreeCommand(ref cmd) => self.execute_tree_command(query, cmd),
5484 QueryExpr::SetConfig { ref key, ref value } => {
5486 if key.starts_with("red.secret.") {
5487 return Err(RedDBError::Query(
5488 "red.secret.* is reserved for vault secrets; use SET SECRET".to_string(),
5489 ));
5490 }
5491 if key.starts_with("red.secrets.") {
5492 return Err(RedDBError::Query(
5493 "red.secrets.* is reserved for vault secrets; use SET SECRET".to_string(),
5494 ));
5495 }
5496 match self.check_managed_config_write_for_set_config(key) {
5497 Err(err) => Err(err),
5498 Ok(()) => {
5499 let store = self.inner.db.store();
5500 let json_val = match value {
5501 Value::Text(s) => crate::serde_json::Value::String(s.to_string()),
5502 Value::Integer(n) => crate::serde_json::Value::Number(*n as f64),
5503 Value::Float(n) => crate::serde_json::Value::Number(*n),
5504 Value::Boolean(b) => crate::serde_json::Value::Bool(*b),
5505 _ => crate::serde_json::Value::String(value.to_string()),
5506 };
5507 store.set_config_tree(key, &json_val);
5508 update_current_config_value(key, value.clone());
5509 self.invalidate_result_cache();
5514 Ok(RuntimeQueryResult::ok_message(
5515 query.to_string(),
5516 &format!("config set: {key}"),
5517 "set",
5518 ))
5519 }
5520 }
5521 }
5522 QueryExpr::SetSecret { ref key, ref value } => {
5524 if key.starts_with("red.config.") {
5525 return Err(RedDBError::Query(
5526 "red.config.* is reserved for config; use SET CONFIG".to_string(),
5527 ));
5528 }
5529 let auth_store = self.inner.auth_store.read().clone().ok_or_else(|| {
5530 RedDBError::Query("SET SECRET requires an enabled, unsealed vault".to_string())
5531 })?;
5532 self.check_secret_write_privilege(&auth_store, key)?;
5533 if matches!(value, Value::Null) {
5534 auth_store
5535 .vault_kv_try_delete(key)
5536 .map_err(|err| RedDBError::Query(err.to_string()))?;
5537 update_current_secret_value(key, None);
5538 self.invalidate_result_cache();
5539 return Ok(RuntimeQueryResult::ok_message(
5540 query.to_string(),
5541 &format!("secret deleted: {key}"),
5542 "delete_secret",
5543 ));
5544 }
5545 let value = secret_sql_value_to_string(value)?;
5546 auth_store
5547 .vault_kv_try_set(key.clone(), value.clone())
5548 .map_err(|err| RedDBError::Query(err.to_string()))?;
5549 update_current_secret_value(key, Some(value));
5550 self.invalidate_result_cache();
5551 Ok(RuntimeQueryResult::ok_message(
5552 query.to_string(),
5553 &format!("secret set: {key}"),
5554 "set_secret",
5555 ))
5556 }
5557 QueryExpr::DeleteSecret { ref key } => {
5559 let auth_store = self.inner.auth_store.read().clone().ok_or_else(|| {
5560 RedDBError::Query(
5561 "DELETE SECRET requires an enabled, unsealed vault".to_string(),
5562 )
5563 })?;
5564 self.check_secret_write_privilege(&auth_store, key)?;
5565 let deleted = auth_store
5566 .vault_kv_try_delete(key)
5567 .map_err(|err| RedDBError::Query(err.to_string()))?;
5568 if deleted {
5569 update_current_secret_value(key, None);
5570 }
5571 self.invalidate_result_cache();
5572 Ok(RuntimeQueryResult::ok_message(
5573 query.to_string(),
5574 &format!("secret deleted: {key}"),
5575 if deleted {
5576 "delete_secret"
5577 } else {
5578 "delete_secret_not_found"
5579 },
5580 ))
5581 }
5582 QueryExpr::SetKv { ref key, ref value } => {
5584 let auth_store = self.inner.auth_store.read().clone().ok_or_else(|| {
5585 RedDBError::Query("SET KV requires an auth store".to_string())
5586 })?;
5587 self.check_kv_write_privilege(&auth_store, key)?;
5588 if matches!(value, Value::Null) {
5590 auth_store.plain_kv_delete(key);
5591 update_current_kv_value(key, None);
5592 self.invalidate_result_cache();
5593 return Ok(RuntimeQueryResult::ok_message(
5594 query.to_string(),
5595 &format!("kv deleted: {key}"),
5596 "delete_kv",
5597 ));
5598 }
5599 let value = secret_sql_value_to_string(value)?;
5600 auth_store.plain_kv_set(key.clone(), value.clone());
5601 update_current_kv_value(key, Some(value));
5602 self.invalidate_result_cache();
5603 Ok(RuntimeQueryResult::ok_message(
5604 query.to_string(),
5605 &format!("kv set: {key}"),
5606 "set_kv",
5607 ))
5608 }
5609 QueryExpr::DeleteKv { ref key } => {
5611 let auth_store = self.inner.auth_store.read().clone().ok_or_else(|| {
5612 RedDBError::Query("DELETE KV requires an auth store".to_string())
5613 })?;
5614 self.check_kv_write_privilege(&auth_store, key)?;
5615 let deleted = auth_store.plain_kv_delete(key);
5616 if deleted {
5617 update_current_kv_value(key, None);
5618 }
5619 self.invalidate_result_cache();
5620 Ok(RuntimeQueryResult::ok_message(
5621 query.to_string(),
5622 &format!("kv deleted: {key}"),
5623 if deleted {
5624 "delete_kv"
5625 } else {
5626 "delete_kv_not_found"
5627 },
5628 ))
5629 }
5630 QueryExpr::ShowSecrets { ref prefix } => {
5632 let auth_store = self.inner.auth_store.read().clone().ok_or_else(|| {
5633 RedDBError::Query("SHOW SECRET requires an enabled, unsealed vault".to_string())
5634 })?;
5635 if !auth_store.is_vault_backed() {
5636 return Err(RedDBError::Query(
5637 "SHOW SECRET requires an enabled, unsealed vault".to_string(),
5638 ));
5639 }
5640 let mut keys = auth_store.vault_kv_keys();
5641 keys.sort();
5642 let mut result = UnifiedResult::with_columns(vec![
5643 "key".into(),
5644 "value".into(),
5645 "status".into(),
5646 ]);
5647 for key in keys {
5648 if !show_secrets_allows_key(&key) {
5649 continue;
5650 }
5651 if let Some(ref pfx) = prefix {
5652 if !key.starts_with(pfx) {
5653 continue;
5654 }
5655 }
5656 let mut record = UnifiedRecord::new();
5657 record.set("key", Value::text(key));
5658 record.set("value", Value::text("***"));
5659 record.set("status", Value::text("active"));
5660 result.push(record);
5661 }
5662 Ok(RuntimeQueryResult {
5663 query: query.to_string(),
5664 mode,
5665 statement: "show_secrets",
5666 engine: "runtime-secret",
5667 result,
5668 affected_rows: 0,
5669 statement_type: "select",
5670 bookmark: None,
5671 })
5672 }
5673 QueryExpr::ShowConfig {
5675 ref prefix,
5676 as_json,
5677 } => {
5678 let store = self.inner.db.store();
5679 let all_collections = store.list_collections();
5680 if !all_collections.contains(&"red_config".to_string()) {
5681 if as_json {
5682 return Ok(show_config_json_result(
5683 query,
5684 mode,
5685 prefix,
5686 crate::serde_json::Value::Object(crate::serde_json::Map::new()),
5687 ));
5688 }
5689 let result = UnifiedResult::with_columns(vec!["key".into(), "value".into()]);
5690 return Ok(RuntimeQueryResult {
5691 query: query.to_string(),
5692 mode,
5693 statement: "show_config",
5694 engine: "runtime-config",
5695 result,
5696 affected_rows: 0,
5697 statement_type: "select",
5698 bookmark: None,
5699 });
5700 }
5701 let manager = store
5702 .get_collection("red_config")
5703 .ok_or_else(|| RedDBError::NotFound("red_config".to_string()))?;
5704 let entities = manager.query_all(|_| true);
5705 let mut latest = std::collections::BTreeMap::<String, (u64, Value, Value)>::new();
5706 for entity in entities {
5707 if let EntityData::Row(ref row) = entity.data {
5708 if let Some(ref named) = row.named {
5709 let key_val = named.get("key").cloned().unwrap_or(Value::Null);
5710 let val = named.get("value").cloned().unwrap_or(Value::Null);
5711 let key_str = match &key_val {
5712 Value::Text(s) => s.as_ref(),
5713 _ => continue,
5714 };
5715 if let Some(ref pfx) = prefix {
5716 if !key_str.starts_with(pfx.as_str()) {
5717 continue;
5718 }
5719 }
5720 let entity_id = entity.id.raw();
5721 match latest.get(key_str) {
5722 Some((prev_id, _, _)) if *prev_id > entity_id => {}
5723 _ => {
5724 latest.insert(key_str.to_string(), (entity_id, key_val, val));
5725 }
5726 }
5727 }
5728 }
5729 }
5730 if as_json {
5731 let mut tree = crate::serde_json::Value::Object(crate::serde_json::Map::new());
5732 for (key, (_, _, val)) in latest {
5733 let relative = match prefix {
5734 Some(pfx) if key == *pfx => "",
5735 Some(pfx) => key
5736 .strip_prefix(pfx.as_str())
5737 .and_then(|tail| tail.strip_prefix('.'))
5738 .unwrap_or(key.as_str()),
5739 None => key.as_str(),
5740 };
5741 insert_config_json_path(
5742 &mut tree,
5743 relative,
5744 crate::presentation::entity_json::storage_value_to_json(&val),
5745 );
5746 }
5747 return Ok(show_config_json_result(query, mode, prefix, tree));
5748 }
5749 let mut result = UnifiedResult::with_columns(vec!["key".into(), "value".into()]);
5750 for (_, key_val, val) in latest.into_values() {
5751 let mut record = UnifiedRecord::new();
5752 record.set("key", key_val);
5753 record.set("value", val);
5754 result.push(record);
5755 }
5756 Ok(RuntimeQueryResult {
5757 query: query.to_string(),
5758 mode,
5759 statement: "show_config",
5760 engine: "runtime-config",
5761 result,
5762 affected_rows: 0,
5763 statement_type: "select",
5764 bookmark: None,
5765 })
5766 }
5767 QueryExpr::SetTenant(ref value) => {
5773 match value {
5774 Some(id) => set_current_tenant(id.clone()),
5775 None => clear_current_tenant(),
5776 }
5777 Ok(RuntimeQueryResult::ok_message(
5778 query.to_string(),
5779 &match value {
5780 Some(id) => format!("tenant set: {id}"),
5781 None => "tenant cleared".to_string(),
5782 },
5783 "set_tenant",
5784 ))
5785 }
5786 QueryExpr::ShowTenant => {
5787 let mut result = UnifiedResult::with_columns(vec!["tenant".into()]);
5788 let mut record = UnifiedRecord::new();
5789 record.set(
5790 "tenant",
5791 current_tenant().map(Value::text).unwrap_or(Value::Null),
5792 );
5793 result.push(record);
5794 Ok(RuntimeQueryResult {
5795 query: query.to_string(),
5796 mode,
5797 statement: "show_tenant",
5798 engine: "runtime-tenant",
5799 result,
5800 affected_rows: 0,
5801 statement_type: "select",
5802 bookmark: None,
5803 })
5804 }
5805 QueryExpr::TransactionControl(ref ctl) => {
5817 use crate::storage::query::ast::TxnControl;
5818 use crate::storage::transaction::snapshot::{TxnContext, Xid};
5819 use crate::storage::transaction::IsolationLevel;
5820
5821 let conn_id = current_connection_id();
5826
5827 let (kind, msg) = match ctl {
5828 TxnControl::Begin => {
5829 let mgr = Arc::clone(&self.inner.snapshot_manager);
5830 let xid = mgr.begin();
5831 let snapshot = mgr.snapshot(xid);
5832 let ctx = TxnContext {
5833 xid,
5834 isolation: IsolationLevel::SnapshotIsolation,
5835 snapshot,
5836 savepoints: Vec::new(),
5837 released_sub_xids: Vec::new(),
5838 };
5839 self.inner.tx_contexts.write().insert(conn_id, ctx);
5840 ("begin", format!("BEGIN — xid={xid} (snapshot isolation)"))
5841 }
5842 TxnControl::Commit => {
5843 self.inner.tx_local_tenants.write().remove(&conn_id);
5845 let ctx = self.inner.tx_contexts.write().remove(&conn_id);
5846 match ctx {
5847 Some(ctx) => {
5848 let mut own_xids = std::collections::HashSet::new();
5849 own_xids.insert(ctx.xid);
5850 for (_, sub) in &ctx.savepoints {
5851 own_xids.insert(*sub);
5852 }
5853 for sub in &ctx.released_sub_xids {
5854 own_xids.insert(*sub);
5855 }
5856 if let Err(err) = self.check_table_row_write_conflicts(
5857 conn_id,
5858 &ctx.snapshot,
5859 &own_xids,
5860 ) {
5861 for (_, sub) in &ctx.savepoints {
5862 self.inner.snapshot_manager.rollback(*sub);
5863 }
5864 for sub in &ctx.released_sub_xids {
5865 self.inner.snapshot_manager.rollback(*sub);
5866 }
5867 self.inner.snapshot_manager.rollback(ctx.xid);
5868 self.revive_pending_versioned_updates(conn_id);
5869 self.revive_pending_tombstones(conn_id);
5870 self.discard_pending_kv_watch_events(conn_id);
5871 self.discard_pending_queue_wakes(conn_id);
5872 self.discard_pending_store_wal_actions(conn_id);
5873 self.release_pending_claim_locks(conn_id);
5874 return Err(err);
5875 }
5876 self.restore_pending_write_stamps(conn_id);
5877 if let Err(err) = self.flush_pending_store_wal_actions(conn_id) {
5878 for (_, sub) in &ctx.savepoints {
5879 self.inner.snapshot_manager.rollback(*sub);
5880 }
5881 for sub in &ctx.released_sub_xids {
5882 self.inner.snapshot_manager.rollback(*sub);
5883 }
5884 self.inner.snapshot_manager.rollback(ctx.xid);
5885 self.revive_pending_versioned_updates(conn_id);
5886 self.revive_pending_tombstones(conn_id);
5887 self.discard_pending_kv_watch_events(conn_id);
5888 self.discard_pending_queue_wakes(conn_id);
5889 self.release_pending_claim_locks(conn_id);
5890 return Err(err);
5891 }
5892 for (_, sub) in &ctx.savepoints {
5898 self.inner.snapshot_manager.commit(*sub);
5899 }
5900 for sub in &ctx.released_sub_xids {
5901 self.inner.snapshot_manager.commit(*sub);
5902 }
5903 self.inner.snapshot_manager.commit(ctx.xid);
5904 self.finalize_pending_versioned_updates(conn_id);
5905 self.finalize_pending_tombstones(conn_id);
5906 self.finalize_pending_kv_watch_events(conn_id);
5907 self.finalize_pending_queue_wakes(conn_id);
5908 self.release_pending_claim_locks(conn_id);
5909 ("commit", format!("COMMIT — xid={} committed", ctx.xid))
5910 }
5911 None => (
5912 "commit",
5913 "COMMIT outside transaction — no-op (autocommit)".to_string(),
5914 ),
5915 }
5916 }
5917 TxnControl::Rollback => {
5918 self.inner.tx_local_tenants.write().remove(&conn_id);
5919 let ctx = self.inner.tx_contexts.write().remove(&conn_id);
5920 match ctx {
5921 Some(ctx) => {
5922 for (_, sub) in &ctx.savepoints {
5925 self.inner.snapshot_manager.rollback(*sub);
5926 }
5927 for sub in &ctx.released_sub_xids {
5928 self.inner.snapshot_manager.rollback(*sub);
5929 }
5930 self.inner.snapshot_manager.rollback(ctx.xid);
5931 self.revive_pending_versioned_updates(conn_id);
5935 self.revive_pending_tombstones(conn_id);
5936 self.discard_pending_kv_watch_events(conn_id);
5937 self.discard_pending_queue_wakes(conn_id);
5938 self.discard_pending_store_wal_actions(conn_id);
5939 self.release_pending_claim_locks(conn_id);
5940 ("rollback", format!("ROLLBACK — xid={} aborted", ctx.xid))
5941 }
5942 None => (
5943 "rollback",
5944 "ROLLBACK outside transaction — no-op (autocommit)".to_string(),
5945 ),
5946 }
5947 }
5948 TxnControl::Savepoint(name) => {
5955 let mgr = Arc::clone(&self.inner.snapshot_manager);
5956 let mut guard = self.inner.tx_contexts.write();
5957 match guard.get_mut(&conn_id) {
5958 Some(ctx) => {
5959 let sub = mgr.begin();
5960 ctx.savepoints.push((name.clone(), sub));
5961 ("savepoint", format!("SAVEPOINT {name} — sub_xid={sub}"))
5962 }
5963 None => (
5964 "savepoint",
5965 "SAVEPOINT outside transaction — no-op".to_string(),
5966 ),
5967 }
5968 }
5969 TxnControl::ReleaseSavepoint(name) => {
5970 let mut guard = self.inner.tx_contexts.write();
5971 match guard.get_mut(&conn_id) {
5972 Some(ctx) => {
5973 let pos = ctx
5974 .savepoints
5975 .iter()
5976 .position(|(n, _)| n == name)
5977 .ok_or_else(|| {
5978 RedDBError::Internal(format!(
5979 "savepoint {name} does not exist"
5980 ))
5981 })?;
5982 let released = ctx.savepoints.len() - pos;
5990 let popped: Vec<Xid> = ctx
5991 .savepoints
5992 .split_off(pos)
5993 .into_iter()
5994 .map(|(_, x)| x)
5995 .collect();
5996 ctx.released_sub_xids.extend(popped);
5997 (
5998 "release_savepoint",
5999 format!("RELEASE SAVEPOINT {name} — {released} level(s)"),
6000 )
6001 }
6002 None => (
6003 "release_savepoint",
6004 "RELEASE outside transaction — no-op".to_string(),
6005 ),
6006 }
6007 }
6008 TxnControl::RollbackToSavepoint(name) => {
6009 let mgr = Arc::clone(&self.inner.snapshot_manager);
6010 let drop_result: Option<(Xid, Vec<Xid>)> = {
6015 let mut guard = self.inner.tx_contexts.write();
6016 if let Some(ctx) = guard.get_mut(&conn_id) {
6017 let pos = ctx
6018 .savepoints
6019 .iter()
6020 .position(|(n, _)| n == name)
6021 .ok_or_else(|| {
6022 RedDBError::Internal(format!(
6023 "savepoint {name} does not exist"
6024 ))
6025 })?;
6026 let savepoint_xid = ctx.savepoints[pos].1;
6027 let aborted: Vec<Xid> = ctx
6028 .savepoints
6029 .split_off(pos)
6030 .into_iter()
6031 .map(|(_, x)| x)
6032 .collect();
6033 Some((savepoint_xid, aborted))
6034 } else {
6035 None
6036 }
6037 };
6038
6039 match drop_result {
6040 Some((savepoint_xid, aborted)) => {
6041 for x in &aborted {
6042 mgr.rollback(*x);
6043 }
6044 let reverted_updates =
6045 self.revive_versioned_updates_since(conn_id, savepoint_xid);
6046 let revived = self.revive_tombstones_since(conn_id, savepoint_xid);
6047 (
6048 "rollback_to_savepoint",
6049 format!(
6050 "ROLLBACK TO SAVEPOINT {name} — aborted {} sub_xid(s), reverted {reverted_updates} update(s), revived {revived} tombstone(s)",
6051 aborted.len(),
6052 ),
6053 )
6054 }
6055 None => (
6056 "rollback_to_savepoint",
6057 "ROLLBACK TO outside transaction — no-op".to_string(),
6058 ),
6059 }
6060 }
6061 };
6062 Ok(RuntimeQueryResult::ok_message(
6063 query.to_string(),
6064 &msg,
6065 kind,
6066 ))
6067 }
6068 QueryExpr::CreateSchema(ref q) => {
6081 let store = self.inner.db.store();
6082 let key = format!("schema.{}", q.name);
6083 if store.get_config(&key).is_some() {
6084 if q.if_not_exists {
6085 return Ok(RuntimeQueryResult::ok_message(
6086 query.to_string(),
6087 &format!("schema {} already exists — skipped", q.name),
6088 "create_schema",
6089 ));
6090 }
6091 return Err(RedDBError::Internal(format!(
6092 "schema {} already exists",
6093 q.name
6094 )));
6095 }
6096 store.set_config_tree(&key, &crate::serde_json::Value::Bool(true));
6097 Ok(RuntimeQueryResult::ok_message(
6098 query.to_string(),
6099 &format!("schema {} created", q.name),
6100 "create_schema",
6101 ))
6102 }
6103 QueryExpr::DropSchema(ref q) => {
6104 let store = self.inner.db.store();
6105 let key = format!("schema.{}", q.name);
6106 let existed = store.get_config(&key).is_some();
6107 if !existed && !q.if_exists {
6108 return Err(RedDBError::Internal(format!(
6109 "schema {} does not exist",
6110 q.name
6111 )));
6112 }
6113 store.set_config_tree(&key, &crate::serde_json::Value::Null);
6115 let suffix = if q.cascade {
6116 " (CASCADE accepted — tables untouched)"
6117 } else {
6118 ""
6119 };
6120 Ok(RuntimeQueryResult::ok_message(
6121 query.to_string(),
6122 &format!("schema {} dropped{}", q.name, suffix),
6123 "drop_schema",
6124 ))
6125 }
6126 QueryExpr::CreateSequence(ref q) => {
6127 let store = self.inner.db.store();
6128 let base = format!("sequence.{}", q.name);
6129 let start_key = format!("{base}.start");
6130 let incr_key = format!("{base}.increment");
6131 let curr_key = format!("{base}.current");
6132 if store.get_config(&start_key).is_some() {
6133 if q.if_not_exists {
6134 return Ok(RuntimeQueryResult::ok_message(
6135 query.to_string(),
6136 &format!("sequence {} already exists — skipped", q.name),
6137 "create_sequence",
6138 ));
6139 }
6140 return Err(RedDBError::Internal(format!(
6141 "sequence {} already exists",
6142 q.name
6143 )));
6144 }
6145 let initial_current = q.start - q.increment;
6148 store.set_config_tree(
6149 &start_key,
6150 &crate::serde_json::Value::Number(q.start as f64),
6151 );
6152 store.set_config_tree(
6153 &incr_key,
6154 &crate::serde_json::Value::Number(q.increment as f64),
6155 );
6156 store.set_config_tree(
6157 &curr_key,
6158 &crate::serde_json::Value::Number(initial_current as f64),
6159 );
6160 Ok(RuntimeQueryResult::ok_message(
6161 query.to_string(),
6162 &format!(
6163 "sequence {} created (start={}, increment={})",
6164 q.name, q.start, q.increment
6165 ),
6166 "create_sequence",
6167 ))
6168 }
6169 QueryExpr::DropSequence(ref q) => {
6170 let store = self.inner.db.store();
6171 let base = format!("sequence.{}", q.name);
6172 let existed = store.get_config(&format!("{base}.start")).is_some();
6173 if !existed && !q.if_exists {
6174 return Err(RedDBError::Internal(format!(
6175 "sequence {} does not exist",
6176 q.name
6177 )));
6178 }
6179 for k in ["start", "increment", "current"] {
6180 store.set_config_tree(&format!("{base}.{k}"), &crate::serde_json::Value::Null);
6181 }
6182 Ok(RuntimeQueryResult::ok_message(
6183 query.to_string(),
6184 &format!("sequence {} dropped", q.name),
6185 "drop_sequence",
6186 ))
6187 }
6188 QueryExpr::CreateView(ref q) => {
6198 let mut views = self.inner.views.write();
6199 if views.contains_key(&q.name) && !q.or_replace {
6200 if q.if_not_exists {
6201 return Ok(RuntimeQueryResult::ok_message(
6202 query.to_string(),
6203 &format!("view {} already exists — skipped", q.name),
6204 "create_view",
6205 ));
6206 }
6207 return Err(RedDBError::Internal(format!(
6208 "view {} already exists",
6209 q.name
6210 )));
6211 }
6212 views.insert(q.name.clone(), Arc::new(q.clone()));
6213 drop(views);
6214
6215 if q.materialized {
6217 use crate::storage::cache::result::{MaterializedViewDef, RefreshPolicy};
6218 let refresh = match q.refresh_every_ms {
6219 Some(ms) => RefreshPolicy::Periodic(std::time::Duration::from_millis(ms)),
6220 None => RefreshPolicy::Manual,
6221 };
6222 let dependencies = collect_table_refs(&q.query);
6223 let def = MaterializedViewDef {
6224 name: q.name.clone(),
6225 query: format!("<parsed view {}>", q.name),
6226 dependencies: dependencies.clone(),
6227 refresh,
6228 retention_duration_ms: q.retention_duration_ms,
6229 };
6230 self.inner.materialized_views.write().register(def);
6231
6232 let descriptor =
6238 crate::runtime::continuous_materialized_view::MaterializedViewDescriptor {
6239 name: q.name.clone(),
6240 source_sql: query.to_string(),
6241 source_collections: dependencies,
6242 refresh_every_ms: q.refresh_every_ms,
6243 retention_duration_ms: q.retention_duration_ms,
6244 };
6245 let store = self.inner.db.store();
6246 crate::runtime::continuous_materialized_view::persist_descriptor(
6247 store.as_ref(),
6248 &descriptor,
6249 )?;
6250
6251 self.ensure_materialized_view_backing(&q.name)?;
6258 }
6259 self.invalidate_plan_cache();
6264 self.invalidate_result_cache();
6265
6266 Ok(RuntimeQueryResult::ok_message(
6267 query.to_string(),
6268 &format!(
6269 "{}view {} created",
6270 if q.materialized { "materialized " } else { "" },
6271 q.name
6272 ),
6273 "create_view",
6274 ))
6275 }
6276 QueryExpr::DropView(ref q) => {
6277 let mut views = self.inner.views.write();
6278 let removed = views.remove(&q.name);
6279 let existed = removed.is_some();
6280 let removed_materialized =
6281 removed.as_ref().map(|v| v.materialized).unwrap_or(false);
6282 drop(views);
6283 if q.materialized || existed {
6284 self.inner.materialized_views.write().remove(&q.name);
6286 let store = self.inner.db.store();
6290 crate::runtime::continuous_materialized_view::remove_by_name(
6291 store.as_ref(),
6292 &q.name,
6293 )?;
6294 }
6295 if removed_materialized || q.materialized {
6299 self.drop_materialized_view_backing(&q.name)?;
6300 }
6301 self.invalidate_plan_cache();
6304 self.invalidate_result_cache();
6305 if !existed && !q.if_exists {
6306 return Err(RedDBError::Internal(format!(
6307 "view {} does not exist",
6308 q.name
6309 )));
6310 }
6311 self.invalidate_plan_cache();
6312 Ok(RuntimeQueryResult::ok_message(
6313 query.to_string(),
6314 &format!("view {} dropped", q.name),
6315 "drop_view",
6316 ))
6317 }
6318 QueryExpr::RefreshMaterializedView(ref q) => {
6319 let view = {
6322 let views = self.inner.views.read();
6323 views.get(&q.name).cloned()
6324 };
6325 let view = match view {
6326 Some(v) => v,
6327 None => {
6328 return Err(RedDBError::Internal(format!(
6329 "view {} does not exist",
6330 q.name
6331 )))
6332 }
6333 };
6334 if !view.materialized {
6335 return Err(RedDBError::Internal(format!(
6336 "view {} is not materialized — REFRESH requires \
6337 CREATE MATERIALIZED VIEW",
6338 q.name
6339 )));
6340 }
6341 let started = std::time::Instant::now();
6343 let now_ms = std::time::SystemTime::now()
6344 .duration_since(std::time::UNIX_EPOCH)
6345 .map(|d| d.as_millis() as u64)
6346 .unwrap_or(0);
6347 match self.execute_query_expr((*view.query).clone()) {
6348 Ok(inner_result) => {
6349 let entities =
6356 view_records_to_entities(&q.name, &inner_result.result.records);
6357 let row_count = entities.len() as u64;
6358 let store = self.inner.db.store();
6359 let serialized_records = match store.refresh_collection(&q.name, entities) {
6360 Ok(records) => records,
6361 Err(err) => {
6362 let duration_ms = started.elapsed().as_millis() as u64;
6363 let msg = err.to_string();
6364 self.inner
6365 .materialized_views
6366 .write()
6367 .record_refresh_failure(
6368 &q.name,
6369 msg.clone(),
6370 duration_ms,
6371 now_ms,
6372 );
6373 return Err(RedDBError::Internal(format!(
6374 "REFRESH MATERIALIZED VIEW {}: {msg}",
6375 q.name
6376 )));
6377 }
6378 };
6379
6380 if let Some(ref primary) = self.inner.db.replication {
6386 let lsn = self.inner.cdc.emit(
6387 crate::replication::cdc::ChangeOperation::Refresh,
6388 &q.name,
6389 0,
6390 "refresh",
6391 );
6392 self.invalidate_result_cache_for_table(&q.name);
6393 let timestamp = std::time::SystemTime::now()
6394 .duration_since(std::time::UNIX_EPOCH)
6395 .unwrap_or_default()
6396 .as_millis() as u64;
6397 let record = ChangeRecord::for_refresh(
6398 lsn,
6399 timestamp,
6400 q.name.clone(),
6401 serialized_records,
6402 )
6403 .with_term(self.current_replication_term());
6404 let encoded = record.encode();
6405 primary.append_logical_record(record.lsn, encoded);
6406 }
6407
6408 let duration_ms = started.elapsed().as_millis() as u64;
6409 let serialized = format!("{:?}", inner_result.result);
6410 self.inner
6411 .materialized_views
6412 .write()
6413 .record_refresh_success(
6414 &q.name,
6415 serialized.into_bytes(),
6416 row_count,
6417 duration_ms,
6418 now_ms,
6419 );
6420 self.invalidate_result_cache();
6425 Ok(RuntimeQueryResult::ok_message(
6426 query.to_string(),
6427 &format!("materialized view {} refreshed", q.name),
6428 "refresh_materialized_view",
6429 ))
6430 }
6431 Err(err) => {
6432 let duration_ms = started.elapsed().as_millis() as u64;
6433 let msg = err.to_string();
6434 self.inner
6435 .materialized_views
6436 .write()
6437 .record_refresh_failure(&q.name, msg.clone(), duration_ms, now_ms);
6438 Err(err)
6439 }
6440 }
6441 }
6442 QueryExpr::CreatePolicy(ref q) => {
6449 let key = (q.table.clone(), q.name.clone());
6450 self.inner
6451 .rls_policies
6452 .write()
6453 .insert(key, Arc::new(q.clone()));
6454 self.invalidate_plan_cache();
6455 self.schema_vocabulary_apply(
6459 crate::runtime::schema_vocabulary::DdlEvent::CreatePolicy {
6460 collection: q.table.clone(),
6461 policy: q.name.clone(),
6462 },
6463 );
6464 Ok(RuntimeQueryResult::ok_message(
6465 query.to_string(),
6466 &format!("policy {} on {} created", q.name, q.table),
6467 "create_policy",
6468 ))
6469 }
6470 QueryExpr::DropPolicy(ref q) => {
6471 let removed = self
6472 .inner
6473 .rls_policies
6474 .write()
6475 .remove(&(q.table.clone(), q.name.clone()))
6476 .is_some();
6477 if !removed && !q.if_exists {
6478 return Err(RedDBError::Internal(format!(
6479 "policy {} on {} does not exist",
6480 q.name, q.table
6481 )));
6482 }
6483 self.invalidate_plan_cache();
6484 self.schema_vocabulary_apply(
6487 crate::runtime::schema_vocabulary::DdlEvent::DropPolicy {
6488 collection: q.table.clone(),
6489 policy: q.name.clone(),
6490 },
6491 );
6492 Ok(RuntimeQueryResult::ok_message(
6493 query.to_string(),
6494 &format!("policy {} on {} dropped", q.name, q.table),
6495 "drop_policy",
6496 ))
6497 }
6498 QueryExpr::CreateServer(ref q) => {
6509 use crate::storage::fdw::FdwOptions;
6510 let registry = Arc::clone(&self.inner.foreign_tables);
6511 if registry.server(&q.name).is_some() {
6512 if q.if_not_exists {
6513 return Ok(RuntimeQueryResult::ok_message(
6514 query.to_string(),
6515 &format!("server {} already exists — skipped", q.name),
6516 "create_server",
6517 ));
6518 }
6519 return Err(RedDBError::Internal(format!(
6520 "server {} already exists",
6521 q.name
6522 )));
6523 }
6524 let mut opts = FdwOptions::new();
6525 for (k, v) in &q.options {
6526 opts.values.insert(k.clone(), v.clone());
6527 }
6528 registry
6529 .create_server(&q.name, &q.wrapper, opts)
6530 .map_err(|e| RedDBError::Internal(e.to_string()))?;
6531 Ok(RuntimeQueryResult::ok_message(
6532 query.to_string(),
6533 &format!("server {} created (wrapper {})", q.name, q.wrapper),
6534 "create_server",
6535 ))
6536 }
6537 QueryExpr::DropServer(ref q) => {
6538 let existed = self.inner.foreign_tables.drop_server(&q.name);
6539 if !existed && !q.if_exists {
6540 return Err(RedDBError::Internal(format!(
6541 "server {} does not exist",
6542 q.name
6543 )));
6544 }
6545 Ok(RuntimeQueryResult::ok_message(
6546 query.to_string(),
6547 &format!(
6548 "server {} dropped{}",
6549 q.name,
6550 if q.cascade { " (cascade)" } else { "" }
6551 ),
6552 "drop_server",
6553 ))
6554 }
6555 QueryExpr::CreateForeignTable(ref q) => {
6556 use crate::storage::fdw::{FdwOptions, ForeignColumn, ForeignTable};
6557 let registry = Arc::clone(&self.inner.foreign_tables);
6558 if registry.foreign_table(&q.name).is_some() {
6559 if q.if_not_exists {
6560 return Ok(RuntimeQueryResult::ok_message(
6561 query.to_string(),
6562 &format!("foreign table {} already exists — skipped", q.name),
6563 "create_foreign_table",
6564 ));
6565 }
6566 return Err(RedDBError::Internal(format!(
6567 "foreign table {} already exists",
6568 q.name
6569 )));
6570 }
6571 let mut opts = FdwOptions::new();
6572 for (k, v) in &q.options {
6573 opts.values.insert(k.clone(), v.clone());
6574 }
6575 let columns: Vec<ForeignColumn> = q
6576 .columns
6577 .iter()
6578 .map(|c| ForeignColumn {
6579 name: c.name.clone(),
6580 data_type: c.data_type.clone(),
6581 not_null: c.not_null,
6582 })
6583 .collect();
6584 registry
6585 .create_foreign_table(ForeignTable {
6586 name: q.name.clone(),
6587 server_name: q.server.clone(),
6588 columns,
6589 options: opts,
6590 })
6591 .map_err(|e| RedDBError::Internal(e.to_string()))?;
6592 self.invalidate_plan_cache();
6593 Ok(RuntimeQueryResult::ok_message(
6594 query.to_string(),
6595 &format!("foreign table {} created (server {})", q.name, q.server),
6596 "create_foreign_table",
6597 ))
6598 }
6599 QueryExpr::DropForeignTable(ref q) => {
6600 let existed = self.inner.foreign_tables.drop_foreign_table(&q.name);
6601 if !existed && !q.if_exists {
6602 return Err(RedDBError::Internal(format!(
6603 "foreign table {} does not exist",
6604 q.name
6605 )));
6606 }
6607 self.invalidate_plan_cache();
6608 Ok(RuntimeQueryResult::ok_message(
6609 query.to_string(),
6610 &format!("foreign table {} dropped", q.name),
6611 "drop_foreign_table",
6612 ))
6613 }
6614 QueryExpr::CopyFrom(ref q) => {
6620 use crate::storage::import::{CsvConfig, CsvImporter};
6621 let store = self.inner.db.store();
6622 let cfg = CsvConfig {
6623 collection: q.table.clone(),
6624 has_header: q.has_header,
6625 delimiter: q.delimiter.map(|c| c as u8).unwrap_or(b','),
6626 ..CsvConfig::default()
6627 };
6628 let importer = CsvImporter::new(cfg);
6629 let stats = importer
6630 .import_file(&q.path, store.as_ref())
6631 .map_err(|e| RedDBError::Internal(format!("COPY failed: {e}")))?;
6632 self.note_table_write(&q.table);
6634 Ok(RuntimeQueryResult::ok_message(
6635 query.to_string(),
6636 &format!(
6637 "COPY imported {} rows into {} ({} errors skipped, {}ms)",
6638 stats.records_imported, q.table, stats.errors_skipped, stats.duration_ms
6639 ),
6640 "copy_from",
6641 ))
6642 }
6643 QueryExpr::MaintenanceCommand(ref cmd) => {
6659 use crate::storage::query::ast::MaintenanceCommand as Mc;
6660 let store = self.inner.db.store();
6661 let (kind, msg) = match cmd {
6662 Mc::Analyze { target } => {
6663 let targets: Vec<String> = match target {
6664 Some(t) => vec![t.clone()],
6665 None => store.list_collections(),
6666 };
6667 for t in &targets {
6668 self.refresh_table_planner_stats(t);
6669 }
6670 (
6671 "analyze",
6672 format!("ANALYZE refreshed stats for {} table(s)", targets.len()),
6673 )
6674 }
6675 Mc::Vacuum { target, full } => {
6676 let targets: Vec<String> = match target {
6677 Some(t) => vec![t.clone()],
6678 None => store.list_collections(),
6679 };
6680 let cutoff_xid = self.mvcc_vacuum_cutoff_xid();
6681 let mut vacuum_stats =
6682 crate::storage::unified::store::MvccVacuumStats::default();
6683 for t in &targets {
6684 let stats = store.vacuum_mvcc_history(t, cutoff_xid).map_err(|e| {
6685 RedDBError::Internal(format!(
6686 "VACUUM MVCC history failed for {t}: {e}"
6687 ))
6688 })?;
6689 if stats.reclaimed_versions > 0 {
6690 self.rebuild_runtime_indexes_for_table(t)?;
6691 }
6692 vacuum_stats.add(&stats);
6693 }
6694 self.inner.snapshot_manager.prune_aborted(cutoff_xid);
6695 for t in &targets {
6697 self.refresh_table_planner_stats(t);
6698 }
6699 let persisted = if *full {
6703 match store.persist() {
6704 Ok(()) => true,
6705 Err(e) => {
6706 return Err(RedDBError::Internal(format!(
6707 "VACUUM FULL persist failed: {e:?}"
6708 )));
6709 }
6710 }
6711 } else {
6712 false
6713 };
6714 self.invalidate_result_cache();
6716 (
6717 "vacuum",
6718 format!(
6719 "VACUUM{} processed {} table(s): scanned_versions={}, retained_versions={}, reclaimed_versions={}, retained_history_versions={}, reclaimed_history_versions={}, retained_tombstones={}, reclaimed_tombstones={}{}",
6720 if *full { " FULL" } else { "" },
6721 targets.len(),
6722 vacuum_stats.scanned_versions,
6723 vacuum_stats.retained_versions,
6724 vacuum_stats.reclaimed_versions,
6725 vacuum_stats.retained_history_versions,
6726 vacuum_stats.reclaimed_history_versions,
6727 vacuum_stats.retained_tombstones,
6728 vacuum_stats.reclaimed_tombstones,
6729 if persisted {
6730 " (pages flushed to disk)"
6731 } else {
6732 ""
6733 }
6734 ),
6735 )
6736 }
6737 };
6738 Ok(RuntimeQueryResult::ok_message(
6739 query.to_string(),
6740 &msg,
6741 kind,
6742 ))
6743 }
6744 QueryExpr::Grant(ref g) => self.execute_grant_statement(query, g),
6751 QueryExpr::Revoke(ref r) => self.execute_revoke_statement(query, r),
6752 QueryExpr::AlterUser(ref a) => self.execute_alter_user_statement(query, a),
6753 QueryExpr::CreateUser(ref u) => self.execute_create_user_statement(query, u),
6754 QueryExpr::CreateIamPolicy { ref id, ref json } => {
6755 self.execute_create_iam_policy(query, id, json)
6756 }
6757 QueryExpr::DropIamPolicy { ref id } => self.execute_drop_iam_policy(query, id),
6758 QueryExpr::AttachPolicy {
6759 ref policy_id,
6760 ref principal,
6761 } => self.execute_attach_policy(query, policy_id, principal),
6762 QueryExpr::DetachPolicy {
6763 ref policy_id,
6764 ref principal,
6765 } => self.execute_detach_policy(query, policy_id, principal),
6766 QueryExpr::ShowPolicies { ref filter } => {
6767 self.execute_show_policies(query, filter.as_ref())
6768 }
6769 QueryExpr::ShowEffectivePermissions {
6770 ref user,
6771 ref resource,
6772 } => self.execute_show_effective_permissions(query, user, resource.as_ref()),
6773 QueryExpr::SimulatePolicy {
6774 ref user,
6775 ref action,
6776 ref resource,
6777 } => self.execute_simulate_policy(query, user, action, resource),
6778 QueryExpr::LintPolicy { ref source } => self.execute_lint_policy(query, source),
6779 QueryExpr::MigratePolicyMode {
6780 ref target,
6781 dry_run,
6782 } => self.execute_migrate_policy_mode(query, target, dry_run),
6783 QueryExpr::CreateMigration(ref q) => self.execute_create_migration(query, q),
6784 QueryExpr::ApplyMigration(ref q) => self.execute_apply_migration(query, q),
6785 QueryExpr::RollbackMigration(ref q) => self.execute_rollback_migration(query, q),
6786 QueryExpr::ExplainMigration(ref q) => self.execute_explain_migration(query, q),
6787 _ => Err(RedDBError::Query(
6788 "unsupported command in runtime dispatcher".to_string(),
6789 )),
6790 };
6791
6792 if !control_event_specs.is_empty() {
6793 let (outcome, reason) = match &query_result {
6794 Ok(_) => (crate::runtime::control_events::Outcome::Allowed, None),
6795 Err(err) => (control_event_outcome_for_error(err), Some(err.to_string())),
6796 };
6797 for spec in &control_event_specs {
6798 self.emit_control_event(
6799 spec.kind,
6800 outcome,
6801 spec.action,
6802 spec.resource.clone(),
6803 reason.clone(),
6804 spec.fields.clone(),
6805 )?;
6806 }
6807 }
6808
6809 if let (Some(plan), Ok(result)) = (&query_audit_plan, &query_result) {
6810 self.emit_query_audit(
6811 query,
6812 plan,
6813 query_audit_started.elapsed().as_millis() as u64,
6814 result,
6815 );
6816 }
6817
6818 let mut query_result = query_result;
6822 if let Ok(ref mut result) = query_result {
6823 if result.statement_type == "select" {
6824 self.apply_secret_decryption(result);
6825 }
6826 }
6827
6828 if let Ok(ref result) = query_result {
6835 frame.write_result_cache(self, result, result_cache_scopes);
6836 }
6837
6838 query_result
6839 }
6840
6841 pub fn materialized_view_metadata(
6845 &self,
6846 ) -> Vec<crate::storage::cache::result::MaterializedViewMetadata> {
6847 let store = self.inner.db.store();
6854 let mut entries = self.inner.materialized_views.read().metadata();
6855 for entry in &mut entries {
6856 if let Some(manager) = store.get_collection(&entry.name) {
6857 entry.current_row_count = manager.count() as u64;
6858 }
6859 }
6860 entries
6861 }
6862
6863 pub(crate) fn retention_sweeper_snapshot(
6874 &self,
6875 ) -> Vec<(String, crate::runtime::retention_sweeper::SweeperState)> {
6876 self.inner.retention_sweeper.read().snapshot()
6877 }
6878
6879 pub fn sweep_retention_tick(&self, batch_size: usize) {
6901 if batch_size == 0 {
6902 return;
6903 }
6904 let now_ms = std::time::SystemTime::now()
6905 .duration_since(std::time::UNIX_EPOCH)
6906 .map(|d| d.as_millis() as u64)
6907 .unwrap_or(0);
6908
6909 let store = self.inner.db.store();
6910 let collections = store.list_collections();
6911 for name in collections {
6912 let Some(contract) = self.inner.db.collection_contract(&name) else {
6913 continue;
6914 };
6915 let Some(retention_ms) = contract.retention_duration_ms else {
6916 continue;
6917 };
6918 let Some(ts_column) =
6919 crate::runtime::retention_filter::resolve_timestamp_column(&contract)
6920 else {
6921 continue;
6922 };
6923 let Some(manager) = store.get_collection(&name) else {
6924 continue;
6925 };
6926 let cutoff = (now_ms as i64).saturating_sub(retention_ms as i64);
6927
6928 let mut expired_ts: Vec<i64> = Vec::new();
6936 manager.for_each_entity(|entity| {
6937 let ts = match ts_column.as_str() {
6938 "created_at" => Some(entity.created_at as i64),
6939 "updated_at" => Some(entity.updated_at as i64),
6940 other => entity
6941 .data
6942 .as_row()
6943 .and_then(|row| row.get_field(other))
6944 .and_then(|v| match v {
6945 crate::storage::schema::Value::TimestampMs(t) => Some(*t),
6946 crate::storage::schema::Value::Timestamp(t) => {
6947 Some(t.saturating_mul(1_000))
6948 }
6949 crate::storage::schema::Value::BigInt(t) => Some(*t),
6950 crate::storage::schema::Value::UnsignedInteger(t) => {
6951 i64::try_from(*t).ok()
6952 }
6953 crate::storage::schema::Value::Integer(t) => Some(*t),
6954 _ => None,
6955 }),
6956 };
6957 if let Some(t) = ts {
6958 if t < cutoff {
6959 expired_ts.push(t);
6960 }
6961 }
6962 true
6963 });
6964
6965 let total_expired = expired_ts.len() as u64;
6966 if total_expired == 0 {
6967 self.inner
6968 .retention_sweeper
6969 .write()
6970 .record_tick(&name, 0, 0, now_ms);
6971 continue;
6972 }
6973
6974 let (effective_cutoff, pending) = if (total_expired as usize) <= batch_size {
6975 (cutoff, 0u64)
6976 } else {
6977 expired_ts.sort_unstable();
6981 let nth = expired_ts[batch_size - 1];
6982 (
6983 nth.saturating_add(1),
6984 total_expired.saturating_sub(batch_size as u64),
6985 )
6986 };
6987
6988 let stmt = format!(
6989 "DELETE FROM {} WHERE {} < {}",
6990 name, ts_column, effective_cutoff
6991 );
6992 let deleted = match self.execute_query(&stmt) {
6993 Ok(r) => r.affected_rows,
6994 Err(_) => 0,
6995 };
6996
6997 self.inner
6998 .retention_sweeper
6999 .write()
7000 .record_tick(&name, deleted, pending, now_ms);
7001 }
7002 }
7003
7004 pub fn refresh_due_materialized_views(&self) {
7005 let due = {
7006 let mut cache = self.inner.materialized_views.write();
7007 cache.claim_due_at(std::time::Instant::now())
7008 };
7009 for name in due {
7010 let stmt = format!("REFRESH MATERIALIZED VIEW {}", name);
7017 let _ = self.execute_query(&stmt);
7018 }
7019 }
7020
7021 pub fn execute_query_expr(&self, expr: QueryExpr) -> RedDBResult<RuntimeQueryResult> {
7027 let _config_snapshot_guard = ConfigSnapshotGuard::install(Arc::clone(&self.inner.db));
7028 let _secret_store_guard = SecretStoreGuard::install(self.inner.auth_store.read().clone());
7029 let expr = self.rewrite_view_refs(expr);
7033
7034 self.validate_model_operations_before_auth(&expr)?;
7035 if let Err(err) = self.check_query_privilege(&expr) {
7039 return Err(RedDBError::Query(format!("permission denied: {err}")));
7040 }
7041
7042 let statement = query_expr_name(&expr);
7043 let mode = detect_mode(statement);
7044 let query_str = statement;
7045
7046 let result = self.dispatch_expr(expr, query_str, mode)?;
7047 let mut r = result;
7048 if r.statement_type == "select" {
7049 self.apply_secret_decryption(&mut r);
7050 }
7051 Ok(r)
7052 }
7053
7054 pub(super) fn validate_model_operations_before_auth(
7055 &self,
7056 expr: &QueryExpr,
7057 ) -> RedDBResult<()> {
7058 use crate::catalog::CollectionModel;
7059 use crate::runtime::ddl::polymorphic_resolver;
7060 use crate::storage::query::ast::KvCommand;
7061
7062 let system_schema_target = match expr {
7063 QueryExpr::DropTable(q) => Some(q.name.as_str()),
7064 QueryExpr::DropGraph(q) => Some(q.name.as_str()),
7065 QueryExpr::DropVector(q) => Some(q.name.as_str()),
7066 QueryExpr::DropDocument(q) => Some(q.name.as_str()),
7067 QueryExpr::DropKv(q) => Some(q.name.as_str()),
7068 QueryExpr::DropCollection(q) => Some(q.name.as_str()),
7069 QueryExpr::Truncate(q) => Some(q.name.as_str()),
7070 _ => None,
7071 };
7072 if system_schema_target.is_some_and(crate::runtime::impl_ddl::is_system_schema_name) {
7073 return Err(RedDBError::Query("system schema is read-only".to_string()));
7074 }
7075
7076 let expected = match expr {
7077 QueryExpr::DropTable(q) => Some((q.name.as_str(), CollectionModel::Table)),
7078 QueryExpr::DropGraph(q) => Some((q.name.as_str(), CollectionModel::Graph)),
7079 QueryExpr::DropVector(q) => Some((q.name.as_str(), CollectionModel::Vector)),
7080 QueryExpr::DropDocument(q) => Some((q.name.as_str(), CollectionModel::Document)),
7081 QueryExpr::DropKv(q) => Some((q.name.as_str(), q.model)),
7082 QueryExpr::DropCollection(q) => q.model.map(|model| (q.name.as_str(), model)),
7083 QueryExpr::Truncate(q) => q.model.map(|model| (q.name.as_str(), model)),
7084 QueryExpr::KvCommand(cmd) => {
7085 let (collection, model) = match cmd {
7086 KvCommand::Put {
7087 collection, model, ..
7088 }
7089 | KvCommand::Get {
7090 collection, model, ..
7091 }
7092 | KvCommand::Incr {
7093 collection, model, ..
7094 }
7095 | KvCommand::Cas {
7096 collection, model, ..
7097 }
7098 | KvCommand::List {
7099 collection, model, ..
7100 }
7101 | KvCommand::Delete {
7102 collection, model, ..
7103 } => (collection.as_str(), *model),
7104 KvCommand::Rotate { collection, .. }
7105 | KvCommand::History { collection, .. }
7106 | KvCommand::Purge { collection, .. } => {
7107 (collection.as_str(), CollectionModel::Vault)
7108 }
7109 KvCommand::InvalidateTags { collection, .. } => {
7110 (collection.as_str(), CollectionModel::Kv)
7111 }
7112 KvCommand::Watch {
7113 collection, model, ..
7114 } => (collection.as_str(), *model),
7115 KvCommand::Unseal { collection, .. } => {
7116 (collection.as_str(), CollectionModel::Vault)
7117 }
7118 };
7119 Some((collection, model))
7120 }
7121 QueryExpr::ConfigCommand(cmd) => {
7122 self.validate_config_command_before_auth(cmd)?;
7123 None
7124 }
7125 _ => None,
7126 };
7127
7128 let Some((name, expected_model)) = expected else {
7129 return Ok(());
7130 };
7131 let snapshot = self.inner.db.catalog_model_snapshot();
7132 let Some(actual_model) = snapshot
7133 .collections
7134 .iter()
7135 .find(|collection| collection.name == name)
7136 .map(|collection| collection.declared_model.unwrap_or(collection.model))
7137 else {
7138 return Ok(());
7139 };
7140 polymorphic_resolver::ensure_model_match(expected_model, actual_model)
7141 }
7142
7143 pub(super) fn rewrite_view_refs(&self, expr: QueryExpr) -> QueryExpr {
7148 if self.inner.views.read().is_empty() {
7150 return expr;
7151 }
7152 self.rewrite_view_refs_inner(expr)
7153 }
7154
7155 fn rewrite_view_refs_inner(&self, expr: QueryExpr) -> QueryExpr {
7156 use crate::storage::query::ast::{Filter, TableSource};
7157 match expr {
7158 QueryExpr::Table(mut tq) => {
7159 if let Some(TableSource::Subquery(body)) = tq.source.take() {
7165 tq.source = Some(TableSource::Subquery(Box::new(
7166 self.rewrite_view_refs_inner(*body),
7167 )));
7168 return QueryExpr::Table(tq);
7169 }
7170
7171 let maybe_view = {
7175 let views = self.inner.views.read();
7176 views.get(&tq.table).cloned()
7177 };
7178 let Some(view) = maybe_view else {
7179 return QueryExpr::Table(tq);
7180 };
7181
7182 if view.materialized {
7188 return QueryExpr::Table(tq);
7189 }
7190
7191 let inner_expr = self.rewrite_view_refs_inner((*view.query).clone());
7195
7196 match inner_expr {
7204 QueryExpr::Table(mut inner_tq) => {
7205 if let Some(outer_filter) = tq.filter.take() {
7206 inner_tq.filter = Some(match inner_tq.filter.take() {
7207 Some(existing) => {
7208 Filter::And(Box::new(existing), Box::new(outer_filter))
7209 }
7210 None => outer_filter,
7211 });
7212 inner_tq.where_expr = inner_tq
7220 .filter
7221 .as_ref()
7222 .map(crate::storage::query::sql_lowering::filter_to_expr);
7223 }
7224 if let Some(outer_limit) = tq.limit {
7225 inner_tq.limit = Some(match inner_tq.limit {
7226 Some(existing) => existing.min(outer_limit),
7227 None => outer_limit,
7228 });
7229 }
7230 if let Some(outer_offset) = tq.offset {
7231 inner_tq.offset = Some(match inner_tq.offset {
7232 Some(existing) => existing + outer_offset,
7233 None => outer_offset,
7234 });
7235 }
7236 QueryExpr::Table(inner_tq)
7237 }
7238 other => other,
7239 }
7240 }
7241 QueryExpr::Join(mut jq) => {
7242 jq.left = Box::new(self.rewrite_view_refs_inner(*jq.left));
7243 jq.right = Box::new(self.rewrite_view_refs_inner(*jq.right));
7244 QueryExpr::Join(jq)
7245 }
7246 other => other,
7249 }
7250 }
7251
7252 fn authorize_relational_table_select(
7255 &self,
7256 mut table: TableQuery,
7257 frame: &dyn super::statement_frame::ReadFrame,
7258 ) -> RedDBResult<Option<TableQuery>> {
7259 if let Some(TableSource::Subquery(inner)) = table.source.take() {
7260 let authorized_inner = self.authorize_relational_select_expr(*inner, frame)?;
7261 table.source = Some(TableSource::Subquery(Box::new(authorized_inner)));
7262 return Ok(Some(table));
7263 }
7264
7265 self.check_table_column_projection_authz(&table, frame)?;
7266
7267 if self.inner.rls_enabled_tables.read().contains(&table.table) {
7268 return Ok(inject_rls_filters(self, frame, table));
7269 }
7270
7271 Ok(Some(table))
7272 }
7273
7274 fn authorize_relational_join_select(
7275 &self,
7276 mut join: JoinQuery,
7277 frame: &dyn super::statement_frame::ReadFrame,
7278 ) -> RedDBResult<Option<JoinQuery>> {
7279 self.check_join_column_projection_authz(&join, frame)?;
7280 join.left = Box::new(self.authorize_relational_join_child(*join.left, frame)?);
7281 join.right = Box::new(self.authorize_relational_join_child(*join.right, frame)?);
7282 Ok(inject_rls_into_join(self, frame, join))
7283 }
7284
7285 fn authorize_relational_join_child(
7286 &self,
7287 expr: QueryExpr,
7288 frame: &dyn super::statement_frame::ReadFrame,
7289 ) -> RedDBResult<QueryExpr> {
7290 match expr {
7291 QueryExpr::Table(mut table) => {
7292 if let Some(TableSource::Subquery(inner)) = table.source.take() {
7293 let authorized_inner = self.authorize_relational_select_expr(*inner, frame)?;
7294 table.source = Some(TableSource::Subquery(Box::new(authorized_inner)));
7295 }
7296 Ok(QueryExpr::Table(table))
7297 }
7298 QueryExpr::Join(join) => self
7299 .authorize_relational_join_select(join, frame)?
7300 .map(QueryExpr::Join)
7301 .ok_or_else(|| {
7302 RedDBError::Query("permission denied: RLS denied relational subquery".into())
7303 }),
7304 other => Ok(other),
7305 }
7306 }
7307
7308 fn authorize_relational_select_expr(
7309 &self,
7310 expr: QueryExpr,
7311 frame: &dyn super::statement_frame::ReadFrame,
7312 ) -> RedDBResult<QueryExpr> {
7313 match expr {
7314 QueryExpr::Table(table) => self
7315 .authorize_relational_table_select(table, frame)?
7316 .map(QueryExpr::Table)
7317 .ok_or_else(|| {
7318 RedDBError::Query("permission denied: RLS denied relational subquery".into())
7319 }),
7320 QueryExpr::Join(join) => self
7321 .authorize_relational_join_select(join, frame)?
7322 .map(QueryExpr::Join)
7323 .ok_or_else(|| {
7324 RedDBError::Query("permission denied: RLS denied relational subquery".into())
7325 }),
7326 other => Ok(other),
7327 }
7328 }
7329
7330 fn check_table_column_projection_authz(
7331 &self,
7332 table: &TableQuery,
7333 frame: &dyn super::statement_frame::ReadFrame,
7334 ) -> RedDBResult<()> {
7335 let Some((username, role)) = frame.identity() else {
7336 return Ok(());
7337 };
7338 let Some(auth_store) = self.inner.auth_store.read().clone() else {
7339 return Ok(());
7340 };
7341
7342 let columns = self.resolved_table_projection_columns(table)?;
7343 let request = ColumnAccessRequest::select(table.table.clone(), columns);
7344 let principal = UserId::from_parts(frame.effective_scope(), username);
7345 let ctx = runtime_iam_context(role, frame.effective_scope());
7346 let outcome = auth_store.check_column_projection_authz(&principal, &request, &ctx);
7347 if outcome.allowed() {
7348 return Ok(());
7349 }
7350
7351 if let Some(denied) = outcome.first_denied_column() {
7352 return Err(RedDBError::Query(format!(
7353 "permission denied: principal=`{username}` cannot select column `{}`",
7354 denied.resource.name
7355 )));
7356 }
7357 Err(RedDBError::Query(format!(
7358 "permission denied: principal=`{username}` cannot select table `{}`",
7359 table.table
7360 )))
7361 }
7362
7363 fn check_join_column_projection_authz(
7364 &self,
7365 join: &JoinQuery,
7366 frame: &dyn super::statement_frame::ReadFrame,
7367 ) -> RedDBResult<()> {
7368 let mut by_table: HashMap<String, BTreeSet<String>> = HashMap::new();
7369 let projections = crate::storage::query::sql_lowering::effective_join_projections(join);
7370 self.collect_join_projection_columns(join, &projections, &mut by_table)?;
7371
7372 for (table, columns) in by_table {
7373 let query = TableQuery {
7374 table,
7375 source: None,
7376 alias: None,
7377 select_items: Vec::new(),
7378 columns: columns.into_iter().map(Projection::Column).collect(),
7379 where_expr: None,
7380 filter: None,
7381 group_by_exprs: Vec::new(),
7382 group_by: Vec::new(),
7383 having_expr: None,
7384 having: None,
7385 order_by: Vec::new(),
7386 limit: None,
7387 limit_param: None,
7388 offset: None,
7389 offset_param: None,
7390 expand: None,
7391 as_of: None,
7392 sessionize: None,
7393 distinct: false,
7394 };
7395 self.check_table_column_projection_authz(&query, frame)?;
7396 }
7397 Ok(())
7398 }
7399
7400 fn collect_join_projection_columns(
7401 &self,
7402 join: &JoinQuery,
7403 projections: &[Projection],
7404 out: &mut HashMap<String, BTreeSet<String>>,
7405 ) -> RedDBResult<()> {
7406 let left = table_side_context(join.left.as_ref());
7407 let right = table_side_context(join.right.as_ref());
7408
7409 if projections
7410 .iter()
7411 .any(|projection| matches!(projection, Projection::All))
7412 {
7413 for side in [left.as_ref(), right.as_ref()].into_iter().flatten() {
7414 out.entry(side.table.clone())
7415 .or_default()
7416 .extend(self.table_all_projection_columns(&side.table)?);
7417 }
7418 return Ok(());
7419 }
7420
7421 for projection in projections {
7422 collect_projection_columns_for_join_side(
7423 projection,
7424 left.as_ref(),
7425 right.as_ref(),
7426 out,
7427 )?;
7428 }
7429 Ok(())
7430 }
7431
7432 fn resolved_table_projection_columns(&self, table: &TableQuery) -> RedDBResult<Vec<String>> {
7433 let projections = crate::storage::query::sql_lowering::effective_table_projections(table);
7434 if projections
7435 .iter()
7436 .any(|projection| matches!(projection, Projection::All))
7437 {
7438 return self.table_all_projection_columns(&table.table);
7439 }
7440
7441 let mut columns = BTreeSet::new();
7442 for projection in &projections {
7443 collect_projection_columns_for_table(
7444 projection,
7445 &table.table,
7446 table.alias.as_deref(),
7447 &mut columns,
7448 );
7449 }
7450 Ok(columns.into_iter().collect())
7451 }
7452
7453 fn table_all_projection_columns(&self, table: &str) -> RedDBResult<Vec<String>> {
7454 if let Some(contract) = self.inner.db.collection_contract_arc(table) {
7455 let columns: Vec<String> = contract
7456 .declared_columns
7457 .iter()
7458 .map(|column| column.name.clone())
7459 .collect();
7460 if !columns.is_empty() {
7461 return Ok(columns);
7462 }
7463 }
7464
7465 let records = scan_runtime_table_source_records_limited(&self.inner.db, table, Some(1))?;
7466 Ok(records
7467 .first()
7468 .map(|record| {
7469 record
7470 .column_names()
7471 .into_iter()
7472 .map(|column| column.to_string())
7473 .collect()
7474 })
7475 .unwrap_or_default())
7476 }
7477
7478 fn resolve_table_expr_subqueries(
7479 &self,
7480 mut table: TableQuery,
7481 frame: &dyn super::statement_frame::ReadFrame,
7482 ) -> RedDBResult<TableQuery> {
7483 match table.source.take() {
7490 Some(TableSource::Subquery(inner)) => {
7491 let inner = self.resolve_select_expr_subqueries(*inner, frame)?;
7492 table.source = Some(TableSource::Subquery(Box::new(inner)));
7493 }
7494 other => table.source = other,
7495 }
7496
7497 let outer_scopes = relation_scopes_for_query(&QueryExpr::Table(table.clone()));
7498 for item in &mut table.select_items {
7499 if let crate::storage::query::ast::SelectItem::Expr { expr, .. } = item {
7500 *expr = self.resolve_expr_subqueries(expr.clone(), &outer_scopes, frame)?;
7501 }
7502 }
7503 if let Some(where_expr) = table.where_expr.take() {
7504 table.where_expr =
7505 Some(self.resolve_expr_subqueries(where_expr, &outer_scopes, frame)?);
7506 table.filter = None;
7507 }
7508 if let Some(having_expr) = table.having_expr.take() {
7509 table.having_expr =
7510 Some(self.resolve_expr_subqueries(having_expr, &outer_scopes, frame)?);
7511 table.having = None;
7512 }
7513 for expr in &mut table.group_by_exprs {
7514 *expr = self.resolve_expr_subqueries(expr.clone(), &outer_scopes, frame)?;
7515 }
7516 for clause in &mut table.order_by {
7517 if let Some(expr) = clause.expr.take() {
7518 clause.expr = Some(self.resolve_expr_subqueries(expr, &outer_scopes, frame)?);
7519 }
7520 }
7521 Ok(table)
7522 }
7523
7524 fn resolve_select_expr_subqueries(
7525 &self,
7526 expr: QueryExpr,
7527 frame: &dyn super::statement_frame::ReadFrame,
7528 ) -> RedDBResult<QueryExpr> {
7529 match expr {
7530 QueryExpr::Table(table) => self
7531 .resolve_table_expr_subqueries(table, frame)
7532 .map(QueryExpr::Table),
7533 QueryExpr::Join(mut join) => {
7534 join.left = Box::new(self.resolve_select_expr_subqueries(*join.left, frame)?);
7535 join.right = Box::new(self.resolve_select_expr_subqueries(*join.right, frame)?);
7536 Ok(QueryExpr::Join(join))
7537 }
7538 other => Ok(other),
7539 }
7540 }
7541
7542 fn resolve_expr_subqueries(
7543 &self,
7544 expr: crate::storage::query::ast::Expr,
7545 outer_scopes: &[String],
7546 frame: &dyn super::statement_frame::ReadFrame,
7547 ) -> RedDBResult<crate::storage::query::ast::Expr> {
7548 use crate::storage::query::ast::Expr;
7549
7550 match expr {
7551 Expr::Subquery { query, span } => {
7552 let values = self.execute_expr_subquery_values(query, outer_scopes, frame)?;
7553 if values.len() > 1 {
7554 return Err(RedDBError::Query(
7555 "scalar subquery returned more than one row".to_string(),
7556 ));
7557 }
7558 Ok(Expr::Literal {
7559 value: values.into_iter().next().unwrap_or(Value::Null),
7560 span,
7561 })
7562 }
7563 Expr::BinaryOp { op, lhs, rhs, span } => Ok(Expr::BinaryOp {
7564 op,
7565 lhs: Box::new(self.resolve_expr_subqueries(*lhs, outer_scopes, frame)?),
7566 rhs: Box::new(self.resolve_expr_subqueries(*rhs, outer_scopes, frame)?),
7567 span,
7568 }),
7569 Expr::UnaryOp { op, operand, span } => Ok(Expr::UnaryOp {
7570 op,
7571 operand: Box::new(self.resolve_expr_subqueries(*operand, outer_scopes, frame)?),
7572 span,
7573 }),
7574 Expr::Cast {
7575 inner,
7576 target,
7577 span,
7578 } => Ok(Expr::Cast {
7579 inner: Box::new(self.resolve_expr_subqueries(*inner, outer_scopes, frame)?),
7580 target,
7581 span,
7582 }),
7583 Expr::FunctionCall { name, args, span } => {
7584 let args = args
7585 .into_iter()
7586 .map(|arg| self.resolve_expr_subqueries(arg, outer_scopes, frame))
7587 .collect::<RedDBResult<Vec<_>>>()?;
7588 Ok(Expr::FunctionCall { name, args, span })
7589 }
7590 Expr::Case {
7591 branches,
7592 else_,
7593 span,
7594 } => {
7595 let branches = branches
7596 .into_iter()
7597 .map(|(cond, value)| {
7598 Ok((
7599 self.resolve_expr_subqueries(cond, outer_scopes, frame)?,
7600 self.resolve_expr_subqueries(value, outer_scopes, frame)?,
7601 ))
7602 })
7603 .collect::<RedDBResult<Vec<_>>>()?;
7604 let else_ = else_
7605 .map(|expr| self.resolve_expr_subqueries(*expr, outer_scopes, frame))
7606 .transpose()?
7607 .map(Box::new);
7608 Ok(Expr::Case {
7609 branches,
7610 else_,
7611 span,
7612 })
7613 }
7614 Expr::IsNull {
7615 operand,
7616 negated,
7617 span,
7618 } => Ok(Expr::IsNull {
7619 operand: Box::new(self.resolve_expr_subqueries(*operand, outer_scopes, frame)?),
7620 negated,
7621 span,
7622 }),
7623 Expr::InList {
7624 target,
7625 values,
7626 negated,
7627 span,
7628 } => {
7629 let target =
7630 Box::new(self.resolve_expr_subqueries(*target, outer_scopes, frame)?);
7631 let mut resolved = Vec::new();
7632 for value in values {
7633 if let Expr::Subquery { query, .. } = value {
7634 resolved.extend(
7635 self.execute_expr_subquery_values(query, outer_scopes, frame)?
7636 .into_iter()
7637 .map(Expr::lit),
7638 );
7639 } else {
7640 resolved.push(self.resolve_expr_subqueries(value, outer_scopes, frame)?);
7641 }
7642 }
7643 Ok(Expr::InList {
7644 target,
7645 values: resolved,
7646 negated,
7647 span,
7648 })
7649 }
7650 Expr::Between {
7651 target,
7652 low,
7653 high,
7654 negated,
7655 span,
7656 } => Ok(Expr::Between {
7657 target: Box::new(self.resolve_expr_subqueries(*target, outer_scopes, frame)?),
7658 low: Box::new(self.resolve_expr_subqueries(*low, outer_scopes, frame)?),
7659 high: Box::new(self.resolve_expr_subqueries(*high, outer_scopes, frame)?),
7660 negated,
7661 span,
7662 }),
7663 other => Ok(other),
7664 }
7665 }
7666
7667 fn execute_expr_subquery_values(
7668 &self,
7669 subquery: crate::storage::query::ast::ExprSubquery,
7670 outer_scopes: &[String],
7671 frame: &dyn super::statement_frame::ReadFrame,
7672 ) -> RedDBResult<Vec<Value>> {
7673 let query = *subquery.query;
7674 if query_references_outer_scope(&query, outer_scopes) {
7675 return Err(RedDBError::Query(
7676 "NOT_YET_SUPPORTED: correlated subqueries are not supported yet; track follow-up issue #470-correlated-subqueries".to_string(),
7677 ));
7678 }
7679 let query = self.rewrite_view_refs(query);
7680 let query = self.resolve_select_expr_subqueries(query, frame)?;
7681 let query = self.authorize_relational_select_expr(query, frame)?;
7682 let result = match query {
7683 QueryExpr::Table(table) => {
7684 execute_runtime_table_query(&self.inner.db, &table, Some(&self.inner.index_store))?
7685 }
7686 QueryExpr::Join(join) => execute_runtime_join_query(&self.inner.db, &join)?,
7687 other => {
7688 return Err(RedDBError::Query(format!(
7689 "expression subquery must be a SELECT query, got {}",
7690 query_expr_name(&other)
7691 )))
7692 }
7693 };
7694 first_column_values(result)
7695 }
7696
7697 fn dispatch_expr(
7698 &self,
7699 expr: QueryExpr,
7700 query_str: &str,
7701 mode: QueryMode,
7702 ) -> RedDBResult<RuntimeQueryResult> {
7703 let statement = query_expr_name(&expr);
7704 match expr {
7705 QueryExpr::Graph(_) | QueryExpr::Path(_) => {
7706 Err(RedDBError::Query(
7708 "graph queries cannot be used as prepared statements".to_string(),
7709 ))
7710 }
7711 QueryExpr::Table(table) => {
7712 let scope = self.ai_scope();
7713 let table = self.resolve_table_expr_subqueries(
7714 table,
7715 &scope as &dyn super::statement_frame::ReadFrame,
7716 )?;
7717 if let Some(TableSource::Function {
7721 name,
7722 args,
7723 named_args,
7724 }) = table.source.clone()
7725 {
7726 return Ok(RuntimeQueryResult {
7727 query: query_str.to_string(),
7728 mode,
7729 statement,
7730 engine: "runtime-graph-tvf",
7731 result: self.execute_table_function(&name, &args, &named_args)?,
7732 affected_rows: 0,
7733 statement_type: "select",
7734 bookmark: None,
7735 });
7736 }
7737 if let Some(TableSource::InlineGraphFunction {
7741 name,
7742 nodes,
7743 edges,
7744 named_args,
7745 }) = table.source.clone()
7746 {
7747 return Ok(RuntimeQueryResult {
7748 query: query_str.to_string(),
7749 mode,
7750 statement,
7751 engine: "runtime-graph-tvf-inline",
7752 result: self.execute_inline_graph_function(
7753 &name,
7754 &nodes,
7755 &edges,
7756 &named_args,
7757 )?,
7758 affected_rows: 0,
7759 statement_type: "select",
7760 bookmark: None,
7761 });
7762 }
7763 if super::red_schema::is_virtual_table(&table.table) {
7764 return Ok(RuntimeQueryResult {
7765 query: query_str.to_string(),
7766 mode,
7767 statement,
7768 engine: "runtime-red-schema",
7769 result: super::red_schema::red_query(
7770 self,
7771 &table.table,
7772 &table,
7773 &scope as &dyn super::statement_frame::ReadFrame,
7774 )?,
7775 affected_rows: 0,
7776 statement_type: "select",
7777 bookmark: None,
7778 });
7779 }
7780 if let Some(view_result) = self.try_resolve_analytics_view(
7782 &table,
7783 &scope as &dyn super::statement_frame::ReadFrame,
7784 )? {
7785 return Ok(RuntimeQueryResult {
7786 query: query_str.to_string(),
7787 mode,
7788 statement,
7789 engine: "runtime-graph-analytics-view",
7790 result: view_result,
7791 affected_rows: 0,
7792 statement_type: "select",
7793 bookmark: None,
7794 });
7795 }
7796 let Some(table_with_rls) = self.authorize_relational_table_select(
7797 table,
7798 &scope as &dyn super::statement_frame::ReadFrame,
7799 )?
7800 else {
7801 return Ok(RuntimeQueryResult {
7802 query: query_str.to_string(),
7803 mode,
7804 statement,
7805 engine: "runtime-table-rls",
7806 result: crate::storage::query::unified::UnifiedResult::empty(),
7807 affected_rows: 0,
7808 statement_type: "select",
7809 bookmark: None,
7810 });
7811 };
7812 Ok(RuntimeQueryResult {
7813 query: query_str.to_string(),
7814 mode,
7815 statement,
7816 engine: "runtime-table",
7817 result: execute_runtime_table_query(
7818 &self.inner.db,
7819 &table_with_rls,
7820 Some(&self.inner.index_store),
7821 )?,
7822 affected_rows: 0,
7823 statement_type: "select",
7824 bookmark: None,
7825 })
7826 }
7827 QueryExpr::Join(join) => {
7828 let scope = self.ai_scope();
7829 let Some(join_with_rls) = self.authorize_relational_join_select(
7830 join,
7831 &scope as &dyn super::statement_frame::ReadFrame,
7832 )?
7833 else {
7834 return Ok(RuntimeQueryResult {
7835 query: query_str.to_string(),
7836 mode,
7837 statement,
7838 engine: "runtime-join-rls",
7839 result: crate::storage::query::unified::UnifiedResult::empty(),
7840 affected_rows: 0,
7841 statement_type: "select",
7842 bookmark: None,
7843 });
7844 };
7845 Ok(RuntimeQueryResult {
7846 query: query_str.to_string(),
7847 mode,
7848 statement,
7849 engine: "runtime-join",
7850 result: execute_runtime_join_query(&self.inner.db, &join_with_rls)?,
7851 affected_rows: 0,
7852 statement_type: "select",
7853 bookmark: None,
7854 })
7855 }
7856 QueryExpr::Vector(vector) => Ok(RuntimeQueryResult {
7857 query: query_str.to_string(),
7858 mode,
7859 statement,
7860 engine: "runtime-vector",
7861 result: execute_runtime_vector_query(&self.inner.db, &vector)?,
7862 affected_rows: 0,
7863 statement_type: "select",
7864 bookmark: None,
7865 }),
7866 QueryExpr::Hybrid(hybrid) => Ok(RuntimeQueryResult {
7867 query: query_str.to_string(),
7868 mode,
7869 statement,
7870 engine: "runtime-hybrid",
7871 result: execute_runtime_hybrid_query(&self.inner.db, &hybrid)?,
7872 affected_rows: 0,
7873 statement_type: "select",
7874 bookmark: None,
7875 }),
7876 QueryExpr::Insert(ref insert) if super::red_schema::is_virtual_table(&insert.table) => {
7877 Err(RedDBError::Query(
7878 super::red_schema::READ_ONLY_ERROR.to_string(),
7879 ))
7880 }
7881 QueryExpr::Update(ref update) if super::red_schema::is_virtual_table(&update.table) => {
7882 Err(RedDBError::Query(
7883 super::red_schema::READ_ONLY_ERROR.to_string(),
7884 ))
7885 }
7886 QueryExpr::Delete(ref delete) if super::red_schema::is_virtual_table(&delete.table) => {
7887 Err(RedDBError::Query(
7888 super::red_schema::READ_ONLY_ERROR.to_string(),
7889 ))
7890 }
7891 QueryExpr::Insert(ref insert) => self
7892 .with_deferred_store_wal_for_dml(self.insert_may_emit_events(insert), || {
7893 self.execute_insert(query_str, insert)
7894 }),
7895 QueryExpr::Update(ref update) => self
7896 .with_deferred_store_wal_for_dml(self.update_may_emit_events(update), || {
7897 self.execute_update(query_str, update)
7898 }),
7899 QueryExpr::Delete(ref delete) => self
7900 .with_deferred_store_wal_for_dml(self.delete_may_emit_events(delete), || {
7901 self.execute_delete(query_str, delete)
7902 }),
7903 QueryExpr::SearchCommand(ref cmd) => self.execute_search_command(query_str, cmd),
7904 QueryExpr::Ask(ref ask) => self.execute_ask(query_str, ask),
7905 _ => Err(RedDBError::Query(format!(
7906 "prepared-statement execution does not support {statement} statements"
7907 ))),
7908 }
7909 }
7910
7911 fn execute_table_function(
7918 &self,
7919 name: &str,
7920 args: &[String],
7921 named_args: &[(String, f64)],
7922 ) -> RedDBResult<crate::storage::query::unified::UnifiedResult> {
7923 if name.eq_ignore_ascii_case("red.diff") {
7924 return self.execute_vcs_diff_tvf(args, named_args);
7925 }
7926 if !is_graph_tvf_name(name) {
7927 return Err(RedDBError::Query(format!("unknown table function: {name}")));
7928 }
7929 if args.len() != 1 {
7931 return Err(RedDBError::Query(format!(
7932 "table function '{name}' takes exactly 1 graph argument, got {}",
7933 args.len()
7934 )));
7935 }
7936
7937 let (nodes, edges) = self.materialize_whole_graph_abstract()?;
7942 self.dispatch_graph_algorithm(name, nodes, edges, named_args)
7943 }
7944
7945 fn execute_vcs_diff_tvf(
7946 &self,
7947 args: &[String],
7948 named_args: &[(String, f64)],
7949 ) -> RedDBResult<crate::storage::query::unified::UnifiedResult> {
7950 use crate::application::vcs::{DiffChange, DiffInput};
7951 use crate::storage::query::unified::{UnifiedRecord, UnifiedResult};
7952 use crate::storage::schema::Value;
7953
7954 if !named_args.is_empty() {
7955 return Err(RedDBError::Query(
7956 "red.diff takes only positional commit arguments".to_string(),
7957 ));
7958 }
7959 if args.len() != 2 {
7960 return Err(RedDBError::Query(format!(
7961 "red.diff takes exactly 2 commit arguments, got {}",
7962 args.len()
7963 )));
7964 }
7965
7966 let diff = self.vcs_diff(DiffInput {
7967 from: args[0].clone(),
7968 to: args[1].clone(),
7969 collection: None,
7970 summary_only: false,
7971 })?;
7972 let mut result = UnifiedResult::with_columns(vec![
7973 "from".into(),
7974 "to".into(),
7975 "collection".into(),
7976 "entity_id".into(),
7977 "change".into(),
7978 "before".into(),
7979 "after".into(),
7980 ]);
7981 for entry in diff.entries {
7982 let (change, before, after) = match entry.change {
7983 DiffChange::Added { after } => ("added", Value::Null, json_runtime_value(after)),
7984 DiffChange::Removed { before } => {
7985 ("removed", json_runtime_value(before), Value::Null)
7986 }
7987 DiffChange::Modified { before, after } => (
7988 "modified",
7989 json_runtime_value(before),
7990 json_runtime_value(after),
7991 ),
7992 };
7993 let mut record = UnifiedRecord::new();
7994 record.set("from", Value::text(diff.from.clone()));
7995 record.set("to", Value::text(diff.to.clone()));
7996 record.set("collection", Value::text(entry.collection));
7997 record.set("entity_id", Value::text(entry.entity_id));
7998 record.set("change", Value::text(change));
7999 record.set("before", before);
8000 record.set("after", after);
8001 result.push(record);
8002 }
8003 Ok(result)
8004 }
8005
8006 fn execute_inline_graph_function(
8016 &self,
8017 name: &str,
8018 nodes_query: &QueryExpr,
8019 edges_query: &QueryExpr,
8020 named_args: &[(String, f64)],
8021 ) -> RedDBResult<crate::storage::query::unified::UnifiedResult> {
8022 if !is_graph_tvf_name(name) {
8023 return Err(RedDBError::Query(format!("unknown table function: {name}")));
8024 }
8025
8026 let node_result = self.execute_query_expr(nodes_query.clone())?.result;
8027 let nodes = inline_node_ids(name, &node_result)?;
8028
8029 let edge_result = self.execute_query_expr(edges_query.clone())?.result;
8030 let edges = inline_edges(name, &edge_result)?;
8031
8032 self.dispatch_graph_algorithm(name, nodes, edges, named_args)
8033 }
8034
8035 fn materialize_whole_graph_abstract(
8038 &self,
8039 ) -> RedDBResult<(
8040 Vec<String>,
8041 Vec<(
8042 String,
8043 String,
8044 crate::storage::engine::graph_algorithms::Weight,
8045 )>,
8046 )> {
8047 use crate::storage::engine::graph_algorithms;
8048
8049 let graph = super::graph_dsl::materialize_graph_with_projection(
8050 self.inner.db.store().as_ref(),
8051 None,
8052 )?;
8053 let nodes: Vec<String> = graph.iter_nodes().map(|n| n.id.clone()).collect();
8054 let edges: Vec<(String, String, graph_algorithms::Weight)> = graph
8055 .iter_all_edges()
8056 .into_iter()
8057 .map(|e| (e.source_id, e.target_id, e.weight))
8058 .collect();
8059 Ok((nodes, edges))
8060 }
8061
8062 fn try_resolve_analytics_view(
8077 &self,
8078 table: &TableQuery,
8079 frame: &dyn super::statement_frame::ReadFrame,
8080 ) -> RedDBResult<Option<crate::storage::query::unified::UnifiedResult>> {
8081 let full = table.table.as_str();
8082 let Some(dot) = full.rfind('.') else {
8083 return Ok(None);
8084 };
8085 if self.inner.db.store().get_collection(full).is_some() {
8087 return Ok(None);
8088 }
8089 let graph_name = &full[..dot];
8090 let output_name = &full[dot + 1..];
8091 let Some(output) = crate::catalog::AnalyticsOutput::from_str(output_name) else {
8092 return Ok(None);
8093 };
8094
8095 let contracts = self.inner.db.collection_contracts();
8096 let Some(contract) = contracts.iter().find(|c| c.name == graph_name) else {
8097 return Ok(None);
8098 };
8099 if contract.declared_model != crate::catalog::CollectionModel::Graph {
8100 return Ok(None);
8101 }
8102 let Some(view) = contract
8103 .analytics_config
8104 .iter()
8105 .find(|view| view.output == output)
8106 else {
8107 return Err(RedDBError::Query(format!(
8110 "analytics output '{output_name}' is not enabled on graph '{graph_name}'; declare it with WITH ANALYTICS (...)"
8111 )));
8112 };
8113
8114 let parent_query = TableQuery::new(graph_name);
8118 if self
8119 .authorize_relational_table_select(parent_query, frame)?
8120 .is_none()
8121 {
8122 return Err(RedDBError::Query(format!(
8123 "permission denied: policy on graph '{graph_name}' denies analytics view '{output_name}'"
8124 )));
8125 }
8126
8127 let (algorithm, named_args) = analytics_view_algorithm(graph_name, view)?;
8128 let (nodes, edges) = self.materialize_whole_graph_abstract()?;
8129 let result = self.dispatch_graph_algorithm(&algorithm, nodes, edges, &named_args)?;
8130 Ok(Some(result))
8131 }
8132
8133 fn dispatch_graph_algorithm(
8140 &self,
8141 name: &str,
8142 nodes: Vec<String>,
8143 edges: Vec<(
8144 String,
8145 String,
8146 crate::storage::engine::graph_algorithms::Weight,
8147 )>,
8148 named_args: &[(String, f64)],
8149 ) -> RedDBResult<crate::storage::query::unified::UnifiedResult> {
8150 use crate::storage::engine::graph_algorithms;
8151 use crate::storage::query::unified::UnifiedResult;
8152 use crate::storage::schema::Value;
8153
8154 if name.eq_ignore_ascii_case("components") {
8155 reject_named_args(name, named_args)?;
8156 let assignment = graph_algorithms::connected_components(&nodes, &edges);
8157 let mut result =
8158 UnifiedResult::with_columns(vec!["node_id".into(), "island_id".into()]);
8159 for (node_id, island_id) in assignment {
8160 let mut record = UnifiedRecord::new();
8161 record.set("node_id", Value::text(node_id));
8162 record.set("island_id", Value::Integer(island_id as i64));
8163 result.push(record);
8164 }
8165 return Ok(result);
8166 }
8167
8168 if name.eq_ignore_ascii_case("louvain") {
8169 let resolution = louvain_resolution(named_args)?;
8174 let assignment = graph_algorithms::louvain(&nodes, &edges, resolution);
8175 let mut result =
8176 UnifiedResult::with_columns(vec!["node_id".into(), "community_id".into()]);
8177 for (node_id, community_id) in assignment {
8178 let mut record = UnifiedRecord::new();
8179 record.set("node_id", Value::text(node_id));
8180 record.set("community_id", Value::Integer(community_id as i64));
8181 result.push(record);
8182 }
8183 return Ok(result);
8184 }
8185
8186 if name.eq_ignore_ascii_case("degree_centrality") {
8187 reject_named_args(name, named_args)?;
8188 let assignment = abstract_degree_centrality(&nodes, &edges);
8189 let mut result = UnifiedResult::with_columns(vec!["node_id".into(), "degree".into()]);
8190 for (node_id, degree) in assignment {
8191 let mut record = UnifiedRecord::new();
8192 record.set("node_id", Value::text(node_id));
8193 record.set("degree", Value::Integer(degree as i64));
8194 result.push(record);
8195 }
8196 return Ok(result);
8197 }
8198
8199 if name.eq_ignore_ascii_case("shortest_path") {
8200 let mut src: Option<String> = None;
8206 let mut dst: Option<String> = None;
8207 let mut max_hops: Option<usize> = None;
8208 let as_node_id = |key: &str, value: f64| -> RedDBResult<String> {
8209 if !value.is_finite() || value < 0.0 || value.fract() != 0.0 {
8210 return Err(RedDBError::Query(format!(
8211 "table function 'shortest_path' argument '{key}' must be a non-negative integer node id, got {value}"
8212 )));
8213 }
8214 Ok((value as i64).to_string())
8215 };
8216 for (key, value) in named_args {
8217 if key.eq_ignore_ascii_case("src") {
8218 src = Some(as_node_id("src", *value)?);
8219 } else if key.eq_ignore_ascii_case("dst") {
8220 dst = Some(as_node_id("dst", *value)?);
8221 } else if key.eq_ignore_ascii_case("max_hops") {
8222 if !value.is_finite() || *value < 0.0 || value.fract() != 0.0 {
8223 return Err(RedDBError::Query(format!(
8224 "table function 'shortest_path' max_hops must be a non-negative integer, got {value}"
8225 )));
8226 }
8227 max_hops = Some(*value as usize);
8228 } else {
8229 return Err(RedDBError::Query(format!(
8230 "table function 'shortest_path' has no named argument '{key}' (expected 'src', 'dst', 'max_hops')"
8231 )));
8232 }
8233 }
8234 let src = src.ok_or_else(|| {
8235 RedDBError::Query(
8236 "table function 'shortest_path' requires named argument 'src'".to_string(),
8237 )
8238 })?;
8239 let dst = dst.ok_or_else(|| {
8240 RedDBError::Query(
8241 "table function 'shortest_path' requires named argument 'dst'".to_string(),
8242 )
8243 })?;
8244
8245 let mut result = UnifiedResult::with_columns(vec![
8252 "hop".into(),
8253 "node_id".into(),
8254 "cumulative_weight".into(),
8255 ]);
8256 if let Some(path) =
8257 graph_algorithms::shortest_path(&nodes, &edges, &src, &dst, max_hops)
8258 {
8259 for (hop, (node_id, cumulative_weight)) in path.into_iter().enumerate() {
8260 let mut record = UnifiedRecord::new();
8261 record.set("hop", Value::Integer(hop as i64));
8262 record.set("node_id", Value::text(node_id));
8263 record.set("cumulative_weight", Value::Float(cumulative_weight));
8264 result.push(record);
8265 }
8266 }
8267 return Ok(result);
8268 }
8269 if name.eq_ignore_ascii_case("betweenness") {
8274 reject_named_args(name, named_args)?;
8275 return Ok(Self::centrality_result(graph_algorithms::betweenness(
8276 &nodes, &edges,
8277 )));
8278 }
8279 if name.eq_ignore_ascii_case("eigenvector") {
8280 let mut max_iterations = 100_usize;
8283 let mut tolerance = 1e-6_f64;
8284 for (key, value) in named_args {
8285 if key.eq_ignore_ascii_case("max_iterations") {
8286 max_iterations = parse_positive_iterations("eigenvector", value)?;
8287 } else if key.eq_ignore_ascii_case("tolerance") {
8288 if !value.is_finite() || *value <= 0.0 {
8289 return Err(RedDBError::Query(format!(
8290 "table function 'eigenvector' tolerance must be > 0, got {value}"
8291 )));
8292 }
8293 tolerance = *value;
8294 } else {
8295 return Err(RedDBError::Query(format!(
8296 "table function 'eigenvector' has no named argument '{key}' (expected 'max_iterations' or 'tolerance')"
8297 )));
8298 }
8299 }
8300 return Ok(Self::centrality_result(graph_algorithms::eigenvector(
8301 &nodes,
8302 &edges,
8303 max_iterations,
8304 tolerance,
8305 )));
8306 }
8307 if name.eq_ignore_ascii_case("pagerank") {
8308 let mut damping = 0.85_f64;
8311 let mut max_iterations = 100_usize;
8312 for (key, value) in named_args {
8313 if key.eq_ignore_ascii_case("damping") {
8314 if !value.is_finite() || *value <= 0.0 || *value >= 1.0 {
8315 return Err(RedDBError::Query(format!(
8316 "table function 'pagerank' damping must be in (0, 1), got {value}"
8317 )));
8318 }
8319 damping = *value;
8320 } else if key.eq_ignore_ascii_case("max_iterations") {
8321 max_iterations = parse_positive_iterations("pagerank", value)?;
8322 } else {
8323 return Err(RedDBError::Query(format!(
8324 "table function 'pagerank' has no named argument '{key}' (expected 'damping' or 'max_iterations')"
8325 )));
8326 }
8327 }
8328 return Ok(Self::centrality_result(graph_algorithms::pagerank(
8329 &nodes,
8330 &edges,
8331 damping,
8332 max_iterations,
8333 )));
8334 }
8335 Err(RedDBError::Query(format!("unknown table function: {name}")))
8336 }
8337
8338 fn execute_components_tvf(
8345 &self,
8346 _collection: &str,
8347 ) -> RedDBResult<crate::storage::query::unified::UnifiedResult> {
8348 use crate::storage::engine::graph_algorithms;
8349 use crate::storage::query::unified::UnifiedResult;
8350 use crate::storage::schema::Value;
8351
8352 let graph = super::graph_dsl::materialize_graph_with_projection(
8358 self.inner.db.store().as_ref(),
8359 None,
8360 )?;
8361
8362 let nodes: Vec<String> = graph.iter_nodes().map(|n| n.id.clone()).collect();
8364 let edges: Vec<(String, String, graph_algorithms::Weight)> = graph
8365 .iter_all_edges()
8366 .into_iter()
8367 .map(|e| (e.source_id, e.target_id, e.weight))
8368 .collect();
8369
8370 let assignment = graph_algorithms::connected_components(&nodes, &edges);
8371
8372 let mut result = UnifiedResult::with_columns(vec!["node_id".into(), "island_id".into()]);
8374 for (node_id, island_id) in assignment {
8375 let mut record = UnifiedRecord::new();
8376 record.set("node_id", Value::text(node_id));
8377 record.set("island_id", Value::Integer(island_id as i64));
8378 result.push(record);
8379 }
8380 Ok(result)
8381 }
8382
8383 fn execute_louvain_tvf(
8393 &self,
8394 _collection: &str,
8395 resolution: f64,
8396 ) -> RedDBResult<crate::storage::query::unified::UnifiedResult> {
8397 use crate::storage::engine::graph_algorithms;
8398 use crate::storage::query::unified::UnifiedResult;
8399 use crate::storage::schema::Value;
8400
8401 let graph = super::graph_dsl::materialize_graph_with_projection(
8402 self.inner.db.store().as_ref(),
8403 None,
8404 )?;
8405
8406 let nodes: Vec<String> = graph.iter_nodes().map(|n| n.id.clone()).collect();
8407 let edges: Vec<(String, String, graph_algorithms::Weight)> = graph
8408 .iter_all_edges()
8409 .into_iter()
8410 .map(|e| (e.source_id, e.target_id, e.weight))
8411 .collect();
8412
8413 let assignment = graph_algorithms::louvain(&nodes, &edges, resolution);
8414
8415 let mut result = UnifiedResult::with_columns(vec!["node_id".into(), "community_id".into()]);
8417 for (node_id, community_id) in assignment {
8418 let mut record = UnifiedRecord::new();
8419 record.set("node_id", Value::text(node_id));
8420 record.set("community_id", Value::Integer(community_id as i64));
8421 result.push(record);
8422 }
8423 Ok(result)
8424 }
8425
8426 fn centrality_result(
8429 rows: Vec<(String, f64)>,
8430 ) -> crate::storage::query::unified::UnifiedResult {
8431 use crate::storage::query::unified::UnifiedResult;
8432 use crate::storage::schema::Value;
8433 let mut result = UnifiedResult::with_columns(vec!["node_id".into(), "score".into()]);
8434 for (node_id, score) in rows {
8435 let mut record = UnifiedRecord::new();
8436 record.set("node_id", Value::text(node_id));
8437 record.set("score", Value::Float(score));
8438 result.push(record);
8439 }
8440 result
8441 }
8442
8443 fn try_fast_entity_lookup(&self, query: &str) -> Option<RedDBResult<RuntimeQueryResult>> {
8446 let q = query.trim();
8449 if !q.starts_with("SELECT") && !q.starts_with("select") {
8450 return None;
8451 }
8452
8453 let where_pos = q
8455 .find("WHERE _entity_id")
8456 .or_else(|| q.find("where _entity_id"))?;
8457 let after_field = &q[where_pos + 16..].trim_start(); let after_eq = after_field.strip_prefix('=')?.trim_start();
8459
8460 let id_str = after_eq.trim();
8462 let entity_id: u64 = id_str.parse().ok()?;
8463
8464 let from_pos = q.find("FROM ").or_else(|| q.find("from "))? + 5;
8466 let table = q[from_pos..where_pos].trim();
8467 if table.is_empty()
8468 || table.contains(' ') && !table.contains(" AS ") && !table.contains(" as ")
8469 {
8470 return None; }
8472 let table_name = table.split_whitespace().next()?;
8473
8474 let store = self.inner.db.store();
8480 let entity = store
8481 .get(
8482 table_name,
8483 crate::storage::unified::EntityId::new(entity_id),
8484 )
8485 .filter(entity_visible_under_current_snapshot)
8486 .filter(|entity| {
8487 self.inner
8488 .db
8489 .replica_allows_entity_at_read(table_name, entity)
8490 });
8491
8492 let count = if entity.is_some() { 1u64 } else { 0 };
8493
8494 let records: Vec<crate::storage::query::unified::UnifiedRecord> = entity
8500 .as_ref()
8501 .and_then(|e| runtime_table_record_from_entity(e.clone()))
8502 .into_iter()
8503 .collect();
8504
8505 let json = match entity {
8506 Some(ref e) => execute_runtime_serialize_single_entity(e),
8507 None => r#"{"columns":[],"record_count":0,"selection":{"scope":"any"},"records":[]}"#
8508 .to_string(),
8509 };
8510
8511 Some(Ok(RuntimeQueryResult {
8512 query: query.to_string(),
8513 mode: crate::storage::query::modes::QueryMode::Sql,
8514 statement: "select",
8515 engine: "fast-entity-lookup",
8516 result: crate::storage::query::unified::UnifiedResult {
8517 columns: Vec::new(),
8518 records,
8519 stats: crate::storage::query::unified::QueryStats {
8520 rows_scanned: count,
8521 ..Default::default()
8522 },
8523 pre_serialized_json: Some(json),
8524 },
8525 affected_rows: 0,
8526 statement_type: "select",
8527 bookmark: None,
8528 }))
8529 }
8530
8531 pub(crate) fn invalidate_plan_cache(&self) {
8532 self.inner.query_cache.write().clear();
8533 self.inner
8534 .ddl_epoch
8535 .fetch_add(1, std::sync::atomic::Ordering::Release);
8536 }
8537
8538 pub fn ddl_epoch(&self) -> u64 {
8542 self.inner
8543 .ddl_epoch
8544 .load(std::sync::atomic::Ordering::Acquire)
8545 }
8546
8547 pub(crate) fn clear_table_planner_stats(&self, table: &str) {
8548 let store = self.inner.db.store();
8549 crate::storage::query::planner::stats_catalog::clear_table_stats(store.as_ref(), table);
8550 self.invalidate_plan_cache();
8551 }
8552
8553 pub(crate) fn rehydrate_tenant_tables(&self) {
8562 let store = self.inner.db.store();
8563 let Some(manager) = store.get_collection("red_config") else {
8564 return;
8565 };
8566 for entity in manager.query_all(|_| true) {
8571 let crate::storage::unified::entity::EntityData::Row(row) = &entity.data else {
8572 continue;
8573 };
8574 let Some(named) = &row.named else { continue };
8575 let Some(crate::storage::schema::Value::Text(key)) = named.get("key") else {
8576 continue;
8577 };
8578 let Some(rest) = key.strip_prefix("tenant_tables.") else {
8580 continue;
8581 };
8582 let Some((table, suffix)) = rest.rsplit_once('.') else {
8583 crate::telemetry::operator_event::OperatorEvent::SchemaCorruption {
8589 collection: "red_config".to_string(),
8590 detail: format!("malformed tenant_tables key: {key}"),
8591 }
8592 .emit_global();
8593 continue;
8594 };
8595 if suffix != "column" {
8596 crate::telemetry::operator_event::OperatorEvent::SchemaCorruption {
8597 collection: "red_config".to_string(),
8598 detail: format!("unexpected tenant_tables suffix: {key}"),
8599 }
8600 .emit_global();
8601 continue;
8602 }
8603 match named.get("value") {
8604 Some(crate::storage::schema::Value::Text(column)) => {
8605 self.register_tenant_table(table, column);
8606 }
8607 Some(crate::storage::schema::Value::Null) | None => {
8609 self.unregister_tenant_table(table);
8610 }
8611 _ => {}
8612 }
8613 }
8614 }
8615
8616 pub(crate) fn rehydrate_materialized_view_descriptors(&self) {
8628 let store = self.inner.db.store();
8629 let descriptors = crate::runtime::continuous_materialized_view::load_all(store.as_ref());
8630 for descriptor in descriptors {
8631 let parsed = match crate::storage::query::parser::parse(&descriptor.source_sql) {
8632 Ok(qc) => qc,
8633 Err(err) => {
8634 crate::telemetry::operator_event::OperatorEvent::SchemaCorruption {
8635 collection:
8636 crate::runtime::continuous_materialized_view::CATALOG_COLLECTION
8637 .to_string(),
8638 detail: format!(
8639 "failed to re-parse materialized-view source for {}: {err}",
8640 descriptor.name
8641 ),
8642 }
8643 .emit_global();
8644 continue;
8645 }
8646 };
8647 let crate::storage::query::ast::QueryExpr::CreateView(create) = parsed.query else {
8648 crate::telemetry::operator_event::OperatorEvent::SchemaCorruption {
8649 collection: crate::runtime::continuous_materialized_view::CATALOG_COLLECTION
8650 .to_string(),
8651 detail: format!(
8652 "materialized-view source for {} did not re-parse as CREATE VIEW",
8653 descriptor.name
8654 ),
8655 }
8656 .emit_global();
8657 continue;
8658 };
8659 let view_name = create.name.clone();
8661 self.inner
8662 .views
8663 .write()
8664 .insert(view_name.clone(), Arc::new(create));
8665 use crate::storage::cache::result::{MaterializedViewDef, RefreshPolicy};
8667 let refresh = match descriptor.refresh_every_ms {
8668 Some(ms) => RefreshPolicy::Periodic(std::time::Duration::from_millis(ms)),
8669 None => RefreshPolicy::Manual,
8670 };
8671 let def = MaterializedViewDef {
8672 name: view_name.clone(),
8673 query: format!("<parsed view {}>", view_name),
8674 dependencies: descriptor.source_collections.clone(),
8675 refresh,
8676 retention_duration_ms: descriptor.retention_duration_ms,
8677 };
8678 self.inner.materialized_views.write().register(def);
8679 if let Err(err) = self.ensure_materialized_view_backing(&view_name) {
8680 crate::telemetry::operator_event::OperatorEvent::SchemaCorruption {
8681 collection: crate::runtime::continuous_materialized_view::CATALOG_COLLECTION
8682 .to_string(),
8683 detail: format!(
8684 "failed to rehydrate backing collection for materialized view {view_name}: {err}"
8685 ),
8686 }
8687 .emit_global();
8688 }
8689 }
8690 self.invalidate_plan_cache();
8693 }
8694
8695 pub(crate) fn rehydrate_declared_column_schemas(&self) {
8696 let store = self.inner.db.store();
8697 for contract in self.inner.db.collection_contracts() {
8698 let columns: Vec<String> = contract
8699 .declared_columns
8700 .iter()
8701 .map(|column| column.name.clone())
8702 .collect();
8703 let Some(manager) = store.get_collection(&contract.name) else {
8704 continue;
8705 };
8706 manager.set_column_schema_if_empty(columns);
8707 }
8708 }
8709
8710 pub fn register_tenant_table(&self, table: &str, column: &str) {
8715 use crate::storage::query::ast::{
8716 CompareOp, CreatePolicyQuery, Expr, FieldRef, Filter, Span,
8717 };
8718 self.inner
8719 .tenant_tables
8720 .write()
8721 .insert(table.to_string(), column.to_string());
8722
8723 let lhs = Expr::Column {
8729 field: FieldRef::TableColumn {
8730 table: table.to_string(),
8731 column: column.to_string(),
8732 },
8733 span: Span::synthetic(),
8734 };
8735 let rhs = Expr::FunctionCall {
8736 name: "CURRENT_TENANT".to_string(),
8737 args: Vec::new(),
8738 span: Span::synthetic(),
8739 };
8740 let policy_filter = Filter::CompareExpr {
8741 lhs,
8742 op: CompareOp::Eq,
8743 rhs,
8744 };
8745
8746 let policy = CreatePolicyQuery {
8747 name: "__tenant_iso".to_string(),
8748 table: table.to_string(),
8749 action: None, role: None, using: Box::new(policy_filter),
8752 target_kind: crate::storage::query::ast::PolicyTargetKind::Table,
8759 };
8760
8761 self.inner.rls_policies.write().insert(
8763 (table.to_string(), "__tenant_iso".to_string()),
8764 Arc::new(policy),
8765 );
8766 self.inner
8767 .rls_enabled_tables
8768 .write()
8769 .insert(table.to_string());
8770
8771 self.ensure_tenant_index(table, column);
8777 }
8778
8779 fn ensure_tenant_index(&self, table: &str, column: &str) {
8787 if column.contains('.') {
8788 return;
8789 }
8790 let index_name = format!("__tenant_idx_{table}");
8791 let registry = self.inner.index_store.list_indices(table);
8792 if registry.iter().any(|idx| idx.name == index_name) {
8793 return;
8794 }
8795 if registry
8796 .iter()
8797 .any(|idx| idx.columns.first().map(|c| c.as_str()) == Some(column))
8798 {
8799 return;
8800 }
8801
8802 let store = self.inner.db.store();
8803 let Some(manager) = store.get_collection(table) else {
8804 return;
8805 };
8806 let entities = manager.query_all(|_| true);
8807 let entity_fields: Vec<(
8808 crate::storage::unified::EntityId,
8809 Vec<(String, crate::storage::schema::Value)>,
8810 )> = entities
8811 .iter()
8812 .map(|e| {
8813 let fields = match &e.data {
8814 crate::storage::EntityData::Row(row) => {
8815 if let Some(ref named) = row.named {
8816 named.iter().map(|(k, v)| (k.clone(), v.clone())).collect()
8817 } else if let Some(ref schema) = row.schema {
8818 schema
8819 .iter()
8820 .zip(row.columns.iter())
8821 .map(|(k, v)| (k.clone(), v.clone()))
8822 .collect()
8823 } else {
8824 Vec::new()
8825 }
8826 }
8827 crate::storage::EntityData::Node(node) => node
8828 .properties
8829 .iter()
8830 .map(|(k, v)| (k.clone(), v.clone()))
8831 .collect(),
8832 _ => Vec::new(),
8833 };
8834 (e.id, fields)
8835 })
8836 .collect();
8837
8838 let columns = vec![column.to_string()];
8839 if self
8840 .inner
8841 .index_store
8842 .create_index(
8843 &index_name,
8844 table,
8845 &columns,
8846 super::index_store::IndexMethodKind::Hash,
8847 false,
8848 &entity_fields,
8849 )
8850 .is_err()
8851 {
8852 return;
8853 }
8854 self.inner
8855 .index_store
8856 .register(super::index_store::RegisteredIndex {
8857 name: index_name,
8858 collection: table.to_string(),
8859 columns,
8860 method: super::index_store::IndexMethodKind::Hash,
8861 unique: false,
8862 });
8863 self.invalidate_plan_cache();
8864 }
8865
8866 fn drop_tenant_index(&self, table: &str) {
8869 let index_name = format!("__tenant_idx_{table}");
8870 self.inner.index_store.drop_index(&index_name, table);
8871 }
8872
8873 pub fn tenant_column(&self, table: &str) -> Option<String> {
8877 self.inner.tenant_tables.read().get(table).cloned()
8878 }
8879
8880 pub fn unregister_tenant_table(&self, table: &str) {
8884 self.inner.tenant_tables.write().remove(table);
8885 self.inner
8886 .rls_policies
8887 .write()
8888 .remove(&(table.to_string(), "__tenant_iso".to_string()));
8889 self.drop_tenant_index(table);
8890 let has_other_policies = self
8892 .inner
8893 .rls_policies
8894 .read()
8895 .keys()
8896 .any(|(t, _)| t == table);
8897 if !has_other_policies {
8898 self.inner.rls_enabled_tables.write().remove(table);
8899 }
8900 }
8901
8902 pub(crate) fn record_pending_tombstone(
8908 &self,
8909 conn_id: u64,
8910 collection: &str,
8911 id: crate::storage::unified::entity::EntityId,
8912 stamper_xid: crate::storage::transaction::snapshot::Xid,
8913 previous_xmax: crate::storage::transaction::snapshot::Xid,
8914 ) {
8915 self.inner
8916 .pending_tombstones
8917 .write()
8918 .entry(conn_id)
8919 .or_default()
8920 .push((collection.to_string(), id, stamper_xid, previous_xmax));
8921 }
8922
8923 pub(crate) fn record_pending_versioned_update(
8924 &self,
8925 conn_id: u64,
8926 collection: &str,
8927 old_id: crate::storage::unified::entity::EntityId,
8928 new_id: crate::storage::unified::entity::EntityId,
8929 stamper_xid: crate::storage::transaction::snapshot::Xid,
8930 previous_xmax: crate::storage::transaction::snapshot::Xid,
8931 ) {
8932 self.inner
8933 .pending_versioned_updates
8934 .write()
8935 .entry(conn_id)
8936 .or_default()
8937 .push((
8938 collection.to_string(),
8939 old_id,
8940 new_id,
8941 stamper_xid,
8942 previous_xmax,
8943 ));
8944 }
8945
8946 fn with_deferred_store_wal_if_transaction<T>(
8947 &self,
8948 f: impl FnOnce() -> RedDBResult<T>,
8949 ) -> RedDBResult<T> {
8950 let conn_id = current_connection_id();
8951 if !self.inner.tx_contexts.read().contains_key(&conn_id) {
8952 return f();
8953 }
8954
8955 crate::storage::UnifiedStore::begin_deferred_store_wal_capture();
8956 let result = f();
8957 let captured = crate::storage::UnifiedStore::take_deferred_store_wal_capture();
8958 match result {
8959 Ok(value) => {
8960 self.record_pending_store_wal_actions(conn_id, captured);
8961 Ok(value)
8962 }
8963 Err(err) => Err(err),
8964 }
8965 }
8966
8967 fn with_deferred_store_wal_for_dml<T>(
8968 &self,
8969 capture_autocommit_events: bool,
8970 f: impl FnOnce() -> RedDBResult<T>,
8971 ) -> RedDBResult<T> {
8972 let conn_id = current_connection_id();
8973 if self.inner.tx_contexts.read().contains_key(&conn_id) {
8974 return self.with_deferred_store_wal_if_transaction(f);
8975 }
8976 if !capture_autocommit_events {
8977 return f();
8978 }
8979
8980 crate::storage::UnifiedStore::begin_deferred_store_wal_capture();
8981 let result = f();
8982 let captured = crate::storage::UnifiedStore::take_deferred_store_wal_capture();
8983 self.inner
8984 .db
8985 .store()
8986 .append_deferred_store_wal_actions(captured)
8987 .map_err(|err| RedDBError::Internal(err.to_string()))?;
8988 result
8989 }
8990
8991 fn insert_may_emit_events(&self, query: &InsertQuery) -> bool {
8992 !query.suppress_events
8993 && self.collection_has_event_subscriptions_for_operation(
8994 &query.table,
8995 crate::catalog::SubscriptionOperation::Insert,
8996 )
8997 }
8998
8999 fn update_may_emit_events(&self, query: &UpdateQuery) -> bool {
9000 !query.suppress_events
9001 && self.collection_has_event_subscriptions_for_operation(
9002 &query.table,
9003 crate::catalog::SubscriptionOperation::Update,
9004 )
9005 }
9006
9007 fn delete_may_emit_events(&self, query: &DeleteQuery) -> bool {
9008 !query.suppress_events
9009 && self.collection_has_event_subscriptions_for_operation(
9010 &query.table,
9011 crate::catalog::SubscriptionOperation::Delete,
9012 )
9013 }
9014
9015 fn collection_has_event_subscriptions_for_operation(
9016 &self,
9017 collection: &str,
9018 operation: crate::catalog::SubscriptionOperation,
9019 ) -> bool {
9020 let Some(contract) = self.db().collection_contract_arc(collection) else {
9021 return false;
9022 };
9023 contract.subscriptions.iter().any(|subscription| {
9024 subscription.enabled
9025 && (subscription.ops_filter.is_empty()
9026 || subscription.ops_filter.contains(&operation))
9027 })
9028 }
9029
9030 fn record_pending_store_wal_actions(
9031 &self,
9032 conn_id: u64,
9033 actions: crate::storage::unified::DeferredStoreWalActions,
9034 ) {
9035 if actions.is_empty() {
9036 return;
9037 }
9038 let mut guard = self.inner.pending_store_wal_actions.write();
9039 guard.entry(conn_id).or_default().extend(actions);
9040 }
9041
9042 fn flush_pending_store_wal_actions(&self, conn_id: u64) -> RedDBResult<()> {
9043 let Some(actions) = self
9044 .inner
9045 .pending_store_wal_actions
9046 .write()
9047 .remove(&conn_id)
9048 else {
9049 return Ok(());
9050 };
9051 self.inner
9052 .db
9053 .store()
9054 .append_deferred_store_wal_actions(actions)
9055 .map_err(|err| RedDBError::Internal(err.to_string()))
9056 }
9057
9058 fn discard_pending_store_wal_actions(&self, conn_id: u64) {
9059 self.inner
9060 .pending_store_wal_actions
9061 .write()
9062 .remove(&conn_id);
9063 }
9064
9065 fn xid_conflicts_with_snapshot(
9066 &self,
9067 xid: crate::storage::transaction::snapshot::Xid,
9068 snapshot: &crate::storage::transaction::snapshot::Snapshot,
9069 own_xids: &std::collections::HashSet<crate::storage::transaction::snapshot::Xid>,
9070 ) -> bool {
9071 xid != 0
9072 && !own_xids.contains(&xid)
9073 && !self.inner.snapshot_manager.is_aborted(xid)
9074 && !self.inner.snapshot_manager.is_active(xid)
9075 && (xid > snapshot.xid || snapshot.in_progress.contains(&xid))
9076 }
9077
9078 fn conflict_error(
9079 collection: &str,
9080 logical_id: crate::storage::unified::entity::EntityId,
9081 xid: crate::storage::transaction::snapshot::Xid,
9082 ) -> RedDBError {
9083 RedDBError::Query(format!(
9084 "serialization conflict: table row {collection}/{} was modified by concurrent transaction {xid}",
9085 logical_id.raw()
9086 ))
9087 }
9088
9089 fn check_logical_row_conflict(
9090 &self,
9091 collection: &str,
9092 logical_id: crate::storage::unified::entity::EntityId,
9093 excluded_ids: &[crate::storage::unified::entity::EntityId],
9094 snapshot: &crate::storage::transaction::snapshot::Snapshot,
9095 own_xids: &std::collections::HashSet<crate::storage::transaction::snapshot::Xid>,
9096 ) -> RedDBResult<()> {
9097 let store = self.inner.db.store();
9098 let Some(manager) = store.get_collection(collection) else {
9099 return Ok(());
9100 };
9101
9102 for candidate in manager.query_all(|_| true) {
9103 if excluded_ids.contains(&candidate.id) || candidate.logical_id() != logical_id {
9104 continue;
9105 }
9106 if self.xid_conflicts_with_snapshot(candidate.xmin, snapshot, own_xids) {
9107 return Err(Self::conflict_error(collection, logical_id, candidate.xmin));
9108 }
9109 if self.xid_conflicts_with_snapshot(candidate.xmax, snapshot, own_xids) {
9110 return Err(Self::conflict_error(collection, logical_id, candidate.xmax));
9111 }
9112 }
9113 Ok(())
9114 }
9115
9116 pub(crate) fn check_table_row_write_conflicts(
9117 &self,
9118 conn_id: u64,
9119 snapshot: &crate::storage::transaction::snapshot::Snapshot,
9120 own_xids: &std::collections::HashSet<crate::storage::transaction::snapshot::Xid>,
9121 ) -> RedDBResult<()> {
9122 let versioned_updates = self
9123 .inner
9124 .pending_versioned_updates
9125 .read()
9126 .get(&conn_id)
9127 .cloned()
9128 .unwrap_or_default();
9129 let tombstones = self
9130 .inner
9131 .pending_tombstones
9132 .read()
9133 .get(&conn_id)
9134 .cloned()
9135 .unwrap_or_default();
9136
9137 let store = self.inner.db.store();
9138 for (collection, old_id, new_id, xid, previous_xmax) in versioned_updates {
9139 let Some(manager) = store.get_collection(&collection) else {
9140 continue;
9141 };
9142 let Some(old) = manager.get(old_id) else {
9143 continue;
9144 };
9145 let logical_id = old.logical_id();
9146 if self.xid_conflicts_with_snapshot(previous_xmax, snapshot, own_xids) {
9147 return Err(Self::conflict_error(&collection, logical_id, previous_xmax));
9148 }
9149 if old.xmax != xid && self.xid_conflicts_with_snapshot(old.xmax, snapshot, own_xids) {
9150 return Err(Self::conflict_error(&collection, logical_id, old.xmax));
9151 }
9152 self.check_logical_row_conflict(
9153 &collection,
9154 logical_id,
9155 &[old_id, new_id],
9156 snapshot,
9157 own_xids,
9158 )?;
9159 }
9160
9161 for (collection, id, xid, previous_xmax) in tombstones {
9162 let Some(manager) = store.get_collection(&collection) else {
9163 continue;
9164 };
9165 let Some(entity) = manager.get(id) else {
9166 continue;
9167 };
9168 let logical_id = entity.logical_id();
9169 if self.xid_conflicts_with_snapshot(previous_xmax, snapshot, own_xids) {
9170 return Err(Self::conflict_error(&collection, logical_id, previous_xmax));
9171 }
9172 if entity.xmax != xid
9173 && self.xid_conflicts_with_snapshot(entity.xmax, snapshot, own_xids)
9174 {
9175 return Err(Self::conflict_error(&collection, logical_id, entity.xmax));
9176 }
9177 self.check_logical_row_conflict(&collection, logical_id, &[id], snapshot, own_xids)?;
9178 }
9179
9180 Ok(())
9181 }
9182
9183 pub(crate) fn restore_pending_write_stamps(&self, conn_id: u64) {
9184 let versioned_updates = self
9185 .inner
9186 .pending_versioned_updates
9187 .read()
9188 .get(&conn_id)
9189 .cloned()
9190 .unwrap_or_default();
9191 let tombstones = self
9192 .inner
9193 .pending_tombstones
9194 .read()
9195 .get(&conn_id)
9196 .cloned()
9197 .unwrap_or_default();
9198
9199 let store = self.inner.db.store();
9200 for (collection, old_id, _new_id, xid, _previous_xmax) in versioned_updates {
9201 if let Some(manager) = store.get_collection(&collection) {
9202 if let Some(mut entity) = manager.get(old_id) {
9203 entity.set_xmax(xid);
9204 let _ = manager.update(entity);
9205 }
9206 }
9207 }
9208 for (collection, id, xid, _previous_xmax) in tombstones {
9209 if let Some(manager) = store.get_collection(&collection) {
9210 if let Some(mut entity) = manager.get(id) {
9211 entity.set_xmax(xid);
9212 let _ = manager.update(entity);
9213 }
9214 }
9215 }
9216 }
9217
9218 pub(crate) fn finalize_pending_versioned_updates(&self, conn_id: u64) {
9219 self.inner
9220 .pending_versioned_updates
9221 .write()
9222 .remove(&conn_id);
9223 }
9224
9225 pub(crate) fn revive_pending_versioned_updates(&self, conn_id: u64) {
9226 let Some(pending) = self
9227 .inner
9228 .pending_versioned_updates
9229 .write()
9230 .remove(&conn_id)
9231 else {
9232 return;
9233 };
9234
9235 let store = self.inner.db.store();
9236 for (collection, old_id, new_id, xid, previous_xmax) in pending {
9237 if let Some(manager) = store.get_collection(&collection) {
9238 if let Some(mut old) = manager.get(old_id) {
9239 if old.xmax == xid {
9240 old.set_xmax(previous_xmax);
9241 let _ = manager.update(old);
9242 }
9243 }
9244 }
9245 let _ = store.delete_batch(&collection, &[new_id]);
9246 }
9247 }
9248
9249 pub(crate) fn revive_versioned_updates_since(&self, conn_id: u64, stamper_xid: u64) -> usize {
9250 let mut guard = self.inner.pending_versioned_updates.write();
9251 let Some(pending) = guard.get_mut(&conn_id) else {
9252 return 0;
9253 };
9254
9255 let store = self.inner.db.store();
9256 let mut reverted = 0usize;
9257 pending.retain(|(collection, old_id, new_id, xid, previous_xmax)| {
9258 if *xid < stamper_xid {
9259 return true;
9260 }
9261 if let Some(manager) = store.get_collection(collection) {
9262 if let Some(mut old) = manager.get(*old_id) {
9263 if old.xmax == *xid {
9264 old.set_xmax(*previous_xmax);
9265 let _ = manager.update(old);
9266 }
9267 }
9268 }
9269 let _ = store.delete_batch(collection, &[*new_id]);
9270 reverted += 1;
9271 false
9272 });
9273 if pending.is_empty() {
9274 guard.remove(&conn_id);
9275 }
9276 reverted
9277 }
9278
9279 pub(crate) fn finalize_pending_tombstones(&self, conn_id: u64) {
9284 let Some(pending) = self.inner.pending_tombstones.write().remove(&conn_id) else {
9285 return;
9286 };
9287 if pending.is_empty() {
9288 return;
9289 }
9290
9291 let store = self.inner.db.store();
9292 for (collection, id, _xid, _previous_xmax) in pending {
9293 store.context_index().remove_entity(id);
9294 self.cdc_emit(
9295 crate::replication::cdc::ChangeOperation::Delete,
9296 &collection,
9297 id.raw(),
9298 "entity",
9299 );
9300 }
9301 }
9302
9303 pub(crate) fn revive_pending_tombstones(&self, conn_id: u64) {
9310 let Some(pending) = self.inner.pending_tombstones.write().remove(&conn_id) else {
9311 return;
9312 };
9313
9314 let store = self.inner.db.store();
9315 for (collection, id, xid, previous_xmax) in pending {
9316 let Some(manager) = store.get_collection(&collection) else {
9317 continue;
9318 };
9319 if let Some(mut entity) = manager.get(id) {
9320 if entity.xmax == xid {
9321 entity.set_xmax(previous_xmax);
9322 let _ = manager.update(entity);
9323 }
9324 }
9325 }
9326 }
9327
9328 pub fn queue_wait_registry(
9330 &self,
9331 ) -> std::sync::Arc<crate::runtime::queue_wait_registry::QueueWaitRegistry> {
9332 self.inner.queue_wait_registry.clone()
9333 }
9334
9335 pub(crate) fn record_queue_wake(&self, scope: &str, queue: &str) {
9340 if self.current_xid().is_some() {
9341 let conn_id = current_connection_id();
9342 self.inner
9343 .pending_queue_wakes
9344 .write()
9345 .entry(conn_id)
9346 .or_default()
9347 .push((scope.to_string(), queue.to_string()));
9348 return;
9349 }
9350 self.inner.queue_wait_registry.notify(scope, queue);
9351 }
9352
9353 pub(crate) fn finalize_pending_queue_wakes(&self, conn_id: u64) {
9354 let Some(pending) = self.inner.pending_queue_wakes.write().remove(&conn_id) else {
9355 return;
9356 };
9357 for (scope, queue) in pending {
9358 self.inner.queue_wait_registry.notify(&scope, &queue);
9359 }
9360 }
9361
9362 pub(crate) fn discard_pending_queue_wakes(&self, conn_id: u64) {
9363 self.inner.pending_queue_wakes.write().remove(&conn_id);
9364 }
9365
9366 pub(crate) fn release_pending_claim_locks(&self, conn_id: u64) {
9367 self.inner
9368 .pending_claim_locks
9369 .write()
9370 .retain(|_, owner| *owner != conn_id);
9371 }
9372
9373 pub(crate) fn finalize_pending_kv_watch_events(&self, conn_id: u64) {
9374 let Some(pending) = self.inner.pending_kv_watch_events.write().remove(&conn_id) else {
9375 return;
9376 };
9377 for event in pending {
9378 self.cdc_emit_kv(
9379 event.op,
9380 &event.collection,
9381 &event.key,
9382 0,
9383 event.before,
9384 event.after,
9385 );
9386 }
9387 }
9388
9389 pub(crate) fn discard_pending_kv_watch_events(&self, conn_id: u64) {
9390 self.inner.pending_kv_watch_events.write().remove(&conn_id);
9391 }
9392
9393 fn materialize_graph_with_rls(
9402 &self,
9403 ) -> RedDBResult<(
9404 crate::storage::engine::GraphStore,
9405 std::collections::HashMap<
9406 String,
9407 std::collections::HashMap<String, crate::storage::schema::Value>,
9408 >,
9409 crate::storage::query::unified::EdgeProperties,
9410 )> {
9411 use crate::storage::engine::GraphStore;
9412 use crate::storage::query::ast::{PolicyAction, PolicyTargetKind};
9413 use crate::storage::unified::entity::{EntityData, EntityKind};
9414 use std::collections::{HashMap, HashSet};
9415
9416 let store = self.inner.db.store();
9417 let snap_ctx = capture_current_snapshot();
9418 let role = current_auth_identity().map(|(_, r)| r.as_str().to_string());
9419
9420 let graph = GraphStore::new();
9421 let mut node_properties: HashMap<String, HashMap<String, crate::storage::schema::Value>> =
9422 HashMap::new();
9423 let mut edge_properties: crate::storage::query::unified::EdgeProperties = HashMap::new();
9424 let mut allowed_nodes: HashSet<String> = HashSet::new();
9425
9426 let mut node_rls: HashMap<String, Option<crate::storage::query::ast::Filter>> =
9430 HashMap::new();
9431 let mut edge_rls: HashMap<String, Option<crate::storage::query::ast::Filter>> =
9432 HashMap::new();
9433
9434 let collections = store.list_collections();
9435
9436 for collection in &collections {
9438 let Some(manager) = store.get_collection(collection) else {
9439 continue;
9440 };
9441 let entities = manager.query_all(|_| true);
9442 for entity in entities {
9443 if !entity_visible_with_context(snap_ctx.as_ref(), &entity) {
9444 continue;
9445 }
9446 let EntityKind::GraphNode(ref node) = entity.kind else {
9447 continue;
9448 };
9449 if !node_passes_rls(self, collection, role.as_deref(), &mut node_rls, &entity) {
9450 continue;
9451 }
9452 let id_str = entity.id.raw().to_string();
9453 graph
9454 .add_node_with_label(
9455 &id_str,
9456 &node.label,
9457 &super::graph_node_label(&node.node_type),
9458 )
9459 .map_err(|err| RedDBError::Query(err.to_string()))?;
9460 allowed_nodes.insert(id_str.clone());
9461 if let EntityData::Node(node_data) = &entity.data {
9462 node_properties.insert(id_str, node_data.properties.clone());
9463 }
9464 }
9465 }
9466
9467 for collection in &collections {
9471 let Some(manager) = store.get_collection(collection) else {
9472 continue;
9473 };
9474 let entities = manager.query_all(|_| true);
9475 for entity in entities {
9476 if !entity_visible_with_context(snap_ctx.as_ref(), &entity) {
9477 continue;
9478 }
9479 let EntityKind::GraphEdge(ref edge) = entity.kind else {
9480 continue;
9481 };
9482 if !allowed_nodes.contains(&edge.from_node)
9483 || !allowed_nodes.contains(&edge.to_node)
9484 {
9485 continue;
9486 }
9487 if !edge_passes_rls(self, collection, role.as_deref(), &mut edge_rls, &entity) {
9488 continue;
9489 }
9490 let weight = match &entity.data {
9491 EntityData::Edge(e) => e.weight,
9492 _ => edge.weight as f32 / 1000.0,
9493 };
9494 let edge_label = super::graph_edge_label(&edge.label);
9495 graph
9496 .add_edge_with_label(&edge.from_node, &edge.to_node, &edge_label, weight)
9497 .map_err(|err| RedDBError::Query(err.to_string()))?;
9498 if let EntityData::Edge(edge_data) = &entity.data {
9499 edge_properties.insert(
9500 (edge.from_node.clone(), edge_label, edge.to_node.clone()),
9501 edge_data.properties.clone(),
9502 );
9503 }
9504 }
9505 }
9506
9507 let _ = (PolicyAction::Select, PolicyTargetKind::Nodes);
9511
9512 Ok((graph, node_properties, edge_properties))
9513 }
9514
9515 pub(crate) fn stamp_xmin_if_in_txn(
9530 &self,
9531 collection: &str,
9532 id: crate::storage::unified::entity::EntityId,
9533 ) {
9534 let Some(xid) = self.current_xid() else {
9535 return;
9536 };
9537 let store = self.inner.db.store();
9538 let Some(manager) = store.get_collection(collection) else {
9539 return;
9540 };
9541 if let Some(mut entity) = manager.get(id) {
9542 entity.set_xmin(xid);
9543 let _ = manager.update(entity);
9544 }
9545 }
9546
9547 pub(crate) fn revive_tombstones_since(&self, conn_id: u64, stamper_xid: u64) -> usize {
9555 let mut guard = self.inner.pending_tombstones.write();
9556 let Some(pending) = guard.get_mut(&conn_id) else {
9557 return 0;
9558 };
9559
9560 let store = self.inner.db.store();
9561 let mut revived = 0usize;
9562 pending.retain(|(collection, id, xid, previous_xmax)| {
9563 if *xid < stamper_xid {
9564 return true;
9566 }
9567 if let Some(manager) = store.get_collection(collection) {
9568 if let Some(mut entity) = manager.get(*id) {
9569 if entity.xmax == *xid {
9570 entity.set_xmax(*previous_xmax);
9571 let _ = manager.update(entity);
9572 revived += 1;
9573 }
9574 }
9575 }
9576 false
9577 });
9578 if pending.is_empty() {
9579 guard.remove(&conn_id);
9580 }
9581 revived
9582 }
9583
9584 pub fn current_snapshot(&self) -> crate::storage::transaction::snapshot::Snapshot {
9593 let conn_id = current_connection_id();
9594 if let Some(ctx) = self.inner.tx_contexts.read().get(&conn_id).cloned() {
9595 return ctx.snapshot;
9596 }
9597 let high_water = self.inner.snapshot_manager.peek_next_xid();
9603 self.inner.snapshot_manager.snapshot(high_water)
9604 }
9605
9606 pub fn current_xid(&self) -> Option<crate::storage::transaction::snapshot::Xid> {
9616 let conn_id = current_connection_id();
9617 self.inner
9618 .tx_contexts
9619 .read()
9620 .get(&conn_id)
9621 .map(|ctx| ctx.writer_xid())
9622 }
9623
9624 pub fn connection_in_transaction(&self, conn_id: u64) -> bool {
9631 self.inner.tx_contexts.read().contains_key(&conn_id)
9632 }
9633
9634 pub fn snapshot_manager(&self) -> Arc<crate::storage::transaction::snapshot::SnapshotManager> {
9637 Arc::clone(&self.inner.snapshot_manager)
9638 }
9639
9640 fn mvcc_vacuum_cutoff_xid(&self) -> crate::storage::transaction::snapshot::Xid {
9641 let manager = &self.inner.snapshot_manager;
9642 let next_xid = manager.peek_next_xid();
9643 let mut cutoff = next_xid;
9644 if let Some(oldest_active) = manager.oldest_active_xid() {
9645 cutoff = cutoff.min(oldest_active);
9646 }
9647 if let Some(oldest_pinned) = manager.oldest_pinned_xid() {
9648 cutoff = cutoff.min(oldest_pinned);
9649 }
9650 let retention_xids = self.config_u64("runtime.mvcc.vacuum_retention_xids", 0);
9651 if retention_xids > 0 {
9652 cutoff = cutoff.min(next_xid.saturating_sub(retention_xids));
9653 }
9654 cutoff
9655 }
9656
9657 fn rebuild_runtime_indexes_for_table(&self, table: &str) -> RedDBResult<()> {
9658 let registered = self.inner.index_store.list_indices(table);
9659 if registered.is_empty() {
9660 return Ok(());
9661 }
9662 let store = self.inner.db.store();
9663 let Some(manager) = store.get_collection(table) else {
9664 return Ok(());
9665 };
9666 let entity_fields = manager
9667 .query_all(|entity| matches!(entity.kind, crate::storage::EntityKind::TableRow { .. }))
9668 .into_iter()
9669 .map(|entity| (entity.id, table_row_index_fields(&entity)))
9670 .collect::<Vec<_>>();
9671
9672 for index in registered {
9673 self.inner.index_store.drop_index(&index.name, table);
9674 self.inner
9675 .index_store
9676 .create_index(
9677 &index.name,
9678 table,
9679 &index.columns,
9680 index.method,
9681 index.unique,
9682 &entity_fields,
9683 )
9684 .map_err(RedDBError::Internal)?;
9685 self.inner.index_store.register(index);
9686 }
9687 self.invalidate_plan_cache();
9688 Ok(())
9689 }
9690
9691 pub(crate) fn persist_runtime_index_descriptor(
9692 &self,
9693 index: super::index_store::RegisteredIndex,
9694 ) -> RedDBResult<()> {
9695 let store = self.inner.db.store();
9696 let _ = store.get_or_create_collection(RUNTIME_INDEX_REGISTRY_COLLECTION);
9697 let entity = crate::storage::UnifiedEntity::new(
9698 crate::storage::EntityId::new(0),
9699 crate::storage::EntityKind::TableRow {
9700 table: std::sync::Arc::from(RUNTIME_INDEX_REGISTRY_COLLECTION),
9701 row_id: 0,
9702 },
9703 crate::storage::EntityData::Row(crate::storage::RowData {
9704 columns: Vec::new(),
9705 named: Some(
9706 [
9707 (
9708 "collection".to_string(),
9709 crate::storage::schema::Value::text(index.collection.clone()),
9710 ),
9711 (
9712 "name".to_string(),
9713 crate::storage::schema::Value::text(index.name.clone()),
9714 ),
9715 (
9716 "columns".to_string(),
9717 crate::storage::schema::Value::text(index.columns.join("\u{1f}")),
9718 ),
9719 (
9720 "method".to_string(),
9721 crate::storage::schema::Value::text(index_method_kind_as_str(
9722 index.method,
9723 )),
9724 ),
9725 (
9726 "resolution".to_string(),
9727 crate::storage::schema::Value::Integer(i64::from(
9728 index_method_kind_resolution(index.method),
9729 )),
9730 ),
9731 (
9732 "unique".to_string(),
9733 crate::storage::schema::Value::Boolean(index.unique),
9734 ),
9735 (
9736 "dropped".to_string(),
9737 crate::storage::schema::Value::Boolean(false),
9738 ),
9739 ]
9740 .into_iter()
9741 .collect(),
9742 ),
9743 schema: None,
9744 }),
9745 );
9746 store
9747 .insert_auto(RUNTIME_INDEX_REGISTRY_COLLECTION, entity)
9748 .map(|_| ())
9749 .map_err(|err| RedDBError::Internal(format!("{err:?}")))
9750 }
9751
9752 pub(crate) fn persist_runtime_index_drop(
9753 &self,
9754 collection: &str,
9755 name: &str,
9756 ) -> RedDBResult<()> {
9757 let store = self.inner.db.store();
9758 let _ = store.get_or_create_collection(RUNTIME_INDEX_REGISTRY_COLLECTION);
9759 let entity = crate::storage::UnifiedEntity::new(
9760 crate::storage::EntityId::new(0),
9761 crate::storage::EntityKind::TableRow {
9762 table: std::sync::Arc::from(RUNTIME_INDEX_REGISTRY_COLLECTION),
9763 row_id: 0,
9764 },
9765 crate::storage::EntityData::Row(crate::storage::RowData {
9766 columns: Vec::new(),
9767 named: Some(
9768 [
9769 (
9770 "collection".to_string(),
9771 crate::storage::schema::Value::text(collection.to_string()),
9772 ),
9773 (
9774 "name".to_string(),
9775 crate::storage::schema::Value::text(name.to_string()),
9776 ),
9777 (
9778 "dropped".to_string(),
9779 crate::storage::schema::Value::Boolean(true),
9780 ),
9781 ]
9782 .into_iter()
9783 .collect(),
9784 ),
9785 schema: None,
9786 }),
9787 );
9788 store
9789 .insert_auto(RUNTIME_INDEX_REGISTRY_COLLECTION, entity)
9790 .map(|_| ())
9791 .map_err(|err| RedDBError::Internal(format!("{err:?}")))
9792 }
9793
9794 fn rehydrate_runtime_index_registry(&self) -> RedDBResult<()> {
9795 let store = self.inner.db.store();
9796 let Some(manager) = store.get_collection(RUNTIME_INDEX_REGISTRY_COLLECTION) else {
9797 return Ok(());
9798 };
9799 let mut rows = manager.query_all(|_| true);
9800 rows.sort_by_key(|entity| entity.id.raw());
9801
9802 let mut latest = std::collections::HashMap::<
9803 (String, String),
9804 Option<super::index_store::RegisteredIndex>,
9805 >::new();
9806 for entity in rows {
9807 let crate::storage::EntityData::Row(row) = &entity.data else {
9808 continue;
9809 };
9810 let Some(named) = &row.named else {
9811 continue;
9812 };
9813 let Some(collection) = named_text(named, "collection") else {
9814 continue;
9815 };
9816 let Some(name) = named_text(named, "name") else {
9817 continue;
9818 };
9819 let dropped = named_bool(named, "dropped").unwrap_or(false);
9820 let key = (collection.clone(), name.clone());
9821 if dropped {
9822 latest.insert(key, None);
9823 continue;
9824 }
9825 let columns = named_text(named, "columns")
9826 .map(|raw| {
9827 raw.split('\u{1f}')
9828 .filter(|part| !part.is_empty())
9829 .map(str::to_string)
9830 .collect::<Vec<_>>()
9831 })
9832 .unwrap_or_default();
9833 let resolution = named_i64(named, "resolution")
9834 .and_then(|v| u8::try_from(v).ok())
9835 .unwrap_or(0);
9836 let Some(method) = named_text(named, "method")
9837 .and_then(|raw| index_method_kind_from_str(&raw, resolution))
9838 else {
9839 continue;
9840 };
9841 latest.insert(
9842 key,
9843 Some(super::index_store::RegisteredIndex {
9844 name,
9845 collection,
9846 columns,
9847 method,
9848 unique: named_bool(named, "unique").unwrap_or(false),
9849 }),
9850 );
9851 }
9852
9853 for index in latest.into_values().flatten() {
9854 let Some(manager) = store.get_collection(&index.collection) else {
9855 continue;
9856 };
9857 let entity_fields = manager
9858 .query_all(|entity| {
9859 matches!(entity.kind, crate::storage::EntityKind::TableRow { .. })
9860 })
9861 .into_iter()
9862 .map(|entity| (entity.id, table_row_index_fields(&entity)))
9863 .collect::<Vec<_>>();
9864 self.inner
9865 .index_store
9866 .create_index(
9867 &index.name,
9868 &index.collection,
9869 &index.columns,
9870 index.method,
9871 index.unique,
9872 &entity_fields,
9873 )
9874 .map_err(RedDBError::Internal)?;
9875 self.inner.index_store.register(index);
9876 }
9877 self.invalidate_plan_cache();
9878 Ok(())
9879 }
9880
9881 pub fn current_txn_own_xids(
9886 &self,
9887 ) -> std::collections::HashSet<crate::storage::transaction::snapshot::Xid> {
9888 let mut set = std::collections::HashSet::new();
9889 if let Some(ctx) = self.inner.tx_contexts.read().get(¤t_connection_id()) {
9890 set.insert(ctx.xid);
9891 for (_, sub) in &ctx.savepoints {
9892 set.insert(*sub);
9893 }
9894 for sub in &ctx.released_sub_xids {
9895 set.insert(*sub);
9896 }
9897 }
9898 set
9899 }
9900
9901 pub fn foreign_tables(&self) -> Arc<crate::storage::fdw::ForeignTableRegistry> {
9908 Arc::clone(&self.inner.foreign_tables)
9909 }
9910
9911 pub fn is_rls_enabled(&self, table: &str) -> bool {
9913 self.inner.rls_enabled_tables.read().contains(table)
9914 }
9915
9916 pub fn matching_rls_policies(
9923 &self,
9924 table: &str,
9925 role: Option<&str>,
9926 action: crate::storage::query::ast::PolicyAction,
9927 ) -> Vec<crate::storage::query::ast::Filter> {
9928 self.matching_rls_policies_for_kind(
9933 table,
9934 role,
9935 action,
9936 crate::storage::query::ast::PolicyTargetKind::Table,
9937 )
9938 }
9939
9940 pub fn matching_rls_policies_for_kind(
9948 &self,
9949 table: &str,
9950 role: Option<&str>,
9951 action: crate::storage::query::ast::PolicyAction,
9952 kind: crate::storage::query::ast::PolicyTargetKind,
9953 ) -> Vec<crate::storage::query::ast::Filter> {
9954 if !self.is_rls_enabled(table) {
9955 return Vec::new();
9956 }
9957 let policies = self.inner.rls_policies.read();
9958 policies
9959 .iter()
9960 .filter_map(|((t, _), p)| {
9961 if t != table {
9962 return None;
9963 }
9964 if p.target_kind != kind
9973 && p.target_kind != crate::storage::query::ast::PolicyTargetKind::Table
9974 {
9975 return None;
9976 }
9977 if let Some(a) = p.action {
9979 if a != action {
9980 return None;
9981 }
9982 }
9983 if let Some(p_role) = p.role.as_deref() {
9985 match role {
9986 Some(r) if r == p_role => {}
9987 _ => return None,
9988 }
9989 }
9990 Some((*p.using).clone())
9991 })
9992 .collect()
9993 }
9994
9995 pub(crate) fn refresh_table_planner_stats(&self, table: &str) {
9996 let store = self.inner.db.store();
9997 if let Some(stats) =
9998 crate::storage::query::planner::stats_catalog::analyze_collection(store.as_ref(), table)
9999 {
10000 crate::storage::query::planner::stats_catalog::persist_table_stats(
10001 store.as_ref(),
10002 &stats,
10003 );
10004 } else {
10005 crate::storage::query::planner::stats_catalog::clear_table_stats(store.as_ref(), table);
10006 }
10007 self.invalidate_plan_cache();
10008 }
10009
10010 pub(crate) fn note_table_write(&self, table: &str) {
10011 let already_dirty = self.inner.planner_dirty_tables.read().contains(table);
10016 if !already_dirty {
10017 self.inner
10018 .planner_dirty_tables
10019 .write()
10020 .insert(table.to_string());
10021 }
10022 self.invalidate_result_cache_for_table(table);
10023 }
10024
10025 fn explain_as_rows(&self, raw_query: &str, inner_sql: &str) -> RedDBResult<RuntimeQueryResult> {
10033 let explain = self.explain_query(inner_sql)?;
10034
10035 let columns = vec![
10036 "op".to_string(),
10037 "source".to_string(),
10038 "est_rows".to_string(),
10039 "est_cost".to_string(),
10040 "depth".to_string(),
10041 ];
10042
10043 let mut records: Vec<crate::storage::query::unified::UnifiedRecord> = Vec::new();
10044
10045 for name in &explain.cte_materializations {
10051 use std::sync::Arc;
10052 let mut rec = crate::storage::query::unified::UnifiedRecord::default();
10053 rec.set_arc(Arc::from("op"), Value::text("CteScan".to_string()));
10054 rec.set_arc(Arc::from("source"), Value::text(name.clone()));
10055 rec.set_arc(Arc::from("est_rows"), Value::Float(0.0));
10056 rec.set_arc(Arc::from("est_cost"), Value::Float(0.0));
10057 rec.set_arc(Arc::from("depth"), Value::Integer(0));
10058 records.push(rec);
10059 }
10060
10061 walk_plan_node(&explain.logical_plan.root, 0, &mut records);
10062
10063 let result = crate::storage::query::unified::UnifiedResult {
10064 columns,
10065 records,
10066 stats: Default::default(),
10067 pre_serialized_json: None,
10068 };
10069
10070 Ok(RuntimeQueryResult {
10071 query: raw_query.to_string(),
10072 mode: explain.mode,
10073 statement: "explain",
10074 engine: "runtime-explain",
10075 result,
10076 affected_rows: 0,
10077 statement_type: "select",
10078 bookmark: None,
10079 })
10080 }
10081
10082 pub(crate) fn check_query_privilege(
10090 &self,
10091 expr: &crate::storage::query::ast::QueryExpr,
10092 ) -> Result<(), String> {
10093 use crate::auth::privileges::{Action, AuthzContext, Resource};
10094 use crate::auth::UserId;
10095 use crate::storage::query::ast::QueryExpr;
10096
10097 let auth_store = match self.inner.auth_store.read().clone() {
10102 Some(s) => s,
10103 None => return Ok(()),
10104 };
10105
10106 let (username, role) = match current_auth_identity() {
10112 Some(p) => p,
10113 None => return Ok(()),
10114 };
10115 let tenant = current_tenant();
10116
10117 let ctx = AuthzContext {
10118 principal: &username,
10119 effective_role: role,
10120 tenant: tenant.as_deref(),
10121 };
10122 let principal_id = UserId::from_parts(tenant.as_deref(), &username);
10123
10124 let (action, resource) = match expr {
10126 QueryExpr::Table(t) => (Action::Select, Resource::table_from_name(&t.table)),
10127 QueryExpr::RankOf(_) | QueryExpr::ApproxRankOf(_) | QueryExpr::RankRange(_) => {
10128 (Action::Select, Resource::Database)
10129 }
10130 QueryExpr::QueueSelect(q) => {
10131 return self.check_queue_op_privilege(
10132 &auth_store,
10133 &principal_id,
10134 role,
10135 tenant.as_deref(),
10136 "queue:peek",
10137 &q.queue,
10138 );
10139 }
10140 QueryExpr::QueueCommand(cmd) => {
10141 use crate::storage::query::ast::QueueCommand;
10142 let (queue, action_verb) = match cmd {
10143 QueueCommand::Push { queue, .. } => (queue.as_str(), "queue:enqueue"),
10144 QueueCommand::Pop { queue, .. }
10145 | QueueCommand::GroupRead { queue, .. }
10146 | QueueCommand::Claim { queue, .. } => (queue.as_str(), "queue:read"),
10147 QueueCommand::Peek { queue, .. }
10148 | QueueCommand::Len { queue }
10149 | QueueCommand::Pending { queue, .. } => (queue.as_str(), "queue:peek"),
10150 QueueCommand::Ack { queue, .. } => (queue.as_str(), "queue:ack"),
10151 QueueCommand::Nack {
10152 queue, delay_ms, ..
10153 } => {
10154 let verb = if delay_ms.is_some() {
10160 "queue:retry"
10161 } else {
10162 "queue:nack"
10163 };
10164 (queue.as_str(), verb)
10165 }
10166 QueueCommand::Purge { queue } => (queue.as_str(), "queue:purge"),
10167 QueueCommand::GroupCreate { queue, .. } => (queue.as_str(), "queue:read"),
10170 QueueCommand::Move { source, .. } => (source.as_str(), "queue:dlq:move"),
10171 };
10172 return self.check_queue_op_privilege(
10173 &auth_store,
10174 &principal_id,
10175 role,
10176 tenant.as_deref(),
10177 action_verb,
10178 queue,
10179 );
10180 }
10181 QueryExpr::Graph(g) => {
10182 self.check_graph_op_privilege(
10185 &auth_store,
10186 &principal_id,
10187 role,
10188 tenant.as_deref(),
10189 "graph:traverse",
10190 )?;
10191 if auth_store.iam_authorization_enabled() {
10192 self.check_graph_property_projection_privilege(
10193 &auth_store,
10194 &principal_id,
10195 role,
10196 tenant.as_deref(),
10197 g,
10198 )?;
10199 return Ok(());
10200 }
10201 return Ok(());
10202 }
10203 QueryExpr::Path(_) => {
10204 return self.check_graph_op_privilege(
10208 &auth_store,
10209 &principal_id,
10210 role,
10211 tenant.as_deref(),
10212 "graph:traverse",
10213 );
10214 }
10215 QueryExpr::GraphCommand(cmd) => {
10216 use crate::storage::query::ast::GraphCommand;
10217 let action_verb = match cmd {
10218 GraphCommand::Properties { .. } => "graph:read",
10220 GraphCommand::Neighborhood { .. }
10222 | GraphCommand::Traverse { .. }
10223 | GraphCommand::ShortestPath { .. } => "graph:traverse",
10224 GraphCommand::Centrality { .. }
10228 | GraphCommand::Community { .. }
10229 | GraphCommand::Components { .. }
10230 | GraphCommand::Cycles { .. }
10231 | GraphCommand::Clustering
10232 | GraphCommand::TopologicalSort => "graph:algorithm:run",
10233 };
10234 return self.check_graph_op_privilege(
10235 &auth_store,
10236 &principal_id,
10237 role,
10238 tenant.as_deref(),
10239 action_verb,
10240 );
10241 }
10242 QueryExpr::Vector(v) => {
10243 if auth_store.iam_authorization_enabled() {
10244 self.check_vector_op_privilege(
10245 &auth_store,
10246 &principal_id,
10247 role,
10248 tenant.as_deref(),
10249 "vector:search",
10250 &v.collection,
10251 )?;
10252 self.check_table_like_column_projection_privilege(
10253 &auth_store,
10254 &principal_id,
10255 role,
10256 tenant.as_deref(),
10257 &v.collection,
10258 &["content".to_string()],
10259 )?;
10260 return Ok(());
10261 }
10262 return Ok(());
10263 }
10264 QueryExpr::SearchCommand(cmd) => {
10265 use crate::storage::query::ast::SearchCommand;
10266 if auth_store.iam_authorization_enabled() {
10267 let collection = match cmd {
10273 SearchCommand::Similar { collection, .. }
10274 | SearchCommand::Hybrid { collection, .. } => Some(collection.as_str()),
10275 _ => None,
10276 };
10277 if let Some(c) = collection {
10278 self.check_vector_op_privilege(
10279 &auth_store,
10280 &principal_id,
10281 role,
10282 tenant.as_deref(),
10283 "vector:search",
10284 c,
10285 )?;
10286 return Ok(());
10287 }
10288 }
10289 return Ok(());
10290 }
10291 QueryExpr::Hybrid(h) => {
10292 if auth_store.iam_authorization_enabled() {
10293 self.check_vector_op_privilege(
10301 &auth_store,
10302 &principal_id,
10303 role,
10304 tenant.as_deref(),
10305 "vector:search",
10306 &h.vector.collection,
10307 )?;
10308 return Ok(());
10309 }
10310 return Ok(());
10311 }
10312 QueryExpr::Insert(i) => (Action::Insert, Resource::table_from_name(&i.table)),
10313 QueryExpr::Update(u) => (Action::Update, Resource::table_from_name(&u.table)),
10314 QueryExpr::Delete(d) => (Action::Delete, Resource::table_from_name(&d.table)),
10315 QueryExpr::Join(_) => (Action::Select, Resource::Database),
10319 QueryExpr::Grant(_)
10322 | QueryExpr::Revoke(_)
10323 | QueryExpr::AlterUser(_)
10324 | QueryExpr::CreateUser(_) => {
10325 return if role == crate::auth::Role::Admin {
10326 Ok(())
10327 } else {
10328 Err(format!(
10329 "principal=`{}` role=`{:?}` cannot issue ACL/auth DDL",
10330 username, role
10331 ))
10332 };
10333 }
10334 QueryExpr::CreateIamPolicy { id, .. } => {
10335 return self.check_policy_management_privilege(
10336 &auth_store,
10337 &principal_id,
10338 role,
10339 tenant.as_deref(),
10340 "policy:put",
10341 "policy",
10342 id,
10343 );
10344 }
10345 QueryExpr::DropIamPolicy { id } => {
10346 return self.check_policy_management_privilege(
10347 &auth_store,
10348 &principal_id,
10349 role,
10350 tenant.as_deref(),
10351 "policy:drop",
10352 "policy",
10353 id,
10354 );
10355 }
10356 QueryExpr::AttachPolicy { policy_id, .. } => {
10357 return self.check_policy_management_privilege(
10358 &auth_store,
10359 &principal_id,
10360 role,
10361 tenant.as_deref(),
10362 "policy:attach",
10363 "policy",
10364 policy_id,
10365 );
10366 }
10367 QueryExpr::DetachPolicy { policy_id, .. } => {
10368 return self.check_policy_management_privilege(
10369 &auth_store,
10370 &principal_id,
10371 role,
10372 tenant.as_deref(),
10373 "policy:detach",
10374 "policy",
10375 policy_id,
10376 );
10377 }
10378 QueryExpr::ShowPolicies { .. } | QueryExpr::ShowEffectivePermissions { .. } => {
10379 return Ok(());
10380 }
10381 QueryExpr::SimulatePolicy { .. } => {
10382 return self.check_policy_management_privilege(
10383 &auth_store,
10384 &principal_id,
10385 role,
10386 tenant.as_deref(),
10387 "policy:simulate",
10388 "policy",
10389 "*",
10390 );
10391 }
10392 QueryExpr::LintPolicy { .. } => {
10393 return self.check_policy_management_privilege(
10396 &auth_store,
10397 &principal_id,
10398 role,
10399 tenant.as_deref(),
10400 "policy:simulate",
10401 "policy",
10402 "*",
10403 );
10404 }
10405 QueryExpr::MigratePolicyMode { dry_run, .. } => {
10406 let action = if *dry_run {
10411 "policy:simulate"
10412 } else {
10413 "policy:put"
10414 };
10415 return self.check_policy_management_privilege(
10416 &auth_store,
10417 &principal_id,
10418 role,
10419 tenant.as_deref(),
10420 action,
10421 "policy",
10422 "*",
10423 );
10424 }
10425 QueryExpr::DropTable(q) => {
10428 return self.check_ddl_collection_privilege(
10429 &auth_store,
10430 &principal_id,
10431 role,
10432 tenant.as_deref(),
10433 &username,
10434 "drop",
10435 &q.name,
10436 );
10437 }
10438 QueryExpr::DropGraph(q) => {
10439 return self.check_ddl_collection_privilege(
10440 &auth_store,
10441 &principal_id,
10442 role,
10443 tenant.as_deref(),
10444 &username,
10445 "drop",
10446 &q.name,
10447 );
10448 }
10449 QueryExpr::DropVector(q) => {
10450 return self.check_ddl_collection_privilege(
10451 &auth_store,
10452 &principal_id,
10453 role,
10454 tenant.as_deref(),
10455 &username,
10456 "drop",
10457 &q.name,
10458 );
10459 }
10460 QueryExpr::DropDocument(q) => {
10461 return self.check_ddl_collection_privilege(
10462 &auth_store,
10463 &principal_id,
10464 role,
10465 tenant.as_deref(),
10466 &username,
10467 "drop",
10468 &q.name,
10469 );
10470 }
10471 QueryExpr::DropKv(q) => {
10472 return self.check_ddl_collection_privilege(
10473 &auth_store,
10474 &principal_id,
10475 role,
10476 tenant.as_deref(),
10477 &username,
10478 "drop",
10479 &q.name,
10480 );
10481 }
10482 QueryExpr::DropCollection(q) => {
10483 return self.check_ddl_collection_privilege(
10484 &auth_store,
10485 &principal_id,
10486 role,
10487 tenant.as_deref(),
10488 &username,
10489 "drop",
10490 &q.name,
10491 );
10492 }
10493 QueryExpr::Truncate(q) => {
10494 return self.check_ddl_collection_privilege(
10495 &auth_store,
10496 &principal_id,
10497 role,
10498 tenant.as_deref(),
10499 &username,
10500 "truncate",
10501 &q.name,
10502 );
10503 }
10504 QueryExpr::CreateTable(q) => {
10516 return self.check_ddl_object_privilege(
10517 &auth_store,
10518 &principal_id,
10519 role,
10520 tenant.as_deref(),
10521 &username,
10522 "create",
10523 "collection",
10524 &q.name,
10525 crate::auth::Role::Write,
10526 );
10527 }
10528 QueryExpr::CreateCollection(q) => {
10529 return self.check_ddl_object_privilege(
10530 &auth_store,
10531 &principal_id,
10532 role,
10533 tenant.as_deref(),
10534 &username,
10535 "create",
10536 "collection",
10537 &q.name,
10538 crate::auth::Role::Write,
10539 );
10540 }
10541 QueryExpr::CreateVector(q) => {
10542 return self.check_ddl_object_privilege(
10543 &auth_store,
10544 &principal_id,
10545 role,
10546 tenant.as_deref(),
10547 &username,
10548 "create",
10549 "collection",
10550 &q.name,
10551 crate::auth::Role::Write,
10552 );
10553 }
10554 QueryExpr::AlterTable(q) => {
10555 return self.check_ddl_object_privilege(
10556 &auth_store,
10557 &principal_id,
10558 role,
10559 tenant.as_deref(),
10560 &username,
10561 "alter",
10562 "collection",
10563 &q.name,
10564 crate::auth::Role::Write,
10565 );
10566 }
10567 QueryExpr::CreateIndex(q) => {
10568 return self.check_ddl_object_privilege(
10569 &auth_store,
10570 &principal_id,
10571 role,
10572 tenant.as_deref(),
10573 &username,
10574 "create",
10575 "collection",
10576 &q.table,
10577 crate::auth::Role::Write,
10578 );
10579 }
10580 QueryExpr::DropIndex(q) => {
10581 return self.check_ddl_object_privilege(
10582 &auth_store,
10583 &principal_id,
10584 role,
10585 tenant.as_deref(),
10586 &username,
10587 "drop",
10588 "collection",
10589 &q.table,
10590 crate::auth::Role::Write,
10591 );
10592 }
10593 QueryExpr::CreateSchema(q) => {
10594 return self.check_ddl_object_privilege(
10595 &auth_store,
10596 &principal_id,
10597 role,
10598 tenant.as_deref(),
10599 &username,
10600 "schema:admin",
10601 "schema",
10602 &q.name,
10603 crate::auth::Role::Admin,
10604 );
10605 }
10606 QueryExpr::DropSchema(q) => {
10607 return self.check_ddl_object_privilege(
10608 &auth_store,
10609 &principal_id,
10610 role,
10611 tenant.as_deref(),
10612 &username,
10613 "schema:admin",
10614 "schema",
10615 &q.name,
10616 crate::auth::Role::Admin,
10617 );
10618 }
10619 QueryExpr::CreateSequence(q) => {
10620 return self.check_ddl_object_privilege(
10621 &auth_store,
10622 &principal_id,
10623 role,
10624 tenant.as_deref(),
10625 &username,
10626 "create",
10627 "collection",
10628 &q.name,
10629 crate::auth::Role::Write,
10630 );
10631 }
10632 QueryExpr::DropSequence(q) => {
10633 return self.check_ddl_object_privilege(
10634 &auth_store,
10635 &principal_id,
10636 role,
10637 tenant.as_deref(),
10638 &username,
10639 "drop",
10640 "collection",
10641 &q.name,
10642 crate::auth::Role::Write,
10643 );
10644 }
10645 QueryExpr::CreateView(q) => {
10646 return self.check_ddl_object_privilege(
10647 &auth_store,
10648 &principal_id,
10649 role,
10650 tenant.as_deref(),
10651 &username,
10652 "create",
10653 "collection",
10654 &q.name,
10655 crate::auth::Role::Write,
10656 );
10657 }
10658 QueryExpr::DropView(q) => {
10659 return self.check_ddl_object_privilege(
10660 &auth_store,
10661 &principal_id,
10662 role,
10663 tenant.as_deref(),
10664 &username,
10665 "drop",
10666 "collection",
10667 &q.name,
10668 crate::auth::Role::Write,
10669 );
10670 }
10671 QueryExpr::RefreshMaterializedView(q) => {
10672 return self.check_ddl_object_privilege(
10673 &auth_store,
10674 &principal_id,
10675 role,
10676 tenant.as_deref(),
10677 &username,
10678 "alter",
10679 "collection",
10680 &q.name,
10681 crate::auth::Role::Write,
10682 );
10683 }
10684 QueryExpr::CreatePolicy(q) => {
10685 return self.check_ddl_object_privilege(
10686 &auth_store,
10687 &principal_id,
10688 role,
10689 tenant.as_deref(),
10690 &username,
10691 "create",
10692 "collection",
10693 &q.table,
10694 crate::auth::Role::Write,
10695 );
10696 }
10697 QueryExpr::DropPolicy(q) => {
10698 return self.check_ddl_object_privilege(
10699 &auth_store,
10700 &principal_id,
10701 role,
10702 tenant.as_deref(),
10703 &username,
10704 "drop",
10705 "collection",
10706 &q.table,
10707 crate::auth::Role::Write,
10708 );
10709 }
10710 QueryExpr::CreateServer(q) => {
10711 return self.check_ddl_object_privilege(
10712 &auth_store,
10713 &principal_id,
10714 role,
10715 tenant.as_deref(),
10716 &username,
10717 "schema:admin",
10718 "schema",
10719 &q.name,
10720 crate::auth::Role::Admin,
10721 );
10722 }
10723 QueryExpr::DropServer(q) => {
10724 return self.check_ddl_object_privilege(
10725 &auth_store,
10726 &principal_id,
10727 role,
10728 tenant.as_deref(),
10729 &username,
10730 "schema:admin",
10731 "schema",
10732 &q.name,
10733 crate::auth::Role::Admin,
10734 );
10735 }
10736 QueryExpr::CreateForeignTable(q) => {
10737 return self.check_ddl_object_privilege(
10738 &auth_store,
10739 &principal_id,
10740 role,
10741 tenant.as_deref(),
10742 &username,
10743 "schema:write",
10744 "schema",
10745 &q.name,
10746 crate::auth::Role::Write,
10747 );
10748 }
10749 QueryExpr::DropForeignTable(q) => {
10750 return self.check_ddl_object_privilege(
10751 &auth_store,
10752 &principal_id,
10753 role,
10754 tenant.as_deref(),
10755 &username,
10756 "schema:write",
10757 "schema",
10758 &q.name,
10759 crate::auth::Role::Write,
10760 );
10761 }
10762 QueryExpr::CreateTimeSeries(q) => {
10763 return self.check_ddl_object_privilege(
10764 &auth_store,
10765 &principal_id,
10766 role,
10767 tenant.as_deref(),
10768 &username,
10769 "create",
10770 "collection",
10771 &q.name,
10772 crate::auth::Role::Write,
10773 );
10774 }
10775 QueryExpr::CreateMetric(q) => {
10776 return self.check_ddl_object_privilege(
10777 &auth_store,
10778 &principal_id,
10779 role,
10780 tenant.as_deref(),
10781 &username,
10782 "create",
10783 "collection",
10784 &q.path,
10785 crate::auth::Role::Write,
10786 );
10787 }
10788 QueryExpr::AlterMetric(q) => {
10789 return self.check_ddl_object_privilege(
10790 &auth_store,
10791 &principal_id,
10792 role,
10793 tenant.as_deref(),
10794 &username,
10795 "alter",
10796 "collection",
10797 &q.path,
10798 crate::auth::Role::Write,
10799 );
10800 }
10801 QueryExpr::CreateSlo(q) => {
10802 return self.check_ddl_object_privilege(
10803 &auth_store,
10804 &principal_id,
10805 role,
10806 tenant.as_deref(),
10807 &username,
10808 "create",
10809 "collection",
10810 &q.path,
10811 crate::auth::Role::Write,
10812 );
10813 }
10814 QueryExpr::DropTimeSeries(q) => {
10815 return self.check_ddl_object_privilege(
10816 &auth_store,
10817 &principal_id,
10818 role,
10819 tenant.as_deref(),
10820 &username,
10821 "drop",
10822 "collection",
10823 &q.name,
10824 crate::auth::Role::Write,
10825 );
10826 }
10827 QueryExpr::CreateQueue(q) => {
10828 return self.check_ddl_object_privilege(
10829 &auth_store,
10830 &principal_id,
10831 role,
10832 tenant.as_deref(),
10833 &username,
10834 "create",
10835 "collection",
10836 &q.name,
10837 crate::auth::Role::Write,
10838 );
10839 }
10840 QueryExpr::AlterQueue(q) => {
10841 return self.check_ddl_object_privilege(
10842 &auth_store,
10843 &principal_id,
10844 role,
10845 tenant.as_deref(),
10846 &username,
10847 "alter",
10848 "collection",
10849 &q.name,
10850 crate::auth::Role::Write,
10851 );
10852 }
10853 QueryExpr::DropQueue(q) => {
10854 return self.check_ddl_object_privilege(
10855 &auth_store,
10856 &principal_id,
10857 role,
10858 tenant.as_deref(),
10859 &username,
10860 "drop",
10861 "collection",
10862 &q.name,
10863 crate::auth::Role::Write,
10864 );
10865 }
10866 QueryExpr::CreateTree(q) => {
10867 return self.check_ddl_object_privilege(
10868 &auth_store,
10869 &principal_id,
10870 role,
10871 tenant.as_deref(),
10872 &username,
10873 "create",
10874 "collection",
10875 &q.collection,
10876 crate::auth::Role::Write,
10877 );
10878 }
10879 QueryExpr::DropTree(q) => {
10880 return self.check_ddl_object_privilege(
10881 &auth_store,
10882 &principal_id,
10883 role,
10884 tenant.as_deref(),
10885 &username,
10886 "drop",
10887 "collection",
10888 &q.collection,
10889 crate::auth::Role::Write,
10890 );
10891 }
10892 QueryExpr::CreateMigration(q) => {
10896 return self.check_ddl_object_privilege(
10897 &auth_store,
10898 &principal_id,
10899 role,
10900 tenant.as_deref(),
10901 &username,
10902 "schema:write",
10903 "schema",
10904 &q.name,
10905 crate::auth::Role::Write,
10906 );
10907 }
10908 QueryExpr::ApplyMigration(_) | QueryExpr::RollbackMigration(_) => {
10910 return if role == crate::auth::Role::Admin {
10911 Ok(())
10912 } else {
10913 Err(format!(
10914 "principal=`{}` role=`{:?}` cannot issue APPLY/ROLLBACK MIGRATION",
10915 username, role
10916 ))
10917 };
10918 }
10919 QueryExpr::ExplainMigration(_) => return Ok(()),
10921 _ => return Ok(()),
10925 };
10926
10927 if auth_store.iam_authorization_enabled() {
10928 let iam_action = legacy_action_to_iam(action);
10929 let iam_resource = legacy_resource_to_iam(&resource, tenant.as_deref());
10930 let iam_ctx = runtime_iam_context(role, tenant.as_deref());
10931 if !auth_store.check_policy_authz_with_role(
10932 &principal_id,
10933 iam_action,
10934 &iam_resource,
10935 &iam_ctx,
10936 role,
10937 ) {
10938 return Err(format!(
10939 "principal=`{}` action=`{}` resource=`{}:{}` denied by IAM policy",
10940 username, iam_action, iam_resource.kind, iam_resource.name
10941 ));
10942 }
10943
10944 if let QueryExpr::Table(table) = expr {
10945 self.check_table_column_projection_privilege(
10946 &auth_store,
10947 &principal_id,
10948 &iam_ctx,
10949 table,
10950 )?;
10951 }
10952
10953 if let QueryExpr::Update(update) = expr {
10954 let columns = update_set_target_columns(update);
10955 if !columns.is_empty() {
10956 let request = column_access_request_for_table_update(&update.table, columns);
10957 let outcome =
10958 auth_store.check_column_projection_authz(&principal_id, &request, &iam_ctx);
10959 if let Some(denied) = outcome.first_denied_column() {
10960 return Err(format!(
10961 "principal=`{}` action=`{}` resource=`{}:{}` denied by IAM column policy",
10962 username, iam_action, denied.resource.kind, denied.resource.name
10963 ));
10964 }
10965 if !outcome.allowed() {
10966 return Err(format!(
10967 "principal=`{}` action=`{}` resource=`{}:{}` denied by IAM policy",
10968 username,
10969 iam_action,
10970 outcome.table_resource.kind,
10971 outcome.table_resource.name
10972 ));
10973 }
10974 }
10975
10976 if let Some(columns) = update_returning_columns_for_policy(self, update) {
10977 let request = column_access_request_for_table_select(&update.table, columns);
10978 let outcome =
10979 auth_store.check_column_projection_authz(&principal_id, &request, &iam_ctx);
10980 if let Some(denied) = outcome.first_denied_column() {
10981 return Err(format!(
10982 "principal=`{}` action=`select` resource=`{}:{}` denied by IAM column policy",
10983 username, denied.resource.kind, denied.resource.name
10984 ));
10985 }
10986 if !outcome.allowed() {
10987 return Err(format!(
10988 "principal=`{}` action=`select` resource=`{}:{}` denied by IAM policy",
10989 username, outcome.table_resource.kind, outcome.table_resource.name
10990 ));
10991 }
10992 }
10993 }
10994
10995 Ok(())
10996 } else {
10997 auth_store
10998 .check_grant(&ctx, action, &resource)
10999 .map_err(|e| e.to_string())
11000 }
11001 }
11002
11003 fn check_table_column_projection_privilege(
11004 &self,
11005 auth_store: &Arc<crate::auth::store::AuthStore>,
11006 principal: &crate::auth::UserId,
11007 ctx: &crate::auth::policies::EvalContext,
11008 table: &crate::storage::query::ast::TableQuery,
11009 ) -> Result<(), String> {
11010 use crate::auth::{ColumnAccessRequest, ColumnDecisionEffect};
11011
11012 let columns = requested_table_columns_for_policy(table);
11013 if columns.is_empty() {
11014 return Ok(());
11015 }
11016
11017 let request = ColumnAccessRequest::select(table.table.clone(), columns);
11018 let outcome = auth_store.check_column_projection_authz(principal, &request, ctx);
11019 if outcome.allowed() {
11020 return Ok(());
11021 }
11022
11023 if !matches!(
11024 outcome.table_decision,
11025 crate::auth::policies::Decision::Allow { .. }
11026 | crate::auth::policies::Decision::AdminBypass
11027 ) {
11028 return Err(format!(
11029 "principal=`{}` action=`select` resource=`{}:{}` denied by IAM policy",
11030 principal, outcome.table_resource.kind, outcome.table_resource.name
11031 ));
11032 }
11033
11034 let denied = outcome
11035 .first_denied_column()
11036 .filter(|decision| decision.effective == ColumnDecisionEffect::Denied);
11037 match denied {
11038 Some(decision) => Err(format!(
11039 "principal=`{}` action=`select` resource=`{}:{}` denied by IAM policy",
11040 principal, decision.resource.kind, decision.resource.name
11041 )),
11042 None => Ok(()),
11043 }
11044 }
11045
11046 fn check_graph_property_projection_privilege(
11047 &self,
11048 auth_store: &Arc<crate::auth::store::AuthStore>,
11049 principal: &crate::auth::UserId,
11050 role: crate::auth::Role,
11051 tenant: Option<&str>,
11052 query: &crate::storage::query::ast::GraphQuery,
11053 ) -> Result<(), String> {
11054 let columns = explicit_graph_projection_properties(query);
11055 if columns.is_empty() {
11056 return Ok(());
11057 }
11058 self.check_table_like_column_projection_privilege(
11059 auth_store, principal, role, tenant, "graph", &columns,
11060 )
11061 }
11062
11063 fn check_table_like_column_projection_privilege(
11064 &self,
11065 auth_store: &Arc<crate::auth::store::AuthStore>,
11066 principal: &crate::auth::UserId,
11067 role: crate::auth::Role,
11068 tenant: Option<&str>,
11069 table: &str,
11070 columns: &[String],
11071 ) -> Result<(), String> {
11072 let iam_ctx = runtime_iam_context(role, tenant);
11073 let request =
11074 crate::auth::ColumnAccessRequest::select(table.to_string(), columns.iter().cloned());
11075 let outcome = auth_store.check_column_projection_authz(principal, &request, &iam_ctx);
11076 if outcome.allowed() {
11077 return Ok(());
11078 }
11079 let denied = outcome
11080 .first_denied_column()
11081 .map(|d| d.resource.name.clone())
11082 .unwrap_or_else(|| format!("{table}.<unknown>"));
11083 Err(format!(
11084 "principal=`{}` action=`select` resource=`column:{}` denied by IAM policy",
11085 principal, denied
11086 ))
11087 }
11088
11089 fn check_policy_management_privilege(
11090 &self,
11091 auth_store: &Arc<crate::auth::store::AuthStore>,
11092 principal: &crate::auth::UserId,
11093 role: crate::auth::Role,
11094 tenant: Option<&str>,
11095 action: &str,
11096 resource_kind: &str,
11097 resource_name: &str,
11098 ) -> Result<(), String> {
11099 let ctx = runtime_iam_context(role, tenant);
11100
11101 if !auth_store.iam_authorization_enabled() {
11102 return if role == crate::auth::Role::Admin {
11103 Ok(())
11104 } else {
11105 Err(format!(
11106 "principal=`{}` role=`{:?}` cannot issue ACL/auth DDL",
11107 principal, role
11108 ))
11109 };
11110 }
11111
11112 if resource_kind == "policy"
11113 && matches!(
11114 action,
11115 "policy:put" | "policy:drop" | "policy:attach" | "policy:detach"
11116 )
11117 && self
11118 .inner
11119 .config_registry
11120 .get_active(resource_name)
11121 .map(|entry| entry.managed)
11122 .unwrap_or(false)
11123 {
11124 return Ok(());
11125 }
11126
11127 let mut resource = crate::auth::policies::ResourceRef::new(
11128 resource_kind.to_string(),
11129 resource_name.to_string(),
11130 );
11131 if let Some(t) = tenant {
11132 resource = resource.with_tenant(t.to_string());
11133 }
11134 if auth_store.check_policy_authz_with_role(principal, action, &resource, &ctx, role) {
11135 Ok(())
11136 } else {
11137 Err(format!(
11138 "principal=`{}` action=`{}` resource=`{}:{}` denied by IAM policy",
11139 principal, action, resource.kind, resource.name
11140 ))
11141 }
11142 }
11143
11144 fn check_managed_config_write_for_set_config(&self, key: &str) -> RedDBResult<()> {
11145 let Some(auth_store) = self.inner.auth_store.read().clone() else {
11146 return Ok(());
11147 };
11148 let (username, role) = current_auth_identity()
11149 .unwrap_or_else(|| ("anonymous".to_string(), crate::auth::Role::Read));
11150 let tenant = current_tenant();
11151 let principal = crate::auth::UserId::from_parts(tenant.as_deref(), &username);
11152 let ctx = runtime_iam_context(role, tenant.as_deref());
11153 let gate = crate::auth::managed_config::ManagedConfigGate::new(
11154 self.inner.config_registry.as_ref(),
11155 );
11156 match gate.check_write(&auth_store, &principal, &ctx, key) {
11157 crate::auth::managed_config::ManagedConfigDecision::PassThrough { .. }
11158 | crate::auth::managed_config::ManagedConfigDecision::Allow { .. } => Ok(()),
11159 crate::auth::managed_config::ManagedConfigDecision::Deny { reason, .. } => {
11160 Err(RedDBError::Query(format!(
11161 "permission denied: managed config mutation blocked for `{key}`: {reason}"
11162 )))
11163 }
11164 }
11165 }
11166
11167 fn check_secret_write_privilege(
11168 &self,
11169 auth_store: &Arc<crate::auth::store::AuthStore>,
11170 key: &str,
11171 ) -> RedDBResult<()> {
11172 let Some((username, role)) = current_auth_identity() else {
11173 return Ok(());
11174 };
11175 let tenant = current_tenant();
11176 let principal = crate::auth::UserId::from_parts(tenant.as_deref(), &username);
11177 let mut resource =
11178 crate::auth::policies::ResourceRef::new("secret".to_string(), key.to_string());
11179 if let Some(tenant) = &tenant {
11180 resource = resource.with_tenant(tenant.clone());
11181 }
11182 let ctx = runtime_iam_context(role, tenant.as_deref());
11183 if auth_store.check_policy_authz_with_role(
11184 &principal,
11185 "secret:write",
11186 &resource,
11187 &ctx,
11188 role,
11189 ) {
11190 return Ok(());
11191 }
11192 Err(RedDBError::Query(format!(
11193 "permission denied: principal=`{}` action=`secret:write` resource=`secret:{}` denied by IAM policy",
11194 principal, key
11195 )))
11196 }
11197
11198 fn check_kv_write_privilege(
11204 &self,
11205 auth_store: &Arc<crate::auth::store::AuthStore>,
11206 key: &str,
11207 ) -> RedDBResult<()> {
11208 let Some((username, role)) = current_auth_identity() else {
11209 return Ok(());
11210 };
11211 let tenant = current_tenant();
11212 let principal = crate::auth::UserId::from_parts(tenant.as_deref(), &username);
11213 let mut resource =
11214 crate::auth::policies::ResourceRef::new("kv".to_string(), key.to_string());
11215 if let Some(tenant) = &tenant {
11216 resource = resource.with_tenant(tenant.clone());
11217 }
11218 let ctx = runtime_iam_context(role, tenant.as_deref());
11219 if auth_store.check_policy_authz_with_role(&principal, "kv:write", &resource, &ctx, role) {
11220 return Ok(());
11221 }
11222 Err(RedDBError::Query(format!(
11223 "permission denied: principal=`{}` action=`kv:write` resource=`kv:{}` denied by IAM policy",
11224 principal, key
11225 )))
11226 }
11227
11228 fn check_queue_op_privilege(
11244 &self,
11245 auth_store: &Arc<crate::auth::store::AuthStore>,
11246 principal: &crate::auth::UserId,
11247 role: crate::auth::Role,
11248 tenant: Option<&str>,
11249 action: &str,
11250 queue: &str,
11251 ) -> Result<(), String> {
11252 if !auth_store.iam_authorization_enabled() {
11253 return Ok(());
11254 }
11255 let mut resource =
11256 crate::auth::policies::ResourceRef::new("queue".to_string(), queue.to_string());
11257 if let Some(t) = tenant {
11258 resource = resource.with_tenant(t.to_string());
11259 }
11260 let ctx = runtime_iam_context(role, tenant);
11261 if auth_store.check_policy_authz_with_role(principal, action, &resource, &ctx, role) {
11262 Ok(())
11263 } else {
11264 Err(format!(
11265 "principal=`{}` action=`{}` resource=`queue:{}` denied by IAM policy",
11266 principal, action, queue
11267 ))
11268 }
11269 }
11270
11271 fn check_graph_op_privilege(
11291 &self,
11292 auth_store: &Arc<crate::auth::store::AuthStore>,
11293 principal: &crate::auth::UserId,
11294 role: crate::auth::Role,
11295 tenant: Option<&str>,
11296 action: &str,
11297 ) -> Result<(), String> {
11298 if !auth_store.iam_authorization_enabled() {
11299 return Ok(());
11300 }
11301 let mut resource =
11302 crate::auth::policies::ResourceRef::new("graph".to_string(), "*".to_string());
11303 if let Some(t) = tenant {
11304 resource = resource.with_tenant(t.to_string());
11305 }
11306 let ctx = runtime_iam_context(role, tenant);
11307 if auth_store.check_policy_authz_with_role(principal, action, &resource, &ctx, role) {
11308 Ok(())
11309 } else {
11310 Err(format!(
11311 "principal=`{}` action=`{}` resource=`graph:*` denied by IAM policy",
11312 principal, action
11313 ))
11314 }
11315 }
11316
11317 fn check_vector_op_privilege(
11332 &self,
11333 auth_store: &Arc<crate::auth::store::AuthStore>,
11334 principal: &crate::auth::UserId,
11335 role: crate::auth::Role,
11336 tenant: Option<&str>,
11337 action: &str,
11338 collection: &str,
11339 ) -> Result<(), String> {
11340 if !auth_store.iam_authorization_enabled() {
11341 return Ok(());
11342 }
11343 let mut resource =
11344 crate::auth::policies::ResourceRef::new("vector".to_string(), collection.to_string());
11345 if let Some(t) = tenant {
11346 resource = resource.with_tenant(t.to_string());
11347 }
11348 let ctx = runtime_iam_context(role, tenant);
11349 if auth_store.check_policy_authz_with_role(principal, action, &resource, &ctx, role) {
11350 Ok(())
11351 } else {
11352 Err(format!(
11353 "principal=`{}` action=`{}` resource=`vector:{}` denied by IAM policy",
11354 principal, action, collection
11355 ))
11356 }
11357 }
11358
11359 fn check_ddl_collection_privilege(
11365 &self,
11366 auth_store: &Arc<crate::auth::store::AuthStore>,
11367 principal: &crate::auth::UserId,
11368 role: crate::auth::Role,
11369 tenant: Option<&str>,
11370 username: &str,
11371 action: &str,
11372 collection: &str,
11373 ) -> Result<(), String> {
11374 self.check_ddl_object_privilege(
11375 auth_store,
11376 principal,
11377 role,
11378 tenant,
11379 username,
11380 action,
11381 "collection",
11382 collection,
11383 crate::auth::Role::Write,
11384 )
11385 }
11386
11387 #[allow(clippy::too_many_arguments)]
11405 fn check_ddl_object_privilege(
11406 &self,
11407 auth_store: &Arc<crate::auth::store::AuthStore>,
11408 principal: &crate::auth::UserId,
11409 role: crate::auth::Role,
11410 tenant: Option<&str>,
11411 username: &str,
11412 action: &str,
11413 resource_kind: &str,
11414 resource_name: &str,
11415 min_role: crate::auth::Role,
11416 ) -> Result<(), String> {
11417 if role < min_role {
11418 let msg = format!(
11419 "principal=`{}` role=`{:?}` cannot issue DDL action=`{}` resource=`{}:{}`",
11420 username, role, action, resource_kind, resource_name
11421 );
11422 self.inner.audit_log.record(
11423 action,
11424 username,
11425 resource_name,
11426 "denied",
11427 crate::json::Value::Null,
11428 );
11429 return Err(msg);
11430 }
11431
11432 if !auth_store.iam_authorization_enabled() {
11433 self.inner.audit_log.record(
11434 action,
11435 username,
11436 resource_name,
11437 "ok",
11438 crate::json::Value::Null,
11439 );
11440 return Ok(());
11441 }
11442
11443 let mut resource = crate::auth::policies::ResourceRef::new(
11444 resource_kind.to_string(),
11445 resource_name.to_string(),
11446 );
11447 if let Some(t) = tenant {
11448 resource = resource.with_tenant(t.to_string());
11449 }
11450 let ctx = runtime_iam_context(role, tenant);
11451 if auth_store.check_policy_authz_with_role(principal, action, &resource, &ctx, role) {
11452 self.inner.audit_log.record(
11453 action,
11454 username,
11455 resource_name,
11456 "ok",
11457 crate::json::Value::Null,
11458 );
11459 Ok(())
11460 } else {
11461 self.inner.audit_log.record(
11462 action,
11463 username,
11464 resource_name,
11465 "denied",
11466 crate::json::Value::Null,
11467 );
11468 Err(format!(
11469 "principal=`{}` action=`{}` resource=`{}:{}` denied by IAM policy",
11470 username, action, resource_kind, resource_name
11471 ))
11472 }
11473 }
11474
11475 fn execute_grant_statement(
11477 &self,
11478 query: &str,
11479 stmt: &crate::storage::query::ast::GrantStmt,
11480 ) -> RedDBResult<RuntimeQueryResult> {
11481 use crate::auth::privileges::{Action, GrantPrincipal, Resource};
11482 use crate::auth::UserId;
11483 use crate::storage::query::ast::{GrantObjectKind, GrantPrincipalRef};
11484
11485 let auth_store = self
11486 .inner
11487 .auth_store
11488 .read()
11489 .clone()
11490 .ok_or_else(|| RedDBError::Query("auth store not configured".to_string()))?;
11491
11492 let (gname, grole) = current_auth_identity().ok_or_else(|| {
11494 RedDBError::Query("GRANT requires an authenticated principal".to_string())
11495 })?;
11496 let granter = UserId::from_parts(current_tenant().as_deref(), &gname);
11497 let granter_role = grole;
11498
11499 let mut actions: Vec<Action> = Vec::new();
11501 if stmt.all {
11502 actions.push(Action::All);
11503 } else {
11504 for kw in &stmt.actions {
11505 let a = Action::from_keyword(kw).ok_or_else(|| {
11506 RedDBError::Query(format!("unknown privilege keyword `{}`", kw))
11507 })?;
11508 actions.push(a);
11509 }
11510 }
11511
11512 let mut applied = 0usize;
11514 for obj in &stmt.objects {
11515 let resource = match stmt.object_kind {
11516 GrantObjectKind::Table => Resource::Table {
11517 schema: obj.schema.clone(),
11518 table: obj.name.clone(),
11519 },
11520 GrantObjectKind::Schema => Resource::Schema(obj.name.clone()),
11521 GrantObjectKind::Database => Resource::Database,
11522 GrantObjectKind::Function => Resource::Function {
11523 schema: obj.schema.clone(),
11524 name: obj.name.clone(),
11525 },
11526 };
11527 for principal in &stmt.principals {
11528 let p = match principal {
11529 GrantPrincipalRef::Public => GrantPrincipal::Public,
11530 GrantPrincipalRef::Group(g) => GrantPrincipal::Group(g.clone()),
11531 GrantPrincipalRef::User { tenant, name } => {
11532 GrantPrincipal::User(UserId::from_parts(tenant.as_deref(), name))
11533 }
11534 };
11535 let tenant = granter.tenant.clone();
11538 auth_store
11539 .grant(
11540 &granter,
11541 granter_role,
11542 p.clone(),
11543 resource.clone(),
11544 actions.clone(),
11545 stmt.with_grant_option,
11546 tenant.clone(),
11547 )
11548 .map_err(|e| RedDBError::Query(e.to_string()))?;
11549
11550 if let Some(policy) =
11554 grant_to_iam_policy(&p, &resource, &actions, tenant.as_deref())
11555 {
11556 let pid = policy.id.clone();
11557 auth_store
11558 .put_policy_internal(policy)
11559 .map_err(|e| RedDBError::Query(e.to_string()))?;
11560 let attachment = match &p {
11561 GrantPrincipal::User(uid) => {
11562 crate::auth::store::PrincipalRef::User(uid.clone())
11563 }
11564 GrantPrincipal::Group(group) => {
11565 crate::auth::store::PrincipalRef::Group(group.clone())
11566 }
11567 GrantPrincipal::Public => crate::auth::store::PrincipalRef::Group(
11568 crate::auth::store::PUBLIC_IAM_GROUP.to_string(),
11569 ),
11570 };
11571 auth_store
11572 .attach_policy(attachment, &pid)
11573 .map_err(|e| RedDBError::Query(e.to_string()))?;
11574 }
11575 applied += 1;
11576 tracing::info!(
11577 target: "audit",
11578 principal = %granter,
11579 action = "grant",
11580 "GRANT applied"
11581 );
11582 }
11583 }
11584
11585 self.invalidate_result_cache();
11586 Ok(RuntimeQueryResult::ok_message(
11587 query.to_string(),
11588 &format!("GRANT applied to {} target(s)", applied),
11589 "grant",
11590 ))
11591 }
11592
11593 fn execute_revoke_statement(
11595 &self,
11596 query: &str,
11597 stmt: &crate::storage::query::ast::RevokeStmt,
11598 ) -> RedDBResult<RuntimeQueryResult> {
11599 use crate::auth::privileges::{Action, GrantPrincipal, Resource};
11600 use crate::auth::UserId;
11601 use crate::storage::query::ast::{GrantObjectKind, GrantPrincipalRef};
11602
11603 let auth_store = self
11604 .inner
11605 .auth_store
11606 .read()
11607 .clone()
11608 .ok_or_else(|| RedDBError::Query("auth store not configured".to_string()))?;
11609
11610 let (_gname, grole) = current_auth_identity().ok_or_else(|| {
11611 RedDBError::Query("REVOKE requires an authenticated principal".to_string())
11612 })?;
11613 let granter_role = grole;
11614
11615 let actions: Vec<Action> = if stmt.all {
11616 vec![Action::All]
11617 } else {
11618 stmt.actions
11619 .iter()
11620 .map(|kw| Action::from_keyword(kw).unwrap_or(Action::Select))
11621 .collect()
11622 };
11623
11624 let mut total_removed = 0usize;
11625 for obj in &stmt.objects {
11626 let resource = match stmt.object_kind {
11627 GrantObjectKind::Table => Resource::Table {
11628 schema: obj.schema.clone(),
11629 table: obj.name.clone(),
11630 },
11631 GrantObjectKind::Schema => Resource::Schema(obj.name.clone()),
11632 GrantObjectKind::Database => Resource::Database,
11633 GrantObjectKind::Function => Resource::Function {
11634 schema: obj.schema.clone(),
11635 name: obj.name.clone(),
11636 },
11637 };
11638 for principal in &stmt.principals {
11639 let p = match principal {
11640 GrantPrincipalRef::Public => GrantPrincipal::Public,
11641 GrantPrincipalRef::Group(g) => GrantPrincipal::Group(g.clone()),
11642 GrantPrincipalRef::User { tenant, name } => {
11643 GrantPrincipal::User(UserId::from_parts(tenant.as_deref(), name))
11644 }
11645 };
11646 let removed = auth_store
11647 .revoke(granter_role, &p, &resource, &actions)
11648 .map_err(|e| RedDBError::Query(e.to_string()))?;
11649 let _removed_policies =
11650 auth_store.delete_synthetic_grant_policies(&p, &resource, &actions);
11651 total_removed += removed;
11652 }
11653 }
11654
11655 self.invalidate_result_cache();
11656 Ok(RuntimeQueryResult::ok_message(
11657 query.to_string(),
11658 &format!("REVOKE removed {} grant(s)", total_removed),
11659 "revoke",
11660 ))
11661 }
11662
11663 fn execute_create_user_statement(
11665 &self,
11666 query: &str,
11667 stmt: &crate::storage::query::ast::CreateUserStmt,
11668 ) -> RedDBResult<RuntimeQueryResult> {
11669 let auth_store = self
11670 .inner
11671 .auth_store
11672 .read()
11673 .clone()
11674 .ok_or_else(|| RedDBError::Query("auth store not configured".to_string()))?;
11675
11676 let (_gname, grole) = current_auth_identity().ok_or_else(|| {
11677 RedDBError::Query("CREATE USER requires an authenticated principal".to_string())
11678 })?;
11679 if grole != crate::auth::Role::Admin {
11680 return Err(RedDBError::Query(
11681 "CREATE USER requires Admin role".to_string(),
11682 ));
11683 }
11684
11685 let role = crate::auth::Role::from_str(&stmt.role)
11686 .ok_or_else(|| RedDBError::Query(format!("invalid role `{}`", stmt.role)))?;
11687 let user = auth_store
11688 .create_user_in_tenant(stmt.tenant.as_deref(), &stmt.username, &stmt.password, role)
11689 .map_err(|e| RedDBError::Query(e.to_string()))?;
11690
11691 self.invalidate_result_cache();
11692 let target = crate::auth::UserId::from_parts(user.tenant_id.as_deref(), &user.username);
11693 tracing::info!(
11694 target: "audit",
11695 principal = %target,
11696 role = %role,
11697 action = "create_user",
11698 "CREATE USER applied"
11699 );
11700
11701 Ok(RuntimeQueryResult::ok_message(
11702 query.to_string(),
11703 &format!("CREATE USER {} applied", target),
11704 "create_user",
11705 ))
11706 }
11707
11708 fn execute_alter_user_statement(
11710 &self,
11711 query: &str,
11712 stmt: &crate::storage::query::ast::AlterUserStmt,
11713 ) -> RedDBResult<RuntimeQueryResult> {
11714 use crate::auth::privileges::UserAttributes;
11715 use crate::auth::UserId;
11716 use crate::storage::query::ast::AlterUserAttribute;
11717
11718 let auth_store = self
11719 .inner
11720 .auth_store
11721 .read()
11722 .clone()
11723 .ok_or_else(|| RedDBError::Query("auth store not configured".to_string()))?;
11724
11725 let (gname, grole) = current_auth_identity().ok_or_else(|| {
11726 RedDBError::Query("ALTER USER requires an authenticated principal".to_string())
11727 })?;
11728 if grole != crate::auth::Role::Admin {
11729 return Err(RedDBError::Query(
11730 "ALTER USER requires Admin role".to_string(),
11731 ));
11732 }
11733
11734 let target = UserId::from_parts(stmt.tenant.as_deref(), &stmt.username);
11735 let actor_tenant = current_tenant();
11736 let actor = UserId::from_parts(actor_tenant.as_deref(), &gname);
11737 for attr in &stmt.attributes {
11738 let action = match attr {
11739 AlterUserAttribute::Disable => "user:disable",
11740 AlterUserAttribute::Password(_) => "user:password:change",
11741 _ => "user:update",
11742 };
11743 if auth_store.has_explicit_user_lifecycle_deny(&actor, grole, action, &target) {
11744 return Err(RedDBError::Query(format!(
11745 "ALTER USER denied by IAM policy: action `{action}` resource `user:{target}`"
11746 )));
11747 }
11748 }
11749
11750 let mut attrs = auth_store.user_attributes(&target);
11753 let mut enable_change: Option<bool> = None;
11754
11755 for a in &stmt.attributes {
11756 match a {
11757 AlterUserAttribute::ValidUntil(ts) => {
11758 let ms = parse_timestamp_to_ms(ts).ok_or_else(|| {
11762 RedDBError::Query(format!("invalid VALID UNTIL timestamp `{ts}`"))
11763 })?;
11764 attrs.valid_until = Some(ms);
11765 }
11766 AlterUserAttribute::ConnectionLimit(n) => {
11767 if *n < 0 {
11768 return Err(RedDBError::Query(
11769 "CONNECTION LIMIT must be non-negative".to_string(),
11770 ));
11771 }
11772 attrs.connection_limit = Some(*n as u32);
11773 }
11774 AlterUserAttribute::SetSearchPath(p) => {
11775 attrs.search_path = Some(p.clone());
11776 }
11777 AlterUserAttribute::AddGroup(g) => {
11778 if !attrs.groups.iter().any(|existing| existing == g) {
11779 attrs.groups.push(g.clone());
11780 attrs.groups.sort();
11781 }
11782 }
11783 AlterUserAttribute::DropGroup(g) => {
11784 attrs.groups.retain(|existing| existing != g);
11785 }
11786 AlterUserAttribute::Enable => enable_change = Some(true),
11787 AlterUserAttribute::Disable => enable_change = Some(false),
11788 AlterUserAttribute::Password(_) => {
11789 }
11793 }
11794 }
11795
11796 auth_store
11797 .set_user_attributes(&target, attrs)
11798 .map_err(|e| RedDBError::Query(e.to_string()))?;
11799 if let Some(en) = enable_change {
11800 auth_store
11801 .set_user_enabled(&target, en)
11802 .map_err(|e| RedDBError::Query(e.to_string()))?;
11803 }
11804 self.invalidate_result_cache();
11805 tracing::info!(
11806 target: "audit",
11807 principal = %target,
11808 action = "alter_user",
11809 "ALTER USER applied"
11810 );
11811
11812 Ok(RuntimeQueryResult::ok_message(
11813 query.to_string(),
11814 &format!("ALTER USER {} applied", target),
11815 "alter_user",
11816 ))
11817 }
11818
11819 fn execute_create_iam_policy(
11824 &self,
11825 query: &str,
11826 id: &str,
11827 json: &str,
11828 ) -> RedDBResult<RuntimeQueryResult> {
11829 use crate::auth::policies::Policy;
11830
11831 let auth_store = self
11832 .inner
11833 .auth_store
11834 .read()
11835 .clone()
11836 .ok_or_else(|| RedDBError::Query("auth store not configured".to_string()))?;
11837
11838 let mut policy = Policy::from_json_str(json)
11843 .map_err(|e| RedDBError::Query(format!("policy parse: {e}")))?;
11844 if policy.id != id {
11845 policy.id = id.to_string();
11846 }
11847 let pid = policy.id.clone();
11848 let tenant = current_tenant();
11849 let (actor_name, actor_role) = current_auth_identity()
11850 .unwrap_or_else(|| ("anonymous".to_string(), crate::auth::Role::Read));
11851 let actor = crate::auth::UserId::from_parts(tenant.as_deref(), &actor_name);
11852 let eval_ctx = runtime_iam_context(actor_role, tenant.as_deref());
11853 let event_ctx = self.policy_mutation_control_ctx(&actor, tenant.as_deref());
11854 let ledger = self.inner.control_event_ledger.read();
11855 let control = crate::auth::store::PolicyMutationControl {
11856 ctx: &event_ctx,
11857 ledger: ledger.as_ref(),
11858 config: self.inner.control_event_config,
11859 registry: Some(self.inner.config_registry.as_ref()),
11860 actor: &actor,
11861 eval_ctx: &eval_ctx,
11862 };
11863 auth_store
11864 .put_policy_with_control_events(policy, &control)
11865 .map_err(|e| RedDBError::Query(e.to_string()))?;
11866
11867 let principal = actor_name;
11868 tracing::info!(
11869 target: "audit",
11870 principal = %principal,
11871 action = "iam:policy.put",
11872 matched_policy_id = %pid,
11873 "CREATE POLICY applied"
11874 );
11875 self.inner.audit_log.record(
11876 "iam/policy.put",
11877 &principal,
11878 &pid,
11879 "ok",
11880 crate::json::Value::Null,
11881 );
11882
11883 self.invalidate_result_cache();
11884 Ok(RuntimeQueryResult::ok_message(
11885 query.to_string(),
11886 &format!("policy `{pid}` stored"),
11887 "create_iam_policy",
11888 ))
11889 }
11890
11891 fn execute_drop_iam_policy(&self, query: &str, id: &str) -> RedDBResult<RuntimeQueryResult> {
11892 let auth_store = self
11893 .inner
11894 .auth_store
11895 .read()
11896 .clone()
11897 .ok_or_else(|| RedDBError::Query("auth store not configured".to_string()))?;
11898 let tenant = current_tenant();
11899 let (actor_name, actor_role) = current_auth_identity()
11900 .unwrap_or_else(|| ("anonymous".to_string(), crate::auth::Role::Read));
11901 let actor = crate::auth::UserId::from_parts(tenant.as_deref(), &actor_name);
11902 let eval_ctx = runtime_iam_context(actor_role, tenant.as_deref());
11903 let event_ctx = self.policy_mutation_control_ctx(&actor, tenant.as_deref());
11904 let ledger = self.inner.control_event_ledger.read();
11905 let control = crate::auth::store::PolicyMutationControl {
11906 ctx: &event_ctx,
11907 ledger: ledger.as_ref(),
11908 config: self.inner.control_event_config,
11909 registry: Some(self.inner.config_registry.as_ref()),
11910 actor: &actor,
11911 eval_ctx: &eval_ctx,
11912 };
11913 auth_store
11914 .delete_policy_with_control_events(id, &control)
11915 .map_err(|e| RedDBError::Query(e.to_string()))?;
11916
11917 let principal = actor_name;
11918 tracing::info!(
11919 target: "audit",
11920 principal = %principal,
11921 action = "iam:policy.drop",
11922 matched_policy_id = %id,
11923 "DROP POLICY applied"
11924 );
11925 self.inner.audit_log.record(
11926 "iam/policy.drop",
11927 &principal,
11928 id,
11929 "ok",
11930 crate::json::Value::Null,
11931 );
11932
11933 self.invalidate_result_cache();
11934 Ok(RuntimeQueryResult::ok_message(
11935 query.to_string(),
11936 &format!("policy `{id}` dropped"),
11937 "drop_iam_policy",
11938 ))
11939 }
11940
11941 fn execute_attach_policy(
11942 &self,
11943 query: &str,
11944 policy_id: &str,
11945 principal: &crate::storage::query::ast::PolicyPrincipalRef,
11946 ) -> RedDBResult<RuntimeQueryResult> {
11947 use crate::auth::store::PrincipalRef;
11948 use crate::auth::UserId;
11949 use crate::storage::query::ast::PolicyPrincipalRef;
11950
11951 let auth_store = self
11952 .inner
11953 .auth_store
11954 .read()
11955 .clone()
11956 .ok_or_else(|| RedDBError::Query("auth store not configured".to_string()))?;
11957 let p = match principal {
11958 PolicyPrincipalRef::User(u) => {
11959 PrincipalRef::User(UserId::from_parts(u.tenant.as_deref(), &u.username))
11960 }
11961 PolicyPrincipalRef::Group(g) => PrincipalRef::Group(g.clone()),
11962 };
11963 let pretty_target = principal_label(principal);
11964 let tenant = current_tenant();
11965 let (actor_name, actor_role) = current_auth_identity()
11966 .unwrap_or_else(|| ("anonymous".to_string(), crate::auth::Role::Read));
11967 let actor = crate::auth::UserId::from_parts(tenant.as_deref(), &actor_name);
11968 let eval_ctx = runtime_iam_context(actor_role, tenant.as_deref());
11969 let event_ctx = self.policy_mutation_control_ctx(&actor, tenant.as_deref());
11970 let ledger = self.inner.control_event_ledger.read();
11971 let control = crate::auth::store::PolicyMutationControl {
11972 ctx: &event_ctx,
11973 ledger: ledger.as_ref(),
11974 config: self.inner.control_event_config,
11975 registry: Some(self.inner.config_registry.as_ref()),
11976 actor: &actor,
11977 eval_ctx: &eval_ctx,
11978 };
11979 auth_store
11980 .attach_policy_with_control_events(p, policy_id, &control)
11981 .map_err(|e| RedDBError::Query(e.to_string()))?;
11982
11983 let principal_str = actor_name;
11984 tracing::info!(
11985 target: "audit",
11986 principal = %principal_str,
11987 action = "iam:policy.attach",
11988 matched_policy_id = %policy_id,
11989 target = %pretty_target,
11990 "ATTACH POLICY applied"
11991 );
11992 self.inner.audit_log.record(
11993 "iam/policy.attach",
11994 &principal_str,
11995 &pretty_target,
11996 "ok",
11997 crate::json::Value::Null,
11998 );
11999
12000 self.invalidate_result_cache();
12001 Ok(RuntimeQueryResult::ok_message(
12002 query.to_string(),
12003 &format!("policy `{policy_id}` attached to {pretty_target}"),
12004 "attach_policy",
12005 ))
12006 }
12007
12008 fn execute_detach_policy(
12009 &self,
12010 query: &str,
12011 policy_id: &str,
12012 principal: &crate::storage::query::ast::PolicyPrincipalRef,
12013 ) -> RedDBResult<RuntimeQueryResult> {
12014 use crate::auth::store::PrincipalRef;
12015 use crate::auth::UserId;
12016 use crate::storage::query::ast::PolicyPrincipalRef;
12017
12018 let auth_store = self
12019 .inner
12020 .auth_store
12021 .read()
12022 .clone()
12023 .ok_or_else(|| RedDBError::Query("auth store not configured".to_string()))?;
12024 let p = match principal {
12025 PolicyPrincipalRef::User(u) => {
12026 PrincipalRef::User(UserId::from_parts(u.tenant.as_deref(), &u.username))
12027 }
12028 PolicyPrincipalRef::Group(g) => PrincipalRef::Group(g.clone()),
12029 };
12030 let pretty_target = principal_label(principal);
12031 let tenant = current_tenant();
12032 let (actor_name, actor_role) = current_auth_identity()
12033 .unwrap_or_else(|| ("anonymous".to_string(), crate::auth::Role::Read));
12034 let actor = crate::auth::UserId::from_parts(tenant.as_deref(), &actor_name);
12035 let eval_ctx = runtime_iam_context(actor_role, tenant.as_deref());
12036 let event_ctx = self.policy_mutation_control_ctx(&actor, tenant.as_deref());
12037 let ledger = self.inner.control_event_ledger.read();
12038 let control = crate::auth::store::PolicyMutationControl {
12039 ctx: &event_ctx,
12040 ledger: ledger.as_ref(),
12041 config: self.inner.control_event_config,
12042 registry: Some(self.inner.config_registry.as_ref()),
12043 actor: &actor,
12044 eval_ctx: &eval_ctx,
12045 };
12046 auth_store
12047 .detach_policy_with_control_events(p, policy_id, &control)
12048 .map_err(|e| RedDBError::Query(e.to_string()))?;
12049
12050 let principal_str = actor_name;
12051 tracing::info!(
12052 target: "audit",
12053 principal = %principal_str,
12054 action = "iam:policy.detach",
12055 matched_policy_id = %policy_id,
12056 target = %pretty_target,
12057 "DETACH POLICY applied"
12058 );
12059 self.inner.audit_log.record(
12060 "iam/policy.detach",
12061 &principal_str,
12062 &pretty_target,
12063 "ok",
12064 crate::json::Value::Null,
12065 );
12066
12067 self.invalidate_result_cache();
12068 Ok(RuntimeQueryResult::ok_message(
12069 query.to_string(),
12070 &format!("policy `{policy_id}` detached from {pretty_target}"),
12071 "detach_policy",
12072 ))
12073 }
12074
12075 fn execute_show_policies(
12076 &self,
12077 query: &str,
12078 filter: Option<&crate::storage::query::ast::PolicyPrincipalRef>,
12079 ) -> RedDBResult<RuntimeQueryResult> {
12080 use crate::auth::UserId;
12081 use crate::storage::query::ast::PolicyPrincipalRef;
12082 use crate::storage::query::unified::UnifiedRecord;
12083 use crate::storage::schema::Value as SchemaValue;
12084 use std::sync::Arc;
12085
12086 let auth_store = self
12087 .inner
12088 .auth_store
12089 .read()
12090 .clone()
12091 .ok_or_else(|| RedDBError::Query("auth store not configured".to_string()))?;
12092
12093 let pols = match filter {
12094 None => auth_store.list_policies(),
12095 Some(PolicyPrincipalRef::User(u)) => {
12096 let id = UserId::from_parts(u.tenant.as_deref(), &u.username);
12097 auth_store.effective_policies(&id)
12098 }
12099 Some(PolicyPrincipalRef::Group(g)) => auth_store.group_policies(g),
12100 };
12101
12102 let mut records = Vec::with_capacity(pols.len() + 1);
12103
12104 let mode = auth_store.enforcement_mode();
12109 let mut header = UnifiedRecord::default();
12110 header.set_arc(
12111 Arc::from("id"),
12112 SchemaValue::text("<enforcement_mode>".to_string()),
12113 );
12114 header.set_arc(Arc::from("statements"), SchemaValue::Integer(0));
12115 header.set_arc(Arc::from("tenant"), SchemaValue::Null);
12116 let header_json = format!(
12117 r#"{{"enforcement_mode":"{}","policy_only_hard_version":"{}"}}"#,
12118 mode.as_str(),
12119 crate::auth::enforcement_mode::POLICY_ONLY_HARD_VERSION
12120 );
12121 header.set_arc(Arc::from("json"), SchemaValue::text(header_json));
12122 records.push(header);
12123
12124 for p in pols.iter() {
12125 let mut rec = UnifiedRecord::default();
12126 rec.set_arc(Arc::from("id"), SchemaValue::text(p.id.clone()));
12127 rec.set_arc(
12128 Arc::from("statements"),
12129 SchemaValue::Integer(p.statements.len() as i64),
12130 );
12131 rec.set_arc(
12132 Arc::from("tenant"),
12133 p.tenant
12134 .as_deref()
12135 .map(|t| SchemaValue::text(t.to_string()))
12136 .unwrap_or(SchemaValue::Null),
12137 );
12138 rec.set_arc(Arc::from("json"), SchemaValue::text(p.to_json_string()));
12139 records.push(rec);
12140 }
12141 let mut result = crate::storage::query::unified::UnifiedResult::empty();
12142 result.records = records;
12143 Ok(RuntimeQueryResult {
12144 query: query.to_string(),
12145 mode: crate::storage::query::modes::QueryMode::Sql,
12146 statement: "show_policies",
12147 engine: "iam-policies",
12148 result,
12149 affected_rows: 0,
12150 statement_type: "select",
12151 bookmark: None,
12152 })
12153 }
12154
12155 fn execute_show_effective_permissions(
12156 &self,
12157 query: &str,
12158 user: &crate::storage::query::ast::PolicyUserRef,
12159 resource: Option<&crate::storage::query::ast::PolicyResourceRef>,
12160 ) -> RedDBResult<RuntimeQueryResult> {
12161 use crate::auth::UserId;
12162 use crate::storage::query::unified::UnifiedRecord;
12163 use crate::storage::schema::Value as SchemaValue;
12164 use std::sync::Arc;
12165
12166 let auth_store = self
12167 .inner
12168 .auth_store
12169 .read()
12170 .clone()
12171 .ok_or_else(|| RedDBError::Query("auth store not configured".to_string()))?;
12172 let id = UserId::from_parts(user.tenant.as_deref(), &user.username);
12173 let pols = auth_store.effective_policies(&id);
12174
12175 let mut records = Vec::new();
12178 for p in pols.iter() {
12179 for (idx, st) in p.statements.iter().enumerate() {
12180 if let Some(_r) = resource {
12181 }
12185 let mut rec = UnifiedRecord::default();
12186 rec.set_arc(Arc::from("policy_id"), SchemaValue::text(p.id.clone()));
12187 rec.set_arc(
12188 Arc::from("statement_index"),
12189 SchemaValue::Integer(idx as i64),
12190 );
12191 rec.set_arc(
12192 Arc::from("sid"),
12193 st.sid
12194 .as_deref()
12195 .map(|s| SchemaValue::text(s.to_string()))
12196 .unwrap_or(SchemaValue::Null),
12197 );
12198 rec.set_arc(
12199 Arc::from("effect"),
12200 SchemaValue::text(match st.effect {
12201 crate::auth::policies::Effect::Allow => "allow",
12202 crate::auth::policies::Effect::Deny => "deny",
12203 }),
12204 );
12205 rec.set_arc(
12206 Arc::from("actions"),
12207 SchemaValue::Integer(st.actions.len() as i64),
12208 );
12209 rec.set_arc(
12210 Arc::from("resources"),
12211 SchemaValue::Integer(st.resources.len() as i64),
12212 );
12213 records.push(rec);
12214 }
12215 }
12216 let mut result = crate::storage::query::unified::UnifiedResult::empty();
12217 result.records = records;
12218 Ok(RuntimeQueryResult {
12219 query: query.to_string(),
12220 mode: crate::storage::query::modes::QueryMode::Sql,
12221 statement: "show_effective_permissions",
12222 engine: "iam-policies",
12223 result,
12224 affected_rows: 0,
12225 statement_type: "select",
12226 bookmark: None,
12227 })
12228 }
12229
12230 fn execute_lint_policy(
12231 &self,
12232 query: &str,
12233 source: &crate::storage::query::ast::LintPolicySource,
12234 ) -> RedDBResult<RuntimeQueryResult> {
12235 use crate::auth::policy_linter::lint;
12236 use crate::storage::query::ast::LintPolicySource;
12237 use crate::storage::query::unified::UnifiedRecord;
12238 use crate::storage::schema::Value as SchemaValue;
12239 use std::sync::Arc;
12240
12241 let policy_text = match source {
12246 LintPolicySource::Json(text) => text.clone(),
12247 LintPolicySource::Id(id) => {
12248 let auth_store =
12249 self.inner.auth_store.read().clone().ok_or_else(|| {
12250 RedDBError::Query("auth store not configured".to_string())
12251 })?;
12252 let policy = auth_store
12253 .get_policy(id)
12254 .ok_or_else(|| RedDBError::Query(format!("policy `{id}` not found")))?;
12255 policy.to_json_string()
12256 }
12257 };
12258 let diagnostics = lint(&policy_text);
12259
12260 let principal_str = current_auth_identity()
12261 .map(|(u, _)| u)
12262 .unwrap_or_else(|| "anonymous".into());
12263 tracing::info!(
12264 target: "audit",
12265 principal = %principal_str,
12266 action = "iam:policy.lint",
12267 diagnostic_count = diagnostics.len(),
12268 "LINT POLICY issued"
12269 );
12270 self.inner.audit_log.record(
12271 "iam/policy.lint",
12272 &principal_str,
12273 match source {
12274 LintPolicySource::Id(id) => id.as_str(),
12275 LintPolicySource::Json(_) => "<json>",
12276 },
12277 "ok",
12278 crate::json::Value::Null,
12279 );
12280
12281 const COLUMNS: [&str; 5] = ["severity", "code", "message", "suggested_fix", "location"];
12284 let schema = Arc::new(
12285 COLUMNS
12286 .iter()
12287 .map(|name| Arc::<str>::from(*name))
12288 .collect::<Vec<_>>(),
12289 );
12290 let records: Vec<UnifiedRecord> = diagnostics
12291 .iter()
12292 .map(|d| {
12293 UnifiedRecord::with_schema(
12294 Arc::clone(&schema),
12295 vec![
12296 SchemaValue::text(d.severity.as_str()),
12297 SchemaValue::text(d.code.as_str()),
12298 SchemaValue::text(d.message.clone()),
12299 d.suggested_fix
12300 .as_deref()
12301 .map(SchemaValue::text)
12302 .unwrap_or(SchemaValue::Null),
12303 d.location
12304 .as_deref()
12305 .map(SchemaValue::text)
12306 .unwrap_or(SchemaValue::Null),
12307 ],
12308 )
12309 })
12310 .collect();
12311 let mut result = crate::storage::query::unified::UnifiedResult::with_columns(
12312 COLUMNS.iter().map(|c| c.to_string()).collect(),
12313 );
12314 result.records = records;
12315 Ok(RuntimeQueryResult {
12316 query: query.to_string(),
12317 mode: crate::storage::query::modes::QueryMode::Sql,
12318 statement: "lint_policy",
12319 engine: "iam-policies",
12320 result,
12321 affected_rows: 0,
12322 statement_type: "select",
12323 bookmark: None,
12324 })
12325 }
12326
12327 fn execute_migrate_policy_mode(
12332 &self,
12333 query: &str,
12334 target: &str,
12335 dry_run: bool,
12336 ) -> RedDBResult<RuntimeQueryResult> {
12337 use crate::auth::enforcement_mode::PolicyEnforcementMode;
12338 use crate::auth::migrate_policy_mode::{
12339 principal_label, simulate_migration_delta, MigratePolicyDelta,
12340 };
12341 use crate::auth::policies::ResourceRef;
12342 use crate::storage::query::unified::UnifiedRecord;
12343 use crate::storage::schema::Value as SchemaValue;
12344 use std::sync::Arc;
12345
12346 let parsed = PolicyEnforcementMode::parse(target).ok_or_else(|| {
12351 RedDBError::Query(format!(
12352 "MIGRATE POLICY MODE: invalid target `{target}` (expected `policy_only`)"
12353 ))
12354 })?;
12355 if parsed != PolicyEnforcementMode::PolicyOnly {
12356 return Err(RedDBError::Query(format!(
12357 "MIGRATE POLICY MODE: target `{target}` is not supported — only `policy_only` may be migrated to via this command"
12358 )));
12359 }
12360
12361 let auth_store = self
12362 .inner
12363 .auth_store
12364 .read()
12365 .clone()
12366 .ok_or_else(|| RedDBError::Query("auth store not configured".to_string()))?;
12367
12368 let snapshot = self.inner.db.catalog_model_snapshot();
12376 let resources: Vec<ResourceRef> = snapshot
12377 .collections
12378 .iter()
12379 .map(|c| ResourceRef::new("table", c.name.clone()))
12380 .collect();
12381
12382 let now_ms = crate::utils::now_unix_millis() as u128;
12383 let deltas: Vec<MigratePolicyDelta> =
12384 simulate_migration_delta(auth_store.as_ref(), &resources, now_ms);
12385
12386 let principal_str = current_auth_identity()
12387 .map(|(u, _)| u)
12388 .unwrap_or_else(|| "anonymous".into());
12389
12390 let outcome_str = if dry_run {
12394 "dry_run"
12395 } else if deltas.is_empty() {
12396 "applied"
12397 } else {
12398 "refused"
12399 };
12400 tracing::info!(
12401 target: "audit",
12402 principal = %principal_str,
12403 action = "iam:policy.migrate_mode",
12404 target = %target,
12405 dry_run,
12406 delta_count = deltas.len(),
12407 outcome = outcome_str,
12408 "MIGRATE POLICY MODE issued"
12409 );
12410 self.inner.audit_log.record(
12411 "iam/policy.migrate_mode",
12412 &principal_str,
12413 target,
12414 outcome_str,
12415 crate::json::Value::Null,
12416 );
12417
12418 if !dry_run && !deltas.is_empty() {
12422 let summary = deltas
12423 .iter()
12424 .take(5)
12425 .map(|d| {
12426 format!(
12427 "{}:{}/{}:{}",
12428 principal_label(&d.principal),
12429 d.action,
12430 d.resource_kind,
12431 d.resource_name
12432 )
12433 })
12434 .collect::<Vec<_>>()
12435 .join(", ");
12436 let more = if deltas.len() > 5 {
12437 format!(" (and {} more)", deltas.len() - 5)
12438 } else {
12439 String::new()
12440 };
12441 return Err(RedDBError::Query(format!(
12442 "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}",
12443 n = deltas.len(),
12444 )));
12445 }
12446
12447 if !dry_run {
12451 auth_store.set_enforcement_mode(parsed);
12452 }
12453
12454 const COLUMNS: [&str; 5] = [
12455 "principal",
12456 "role",
12457 "action",
12458 "resource_kind",
12459 "resource_name",
12460 ];
12461 let schema = Arc::new(
12462 COLUMNS
12463 .iter()
12464 .map(|name| Arc::<str>::from(*name))
12465 .collect::<Vec<_>>(),
12466 );
12467 let records: Vec<UnifiedRecord> = deltas
12468 .iter()
12469 .map(|d| {
12470 UnifiedRecord::with_schema(
12471 Arc::clone(&schema),
12472 vec![
12473 SchemaValue::text(principal_label(&d.principal)),
12474 SchemaValue::text(d.role.as_str()),
12475 SchemaValue::text(d.action.clone()),
12476 SchemaValue::text(d.resource_kind.clone()),
12477 SchemaValue::text(d.resource_name.clone()),
12478 ],
12479 )
12480 })
12481 .collect();
12482 let mut result = crate::storage::query::unified::UnifiedResult::with_columns(
12483 COLUMNS.iter().map(|c| c.to_string()).collect(),
12484 );
12485 result.records = records;
12486 Ok(RuntimeQueryResult {
12487 query: query.to_string(),
12488 mode: crate::storage::query::modes::QueryMode::Sql,
12489 statement: "migrate_policy_mode",
12490 engine: "iam-policies",
12491 result,
12492 affected_rows: 0,
12493 statement_type: "select",
12494 bookmark: None,
12495 })
12496 }
12497
12498 fn execute_simulate_policy(
12499 &self,
12500 query: &str,
12501 user: &crate::storage::query::ast::PolicyUserRef,
12502 action: &str,
12503 resource: &crate::storage::query::ast::PolicyResourceRef,
12504 ) -> RedDBResult<RuntimeQueryResult> {
12505 use crate::auth::policies::ResourceRef;
12506 use crate::auth::store::SimCtx;
12507 use crate::auth::UserId;
12508 use crate::storage::query::unified::UnifiedRecord;
12509 use crate::storage::schema::Value as SchemaValue;
12510 use std::sync::Arc;
12511
12512 let auth_store = self
12513 .inner
12514 .auth_store
12515 .read()
12516 .clone()
12517 .ok_or_else(|| RedDBError::Query("auth store not configured".to_string()))?;
12518 let id = UserId::from_parts(user.tenant.as_deref(), &user.username);
12519 let r = ResourceRef::new(resource.kind.clone(), resource.name.clone());
12520 let outcome = auth_store.simulate(&id, action, &r, SimCtx::default());
12521
12522 let principal_str = current_auth_identity()
12523 .map(|(u, _)| u)
12524 .unwrap_or_else(|| "anonymous".into());
12525 let (decision_str, matched_pid, matched_sid) = decision_to_strings(&outcome.decision);
12526 tracing::info!(
12527 target: "audit",
12528 principal = %principal_str,
12529 action = "iam:policy.simulate",
12530 decision = %decision_str,
12531 matched_policy_id = ?matched_pid,
12532 matched_sid = ?matched_sid,
12533 "SIMULATE issued"
12534 );
12535 self.inner.audit_log.record(
12536 "iam/policy.simulate",
12537 &principal_str,
12538 &id.to_string(),
12539 "ok",
12540 crate::json::Value::Null,
12541 );
12542
12543 let mut rec = UnifiedRecord::default();
12544 rec.set_arc(Arc::from("decision"), SchemaValue::text(decision_str));
12545 rec.set_arc(
12546 Arc::from("matched_policy_id"),
12547 matched_pid
12548 .map(SchemaValue::text)
12549 .unwrap_or(SchemaValue::Null),
12550 );
12551 rec.set_arc(
12552 Arc::from("matched_sid"),
12553 matched_sid
12554 .map(SchemaValue::text)
12555 .unwrap_or(SchemaValue::Null),
12556 );
12557 rec.set_arc(Arc::from("reason"), SchemaValue::text(outcome.reason));
12558 rec.set_arc(
12559 Arc::from("trail_len"),
12560 SchemaValue::Integer(outcome.trail.len() as i64),
12561 );
12562 let mut result = crate::storage::query::unified::UnifiedResult::empty();
12563 result.records = vec![rec];
12564 Ok(RuntimeQueryResult {
12565 query: query.to_string(),
12566 mode: crate::storage::query::modes::QueryMode::Sql,
12567 statement: "simulate_policy",
12568 engine: "iam-policies",
12569 result,
12570 affected_rows: 0,
12571 statement_type: "select",
12572 bookmark: None,
12573 })
12574 }
12575}
12576
12577fn grant_to_iam_policy(
12582 principal: &crate::auth::privileges::GrantPrincipal,
12583 resource: &crate::auth::privileges::Resource,
12584 actions: &[crate::auth::privileges::Action],
12585 tenant: Option<&str>,
12586) -> Option<crate::auth::policies::Policy> {
12587 use crate::auth::policies::{
12588 compile_action, ActionPattern, Effect, Policy, ResourcePattern, Statement,
12589 };
12590 use crate::auth::privileges::{Action, GrantPrincipal, Resource};
12591
12592 if matches!(principal, GrantPrincipal::Group(_)) {
12593 return None;
12594 }
12595
12596 let now = crate::auth::now_ms();
12597 let id = format!("_grant_{:x}_{:x}", now, std::process::id());
12598
12599 let resource_str = match resource {
12600 Resource::Database => "table:*".to_string(),
12601 Resource::Schema(s) => format!("table:{s}.*"),
12602 Resource::Table { schema, table } => match schema {
12603 Some(s) => format!("table:{s}.{table}"),
12604 None => format!("table:{table}"),
12605 },
12606 Resource::Function { schema, name } => match schema {
12607 Some(s) => format!("function:{s}.{name}"),
12608 None => format!("function:{name}"),
12609 },
12610 };
12611
12612 let action_patterns: Vec<ActionPattern> = if actions.contains(&Action::All) {
12616 vec![ActionPattern::Wildcard]
12617 } else {
12618 actions
12619 .iter()
12620 .map(|a| compile_action(&a.as_str().to_ascii_lowercase()))
12621 .collect()
12622 };
12623 if action_patterns.is_empty() {
12624 return None;
12625 }
12626
12627 let resource_patterns = if resource_str == "*" {
12632 vec![ResourcePattern::Wildcard]
12633 } else if resource_str.contains('*') {
12634 vec![ResourcePattern::Glob(resource_str.clone())]
12635 } else if let Some((kind, name)) = resource_str.split_once(':') {
12636 vec![ResourcePattern::Exact {
12637 kind: kind.to_string(),
12638 name: name.to_string(),
12639 }]
12640 } else {
12641 vec![ResourcePattern::Wildcard]
12642 };
12643
12644 let policy = Policy {
12645 id,
12646 version: 1,
12647 tenant: tenant.map(|t| t.to_string()),
12648 created_at: now,
12649 updated_at: now,
12650 statements: vec![Statement {
12651 sid: None,
12652 effect: Effect::Allow,
12653 actions: action_patterns,
12654 resources: resource_patterns,
12655 condition: None,
12656 }],
12657 };
12658 if policy.validate().is_err() {
12659 return None;
12660 }
12661 Some(policy)
12662}
12663
12664fn parse_positive_iterations(func: &str, value: &f64) -> RedDBResult<usize> {
12670 if !value.is_finite() || *value < 1.0 || value.fract() != 0.0 {
12671 return Err(RedDBError::Query(format!(
12672 "table function '{func}' max_iterations must be a positive integer, got {value}"
12673 )));
12674 }
12675 Ok(*value as usize)
12676}
12677
12678fn legacy_action_to_iam(action: crate::auth::privileges::Action) -> &'static str {
12679 use crate::auth::privileges::Action;
12680 match action {
12681 Action::Select => "select",
12682 Action::Insert => "insert",
12683 Action::Update => "update",
12684 Action::Delete => "delete",
12685 Action::Truncate => "truncate",
12686 Action::References => "references",
12687 Action::Execute => "execute",
12688 Action::Usage => "usage",
12689 Action::All => "*",
12690 }
12691}
12692
12693fn update_set_target_columns(query: &crate::storage::query::ast::UpdateQuery) -> Vec<String> {
12694 let mut columns = Vec::new();
12695 for (column, _) in &query.assignment_exprs {
12696 if !columns.iter().any(|seen| seen == column) {
12697 columns.push(column.clone());
12698 }
12699 }
12700 columns
12701}
12702
12703fn column_access_request_for_table_update(
12704 table_name: &str,
12705 columns: Vec<String>,
12706) -> crate::auth::ColumnAccessRequest {
12707 match table_name.split_once('.') {
12708 Some((schema, table)) => {
12709 crate::auth::ColumnAccessRequest::update(table.to_string(), columns)
12710 .with_schema(schema.to_string())
12711 }
12712 None => crate::auth::ColumnAccessRequest::update(table_name.to_string(), columns),
12713 }
12714}
12715
12716fn column_access_request_for_table_select(
12717 table_name: &str,
12718 columns: Vec<String>,
12719) -> crate::auth::ColumnAccessRequest {
12720 match table_name.split_once('.') {
12721 Some((schema, table)) => {
12722 crate::auth::ColumnAccessRequest::select(table.to_string(), columns)
12723 .with_schema(schema.to_string())
12724 }
12725 None => crate::auth::ColumnAccessRequest::select(table_name.to_string(), columns),
12726 }
12727}
12728
12729fn update_returning_columns_for_policy(
12730 runtime: &RedDBRuntime,
12731 query: &crate::storage::query::ast::UpdateQuery,
12732) -> Option<Vec<String>> {
12733 let items = query.returning.as_ref()?;
12734 let mut columns = Vec::new();
12735 let project_all = items
12736 .iter()
12737 .any(|item| matches!(item, crate::storage::query::ast::ReturningItem::All));
12738 if project_all {
12739 collect_returning_star_columns(runtime, query, &mut columns);
12740 } else {
12741 for item in items {
12742 let crate::storage::query::ast::ReturningItem::Column(column) = item else {
12743 continue;
12744 };
12745 push_returning_policy_column(&mut columns, column);
12746 }
12747 }
12748 (!columns.is_empty()).then_some(columns)
12749}
12750
12751fn collect_returning_star_columns(
12752 runtime: &RedDBRuntime,
12753 query: &crate::storage::query::ast::UpdateQuery,
12754 columns: &mut Vec<String>,
12755) {
12756 let store = runtime.db().store();
12757 let Some(manager) = store.get_collection(&query.table) else {
12758 return;
12759 };
12760 if let Some(schema) = manager.column_schema() {
12761 for column in schema.iter() {
12762 push_returning_policy_column(columns, column);
12763 }
12764 }
12765 for entity in manager.query_all(|_| true) {
12766 if !returning_entity_matches_update_target(&entity, query.target) {
12767 continue;
12768 }
12769 match &entity.data {
12770 crate::storage::EntityData::Row(row) => {
12771 for (column, _) in row.iter_fields() {
12772 push_returning_policy_column(columns, column);
12773 }
12774 }
12775 crate::storage::EntityData::Node(node) => {
12776 push_returning_policy_column(columns, "label");
12777 push_returning_policy_column(columns, "node_type");
12778 for column in node.properties.keys() {
12779 push_returning_policy_column(columns, column);
12780 }
12781 }
12782 crate::storage::EntityData::Edge(edge) => {
12783 push_returning_policy_column(columns, "label");
12784 push_returning_policy_column(columns, "from_rid");
12785 push_returning_policy_column(columns, "to_rid");
12786 push_returning_policy_column(columns, "weight");
12787 for column in edge.properties.keys() {
12788 push_returning_policy_column(columns, column);
12789 }
12790 }
12791 _ => {}
12792 }
12793 }
12794}
12795
12796fn push_returning_policy_column(columns: &mut Vec<String>, column: &str) {
12797 if returning_public_envelope_column(column) {
12798 return;
12799 }
12800 if !columns.iter().any(|seen| seen == column) {
12801 columns.push(column.to_string());
12802 }
12803}
12804
12805fn returning_public_envelope_column(column: &str) -> bool {
12806 matches!(
12807 column.to_ascii_lowercase().as_str(),
12808 "rid" | "collection" | "kind" | "tenant" | "created_at" | "updated_at"
12809 )
12810}
12811
12812fn returning_entity_matches_update_target(
12813 entity: &crate::storage::UnifiedEntity,
12814 target: crate::storage::query::ast::UpdateTarget,
12815) -> bool {
12816 use crate::storage::query::ast::UpdateTarget;
12817 match target {
12818 UpdateTarget::Rows => {
12819 matches!(returning_row_item_kind(entity), Some(ReturningRowKind::Row))
12820 }
12821 UpdateTarget::Documents => {
12822 matches!(
12823 returning_row_item_kind(entity),
12824 Some(ReturningRowKind::Document)
12825 )
12826 }
12827 UpdateTarget::Kv => matches!(returning_row_item_kind(entity), Some(ReturningRowKind::Kv)),
12828 UpdateTarget::Nodes => matches!(
12829 (&entity.kind, &entity.data),
12830 (
12831 crate::storage::EntityKind::GraphNode(_),
12832 crate::storage::EntityData::Node(_)
12833 )
12834 ),
12835 UpdateTarget::Edges => matches!(
12836 (&entity.kind, &entity.data),
12837 (
12838 crate::storage::EntityKind::GraphEdge(_),
12839 crate::storage::EntityData::Edge(_)
12840 )
12841 ),
12842 }
12843}
12844
12845#[derive(Debug, Clone, Copy, PartialEq, Eq)]
12846enum ReturningRowKind {
12847 Row,
12848 Document,
12849 Kv,
12850}
12851
12852fn returning_row_item_kind(entity: &crate::storage::UnifiedEntity) -> Option<ReturningRowKind> {
12853 let row = entity.data.as_row()?;
12854 let is_kv = row.iter_fields().all(|(column, _)| {
12855 column.eq_ignore_ascii_case("key") || column.eq_ignore_ascii_case("value")
12856 });
12857 if is_kv {
12858 return Some(ReturningRowKind::Kv);
12859 }
12860 let is_document = row
12861 .iter_fields()
12862 .any(|(_, value)| matches!(value, crate::storage::schema::Value::Json(_)));
12863 if is_document {
12864 Some(ReturningRowKind::Document)
12865 } else {
12866 Some(ReturningRowKind::Row)
12867 }
12868}
12869
12870fn requested_table_columns_for_policy(
12871 table: &crate::storage::query::ast::TableQuery,
12872) -> Vec<String> {
12873 use crate::storage::query::sql_lowering::{
12874 effective_table_filter, effective_table_group_by_exprs, effective_table_having_filter,
12875 effective_table_projections,
12876 };
12877
12878 let table_name = table.table.as_str();
12879 let table_alias = table.alias.as_deref();
12880 let mut columns = std::collections::BTreeSet::new();
12881
12882 for projection in effective_table_projections(table) {
12883 collect_projection_columns(&projection, table_name, table_alias, &mut columns);
12884 }
12885 if let Some(filter) = effective_table_filter(table) {
12886 collect_filter_columns(&filter, table_name, table_alias, &mut columns);
12887 }
12888 for expr in effective_table_group_by_exprs(table) {
12889 collect_expr_columns(&expr, table_name, table_alias, &mut columns);
12890 }
12891 if let Some(filter) = effective_table_having_filter(table) {
12892 collect_filter_columns(&filter, table_name, table_alias, &mut columns);
12893 }
12894 for order in &table.order_by {
12895 if let Some(expr) = order.expr.as_ref() {
12896 collect_expr_columns(expr, table_name, table_alias, &mut columns);
12897 } else {
12898 collect_field_ref_column(&order.field, table_name, table_alias, &mut columns);
12899 }
12900 }
12901
12902 columns.into_iter().collect()
12903}
12904
12905fn collect_projection_columns(
12906 projection: &crate::storage::query::ast::Projection,
12907 table_name: &str,
12908 table_alias: Option<&str>,
12909 columns: &mut std::collections::BTreeSet<String>,
12910) {
12911 use crate::storage::query::ast::Projection;
12912 match projection {
12913 Projection::All => {
12914 columns.insert("*".to_string());
12915 }
12916 Projection::Column(column) | Projection::Alias(column, _) => {
12917 if column != "*" {
12918 columns.insert(column.clone());
12919 }
12920 }
12921 Projection::Function(_, args) => {
12922 for arg in args {
12923 collect_projection_columns(arg, table_name, table_alias, columns);
12924 }
12925 }
12926 Projection::Expression(filter, _) => {
12927 collect_filter_columns(filter, table_name, table_alias, columns);
12928 }
12929 Projection::Field(field, _) => {
12930 collect_field_ref_column(field, table_name, table_alias, columns);
12931 }
12932 Projection::Window { args, .. } => {
12936 for arg in args {
12937 collect_projection_columns(arg, table_name, table_alias, columns);
12938 }
12939 }
12940 }
12941}
12942
12943fn collect_filter_columns(
12944 filter: &crate::storage::query::ast::Filter,
12945 table_name: &str,
12946 table_alias: Option<&str>,
12947 columns: &mut std::collections::BTreeSet<String>,
12948) {
12949 use crate::storage::query::ast::Filter;
12950 match filter {
12951 Filter::Compare { field, .. }
12952 | Filter::IsNull(field)
12953 | Filter::IsNotNull(field)
12954 | Filter::In { field, .. }
12955 | Filter::Between { field, .. }
12956 | Filter::Like { field, .. }
12957 | Filter::StartsWith { field, .. }
12958 | Filter::EndsWith { field, .. }
12959 | Filter::Contains { field, .. } => {
12960 collect_field_ref_column(field, table_name, table_alias, columns);
12961 }
12962 Filter::CompareFields { left, right, .. } => {
12963 collect_field_ref_column(left, table_name, table_alias, columns);
12964 collect_field_ref_column(right, table_name, table_alias, columns);
12965 }
12966 Filter::CompareExpr { lhs, rhs, .. } => {
12967 collect_expr_columns(lhs, table_name, table_alias, columns);
12968 collect_expr_columns(rhs, table_name, table_alias, columns);
12969 }
12970 Filter::And(left, right) | Filter::Or(left, right) => {
12971 collect_filter_columns(left, table_name, table_alias, columns);
12972 collect_filter_columns(right, table_name, table_alias, columns);
12973 }
12974 Filter::Not(inner) => collect_filter_columns(inner, table_name, table_alias, columns),
12975 }
12976}
12977
12978fn collect_expr_columns(
12979 expr: &crate::storage::query::ast::Expr,
12980 table_name: &str,
12981 table_alias: Option<&str>,
12982 columns: &mut std::collections::BTreeSet<String>,
12983) {
12984 use crate::storage::query::ast::Expr;
12985 match expr {
12986 Expr::Column { field, .. } => {
12987 collect_field_ref_column(field, table_name, table_alias, columns);
12988 }
12989 Expr::Literal { .. } | Expr::Parameter { .. } => {}
12990 Expr::UnaryOp { operand, .. } | Expr::Cast { inner: operand, .. } => {
12991 collect_expr_columns(operand, table_name, table_alias, columns);
12992 }
12993 Expr::BinaryOp { lhs, rhs, .. } => {
12994 collect_expr_columns(lhs, table_name, table_alias, columns);
12995 collect_expr_columns(rhs, table_name, table_alias, columns);
12996 }
12997 Expr::FunctionCall { args, .. } => {
12998 for arg in args {
12999 collect_expr_columns(arg, table_name, table_alias, columns);
13000 }
13001 }
13002 Expr::Case {
13003 branches, else_, ..
13004 } => {
13005 for (condition, value) in branches {
13006 collect_expr_columns(condition, table_name, table_alias, columns);
13007 collect_expr_columns(value, table_name, table_alias, columns);
13008 }
13009 if let Some(value) = else_ {
13010 collect_expr_columns(value, table_name, table_alias, columns);
13011 }
13012 }
13013 Expr::IsNull { operand, .. } => {
13014 collect_expr_columns(operand, table_name, table_alias, columns);
13015 }
13016 Expr::InList { target, values, .. } => {
13017 collect_expr_columns(target, table_name, table_alias, columns);
13018 for value in values {
13019 collect_expr_columns(value, table_name, table_alias, columns);
13020 }
13021 }
13022 Expr::Between {
13023 target, low, high, ..
13024 } => {
13025 collect_expr_columns(target, table_name, table_alias, columns);
13026 collect_expr_columns(low, table_name, table_alias, columns);
13027 collect_expr_columns(high, table_name, table_alias, columns);
13028 }
13029 Expr::Subquery { .. } => {}
13030 Expr::WindowFunctionCall { args, window, .. } => {
13031 for arg in args {
13032 collect_expr_columns(arg, table_name, table_alias, columns);
13033 }
13034 for e in &window.partition_by {
13035 collect_expr_columns(e, table_name, table_alias, columns);
13036 }
13037 for o in &window.order_by {
13038 collect_expr_columns(&o.expr, table_name, table_alias, columns);
13039 }
13040 }
13041 }
13042}
13043
13044fn collect_field_ref_column(
13045 field: &crate::storage::query::ast::FieldRef,
13046 table_name: &str,
13047 table_alias: Option<&str>,
13048 columns: &mut std::collections::BTreeSet<String>,
13049) {
13050 if let Some(column) = policy_column_name_from_field_ref(field, table_name, table_alias) {
13051 if column != "*" {
13052 columns.insert(column);
13053 }
13054 }
13055}
13056
13057fn policy_column_name_from_field_ref(
13058 field: &crate::storage::query::ast::FieldRef,
13059 table_name: &str,
13060 table_alias: Option<&str>,
13061) -> Option<String> {
13062 match field {
13063 crate::storage::query::ast::FieldRef::TableColumn { table, column } => {
13064 if column == "*" {
13065 return Some("*".to_string());
13066 }
13067 if table.is_empty() || table == table_name || Some(table.as_str()) == table_alias {
13068 Some(column.clone())
13069 } else {
13070 Some(format!("{table}.{column}"))
13071 }
13072 }
13073 _ => None,
13074 }
13075}
13076
13077fn legacy_resource_to_iam(
13078 resource: &crate::auth::privileges::Resource,
13079 tenant: Option<&str>,
13080) -> crate::auth::policies::ResourceRef {
13081 use crate::auth::privileges::Resource;
13082
13083 let (kind, name) = match resource {
13084 Resource::Database => ("database".to_string(), "*".to_string()),
13085 Resource::Schema(s) => ("schema".to_string(), format!("{s}.*")),
13086 Resource::Table { schema, table } => (
13087 "table".to_string(),
13088 match schema {
13089 Some(s) => format!("{s}.{table}"),
13090 None => table.clone(),
13091 },
13092 ),
13093 Resource::Function { schema, name } => (
13094 "function".to_string(),
13095 match schema {
13096 Some(s) => format!("{s}.{name}"),
13097 None => name.clone(),
13098 },
13099 ),
13100 };
13101
13102 let mut out = crate::auth::policies::ResourceRef::new(kind, name);
13103 if let Some(t) = tenant {
13104 out = out.with_tenant(t.to_string());
13105 }
13106 out
13107}
13108
13109#[derive(Debug)]
13110struct JoinTableSide {
13111 table: String,
13112 alias: String,
13113}
13114
13115fn table_side_context(expr: &QueryExpr) -> Option<JoinTableSide> {
13116 match expr {
13117 QueryExpr::Table(table) => Some(JoinTableSide {
13118 table: table.table.clone(),
13119 alias: table.alias.clone().unwrap_or_else(|| table.table.clone()),
13120 }),
13121 _ => None,
13122 }
13123}
13124
13125fn collect_projection_columns_for_table(
13126 projection: &Projection,
13127 table: &str,
13128 alias: Option<&str>,
13129 out: &mut BTreeSet<String>,
13130) {
13131 match projection {
13132 Projection::Column(column) | Projection::Alias(column, _) => {
13133 match split_qualified_column(column) {
13134 Some((qualifier, column))
13135 if qualifier == table || alias.is_some_and(|alias| qualifier == alias) =>
13136 {
13137 push_policy_column(column, out);
13138 }
13139 Some(_) => {}
13140 None => push_policy_column(column, out),
13141 }
13142 }
13143 Projection::Field(
13144 FieldRef::TableColumn {
13145 table: qualifier,
13146 column,
13147 },
13148 _,
13149 ) => {
13150 if qualifier.is_empty()
13151 || qualifier == table
13152 || alias.is_some_and(|alias| qualifier == alias)
13153 {
13154 push_policy_column(column, out);
13155 }
13156 }
13157 Projection::Field(
13158 FieldRef::NodeProperty {
13159 alias: qualifier,
13160 property,
13161 },
13162 _,
13163 )
13164 | Projection::Field(
13165 FieldRef::EdgeProperty {
13166 alias: qualifier,
13167 property,
13168 },
13169 _,
13170 ) => {
13171 if qualifier == table || alias.is_some_and(|alias| qualifier == alias) {
13172 push_policy_column(property, out);
13173 }
13174 }
13175 Projection::Function(_, args) => {
13176 for arg in args {
13177 collect_projection_columns_for_table(arg, table, alias, out);
13178 }
13179 }
13180 Projection::Expression(_, _) | Projection::All | Projection::Field(_, _) => {}
13181 Projection::Window { args, .. } => {
13182 for arg in args {
13183 collect_projection_columns_for_table(arg, table, alias, out);
13184 }
13185 }
13186 }
13187}
13188
13189fn collect_projection_columns_for_join_side(
13190 projection: &Projection,
13191 left: Option<&JoinTableSide>,
13192 right: Option<&JoinTableSide>,
13193 out: &mut HashMap<String, BTreeSet<String>>,
13194) -> RedDBResult<()> {
13195 match projection {
13196 Projection::Column(column) | Projection::Alias(column, _) => {
13197 if let Some((qualifier, column)) = split_qualified_column(column) {
13198 push_qualified_join_column(qualifier, column, left, right, out);
13199 } else {
13200 push_unqualified_join_column(column, left, right, out);
13201 }
13202 }
13203 Projection::Field(FieldRef::TableColumn { table, column }, _) => {
13204 if table.is_empty() {
13205 push_unqualified_join_column(column, left, right, out);
13206 } else if let Some(side) = [left, right]
13207 .into_iter()
13208 .flatten()
13209 .find(|side| table == side.table.as_str() || table == side.alias.as_str())
13210 {
13211 push_join_column(&side.table, column, out);
13212 }
13213 }
13214 Projection::Field(FieldRef::NodeProperty { alias, property }, _)
13215 | Projection::Field(FieldRef::EdgeProperty { alias, property }, _) => {
13216 push_qualified_join_column(alias, property, left, right, out);
13217 }
13218 Projection::Function(_, args) => {
13219 for arg in args {
13220 collect_projection_columns_for_join_side(arg, left, right, out)?;
13221 }
13222 }
13223 Projection::Expression(_, _) | Projection::All | Projection::Field(_, _) => {}
13224 Projection::Window { args, .. } => {
13225 for arg in args {
13226 collect_projection_columns_for_join_side(arg, left, right, out)?;
13227 }
13228 }
13229 }
13230 Ok(())
13231}
13232
13233fn split_qualified_column(column: &str) -> Option<(&str, &str)> {
13234 let (qualifier, column) = column.split_once('.')?;
13235 if qualifier.is_empty() || column.is_empty() || column.contains('.') {
13236 return None;
13237 }
13238 Some((qualifier, column))
13239}
13240
13241fn push_qualified_join_column(
13242 qualifier: &str,
13243 column: &str,
13244 left: Option<&JoinTableSide>,
13245 right: Option<&JoinTableSide>,
13246 out: &mut HashMap<String, BTreeSet<String>>,
13247) {
13248 if let Some(side) = [left, right]
13249 .into_iter()
13250 .flatten()
13251 .find(|side| qualifier == side.table.as_str() || qualifier == side.alias.as_str())
13252 {
13253 push_join_column(&side.table, column, out);
13254 }
13255}
13256
13257fn push_unqualified_join_column(
13258 column: &str,
13259 left: Option<&JoinTableSide>,
13260 right: Option<&JoinTableSide>,
13261 out: &mut HashMap<String, BTreeSet<String>>,
13262) {
13263 for side in [left, right].into_iter().flatten() {
13264 push_join_column(&side.table, column, out);
13265 }
13266}
13267
13268fn push_join_column(table: &str, column: &str, out: &mut HashMap<String, BTreeSet<String>>) {
13269 if is_policy_column_name(column) {
13270 out.entry(table.to_string())
13271 .or_default()
13272 .insert(column.to_string());
13273 }
13274}
13275
13276fn push_policy_column(column: &str, out: &mut BTreeSet<String>) {
13277 if is_policy_column_name(column) {
13278 out.insert(column.to_string());
13279 }
13280}
13281
13282fn is_policy_column_name(column: &str) -> bool {
13283 !column.is_empty()
13284 && column != "*"
13285 && !column.starts_with("LIT:")
13286 && !column.starts_with("TYPE:")
13287}
13288
13289fn runtime_iam_context(
13290 role: crate::auth::Role,
13291 tenant: Option<&str>,
13292) -> crate::auth::policies::EvalContext {
13293 crate::auth::policies::EvalContext {
13294 principal_tenant: tenant.map(|t| t.to_string()),
13295 current_tenant: tenant.map(|t| t.to_string()),
13296 peer_ip: None,
13297 mfa_present: false,
13298 now_ms: crate::auth::now_ms(),
13299 principal_is_admin_role: role == crate::auth::Role::Admin,
13300 principal_is_platform_scoped: tenant.is_none(),
13301 }
13302}
13303
13304fn explicit_table_projection_columns(
13305 query: &crate::storage::query::ast::TableQuery,
13306) -> Vec<String> {
13307 use crate::storage::query::ast::{FieldRef, Projection};
13308
13309 let mut columns = Vec::new();
13310 for projection in crate::storage::query::sql_lowering::effective_table_projections(query) {
13311 match projection {
13312 Projection::Column(column) | Projection::Alias(column, _) => {
13313 push_unique(&mut columns, column)
13314 }
13315 Projection::Field(FieldRef::TableColumn { column, .. }, _) => {
13316 push_unique(&mut columns, column)
13317 }
13318 _ => {}
13322 }
13323 }
13324 columns
13325}
13326
13327fn explicit_graph_projection_properties(
13328 query: &crate::storage::query::ast::GraphQuery,
13329) -> Vec<String> {
13330 use crate::storage::query::ast::{FieldRef, Projection};
13331
13332 let mut columns = Vec::new();
13333 for projection in &query.return_ {
13334 match projection {
13335 Projection::Field(FieldRef::NodeProperty { property, .. }, _)
13336 | Projection::Field(FieldRef::EdgeProperty { property, .. }, _) => {
13337 push_unique(&mut columns, property.clone())
13338 }
13339 _ => {}
13340 }
13341 }
13342 columns
13343}
13344
13345fn push_unique(columns: &mut Vec<String>, column: String) {
13346 if !columns.iter().any(|existing| existing == &column) {
13347 columns.push(column);
13348 }
13349}
13350
13351fn principal_label(p: &crate::storage::query::ast::PolicyPrincipalRef) -> String {
13352 use crate::storage::query::ast::PolicyPrincipalRef;
13353 match p {
13354 PolicyPrincipalRef::User(u) => match &u.tenant {
13355 Some(t) => format!("user:{t}/{}", u.username),
13356 None => format!("user:{}", u.username),
13357 },
13358 PolicyPrincipalRef::Group(g) => format!("group:{g}"),
13359 }
13360}
13361
13362pub(crate) fn decision_to_strings(
13365 d: &crate::auth::policies::Decision,
13366) -> (String, Option<String>, Option<String>) {
13367 use crate::auth::policies::Decision;
13368 match d {
13369 Decision::Allow {
13370 matched_policy_id,
13371 matched_sid,
13372 } => (
13373 "allow".into(),
13374 Some(matched_policy_id.clone()),
13375 matched_sid.clone(),
13376 ),
13377 Decision::Deny {
13378 matched_policy_id,
13379 matched_sid,
13380 } => (
13381 "deny".into(),
13382 Some(matched_policy_id.clone()),
13383 matched_sid.clone(),
13384 ),
13385 Decision::DefaultDeny => ("default_deny".into(), None, None),
13386 Decision::AdminBypass => ("admin_bypass".into(), None, None),
13387 }
13388}
13389
13390fn relation_scopes_for_query(query: &QueryExpr) -> Vec<String> {
13391 let mut scopes = Vec::new();
13392 collect_relation_scopes(query, &mut scopes);
13393 scopes.sort();
13394 scopes.dedup();
13395 scopes
13396}
13397
13398fn collect_relation_scopes(query: &QueryExpr, scopes: &mut Vec<String>) {
13399 match query {
13400 QueryExpr::Table(table) => {
13401 if !table.table.is_empty() {
13402 scopes.push(table.table.clone());
13403 }
13404 if let Some(alias) = &table.alias {
13405 scopes.push(alias.clone());
13406 }
13407 }
13408 QueryExpr::Join(join) => {
13409 collect_relation_scopes(&join.left, scopes);
13410 collect_relation_scopes(&join.right, scopes);
13411 }
13412 _ => {}
13413 }
13414}
13415
13416fn query_references_outer_scope(query: &QueryExpr, outer_scopes: &[String]) -> bool {
13417 let inner_scopes = relation_scopes_for_query(query);
13418 query_expr_references_outer_scope(query, outer_scopes, &inner_scopes)
13419}
13420
13421fn query_expr_references_outer_scope(
13422 query: &QueryExpr,
13423 outer_scopes: &[String],
13424 inner_scopes: &[String],
13425) -> bool {
13426 match query {
13427 QueryExpr::Table(table) => {
13428 table.select_items.iter().any(|item| match item {
13429 crate::storage::query::ast::SelectItem::Wildcard => false,
13430 crate::storage::query::ast::SelectItem::Expr { expr, .. } => {
13431 expr_references_outer_scope(expr, outer_scopes, inner_scopes)
13432 }
13433 }) || table
13434 .where_expr
13435 .as_ref()
13436 .is_some_and(|expr| expr_references_outer_scope(expr, outer_scopes, inner_scopes))
13437 || table.filter.as_ref().is_some_and(|filter| {
13438 filter_references_outer_scope(filter, outer_scopes, inner_scopes)
13439 })
13440 || table.having_expr.as_ref().is_some_and(|expr| {
13441 expr_references_outer_scope(expr, outer_scopes, inner_scopes)
13442 })
13443 || table.having.as_ref().is_some_and(|filter| {
13444 filter_references_outer_scope(filter, outer_scopes, inner_scopes)
13445 })
13446 || table
13447 .group_by_exprs
13448 .iter()
13449 .any(|expr| expr_references_outer_scope(expr, outer_scopes, inner_scopes))
13450 || table.order_by.iter().any(|clause| {
13451 clause.expr.as_ref().is_some_and(|expr| {
13452 expr_references_outer_scope(expr, outer_scopes, inner_scopes)
13453 })
13454 })
13455 }
13456 QueryExpr::Join(join) => {
13457 query_expr_references_outer_scope(&join.left, outer_scopes, inner_scopes)
13458 || query_expr_references_outer_scope(&join.right, outer_scopes, inner_scopes)
13459 || join.filter.as_ref().is_some_and(|filter| {
13460 filter_references_outer_scope(filter, outer_scopes, inner_scopes)
13461 })
13462 || join.return_items.iter().any(|item| match item {
13463 crate::storage::query::ast::SelectItem::Wildcard => false,
13464 crate::storage::query::ast::SelectItem::Expr { expr, .. } => {
13465 expr_references_outer_scope(expr, outer_scopes, inner_scopes)
13466 }
13467 })
13468 }
13469 _ => false,
13470 }
13471}
13472
13473fn filter_references_outer_scope(
13474 filter: &crate::storage::query::ast::Filter,
13475 outer_scopes: &[String],
13476 inner_scopes: &[String],
13477) -> bool {
13478 use crate::storage::query::ast::Filter;
13479 match filter {
13480 Filter::Compare { field, .. }
13481 | Filter::IsNull(field)
13482 | Filter::IsNotNull(field)
13483 | Filter::In { field, .. }
13484 | Filter::Between { field, .. }
13485 | Filter::Like { field, .. }
13486 | Filter::StartsWith { field, .. }
13487 | Filter::EndsWith { field, .. }
13488 | Filter::Contains { field, .. } => {
13489 field_ref_references_outer_scope(field, outer_scopes, inner_scopes)
13490 }
13491 Filter::CompareFields { left, right, .. } => {
13492 field_ref_references_outer_scope(left, outer_scopes, inner_scopes)
13493 || field_ref_references_outer_scope(right, outer_scopes, inner_scopes)
13494 }
13495 Filter::CompareExpr { lhs, rhs, .. } => {
13496 expr_references_outer_scope(lhs, outer_scopes, inner_scopes)
13497 || expr_references_outer_scope(rhs, outer_scopes, inner_scopes)
13498 }
13499 Filter::And(left, right) | Filter::Or(left, right) => {
13500 filter_references_outer_scope(left, outer_scopes, inner_scopes)
13501 || filter_references_outer_scope(right, outer_scopes, inner_scopes)
13502 }
13503 Filter::Not(inner) => filter_references_outer_scope(inner, outer_scopes, inner_scopes),
13504 }
13505}
13506
13507fn expr_references_outer_scope(
13508 expr: &crate::storage::query::ast::Expr,
13509 outer_scopes: &[String],
13510 inner_scopes: &[String],
13511) -> bool {
13512 use crate::storage::query::ast::Expr;
13513 match expr {
13514 Expr::Column { field, .. } => {
13515 field_ref_references_outer_scope(field, outer_scopes, inner_scopes)
13516 }
13517 Expr::BinaryOp { lhs, rhs, .. } => {
13518 expr_references_outer_scope(lhs, outer_scopes, inner_scopes)
13519 || expr_references_outer_scope(rhs, outer_scopes, inner_scopes)
13520 }
13521 Expr::UnaryOp { operand, .. }
13522 | Expr::Cast { inner: operand, .. }
13523 | Expr::IsNull { operand, .. } => {
13524 expr_references_outer_scope(operand, outer_scopes, inner_scopes)
13525 }
13526 Expr::FunctionCall { args, .. } => args
13527 .iter()
13528 .any(|arg| expr_references_outer_scope(arg, outer_scopes, inner_scopes)),
13529 Expr::Case {
13530 branches, else_, ..
13531 } => {
13532 branches.iter().any(|(cond, value)| {
13533 expr_references_outer_scope(cond, outer_scopes, inner_scopes)
13534 || expr_references_outer_scope(value, outer_scopes, inner_scopes)
13535 }) || else_
13536 .as_ref()
13537 .is_some_and(|expr| expr_references_outer_scope(expr, outer_scopes, inner_scopes))
13538 }
13539 Expr::InList { target, values, .. } => {
13540 expr_references_outer_scope(target, outer_scopes, inner_scopes)
13541 || values
13542 .iter()
13543 .any(|value| expr_references_outer_scope(value, outer_scopes, inner_scopes))
13544 }
13545 Expr::Between {
13546 target, low, high, ..
13547 } => {
13548 expr_references_outer_scope(target, outer_scopes, inner_scopes)
13549 || expr_references_outer_scope(low, outer_scopes, inner_scopes)
13550 || expr_references_outer_scope(high, outer_scopes, inner_scopes)
13551 }
13552 Expr::Subquery { query, .. } => query_references_outer_scope(&query.query, inner_scopes),
13553 Expr::Literal { .. } | Expr::Parameter { .. } => false,
13554 Expr::WindowFunctionCall { args, window, .. } => {
13555 args.iter()
13556 .any(|arg| expr_references_outer_scope(arg, outer_scopes, inner_scopes))
13557 || window
13558 .partition_by
13559 .iter()
13560 .any(|e| expr_references_outer_scope(e, outer_scopes, inner_scopes))
13561 || window
13562 .order_by
13563 .iter()
13564 .any(|o| expr_references_outer_scope(&o.expr, outer_scopes, inner_scopes))
13565 }
13566 }
13567}
13568
13569fn field_ref_references_outer_scope(
13570 field: &crate::storage::query::ast::FieldRef,
13571 outer_scopes: &[String],
13572 inner_scopes: &[String],
13573) -> bool {
13574 match field {
13575 crate::storage::query::ast::FieldRef::TableColumn { table, .. } if !table.is_empty() => {
13576 outer_scopes.iter().any(|scope| scope == table)
13577 && !inner_scopes.iter().any(|scope| scope == table)
13578 }
13579 _ => false,
13580 }
13581}
13582
13583fn first_column_values(
13584 result: crate::storage::query::unified::UnifiedResult,
13585) -> RedDBResult<Vec<Value>> {
13586 if result.columns.len() > 1 {
13587 return Err(RedDBError::Query(
13588 "expression subquery must return exactly one column".to_string(),
13589 ));
13590 }
13591 let fallback_column = result
13592 .records
13593 .first()
13594 .and_then(|record| record.column_names().into_iter().next())
13595 .map(|name| name.to_string());
13596 let column = result.columns.first().cloned().or(fallback_column);
13597 let Some(column) = column else {
13598 return Ok(Vec::new());
13599 };
13600 Ok(result
13601 .records
13602 .iter()
13603 .map(|record| record.get(column.as_str()).cloned().unwrap_or(Value::Null))
13604 .collect())
13605}
13606
13607fn parse_timestamp_to_ms(s: &str) -> Option<u128> {
13608 if let Ok(n) = s.parse::<u128>() {
13610 return Some(n);
13611 }
13612 if let Some(date) = s.split_whitespace().next() {
13616 let parts: Vec<&str> = date.split('-').collect();
13617 if parts.len() == 3 {
13618 let (y, m, d) = (parts[0], parts[1], parts[2]);
13619 if let (Ok(y), Ok(m), Ok(d)) = (y.parse::<i64>(), m.parse::<u32>(), d.parse::<u32>()) {
13620 let days_in = days_from_civil(y, m, d);
13624 return Some((days_in as u128) * 86_400_000u128);
13625 }
13626 }
13627 }
13628 None
13629}
13630
13631fn days_from_civil(y: i64, m: u32, d: u32) -> i64 {
13634 let y = if m <= 2 { y - 1 } else { y };
13635 let era = if y >= 0 { y } else { y - 399 } / 400;
13636 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;
13638 let doe = yoe * 365 + yoe / 4 - yoe / 100 + doy;
13639 era * 146097 + doe as i64 - 719468
13640}
13641
13642fn walk_plan_node(
13643 node: &crate::storage::query::planner::CanonicalLogicalNode,
13644 depth: usize,
13645 out: &mut Vec<crate::storage::query::unified::UnifiedRecord>,
13646) {
13647 use std::sync::Arc;
13648 let mut rec = crate::storage::query::unified::UnifiedRecord::default();
13649 rec.set_arc(Arc::from("op"), Value::text(node.operator.clone()));
13650 rec.set_arc(
13651 Arc::from("source"),
13652 node.source.clone().map(Value::text).unwrap_or(Value::Null),
13653 );
13654 rec.set_arc(Arc::from("est_rows"), Value::Float(node.estimated_rows));
13655 rec.set_arc(Arc::from("est_cost"), Value::Float(node.operator_cost));
13656 rec.set_arc(Arc::from("depth"), Value::Integer(depth as i64));
13657 out.push(rec);
13658 for child in &node.children {
13659 walk_plan_node(child, depth + 1, out);
13660 }
13661}
13662
13663#[cfg(test)]
13664mod inline_graph_tvf_tests {
13665 use super::*;
13666
13667 fn scopes_for(sql: &str) -> HashSet<String> {
13668 let expr = crate::storage::query::parser::parse(sql)
13669 .expect("parse")
13670 .query;
13671 query_expr_result_cache_scopes(&expr)
13672 }
13673
13674 #[test]
13675 fn inline_tvf_cache_scopes_include_source_collections() {
13676 let scopes = scopes_for(
13680 "SELECT * FROM components(nodes => (SELECT id FROM hosts), edges => (SELECT src, dst FROM links))",
13681 );
13682 assert!(scopes.contains("hosts"), "nodes source scoped: {scopes:?}");
13683 assert!(scopes.contains("links"), "edges source scoped: {scopes:?}");
13684 }
13685
13686 #[test]
13687 fn graph_collection_tvf_cache_scope_is_graph_argument() {
13688 let scopes = scopes_for("SELECT * FROM components(g)");
13692 assert!(scopes.contains("g"), "collection form scoped: {scopes:?}");
13693 }
13694
13695 #[test]
13696 fn abstract_degree_centrality_counts_undirected_endpoints() {
13697 let nodes = vec!["a".to_string(), "b".to_string(), "c".to_string()];
13698 let edges = vec![
13699 ("a".to_string(), "b".to_string(), 1.0_f32),
13700 ("b".to_string(), "c".to_string(), 1.0_f32),
13701 ];
13702 let degrees = abstract_degree_centrality(&nodes, &edges);
13703 assert_eq!(
13704 degrees,
13705 vec![
13706 ("a".to_string(), 1),
13707 ("b".to_string(), 2),
13708 ("c".to_string(), 1),
13709 ]
13710 );
13711 }
13712}