From 66108c1dee4d2523db1f787c6d2b5861ba0f0a85 Mon Sep 17 00:00:00 2001 From: andrew Date: Wed, 14 Jan 2026 15:05:08 +0300 Subject: [PATCH 1/2] support union in IN --- mindsdb_sql_parser/parser.py | 40 ++++++++------------ tests/test_base_sql/test_select_structure.py | 27 +++++++++++++ 2 files changed, 42 insertions(+), 25 deletions(-) diff --git a/mindsdb_sql_parser/parser.py b/mindsdb_sql_parser/parser.py index a340612..92ea81c 100644 --- a/mindsdb_sql_parser/parser.py +++ b/mindsdb_sql_parser/parser.py @@ -667,13 +667,11 @@ def update(self, p): from_select=p.select) # INSERT - @_('INSERT INTO identifier LPAREN column_list RPAREN select', - 'INSERT INTO identifier LPAREN column_list RPAREN union', - 'INSERT INTO identifier select', + @_('INSERT INTO identifier LPAREN column_list RPAREN union', 'INSERT INTO identifier union') def insert(self, p): columns = getattr(p, 'column_list', None) - query = p.select if hasattr(p, 'select') else p.union + query = p.union return Insert(table=p.identifier, columns=columns, from_select=query) @_('INSERT INTO identifier LPAREN column_list RPAREN VALUES expr_list_set', @@ -1086,33 +1084,24 @@ def database_engine(self, p): return {'identifier':p.identifier, 'engine':engine, 'if_not_exists':p.if_not_exists_or_empty} # Combining - @_('select UNION select', - 'union UNION select', - 'select UNION ALL select', + @_('union UNION select', 'union UNION ALL select', - 'select UNION DISTINCT select', 'union UNION DISTINCT select') def union(self, p): unique = not hasattr(p, 'ALL') distinct_key = hasattr(p, 'DISTINCT') return Union(left=p[0], right=p[-1], unique=unique, distinct_key=distinct_key) - @_('select INTERSECT select', - 'union INTERSECT select', - 'select INTERSECT ALL select', + @_('union INTERSECT select', 'union INTERSECT ALL select', - 'select INTERSECT DISTINCT select', 'union INTERSECT DISTINCT select') def union(self, p): unique = not hasattr(p, 'ALL') distinct_key = hasattr(p, 'DISTINCT') return Intersect(left=p[0], right=p[-1], unique=unique, distinct_key=distinct_key) - @_('select EXCEPT select', - 'union EXCEPT select', - 'select EXCEPT ALL select', + @_('union EXCEPT select', 'union EXCEPT ALL select', - 'select EXCEPT DISTINCT select', 'union EXCEPT DISTINCT select') def union(self, p): unique = not hasattr(p, 'ALL') @@ -1121,10 +1110,13 @@ def union(self, p): # tableau @_('LPAREN select RPAREN') - @_('LPAREN union RPAREN') def select(self, p): return p[1] + @_('select') + def union(self, p): + return p[0] + # WITH @_('ctes select') def select(self, p): @@ -1132,8 +1124,7 @@ def select(self, p): select.cte = p.ctes return select - @_('ctes COMMA identifier cte_columns_or_nothing AS LPAREN select RPAREN', - 'ctes COMMA identifier cte_columns_or_nothing AS LPAREN union RPAREN') + @_('ctes COMMA identifier cte_columns_or_nothing AS LPAREN union RPAREN') def ctes(self, p): ctes = p.ctes ctes = ctes + [ @@ -1144,8 +1135,7 @@ def ctes(self, p): ] return ctes - @_('WITH identifier cte_columns_or_nothing AS LPAREN select RPAREN', - 'WITH identifier cte_columns_or_nothing AS LPAREN union RPAREN') + @_('WITH identifier cte_columns_or_nothing AS LPAREN union RPAREN') def ctes(self, p): return [ CommonTableExpression( @@ -1518,11 +1508,11 @@ def window(self, p): # OPERATIONS - @_('LPAREN select RPAREN') + @_('LPAREN union RPAREN') def expr(self, p): - select = p.select - select.parentheses = True - return select + union = p.union + union.parentheses = True + return union @_('LPAREN expr RPAREN') def expr(self, p): diff --git a/tests/test_base_sql/test_select_structure.py b/tests/test_base_sql/test_select_structure.py index 9829807..67d1614 100644 --- a/tests/test_base_sql/test_select_structure.py +++ b/tests/test_base_sql/test_select_structure.py @@ -877,6 +877,33 @@ def test_is_not_precedence(self): assert str(ast) == str(expected_ast) assert ast.to_tree() == expected_ast.to_tree() + def test_in_union(self): + sql = f''' + SELECT * FROM tbl1 + where col in ( + select id from tbl2 + union + select id from tbl3 + ) + ''' + ast = parse_sql(sql) + + expected_ast = Select( + targets=[Star()], + from_table=Identifier('tbl1'), + where=BinaryOperation(op='in', args=( + Identifier('col'), + Union( + left=Select(targets=[Identifier('id')], from_table=Identifier('tbl2')), + right=Select(targets=[Identifier('id')], from_table=Identifier('tbl3')), + parentheses=True + ) + )) + ) + + assert str(ast) == str(expected_ast) + assert ast.to_tree() == expected_ast.to_tree() + class TestSelectStructureNoSqlite: def test_select_from_plugins(self): From 86ba838bd95993d468240e1c341cb16a2e5ec9fd Mon Sep 17 00:00:00 2001 From: andrew Date: Wed, 14 Jan 2026 15:51:14 +0300 Subject: [PATCH 2/2] bump version --- mindsdb_sql_parser/__about__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mindsdb_sql_parser/__about__.py b/mindsdb_sql_parser/__about__.py index dd1123a..bea2edf 100644 --- a/mindsdb_sql_parser/__about__.py +++ b/mindsdb_sql_parser/__about__.py @@ -1,6 +1,6 @@ __title__ = 'mindsdb_sql_parser' __package_name__ = 'mindsdb_sql_parser' -__version__ = '0.13.3' +__version__ = '0.13.4' __description__ = "Mindsdb SQL parser" __email__ = "jorge@mindsdb.com" __author__ = 'MindsDB Inc'