← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] lp:~wgrant/launchpad/stormify-more-bug-search into lp:launchpad

 

William Grant has proposed merging lp:~wgrant/launchpad/stormify-more-bug-search into lp:launchpad.

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)

For more details, see:
https://code.launchpad.net/~wgrant/launchpad/stormify-more-bug-search/+merge/101713

This branch Stormifies most of the remaining bits of bug search. This allows the elimination of a fair bit of duplication, and paves the way for the BugTaskFlat feature flag. The only functional change should be the removal of the "if params.product" branch from _build_pending_bugwatch_elsewhere_clause, which was broken and unused anyway (pending_bugwatch is only linked for distributions).
-- 
https://code.launchpad.net/~wgrant/launchpad/stormify-more-bug-search/+merge/101713
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~wgrant/launchpad/stormify-more-bug-search into lp:launchpad.
=== modified file 'lib/lp/bugs/model/bugtask.py'
--- lib/lp/bugs/model/bugtask.py	2012-04-03 06:14:09 +0000
+++ lib/lp/bugs/model/bugtask.py	2012-04-12 10:29:36 +0000
@@ -1980,10 +1980,7 @@
 
         See `IBugTask.getBugCountsForPackages` for more information.
         """
-        from lp.bugs.model.bugtasksearch import (
-            get_bug_privacy_filter,
-            search_value_to_where_condition,
-            )
+        from lp.bugs.model.bugtasksearch import get_bug_privacy_filter
 
         packages = [
             package for package in packages
@@ -1992,23 +1989,23 @@
             package.sourcepackagename.id for package in packages]
 
         open_bugs_cond = (
-            'BugTask.status %s' % search_value_to_where_condition(
-                any(*DB_UNRESOLVED_BUGTASK_STATUSES)))
+            'BugTask.status IN %s' %
+            sqlvalues(DB_UNRESOLVED_BUGTASK_STATUSES))
 
         sum_template = "SUM(CASE WHEN %s THEN 1 ELSE 0 END) AS %s"
         sums = [
             sum_template % (open_bugs_cond, 'open_bugs'),
             sum_template % (
-                'BugTask.importance %s' % search_value_to_where_condition(
-                    BugTaskImportance.CRITICAL), 'open_critical_bugs'),
+                'BugTask.importance = %s' %
+                sqlvalues(BugTaskImportance.CRITICAL), 'open_critical_bugs'),
             sum_template % (
                 'BugTask.assignee IS NULL', 'open_unassigned_bugs'),
             sum_template % (
-                'BugTask.status %s' % search_value_to_where_condition(
-                    BugTaskStatus.INPROGRESS), 'open_inprogress_bugs'),
+                'BugTask.status = %s' %
+                sqlvalues(BugTaskStatus.INPROGRESS), 'open_inprogress_bugs'),
             sum_template % (
-                'BugTask.importance %s' % search_value_to_where_condition(
-                    BugTaskImportance.HIGH), 'open_high_bugs'),
+                'BugTask.importance = %s' %
+                sqlvalues(BugTaskImportance.HIGH), 'open_high_bugs'),
             ]
 
         conditions = [

=== modified file 'lib/lp/bugs/model/bugtasksearch.py'
--- lib/lp/bugs/model/bugtasksearch.py	2012-04-12 04:39:06 +0000
+++ lib/lp/bugs/model/bugtasksearch.py	2012-04-12 10:29:36 +0000
@@ -7,7 +7,6 @@
     'get_bug_privacy_filter',
     'orderby_expression',
     'search_bugs',
-    'search_value_to_where_condition',
     ]
 
 from operator import itemgetter
@@ -61,11 +60,13 @@
 from lp.bugs.model.bugnomination import BugNomination
 from lp.bugs.model.bugsubscription import BugSubscription
 from lp.bugs.model.bugtask import BugTask
+from lp.bugs.model.structuralsubscription import StructuralSubscription
 from lp.registry.interfaces.distribution import IDistribution
 from lp.registry.interfaces.distroseries import IDistroSeries
 from lp.registry.interfaces.milestone import IProjectGroupMilestone
 from lp.registry.interfaces.product import IProduct
 from lp.registry.interfaces.productseries import IProductSeries
+from lp.registry.model.distribution import Distribution
 from lp.registry.model.milestone import Milestone
 from lp.registry.model.milestonetag import MilestoneTag
 from lp.registry.model.person import Person
@@ -76,6 +77,10 @@
     quote,
     sqlvalues,
     )
+from lp.services.database.stormexpr import (
+    Array,
+    NullCount,
+    )
 from lp.services.propertycache import get_property_cache
 from lp.services.searchbuilder import (
     all,
@@ -167,42 +172,6 @@
     }
 
 
-def search_value_to_where_condition(search_value):
-    """Convert a search value to a string WHERE condition.
-
-        >>> search_value_to_where_condition(any(1, 2, 3))
-        'IN (1,2,3)'
-        >>> search_value_to_where_condition(any()) is None
-        True
-        >>> search_value_to_where_condition(not_equals('foo'))
-        "!= 'foo'"
-        >>> search_value_to_where_condition(greater_than('foo'))
-        "> 'foo'"
-        >>> search_value_to_where_condition(1)
-        '= 1'
-        >>> search_value_to_where_condition(NULL)
-        'IS NULL'
-
-    """
-    if zope_isinstance(search_value, any):
-        # When an any() clause is provided, the argument value
-        # is a list of acceptable filter values.
-        if not search_value.query_values:
-            return None
-        return "IN (%s)" % ",".join(sqlvalues(*search_value.query_values))
-    elif zope_isinstance(search_value, not_equals):
-        return "!= %s" % sqlvalues(search_value.value)
-    elif zope_isinstance(search_value, greater_than):
-        return "> %s" % sqlvalues(search_value.value)
-    elif search_value is not NULL:
-        return "= %s" % sqlvalues(search_value)
-    else:
-        # The argument value indicates we should match
-        # only NULL values for the column named by
-        # arg_name.
-        return "IS NULL"
-
-
 def search_value_to_storm_where_condition(comp, search_value):
     """Convert a search value to a Storm WHERE condition."""
     if zope_isinstance(search_value, any):
@@ -496,59 +465,70 @@
             '''ss as (SELECT * from StructuralSubscription
             WHERE StructuralSubscription.subscriber = %s)'''
             % sqlvalues(params.structural_subscriber))
+
+        class StructuralSubscriptionWith(StructuralSubscription):
+            __storm_table__ = 'ss'
+
         join_tables.append(
             (Product, LeftJoin(Product, And(
                             BugTask.productID == Product.id,
                             Product.active))))
-        join_tables.append(
-            (None,
-                LeftJoin(
-                SQL('ss ss1'),
-                BugTask.product == SQL('ss1.product'))))
-        join_tables.append(
-            (None,
-                LeftJoin(
-                SQL('ss ss2'),
-                BugTask.productseries == SQL('ss2.productseries'))))
-        join_tables.append(
-            (None,
-                LeftJoin(
-                SQL('ss ss3'),
-                Product.project == SQL('ss3.project'))))
-        join_tables.append(
-            (None,
-                LeftJoin(
-                SQL('ss ss4'),
-                And(BugTask.distribution == SQL('ss4.distribution'),
-                    Or(BugTask.sourcepackagename ==
-                        SQL('ss4.sourcepackagename'),
-                        SQL('ss4.sourcepackagename IS NULL'))))))
+        ProductSub = ClassAlias(StructuralSubscriptionWith)
+        join_tables.append((
+            ProductSub,
+            LeftJoin(
+                ProductSub,
+                BugTask.productID == ProductSub.productID)))
+        ProductSeriesSub = ClassAlias(StructuralSubscriptionWith)
+        join_tables.append((
+            ProductSeriesSub,
+            LeftJoin(
+                ProductSeriesSub,
+                BugTask.productseriesID == ProductSub.productseriesID)))
+        ProjectSub = ClassAlias(StructuralSubscriptionWith)
+        join_tables.append((
+            ProjectSub,
+            LeftJoin(
+                ProjectSub,
+                Product.projectID == ProjectSub.projectID)))
+        DistributionSub = ClassAlias(StructuralSubscriptionWith)
+        join_tables.append((
+            DistributionSub,
+            LeftJoin(
+                DistributionSub,
+                And(BugTask.distributionID == DistributionSub.distributionID,
+                    Or(
+                        DistributionSub.sourcepackagenameID ==
+                            BugTask.sourcepackagenameID,
+                        DistributionSub.sourcepackagenameID == None)))))
         if params.distroseries is not None:
             parent_distro_id = params.distroseries.distributionID
         else:
             parent_distro_id = 0
-        join_tables.append(
-            (None,
-                LeftJoin(
-                SQL('ss ss5'),
-                Or(BugTask.distroseries == SQL('ss5.distroseries'),
+        DistroSeriesSub = ClassAlias(StructuralSubscriptionWith)
+        join_tables.append((
+            DistroSeriesSub,
+            LeftJoin(
+                DistroSeriesSub,
+                Or(BugTask.distroseriesID == DistroSeriesSub.distroseriesID,
                     # There is a mismatch between BugTask and
                     # StructuralSubscription. SS does not support
                     # distroseries. This clause works because other
                     # joins ensure the match bugtask is the right
                     # series.
-                    And(parent_distro_id == SQL('ss5.distribution'),
-                        BugTask.sourcepackagename == SQL(
-                            'ss5.sourcepackagename'))))))
-        join_tables.append(
-            (None,
-                LeftJoin(
-                SQL('ss ss6'),
-                BugTask.milestone == SQL('ss6.milestone'))))
+                    And(parent_distro_id == DistroSeriesSub.distributionID,
+                        BugTask.sourcepackagenameID ==
+                            DistroSeriesSub.sourcepackagenameID)))))
+        MilestoneSub = ClassAlias(StructuralSubscriptionWith)
+        join_tables.append((
+            MilestoneSub,
+            LeftJoin(
+                MilestoneSub,
+                BugTask.milestoneID == MilestoneSub.milestoneID)))
         extra_clauses.append(
-            "NULL_COUNT("
-            "ARRAY[ss1.id, ss2.id, ss3.id, ss4.id, ss5.id, ss6.id]"
-            ") < 6")
+            NullCount(Array(
+                ProductSub.id, ProductSeriesSub.id, ProjectSub.id,
+                DistributionSub.id, DistroSeriesSub.id, MilestoneSub.id)) < 6)
         has_duplicate_results = True
 
     # Remove bugtasks from deactivated products, if necessary.
@@ -703,7 +683,7 @@
         if branches is not None:
             where.append(
                 search_value_to_storm_where_condition(
-                    BugBranch.branchID, params.linked_branches))
+                    BugBranch.branchID, branches))
         return Exists(Select(1, tables=[BugBranch], where=And(*where)))
 
     if zope_isinstance(params.linked_branches, BaseItem):
@@ -848,20 +828,18 @@
     if fast:
         assert params.searchtext is None, (
             'Cannot use searchtext at the same time as fast_searchtext.')
-        searchtext_quoted = quote(params.fast_searchtext)
+        searchtext = params.fast_searchtext
     else:
         assert params.fast_searchtext is None, (
             'Cannot use fast_searchtext at the same time as searchtext.')
-        searchtext_quoted = quote(params.searchtext)
+        searchtext = params.searchtext
 
     if params.orderby is None:
         # Unordered search results aren't useful, so sort by relevance
         # instead.
-        params.orderby = [
-            SQLConstant("-rank(Bug.fti, ftq(%s))" % searchtext_quoted),
-            ]
+        params.orderby = [SQL("-rank(Bug.fti, ftq(?))", params=(searchtext,))]
 
-    return "Bug.fti @@ ftq(%s)" % searchtext_quoted
+    return SQL("Bug.fti @@ ftq(?)", params=(searchtext,))
 
 
 def _build_status_clause(col, status):
@@ -911,7 +889,7 @@
     # Perform a LEFT JOIN to the conjoined master bugtask.  If the
     # conjoined master is not null, it gets filtered out.
     ConjoinedMaster = ClassAlias(BugTask, 'ConjoinedMaster')
-    extra_clauses = ["ConjoinedMaster.id IS NULL"]
+    extra_clauses = [ConjoinedMaster.id == None]
     if milestone.distribution is not None:
         current_series = milestone.distribution.currentseries
         join = LeftJoin(
@@ -1031,109 +1009,57 @@
 def _build_blueprint_related_clause(params):
     """Find bugs related to Blueprints, or not."""
     linked_blueprints = params.linked_blueprints
+
+    def make_clause(blueprints=None):
+        where = [SpecificationBug.bugID == Bug.id]
+        if blueprints is not None:
+            where.append(
+                search_value_to_storm_where_condition(
+                    SpecificationBug.specificationID, blueprints))
+        return Exists(Select(1, tables=[SpecificationBug], where=And(*where)))
+
     if linked_blueprints is None:
         return None
     elif zope_isinstance(linked_blueprints, BaseItem):
         if linked_blueprints == BugBlueprintSearch.BUGS_WITH_BLUEPRINTS:
-            return "EXISTS (%s)" % (
-                "SELECT 1 FROM SpecificationBug"
-                " WHERE SpecificationBug.bug = Bug.id")
+            return make_clause()
         elif (linked_blueprints ==
                 BugBlueprintSearch.BUGS_WITHOUT_BLUEPRINTS):
-            return "NOT EXISTS (%s)" % (
-                "SELECT 1 FROM SpecificationBug"
-                " WHERE SpecificationBug.bug = Bug.id")
+            return Not(make_clause())
     else:
         # A specific search term has been supplied.
-        return """EXISTS (
-                SELECT TRUE FROM SpecificationBug
-                WHERE SpecificationBug.bug=Bug.id AND
-                SpecificationBug.specification %s)
-            """ % search_value_to_where_condition(linked_blueprints)
+        return make_clause(linked_blueprints)
 
 
 # Upstream task restrictions
 
-_open_resolved_upstream = """
-    EXISTS (
-        SELECT TRUE FROM BugTask AS RelatedBugTask
-        WHERE RelatedBugTask.bug = BugTask.bug
-            AND RelatedBugTask.id != BugTask.id
-            AND ((
-                RelatedBugTask.bugwatch IS NOT NULL AND
-                RelatedBugTask.status %s)
-                OR (
-                RelatedBugTask.product IS NOT NULL AND
-                RelatedBugTask.bugwatch IS NULL AND
-                RelatedBugTask.status %s))
-        )
-    """
-
-_open_resolved_upstream_with_target = """
-    EXISTS (
-        SELECT TRUE FROM BugTask AS RelatedBugTask
-        WHERE RelatedBugTask.bug = BugTask.bug
-            AND RelatedBugTask.id != BugTask.id
-            AND ((
-                RelatedBugTask.%(target_column)s = %(target_id)s AND
-                RelatedBugTask.bugwatch IS NOT NULL AND
-                RelatedBugTask.status %(status_with_watch)s)
-                OR (
-                RelatedBugTask.%(target_column)s = %(target_id)s AND
-                RelatedBugTask.bugwatch IS NULL AND
-                RelatedBugTask.status %(status_without_watch)s))
-        )
-    """
-
-
 def _build_pending_bugwatch_elsewhere_clause(params):
     """Return a clause for BugTaskSearchParams.pending_bugwatch_elsewhere
     """
-    if params.product:
-        # Include only bugtasks that do no have bug watches that
-        # belong to a product that does not use Malone.
-        return """
-            EXISTS (
-                SELECT TRUE
-                FROM BugTask AS RelatedBugTask
-                    LEFT OUTER JOIN Product AS OtherProduct
-                        ON RelatedBugTask.product = OtherProduct.id
-                WHERE RelatedBugTask.bug = BugTask.bug
-                    AND RelatedBugTask.id = BugTask.id
-                    AND RelatedBugTask.bugwatch IS NULL
-                    AND OtherProduct.official_malone IS FALSE
-                    AND RelatedBugTask.status != %s)
-            """ % sqlvalues(BugTaskStatus.INVALID)
-    elif params.upstream_target is None:
-        # Include only bugtasks that have other bugtasks on targets
-        # not using Malone, which are not Invalid, and have no bug
-        # watch.
-        return """
-            EXISTS (
-                SELECT TRUE
-                FROM BugTask AS RelatedBugTask
-                    LEFT OUTER JOIN Distribution AS OtherDistribution
-                        ON RelatedBugTask.distribution =
-                            OtherDistribution.id
-                    LEFT OUTER JOIN Product AS OtherProduct
-                        ON RelatedBugTask.product = OtherProduct.id
-                WHERE RelatedBugTask.bug = BugTask.bug
-                    AND RelatedBugTask.id != BugTask.id
-                    AND RelatedBugTask.bugwatch IS NULL
-                    AND (
-                        OtherDistribution.official_malone IS FALSE
-                        OR OtherProduct.official_malone IS FALSE)
-                    AND RelatedBugTask.status != %s)
-            """ % sqlvalues(BugTaskStatus.INVALID)
+    RelatedBugTask = ClassAlias(BugTask)
+    extra_joins = []
+    if params.upstream_target is None:
+        # Restrict the target to distributions or products which don't
+        # use Launchpad for bug tracking.
+        OtherDistribution = ClassAlias(Distribution)
+        OtherProduct = ClassAlias(Product)
+        extra_joins = [
+            LeftJoin(
+                OtherDistribution,
+                OtherDistribution.id == RelatedBugTask.distributionID),
+            LeftJoin(
+                OtherProduct,
+                OtherProduct.id == RelatedBugTask.productID),
+            ]
+        target_clause = Or(
+            OtherDistribution.official_malone == False,
+            OtherProduct.official_malone == False)
     else:
-        # Include only bugtasks that have other bugtasks on
-        # params.upstream_target, but only if this this product
-        # does not use Malone and if the bugtasks are not Invalid,
-        # and have no bug watch.
+        # Restrict the target to params.upstream_target.
         if IProduct.providedBy(params.upstream_target):
-            target_clause = 'RelatedBugTask.product = %s'
+            target_col = RelatedBugTask.productID
         elif IDistribution.providedBy(params.upstream_target):
-            target_clause = 'RelatedBugTask.distribution = %s'
+            target_col = RelatedBugTask.distributionID
         else:
             raise AssertionError(
                 'params.upstream_target must be a Distribution or '
@@ -1141,51 +1067,37 @@
         # There is no point to construct a real sub-select if we
         # already know that the result will be empty.
         if params.upstream_target.official_malone:
-            return 'false'
-        target_clause = target_clause % sqlvalues(
-            params.upstream_target.id)
-        return """
-            EXISTS (
-                SELECT TRUE
-                FROM BugTask AS RelatedBugTask
-                WHERE RelatedBugTask.bug = BugTask.bug
-                    AND RelatedBugTask.id != BugTask.id
-                    AND RelatedBugTask.bugwatch IS NULL
-                    AND %s
-                    AND RelatedBugTask.status != %s)
-            """ % (target_clause, sqlvalues(BugTaskStatus.INVALID)[0])
+            return False
+        target_clause = target_col == params.upstream_target.id
+    # Include only bugtasks that have other bugtasks on matching
+    # targets which are not Invalid and have no bug watch.
+    return Exists(Select(
+        1,
+        tables=[RelatedBugTask] + extra_joins,
+        where=And(
+            RelatedBugTask.bugID == BugTask.bugID,
+            RelatedBugTask.id != BugTask.id,
+            RelatedBugTask.bugwatchID == None,
+            RelatedBugTask.status != BugTaskStatus.INVALID,
+            target_clause)))
 
 
 def _build_no_upstream_bugtask_clause(params):
     """Return a clause for BugTaskSearchParams.has_no_upstream_bugtask."""
+    OtherBugTask = ClassAlias(BugTask)
     if params.upstream_target is None:
-        # Find all bugs that has no product bugtask. We limit the
-        # SELECT by matching against BugTask.bug to make the query
-        # faster.
-        return """
-            NOT EXISTS (SELECT TRUE
-                        FROM BugTask AS OtherBugTask
-                        WHERE OtherBugTask.bug = BugTask.bug
-                            AND OtherBugTask.product IS NOT NULL)
-        """
+        target = OtherBugTask.productID != None
     elif IProduct.providedBy(params.upstream_target):
-        return """
-            NOT EXISTS (SELECT TRUE
-                        FROM BugTask AS OtherBugTask
-                        WHERE OtherBugTask.bug = BugTask.bug
-                            AND OtherBugTask.product=%s)
-        """ % sqlvalues(params.upstream_target.id)
+        target = OtherBugTask.productID == params.upstream_target.id
     elif IDistribution.providedBy(params.upstream_target):
-        return """
-            NOT EXISTS (SELECT TRUE
-                        FROM BugTask AS OtherBugTask
-                        WHERE OtherBugTask.bug = BugTask.bug
-                            AND OtherBugTask.distribution=%s)
-        """ % sqlvalues(params.upstream_target.id)
+        target = OtherBugTask.distributionID == params.upstream_target.id
     else:
         raise AssertionError(
             'params.upstream_target must be a Distribution or '
             'a Product')
+    return Not(Exists(Select(
+        1, tables=[OtherBugTask],
+        where=And(OtherBugTask.bugID == BugTask.bugID, target))))
 
 
 def _build_open_or_resolved_upstream_clause(params,
@@ -1193,26 +1105,38 @@
                                       statuses_for_upstream_tasks):
     """Return a clause for BugTaskSearchParams.open_upstream or
     BugTaskSearchParams.resolved_upstream."""
+    RelatedBugTask = ClassAlias(BugTask)
+    watch_status_clause = search_value_to_storm_where_condition(
+        RelatedBugTask._status, any(*statuses_for_watch_tasks))
+    no_watch_status_clause = search_value_to_storm_where_condition(
+        RelatedBugTask._status, any(*statuses_for_upstream_tasks))
     if params.upstream_target is None:
-        return _open_resolved_upstream % (
-                search_value_to_where_condition(
-                    any(*statuses_for_watch_tasks)),
-                search_value_to_where_condition(
-                    any(*statuses_for_upstream_tasks)))
-    elif IProduct.providedBy(params.upstream_target):
-        query_values = {'target_column': 'product'}
-    elif IDistribution.providedBy(params.upstream_target):
-        query_values = {'target_column': 'distribution'}
+        watch_target_clause = True
+        no_watch_target_clause = RelatedBugTask.productID != None
     else:
-        raise AssertionError(
-            'params.upstream_target must be a Distribution or '
-            'a Product')
-    query_values['target_id'] = sqlvalues(params.upstream_target.id)[0]
-    query_values['status_with_watch'] = search_value_to_where_condition(
-        any(*statuses_for_watch_tasks))
-    query_values['status_without_watch'] = search_value_to_where_condition(
-        any(*statuses_for_upstream_tasks))
-    return _open_resolved_upstream_with_target % query_values
+        if IProduct.providedBy(params.upstream_target):
+            target_col = RelatedBugTask.productID
+        elif IDistribution.providedBy(params.upstream_target):
+            target_col = RelatedBugTask.distributionID
+        else:
+            raise AssertionError(
+                'params.upstream_target must be a Distribution or '
+                'a Product')
+        watch_target_clause = no_watch_target_clause = (
+            target_col == params.upstream_target.id)
+    return Exists(Select(
+        1,
+        tables=[RelatedBugTask],
+        where=And(
+            RelatedBugTask.bugID == BugTask.bugID,
+            RelatedBugTask.id != BugTask.id,
+            Or(
+                And(watch_target_clause,
+                    RelatedBugTask.bugwatchID != None,
+                    watch_status_clause),
+                And(no_watch_target_clause,
+                    RelatedBugTask.bugwatchID == None,
+                    no_watch_status_clause)))))
 
 
 def _build_open_upstream_clause(params):
@@ -1272,8 +1196,7 @@
         upstream_clauses.append(_build_open_upstream_clause(params))
 
     if upstream_clauses:
-        upstream_clause = " OR ".join(upstream_clauses)
-        return '(%s)' % upstream_clause
+        return Or(*upstream_clauses)
     return None
 
 

=== modified file 'lib/lp/services/database/stormexpr.py'
--- lib/lp/services/database/stormexpr.py	2012-02-20 12:26:57 +0000
+++ lib/lp/services/database/stormexpr.py	2012-04-12 10:29:36 +0000
@@ -3,13 +3,16 @@
 
 __metaclass__ = type
 __all__ = [
+    'Array',
     'Concatenate',
     'CountDistinct',
     'Greatest',
+    'NullCount',
     ]
 
 from storm.expr import (
     BinaryOper,
+    ComparableExpr,
     compile,
     EXPR,
     Expr,
@@ -48,3 +51,23 @@
     """Storm operator for string concatenation."""
     __slots__ = ()
     oper = " || "
+
+
+class NullCount(NamedFunc):
+    __slots__ = ()
+    name = "NULL_COUNT"
+
+
+class Array(ComparableExpr):
+    __slots__ = ("args",)
+
+    def __init__(self, *args):
+        self.args = args
+
+
+@compile.when(Array)
+def compile_array(compile, array, state):
+    state.push("context", EXPR)
+    args = compile(array.args, state)
+    state.pop()
+    return "ARRAY[%s]" % args