Coverage for tests/test_sql_engine.py: 11%

143 statements  

« prev     ^ index     » next       coverage.py v6.5.0, created at 2023-01-13 10:31 +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# (http://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 <http://www.gnu.org/licenses/>. 

21 

22from __future__ import annotations 

23 

24import unittest 

25from typing import TypeAlias 

26 

27import sqlalchemy 

28from lsst.daf.relation import ( 

29 ColumnExpression, 

30 ColumnTag, 

31 Identity, 

32 IgnoreOne, 

33 Relation, 

34 RelationalAlgebraError, 

35 SortTerm, 

36 sql, 

37 tests, 

38) 

39 

40_L: TypeAlias = sqlalchemy.sql.ColumnElement 

41 

42 

43def _make_leaf( 

44 engine: sql.Engine, md: sqlalchemy.schema.MetaData, name: str, *columns: ColumnTag 

45) -> Relation: 

46 """Make a leaf relation in a SQL engine, backed by SQLAlchemy tables. 

47 

48 This doesn't actually create tables in any database (even in memory); it 

49 just creates their SQLAlchemy representations so their names and columns 

50 render as expected in SQL strings. 

51 

52 Parameters 

53 ---------- 

54 engine : `lsst.daf.relation.sql.Engine` 

55 Relation engine for the new leaf. 

56 md : `sqlalchemy.schema.MetaData` 

57 SQLAlchemy metadata object to add tables to. 

58 name : `str` 

59 Name of the relation and its table. 

60 *columns: `ColumnTag` 

61 Columns to include in the relation and its table. 

62 

63 Returns 

64 ------- 

65 leaf : `Relation` 

66 Leaf relation backed by the new table. 

67 """ 

68 columns_available = { 

69 tag: sqlalchemy.schema.Column(tag.qualified_name, sqlalchemy.Integer) for tag in columns 

70 } 

71 table = sqlalchemy.schema.Table(name, md, *columns_available.values()) 

72 payload = sql.Payload[_L](from_clause=table, columns_available=columns_available) 

73 return engine.make_leaf(columns_available.keys(), payload=payload, name=name) 

74 

75 

76class SqlEngineTestCase(tests.RelationTestCase): 

77 def setUp(self): 

78 self.maxDiff = None 

79 

80 def test_select_operations(self) -> None: 

81 """Test SQL engine conversion for different combinations and 

82 permutations of the operation types managed by the `Select` marker 

83 relation. 

84 """ 

85 engine = sql.Engine[_L]() 

86 md = sqlalchemy.schema.MetaData() 

87 a = tests.ColumnTag("a") 

88 b = tests.ColumnTag("b") 

89 c = tests.ColumnTag("c") 

90 d = tests.ColumnTag("d") 

91 expression = ColumnExpression.reference(b).method("__neg__") 

92 predicate = ColumnExpression.reference(c).gt(ColumnExpression.literal(0)) 

93 terms = [SortTerm(ColumnExpression.reference(b))] 

94 leaf1 = _make_leaf(engine, md, "leaf1", a, b) 

95 leaf2 = _make_leaf(engine, md, "leaf2", a, c) 

96 r = leaf1.with_calculated_column(d, expression).join(leaf2.with_rows_satisfying(predicate)) 

97 self.check_sql_str( 

98 "SELECT leaf2.a AS a, leaf1.b AS b, leaf2.c AS c, -leaf1.b AS d " 

99 "FROM leaf1 JOIN leaf2 ON leaf1.a = leaf2.a WHERE leaf2.c > 0", 

100 engine.to_executable(r), 

101 ) 

102 # Add modifiers to that query via relation operations. 

103 self.check_sql_str( 

104 "SELECT DISTINCT leaf2.a AS a, leaf1.b AS b, leaf2.c AS c, -leaf1.b AS d " 

105 "FROM leaf1 JOIN leaf2 ON leaf1.a = leaf2.a WHERE leaf2.c > 0", 

106 engine.to_executable(r.without_duplicates()), 

107 ) 

108 self.check_sql_str( 

109 "SELECT leaf1.b AS b, leaf2.c AS c, -leaf1.b AS d " 

110 "FROM leaf1 JOIN leaf2 ON leaf1.a = leaf2.a WHERE leaf2.c > 0", 

111 engine.to_executable(r.with_only_columns({b, c, d})), 

112 ) 

113 self.check_sql_str( 

114 "SELECT leaf2.a AS a, leaf1.b AS b, leaf2.c AS c, -leaf1.b AS d " 

115 "FROM leaf1 JOIN leaf2 ON leaf1.a = leaf2.a WHERE leaf2.c > 0 ORDER BY leaf1.b", 

116 engine.to_executable(r.sorted(terms)), 

117 ) 

118 self.check_sql_str( 

119 "SELECT leaf2.a AS a, leaf1.b AS b, leaf2.c AS c, -leaf1.b AS d " 

120 "FROM leaf1 JOIN leaf2 ON leaf1.a = leaf2.a WHERE leaf2.c > 0 LIMIT 3 OFFSET 2", 

121 engine.to_executable(r[2:5]), 

122 ) 

123 # Add both a Projection and then a Deduplication. 

