Skip to content

Commit 3daf518

Browse files
Apply string interpolation in mogrify if args is not None (#118)
* Apply string interpolation in mogrify regardless of args presence * Fix command with one % * Same fix for http * Back to consistent behavior * Fix test data * mogrify empty args under options * Rename option * Refactor should_interpolate block * Uppercase env var --------- Co-authored-by: Kevin D Smith <ksmith@singlestore.com>
1 parent 20f7e40 commit 3daf518

File tree

8 files changed

+70
-4
lines changed

8 files changed

+70
-4
lines changed

singlestoredb/config.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -251,6 +251,12 @@
251251
environ='SINGLESTOREDB_VECTOR_DATA_FORMAT',
252252
)
253253

254+
register_option(
255+
'interpolate_query_with_empty_args', 'bool', check_bool, False,
256+
'Should mogrify apply string interpolation when args is an empty tuple/list? ',
257+
environ='SINGLESTOREDB_INTERPOLATE_QUERY_WITH_EMPTY_ARGS',
258+
)
259+
254260
register_option(
255261
'fusion.enabled', 'bool', check_bool, False,
256262
'Should Fusion SQL queries be enabled?',

singlestoredb/connection.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1118,9 +1118,15 @@ def __init__(self, **kwargs: Any):
11181118
def _convert_params(
11191119
cls, oper: str,
11201120
params: Optional[Union[Sequence[Any], Dict[str, Any], Any]],
1121+
interpolate_query_with_empty_args: bool = False,
11211122
) -> Tuple[Any, ...]:
11221123
"""Convert query to correct parameter format."""
1123-
if params:
1124+
if interpolate_query_with_empty_args:
1125+
should_convert = params is not None
1126+
else:
1127+
should_convert = bool(params)
1128+
1129+
if should_convert:
11241130

11251131
if cls._map_param_converter is None:
11261132
cls._map_param_converter = sqlparams.SQLParams(
@@ -1333,6 +1339,7 @@ def connect(
13331339
enable_extended_data_types: Optional[bool] = None,
13341340
vector_data_format: Optional[str] = None,
13351341
parse_json: Optional[bool] = None,
1342+
interpolate_query_with_empty_args: Optional[bool] = None,
13361343
) -> Connection:
13371344
"""
13381345
Return a SingleStoreDB connection.
@@ -1418,6 +1425,9 @@ def connect(
14181425
Should extended data types (BSON, vector) be enabled?
14191426
vector_data_format : str, optional
14201427
Format for vector types: json or binary
1428+
interpolate_query_with_empty_args : bool, optional
1429+
Should the connector apply parameter interpolation even when the
1430+
parameters are empty? This corresponds to pymysql/mysqlclient's handling
14211431
14221432
Examples
14231433
--------

singlestoredb/http/connection.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -548,7 +548,12 @@ def _execute(
548548
if handler is not None:
549549
return self._execute_fusion_query(oper, params, handler=handler)
550550

551-
oper, params = self._connection._convert_params(oper, params)
551+
interpolate_query_with_empty_args = self._connection.connection_params.get(
552+
'interpolate_query_with_empty_args', False,
553+
)
554+
oper, params = self._connection._convert_params(
555+
oper, params, interpolate_query_with_empty_args,
556+
)
552557

553558
log_query(oper, params)
554559

singlestoredb/mysql/connection.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -360,6 +360,7 @@ def __init__( # noqa: C901
360360
track_env=False,
361361
enable_extended_data_types=True,
362362
vector_data_format='binary',
363+
interpolate_query_with_empty_args=None,
363364
):
364365
BaseConnection.__init__(**dict(locals()))
365366

@@ -614,6 +615,7 @@ def _config(key, arg):
614615
self._auth_plugin_map = auth_plugin_map or {}
615616
self._binary_prefix = binary_prefix
616617
self.server_public_key = server_public_key
618+
self.interpolate_query_with_empty_args = interpolate_query_with_empty_args
617619

618620
if self.connection_params['nan_as_null'] or \
619621
self.connection_params['inf_as_null']:

singlestoredb/mysql/cursors.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from ..connection import Cursor as BaseCursor
77
from ..utils import results
88
from ..utils.debug import log_query
9+
from ..utils.mogrify import should_interpolate_query
910
from ..utils.results import get_schema
1011

1112
try:
@@ -181,7 +182,7 @@ def mogrify(self, query, args=None):
181182
"""
182183
conn = self._get_db()
183184

184-
if args:
185+
if should_interpolate_query(conn.interpolate_query_with_empty_args, args):
185186
query = query % self._escape_args(args, conn)
186187

187188
return query

singlestoredb/tests/test.sql

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -708,5 +708,10 @@ INSERT INTO bool_data_with_nulls SET id='nt', bool_a=NULL, bool_b=TRUE;
708708
INSERT INTO bool_data_with_nulls SET id='nn', bool_a=NULL, bool_b=NULL;
709709
INSERT INTO bool_data_with_nulls SET id='ff', bool_a=FALSE, bool_b=FALSE;
710710

711+
CREATE TABLE IF NOT EXISTS test_val_with_percent (
712+
i VARCHAR(16)
713+
);
714+
-- Double percent sign for execution from python
715+
INSERT INTO test_val_with_percent VALUES ('a%a');
711716

712717
COMMIT;

singlestoredb/tests/test_connection.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3144,6 +3144,30 @@ def test_f16_vectors(self):
31443144
decimal=2,
31453145
)
31463146

3147+
def test_mogrify_val_with_percent(self):
3148+
conn = s2.connect(
3149+
database=type(self).dbname,
3150+
interpolate_query_with_empty_args=True,
3151+
)
3152+
cur = conn.cursor()
3153+
val_with_percent = 'a%a'
3154+
cur.execute(
3155+
'''SELECT REPLACE(i, "%%", "\\%%")
3156+
FROM test_val_with_percent''',
3157+
(),
3158+
)
3159+
res1 = cur.fetchall()
3160+
assert res1[0][0] == 'a\\%a'
3161+
3162+
cur.execute(
3163+
'''SELECT REPLACE(i, "%%", "\\%%")
3164+
FROM test_val_with_percent
3165+
WHERE i = %s''',
3166+
(val_with_percent,),
3167+
)
3168+
res2 = cur.fetchall()
3169+
assert res2[0][0] == 'a\\%a'
3170+
31473171

31483172
if __name__ == '__main__':
31493173
import nose2

singlestoredb/utils/mogrify.py

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,7 @@ def mogrify(
123123
encoders: Optional[Encoders] = None,
124124
server_status: int = 0,
125125
binary_prefix: bool = False,
126+
interpolate_query_with_empty_args: bool = False,
126127
) -> Union[str, bytes]:
127128
"""
128129
Returns the exact string sent to the database by calling the execute() method.
@@ -135,17 +136,29 @@ def mogrify(
135136
Query to mogrify.
136137
args : Sequence[Any] or Dict[str, Any] or Any, optional
137138
Parameters used with query. (optional)
139+
interpolate_query_with_empty_args : bool, optional
140+
If True, apply string interpolation even when args is an empty tuple/list.
141+
If False, only apply when args is truthy. Defaults to False.
138142
139143
Returns
140144
-------
141145
str : The query with argument binding applied.
142146
143147
"""
144-
if args:
148+
if should_interpolate_query(interpolate_query_with_empty_args, args):
145149
query = query % _escape_args(
146150
args, charset=charset,
147151
encoders=encoders,
148152
server_status=server_status,
149153
binary_prefix=binary_prefix,
150154
)
151155
return query
156+
157+
158+
def should_interpolate_query(
159+
interpolate_query_with_empty_args: bool,
160+
args: Union[Sequence[Any], Dict[str, Any], None],
161+
) -> bool:
162+
if interpolate_query_with_empty_args:
163+
return args is not None
164+
return bool(args)

0 commit comments

Comments
 (0)