Skip to content

Commit

Permalink
Fusion fixes
Browse files Browse the repository at this point in the history
  • Loading branch information
kesmit13 committed Oct 25, 2023
1 parent 509b6c5 commit 4b2b29e
Show file tree
Hide file tree
Showing 4 changed files with 166 additions and 15 deletions.
41 changes: 27 additions & 14 deletions singlestoredb/fusion/handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@

CORE_GRAMMAR = r'''
ws = ~r"(\s*(/\*.*\*/)*\s*)*"
qs = ~r"\"([^\"]*)\"|'([^\']*)'"
number = ~r"-?\d+(\.\d+)?|-?\.d+"
qs = ~r"\"([^\"]*)\"|'([^\']*)'|`([^\`]*)`|(\S+)"
number = ~r"[-+]?(\d*\.)?\d+(e[-+]?\d+)?"i
integer = ~r"-?\d+"
comma = ws "," ws
open_paren = ws "(" ws
Expand All @@ -32,10 +32,10 @@

def get_keywords(grammar: str) -> Tuple[str, ...]:
"""Return all all-caps words from the beginning of the line."""
m = re.match(r'^\s*([A-Z0-9_]+(\s+|$))+', grammar)
m = re.match(r'^\s*([A-Z0-9_]+(\s+|$|;))+', grammar)
if not m:
return tuple()
return tuple(re.split(r'\s+', m.group(0).strip()))
return tuple(re.split(r'\s+', m.group(0).replace(';', '').strip()))


def process_optional(m: Any) -> str:
Expand Down Expand Up @@ -322,6 +322,7 @@ class SQLHandler(NodeVisitor):
#: Rule validation functions
validators: Dict[str, Callable[..., Any]] = {}

_grammar: str = CORE_GRAMMAR
_is_compiled: bool = False

def __init__(self, connection: Connection):
Expand All @@ -347,6 +348,7 @@ def compile(cls, grammar: str = '') -> None:
cls.grammar, cls.command_key, cls.rule_info, cls.help = \
process_grammar(grammar or cls.__doc__ or '')

cls._grammar = grammar or cls.__doc__ or ''
cls._is_compiled = True

def create_result(self) -> result.FusionSQLResult:
Expand Down Expand Up @@ -384,10 +386,15 @@ def execute(self, sql: str) -> result.FusionSQLResult:
"""
type(self).compile()
try:
res = self.run(self.visit(type(self).grammar.parse(sql)))
params = self.visit(type(self).grammar.parse(sql))
for k, v in params.items():
params[k] = self.validate_rule(k, v)
res = self.run(params)
if res is not None:
return res
return result.FusionSQLResult(self.connection)
res = result.FusionSQLResult(self.connection)
res.set_rows([])
return res
except ParseError as exc:
s = str(exc)
msg = ''
Expand Down Expand Up @@ -421,7 +428,7 @@ def run(self, params: Dict[str, Any]) -> Optional[result.FusionSQLResult]:
"""
raise NotImplementedError

def create_like_func(self, like: str) -> Callable[[str], bool]:
def create_like_func(self, like: Optional[str]) -> Callable[[str], bool]:
"""
Construct a function to apply the LIKE clause.
Expand Down Expand Up @@ -457,7 +464,16 @@ def visit_qs(self, node: Node, visited_children: Iterable[Any]) -> Any:
"""Quoted strings."""
if node is None:
return None
return node.match.group(1) or node.match.group(2)
return node.match.group(1) or node.match.group(2) or \
node.match.group(3) or node.match.group(4)

def visit_number(self, node: Node, visited_children: Iterable[Any]) -> Any:
"""Numeric value."""
return float(node.match.group(0))

def visit_integer(self, node: Node, visited_children: Iterable[Any]) -> Any:
"""Integer value."""
return int(node.match.group(0))

def visit_ws(self, node: Node, visited_children: Iterable[Any]) -> Any:
"""Whitespace and comments."""
Expand Down Expand Up @@ -505,13 +521,10 @@ def generic_visit(self, node: Node, visited_children: Iterable[Any]) -> Any:
# Filter out stray empty strings
out = [x for x in flatten(visited_children)[n_keywords:] if x]

if repeats:
return {node.expr_name: self.validate_rule(node.expr_name, out)}
if repeats or len(out) > 1:
return {node.expr_name: out}

return {
node.expr_name:
self.validate_rule(node.expr_name, out[0]) if out else True,
}
return {node.expr_name: out[0] if out else True}

if hasattr(node, 'match'):
if not visited_children and not node.match.groups():
Expand Down
55 changes: 55 additions & 0 deletions singlestoredb/fusion/registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@
import re
from typing import Any
from typing import Dict
from typing import List
from typing import Optional
from typing import Tuple
from typing import Type
from typing import Union

Expand Down Expand Up @@ -110,3 +112,56 @@ def execute(
raise RuntimeError(f'could not find handler for query: {sql}')

return handler(connection).execute(sql)


class ShowFusionCommandsHandler(SQLHandler):
"""
SHOW FUSION COMMANDS [ like ];
# LIKE pattern
like = LIKE '<pattern>'
"""

def run(self, params: Dict[str, Any]) -> Optional[result.FusionSQLResult]:
res = self.create_result()
res.add_field('Command', result.STRING)

is_like = self.create_like_func(params.get('like'))

data: List[Tuple[Any, ...]] = []
for _, v in sorted(_handlers.items()):
if v is type(self):
continue
if is_like(' '.join(v.command_key)):
data.append((v.help,))

res.set_rows(data)

return res


ShowFusionCommandsHandler.register()


class ShowFusionGrammarHandler(SQLHandler):
"""
SHOW FUSION GRAMMAR for_query;
# Query to show grammar for
for_query = FOR '<query>'
"""

def run(self, params: Dict[str, Any]) -> Optional[result.FusionSQLResult]:
res = self.create_result()
res.add_field('Grammar', result.STRING)
handler = get_handler(params['for_query'])
data: List[Tuple[Any, ...]] = []
if handler is not None:
data.append((handler._grammar,))
res.set_rows(data)
return res


ShowFusionGrammarHandler.register()
83 changes: 83 additions & 0 deletions singlestoredb/tests/test_fusion.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
#!/usr/bin/env python
# type: ignore
"""SingleStoreDB Fusion testing."""
import os
import unittest

import singlestoredb as s2
from singlestoredb.tests import utils


class TestFusion(unittest.TestCase):

dbname: str = ''
dbexisted: bool = False

@classmethod
def setUpClass(cls):
sql_file = os.path.join(os.path.dirname(__file__), 'test.sql')
cls.dbname, cls.dbexisted = utils.load_sql(sql_file)

@classmethod
def tearDownClass(cls):
if not cls.dbexisted:
utils.drop_database(cls.dbname)

def setUp(self):
self.enabled = os.environ.get('SINGLESTOREDB_ENABLE_FUSION')
os.environ['SINGLESTOREDB_ENABLE_FUSION'] = '1'
self.conn = s2.connect(database=type(self).dbname, local_infile=True)
self.cur = self.conn.cursor()

def tearDown(self):
if self.enabled:
os.environ['SINGLESTOREDB_ENABLE_FUSION'] = self.enabled
else:
del os.environ['SINGLESTOREDB_ENABLE_FUSION']

try:
if self.cur is not None:
self.cur.close()
except Exception:
# traceback.print_exc()
pass

try:
if self.conn is not None:
self.conn.close()
except Exception:
# traceback.print_exc()
pass

def test_env_var(self):
os.environ['SINGLESTOREDB_ENABLE_FUSION'] = '0'

with self.assertRaises(s2.ProgrammingError):
self.cur.execute('show fusion commands')

del os.environ['SINGLESTOREDB_ENABLE_FUSION']

with self.assertRaises(s2.ProgrammingError):
self.cur.execute('show fusion commands')

os.environ['SINGLESTOREDB_ENABLE_FUSION'] = 'yes'

self.cur.execute('show fusion commands')
assert list(self.cur)

def test_show_commands(self):
self.cur.execute('show fusion commands')
cmds = [x[0] for x in self.cur.fetchall()]
assert cmds
assert [x for x in cmds if x.strip().startswith('SHOW FUSION GRAMMAR')], cmds

self.cur.execute('show fusion commands like "create%"')
cmds = [x[0] for x in self.cur.fetchall()]
assert cmds
assert [x for x in cmds if x.strip().startswith('CREATE')] == cmds, cmds

def test_show_grammar(self):
self.cur.execute('show fusion grammar for "create workspace"')
cmds = [x[0] for x in self.cur.fetchall()]
assert cmds
assert [x for x in cmds if x.strip().startswith('CREATE WORKSPACE')], cmds
2 changes: 1 addition & 1 deletion singlestoredb/tests/test_management.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
#!/usr/bin/env python
# type: ignore
"""SingleStoreDB HTTP connection testing."""
"""SingleStoreDB Management API testing."""
import os
import pathlib
import random
Expand Down

0 comments on commit 4b2b29e

Please sign in to comment.