Coverage for python/lsst/daf/relation/_relation.py: 64%

139 statements  

« prev     ^ index     » next       coverage.py v6.5.0, created at 2023-03-30 09:29 +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 

24__all__ = ( 

25 "BaseRelation", 

26 "Relation", 

27) 

28 

29import dataclasses 

30from abc import abstractmethod 

31from collections.abc import Sequence, Set 

32from typing import TYPE_CHECKING, Any, Protocol, TypeVar 

33 

34from ._columns import ColumnTag 

35 

36if TYPE_CHECKING: 36 ↛ 37line 36 didn't jump to line 37, because the condition on line 36 was never true

37 from ._columns import ColumnExpression, Predicate 

38 from ._engine import Engine 

39 from ._operations import SortTerm 

40 

41 

42class Relation(Protocol): 

43 """An abstract interface for expression trees on tabular data. 

44 

45 Notes 

46 ----- 

47 This ABC is a `typing.Protocol`, which means that classes that implement 

48 its interface can be recognized as such by static type checkers without 

49 actually inheriting from it, and in fact all concrete relation types 

50 inherit only from `BaseRelation` (which provides implementations of many 

51 `Relation` methods, but does not include the complete interface or inherit 

52 from `Relation` itself) instead. This split allows subclasses to implement 

53 attributes that are defined as properties here as `~dataclasses.dataclass` 

54 attributes instead of true properties, something `typing.Protocol` 

55 explicitly permits and recommends that nevertheless works only if the 

56 protocol is not actually inherited from. 

57 

58 In almost all cases, users should use `Relation` instead of `BaseRelation`: 

59 the only exception is when writing an `isinstance` check to see if a type 

60 is a relation at all, rather than a particular relation subclass. 

61 `BaseRelation` may become an alias to `Relation` itself in the future if 

62 `typing.Protocol` inheritance interaction with properties is improved. 

63 

64 All concrete `Relation` types are frozen, equality-comparable 

65 `dataclasses`. They also provide a very concise `str` representation (in 

66 addition to the dataclass-provided `repr`) suitable for summarizing an 

67 entire relation tree. 

68 

69 See Also 

70 -------- 

71 :ref:`lsst.daf.relation-overview` 

72 """ 

73 

74 @property 

75 @abstractmethod 

76 def columns(self) -> Set[ColumnTag]: 

77 """The columns in this relation (`~collections.abc.Set` [ `ColumnTag` ] 

78 ). 

79 """ 

80 raise NotImplementedError() 

81 

82 @property 

83 @abstractmethod 

84 def payload(self) -> Any: 

85 """The engine-specific contents of the relation. 

86 

87 This is `None` in the common case that engine-specific contents are to 

88 be computed on-the-fly. Relation payloads permit "deferred 

89 initialization" - while relation objects are otherwise immutable, the 

90 payload may be set (once) after construction, via `attach_payload`. 

91 """ 

92 raise NotImplementedError() 

93 

94 @property 

95 @abstractmethod 

96 def engine(self) -> Engine: 

97 """The engine that is responsible for interpreting this relation 

98 (`Engine`). 

99 """ 

100 raise NotImplementedError() 

101 

102 @property 

103 @abstractmethod 

104 def min_rows(self) -> int: 

105 """The minimum number of rows this relation might have (`int`).""" 

106 raise NotImplementedError() 

107 

108 @property 

109 @abstractmethod 

110 def max_rows(self) -> int | None: 

111 """The maximum number of rows this relation might have (`int` or 

112 `None`). 

113 

114 This is `None` for relations whose size is not bounded from above. 

115 """ 

116 raise NotImplementedError() 

117 

118 @property 

119 @abstractmethod 

120 def is_locked(self) -> bool: 

121 """Whether this relation and those upstream of it should be considered 

122 fixed by tree-manipulation algorithms (`bool`). 

123 """ 

124 raise NotImplementedError() 

125 

126 @property 

127 @abstractmethod 

128 def is_join_identity(self) -> bool: 

129 """Whether a `join` to this relation will result in the other relation 

130 being returned directly (`bool`). 

131 

132 Join identity relations have exactly one row and no columns. 

133 

134 See Also 

135 -------- 

136 LeafRelation.make_join_identity 

137 """ 

138 raise NotImplementedError() 

139 

