pixeltable 0.3.1__py3-none-any.whl → 0.3.3__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 (147) hide show
  1. pixeltable/__init__.py +64 -11
  2. pixeltable/__version__.py +2 -2
  3. pixeltable/catalog/__init__.py +1 -1
  4. pixeltable/catalog/catalog.py +50 -27
  5. pixeltable/catalog/column.py +27 -11
  6. pixeltable/catalog/dir.py +6 -4
  7. pixeltable/catalog/globals.py +8 -1
  8. pixeltable/catalog/insertable_table.py +25 -15
  9. pixeltable/catalog/named_function.py +10 -6
  10. pixeltable/catalog/path.py +3 -2
  11. pixeltable/catalog/path_dict.py +8 -6
  12. pixeltable/catalog/schema_object.py +2 -1
  13. pixeltable/catalog/table.py +123 -103
  14. pixeltable/catalog/table_version.py +292 -143
  15. pixeltable/catalog/table_version_path.py +8 -5
  16. pixeltable/catalog/view.py +68 -27
  17. pixeltable/dataframe.py +102 -72
  18. pixeltable/env.py +39 -23
  19. pixeltable/exec/__init__.py +2 -2
  20. pixeltable/exec/aggregation_node.py +10 -4
  21. pixeltable/exec/cache_prefetch_node.py +5 -3
  22. pixeltable/exec/component_iteration_node.py +9 -8
  23. pixeltable/exec/data_row_batch.py +21 -10
  24. pixeltable/exec/exec_context.py +10 -3
  25. pixeltable/exec/exec_node.py +23 -12
  26. pixeltable/exec/expr_eval/evaluators.py +18 -17
  27. pixeltable/exec/expr_eval/expr_eval_node.py +29 -16
  28. pixeltable/exec/expr_eval/globals.py +33 -11
  29. pixeltable/exec/expr_eval/row_buffer.py +5 -6
  30. pixeltable/exec/expr_eval/schedulers.py +170 -42
  31. pixeltable/exec/in_memory_data_node.py +8 -7
  32. pixeltable/exec/row_update_node.py +15 -5
  33. pixeltable/exec/sql_node.py +56 -27
  34. pixeltable/exprs/__init__.py +2 -2
  35. pixeltable/exprs/arithmetic_expr.py +57 -26
  36. pixeltable/exprs/array_slice.py +1 -1
  37. pixeltable/exprs/column_property_ref.py +2 -1
  38. pixeltable/exprs/column_ref.py +20 -15
  39. pixeltable/exprs/comparison.py +6 -2
  40. pixeltable/exprs/compound_predicate.py +1 -3
  41. pixeltable/exprs/data_row.py +2 -2
  42. pixeltable/exprs/expr.py +101 -72
  43. pixeltable/exprs/expr_dict.py +2 -1
  44. pixeltable/exprs/expr_set.py +3 -1
  45. pixeltable/exprs/function_call.py +39 -41
  46. pixeltable/exprs/globals.py +1 -0
  47. pixeltable/exprs/in_predicate.py +2 -2
  48. pixeltable/exprs/inline_expr.py +20 -17
  49. pixeltable/exprs/json_mapper.py +4 -2
  50. pixeltable/exprs/json_path.py +12 -18
  51. pixeltable/exprs/literal.py +5 -9
  52. pixeltable/exprs/method_ref.py +1 -0
  53. pixeltable/exprs/object_ref.py +1 -1
  54. pixeltable/exprs/row_builder.py +31 -16
  55. pixeltable/exprs/rowid_ref.py +14 -5
  56. pixeltable/exprs/similarity_expr.py +11 -6
  57. pixeltable/exprs/sql_element_cache.py +1 -1
  58. pixeltable/exprs/type_cast.py +24 -9
  59. pixeltable/ext/__init__.py +1 -0
  60. pixeltable/ext/functions/__init__.py +1 -0
  61. pixeltable/ext/functions/whisperx.py +2 -2
  62. pixeltable/ext/functions/yolox.py +11 -11
  63. pixeltable/func/aggregate_function.py +17 -13
  64. pixeltable/func/callable_function.py +6 -6
  65. pixeltable/func/expr_template_function.py +15 -14
  66. pixeltable/func/function.py +16 -16
  67. pixeltable/func/function_registry.py +11 -8
  68. pixeltable/func/globals.py +4 -2
  69. pixeltable/func/query_template_function.py +12 -13
  70. pixeltable/func/signature.py +18 -9
  71. pixeltable/func/tools.py +10 -17
  72. pixeltable/func/udf.py +106 -11
  73. pixeltable/functions/__init__.py +21 -2
  74. pixeltable/functions/anthropic.py +21 -15
  75. pixeltable/functions/fireworks.py +63 -5
  76. pixeltable/functions/gemini.py +13 -3
  77. pixeltable/functions/globals.py +18 -6
  78. pixeltable/functions/huggingface.py +20 -38
  79. pixeltable/functions/image.py +7 -3
  80. pixeltable/functions/json.py +1 -0
  81. pixeltable/functions/llama_cpp.py +1 -4
  82. pixeltable/functions/mistralai.py +31 -20
  83. pixeltable/functions/ollama.py +4 -18
  84. pixeltable/functions/openai.py +214 -109
  85. pixeltable/functions/replicate.py +11 -10
  86. pixeltable/functions/string.py +70 -7
  87. pixeltable/functions/timestamp.py +21 -8
  88. pixeltable/functions/together.py +66 -52
  89. pixeltable/functions/video.py +1 -0
  90. pixeltable/functions/vision.py +14 -11
  91. pixeltable/functions/whisper.py +2 -1
  92. pixeltable/globals.py +61 -28
  93. pixeltable/index/__init__.py +1 -1
  94. pixeltable/index/btree.py +5 -3
  95. pixeltable/index/embedding_index.py +15 -14
  96. pixeltable/io/__init__.py +1 -1
  97. pixeltable/io/external_store.py +30 -25
  98. pixeltable/io/fiftyone.py +6 -14
  99. pixeltable/io/globals.py +33 -27
  100. pixeltable/io/hf_datasets.py +3 -2
  101. pixeltable/io/label_studio.py +80 -71
  102. pixeltable/io/pandas.py +33 -9
  103. pixeltable/io/parquet.py +10 -13
  104. pixeltable/iterators/__init__.py +1 -0
  105. pixeltable/iterators/audio.py +205 -0
  106. pixeltable/iterators/document.py +19 -8
  107. pixeltable/iterators/image.py +6 -24
  108. pixeltable/iterators/string.py +3 -6
  109. pixeltable/iterators/video.py +1 -7
  110. pixeltable/metadata/__init__.py +9 -2
  111. pixeltable/metadata/converters/convert_10.py +2 -2
  112. pixeltable/metadata/converters/convert_15.py +1 -5
  113. pixeltable/metadata/converters/convert_16.py +2 -4
  114. pixeltable/metadata/converters/convert_17.py +2 -4
  115. pixeltable/metadata/converters/convert_18.py +2 -4
  116. pixeltable/metadata/converters/convert_19.py +2 -5
  117. pixeltable/metadata/converters/convert_20.py +1 -4
  118. pixeltable/metadata/converters/convert_21.py +4 -6
  119. pixeltable/metadata/converters/convert_22.py +1 -0
  120. pixeltable/metadata/converters/convert_23.py +5 -5
  121. pixeltable/metadata/converters/convert_24.py +12 -13
  122. pixeltable/metadata/converters/convert_26.py +23 -0
  123. pixeltable/metadata/converters/util.py +3 -4
  124. pixeltable/metadata/notes.py +1 -0
  125. pixeltable/metadata/schema.py +13 -2
  126. pixeltable/plan.py +173 -98
  127. pixeltable/store.py +42 -26
  128. pixeltable/type_system.py +130 -85
  129. pixeltable/utils/arrow.py +1 -7
  130. pixeltable/utils/coco.py +16 -17
  131. pixeltable/utils/code.py +1 -1
  132. pixeltable/utils/console_output.py +44 -0
  133. pixeltable/utils/description_helper.py +7 -7
  134. pixeltable/utils/documents.py +3 -1
  135. pixeltable/utils/filecache.py +13 -8
  136. pixeltable/utils/http_server.py +9 -8
  137. pixeltable/utils/media_store.py +2 -1
  138. pixeltable/utils/pytorch.py +11 -14
  139. pixeltable/utils/s3.py +1 -0
  140. pixeltable/utils/sql.py +1 -0
  141. pixeltable/utils/transactional_directory.py +2 -2
  142. {pixeltable-0.3.1.dist-info → pixeltable-0.3.3.dist-info}/METADATA +7 -8
  143. pixeltable-0.3.3.dist-info/RECORD +163 -0
  144. pixeltable-0.3.1.dist-info/RECORD +0 -160
  145. {pixeltable-0.3.1.dist-info → pixeltable-0.3.3.dist-info}/LICENSE +0 -0
  146. {pixeltable-0.3.1.dist-info → pixeltable-0.3.3.dist-info}/WHEEL +0 -0
  147. {pixeltable-0.3.1.dist-info → pixeltable-0.3.3.dist-info}/entry_points.txt +0 -0
