Skip to main content

radixdb_executor/mutation/
view_binding.rs

1// Copyright 2026 RadixDB Contributors
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8
9//! SQL-owned binding of durable view dependencies.
10
11use rustc_hash::FxHashSet;
12
13use radixdb_core::{Error, Result};
14use radixdb_sql::ast::{self, SelectStatement, Statement};
15
16pub fn bind_from_select(select: &SelectStatement) -> Vec<String> {
17    let mut dependencies = FxHashSet::default();
18    ast::walk_physical_table_sources(select, &mut |source| {
19        dependencies.insert(source.name.value_lower.to_string());
20    });
21    let mut dependencies: Vec<String> = dependencies.into_iter().collect();
22    dependencies.sort_unstable();
23    dependencies
24}
25
26/// Rebind a persisted view query during recovery without exposing SQL AST to
27/// storage. The storage layer receives only the canonical dependency names.
28pub fn bind_from_sql(query: &str) -> Result<Vec<String>> {
29    let statements = radixdb_sql::parse_sql(query)
30        .map_err(|error| Error::parse(format!("invalid persisted view query: {error}")))?;
31    let [Statement::Select(select)] = statements.as_slice() else {
32        return Err(Error::invalid_argument(
33            "view definition must contain exactly one SELECT statement",
34        ));
35    };
36    Ok(bind_from_select(select))
37}
38
39#[cfg(test)]
40mod tests {
41    use super::*;
42
43    #[test]
44    fn scoped_binding_excludes_cte_aliases_and_normalizes_sources() {
45        assert_eq!(
46            bind_from_sql(
47                "WITH local_rows AS (SELECT * FROM source_rows) \
48                 SELECT * FROM local_rows JOIN other_rows \
49                 ON local_rows.id = other_rows.id",
50            )
51            .unwrap(),
52            vec!["other_rows", "source_rows"],
53        );
54    }
55
56    #[test]
57    fn persisted_binding_rejects_non_select_payloads() {
58        let error = bind_from_sql("DELETE FROM source_rows").unwrap_err();
59        assert!(error.to_string().contains("exactly one SELECT statement"));
60    }
61}