140 @property 

141 @abstractmethod 

142 def is_trivial(self) -> bool: 

143 """Whether this relation has no real content (`bool`). 

144 

145 A trivial relation is either a `join identity <is_join_identity>` with 

146 no columns and exactly one row, or a relation with an arbitrary number 

147 of columns and no rows (i.e. ``min_rows==max_rows==0``). 

148 """ 

149 raise NotImplementedError() 

150 

151 @abstractmethod 

152 def attach_payload(self, payload: Any) -> None: 

153 """Attach an engine-specific `payload` to this relation. 

154 

155 This method may be called exactly once on a `Relation` instance that 

156 was not initialized with a `payload`, despite the fact that `Relation` 

157 objects are otherwise considered immutable. 

158 

159 Parameters 

160 ---------- 

161 payload 

162 Engine-specific content to attach. 

163 

164 Raises 

165 ------ 

166 TypeError 

167 Raised if this relation already has a payload, or can never have a 

168 payload. `TypeError` is used here for consistency with other 

169 attempts to assign to an attribute of an immutable object. 

170 """ 

171 raise NotImplementedError() 

172 

173 @abstractmethod 

174 def with_calculated_column( 

175 self, 

176 tag: ColumnTag, 

177 expression: ColumnExpression, 

178 *, 

179 preferred_engine: Engine | None = None, 

180 backtrack: bool = True, 

181 transfer: bool = False, 

182 require_preferred_engine: bool = False, 

183 ) -> Relation: 

184 """Return a new relation that adds a calculated column to this one. 

185 

186 This is a convenience method chat constructs and applies a 

187 `Calculation` operation. 

188 

189 Parameters 

190 ---------- 

191 tag : `ColumnTag` 

192 Identifier for the new column. 

193 expression : `ColumnExpression` 

194 Expression used to populate the new column. 

195 preferred_engine : `Engine`, optional 

196 Engine that the operation would ideally be performed in. If this 

197 is not equal to ``self.engine``, the ``backtrack``, ``transfer``, 

198 and ``require_preferred_engine`` arguments control the behavior. 

199 backtrack : `bool`, optional 

200 If `True` (default) and the current engine is not the preferred 

201 engine, attempt to insert this calculation before a transfer 

202 upstream of the current relation, as long as this can be done 

203 without breaking up any locked relations or changing the resulting 

204 relation content. 

205 transfer : `bool`, optional 

206 If `True` (`False` is default) and the current engine is not the 

207 preferred engine, insert a new `Transfer` before the `Calculation`. 

208 If ``backtrack`` is also true, the transfer is added only if the 

209 backtrack attempt fails. 

210 require_preferred_engine : `bool`, optional 

211 If `True` (`False` is default) and the current engine is not the 

212 preferred engine, raise `EngineError`. If ``backtrack`` is also 

213 true, the exception is only raised if the backtrack attempt fails. 

214 Ignored if ``transfer`` is true. 

215 

216 Returns 

217 ------- 

218 relation : `Relation` 

219 Relation that contains the calculated column. 

220 

221 Raises 

222 ------ 

223 ColumnError 

224 Raised if the expression requires columns that are not present in 

225 ``self.columns``, or if ``tag`` is already present in 

226 ``self.columns``. 

227 EngineError 

228 Raised if ``require_preferred_engine=True`` and it was impossible 

229 to insert this operation in the preferred engine, or if the 

230 expression was not supported by the engine. 

231 """ 

232 raise NotImplementedError() 

233 

234 @abstractmethod 

235 def chain(self, rhs: Relation) -> Relation: 

236 """Return a new relation with all rows from this relation and another. 

237 

238 This is a convenience method that constructs and applies a `Chain` 

239 operation. 

240 

241 Parameters 

242 ---------- 

243 rhs : `Relation` 

244 Other relation to chain to ``self``. Must have the same columns 

245 and engine as ``self``. 

246 

247 Returns 

248 ------- 

249 relation : `Relation` 

250 New relation with all rows from both relations. If the engine 

251 `preserves order <Engine.preserves_chain_order>` for chains, all 

252 rows from ``self`` will appear before all rows from ``rhs``, in 

253 their original order. This method never returns an operand 

254 directly, even if the other has ``max_rows==0``, as it is assumed 

255 that even relations with no rows are useful to preserve in the tree 

256 for `diagnostics <Diagnostics>`. 

257 

258 Raises 

259 ------ 

260 ColumnError 

261 Raised if the two relations do not have the same columns. 

262 EngineError 

263 Raised if the two relations do not have the same engine. 

264 RowOrderError 

265 Raised if ``self`` or ``rhs`` is unnecessarily ordered; see 

266 `expect_unordered`. 

267 """ 

