← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] ~cjwatson/launchpad:stormify-productrelease into launchpad:master

 

Colin Watson has proposed merging ~cjwatson/launchpad:stormify-productrelease into launchpad:master.

Commit message:
Convert ProductRelease and ProductReleaseFile to Storm

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)

For more details, see:
https://code.launchpad.net/~cjwatson/launchpad/+git/launchpad/+merge/447211
-- 
Your team Launchpad code reviewers is requested to review the proposed merge of ~cjwatson/launchpad:stormify-productrelease into launchpad:master.
diff --git a/lib/lp/registry/model/productrelease.py b/lib/lp/registry/model/productrelease.py
index aec8786..a73d29a 100644
--- a/lib/lp/registry/model/productrelease.py
+++ b/lib/lp/registry/model/productrelease.py
@@ -9,10 +9,15 @@ __all__ = [
 ]
 
 import os
+from datetime import timezone
 from io import BufferedIOBase, BytesIO
+from operator import itemgetter
 
-from storm.expr import And, Desc
-from storm.store import EmptyResultSet
+from storm.expr import And, Desc, Join, LeftJoin
+from storm.info import ClassAlias
+from storm.properties import DateTime, Int, Unicode
+from storm.references import Reference, ReferenceSet
+from storm.store import EmptyResultSet, Store
 from zope.component import getUtility
 from zope.interface import implementer
 
@@ -30,16 +35,12 @@ from lp.registry.interfaces.productrelease import (
     UpstreamFileType,
 )
 from lp.services.database.constants import UTC_NOW
-from lp.services.database.datetimecol import UtcDateTimeCol
+from lp.services.database.decoratedresultset import DecoratedResultSet
 from lp.services.database.enumcol import DBEnum
 from lp.services.database.interfaces import IStore
-from lp.services.database.sqlbase import SQLBase, sqlvalues
-from lp.services.database.sqlobject import (
-    ForeignKey,
-    SQLMultipleJoin,
-    StringCol,
-)
+from lp.services.database.stormbase import StormBase
 from lp.services.librarian.interfaces import ILibraryFileAliasSet