124 self.check_sql_str( 

125 "SELECT DISTINCT leaf1.b AS b, leaf2.c AS c, -leaf1.b AS d " 

126 "FROM leaf1 JOIN leaf2 ON leaf1.a = leaf2.a WHERE leaf2.c > 0", 

127 engine.to_executable(r.with_only_columns({b, c, d}).without_duplicates()), 

128 ) 

129 # Add a Deduplication and then a Projection, which requires a subquery. 

130 self.check_sql_str( 

131 "SELECT anon_1.b AS b, anon_1.c AS c, anon_1.d AS d FROM (" 

132 "SELECT DISTINCT leaf2.a AS a, leaf1.b AS b, leaf2.c AS c, -leaf1.b AS d " 

133 "FROM leaf1 JOIN leaf2 ON leaf1.a = leaf2.a WHERE leaf2.c > 0" 

134 ") AS anon_1", 

135 engine.to_executable(r.without_duplicates().with_only_columns({b, c, d})), 

136 ) 

137 # Projection and Sort together, in any order. 

138 self.check_sql_str( 

139 "SELECT leaf1.b AS b, leaf2.c AS c, -leaf1.b AS d " 

140 "FROM leaf1 JOIN leaf2 ON leaf1.a = leaf2.a WHERE leaf2.c > 0 ORDER BY leaf1.b", 

141 engine.to_executable(r.with_only_columns({b, c, d}).sorted(terms)), 

142 engine.to_executable(r.sorted(terms).with_only_columns({b, c, d})), 

143 ) 

144 # Projection and Slice together, in any order. 

145 self.check_sql_str( 

146 "SELECT leaf1.b AS b, leaf2.c AS c, -leaf1.b AS d " 

147 "FROM leaf1 JOIN leaf2 ON leaf1.a = leaf2.a WHERE leaf2.c > 0 LIMIT 2 OFFSET 1", 

148 engine.to_executable(r.with_only_columns({b, c, d})[1:3]), 

149 engine.to_executable(r[1:3].with_only_columns({b, c, d})), 

150 ) 

151 # Deduplication and Sort together, in any order. 

152 self.check_sql_str( 

153 "SELECT DISTINCT leaf2.a AS a, leaf1.b AS b, leaf2.c AS c, -leaf1.b AS d " 

154 "FROM leaf1 JOIN leaf2 ON leaf1.a = leaf2.a WHERE leaf2.c > 0 ORDER BY leaf1.b", 

155 engine.to_executable(r.without_duplicates().sorted(terms)), 

156 engine.to_executable(r.sorted(terms).without_duplicates()), 

157 ) 

158 # Deduplication and then Slice. 

159 self.check_sql_str( 

160 "SELECT DISTINCT leaf2.a AS a, leaf1.b AS b, leaf2.c AS c, -leaf1.b AS d " 

161 "FROM leaf1 JOIN leaf2 ON leaf1.a = leaf2.a WHERE leaf2.c > 0 LIMIT 2 OFFSET 1", 

162 engine.to_executable(r.without_duplicates()[1:3]), 

163 ) 

164 # Slice and then Deduplication, which requires a subquery. 

165 self.check_sql_str( 

166 "SELECT DISTINCT anon_1.a AS a, anon_1.b AS b, anon_1.c AS c, anon_1.d AS d FROM (" 

167 "SELECT leaf2.a AS a, leaf1.b AS b, leaf2.c AS c, -leaf1.b AS d " 

168 "FROM leaf1 JOIN leaf2 ON leaf1.a = leaf2.a WHERE leaf2.c > 0 LIMIT 2 OFFSET 1" 

169 ") AS anon_1", 

170 engine.to_executable(r[1:3].without_duplicates()), 

171 ) 

172 # Sort and then Slice. 

173 self.check_sql_str( 

174 "SELECT leaf2.a AS a, leaf1.b AS b, leaf2.c AS c, -leaf1.b AS d " 

175 "FROM leaf1 JOIN leaf2 ON leaf1.a = leaf2.a WHERE leaf2.c > 0 ORDER BY leaf1.b LIMIT 2 OFFSET 1", 

176 engine.to_executable(r.sorted(terms)[1:3]), 

177 ) 

178 # Slice and then Sort, which requires a subquery. 

179 self.check_sql_str( 

180 "SELECT anon_1.a AS a, anon_1.b AS b, anon_1.c AS c, anon_1.d AS d FROM (" 

181 "SELECT leaf2.a AS a, leaf1.b AS b, leaf2.c AS c, -leaf1.b AS d " 

182 "FROM leaf1 JOIN leaf2 ON leaf1.a = leaf2.a WHERE leaf2.c > 0 LIMIT 2 OFFSET 1" 

183 ") AS anon_1 ORDER BY anon_1.b", 

184 engine.to_executable(r[1:3].sorted(terms)), 

185 ) 

186 # Projection then Deduplication, with Sort at any point since it should 

187 # commute with both. 

188 self.check_sql_str( 

189 "SELECT DISTINCT leaf1.b AS b, leaf2.c AS c, -leaf1.b AS d " 

190 "FROM leaf1 JOIN leaf2 ON leaf1.a = leaf2.a WHERE leaf2.c > 0 ORDER BY leaf1.b", 

191 engine.to_executable(r.with_only_columns({b, c, d}).sorted(terms).without_duplicates()), 

192 engine.to_executable(r.with_only_columns({b, c, d}).without_duplicates().sorted(terms)), 

193 engine.to_executable(r.sorted(terms).with_only_columns({b, c, d}).without_duplicates()), 

194 ) 

