← Back to team overview

launchpad-reviewers team mailing list archive

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

 

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

Commit message:
Convert remaining lp.blueprints models to Storm

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)

For more details, see:
https://code.launchpad.net/~cjwatson/launchpad/+git/launchpad/+merge/428840

This ports `Specification`, `SpecificationDependency`, and `SpecificationMessage` away from the deprecated SQLObject style.
-- 
Your team Launchpad code reviewers is requested to review the proposed merge of ~cjwatson/launchpad:stormify-specification into launchpad:master.
diff --git a/lib/lp/blueprints/doc/specification.rst b/lib/lp/blueprints/doc/specification.rst
index 47b6121..6c490cb 100644
--- a/lib/lp/blueprints/doc/specification.rst
+++ b/lib/lp/blueprints/doc/specification.rst
@@ -320,13 +320,13 @@ If there are dependencies between the specs, the method returns a
 mapping between them.
 
     >>> spec_a.createDependency(spec_b)
-    <SpecificationDependency at ...>
+    <...SpecificationDependency object at ...>
 
     >>> spec_a.createDependency(spec_c)
-    <SpecificationDependency at ...>
+    <...SpecificationDependency object at ...>
 
     >>> spec_c.createDependency(spec_d)
-    <SpecificationDependency at ...>
+    <...SpecificationDependency object at ...>
 
     >>> deps_dict = specset.getDependencyDict(
     ...     [spec_a, spec_b, spec_c, spec_d])
diff --git a/lib/lp/blueprints/interfaces/specificationsubscription.py b/lib/lp/blueprints/interfaces/specificationsubscription.py
index c727957..eef862e 100644
--- a/lib/lp/blueprints/interfaces/specificationsubscription.py
+++ b/lib/lp/blueprints/interfaces/specificationsubscription.py
@@ -38,7 +38,6 @@ class ISpecificationSubscription(Interface):
     )
     personID = Attribute("db person value")
     specification = Int(title=_("Specification"), required=True, readonly=True)
