pixeltable 0.2.21__py3-none-any.whl → 0.2.22__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of pixeltable might be problematic. Click here for more details.

Files changed (82) hide show
  1. pixeltable/__version__.py +2 -2
  2. pixeltable/catalog/__init__.py +1 -1
  3. pixeltable/catalog/column.py +37 -11
  4. pixeltable/catalog/globals.py +18 -0
  5. pixeltable/catalog/insertable_table.py +6 -4
  6. pixeltable/catalog/table.py +19 -3
  7. pixeltable/catalog/table_version.py +34 -14
  8. pixeltable/catalog/view.py +16 -17
  9. pixeltable/dataframe.py +7 -8
  10. pixeltable/env.py +5 -0
  11. pixeltable/exec/__init__.py +0 -1
  12. pixeltable/exec/aggregation_node.py +6 -3
  13. pixeltable/exec/cache_prefetch_node.py +1 -1
  14. pixeltable/exec/data_row_batch.py +2 -19
  15. pixeltable/exec/exec_node.py +2 -1
  16. pixeltable/exec/expr_eval_node.py +17 -10
  17. pixeltable/exec/in_memory_data_node.py +6 -3
  18. pixeltable/exec/sql_node.py +24 -25
  19. pixeltable/exprs/arithmetic_expr.py +3 -1
  20. pixeltable/exprs/array_slice.py +7 -7
  21. pixeltable/exprs/column_property_ref.py +37 -10
  22. pixeltable/exprs/column_ref.py +93 -14
  23. pixeltable/exprs/comparison.py +5 -5
  24. pixeltable/exprs/compound_predicate.py +8 -7
  25. pixeltable/exprs/data_row.py +27 -18
  26. pixeltable/exprs/expr.py +53 -52
  27. pixeltable/exprs/expr_set.py +5 -0
  28. pixeltable/exprs/function_call.py +32 -16
  29. pixeltable/exprs/globals.py +4 -1
  30. pixeltable/exprs/in_predicate.py +8 -7
  31. pixeltable/exprs/inline_expr.py +4 -4
  32. pixeltable/exprs/is_null.py +4 -4
  33. pixeltable/exprs/json_mapper.py +11 -12
  34. pixeltable/exprs/json_path.py +5 -10
  35. pixeltable/exprs/literal.py +5 -5
  36. pixeltable/exprs/method_ref.py +5 -4
  37. pixeltable/exprs/object_ref.py +2 -1
  38. pixeltable/exprs/row_builder.py +88 -36
  39. pixeltable/exprs/rowid_ref.py +12 -11
  40. pixeltable/exprs/similarity_expr.py +12 -7
  41. pixeltable/exprs/sql_element_cache.py +7 -5
  42. pixeltable/exprs/type_cast.py +8 -6
  43. pixeltable/exprs/variable.py +5 -4
  44. pixeltable/func/aggregate_function.py +1 -1
  45. pixeltable/func/function.py +11 -10
  46. pixeltable/functions/__init__.py +2 -2
  47. pixeltable/functions/globals.py +5 -7
  48. pixeltable/functions/huggingface.py +19 -20
  49. pixeltable/functions/llama_cpp.py +106 -0
  50. pixeltable/functions/ollama.py +147 -0
  51. pixeltable/functions/replicate.py +72 -0
  52. pixeltable/functions/string.py +9 -0
  53. pixeltable/globals.py +12 -20
  54. pixeltable/index/btree.py +16 -3
  55. pixeltable/index/embedding_index.py +4 -4
  56. pixeltable/io/__init__.py +1 -2
  57. pixeltable/io/fiftyone.py +178 -0
  58. pixeltable/io/globals.py +96 -2
  59. pixeltable/iterators/base.py +3 -2
  60. pixeltable/iterators/document.py +1 -1
  61. pixeltable/iterators/video.py +120 -63
  62. pixeltable/metadata/__init__.py +1 -1
  63. pixeltable/metadata/converters/convert_21.py +34 -0
  64. pixeltable/metadata/converters/util.py +45 -4
  65. pixeltable/metadata/notes.py +1 -0
  66. pixeltable/metadata/schema.py +8 -0
  67. pixeltable/plan.py +16 -14
  68. pixeltable/py.typed +0 -0
  69. pixeltable/store.py +7 -2
  70. pixeltable/tool/create_test_video.py +1 -1
  71. pixeltable/tool/embed_udf.py +1 -1
  72. pixeltable/tool/mypy_plugin.py +28 -5
  73. pixeltable/type_system.py +17 -1
  74. pixeltable/utils/documents.py +15 -1
  75. pixeltable/utils/formatter.py +9 -10
  76. {pixeltable-0.2.21.dist-info → pixeltable-0.2.22.dist-info}/METADATA +46 -10
  77. pixeltable-0.2.22.dist-info/RECORD +153 -0
  78. pixeltable/exec/media_validation_node.py +0 -43
  79. pixeltable-0.2.21.dist-info/RECORD +0 -148
  80. {pixeltable-0.2.21.dist-info → pixeltable-0.2.22.dist-info}/LICENSE +0 -0
  81. {pixeltable-0.2.21.dist-info → pixeltable-0.2.22.dist-info}/WHEEL +0 -0
  82. {pixeltable-0.2.21.dist-info → pixeltable-0.2.22.dist-info}/entry_points.txt +0 -0