@@ -1,13 +1,14 @@
1
1
  import logging
2
2
  import warnings
3
3
  from decimal import Decimal
4
- from typing import Iterable, Iterator, NamedTuple, Optional, TYPE_CHECKING, Sequence, AsyncIterator
4
+ from typing import TYPE_CHECKING, AsyncIterator, Iterable, Iterator, NamedTuple, Optional, Sequence
5
5
  from uuid import UUID
6
6
 
7
7
  import sqlalchemy as sql
8
8
 
9
9
  import pixeltable.catalog as catalog
10
10
  import pixeltable.exprs as exprs
11
+
11
12
  from .data_row_batch import DataRowBatch
12
13
  from .exec_node import ExecNode
13
14
 
@@ -53,10 +54,12 @@ def combine_order_by_clauses(clauses: Iterable[OrderByClause]) -> Optional[Order
53
54
 
54
55
 
55
56
  def print_order_by_clause(clause: OrderByClause) -> str:
56
- return ', '.join([
57
- f'({item.expr}{", asc=True" if item.asc is True else ""}{", asc=False" if item.asc is False else ""})'
58
- for item in clause
59
- ])
57
+ return ', '.join(
58
+ [
59
+ f'({item.expr}{", asc=True" if item.asc is True else ""}{", asc=False" if item.asc is False else ""})'
60
+ for item in clause
61
+ ]
62
+ )
60
63
 
61
64
 
62
65
  class SqlNode(ExecNode):
@@ -82,8 +85,12 @@ class SqlNode(ExecNode):
82
85
  limit: Optional[int]
83
86
 
84
87
  def __init__(
85
- self, tbl: Optional[catalog.TableVersionPath], row_builder: exprs.RowBuilder,
86
- select_list: Iterable[exprs.Expr], sql_elements: exprs.SqlElementCache, set_pk: bool = False
88
+ self,
89
+ tbl: Optional[catalog.TableVersionPath],
90
+ row_builder: exprs.RowBuilder,
91
+ select_list: Iterable[exprs.Expr],
92
+ sql_elements: exprs.SqlElementCache,
93
+ set_pk: bool = False,
87
94
  ):
88
95
  """
89
96
  If row_builder contains references to unstored iter columns, expands the select list to include their
@@ -186,8 +193,11 @@ class SqlNode(ExecNode):
186
193
 
187
194
  @classmethod
188
195
  def create_from_clause(
189
- cls, tbl: catalog.TableVersionPath, stmt: sql.Select, refd_tbl_ids: Optional[set[UUID]] = None,
190
- exact_version_only: Optional[set[UUID]] = None
196
+ cls,
197
+ tbl: catalog.TableVersionPath,
198
+ stmt: sql.Select,
199
+ refd_tbl_ids: Optional[set[UUID]] = None,
200
+ exact_version_only: Optional[set[UUID]] = None,
191
201
  ) -> sql.Select:
192
202
  """Add From clause to stmt for tables/views referenced by materialized_exprs
193
203
  Args:
@@ -220,15 +230,14 @@ class SqlNode(ExecNode):
220
230
  # join tbl to prev_tbl on prev_tbl's rowid cols
221
231
  prev_tbl_rowid_cols = prev_tbl.store_tbl.rowid_columns()
222
232
  tbl_rowid_cols = tbl.store_tbl.rowid_columns()
223
- rowid_clauses = \
224
- [c1 == c2 for c1, c2 in zip(prev_tbl_rowid_cols, tbl_rowid_cols[:len(prev_tbl_rowid_cols)])]
233
+ rowid_clauses = [
234
+ c1 == c2 for c1, c2 in zip(prev_tbl_rowid_cols, tbl_rowid_cols[: len(prev_tbl_rowid_cols)])
235
+ ]
225
236
  stmt = stmt.join(tbl.store_tbl.sa_tbl, sql.and_(*rowid_clauses))
226
237
  if tbl.id in exact_version_only:
227
238
  stmt = stmt.where(tbl.store_tbl.v_min_col == tbl.version)
228
239
  else:
229
- stmt = stmt \
230
- .where(tbl.store_tbl.v_min_col <= tbl.version) \
231
- .where(tbl.store_tbl.v_max_col > tbl.version)
240
+ stmt = stmt.where(tbl.store_tbl.v_min_col <= tbl.version).where(tbl.store_tbl.v_max_col > tbl.version)
232
241
  prev_tbl = tbl
233
242
  return stmt
234
243
 
@@ -291,7 +300,7 @@ class SqlNode(ExecNode):
291
300
 
292
301
  # populate output_row
293
302
  if self.num_pk_cols > 0:
294
- output_row.set_pk(tuple(sql_row[-self.num_pk_cols:]))
303
+ output_row.set_pk(tuple(sql_row[-self.num_pk_cols :]))
295
304
  # copy the output of the SQL query into the output row
296
305
  for i, e in enumerate(self.select_list):
297
306
  slot_idx = e.slot_idx
@@ -341,12 +350,16 @@ class SqlScanNode(SqlNode):
341
350
 
342
351
  Supports filtering and ordering.
343
352
  """
353
+
344
354
  exact_version_only: list[catalog.TableVersion]
345
355
 
346
356
  def __init__(
347
- self, tbl: catalog.TableVersionPath, row_builder: exprs.RowBuilder,
357
+ self,
358
+ tbl: catalog.TableVersionPath,
359
+ row_builder: exprs.RowBuilder,
348
360
  select_list: Iterable[exprs.Expr],
349
- set_pk: bool = False, exact_version_only: Optional[list[catalog.TableVersion]] = None
361
+ set_pk: bool = False,
362
+ exact_version_only: Optional[list[catalog.TableVersion]] = None,
350
363
  ):
351
364
  """
352
365
  Args:
@@ -367,7 +380,8 @@ class SqlScanNode(SqlNode):
367
380
  where_clause_tbl_ids = self.where_clause.tbl_ids() if self.where_clause is not None else set()
368
381
  refd_tbl_ids = exprs.Expr.all_tbl_ids(self.select_list) | where_clause_tbl_ids | self._ordering_tbl_ids()
369
382
  stmt = self.create_from_clause(
370
- self.tbl, stmt, refd_tbl_ids, exact_version_only={t.id for t in self.exact_version_only})
383
+ self.tbl, stmt, refd_tbl_ids, exact_version_only={t.id for t in self.exact_version_only}
384
+ )
371
385
  return stmt