-    specificationID = Attribute("db specification value")
     essential = Bool(
         title=_("Participation essential"),
         required=True,
diff --git a/lib/lp/blueprints/model/specification.py b/lib/lp/blueprints/model/specification.py
index 707d397..cffe3d2 100644
--- a/lib/lp/blueprints/model/specification.py
+++ b/lib/lp/blueprints/model/specification.py
@@ -12,9 +12,23 @@ __all__ = [
 
 import operator
 
+import pytz
 from lazr.lifecycle.event import ObjectCreatedEvent
 from lazr.lifecycle.objectdelta import ObjectDelta
-from storm.locals import SQL, Count, Desc, Join, Or, ReferenceSet, Store
+from storm.locals import (
+    SQL,
+    Bool,
+    Count,
+    DateTime,
+    Desc,
+    Int,
+    Join,
+    Or,
+    Reference,
+    ReferenceSet,
+    Store,
+    Unicode,
+)
 from zope.component import getUtility
 from zope.event import notify
 from zope.interface import implementer
@@ -71,22 +85,13 @@ from lp.registry.interfaces.productseries import IProductSeries
 from lp.registry.model.milestone import Milestone
 from lp.services.database import bulk
 from lp.services.database.constants import DEFAULT, UTC_NOW
-from lp.services.database.datetimecol import UtcDateTimeCol
 from lp.services.database.enumcol import DBEnum
 from lp.services.database.interfaces import IStore
 from lp.services.database.sqlbase import (
-    SQLBase,
     convert_storm_clause_to_string,
     sqlvalues,
 )
-from lp.services.database.sqlobject import (
-    BoolCol,
-    ForeignKey,
-    IntCol,
-    SQLMultipleJoin,
-    SQLRelatedJoin,
-    StringCol,
-)
+from lp.services.database.stormbase import StormBase
 from lp.services.mail.helpers import get_contact_email_addresses
 from lp.services.propertycache import cachedproperty, get_property_cache
 from lp.services.webapp.interfaces import ILaunchBag
@@ -160,15 +165,17 @@ SPECIFICATION_POLICY_DEFAULT_TYPES = {
 
 
 @implementer(ISpecification, IBugLinkTarget, IInformationType)
-class Specification(SQLBase, BugLinkTargetMixin, InformationTypeMixin):
+class Specification(StormBase, BugLinkTargetMixin, InformationTypeMixin):
     """See ISpecification."""
 
-    _defaultOrder = ["-priority", "definition_status", "name", "id"]
+    __storm_table__ = "Specification"
+    __storm_order__ = ("-priority", "definition_status", "name", "id")
 
     # db field names
-    name = StringCol(unique=True, notNull=True)
-    title = StringCol(notNull=True)
-    summary = StringCol(notNull=True)
+    id = Int(primary=True)
+    name = Unicode(allow_none=False)
+    title = Unicode(allow_none=False)
+    summary = Unicode(allow_none=False)
     definition_status = DBEnum(
         enum=SpecificationDefinitionStatus,
         allow_none=False,
@@ -179,110 +186,94 @@ class Specification(SQLBase, BugLinkTargetMixin, InformationTypeMixin):
         allow_none=False,
         default=SpecificationPriority.UNDEFINED,
     )
-    _assignee = ForeignKey(
-        dbName="assignee",
-        notNull=False,
-        foreignKey="Person",
-        storm_validator=validate_public_person,
-        default=None,
-    )
-    _drafter = ForeignKey(
-        dbName="drafter",
-        notNull=False,
-        foreignKey="Person",
-        storm_validator=validate_public_person,
-        default=None,
-    )
-    _approver = ForeignKey(
-        dbName="approver",
-        notNull=False,
-        foreignKey="Person",
-        storm_validator=validate_public_person,
+    _assignee_id = Int(
+        name="assignee",
+        allow_none=True,
+        validator=validate_public_person,
         default=None,
     )
-    owner = ForeignKey(
-        dbName="owner",
-        foreignKey="Person",
-        storm_validator=validate_public_person,
-        notNull=True,
-    )
-    datecreated = UtcDateTimeCol(notNull=True, default=DEFAULT)
-    product = ForeignKey(
-        dbName="product", foreignKey="Product", notNull=False, default=None
-    )
-    productseries = ForeignKey(
-        dbName="productseries",
-        foreignKey="ProductSeries",
-        notNull=False,
+    _assignee = Reference(_assignee_id, "Person.id")
+    _drafter_id = Int(
+        name="drafter",
+        allow_none=True,
+        validator=validate_public_person,
         default=None,
     )
-    distribution = ForeignKey(
-        dbName="distribution",
-        foreignKey="Distribution",
-        notNull=False,
+    _drafter = Reference(_drafter_id, "Person.id")
+    _approver_id = Int(
+        name="approver",
+        allow_none=True,
+        validator=validate_public_person,
         default=None,
     )
-    distroseries = ForeignKey(
-        dbName="distroseries",
-        foreignKey="DistroSeries",
-        notNull=False,
-        default=None,
+    _approver = Reference(_approver_id, "Person.id")
+    owner_id = Int(
+        name="owner", validator=validate_public_person, allow_none=False
     )
+    owner = Reference(owner_id, "Person.id")
+    datecreated = DateTime(allow_none=False, default=DEFAULT, tzinfo=pytz.UTC)
+    product_id = Int(name="product", allow_none=True, default=None)
+    product = Reference(product_id, "Product.id")
+    productseries_id = Int(name="productseries", allow_none=True, default=None)
+    productseries = Reference(productseries_id, "ProductSeries.id")
+    distribution_id = Int(name="distribution", allow_none=True, default=None)
+    distribution = Reference(distribution_id, "Distribution.id")
+    distroseries_id = Int(name="distroseries", allow_none=True, default=None)
+    distroseries = Reference(distroseries_id, "DistroSeries.id")
     goalstatus = DBEnum(
         enum=SpecificationGoalStatus,
         allow_none=False,
         default=SpecificationGoalStatus.PROPOSED,
     )
-    goal_proposer = ForeignKey(
-        dbName="goal_proposer",
-        notNull=False,
-        foreignKey="Person",
-        storm_validator=validate_public_person,
+    goal_proposer_id = Int(
+        name="goal_proposer",
+        allow_none=True,
+        validator=validate_public_person,
         default=None,
     )
-    date_goal_proposed = UtcDateTimeCol(notNull=False, default=None)
-    goal_decider = ForeignKey(
-        dbName="goal_decider",
-        notNull=False,
-        foreignKey="Person",
-        storm_validator=validate_public_person,
+    goal_proposer = Reference(goal_proposer_id, "Person.id")
+    date_goal_proposed = DateTime(
+        allow_none=True, default=None, tzinfo=pytz.UTC
+    )
+    goal_decider_id = Int(
+        name="goal_decider",
+        allow_none=True,
+        validator=validate_public_person,
         default=None,
     )
-    date_goal_decided = UtcDateTimeCol(notNull=False, default=None)
-    milestone = ForeignKey(
-        dbName="milestone", foreignKey="Milestone", notNull=False, default=None
+    goal_decider = Reference(goal_decider_id, "Person.id")
+    date_goal_decided = DateTime(
+        allow_none=True, default=None, tzinfo=pytz.UTC
     )
-    specurl = StringCol(notNull=False, default=None)
-    whiteboard = StringCol(notNull=False, default=None)
-    direction_approved = BoolCol(notNull=True, default=False)
-    man_days = IntCol(notNull=False, default=None)
+    milestone_id = Int(name="milestone", allow_none=True, default=None)
+    milestone = Reference(milestone_id, "Milestone.id")
+    specurl = Unicode(allow_none=True, default=None)
+    whiteboard = Unicode(allow_none=True, default=None)
+    direction_approved = Bool(allow_none=False, default=False)
+    man_days = Int(allow_none=True, default=None)
     implementation_status = DBEnum(
         enum=SpecificationImplementationStatus,
         allow_none=False,
         default=SpecificationImplementationStatus.UNKNOWN,
     )
-    superseded_by = ForeignKey(
-        dbName="superseded_by",
-        foreignKey="Specification",
-        notNull=False,
-        default=None,
-    )
-    completer = ForeignKey(
-        dbName="completer",
-        notNull=False,
-        foreignKey="Person",
-        storm_validator=validate_public_person,
+    superseded_by_id = Int(name="superseded_by", allow_none=True, default=None)
+    superseded_by = Reference(superseded_by_id, "Specification.id")
+    completer_id = Int(
+        name="completer",
+        allow_none=True,
+        validator=validate_public_person,
         default=None,
     )
-    date_completed = UtcDateTimeCol(notNull=False, default=None)
-    starter = ForeignKey(
-        dbName="starter",
-        notNull=False,
-        foreignKey="Person",
-        storm_validator=validate_public_person,
+    completer = Reference(completer_id, "Person.id")
+    date_completed = DateTime(allow_none=True, default=None, tzinfo=pytz.UTC)
+    starter_id = Int(
+        name="starter",
+        allow_none=True,
+        validator=validate_public_person,
         default=None,
     )
-    date_started = UtcDateTimeCol(notNull=False, default=None)
+    starter = Reference(starter_id, "Person.id")
+    date_started = DateTime(allow_none=True, default=None, tzinfo=pytz.UTC)
 
     # useful joins
     _subscriptions = ReferenceSet(
@@ -298,32 +289,59 @@ class Specification(SQLBase, BugLinkTargetMixin, InformationTypeMixin):
         order_by=("Person.display_name", "Person.name"),
     )
     sprint_links = ReferenceSet(
-        "<primary key>",
+        "id",
         "SprintSpecification.specification_id",
         order_by="SprintSpecification.id",
     )
     sprints = ReferenceSet(
-        "<primary key>",
+        "id",
         "SprintSpecification.specification_id",
         "SprintSpecification.sprint_id",
         "Sprint.id",
         order_by="Sprint.name",
     )
-    spec_dependency_links = SQLMultipleJoin(
-        "SpecificationDependency", joinColumn="specification", orderBy="id"
+    spec_dependency_links = ReferenceSet(
+        "id",
+        "SpecificationDependency.specification_id",
+        order_by="SpecificationDependency.id",
     )
 
-    dependencies = SQLRelatedJoin(
-        "Specification",
-        joinColumn="specification",
-        otherColumn="dependency",
-        orderBy="title",
-        intermediateTable="SpecificationDependency",
+    dependencies = ReferenceSet(
+        "id",
+        "SpecificationDependency.specification_id",
+        "SpecificationDependency.dependency_id",
+        "Specification.id",
+        order_by="Specification.title",
     )
     information_type = DBEnum(
         enum=InformationType, allow_none=False, default=InformationType.PUBLIC
     )
 
+    def __init__(
+        self,
+        name,
+        title,
+        summary,
+        owner,
+        definition_status=DEFAULT,
+        assignee=None,
+        drafter=None,
+        approver=None,
+        specurl=None,
+        whiteboard=None,
+    ):
+        super().__init__()
+        self.name = name
+        self.title = title
+        self.summary = summary
+        self.owner = owner
+        self.definition_status = definition_status
+        self._assignee = assignee
+        self._drafter = drafter
+        self._approver = approver
+        self.specurl = specurl
+        self.whiteboard = whiteboard
+
     @cachedproperty
     def linked_branches(self):
         return list(
@@ -355,44 +373,44 @@ class Specification(SQLBase, BugLinkTargetMixin, InformationTypeMixin):
 
     def getDependencies(self, user=None):
         return self._fetch_children_or_parents(
-            SpecificationDependency.specificationID,
-            SpecificationDependency.dependencyID,
+            SpecificationDependency.specification_id,
+            SpecificationDependency.dependency_id,
             user,
         )
 
     def getBlockedSpecs(self, user=None):
         return self._fetch_children_or_parents(
-            SpecificationDependency.dependencyID,
-            SpecificationDependency.specificationID,
+            SpecificationDependency.dependency_id,
+            SpecificationDependency.specification_id,
             user,
         )
 
-    def set_assignee(self, person):
-        self.subscribeIfAccessGrantNeeded(person)
-        self._assignee = person
-
-    def get_assignee(self):
+    @property
+    def assignee(self):
         return self._assignee
 
-    assignee = property(get_assignee, set_assignee)
-
-    def set_drafter(self, person):
+    @assignee.setter
+    def assignee(self, person):
         self.subscribeIfAccessGrantNeeded(person)
-        self._drafter = person
+        self._assignee = person
 
-    def get_drafter(self):
+    @property
+    def drafter(self):
         return self._drafter
 
-    drafter = property(get_drafter, set_drafter)
-
-    def set_approver(self, person):
+    @drafter.setter
+    def drafter(self, person):
         self.subscribeIfAccessGrantNeeded(person)
-        self._approver = person
+        self._drafter = person
 
-    def get_approver(self):
+    @property
+    def approver(self):
         return self._approver
 
-    approver = property(get_approver, set_approver)
+    @approver.setter
+    def approver(self, person):
+        self.subscribeIfAccessGrantNeeded(person)
+        self._approver = person
 
     def subscribeIfAccessGrantNeeded(self, person):
         """Subscribe person if this specification is not public and if
@@ -777,22 +795,22 @@ class Specification(SQLBase, BugLinkTargetMixin, InformationTypeMixin):
         """See ISpecification."""
         newstatus = None
         if self.is_started:
-            if self.starterID is None:
+            if self.starter_id is None:
                 newstatus = SpecificationLifecycleStatus.STARTED
                 self.date_started = UTC_NOW
                 self.starter = user
         else:
-            if self.starterID is not None:
+            if self.starter_id is not None:
                 newstatus = SpecificationLifecycleStatus.NOTSTARTED
                 self.date_started = None
                 self.starter = None
         if self.is_complete:
-            if self.completerID is None:
+            if self.completer_id is None:
                 newstatus = SpecificationLifecycleStatus.COMPLETE
                 self.date_completed = UTC_NOW
                 self.completer = user
         else:
-            if self.completerID is not None:
+            if self.completer_id is not None:
                 self.date_completed = None
                 self.completer = None
                 if self.is_started:
@@ -1025,8 +1043,8 @@ class Specification(SQLBase, BugLinkTargetMixin, InformationTypeMixin):
         # see if a relevant dependency link exists, and if so, delete it
         for deplink in self.spec_dependency_links:
             if deplink.dependency.id == specification.id:
-                SpecificationDependency.delete(deplink.id)
-                return deplink
+                Store.of(deplink).remove(deplink)
+                return
 
     def all_deps(self, user=None):
         return list(
@@ -1253,7 +1271,7 @@ class SpecificationSet(HasSpecificationsMixin):
                 (Specification.implementation_status, Count()),
                 Or(
                     Specification.productseries == product_series,
-                    Specification.milestoneID.is_in(
+                    Specification.milestone_id.is_in(
                         list(
                             product_series.all_milestones.values(Milestone.id)
                         )
@@ -1291,15 +1309,15 @@ class SpecificationSet(HasSpecificationsMixin):
 
     def getByURL(self, url):
         """See ISpecificationSet."""
-        return Specification.selectOneBy(specurl=url)
+        return IStore(Specification).find(Specification, specurl=url).one()
 
     def getByName(self, pillar, name):
         """See ISpecificationSet."""
         clauses = [Specification.name == name]
         if IDistribution.providedBy(pillar):
-            clauses.append(Specification.distributionID == pillar.id)
+            clauses.append(Specification.distribution == pillar)
         elif IProduct.providedBy(pillar):
-            clauses.append(Specification.productID == pillar.id)
+            clauses.append(Specification.product == pillar)
         return IStore(Specification).find(Specification, *clauses).one()
 
     @property
@@ -1349,9 +1367,9 @@ class SpecificationSet(HasSpecificationsMixin):
             summary=summary,
             definition_status=definition_status,
             owner=owner,
-            _approver=approver,
-            _assignee=assignee,
-            _drafter=drafter,
+            approver=approver,
+            assignee=assignee,
+            drafter=drafter,
             whiteboard=whiteboard,
         )
         spec.setTarget(target)
@@ -1386,14 +1404,14 @@ class SpecificationSet(HasSpecificationsMixin):
         for spec_id, dep_id in results:
             if spec_id not in dependencies:
                 dependencies[spec_id] = []
-            dependency = Specification.get(dep_id)
+            dependency = IStore(Specification).get(Specification, dep_id)
             dependencies[spec_id].append(dependency)
 
         return dependencies
 
     def get(self, spec_id):
         """See lp.blueprints.interfaces.specification.ISpecificationSet."""
-        return Specification.get(spec_id)
+        return IStore(Specification).get(Specification, spec_id)
 
     def empty_list(self):
         """See `ISpecificationSet`."""
diff --git a/lib/lp/blueprints/model/specificationdependency.py b/lib/lp/blueprints/model/specificationdependency.py
index 365b4be..0a90436 100644
--- a/lib/lp/blueprints/model/specificationdependency.py
+++ b/lib/lp/blueprints/model/specificationdependency.py
@@ -3,23 +3,30 @@
 
 __all__ = ["SpecificationDependency"]
 
+from storm.locals import Int, Reference
 from zope.interface import implementer
 
 from lp.blueprints.interfaces.specificationdependency import (
     ISpecificationDependency,
 )
-from lp.services.database.sqlbase import SQLBase
-from lp.services.database.sqlobject import ForeignKey
+from lp.services.database.stormbase import StormBase
 
 
 @implementer(ISpecificationDependency)
-class SpecificationDependency(SQLBase):
+class SpecificationDependency(StormBase):
     """A link between a spec and a bug."""
 
-    _table = "SpecificationDependency"
-    specification = ForeignKey(
-        dbName="specification", foreignKey="Specification", notNull=True
-    )
-    dependency = ForeignKey(
-        dbName="dependency", foreignKey="Specification", notNull=True
-    )
+    __storm_table__ = "SpecificationDependency"
+
+    id = Int(primary=True)
+
+    specification_id = Int(name="specification", allow_none=False)
+    specification = Reference(specification_id, "Specification.id")
+
+    dependency_id = Int(name="dependency", allow_none=False)
+    dependency = Reference(dependency_id, "Specification.id")
+
+    def __init__(self, specification, dependency):
+        super().__init__()
+        self.specification = specification
+        self.dependency = dependency
diff --git a/lib/lp/blueprints/model/specificationmessage.py b/lib/lp/blueprints/model/specificationmessage.py
index 6edb0e7..3603a9e 100644
--- a/lib/lp/blueprints/model/specificationmessage.py
+++ b/lib/lp/blueprints/model/specificationmessage.py
@@ -5,28 +5,38 @@ __all__ = ["SpecificationMessage", "SpecificationMessageSet"]
 
 from email.utils import make_msgid
 
+from storm.locals import Bool, Int, Reference
 from zope.interface import implementer
 
 from lp.blueprints.interfaces.specificationmessage import (
     ISpecificationMessage,
     ISpecificationMessageSet,
 )
-from lp.services.database.sqlbase import SQLBase
-from lp.services.database.sqlobject import BoolCol, ForeignKey
+from lp.services.database.interfaces import IStore
+from lp.services.database.stormbase import StormBase
 from lp.services.messages.model.message import Message, MessageChunk
 
 
 @implementer(ISpecificationMessage)
-class SpecificationMessage(SQLBase):
+class SpecificationMessage(StormBase):
     """A table linking specifications and messages."""
 
-    _table = "SpecificationMessage"
+    __storm_table__ = "SpecificationMessage"
 
-    specification = ForeignKey(
-        dbName="specification", foreignKey="Specification", notNull=True
-    )
-    message = ForeignKey(dbName="message", foreignKey="Message", notNull=True)
-    visible = BoolCol(notNull=True, default=True)
+    id = Int(primary=True)
+
+    specification_id = Int(name="specification", allow_none=False)
+    specification = Reference(specification_id, "Specification.id")
+
+    message_id = Int(name="message", allow_none=False)
+    message = Reference(message_id, "Message.id")
+
+    visible = Bool(allow_none=False, default=True)
+
+    def __init__(self, specification, message):
+        super().__init__()
+        self.specification = specification
+        self.message = message
 
 
 @implementer(ISpecificationMessageSet)
@@ -39,8 +49,12 @@ class SpecificationMessageSet:
             owner=owner, rfc822msgid=make_msgid("blueprint"), subject=subject
         )
         MessageChunk(message=msg, content=content, sequence=1)
-        return SpecificationMessage(specification=spec, message=msg)
+        specmessage = SpecificationMessage(specification=spec, message=msg)
+        IStore(SpecificationMessage).flush()
+        return specmessage
 
     def get(self, specmessageid):
         """See ISpecificationMessageSet."""
-        return SpecificationMessage.get(specmessageid)
+        return IStore(SpecificationMessage).get(
+            SpecificationMessage, specmessageid
+        )
diff --git a/lib/lp/blueprints/model/specificationsearch.py b/lib/lp/blueprints/model/specificationsearch.py
index 2f00a54..f505628 100644
--- a/lib/lp/blueprints/model/specificationsearch.py
+++ b/lib/lp/blueprints/model/specificationsearch.py
@@ -153,9 +153,9 @@ def search_specifications(
         for spec in rows:
             if need_people:
                 person_ids |= {
-                    spec._assigneeID,
-                    spec._approverID,
-                    spec._drafterID,
+                    spec._assignee_id,
+                    spec._approver_id,
+                    spec._drafter_id,
                 }
             if need_branches:
                 get_property_cache(spec).linked_branches = []
@@ -215,7 +215,7 @@ def get_specification_active_product_filter(context):
         return [], []
     from lp.registry.model.product import Product
 
-    tables = [LeftJoin(Product, Specification.productID == Product.id)]
+    tables = [LeftJoin(Product, Specification.product_id == Product.id)]
     active_products = Or(Specification.product == None, Product.active == True)
     return tables, [active_products]
 
diff --git a/lib/lp/blueprints/tests/test_specification.py b/lib/lp/blueprints/tests/test_specification.py
index fa7404e..62fbea3 100644
--- a/lib/lp/blueprints/tests/test_specification.py
+++ b/lib/lp/blueprints/tests/test_specification.py
@@ -586,7 +586,7 @@ class SpecificationTests(TestCaseWithFactory):
     def _fetch_specs_visible_for_user(self, user):
         return Store.of(self.product).find(
             Specification,
-            Specification.productID == self.product.id,
+            Specification.product == self.product,
             *get_specification_privacy_filter(user),
         )
 
diff --git a/lib/lp/blueprints/tests/test_specification_access_policy_triggers.py b/lib/lp/blueprints/tests/test_specification_access_policy_triggers.py
index e192cb7..2ba7153 100644
--- a/lib/lp/blueprints/tests/test_specification_access_policy_triggers.py
+++ b/lib/lp/blueprints/tests/test_specification_access_policy_triggers.py
@@ -17,15 +17,14 @@ class TestSpecificationAccessPolicyTriggers(TestCaseWithFactory):
 
     def fetchPolicies(self, specification):
         # We may be dealing with private specs, so just ignore security.
-        return (
-            IStore(Specification)
-            .execute(
-                "SELECT access_policy, access_grants FROM specification WHERE "
-                "id = ?",
-                (removeSecurityProxy(specification).id,),
-            )
-            .get_one()
-        )
+        store = IStore(Specification)
+        # Ensure that the specification's ID is available.
+        store.flush()
+        return store.execute(
+            "SELECT access_policy, access_grants FROM specification WHERE "
+            "id = ?",
+            (removeSecurityProxy(specification).id,),
+        ).get_one()
 
     def assertAccess(self, specification, expected_policy, expected_grants):
         policy, grants = self.fetchPolicies(specification)
diff --git a/lib/lp/blueprints/vocabularies/specification.py b/lib/lp/blueprints/vocabularies/specification.py
index 55526cc..68a6a51 100644
--- a/lib/lp/blueprints/vocabularies/specification.py
+++ b/lib/lp/blueprints/vocabularies/specification.py
@@ -14,16 +14,16 @@ from zope.schema.vocabulary import SimpleTerm
 
 from lp.blueprints.model.specification import Specification
 from lp.services.webapp.interfaces import ILaunchBag
-from lp.services.webapp.vocabulary import SQLObjectVocabularyBase
+from lp.services.webapp.vocabulary import StormVocabularyBase
 
 
-class SpecificationVocabulary(SQLObjectVocabularyBase):
+class SpecificationVocabulary(StormVocabularyBase):
     """List specifications for the current product or distribution in
     ILaunchBag, EXCEPT for the current spec in LaunchBag if one exists.
     """
 
     _table = Specification
-    _orderBy = "title"
+    _order_by = "title"
 
     def __iter__(self):
         launchbag = getUtility(ILaunchBag)
diff --git a/lib/lp/blueprints/vocabularies/specificationdependency.py b/lib/lp/blueprints/vocabularies/specificationdependency.py
index de5b89c..d458c05 100644
--- a/lib/lp/blueprints/vocabularies/specificationdependency.py
+++ b/lib/lp/blueprints/vocabularies/specificationdependency.py
@@ -8,7 +8,7 @@ __all__ = [
     "SpecificationDependenciesVocabulary",
 ]
 
-from storm.locals import SQL, And, Store
+from storm.locals import SQL, Store
 from zope.component import getUtility
 from zope.interface import implementer
 from zope.schema.vocabulary import SimpleTerm
@@ -28,12 +28,12 @@ from lp.services.webapp.interfaces import ILaunchBag
 from lp.services.webapp.vocabulary import (
     CountableIterator,
     IHugeVocabulary,
-    SQLObjectVocabularyBase,
+    StormVocabularyBase,
 )
 
 
 @implementer(IHugeVocabulary)
-class SpecificationDepCandidatesVocabulary(SQLObjectVocabularyBase):
+class SpecificationDepCandidatesVocabulary(StormVocabularyBase):
     """Specifications that could be dependencies of this spec.
 
     This includes only those specs that are not blocked by this spec (directly
@@ -54,7 +54,7 @@ class SpecificationDepCandidatesVocabulary(SQLObjectVocabularyBase):
     """
 
     _table = Specification
-    _orderBy = "name"
+    _order_by = "name"
     displayname = "Select a blueprint"
     step_title = "Search"
 
@@ -74,7 +74,7 @@ class SpecificationDepCandidatesVocabulary(SQLObjectVocabularyBase):
         user = getattr(getUtility(ILaunchBag), "user", None)
         return spec not in set(self.context.all_blocked(user=user))
 
-    def _order_by(self):
+    def _order_search_by(self):
         """Look at the context to provide grouping.
 
         If the blueprint is for a project, then matching results for that
@@ -187,7 +187,7 @@ class SpecificationDepCandidatesVocabulary(SQLObjectVocabularyBase):
                 fti_search(Specification, query),
                 self._exclude_blocked_query(),
             )
-            .order_by(self._order_by())
+            .order_by(self._order_search_by())
         )
 
     def __iter__(self):
@@ -198,17 +198,17 @@ class SpecificationDepCandidatesVocabulary(SQLObjectVocabularyBase):
         return self._is_valid_candidate(obj)
 
 
-class SpecificationDependenciesVocabulary(SQLObjectVocabularyBase):
+class SpecificationDependenciesVocabulary(StormVocabularyBase):
     """List specifications on which the current specification depends."""
 
     _table = Specification
-    _orderBy = "title"
+    _order_by = "title"
 
     @property
-    def _filter(self):
+    def _clauses(self):
         user = getattr(getUtility(ILaunchBag), "user", None)
-        return And(
-            SpecificationDependency.specificationID == self.context.id,
-            SpecificationDependency.dependencyID == Specification.id,
+        return [
+            SpecificationDependency.specification == self.context,
+            SpecificationDependency.dependency_id == Specification.id,
             *get_specification_privacy_filter(user),
-        )
+        ]
diff --git a/lib/lp/registry/browser/__init__.py b/lib/lp/registry/browser/__init__.py
index 403345b..5cad743 100644
--- a/lib/lp/registry/browser/__init__.py
+++ b/lib/lp/registry/browser/__init__.py
@@ -244,7 +244,9 @@ class RegistryDeleteViewMixin:
                 Store.of(bugtask).remove(nb.conjoined_primary)
             else:
                 nb.milestone = None
-        removeSecurityProxy(milestone.all_specifications).set(milestoneID=None)
+        removeSecurityProxy(milestone.all_specifications).set(
+            milestone_id=None
+        )
         getUtility(ISpecificationWorkItemSet).unlinkMilestone(milestone)
         self._deleteRelease(milestone.product_release)
         milestone.destroySelf()
diff --git a/lib/lp/registry/doc/person-account.rst b/lib/lp/registry/doc/person-account.rst
index 0ed70c6..f5e2424 100644
--- a/lib/lp/registry/doc/person-account.rst
+++ b/lib/lp/registry/doc/person-account.rst
@@ -76,7 +76,10 @@ will cause this spec to be reassigned.
 
     >>> from lp.blueprints.model.specification import Specification
     >>> from lp.registry.model.person import Person
-    >>> spec = Specification.selectFirst("assignee IS NULL", orderBy='id')
+    >>> from lp.services.database.interfaces import IStore
+    >>> spec = IStore(Specification).find(
+    ...     Specification, _assignee=None
+    ... ).order_by("id").first()
     >>> spec.assignee = foobar
 
     >>> for membership in foobar.team_memberships:
diff --git a/lib/lp/registry/doc/productseries.rst b/lib/lp/registry/doc/productseries.rst
index 18d0c9c..44d6516 100644
--- a/lib/lp/registry/doc/productseries.rst
+++ b/lib/lp/registry/doc/productseries.rst
@@ -201,25 +201,27 @@ is informational.
 We will create two specs for onezero and use them to demonstrate the
 filtering.
 
-    >>> from lp.services.database.constants import UTC_NOW
+    >>> from lp.blueprints.enums import SpecificationDefinitionStatus
+    >>> from lp.blueprints.interfaces.specification import ISpecificationSet
     >>> carlos = getUtility(IPersonSet).getByName('carlos')
-    >>> from lp.blueprints.model.specification import Specification
-    >>> a = Specification(name='a', title='A', summary='AA', owner=carlos,
-    ...                   product=firefox, productseries=onezero,
-    ...                   specurl='http://wbc.com/two', goal_proposer=carlos,
-    ...                   date_goal_proposed=UTC_NOW)
-    >>> b = Specification(name='b', title='b', summary='bb', owner=carlos,
-    ...                   product=firefox, productseries=onezero,
-    ...                   specurl='http://fds.com/adsf', goal_proposer=carlos,
-    ...                   date_goal_proposed=UTC_NOW)
+    >>> _ = login_person(carlos)
+    >>> a = getUtility(ISpecificationSet).new(
+    ...     name="a", title="A", specurl="http://wbc.com/two";, summary="AA",
+    ...     definition_status=SpecificationDefinitionStatus.NEW, owner=carlos,
+    ...     target=firefox,
+    ... )
+    >>> a.proposeGoal(onezero, carlos)
+    >>> b = getUtility(ISpecificationSet).new(
+    ...     name="b", title="b", specurl="http://fds.com/adsf";, summary="bb",
+    ...     definition_status=SpecificationDefinitionStatus.NEW, owner=carlos,
+    ...     target=firefox,
+    ... )
+    >>> b.proposeGoal(onezero, carlos)
 
 Now, we will make one of them accepted, the other declined, and both of
 them informational.
 
-    >>> from lp.blueprints.enums import (
-    ...     SpecificationDefinitionStatus,
-    ...     SpecificationImplementationStatus,
-    ...     )
+    >>> from lp.blueprints.enums import SpecificationImplementationStatus
     >>> a.definition_status = b.definition_status = (
     ...     SpecificationDefinitionStatus.APPROVED)
     >>> a.implementation_status = (
diff --git a/lib/lp/registry/model/distribution.py b/lib/lp/registry/model/distribution.py
index 3dd493e..d8ed65c 100644
--- a/lib/lp/registry/model/distribution.py
+++ b/lib/lp/registry/model/distribution.py
@@ -1404,7 +1404,7 @@ class Distribution(
           - informationalness: we will show ANY if nothing is said
 
         """
-        base_clauses = [Specification.distributionID == self.id]
+        base_clauses = [Specification.distribution == self]
         return search_specifications(
             self,
             base_clauses,
@@ -1419,7 +1419,11 @@ class Distribution(
 
     def getSpecification(self, name):
         """See `ISpecificationTarget`."""
-        return Specification.selectOneBy(distribution=self, name=name)
+        return (
+            IStore(Specification)
+            .find(Specification, distribution=self, name=name)
+            .one()
+        )
 
     def getAllowedSpecificationInformationTypes(self):
         """See `ISpecificationTarget`."""
diff --git a/lib/lp/registry/model/distroseries.py b/lib/lp/registry/model/distroseries.py
index c3f912d..41f4e9a 100644
--- a/lib/lp/registry/model/distroseries.py
+++ b/lib/lp/registry/model/distroseries.py
@@ -974,7 +974,7 @@ class DistroSeries(
           - informationalness: if nothing is said, ANY
 
         """
-        base_clauses = [Specification.distroseriesID == self.id]
+        base_clauses = [Specification.distroseries == self]
         return search_specifications(
             self,
             base_clauses,
diff --git a/lib/lp/registry/model/milestone.py b/lib/lp/registry/model/milestone.py
index 0171a61..f7b1abb 100644
--- a/lib/lp/registry/model/milestone.py
+++ b/lib/lp/registry/model/milestone.py
@@ -147,7 +147,7 @@ class MilestoneData:
         from lp.blueprints.model.specification import Specification
 
         return Store.of(self).find(
-            Specification, Specification.milestoneID == self.id
+            Specification, Specification.milestone == self
         )
 
     def getSpecifications(self, user):
@@ -166,7 +166,9 @@ class MilestoneData:
         product_origin, clauses = get_specification_active_product_filter(self)
         origin.extend(product_origin)
         clauses.extend(get_specification_privacy_filter(user))
-        origin.append(LeftJoin(Person, Specification._assigneeID == Person.id))
+        origin.append(
+            LeftJoin(Person, Specification._assignee_id == Person.id)
+        )
         milestones = self._milestone_ids_expr(user)
 
         results = (
@@ -180,7 +182,7 @@ class MilestoneData:
                             Specification.id,
                             tables=[Specification],
                             where=(
-                                Specification.milestoneID.is_in(milestones)
+                                Specification.milestone_id.is_in(milestones)
                             ),
                         ),
                         Select(
diff --git a/lib/lp/registry/model/person.py b/lib/lp/registry/model/person.py
index db8a777..9239372 100644
--- a/lib/lp/registry/model/person.py
+++ b/lib/lp/registry/model/person.py
@@ -1630,7 +1630,7 @@ class Person(
                         SpecificationWorkItem.specification_id.is_in(
                             Select(
                                 Specification.id,
-                                where=Specification._assigneeID.is_in(
+                                where=Specification._assignee_id.is_in(
                                     self.participant_ids
                                 ),
                             )
@@ -1670,7 +1670,7 @@ class Person(
                     Milestone,
                     Coalesce(
                         SpecificationWorkItem.milestone_id,
-                        Specification.milestoneID,
+                        Specification.milestone_id,
                     )
                     == Milestone.id,
                 ),
@@ -1697,17 +1697,17 @@ class Person(
             specs = bulk.load_related(
                 Specification, workitems, ["specification_id"]
             )
-            bulk.load_related(Product, specs, ["productID"])
-            bulk.load_related(Distribution, specs, ["distributionID"])
+            bulk.load_related(Product, specs, ["product_id"])
+            bulk.load_related(Distribution, specs, ["distribution_id"])
             assignee_ids = set(
                 [workitem.assignee_id for workitem in workitems]
-                + [spec._assigneeID for spec in specs]
+                + [spec._assignee_id for spec in specs]
             )
             assignee_ids.discard(None)
             bulk.load(Person, assignee_ids, store)
             milestone_ids = set(
                 [workitem.milestone_id for workitem in workitems]
-                + [spec.milestoneID for spec in specs]
+                + [spec.milestone_id for spec in specs]
             )
             milestone_ids.discard(None)
             bulk.load(Milestone, milestone_ids, store)
diff --git a/lib/lp/registry/model/product.py b/lib/lp/registry/model/product.py
index 357e5c4..3cae421 100644
--- a/lib/lp/registry/model/product.py
+++ b/lib/lp/registry/model/product.py
@@ -1418,7 +1418,7 @@ class Product(
         need_workitems=False,
     ):
         """See `IHasSpecifications`."""
-        base_clauses = [Specification.productID == self.id]
+        base_clauses = [Specification.product == self]
         return search_specifications(
             self,
             base_clauses,
@@ -1433,7 +1433,11 @@ class Product(
 
     def getSpecification(self, name):
         """See `ISpecificationTarget`."""
-        return Specification.selectOneBy(product=self, name=name)
+        return (
+            IStore(Specification)
+            .find(Specification, product=self, name=name)
+            .one()
+        )
 
     def getSeries(self, name):
         """See `IProduct`."""
diff --git a/lib/lp/registry/model/productseries.py b/lib/lp/registry/model/productseries.py
index 16cbb10..0edd632 100644
--- a/lib/lp/registry/model/productseries.py
+++ b/lib/lp/registry/model/productseries.py
@@ -348,7 +348,7 @@ class ProductSeries(
           - informational, which defaults to showing BOTH if nothing is said
 
         """
-        base_clauses = [Specification.productseriesID == self.id]
+        base_clauses = [Specification.productseries == self]
         return search_specifications(
             self,
             base_clauses,
@@ -365,7 +365,7 @@ class ProductSeries(
     @property
     def all_specifications(self):
         return Store.of(self).find(
-            Specification, Specification.productseriesID == self.id
+            Specification, Specification.productseries == self
         )
 
     def _customizeSearchParams(self, search_params):
diff --git a/lib/lp/registry/model/projectgroup.py b/lib/lp/registry/model/projectgroup.py
index b50b11c..0dda2e0 100644
--- a/lib/lp/registry/model/projectgroup.py
+++ b/lib/lp/registry/model/projectgroup.py
@@ -287,7 +287,7 @@ class ProjectGroup(
     ):
         """See `IHasSpecifications`."""
         base_clauses = [
-            Specification.productID == Product.id,
+            Specification.product_id == Product.id,
             Product.projectgroupID == self.id,
         ]
         tables = [Specification]
@@ -296,7 +296,7 @@ class ProjectGroup(
             tables.append(
                 Join(
                     ProductSeries,
-                    Specification.productseriesID == ProductSeries.id,
+                    Specification.productseries_id == ProductSeries.id,
                 )
             )
         return search_specifications(
diff --git a/lib/lp/registry/services/sharingservice.py b/lib/lp/registry/services/sharingservice.py
index 5548b4a..b628808 100644
--- a/lib/lp/registry/services/sharingservice.py
+++ b/lib/lp/registry/services/sharingservice.py
@@ -435,9 +435,9 @@ class SharingService:
                 AccessPolicy,
                 And(
                     Or(
-                        Specification.distributionID
+                        Specification.distribution_id
                         == AccessPolicy.distribution_id,
-                        Specification.productID == AccessPolicy.product_id,
+                        Specification.product_id == AccessPolicy.product_id,
                     ),
                     AccessPolicy.type == Specification.information_type,
                 ),
diff --git a/lib/lp/registry/tests/test_person.py b/lib/lp/registry/tests/test_person.py
index 7fa6c28..2fa9c5c 100644
--- a/lib/lp/registry/tests/test_person.py
+++ b/lib/lp/registry/tests/test_person.py
@@ -1027,7 +1027,7 @@ class TestPersonStates(TestCaseWithFactory):
             )
 
     def test_Specification_person_validator(self):
-        specification = Specification.select(limit=1)[0]
+        specification = IStore(Specification).find(Specification).first()
         for attr_name in [
             "assignee",
             "drafter",
diff --git a/lib/lp/registry/tests/test_sharingjob.py b/lib/lp/registry/tests/test_sharingjob.py
index 6c7c65a..e017e72 100644
--- a/lib/lp/registry/tests/test_sharingjob.py
+++ b/lib/lp/registry/tests/test_sharingjob.py
@@ -159,6 +159,7 @@ class SharingJobDerivedTestCase(TestCaseWithFactory):
     def test_repr_specifications(self):
         requestor = self.factory.makePerson()
         specification = self.factory.makeSpecification()
+        IStore(specification).flush()
         job = getUtility(IRemoveArtifactSubscriptionsJobSource).create(
             requestor, artifacts=[specification]
         )
diff --git a/lib/lp/scripts/harness.py b/lib/lp/scripts/harness.py
index c33077c..4bf8084 100644
--- a/lib/lp/scripts/harness.py
+++ b/lib/lp/scripts/harness.py
@@ -81,8 +81,8 @@ def _get_locals():
         proj = ProjectGroup.get(1)
         b2 = Bug.get(2)
         b1 = Bug.get(1)
-        s = Specification.get(1)
-        q = Question.get(1)
+        s = store.get(Specification, 1)
+        q = store.get(Question, 1)
         # Silence unused name warnings
         d, p, ds, prod, proj, b2, b1, s, q