@@ -1,20 +1,21 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import operator
4
- from typing import Optional, List, Any, Dict, Callable
4
+ from typing import Any, Callable, Optional
5
5
 
6
6
  import sqlalchemy as sql
7
7
 
8
+ import pixeltable.type_system as ts
9
+
8
10
  from .data_row import DataRow
9
11
  from .expr import Expr
10
12
  from .globals import LogicalOperator
11
13
  from .row_builder import RowBuilder
12
14
  from .sql_element_cache import SqlElementCache
13
- import pixeltable.type_system as ts
14
15
 
15
16
 
16
17
  class CompoundPredicate(Expr):
17
- def __init__(self, operator: LogicalOperator, operands: List[Expr]):
18
+ def __init__(self, operator: LogicalOperator, operands: list[Expr]):
18
19
  super().__init__(ts.BoolType())
19
20
  self.operator = operator
20
21
  # operands are stored in self.components
@@ -23,7 +24,7 @@ class CompoundPredicate(Expr):
23
24
  self.components = operands
24
25
  else:
25
26
  assert len(operands) > 1
26
- self.operands: List[Expr] = []
27
+ self.operands: list[Expr] = []
27
28
  for operand in operands:
28
29
  self._merge_operand(operand)
29
30
 
@@ -35,7 +36,7 @@ class CompoundPredicate(Expr):
35
36
  return f' {self.operator} '.join([f'({e})' for e in self.components])
36
37
 
37
38
  @classmethod
38
- def make_conjunction(cls, operands: List[Expr]) -> Optional[Expr]:
39
+ def make_conjunction(cls, operands: list[Expr]) -> Optional[Expr]:
39
40
  if len(operands) == 0:
40
41
  return None
41
42
  if len(operands) == 1:
@@ -89,11 +90,11 @@ class CompoundPredicate(Expr):
89
90
  val = op_function(val, data_row[op.slot_idx])
90
91
  data_row[self.slot_idx] = val
91
92
 
92
- def _as_dict(self) -> Dict:
93
+ def _as_dict(self) -> dict:
93
94
  return {'operator': self.operator.value, **super()._as_dict()}
94
95
 
95
96
  @classmethod
96
- def _from_dict(cls, d: Dict, components: List[Expr]) -> Expr:
97
+ def _from_dict(cls, d: dict, components: list[Expr]) -> CompoundPredicate:
97
98
  assert 'operator' in d
98
99
  return cls(LogicalOperator(d['operator']), components)
99
100
 
@@ -4,13 +4,13 @@ import datetime
4
4
  import io
5
5
  import urllib.parse
6
6
  import urllib.request
7
- from typing import Optional, List, Any, Tuple
7
+ from typing import Any, Optional
8
8
 
9
- import sqlalchemy as sql
10
- import pgvector.sqlalchemy
9
+ import numpy as np
10
+ import pgvector.sqlalchemy # type: ignore[import-untyped]
11
11
  import PIL
12
12
  import PIL.Image
13
- import numpy as np
13
+ import sqlalchemy as sql
14
14
 
15
15
  from pixeltable import env
16
16
 
@@ -57,7 +57,7 @@ class DataRow:
57
57
  # - None if vals[i] is not a media type or if there is no local file yet for file_urls[i]
58
58
  file_paths: list[Optional[str]]
59
59
 
60
- def __init__(self, size: int, img_slot_idxs: List[int], media_slot_idxs: List[int], array_slot_idxs: List[int]):
60
+ def __init__(self, size: int, img_slot_idxs: list[int], media_slot_idxs: list[int], array_slot_idxs: list[int]):
61
61
  self.vals = [None] * size
62
62
  self.has_val = [False] * size
63
63
  self.excs = [None] * size
@@ -89,27 +89,35 @@ class DataRow:
89
89
  target.file_urls = self.file_urls.copy()
90
90
  target.file_paths = self.file_paths.copy()
91
91
 
92
- def set_pk(self, pk: Tuple[int, ...]) -> None:
92
+ def set_pk(self, pk: tuple[int, ...]) -> None:
93
93
  self.pk = pk
94
94
 
95
- def has_exc(self, slot_idx: int) -> bool:
96
- return self.excs[slot_idx] is not None
95
+ def has_exc(self, slot_idx: Optional[int] = None) -> bool:
96
+ """
97
+ Returns True if an exception has been set for the given slot index, or for any slot index if slot_idx is None
98
+ """
99
+ if slot_idx is not None:
100
+ return self.excs[slot_idx] is not None
101
+ return any(exc is not None for exc in self.excs)
97
102
 
98
- def get_exc(self, slot_idx: int) -> Exception:
99
- assert self.has_val[slot_idx] is False
100
- assert self.excs[slot_idx] is not None
103
+ def get_exc(self, slot_idx: int) -> Optional[Exception]:
101
104
  return self.excs[slot_idx]
102
105
 
106
+ def get_first_exc(self) -> Optional[Exception]:
107
+ for exc in self.excs:
108
+ if exc is not None:
109
+ return exc
110
+ return None
111
+
103
112
  def set_exc(self, slot_idx: int, exc: Exception) -> None:
104
113
  assert self.excs[slot_idx] is None
105
114
  self.excs[slot_idx] = exc
106
115
 
107
- if self.has_val[slot_idx]:
108
- # eg. during validation, where contents of file is found invalid
109
- self.has_val[slot_idx] = False
110
- self.vals[slot_idx] = None
111
- self.file_paths[slot_idx] = None
112
- self.file_urls[slot_idx] = None
116
+ # an exception means the value is None
117
+ self.has_val[slot_idx] = True
118
+ self.vals[slot_idx] = None
119
+ self.file_paths[slot_idx] = None
120
+ self.file_urls[slot_idx] = None
113
121
 
114
122
  def __len__(self) -> int:
115
123
  return len(self.vals)
@@ -124,6 +132,7 @@ class DataRow:
124
132
 
125
133
  if self.file_urls[index] is not None and index in self.img_slot_idxs:
126
134
  # if we need to load this from a file, it should have been materialized locally
135
+ # TODO this fails if the url was instantiated dynamically using astype()
127
136
  assert self.file_paths[index] is not None
128
137
  if self.vals[index] is None:
129
138
  self.vals[index] = PIL.Image.open(self.file_paths[index])
@@ -231,7 +240,7 @@ class DataRow:
231
240
  self.vals[index] = None
232
241
 
233
242
  @property
234
- def rowid(self) -> Tuple[int]:
243
+ def rowid(self) -> tuple[int, ...]:
235
244
  return self.pk[:-1]
236
245
 
237
246
  @property
pixeltable/exprs/expr.py CHANGED
@@ -13,7 +13,6 @@ from uuid import UUID
13
13
  import sqlalchemy as sql
14
14
  from typing_extensions import _AnnotatedAlias, Self
15
15
 
16
- import pixeltable
17
16
  import pixeltable.catalog as catalog
18
17
  import pixeltable.exceptions as excs
19
18
  import pixeltable.func as func
@@ -91,7 +90,7 @@ class Expr(abc.ABC):
91
90
  result = c_scope
92
91
  return result
93
92
 
94
- def bind_rel_paths(self, mapper: Optional['pixeltable.exprs.JsonMapper'] = None) -> None:
93
+ def bind_rel_paths(self, mapper: Optional['exprs.JsonMapper'] = None) -> None:
95
94
  """
96
95
  Binds relative JsonPaths to mapper.
97
96
  This needs to be done in a separate phase after __init__(), because RelativeJsonPath()(-1) cannot be resolved
@@ -121,7 +120,7 @@ class Expr(abc.ABC):
121
120
  return False
122
121
  return self._equals(other)
123
122
 
124
- def _equals(self, other: Expr) -> bool:
123
+ def _equals(self, other: Self) -> bool:
125
124
  # we already compared the type and components in equals(); subclasses that require additional comparisons
126
125
  # override this
127
126
  return True
@@ -232,12 +231,6 @@ class Expr(abc.ABC):
232
231
  return self._retarget(tbl_versions)
233
232
 
234
233
  def _retarget(self, tbl_versions: dict[UUID, catalog.TableVersion]) -> Self:
235
- from .column_ref import ColumnRef
236
- if isinstance(self, ColumnRef):
237
- target = tbl_versions[self.col.tbl.id]
238
- assert self.col.id in target.cols_by_id
239
- col = target.cols_by_id[self.col.id]
240
- return ColumnRef(col)
241
234
  for i in range (len(self.components)):
242
235
  self.components[i] = self.components[i]._retarget(tbl_versions)
243
236
  return self
@@ -289,22 +282,24 @@ class Expr(abc.ABC):
289
282
  for c in self.components:
290
283
  yield from c.subexprs(expr_class=expr_class, filter=filter, traverse_matches=traverse_matches)
291
284
  if is_match:
292
- yield self
285
+ yield self # type: ignore[misc]
293
286
 
294
287
  @overload
288
+ @classmethod
295
289
  def list_subexprs(
296
- expr_list: Iterable[Expr], *, filter: Optional[Callable[[Expr], bool]] = None, traverse_matches: bool = True
290
+ cls, expr_list: Iterable[Expr], *, filter: Optional[Callable[[Expr], bool]] = None, traverse_matches: bool = True
297
291
  ) -> Iterator[Expr]: ...
298
292
 
299
293
  @overload
294
+ @classmethod
300
295
  def list_subexprs(
301
- expr_list: list[Expr], expr_class: type[T], filter: Optional[Callable[[Expr], bool]] = None,
296
+ cls, expr_list: Iterable[Expr], expr_class: type[T], filter: Optional[Callable[[Expr], bool]] = None,
302
297
  traverse_matches: bool = True
303
298
  ) -> Iterator[T]: ...
304
299
 
305
300
  @classmethod
306
301
  def list_subexprs(
307
- cls, expr_list: list[Expr], expr_class: Optional[type[T]] = None,
302
+ cls, expr_list: Iterable[Expr], expr_class: Optional[type[T]] = None,
308
303
  filter: Optional[Callable[[Expr], bool]] = None, traverse_matches: bool = True
309
304
  ) -> Iterator[T]:
310
305
  """Produce subexprs for all exprs in list. Can contain duplicates."""
@@ -329,11 +324,8 @@ class Expr(abc.ABC):
329
324
  return {ref.col.tbl.id for ref in self.subexprs(ColumnRef)} | {ref.tbl.id for ref in self.subexprs(RowidRef)}
330
325
 
331
326
  @classmethod
332
- def list_tbl_ids(cls, expr_list: list[Expr]) -> set[UUID]:
333
- ids: set[UUID] = set()
334
- for e in expr_list:
335
- ids.update(e.tbl_ids())
336
- return ids
327
+ def all_tbl_ids(cls, exprs_: Iterable[Expr]) -> set[UUID]:
328
+ return set(tbl_id for e in exprs_ for tbl_id in e.tbl_ids())
337
329
 
338
330
  @classmethod
339
331
  def get_refd_columns(cls, expr_dict: dict[str, Any]) -> list[catalog.Column]:
@@ -383,7 +375,7 @@ class Expr(abc.ABC):
383
375
  pass
384
376
 
385
377
  @abc.abstractmethod
386
- def eval(self, data_row: DataRow, row_builder: 'pixeltable.exprs.RowBuilder') -> None:
378
+ def eval(self, data_row: DataRow, row_builder: 'exprs.RowBuilder') -> None:
387
379
  """