372
386
 
373
387
 
@@ -377,8 +391,12 @@ class SqlLookupNode(SqlNode):
377
391
  """
378
392
 
379
393
  def __init__(
380
- self, tbl: catalog.TableVersionPath, row_builder: exprs.RowBuilder,
381
- select_list: Iterable[exprs.Expr], sa_key_cols: list[sql.Column], key_vals: list[tuple],
394
+ self,
395
+ tbl: catalog.TableVersionPath,
396
+ row_builder: exprs.RowBuilder,
397
+ select_list: Iterable[exprs.Expr],
398
+ sa_key_cols: list[sql.Column],
399
+ key_vals: list[tuple],
382
400
  ):
383
401
  """
384
402
  Args:
@@ -406,11 +424,13 @@ class SqlAggregationNode(SqlNode):
406
424
  group_by_items: Optional[list[exprs.Expr]]
407
425
 
408
426
  def __init__(
409
- self, row_builder: exprs.RowBuilder,
427
+ self,
428
+ row_builder: exprs.RowBuilder,
410
429
  input: SqlNode,
411
430
  select_list: Iterable[exprs.Expr],
412
431
  group_by_items: Optional[list[exprs.Expr]] = None,
413
- limit: Optional[int] = None, exact_version_only: Optional[list[catalog.TableVersion]] = None
432
+ limit: Optional[int] = None,
433
+ exact_version_only: Optional[list[catalog.TableVersion]] = None,
414
434
  ):
