Coverage for python/lsst/daf/relation/sql/_select.py: 43%
78 statements
« prev ^ index » next coverage.py v7.3.1, created at 2023-09-27 10:10 +0000
« prev ^ index » next coverage.py v7.3.1, created at 2023-09-27 10:10 +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 """
139 return bool(self.sort.terms)
141 @property
142 def has_projection(self) -> bool:
143 """Whether there is a `Projection` between `skip_to` and `target`
144 (`bool`).
145 """
146 return self.projection is not None
148 @property
149 def has_deduplication(self) -> bool:
150 """Whether there is a `Deduplication` between `skip_to` and `target`
151 (`bool`).
152 """
153 return self.deduplication is not None
155 @property
156 def has_slice(self) -> bool:
157 """Whether there is a `Slice` between `skip_to` and `target`
158 (`bool`).
159 """
160 return bool(self.slice.start) or self.slice.stop is not None
162 def reapply(self, target: Relation, payload: Any | None = None) -> Select:
163 # Docstring inherited.
164 if payload is not None:
165 raise EngineError("Select marker relations never have a payload.")
166 if target is self.target:
167 return self
168 result = target.engine.conform(target)
169 assert isinstance(
170 result, Select
171 ), "Guaranteed if a SQL engine, and Select should not appear in any other engine."
172 return result
174 @classmethod
175 def apply_skip(
176 cls,
177 skip_to: Relation,
178 sort: Sort | None = None,
179 projection: Projection | None = None,
180 deduplication: Deduplication | None = None,
181 slice: Slice | None = None,
182 ) -> Select:
183 """Wrap a relation in a `Select` and add all of the operations it
184 manages.
186 Parameters
187 ----------
188 skip_to : `Relation`
189 The relation to add the `Select` to, after first adding any
190 requested operations. This must not have any of the operation
191 types managed by the `Select` class unless they are immediately
192 upstream of another existing `Select`.
193 sort : `Sort`, optional
194 A sort to apply to `skip_to` and add to the new `Select`.
195 projection : `Projection`, optional
196 A projection to apply to `skip_to` and add to the new `Select`.
197 deduplication : `Deduplication`, optional
198 A deduplication to apply to `skip_to` and add to the new `Select`.
199 slice : `Slice`, optional
200 A slice to apply to `skip_to` and add to the new `Select`.
202 Returns
203 -------
204 select : `Select`
205 A relation tree terminated by a new `Select`.
206 """
207 target = skip_to
208 if sort is None:
209 sort = Sort()
210 if slice is None:
211 slice = Slice()
212 if sort.terms:
213 # In the relation tree, we need to apply the Sort before the
214 # Projection in case a SortTerm depends on a column that the
215 # Projection would drop.
216 target = sort._finish_apply(target)
217 if projection is not None:
218 target = projection._finish_apply(target)
219 if deduplication is not None:
220 target = deduplication._finish_apply(target)
221 if slice.start or slice.limit is not None:
222 target = slice._finish_apply(target)
223 is_compound = False
224 match skip_to:
225 case BinaryOperationRelation(operation=Chain()):
226 is_compound = True
227 return cls(
228 target=target,
229 projection=projection,
230 deduplication=deduplication,
231 sort=sort,
232 slice=slice,
233 skip_to=skip_to,
234 is_compound=is_compound,
235 )
237 def reapply_skip(
238 self,
239 skip_to: Relation | None = None,
240 after: UnaryOperation | None = None,
241 **kwargs: Any,
242 ) -> Select:
243 """Return a modified version of this `Select`.
245 Parameters
246 ----------
247 skip_to : `Relation`, optional
248 The relation to add the `Select` to, after first adding any
249 requested operations. This must not have any of the operation
250 types managed by the `Select` class unless they are immediately
251 upstream of another existing `Select`. If not provided,
252 ``self.skip_to`` is used.
253 after : `UnaryOperation`, optional
254 A unary operation to apply to ``skip_to`` before the operations
255 managed by `Select`. Must not be one of the operattion types
256 managed by `Select`.
257 **kwargs
258 Operations to include in the `Select`, forwarded to `apply_skip`.
259 Default is to apply the same operations already in ``self``, and
260 `None` can be passed to drop one of these operations.
262 Returns
263 -------
264 select : `Select`
265 Relation tree wrapped in a modified `Select`.
266 """
267 if skip_to is None:
268 skip_to = self.skip_to
269 if after is not None:
270 skip_to = after._finish_apply(skip_to)
271 if kwargs or skip_to is not self.skip_to:
272 return Select.apply_skip(
273 skip_to,
274 projection=kwargs.get("projection", self.projection),
275 deduplication=kwargs.get("deduplication", self.deduplication),
276 sort=kwargs.get("sort", self.sort),
277 slice=kwargs.get("slice", self.slice),
278 )
279 else:
280 return self
282 def strip(self) -> tuple[Relation, bool]:
283 """Remove the `Select` marker and any preceding `Projection` from a
284 relation if it has no other managed operations.
286 Returns
287 -------
288 relation : `Relation`
289 Upstream relation with the `Select`.
290 removed_projection : `bool`
291 Whether a `Projection` operation was also stripped.
292 """
293 if not self.has_deduplication and not self.has_sort and not self.has_slice:
294 return self.skip_to, self.has_projection
295 else:
296 return self, False