Skip to content

Commit 4b2b29e

Browse files
committed
Fusion fixes
1 parent 509b6c5 commit 4b2b29e

File tree

4 files changed

+166
-15
lines changed

4 files changed

+166
-15
lines changed

singlestoredb/fusion/handler.py

Lines changed: 27 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,8 @@
2121

2222
CORE_GRAMMAR = r'''
2323
ws = ~r"(\s*(/\*.*\*/)*\s*)*"
24-
qs = ~r"\"([^\"]*)\"|'([^\']*)'"
25-
number = ~r"-?\d+(\.\d+)?|-?\.d+"
24+
qs = ~r"\"([^\"]*)\"|'([^\']*)'|`([^\`]*)`|(\S+)"
25+
number = ~r"[-+]?(\d*\.)?\d+(e[-+]?\d+)?"i
2626
integer = ~r"-?\d+"
2727
comma = ws "," ws
2828
open_paren = ws "(" ws
@@ -32,10 +32,10 @@
3232

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

4040

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

325+
_grammar: str = CORE_GRAMMAR
325326
_is_compiled: bool = False
326327

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

351+
cls._grammar = grammar or cls.__doc__ or ''
350352
cls._is_compiled = True
351353

352354
def create_result(self) -> result.FusionSQLResult:
@@ -384,10 +386,15 @@ def execute(self, sql: str) -> result.FusionSQLResult:
384386
"""
385387
type(self).compile()
386388
try:
387-
res = self.run(self.visit(type(self).grammar.parse(sql)))
389+
params = self.visit(type(self).grammar.parse(sql))
390+
for k, v in params.items():
391+
params[k] = self.validate_rule(k, v)
392+
res = self.run(params)
388393
if res is not None:
389394
return res
390-
return result.FusionSQLResult(self.connection)
395+
res = result.FusionSQLResult(self.connection)
396+
res.set_rows([])
397+
return res
391398
except ParseError as exc:
392399
s = str(exc)
393400
msg = ''
@@ -421,7 +428,7 @@ def run(self, params: Dict[str, Any]) -> Optional[result.FusionSQLResult]:
421428
"""
422429
raise NotImplementedError
423430

424-
def create_like_func(self, like: str) -> Callable[[str], bool]:
431+
def create_like_func(self, like: Optional[str]) -> Callable[[str], bool]:
425432
"""
426433
Construct a function to apply the LIKE clause.
427434
@@ -457,7 +464,16 @@ def visit_qs(self, node: Node, visited_children: Iterable[Any]) -> Any:
457464
"""Quoted strings."""
458465
if node is None:
459466
return None
460-
return node.match.group(1) or node.match.group(2)
467+
return node.match.group(1) or node.match.group(2) or \
468+
node.match.group(3) or node.match.group(4)
469+
470+
def visit_number(self, node: Node, visited_children: Iterable[Any]) -> Any:
471+
"""Numeric value."""
472+
return float(node.match.group(0))
473+
474+
def visit_integer(self, node: Node, visited_children: Iterable[Any]) -> Any:
475+
"""Integer value."""
476+
return int(node.match.group(0))
461477

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

508-
if repeats:
509-
return {node.expr_name: self.validate_rule(node.expr_name, out)}
524+
if repeats or len(out) > 1:
525+
return {node.expr_name: out}
510526

511-
return {
512-
node.expr_name:
513-
self.validate_rule(node.expr_name, out[0]) if out else True,
514-
}
527+
return {node.expr_name: out[0] if out else True}
515528

516529
if hasattr(node, 'match'):
517530
if not visited_children and not node.match.groups():

singlestoredb/fusion/registry.py

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@
33
import re
44
from typing import Any
55
from typing import Dict
6+
from typing import List
67
from typing import Optional
8+
from typing import Tuple
79
from typing import Type
810
from typing import Union
911

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

112114
return handler(connection).execute(sql)
115+
116+
117+
class ShowFusionCommandsHandler(SQLHandler):
118+
"""
119+
SHOW FUSION COMMANDS [ like ];
120+
121+
# LIKE pattern
122+
like = LIKE '<pattern>'
123+
124+
"""
125+
126+
def run(self, params: Dict[str, Any]) -> Optional[result.FusionSQLResult]:
127+
res = self.create_result()
128+
res.add_field('Command', result.STRING)
129+
130+
is_like = self.create_like_func(params.get('like'))
131+
132+
data: List[Tuple[Any, ...]] = []
133+
for _, v in sorted(_handlers.items()):
134+
if v is type(self):
135+
continue
136+
if is_like(' '.join(v.command_key)):
137+
data.append((v.help,))
138+
139+
res.set_rows(data)
140+
141+
return res
142+
143+
144+
ShowFusionCommandsHandler.register()
145+
146+
147+
class ShowFusionGrammarHandler(SQLHandler):
148+
"""
149+
SHOW FUSION GRAMMAR for_query;
150+
151+
# Query to show grammar for
152+
for_query = FOR '<query>'
153+
154+
"""
155+
156+
def run(self, params: Dict[str, Any]) -> Optional[result.FusionSQLResult]:
157+
res = self.create_result()
158+
res.add_field('Grammar', result.STRING)
159+
handler = get_handler(params['for_query'])
160+
data: List[Tuple[Any, ...]] = []
161+
if handler is not None:
162+
data.append((handler._grammar,))
163+
res.set_rows(data)
164+
return res
165+
166+
167+
ShowFusionGrammarHandler.register()

singlestoredb/tests/test_fusion.py

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
#!/usr/bin/env python
2+
# type: ignore
3+
"""SingleStoreDB Fusion testing."""
4+
import os
5+
import unittest
6+
7+
import singlestoredb as s2
8+
from singlestoredb.tests import utils
9+
10+
11+
class TestFusion(unittest.TestCase):
12+
13+
dbname: str = ''
14+
dbexisted: bool = False
15+
16+
@classmethod
17+
def setUpClass(cls):
18+
sql_file = os.path.join(os.path.dirname(__file__), 'test.sql')
19+
cls.dbname, cls.dbexisted = utils.load_sql(sql_file)
20+
21+
@classmethod
22+
def tearDownClass(cls):
23+
if not cls.dbexisted:
24+
utils.drop_database(cls.dbname)
25+
26+
def setUp(self):
27+
self.enabled = os.environ.get('SINGLESTOREDB_ENABLE_FUSION')
28+
os.environ['SINGLESTOREDB_ENABLE_FUSION'] = '1'
29+
self.conn = s2.connect(database=type(self).dbname, local_infile=True)
30+
self.cur = self.conn.cursor()
31+
32+
def tearDown(self):
33+
if self.enabled:
34+
os.environ['SINGLESTOREDB_ENABLE_FUSION'] = self.enabled
35+
else:
36+
del os.environ['SINGLESTOREDB_ENABLE_FUSION']
37+
38+
try:
39+
if self.cur is not None:
40+
self.cur.close()
41+
except Exception:
42+
# traceback.print_exc()
43+
pass
44+
45+
try:
46+
if self.conn is not None:
47+
self.conn.close()
48+
except Exception:
49+
# traceback.print_exc()
50+
pass
51+
52+
def test_env_var(self):
53+
os.environ['SINGLESTOREDB_ENABLE_FUSION'] = '0'
54+
55+
with self.assertRaises(s2.ProgrammingError):
56+
self.cur.execute('show fusion commands')
57+
58+
del os.environ['SINGLESTOREDB_ENABLE_FUSION']
59+
60+
with self.assertRaises(s2.ProgrammingError):
61+
self.cur.execute('show fusion commands')
62+
63+
os.environ['SINGLESTOREDB_ENABLE_FUSION'] = 'yes'
64+
65+
self.cur.execute('show fusion commands')
66+
assert list(self.cur)
67+
68+
def test_show_commands(self):
69+
self.cur.execute('show fusion commands')
70+
cmds = [x[0] for x in self.cur.fetchall()]
71+
assert cmds
72+
assert [x for x in cmds if x.strip().startswith('SHOW FUSION GRAMMAR')], cmds
73+
74+
self.cur.execute('show fusion commands like "create%"')
75+
cmds = [x[0] for x in self.cur.fetchall()]
76+
assert cmds
77+
assert [x for x in cmds if x.strip().startswith('CREATE')] == cmds, cmds
78+
79+
def test_show_grammar(self):
80+
self.cur.execute('show fusion grammar for "create workspace"')
81+
cmds = [x[0] for x in self.cur.fetchall()]
82+
assert cmds
83+
assert [x for x in cmds if x.strip().startswith('CREATE WORKSPACE')], cmds

singlestoredb/tests/test_management.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
#!/usr/bin/env python
22
# type: ignore
3-
"""SingleStoreDB HTTP connection testing."""
3+
"""SingleStoreDB Management API testing."""
44
import os
55
import pathlib
66
import random

0 commit comments

Comments
 (0)