Skip to content

Commit 410c2f9

Browse files
authored
Merge pull request #411 from mindsdb/combining-queries
Support combining queries: intersect, except
2 parents 57ba416 + 64622e9 commit 410c2f9

13 files changed

Lines changed: 332 additions & 120 deletions

File tree

mindsdb_sql/parser/ast/select/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from .select import Select
22
from .common_table_expression import CommonTableExpression
3-
from .union import Union
3+
from .union import Union, Except, Intersect
44
from .constant import Constant, NullConstant, Last
55
from .star import Star
66
from .identifier import Identifier

mindsdb_sql/parser/ast/select/case.py

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,14 @@
44

55

66
class Case(ASTNode):
7-
def __init__(self, rules, default=None, *args, **kwargs):
7+
def __init__(self, rules, default=None, arg=None, *args, **kwargs):
88
super().__init__(*args, **kwargs)
99

1010
# structure:
1111
# [
1212
# [ condition, result ]
1313
# ]
14+
self.arg = arg
1415
self.rules = rules
1516
self.default = default
1617

@@ -36,7 +37,12 @@ def to_tree(self, *args, level=0, **kwargs):
3637
if self.default is not None:
3738
default_str = f'{ind1}default => {self.default.to_string()}\n'
3839

40+
arg_str = ''
41+
if self.arg is not None:
42+
arg_str = f'{ind1}arg => {self.arg.to_string()}\n'
43+
3944
return f'{ind}Case(\n' \
45+
f'{arg_str}'\
4046
f'{rules_str}\n' \
4147
f'{default_str}' \
4248
f'{ind})'
@@ -53,4 +59,8 @@ def get_string(self, *args, alias=True, **kwargs):
5359
default_str = ''
5460
if self.default is not None:
5561
default_str = f' ELSE {self.default.to_string()}'
56-
return f"CASE {rules_str}{default_str} END"
62+
63+
arg_str = ''
64+
if self.arg is not None:
65+
arg_str = f'{self.arg.to_string()} '
66+
return f"CASE {arg_str}{rules_str}{default_str} END"

mindsdb_sql/parser/ast/select/operation.py

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -98,12 +98,13 @@ def get_string(self, *args, **kwargs):
9898

9999

100100
class WindowFunction(ASTNode):
101-
def __init__(self, function, partition=None, order_by=None, alias=None):
101+
def __init__(self, function, partition=None, order_by=None, alias=None, modifier=None):
102102
super().__init__()
103103
self.function = function
104104
self.partition = partition
105105
self.order_by = order_by
106106
self.alias = alias
107+
self.modifier = modifier
107108

108109
def to_tree(self, *args, level=0, **kwargs):
109110
fnc_str = self.function.to_tree(level=level+2)
@@ -143,7 +144,8 @@ def to_string(self, *args, **kwargs):
143144
alias_str = self.alias.to_string()
144145
else:
145146
alias_str = ''
146-
return f'{fnc_str} over({partition_str} {order_str}) {alias_str}'
147+
modifier_str = ' ' + self.modifier if self.modifier else ''
148+
return f'{fnc_str} over({partition_str} {order_str}{modifier_str}) {alias_str}'
147149

148150

149151
class Object(ASTNode):
@@ -177,7 +179,12 @@ def __init__(self, info):
177179
super().__init__(op='interval', args=[info, ])
178180

179181
def get_string(self, *args, **kwargs):
180-
return f'INTERVAL {self.args[0]}'
182+
183+
arg = self.args[0]
184+
items = arg.split(' ', maxsplit=1)
185+
# quote first element
186+
items[0] = f"'{items[0]}'"
187+
return "INTERVAL " + " ".join(items)
181188

182189
def to_tree(self, *args, level=0, **kwargs):
183190
return self.get_string( *args, **kwargs)

mindsdb_sql/parser/ast/select/union.py

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@
22
from mindsdb_sql.parser.utils import indent
33

44

5-
class Union(ASTNode):
5+
class CombiningQuery(ASTNode):
6+
operation = None
67

78
def __init__(self,
89
left,
@@ -24,7 +25,8 @@ def to_tree(self, *args, level=0, **kwargs):
2425
left_str = f'\n{ind1}left=\n{self.left.to_tree(level=level + 2)},'
2526
right_str = f'\n{ind1}right=\n{self.right.to_tree(level=level + 2)},'
2627

27-
out_str = f'{ind}Union(unique={repr(self.unique)},' \
28+
cls_name = self.__class__.__name__
29+
out_str = f'{ind}{cls_name}(unique={repr(self.unique)},' \
2830
f'{left_str}' \
2931
f'{right_str}' \
3032
f'\n{ind})'
@@ -33,7 +35,21 @@ def to_tree(self, *args, level=0, **kwargs):
3335
def get_string(self, *args, **kwargs):
3436
left_str = str(self.left)
3537
right_str = str(self.right)
36-
keyword = 'UNION' if self.unique else 'UNION ALL'
38+
keyword = self.operation
39+
if not self.unique:
40+
keyword += ' ALL'
3741
out_str = f"""{left_str}\n{keyword}\n{right_str}"""
3842

3943
return out_str
44+
45+
46+
class Union(CombiningQuery):
47+
operation = 'UNION'
48+
49+
50+
class Intersect(CombiningQuery):
51+
operation = 'INTERSECT'
52+
53+
54+
class Except(CombiningQuery):
55+
operation = 'EXCEPT'

mindsdb_sql/parser/dialects/mindsdb/lexer.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ class MindsDBLexer(Lexer):
5555

5656
JOIN, INNER, OUTER, CROSS, LEFT, RIGHT, ON,
5757

58-
UNION, ALL,
58+
UNION, ALL, INTERSECT, EXCEPT,
5959

6060
# CASE
6161
CASE, ELSE, END, THEN, WHEN,
@@ -238,6 +238,8 @@ class MindsDBLexer(Lexer):
238238
# UNION
239239

240240
UNION = r'\bUNION\b'
241+
INTERSECT = r'\bINTERSECT\b'
242+
EXCEPT = r'\bEXCEPT\b'
241243
ALL = r'\bALL\b'
242244

243245
# CASE

mindsdb_sql/parser/dialects/mindsdb/parser.py

Lines changed: 50 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ class MindsDBParser(Parser):
7070
'drop_dataset',
7171
'select',
7272
'insert',
73+
'union',
7374
'update',
7475
'delete',
7576
'evaluate',
@@ -614,10 +615,13 @@ def update(self, p):
614615

615616
# INSERT
616617
@_('INSERT INTO identifier LPAREN column_list RPAREN select',
617-
'INSERT INTO identifier select')
618+
'INSERT INTO identifier LPAREN column_list RPAREN union',
619+
'INSERT INTO identifier select',
620+
'INSERT INTO identifier union')
618621
def insert(self, p):
619622
columns = getattr(p, 'column_list', None)
620-
return Insert(table=p.identifier, columns=columns, from_select=p.select)
623+
query = p.select if hasattr(p, 'select') else p.union
624+
return Insert(table=p.identifier, columns=columns, from_select=query)
621625