268 raise NotImplementedError() 

269 

270 @abstractmethod 

271 def without_duplicates( 

272 self, 

273 *, 

274 preferred_engine: Engine | None = None, 

275 backtrack: bool = True, 

276 transfer: bool = False, 

277 require_preferred_engine: bool = False, 

278 ) -> Relation: 

279 """Return a new relation that removes any duplicate rows from this one. 

280 

281 This is a convenience method that constructs and applies a 

282 `Deduplication` operation. 

283 

284 Parameters 

285 ---------- 

286 preferred_engine : `Engine`, optional 

287 Engine that the operation would ideally be performed in. If this 

288 is not equal to ``self.engine``, the ``backtrack``, ``transfer``, 

289 and ``require_preferred_engine`` arguments control the behavior. 

290 backtrack : `bool`, optional 

291 If `True` (default) and the current engine is not the preferred 

292 engine, attempt to insert this deduplication before a transfer 

293 upstream of the current relation, as long as this can be done 

294 without breaking up any locked relations or changing the resulting 

295 relation content. 

296 transfer : `bool`, optional 

297 If `True` (`False` is default) and the current engine is not the 

298 preferred engine, insert a new `Transfer` before the 

299 `Deduplication`. If ``backtrack`` is also true, the transfer is 

300 added only if the backtrack attempt fails. 

301 require_preferred_engine : `bool`, optional 

302 If `True` (`False` is default) and the current engine is not the 

303 preferred engine, raise `EngineError`. If ``backtrack`` is also 

304 true, the exception is only raised if the backtrack attempt fails. 

305 Ignored if ``transfer`` is true. 

306 

307 Returns 

308 ------- 

309 relation : `Relation` 

310 Relation with no duplicate rows. This may be ``self`` if it can be 

311 determined that there is no duplication already, but this is not 

312 guaranteed. 

313 

314 Raises 

315 ------ 

316 EngineError 

317 Raised if ``require_preferred_engine=True`` and it was impossible 

318 to insert this operation in the preferred engine. 

319 """ 

320 raise NotImplementedError() 

321 

322 @abstractmethod 

323 def join( 

324 self, 

325 rhs: Relation, 

326 predicate: Predicate | None = None, 

327 *, 

328 backtrack: bool = True, 

329 transfer: bool = False, 

330 ) -> Relation: 

331 """Return a new relation that joins this one to the given one. 

332 

333 This is a convenience method that constructs and applies a `Join` 

334 operation, via `PartialJoin.apply`. 

335 

336 Parameters 

337 ---------- 

338 rhs : `Relation` 

339 Relation to join to ``self``. 

340 predicate : `Predicate`, optional 

341 Boolean expression that must evaluate to true in order to join a a 

342 pair of rows, in addition to an implicit equality constraint on any 

343 columns in both relations. 

344 backtrack : `bool`, optional 

345 If `True` (default) and ``self.engine != rhs.engine``, attempt to 

346 insert this join before a transfer upstream of ``self``, as long as 

347 this can be done without breaking up any locked relations or 

348 changing the resulting relation content. 

349 transfer : `bool`, optional 

350 If `True` (`False` is default) and ``self.engine != rhs.engine``, 

351 insert a new `Transfer` before the `Join`. If ``backtrack`` is 

352 also true, the transfer is added only if the backtrack attempt 

353 fails. 

354 

355 Returns 

356 ------- 

357 relation : `Relation` 

358 New relation that joins ``self`` to ``rhs``. May be ``self`` or 

359 ``rhs`` if the other is a `join identity <is_join_identity>`. 

360 

361 Raises 

362 ------ 

363 ColumnError 

364 Raised if the given predicate requires columns not present in 

365 ``self`` or ``rhs``. 

366 EngineError 

367 Raised if it was impossible to insert this operation in 

368 ``rhs.engine`` via backtracks or transfers on ``self``, or if the 

369 predicate was not supported by the engine. 

370 RowOrderError 

371 Raised if ``self`` or ``rhs`` is unnecessarily ordered; see 

372 `expect_unordered`. 

373 

374 Notes 

375 ----- 

376 This method does not treat ``self`` and ``rhs`` symmetrically: it 

377 always considers ``rhs`` fixed, and only backtracks into or considers 

378 applying transfers to ``self``. 

379 """ 