415
435
  """
416
436
  Args:
@@ -436,12 +456,16 @@ class SqlJoinNode(SqlNode):
436
456
  """
437
457
  Materializes data from the store via a Select ... From ... that contains joins
438
458
  """
459
+
439
460
  input_ctes: list[sql.CTE]
440
461
  join_clauses: list['pixeltable.plan.JoinClause']
441
462
 
442
463
  def __init__(
443
- self, row_builder: exprs.RowBuilder,
444
- inputs: Sequence[SqlNode], join_clauses: list['pixeltable.plan.JoinClause'], select_list: Iterable[exprs.Expr]
464
+ self,
465
+ row_builder: exprs.RowBuilder,
466
+ inputs: Sequence[SqlNode],
467
+ join_clauses: list['pixeltable.plan.JoinClause'],
468
+ select_list: Iterable[exprs.Expr],
445
469
  ):
446
470
  assert len(inputs) > 1
447
471
  assert len(inputs) == len(join_clauses) + 1
@@ -456,16 +480,21 @@ class SqlJoinNode(SqlNode):
456
480
 
457
481
  def _create_stmt(self) -> sql.Select:
458
482
  from pixeltable import plan
483
+
459
484
  stmt = super()._create_stmt()
460
485
  stmt = stmt.select_from(self.input_ctes[0])
461
486
  for i in range(len(self.join_clauses)):
462
487
  join_clause = self.join_clauses[i]
463
488
  on_clause = (
464
- self.sql_elements.get(join_clause.join_predicate) if join_clause.join_type != plan.JoinType.CROSS
489
+ self.sql_elements.get(join_clause.join_predicate)
490
+ if join_clause.join_type != plan.JoinType.CROSS
465
491
  else sql.sql.expression.literal(True)
466
492
  )
467
493
  is_outer = join_clause.join_type == plan.JoinType.LEFT or join_clause.join_type == plan.JoinType.FULL_OUTER
468
494
  stmt = stmt.join(
469
- self.input_ctes[i + 1], onclause=on_clause, isouter=is_outer,
470
- full=join_clause == plan.JoinType.FULL_OUTER)
495
+ self.input_ctes[i + 1],
496
+ onclause=on_clause,
497
+ isouter=is_outer,
498
+ full=join_clause == plan.JoinType.FULL_OUTER,
499
+ )
471
500
  return stmt
@@ -9,6 +9,7 @@ from .expr import Expr
9
9
  from .expr_dict import ExprDict
10
10
  from .expr_set import ExprSet
11
11
  from .function_call import FunctionCall
12
+ from .globals import ArithmeticOperator, ComparisonOperator, LogicalOperator
12
13
  from .in_predicate import InPredicate
13
14
  from .inline_expr import InlineArray, InlineDict, InlineList
14
15
  from .is_null import IsNull
@@ -17,10 +18,9 @@ from .json_path import RELATIVE_PATH_ROOT, JsonPath
17
18
  from .literal import Literal
18
19
  from .method_ref import MethodRef
19
20
  from .object_ref import ObjectRef
20
- from .row_builder import RowBuilder, ColumnSlotIdx, ExecProfile
21
+ from .row_builder import ColumnSlotIdx, ExecProfile, RowBuilder
21
22
  from .rowid_ref import RowidRef
22
23
  from .similarity_expr import SimilarityExpr
23
24
  from .sql_element_cache import SqlElementCache
24
25
  from .type_cast import TypeCast
25
26
  from .variable import Variable
26
- from .globals import ComparisonOperator, LogicalOperator, ArithmeticOperator
@@ -1,15 +1,17 @@
1
1
  from __future__ import annotations
2
2
 
3
- from typing import Any, Optional
3
+ from typing import Any, Optional, Union
4
4
 
5
5
  import sqlalchemy as sql
6
6
 
7
7
  import pixeltable.exceptions as excs
8
+ import pixeltable.exprs as exprs
8
9
  import pixeltable.type_system as ts
9
10
 
10
11
  from .data_row import DataRow
11
12
  from .expr import Expr
12
13
  from .globals import ArithmeticOperator
14
+ from .literal import Literal
13
15
  from .row_builder import RowBuilder
14
16
  from .sql_element_cache import SqlElementCache
15
17
 
@@ -18,6 +20,7 @@ class ArithmeticExpr(Expr):
18
20
  """
