Coverage for tests/test_sql_engine.py: 11%
143 statements
« prev ^ index » next coverage.py v6.5.0, created at 2023-04-06 02:37 -0700
« 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# (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/>.
22from __future__ import annotations
24import unittest
25from typing import TypeAlias
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)
40_L: TypeAlias = sqlalchemy.sql.ColumnElement
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.
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.
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.
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)
76class SqlEngineTestCase(tests.RelationTestCase):
77 def setUp(self):
78 self.maxDiff = None
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 )
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)
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)
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 )
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)
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 )
652if __name__ == "__main__": 652 ↛ 653line 652 didn't jump to line 653, because the condition on line 652 was never true
653 unittest.main()