Skip to main content

cratestack_core/schema/
view.rs

1//! View IR — the `view <Name> from <Model>, ... { ... }` block.
2//!
3//! Views are read-only, SQL-defined projections over one or more
4//! existing `model` blocks (see ADR-0003). Their fields, attributes,
5//! and span tracking mirror [`Model`](super::Model); the extra state is
6//! the explicit source-model dependency list and the per-backend SQL
7//! bodies parsed out of `@@server_sql` / `@@embedded_sql` / `@@sql`.
8
9use serde::{Deserialize, Serialize};
10
11use super::{Attribute, Field, SourceSpan};
12
13#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
14pub struct View {
15    pub docs: Vec<String>,
16    pub name: String,
17    pub name_span: SourceSpan,
18    /// The `from <Model>, <Model>, ...` dependency list. Source model
19    /// names are stored as raw identifiers — the validator resolves
20    /// them against the schema's models. Carries spans so error
21    /// reporting can point at the offending identifier.
22    pub sources: Vec<ViewSource>,
23    pub fields: Vec<Field>,
24    /// Block-level attributes — `@@server_sql`, `@@embedded_sql`,
25    /// `@@sql`, `@@materialized`, `@@no_unique`, `@@allow("read", …)`.
26    /// Stored raw; helper methods below extract typed views.
27    pub attributes: Vec<Attribute>,
28    pub span: SourceSpan,
29}
30
31#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
32pub struct ViewSource {
33    pub name: String,
34    pub name_span: SourceSpan,
35}
36
37impl View {
38    /// Returns the SQL body declared via `@@server_sql("…")`, or the
39    /// `@@sql("…")` shorthand if no backend-specific body is set.
40    /// `None` means the view is embedded-only.
41    pub fn server_sql(&self) -> Option<&str> {
42        self.body_attribute("@@server_sql")
43            .or_else(|| self.body_attribute("@@sql"))
44    }
45
46    /// Returns the SQL body declared via `@@embedded_sql("…")`, or the
47    /// `@@sql("…")` shorthand if no backend-specific body is set.
48    /// `None` means the view is server-only.
49    pub fn embedded_sql(&self) -> Option<&str> {
50        self.body_attribute("@@embedded_sql")
51            .or_else(|| self.body_attribute("@@sql"))
52    }
53
54    /// `true` if the view was declared with `@@materialized`.
55    /// Materialized views are server-only — the embedded composer
56    /// emits a hard compile error when it encounters one.
57    pub fn is_materialized(&self) -> bool {
58        self.has_bare_attribute("@@materialized")
59    }
60
61    /// `true` if the view opts out of a natural unique key via
62    /// `@@no_unique`. Drops `find_unique` from the generated delegate.
63    pub fn no_unique(&self) -> bool {
64        self.has_bare_attribute("@@no_unique")
65    }
66
67    fn body_attribute(&self, prefix: &str) -> Option<&str> {
68        self.attributes
69            .iter()
70            .filter(|attr| attr.raw.starts_with(prefix))
71            .find_map(|attr| extract_sql_body(&attr.raw, prefix))
72    }
73
74    fn has_bare_attribute(&self, name: &str) -> bool {
75        self.attributes.iter().any(|attr| {
76            let trimmed = attr.raw.trim();
77            trimmed == name || trimmed.starts_with(&format!("{name}(")) || trimmed == name
78        })
79    }
80}
81
82/// Extract the SQL body from an attribute like `@@server_sql("…")`.
83/// Accepts both `"single-line"` and `"""multi-line"""` strings. The
84/// outer quotes are stripped; embedded newlines and quotes are
85/// preserved verbatim.
86fn extract_sql_body<'a>(raw: &'a str, prefix: &str) -> Option<&'a str> {
87    let after_prefix = raw.strip_prefix(prefix)?.trim_start();
88    let inside_parens = after_prefix
89        .strip_prefix('(')?
90        .rsplit_once(')')
91        .map(|(body, _tail)| body)?
92        .trim();
93    if let Some(rest) = inside_parens.strip_prefix("\"\"\"") {
94        rest.strip_suffix("\"\"\"")
95    } else if let Some(rest) = inside_parens.strip_prefix('"') {
96        rest.strip_suffix('"')
97    } else {
98        None
99    }
100}