Coverage for python/lsst/daf/relation/sql/_select.py: 38%
71 statements
« prev ^ index » next coverage.py v6.5.0, created at 2023-01-07 02:01 -0800
« prev ^ index » next coverage.py v6.5.0, created at 2023-01-07 02:01 -0800
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 .._operations import Deduplication, Projection, Slice, Sort
32from .._relation import Relation
33from .._unary_operation import UnaryOperation
36@dataclasses.dataclass(frozen=True, kw_only=True)
37class Select(MarkerRelation):
38 """A marker operation used by a the SQL engine to group relation trees into
39 SELECT statements.
41 `Select` objects should not generally be added to relation trees by code
42 outside the SQL engine itself, except via the inherited `reapply`
43 interface. Use `Engine.conform` to insert `Select` markers into an
44 arbitrary relation tree (while reordering its operations accordingly).
46 Notes
47 -----
48 A conformed SQL relation tree always starts with a `Select` relation,
49 immediately followed by any projection, deduplication, sort, and slice (in
50 that order) that appear within the corresponding SQL ``SELECT`` statement.
51 These operations are held directly by the `Select` itself as attributes,
52 and the first upstream relation that isn't one of those operations is held
53 as the `skip_to` attribute. Nested `Select` instances correspond to sets
54 of relations that will appear within subqueries. This practice allows the
55 SQL engine to reduce subquery nesting and bring `Sort` operations
56 downstream into the outermost ``SELECT`` statement whenever possible, since
57 ``ORDER BY`` clauses in subqueries do not propagate to the order of the
58 outer statement.
60 The SQL engine's relation-processing algorithms typically traverse a tree
61 that starts with a `Select` by recursing to `skip_to` rather than `target`,
62 since the `Select` object's attributes also fully determine the operations
63 between `skip_to` and `target`. In this pattern, the `apply_skip` and
64 `reapply_skip` methods are used to add possibly-modified `Select` markers
65 after the upstream `skip_to` tree has been processed.
67 In contrast, general relation-processing algorithms that only see the
68 `Select` as an opaque `MarkerRelation` recurse via `target` and use
69 `reapply` to restore a possibly-modified `Select` marker.
71 The operations managed by the `Select` are always added in the same order,
72 which is consistent with the order the equivalent ``SELECT`` statement
73 would apply them:
75 1. `Sort`
76 2. `Projection`
77 3. `Deduplication`
78 4. `Slice`
80 Note that the `Projection` needs to follow the `Sort` in order to allow
81 the ``ORDER BY`` clause to reference columns that do not appear in the
82 ``SELECT`` clause (otherwise these operations would commute with each other
83 and the `Deduplication`).
84 """
86 sort: Sort
87 """The sort that will be applied by the ``SELECT`` statement via an
88 ``ORDER BY`` clause.
90 If `Sort.terms` is empty, no ``ORDER BY`` clause will be added, and this
91 `Sort` will not be applied to `skip_to` by `apply_skip` and other methods.
92 """
94 projection: Projection | None
95 """The projection that will be applied by the ``SELECT`` statement.
97 If `None` if all columns present in the `skip_to` relation should appear
98 in the ``SELECT`` clause.
99 """
101 deduplication: Deduplication | None
102 """The deduplication that will be applied by the ``SELECT`` statement.
104 If not `None`, this transforms a ``SELECT`` statement into a
105 ``SELECT DISTINCT``, or (if `skip_to` is a `Chain` relation) a
106 ``UNION ALL`` into a ``UNION``.
107 """
109 slice: Slice
110 """The slice that will be applied by the ``SELECT`` statement via
111 ``OFFSET`` and/or ``LIMIT`` clauses.
113 If `Slice.start` is zero, no ``OFFSET`` clause will be added. If
114 `Slice.stop` is `None`, no ``LIMIT`` clause will be added. If the `Slice`
115 does nothing at all, it will not be applied to `skip_to` by `apply_skip`
116 and similar methods.
117 """
119 skip_to: Relation
120 """The first relation upstream of this one that is not one of the
121 operations is holds.
122 """
124 def __str__(self) -> str:
125 return f"select({self.target})"
127 @property
128 def has_sort(self) -> bool:
129 """Whether there is a `Sort` between `skip_to` and `target`.
130 (`bool`)"""
131 return bool(self.sort.terms)
133 @property
134 def has_projection(self) -> bool:
135 """Whether there is a `Projection` between `skip_to` and `target`
136 (`bool`).
137 """
138 return self.projection is not None
140 @property
141 def has_deduplication(self) -> bool:
142 """Whether there is a `Deduplication` between `skip_to` and `target`
143 (`bool`).
144 """
145 return self.deduplication is not None
147 @property
148 def has_slice(self) -> bool:
149 """Whether there is a `Slice` between `skip_to` and `target`
150 (`bool`)."""
151 return bool(self.slice.start) or self.slice.stop is not None
153 def reapply(self, target: Relation, payload: Any | None = None) -> Select:
154 # Docstring inherited.
155 if payload is not None:
156 raise EngineError("Select marker relations never have a payload.")
157 if target is self.target:
158 return self
159 result = target.engine.conform(target)
160 assert isinstance(
161 result, Select
162 ), "Guaranteed if a SQL engine, and Select should not appear in any other engine."
163 return result
165 @classmethod
166 def apply_skip(
167 cls,
168 skip_to: Relation,
169 sort: Sort | None = None,
170 projection: Projection | None = None,
171 deduplication: Deduplication | None = None,
172 slice: Slice | None = None,
173 ) -> Select:
174 """Wrap a relation in a `Select` and add all of the operations it
175 manages.
177 Parameters
178 ----------
179 skip_to : `Relation`
180 The relation to add the `Select` to, after first adding any
181 requested operations. This must not have any of the operation
182 types managed by the `Select` class unless they are immediately
183 upstream of another existing `Select`.
184 sort : `Sort`, optional
185 A sort to apply to `skip_to` and add to the new `Select`.
186 projection : `Projection`, optional
187 A projection to apply to `skip_to` and add to the new `Select`.
188 deduplication : `Deduplication`, optional
189 A deduplication to apply to `skip_to` and add to the new `Select`.
190 slice : `Slice`, optional
191 A slice to apply to `skip_to` and add to the new `Select`.
193 Returns
194 -------
195 select : `Select`
196 A relation tree terminated by a new `Select`.
197 """
198 target = skip_to
199 if sort is None:
200 sort = Sort()
201 if slice is None:
202 slice = Slice()
203 if sort.terms:
204 # In the relation tree, we need to apply the Sort before the
205 # Projection in case a SortTerm depends on a column that the
206 # Projection would drop.
207 target = sort._finish_apply(target)
208 if projection is not None:
209 target = projection._finish_apply(target)
210 if deduplication is not None:
211 target = deduplication._finish_apply(target)
212 if slice.start or slice.limit is not None:
213 target = slice._finish_apply(target)
214 return cls(
215 target=target,
216 projection=projection,
217 deduplication=deduplication,
218 sort=sort,
219 slice=slice,
220 skip_to=skip_to,
221 )
223 def reapply_skip(
224 self,
225 skip_to: Relation | None = None,
226 after: UnaryOperation | None = None,
227 **kwargs: Any,
228 ) -> Select:
229 """Return a modified version of this `Select`.
231 Parameters
232 ----------
233 skip_to : `Relation`, optional
234 The relation to add the `Select` to, after first adding any
235 requested operations. This must not have any of the operation
236 types managed by the `Select` class unless they are immediately
237 upstream of another existing `Select`. If not provided,
238 ``self.skip_to`` is used.
239 after : `UnaryOperation`, optional
240 A unary operation to apply to ``skip_to`` before the operations
241 managed by `Select`. Must not be one of the operattion types
242 managed by `Select`.
243 **kwargs
244 Operations to include in the `Select`, forwarded to `apply_skip`.
245 Default is to apply the same operations already in ``self``, and
246 `None` can be passed to drop one of these operations.
248 Returns
249 -------
250 select : `Select`
251 Relation tree wrapped in a modified `Select`.
252 """
253 if skip_to is None:
254 skip_to = self.skip_to
255 if after is not None:
256 skip_to = after._finish_apply(skip_to)
257 if kwargs or skip_to is not self.skip_to:
258 return Select.apply_skip(
259 skip_to,
260 projection=kwargs.get("projection", self.projection),
261 deduplication=kwargs.get("deduplication", self.deduplication),
262 sort=kwargs.get("sort", self.sort),
263 slice=kwargs.get("slice", self.slice),
264 )
265 else:
266 return self
268 def strip(self) -> tuple[Relation, bool]:
269 """Remove the `Select` marker and any preceding `Projection` from a
270 relation if it has no other managed operations.
272 Returns
273 -------
274 relation : `Relation`
275 Upstream relation with the `Select`.
276 removed_projection : `bool`
277 Whether a `Projection` operation was also stripped.
278 """
279 if not self.has_deduplication and not self.has_sort and not self.has_slice:
280 return self.skip_to, self.has_projection
281 else:
282 return self, False