Coverage for python/lsst/daf/relation/sql/_select.py: 38%
78 statements
« prev ^ index » next coverage.py v7.2.3, created at 2023-04-26 13:07 +0000
« prev ^ index » next coverage.py v7.2.3, created at 2023-04-26 13:07 +0000
1# This file is part of daf_relation.
2#
3# Developed for the LSST Data Management System.
4# This product includes software developed by the LSST Project
5# (https://www.lsst.org).
6# See the COPYRIGHT file at the top-level directory of this distribution
7# for details of code ownership.
8#
9# This program is free software: you can redistribute it and/or modify
10# it under the terms of the GNU General Public License as published by
11# the Free Software Foundation, either version 3 of the License, or
12# (at your option) any later version.
13#
14# This program is distributed in the hope that it will be useful,
15# but WITHOUT ANY WARRANTY; without even the implied warranty of
16# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17# GNU General Public License for more details.
18#
19# You should have received a copy of the GNU General Public License
20# along with this program. If not, see <https://www.gnu.org/licenses/>.
22from __future__ import annotations
24__all__ = ("Select",)
26import dataclasses
27from typing import Any
29from .._exceptions import EngineError
30from .._marker_relation import MarkerRelation
31from .._operation_relations import BinaryOperationRelation
32from .._operations import Chain, Deduplication, Projection, Slice, Sort
33from .._relation import Relation
34from .._unary_operation import UnaryOperation
37@dataclasses.dataclass(frozen=True, kw_only=True)
38class Select(MarkerRelation):
39 """A marker operation used by a the SQL engine to group relation trees into
40 SELECT statements.
42 `Select` objects should not generally be added to relation trees by code
43 outside the SQL engine itself, except via the inherited `reapply`
44 interface. Use `Engine.conform` to insert `Select` markers into an
45 arbitrary relation tree (while reordering its operations accordingly).
47 Notes
48 -----
49 A conformed SQL relation tree always starts with a `Select` relation,
50 immediately followed by any projection, deduplication, sort, and slice (in
51 that order) that appear within the corresponding SQL ``SELECT`` statement.
52 These operations are held directly by the `Select` itself as attributes,
53 and the first upstream relation that isn't one of those operations is held
54 as the `skip_to` attribute. Nested `Select` instances correspond to sets
55 of relations that will appear within subqueries. This practice allows the
56 SQL engine to reduce subquery nesting and bring `Sort` operations
57 downstream into the outermost ``SELECT`` statement whenever possible, since
58 ``ORDER BY`` clauses in subqueries do not propagate to the order of the
59 outer statement.
61 The SQL engine's relation-processing algorithms typically traverse a tree
62 that starts with a `Select` by recursing to `skip_to` rather than `target`,
63 since the `Select` object's attributes also fully determine the operations
64 between `skip_to` and `target`. In this pattern, the `apply_skip` and
65 `reapply_skip` methods are used to add possibly-modified `Select` markers
66 after the upstream `skip_to` tree has been processed.
68 In contrast, general relation-processing algorithms that only see the
69 `Select` as an opaque `MarkerRelation` recurse via `target` and use
70 `reapply` to restore a possibly-modified `Select` marker.
72 The operations managed by the `Select` are always added in the same order,
73 which is consistent with the order the equivalent ``SELECT`` statement
74 would apply them:
76 1. `Sort`
77 2. `Projection`
78 3. `Deduplication`
79 4. `Slice`
81 Note that the `Projection` needs to follow the `Sort` in order to allow
82 the ``ORDER BY`` clause to reference columns that do not appear in the
83 ``SELECT`` clause (otherwise these operations would commute with each other
84 and the `Deduplication`).
85 """
87 sort: Sort
88 """The sort that will be applied by the ``SELECT`` statement via an
89 ``ORDER BY`` clause.
91 If `Sort.terms` is empty, no ``ORDER BY`` clause will be added, and this
92 `Sort` will not be applied to `skip_to` by `apply_skip` and other methods.
93 """
95 projection: Projection | None
96 """The projection that will be applied by the ``SELECT`` statement.
98 If `None` if all columns present in the `skip_to` relation should appear
99 in the ``SELECT`` clause.
100 """
102 deduplication: Deduplication | None
103 """The deduplication that will be applied by the ``SELECT`` statement.
105 If not `None`, this transforms a ``SELECT`` statement into a
106 ``SELECT DISTINCT``, or (if `skip_to` is a `Chain` relation) a
107 ``UNION ALL`` into a ``UNION``.
108 """
110 slice: Slice
111 """The slice that will be applied by the ``SELECT`` statement via
112 ``OFFSET`` and/or ``LIMIT`` clauses.
114 If `Slice.start` is zero, no ``OFFSET`` clause will be added. If
115 `Slice.stop` is `None`, no ``LIMIT`` clause will be added. If the `Slice`
116 does nothing at all, it will not be applied to `skip_to` by `apply_skip`
117 and similar methods.
118 """
120 skip_to: Relation
121 """The first relation upstream of this one that is not one of the
122 operations is holds.
123 """
125 is_compound: bool
126 """Whether this `Select` represents a SQL ``UNION`` or ``UNION ALL``.
128 This is `True` if and only if `skip_to` is a `.Chain` operation relation.
129 """
131 def __str__(self) -> str:
132 return f"select({self.target})"
134 @property
135 def has_sort(self) -> bool:
136 """Whether there is a `Sort` between `skip_to` and `target`.
137 (`bool`)"""
138 return bool(self.sort.terms)
140 @property
141 def has_projection(self) -> bool:
142 """Whether there is a `Projection` between `skip_to` and `target`
143 (`bool`).
144 """
145 return self.projection is not None
147 @property
148 def has_deduplication(self) -> bool:
149 """Whether there is a `Deduplication` between `skip_to` and `target`
150 (`bool`).
151 """
152 return self.deduplication is not None
154 @property
155 def has_slice(self) -> bool:
156 """Whether there is a `Slice` between `skip_to` and `target`
157 (`bool`)."""
158 return bool(self.slice.start) or self.slice.stop is not None
160 def reapply(self, target: Relation, payload: Any | None = None) -> Select:
161 # Docstring inherited.
162 if payload is not None:
163 raise EngineError("Select marker relations never have a payload.")
164 if target is self.target:
165 return self
166 result = target.engine.conform(target)
167 assert isinstance(
168 result, Select
169 ), "Guaranteed if a SQL engine, and Select should not appear in any other engine."
170 return result
172 @classmethod
173 def apply_skip(
174 cls,
175 skip_to: Relation,
176 sort: Sort | None = None,
177 projection: Projection | None = None,
178 deduplication: Deduplication | None = None,
179 slice: Slice | None = None,
180 ) -> Select:
181 """Wrap a relation in a `Select` and add all of the operations it
182 manages.
184 Parameters
185 ----------
186 skip_to : `Relation`
187 The relation to add the `Select` to, after first adding any
188 requested operations. This must not have any of the operation
189 types managed by the `Select` class unless they are immediately
190 upstream of another existing `Select`.
191 sort : `Sort`, optional
192 A sort to apply to `skip_to` and add to the new `Select`.
193 projection : `Projection`, optional
194 A projection to apply to `skip_to` and add to the new `Select`.
195 deduplication : `Deduplication`, optional
196 A deduplication to apply to `skip_to` and add to the new `Select`.
197 slice : `Slice`, optional
198 A slice to apply to `skip_to` and add to the new `Select`.
200 Returns
201 -------
202 select : `Select`
203 A relation tree terminated by a new `Select`.
204 """
205 target = skip_to
206 if sort is None:
207 sort = Sort()
208 if slice is None:
209 slice = Slice()
210 if sort.terms:
211 # In the relation tree, we need to apply the Sort before the
212 # Projection in case a SortTerm depends on a column that the
213 # Projection would drop.
214 target = sort._finish_apply(target)
215 if projection is not None:
216 target = projection._finish_apply(target)
217 if deduplication is not None:
218 target = deduplication._finish_apply(target)
219 if slice.start or slice.limit is not None:
220 target = slice._finish_apply(target)
221 is_compound = False
222 match skip_to:
223 case BinaryOperationRelation(operation=Chain()):
224 is_compound = True
225 return cls(
226 target=target,
227 projection=projection,
228 deduplication=deduplication,
229 sort=sort,
230 slice=slice,
231 skip_to=skip_to,
232 is_compound=is_compound,
233 )
235 def reapply_skip(
236 self,
237 skip_to: Relation | None = None,
238 after: UnaryOperation | None = None,
239 **kwargs: Any,
240 ) -> Select:
241 """Return a modified version of this `Select`.
243 Parameters
244 ----------
245 skip_to : `Relation`, optional
246 The relation to add the `Select` to, after first adding any
247 requested operations. This must not have any of the operation
248 types managed by the `Select` class unless they are immediately
249 upstream of another existing `Select`. If not provided,
250 ``self.skip_to`` is used.
251 after : `UnaryOperation`, optional
252 A unary operation to apply to ``skip_to`` before the operations
253 managed by `Select`. Must not be one of the operattion types
254 managed by `Select`.
255 **kwargs
256 Operations to include in the `Select`, forwarded to `apply_skip`.
257 Default is to apply the same operations already in ``self``, and
258 `None` can be passed to drop one of these operations.
260 Returns
261 -------
262 select : `Select`
263 Relation tree wrapped in a modified `Select`.
264 """
265 if skip_to is None:
266 skip_to = self.skip_to
267 if after is not None:
268 skip_to = after._finish_apply(skip_to)
269 if kwargs or skip_to is not self.skip_to:
270 return Select.apply_skip(
271 skip_to,
272 projection=kwargs.get("projection", self.projection),
273 deduplication=kwargs.get("deduplication", self.deduplication),
274 sort=kwargs.get("sort", self.sort),
275 slice=kwargs.get("slice", self.slice),
276 )
277 else:
278 return self
280 def strip(self) -> tuple[Relation, bool]:
281 """Remove the `Select` marker and any preceding `Projection` from a
282 relation if it has no other managed operations.
284 Returns
285 -------
286 relation : `Relation`
287 Upstream relation with the `Select`.
288 removed_projection : `bool`
289 Whether a `Projection` operation was also stripped.
290 """
291 if not self.has_deduplication and not self.has_sort and not self.has_slice:
292 return self.skip_to, self.has_projection
293 else:
294 return self, False