Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 21 additions & 2 deletions crates/lib-core/src/parser/segments/join.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ impl JoinClauseSegment {
.children(const { &SyntaxSet::new(&[SyntaxKind::Keyword]) })
.any(|kw| kw.raw().to_uppercase() == "APPLY");

let from_expression = if is_apply {
let from_expression_element = if is_apply {
// For APPLY clauses, find the FromExpressionElement in the sequence
self.0
.children(const { &SyntaxSet::new(&[SyntaxKind::FromExpressionElement]) })
Expand All @@ -28,11 +28,30 @@ impl JoinClauseSegment {
.child(const { &SyntaxSet::new(&[SyntaxKind::FromExpressionElement]) })
};

if let Some(from_expr) = from_expression {
if let Some(from_expr) = from_expression_element {
let alias = FromExpressionElementSegment(from_expr.clone()).eventual_alias();
buff.push((from_expr.clone(), alias));
}

// Handle parenthesized joined tables: JOIN (table1 JOIN table2 ON ...)
// In this case, the join clause contains a Bracketed segment with a FromExpression
// that has its own FromExpressionElement and JoinClause children
if let Some(bracketed) = self
.0
.child(const { &SyntaxSet::new(&[SyntaxKind::Bracketed]) })
&& let Some(from_expression) =
bracketed.child(const { &SyntaxSet::new(&[SyntaxKind::FromExpression]) })
{
// Get the direct table from the FromExpression
for from_expr_elem in from_expression
.children(const { &SyntaxSet::new(&[SyntaxKind::FromExpressionElement]) })
{
let alias =
FromExpressionElementSegment(from_expr_elem.clone()).eventual_alias();
buff.push((from_expr_elem.clone(), alias));
}
}

for join_clause in self.0.recursive_crawl(
const { &SyntaxSet::new(&[SyntaxKind::JoinClause]) },
true,
Expand Down
18 changes: 15 additions & 3 deletions crates/lib-dialects/src/ansi.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1067,6 +1067,18 @@ pub fn raw_dialect() -> Dialect {
"NestedJoinGrammar".into(),
Nothing::new().to_matchable().into(),
),
// Grammar for join targets - either a simple table expression or a
// bracketed join expression (for parenthesized joined tables)
(
"JoinTargetGrammar".into(),
one_of(vec![
Ref::new("FromExpressionElementSegment").to_matchable(),
Bracketed::new(vec![Ref::new("FromExpressionSegment").to_matchable()])
.to_matchable(),
])
.to_matchable()
.into(),
),
(
"ReferentialActionGrammar".into(),
one_of(vec![
Expand Down Expand Up @@ -2338,7 +2350,7 @@ pub fn raw_dialect() -> Dialect {
.to_matchable(),
Ref::new("JoinKeywordsGrammar").to_matchable(),
MetaSegment::indent().to_matchable(),
Ref::new("FromExpressionElementSegment").to_matchable(),
Ref::new("JoinTargetGrammar").to_matchable(),
AnyNumberOf::new(vec![Ref::new("NestedJoinGrammar").to_matchable()])
.to_matchable(),
MetaSegment::dedent().to_matchable(),
Expand Down Expand Up @@ -2376,14 +2388,14 @@ pub fn raw_dialect() -> Dialect {
Ref::new("NaturalJoinKeywordsGrammar").to_matchable(),
Ref::new("JoinKeywordsGrammar").to_matchable(),
MetaSegment::indent().to_matchable(),
Ref::new("FromExpressionElementSegment").to_matchable(),
Ref::new("JoinTargetGrammar").to_matchable(),
MetaSegment::dedent().to_matchable(),
])
.to_matchable(),
Sequence::new(vec![
Ref::new("ExtendedNaturalJoinKeywordsGrammar").to_matchable(),
MetaSegment::indent().to_matchable(),
Ref::new("FromExpressionElementSegment").to_matchable(),
Ref::new("JoinTargetGrammar").to_matchable(),
MetaSegment::dedent().to_matchable(),
])
.to_matchable(),
Expand Down
23 changes: 23 additions & 0 deletions crates/lib/test/fixtures/rules/std_rule_cases/RF01.yml
Original file line number Diff line number Diff line change
Expand Up @@ -323,3 +323,26 @@ test_pass_postgres_select_with_alias_quoted:
configs:
core:
dialect: postgres

test_pass_postgres_parenthesized_join:
# https://github.com/quarylabs/sqruff/issues/1573
pass_str: |
SELECT
c.short_name || ': ' || c.title || ', ' || ci.long_name AS label,
c.short_name || ', ' || ci.long_name AS short_label,
ci.id AS course_instance_id,
(e.id IS NOT NULL) AS enrolled,
users_is_instructor_in_course (u.user_id, c.id) AS instructor_access
FROM
users AS u
CROSS JOIN (
course_instances AS ci
JOIN pl_courses AS c ON (c.id = ci.course_id)
)
LEFT JOIN enrollments AS e ON (
e.user_id = u.user_id
AND e.course_instance_id = ci.id
)
configs:
core:
dialect: postgres
Loading