195 # Deduplication then Projection (via a subquery), with a Sort at any 

196 # point - but the engine will need to make sure the Sort always appears 

197 # in the outer query, since the subquery does not preserve order. 

198 self.check_sql_str( 

199 "SELECT anon_1.b AS b, anon_1.c AS c, anon_1.d AS d FROM (" 

200 "SELECT DISTINCT leaf2.a AS a, leaf1.b AS b, leaf2.c AS c, -leaf1.b AS d " 

201 "FROM leaf1 JOIN leaf2 ON leaf1.a = leaf2.a WHERE leaf2.c > 0" 

202 ") AS anon_1 ORDER BY anon_1.b", 

203 engine.to_executable(r.without_duplicates().with_only_columns({b, c, d}).sorted(terms)), 

204 engine.to_executable(r.without_duplicates().sorted(terms).with_only_columns({b, c, d})), 

205 engine.to_executable(r.sorted(terms).without_duplicates().with_only_columns({b, c, d})), 

206 ) 

207 # Projection then Deduplication then Slice. 

208 self.check_sql_str( 

209 "SELECT DISTINCT leaf1.b AS b, leaf2.c AS c, -leaf1.b AS d " 

210 "FROM leaf1 JOIN leaf2 ON leaf1.a = leaf2.a WHERE leaf2.c > 0 LIMIT 2 OFFSET 1", 

211 engine.to_executable(r.with_only_columns({b, c, d}).without_duplicates()[1:3]), 

212 ) 

213 # Projection and Slice in any order, then Deduplication, which requires 

214 # a subquery. 

215 self.check_sql_str( 

216 "SELECT DISTINCT anon_1.b AS b, anon_1.c AS c, anon_1.d AS d FROM (" 

217 "SELECT leaf1.b AS b, leaf2.c AS c, -leaf1.b AS d " 

218 "FROM leaf1 JOIN leaf2 ON leaf1.a = leaf2.a WHERE leaf2.c > 0 LIMIT 2 OFFSET 1" 

219 ") AS anon_1", 

220 engine.to_executable(r.with_only_columns({b, c, d})[1:3].without_duplicates()), 

221 engine.to_executable(r[1:3].with_only_columns({b, c, d}).without_duplicates()), 

222 ) 

223 # Deduplication then Projection and Slice in any order, which requires 

224 # a subquery. 

225 self.check_sql_str( 

226 "SELECT anon_1.b AS b, anon_1.c AS c, anon_1.d AS d FROM (" 

227 "SELECT DISTINCT leaf2.a AS a, leaf1.b AS b, leaf2.c AS c, -leaf1.b AS d " 

228 "FROM leaf1 JOIN leaf2 ON leaf1.a = leaf2.a WHERE leaf2.c > 0" 

229 ") AS anon_1 LIMIT 2 OFFSET 1", 

230 engine.to_executable(r.without_duplicates().with_only_columns({b, c, d})[1:3]), 

231 engine.to_executable(r.without_duplicates()[1:3].with_only_columns({b, c, d})), 

232 ) 

233 # Slice then Deduplication then Projection, which requires two 

234 # subqueries. 

235 self.check_sql_str( 

236 "SELECT anon_1.b AS b, anon_1.c AS c, anon_1.d AS d FROM (" 

237 "SELECT DISTINCT anon_2.a AS a, anon_2.b AS b, anon_2.c AS c, anon_2.d AS d FROM (" 

238 "SELECT leaf2.a AS a, leaf1.b AS b, leaf2.c AS c, -leaf1.b AS d " 

239 "FROM leaf1 JOIN leaf2 ON leaf1.a = leaf2.a WHERE leaf2.c > 0 LIMIT 2 OFFSET 1" 

240 ") AS anon_2" 

241 ") AS anon_1", 

242 engine.to_executable(r[1:3].without_duplicates().with_only_columns({b, c, d})), 

243 ) 

244 # Sort then Slice, with Projection anywhere. 

245 self.check_sql_str( 

246 "SELECT leaf1.b AS b, leaf2.c AS c, -leaf1.b AS d " 

247 "FROM leaf1 JOIN leaf2 ON leaf1.a = leaf2.a WHERE leaf2.c > 0 ORDER BY leaf1.b LIMIT 2 OFFSET 1", 

248 engine.to_executable(r.sorted(terms)[1:3].with_only_columns({b, c, d})), 

249 engine.to_executable(r.sorted(terms).with_only_columns({b, c, d})[1:3]), 

250 engine.to_executable(r.with_only_columns({b, c, d}).sorted(terms)[1:3]), 

251 ) 

252 # Slice then Sort then Projection, which requires a subquery. 

253 self.check_sql_str( 

254 "SELECT anon_1.b AS b, anon_1.c AS c, anon_1.d AS d FROM (" 

255 "SELECT leaf2.a AS a, leaf1.b AS b, leaf2.c AS c, -leaf1.b AS d " 

256 "FROM leaf1 JOIN leaf2 ON leaf1.a = leaf2.a WHERE leaf2.c > 0 LIMIT 2 OFFSET 1" 

257 ") AS anon_1 ORDER BY anon_1.b", 

258 engine.to_executable(r[1:3].sorted(terms).with_only_columns({b, c, d})), 

259 ) 