19
21
  Allows arithmetic exprs on json paths
20
22
  """
23
+
21
24
  def __init__(self, operator: ArithmeticOperator, op1: Expr, op2: Expr):
22
25
  if op1.col_type.is_json_type() or op2.col_type.is_json_type() or operator == ArithmeticOperator.DIV:
23
26
  # we assume it's a float
@@ -35,6 +38,14 @@ class ArithmeticExpr(Expr):
35
38
 
36
39
  self.id = self._create_id()
37
40
 
41
+ @property
42
+ def _op1(self) -> Expr:
43
+ return self.components[0]
44
+
45
+ @property
46
+ def _op2(self) -> Expr:
47
+ return self.components[1]
48
+
38
49
  def __repr__(self) -> str:
39
50
  # add parentheses around operands that are ArithmeticExprs to express precedence
40
51
  op1_str = f'({self._op1})' if isinstance(self._op1, ArithmeticExpr) else str(self._op1)
@@ -47,14 +58,6 @@ class ArithmeticExpr(Expr):
47
58
  def _id_attrs(self) -> list[tuple[str, Any]]:
48
59
  return super()._id_attrs() + [('operator', self.operator.value)]
49
60
 
50
- @property
51
- def _op1(self) -> Expr:
52
- return self.components[0]
53
-
54
- @property
55
- def _op2(self) -> Expr:
56
- return self.components[1]
57
-
58
61
  def sql_expr(self, sql_elements: SqlElementCache) -> Optional[sql.ColumnElement]:
59
62
  assert self.col_type.is_int_type() or self.col_type.is_float_type() or self.col_type.is_json_type()
60
63
  left = sql_elements.get(self._op1)
@@ -73,7 +76,7 @@ class ArithmeticExpr(Expr):
73
76
  # TODO: Should we cast the NULLs to NaNs when they are retrieved back into Python?
74
77
  nullif = sql.sql.func.nullif(right, 0)
75
78
  # We have to cast to a `float`, or else we'll get a `Decimal`
76
- return sql.sql.expression.cast(left / nullif, sql.Float)
79
+ return sql.sql.expression.cast(left / nullif, self.col_type.to_sa_type())
77
80
  if self.operator == ArithmeticOperator.MOD:
78
81
  if self.col_type.is_int_type():
79
82
  nullif = sql.sql.func.nullif(right, 0)
@@ -89,9 +92,9 @@ class ArithmeticExpr(Expr):
89
92
  # mimic the behavior of Python's // operator.
90
93
  nullif = sql.sql.func.nullif(right, 0)
91
94
  if self.col_type.is_int_type():
92
- return sql.sql.expression.cast(sql.func.floor(left / nullif), sql.Integer)
95
+ return sql.sql.expression.cast(sql.func.floor(left / nullif), self.col_type.to_sa_type())
93
96
  if self.col_type.is_float_type():
94
- return sql.sql.expression.cast(sql.func.floor(left / nullif), sql.Float)
97
+ return sql.sql.expression.cast(sql.func.floor(left / nullif), self.col_type.to_sa_type())
95
98
  assert False
96
99
 
97
100
  def eval(self, data_row: DataRow, row_builder: RowBuilder) -> None:
@@ -99,30 +102,58 @@ class ArithmeticExpr(Expr):
99
102
  op2_val = data_row[self._op2.slot_idx]
100
103
 
101
104
  # if one or both columns is JsonTyped, we need a dynamic check that they are numeric
102
- if self._op1.col_type.is_json_type() and not isinstance(op1_val, int) and not isinstance(op1_val, float):
105
+ if self._op1.col_type.is_json_type() and op1_val is not None and not isinstance(op1_val, (int, float)):
103
106
  raise excs.Error(
104
- f'{self.operator} requires numeric type, but {self._op1} has type {type(op1_val).__name__}')
105
- if self._op2.col_type.is_json_type() and not isinstance(op2_val, int) and not isinstance(op2_val, float):
107
+ f'{self.operator} requires numeric types, but {self._op1} has type {type(op1_val).__name__}'
108
+ )
109
+ if self._op2.col_type.is_json_type() and op2_val is not None and not isinstance(op2_val, (int, float)):
106
110
  raise excs.Error(
107
- f'{self.operator} requires numeric type, but {self._op2} has type {type(op2_val).__name__}')
108
-
109
- # if either operand is None, always return None
111
+ f'{self.operator} requires numeric types, but {self._op2} has type {type(op2_val).__name__}'
112
+ )
113
+
114
+ data_row[self.slot_idx] = self.eval_nullable(op1_val, op2_val)
115
+
116
+ def eval_nullable(
117
+ self, op1_val: Union[int, float, None], op2_val: Union[int, float, None]
118
+ ) -> Union[int, float, None]:
119
+ """
120
+ Return the result of evaluating the expression on two nullable int/float operands,
121
+ None is interpreted as SQL NULL
122
+ """
110
123
  if op1_val is None or op2_val is None:
111
- data_row[self.slot_idx] = None
112
- return
124
+ return None
125
+ return self.eval_non_null(op1_val, op2_val)
113
126
 
127
+ def eval_non_null(self, op1_val: Union[int, float], op2_val: Union[int, float]) -> Union[int, float]:
128
+ """
129
+ Return the result of evaluating the expression on two int/float operands
130
+ """
114
131
  if self.operator == ArithmeticOperator.ADD:
115
- data_row[self.slot_idx] = op1_val + op2_val
132
+ return op1_val + op2_val
116
133
  elif self.operator == ArithmeticOperator.SUB:
117
- data_row[self.slot_idx] = op1_val - op2_val
134
+ return op1_val - op2_val
118
135
  elif self.operator == ArithmeticOperator.MUL:
119
- data_row[self.slot_idx] = op1_val * op2_val
136
+ return op1_val * op2_val
120
137
  elif self.operator == ArithmeticOperator.DIV:
121
- data_row[self.slot_idx] = op1_val / op2_val
138
+ return op1_val / op2_val
122
139
  elif self.operator == ArithmeticOperator.MOD:
123
- data_row[self.slot_idx] = op1_val % op2_val
140
+ return op1_val % op2_val
124
141
  elif self.operator == ArithmeticOperator.FLOORDIV:
125
- data_row[self.slot_idx] = op1_val // op2_val
142
+ return op1_val // op2_val
143
+
144
+ def as_literal(self) -> Optional[Literal]:
145
+ op1_lit = self._op1.as_literal()
146
+ if op1_lit is None:
147
+ return None
148
+ op2_lit = self._op2.as_literal()
149
+ if op2_lit is None:
150
+ return None
151
+ op1_val = op1_lit.val
152
+ assert op1_lit.col_type.is_numeric_type() or op1_val is None
153
+ op2_val = op2_lit.val
154
+ assert op2_lit.col_type.is_numeric_type() or op2_val is None
155
+
156
+ return Literal(self.eval_nullable(op1_val, op2_val), self.col_type) # type: ignore[arg-type]
126
157
 
127
158
  def _as_dict(self) -> dict:
128
159
  return {'operator': self.operator.value, **super()._as_dict()}
@@ -15,6 +15,7 @@ class ArraySlice(Expr):
15
15
  """
