Skip to content

Commit 6aa9bcf

Browse files
committed
BUG28646344: Remove expression parsing on values
Connector/Python assumes all values as expressions, which can lead to errors. This patch fixes this issue by requiring the usage of `mysqlx.expr()` function to mark the values as expressions. Tests were added/changed for regression.
1 parent ee4d6d0 commit 6aa9bcf

File tree

8 files changed

+52
-26
lines changed

8 files changed

+52
-26
lines changed

docs/mysqlx/mysqlx.expr.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
mysqlx.expr
2+
===========
3+
4+
.. autofunction:: mysqlx.expr

docs/mysqlx/mysqlx.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ CRUD
2222
mysqlx.Collection
2323
mysqlx.Table
2424
mysqlx.View
25+
mysqlx.expr
2526

2627
Result
2728
------

docs/mysqlx/tutorials/getting_started.rst

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -257,12 +257,15 @@ This example uses the previews one to show how to remove of the nested field
257257
``Viserion`` on ``dragons`` field and at the same time how to update the value of
258258
the ``count`` field with a new value based in the current one.
259259

260+
.. note:: In the :func:`mysqlx.ModifyStatement.patch()` all strings are considered literals,
261+
for expressions the usage of the :func:`mysqlx.expr()` is required.
262+
260263
.. code-block:: python
261264
262-
collection.modify('name == "Daenerys"').patch('''
265+
collection.modify('name == "Daenerys"').patch(mysqlx.expr('''
263266
JSON_OBJECT("dragons", JSON_OBJECT("count", $.dragons.count -1,
264267
"Viserion", Null))
265-
''').execute()
268+
''')).execute()
266269
doc = mys.collection.find("name = 'Daenerys'").execute().fetch_all()[0]
267270
assert(doc.dragons == {'count': 2,
268271
'Rhaegal': 'green with bronze markings',

lib/mysqlx/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -387,7 +387,7 @@ def get_client(connection_string, options_string):
387387

388388
__all__ = [
389389
# mysqlx.connection
390-
"Client", "Session", "get_client", "get_session",
390+
"Client", "Session", "get_client", "get_session", "expr",
391391

392392
# mysqlx.constants
393393
"Auth", "LockContention", "SSLMode",

lib/mysqlx/dbdoc.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,16 @@
3434
from .errors import ProgrammingError
3535

3636

37+
class ExprJSONEncoder(json.JSONEncoder):
38+
"""A :class:`json.JSONEncoder` subclass, which enables encoding of
39+
:class:`mysqlx.ExprParser` objects."""
40+
def default(self, o): # pylint: disable=E0202
41+
if hasattr(o, "expr"):
42+
return "{0}".format(o)
43+
# Let the base class default method raise the TypeError
44+
return json.JSONEncoder.default(self, o)
45+
46+
3747
class DbDoc(object):
3848
"""Represents a generic document in JSON format.
3949
@@ -52,7 +62,7 @@ def __init__(self, value):
5262
raise ValueError("Unable to handle type: {0}".format(type(value)))
5363

5464
def __str__(self):
55-
return json.dumps(self.__dict__)
65+
return json.dumps(self.__dict__, cls=ExprJSONEncoder)
5666

5767
def __repr__(self):
5868
return repr(self.__dict__)

lib/mysqlx/expr.py

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Copyright (c) 2016, 2017, Oracle and/or its affiliates. All rights reserved.
1+
# Copyright (c) 2016, 2018, Oracle and/or its affiliates. All rights reserved.
22
#
33
# This program is free software; you can redistribute it and/or modify
44
# it under the terms of the GNU General Public License, version 2.0, as
@@ -297,11 +297,6 @@ def build_expr(value):
297297
elif isinstance(value, (list, tuple)):
298298
msg["type"] = mysqlxpb_enum("Mysqlx.Expr.Expr.Type.ARRAY")
299299
msg["array"] = build_array(value).get_message()
300-
# look for MySQL expressions
301-
elif isinstance(value, STRING_TYPES) and \
302-
(("(" in value and ")" in value) or ("{" in value and "}" in value)):
303-
expr_parser = ExprParser(value, False)
304-
return expr_parser.expr()
305300
else:
306301
msg["type"] = mysqlxpb_enum("Mysqlx.Expr.Expr.Type.LITERAL")
307302
msg["literal"] = build_scalar(value).get_message()
@@ -423,6 +418,9 @@ def __init__(self, string, allow_relational=False):
423418
self.clean_expression()
424419
self.lex()
425420

421+
def __str__(self):
422+
return "<mysqlx.ExprParser '{}'>".format(self.string)
423+
426424
def clean_expression(self):
427425
"""Removes the keywords that does not form part of the expression.
428426

lib/mysqlx/statement.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -753,14 +753,14 @@ def patch(self, doc):
753753
"""
754754
if doc is None:
755755
doc = ''
756-
if not isinstance(doc, (dict, DbDoc, str)):
756+
if not isinstance(doc, (ExprParser, dict, DbDoc, str)):
757757
raise ProgrammingError(
758758
"Invalid data for update operation on document collection "
759759
"table")
760760
self._update_ops.append(
761761
UpdateSpec(mysqlxpb_enum(
762762
"Mysqlx.Crud.UpdateOperation.UpdateType.MERGE_PATCH"),
763-
'', doc))
763+
'', doc.expr() if isinstance(doc, ExprParser) else doc))
764764
return self
765765

766766
def execute(self):

tests/test_mysqlx_crud.py

Lines changed: 24 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -572,22 +572,33 @@ def test_add(self):
572572
self.assertEqual(result.get_affected_items_count(), 1)
573573
self.assertEqual(1, collection.count())
574574

575-
# now add multiple dictionaries at once
575+
# Adding multiple dictionaries at once
576576
result = collection.add(
577577
{"_id": 2, "name": "Wilma", "age": 33},
578578
{"_id": 3, "name": "Barney", "age": 42}
579579
).execute()
580580
self.assertEqual(result.get_affected_items_count(), 2)
581581
self.assertEqual(3, collection.count())
582582

583-
# now let's try adding strings
583+
# Adding JSON strings
584584
result = collection.add(
585585
'{"_id": 4, "name": "Bambam", "age": 8}',
586586
'{"_id": 5, "name": "Pebbles", "age": 8}'
587587
).execute()
588588
self.assertEqual(result.get_affected_items_count(), 2)
589589
self.assertEqual(5, collection.count())
590590

591+
# All strings should be considered literal, for expressions
592+
# mysqlx.expr() function must be used
593+
collection.add(
594+
{"_id": "6", "status": "Approved",
595+
"email": "Fred (fred@example.com)"},
596+
{"_id": "7", "status": "Rejected\n(ORA:Pending)",
597+
"email": "Barney (barney@example.com)"},
598+
).execute()
599+
result = collection.find().execute()
600+
self.assertEqual(7, len(result.fetch_all()))
601+
591602
if tests.MYSQL_VERSION > (8, 0, 4):
592603
# Following test are only possible on servers with id generetion.
593604
# Ensure _id is created at the server side
@@ -1370,10 +1381,10 @@ def test_modify_patch(self):
13701381
"Rhaegal": "green with bronze markings"},
13711382
doc.dragons)
13721383

1373-
# Test add new attribute using expresion (function call)
1374-
result = collection.modify('name == "Daenerys"').patch(
1384+
# Test add new attribute using expression
1385+
result = collection.modify('name == "Daenerys"').patch(mysqlx.expr(
13751386
'JSON_OBJECT("dragons", JSON_OBJECT("count", 3))'
1376-
).execute()
1387+
)).execute()
13771388
self.assertEqual(1, result.get_affected_items_count())
13781389
doc = collection.find("name = 'Daenerys'").execute().fetch_all()[0]
13791390
self.assertEqual(
@@ -1382,11 +1393,10 @@ def test_modify_patch(self):
13821393
"count": 3},
13831394
doc.dragons)
13841395

1385-
# Test update attribute value using expresion (function call)
1386-
result = collection.modify('name == "Daenerys"').patch(
1396+
# Test update attribute value using expression
1397+
result = collection.modify('name == "Daenerys"').patch(mysqlx.expr(
13871398
'JSON_OBJECT("dragons",'
1388-
' JSON_OBJECT("count", $.dragons.count - 1))'
1389-
).execute()
1399+
' JSON_OBJECT("count", $.dragons.count - 1))')).execute()
13901400
self.assertEqual(1, result.get_affected_items_count())
13911401
doc = collection.find("name = 'Daenerys'").execute().fetch_all()[0]
13921402
self.assertEqual(
@@ -1395,10 +1405,10 @@ def test_modify_patch(self):
13951405
"count": 2},
13961406
doc.dragons)
13971407

1398-
# Test update attribute value using expresion without JSON functions
1399-
result = collection.modify('TRUE').patch(
1408+
# Test update attribute value using expression without JSON functions
1409+
result = collection.modify('TRUE').patch(mysqlx.expr(
14001410
'{"actors_bio": {"current": {"day_of_birth": CAST(SUBSTRING_INDEX('
1401-
' $.actors_bio.bd, " ", - 1) AS DECIMAL)}}}').execute()
1411+
' $.actors_bio.bd, " ", - 1) AS DECIMAL)}}}')).execute()
14021412
self.assertEqual(8, result.get_affected_items_count())
14031413

14041414
# Test update attribute value using mysqlx.expr
@@ -1436,9 +1446,9 @@ def test_modify_patch(self):
14361446
doc.actors_bio)
14371447

14381448
# test use of year funtion.
1439-
result = collection.modify('TRUE').patch(
1449+
result = collection.modify('TRUE').patch(mysqlx.expr(
14401450
'{"actors_bio": {"current": {"last_update": Year(CURDATE())}}}'
1441-
).execute()
1451+
)).execute()
14421452
self.assertEqual(8, result.get_affected_items_count())
14431453

14441454
# Collection.modify() is not allowed without a condition

0 commit comments

Comments
 (0)