260 # Slice and Projection in any order, then Sort, which requires a 

261 # subquery. 

262 self.check_sql_str( 

263 "SELECT anon_1.b AS b, anon_1.c AS c, anon_1.d AS d FROM (" 

264 "SELECT leaf1.b AS b, leaf2.c AS c, -leaf1.b AS d " 

265 "FROM leaf1 JOIN leaf2 ON leaf1.a = leaf2.a WHERE leaf2.c > 0 LIMIT 2 OFFSET 1" 

266 ") AS anon_1 ORDER BY anon_1.b", 

267 engine.to_executable(r[1:3].with_only_columns({b, c, d}).sorted(terms)), 

268 engine.to_executable(r.with_only_columns({b, c, d})[1:3].sorted(terms)), 

269 ) 

270 # Deduplication and Sort in any order, then Slice. 

271 self.check_sql_str( 

272 "SELECT DISTINCT leaf2.a AS a, leaf1.b AS b, leaf2.c AS c, -leaf1.b AS d " 

273 "FROM leaf1 JOIN leaf2 ON leaf1.a = leaf2.a WHERE leaf2.c > 0 ORDER BY leaf1.b " 

274 "LIMIT 2 OFFSET 1", 

275 engine.to_executable(r.without_duplicates().sorted(terms)[1:3]), 

276 engine.to_executable(r.sorted(terms).without_duplicates()[1:3]), 

277 ) 

278 # Duplication then Slice then Sort, which requires a subquery. 

279 self.check_sql_str( 

280 "SELECT anon_1.a AS a, anon_1.b AS b, anon_1.c AS c, anon_1.d AS d FROM (" 

281 "SELECT DISTINCT leaf2.a AS a, leaf1.b AS b, leaf2.c AS c, -leaf1.b AS d " 

282 "FROM leaf1 JOIN leaf2 ON leaf1.a = leaf2.a WHERE leaf2.c > 0 LIMIT 2 OFFSET 1" 

283 ") AS anon_1 ORDER BY anon_1.b", 

284 engine.to_executable(r.without_duplicates()[1:3].sorted(terms)), 

285 ) 

286 # Slice then Sort and Deduplication in any order, which requires a 

287 # subquery. 

288 self.check_sql_str( 

289 "SELECT DISTINCT anon_1.a AS a, anon_1.b AS b, anon_1.c AS c, anon_1.d AS d FROM (" 

290 "SELECT leaf2.a AS a, leaf1.b AS b, leaf2.c AS c, -leaf1.b AS d " 

291 "FROM leaf1 JOIN leaf2 ON leaf1.a = leaf2.a WHERE leaf2.c > 0 LIMIT 2 OFFSET 1" 

292 ") AS anon_1 ORDER BY anon_1.b", 

293 engine.to_executable(r[1:3].without_duplicates().sorted(terms)), 

294 engine.to_executable(r[1:3].sorted(terms).without_duplicates()), 

295 ) 

296 # Projection then Deduplication, with Sort at any point, and finally a 

297 # Slice. 

298 self.check_sql_str( 

299 "SELECT DISTINCT leaf1.b AS b, leaf2.c AS c, -leaf1.b AS d " 

300 "FROM leaf1 JOIN leaf2 ON leaf1.a = leaf2.a WHERE leaf2.c > 0 ORDER BY leaf1.b LIMIT 2 OFFSET 1", 

301 engine.to_executable(r.with_only_columns({b, c, d}).sorted(terms).without_duplicates()[1:3]), 

302 engine.to_executable(r.with_only_columns({b, c, d}).without_duplicates().sorted(terms)[1:3]), 

303 engine.to_executable(r.sorted(terms).with_only_columns({b, c, d}).without_duplicates()[1:3]), 

304 ) 

305 # Projection, Deduplication, Slice, Sort. 

306 self.check_sql_str( 

307 "SELECT anon_1.b AS b, anon_1.c AS c, anon_1.d AS d FROM (" 

308 "SELECT DISTINCT leaf1.b AS b, leaf2.c AS c, -leaf1.b AS d " 

309 "FROM leaf1 JOIN leaf2 ON leaf1.a = leaf2.a WHERE leaf2.c > 0 LIMIT 2 OFFSET 1" 

310 ") AS anon_1 ORDER BY anon_1.b", 

311 engine.to_executable(r.with_only_columns({b, c, d}).without_duplicates()[1:3].sorted(terms)), 

312 ) 

313 # Projection and Slice in any order, then Deduplication and Sort in any 

314 # order. 

315 self.check_sql_str( 

316 "SELECT DISTINCT anon_1.b AS b, anon_1.c AS c, anon_1.d AS d FROM (" 

317 "SELECT leaf1.b AS b, leaf2.c AS c, -leaf1.b AS d " 

318 "FROM leaf1 JOIN leaf2 ON leaf1.a = leaf2.a WHERE leaf2.c > 0 LIMIT 2 OFFSET 1" 

319 ") AS anon_1 ORDER BY anon_1.b", 

320 engine.to_executable(r.with_only_columns({b, c, d})[1:3].without_duplicates().sorted(terms)), 

321 engine.to_executable(r.with_only_columns({b, c, d})[1:3].sorted(terms).without_duplicates()), 

322 engine.to_executable(r[1:3].with_only_columns({b, c, d}).without_duplicates().sorted(terms)), 

323 engine.to_executable(r[1:3].with_only_columns({b, c, d}).sorted(terms).without_duplicates()), 

324 ) 

