pixeltable 0.4.3__py3-none-any.whl → 0.4.5__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 (52) hide show
  1. pixeltable/__version__.py +2 -2
  2. pixeltable/catalog/__init__.py +1 -1
  3. pixeltable/catalog/catalog.py +619 -255
  4. pixeltable/catalog/dir.py +1 -2
  5. pixeltable/catalog/insertable_table.py +9 -9
  6. pixeltable/catalog/path.py +59 -20
  7. pixeltable/catalog/schema_object.py +10 -4
  8. pixeltable/catalog/table.py +51 -53
  9. pixeltable/catalog/table_version.py +216 -156
  10. pixeltable/catalog/table_version_path.py +1 -1
  11. pixeltable/catalog/tbl_ops.py +44 -0
  12. pixeltable/catalog/view.py +63 -65
  13. pixeltable/config.py +12 -4
  14. pixeltable/dataframe.py +75 -6
  15. pixeltable/env.py +46 -17
  16. pixeltable/exec/aggregation_node.py +1 -1
  17. pixeltable/exec/cache_prefetch_node.py +2 -6
  18. pixeltable/exec/component_iteration_node.py +4 -3
  19. pixeltable/exec/data_row_batch.py +10 -51
  20. pixeltable/exec/expr_eval/expr_eval_node.py +2 -2
  21. pixeltable/exec/in_memory_data_node.py +17 -16
  22. pixeltable/exec/sql_node.py +6 -7
  23. pixeltable/exprs/column_ref.py +2 -1
  24. pixeltable/exprs/data_row.py +13 -13
  25. pixeltable/exprs/row_builder.py +16 -4
  26. pixeltable/exprs/string_op.py +1 -1
  27. pixeltable/func/expr_template_function.py +1 -4
  28. pixeltable/functions/date.py +1 -1
  29. pixeltable/functions/gemini.py +4 -4
  30. pixeltable/functions/math.py +1 -1
  31. pixeltable/functions/openai.py +9 -6
  32. pixeltable/functions/timestamp.py +6 -6
  33. pixeltable/functions/video.py +2 -6
  34. pixeltable/globals.py +62 -33
  35. pixeltable/io/datarows.py +2 -1
  36. pixeltable/io/pandas.py +1 -0
  37. pixeltable/io/table_data_conduit.py +12 -13
  38. pixeltable/iterators/audio.py +17 -8
  39. pixeltable/iterators/image.py +5 -2
  40. pixeltable/metadata/schema.py +39 -2
  41. pixeltable/plan.py +5 -14
  42. pixeltable/share/packager.py +13 -13
  43. pixeltable/store.py +31 -7
  44. pixeltable/type_system.py +2 -1
  45. pixeltable/utils/filecache.py +1 -1
  46. pixeltable/utils/http_server.py +2 -3
  47. pixeltable/utils/media_store.py +90 -34
  48. {pixeltable-0.4.3.dist-info → pixeltable-0.4.5.dist-info}/METADATA +1 -1
  49. {pixeltable-0.4.3.dist-info → pixeltable-0.4.5.dist-info}/RECORD +52 -51
  50. {pixeltable-0.4.3.dist-info → pixeltable-0.4.5.dist-info}/LICENSE +0 -0
  51. {pixeltable-0.4.3.dist-info → pixeltable-0.4.5.dist-info}/WHEEL +0 -0
  52. {pixeltable-0.4.3.dist-info → pixeltable-0.4.5.dist-info}/entry_points.txt +0 -0
pixeltable/catalog/dir.py CHANGED
@@ -34,8 +34,7 @@ class Dir(SchemaObject):
34
34
  dir = cls(dir_record.id, parent_id, name)
35
35
  return dir
36
36
 
37
- @classmethod
38
- def _display_name(cls) -> str:
37
+ def _display_name(self) -> str:
39
38
  return 'directory'
40
39
 
41
40
  def _path(self) -> str:
@@ -54,8 +54,8 @@ class InsertableTable(Table):
54
54
  super().__init__(tbl_version.id, dir_id, tbl_version.get().name, tbl_version_path)
55
55
  self._tbl_version = tbl_version
56
56
 
