Skip to content

Commit 11680b1

Browse files
committed
Add support for SEMANTIC_VIEW table factor
1 parent e9eee00 commit 11680b1

File tree

7 files changed

+289
-0
lines changed

7 files changed

+289
-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/sql-reference/constructs/semantic_view>
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 or expression referring to dimensions (e.g. DATE_PART('year', col))
1428+
dimensions: Vec<Expr>,
1429+
/// List of metrics (references to objects like orders.value, value, orders.*)
1430+
metrics: Vec<ObjectName>,
1431+
/// List of facts or expressions referring to facts or dimensions.
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
@@ -2112,6 +2137,40 @@ impl fmt::Display for TableFactor {
21122137
}
21132138
Ok(())
21142139
}
2140+
TableFactor::SemanticView {
2141+
name,
2142+
dimensions,
2143+
metrics,
2144+
facts,
2145+
where_clause,
2146+
alias,
2147+
} => {
2148+
write!(f, "SEMANTIC_VIEW({name}")?;
2149+
2150+
if !dimensions.is_empty() {
2151+
write!(f, " DIMENSIONS {}", display_comma_separated(dimensions))?;
2152+
}
2153+
2154+
if !metrics.is_empty() {
2155+
write!(f, " METRICS {}", display_comma_separated(metrics))?;
2156+
}
2157+
2158+
if !facts.is_empty() {
2159+
write!(f, " FACTS {}", display_comma_separated(facts))?;
2160+
}
2161+
2162+
if let Some(where_clause) = where_clause {
2163+
write!(f, " WHERE {where_clause}")?;
2164+
}
2165+
2166+
write!(f, ")")?;
2167+
2168+
if let Some(alias) = alias {
2169+
write!(f, " AS {alias}")?;
2170+
}
2171+
2172+
Ok(())
2173+
}
21152174
}
21162175
}
21172176
}

src/ast/spans.rs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2044,6 +2044,23 @@ impl Spanned for TableFactor {
20442044
.chain(symbols.iter().map(|i| i.span()))
20452045
.chain(alias.as_ref().map(|i| i.span())),
20462046
),
2047+
TableFactor::SemanticView {
2048+
name,
2049+
dimensions,
2050+
metrics,
2051+
facts,
2052+
where_clause,
2053+
alias,
2054+
} => union_spans(
2055+
name.0
2056+
.iter()
2057+
.map(|i| i.span())
2058+
.chain(dimensions.iter().map(|d| d.span()))
2059+
.chain(metrics.iter().map(|m| m.span()))
2060+
.chain(facts.iter().map(|f| f.span()))
2061+
.chain(where_clause.as_ref().map(|e| e.span()))
2062+
.chain(alias.as_ref().map(|a| a.span())),
2063+
),
20472064
TableFactor::OpenJsonTable { .. } => Span::empty(),
20482065
}
20492066
}

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
@@ -566,6 +566,10 @@ impl Dialect for SnowflakeDialect {
566566
fn supports_select_wildcard_exclude(&self) -> bool {
567567
true
568568
}
569+
570+
fn supports_semantic_view(&self) -> bool {
571+
true
572+
}
569573
}
570574

