Coverage for python/lsst/daf/relation/sql/_select.py: 38%

78 statements  

« prev     ^ index     » next       coverage.py v6.5.0, created at 2023-04-06 02:37 -0700

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/>. 

21 

22from __future__ import annotations 

23 

24__all__ = ("Select",) 

25 

26import dataclasses 

27from typing import Any 

28 

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 

35 

36 

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. 

41 

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). 

46 

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. 

60 

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. 

67 

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. 

71 

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: 

75 

76 1. `Sort` 

77 2. `Projection` 

78 3. `Deduplication` 

79 4. `Slice` 

80 

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 """ 

86 

87 sort: Sort 

88 """The sort that will be applied by the ``SELECT`` statement via an 

89 ``ORDER BY`` clause. 

90 

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 """ 

94 

95 projection: Projection | None 

96 """The projection that will be applied by the ``SELECT`` statement. 

97 

98 If `None` if all columns present in the `skip_to` relation should appear 

99 in the ``SELECT`` clause. 

100 """ 

101 

102 deduplication: Deduplication | None 

103 """The deduplication that will be applied by the ``SELECT`` statement. 

104 

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 """ 

109 

110 slice: Slice 

111 """The slice that will be applied by the ``SELECT`` statement via 

112 ``OFFSET`` and/or ``LIMIT`` clauses. 

113 

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 """ 

119 

120 skip_to: Relation 

121 """The first relation upstream of this one that is not one of the 

122 operations is holds. 

123 """ 

124 

125 is_compound: bool 

126 """Whether this `Select` represents a SQL ``UNION`` or ``UNION ALL``. 

127 

128 This is `True` if and only if `skip_to` is a `.Chain` operation relation. 

129 """ 

130 

131 def __str__(self) -> str: 

132 return f"select({self.target})" 

133 

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) 

139 

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 

146 

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 

153 

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 

159 

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 

171 

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. 

183 

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`. 

199 

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 ) 

234 

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`. 

242 

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. 

259 

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 

279 

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. 

283 

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