622626
@_('INSERT INTO identifier LPAREN column_list RPAREN VALUES expr_list_set',
623627
'INSERT INTO identifier VALUES expr_list_set')
@@ -998,19 +1002,35 @@ def database_engine(self, p):
9981002
engine = p.string
9991003
return {'identifier':p.identifier, 'engine':engine, 'if_not_exists':p.if_not_exists_or_empty}
10001004

1001-
# UNION / UNION ALL
1002-
@_('select UNION select')
1003-
def select(self, p):
1004-
return Union(left=p[0], right=p[2], unique=True)
1005-
1006-
@_('select UNION ALL select')
1007-
def select(self, p):
1008-
return Union(left=p[0], right=p[3], unique=False)
1005+
# Combining
1006+
@_('select UNION select',
1007+
'union UNION select',
1008+
'select UNION ALL select',
1009+
'union UNION ALL select')
1010+
def union(self, p):
1011+
unique = not hasattr(p, 'ALL')
1012+
return Union(left=p[0], right=p[2] if unique else p[3], unique=unique)
1013+
1014+
@_('select INTERSECT select',
1015+
'union INTERSECT select',
1016+
'select INTERSECT ALL select',
1017+
'union INTERSECT ALL select')
1018+
def union(self, p):
1019+
unique = not hasattr(p, 'ALL')
1020+
return Intersect(left=p[0], right=p[2] if unique else p[3], unique=unique)
1021+
@_('select EXCEPT select',
1022+
'union EXCEPT select',
1023+
'select EXCEPT ALL select',
1024+
'union EXCEPT ALL select')
1025+
def union(self, p):
1026+
unique = not hasattr(p, 'ALL')
1027+
return Except(left=p[0], right=p[2] if unique else p[3], unique=unique)
10091028

10101029
# tableau
10111030
@_('LPAREN select RPAREN')
1031+
@_('LPAREN union RPAREN')
10121032
def select(self, p):
1013-
return p.select
1033+
return p[1]
10141034

10151035
# WITH
10161036
@_('ctes select')
@@ -1030,13 +1050,14 @@ def ctes(self, p):
10301050
]
10311051
return ctes
10321052

1033-
@_('WITH identifier cte_columns_or_nothing AS LPAREN select RPAREN')
1053+
@_('WITH identifier cte_columns_or_nothing AS LPAREN select RPAREN',
1054+
'WITH identifier cte_columns_or_nothing AS LPAREN union RPAREN')
10341055
def ctes(self, p):
10351056
return [
10361057
CommonTableExpression(
10371058
name=p.identifier,
10381059
columns=p.cte_columns_or_nothing,
1039-
query=p.select)
1060+
query=p[5])
10401061
]
10411062

10421063
@_('empty')
@@ -1331,6 +1352,15 @@ def column_list(self, p):
13311352
def case(self, p):
13321353
return Case(rules=p.case_conditions, default=getattr(p, 'expr', None))
13331354

1355+
@_('CASE expr case_conditions ELSE expr END',
1356+
'CASE expr case_conditions END')
1357+
def case(self, p):
1358+
if hasattr(p, 'expr'):
1359+
arg, default = p.expr, None
1360+
else:
1361+
arg, default = p.expr0, p.expr1
1362+
return Case(rules=p.case_conditions, default=default, arg=arg)
1363+
13341364
@_('case_condition',
13351365
'case_conditions case_condition')
13361366
def case_conditions(self, p):
@@ -1343,13 +1373,18 @@ def case_condition(self, p):
13431373
return [p.expr0, p.expr1]
13441374

13451375
# Window function
1346-
@_('function OVER LPAREN window RPAREN')
1376+
@_('expr OVER LPAREN window RPAREN',
1377+
'expr OVER LPAREN window id BETWEEN id id AND id id RPAREN')
13471378
def window_function(self, p):
13481379

1380+
modifier = None
1381+
if hasattr(p, 'BETWEEN'):
1382+
modifier = f'{p.id0} BETWEEN {p.id1} {p.id2} AND {p.id3} {p.id4}'
13491383
return WindowFunction(
1350-
function=p.function,
1384+
function=p.expr,
13511385
order_by=p.window.get('order_by'),
13521386
partition=p.window.get('partition'),
1387+
modifier=modifier,
13531388
)
13541389

13551390
@_('window PARTITION_BY expr_list')

0 commit comments

Comments
 (0)