Skip to content

Commit 9f7be32

Browse files
committed
Improve the support of clause groups
This reverts "Support Clause groups and statement separators", commit 77c900f, and instead uses the existing Group type with unions as clauses rather than separators. This is a better solution as it matches how the sql is parsed (for groups and clauses) without adding in a new type.
1 parent 86b5b5f commit 9f7be32

4 files changed

Lines changed: 59 additions & 45 deletions

File tree

src/sql_tstring/__init__.py

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@
99

1010
from sql_tstring.parser import (
1111
Clause,
12-
ClauseGroup,
1312
Element,
1413
Expression,
1514
ExpressionGroup,
@@ -103,10 +102,10 @@ def sql(
103102
for raw_parsed_query in parsed_queries:
104103
parsed_query = deepcopy(raw_parsed_query)
105104
new_values = _replace_placeholders(parsed_query, 0)
106-
result_str += " " + _print_node(parsed_query, [None] * len(result_values), ctx.dialect)
105+
result_str += _print_node(parsed_query, [None] * len(result_values), ctx.dialect)
107106
result_values.extend(new_values)
108107

109-
return result_str.strip(), result_values
108+
return result_str, result_values
110109

111110

112111
class _ContextManager:
@@ -158,12 +157,7 @@ def _print_node(
158157

159158
match node:
160159
case Statement():
161-
addition = " ".join(
162-
_print_node(clause, placeholders, dialect) for clause in node.clauses
163-
)
164-
result = f"{node.separator} {addition}"
165-
case ClauseGroup():
166-
result = f"({" ".join(_print_node(clause, placeholders, dialect) for clause in node.clauses)})" # noqa: E501
160+
result = " ".join(_print_node(clause, placeholders, dialect) for clause in node.clauses)
167161
case Clause() | ExpressionGroup():
168162
result = ""
169163

@@ -261,7 +255,9 @@ def _replace_placeholder(
261255
if clause is not None:
262256
clause.removed = True
263257
else:
264-
expression = node.parent
258+
expression: Expression | ExpressionGroup | Function | Group | Literal | Statement = (
259+
node.parent
260+
)
265261
while not isinstance(expression, Expression):
266262
expression = expression.parent
267263

src/sql_tstring/parser.py

Lines changed: 31 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,16 @@ class ClauseProperties:
217217
allow_empty=False, placeholder_type=PlaceholderType.VARIABLE, separators={","}
218218
)
219219
},
220+
"union": {
221+
"": ClauseProperties(
222+
allow_empty=True, placeholder_type=PlaceholderType.DISALLOWED, separators=set()
223+
),
224+
"all": {
225+
"": ClauseProperties(
226+
allow_empty=True, placeholder_type=PlaceholderType.DISALLOWED, separators=set()
227+
)
228+
},
229+
},
220230
"update": {
221231
"": ClauseProperties(
222232
allow_empty=False, placeholder_type=PlaceholderType.DISALLOWED, separators=set()
@@ -274,14 +284,13 @@ class ClauseProperties:
274284

275285
@dataclass
276286
class Statement:
277-
clauses: list[Clause | ClauseGroup] = field(default_factory=list)
287+
clauses: list[Clause | Group] = field(default_factory=list)
278288
parent: ExpressionGroup | Function | Group | None = None
279-
separator: str = ""
280289

281290

282291
@dataclass
283292
class Clause:
284-
parent: ClauseGroup | Statement
293+
parent: Statement
285294
properties: ClauseProperties
286295
text: str
287296
expressions: list[Expression] = field(init=False)
@@ -291,12 +300,6 @@ def __post_init__(self) -> None:
291300
self.expressions = [Expression(self)]
292301

293302

294-
@dataclass
295-
class ClauseGroup:
296-
parent: Statement
297-
clauses: list[Clause] = field(default_factory=list)
298-
299-
300303
@dataclass
301304
class Expression:
302305
parent: Clause | ExpressionGroup
@@ -321,7 +324,7 @@ class Placeholder:
321324

322325
@dataclass
323326
class Group:
324-
parent: Expression | Function | Group
327+
parent: Expression | Function | Group | Statement
325328
parts: list[Function | Group | Literal | Operator | Part | Placeholder | Statement] = field(
326329
default_factory=list
327330
)
@@ -358,7 +361,7 @@ class Operator:
358361

359362

360363
type ParentNode = Clause | Expression | ExpressionGroup | Function | Group
361-
type Node = ParentNode | ClauseGroup | Literal | Statement
364+
type Node = ParentNode | Literal | Statement
362365
type Element = Node | Operator | Part | Placeholder
363366

364367

@@ -394,7 +397,7 @@ def _parse_placeholder(
394397
) -> None:
395398
if isinstance(current_node, (Expression, Function, Group, Literal)):
396399
parent = current_node
397-
elif isinstance(current_node, (Statement, ClauseGroup)):
400+
elif isinstance(current_node, Statement):
398401
raise ValueError("Invalid syntax")
399402
else: # Clause | ExpressionGroup
400403
parent = current_node.expressions[-1]
@@ -468,7 +471,7 @@ def _parse_string(
468471
current_node, consumed = _parse_token(
469472
current_node, raw_current_token, current_token, tokens[index:], statements
470473
)
471-
else: # ClauseGroup | Statement
474+
else: # Statement
472475
current_node, consumed = _parse_token(
473476
current_node, raw_current_token, current_token, tokens[index:], statements
474477
)
@@ -479,7 +482,7 @@ def _parse_string(
479482

480483

481484
def _parse_token(
482-
current_node: ParentNode | ClauseGroup | Statement,
485+
current_node: ParentNode | Statement,
483486
raw_current_token: str,
484487
current_token: str,
485488
tokens: list[str],
@@ -489,17 +492,10 @@ def _parse_token(
489492
return _parse_clause(current_node, tokens)
490493
elif current_token == ";":
491494
statements.append(Statement())
492-
statements[-1].separator = ";"
493495
return statements[-1], 1
494-
elif current_token == "union":
495-
statements.append(Statement())
496-
statements[-1].separator = raw_current_token
497-
consumed = 1
498-
if tokens[1].lower() == "all":
499-
statements[-1].separator += f" {tokens[1]}"
500-
consumed = 2
501-
return statements[-1], consumed
502-
elif not isinstance(current_node, (ClauseGroup, Statement)):
496+
elif current_token == "(":
497+
return _parse_group(current_node)
498+
elif not isinstance(current_node, Statement):
503499
if current_token in OPERATORS:
504500
return _parse_operator(current_node, tokens)
505501
elif current_token == "'":
@@ -510,21 +506,17 @@ def _parse_token(
510506
return _parse_function(current_node, raw_current_token[:-1])
511507
elif current_token == ")":
512508
current_node = _find_node( # type: ignore[assignment]
513-
current_node, (ExpressionGroup, Function, Group, ClauseGroup)
509+
current_node, (ExpressionGroup, Function, Group)
514510
)
515511
return current_node.parent, 1
516512
else:
517513
return _parse_part(current_node, raw_current_token)
518-
elif isinstance(current_node, Statement) and current_token == "(":
519-
statement_group = ClauseGroup(current_node)
520-
current_node.clauses.append(statement_group)
521-
return statement_group, 1
522514
else:
523515
raise ValueError("Invalid syntax")
524516

525517

526518
def _parse_clause(
527-
current_node: ParentNode | ClauseGroup | Statement,
519+
current_node: ParentNode | Statement,
528520
tokens: list[str],
529521
) -> tuple[Clause, int]:
530522
index = 0
@@ -543,16 +535,16 @@ def _parse_clause(
543535
statement = Statement(parent=current_node)
544536
current_node.expressions[-1].parts.append(statement)
545537
current_node = statement
546-
else: # Clause | Expression | Statement | ClauseGroup
547-
current_node = _find_node(current_node, (Statement, ClauseGroup)) # type: ignore[assignment] # noqa: E501
538+
else: # Clause | Expression | Statement
539+
current_node = _find_node(current_node, Statement)
548540

549541
clause_properties = cast(ClauseProperties, clause_entry[""])
550542
clause = Clause(
551-
parent=current_node, # type: ignore[arg-type]
543+
parent=current_node,
552544
properties=clause_properties,
553545
text=text,
554546
)
555-
current_node.clauses.append(clause) # type: ignore[union-attr]
547+
current_node.clauses.append(clause)
556548
return clause, index
557549

558550

@@ -576,13 +568,17 @@ def _parse_operator[T: ParentNode](
576568

577569

578570
def _parse_group(
579-
current_node: ParentNode,
571+
current_node: ParentNode | Statement,
580572
) -> tuple[ExpressionGroup | Group, int]:
581573
group: ExpressionGroup | Group
582574
if isinstance(current_node, (Expression, Function, Group)):
583575
group = Group(parent=current_node)
584576
current_node.parts.append(group)
585577
return group, 1
578+
elif isinstance(current_node, Statement):
579+
group = Group(parent=current_node)
580+
current_node.clauses.append(group)
581+
return group, 1
586582
else: # Clause | ExpressionGroup
587583
parent = current_node.expressions[-1]
588584
if len(parent.parts) == 0:

tests/test_identifiers.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,14 @@ def test_frame_clause_invalid(frame_clause: str) -> None:
7070
sql(f"SELECT x OVER(PARTITION BY x {frame_clause} {{b}} PRECEDING) FROM y", locals())
7171

7272

73+
@pytest.mark.xfail
74+
def test_over_clause() -> None:
75+
a = 2
76+
assert ("SELECT AVG(x) OVER y - (2 / 365) FROM x", []) == sql(
77+
"SELECT AVG(x) OVER y - ({a} / 365) FROM x", locals()
78+
)
79+
80+
7381
@pytest.mark.parametrize(
7482
"lock_type, expected",
7583
(

tests/test_parsing.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,20 @@ def test_cte() -> None:
5959
)
6060

6161

62+
def test_cte_union() -> None:
63+
query, _ = sql(
64+
"""WITH cte AS (SELECT DISTINCT x FROM y UNION SELECT a FROM b)
65+
SELECT DISTINCT x
66+
FROM z
67+
WHERE x NOT IN (SELECT a FROM b)""",
68+
locals(),
69+
)
70+
assert (
71+
query
72+
== """WITH cte AS (SELECT DISTINCT x FROM y UNION SELECT a FROM b) SELECT DISTINCT x FROM z WHERE x NOT IN (SELECT a FROM b)""" # noqa: E501
73+
)
74+
75+
6276
def test_with_conflict() -> None:
6377
a = "A"
6478
b = "B"

0 commit comments

Comments
 (0)