16
16
  Slice operation on an array, eg, t.array_col[:, 1:2].
17
17
  """
18
+
18
19
  def __init__(self, arr: Expr, index: tuple[Union[int, slice], ...]):
19
20
  assert arr.col_type.is_array_type()
20
21
  # determine result type
@@ -68,4 +69,3 @@ class ArraySlice(Expr):
68
69
  else:
69
70
  index.append(el)
70
71
  return cls(components[0], tuple(index))
71
-
@@ -7,6 +7,7 @@ import sqlalchemy as sql
7
7
 
8
8
  import pixeltable.type_system as ts
9
9
  from pixeltable import catalog
10
+
10
11
  from .column_ref import ColumnRef
11
12
  from .data_row import DataRow
12
13
  from .expr import Expr
@@ -19,6 +20,7 @@ class ColumnPropertyRef(Expr):
19
20
 
20
21
  The properties themselves are type-specific and may or may not need to reference the underlying column data.
21
22
  """
23
+
22
24
  class Property(enum.Enum):
23
25
  ERRORTYPE = 0
24
26
  ERRORMSG = 1
@@ -103,4 +105,3 @@ class ColumnPropertyRef(Expr):
103
105
  assert 'prop' in d
104
106
  assert isinstance(components[0], ColumnRef)
