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
1diff --git a/lib/lp/registry/vocabularies.py b/lib/lp/registry/vocabularies.py
2index 952899d..a38082b 100644
3--- a/lib/lp/registry/vocabularies.py
4+++ b/lib/lp/registry/vocabularies.py
5@@ -176,13 +176,12 @@ from lp.services.database import bulk
6 from lp.services.database.decoratedresultset import DecoratedResultSet
7 from lp.services.database.interfaces import IStore
8 from lp.services.database.sqlbase import (
9- quote,
10- quote_like,
11 SQLBase,
12 sqlvalues,
13 )
14 from lp.services.database.stormexpr import (
15 fti_search,
16+ rank_by_fti,
17 RegexpMatch,
18 )
19 from lp.services.helpers import shortlist
20@@ -310,23 +309,25 @@ class ProductVocabulary(SQLObjectVocabularyBase):
21 if query is None or an empty string.
22 """
23 if query:
24- like_query = query.lower()
25- like_query = "'%%%%' || %s || '%%%%'" % quote_like(like_query)
26- fti_query = quote(query)
27+ query = six.ensure_text(query)
28 if vocab_filter is None:
29 vocab_filter = []
30 where_clause = And(
31- SQL(
32- "active = 't' AND (name LIKE %s OR fti @@ ftq(%s))" % (
33- like_query, fti_query)),
34+ self._table.active,
35+ Or(
36+ self._table.name.contains_string(query.lower()),
37+ fti_search(self._table, query)),
38 ProductSet.getProductPrivacyFilter(
39 getUtility(ILaunchBag).user), *vocab_filter)
40- order_by = SQL(
41- '(CASE name WHEN %s THEN 1 '
42- ' ELSE ts_rank(fti, ftq(%s)) END) DESC, displayname, name'
43- % (fti_query, fti_query))
44+ order_by = (
45+ Case(
46+ cases=((query, -1),),
47+ expression=self._table.name,
48+ default=rank_by_fti(self._table, query)),
49+ self._table.display_name,
50+ self._table.name)
51 return IStore(Product).find(self._table, where_clause).order_by(
52- order_by).config(limit=100)
53+ *order_by).config(limit=100)
54
55 return self.emptySelectResults()
56
57@@ -368,12 +369,13 @@ class ProjectGroupVocabulary(SQLObjectVocabularyBase):
58 if query is None or an empty string.
59 """
60 if query:
61- like_query = query.lower()
62- like_query = "'%%' || %s || '%%'" % quote_like(like_query)
63- fti_query = quote(query)
64- sql = "active = 't' AND (name LIKE %s OR fti @@ ftq(%s))" % (
65- like_query, fti_query)
66- return self._table.select(sql)
67+ query = six.ensure_text(query)
68+ return IStore(self._table).find(
69+ self._table,
70+ self._table.active,
71+ Or(
72+ self._table.name.contains_string(query.lower()),
73+ fti_search(self._table, query)))
74 return self.emptySelectResults()
75
76
77@@ -1526,12 +1528,12 @@ class DistributionVocabulary(NamedSQLObjectVocabulary):
78 if not query:
79 return self.emptySelectResults()
80
81- query = query.lower()
82- like_query = "'%%' || %s || '%%'" % quote_like(query)
83- kw = {}
84+ rows = IStore(self._table).find(
85+ self._table,
86+ self._table.name.contains_string(six.ensure_text(query).lower()))
87 if self._orderBy:
88- kw['orderBy'] = self._orderBy
89- return self._table.select("name LIKE %s" % like_query, **kw)
90+ rows = rows.order_by(self._orderBy)
91+ return rows
92
93
94 class DistroSeriesVocabulary(NamedSQLObjectVocabulary):
95diff --git a/lib/lp/services/database/sqlbase.py b/lib/lp/services/database/sqlbase.py
96index 65881f7..0476df1 100644
97--- a/lib/lp/services/database/sqlbase.py
98+++ b/lib/lp/services/database/sqlbase.py
99@@ -20,7 +20,6 @@ __all__ = [
100 'ISOLATION_LEVEL_REPEATABLE_READ',
101 'ISOLATION_LEVEL_SERIALIZABLE',
102 'quote',
103- 'quote_like',
104 'quoteIdentifier',
105 'quote_identifier',
106 'reset_store',
107@@ -41,7 +40,6 @@ from psycopg2.extensions import (
108 ISOLATION_LEVEL_SERIALIZABLE,
109 )
110 import pytz
111-import six
112 from sqlobject.sqlbuilder import sqlrepr
113 import storm
114 from storm.databases.postgres import compile as postgres_compile
115@@ -71,7 +69,6 @@ from lp.services.database.interfaces import (
116 IStoreSelector,
117 MAIN_STORE,
118 )
119-from lp.services.helpers import backslashreplace
120 from lp.services.propertycache import clear_property_cache
121
122 # Default we want for scripts, and the PostgreSQL default. Note psycopg1 will
123@@ -302,7 +299,9 @@ def get_transaction_timestamp(store):
124
125 def quote(x):
126 r"""Quote a variable ready for inclusion into an SQL statement.
127- Note that you should use quote_like to create a LIKE comparison.
128+
129+ >>> import six
130+ >>> from lp.services.helpers import backslashreplace
131
132 Basic SQL quoting works
133
134@@ -366,45 +365,6 @@ def quote(x):
135 return sqlrepr(x, 'postgres')
136
137
138-def quote_like(x):
139- r"""Quote a variable ready for inclusion in a SQL statement's LIKE clause
140-
141- XXX: StuartBishop 2004-11-24:
142- Including the single quotes was a stupid decision.
143-
144- To correctly generate a SELECT using a LIKE comparision, we need
145- to make use of the SQL string concatination operator '||' and the
146- quote_like method to ensure that any characters with special meaning
147- to the LIKE operator are correctly escaped.
148-
149- >>> "SELECT * FROM mytable WHERE mycol LIKE '%%' || %s || '%%'" \
150- ... % quote_like('%')
151- "SELECT * FROM mytable WHERE mycol LIKE '%' || E'\\\\%' || '%'"
152-
153- Note that we need 2 backslashes to quote, as per the docs on
154- the LIKE operator. This is because, unless overridden, the LIKE
155- operator uses the same escape character as the SQL parser.
156-
157- >>> quote_like('100%')
158- "E'100\\\\%'"
159- >>> quote_like('foobar_alpha1')
160- "E'foobar\\\\_alpha1'"
161- >>> quote_like('hello')
162- "E'hello'"
163-
164- Only strings are supported by this method.
165-
166- >>> quote_like(1)
167- Traceback (most recent call last):
168- [...]
169- TypeError: Not a string (<... 'int'>)
170-
171- """
172- if not isinstance(x, six.string_types):
173- raise TypeError('Not a string (%s)' % type(x))
174- return quote(x.replace('%', r'\%').replace('_', r'\_'))
175-
176-
177 def sqlvalues(*values, **kwvalues):
178 """Return a tuple of converted sql values for each value in some_tuple.
179
180diff --git a/lib/lp/translations/model/translationimportqueue.py b/lib/lp/translations/model/translationimportqueue.py
181index b8830ff..8ecd0a1 100644
182--- a/lib/lp/translations/model/translationimportqueue.py
183+++ b/lib/lp/translations/model/translationimportqueue.py
184@@ -28,9 +28,12 @@ import posixpath
185 import pytz
186 import six
187 from storm.expr import (
188+ Alias,
189 And,
190+ Func,
191 Or,
192 Select,
193+ SQL,
194 )
195 from storm.locals import (
196 Bool,
197@@ -69,10 +72,7 @@ from lp.services.database.interfaces import (
198 ISlaveStore,
199 IStore,
200 )
201-from lp.services.database.sqlbase import (
202- quote,
203- quote_like,
204- )
205+from lp.services.database.sqlbase import quote
206 from lp.services.database.stormbase import StormBase
207 from lp.services.database.stormexpr import IsFalse
208 from lp.services.librarian.interfaces.client import ILibrarianClient
209@@ -1420,29 +1420,29 @@ class TranslationImportQueue:
210 returned by this method.
211 """
212 importer = getUtility(ITranslationImporter)
213- template_patterns = "(%s)" % ' OR '.join([
214- "path LIKE ('%%' || %s)" % quote_like(suffix)
215- for suffix in importer.template_suffixes])
216
217 store = self._getSlaveStore()
218- result = store.execute("""
219- SELECT
220- distroseries,
221- sourcepackagename,
222- productseries,
223- regexp_replace(
224- regexp_replace(path, '^[^/]*$', ''),
225- '/[^/]*$',
226- '') AS directory
227- FROM TranslationImportQueueEntry
228- WHERE %(is_template)s
229- GROUP BY distroseries, sourcepackagename, productseries, directory
230- HAVING bool_and(status = %(blocked)s)
231- ORDER BY distroseries, sourcepackagename, productseries, directory
232- """ % {
233- 'blocked': quote(RosettaImportStatus.BLOCKED),
234- 'is_template': template_patterns,
235- })
236+ TIQE = TranslationImportQueueEntry
237+ result = store.find(
238+ (TIQE.distroseries_id, TIQE.sourcepackagename_id,
239+ TIQE.productseries_id,
240+ Alias(
241+ Func("regexp_replace",
242+ Func("regexp_replace", TIQE.path, r"^[^/]*$", ""),
243+ r"/[^/]*$",
244+ ""),
245+ "directory")),
246+ Or(*(
247+ TIQE.path.endswith(suffix)
248+ for suffix in importer.template_suffixes)))
249+ result = result.group_by(
250+ TIQE.distroseries_id, TIQE.sourcepackagename_id,
251+ TIQE.productseries_id, SQL("directory"))
252+ result = result.having(
253+ Func("bool_and", TIQE.status == RosettaImportStatus.BLOCKED))
254+ result = result.order_by(
255+ TIQE.distroseries_id, TIQE.sourcepackagename_id,
256+ TIQE.productseries_id, SQL("directory"))
257
258 return set(result)
259

Subscribers

People subscribed via source and target branches

to status/vote changes: