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

78 statements  

« prev     ^ index     » next       coverage.py v7.3.0, created at 2023-08-16 09:55 +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/>. 

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

139 return bool(self.sort.terms) 

140 

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 

147 

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 

154 

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 

161 

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 

173 

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. 

185 

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

201 

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 ) 

236 

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

244 

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. 

261 

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 

281 

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. 

285 

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