325 # Projection and Sort in any order, then Slice, then Deduplication. 

326 self.check_sql_str( 

327 "SELECT DISTINCT anon_1.b AS b, anon_1.c AS c, anon_1.d AS d FROM (" 

328 "SELECT leaf1.b AS b, leaf2.c AS c, -leaf1.b AS d " 

329 "FROM leaf1 JOIN leaf2 ON leaf1.a = leaf2.a WHERE leaf2.c > 0 ORDER BY leaf1.b " 

330 "LIMIT 2 OFFSET 1" 

331 ") AS anon_1", 

332 engine.to_executable(r.with_only_columns({b, c, d}).sorted(terms)[1:3].without_duplicates()), 

333 engine.to_executable(r.sorted(terms).with_only_columns({b, c, d})[1:3].without_duplicates()), 

334 ) 

335 # Deduplication then Projection (via a subquery), with a Sort at any 

336 # point, and then finally a Slice. 

337 self.check_sql_str( 

338 "SELECT anon_1.b AS b, anon_1.c AS c, anon_1.d AS d FROM (" 

339 "SELECT DISTINCT leaf2.a AS a, leaf1.b AS b, leaf2.c AS c, -leaf1.b AS d " 

340 "FROM leaf1 JOIN leaf2 ON leaf1.a = leaf2.a WHERE leaf2.c > 0" 

341 ") AS anon_1 ORDER BY anon_1.b LIMIT 2 OFFSET 1", 

342 engine.to_executable(r.without_duplicates().with_only_columns({b, c, d}).sorted(terms)[1:3]), 

343 engine.to_executable(r.without_duplicates().sorted(terms).with_only_columns({b, c, d})[1:3]), 

344 engine.to_executable(r.sorted(terms).without_duplicates().with_only_columns({b, c, d})[1:3]), 

345 ) 

346 # Deduplication, then Projection and Slice in any order, then Sort. 

347 self.check_sql_str( 

348 "SELECT anon_1.b AS b, anon_1.c AS c, anon_1.d AS d FROM (" 

349 "SELECT anon_2.b AS b, anon_2.c AS c, anon_2.d AS d FROM (" 

350 "SELECT DISTINCT leaf2.a AS a, leaf1.b AS b, leaf2.c AS c, -leaf1.b AS d " 

351 "FROM leaf1 JOIN leaf2 ON leaf1.a = leaf2.a WHERE leaf2.c > 0" 

352 ") AS anon_2 LIMIT 2 OFFSET 1" 

353 ") AS anon_1 ORDER BY anon_1.b", 

354 engine.to_executable(r.without_duplicates().with_only_columns({b, c, d})[1:3].sorted(terms)), 

355 engine.to_executable(r.without_duplicates()[1:3].with_only_columns({b, c, d}).sorted(terms)), 

356 ) 

357 # Sort and Deduplication in any order, then Projection and Slice in any 

358 # order. 

359 self.check_sql_str( 

360 "SELECT anon_1.b AS b, anon_1.c AS c, anon_1.d AS d FROM (" 

361 "SELECT DISTINCT leaf2.a AS a, leaf1.b AS b, leaf2.c AS c, -leaf1.b AS d " 

362 "FROM leaf1 JOIN leaf2 ON leaf1.a = leaf2.a WHERE leaf2.c > 0" 

363 ") AS anon_1 ORDER BY anon_1.b LIMIT 2 OFFSET 1", 

364 engine.to_executable(r.without_duplicates().sorted(terms).with_only_columns({b, c, d})[1:3]), 

365 engine.to_executable(r.sorted(terms).without_duplicates().with_only_columns({b, c, d})[1:3]), 

366 engine.to_executable(r.without_duplicates().sorted(terms)[1:3].with_only_columns({b, c, d})), 

367 engine.to_executable(r.sorted(terms).without_duplicates()[1:3].with_only_columns({b, c, d})), 

368 ) 

369 # Sort, then Slice and Projection in any order, then Deduplication. 

370 self.check_sql_str( 

371 "SELECT DISTINCT anon_1.b AS b, anon_1.c AS c, anon_1.d AS d FROM (" 

372 "SELECT leaf1.b AS b, leaf2.c AS c, -leaf1.b AS d " 

373 "FROM leaf1 JOIN leaf2 ON leaf1.a = leaf2.a WHERE leaf2.c > 0 ORDER BY leaf1.b LIMIT 2 OFFSET 1" 

374 ") AS anon_1", 

375 engine.to_executable(r.sorted(terms)[1:3].with_only_columns({b, c, d}).without_duplicates()), 

376 engine.to_executable(r.sorted(terms).with_only_columns({b, c, d})[1:3].without_duplicates()), 

377 ) 

378 # Slice, then Projection, then Deduplication and Sort in any order. 