380 raise NotImplementedError() 

381 

382 @abstractmethod 

383 def materialized( 

384 self, 

385 name: str | None = None, 

386 *, 

387 name_prefix: str = "materialization", 

388 ) -> Relation: 

389 """Return a new relation that indicates that this relation's 

390 payload should be cached after it is first processed. 

391 

392 This is a convenience method that constructs and applies a 

393 `Materialization` operation. 

394 

395 Parameters 

396 ---------- 

397 name : `str`, optional 

398 Name to use for the cached payload within the engine (e.g. the name 

399 for a temporary table in SQL). If not provided, a name will be 

400 created via a call to `Engine.get_relation_name`. 

401 name_prefix : `str`, optional 

402 Prefix to pass to `Engine.get_relation_name`; ignored if ``name`` 

403 is provided. Unlike 

404 most operations, `Materialization` relations are locked by default, 

405 since they reflect user intent to mark a specific tree as 

406 cacheable. 

407 

408 Returns 

409 ------- 

410 relation : `Relation` 

411 New relation that marks its upstream tree for caching. May be 

412 ``self`` if it is already a `LeafRelation` or another 

413 materialization (in which case the given name or name prefix will 

414 be ignored). 

415 

416 Raises 

417 ------ 

418 

419 See Also 

420 -------- 

421 Processor.materialize 

422 """ 

423 raise NotImplementedError() 

424 

425 @abstractmethod 

426 def with_only_columns( 

427 self, 

428 columns: Set[ColumnTag], 

429 *, 

430 preferred_engine: Engine | None = None, 

431 backtrack: bool = True, 

432 transfer: bool = False, 

433 require_preferred_engine: bool = False, 

434 ) -> Relation: 

435 """Return a new relation whose columns are a subset of this relation's. 

436 

437 This is a convenience method that constructs and applies a `Projection` 

438 operation. 

439 

440 Parameters 

441 ---------- 

442 columns : `~collections.abc.Set` [ `ColumnTag` ] 

443 Columns to be propagated to the new relation; must be a subset of 

444 ``self.columns``. 

445 preferred_engine : `Engine`, optional 

446 Engine that the operation would ideally be performed in. If this 

447 is not equal to ``self.engine``, the ``backtrack``, ``transfer``, 

448 and ``require_preferred_engine`` arguments control the behavior. 

449 backtrack : `bool`, optional 

450 If `True` (default) and the current engine is not the preferred 

451 engine, attempt to insert this projection before a transfer 

452 upstream of the current relation, as long as this can be done 

453 without breaking up any locked relations or changing the resulting 

454 relation content. 

455 transfer : `bool`, optional 

456 If `True` (`False` is default) and the current engine is not the 

457 preferred engine, insert a new `Transfer` before the 

458 `Projection`. If ``backtrack`` is also true, the transfer is 

459 added only if the backtrack attempt fails. 

460 require_preferred_engine : `bool`, optional 

461 If `True` (`False` is default) and the current engine is not the 

462 preferred engine, raise `EngineError`. If ``backtrack`` is also 

463 true, the exception is only raised if the backtrack attempt fails. 

464 Ignored if ``transfer`` is true. 

465 

466 Returns 

467 ------- 

468 relation : `Relation` 

469 New relation with only the given columns. Will be ``self`` if 

470 ``columns == self.columns``. 

471 

472 Raises 

473 ------ 

474 ColumnError 

475 Raised if ``columns`` is not a subset of ``self.columns``. 

476 EngineError 

477 Raised if ``require_preferred_engine=True`` and it was impossible 

478 to insert this operation in the preferred engine. 

479 """ 

480 raise NotImplementedError() 

481 

482 @abstractmethod 

483 def with_rows_satisfying( 

484 self, 

485 predicate: Predicate, 

486 *, 

487 preferred_engine: Engine | None = None, 

488 backtrack: bool = True, 

489 transfer: bool = False, 

490 require_preferred_engine: bool = False, 

491 ) -> Relation: 

