Merge ~cjwatson/launchpad:abolish-quote-like into launchpad:master

Proposed by Colin Watson
Status: Merged
Approved by: Colin Watson
Approved revision: 04472c03f3564b0a31724f006ab9921d084d2cb2
Merge reported by: Otto Co-Pilot
Merged at revision: not available
Proposed branch: ~cjwatson/launchpad:abolish-quote-like
Merge into: launchpad:master
Diff against target: 258 lines (+54/-92)
3 files modified
lib/lp/registry/vocabularies.py (+26/-24)
lib/lp/services/database/sqlbase.py (+3/-43)
lib/lp/translations/model/translationimportqueue.py (+25/-25)
Reviewer Review Type Date Requested Status
Thiago F. Pappacena (community) Approve
Review via email: mp+400197@code.launchpad.net

Commit message

Abolish quote_like

Description of the change

The `startswith`/`endswith`/`contains_string` helper methods on `storm.expr.Comparable` are much easier to use in most cases, and avoid some escaping hazards when substituting the output of `quote_like` into SQL statements.

To post a comment you must log in.
04472c0... by Colin Watson

Abolish quote_like

The `startswith`/`endswith`/`contains_string` helper methods on
`storm.expr.Comparable` are much easier to use in most cases, and avoid
some escaping hazards when substituting the output of `quote_like` into
SQL statements.

Revision history for this message
Thiago F. Pappacena (pappacena) wrote :