571575
// 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
@@ -290,6 +290,7 @@ define_keywords!(
290290
DETACH,
291291
DETAIL,
292292
DETERMINISTIC,
293+
DIMENSIONS,
293294
DIRECTORY,
294295
DISABLE,
295296
DISCARD,
@@ -359,6 +360,7 @@ define_keywords!(
359360
EXTERNAL,
360361
EXTERNAL_VOLUME,
361362
EXTRACT,
363+
FACTS,
362364
FAIL,
363365
FAILOVER,
364366
FALSE,
@@ -566,6 +568,7 @@ define_keywords!(
566568
METADATA,
567569
METHOD,
568570
METRIC,
571+
METRICS,
569572
MICROSECOND,
570573
MICROSECONDS,
571574
MILLENIUM,
@@ -828,6 +831,7 @@ define_keywords!(
828831
SECURITY,
829832
SEED,
830833
SELECT,
834+
SEMANTIC_VIEW,
831835
SEMI,
832836
SENSITIVE,
833837
SEPARATOR,

src/parser/mod.rs

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13397,6 +13397,7 @@ impl<'a> Parser<'a> {
1339713397
| TableFactor::Pivot { alias, .. }
1339813398
| TableFactor::Unpivot { alias, .. }
1339913399
| TableFactor::MatchRecognize { alias, .. }
13400+
| TableFactor::SemanticView { alias, .. }
1340013401
| TableFactor::NestedJoin { alias, .. } => {
1340113402
// but not `FROM (mytable AS alias1) AS alias2`.
1340213403
if let Some(inner_alias) = alias {
@@ -13511,6 +13512,10 @@ impl<'a> Parser<'a> {
1351113512
} else if self.parse_keyword_with_tokens(Keyword::XMLTABLE, &[Token::LParen]) {
1351213513
self.prev_token();
1351313514
self.parse_xml_table_factor()
13515+
} else if self.dialect.supports_semantic_view()
13516+
&& self.parse_keyword_with_tokens(Keyword::SEMANTIC_VIEW, &[Token::LParen])
13517+
{
13518+
self.parse_semantic_view_table_factor()
1351413519
} else {
1351513520
let name = self.parse_object_name(true)?;
1351613521

@@ -13842,6 +13847,66 @@ impl<'a> Parser<'a> {
1384213847
Ok(XmlPassingClause { arguments })
1384313848
}
1384413849

13850+
fn parse_semantic_view_table_factor(&mut self) -> Result<TableFactor, ParserError> {
13851+
let name = self.parse_object_name(true)?;
13852+
13853+
// Parse DIMENSIONS, METRICS, FACTS and WHERE clauses in flexible order
13854+
let mut dimensions = Vec::new();
13855+
let mut metrics = Vec::new();
13856+
let mut facts = Vec::new();
13857+
let mut where_clause = None;
13858+
13859+
while self.peek_token().token != Token::RParen {
13860+
if self.parse_keyword(Keyword::DIMENSIONS) {
13861+
if !dimensions.is_empty() {
13862+
return Err(ParserError::ParserError(
13863+
"DIMENSIONS clause can only be specified once".to_string(),
13864+
));
13865+
}
13866+
dimensions = self.parse_comma_separated(Parser::parse_expr)?;
13867+
} else if self.parse_keyword(Keyword::METRICS) {
13868+
if !metrics.is_empty() {
13869+
return Err(ParserError::ParserError(
13870+
"METRICS clause can only be specified once".to_string(),
13871+
));
13872+
}
13873+
metrics = self.parse_comma_separated(|parser| parser.parse_object_name(true))?;
13874+
} else if self.parse_keyword(Keyword::FACTS) {
13875+
if !facts.is_empty() {
13876+
return Err(ParserError::ParserError(
13877+
"FACTS clause can only be specified once".to_string(),
13878+
));
13879+
}
13880+
facts = self.parse_comma_separated(Parser::parse_expr)?;
13881+
} else if self.parse_keyword(Keyword::WHERE) {
13882+
if where_clause.is_some() {
13883+
return Err(ParserError::ParserError(
13884+
"WHERE clause can only be specified once".to_string(),
13885+
));
13886+
}
13887+
where_clause = Some(self.parse_expr()?);
13888+
} else {
13889+
return parser_err!(
13890+
"Expected one of DIMENSIONS, METRICS, FACTS or WHERE",
13891+
self.peek_token().span.start
13892+
)?;
13893+
}
13894+
}
13895+
13896+
self.expect_token(&Token::RParen)?;
13897+
13898+
let alias = self.maybe_parse_table_alias()?;
13899+
13900+
Ok(TableFactor::SemanticView {
13901+
name,
13902+
dimensions,
13903+
metrics,
13904+
facts,
13905+
where_clause,
13906+
alias,
13907+
})
13908+
}
13909+
1384513910
fn parse_match_recognize(&mut self, table: TableFactor) -> Result<TableFactor, ParserError> {
1384613911
self.expect_token(&Token::LParen)?;
1384713912

tests/sqlparser_snowflake.rs

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4613,3 +4613,129 @@ fn test_drop_constraints() {
46134613
snowflake().verified_stmt("ALTER TABLE tbl DROP FOREIGN KEY k1 RESTRICT");
46144614
snowflake().verified_stmt("ALTER TABLE tbl DROP CONSTRAINT c1 CASCADE");
46154615
}
4616+
4617+
#[test]
4618+
fn test_semantic_view_all_variants_should_pass() {
4619+
let test_cases = [
4620+
("SELECT * FROM SEMANTIC_VIEW(model)", None),
4621+
(
4622+
"SELECT * FROM SEMANTIC_VIEW(model DIMENSIONS dim1, dim2)",
4623+
None,
4624+
),
4625+
(
4626+
"SELECT * FROM SEMANTIC_VIEW(model METRICS met1, met2)",
4627+
None,
4628+
),
4629+
(
4630+
"SELECT * FROM SEMANTIC_VIEW(model FACTS fact1, fact2)",
4631+
None,
4632+
),
4633+
(
4634+
"SELECT * FROM SEMANTIC_VIEW(model DIMENSIONS dim1 METRICS met1)",
4635+
None,
4636+
),
4637+
(
4638+
"SELECT * FROM SEMANTIC_VIEW(model DIMENSIONS dim1 WHERE x > 0)",
4639+
None,
4640+
),
4641+
(
4642+
"SELECT * FROM SEMANTIC_VIEW(model DIMENSIONS dim1) AS sv",
4643+
None,
4644+
),
4645+
(
4646+
"SELECT * FROM SEMANTIC_VIEW(model DIMENSIONS DATE_PART('year', col))",
4647+
None,
4648+
),
4649+
(
4650+
"SELECT * FROM SEMANTIC_VIEW(model METRICS orders.col, orders.col2)",
4651+
None,
4652+
),
4653+
// We can parse in any order bu will always produce a result in a fixed order.
4654+
(
4655+
"SELECT * FROM SEMANTIC_VIEW(model WHERE x > 0 DIMENSIONS dim1)",
4656+
Some("SELECT * FROM SEMANTIC_VIEW(model DIMENSIONS dim1 WHERE x > 0)"),
4657+
),
4658+
(
4659+
"SELECT * FROM SEMANTIC_VIEW(model METRICS met1 DIMENSIONS dim1)",
4660+
Some("SELECT * FROM SEMANTIC_VIEW(model DIMENSIONS dim1 METRICS met1)"),
4661+
),
4662+
(
4663+
"SELECT * FROM SEMANTIC_VIEW(model FACTS fact1 DIMENSIONS dim1)",
4664+
Some("SELECT * FROM SEMANTIC_VIEW(model DIMENSIONS dim1 FACTS fact1)"),
4665+
),
4666+
];
4667+
4668+
for (input_sql, expected_sql) in test_cases {
4669+
if let Some(expected) = expected_sql {
4670+
// Test that non-canonical order gets normalized
4671+
let parsed = snowflake().parse_sql_statements(input_sql).unwrap();
4672+
let formatted = parsed[0].to_string();
4673+
assert_eq!(formatted, expected);
4674+
} else {
4675+
snowflake().verified_stmt(input_sql);
4676+
}
4677+
}
4678+
}
4679+
4680+
#[test]
4681+
fn test_semantic_view_invalid_queries_should_fail() {
4682+
let invalid_sqls = [
4683+
"SELECT * FROM SEMANTIC_VIEW(model DIMENSIONS dim1 INVALID inv1)",
4684+
"SELECT * FROM SEMANTIC_VIEW(model DIMENSIONS dim1 DIMENSIONS dim2)",
4685+
"SELECT * FROM SEMANTIC_VIEW(model METRICS SUM(met1.avg))",
4686+
];
4687+
4688+
for sql in invalid_sqls {
4689+
let result = snowflake().parse_sql_statements(sql);
4690+
assert!(result.is_err(), "Expected error for invalid SQL: {}", sql);
4691+
}
4692+
}
4693+
4694+
#[test]
4695+
fn test_semantic_view_ast_structure() {
4696+
let sql = r#"SELECT * FROM SEMANTIC_VIEW(
4697+
my_model
4698+
DIMENSIONS DATE_PART('year', date_col), region_name
4699+
METRICS orders.revenue, orders.count
4700+
WHERE active = true
4701+
) AS model_alias"#;
4702+
4703+
let stmt = snowflake().parse_sql_statements(sql).unwrap();
4704+
match &stmt[0] {
4705+
Statement::Query(q) => {
4706+
if let SetExpr::Select(select) = q.body.as_ref() {
4707+
if let Some(TableWithJoins { relation, .. }) = select.from.first() {
4708+
match relation {
4709+
TableFactor::SemanticView {
4710+
name,
4711+
dimensions,
4712+
metrics,
4713+
facts,
4714+
where_clause,
4715+
alias,
4716+
} => {
4717+
assert_eq!(name.to_string(), "my_model");
4718+
assert_eq!(dimensions.len(), 2);
4719+
assert_eq!(dimensions[0].to_string(), "DATE_PART('year', date_col)");
4720+
assert_eq!(dimensions[1].to_string(), "region_name");
4721+
assert_eq!(metrics.len(), 2);
4722+
assert_eq!(metrics[0].to_string(), "orders.revenue");
4723+
assert_eq!(metrics[1].to_string(), "orders.count");
4724+
assert!(facts.is_empty());
4725+
assert!(where_clause.is_some());
4726+
assert_eq!(where_clause.as_ref().unwrap().to_string(), "active = true");
4727+
assert!(alias.is_some());
4728+
assert_eq!(alias.as_ref().unwrap().name.value, "model_alias");
4729+
}
4730+
_ => panic!("Expected SemanticView table factor"),
4731+
}
4732+
} else {
4733+
panic!("Expected table in FROM clause");
4734+
}
4735+
} else {
4736+
panic!("Expected SELECT statement");
4737+
}
4738+
}
4739+
_ => panic!("Expected Query statement"),
4740+
}
4741+
}

0 commit comments

Comments
 (0)