388
380
  Compute the expr value for data_row and store the result in data_row[slot_idx].
389
381
  Not called if sql_expr() != None (exception: Literal).
@@ -449,18 +441,18 @@ class Expr(abc.ABC):
449
441
  def _from_dict(cls, d: dict, components: list[Expr]) -> Self:
450
442
  assert False, 'not implemented'
451
443
 
452
- def isin(self, value_set: Any) -> 'pixeltable.exprs.InPredicate':
444
+ def isin(self, value_set: Any) -> 'exprs.InPredicate':
453
445
  from .in_predicate import InPredicate
454
446
  if isinstance(value_set, Expr):
455
447
  return InPredicate(self, value_set_expr=value_set)
456
448
  else:
457
449
  return InPredicate(self, value_set_literal=value_set)
458
450
 
459
- def astype(self, new_type: Union[ts.ColumnType, type, _AnnotatedAlias]) -> 'pixeltable.exprs.TypeCast':
451
+ def astype(self, new_type: Union[ts.ColumnType, type, _AnnotatedAlias]) -> 'exprs.TypeCast':
460
452
  from pixeltable.exprs import TypeCast
461
453
  return TypeCast(self, ts.ColumnType.normalize_type(new_type))
462
454
 
463
- def apply(self, fn: Callable, *, col_type: Union[ts.ColumnType, type, _AnnotatedAlias, None] = None) -> 'pixeltable.exprs.FunctionCall':
455
+ def apply(self, fn: Callable, *, col_type: Union[ts.ColumnType, type, _AnnotatedAlias, None] = None) -> 'exprs.FunctionCall':
464
456
  if col_type is not None:
465
457
  col_type = ts.ColumnType.normalize_type(col_type)
466
458
  function = self._make_applicator_function(fn, col_type)
@@ -475,23 +467,32 @@ class Expr(abc.ABC):
475
467
  ]
476
468
  return attrs
477
469
 
470
+ def __call__(self, *args: Any, **kwargs: Any) -> Any:
471
+ raise NotImplementedError(f'Expression of type `{type(self)}` is not callable')
472
+
478
473
  def __getitem__(self, index: object) -> Expr:
479
474
  if self.col_type.is_json_type():
480
475
  from .json_path import JsonPath
481
- return JsonPath(self).__getitem__(index)
476
+ return JsonPath(self)[index]
482
477
  if self.col_type.is_array_type():
483
478
  from .array_slice import ArraySlice
479
+ if not isinstance(index, tuple):
480
+ index = (index,)
481
+ if any(not isinstance(i, (int, slice)) for i in index):
482
+ raise AttributeError(f'Invalid array indices: {index}')
484
483
  return ArraySlice(self, index)
485
484
  raise AttributeError(f'Type {self.col_type} is not subscriptable')
486
485
 
487
- def __getattr__(self, name: str) -> Union['pixeltable.exprs.MethodRef', 'pixeltable.exprs.FunctionCall', 'pixeltable.exprs.JsonPath']:
486
+ def __getattr__(self, name: str) -> 'exprs.Expr':
488
487
  """
489
488
  ex.: <img col>.rotate(60)
490
489
  """
490
+ from .json_path import JsonPath
491
+ from .method_ref import MethodRef
491
492
  if self.col_type.is_json_type():
492
- return pixeltable.exprs.JsonPath(self).__getattr__(name)
493
+ return JsonPath(self).__getattr__(name)
493
494
  else:
494
- method_ref = pixeltable.exprs.MethodRef(self, name)
495
+ method_ref = MethodRef(self, name)
495
496
  if method_ref.fn.is_property:
