Merge ~cjwatson/launchpad:sqlvalues-text into launchpad:master

Proposed by Colin Watson
Status: Merged
Approved by: Colin Watson
Approved revision: e03e5221a279de4c68a3cdbada05d37cb0d2e8b6
Merge reported by: Otto Co-Pilot
Merged at revision: not available
Proposed branch: ~cjwatson/launchpad:sqlvalues-text
Merge into: launchpad:master
Prerequisite: ~cjwatson/launchpad:abolish-quote-like
Diff against target: 723 lines (+180/-157)
17 files modified
lib/lp/bugs/model/structuralsubscription.py (+5/-5)
lib/lp/oci/tests/test_ocirecipe.py (+7/-2)
lib/lp/registry/model/distribution.py (+2/-4)
lib/lp/registry/model/distroseries.py (+1/-1)
lib/lp/registry/model/person.py (+2/-2)
lib/lp/registry/model/pillar.py (+42/-31)
lib/lp/registry/model/product.py (+9/-8)
lib/lp/registry/model/productseries.py (+4/-6)
lib/lp/registry/model/projectgroup.py (+17/-19)
lib/lp/registry/tests/test_pillar.py (+12/-0)
lib/lp/registry/vocabularies.py (+6/-7)
lib/lp/services/database/sqlbase.py (+7/-2)
lib/lp/services/database/stormexpr.py (+4/-3)
lib/lp/services/identity/model/emailaddress.py (+10/-4)
lib/lp/services/statistics/model/statistics.py (+0/-1)
lib/lp/soyuz/model/archive.py (+16/-17)
lib/lp/soyuz/scripts/gina/handlers.py (+36/-45)
Reviewer Review Type Date Requested Status
Thiago F. Pappacena (community) Approve
Review via email: mp+400241@code.launchpad.net

Commit message

Avoid quote() and sqlvalues() on arbitrary text

Description of the change

The `quote` and `sqlvalues` functions purported to do safe SQL quoting, but in fact they don't get it quite right: when a constructed query is executed, `psycopg2` expects to be able to do %-substitution of parameters, and so '%' characters in parameters must also be escaped. This can cause problems when substituting arbitrary user-supplied text into queries using these functions; I don't believe SQL injection is possible, but it can cause `IndexError` exceptions due to the parameter count being wrong. `ProjectGroupSet.search` handled its own escaping, but this wasn't done systematically.

A better approach is to avoid these functions where arbitrary text is involved, and instead use the style of passing parameters separately (either directly or via the Storm query compiler) so that `psycopg2` can deal with all the substitutions.

Ultimately we should remove all these old quoting functions entirely, but that's quite a bit more work.