105
107
  return cls(components[0], cls.Property(d['prop']))
106
-
@@ -51,8 +51,9 @@ class ColumnRef(Expr):
51
51
  super().__init__(col.col_type)
52
52
  assert col.tbl is not None
53
53
  self.col = col
54
- self.is_unstored_iter_col = \
54
+ self.is_unstored_iter_col = (
55
55
  col.tbl.is_component_view() and col.tbl.is_iterator_column(col) and not col.is_stored
56
+ )
56
57
  self.iter_arg_ctx = None
57
58
  # number of rowid columns in the base table
58
59
  self.base_rowid_len = col.tbl.base.num_rowid_columns() if self.is_unstored_iter_col else 0
@@ -83,10 +84,11 @@ class ColumnRef(Expr):
83
84
  assert len(self.iter_arg_ctx.target_slot_idxs) == 1 # a single inline dict
84
85
 
85
86
  def _id_attrs(self) -> list[tuple[str, Any]]:
86
- return (
87
- super()._id_attrs()
88
- + [('tbl_id', self.col.tbl.id), ('col_id', self.col.id), ('perform_validation', self.perform_validation)]
89
- )
87
+ return super()._id_attrs() + [
88
+ ('tbl_id', self.col.tbl.id),
89
+ ('col_id', self.col.id),
90
+ ('perform_validation', self.perform_validation),
91
+ ]
90
92
 
91
93
  # override
92
94
  def _retarget(self, tbl_versions: dict[UUID, catalog.TableVersion]) -> ColumnRef:
@@ -99,14 +101,18 @@ class ColumnRef(Expr):
99
101
  from .column_property_ref import ColumnPropertyRef
100
102
 
101
103
  # resolve column properties
102
- if name == ColumnPropertyRef.Property.ERRORTYPE.name.lower() \
103
- or name == ColumnPropertyRef.Property.ERRORMSG.name.lower():
104
+ if (
105
+ name == ColumnPropertyRef.Property.ERRORTYPE.name.lower()
106
+ or name == ColumnPropertyRef.Property.ERRORMSG.name.lower()
107
+ ):
104
108
  property_is_present = self.col.is_stored and (self.col.is_computed or self.col_type.is_media_type())
105
109
  if not property_is_present:
106
110
  raise excs.Error(f'{name} only valid for a stored computed or media column: {self}')
107
111
  return ColumnPropertyRef(self, ColumnPropertyRef.Property[name.upper()])
108
- if name == ColumnPropertyRef.Property.FILEURL.name.lower() \
109
- or name == ColumnPropertyRef.Property.LOCALPATH.name.lower():
112
+ if (
113
+ name == ColumnPropertyRef.Property.FILEURL.name.lower()
114
+ or name == ColumnPropertyRef.Property.LOCALPATH.name.lower()
115
+ ):
110
116
  if not self.col.col_type.is_media_type():
111
117
  raise excs.Error(f'{name} only valid for image/video/audio/document columns: {self}')
112
118
  if self.col.is_computed and not self.col.is_stored:
@@ -115,12 +121,14 @@ class ColumnRef(Expr):
115
121
 
116
122
  if self.col_type.is_json_type():
117
123
  from .json_path import JsonPath
124
+
118
125
  return JsonPath(self, [name])
119
126
 
120
127
  return super().__getattr__(name)
121
128
 
122
129
  def similarity(self, item: Any, *, idx: Optional[str] = None) -> Expr:
123
130
  from .similarity_expr import SimilarityExpr
131
+
124
132
  return SimilarityExpr(self, item, idx_name=idx)
125
133
 
126
134
  def default_column_name(self) -> Optional[str]:
@@ -206,11 +214,11 @@ class ColumnRef(Expr):
206
214
  return
207
215
 
208
216
  # if this is a new base row, we need to instantiate a new iterator
209
- if self.base_rowid != data_row.pk[:self.base_rowid_len]:
217
+ if self.base_rowid != data_row.pk[: self.base_rowid_len]:
210
218
  row_builder.eval(data_row, self.iter_arg_ctx)
211
219
  iterator_args = data_row[self.iter_arg_ctx.target_slot_idxs[0]]
212
220
  self.iterator = self.col.tbl.iterator_cls(**iterator_args)
213
- self.base_rowid = data_row.pk[:self.base_rowid_len]
221
+ self.base_rowid = data_row.pk[: self.base_rowid_len]
214
222
  self.iterator.set_pos(data_row.pk[self.pos_idx])
215
223
  res = next(self.iterator)
216
224
  data_row[self.slot_idx] = res[self.col.name]
@@ -224,7 +232,7 @@ class ColumnRef(Expr):
224
232
  'tbl_id': str(tbl.id),
225
233
  'tbl_version': version,
226
234
  'col_id': self.col.id,
227
- 'perform_validation': self.perform_validation
235
+ 'perform_validation': self.perform_validation,
228
236
  }
229
237
 
230
238
  @classmethod
@@ -240,6 +248,3 @@ class ColumnRef(Expr):
240
248
  col = cls.get_column(d)
241
249
  perform_validation = d['perform_validation']
242
250
  return cls(col, perform_validation=perform_validation)
243
-
244
- def is_constant(self) -> bool:
245
- return False
@@ -39,8 +39,12 @@ class Comparison(Expr):
39
39
  self.components = [op1, op2]
40
40
 
41
41
  import pixeltable.index as index
42
- if self.is_search_arg_comparison and self._op2.col_type.is_string_type() \
43
- and len(self._op2.val) >= index.BtreeIndex.MAX_STRING_LEN:
42
+
43
+ if (
44
+ self.is_search_arg_comparison
45
+ and self._op2.col_type.is_string_type()
46
+ and len(self._op2.val) >= index.BtreeIndex.MAX_STRING_LEN
47
+ ):
44
48
  # we can't use an index for this after all
45
49
  raise excs.Error(
46
50
  f'String literal too long for comparison against indexed column {self._op1.col.name!r} '
@@ -60,8 +60,7 @@ class CompoundPredicate(Expr):
60
60
  def _id_attrs(self) -> list[tuple[str, Any]]:
61
61
  return super()._id_attrs() + [('operator', self.operator.value)]
62
62
 
63
- def split_conjuncts(
64
- self, condition: Callable[[Expr], bool]) -> tuple[list[Expr], Optional[Expr]]:
63
+ def split_conjuncts(self, condition: Callable[[Expr], bool]) -> tuple[list[Expr], Optional[Expr]]:
65
64
  if self.operator == LogicalOperator.OR or self.operator == LogicalOperator.NOT:
66
65
  return super().split_conjuncts(condition)
67
66
  matches = [op for op in self.components if condition(op)]
@@ -97,4 +96,3 @@ class CompoundPredicate(Expr):
97
96
  def _from_dict(cls, d: dict, components: list[Expr]) -> CompoundPredicate:
98
97
  assert 'operator' in d
99
98
  return cls(LogicalOperator(d['operator']), components)
100
-
@@ -6,10 +6,10 @@ import urllib.parse
6
6
  import urllib.request
7
7
  from typing import Any, Optional
8
8
 
9
- import PIL
10
- import PIL.Image
11
9
  import numpy as np
12
10
  import pgvector.sqlalchemy # type: ignore[import-untyped]
11
+ import PIL
12
+ import PIL.Image
13
13
  import sqlalchemy as sql
14
14
 
15
15
  from pixeltable import env