Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Parser updates 03.24 #359

Merged
merged 12 commits into from
Mar 13, 2024
11 changes: 8 additions & 3 deletions mindsdb_sql/parser/ast/create.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,12 @@


class TableColumn():
def __init__(self, name, type='integer'):
def __init__(self, name, type='integer', length=None):
self.name = name
self.type = type
self.is_primary_key = False
self.default = None
self.length = length


class CreateTable(ASTNode):
Expand Down Expand Up @@ -72,14 +73,18 @@ def get_string(self, *args, **kwargs):
if self.columns is not None:
columns = []
for col in self.columns:
type = str(col.type)
if sa_types is not None:

if not isinstance(col.type, str) and sa_types is not None:
if issubclass(col.type, sa_types.Integer):
type = 'int'
elif issubclass(col.type, sa_types.Float):
type = 'float'
elif issubclass(col.type, sa_types.Text):
type = 'text'
else:
type = str(col.type)
if col.length is not None:
type = f'{type}({col.length})'
columns.append( f'{col.name} {type}')

columns_str = '({})'.format(', '.join(columns))
Expand Down
1 change: 0 additions & 1 deletion mindsdb_sql/parser/dialects/mindsdb/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
from .drop_dataset import DropDataset
from .evaluate import Evaluate
from .latest import Latest
from .create_file import CreateFile
from .create_ml_engine import CreateMLEngine
from .drop_ml_engine import DropMLEngine
from .create_job import CreateJob
Expand Down
29 changes: 0 additions & 29 deletions mindsdb_sql/parser/dialects/mindsdb/create_file.py

This file was deleted.

6 changes: 3 additions & 3 deletions mindsdb_sql/parser/dialects/mindsdb/lexer.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ class MindsDBLexer(Lexer):
ignore_line_comment = r'--[^\n]*'

