Skip to content

Commit d207b15

Browse files
perf: Fir 13517 allow skipping query parsing i (#170)
* allow skipping parsing * add unit tests
1 parent 3a9dbd3 commit d207b15

File tree

4 files changed

+119
-10
lines changed

4 files changed

+119
-10
lines changed

src/firebolt/async_db/cursor.py

Lines changed: 72 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
Optional,
1717
Sequence,
1818
Tuple,
19+
Union,
1920
)
2021

2122
from aiorwlock import RWLock
@@ -289,6 +290,7 @@ async def _do_execute(
289290
raw_query: str,
290291
parameters: Sequence[Sequence[ParameterType]],
291292
set_parameters: Optional[Dict] = None,
293+
skip_parsing: bool = False,
292294
) -> None:
293295
self._reset()
294296
if set_parameters is not None:
@@ -298,7 +300,16 @@ async def _do_execute(
298300
)
299301
try:
300302

301-
queries = split_format_sql(raw_query, parameters)
303+
if parameters and skip_parsing:
304+
logger.warning(
305+
"Query formatting parameters are provided with skip_parsing."
306+
" They will be ignored"
307+
)
308+
309+
# Allow users to manually skip parsing for performance improvement
310+
queries: List[Union[SetParameter, str]] = (
311+
[raw_query] if skip_parsing else split_format_sql(raw_query, parameters)
312+
)
302313

303314
for query in queries:
304315

@@ -351,20 +362,70 @@ async def execute(
351362
query: str,
352363
parameters: Optional[Sequence[ParameterType]] = None,
353364
set_parameters: Optional[Dict] = None,
365+
skip_parsing: bool = False,
354366
) -> int:
355-
"""Prepare and execute a database query. Return row count."""
356-
367+
"""Prepare and execute a database query.
368+
369+
Supported features:
370+
Parameterized queries: placeholder characters ('?') are substituted
371+
with values provided in `parameters`. Values are formatted to
372+
be properly recognized by database and to exclude SQL injection.
373+
Multi-statement queries: multiple statements, provided in a single query
374+
and separated by semicolon are executed separatelly and sequentially.
375+
To switch to next statement result, `nextset` method should be used.
376+
SET statements: to provide additional query execution parameters, execute
377+
`SET param=value` statement before it. All parameters are stored in
378+
cursor object until it's closed. They can also be removed with
379+
`flush_parameters` method call.
380+
381+
Args:
382+
query (str): SQL query to execute
383+
parameters (Optional[Sequence[ParameterType]]): A sequence of substitution
384+
parameters. Used to replace '?' placeholders inside a query with
385+
actual values
386+
set_parameters (Optional[Dict]): List of set parameters to execute
387+
a query with. DEPRECATED: Use SET SQL statements instead
388+
skip_parsing (bool): Flag to disable query parsing. This will
389+
disable parameterized, multi-statement and SET queries,
390+
while improving performance
391+
392+
Returns:
393+
int: Query row count
394+
"""
357395
params_list = [parameters] if parameters else []
358-
await self._do_execute(query, params_list, set_parameters)
396+
await self._do_execute(query, params_list, set_parameters, skip_parsing)
359397
return self.rowcount
360398

