Skip to content

Commit 23b5651

Browse files
hovaescoclaude
andauthored
Task LAV-2529: parse SEMANTIC VIEW DDL in the vendored parser (#2937)
Add real grammar for the five SEMANTIC VIEW DDL statements (CREATE/ALTER/DROP/DESCRIBE/SHOW) as boxed Statement variants that preserve every clause list in declaration order, with each element carrying its alias, expression, synonyms, tags, access modifier and comment. Allow-list all five in the identifier gate (090106 unprobed). No rewriter arms yet — statements parse and round-trip only. Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
1 parent c2f539b commit 23b5651

6 files changed

Lines changed: 798 additions & 4 deletions

File tree

‎src/ast/ddl.rs‎

Lines changed: 275 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1546,6 +1546,281 @@ impl fmt::Display for AlterPasswordPolicyOperation {
15461546
}
15471547
}
15481548

1549+
/// The `PRIVATE` / `PUBLIC` access modifier on a semantic view fact or metric.
1550+
///
1551+
/// See <https://docs.snowflake.com/en/sql-reference/sql/create-semantic-view>
1552+
#[derive(Debug, Clone, Copy, PartialEq, PartialOrd, Eq, Ord, Hash)]
1553+
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
1554+
#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))]
1555+
pub enum SemanticViewColumnAccess {
1556+
/// `PRIVATE` — the fact or metric cannot be queried directly.
1557+
Private,
1558+
/// `PUBLIC` — the default; the element can be queried.
1559+
Public,
1560+
}
1561+
1562+
impl fmt::Display for SemanticViewColumnAccess {
1563+
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
1564+
match self {
1565+
SemanticViewColumnAccess::Private => write!(f, "PRIVATE"),
1566+
SemanticViewColumnAccess::Public => write!(f, "PUBLIC"),
1567+
}
1568+
}
1569+
}
1570+
1571+
fn fmt_semantic_view_synonyms(f: &mut fmt::Formatter, synonyms: &[String]) -> fmt::Result {
1572+
write!(f, " WITH SYNONYMS (")?;
1573+
for (i, synonym) in synonyms.iter().enumerate() {
1574+
if i > 0 {
1575+
write!(f, ", ")?;
1576+
}
1577+
write!(f, "'{}'", escape_single_quote_string(synonym))?;
1578+
}
1579+
write!(f, ")")
1580+
}
1581+
1582+
/// A logical table in the `TABLES (...)` clause of a `CREATE SEMANTIC VIEW`.
1583+
#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
1584+
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
1585+
#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))]
1586+
pub struct SemanticViewTable {
1587+
/// Optional `<alias> AS` used to refer to the table elsewhere.
1588+
pub alias: Option<Ident>,
1589+
/// The physical table name.
1590+
pub name: ObjectName,
1591+
/// `PRIMARY KEY ( ... )` columns (empty if absent).
1592+
pub primary_key: Vec<Ident>,
1593+
/// Zero or more `UNIQUE ( ... )` column groups.
1594+
pub unique: Vec<Vec<Ident>>,
1595+
/// `WITH SYNONYMS ( ... )` values (empty if absent).
1596+
pub synonyms: Vec<String>,
1597+
/// `WITH TAG ( ... )` entries (empty if absent).
1598+
pub tags: Vec<Tag>,
1599+
/// `COMMENT = '...'`.
1600+
pub comment: Option<String>,
1601+
}
1602+
1603+
impl fmt::Display for SemanticViewTable {
1604+
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
1605+
if let Some(alias) = &self.alias {
1606+
write!(f, "{alias} AS ")?;
1607+
}
1608+
write!(f, "{}", self.name)?;
1609+
if !self.primary_key.is_empty() {
1610+
write!(f, " PRIMARY KEY ({})", display_comma_separated(&self.primary_key))?;
1611+
}
1612+
for unique in &self.unique {
1613+
write!(f, " UNIQUE ({})", display_comma_separated(unique))?;
1614+
}
1615+
if !self.synonyms.is_empty() {
1616+
fmt_semantic_view_synonyms(f, &self.synonyms)?;
1617+
}
1618+
if !self.tags.is_empty() {
1619+
write!(f, " WITH TAG ({})", display_comma_separated(&self.tags))?;
1620+
}
1621+
if let Some(comment) = &self.comment {
1622+
write!(f, " COMMENT = '{}'", escape_single_quote_string(comment))?;
1623+
}
1624+
Ok(())
1625+
}
1626+
}
1627+
1628+
/// A relationship in the `RELATIONSHIPS (...)` clause of a `CREATE SEMANTIC VIEW`.
1629+
#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
1630+
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
1631+
#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))]
1632+
pub struct SemanticViewRelationship {
1633+
/// Optional `<identifier> AS` naming the relationship.
1634+
pub identifier: Option<Ident>,
1635+
/// The referencing logical table alias.
1636+
pub table: Ident,
1637+
/// The referencing columns.
1638+
pub columns: Vec<Ident>,
1639+
/// The referenced logical table.
1640+
pub ref_table: ObjectName,
1641+
/// The referenced columns (empty when omitted).
1642+
pub ref_columns: Vec<Ident>,
1643+
}
1644+
1645+
impl fmt::Display for SemanticViewRelationship {
1646+
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
1647+
if let Some(identifier) = &self.identifier {
1648+
write!(f, "{identifier} AS ")?;
1649+
}
1650+
write!(
1651+
f,
1652+
"{} ({}) REFERENCES {}",
1653+
self.table,
1654+
display_comma_separated(&self.columns),
1655+
self.ref_table
1656+
)?;
1657+
if !self.ref_columns.is_empty() {
1658+
write!(f, " ({})", display_comma_separated(&self.ref_columns))?;
1659+
}
1660+
Ok(())
1661+
}
1662+
}
1663+
1664+
/// A fact, dimension, or metric semantic expression in a `CREATE SEMANTIC VIEW`.
1665+
#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
1666+
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
1667+
#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))]
1668+
pub struct SemanticViewExpr {
1669+
/// Optional `PRIVATE` / `PUBLIC` access modifier.
1670+
pub access: Option<SemanticViewColumnAccess>,
1671+
/// The (optionally table-qualified) element name.
1672+
pub name: ObjectName,
1673+
/// The `AS <sql_expr>` definition, when present.
1674+
pub expr: Option<Expr>,
1675+
/// `WITH SYNONYMS ( ... )` values (empty if absent).
1676+
pub synonyms: Vec<String>,
1677+
/// `WITH TAG ( ... )` entries (empty if absent).
1678+
pub tags: Vec<Tag>,
1679+
/// `COMMENT = '...'`.
1680+
pub comment: Option<String>,
1681+
}
1682+
1683+
impl fmt::Display for SemanticViewExpr {
1684+
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
1685+
if let Some(access) = &self.access {
1686+
write!(f, "{access} ")?;
1687+
}
1688+
write!(f, "{}", self.name)?;
1689+
if let Some(expr) = &self.expr {
1690+
write!(f, " AS {expr}")?;
1691+
}
1692+
if !self.synonyms.is_empty() {
1693+
fmt_semantic_view_synonyms(f, &self.synonyms)?;
1694+
}
1695+
if !self.tags.is_empty() {
1696+
write!(f, " WITH TAG ({})", display_comma_separated(&self.tags))?;
1697+
}
1698+
if let Some(comment) = &self.comment {
1699+
write!(f, " COMMENT = '{}'", escape_single_quote_string(comment))?;
1700+
}
1701+
Ok(())
1702+
}
1703+
}
1704+
1705+
/// One clause of a `CREATE SEMANTIC VIEW`. The clauses are stored in
1706+
/// declaration order so the original ordering survives round-tripping.
1707+
#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
1708+
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
1709+
#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))]
1710+
pub enum SemanticViewClause {
1711+
/// `TABLES ( ... )`
1712+
Tables(Vec<SemanticViewTable>),
1713+
/// `RELATIONSHIPS ( ... )`
1714+
Relationships(Vec<SemanticViewRelationship>),
1715+
/// `FACTS ( ... )`
1716+
Facts(Vec<SemanticViewExpr>),
1717+
/// `DIMENSIONS ( ... )`
1718+
Dimensions(Vec<SemanticViewExpr>),
1719+
/// `METRICS ( ... )`
1720+
Metrics(Vec<SemanticViewExpr>),
1721+
}
1722+
1723+
impl fmt::Display for SemanticViewClause {
1724+
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
1725+
match self {
1726+
SemanticViewClause::Tables(items) => {
1727+
write!(f, "TABLES ({})", display_comma_separated(items))
1728+
}
1729+
SemanticViewClause::Relationships(items) => {
1730+
write!(f, "RELATIONSHIPS ({})", display_comma_separated(items))
1731+
}
1732+
SemanticViewClause::Facts(items) => {
1733+
write!(f, "FACTS ({})", display_comma_separated(items))
1734+
}
1735+
SemanticViewClause::Dimensions(items) => {
1736+
write!(f, "DIMENSIONS ({})", display_comma_separated(items))
1737+
}
1738+
SemanticViewClause::Metrics(items) => {
1739+
write!(f, "METRICS ({})", display_comma_separated(items))
1740+
}
1741+
}
1742+
}
1743+
}
1744+
1745+
/// The payload of a `CREATE [OR REPLACE] SEMANTIC VIEW` statement.
1746+
#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
1747+
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
1748+
#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))]
1749+
pub struct CreateSemanticView {
1750+
/// `OR REPLACE` flag.
1751+
pub or_replace: bool,
1752+
/// `IF NOT EXISTS` flag.
1753+
pub if_not_exists: bool,
1754+
/// The semantic view name.
1755+
pub name: ObjectName,
1756+
/// The clause lists in declaration order.
1757+
pub clauses: Vec<SemanticViewClause>,
1758+
/// `COMMENT = '...'`.
1759+
pub comment: Option<String>,
1760+
}
1761+
1762+
impl fmt::Display for CreateSemanticView {
1763+
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
1764+
write!(
1765+
f,
1766+
"CREATE {or_replace}SEMANTIC VIEW {if_not_exists}{name}",
1767+
or_replace = if self.or_replace { "OR REPLACE " } else { "" },
1768+
if_not_exists = if self.if_not_exists { "IF NOT EXISTS " } else { "" },
1769+
name = self.name,
1770+
)?;
1771+
for clause in &self.clauses {
1772+
write!(f, " {clause}")?;
1773+
}
1774+
if let Some(comment) = &self.comment {
1775+
write!(f, " COMMENT = '{}'", escape_single_quote_string(comment))?;
1776+
}
1777+
Ok(())
1778+
}
1779+
}
1780+
1781+
/// An operation in an `ALTER SEMANTIC VIEW` statement.
1782+
#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
1783+
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
1784+
#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))]
1785+
pub enum AlterSemanticViewOperation {
1786+
/// `RENAME TO <name>`
1787+
RenameTo {
1788+
/// The new semantic view name.
1789+
new_name: ObjectName,
1790+
},
1791+
/// `SET COMMENT = '...'`
1792+
SetComment {
1793+
/// The comment value.
1794+
value: String,
1795+
},
1796+
/// `UNSET COMMENT`
1797+
UnsetComment,
1798+
/// `SET TAG <tag> = '<value>' [ , ... ]`
1799+
SetTags(Vec<Tag>),
1800+
/// `UNSET TAG <tag> [ , ... ]`
1801+
UnsetTags(Vec<ObjectName>),
1802+
}
1803+
1804+
impl fmt::Display for AlterSemanticViewOperation {
1805+
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
1806+
match self {
1807+
AlterSemanticViewOperation::RenameTo { new_name } => {
1808+
write!(f, "RENAME TO {new_name}")
1809+
}
1810+
AlterSemanticViewOperation::SetComment { value } => {
1811+
write!(f, "SET COMMENT = '{}'", escape_single_quote_string(value))
1812+
}
1813+
AlterSemanticViewOperation::UnsetComment => write!(f, "UNSET COMMENT"),
1814+
AlterSemanticViewOperation::SetTags(tags) => {
1815+
write!(f, "SET TAG {}", display_comma_separated(tags))
1816+
}
1817+
AlterSemanticViewOperation::UnsetTags(tags) => {
1818+
write!(f, "UNSET TAG {}", display_comma_separated(tags))
1819+
}
1820+
}
1821+
}
1822+
}
1823+
15491824
/// An operation on a session policy in an `ALTER SESSION POLICY` statement.
15501825
#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
15511826
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]