57
- @classmethod
58
- def _display_name(cls) -> str:
57
+ def _display_name(self) -> str:
58
+ assert not self._tbl_version_path.is_replica()
59
59
  return 'table'
60
60
 
61
61
  @classmethod
@@ -75,10 +75,10 @@ class InsertableTable(Table):
75
75
  column_names = [col.name for col in columns]
76
76
  for pk_col in primary_key:
77
77
  if pk_col not in column_names:
78
- raise excs.Error(f'Primary key column {pk_col} not found in table schema')
78
+ raise excs.Error(f'Primary key column {pk_col!r} not found in table schema.')
79
79
  col = columns[column_names.index(pk_col)]
80
80
  if col.col_type.nullable:
81
- raise excs.Error(f'Primary key column {pk_col} cannot be nullable')
81
+ raise excs.Error(f'Primary key column {pk_col!r} cannot be nullable.')
82
82
  col.is_pk = True
83
83
 
84
84
  _, tbl_version = TableVersion.create(
@@ -101,8 +101,8 @@ class InsertableTable(Table):
101
101
  tbl_version.insert(None, df, fail_on_exception=True)
102
102
  session.commit()
103
103
 
104
- _logger.info(f'Created table `{name}`, id={tbl_version.id}')
105
- Env.get().console_logger.info(f'Created table `{name}`.')
104
+ _logger.info(f'Created table {name!r}, id={tbl_version.id}')
105
+ Env.get().console_logger.info(f'Created table {name!r}.')
106
106
  return tbl
107
107
 
108
108
  def _get_metadata(self) -> dict[str, Any]:
@@ -204,9 +204,9 @@ class InsertableTable(Table):
204
204
 
205
205
  for col_name, val in row.items():
206
206
  if col_name not in valid_col_names:
207
- raise excs.Error(f'Unknown column name {col_name} in row {row}')
207
+ raise excs.Error(f'Unknown column name {col_name!r} in row {row}')
208
208
  if col_name in computed_col_names:
209
- raise excs.Error(f'Value for computed column {col_name} in row {row}')
209
+ raise excs.Error(f'Value for computed column {col_name!r} in row {row}')
210
210
 
211
211
  # validate data
212
212
  col = self._tbl_version_path.get_column(col_name)
@@ -246,4 +246,4 @@ class InsertableTable(Table):
246
246
  return []
247
247
 
248
248
  def _table_descriptor(self) -> str:
249
- return f'Table {self._path()!r}'
249
+ return self._display_str()
@@ -1,20 +1,57 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import logging
4
- from typing import Iterator
4
+ from typing import Optional
5
5
 
6
6
  from pixeltable import exceptions as excs
7
7
 
8
- from .globals import is_valid_path
8
+ from .globals import is_valid_identifier
9
9
 
10
10
  _logger = logging.getLogger('pixeltable')
11
11
 
12
12
 
13
13
  class Path:
14
- def __init__(self, path: str, empty_is_valid: bool = False, allow_system_paths: bool = False):
15
- if not is_valid_path(path, empty_is_valid, allow_system_paths):
16
- raise excs.Error(f"Invalid path format: '{path}'")
17
- self.components = path.split('.')
14
+ components: list[str]
15
+ version: Optional[int]
16
+
17
+ def __init__(self, components: list[str], version: Optional[int] = None) -> None:
18
+ assert len(components) > 0
19
+ self.components = components
20
+ self.version = version
21
+
22
+ @classmethod
23
+ def parse(
24
+ cls,
25
+ path: str,
26
+ allow_empty_path: bool = False,
27
+ allow_system_path: bool = False,
28
+ allow_versioned_path: bool = False,
29
+ ) -> Path:
30
+ components: list[str]
31
+ version: Optional[int]
32
+ if ':' in path:
33
+ parts = path.split(':')
34
+ if len(parts) != 2:
35
+ raise excs.Error(f'Invalid path: {path}')
36
+ try:
37
+ components = parts[0].split('.')
38
+ version = int(parts[1])
39
+ except ValueError:
40
+ raise excs.Error(f'Invalid path: {path}') from None
41
+ else:
42
+ components = path.split('.')
43
+ version = None
44
+
45
+ if components == [''] and not allow_empty_path:
46
+ raise excs.Error(f'Invalid path: {path}')
47
+
48
+ if components != [''] and not all(is_valid_identifier(c, allow_system_path) for c in components):
49
+ raise excs.Error(f'Invalid path: {path}')
50
+
51
+ if version is not None and not allow_versioned_path:
52
+ raise excs.Error(f'Versioned path not allowed here: {path}')
53
+
54
+ return Path(components, version)
18
55
 
19
56
  @property
20
57
  def len(self) -> int:
@@ -22,7 +59,6 @@ class Path:
22
59
 
23
60
  @property
24
61
  def name(self) -> str:
25
- assert len(self.components) > 0
26
62
  return self.components[-1]
27
63
 
28
64
  @property
@@ -36,18 +72,15 @@ class Path:
36
72
  @property
37
73
  def parent(self) -> Path:
38
74
  if len(self.components) == 1:
39
- if self.is_root:
40
- return self
41
- else:
42
- return Path('', empty_is_valid=True, allow_system_paths=True)
75
+ return ROOT_PATH # Includes the case of the root path, which is its own parent.
43
76
  else:
44
- return Path('.'.join(self.components[:-1]), allow_system_paths=True)
77
+ return Path(self.components[:-1])
45
78
 
46
79
  def append(self, name: str) -> Path:
47
80
  if self.is_root:
48
- return Path(name, allow_system_paths=True)
81
+ return Path([name])
49
82
  else:
50
- return Path(f'{self}.{name}', allow_system_paths=True)
83
+ return Path([*self.components, name])
51
84
 
52
85
  def is_ancestor(self, other: Path, is_parent: bool = False) -> bool:
53
86
  """
@@ -60,22 +93,25 @@ class Path:
60
93
  is_prefix = self.components == other.components[: self.len]
61
94
  return is_prefix and (self.len == (other.len - 1) or not is_parent)
62
95
 
63
- def ancestors(self) -> Iterator[Path]:
96
+ def ancestors(self) -> list[Path]:
64
97
  """
65
- Return all ancestors of this path in top-down order including root.
98
+ Return all proper ancestors of this path in top-down order including root.
66
99
  If this path is for the root directory, which has no parent, then None is returned.
67
100
  """
68
101
  if self.is_root:
69
- return
102
+ return []
70
103
  else:
71
- for i in range(0, len(self.components)):
72
- yield Path('.'.join(self.components[0:i]), empty_is_valid=True)
104
+ return [Path(self.components[:i]) if i > 0 else ROOT_PATH for i in range(len(self.components))]
73
105
 
74
106
  def __repr__(self) -> str:
75
107
  return repr(str(self))
76
108
 
77
109
  def __str__(self) -> str:
78
- return '.'.join(self.components)
110
+ base = '.'.join(self.components)
111
+ if self.version is not None:
112
+ return f'{base}:{self.version}'
113
+ else:
114
+ return base
79
115
 
80
116
  def __eq__(self, other: object) -> bool:
81
117
  return isinstance(other, Path) and str(self) == str(other)
@@ -85,3 +121,6 @@ class Path:
85
121
 
86
122
  def __lt__(self, other: Path) -> bool:
87
123
  return str(self) < str(other)
124
+
125
+
126
+ ROOT_PATH = Path([''])
@@ -18,6 +18,7 @@ class SchemaObject:
18
18
 
19
19
  def __init__(self, obj_id: UUID, name: str, dir_id: Optional[UUID]):
20
20
  # make these private so they don't collide with column names (id and name are fairly common)
21
+ assert dir_id is None or isinstance(dir_id, UUID), type(dir_id)
21
22
  self._id = obj_id
22
23
  self._name = name
23
24
  self._dir_id = dir_id
@@ -42,22 +43,27 @@ class SchemaObject:
42
43
 
43
44
  def get_metadata(self) -> dict[str, Any]:
44
45
  """Returns metadata associated with this schema object."""
45
- from pixeltable.catalog import Catalog
46
+ from pixeltable.catalog import retry_loop
46
47
 
47
- with Catalog.get().begin_xact(for_write=False):
48
+ @retry_loop(for_write=False)
49
+ def op() -> dict[str, Any]:
48
50
  return self._get_metadata()
49
51
 
52
+ return op()
53
+
50
54
  def _get_metadata(self) -> dict[str, Any]:
51
55
  return {'name': self._name, 'path': self._path()}
52
56
 
53
- @classmethod
54
57
  @abstractmethod
55
- def _display_name(cls) -> str:
58
+ def _display_name(self) -> str:
56
59
  """
57
60
  Return name displayed in error messages.
58
61
  """
59
62
  pass
60
63
 
64
+ def _display_str(self) -> str:
65
+ return f'{self._display_name()} {self._path()!r}'
66
+
61
67
  def _move(self, new_name: str, new_dir_id: UUID) -> None:
62
68
  """Subclasses need to override this to make the change persistent"""
63
69
  self._name = new_name
@@ -89,6 +89,8 @@ class Table(SchemaObject):
89
89
 
90
90
  ```python
91
91
  {
92
+ 'name': 'my_table',
93
+ 'path': 'my_dir.my_subdir.my_table',
92
94
  'base': None, # If this is a view or snapshot, will contain the name of its base table
93
95
  'schema': {
94
96
  'col1': StringType(),
@@ -96,6 +98,7 @@ class Table(SchemaObject):
96
98
  },
97
99
  'is_replica': False,
98
100
  'version': 22,
101
+ 'version_created': datetime.datetime(...),
99
102
  'schema_version': 1,
100
103
  'comment': '',
101
104
  'num_retained_versions': 10,
@@ -112,6 +115,9 @@ class Table(SchemaObject):
112
115
  md['schema'] = self._get_schema()
113
116
  md['is_replica'] = self._tbl_version_path.is_replica()
114
117
  md['version'] = self._get_version()
118
+ md['version_created'] = datetime.datetime.fromtimestamp(
119
+ self._tbl_version_path.tbl_version.get().created_at, tz=datetime.timezone.utc
120
+ )
115
121
  md['schema_version'] = self._tbl_version_path.schema_version()
116
122
  md['comment'] = self._get_comment()
117
123
  md['num_retained_versions'] = self._get_num_retained_versions()
@@ -147,11 +153,15 @@ class Table(SchemaObject):
147
153
  Returns:
148
154
  A list of view paths.
149
155
  """
150
- from pixeltable.catalog import Catalog
156
+ from pixeltable.catalog import retry_loop
151
157
 
152
- with Catalog.get().begin_xact(for_write=False):
158
+ # we need retry_loop() here, because we end up loading Tables for the views
159
+ @retry_loop(tbl=self._tbl_version_path, for_write=False)
160
+ def op() -> list[str]:
153
161
  return [t._path() for t in self._get_views(recursive=recursive)]
154
162
 
163
+ return op()
164
+
155
165
  def _get_views(self, *, recursive: bool = True, include_snapshots: bool = True) -> list['Table']:
156
166
  cat = catalog.Catalog.get()
157
167
  view_ids = cat.get_view_ids(self._id)
@@ -178,7 +188,7 @@ class Table(SchemaObject):
178
188
  """
179
189
  from pixeltable.catalog import Catalog
180
190
 
181
- with Catalog.get().begin_xact(for_write=False):
191
+ with Catalog.get().begin_xact(tbl=self._tbl_version_path, for_write=False):
182
192
  return self._df().select(*items, **named_items)
183
193
 
184
194
  def where(self, pred: 'exprs.Expr') -> 'pxt.DataFrame':
@@ -188,7 +198,7 @@ class Table(SchemaObject):
188
198
  """
189
199
  from pixeltable.catalog import Catalog
190
200
 
191
- with Catalog.get().begin_xact(for_write=False):
201
+ with Catalog.get().begin_xact(tbl=self._tbl_version_path, for_write=False):
192
202
  return self._df().where(pred)
193
203
 
194
204
  def join(
@@ -201,7 +211,7 @@ class Table(SchemaObject):
201
211
  """Join this table with another table."""
202
212
  from pixeltable.catalog import Catalog
203
213
 
204
- with Catalog.get().begin_xact(for_write=False):
214
+ with Catalog.get().begin_xact(tbl=self._tbl_version_path, for_write=False):
205
215
  return self._df().join(other, on=on, how=how)
206
216
 
207
217
  def order_by(self, *items: 'exprs.Expr', asc: bool = True) -> 'pxt.DataFrame':
@@ -211,7 +221,7 @@ class Table(SchemaObject):
211
221
  """
212
222
  from pixeltable.catalog import Catalog
213
223
 
214
- with Catalog.get().begin_xact(for_write=False):
224
+ with Catalog.get().begin_xact(tbl=self._tbl_version_path, for_write=False):
215
225
  return self._df().order_by(*items, asc=asc)
216
226
 
217
227
  def group_by(self, *items: 'exprs.Expr') -> 'pxt.DataFrame':
@@ -221,7 +231,7 @@ class Table(SchemaObject):
221
231
  """
222
232
  from pixeltable.catalog import Catalog
223
233
 
224
- with Catalog.get().begin_xact(for_write=False):
234
+ with Catalog.get().begin_xact(tbl=self._tbl_version_path, for_write=False):
225
235
  return self._df().group_by(*items)
226
236
 
227
237
  def distinct(self) -> 'pxt.DataFrame':
@@ -277,10 +287,7 @@ class Table(SchemaObject):
277
287
  return {c.name: c.col_type for c in self._tbl_version_path.columns()}
278
288
 
279
289
  def get_base_table(self) -> Optional['Table']:
280
- from pixeltable.catalog import Catalog
281
-
282
- with Catalog.get().begin_xact(for_write=False):
283
- return self._get_base_table()
290
+ return self._get_base_table()
284
291
 
285
292
  @abc.abstractmethod
286
293
  def _get_base_table(self) -> Optional['Table']:
@@ -321,7 +328,7 @@ class Table(SchemaObject):
321
328
  """
322
329
  from pixeltable.catalog import Catalog
323
330
 
324
- with Catalog.get().begin_xact(for_write=False):
331
+ with Catalog.get().begin_xact(tbl=self._tbl_version_path, for_write=False):
325
332
  helper = DescriptionHelper()
326
333
  helper.append(self._table_descriptor())
327
334
  helper.append(self._col_descriptor())
@@ -492,8 +499,7 @@ class Table(SchemaObject):
492
499
 
493
500
  # lock_mutable_tree=True: we might end up having to drop existing columns, which requires locking the tree
494
501
  with Catalog.get().begin_xact(tbl=self._tbl_version_path, for_write=True, lock_mutable_tree=True):
495
- if self._tbl_version_path.is_snapshot():
496
- raise excs.Error('Cannot add column to a snapshot.')
502
+ self.__check_mutable('add columns to')
497
503
  col_schema = {
498
504
  col_name: {'type': ts.ColumnType.normalize_type(spec, nullable_default=True, allow_builtin_types=False)}
499
505
  for col_name, spec in schema.items()
@@ -553,24 +559,18 @@ class Table(SchemaObject):
553
559
 
554
560
  >>> tbl.add_columns({'new_col': pxt.Int})
555
561
  """
556
- from pixeltable.catalog import Catalog
557
-
558
- with Catalog.get().begin_xact(tbl=self._tbl_version_path, for_write=True, lock_mutable_tree=True):
559
- # verify kwargs
560
- if self._tbl_version_path.is_snapshot():
561
- raise excs.Error('Cannot add column to a snapshot.')
562
- # verify kwargs and construct column schema dict
563
- if len(kwargs) != 1:
564
- raise excs.Error(
565
- f'add_column() requires exactly one keyword argument of the form "col_name=col_type"; '
566
- f'got {len(kwargs)} instead ({", ".join(kwargs.keys())})'
567
- )
568
- col_type = next(iter(kwargs.values()))
569
- if not isinstance(col_type, (ts.ColumnType, type, _GenericAlias)):
570
- raise excs.Error(
571
- 'The argument to add_column() must be a type; did you intend to use add_computed_column() instead?'
572
- )
573
- return self.add_columns(kwargs, if_exists=if_exists)
562
+ # verify kwargs and construct column schema dict
563
+ if len(kwargs) != 1:
564
+ raise excs.Error(
565
+ f'add_column() requires exactly one keyword argument of the form "col_name=col_type"; '
566
+ f'got {len(kwargs)} instead ({", ".join(kwargs.keys())})'
567
+ )
568
+ col_type = next(iter(kwargs.values()))
569
+ if not isinstance(col_type, (ts.ColumnType, type, _GenericAlias)):
570
+ raise excs.Error(
571
+ 'The argument to add_column() must be a type; did you intend to use add_computed_column() instead?'
572
+ )
573
+ return self.add_columns(kwargs, if_exists=if_exists)
574
574
 
575
575
  def add_computed_column(
576
576
  self,
@@ -622,8 +622,7 @@ class Table(SchemaObject):
622
622
  from pixeltable.catalog import Catalog
623
623
 
624
624
  with Catalog.get().begin_xact(tbl=self._tbl_version_path, for_write=True, lock_mutable_tree=True):
625
- if self._tbl_version_path.is_snapshot():
626
- raise excs.Error('Cannot add column to a snapshot.')
625
+ self.__check_mutable('add columns to')
627
626
  if len(kwargs) != 1:
628
627
  raise excs.Error(
629
628
  f'add_computed_column() requires exactly one keyword argument of the form '
@@ -808,10 +807,10 @@ class Table(SchemaObject):
808
807
  from pixeltable.catalog import Catalog
809
808
 
810
809
  cat = Catalog.get()
810
+
811
811
  # lock_mutable_tree=True: we need to be able to see whether any transitive view has column dependents
812
812
  with cat.begin_xact(tbl=self._tbl_version_path, for_write=True, lock_mutable_tree=True):
813
- if self._tbl_version_path.is_snapshot():
814
- raise excs.Error('Cannot drop column from a snapshot.')
813
+ self.__check_mutable('drop columns from')
815
814
  col: Column = None
816
815
  if_not_exists_ = IfNotExistsParam.validated(if_not_exists, 'if_not_exists')
817
816
 
@@ -835,7 +834,7 @@ class Table(SchemaObject):
835
834
  dependent_user_cols = [c for c in cat.get_column_dependents(col.tbl.id, col.id) if c.name is not None]
836
835
  if len(dependent_user_cols) > 0:
837
836
  raise excs.Error(
838
- f'Cannot drop column `{col.name}` because the following columns depend on it:\n'
837
+ f'Cannot drop column {col.name!r} because the following columns depend on it:\n'
839
838
  f'{", ".join(c.name for c in dependent_user_cols)}'
840
839
  )
841
840
 
@@ -989,8 +988,7 @@ class Table(SchemaObject):
989
988
  from pixeltable.catalog import Catalog
990
989
 
991
990
  with Catalog.get().begin_xact(tbl=self._tbl_version_path, for_write=True, lock_mutable_tree=True):
992
- if self._tbl_version_path.is_snapshot():
993
- raise excs.Error('Cannot add an index to a snapshot')
991
+ self.__check_mutable('add an index to')
994
992
  col = self._resolve_column_parameter(column)
995
993
 
996
994
  if idx_name is not None and idx_name in self._tbl_version.get().idxs_by_name:
@@ -1174,8 +1172,7 @@ class Table(SchemaObject):
1174
1172
  ) -> None:
1175
1173
  from pixeltable.catalog import Catalog
1176
1174
 
1177
- if self._tbl_version_path.is_snapshot():
1178
- raise excs.Error('Cannot drop an index from a snapshot')
1175
+ self.__check_mutable('drop an index from')
1179
1176
  assert (col is None) != (idx_name is None)
1180
1177
 
1181
1178
  if idx_name is not None:
@@ -1347,8 +1344,7 @@ class Table(SchemaObject):
1347
1344
  from pixeltable.catalog import Catalog
1348
1345
 
1349
1346
  with Catalog.get().begin_xact(tbl=self._tbl_version_path, for_write=True, lock_mutable_tree=True):
1350
- if self._tbl_version_path.is_snapshot():
1351
- raise excs.Error('Cannot update a snapshot')
1347
+ self.__check_mutable('update')
1352
1348
  result = self._tbl_version.get().update(value_spec, where, cascade)
1353
1349
  FileCache.get().emit_eviction_warnings()
1354
1350
  return result
@@ -1387,8 +1383,7 @@ class Table(SchemaObject):
1387
1383
  from pixeltable.catalog import Catalog
1388
1384
 
1389
1385
  with Catalog.get().begin_xact(tbl=self._tbl_version_path, for_write=True, lock_mutable_tree=True):
1390
- if self._tbl_version_path.is_snapshot():
1391
- raise excs.Error('Cannot update a snapshot')
1386
+ self.__check_mutable('update')
1392
1387
  rows = list(rows)
1393
1388
 
1394
1389
  row_updates: list[dict[Column, exprs.Expr]] = []
@@ -1456,8 +1451,7 @@ class Table(SchemaObject):
1456
1451
  cat = Catalog.get()
1457
1452
  # lock_mutable_tree=True: we need to be able to see whether any transitive view has column dependents
1458
1453
  with cat.begin_xact(tbl=self._tbl_version_path, for_write=True, lock_mutable_tree=True):
1459
- if self._tbl_version_path.is_snapshot():
1460
- raise excs.Error('Cannot recompute columns of a snapshot.')
1454
+ self.__check_mutable('recompute columns of')
1461
1455
  if len(columns) == 0:
1462
1456
  raise excs.Error('At least one column must be specified to recompute')
1463
1457
  if errors_only and len(columns) > 1:
@@ -1514,8 +1508,7 @@ class Table(SchemaObject):
1514
1508
  from pixeltable.catalog import Catalog
1515
1509
 
1516
1510
  with Catalog.get().begin_xact(tbl=self._tbl_version_path, for_write=True, lock_mutable_tree=True):
1517
- if self._tbl_version_path.is_snapshot():
1518
- raise excs.Error('Cannot revert a snapshot')
1511
+ self.__check_mutable('revert')
1519
1512
  self._tbl_version.get().revert()
1520
1513
  # remove cached md in order to force a reload on the next operation
1521
1514
  self._tbl_version_path.clear_cached_md()
@@ -1530,8 +1523,7 @@ class Table(SchemaObject):
1530
1523
  from pixeltable.catalog import Catalog
1531
1524
 
1532
1525
  with Catalog.get().begin_xact(tbl=self._tbl_version_path, for_write=True, lock_mutable_tree=False):
1533
- if self._tbl_version_path.is_snapshot():
1534
- raise excs.Error(f'Table `{self._name}` is a snapshot, so it cannot be linked to an external store.')
1526
+ self.__check_mutable('link an external store to')
1535
1527
  if store.name in self.external_stores():
1536
1528
  raise excs.Error(f'Table `{self._name}` already has an external store with that name: {store.name}')
1537
1529
  _logger.info(f'Linking external store `{store.name}` to table `{self._name}`')
@@ -1560,7 +1552,7 @@ class Table(SchemaObject):
1560
1552
  """
1561
1553
  from pixeltable.catalog import Catalog
1562
1554
 
1563
- if self._tbl_version_path.is_snapshot():
1555
+ if not self._tbl_version_path.is_mutable():
1564
1556
  return
1565
1557
  with Catalog.get().begin_xact(tbl=self._tbl_version_path, for_write=True, lock_mutable_tree=False):
1566
1558
  all_stores = self.external_stores()
@@ -1600,7 +1592,7 @@ class Table(SchemaObject):
1600
1592
  """
1601
1593
  from pixeltable.catalog import Catalog
1602
1594
 
1603
- if self._tbl_version_path.is_snapshot():
1595
+ if not self._tbl_version_path.is_mutable():
1604
1596
  return UpdateStatus()
1605
1597
  # we lock the entire tree starting at the root base table in order to ensure that all synced columns can
1606
1598
  # have their updates propagated down the tree
@@ -1711,3 +1703,9 @@ class Table(SchemaObject):
1711
1703
  report_lines.append(report_line)
1712
1704
 
1713
1705
  return pxt.dataframe.DataFrameResultSet(report_lines, self._REPORT_SCHEMA)
1706
+
1707
+ def __check_mutable(self, op_descr: str) -> None:
1708
+ if self._tbl_version_path.is_snapshot():
1709
+ raise excs.Error(f'{self._display_str()}: Cannot {op_descr} a snapshot.')
1710
+ if self._tbl_version_path.is_replica():
1711
+ raise excs.Error(f'{self._display_str()}: Cannot {op_descr} a {self._display_name()}.')