Skip to content

Commit f9af45b

Browse files
committed
fix(cursor, execute): remove unnecessary bind param parsing
1 parent 910be71 commit f9af45b

File tree

2 files changed

+45
-17
lines changed

2 files changed

+45
-17
lines changed

redshift_connector/core.py

Lines changed: 24 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1454,17 +1454,17 @@ def inspect_int(self: "Connection", value: int):
14541454
return self.py_types[RedshiftOID.BIGINT]
14551455
return self.py_types[Decimal]
14561456

1457-
def make_params(self: "Connection", values):
1458-
params = []
1457+
def make_params(self: "Connection", values) -> typing.Tuple[typing.Tuple[int, int, typing.Callable], ...]:
1458+
params: typing.List[typing.Tuple[int, int, typing.Callable]] = []
14591459
for value in values:
1460-
typ = type(value)
1460+
typ: typing.Type = type(value)
14611461
try:
14621462
params.append(self.py_types[typ])
14631463
except KeyError:
14641464
try:
14651465
params.append(self.inspect_funcs[typ](value))
14661466
except KeyError as e:
1467-
param = None
1467+
param: typing.Optional[typing.Tuple[int, int, typing.Callable]] = None
14681468
for k, v in self.py_types.items():
14691469
try:
14701470
if isinstance(value, typing.cast(type, k)):
@@ -1593,16 +1593,19 @@ def execute(self: "Connection", cursor: Cursor, operation: str, vals) -> None:
15931593
-------
15941594
None:None
15951595
"""
1596-
if vals is None:
1597-
vals = ()
15981596

15991597
# get the process ID of the calling process.
16001598
pid: int = getpid()
1599+
1600+
args: typing.Tuple[typing.Optional[typing.Tuple[str, typing.Any]], ...] = ()
1601+
# transforms user provided bind parameters to server friendly bind parameters
1602+
params: typing.Tuple[typing.Optional[typing.Tuple[int, int, typing.Callable]], ...] = ()
1603+
16011604
# multi dimensional dictionary to store the data
16021605
# cache = self._caches[cursor.paramstyle][pid]
16031606
# cache = {'statement': {}, 'ps': {}}
1604-
# statement store the data of statement, ps store the data of prepared statement
1605-
# statement = {operation(query): tuple from 'conver_paramstyle'(statement, make_args)}
1607+
# statement stores the data of the statement, ps store the data of the prepared statement
1608+
# statement = {operation(query): tuple from 'convert_paramstyle'(statement, make_args)}
16061609
try:
16071610
cache = self._caches[cursor.paramstyle][pid]
16081611
except KeyError:
@@ -1619,12 +1622,16 @@ def execute(self: "Connection", cursor: Cursor, operation: str, vals) -> None:
16191622
try:
16201623
statement, make_args = cache["statement"][operation]
16211624
except KeyError:
1622-
statement, make_args = cache["statement"][operation] = convert_paramstyle(cursor.paramstyle, operation)
1623-
1624-
args = make_args(vals)
1625-
# change the args to the format that the DB will identify
1626-
# take reference from self.py_types
1627-
params = self.make_params(args)
1625+
if vals:
1626+
statement, make_args = cache["statement"][operation] = convert_paramstyle(cursor.paramstyle, operation)
1627+
else:
1628+
# use a no-op make_args in lieu of parsing the sql statement
1629+
statement, make_args = cache["statement"][operation] = operation, lambda p: ()
1630+
if vals:
1631+
args = make_args(vals)
1632+
# change the args to the format that the DB will identify
1633+
# take reference from self.py_types
1634+
params = self.make_params(args)
16281635
key = operation, params
16291636

16301637
try:
@@ -1653,11 +1660,11 @@ def execute(self: "Connection", cursor: Cursor, operation: str, vals) -> None:
16531660
"pid": pid,
16541661
"statement_num": statement_num,
16551662
"row_desc": [],
1656-
"param_funcs": tuple(x[2] for x in params),
1663+
"param_funcs": tuple(x[2] for x in params), # type: ignore
16571664
}
16581665
cursor.ps = ps
16591666

1660-
param_fcs = tuple(x[1] for x in params)
1667+
param_fcs: typing.Tuple[typing.Optional[int], ...] = tuple(x[1] for x in params) # type: ignore
16611668

16621669
# Byte1('P') - Identifies the message as a Parse command.
16631670
# Int32 - Message length, including self.
@@ -1670,7 +1677,7 @@ def execute(self: "Connection", cursor: Cursor, operation: str, vals) -> None:
16701677
val: typing.Union[bytes, bytearray] = bytearray(statement_name_bin)
16711678
typing.cast(bytearray, val).extend(statement.encode(_client_encoding) + NULL_BYTE)
16721679
typing.cast(bytearray, val).extend(h_pack(len(params)))
1673-
for oid, fc, send_func in params:
1680+
for oid, fc, send_func in params: # type: ignore
16741681
# Parse message doesn't seem to handle the -1 type_oid for NULL
16751682
# values that other messages handle. So we'll provide type_oid
16761683
# 705, the PG "unknown" type.

test/integration/test_connection.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -291,3 +291,24 @@ def test_stl_connection_log_contains_application_name(db_kwargs):
291291
res = cursor.fetchone()
292292
assert res is not None
293293
assert res[0] == 1
294+
295+
296+
@pytest.mark.parametrize("sql", ["select 1", "grant role sys:monitor to awsuser"])
297+
def test_execute_skip_parse_bind_params_when_dne(mocker, db_kwargs, sql):
298+
convert_paramstyle_spy = mocker.spy(redshift_connector.core, "convert_paramstyle")
299+
300+
with redshift_connector.connect(**db_kwargs) as conn:
301+
conn.cursor().execute(sql)
302+
assert not convert_paramstyle_spy.called
303+
304+
305+
@pytest.mark.parametrize(
306+
"sql, args",
307+
[("select %s", "hello world"), ("select %s, %s", ("hello", "world")), ("select %s, %s", [1, "hello world"])],
308+
)
309+
def test_execute_do_parsing_bind_params_when_exist(mocker, db_kwargs, sql, args):
310+
convert_paramstyle_spy = mocker.spy(redshift_connector.core, "convert_paramstyle")
311+
312+
with redshift_connector.connect(**db_kwargs) as conn:
313+
conn.cursor().execute(sql, args)
314+
assert convert_paramstyle_spy.called

0 commit comments

Comments
 (0)