tokens = {
USE, DROP, CREATE, DESCRIBE, RETRAIN,REPLACE,
USE, DROP, CREATE, DESCRIBE, RETRAIN, REPLACE,

# Misc
SET, START, TRANSACTION, COMMIT, ROLLBACK, ALTER, EXPLAIN,
Expand Down Expand Up @@ -308,12 +308,12 @@ def FLOAT(self, t):
def INTEGER(self, t):
return t

@_(r"'(?:[^\'\\]|\\.)*'")
@_(r"'(?:\\.|[^'])*'")
def QUOTE_STRING(self, t):
t.value = t.value.replace('\\"', '"').replace("\\'", "'")
return t

@_(r'"(?:[^\"\\]|\\.)*"')
@_(r'"(?:\\.|[^"])*"')
def DQUOTE_STRING(self, t):
t.value = t.value.replace('\\"', '"').replace("\\'", "'")
return t
Expand Down
87 changes: 52 additions & 35 deletions mindsdb_sql/parser/dialects/mindsdb/parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@
from mindsdb_sql.parser.dialects.mindsdb.trigger import CreateTrigger, DropTrigger
from mindsdb_sql.parser.dialects.mindsdb.latest import Latest
from mindsdb_sql.parser.dialects.mindsdb.evaluate import Evaluate
from mindsdb_sql.parser.dialects.mindsdb.create_file import CreateFile
from mindsdb_sql.parser.dialects.mindsdb.knowledge_base import CreateKnowledgeBase, DropKnowledgeBase
from mindsdb_sql.parser.dialects.mindsdb.skills import CreateSkill, DropSkill, UpdateSkill
from mindsdb_sql.exceptions import ParsingException
Expand Down Expand Up @@ -691,27 +690,44 @@ def drop_table(self, p):
return DropTables(tables=[p.identifier], if_exists=p.if_exists_or_empty)

# create table
@_('CREATE TABLE identifier select', # TODO tests failing without it
'CREATE TABLE if_not_exists_or_empty identifier select',
'CREATE TABLE if_not_exists_or_empty identifier LPAREN select RPAREN',
'CREATE OR REPLACE TABLE identifier select',
'CREATE OR REPLACE TABLE identifier LPAREN select RPAREN')
@_('id id',
'id id LPAREN INTEGER RPAREN')
def table_column(self, p):
return TableColumn(
name=p[0],
type=p[1],
length=getattr(p, 'INTEGER', None)
)

@_('table_column',
'table_column_list COMMA table_column')
def table_column_list(self, p):
items = getattr(p, 'table_column_list', [])
items.append(p.table_column)
return items

@_('CREATE replace_or_empty TABLE if_not_exists_or_empty identifier LPAREN table_column_list RPAREN')
def create_table(self, p):
# TODO create table with columns
is_replace = False
if hasattr(p, 'REPLACE'):
is_replace = True
return CreateTable(
name=p.identifier,
is_replace=is_replace,
from_select=p.select,
columns=p.table_column_list,
is_replace=getattr(p, 'replace_or_empty', False),
if_not_exists=getattr(p, 'if_not_exists_or_empty', False)
)

@_('CREATE TABLE identifier USING kw_parameter_list')
@_(
'CREATE replace_or_empty TABLE if_not_exists_or_empty identifier select',
'CREATE replace_or_empty TABLE if_not_exists_or_empty identifier LPAREN select RPAREN',
)
def create_table(self, p):
params = p.kw_parameter_list
return CreateFile(name=p.identifier, **params)
is_replace = getattr(p, 'replace_or_empty', False)

return CreateTable(
name=p.identifier,
is_replace=is_replace,
from_select=p.select,
if_not_exists=getattr(p, 'if_not_exists_or_empty', False)
)

# create predictor

Expand Down Expand Up @@ -889,19 +905,14 @@ def create_integration(self, p):
return DropMLEngine(name=p.identifier, if_exists=p.if_exists_or_empty)

# CREATE INTEGRATION
@_('CREATE database_engine',
'CREATE database_engine COMMA PARAMETERS EQUALS json',
'CREATE database_engine COMMA PARAMETERS json',
'CREATE database_engine PARAMETERS EQUALS json',
'CREATE database_engine PARAMETERS json',
'CREATE OR REPLACE database_engine COMMA PARAMETERS EQUALS json',
'CREATE OR REPLACE database_engine COMMA PARAMETERS json',
'CREATE OR REPLACE database_engine PARAMETERS EQUALS json',
'CREATE OR REPLACE database_engine PARAMETERS json')
@_('CREATE replace_or_empty database_engine',
'CREATE replace_or_empty database_engine COMMA PARAMETERS EQUALS json',
'CREATE replace_or_empty database_engine COMMA PARAMETERS json',
'CREATE replace_or_empty database_engine PARAMETERS EQUALS json',
'CREATE replace_or_empty database_engine PARAMETERS json',
)
def create_integration(self, p):
is_replace = False
if hasattr(p, 'REPLACE'):
is_replace = True
is_replace = getattr(p, 'replace_or_empty', False)

parameters = None
if hasattr(p, 'json'):
Expand Down Expand Up @@ -1295,28 +1306,28 @@ def expr(self, p):
p.expr.parentheses = True
return p.expr

@_('id LPAREN expr FROM expr RPAREN')
@_('function_name LPAREN expr FROM expr RPAREN')
def function(self, p):
return Function(op=p.id, args=[p.expr0], from_arg=p.expr1)
return Function(op=p[0], args=[p.expr0], from_arg=p.expr1)

@_('DATABASE LPAREN RPAREN')
def function(self, p):
return Function(op=p.DATABASE, args=[])

@_('id LPAREN DISTINCT expr_list RPAREN')
@_('function_name LPAREN DISTINCT expr_list RPAREN')
def function(self, p):
return Function(op=p.id, distinct=True, args=p.expr_list)
return Function(op=p[0], distinct=True, args=p.expr_list)

@_('id LPAREN expr_list_or_nothing RPAREN',
'id LPAREN star RPAREN')
@_('function_name LPAREN expr_list_or_nothing RPAREN',
'function_name LPAREN star RPAREN')
def function(self, p):
if hasattr(p, 'star'):
args = [p.star]
else:
args = p.expr_list_or_nothing
if not args:
args = []
return Function(op=p.id, args=args)
return Function(op=p[0], args=args)

# arguments are optional in functions, so that things like `select database()` are possible
@_('expr BETWEEN expr AND expr')
Expand Down Expand Up @@ -1566,6 +1577,13 @@ def string(self, p):
def parameter(self, p):
return Parameter(value=p.PARAMETER)

@_('id',
'FULL',
'RIGHT',
'LEFT')
def function_name(self, p):
return p[0]

# convert to types
@_('ID',
'BEGIN',
Expand All @@ -1588,7 +1606,6 @@ def parameter(self, p):
'ENGINES',
'EXTENDED',
'FIELDS',
# 'FULL', # fixme: is parsed as alias
'GLOBAL',
'HORIZON',
'HOSTS',
Expand Down
3 changes: 3 additions & 0 deletions mindsdb_sql/planner/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -299,6 +299,9 @@ def query_traversal(node, callback, is_table=False, is_target=False, parent_quer
array.append(node_out)
return array

# keep original node
return None


def convert_join_to_list(join):
# join tree to table list
Expand Down
4 changes: 3 additions & 1 deletion mindsdb_sql/render/sqlalchemy_render.py
Original file line number Diff line number Diff line change
Expand Up @@ -156,9 +156,11 @@ def to_expression(self, t):
sa_op = getattr(arg0, method)

col = sa_op(arg1)
else:
elif t.op.lower() in functions:
func = functions[t.op.lower()]
col = func(arg0, arg1)
else:
col = arg0.op(t.op)(arg1)

if t.alias:
alias = self.get_alias(t.alias)
Expand Down
34 changes: 19 additions & 15 deletions tests/test_parser/test_base_sql/test_base_sql.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import pytest
from textwrap import dedent
from mindsdb_sql import parse_sql

from mindsdb_sql.parser.ast import *
Expand Down Expand Up @@ -34,22 +34,26 @@ def test_not_equal(self):

def test_escaping(self):
expected_ast = Select(
targets=[Constant(value="a ' \" b")]
targets=[
Constant(value="a ' \" b"),
Constant(value="a ' \" b"),
Constant(value="a \\n b"),
Constant(value="a \\\n b"),
Constant(value="a \\\n b"),
Constant(value="a\nb"),
]
)

sql = """
select 'a \\' \\" b'
"""

ast = parse_sql(sql)

assert str(ast).lower() == str(expected_ast).lower()
assert ast.to_tree() == expected_ast.to_tree()

# in double quotes
sql = """
select "a \\' \\" b"
"""
sql = dedent('''
select
'a \\' \\" b', -- double quote
"a \\' \\" b", -- single quote
"a \\n b",
"a \\\n b", -- double quote
'a \\\n b', -- single quote
"a
b"
''')

ast = parse_sql(sql)

Expand Down
40 changes: 39 additions & 1 deletion tests/test_parser/test_base_sql/test_create.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,10 @@
from mindsdb_sql import parse_sql
from mindsdb_sql.parser.ast import *


@pytest.mark.parametrize('dialect', ['mysql', 'mindsdb'])
class TestCreate:
def test_create(self, dialect):
def test_create_from_select(self, dialect):
expected_ast = CreateTable(
name=Identifier('int1.model_name'),
is_replace=True,
Expand Down Expand Up @@ -49,3 +50,40 @@ def test_create(self, dialect):
assert ast.to_tree() == expected_ast.to_tree()


class TestCreateMindsdb:

def test_create(self):

for is_replace in [True, False]:
for if_not_exists in [True, False]:

expected_ast = CreateTable(
name=Identifier('mydb.Persons'),
is_replace=is_replace,
if_not_exists=if_not_exists,
columns=[
TableColumn(name='PersonID', type='int'),
TableColumn(name='LastName', type='varchar', length=255),
TableColumn(name='FirstName', type='char', length=10),
TableColumn(name='Info', type='json'),
TableColumn(name='City', type='varchar'),
]
)
replace_str = 'OR REPLACE' if is_replace else ''
exist_str = 'IF NOT EXISTS' if if_not_exists else ''

sql = f'''
CREATE {replace_str} TABLE {exist_str} mydb.Persons(
PersonID int,
LastName varchar(255),
FirstName char(10),
Info json,
City varchar
)
'''
print(sql)
ast = parse_sql(sql)

assert str(ast).lower() == str(expected_ast).lower()
assert ast.to_tree() == expected_ast.to_tree()

17 changes: 17 additions & 0 deletions tests/test_parser/test_base_sql/test_select_structure.py
Original file line number Diff line number Diff line change
Expand Up @@ -1019,3 +1019,20 @@ def test_case(self):

assert ast.to_tree() == expected_ast.to_tree()
assert str(ast) == str(expected_ast)

def test_select_left(self):
sql = f'select left(a, 1) from tab1'
ast = parse_sql(sql)

expected_ast = Select(
targets=[
Function(op='left', args=[
Identifier('a'),
Constant(1)
])
],
from_table=Identifier('tab1')
)

assert ast.to_tree() == expected_ast.to_tree()
assert str(ast) == str(expected_ast)
Loading
Loading