492 """Return a new relation that filters out rows via a boolean 

493 expression. 

494 

495 This is a convenience method that constructions and applies a 

496 `Selection` operation. 

497 

498 Parameters 

499 ---------- 

500 predicate : `Predicate` 

501 Boolean expression that evaluates to `False` for rows that should 

502 be included and `False` for rows that should be filtered out. 

503 preferred_engine : `Engine`, optional 

504 Engine that the operation would ideally be performed in. If this 

505 is not equal to ``self.engine``, the ``backtrack``, ``transfer``, 

506 and ``require_preferred_engine`` arguments control the behavior. 

507 backtrack : `bool`, optional 

508 If `True` (default) and the current engine is not the preferred 

509 engine, attempt to insert this selection before a transfer 

510 upstream of the current relation, as long as this can be done 

511 without breaking up any locked relations or changing the resulting 

512 relation content. 

513 transfer : `bool`, optional 

514 If `True` (`False` is default) and the current engine is not the 

515 preferred engine, insert a new `Transfer` before the 

516 `Selection`. If ``backtrack`` is also true, the transfer is 

517 added only if the backtrack attempt fails. 

518 require_preferred_engine : `bool`, optional 

519 If `True` (`False` is default) and the current engine is not the 

520 preferred engine, raise `EngineError`. If ``backtrack`` is also 

521 true, the exception is only raised if the backtrack attempt fails. 

522 Ignored if ``transfer`` is true. 

523 

524 Returns 

525 ------- 

526 relation : `Relation` 

527 New relation with only the rows that satisfy the given predicate. 

528 May be ``self`` if the predicate is 

529 `trivially True <Predicate.as_trivial>`. 

530 

531 Raises 

532 ------ 

533 ColumnError 

534 Raised if ``predicate.columns_required`` is not a subset of 

535 ``self.columns``. 

536 EngineError 

537 Raised if ``require_preferred_engine=True`` and it was impossible 

538 to insert this operation in the preferred engine, or if the 

539 expression was not supported by the engine. 

540 """ 

541 raise NotImplementedError() 

542 

543 @abstractmethod 

544 def __getitem__(self, key: slice) -> Relation: 

545 """Return a new relation whose rows are a slice of ``self``. 

546 

547 This is a convenience method that constructs and applies a `Slice` 

548 operation. 

549 

550 Parameters 

551 ---------- 

552 key : `slice` 

553 Start and stop for the slice. Non-unit step values are not 

554 supported. 

555 

556 Returns 

557 ------- 

558 relation : `Relation` 

559 New relation with only the rows between the given start and stop 

560 indices. May be ``self`` if ``start=0`` and ``stop=None``. If 

561 ``self`` is already a slice operation relation, the operations will 

562 be merged. 

563 

564 Raises 

565 ------ 

566 TypeError 

567 Raised if ``slice.step`` is a value other than ``1`` or ``None``. 

568 """ 

569 raise NotImplementedError() 

570 

571 @abstractmethod 

572 def sorted( 

573 self, 

574 terms: Sequence[SortTerm], 

575 *, 

576 preferred_engine: Engine | None = None, 

577 backtrack: bool = True, 

578 transfer: bool = False, 

579 require_preferred_engine: bool = False, 

580 ) -> Relation: 