379 self.check_sql_str( 

380 "SELECT DISTINCT anon_1.b AS b, anon_1.c AS c, anon_1.d AS d FROM (" 

381 "SELECT leaf1.b AS b, leaf2.c AS c, -leaf1.b AS d " 

382 "FROM leaf1 JOIN leaf2 ON leaf1.a = leaf2.a WHERE leaf2.c > 0 LIMIT 2 OFFSET 1" 

383 ") AS anon_1 ORDER BY anon_1.b", 

384 engine.to_executable(r[1:3].with_only_columns({b, c, d}).without_duplicates().sorted(terms)), 

385 engine.to_executable(r[1:3].with_only_columns({b, c, d}).sorted(terms).without_duplicates()), 

386 ) 

387 # Slice, then Sort, then Projection, then Deduplication. 

388 self.check_sql_str( 

389 "SELECT DISTINCT anon_1.b AS b, anon_1.c AS c, anon_1.d AS d FROM (" 

390 "SELECT leaf2.a AS a, leaf1.b AS b, leaf2.c AS c, -leaf1.b AS d " 

391 "FROM leaf1 JOIN leaf2 ON leaf1.a = leaf2.a WHERE leaf2.c > 0 LIMIT 2 OFFSET 1" 

392 ") AS anon_1 ORDER BY anon_1.b", 

393 engine.to_executable(r[1:3].sorted(terms).with_only_columns({b, c, d}).without_duplicates()), 

394 ) 

395 # Slice, then Sort, Deduplication, and Projection as long as the 

396 # Deduplication precedes the Projection. 

397 self.check_sql_str( 

398 "SELECT anon_1.b AS b, anon_1.c AS c, anon_1.d AS d FROM (" 

399 "SELECT DISTINCT anon_2.a AS a, anon_2.b AS b, anon_2.c AS c, anon_2.d AS d FROM (" 

400 "SELECT leaf2.a AS a, leaf1.b AS b, leaf2.c AS c, -leaf1.b AS d " 

401 "FROM leaf1 JOIN leaf2 ON leaf1.a = leaf2.a WHERE leaf2.c > 0 LIMIT 2 OFFSET 1" 

402 ") AS anon_2" 

403 ") AS anon_1 ORDER BY anon_1.b", 

404 engine.to_executable(r[1:3].sorted(terms).without_duplicates().with_only_columns({b, c, d})), 

405 engine.to_executable(r[1:3].without_duplicates().sorted(terms).with_only_columns({b, c, d})), 

406 engine.to_executable(r[1:3].without_duplicates().with_only_columns({b, c, d}).sorted(terms)), 

407 ) 

408 

409 def test_additional_append_unary(self) -> None: 

410 """Test append_unary rules that involve more than just the 

411 Select-managed operation types. 

412 """ 

413 engine = sql.Engine[_L]() 

414 md = sqlalchemy.schema.MetaData() 

415 a = tests.ColumnTag("a") 

416 b = tests.ColumnTag("b") 

417 c = tests.ColumnTag("c") 

418 d = tests.ColumnTag("d") 

419 expression = ColumnExpression.reference(b).method("__neg__") 

420 predicate = ColumnExpression.reference(c).gt(ColumnExpression.literal(0)) 

421 leaf1 = _make_leaf(engine, md, "leaf1", a, b) 

422 leaf2 = _make_leaf(engine, md, "leaf2", a, c) 

423 # Applying a Calculation to a Projection expands the latter and 

424 # commutes them. 

425 self.assert_relations_equal( 

426 leaf1.with_only_columns({b}).with_calculated_column(d, expression), 

427 leaf1.with_calculated_column(d, expression).with_only_columns({b, d}), 

428 ) 

429 # Back-to-back Deduplications reduce to one. 

430 self.assert_relations_equal( 

431 leaf1.without_duplicates().without_duplicates(), 

432 leaf1.without_duplicates(), 

433 ) 

434 # Selections after Slices involve a subquery. 

435 self.check_sql_str( 

436 "SELECT anon_1.a AS a, anon_1.c AS c FROM (" 

437 "SELECT leaf2.a AS a, leaf2.c AS c FROM leaf2 LIMIT 2 OFFSET 1" 

438 ") AS anon_1 WHERE anon_1.c > 0", 

439 engine.to_executable(leaf2[1:3].with_rows_satisfying(predicate)), 

440 ) 

441 # Identity does nothing. 

442 self.assert_relations_equal(Identity().apply(leaf1), leaf1) 

443 

444 def test_additional_append_binary(self) -> None: 

445 """Test append_binary rules that involve more than just the 

446 Select-managed operation types. 

447 """ 

448 engine = sql.Engine[_L]() 

449 md = sqlalchemy.schema.MetaData() 

450 a = tests.ColumnTag("a") 

451 b = tests.ColumnTag("b") 

452 c = tests.ColumnTag("c") 

453 leaf1 = _make_leaf(engine, md, "leaf1", a, b) 

454 leaf2 = _make_leaf(engine, md, "leaf2", a, c) 

455 # Projections are moved outside joins. 

456 self.assert_relations_equal( 

457 leaf1.join(leaf2.with_only_columns({a})), 

458 leaf1.join(leaf2).with_only_columns({a, b}), 

459 ) 

460 # IgnoreOne does what it should. 

461 self.assert_relations_equal(IgnoreOne(ignore_lhs=True).apply(leaf1, leaf2), leaf2) 

462 self.assert_relations_equal(IgnoreOne(ignore_lhs=False).apply(leaf1, leaf2), leaf1) 

463 

464 def test_chains(self) -> None: 

