Coverage for tests/test_sql_engine.py: 10%

141 statements  

« prev     ^ index     » next       coverage.py v7.3.1, created at 2023-09-27 10:10 +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 """Test the SQL engine.""" 

78 

79 def setUp(self): 

80 self.maxDiff = None 

81 

82 def test_select_operations(self) -> None: 

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

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

85 relation. 

86 """ 

87 engine = sql.Engine[_L]() 

88 md = sqlalchemy.schema.MetaData() 

89 a = tests.ColumnTag("a") 

90 b = tests.ColumnTag("b") 

91 c = tests.ColumnTag("c") 

92 d = tests.ColumnTag("d") 

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

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

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

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

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

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

99 self.check_sql_str( 

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

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

102 engine.to_executable(r), 

103 ) 

104 # Add modifiers to that query via relation operations. 

105 self.check_sql_str( 

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

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

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

109 ) 

110 self.check_sql_str( 

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

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

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

114 ) 

115 self.check_sql_str( 

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

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

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

119 ) 

120 self.check_sql_str( 

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

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

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

124 ) 

125 # Add both a Projection and then a Deduplication. 

126 self.check_sql_str( 

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

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

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

130 ) 

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

132 self.check_sql_str( 

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

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

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

136 ") AS anon_1", 

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

138 ) 

139 # Projection and Sort together, in any order. 

140 self.check_sql_str( 

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

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

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

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

145 ) 

146 # Projection and Slice together, in any order. 

147 self.check_sql_str( 

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

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

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

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

152 ) 

153 # Deduplication and Sort together, in any order. 

154 self.check_sql_str( 

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

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

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

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

159 ) 

160 # Deduplication and then Slice. 

161 self.check_sql_str( 

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

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

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

165 ) 

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

167 self.check_sql_str( 

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

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

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

171 ") AS anon_1", 

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

173 ) 

174 # Sort and then Slice. 

175 self.check_sql_str( 

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

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

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

179 ) 

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

181 self.check_sql_str( 

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

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

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

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

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

187 ) 

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

189 # commute with both. 

190 self.check_sql_str( 

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

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

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

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

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

196 ) 

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

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

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

200 self.check_sql_str( 

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

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

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

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

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

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

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

208 ) 

209 # Projection then Deduplication then Slice. 

210 self.check_sql_str( 

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

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

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

214 ) 

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

216 # a subquery. 

217 self.check_sql_str( 

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

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

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

221 ") AS anon_1", 

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

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

224 ) 

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

226 # a subquery. 

227 self.check_sql_str( 

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

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

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

231 ") AS anon_1 LIMIT 2 OFFSET 1", 

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

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

234 ) 

235 # Slice then Deduplication then Projection, which requires two 

236 # subqueries. 

237 self.check_sql_str( 

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

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

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

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

242 ") AS anon_2" 

243 ") AS anon_1", 

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

245 ) 

246 # Sort then Slice, with Projection anywhere. 

247 self.check_sql_str( 

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

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

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

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

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

253 ) 

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

255 self.check_sql_str( 

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

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

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

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

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

261 ) 

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

263 # subquery. 

264 self.check_sql_str( 

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

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

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

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

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

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

271 ) 

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

273 self.check_sql_str( 

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

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

276 "LIMIT 2 OFFSET 1", 

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

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

279 ) 

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

281 self.check_sql_str( 

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

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

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

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

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

287 ) 

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

289 # subquery. 

290 self.check_sql_str( 

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

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

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

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

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

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

297 ) 

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

299 # Slice. 

300 self.check_sql_str( 

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

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

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

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

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

306 ) 

307 # Projection, Deduplication, Slice, Sort. 

308 self.check_sql_str( 

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

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

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

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

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

314 ) 

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

316 # order. 

317 self.check_sql_str( 

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

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

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

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

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

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

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

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

326 ) 

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

328 self.check_sql_str( 

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

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

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

332 "LIMIT 2 OFFSET 1" 

333 ") AS anon_1", 

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

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

336 ) 

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

338 # point, and then finally a Slice. 

339 self.check_sql_str( 

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

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

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

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

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

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

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

347 ) 

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

349 self.check_sql_str( 

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

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

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

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

354 ") AS anon_2 LIMIT 2 OFFSET 1" 

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

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

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

358 ) 

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

360 # order. 

361 self.check_sql_str( 

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

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

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

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

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

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

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

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

370 ) 

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

372 self.check_sql_str( 

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

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

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

376 ") AS anon_1", 

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

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

379 ) 

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

381 self.check_sql_str( 

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

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

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

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

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

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

388 ) 

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

390 self.check_sql_str( 

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

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

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

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

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

396 ) 

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

398 # Deduplication precedes the Projection. 

399 self.check_sql_str( 

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

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

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

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

404 ") AS anon_2" 

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

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

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

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

409 ) 

410 

411 def test_additional_append_unary(self) -> None: 

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

413 Select-managed operation types. 

414 """ 

415 engine = sql.Engine[_L]() 

416 md = sqlalchemy.schema.MetaData() 

417 a = tests.ColumnTag("a") 

418 b = tests.ColumnTag("b") 

419 c = tests.ColumnTag("c") 

420 d = tests.ColumnTag("d") 

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

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

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

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

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

426 # commutes them. 

427 self.assert_relations_equal( 

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

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

430 ) 

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

432 self.assert_relations_equal( 

433 leaf1.without_duplicates().without_duplicates(), 

434 leaf1.without_duplicates(), 

435 ) 

436 # Selections after Slices involve a subquery. 

437 self.check_sql_str( 

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

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

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

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

442 ) 

443 # Identity does nothing. 

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

445 

446 def test_additional_append_binary(self) -> None: 

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

448 Select-managed operation types. 

449 """ 

450 engine = sql.Engine[_L]() 

451 md = sqlalchemy.schema.MetaData() 

452 a = tests.ColumnTag("a") 

453 b = tests.ColumnTag("b") 

454 c = tests.ColumnTag("c") 

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

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

457 # Projections are moved outside joins. 

458 self.assert_relations_equal( 

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

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

461 ) 

462 # IgnoreOne does what it should. 

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

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

465 

466 def test_chains(self) -> None: 

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

468 engine = sql.Engine[_L]() 

469 md = sqlalchemy.schema.MetaData() 

470 a = tests.ColumnTag("a") 

471 b = tests.ColumnTag("b") 

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

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

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

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

476 self.check_sql_str( 

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

478 "UNION ALL " 

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

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

481 ) 

482 # Deduplication transforms this to UNION ALL. 

483 self.check_sql_str( 

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

485 "UNION " 

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

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

488 ) 

489 # Sorting happens after the second SELECT. 

490 self.check_sql_str( 

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

492 "UNION ALL " 

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

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

495 ) 

496 # Slicing also happens after the second SELECT. 

497 self.check_sql_str( 

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

499 "UNION ALL " 

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

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

502 ) 

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

504 # subqueries. 

505 self.check_sql_str( 

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

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

508 ) 

509 # Deduplication prior to Chain adds DISTINCT. 

510 self.check_sql_str( 

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

512 "UNION ALL " 

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

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

515 ) 

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

517 self.check_sql_str( 

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

519 engine.to_executable( 

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

521 ), 

522 ) 

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

524 # parentheses for precedence. 

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

526 self.check_sql_str( 

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

528 "UNION ALL " 

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

530 "UNION ALL " 

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

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

533 ) 

534 # Nested chains with deduplication should do the same. 

535 self.check_sql_str( 

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

537 "UNION ALL " 

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

539 "UNION " 

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

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

542 ) 

543 self.check_sql_str( 

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

545 "UNION " 

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

547 "UNION " 

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

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

550 ) 

551 # Nested chains with projections. 

552 self.check_sql_str( 

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

554 "UNION ALL " 

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

556 "UNION ALL " 

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

558 engine.to_executable( 

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

560 ), 

561 ) 

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

563 self.check_sql_str( 

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

565 "UNION " 

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

567 "UNION " 

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

569 engine.to_executable( 

570 leaf1.chain(leaf2) 

571 .with_only_columns({a}) 

572 .without_duplicates() 

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

574 .without_duplicates() 

575 ), 

576 ) 

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

578 self.check_sql_str( 

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

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

581 "UNION ALL " 

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

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

584 ) 

585 self.check_sql_str( 

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

587 "UNION ALL " 

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

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

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

591 ) 

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

593 # to avoid reordering operations. 

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

595 self.check_sql_str( 

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

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

598 "UNION ALL " 

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

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

601 ) 

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

603 self.check_sql_str( 

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

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

606 "UNION ALL " 

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

608 "WHERE anon_1.a > 0", 

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

610 ) 

611 

612 def test_row_ordering_loss(self) -> None: 

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

614 nothing by putting it in a subquery. 

615 """ 

616 engine = sql.Engine[_L]() 

617 md = sqlalchemy.schema.MetaData() 

618 a = tests.ColumnTag("a") 

619 b = tests.ColumnTag("b") 

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

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

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

623 with self.assertRaises(RelationalAlgebraError): 

624 relation.materialized() 

625 with self.assertRaises(RelationalAlgebraError): 

626 relation.chain(leaf2) 

627 with self.assertRaises(RelationalAlgebraError): 

628 leaf2.chain(relation) 

629 with self.assertRaises(RelationalAlgebraError): 

630 relation.join(leaf2) 

631 with self.assertRaises(RelationalAlgebraError): 

632 leaf2.join(relation) 

633 

634 def test_trivial(self) -> None: 

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

636 no rows. 

637 """ 

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

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

640 # and that's not worth it. 

641 engine = sql.Engine[_L]() 

642 join_identity = engine.make_join_identity_relation() 

643 self.check_sql_str( 

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

645 engine.to_executable(join_identity), 

646 ) 

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

648 self.check_sql_str( 

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

650 engine.to_executable(doomed), 

651 ) 

652 

653 

654if __name__ == "__main__": 

655 unittest.main()