581 """Return a new relation that sorts rows according to a sequence of 

582 column expressions. 

583 

584 This is a convenience method that constructs and applies a `Sort` 

585 operation. 

586 

587 Parameters 

588 ---------- 

589 terms : `~collections.abc.Sequence` [ `SortTerm` ] 

590 Ordered sequence of column expressions to sort on, with whether to 

591 apply them in ascending or descending order. 

592 preferred_engine : `Engine`, optional 

593 Engine that the operation would ideally be performed in. If this 

594 is not equal to ``self.engine``, the ``backtrack``, ``transfer``, 

595 and ``require_preferred_engine`` arguments control the behavior. 

596 backtrack : `bool`, optional 

597 If `True` (default) and the current engine is not the preferred 

598 engine, attempt to insert this sort before a transfer upstream of 

599 the current relation, as long as this can be done without breaking 

600 up any locked relations or changing the resulting relation content. 

601 transfer : `bool`, optional 

602 If `True` (`False` is default) and the current engine is not the 

603 preferred engine, insert a new `Transfer` before the `Sort`. If 

604 ``backtrack`` is also true, the transfer is added only if the 

605 backtrack attempt fails. 

606 require_preferred_engine : `bool`, optional 

607 If `True` (`False` is default) and the current engine is not the 

608 preferred engine, raise `EngineError`. If ``backtrack`` is also 

609 true, the exception is only raised if the backtrack attempt fails. 

610 Ignored if ``transfer`` is true. 

611 

612 Returns 

613 ------- 

614 relation : `Relation` 

615 New relation with sorted rows. Will be ``self`` if ``terms`` is 

616 empty. If ``self`` is already a sort operation relation, the 

617 operations will be merged by concatenating their terms, which may 

618 result in duplicate sort terms that have no effect. 

619 

620 Raises 

621 ------ 

622 ColumnError 

623 Raised if any column required by a `SortTerm` is not present in 

624 ``self.columns``. 

625 EngineError 

626 Raised if ``require_preferred_engine=True`` and it was impossible 

627 to insert this operation in the preferred engine, or if a 

628 `SortTerm` expression was not supported by the engine. 

629 """ 

630 raise NotImplementedError() 

631 

632 @abstractmethod 

633 def transferred_to(self, destination: Engine) -> Relation: 

634 """Return a new relation that transfers this relation to a new engine. 

635 

636 This is a convenience method that constructs and applies a `Transfer` 

637 operation. 

638 

639 Parameters 

640 ---------- 

641 destination : `Engine` 

642 Engine for the new relation. 

643 

644 Returns 

645 ------- 

646 relation : `Relation` 

647 New relation in the given engine. Will be ``self`` if 

648 ``self.engine == destination``. 

649 

650 Raises 

651 ------ 

652 """ 

653 raise NotImplementedError() 

654 

655 

656_M = TypeVar("_M", bound=Any) 

657 

658 

659def _copy_relation_docs(method: _M) -> _M: 

660 """Decorator that copies a docstring from the `Relation` class for the 

661 method of the same name. 

662 

663 We want to document `Relation` since that's the public interface, but we 

664 also want those docs to appear in the concrete derived classes, and that 

665 means we need to put them on the `BaseRelation` class so they can be 

666 inherited. 

667 """ 

668 method.__doc__ = getattr(Relation, method.__name__).__doc__ 

669 return method 

670 

671 

672@dataclasses.dataclass(frozen=True) 

673class BaseRelation: 

674 """An implementation-focused target class for concrete `Relation` objects. 

675 

676 This class provides method implementations for much of the `Relation` 

677 interface and is actually inherited from (unlike `Relation` itself) by all 

678 concrete relations. It should only be used outside of the 

679 `lsst.daf.relation` package when needed for `isinstance` checks. 

680 """ 

681 

682 def __init_subclass__(cls) -> None: 

683 assert ( 

684 cls.__name__ 

685 in { 

686 "LeafRelation", 

687 "UnaryOperationRelation", 

688 "BinaryOperationRelation", 

689 "MarkerRelation", 

690 } 

691 or cls.__base__.__name__ != "Relation" 

692 ), "Relation inheritance is closed to predefined types in daf_relation and MarkerRelation subclasses." 

693 

694 @property 

695 @_copy_relation_docs 

696 def is_join_identity(self: Relation) -> bool: 

697 return not self.columns and self.max_rows == 1 and self.min_rows == 1 

698 

699 @property 

700 @_copy_relation_docs 

701 def is_trivial(self: Relation) -> bool: 

702 return self.is_join_identity or self.max_rows == 0 

703 

704 @_copy_relation_docs 

705 def attach_payload(self: Relation, payload: Any) -> None: 

706 raise TypeError(f"Cannot attach payload {payload} to relation {self}.") 

707 

708 @_copy_relation_docs 

709 def with_calculated_column( 

710 self: Relation, 

711 tag: ColumnTag, 

712 expression: ColumnExpression, 

713 *, 

714 preferred_engine: Engine | None = None, 

715 backtrack: bool = True, 

716 transfer: bool = False, 

717 require_preferred_engine: bool = False, 

718 ) -> Relation: 

719 from ._operations import Calculation 

720 