465 """Test relation trees that involve Chain operations.""" 

466 engine = sql.Engine[_L]() 

467 md = sqlalchemy.schema.MetaData() 

468 a = tests.ColumnTag("a") 

469 b = tests.ColumnTag("b") 

470 leaf1 = _make_leaf(engine, md, "leaf1", a, b) 

471 leaf2 = _make_leaf(engine, md, "leaf2", a, b) 

472 sort_terms = [SortTerm(ColumnExpression.reference(b))] 

473 # A Chain on its own maps directly to SQL UNION. 

474 self.check_sql_str( 

475 "SELECT leaf1.a AS a, leaf1.b AS b FROM leaf1 " 

476 "UNION ALL " 

477 "SELECT leaf2.a AS a, leaf2.b AS b FROM leaf2", 

478 engine.to_executable(leaf1.chain(leaf2)), 

479 ) 

480 # Deduplication transforms this to UNION ALL. 

481 self.check_sql_str( 

482 "SELECT leaf1.a AS a, leaf1.b AS b FROM leaf1 " 

483 "UNION " 

484 "SELECT leaf2.a AS a, leaf2.b AS b FROM leaf2", 

485 engine.to_executable(leaf1.chain(leaf2).without_duplicates()), 

486 ) 

487 # Sorting happens after the second SELECT. 

488 self.check_sql_str( 

489 "SELECT leaf1.a AS a, leaf1.b AS b FROM leaf1 " 

490 "UNION ALL " 

491 "SELECT leaf2.a AS a, leaf2.b AS b FROM leaf2 ORDER BY b", 

492 engine.to_executable(leaf1.chain(leaf2).sorted(sort_terms)), 

493 ) 

494 # Slicing also happens after the second SELECT. 

495 self.check_sql_str( 

496 "SELECT leaf1.a AS a, leaf1.b AS b FROM leaf1 " 

497 "UNION ALL " 

498 "SELECT leaf2.a AS a, leaf2.b AS b FROM leaf2 LIMIT 3 OFFSET 2", 

499 engine.to_executable(leaf1.chain(leaf2)[2:5]), 

500 ) 

501 # Projection just before the Chain is handled without any extra 

502 # subqueries. 

503 self.check_sql_str( 

504 "SELECT leaf1.a AS a FROM leaf1 UNION ALL SELECT leaf2.a AS a FROM leaf2", 

505 engine.to_executable(leaf1.with_only_columns({a}).chain(leaf2.with_only_columns({a}))), 

506 ) 

507 # Deduplication prior to Chain adds DISTINCT. 

508 self.check_sql_str( 

509 "SELECT DISTINCT leaf1.a AS a, leaf1.b AS b FROM leaf1 " 

510 "UNION ALL " 

511 "SELECT leaf2.a AS a, leaf2.b AS b FROM leaf2", 

512 engine.to_executable(leaf1.without_duplicates().chain(leaf2)), 

513 ) 

514 # Projection and Deduplication prior to Chain also just adds DISTINCT. 

515 self.check_sql_str( 

516 "SELECT DISTINCT leaf1.a AS a FROM leaf1 UNION ALL SELECT leaf2.a AS a FROM leaf2", 

517 engine.to_executable( 

518 leaf1.with_only_columns({a}).without_duplicates().chain(leaf2.with_only_columns({a})) 

519 ), 

520 ) 

521 # Nested chains should flatten out (again, no subqueries), but use 

522 # parentheses for precedence. 

523 leaf3 = _make_leaf(engine, md, "leaf3", a, b) 

524 self.check_sql_str( 

525 "(SELECT leaf1.a AS a, leaf1.b AS b FROM leaf1 " 

526 "UNION ALL " 

527 "SELECT leaf2.a AS a, leaf2.b AS b FROM leaf2) " 

528 "UNION ALL " 

529 "SELECT leaf3.a AS a, leaf3.b AS b FROM leaf3", 

530 engine.to_executable(leaf1.chain(leaf2).chain(leaf3)), 

531 ) 

532 # Nested chains with deduplication should do the same. 

533 self.check_sql_str( 

534 "(SELECT leaf1.a AS a, leaf1.b AS b FROM leaf1 " 

535 "UNION ALL " 

536 "SELECT leaf2.a AS a, leaf2.b AS b FROM leaf2) " 

537 "UNION " 

538 "SELECT leaf3.a AS a, leaf3.b AS b FROM leaf3", 

539 engine.to_executable(leaf1.chain(leaf2).chain(leaf3).without_duplicates()), 

540 ) 

541 self.check_sql_str( 

542 "(SELECT leaf1.a AS a, leaf1.b AS b FROM leaf1 " 

543 "UNION " 

544 "SELECT leaf2.a AS a, leaf2.b AS b FROM leaf2) " 

545 "UNION " 

546 "SELECT leaf3.a AS a, leaf3.b AS b FROM leaf3", 

547 engine.to_executable(leaf1.chain(leaf2).without_duplicates().chain(leaf3).without_duplicates()), 

548 ) 

549 # Nested chains with projections. 

550 self.check_sql_str( 

551 "(SELECT leaf1.a AS a FROM leaf1 " 

552 "UNION ALL " 

553 "SELECT leaf2.a AS a FROM leaf2) " 

554 "UNION ALL " 

555 "SELECT leaf3.a AS a FROM leaf3", 

556 engine.to_executable( 

557 leaf1.chain(leaf2).with_only_columns({a}).chain(leaf3.with_only_columns({a})) 

558 ), 

559 ) 