496
497
  # Marked as a property, so autoinvoke the method to obtain a `FunctionCall`
497
498
  assert method_ref.fn.arity == 1
@@ -504,32 +505,32 @@ class Expr(abc.ABC):
504
505
  raise TypeError(
505
506
  'Pixeltable expressions cannot be used in conjunction with Python boolean operators (and/or/not)')
506
507
 
507
- def __lt__(self, other: object) -> 'pixeltable.exprs.Comparison':
508
+ def __lt__(self, other: object) -> 'exprs.Comparison':
508
509
  return self._make_comparison(ComparisonOperator.LT, other)
509
510
 
510
- def __le__(self, other: object) -> 'pixeltable.exprs.Comparison':
511
+ def __le__(self, other: object) -> 'exprs.Comparison':
511
512
  return self._make_comparison(ComparisonOperator.LE, other)
512
513
 
513
- def __eq__(self, other: object) -> 'pixeltable.exprs.Comparison':
514
+ def __eq__(self, other: object) -> 'exprs.Expr': # type: ignore[override]
514
515
  if other is None:
515
516
  from .is_null import IsNull
516
517
  return IsNull(self)
517
518
  return self._make_comparison(ComparisonOperator.EQ, other)
518
519
 
519
- def __ne__(self, other: object) -> 'pixeltable.exprs.Comparison':
520
+ def __ne__(self, other: object) -> 'exprs.Expr': # type: ignore[override]
520
521
  if other is None:
521
522
  from .compound_predicate import CompoundPredicate
522
523
  from .is_null import IsNull
523
524
  return CompoundPredicate(LogicalOperator.NOT, [IsNull(self)])
524
525
  return self._make_comparison(ComparisonOperator.NE, other)
525
526
 
526
- def __gt__(self, other: object) -> 'pixeltable.exprs.Comparison':
527
+ def __gt__(self, other: object) -> 'exprs.Comparison':
527
528
  return self._make_comparison(ComparisonOperator.GT, other)
528
529
 
529
- def __ge__(self, other: object) -> 'pixeltable.exprs.Comparison':
530
+ def __ge__(self, other: object) -> 'exprs.Comparison':
530
531
  return self._make_comparison(ComparisonOperator.GE, other)
531
532
 
532
- def _make_comparison(self, op: ComparisonOperator, other: object) -> 'pixeltable.exprs.Comparison':
533
+ def _make_comparison(self, op: ComparisonOperator, other: object) -> 'exprs.Comparison':
533
534
  """
534
535
  other: Union[Expr, LiteralPythonTypes]
535
536
  """
@@ -539,49 +540,49 @@ class Expr(abc.ABC):
539
540
  if isinstance(other, Expr):
540
541
  return Comparison(op, self, other)
541
542
  if isinstance(other, typing.get_args(LiteralPythonTypes)):
542
- return Comparison(op, self, Literal(other)) # type: ignore[arg-type]
543
+ return Comparison(op, self, Literal(other))
543
544
  raise TypeError(f'Other must be Expr or literal: {type(other)}')
544
545
 
545
- def __neg__(self) -> 'pixeltable.exprs.ArithmeticExpr':
546
+ def __neg__(self) -> 'exprs.ArithmeticExpr':
546
547
  return self._make_arithmetic_expr(ArithmeticOperator.MUL, -1)
547
548
 
548
- def __add__(self, other: object) -> 'pixeltable.exprs.ArithmeticExpr':
549
+ def __add__(self, other: object) -> 'exprs.ArithmeticExpr':
549
550
  return self._make_arithmetic_expr(ArithmeticOperator.ADD, other)
550
551
 
551
- def __sub__(self, other: object) -> 'pixeltable.exprs.ArithmeticExpr':
552
+ def __sub__(self, other: object) -> 'exprs.ArithmeticExpr':
552
553
  return self._make_arithmetic_expr(ArithmeticOperator.SUB, other)
553
554
 
554
- def __mul__(self, other: object) -> 'pixeltable.exprs.ArithmeticExpr':
555
+ def __mul__(self, other: object) -> 'exprs.ArithmeticExpr':
555
556
  return self._make_arithmetic_expr(ArithmeticOperator.MUL, other)
556
557
 
557
- def __truediv__(self, other: object) -> 'pixeltable.exprs.ArithmeticExpr':
558
+ def __truediv__(self, other: object) -> 'exprs.ArithmeticExpr':
558
559
  return self._make_arithmetic_expr(ArithmeticOperator.DIV, other)
559
560
 
560
- def __mod__(self, other: object) -> 'pixeltable.exprs.ArithmeticExpr':
561
+ def __mod__(self, other: object) -> 'exprs.ArithmeticExpr':
561
562
  return self._make_arithmetic_expr(ArithmeticOperator.MOD, other)
562
563
 
563
- def __floordiv__(self, other: object) -> 'pixeltable.exprs.ArithmeticExpr':
564
+ def __floordiv__(self, other: object) -> 'exprs.ArithmeticExpr':
564
565
  return self._make_arithmetic_expr(ArithmeticOperator.FLOORDIV, other)
565
566
 
566
- def __radd__(self, other: object) -> 'pixeltable.exprs.ArithmeticExpr':
567
+ def __radd__(self, other: object) -> 'exprs.ArithmeticExpr':
567
568
  return self._rmake_arithmetic_expr(ArithmeticOperator.ADD, other)
568
569
 
569
- def __rsub__(self, other: object) -> 'pixeltable.exprs.ArithmeticExpr':
570
+ def __rsub__(self, other: object) -> 'exprs.ArithmeticExpr':
570
571
  return self._rmake_arithmetic_expr(ArithmeticOperator.SUB, other)
571
572
 
572
- def __rmul__(self, other: object) -> 'pixeltable.exprs.ArithmeticExpr':
573
+ def __rmul__(self, other: object) -> 'exprs.ArithmeticExpr':
573
574
  return self._rmake_arithmetic_expr(ArithmeticOperator.MUL, other)
574
575
 
575
- def __rtruediv__(self, other: object) -> 'pixeltable.exprs.ArithmeticExpr':
576
+ def __rtruediv__(self, other: object) -> 'exprs.ArithmeticExpr':
576
577
  return self._rmake_arithmetic_expr(ArithmeticOperator.DIV, other)
577
578
 
578
- def __rmod__(self, other: object) -> 'pixeltable.exprs.ArithmeticExpr':
579
+ def __rmod__(self, other: object) -> 'exprs.ArithmeticExpr':
579
580
  return self._rmake_arithmetic_expr(ArithmeticOperator.MOD, other)
580
581
 
581
- def __rfloordiv__(self, other: object) -> 'pixeltable.exprs.ArithmeticExpr':
582
+ def __rfloordiv__(self, other: object) -> 'exprs.ArithmeticExpr':
582
583
  return self._rmake_arithmetic_expr(ArithmeticOperator.FLOORDIV, other)
583
584
 
584
- def _make_arithmetic_expr(self, op: ArithmeticOperator, other: object) -> 'pixeltable.exprs.ArithmeticExpr':
585
+ def _make_arithmetic_expr(self, op: ArithmeticOperator, other: object) -> 'exprs.ArithmeticExpr':
585
586
  """
586
587
  other: Union[Expr, LiteralPythonTypes]
587
588
  """
@@ -591,10 +592,10 @@ class Expr(abc.ABC):
591
592
  if isinstance(other, Expr):
592
593
  return ArithmeticExpr(op, self, other)
593
594
  if isinstance(other, typing.get_args(LiteralPythonTypes)):
594
- return ArithmeticExpr(op, self, Literal(other)) # type: ignore[arg-type]
595
+ return ArithmeticExpr(op, self, Literal(other))
595
596
  raise TypeError(f'Other must be Expr or literal: {type(other)}')
596
597
 
597
- def _rmake_arithmetic_expr(self, op: ArithmeticOperator, other: object) -> 'pixeltable.exprs.ArithmeticExpr':
598
+ def _rmake_arithmetic_expr(self, op: ArithmeticOperator, other: object) -> 'exprs.ArithmeticExpr':
598
599
  """
599
600
  Right-handed version of _make_arithmetic_expr. other must be a literal; if it were an Expr,
600
601
  the operation would have already been evaluated in its left-handed form.
@@ -604,7 +605,7 @@ class Expr(abc.ABC):
604
605
  from .literal import Literal
605
606
  assert not isinstance(other, Expr) # Else the left-handed form would have evaluated first
606
607
  if isinstance(other, typing.get_args(LiteralPythonTypes)):
607
- return ArithmeticExpr(op, Literal(other), self) # type: ignore[arg-type]
608
+ return ArithmeticExpr(op, Literal(other), self)
608
609
  raise TypeError(f'Other must be Expr or literal: {type(other)}')
609
610
 
610
611
  def __and__(self, other: object) -> Expr:
@@ -639,7 +640,7 @@ class Expr(abc.ABC):
639
640
  else:
640
641
  return [], self
641
642
 
642
- def _make_applicator_function(self, fn: Callable, col_type: Optional[ts.ColumnType]) -> 'pixeltable.func.Function':
643
+ def _make_applicator_function(self, fn: Callable, col_type: Optional[ts.ColumnType]) -> 'func.Function':
643
644
  """
644
645
  Creates a unary pixeltable `Function` that encapsulates a python `Callable`. The result type of
645
646
  the new `Function` is given by `col_type`, and its parameter type will be `self.col_type`.
@@ -66,3 +66,8 @@ class ExprSet(Generic[T]):
66
66
 
67
67
  def __sub__(self, other: ExprSet[T]) -> ExprSet[T]:
68
68
  return self.difference(other)
69
+
70
+ def __add__(self, other: ExprSet) -> ExprSet:
71
+ exprs = self.exprs.copy()
72
+ exprs.update(other.exprs)
73
+ return ExprSet(exprs.values())
@@ -50,14 +50,29 @@ class FunctionCall(Expr):
50
50
  if group_by_clause is None:
51
51
  group_by_clause = []
52
52
  signature = fn.signature
53
- super().__init__(fn.call_return_type(bound_args))
53
+ return_type = fn.call_return_type(bound_args)
54
54
  self.fn = fn