LGTM

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
diff --git a/lib/lp/registry/vocabularies.py b/lib/lp/registry/vocabularies.py
index 952899d..a38082b 100644
--- a/lib/lp/registry/vocabularies.py
+++ b/lib/lp/registry/vocabularies.py
@@ -176,13 +176,12 @@ from lp.services.database import bulk
176from lp.services.database.decoratedresultset import DecoratedResultSet176from lp.services.database.decoratedresultset import DecoratedResultSet
177from lp.services.database.interfaces import IStore177from lp.services.database.interfaces import IStore
178from lp.services.database.sqlbase import (178from lp.services.database.sqlbase import (
179 quote,
180 quote_like,
181 SQLBase,179 SQLBase,
182 sqlvalues,180 sqlvalues,
183 )181 )
184from lp.services.database.stormexpr import (182from lp.services.database.stormexpr import (
185 fti_search,183 fti_search,
184 rank_by_fti,
186 RegexpMatch,185 RegexpMatch,
187 )186 )
188from lp.services.helpers import shortlist187from lp.services.helpers import shortlist
@@ -310,23 +309,25 @@ class ProductVocabulary(SQLObjectVocabularyBase):
310 if query is None or an empty string.309 if query is None or an empty string.
311 """310 """
312 if query:311 if query:
313 like_query = query.lower()312 query = six.ensure_text(query)
314 like_query = "'%%%%' || %s || '%%%%'" % quote_like(like_query)
315 fti_query = quote(query)
316 if vocab_filter is None:313 if vocab_filter is None:
317 vocab_filter = []314 vocab_filter = []
318 where_clause = And(315 where_clause = And(
319 SQL(316 self._table.active,
320 "active = 't' AND (name LIKE %s OR fti @@ ftq(%s))" % (317 Or(
321 like_query, fti_query)),318 self._table.name.contains_string(query.lower()),
319 fti_search(self._table, query)),
322 ProductSet.getProductPrivacyFilter(320 ProductSet.getProductPrivacyFilter(
323 getUtility(ILaunchBag).user), *vocab_filter)321 getUtility(ILaunchBag).user), *vocab_filter)
324 order_by = SQL(322 order_by = (
325 '(CASE name WHEN %s THEN 1 '323 Case(
326 ' ELSE ts_rank(fti, ftq(%s)) END) DESC, displayname, name'324 cases=((query, -1),),
327 % (fti_query, fti_query))325 expression=self._table.name,
326 default=rank_by_fti(self._table, query)),
327 self._table.display_name,
328 self._table.name)
328 return IStore(Product).find(self._table, where_clause).order_by(329 return IStore(Product).find(self._table, where_clause).order_by(
329 order_by).config(limit=100)330 *order_by).config(limit=100)
330331
331 return self.emptySelectResults()332 return self.emptySelectResults()
332333
@@ -368,12 +369,13 @@ class ProjectGroupVocabulary(SQLObjectVocabularyBase):
368 if query is None or an empty string.369 if query is None or an empty string.
369 """370 """
370 if query:371 if query:
371 like_query = query.lower()372 query = six.ensure_text(query)
372 like_query = "'%%' || %s || '%%'" % quote_like(like_query)373 return IStore(self._table).find(
373 fti_query = quote(query)374 self._table,
374 sql = "active = 't' AND (name LIKE %s OR fti @@ ftq(%s))" % (375 self._table.active,
375 like_query, fti_query)376 Or(
376 return self._table.select(sql)377 self._table.name.contains_string(query.lower()),
378 fti_search(self._table, query)))
377 return self.emptySelectResults()379 return self.emptySelectResults()
378380
379381
@@ -1526,12 +1528,12 @@ class DistributionVocabulary(NamedSQLObjectVocabulary):
1526 if not query:1528 if not query:
1527 return self.emptySelectResults()1529 return self.emptySelectResults()
15281530
1529 query = query.lower()1531 rows = IStore(self._table).find(
1530 like_query = "'%%' || %s || '%%'" % quote_like(query)1532 self._table,
1531 kw = {}1533 self._table.name.contains_string(six.ensure_text(query).lower()))
1532 if self._orderBy:1534 if self._orderBy:
1533 kw['orderBy'] = self._orderBy1535 rows = rows.order_by(self._orderBy)
1534 return self._table.select("name LIKE %s" % like_query, **kw)1536 return rows
15351537
15361538
1537class DistroSeriesVocabulary(NamedSQLObjectVocabulary):1539class DistroSeriesVocabulary(NamedSQLObjectVocabulary):
diff --git a/lib/lp/services/database/sqlbase.py b/lib/lp/services/database/sqlbase.py
index 65881f7..0476df1 100644
--- a/lib/lp/services/database/sqlbase.py
+++ b/lib/lp/services/database/sqlbase.py
@@ -20,7 +20,6 @@ __all__ = [
20 'ISOLATION_LEVEL_REPEATABLE_READ',20 'ISOLATION_LEVEL_REPEATABLE_READ',
21 'ISOLATION_LEVEL_SERIALIZABLE',21 'ISOLATION_LEVEL_SERIALIZABLE',
22 'quote',22 'quote',
23 'quote_like',
24 'quoteIdentifier',23 'quoteIdentifier',
25 'quote_identifier',24 'quote_identifier',
26 'reset_store',25 'reset_store',
@@ -41,7 +40,6 @@ from psycopg2.extensions import (
41 ISOLATION_LEVEL_SERIALIZABLE,40 ISOLATION_LEVEL_SERIALIZABLE,
42 )41 )
43import pytz42import pytz
44import six
45from sqlobject.sqlbuilder import sqlrepr43from sqlobject.sqlbuilder import sqlrepr
46import storm44import storm
47from storm.databases.postgres import compile as postgres_compile45from storm.databases.postgres import compile as postgres_compile
@@ -71,7 +69,6 @@ from lp.services.database.interfaces import (
71 IStoreSelector,69 IStoreSelector,
72 MAIN_STORE,70 MAIN_STORE,
73 )71 )
74from lp.services.helpers import backslashreplace
75from lp.services.propertycache import clear_property_cache72from lp.services.propertycache import clear_property_cache
7673
77# Default we want for scripts, and the PostgreSQL default. Note psycopg1 will74# Default we want for scripts, and the PostgreSQL default. Note psycopg1 will
@@ -302,7 +299,9 @@ def get_transaction_timestamp(store):
302299
303def quote(x):300def quote(x):
304 r"""Quote a variable ready for inclusion into an SQL statement.301 r"""Quote a variable ready for inclusion into an SQL statement.
305 Note that you should use quote_like to create a LIKE comparison.302
303 >>> import six
304 >>> from lp.services.helpers import backslashreplace
306305
307 Basic SQL quoting works306 Basic SQL quoting works
308307
@@ -366,45 +365,6 @@ def quote(x):
366 return sqlrepr(x, 'postgres')365 return sqlrepr(x, 'postgres')
367366
368367
369def quote_like(x):
370 r"""Quote a variable ready for inclusion in a SQL statement's LIKE clause
371
372 XXX: StuartBishop 2004-11-24:
373 Including the single quotes was a stupid decision.
374
375 To correctly generate a SELECT using a LIKE comparision, we need
376 to make use of the SQL string concatination operator '||' and the
377 quote_like method to ensure that any characters with special meaning
378 to the LIKE operator are correctly escaped.
379
380 >>> "SELECT * FROM mytable WHERE mycol LIKE '%%' || %s || '%%'" \
381 ... % quote_like('%')
382 "SELECT * FROM mytable WHERE mycol LIKE '%' || E'\\\\%' || '%'"
383
384 Note that we need 2 backslashes to quote, as per the docs on
385 the LIKE operator. This is because, unless overridden, the LIKE
386 operator uses the same escape character as the SQL parser.
387
388 >>> quote_like('100%')
389 "E'100\\\\%'"
390 >>> quote_like('foobar_alpha1')
391 "E'foobar\\\\_alpha1'"
392 >>> quote_like('hello')
393 "E'hello'"
394
395 Only strings are supported by this method.
396
397 >>> quote_like(1)
398 Traceback (most recent call last):
399 [...]
400 TypeError: Not a string (<... 'int'>)
401
402 """
403 if not isinstance(x, six.string_types):
404 raise TypeError('Not a string (%s)' % type(x))
405 return quote(x.replace('%', r'\%').replace('_', r'\_'))
406
407
408def sqlvalues(*values, **kwvalues):368def sqlvalues(*values, **kwvalues):
409 """Return a tuple of converted sql values for each value in some_tuple.369 """Return a tuple of converted sql values for each value in some_tuple.
410370
diff --git a/lib/lp/translations/model/translationimportqueue.py b/lib/lp/translations/model/translationimportqueue.py
index b8830ff..8ecd0a1 100644
--- a/lib/lp/translations/model/translationimportqueue.py
+++ b/lib/lp/translations/model/translationimportqueue.py
@@ -28,9 +28,12 @@ import posixpath
28import pytz28import pytz
29import six29import six
30from storm.expr import (30from storm.expr import (
31 Alias,
31 And,32 And,
33 Func,
32 Or,34 Or,
33 Select,35 Select,
36 SQL,
34 )37 )
35from storm.locals import (38from storm.locals import (
36 Bool,39 Bool,
@@ -69,10 +72,7 @@ from lp.services.database.interfaces import (
69 ISlaveStore,72 ISlaveStore,
70 IStore,73 IStore,
71 )74 )
72from lp.services.database.sqlbase import (75from lp.services.database.sqlbase import quote
73 quote,
74 quote_like,
75 )
76from lp.services.database.stormbase import StormBase76from lp.services.database.stormbase import StormBase
77from lp.services.database.stormexpr import IsFalse77from lp.services.database.stormexpr import IsFalse
78from lp.services.librarian.interfaces.client import ILibrarianClient78from lp.services.librarian.interfaces.client import ILibrarianClient
@@ -1420,29 +1420,29 @@ class TranslationImportQueue:
1420 returned by this method.1420 returned by this method.
1421 """1421 """
1422 importer = getUtility(ITranslationImporter)1422 importer = getUtility(ITranslationImporter)
1423 template_patterns = "(%s)" % ' OR '.join([
1424 "path LIKE ('%%' || %s)" % quote_like(suffix)
1425 for suffix in importer.template_suffixes])
14261423
1427 store = self._getSlaveStore()1424 store = self._getSlaveStore()
1428 result = store.execute("""1425 TIQE = TranslationImportQueueEntry
1429 SELECT1426 result = store.find(
1430 distroseries,1427 (TIQE.distroseries_id, TIQE.sourcepackagename_id,
1431 sourcepackagename,1428 TIQE.productseries_id,
1432 productseries,1429 Alias(
1433 regexp_replace(1430 Func("regexp_replace",
1434 regexp_replace(path, '^[^/]*$', ''),1431 Func("regexp_replace", TIQE.path, r"^[^/]*$", ""),
1435 '/[^/]*$',1432 r"/[^/]*$",
1436 '') AS directory1433 ""),
1437 FROM TranslationImportQueueEntry1434 "directory")),
1438 WHERE %(is_template)s1435 Or(*(
1439 GROUP BY distroseries, sourcepackagename, productseries, directory1436 TIQE.path.endswith(suffix)
1440 HAVING bool_and(status = %(blocked)s)1437 for suffix in importer.template_suffixes)))
1441 ORDER BY distroseries, sourcepackagename, productseries, directory1438 result = result.group_by(
1442 """ % {1439 TIQE.distroseries_id, TIQE.sourcepackagename_id,
1443 'blocked': quote(RosettaImportStatus.BLOCKED),1440 TIQE.productseries_id, SQL("directory"))
1444 'is_template': template_patterns,1441 result = result.having(
1445 })1442 Func("bool_and", TIQE.status == RosettaImportStatus.BLOCKED))
1443 result = result.order_by(
1444 TIQE.distroseries_id, TIQE.sourcepackagename_id,
1445 TIQE.productseries_id, SQL("directory"))
14461446
1447 return set(result)1447 return set(result)
14481448

Subscribers

People subscribed via source and target branches

to status/vote changes: