Skip to content

Commit 43bb773

Browse files
committed
Fix SQL wildcards in string search
Fix String search with special SQL wildcard characters in LIKE/ILIKE clause and add testcase.
1 parent 138ed7f commit 43bb773

File tree

3 files changed

+43
-14
lines changed

3 files changed

+43
-14
lines changed

CHANGES.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,8 @@ Fixed:
4141
(Thomas Arendsen Hein)
4242
- issue2550822: Fix showing more than one additional property in class menu
4343
(James Mack)
44+
- Fix String search with special SQL wildcard characters in LIKE/ILIKE
45+
clause and add testcase (Ralf Schlatterbeck)
4446

4547

4648
2013-07-06: 1.5.0

roundup/backends/rdbms_common.py

Lines changed: 25 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -240,10 +240,16 @@ def sql_fetchiter(self):
240240
if not row: break
241241
yield row
242242

243-
def sql_stringquote(self, value):
244-
""" Quote the string so it's safe to put in the 'sql quotes'
243+
def search_stringquote(self, value):
244+
""" Quote a search string to escape magic search characters
245+
'%' and '_', also need to quote '\' (first)
246+
Then put '%' around resulting string for LIKE (or ILIKE) operator
245247
"""
246-
return re.sub("'", "''", str(value))
248+
v = value.replace('\\', '\\\\')
249+
v = v.replace('%', '\\%')
250+
v = v.replace('_', '\\_')
251+
return '%' + v + '%'
252+
247253

248254
def init_dbschema(self):
249255
self.database_schema = {
@@ -1463,6 +1469,10 @@ class Class(hyperdb.Class):
14631469
All methods except __repr__ and getnode must be implemented by a
14641470
concrete backend Class.
14651471
"""
1472+
# For many databases the LIKE operator ignores case.
1473+
# Postgres and Oracle have an ILIKE operator to support this.
1474+
# We define the default here, can be changed in derivative class
1475+
case_insensitive_like = 'LIKE'
14661476

14671477
def schema(self):
14681478
""" A dumpable version of the schema that we can store in the
@@ -2433,23 +2443,24 @@ def _filter_sql (self, search_matches, filterspec, srt=[], grp=[], retr=0):
24332443
if not isinstance(v, type([])):
24342444
v = [v]
24352445

2436-
# Quote the bits in the string that need it and then embed
2437-
# in a "substring" search. Note - need to quote the '%' so
2438-
# they make it through the python layer happily
2439-
v = ['%%'+self.db.sql_stringquote(s)+'%%' for s in v]
2446+
# Quote special search characters '%' and '_' for
2447+
# correct matching with LIKE/ILIKE
2448+
# Note that we now pass the elements of v as query
2449+
# arguments and don't interpolate the quoted string
2450+
# into the sql statement. Should be safer.
2451+
v = [self.db.search_stringquote(s) for s in v]
24402452

24412453
# now add to the where clause
24422454
where.append('('
2443-
+' and '.join(["_%s._%s %s '%s'"%(
2455+
+' and '.join(["_%s._%s %s %s ESCAPE %s"%(
24442456
pln,
24452457
k,
2446-
# For many databases the LIKE operator
2447-
# ignores case. Postgres and Oracle have
2448-
# an ILIKE operator to support this.
2449-
getattr(self,'case_insensitive_like','LIKE'),
2450-
s) for s in v])
2458+
self.case_insensitive_like,
2459+
a,
2460+
a) for s in v])
24512461
+')')
2452-
# note: args are embedded in the query string now
2462+
for vv in v:
2463+
args.extend((vv, '\\'))
24532464
if 'sort' in p.need_for:
24542465
oc = ac = 'lower(_%s._%s)'%(pln, k)
24552466
elif isinstance(propclass, Link):

test/db_test_base.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1252,6 +1252,22 @@ def testFilteringStringCase(self):
12521252
ae(filt(None, {'title': ['One', 'Two']}, ('+','id'), (None,None)),
12531253
[])
12541254

1255+
def testFilteringSpecialChars(self):
1256+
""" Special characters in SQL search are '%' and '_', some used
1257+
to lead to a traceback.
1258+
"""
1259+
ae, filter, filter_iter = self.filteringSetup()
1260+
self.db.issue.set('1', title="With % symbol")
1261+
self.db.issue.set('2', title="With _ symbol")
1262+
self.db.issue.set('3', title="With \\ symbol")
1263+
self.db.issue.set('4', title="With ' symbol")
1264+
d = dict (status = '1')
1265+
for filt in filter, filter_iter:
1266+
ae(filt(None, dict(title='%'), ('+','id'), (None,None)), ['1'])
1267+
ae(filt(None, dict(title='_'), ('+','id'), (None,None)), ['2'])
1268+
ae(filt(None, dict(title='\\'), ('+','id'), (None,None)), ['3'])
1269+
ae(filt(None, dict(title="'"), ('+','id'), (None,None)), ['4'])
1270+
12551271
def testFilteringLink(self):
12561272
ae, filter, filter_iter = self.filteringSetup()
12571273
a = 'assignedto'

0 commit comments

Comments
 (0)