Skip to content

Commit 2ca88f9

Browse files
committed
Add support for SEMANTIC_VIEW table factor
1 parent 12c0878 commit 2ca88f9

File tree

7 files changed

+285
-0
lines changed

7 files changed

+285
-0
lines changed

src/ast/query.rs

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1410,6 +1410,31 @@ pub enum TableFactor {
14101410
/// The alias for the table.
14111411
alias: Option<TableAlias>,
14121412
},
1413+
/// Snowflake's SEMANTIC_VIEW function for semantic models.
1414+
///
1415+
/// <https://docs.snowflake.com/en/user-guide/views-semantic/querying>
1416+
///
1417+
/// ```sql
1418+
/// SELECT * FROM SEMANTIC_VIEW(
1419+
/// tpch_analysis
1420+
/// DIMENSIONS customer.customer_market_segment
1421+
/// METRICS orders.order_average_value
1422+
/// );
1423+
/// ```
1424+
SemanticView {
1425+
/// The name of the semantic model
1426+
name: ObjectName,
1427+
/// List of dimensions (can be expressions like DATE_PART('year', col))
1428+
dimensions: Vec<Expr>,
1429+
/// List of metrics (can be expressions or simple names)
1430+
metrics: Vec<Expr>,
1431+
/// List of facts
1432+
facts: Vec<Expr>,
1433+
/// WHERE clause for filtering
1434+
where_clause: Option<Expr>,
1435+
/// The alias for the table
1436+
alias: Option<TableAlias>,
1437+
},
14131438
}
14141439

14151440
/// The table sample modifier options
@@ -2107,6 +2132,40 @@ impl fmt::Display for TableFactor {
21072132
}
21082133
Ok(())
21092134
}
2135+
TableFactor::SemanticView {
2136+
name,
2137+
dimensions,
2138+
metrics,
2139+
facts,
2140+
where_clause,
2141+
alias,
2142+
} => {
2143+
write!(f, "SEMANTIC_VIEW({name}")?;
2144+
2145+
if !dimensions.is_empty() {
2146+
write!(f, " DIMENSIONS {}", display_comma_separated(dimensions))?;
2147+
}
2148+
2149+
if !metrics.is_empty() {
2150+
write!(f, " METRICS {}", display_comma_separated(metrics))?;
2151+
}
2152+
2153+
if !facts.is_empty() {
2154+
write!(f, " FACTS {}", display_comma_separated(facts))?;
2155+
}
2156+
2157+
if let Some(where_clause) = where_clause {
2158+
write!(f, " WHERE {where_clause}")?;
2159+
}
2160+
2161+
write!(f, ")")?;
2162+
2163+
if let Some(alias) = alias {
2164+
write!(f, " AS {alias}")?;
2165+
}
2166+
2167+
Ok(())
2168+
}
21102169
}
21112170
}
21122171
}

src/ast/spans.rs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2032,6 +2032,23 @@ impl Spanned for TableFactor {
20322032
.chain(symbols.iter().map(|i| i.span()))
20332033
.chain(alias.as_ref().map(|i| i.span())),
20342034
),
2035+
TableFactor::SemanticView {
2036+
name,
2037+
dimensions,
2038+
metrics,
2039+
facts,
2040+
where_clause,
2041+
alias,
2042+
} => union_spans(
2043+
name.0
2044+
.iter()
2045+
.map(|i| i.span())
2046+
.chain(dimensions.iter().map(|d| d.span()))
2047+
.chain(metrics.iter().map(|m| m.span()))
2048+
.chain(facts.iter().map(|f| f.span()))
2049+
.chain(where_clause.as_ref().map(|e| e.span()))
2050+
.chain(alias.as_ref().map(|a| a.span())),
2051+
),
20352052
TableFactor::OpenJsonTable { .. } => Span::empty(),
20362053
}
20372054
}

src/dialect/mod.rs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1182,6 +1182,20 @@ pub trait Dialect: Debug + Any {
11821182
fn supports_create_table_like_parenthesized(&self) -> bool {
11831183
false
11841184
}
1185+
1186+
/// Returns true if the dialect supports `SEMANTIC_VIEW()` table functions.
1187+
///
1188+
/// ```sql
1189+
/// SELECT * FROM SEMANTIC_VIEW(
1190+
/// model_name
1191+
/// DIMENSIONS customer.name, customer.region
1192+
/// METRICS orders.revenue, orders.count
1193+
/// WHERE customer.active = true
1194+
/// )
1195+
/// ```
1196+
fn supports_semantic_view(&self) -> bool {
1197+
false
1198+
}
11851199
}
11861200

11871201
/// This represents the operators for which precedence must be defined

src/dialect/snowflake.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -564,6 +564,10 @@ impl Dialect for SnowflakeDialect {
564564
fn supports_select_wildcard_exclude(&self) -> bool {
565565
true
566566
}
567+
568+
fn supports_semantic_view(&self) -> bool {
569+
true
570+
}
567571
}
568572

569573
// Peeks ahead to identify tokens that are expected after

src/keywords.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -289,6 +289,7 @@ define_keywords!(
289289
DETACH,
290290
DETAIL,
291291
DETERMINISTIC,
292+
DIMENSIONS,
292293
DIRECTORY,
293294
DISABLE,
294295
DISCARD,
@@ -357,6 +358,7 @@ define_keywords!(
357358
EXTERNAL,
358359
EXTERNAL_VOLUME,
359360
EXTRACT,
361+
FACTS,
360362
FAIL,
361363
FAILOVER,
362364
FALSE,
@@ -560,6 +562,7 @@ define_keywords!(
560562
METADATA,
561563
METHOD,
562564
METRIC,
565+
METRICS,
563566
MICROSECOND,
564567
MICROSECONDS,
565568
MILLENIUM,
@@ -817,6 +820,7 @@ define_keywords!(
817820
SECURITY,
818821
SEED,
819822
SELECT,
823+
SEMANTIC_VIEW,
820824
SEMI,
821825
SENSITIVE,
822826
SEPARATOR,

src/parser/mod.rs

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13324,6 +13324,7 @@ impl<'a> Parser<'a> {
1332413324
| TableFactor::Pivot { alias, .. }
1332513325
| TableFactor::Unpivot { alias, .. }
1332613326
| TableFactor::MatchRecognize { alias, .. }
13327+
| TableFactor::SemanticView { alias, .. }
1332713328
| TableFactor::NestedJoin { alias, .. } => {
1332813329
// but not `FROM (mytable AS alias1) AS alias2`.
1332913330
if let Some(inner_alias) = alias {
@@ -13438,6 +13439,10 @@ impl<'a> Parser<'a> {
1343813439
} else if self.parse_keyword_with_tokens(Keyword::XMLTABLE, &[Token::LParen]) {
1343913440
self.prev_token();
1344013441
self.parse_xml_table_factor()
13442+
} else if self.dialect.supports_semantic_view()
13443+
&& self.parse_keyword_with_tokens(Keyword::SEMANTIC_VIEW, &[Token::LParen])
13444+
{
13445+
self.parse_semantic_view_table_factor()
1344113446
} else {
1344213447
let name = self.parse_object_name(true)?;
1344313448

@@ -13769,6 +13774,63 @@ impl<'a> Parser<'a> {
1376913774
Ok(XmlPassingClause { arguments })
1377013775
}
1377113776

13777+
fn parse_semantic_view_table_factor(&mut self) -> Result<TableFactor, ParserError> {
13778+
let name = self.parse_object_name(true)?;
13779+
13780+
// Parse DIMENSIONS, METRICS, and FACTS clauses in flexible order
13781+
let mut dimensions = Vec::new();
13782+
let mut metrics = Vec::new();
13783+
let mut facts = Vec::new();
13784+
13785+
// Keep parsing DIMENSIONS, METRICS, and FACTS clauses until we hit WHERE or closing parenthesis
13786+
while self.peek_token().token != Token::RParen {
13787+
if self.parse_keyword(Keyword::DIMENSIONS) {
13788+
if !dimensions.is_empty() {
13789+
return Err(ParserError::ParserError(
13790+
"DIMENSIONS clause can only be specified once".to_string(),
13791+
));
13792+
}
13793+
dimensions = self.parse_comma_separated(Parser::parse_expr)?;
13794+
} else if self.parse_keyword(Keyword::METRICS) {
13795+
if !metrics.is_empty() {
13796+
return Err(ParserError::ParserError(
13797+
"METRICS clause can only be specified once".to_string(),
13798+
));
13799+
}
13800+
metrics = self.parse_comma_separated(Parser::parse_expr)?;
13801+
} else if self.parse_keyword(Keyword::FACTS) {
13802+
if !facts.is_empty() {
13803+
return Err(ParserError::ParserError(
13804+
"FACTS clause can only be specified once".to_string(),
13805+
));
13806+
}
13807+
facts = self.parse_comma_separated(Parser::parse_expr)?;
13808+
} else {
13809+
// If we hit WHERE or anything else, stop parsing these clauses
13810+
break;
13811+
}
13812+
}
13813+
13814+
let where_clause = if self.parse_keyword(Keyword::WHERE) {
13815+
Some(self.parse_expr()?)
13816+
} else {
13817+
None
13818+
};
13819+
13820+
self.expect_token(&Token::RParen)?;
13821+
13822+
let alias = self.maybe_parse_table_alias()?;
13823+
13824+
Ok(TableFactor::SemanticView {
13825+
name,
13826+
dimensions,
13827+
metrics,
13828+
facts,
13829+
where_clause,
13830+
alias,
13831+
})
13832+
}
13833+
1377213834
fn parse_match_recognize(&mut self, table: TableFactor) -> Result<TableFactor, ParserError> {
1377313835
self.expect_token(&Token::LParen)?;
1377413836

tests/sqlparser_snowflake.rs

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4581,3 +4581,128 @@ fn test_drop_constraints() {
45814581
snowflake().verified_stmt("ALTER TABLE tbl DROP FOREIGN KEY k1 RESTRICT");
45824582
snowflake().verified_stmt("ALTER TABLE tbl DROP CONSTRAINT c1 CASCADE");
45834583
}
4584+
4585+
#[test]
4586+
fn test_semantic_view_all_variants() {
4587+
let test_cases = [
4588+
("SELECT * FROM SEMANTIC_VIEW(model)", None),
4589+
(
4590+
"SELECT * FROM SEMANTIC_VIEW(model DIMENSIONS dim1, dim2)",
4591+
None,
4592+
),
4593+
(
4594+
"SELECT * FROM SEMANTIC_VIEW(model METRICS met1, met2)",
4595+
None,
4596+
),
4597+
(
4598+
"SELECT * FROM SEMANTIC_VIEW(model FACTS fact1, fact2)",
4599+
None,
4600+
),
4601+
(
4602+
"SELECT * FROM SEMANTIC_VIEW(model DIMENSIONS dim1 METRICS met1)",
4603+
None,
4604+
),
4605+
(
4606+
"SELECT * FROM SEMANTIC_VIEW(model DIMENSIONS dim1 WHERE x > 0)",
4607+
None,
4608+
),
4609+
(
4610+
"SELECT * FROM SEMANTIC_VIEW(model DIMENSIONS dim1) AS sv",
4611+
None,
4612+
),
4613+
(
4614+
"SELECT * FROM SEMANTIC_VIEW(model DIMENSIONS DATE_PART('year', col))",
4615+
None,
4616+
),
4617+
(
4618+
"SELECT * FROM SEMANTIC_VIEW(model METRICS SUM(col), AVG(col2))",
4619+
None,
4620+
),
4621+
// We can parse in any order bu will always produce a result in a fixed order.
4622+
(
4623+
"SELECT * FROM SEMANTIC_VIEW(model METRICS met1 DIMENSIONS dim1)",
4624+
Some("SELECT * FROM SEMANTIC_VIEW(model DIMENSIONS dim1 METRICS met1)"),
4625+
),
4626+
(
4627+
"SELECT * FROM SEMANTIC_VIEW(model FACTS fact1 DIMENSIONS dim1)",
4628+
Some("SELECT * FROM SEMANTIC_VIEW(model DIMENSIONS dim1 FACTS fact1)"),
4629+
),
4630+
];
4631+
4632+
for (input_sql, expected_sql) in test_cases {
4633+
if let Some(expected) = expected_sql {
4634+
// Test that non-canonical order gets normalized
4635+
let parsed = snowflake().parse_sql_statements(input_sql).unwrap();
4636+
let formatted = parsed[0].to_string();
4637+
assert_eq!(formatted, expected, "Input: {}", input_sql);
4638+
} else {
4639+
// Test round-trip parsing
4640+
snowflake().verified_stmt(input_sql);
4641+
}
4642+
}
4643+
}
4644+
4645+
#[test]
4646+
fn test_semantic_view_negative_cases() {
4647+
// Test invalid syntax that should fail
4648+
let invalid_sqls = [
4649+
"SELECT * FROM SEMANTIC_VIEW(model WHERE x > 0 DIMENSIONS dim1)",
4650+
"SELECT * FROM SEMANTIC_VIEW(model DIMENSIONS dim1 WHERE x > 0 METRICS met1)",
4651+
"SELECT * FROM SEMANTIC_VIEW(model DIMENSIONS dim1 DIMENSIONS dim2)",
4652+
];
4653+
4654+
for sql in invalid_sqls {
4655+
let result = snowflake().parse_sql_statements(sql);
4656+
assert!(result.is_err(), "Expected error for invalid SQL: {}", sql);
4657+
}
4658+
}
4659+
4660+
#[test]
4661+
fn test_semantic_view_ast_structure() {
4662+
// Test that we correctly parse into the expected AST structure
4663+
let sql = r#"SELECT * FROM SEMANTIC_VIEW(
4664+
my_model
4665+
DIMENSIONS DATE_PART('year', date_col), region_name
4666+
METRICS SUM(revenue), COUNT(*)
4667+
WHERE active = true
4668+
) AS model_alias"#;
4669+
4670+
let stmt = snowflake().parse_sql_statements(sql).unwrap();
4671+
match &stmt[0] {
4672+
Statement::Query(q) => {
4673+
if let SetExpr::Select(select) = q.body.as_ref() {
4674+
if let Some(TableWithJoins { relation, .. }) = select.from.first() {
4675+
match relation {
4676+
TableFactor::SemanticView {
4677+
name,
4678+
dimensions,
4679+
metrics,
4680+
facts,
4681+
where_clause,
4682+
alias,
4683+
} => {
4684+
assert_eq!(name.to_string(), "my_model");
4685+
assert_eq!(dimensions.len(), 2);
4686+
assert_eq!(dimensions[0].to_string(), "DATE_PART('year', date_col)");
4687+
assert_eq!(dimensions[1].to_string(), "region_name");
4688+
assert_eq!(metrics.len(), 2);
4689+
assert_eq!(metrics[0].to_string(), "SUM(revenue)");
4690+
assert_eq!(metrics[1].to_string(), "COUNT(*)");
4691+
assert!(facts.is_empty());
4692+
assert!(where_clause.is_some());
4693+
assert_eq!(where_clause.as_ref().unwrap().to_string(), "active = true");
4694+
assert!(alias.is_some());
4695+
assert_eq!(alias.as_ref().unwrap().name.value, "model_alias");
4696+
}
4697+
_ => panic!("Expected SemanticView table factor"),
4698+
}
4699+
} else {
4700+
panic!("Expected table in FROM clause");
4701+
}
4702+
} else {
4703+
panic!("Expected SELECT statement");
4704+
}
4705+
}
4706+
_ => panic!("Expected Query statement"),
4707+
}
4708+
}

0 commit comments

Comments
 (0)