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 10:05 +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 .._operations import Deduplication, Projection, Slice, Sort 

32from .._relation import Relation 

33from .._unary_operation import UnaryOperation 

34 

35 

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. 

40 

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

45 

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. 

59 

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. 

66 

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. 

70 

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: 

74 

75 1. `Sort` 

76 2. `Projection` 

77 3. `Deduplication` 

78 4. `Slice` 

79 

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

85 

86 sort: Sort 

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

88 ``ORDER BY`` clause. 

89 

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

93 

94 projection: Projection | None 

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

96 

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

98 in the ``SELECT`` clause. 

99 """ 

100 

101 deduplication: Deduplication | None 

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

103 

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

108 

109 slice: Slice 

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

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

112 

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

118 

119 skip_to: Relation 

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

121 operations is holds. 

122 """ 

123 

124 def __str__(self) -> str: 

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

126 

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) 

132 

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 

139 

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 

146 

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 

152 

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 

164 

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. 

176 

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

192 

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 ) 

222 

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

230 

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. 

247 

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 

267 

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. 

271 

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