‎src/ast/mod.rs‎

Lines changed: 88 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,8 @@ pub use self::ddl::{
6969
AlterOperatorClass, AlterOperatorClassOperation, AlterOperatorFamily,
7070
AlterAuthenticationPolicyOperation, AlterOperatorFamilyOperation, AlterOperatorOperation,
7171
AlterPasswordPolicyOperation, AlterPolicy, AlterPolicyOperation, AlterProcedure,
72-
AlterProcedureOperation, AlterSchema, AlterSchemaOperation, AlterSessionPolicyOperation,
72+
AlterProcedureOperation, AlterSchema, AlterSchemaOperation, AlterSemanticViewOperation,
73+
AlterSessionPolicyOperation,
7374
AlterSnowflakeSecretOperation, AlterTable, AlterTableAlgorithm, AlterTableLock,
7475
AlterTableOperation, AlterTableType, AlterTagOperation, AlterType, AlterTypeAddValue,
7576
AlterTypeAddValuePosition, AlterTypeOperation, AlterTypeRename, AlterTypeRenameValue,
@@ -86,7 +87,9 @@ pub use self::ddl::{
8687
IndexOption, IndexType, KeyOrIndexDisplay, Msck, NullsDistinctOption, OperatorArgTypes,
8788
OperatorClassItem, OperatorFamilyDropItem, OperatorFamilyItem, OperatorOption, OperatorPurpose,
8889
Owner, Partition, PartitionBoundValue, ProcedureExecuteAs, ProcedureParam, ReferentialAction,
89-
RenameTableNameKind, ReplicaIdentity, TagsColumnOption, TriggerObjectKind, Truncate,
90+
RenameTableNameKind, ReplicaIdentity, CreateSemanticView, SemanticViewClause,
91+
SemanticViewColumnAccess, SemanticViewExpr, SemanticViewRelationship, SemanticViewTable,
92+
TagsColumnOption, TriggerObjectKind, Truncate,
9093
UserDefinedTypeCompositeAttributeDef, UserDefinedTypeInternalLength,
9194
UserDefinedTypeRangeOption, UserDefinedTypeRepresentation, UserDefinedTypeSqlDefinitionOption,
9295
UserDefinedTypeStorage, ViewColumnDef,
@@ -5796,6 +5799,57 @@ pub enum Statement {
57965799
on_entity: Option<ShowPolicyEntity>,
57975800
},
57985801
/// ```sql
5802+
/// CREATE [OR REPLACE] SEMANTIC VIEW [IF NOT EXISTS] <name>
5803+
/// TABLES ( ... ) [ RELATIONSHIPS ( ... ) ] [ FACTS ( ... ) ]
5804+
/// [ DIMENSIONS ( ... ) ] [ METRICS ( ... ) ] [ COMMENT = '<comment>' ]
5805+
/// ```
5806+
/// The payload is boxed to keep the `Statement` enum small (the clause
5807+
/// lists are recursive/expression-bearing). See
5808+
/// <https://docs.snowflake.com/en/sql-reference/sql/create-semantic-view>
5809+
CreateSemanticView(Box<CreateSemanticView>),
5810+
/// ```sql
5811+
/// ALTER SEMANTIC VIEW [IF EXISTS] <name>
5812+
/// { RENAME TO <name> | SET COMMENT = '...' | UNSET COMMENT
5813+
/// | SET TAG <t> = '<v>' [, ...] | UNSET TAG <t> [, ...] }
5814+
/// ```
5815+
/// See <https://docs.snowflake.com/en/sql-reference/sql/alter-semantic-view>
5816+
AlterSemanticView {
5817+
/// `IF EXISTS` flag.
5818+
if_exists: bool,
5819+
/// The semantic view name.
5820+
name: ObjectName,
5821+
/// The operation to apply.
5822+
operation: AlterSemanticViewOperation,
5823+
},
5824+
/// ```sql
5825+
/// DROP SEMANTIC VIEW [IF EXISTS] <name>
5826+
/// ```
5827+
/// See <https://docs.snowflake.com/en/sql-reference/sql/drop-semantic-view>
5828+
DropSemanticView {
5829+
/// `IF EXISTS` flag.
5830+
if_exists: bool,
5831+
/// The semantic view name.
5832+
name: ObjectName,
5833+
},
5834+
/// ```sql
5835+
/// DESC[RIBE] SEMANTIC VIEW <name>
5836+
/// ```
5837+
/// See <https://docs.snowflake.com/en/sql-reference/sql/desc-semantic-view>
5838+
DescribeSemanticView {
5839+
/// The semantic view name.
5840+
name: ObjectName,
5841+
},
5842+
/// ```sql
5843+
/// SHOW [TERSE] SEMANTIC VIEWS [ LIKE '<pattern>' ] [ IN <scope> ]
5844+
/// ```
5845+
/// See <https://docs.snowflake.com/en/sql-reference/sql/show-semantic-views>
5846+
ShowSemanticViews {
5847+
/// `TERSE` flag.
5848+
terse: bool,
5849+
/// Options controlling the SHOW output (filter, `IN <scope>`, etc.).
5850+
show_options: ShowStatementOptions,
5851+
},
5852+
/// ```sql
57995853
/// CREATE [OR REPLACE] SESSION POLICY [IF NOT EXISTS] <name>
58005854
/// [ <property> = <value> ... ] [ COMMENT = '<comment>' ]
58015855
/// ```
@@ -9184,6 +9238,38 @@ impl fmt::Display for Statement {
91849238
}
91859239
Ok(())
91869240
}
9241+
Statement::CreateSemanticView(create) => write!(f, "{create}"),
9242+
Statement::AlterSemanticView {
9243+
if_exists,
9244+
name,
9245+
operation,
9246+
} => {
9247+
write!(
9248+
f,
9249+
"ALTER SEMANTIC VIEW {if_exists}{name} {operation}",
9250+
if_exists = if *if_exists { "IF EXISTS " } else { "" },
9251+
)
9252+
}
9253+
Statement::DropSemanticView { if_exists, name } => {
9254+
write!(
9255+
f,
9256+
"DROP SEMANTIC VIEW {if_exists}{name}",
9257+
if_exists = if *if_exists { "IF EXISTS " } else { "" },
9258+
)
9259+
}
9260+
Statement::DescribeSemanticView { name } => {
9261+
write!(f, "DESCRIBE SEMANTIC VIEW {name}")
9262+
}
9263+
Statement::ShowSemanticViews {
9264+
terse,
9265+
show_options,
9266+
} => {
9267+
write!(
9268+
f,
9269+
"SHOW {terse}SEMANTIC VIEWS{show_options}",
9270+
terse = if *terse { "TERSE " } else { "" },
9271+
)
9272+
}
91879273
Statement::CreateSessionPolicy {
91889274
or_replace,
91899275
if_not_exists,

0 commit comments

Comments
 (0)