55
55
  self.is_method_call = is_method_call
56
56
  self.normalize_args(fn.name, signature, bound_args)
57
57
 
58
+ # If `return_type` is non-nullable, but the function call has a nullable input to any of its non-nullable
59
+ # parameters, then we need to make it nullable. This is because Pixeltable defaults a function output to
60
+ # `None` when any of its non-nullable inputs are `None`.
61
+ for arg_name, arg in bound_args.items():
62
+ param = signature.parameters[arg_name]
63
+ if (
64
+ param.col_type is not None and not param.col_type.nullable
65
+ and isinstance(arg, Expr) and arg.col_type.nullable
66
+ ):
67
+ return_type = return_type.copy(nullable=True)
68
+ break
69
+
70
+ super().__init__(return_type)
71
+
58
72
  self.agg_init_args = {}
59
73
  if self.is_agg_fn_call:
60
74
  # we separate out the init args for the aggregator
75
+ assert isinstance(fn, func.AggregateFunction)
61
76
  self.agg_init_args = {
62
77
  arg_name: arg for arg_name, arg in bound_args.items() if arg_name in fn.init_param_names
63
78
  }
@@ -71,17 +86,17 @@ class FunctionCall(Expr):
71
86
  self.arg_types = []
72
87
  self.kwarg_types = {}
73
88
  # the prefix of parameters that are bound can be passed by position
74
- for param in fn.signature.py_signature.parameters.values():
75
- if param.name not in bound_args or param.kind == inspect.Parameter.KEYWORD_ONLY:
89
+ for py_param in fn.signature.py_signature.parameters.values():
90
+ if py_param.name not in bound_args or py_param.kind == inspect.Parameter.KEYWORD_ONLY:
76
91
  break
77
- arg = bound_args[param.name]
92
+ arg = bound_args[py_param.name]
78
93
  if isinstance(arg, Expr):
79
94
  self.args.append((len(self.components), None))
80
95
  self.components.append(arg.copy())
81
96
  else:
82
97
  self.args.append((None, arg))
83
- if param.kind != inspect.Parameter.VAR_POSITIONAL and param.kind != inspect.Parameter.VAR_KEYWORD:
84
- self.arg_types.append(signature.parameters[param.name].col_type)
98
+ if py_param.kind != inspect.Parameter.VAR_POSITIONAL and py_param.kind != inspect.Parameter.VAR_KEYWORD:
99
+ self.arg_types.append(signature.parameters[py_param.name].col_type)
85
100
 
86
101
  # the remaining args are passed as keywords
87
102
  kw_param_names = set(bound_args.keys()) - set(list(fn.signature.py_signature.parameters.keys())[:len(self.args)])
@@ -138,13 +153,11 @@ class FunctionCall(Expr):
138
153
  return [RowidRef(target, i) for i in range(target.num_rowid_columns())]
139
154
 
140
155
  def default_column_name(self) -> Optional[str]:
141
- if self.fn.is_property:
142
- return self.fn.name
143
- return super().default_column_name()
156
+ return self.fn.name
144
157
 
145
158
  @classmethod
146
159
  def normalize_args(cls, fn_name: str, signature: func.Signature, bound_args: dict[str, Any]) -> None:
147
- """Converts all args to Exprs and checks that they are compatible with signature.
160
+ """Converts args to Exprs where appropriate and checks that they are compatible with signature.
148
161
 
149
162
  Updates bound_args in place, where necessary.