560 # Nested chains with projections and (then) deduplication. 

561 self.check_sql_str( 

562 "(SELECT leaf1.a AS a FROM leaf1 " 

563 "UNION " 

564 "SELECT leaf2.a AS a FROM leaf2) " 

565 "UNION " 

566 "SELECT leaf3.a AS a FROM leaf3", 

567 engine.to_executable( 

568 leaf1.chain(leaf2) 

569 .with_only_columns({a}) 

570 .without_duplicates() 

571 .chain(leaf3.with_only_columns({a})) 

572 .without_duplicates() 

573 ), 

574 ) 

575 # Chains with Slices and possibly Sorts in operands need subqueries. 

576 self.check_sql_str( 

577 "SELECT anon_1.a AS a, anon_1.b AS b FROM " 

578 "(SELECT leaf1.a AS a, leaf1.b AS b FROM leaf1 LIMIT 2 OFFSET 1) AS anon_1 " 

579 "UNION ALL " 

580 "SELECT leaf2.a AS a, leaf2.b AS b FROM leaf2", 

581 engine.to_executable(leaf1[1:3].chain(leaf2)), 

582 ) 

583 self.check_sql_str( 

584 "SELECT leaf1.a AS a, leaf1.b AS b FROM leaf1 " 

585 "UNION ALL " 

586 "SELECT anon_1.a AS a, anon_1.b AS b FROM " 

587 "(SELECT leaf2.a AS a, leaf2.b AS b FROM leaf2 LIMIT 2 OFFSET 1) AS anon_1", 

588 engine.to_executable(leaf1.chain(leaf2[1:3])), 

589 ) 

590 # Add a Selection or Calculation on top of a Chain yields subqueries 

591 # to avoid reordering operations. 

592 expression = ColumnExpression.reference(b).method("__neg__") 

593 self.check_sql_str( 

594 "SELECT anon_1.a AS a, anon_1.b AS b, -anon_1.b AS c FROM " 

595 "(SELECT leaf1.a AS a, leaf1.b AS b FROM leaf1 " 

596 "UNION ALL " 

597 "SELECT leaf2.a AS a, leaf2.b AS b FROM leaf2) AS anon_1", 

598 engine.to_executable(leaf1.chain(leaf2).with_calculated_column(tests.ColumnTag("c"), expression)), 

599 ) 

600 predicate = ColumnExpression.reference(a).gt(ColumnExpression.literal(0)) 

601 self.check_sql_str( 

602 "SELECT anon_1.a AS a, anon_1.b AS b FROM " 

603 "(SELECT leaf1.a AS a, leaf1.b AS b FROM leaf1 " 

604 "UNION ALL " 

605 "SELECT leaf2.a AS a, leaf2.b AS b FROM leaf2) AS anon_1 " 

606 "WHERE anon_1.a > 0", 

607 engine.to_executable(leaf1.chain(leaf2).with_rows_satisfying(predicate)), 

608 ) 

609 

610 def test_row_ordering_loss(self) -> None: 

611 """Test that we raise when we would have to make an existing Sort do 

612 nothing by putting it in a subquery. 

613 """ 

614 engine = sql.Engine[_L]() 

615 md = sqlalchemy.schema.MetaData() 

616 a = tests.ColumnTag("a") 

617 b = tests.ColumnTag("b") 

618 leaf1 = _make_leaf(engine, md, "leaf1", a, b) 

619 leaf2 = _make_leaf(engine, md, "leaf2", a, b) 

620 relation = leaf1.sorted([SortTerm(ColumnExpression.reference(b))]) 

621 with self.assertRaises(RelationalAlgebraError): 

622 relation.materialized() 

623 with self.assertRaises(RelationalAlgebraError): 

624 relation.chain(leaf2) 

625 with self.assertRaises(RelationalAlgebraError): 

626 leaf2.chain(relation) 

627 with self.assertRaises(RelationalAlgebraError): 

628 relation.join(leaf2) 

629 with self.assertRaises(RelationalAlgebraError): 

630 leaf2.join(relation) 

631 

632 def test_trivial(self) -> None: 

633 """Test that we can emit valid SQL for relations with no columns or 

634 no rows. 

635 """ 

636 # No points for pretty; subqueries here are unnecessary but 

637 # Payload.from_clause would need to be able to be None to drop them, 

638 # and that's not worth it. 

639 engine = sql.Engine[_L]() 

640 join_identity = engine.make_join_identity_relation() 

641 self.check_sql_str( 

642 'SELECT 1 AS "IGNORED" FROM (SELECT 1 AS "IGNORED") AS anon_1', 

643 engine.to_executable(join_identity), 

644 ) 

645 doomed = engine.make_doomed_relation({tests.ColumnTag("a")}, messages=[]) 

646 self.check_sql_str( 

647 "SELECT anon_1.a AS a FROM (SELECT NULL AS a) AS anon_1 WHERE 0 = 1", 

648 engine.to_executable(doomed), 

649 ) 

650 

651 

652if __name__ == "__main__": 652 ↛ 653line 652 didn't jump to line 653, because the condition on line 652 was never true

653 unittest.main()