361399
@check_not_closed
362400
async def executemany(
363401
self, query: str, parameters_seq: Sequence[Sequence[ParameterType]]
364402
) -> int:
365-
"""
366-
Prepare and execute a database query against all parameter
367-
sequences provided. Return last query row count.
403+
"""Prepare and execute a database query.
404+
405+
Supports providing multiple substitution parameter sets, executing them
406+
as multiple statements sequentially.
407+
408+
Supported features:
409+
Parameterized queries: placeholder characters ('?') are substituted
410+
with values provided in `parameters`. Values are formatted to
411+
be properly recognized by database and to exclude SQL injection.
412+
Multi-statement queries: multiple statements, provided in a single query
413+
and separated by semicolon are executed separatelly and sequentially.
414+
To switch to next statement result, `nextset` method should be used.
415+
SET statements: to provide additional query execution parameters, execute
416+
`SET param=value` statement before it. All parameters are stored in
417+
cursor object until it's closed. They can also be removed with
418+
`flush_parameters` method call.
419+
420+
Args:
421+
query (str): SQL query to execute
422+
parameters_seq (Sequence[Sequence[ParameterType]]): A sequence of
423+
substitution parameter sets. Used to replace '?' placeholders inside a
424+
query with actual values from each set in a sequence. Resulting queries
425+
for each subset are executed sequentially.
426+
427+
Returns:
428+
int: Query row count
368429
"""
369430
await self._do_execute(query, parameters_seq)
370431
return self.rowcount
@@ -472,10 +533,12 @@ async def execute(
472533
query: str,
473534
parameters: Optional[Sequence[ParameterType]] = None,
474535
set_parameters: Optional[Dict] = None,
536+
skip_parsing: bool = False,
475537
) -> int:
476538
async with self._async_query_lock.writer:
477-
return await super().execute(query, parameters, set_parameters)
478-
"""Prepare and execute a database query"""
539+
return await super().execute(
540+
query, parameters, set_parameters, skip_parsing
541+
)
479542

480543
@wraps(BaseCursor.executemany)
481544
async def executemany(

src/firebolt/db/cursor.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,10 +48,11 @@ def execute(
4848
query: str,
4949
parameters: Optional[Sequence[ParameterType]] = None,
5050
set_parameters: Optional[Dict] = None,
51+
skip_parsing: bool = False,
5152
) -> int:
5253
with self._query_lock.gen_wlock():
5354
return async_to_sync(super().execute, self._async_job_thread)(
54-
query, parameters, set_parameters
55+
query, parameters, set_parameters, skip_parsing
5556
)
5657

5758
@wraps(AsyncBaseCursor.executemany)

tests/unit/async_db/test_cursor.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from inspect import cleandoc
22
from typing import Callable, Dict, List
3+
from unittest.mock import patch
34

45
from httpx import HTTPStatusError, StreamError, codes
56
from pytest import mark, raises
@@ -575,3 +576,25 @@ async def test_cursor_set_parameters_sent(
575576

576577
httpx_mock.add_callback(query_with_params_callback, url=f"{query_url}{params}")
577578
await cursor.execute("select 1")
579+
580+
581+
@mark.asyncio
582+
async def test_cursor_skip_parse(
583+
httpx_mock: HTTPXMock,
584+
auth_callback: Callable,
585+
auth_url: str,
586+
query_url: str,
587+
query_callback: Callable,
588+
cursor: Cursor,
589+
):
590+
"""Cursor doesn't process a query if skip_parsing is provided"""
591+
httpx_mock.add_callback(auth_callback, url=auth_url)
592+
httpx_mock.add_callback(query_callback, url=query_url)
593+
594+
with patch("firebolt.async_db.cursor.split_format_sql") as split_format_sql_mock:
595+
await cursor.execute("non-an-actual-sql")
596+
split_format_sql_mock.assert_called_once()
597+
598+
with patch("firebolt.async_db.cursor.split_format_sql") as split_format_sql_mock:
599+
await cursor.execute("non-an-actual-sql", skip_parsing=True)
600+
split_format_sql_mock.assert_not_called()

tests/unit/db/test_cursor.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from inspect import cleandoc
22
from typing import Callable, Dict, List
3+
from unittest.mock import patch
34

45
from httpx import HTTPStatusError, StreamError, codes
56
from pytest import raises
@@ -518,3 +519,24 @@ def test_cursor_set_parameters_sent(
518519

519520
httpx_mock.add_callback(query_with_params_callback, url=f"{query_url}{params}")
520521
cursor.execute("select 1")
522+
523+
524+
def test_cursor_skip_parse(
525+
httpx_mock: HTTPXMock,
526+
auth_callback: Callable,
527+
auth_url: str,
528+
query_url: str,
529+
query_callback: Callable,
530+
cursor: Cursor,
531+
):
532+
"""Cursor doesn't process a query if skip_parsing is provided"""
533+
httpx_mock.add_callback(auth_callback, url=auth_url)
534+
httpx_mock.add_callback(query_callback, url=query_url)
535+
536+
with patch("firebolt.async_db.cursor.split_format_sql") as split_format_sql_mock:
537+
cursor.execute("non-an-actual-sql")
538+
split_format_sql_mock.assert_called_once()
539+
540+
with patch("firebolt.async_db.cursor.split_format_sql") as split_format_sql_mock:
541+
cursor.execute("non-an-actual-sql", skip_parsing=True)
542+
split_format_sql_mock.assert_not_called()

0 commit comments

Comments
 (0)