Coverage for python/lsst/daf/relation/_relation.py: 64%
139 statements
« prev ^ index » next coverage.py v6.5.0, created at 2023-01-25 02:29 -0800
« prev ^ index » next coverage.py v6.5.0, created at 2023-01-25 02:29 -0800
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
24__all__ = (
25 "BaseRelation",
26 "Relation",
27)
29import dataclasses
30from abc import abstractmethod
31from collections.abc import Sequence, Set
32from typing import TYPE_CHECKING, Any, Protocol, TypeVar
34from ._columns import ColumnTag
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
42class Relation(Protocol):
43 """An abstract interface for expression trees on tabular data.
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.
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.
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.
69 See Also
70 --------
71 :ref:`lsst.daf.relation-overview`
72 """
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()
82 @property
83 @abstractmethod
84 def payload(self) -> Any:
85 """The engine-specific contents of the relation.
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()
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()
102 @property
103 @abstractmethod
104 def min_rows(self) -> int:
105 """The minimum number of rows this relation might have (`int`)."""
106 raise NotImplementedError()
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`).
114 This is `None` for relations whose size is not bounded from above.
115 """
116 raise NotImplementedError()
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()
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`).
132 Join identity relations have exactly one row and no columns.
134 See Also
135 --------
136 LeafRelation.make_join_identity
137 """
138 raise NotImplementedError()
140 @property
141 @abstractmethod
142 def is_trivial(self) -> bool:
143 """Whether this relation has no real content (`bool`).
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()
151 @abstractmethod
152 def attach_payload(self, payload: Any) -> None:
153 """Attach an engine-specific `payload` to this relation.
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.
159 Parameters
160 ----------
161 payload
162 Engine-specific content to attach.
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()
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.
186 This is a convenience method chat constructs and applies a
187 `Calculation` operation.
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.
216 Returns
217 -------
218 relation : `Relation`
219 Relation that contains the calculated column.
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()
234 @abstractmethod
235 def chain(self, rhs: Relation) -> Relation:
236 """Return a new relation with all rows from this relation and another.
238 This is a convenience method that constructs and applies a `Chain`
239 operation.
241 Parameters
242 ----------
243 rhs : `Relation`
244 Other relation to chain to ``self``. Must have the same columns
245 and engine as ``self``.
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>`.
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()
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.
281 This is a convenience method that constructs and applies a
282 `Deduplication` operation.
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.
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.
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()
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.
333 This is a convenience method that constructs and applies a `Join`
334 operation, via `PartialJoin.apply`.
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.
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>`.
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`.
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()
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.
392 This is a convenience method that constructs and applies a
393 `Materialization` operation.
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.
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).
416 Raises
417 ------
419 See Also
420 --------
421 Processor.materialize
422 """
423 raise NotImplementedError()
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.
437 This is a convenience method that constructs and applies a `Projection`
438 operation.
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.
466 Returns
467 -------
468 relation : `Relation`
469 New relation with only the given columns. Will be ``self`` if
470 ``columns == self.columns``.
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()
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.
495 This is a convenience method that constructions and applies a
496 `Selection` operation.
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.
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>`.
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()
543 @abstractmethod
544 def __getitem__(self, key: slice) -> Relation:
545 """Return a new relation whose rows are a slice of ``self``.
547 This is a convenience method that constructs and applies a `Slice`
548 operation.
550 Parameters
551 ----------
552 key : `slice`
553 Start and stop for the slice. Non-unit step values are not
554 supported.
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.
564 Raises
565 ------
566 TypeError
567 Raised if ``slice.step`` is a value other than ``1`` or ``None``.
568 """
569 raise NotImplementedError()
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.
584 This is a convenience method that constructs and applies a `Sort`
585 operation.
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.
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.
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()
632 @abstractmethod
633 def transferred_to(self, destination: Engine) -> Relation:
634 """Return a new relation that transfers this relation to a new engine.
636 This is a convenience method that constructs and applies a `Transfer`
637 operation.
639 Parameters
640 ----------
641 destination : `Engine`
642 Engine for the new relation.
644 Returns
645 -------
646 relation : `Relation`
647 New relation in the given engine. Will be ``self`` if
648 ``self.engine == destination``.
650 Raises
651 ------
652 """
653 raise NotImplementedError()
656_M = TypeVar("_M", bound=Any)
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.
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
672@dataclasses.dataclass(frozen=True)
673class BaseRelation:
674 """An implementation-focused target class for concrete `Relation` objects.
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 """
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 ), (
693 "Relation inheritance is closed to predefined types "
694 "in daf_relation and MarkerRelation subclasses."
695 )
697 @property
698 @_copy_relation_docs
699 def is_join_identity(self: Relation) -> bool:
700 return not self.columns and self.max_rows == 1 and self.min_rows == 1
702 @property
703 @_copy_relation_docs
704 def is_trivial(self: Relation) -> bool:
705 return self.is_join_identity or self.max_rows == 0
707 @_copy_relation_docs
708 def attach_payload(self: Relation, payload: Any) -> None:
709 raise TypeError(f"Cannot attach payload {payload} to relation {self}.")
711 @_copy_relation_docs
712 def with_calculated_column(
713 self: Relation,
714 tag: ColumnTag,
715 expression: ColumnExpression,
716 *,
717 preferred_engine: Engine | None = None,
718 backtrack: bool = True,
719 transfer: bool = False,
720 require_preferred_engine: bool = False,
721 ) -> Relation:
722 from ._operations import Calculation
724 return Calculation(tag, expression).apply(
725 self,
726 preferred_engine=preferred_engine,
727 backtrack=backtrack,
728 transfer=transfer,
729 require_preferred_engine=require_preferred_engine,
730 )
732 @_copy_relation_docs
733 def chain(self: Relation, rhs: Relation) -> Relation:
734 from ._operations import Chain
736 return Chain().apply(self, rhs)
738 @_copy_relation_docs
739 def without_duplicates(
740 self: Relation,
741 *,
742 preferred_engine: Engine | None = None,
743 backtrack: bool = True,
744 transfer: bool = False,
745 require_preferred_engine: bool = False,
746 ) -> Relation:
747 from ._operations import Deduplication
749 return Deduplication().apply(
750 self,
751 preferred_engine=preferred_engine,
752 backtrack=backtrack,
753 transfer=transfer,
754 require_preferred_engine=require_preferred_engine,
755 )
757 @_copy_relation_docs
758 def join(
759 self: Relation,
760 rhs: Relation,
761 predicate: Predicate | None = None,
762 *,
763 backtrack: bool = True,
764 transfer: bool = False,
765 ) -> Relation:
766 from ._columns import Predicate
767 from ._operations import Join
769 return (
770 Join(predicate if predicate is not None else Predicate.literal(True))
771 .partial(rhs)
772 .apply(self, backtrack=backtrack, transfer=transfer)
773 )
775 @_copy_relation_docs
776 def materialized(
777 self: Relation,
778 name: str | None = None,
779 *,
780 name_prefix: str = "materialization",
781 ) -> Relation:
782 return self.engine.materialize(self, name, name_prefix)
784 @_copy_relation_docs
785 def with_only_columns(
786 self: Relation,
787 columns: Set[ColumnTag],
788 *,
789 preferred_engine: Engine | None = None,
790 backtrack: bool = True,
791 transfer: bool = False,
792 require_preferred_engine: bool = False,
793 ) -> Relation:
794 from ._operations import Projection
796 return Projection(frozenset(columns)).apply(
797 self,
798 preferred_engine=preferred_engine,
799 backtrack=backtrack,
800 transfer=transfer,
801 require_preferred_engine=require_preferred_engine,
802 )
804 @_copy_relation_docs
805 def with_rows_satisfying(
806 self: Relation,
807 predicate: Predicate,
808 *,
809 preferred_engine: Engine | None = None,
810 backtrack: bool = True,
811 transfer: bool = False,
812 require_preferred_engine: bool = False,
813 ) -> Relation:
814 from ._operations import Selection
816 return Selection(predicate).apply(
817 self,
818 preferred_engine=preferred_engine,
819 backtrack=backtrack,
820 transfer=transfer,
821 require_preferred_engine=require_preferred_engine,
822 )
824 @_copy_relation_docs
825 def __getitem__(self: Relation, key: slice) -> Relation:
826 from ._operations import Slice
828 if not isinstance(key, slice):
829 raise TypeError("Only slices are supported in relation indexing.")
830 if key.step not in (1, None):
831 raise TypeError("Slices with non-unit step are not supported.")
832 return Slice(key.start if key.start is not None else 0, key.stop).apply(self)
834 @_copy_relation_docs
835 def sorted(
836 self: Relation,
837 terms: Sequence[SortTerm],
838 *,
839 preferred_engine: Engine | None = None,
840 backtrack: bool = True,
841 transfer: bool = False,
842 require_preferred_engine: bool = False,
843 ) -> Relation:
844 from ._operations import Sort
846 return Sort(tuple(terms)).apply(
847 self,
848 preferred_engine=preferred_engine,
849 backtrack=backtrack,
850 transfer=transfer,
851 require_preferred_engine=require_preferred_engine,
852 )
854 @_copy_relation_docs
855 def transferred_to(self: Relation, destination: Engine) -> Relation:
856 return destination.transfer(self)