+from lp.services.librarian.model import LibraryFileAlias, LibraryFileContent
 from lp.services.propertycache import cachedproperty
 from lp.services.webapp.publisher import (
     get_raw_form_value_from_current_request,
@@ -47,33 +48,51 @@ from lp.services.webapp.publisher import (
 
 
 @implementer(IProductRelease)
-class ProductRelease(SQLBase):
+class ProductRelease(StormBase):
     """A release of a product."""
 
-    _table = "ProductRelease"
-    _defaultOrder = ["-datereleased"]
+    __storm_table__ = "ProductRelease"
+    __storm_order__ = ("-datereleased",)
 
-    datereleased = UtcDateTimeCol(notNull=True)
-    release_notes = StringCol(notNull=False, default=None)
-    changelog = StringCol(notNull=False, default=None)
-    datecreated = UtcDateTimeCol(
-        dbName="datecreated", notNull=True, default=UTC_NOW
+    id = Int(primary=True)
+    datereleased = DateTime(allow_none=False, tzinfo=timezone.utc)
+    release_notes = Unicode(allow_none=True, default=None)
+    changelog = Unicode(allow_none=True, default=None)
+    datecreated = DateTime(
+        name="datecreated",
+        allow_none=False,
+        default=UTC_NOW,
+        tzinfo=timezone.utc,
     )
-    owner = ForeignKey(
-        dbName="owner",
-        foreignKey="Person",
-        storm_validator=validate_person,
-        notNull=True,
-    )
-    milestone = ForeignKey(dbName="milestone", foreignKey="Milestone")
-
-    _files = SQLMultipleJoin(
-        "ProductReleaseFile",
-        joinColumn="productrelease",
-        orderBy="-date_uploaded",
-        prejoins=["productrelease"],
+    owner_id = Int(name="owner", validator=validate_person, allow_none=False)
+    owner = Reference(owner_id, "Person.id")
+    milestone_id = Int(name="milestone", allow_none=False)
+    milestone = Reference(milestone_id, "Milestone.id")
+
+    _files = ReferenceSet(
+        "id",
+        "ProductReleaseFile.productrelease_id",
+        order_by=Desc("ProductReleaseFile.date_uploaded"),
     )
 
+    def __init__(
+        self,
+        datereleased,
+        owner,
+        milestone,
+        release_notes=None,
+        changelog=None,
+    ):
+        super().__init__()
+        self.owner = owner
+        self.milestone = milestone
+        self.datereleased = datereleased
+        self.release_notes = release_notes
+        self.changelog = changelog
+
+    # This is cached so that
+    # lp.registry.model.product.get_precached_products can populate the
+    # cache from a bulk query.
     @cachedproperty
     def files(self):
         return self._files
@@ -119,7 +138,7 @@ class ProductRelease(SQLBase):
             "You can't delete a product release which has files associated "
             "with it."
         )
-        SQLBase.destroySelf(self)
+        Store.of(self).remove(self)
 
     def _getFileObjectAndSize(self, file_or_data):
         """Return an object and length for file_or_data.
@@ -232,20 +251,21 @@ class ProductRelease(SQLBase):
 
 
 @implementer(IProductReleaseFile)
-class ProductReleaseFile(SQLBase):
+class ProductReleaseFile(StormBase):
     """A file of a product release."""
 
-    _table = "ProductReleaseFile"
+    __storm_table__ = "ProductReleaseFile"
 
-    productrelease = ForeignKey(
-        dbName="productrelease", foreignKey="ProductRelease", notNull=True
-    )
+    id = Int(primary=True)
 
-    libraryfile = ForeignKey(
-        dbName="libraryfile", foreignKey="LibraryFileAlias", notNull=True
-    )
+    productrelease_id = Int(name="productrelease", allow_none=False)
+    productrelease = Reference(productrelease_id, "ProductRelease.id")
 
-    signature = ForeignKey(dbName="signature", foreignKey="LibraryFileAlias")
+    libraryfile_id = Int(name="libraryfile", allow_none=False)
+    libraryfile = Reference(libraryfile_id, "LibraryFileAlias.id")
+
+    signature_id = Int(name="signature", allow_none=True)
+    signature = Reference(signature_id, "LibraryFileAlias.id")
 
     filetype = DBEnum(
         name="filetype",
@@ -254,16 +274,37 @@ class ProductReleaseFile(SQLBase):
         default=UpstreamFileType.CODETARBALL,
     )
 
-    description = StringCol(notNull=False, default=None)
+    description = Unicode(name="description", allow_none=True, default=None)
+
+    uploader_id = Int(
+        name="uploader", validator=validate_public_person, allow_none=False
+    )
+    uploader = Reference(uploader_id, "Person.id")
 
-    uploader = ForeignKey(
-        dbName="uploader",
-        foreignKey="Person",
-        storm_validator=validate_public_person,
-        notNull=True,
+    date_uploaded = DateTime(
+        allow_none=False, default=UTC_NOW, tzinfo=timezone.utc
     )
 
-    date_uploaded = UtcDateTimeCol(notNull=True, default=UTC_NOW)
+    def __init__(
+        self,
+        productrelease,
+        libraryfile,
+        filetype,
+        uploader,
+        signature=None,
+        description=None,
+    ):
+        super().__init__()
+        self.productrelease = productrelease
+        self.libraryfile = libraryfile
+        self.filetype = filetype
+        self.uploader = uploader
+        self.signature = signature
+        self.description = description
+
+    def destroySelf(self):
+        """See `IProductReleaseFile`."""
+        Store.of(self).remove(self)
 
 
 @implementer(IProductReleaseSet)
@@ -313,16 +354,42 @@ class ProductReleaseSet:
         releases = list(releases)
         if len(releases) == 0:
             return EmptyResultSet()
-        return ProductReleaseFile.select(
-            """ProductReleaseFile.productrelease IN %s"""
-            % (sqlvalues([release.id for release in releases])),
-            orderBy="-date_uploaded",
-            prejoins=[
-                "libraryfile",
-                "libraryfile.content",
-                "productrelease",
-                "signature",
-            ],
+        SignatureAlias = ClassAlias(LibraryFileAlias)
+        return DecoratedResultSet(
+            IStore(ProductReleaseFile)
+            .using(
+                ProductReleaseFile,
+                Join(
+                    LibraryFileAlias,
+                    ProductReleaseFile.libraryfile_id == LibraryFileAlias.id,
+                ),
+                LeftJoin(
+                    LibraryFileContent,
+                    LibraryFileAlias.contentID == LibraryFileContent.id,
+                ),
+                Join(
+                    ProductRelease,
+                    ProductReleaseFile.productrelease_id == ProductRelease.id,
+                ),
+                LeftJoin(
+                    SignatureAlias,
+                    ProductReleaseFile.signature_id == SignatureAlias.id,
+                ),
+            )
+            .find(
+                (
+                    ProductReleaseFile,
+                    LibraryFileAlias,
+                    LibraryFileContent,
+                    ProductRelease,
+                    SignatureAlias,
+                ),
+                ProductReleaseFile.productrelease_id.is_in(
+                    [release.id for release in releases]
+                ),
+            )
+            .order_by(Desc(ProductReleaseFile.date_uploaded)),
+            result_decorator=itemgetter(0),
         )
 
 
diff --git a/lib/lp/registry/model/productseries.py b/lib/lp/registry/model/productseries.py
index 10d30ea..0bd1479 100644
--- a/lib/lp/registry/model/productseries.py
+++ b/lib/lp/registry/model/productseries.py
@@ -10,6 +10,7 @@ __all__ = [
 ]
 
 import datetime
+from operator import itemgetter
 
 from lazr.delegates import delegate_to
 from storm.expr import Max, Sum
@@ -222,17 +223,13 @@ class ProductSeries(
 
         # The Milestone is cached too because most uses of a ProductRelease
         # need it. The decorated resultset returns just the ProductRelease.
-        def decorate(row):
-            product_release, milestone = row
-            return product_release
-
         result = store.find(
             (ProductRelease, Milestone),
             Milestone.productseries == self,
             ProductRelease.milestone == Milestone.id,
         )
         result = result.order_by(Desc("datereleased"))
-        return DecoratedResultSet(result, decorate)
+        return DecoratedResultSet(result, result_decorator=itemgetter(0))
 
     @cachedproperty
     def _cached_releases(self):
diff --git a/lib/lp/registry/scripts/productreleasefinder/finder.py b/lib/lp/registry/scripts/productreleasefinder/finder.py
index 6126fb0..107a570 100644
--- a/lib/lp/registry/scripts/productreleasefinder/finder.py
+++ b/lib/lp/registry/scripts/productreleasefinder/finder.py
@@ -172,9 +172,9 @@ class ProductReleaseFinder:
             Product.name == product_name,
             Product.id == ProductSeries.productID,
             Milestone.productseriesID == ProductSeries.id,
-            ProductRelease.milestoneID == Milestone.id,
-            ProductReleaseFile.productreleaseID == ProductRelease.id,
-            LibraryFileAlias.id == ProductReleaseFile.libraryfileID,
+            ProductRelease.milestone_id == Milestone.id,
+            ProductReleaseFile.productrelease_id == ProductRelease.id,
+            LibraryFileAlias.id == ProductReleaseFile.libraryfile_id,
         )
         file_names = set(found_names)
         return file_names
diff --git a/lib/lp/registry/vocabularies.py b/lib/lp/registry/vocabularies.py
index a9acabf..98333e0 100644
--- a/lib/lp/registry/vocabularies.py
+++ b/lib/lp/registry/vocabularies.py
@@ -1200,7 +1200,7 @@ class AllUserTeamsParticipationPlusSelfSimpleDisplayVocabulary(
 
 
 @implementer(IHugeVocabulary)
-class ProductReleaseVocabulary(SQLObjectVocabularyBase):
+class ProductReleaseVocabulary(StormVocabularyBase):
     """All `IProductRelease` objects vocabulary."""
 
     displayname = "Select a Product Release"
@@ -1210,8 +1210,12 @@ class ProductReleaseVocabulary(SQLObjectVocabularyBase):
     # Sorting by version won't give the expected results, because it's just a
     # text field.  e.g. ["1.0", "2.0", "11.0"] would be sorted as ["1.0",
     # "11.0", "2.0"].
-    _orderBy = [Product.q.name, ProductSeries.q.name, Milestone.q.name]
-    _clauseTables = ["Product", "ProductSeries"]
+    _order_by = [Product.name, ProductSeries.name, Milestone.name]
+    _clauses = [
+        ProductRelease.milestone_id == Milestone.id,
+        Milestone.productseriesID == ProductSeries.id,
+        ProductSeries.productID == Product.id,
+    ]
 
     def toTerm(self, obj):
         """See `IVocabulary`."""
@@ -1240,14 +1244,17 @@ class ProductReleaseVocabulary(SQLObjectVocabularyBase):
         except ValueError:
             raise LookupError(token)
 
-        obj = ProductRelease.selectOne(
-            AND(
-                ProductRelease.q.milestoneID == Milestone.q.id,
-                Milestone.q.productseriesID == ProductSeries.q.id,
-                ProductSeries.q.productID == Product.q.id,
-                Product.q.name == productname,
-                ProductSeries.q.name == productseriesname,
+        obj = (
+            IStore(ProductRelease)
+            .find(
+                ProductRelease,
+                ProductRelease.milestone_id == Milestone.id,
+                Milestone.productseriesID == ProductSeries.id,
+                ProductSeries.productID == Product.id,
+                Product.name == productname,
+                ProductSeries.name == productseriesname,
             )
+            .one()
         )
         try:
             return self.toTerm(obj)
@@ -1259,22 +1266,22 @@ class ProductReleaseVocabulary(SQLObjectVocabularyBase):
         if not query:
             return self.emptySelectResults()
 
-        query = six.ensure_text(query).lower()
-        objs = self._table.select(
-            AND(
-                Milestone.q.id == ProductRelease.q.milestoneID,
-                ProductSeries.q.id == Milestone.q.productseriesID,
-                Product.q.id == ProductSeries.q.productID,
-                OR(
-                    CONTAINSSTRING(Product.q.name, query),
-                    CONTAINSSTRING(ProductSeries.q.name, query),
+        query = query.lower()
+        return (
+            IStore(self._table)
+            .find(
+                self._table,
+                ProductRelease.milestone_id == Milestone.id,
+                Milestone.productseriesID == ProductSeries.id,
+                ProductSeries.productID == Product.id,
+                Or(
+                    Product.name.contains_string(query),
+                    ProductSeries.name.contains_string(query),
                 ),
-            ),
-            orderBy=self._orderBy,
+            )
+            .order_by(self._order_by)
         )
 
-        return objs
-
 
 @implementer(IHugeVocabulary)
 class ProductSeriesVocabulary(SQLObjectVocabularyBase):
diff --git a/lib/lp/translations/doc/poimport-pofile-not-exported-from-rosetta.rst b/lib/lp/translations/doc/poimport-pofile-not-exported-from-rosetta.rst
index a683b72..b0bf780 100644
--- a/lib/lp/translations/doc/poimport-pofile-not-exported-from-rosetta.rst
+++ b/lib/lp/translations/doc/poimport-pofile-not-exported-from-rosetta.rst
@@ -22,6 +22,7 @@ Here are some imports we need to get this test running.
 
     >>> from lp.app.interfaces.launchpad import ILaunchpadCelebrities
     >>> from lp.registry.interfaces.person import IPersonSet
+    >>> from lp.services.database.interfaces import IStore
     >>> from lp.translations.enums import RosettaImportStatus
     >>> from lp.translations.interfaces.translationimportqueue import (
     ...     ITranslationImportQueue,
@@ -46,7 +47,7 @@ Here's the person who'll be doing the import.
 Now, is time to create the new potemplate
 
     >>> from lp.registry.model.productrelease import ProductRelease
-    >>> release = ProductRelease.get(3)
+    >>> release = IStore(ProductRelease).get(ProductRelease, 3)
     >>> print(release.milestone.productseries.product.name)
     firefox
     >>> series = release.milestone.productseries
@@ -140,10 +141,9 @@ We should also be sure that we don't block any import that is coming from
 upstream. That kind of import is not blocked if they lack the
 'X-Rosetta-Export-Date' header.
 
-We need to fetch again some SQLObjects because we did a transaction
-commit.
+We need to fetch some rows again because we committed a transaction.
 
-    >>> release = ProductRelease.get(3)
+    >>> release = IStore(ProductRelease).get(ProductRelease, 3)
     >>> series = release.milestone.productseries
     >>> subset = POTemplateSubset(productseries=series)
     >>> potemplate = subset.getPOTemplateByName("firefox")
diff --git a/lib/lp/translations/doc/poimport-pofile-old-po-imported.rst b/lib/lp/translations/doc/poimport-pofile-old-po-imported.rst
index 4ee040a..9b077c8 100644
--- a/lib/lp/translations/doc/poimport-pofile-old-po-imported.rst
+++ b/lib/lp/translations/doc/poimport-pofile-old-po-imported.rst
@@ -20,6 +20,7 @@ Here are some imports we need to get this test running.
 
     >>> from lp.app.interfaces.launchpad import ILaunchpadCelebrities
     >>> from lp.registry.interfaces.person import IPersonSet
+    >>> from lp.services.database.interfaces import IStore
     >>> from lp.translations.interfaces.translationimportqueue import (
     ...     ITranslationImportQueue,
     ... )
@@ -44,7 +45,7 @@ Here's the person who'll be doing the import.
 Now it's time to create the new potemplate
 
     >>> from lp.registry.model.productrelease import ProductRelease
-    >>> release = ProductRelease.get(3)
+    >>> release = IStore(ProductRelease).get(ProductRelease, 3)
     >>> print(release.milestone.productseries.product.name)
     firefox
     >>> series = release.milestone.productseries
diff --git a/lib/lp/translations/doc/poimport-pofile-syntax-error.rst b/lib/lp/translations/doc/poimport-pofile-syntax-error.rst
index e2f7c91..30be238 100644
--- a/lib/lp/translations/doc/poimport-pofile-syntax-error.rst
+++ b/lib/lp/translations/doc/poimport-pofile-syntax-error.rst
@@ -10,6 +10,7 @@ Here are some imports we need to get this test running.
 
     >>> from lp.app.interfaces.launchpad import ILaunchpadCelebrities
     >>> from lp.registry.interfaces.person import IPersonSet
+    >>> from lp.services.database.interfaces import IStore
     >>> from lp.translations.interfaces.translationimportqueue import (
     ...     ITranslationImportQueue,
     ... )
@@ -38,7 +39,7 @@ Here's the person who'll be doing the import.
 Now, is time to create the new potemplate
 
     >>> from lp.registry.model.productrelease import ProductRelease
-    >>> release = ProductRelease.get(3)
+    >>> release = IStore(ProductRelease).get(ProductRelease, 3)
     >>> print(release.milestone.productseries.product.name)
     firefox
 
diff --git a/lib/lp/translations/doc/poimport-potemplate-syntax-error.rst b/lib/lp/translations/doc/poimport-potemplate-syntax-error.rst
index c14673d..a45fa79 100644
--- a/lib/lp/translations/doc/poimport-potemplate-syntax-error.rst
+++ b/lib/lp/translations/doc/poimport-potemplate-syntax-error.rst
@@ -10,6 +10,7 @@ Here are some imports we need to get this test running.
 
     >>> from lp.app.interfaces.launchpad import ILaunchpadCelebrities
     >>> from lp.registry.interfaces.person import IPersonSet
+    >>> from lp.services.database.interfaces import IStore
     >>> from lp.translations.interfaces.translationimportqueue import (
     ...     ITranslationImportQueue,
     ... )
@@ -37,7 +38,7 @@ Here's the person who'll be doing the import.
 Now, is time to create the new potemplate
 
     >>> from lp.registry.model.productrelease import ProductRelease
-    >>> release = ProductRelease.get(3)
+    >>> release = IStore(ProductRelease).get(ProductRelease, 3)
     >>> print(release.milestone.productseries.product.name)
     firefox
     >>> series = release.milestone.productseries