Coverage for tests/test_sql_engine.py: 10%
141 statements
« prev ^ index » next coverage.py v7.2.7, created at 2023-07-21 09:39 +0000
« prev ^ index » next coverage.py v7.2.7, created at 2023-07-21 09:39 +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/>.
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 """Test the SQL engine."""
79 def setUp(self):
80 self.maxDiff = None
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 )
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)
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)
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 )
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)
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 )
654if __name__ == "__main__":
655 unittest.main()