150
163
  """
@@ -263,6 +276,7 @@ class FunctionCall(Expr):
263
276
  for param_name, (idx, arg) in self.kwargs.items()
264
277
  ])
265
278
  if len(self.order_by) > 0:
279
+ assert isinstance(self.fn, func.AggregateFunction)
266
280
  if self.fn.requires_order_by:
267
281
  arg_strs.insert(0, Expr.print_list(self.order_by))
268
282
  else:
@@ -273,7 +287,7 @@ class FunctionCall(Expr):
273
287
  separator = ', ' if inline else ',\n '
274
288
  return separator.join(arg_strs)
275
289
 
276
- def has_group_by(self) -> list[Expr]:
290
+ def has_group_by(self) -> bool:
277
291
  return self.group_by_stop_idx != 0
278
292
 
279
293
  @property
@@ -286,10 +300,11 @@ class FunctionCall(Expr):
286
300
 
287
301
  @property
288
302
  def is_window_fn_call(self) -> bool:
289
- return isinstance(self.fn, func.AggregateFunction) and self.fn.allows_window and \
290
- (not self.fn.allows_std_agg \
291
- or self.has_group_by() \
292
- or (len(self.order_by) > 0 and not self.fn.requires_order_by))
303
+ return isinstance(self.fn, func.AggregateFunction) and self.fn.allows_window and (
304
+ not self.fn.allows_std_agg
305
+ or self.has_group_by()
306
+ or (len(self.order_by) > 0 and not self.fn.requires_order_by)
307
+ )
293
308
 
294
309
  def get_window_sort_exprs(self) -> tuple[list[Expr], list[Expr]]:
295
310
  return self.group_by, self.order_by
@@ -413,6 +428,7 @@ class FunctionCall(Expr):
413
428
  # optimization: avoid additional level of indirection we'd get from calling Function.exec()
414
429
  data_row[self.slot_idx] = self.fn.py_fn(*args, **kwargs)
415
430
  elif self.is_window_fn_call:
431
+ assert isinstance(self.fn, func.AggregateFunction)
416
432
  if self.has_group_by():
417
433
  if self.current_partition_vals is None:
418
434
  self.current_partition_vals = [None] * len(self.group_by)
@@ -438,7 +454,7 @@ class FunctionCall(Expr):
438
454
  return result
439
455
 
440
456
  @classmethod
441
- def _from_dict(cls, d: dict, components: list[Expr]) -> Expr:
457
+ def _from_dict(cls, d: dict, components: list[Expr]) -> FunctionCall:
442
458
  assert 'fn' in d
443
459
  assert 'args' in d
444
460
  assert 'kwargs' in d
@@ -5,7 +5,7 @@ import enum
5
5
  from typing import Union
6
6
 
7
7
  # Python types corresponding to our literal types
8
- LiteralPythonTypes = Union[str, int, float, bool, datetime.datetime, datetime.date]
8
+ LiteralPythonTypes = Union[str, int, float, bool, datetime.datetime]
9
9
 
10
10
  def print_slice(s: slice) -> str:
11
11
  start_str = f'{str(s.start) if s.start is not None else ""}'
@@ -35,6 +35,7 @@ class ComparisonOperator(enum.Enum):
35
35
  return '>'
36
36
  if self == self.GE:
37
37
  return '>='
38
+ assert False
38
39
 
39
40
  def reverse(self) -> ComparisonOperator:
40
41
  if self == self.LT:
@@ -60,6 +61,7 @@ class LogicalOperator(enum.Enum):
60
61
  return '|'
61
62
  if self == self.NOT:
62
63
  return '~'
64
+ assert False
63
65
 
64
66
 
65
67
  class ArithmeticOperator(enum.Enum):
@@ -83,3 +85,4 @@ class ArithmeticOperator(enum.Enum):
83
85
  return '%'
84
86
  if self == self.FLOORDIV:
85
87
  return '//'
88
+ assert False
@@ -1,15 +1,16 @@
1
1
  from __future__ import annotations
2
2
 
3
- from typing import Optional, List, Any, Dict, Tuple, Iterable
3
+ from typing import Any, Iterable, Optional
4
4
 
5
5
  import sqlalchemy as sql
6
6
 
7
7
  import pixeltable.exceptions as excs
8
- from .sql_element_cache import SqlElementCache
9
8
  import pixeltable.type_system as ts
9
+
10
10
  from .data_row import DataRow
11
11
  from .expr import Expr
12
12
  from .row_builder import RowBuilder
13
+ from .sql_element_cache import SqlElementCache
13
14
 
14
15
 
15
16
  class InPredicate(Expr):
@@ -43,7 +44,7 @@ class InPredicate(Expr):
43
44
  assert len(self.components) == 2
44
45
  return self.components[1]
45
46
 
46
- def _normalize_value_set(self, value_set: Any, filter_type_mismatches: bool = True) -> Iterable:
47
+ def _normalize_value_set(self, value_set: Iterable, filter_type_mismatches: bool = True) -> list:
47
48
  if not isinstance(value_set, Iterable):
48
49
  raise excs.Error(f'isin(): argument must be an Iterable (eg, list, dict, ...), not {value_set!r}')
49
50
  value_list = list(value_set)
@@ -68,10 +69,10 @@ class InPredicate(Expr):
68
69
  def _equals(self, other: InPredicate) -> bool:
69
70
  return self.value_list == other.value_list
70
71
 
71
- def _id_attrs(self) -> List[Tuple[str, Any]]:
72
+ def _id_attrs(self) -> list[tuple[str, Any]]:
72
73
  return super()._id_attrs() + [('value_list', self.value_list)]
73
74
 
74
- def sql_expr(self, sql_elements: SqlElementCache) -> Optional[sql.ClauseElement]:
75
+ def sql_expr(self, sql_elements: SqlElementCache) -> Optional[sql.ColumnElement]:
75
76
  lhs_sql_exprs = sql_elements.get(self.components[0])
76
77
  if lhs_sql_exprs is None or self.value_list is None:
77
78
  return None
@@ -86,11 +87,11 @@ class InPredicate(Expr):
86
87
  value_list = self._normalize_value_set(value_set, filter_type_mismatches=False)
87
88
  data_row[self.slot_idx] = lhs_val in value_list
88
89
 
89
- def _as_dict(self) -> Dict:
90
+ def _as_dict(self) -> dict:
90
91
  return {'value_list': self.value_list, **super()._as_dict()}
91
92
 
92
93
  @classmethod
93
- def _from_dict(cls, d: Dict, components: List[Expr]) -> Expr:
94
+ def _from_dict(cls, d: dict, components: list[Expr]) -> InPredicate:
94
95
  assert 'value_list' in d
95
96
  assert len(components) <= 2
96
97
  return cls(components[0], d['value_list'], components[1] if len(components) == 2 else None)