To post a comment you must log in.
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/bugs/model/structuralsubscription.py b/lib/lp/bugs/model/structuralsubscription.py
2index 59955d3..9d69921 100644
3--- a/lib/lp/bugs/model/structuralsubscription.py
4+++ b/lib/lp/bugs/model/structuralsubscription.py
5@@ -19,6 +19,7 @@ import six
6 from storm.base import Storm
7 from storm.expr import (
8 And,
9+ Cast,
10 Count,
11 In,
12 Join,
13@@ -82,8 +83,8 @@ from lp.registry.interfaces.sourcepackage import ISourcePackage
14 from lp.registry.model.teammembership import TeamParticipation
15 from lp.services.database.constants import UTC_NOW
16 from lp.services.database.interfaces import IStore
17-from lp.services.database.sqlbase import quote
18 from lp.services.database.stormexpr import (
19+ Array,
20 ArrayAgg,
21 ArrayContains,
22 ArrayIntersects,
23@@ -927,8 +928,7 @@ def _calculate_tag_query(conditions, tags):
24 # space as, effectively, NULL. This is safe because a
25 # single space is not an acceptable tag. Again, the
26 # clearest alternative is defining a custom Postgres aggregator.
27- tags_array = "ARRAY[%s,' ']::TEXT[]" % ",".join(
28- quote(tag) for tag in tags)
29+ tags_array = Cast(Array(tuple(tags) + (u" ",)), "text[]")
30 # Now let's build the select itself.
31 second_select = Select(
32 BugSubscriptionFilter.id,
33@@ -944,7 +944,7 @@ def _calculate_tag_query(conditions, tags):
34 # The list of tags should be a superset of the filter tags to
35 # be included.
36 ArrayContains(
37- SQL(tags_array),
38+ tags_array,
39 # This next line gives us an array of the tags that the
40 # filter wants to include. Notice that it includes the
41 # empty string when the condition does not match, per the
42@@ -957,7 +957,7 @@ def _calculate_tag_query(conditions, tags):
43 # tags that the filter wants to exclude.
44 Not(
45 ArrayIntersects(
46- SQL(tags_array),
47+ tags_array,
48 # This next line gives us an array of the tags
49 # that the filter wants to exclude. We do not bother
50 # with the empty string, and therefore allow NULLs
51diff --git a/lib/lp/oci/tests/test_ocirecipe.py b/lib/lp/oci/tests/test_ocirecipe.py
52index 2879b37..34e19b4 100644
53--- a/lib/lp/oci/tests/test_ocirecipe.py
54+++ b/lib/lp/oci/tests/test_ocirecipe.py
55@@ -600,10 +600,14 @@ class TestOCIRecipe(OCIConfigHelperMixin, TestCaseWithFactory):
56 for recipe in oci_proj1_recipes + oci_proj2_recipes:
57 self.assertFalse(recipe.official)
58
59+ # Cache permissions.
60+ oci_project1.setOfficialRecipeStatus
61+
62 # Set official for project1 and make sure nothing else got changed.
63 with StormStatementRecorder() as recorder:
64 oci_project1.setOfficialRecipeStatus(oci_proj1_recipes[0], True)
65- self.assertEqual(1, recorder.count)
66+ Store.of(oci_project1).flush()
67+ self.assertEqual(1, recorder.count)
68
69 self.assertTrue(oci_project2.getOfficialRecipes().is_empty())
70 self.assertEqual(
71@@ -615,7 +619,8 @@ class TestOCIRecipe(OCIConfigHelperMixin, TestCaseWithFactory):
72 # Set back no recipe as official.
73 with StormStatementRecorder() as recorder:
74 oci_project1.setOfficialRecipeStatus(oci_proj1_recipes[0], False)
75- self.assertEqual(0, recorder.count)
76+ Store.of(oci_project1).flush()
77+ self.assertEqual(1, recorder.count)
78
79 for recipe in oci_proj1_recipes + oci_proj2_recipes:
80 self.assertFalse(recipe.official)
81diff --git a/lib/lp/registry/model/distribution.py b/lib/lp/registry/model/distribution.py
82index 1000bea..26271b5 100644
83--- a/lib/lp/registry/model/distribution.py
84+++ b/lib/lp/registry/model/distribution.py
85@@ -874,10 +874,8 @@ class Distribution(SQLBase, BugTargetBase, MakesAnnouncements,
86
87 def getMilestone(self, name):
88 """See `IDistribution`."""
89- return Milestone.selectOne("""
90- distribution = %s AND
91- name = %s
92- """ % sqlvalues(self.id, name))
93+ return Store.of(self).find(
94+ Milestone, distribution=self, name=name).one()
95
96 def getOCIProject(self, name):
97 oci_project = getUtility(IOCIProjectSet).getByPillarAndName(
98diff --git a/lib/lp/registry/model/distroseries.py b/lib/lp/registry/model/distroseries.py
99index 3242c33..0ceff83 100644
100--- a/lib/lp/registry/model/distroseries.py
101+++ b/lib/lp/registry/model/distroseries.py
102@@ -1180,7 +1180,7 @@ class DistroSeries(SQLBase, BugTargetBase, HasSpecificationsMixin,
103 find_spec = (
104 DistroSeriesPackageCache,
105 BinaryPackageName,
106- SQL('ts_rank(fti, ftq(%s)) AS rank' % sqlvalues(text)))
107+ SQL('ts_rank(fti, ftq(?)) AS rank', params=(text,)))
108 origin = [
109 DistroSeriesPackageCache,
110 Join(
111diff --git a/lib/lp/registry/model/person.py b/lib/lp/registry/model/person.py
112index 69ea961..6785698 100644
113--- a/lib/lp/registry/model/person.py
114+++ b/lib/lp/registry/model/person.py
115@@ -3243,8 +3243,8 @@ class PersonSet:
116 user_id = user.id
117 cur = cursor()
118 cur.execute(
119- "SELECT is_blacklisted_name(%(name)s, %(user_id)s)" % sqlvalues(
120- name=name, user_id=user_id))
121+ "SELECT is_blacklisted_name(%(name)s, %(user_id)s)",
122+ {"name": name, "user_id": user_id})
123 return bool(cur.fetchone()[0])
124
125 def getTopContributors(self, limit=50):
126diff --git a/lib/lp/registry/model/pillar.py b/lib/lp/registry/model/pillar.py
127index e138952..c591925 100644
128--- a/lib/lp/registry/model/pillar.py
129+++ b/lib/lp/registry/model/pillar.py
130@@ -17,10 +17,14 @@ from sqlobject import (
131 ForeignKey,
132 StringCol,
133 )
134+from storm.databases.postgres import Case
135 from storm.expr import (
136 And,
137+ Coalesce,
138+ Desc,
139 LeftJoin,
140- SQL,
141+ Lower,
142+ Or,
143 )
144 from storm.info import ClassAlias
145 from storm.store import Store
146@@ -51,9 +55,10 @@ from lp.services.config import config
147 from lp.services.database.bulk import load_related
148 from lp.services.database.decoratedresultset import DecoratedResultSet
149 from lp.services.database.interfaces import IStore
150-from lp.services.database.sqlbase import (
151- SQLBase,
152- sqlvalues,
153+from lp.services.database.sqlbase import SQLBase
154+from lp.services.database.stormexpr import (
155+ fti_search,
156+ rank_by_fti,
157 )
158 from lp.services.librarian.model import LibraryFileAlias
159
160@@ -172,20 +177,16 @@ class PillarNameSet:
161 LeftJoin(
162 Distribution, PillarName.distribution == Distribution.id),
163 ]
164- conditions = SQL('''
165- PillarName.active = TRUE
166- AND (PillarName.name = lower(%(text)s) OR
167-
168- Product.fti @@ ftq(%(text)s) OR
169- lower(Product.title) = lower(%(text)s) OR
170-
171- Project.fti @@ ftq(%(text)s) OR
172- lower(Project.title) = lower(%(text)s) OR
173-
174- Distribution.fti @@ ftq(%(text)s) OR
175- lower(Distribution.title) = lower(%(text)s)
176- )
177- ''' % sqlvalues(text=text))
178+ conditions = And(
179+ PillarName.active,
180+ Or(
181+ PillarName.name == Lower(text),
182+ fti_search(Product, text),
183+ Lower(Product._title) == Lower(text),
184+ fti_search(ProjectGroup, text),
185+ Lower(ProjectGroup._title) == Lower(text),
186+ fti_search(Distribution, text),
187+ Lower(Distribution._title) == Lower(text)))
188 columns = [
189 PillarName, OtherPillarName, Product, ProjectGroup, Distribution]
190 return IStore(PillarName).using(*origin).find(
191@@ -193,14 +194,21 @@ class PillarNameSet:
192 And(conditions, ProductSet.getProductPrivacyFilter(user)))
193
194 def count_search_matches(self, user, text):
195+ text = six.ensure_text(text)
196 result = self.build_search_query(user, text)
197 return result.count()
198
199 def search(self, user, text, limit):
200 """See `IPillarSet`."""
201- # Avoid circular import.
202- from lp.registry.model.product import get_precached_products
203+ # Avoid circular imports.
204+ from lp.registry.model.distribution import Distribution
205+ from lp.registry.model.product import (
206+ get_precached_products,
207+ Product,
208+ )
209+ from lp.registry.model.projectgroup import ProjectGroup
210
211+ text = six.ensure_text(text)
212 if limit is None:
213 limit = config.launchpad.default_batch_size
214
215@@ -216,17 +224,20 @@ class PillarNameSet:
216 # of either the Product, Project, or Distribution tables,
217 # so the coalesce() is necessary to find the ts_rank() which
218 # is not null.
219- result.order_by(SQL('''
220- (CASE WHEN PillarName.name = lower(%(text)s)
221- OR lower(Product.title) = lower(%(text)s)
222- OR lower(Project.title) = lower(%(text)s)
223- OR lower(Distribution.title) = lower(%(text)s)
224- THEN 9999999
225- ELSE coalesce(ts_rank(Product.fti, ftq(%(text)s)),
226- ts_rank(Project.fti, ftq(%(text)s)),
227- ts_rank(Distribution.fti, ftq(%(text)s)))
228- END) DESC, PillarName.name
229- ''' % sqlvalues(text=text)))
230+ result.order_by(
231+ Desc(Case(
232+ cases=(
233+ (Or(
234+ PillarName.name == Lower(text),
235+ Lower(Product._title) == Lower(text),
236+ Lower(ProjectGroup._title) == Lower(text),
237+ Lower(Distribution._title) == Lower(text)),
238+ 9999999),),
239+ default=Coalesce(
240+ rank_by_fti(Product, text, desc=False),
241+ rank_by_fti(ProjectGroup, text, desc=False),
242+ rank_by_fti(Distribution, text, desc=False)))),
243+ PillarName.name)
244 # People shouldn't be calling this method with too big limits
245 longest_expected = 2 * config.launchpad.default_batch_size
246 if limit > longest_expected:
247diff --git a/lib/lp/registry/model/product.py b/lib/lp/registry/model/product.py
248index 5ab5d1b..441e95f 100644
249--- a/lib/lp/registry/model/product.py
250+++ b/lib/lp/registry/model/product.py
251@@ -33,8 +33,10 @@ from storm.expr import (
252 Coalesce,
253 Desc,
254 Exists,
255+ Func,
256 Join,
257 LeftJoin,
258+ Lower,
259 Not,
260 Or,
261 Select,
262@@ -1171,8 +1173,7 @@ class Product(SQLBase, BugTargetBase, MakesAnnouncements,
263
264 def getMilestone(self, name):
265 """See `IProduct`."""
266- return Milestone.selectOne("""
267- product = %s AND name = %s""" % sqlvalues(self.id, name))
268+ return IStore(Milestone).find(Milestone, product=self, name=name).one()
269
270 def getBugSummaryContextWhereClause(self):
271 """See BugTargetBase."""
272@@ -1859,12 +1860,12 @@ class ProductSet:
273 conditions.append(Product.active == active)
274
275 if search_text is not None and search_text.strip() != '':
276- conditions.append(SQL('''
277- Product.fti @@ ftq(%(text)s) OR
278- Product.name = %(text)s OR
279- strpos(lower(Product.license_info), %(text)s) > 0 OR
280- strpos(lower(Product.reviewer_whiteboard), %(text)s) > 0
281- ''' % sqlvalues(text=search_text.lower())))
282+ text = search_text.lower()
283+ conditions.append(Or(
284+ fti_search(Product, text),
285+ Product.name == text,
286+ Func("strpos", Lower(Product.license_info), text) > 0,
287+ Func("strpos", Lower(Product.reviewer_whiteboard), text) > 0))
288
289 def dateToDatetime(date):
290 """Convert a datetime.date to a datetime.datetime
291diff --git a/lib/lp/registry/model/productseries.py b/lib/lp/registry/model/productseries.py
292index 3459214..82904cf 100644
293--- a/lib/lp/registry/model/productseries.py
294+++ b/lib/lp/registry/model/productseries.py
295@@ -72,10 +72,8 @@ from lp.services.database.constants import UTC_NOW
296 from lp.services.database.datetimecol import UtcDateTimeCol
297 from lp.services.database.decoratedresultset import DecoratedResultSet
298 from lp.services.database.enumcol import EnumCol
299-from lp.services.database.sqlbase import (
300- SQLBase,
301- sqlvalues,
302- )
303+from lp.services.database.interfaces import IStore
304+from lp.services.database.sqlbase import SQLBase
305 from lp.services.propertycache import cachedproperty
306 from lp.services.webapp.publisher import canonical_url
307 from lp.services.webapp.sorting import sorted_dotted_numbers
308@@ -274,8 +272,8 @@ class ProductSeries(SQLBase, BugTargetBase, HasMilestonesMixin,
309
310 def getPOTemplate(self, name):
311 """See IProductSeries."""
312- return POTemplate.selectOne(
313- "productseries = %s AND name = %s" % sqlvalues(self.id, name))
314+ return IStore(POTemplate).find(
315+ POTemplate, productseries=self, name=name).one()
316
317 @property
318 def title(self):
319diff --git a/lib/lp/registry/model/projectgroup.py b/lib/lp/registry/model/projectgroup.py
320index cfeda26..b56499a 100644
321--- a/lib/lp/registry/model/projectgroup.py
322+++ b/lib/lp/registry/model/projectgroup.py
323@@ -10,6 +10,7 @@ __all__ = [
324 'ProjectGroupSet',
325 ]
326
327+import six
328 from sqlobject import (
329 AND,
330 BoolCol,
331@@ -95,10 +96,12 @@ from lp.registry.model.productseries import ProductSeries
332 from lp.services.database.constants import UTC_NOW
333 from lp.services.database.datetimecol import UtcDateTimeCol
334 from lp.services.database.enumcol import EnumCol
335+from lp.services.database.interfaces import IStore
336 from lp.services.database.sqlbase import (
337 SQLBase,
338 sqlvalues,
339 )
340+from lp.services.database.stormexpr import fti_search
341 from lp.services.helpers import shortlist
342 from lp.services.propertycache import cachedproperty
343 from lp.services.webapp.authorization import check_permission
344@@ -592,32 +595,27 @@ class ProjectGroupSet:
345 should be limited to projects that are active in those Launchpad
346 applications.
347 """
348- if text:
349- text = text.replace("%", "%%")
350- clauseTables = set()
351- clauseTables.add('Project')
352- queries = []
353+ joining_product = False
354+ clauses = []
355
356 if text:
357+ text = six.ensure_text(text)
358 if search_products:
359- clauseTables.add('Product')
360- product_query = "Product.fti @@ ftq(%s)" % sqlvalues(text)
361- queries.append(product_query)
362+ joining_product = True
363+ clauses.extend([
364+ Product.projectgroup == ProjectGroup.id,
365+ fti_search(Product, text),
366+ ])
367 else:
368- projectgroup_query = "Project.fti @@ ftq(%s)" % sqlvalues(text)
369- queries.append(projectgroup_query)
370-
371- if 'Product' in clauseTables:
372- queries.append('Product.project=Project.id')
373+ clauses.append(fti_search(ProjectGroup, text))
374
375 if not show_inactive:
376- queries.append('Project.active IS TRUE')
377- if 'Product' in clauseTables:
378- queries.append('Product.active IS TRUE')
379+ clauses.append(ProjectGroup.active)
380+ if joining_product:
381+ clauses.append(Product.active)
382
383- query = " AND ".join(queries)
384- return ProjectGroup.select(
385- query, distinct=True, clauseTables=clauseTables)
386+ return IStore(ProjectGroup).find(
387+ ProjectGroup, *clauses).config(distinct=True)
388
389
390 @implementer(IProjectGroupSeries)
391diff --git a/lib/lp/registry/tests/test_pillar.py b/lib/lp/registry/tests/test_pillar.py
392index 10a259e..0ff1db2 100644
393--- a/lib/lp/registry/tests/test_pillar.py
394+++ b/lib/lp/registry/tests/test_pillar.py
395@@ -42,6 +42,18 @@ class TestPillarNameSet(TestCaseWithFactory):
396 getUtility(IPersonSet).getByName('mark'), 'lz', limit=5)]
397 self.assertEqual(result_names, [u'launchzap', u'lz-bar', u'lz-foo'])
398
399+ def test_search_percent(self):
400+ """Searches involving '%' characters work correctly."""
401+ login('mark@example.com')
402+ self.factory.makeProduct(name='percent', title='contains % character')
403+ self.factory.makeProduct()
404+ pillar_set = getUtility(IPillarNameSet)
405+ mark = getUtility(IPersonSet).getByName('mark')
406+ result_names = [
407+ pillar.name
408+ for pillar in pillar_set.search(mark, '% character', limit=5)]
409+ self.assertEqual([u'percent'], result_names)
410+
411
412 class TestPillarPerson(TestCaseWithFactory):
413
414diff --git a/lib/lp/registry/vocabularies.py b/lib/lp/registry/vocabularies.py
415index a38082b..02e3158 100644
416--- a/lib/lp/registry/vocabularies.py
417+++ b/lib/lp/registry/vocabularies.py
418@@ -1517,7 +1517,7 @@ class DistributionVocabulary(NamedSQLObjectVocabulary):
419
420 def getTermByToken(self, token):
421 """See `IVocabularyTokenized`."""
422- obj = Distribution.selectOne("name=%s" % sqlvalues(token))
423+ obj = IStore(Distribution).find(Distribution, name=token).one()
424 if obj is None:
425 raise LookupError(token)
426 else:
427@@ -1565,12 +1565,11 @@ class DistroSeriesVocabulary(NamedSQLObjectVocabulary):
428 except ValueError:
429 raise LookupError(token)
430
431- obj = DistroSeries.selectOne('''
432- Distribution.id = DistroSeries.distribution AND
433- Distribution.name = %s AND
434- DistroSeries.name = %s
435- ''' % sqlvalues(distroname, distroseriesname),
436- clauseTables=['Distribution'])
437+ obj = IStore(DistroSeries).find(
438+ DistroSeries,
439+ DistroSeries.distribution == Distribution.id,
440+ Distribution.name == distroname,
441+ DistroSeries.name == distroseriesname).one()
442 if obj is None:
443 raise LookupError(token)
444 else:
445diff --git a/lib/lp/services/database/sqlbase.py b/lib/lp/services/database/sqlbase.py
446index 0476df1..9d53175 100644
447--- a/lib/lp/services/database/sqlbase.py
448+++ b/lib/lp/services/database/sqlbase.py
449@@ -368,8 +368,8 @@ def quote(x):
450 def sqlvalues(*values, **kwvalues):
451 """Return a tuple of converted sql values for each value in some_tuple.
452
453- This safely quotes strings, or gives representations of dbschema items,
454- for example.
455+ This safely quotes strings (except for '%'!), or gives representations
456+ of dbschema items, for example.
457
458 Use it when constructing a string for use in a SELECT. Always use
459 %s as the replacement marker.
460@@ -377,6 +377,11 @@ def sqlvalues(*values, **kwvalues):
461 ('SELECT foo from Foo where bar = %s and baz = %s'
462 % sqlvalues(BugTaskSeverity.CRITICAL, 'foo'))
463
464+ This is DEPRECATED in favour of passing parameters to SQL statements
465+ using the second parameter to `cursor.execute` (normally via the Storm
466+ query compiler), because it does not deal with escaping '%' characters
467+ in strings.
468+
469 >>> sqlvalues()
470 Traceback (most recent call last):
471 ...
472diff --git a/lib/lp/services/database/stormexpr.py b/lib/lp/services/database/stormexpr.py
473index 37772f1..648bec2 100644
474--- a/lib/lp/services/database/stormexpr.py
475+++ b/lib/lp/services/database/stormexpr.py
476@@ -329,8 +329,9 @@ def fti_search(table, text, ftq=True):
477 tables=(table,))
478
479
480-def rank_by_fti(table, text, ftq=True):
481+def rank_by_fti(table, text, ftq=True, desc=True):
482 table, query_fragment = determine_table_and_fragment(table, ftq)
483 return SQL(
484- '-ts_rank(%s.fti, %s)' % (table.name, query_fragment), params=(text,),
485- tables=(table,))
486+ '%sts_rank(%s.fti, %s)' % (
487+ '-' if desc else '', table.name, query_fragment),
488+ params=(text,), tables=(table,))
489diff --git a/lib/lp/services/identity/model/emailaddress.py b/lib/lp/services/identity/model/emailaddress.py
490index a866a0a..c9c9a4e 100644
491--- a/lib/lp/services/identity/model/emailaddress.py
492+++ b/lib/lp/services/identity/model/emailaddress.py
493@@ -13,17 +13,21 @@ __all__ = [
494 import hashlib
495 import operator
496
497+import six
498 from sqlobject import (
499 ForeignKey,
500 StringCol,
501 )
502+from storm.expr import Lower
503 from zope.interface import implementer
504
505 from lp.app.validators.email import valid_email
506 from lp.services.database.enumcol import EnumCol
507-from lp.services.database.interfaces import IMasterStore
508+from lp.services.database.interfaces import (
509+ IMasterStore,
510+ IStore,
511+ )
512 from lp.services.database.sqlbase import (
513- quote,
514 SQLBase,
515 sqlvalues,
516 )
517@@ -111,8 +115,10 @@ class EmailAddressSet:
518
519 def getByEmail(self, email):
520 """See `IEmailAddressSet`."""
521- return EmailAddress.selectOne(
522- "lower(email) = %s" % quote(email.strip().lower()))
523+ return IStore(EmailAddress).find(
524+ EmailAddress,
525+ Lower(EmailAddress.email) ==
526+ six.ensure_text(email).strip().lower()).one()
527
528 def new(self, email, person=None, status=EmailAddressStatus.NEW):
529 """See IEmailAddressSet."""
530diff --git a/lib/lp/services/statistics/model/statistics.py b/lib/lp/services/statistics/model/statistics.py
531index 75487be..e7bce2d 100644
532--- a/lib/lp/services/statistics/model/statistics.py
533+++ b/lib/lp/services/statistics/model/statistics.py
534@@ -33,7 +33,6 @@ from lp.services.database.interfaces import IStore
535 from lp.services.database.sqlbase import (
536 cursor,
537 SQLBase,
538- sqlvalues,
539 )
540 from lp.services.statistics.interfaces.statistic import (
541 ILaunchpadStatistic,
542diff --git a/lib/lp/soyuz/model/archive.py b/lib/lp/soyuz/model/archive.py
543index df275c1..9696b8f 100644
544--- a/lib/lp/soyuz/model/archive.py
545+++ b/lib/lp/soyuz/model/archive.py
546@@ -2593,16 +2593,16 @@ class ArchiveSet:
547 def getPPAByDistributionAndOwnerName(self, distribution, person_name,
548 ppa_name):
549 """See `IArchiveSet`"""
550- query = """
551- Archive.purpose = %s AND
552- Archive.distribution = %s AND
553- Person.id = Archive.owner AND
554- Archive.name = %s AND
555- Person.name = %s
556- """ % sqlvalues(
557- ArchivePurpose.PPA, distribution, ppa_name, person_name)
558+ # Circular import.
559+ from lp.registry.model.person import Person
560
561- return Archive.selectOne(query, clauseTables=['Person'])
562+ return IStore(Archive).find(
563+ Archive,
564+ Archive.purpose == ArchivePurpose.PPA,
565+ Archive.distribution == distribution,
566+ Archive.owner == Person.id,
567+ Archive.name == ppa_name,
568+ Person.name == person_name).one()
569
570 def _getDefaultArchiveNameByPurpose(self, purpose):
571 """Return the default for a archive in a given purpose.
572@@ -2641,11 +2641,11 @@ class ArchiveSet:
573
574 def getByDistroAndName(self, distribution, name):
575 """See `IArchiveSet`."""
576- return Archive.selectOne("""
577- Archive.distribution = %s AND
578- Archive.name = %s AND
579- Archive.purpose != %s
580- """ % sqlvalues(distribution, name, ArchivePurpose.PPA))
581+ return IStore(Archive).find(
582+ Archive,
583+ Archive.distribution == distribution,
584+ Archive.name == name,
585+ Archive.purpose != ArchivePurpose.PPA).one()
586
587 def _getDefaultDisplayname(self, name, owner, distribution, purpose):
588 """See `IArchive`."""
589@@ -2697,9 +2697,8 @@ class ArchiveSet:
590 # For non-PPA archives we enforce unique names within the context of a
591 # distribution.
592 if purpose != ArchivePurpose.PPA:
593- archive = Archive.selectOne(
594- "Archive.distribution = %s AND Archive.name = %s" %
595- sqlvalues(distribution, name))
596+ archive = IStore(Archive).find(
597+ Archive, distribution=distribution, name=name).one()
598 if archive is not None:
599 raise AssertionError(
600 "archive '%s' exists already in '%s'." %
601diff --git a/lib/lp/soyuz/scripts/gina/handlers.py b/lib/lp/soyuz/scripts/gina/handlers.py
602index 94aac78..432efcf 100644
603--- a/lib/lp/soyuz/scripts/gina/handlers.py
604+++ b/lib/lp/soyuz/scripts/gina/handlers.py
605@@ -29,6 +29,11 @@ from sqlobject import (
606 SQLObjectMoreThanOneResultError,
607 SQLObjectNotFound,
608 )
609+from storm.exceptions import NotOneError
610+from storm.expr import (
611+ Cast,
612+ Desc,
613+ )
614 from zope.component import getUtility
615
616 from lp.archivepublisher.diskpool import poolify
617@@ -46,10 +51,7 @@ from lp.registry.model.distroseries import DistroSeries
618 from lp.registry.model.sourcepackagename import SourcePackageName
619 from lp.services.database.constants import UTC_NOW
620 from lp.services.database.interfaces import IStore
621-from lp.services.database.sqlbase import (
622- quote,
623- sqlvalues,
624- )
625+from lp.services.database.sqlbase import quote
626 from lp.services.librarian.interfaces import ILibraryFileAliasSet
627 from lp.services.scripts import log
628 from lp.soyuz.enums import (
629@@ -561,26 +563,19 @@ class SourcePackageHandler:
630
631 # Check here to see if this release has ever been published in
632 # the distribution, no matter what status.
633- query = """
634- SourcePackageRelease.sourcepackagename = %s AND
635- SourcePackageRelease.version::text = %s AND
636- SourcePackagePublishingHistory.sourcepackagerelease =
637- SourcePackageRelease.id AND
638- SourcePackagePublishingHistory.distroseries =
639- DistroSeries.id AND
640- SourcePackagePublishingHistory.archive = %s AND
641- SourcePackagePublishingHistory.sourcepackagename = %s AND
642- DistroSeries.distribution = %s
643- """ % sqlvalues(sourcepackagename, version,
644- distroseries.main_archive,
645- sourcepackagename,
646- distroseries.distribution)
647- ret = SourcePackageRelease.select(query,
648- clauseTables=['SourcePackagePublishingHistory', 'DistroSeries'],
649- orderBy=["-SourcePackagePublishingHistory.datecreated"])
650- if not ret:
651- return None
652- return ret[0]
653+ SPR = SourcePackageRelease
654+ SPPH = SourcePackagePublishingHistory
655+ rows = IStore(SPR).find(
656+ SPR,
657+ SPR.sourcepackagename == sourcepackagename,
658+ Cast(SPR.version, "text") == version,
659+ SPPH.sourcepackagerelease == SPR.id,
660+ SPPH.distroseries == DistroSeries.id,
661+ SPPH.archive == distroseries.main_archive,
662+ SPPH.sourcepackagename == sourcepackagename,
663+ DistroSeries.distribution == distroseries.distribution)
664+ return rows.order_by(
665+ Desc(SourcePackagePublishingHistory.datecreated)).first()
666
667 def createSourcePackageRelease(self, src, distroseries):
668 """Create a SourcePackagerelease and db dependencies if needed.
669@@ -749,37 +744,33 @@ class BinaryPackageHandler:
670 version = binarypackagedata.version
671 architecture = binarypackagedata.architecture
672
673- clauseTables = ["BinaryPackageRelease", "DistroSeries",
674- "DistroArchSeries", "BinaryPackageBuild",
675- "BinaryPackagePublishingHistory"]
676 distroseries = distroarchseries.distroseries
677
678 # When looking for binaries, we need to remember that they are
679 # shared between distribution releases, so match on the
680 # distribution and the architecture tag of the distroarchseries
681 # they were built for
682- query = (
683- "BinaryPackagePublishingHistory.archive = %s AND "
684- "BinaryPackagePublishingHistory.binarypackagename = %s AND "
685- "BinaryPackageRelease.id ="
686- " BinaryPackagePublishingHistory.binarypackagerelease AND "
687- "BinaryPackageRelease.binarypackagename=%s AND "
688- "BinaryPackageRelease.version::text = %s AND "
689- "BinaryPackageRelease.build = BinaryPackageBuild.id AND "
690- "BinaryPackageBuild.distro_arch_series = DistroArchSeries.id AND "
691- "DistroArchSeries.distroseries = DistroSeries.id AND "
692- "DistroSeries.distribution = %s" %
693- sqlvalues(distroseries.main_archive, binaryname, binaryname,
694- version, distroseries.distribution))
695+ BPR = BinaryPackageRelease
696+ BPPH = BinaryPackagePublishingHistory
697+ BPB = BinaryPackageBuild
698+ clauses = [
699+ BPPH.archive == distroseries.main_archive,
700+ BPPH.binarypackagename == binaryname,
701+ BPPH.binarypackagerelease == BPR.id,
702+ BPR.binarypackagename == binaryname,
703+ Cast(BPR.version, "text") == version,
704+ BPR.build == BPB.id,
705+ BPB.distro_arch_series == DistroArchSeries.id,
706+ DistroArchSeries.distroseries == DistroSeries.id,
707+ DistroSeries.distribution == distroseries.distribution,
708+ ]
709
710 if architecture != "all":
711- query += ("AND DistroArchSeries.architecturetag = %s" %
712- quote(architecture))
713+ clauses.append(DistroArchSeries.architecturetag == architecture)
714
715 try:
716- bpr = BinaryPackageRelease.selectOne(
717- query, clauseTables=clauseTables, distinct=True)
718- except SQLObjectMoreThanOneResultError:
719+ bpr = IStore(BPR).find(BPR, *clauses).config(distinct=True).one()
720+ except NotOneError:
721 # XXX kiko 2005-10-27: Untested
722 raise MultiplePackageReleaseError("Found more than one "
723 "entry for %s (%s) for %s in %s" %

Subscribers

People subscribed via source and target branches

to status/vote changes: