Skip to content

Commit 7cf9f5f

Browse files
committed
Add reserved words support for select
1 parent e6f21e7 commit 7cf9f5f

7 files changed

Lines changed: 104 additions & 6 deletions

File tree

pydynamodb/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
if TYPE_CHECKING:
77
from .connection import Connection
88

9-
__version__: str = "0.7.4"
9+
__version__: str = "0.7.5"
1010

1111
# Globals https://www.python.org/dev/peps/pep-0249/#globals
1212
apilevel: str = "2.0"

pydynamodb/sql/common.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -351,6 +351,12 @@ def get_query_type(sql: str) -> QueryType:
351351
raise LookupError("Not supported query type")
352352

353353

354+
def escape_keyword(word: str) -> str:
355+
if word.upper() in RESERVED_WORDS:
356+
return f"\"{word}\""
357+
else:
358+
return word
359+
354360
# DynamoDB reserved words
355361
# https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/ReservedWords.html
356362
RESERVED_WORDS = [

pydynamodb/sql/dml_select.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@
3636
from abc import ABCMeta
3737
from collections import OrderedDict
3838
from .dml_sql import DmlBase, DmlFunction
39-
from .common import KeyWords, Tokens
39+
from .common import KeyWords, Tokens, escape_keyword
4040
from pyparsing import Opt, Forward, Group, ZeroOrMore, delimited_list, Regex
4141
from typing import Any, Dict, List, Optional
4242

@@ -212,7 +212,7 @@ def _construct_columns(self, columns: List[Any]) -> str:
212212
return "*"
213213

214214
column_ = []
215-
column_.append(column["column_name"])
215+
column_.append(escape_keyword(column["column_name"]))
216216

217217
for rcolumn in column["column_ops"]:
218218
column_.append(rcolumn["arithmetic_operators"])

pydynamodb/sql/dml_sql.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ class DmlBase(Base):
3636
],
3737
)
3838

39-
ATTR_NAME = Opt('"') + Word(alphanums + "_-") + Opt('"')
39+
ATTR_NAME = Opt('"') + Word(alphanums + "_-")("attr_name").set_name("attr_name") + Opt('"')
4040
ATTR_ARRAY_NAME = ATTR_NAME + "[" + Word(nums) + "]"
4141

4242
_COLUMN_NAME = (

tests/test_cursor_dml.py

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -379,7 +379,7 @@ def test_reserved_words(self, cursor):
379379
sql_reserved_words_1 = (
380380
"""
381381
INSERT INTO %s VALUE {
382-
'key_partition': ?, 'key_sort': ?, 'username': ?, 'password': ?
382+
'key_partition': ?, 'key_sort': ?, 'username': ?, 'password': ?, 'default': ?, 'comment': ?
383383
}
384384
"""
385385
% USER_TABLE
@@ -389,14 +389,27 @@ def test_reserved_words(self, cursor):
389389
0,
390390
"admin",
391391
"admin",
392+
1,
393+
"",
392394
]
393395
cursor.execute(sql_reserved_words_1, params_1_)
394396

397+
cursor.execute(
398+
"""
399+
SELECT username, password, "default", "comment" FROM %s
400+
WHERE key_partition=? AND key_sort=?
401+
"""
402+
% USER_TABLE,
403+
["test_user_row_1", 0],
404+
)
405+
assert cursor.fetchone() == ("admin", "admin", 1, "")
406+
395407
sql_reserved_words_2 = (
396408
"""
397409
UPDATE %s
398410
SET username=?
399411
SET password=?
412+
SET "default"=?
400413
WHERE key_partition=? AND key_sort=?
401414
RETURNING ALL OLD *
402415
"""
@@ -405,11 +418,22 @@ def test_reserved_words(self, cursor):
405418
params_2_ = [
406419
"admin1",
407420
"admin1",
421+
0,
408422
"test_user_row_1",
409423
0,
410424
]
411425
cursor.execute(sql_reserved_words_2, params_2_)
412426

427+
cursor.execute(
428+
"""
429+
SELECT username, password, "default" FROM %s
430+
WHERE key_partition=? AND key_sort=?
431+
"""
432+
% USER_TABLE,
433+
["test_user_row_1", 0],
434+
)
435+
assert cursor.fetchone() == ("admin1", "admin1", 0)
436+
413437
sql_reserved_words_3 = (
414438
"""
415439
DELETE FROM %s

tests/test_dml_select.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -187,7 +187,7 @@ def test_parse_completed_case(self):
187187
"""
188188
ret = SQLParser(sql).transform()
189189
assert ret == {
190-
"Statement": 'SELECT IssueId,Total,Content.DateWatched[0] FROM "Issues"."CreateDateIndex" '
190+
"Statement": 'SELECT IssueId,"Total",Content.DateWatched[0] FROM "Issues"."CreateDateIndex" '
191191
+ "WHERE IssueId IN [100,300,234] "
192192
+ "AND Title = 'some title' "
193193
+ "AND Content[0] >= 100 "

tests/test_sqlalchemy_dynamodb.py

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
Base = declarative_base()
88

99
TESTCASE02_TABLE = "pydynamodb_test_case02"
10+
USER_TABLE = "user"
1011

1112

1213
# Declarative Mapping
@@ -19,6 +20,16 @@ class _TestCase02(Base):
1920
col_num = Column(Numeric)
2021
col_nested = Column()
2122

23+
class _User(Base):
24+
__tablename__ = USER_TABLE
25+
26+
key_partition = Column(String, primary_key=True)
27+
key_sort = Column(Integer, primary_key=True)
28+
username = Column(String)
29+
password = Column(String)
30+
default = Column(Numeric)
31+
comment = Column(String)
32+
2233

2334
class TestSQLAlchemyDynamoDB:
2435
def test_ping(self, engine):
@@ -370,3 +381,60 @@ def test_data_limit(self, engine):
370381
)
371382
rows = conn.execute(table.select().limit(1)).fetchall()
372383
assert len(rows) == 1
384+
385+
def test_reserved_word_table_insert(self, engine):
386+
engine, conn = engine
387+
388+
with Session(engine) as session:
389+
for i in range(0, 5):
390+
user = _User()
391+
user.key_partition = "test_user_row_2"
392+
user.key_sort = i
393+
user.username = "user" + str(i)
394+
user.password = "pwd" + str(i)
395+
user.default = 1
396+
user.comment = "user account"
397+
session.add(user)
398+
session.commit()
399+
400+
rows = conn.execute(
401+
text(
402+
"""
403+
SELECT * FROM %s WHERE key_partition = :pk
404+
"""
405+
% USER_TABLE
406+
),
407+
{"pk": "test_user_row_2"},
408+
).fetchall()
409+
assert len(rows) == 5
410+
411+
def test_reserved_word_table_update(self, engine):
412+
engine, conn = engine
413+
414+
with Session(engine) as session:
415+
user = session.scalars(
416+
select(_User).where(
417+
_User.key_partition == "test_user_row_2",
418+
_User.key_sort == 1,
419+
)
420+
).one()
421+
user.username = "user_updated"
422+
user.default = 0
423+
user.comment = "user account updated"
424+
session.commit()
425+
426+
rows = conn.execute(
427+
text(
428+
"""
429+
SELECT username, "default", "comment" FROM %s
430+
WHERE key_partition = :pk
431+
AND key_sort = :sk
432+
"""
433+
% USER_TABLE
434+
),
435+
{"pk": "test_user_row_2", "sk": 1},
436+
).fetchall()
437+
assert len(rows) == 1
438+
assert rows[0][0] == "user_updated"
439+
assert rows[0][1] == 0
440+
assert rows[0][2] == "user account updated"

0 commit comments

Comments
 (0)