721 return Calculation(tag, expression).apply( 

722 self, 

723 preferred_engine=preferred_engine, 

724 backtrack=backtrack, 

725 transfer=transfer, 

726 require_preferred_engine=require_preferred_engine, 

727 ) 

728 

729 @_copy_relation_docs 

730 def chain(self: Relation, rhs: Relation) -> Relation: 

731 from ._operations import Chain 

732 

733 return Chain().apply(self, rhs) 

734 

735 @_copy_relation_docs 

736 def without_duplicates( 

737 self: Relation, 

738 *, 

739 preferred_engine: Engine | None = None, 

740 backtrack: bool = True, 

741 transfer: bool = False, 

742 require_preferred_engine: bool = False, 

743 ) -> Relation: 

744 from ._operations import Deduplication 

745 

746 return Deduplication().apply( 

747 self, 

748 preferred_engine=preferred_engine, 

749 backtrack=backtrack, 

750 transfer=transfer, 

751 require_preferred_engine=require_preferred_engine, 

752 ) 

753 

754 @_copy_relation_docs 

755 def join( 

756 self: Relation, 

757 rhs: Relation, 

758 predicate: Predicate | None = None, 

759 *, 

760 backtrack: bool = True, 

761 transfer: bool = False, 

762 ) -> Relation: 

763 from ._columns import Predicate 

764 from ._operations import Join 

765 

766 return ( 

767 Join(predicate if predicate is not None else Predicate.literal(True)) 

768 .partial(rhs) 

769 .apply(self, backtrack=backtrack, transfer=transfer) 

770 ) 

771 

772 @_copy_relation_docs 

773 def materialized( 

774 self: Relation, 

775 name: str | None = None, 

776 *, 

777 name_prefix: str = "materialization", 

778 ) -> Relation: 

779 return self.engine.materialize(self, name, name_prefix) 

780 

781 @_copy_relation_docs 

782 def with_only_columns( 

783 self: Relation, 

784 columns: Set[ColumnTag], 

785 *, 

786 preferred_engine: Engine | None = None, 

787 backtrack: bool = True, 

788 transfer: bool = False, 

789 require_preferred_engine: bool = False, 

790 ) -> Relation: 

791 from ._operations import Projection 

792 

793 return Projection(frozenset(columns)).apply( 

794 self, 

795 preferred_engine=preferred_engine, 

796 backtrack=backtrack, 

797 transfer=transfer, 

798 require_preferred_engine=require_preferred_engine, 

799 ) 

800 

801 @_copy_relation_docs 

802 def with_rows_satisfying( 

803 self: Relation, 

804 predicate: Predicate, 

805 *, 

806 preferred_engine: Engine | None = None, 

807 backtrack: bool = True, 

808 transfer: bool = False, 

809 require_preferred_engine: bool = False, 

810 ) -> Relation: 

811 from ._operations import Selection 

812 

813 return Selection(predicate).apply( 

814 self, 

815 preferred_engine=preferred_engine, 

816 backtrack=backtrack, 

817 transfer=transfer, 

818 require_preferred_engine=require_preferred_engine, 

819 ) 

820 

821 @_copy_relation_docs 

822 def __getitem__(self: Relation, key: slice) -> Relation: 

823 from ._operations import Slice 

824 

825 if not isinstance(key, slice): 

826 raise TypeError("Only slices are supported in relation indexing.") 

827 if key.step not in (1, None): 

828 raise TypeError("Slices with non-unit step are not supported.") 

829 return Slice(key.start if key.start is not None else 0, key.stop).apply(self) 

830 

831 @_copy_relation_docs 

832 def sorted( 

833 self: Relation, 

834 terms: Sequence[SortTerm], 

835 *, 

836 preferred_engine: Engine | None = None, 

837 backtrack: bool = True, 

838 transfer: bool = False, 

839 require_preferred_engine: bool = False, 

840 ) -> Relation: 

841 from ._operations import Sort 

842 

843 return Sort(tuple(terms)).apply( 

844 self, 

845 preferred_engine=preferred_engine, 

846 backtrack=backtrack, 

847 transfer=transfer, 

848 require_preferred_engine=require_preferred_engine, 

849 ) 

850 

851 @_copy_relation_docs 

852 def transferred_to(self: Relation, destination: Engine) -> Relation: 

853 return destination.transfer(self)