pixeltable 0.4.0rc3__py3-none-any.whl → 0.4.1__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/__init__.py +1 -1
  2. pixeltable/__version__.py +2 -2
  3. pixeltable/catalog/__init__.py +9 -1
  4. pixeltable/catalog/catalog.py +333 -99
  5. pixeltable/catalog/column.py +28 -26
  6. pixeltable/catalog/globals.py +12 -0
  7. pixeltable/catalog/insertable_table.py +8 -8
  8. pixeltable/catalog/schema_object.py +6 -0
  9. pixeltable/catalog/table.py +111 -116
  10. pixeltable/catalog/table_version.py +36 -50
  11. pixeltable/catalog/table_version_handle.py +4 -1
  12. pixeltable/catalog/table_version_path.py +28 -4
  13. pixeltable/catalog/view.py +10 -18
  14. pixeltable/config.py +4 -0
  15. pixeltable/dataframe.py +10 -9
  16. pixeltable/env.py +5 -11
  17. pixeltable/exceptions.py +6 -0
  18. pixeltable/exec/exec_node.py +2 -0
  19. pixeltable/exec/expr_eval/expr_eval_node.py +4 -4
  20. pixeltable/exec/sql_node.py +47 -30
  21. pixeltable/exprs/column_property_ref.py +2 -1
  22. pixeltable/exprs/column_ref.py +7 -6
  23. pixeltable/exprs/expr.py +4 -4
  24. pixeltable/func/__init__.py +1 -0
  25. pixeltable/func/mcp.py +74 -0
  26. pixeltable/func/query_template_function.py +4 -2
  27. pixeltable/func/tools.py +12 -2
  28. pixeltable/func/udf.py +2 -2
  29. pixeltable/functions/__init__.py +1 -0
  30. pixeltable/functions/groq.py +108 -0
  31. pixeltable/functions/huggingface.py +8 -6
  32. pixeltable/functions/mistralai.py +2 -13
  33. pixeltable/functions/openai.py +1 -6
  34. pixeltable/functions/replicate.py +2 -2
  35. pixeltable/functions/util.py +6 -1
  36. pixeltable/globals.py +0 -2
  37. pixeltable/io/external_store.py +2 -2
  38. pixeltable/io/label_studio.py +4 -4
  39. pixeltable/io/table_data_conduit.py +1 -1
  40. pixeltable/metadata/__init__.py +1 -1
  41. pixeltable/metadata/converters/convert_37.py +15 -0
  42. pixeltable/metadata/notes.py +1 -0
  43. pixeltable/metadata/schema.py +5 -0
  44. pixeltable/plan.py +37 -121
  45. pixeltable/share/packager.py +2 -2
  46. pixeltable/type_system.py +30 -0
  47. {pixeltable-0.4.0rc3.dist-info → pixeltable-0.4.1.dist-info}/METADATA +1 -1
  48. {pixeltable-0.4.0rc3.dist-info → pixeltable-0.4.1.dist-info}/RECORD +51 -49
  49. pixeltable/utils/sample.py +0 -25
  50. {pixeltable-0.4.0rc3.dist-info → pixeltable-0.4.1.dist-info}/LICENSE +0 -0
  51. {pixeltable-0.4.0rc3.dist-info → pixeltable-0.4.1.dist-info}/WHEEL +0 -0
  52. {pixeltable-0.4.0rc3.dist-info → pixeltable-0.4.1.dist-info}/entry_points.txt +0 -0
@@ -48,21 +48,23 @@ class Table(SchemaObject):
48
48
  """
49
49
  A handle to a table, view, or snapshot. This class is the primary interface through which table operations
50
50
  (queries, insertions, updates, etc.) are performed in Pixeltable.
51
+
52
+ Every user-invoked operation that runs an ExecNode tree (directly or indirectly) needs to call
53
+ FileCache.emit_eviction_warnings() at the end of the operation.
51
54
  """
52
55
 
53
- # Every user-invoked operation that runs an ExecNode tree (directly or indirectly) needs to call
54
- # FileCache.emit_eviction_warnings() at the end of the operation.
56
+ # the chain of TableVersions needed to run queries and supply metadata (eg, schema)
57
+ _tbl_version_path: TableVersionPath
55
58
 
56
- _is_dropped: bool
57
- __tbl_version_path: TableVersionPath
59
+ # the physical TableVersion backing this Table; None for pure snapshots
60
+ _tbl_version: Optional[TableVersionHandle]
58
61
 
59
62
  def __init__(self, id: UUID, dir_id: UUID, name: str, tbl_version_path: TableVersionPath):
60
63
  super().__init__(id, name, dir_id)
61
- self._is_dropped = False
62
- self.__tbl_version_path = tbl_version_path
64
+ self._tbl_version_path = tbl_version_path
65
+ self._tbl_version = None
63
66
 
64
67
  def _move(self, new_name: str, new_dir_id: UUID) -> None:
65
- self._check_is_dropped()
66
68
  super()._move(new_name, new_dir_id)
67
69
  conn = env.Env.get().conn
68
70
  stmt = sql.text(
@@ -75,6 +77,7 @@ class Table(SchemaObject):
75
77
  )
76
78
  conn.execute(stmt, {'new_dir_id': new_dir_id, 'new_name': json.dumps(new_name), 'id': self._id})
77
79
 
80
+ # this is duplicated from SchemaObject so that our API docs show the docstring for Table
78
81
  def get_metadata(self) -> dict[str, Any]:
79
82
  """
80
83
  Retrieves metadata associated with this table.
@@ -100,42 +103,27 @@ class Table(SchemaObject):
100
103
  }
101
104
  ```
102
105
  """
103
- from pixeltable.catalog import Catalog
104
-
105
- with Catalog.get().begin_xact(for_write=False):
106
- self._check_is_dropped()
107
- md = super().get_metadata()
108
- md['base'] = self._base_table._path() if self._base_table is not None else None
109
- md['schema'] = self._schema
110
- md['is_replica'] = self._tbl_version.get().is_replica
111
- md['version'] = self._version
112
- md['schema_version'] = self._tbl_version.get().schema_version
113
- md['comment'] = self._comment
114
- md['num_retained_versions'] = self._num_retained_versions
115
- md['media_validation'] = self._media_validation.name.lower()
116
- return md
117
-
118
- @property
119
- def _version(self) -> int:
106
+ return super().get_metadata()
107
+
108
+ def _get_metadata(self) -> dict[str, Any]:
109
+ md = super()._get_metadata()
110
+ base = self._get_base_table()
111
+ md['base'] = base._path() if base is not None else None
112
+ md['schema'] = self._get_schema()
113
+ md['is_replica'] = self._tbl_version_path.is_replica()
114
+ md['version'] = self._get_version()
115
+ md['schema_version'] = self._tbl_version_path.schema_version()
116
+ md['comment'] = self._get_comment()
117
+ md['num_retained_versions'] = self._get_num_retained_versions()
118
+ md['media_validation'] = self._get_media_validation().name.lower()
119
+ return md
120
+
121
+ def _get_version(self) -> int:
120
122
  """Return the version of this table. Used by tests to ascertain version changes."""
121
- return self._tbl_version.get().version
122
-
123
- @property
124
- def _tbl_version(self) -> TableVersionHandle:
125
- """Return TableVersion for just this table."""
126
- return self._tbl_version_path.tbl_version
127
-
128
- @property
129
- def _tbl_version_path(self) -> TableVersionPath:
130
- self._check_is_dropped()
131
- return self.__tbl_version_path
123
+ return self._tbl_version_path.version()
132
124
 
133
125
  def __hash__(self) -> int:
134
- return hash(self._tbl_version.id)
135
-
136
- def _check_is_dropped(self) -> None:
137
- if self._is_dropped:
138
- raise excs.Error(f'{self._display_name()} {self._name} has been dropped')
126
+ return hash(self._tbl_version_path.tbl_id)
139
127
 
140
128
  def __getattr__(self, name: str) -> 'exprs.ColumnRef':
141
129
  """Return a ColumnRef for the given name."""
@@ -162,15 +150,18 @@ class Table(SchemaObject):
162
150
  from pixeltable.catalog import Catalog
163
151
 
164
152
  with Catalog.get().begin_xact(for_write=False):
165
- self._check_is_dropped()
166
153
  return [t._path() for t in self._get_views(recursive=recursive)]
167
154
 
168
- def _get_views(self, *, recursive: bool = True) -> list['Table']:
155
+ def _get_views(self, *, recursive: bool = True, include_snapshots: bool = True) -> list['Table']:
169
156
  cat = catalog.Catalog.get()
170
157
  view_ids = cat.get_view_ids(self._id)
171
158
  views = [cat.get_table_by_id(id) for id in view_ids]
159
+ if not include_snapshots:
160
+ views = [t for t in views if not t._tbl_version_path.is_snapshot()]
172
161
  if recursive:
173
- views.extend([t for view in views for t in view._get_views(recursive=True)])
162
+ views.extend(
163
+ t for view in views for t in view._get_views(recursive=True, include_snapshots=include_snapshots)
164
+ )
174
165
  return views
175
166
 
176
167
  def _df(self) -> 'pxt.dataframe.DataFrame':
@@ -276,35 +267,32 @@ class Table(SchemaObject):
276
267
  """Return the number of rows in this table."""
277
268
  return self._df().count()
278
269
 
279
- @property
280
270
  def columns(self) -> list[str]:
281
271
  """Return the names of the columns in this table."""
282
272
  cols = self._tbl_version_path.columns()
283
273
  return [c.name for c in cols]
284
274
 
285
- @property
286
- def _schema(self) -> dict[str, ts.ColumnType]:
275
+ def _get_schema(self) -> dict[str, ts.ColumnType]:
287
276
  """Return the schema (column names and column types) of this table."""
288
277
  return {c.name: c.col_type for c in self._tbl_version_path.columns()}
289
278
 
290
- @property
291
- def base_table(self) -> Optional['Table']:
292
- with env.Env.get().begin_xact():
293
- return self._base_table
279
+ 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()
294
284
 
295
- @property
296
285
  @abc.abstractmethod
297
- def _base_table(self) -> Optional['Table']:
298
- """The base's Table instance"""
286
+ def _get_base_table(self) -> Optional['Table']:
287
+ """The base's Table instance. Requires a transaction context"""
299
288
 
300
- @property
301
- def _base_tables(self) -> list['Table']:
302
- """The ancestor list of bases of this table, starting with its immediate base."""
303
- bases = []
304
- base = self._base_table
289
+ def _get_base_tables(self) -> list['Table']:
290
+ """The ancestor list of bases of this table, starting with its immediate base. Requires a transaction context"""
291
+ bases: list[Table] = []
292
+ base = self._get_base_table()
305
293
  while base is not None:
306
294
  bases.append(base)
307
- base = base._base_table
295
+ base = base._get_base_table()
308
296
  return bases
309
297
 
310
298
  @property
@@ -312,17 +300,14 @@ class Table(SchemaObject):
312
300
  def _effective_base_versions(self) -> list[Optional[int]]:
313
301
  """The effective versions of the ancestor bases, starting with its immediate base."""
314
302
 
315
- @property
316
- def _comment(self) -> str:
317
- return self._tbl_version.get().comment
303
+ def _get_comment(self) -> str:
304
+ return self._tbl_version_path.comment()
318
305
 
319
- @property
320
- def _num_retained_versions(self) -> int:
321
- return self._tbl_version.get().num_retained_versions
306
+ def _get_num_retained_versions(self) -> int:
307
+ return self._tbl_version_path.num_retained_versions()
322
308
 
323
- @property
324
- def _media_validation(self) -> MediaValidation:
325
- return self._tbl_version.get().media_validation
309
+ def _get_media_validation(self) -> MediaValidation:
310
+ return self._tbl_version_path.media_validation()
326
311
 
327
312
  def __repr__(self) -> str:
328
313
  return self._descriptors().to_string()
@@ -346,8 +331,8 @@ class Table(SchemaObject):
346
331
  stores = self._external_store_descriptor()
347
332
  if not stores.empty:
348
333
  helper.append(stores)
349
- if self._comment:
350
- helper.append(f'COMMENT: {self._comment}')
334
+ if self._get_comment():
335
+ helper.append(f'COMMENT: {self._get_comment()}')
351
336
  return helper
352
337
 
353
338
  def _col_descriptor(self, columns: Optional[list[str]] = None) -> pd.DataFrame:
@@ -364,6 +349,8 @@ class Table(SchemaObject):
364
349
  def _index_descriptor(self, columns: Optional[list[str]] = None) -> pd.DataFrame:
365
350
  from pixeltable import index
366
351
 
352
+ if self._tbl_version is None:
353
+ return pd.DataFrame([])
367
354
  pd_rows = []
368
355
  for name, info in self._tbl_version.get().idxs_by_name.items():
369
356
  if isinstance(info.idx, index.EmbeddingIndex) and (columns is None or info.col.name in columns):
@@ -383,7 +370,7 @@ class Table(SchemaObject):
383
370
 
384
371
  def _external_store_descriptor(self) -> pd.DataFrame:
385
372
  pd_rows = []
386
- for name, store in self._tbl_version.get().external_stores.items():
373
+ for name, store in self._tbl_version_path.tbl_version.get().external_stores.items():
387
374
  row = {'External Store': name, 'Type': type(store).__name__}
388
375
  pd_rows.append(row)
389
376
  return pd.DataFrame(pd_rows)
@@ -392,7 +379,6 @@ class Table(SchemaObject):
392
379
  """
393
380
  Print the table schema.
394
381
  """
395
- self._check_is_dropped()
396
382
  if getattr(builtins, '__IPYTHON__', False):
397
383
  from IPython.display import Markdown, display
398
384
 
@@ -400,11 +386,6 @@ class Table(SchemaObject):
400
386
  else:
401
387
  print(repr(self))
402
388
 
403
- def _drop(self) -> None:
404
- self._check_is_dropped()
405
- self._tbl_version.get().drop()
406
- self._is_dropped = True
407
-
408
389
  # TODO Factor this out into a separate module.
409
390
  # The return type is unresolvable, but torch can't be imported since it's an optional dependency.
410
391
  def to_pytorch_dataset(self, image_format: str = 'pt') -> 'torch.utils.data.IterableDataset':
@@ -422,9 +403,11 @@ class Table(SchemaObject):
422
403
  def _column_has_dependents(self, col: Column) -> bool:
423
404
  """Returns True if the column has dependents, False otherwise."""
424
405
  assert col is not None
425
- assert col.name in self._schema
426
- if any(c.name is not None for c in col.dependent_cols):
406
+ assert col.name in self._get_schema()
407
+ cat = catalog.Catalog.get()
408
+ if any(c.name is not None for c in cat.get_column_dependents(col.tbl.id, col.id)):
427
409
  return True
410
+ assert self._tbl_version is not None
428
411
  return any(
429
412
  col in store.get_local_columns()
430
413
  for view in (self, *self._get_views(recursive=True))
@@ -436,8 +419,8 @@ class Table(SchemaObject):
436
419
 
437
420
  If `if_exists='ignore'`, returns a list of existing columns, if any, in `new_col_names`.
438
421
  """
439
- assert not self.get_metadata()['is_snapshot']
440
- existing_col_names = set(self._schema.keys())
422
+ assert self._tbl_version is not None
423
+ existing_col_names = set(self._get_schema().keys())
441
424
  cols_to_ignore = []
442
425
  for new_col_name in new_col_names:
443
426
  if new_col_name in existing_col_names:
@@ -507,9 +490,9 @@ class Table(SchemaObject):
507
490
  """
508
491
  from pixeltable.catalog import Catalog
509
492
 
510
- with Catalog.get().begin_xact(tbl_id=self._id, for_write=True):
511
- self._check_is_dropped()
512
- if self.get_metadata()['is_snapshot']:
493
+ # lock_mutable_tree=True: we might end up having to drop existing columns, which requires locking the tree
494
+ 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():
513
496
  raise excs.Error('Cannot add column to a snapshot.')
514
497
  col_schema = {
515
498
  col_name: {'type': ts.ColumnType.normalize_type(spec, nullable_default=True, allow_builtin_types=False)}
@@ -530,6 +513,7 @@ class Table(SchemaObject):
530
513
  new_cols = self._create_columns(col_schema)
531
514
  for new_col in new_cols:
532
515
  self._verify_column(new_col)
516
+ assert self._tbl_version is not None
533
517
  status = self._tbl_version.get().add_columns(new_cols, print_stats=False, on_error='abort')
534
518
  FileCache.get().emit_eviction_warnings()
535
519
  return status
@@ -570,10 +554,9 @@ class Table(SchemaObject):
570
554
  """
571
555
  from pixeltable.catalog import Catalog
572
556
 
573
- with Catalog.get().begin_xact(tbl_id=self._id, for_write=True):
574
- self._check_is_dropped()
557
+ with Catalog.get().begin_xact(tbl=self._tbl_version_path, for_write=True, lock_mutable_tree=True):
575
558
  # verify kwargs
576
- if self._tbl_version.get().is_snapshot:
559
+ if self._tbl_version_path.is_snapshot():
577
560
  raise excs.Error('Cannot add column to a snapshot.')
578
561
  # verify kwargs and construct column schema dict
579
562
  if len(kwargs) != 1:
@@ -637,9 +620,8 @@ class Table(SchemaObject):
637
620
  """
638
621
  from pixeltable.catalog import Catalog
639
622
 
640
- with Catalog.get().begin_xact(tbl_id=self._id, for_write=True):
641
- self._check_is_dropped()
642
- if self.get_metadata()['is_snapshot']:
623
+ with Catalog.get().begin_xact(tbl=self._tbl_version_path, for_write=True, lock_mutable_tree=True):
624
+ if self._tbl_version_path.is_snapshot():
643
625
  raise excs.Error('Cannot add column to a snapshot.')
644
626
  if len(kwargs) != 1:
645
627
  raise excs.Error(
@@ -676,6 +658,7 @@ class Table(SchemaObject):
676
658
 
677
659
  new_col = self._create_columns({col_name: col_schema})[0]
678
660
  self._verify_column(new_col)
661
+ assert self._tbl_version is not None
679
662
  status = self._tbl_version.get().add_columns([new_col], print_stats=print_stats, on_error=on_error)
680
663
  FileCache.get().emit_eviction_warnings()
681
664
  return status
@@ -822,8 +805,9 @@ class Table(SchemaObject):
822
805
  """
823
806
  from pixeltable.catalog import Catalog
824
807
 
825
- with Catalog.get().begin_xact(tbl_id=self._id, for_write=True):
826
- self._check_is_dropped()
808
+ cat = Catalog.get()
809
+ # lock_mutable_tree=True: we need to be able to see whether any transitive view has column dependents
810
+ with cat.begin_xact(tbl=self._tbl_version_path, for_write=True, lock_mutable_tree=True):
827
811
  if self._tbl_version_path.is_snapshot():
828
812
  raise excs.Error('Cannot drop column from a snapshot.')
829
813
  col: Column = None
@@ -846,18 +830,19 @@ class Table(SchemaObject):
846
830
  return
847
831
  col = column.col
848
832
 
849
- dependent_user_cols = [c for c in col.dependent_cols if c.name is not None]
833
+ dependent_user_cols = [c for c in cat.get_column_dependents(col.tbl.id, col.id) if c.name is not None]
850
834
  if len(dependent_user_cols) > 0:
851
835
  raise excs.Error(
852
836
  f'Cannot drop column `{col.name}` because the following columns depend on it:\n'
853
837
  f'{", ".join(c.name for c in dependent_user_cols)}'
854
838
  )
855
839
 
840
+ _ = self._get_views(recursive=True, include_snapshots=False)
856
841
  # See if this column has a dependent store. We need to look through all stores in all
857
842
  # (transitive) views of this table.
858
843
  dependent_stores = [
859
844
  (view, store)
860
- for view in (self, *self._get_views(recursive=True))
845
+ for view in (self, *self._get_views(recursive=True, include_snapshots=False))
861
846
  for store in view._tbl_version.get().external_stores.values()
862
847
  if col in store.get_local_columns()
863
848
  ]
@@ -891,7 +876,7 @@ class Table(SchemaObject):
891
876
  """
892
877
  from pixeltable.catalog import Catalog
893
878
 
894
- with Catalog.get().begin_xact(tbl_id=self._id, for_write=True):
879
+ with Catalog.get().begin_xact(tbl=self._tbl_version_path, for_write=True, lock_mutable_tree=False):
895
880
  self._tbl_version.get().rename_column(old_name, new_name)
896
881
 
897
882
  def _list_index_info_for_test(self) -> list[dict[str, Any]]:
@@ -902,7 +887,6 @@ class Table(SchemaObject):
902
887
  A list of index information, each containing the index's
903
888
  id, name, and the name of the column it indexes.
904
889
  """
905
- assert not self._is_dropped
906
890
  index_info = []
907
891
  for idx_name, idx in self._tbl_version.get().idxs_by_name.items():
908
892
  index_info.append({'_id': idx.id, '_name': idx_name, '_column': idx.col.name})
@@ -1001,7 +985,7 @@ class Table(SchemaObject):
1001
985
  """
1002
986
  from pixeltable.catalog import Catalog
1003
987
 
1004
- with Catalog.get().begin_xact(tbl_id=self._id, for_write=True):
988
+ with Catalog.get().begin_xact(tbl=self._tbl_version_path, for_write=True, lock_mutable_tree=True):
1005
989
  if self._tbl_version_path.is_snapshot():
1006
990
  raise excs.Error('Cannot add an index to a snapshot')
1007
991
  col = self._resolve_column_parameter(column)
@@ -1090,7 +1074,7 @@ class Table(SchemaObject):
1090
1074
  if (column is None) == (idx_name is None):
1091
1075
  raise excs.Error("Exactly one of 'column' or 'idx_name' must be provided")
1092
1076
 
1093
- with Catalog.get().begin_xact(tbl_id=self._id, for_write=True):
1077
+ with Catalog.get().begin_xact(tbl=self._tbl_version_path, for_write=True, lock_mutable_tree=True):
1094
1078
  col: Column = None
1095
1079
  if idx_name is None:
1096
1080
  col = self._resolve_column_parameter(column)
@@ -1169,7 +1153,7 @@ class Table(SchemaObject):
1169
1153
  if (column is None) == (idx_name is None):
1170
1154
  raise excs.Error("Exactly one of 'column' or 'idx_name' must be provided")
1171
1155
 
1172
- with Catalog.get().begin_xact(tbl_id=self._id, for_write=True):
1156
+ with Catalog.get().begin_xact(tbl=self._tbl_version_path, for_write=True, lock_mutable_tree=False):
1173
1157
  col: Column = None
1174
1158
  if idx_name is None:
1175
1159
  col = self._resolve_column_parameter(column)
@@ -1185,6 +1169,8 @@ class Table(SchemaObject):
1185
1169
  _idx_class: Optional[type[index.IndexBase]] = None,
1186
1170
  if_not_exists: Literal['error', 'ignore'] = 'error',
1187
1171
  ) -> None:
1172
+ from pixeltable.catalog import Catalog
1173
+
1188
1174
  if self._tbl_version_path.is_snapshot():
1189
1175
  raise excs.Error('Cannot drop an index from a snapshot')
1190
1176
  assert (col is None) != (idx_name is None)
@@ -1216,7 +1202,10 @@ class Table(SchemaObject):
1216
1202
  idx_info = idx_info_list[0]
1217
1203
 
1218
1204
  # Find out if anything depends on this index
1219
- dependent_user_cols = [c for c in idx_info.val_col.dependent_cols if c.name is not None]
1205
+ val_col = idx_info.val_col
1206
+ dependent_user_cols = [
1207
+ c for c in Catalog.get().get_column_dependents(val_col.tbl.id, val_col.id) if c.name is not None
1208
+ ]
1220
1209
  if len(dependent_user_cols) > 0:
1221
1210
  raise excs.Error(
1222
1211
  f'Cannot drop index because the following columns depend on it:\n'
@@ -1351,7 +1340,9 @@ class Table(SchemaObject):
1351
1340
  """
1352
1341
  from pixeltable.catalog import Catalog
1353
1342
 
1354
- with Catalog.get().begin_xact(tbl_id=self._id, for_write=True):
1343
+ with Catalog.get().begin_xact(tbl=self._tbl_version_path, for_write=True, lock_mutable_tree=True):
1344
+ if self._tbl_version_path.is_snapshot():
1345
+ raise excs.Error('Cannot update a snapshot')
1355
1346
  status = self._tbl_version.get().update(value_spec, where, cascade)
1356
1347
  FileCache.get().emit_eviction_warnings()
1357
1348
  return status
@@ -1389,7 +1380,7 @@ class Table(SchemaObject):
1389
1380
  """
1390
1381
  from pixeltable.catalog import Catalog
1391
1382
 
1392
- with Catalog.get().begin_xact(tbl_id=self._id, for_write=True):
1383
+ with Catalog.get().begin_xact(tbl=self._tbl_version_path, for_write=True, lock_mutable_tree=True):
1393
1384
  if self._tbl_version_path.is_snapshot():
1394
1385
  raise excs.Error('Cannot update a snapshot')
1395
1386
  rows = list(rows)
@@ -1453,14 +1444,13 @@ class Table(SchemaObject):
1453
1444
  """
1454
1445
  from pixeltable.catalog import Catalog
1455
1446
 
1456
- with Catalog.get().begin_xact(tbl_id=self._id, for_write=True):
1447
+ with Catalog.get().begin_xact(tbl=self._tbl_version_path, for_write=True, lock_mutable_tree=True):
1457
1448
  if self._tbl_version_path.is_snapshot():
1458
1449
  raise excs.Error('Cannot revert a snapshot')
1459
1450
  self._tbl_version.get().revert()
1460
1451
  # remove cached md in order to force a reload on the next operation
1461
- self.__tbl_version_path.clear_cached_md()
1452
+ self._tbl_version_path.clear_cached_md()
1462
1453
 
1463
- @property
1464
1454
  def external_stores(self) -> list[str]:
1465
1455
  return list(self._tbl_version.get().external_stores.keys())
1466
1456
 
@@ -1470,10 +1460,10 @@ class Table(SchemaObject):
1470
1460
  """
1471
1461
  from pixeltable.catalog import Catalog
1472
1462
 
1473
- with Catalog.get().begin_xact(tbl_id=self._id, for_write=True):
1474
- if self._tbl_version.get().is_snapshot:
1463
+ with Catalog.get().begin_xact(tbl=self._tbl_version_path, for_write=True, lock_mutable_tree=False):
1464
+ if self._tbl_version_path.is_snapshot():
1475
1465
  raise excs.Error(f'Table `{self._name}` is a snapshot, so it cannot be linked to an external store.')
1476
- if store.name in self.external_stores:
1466
+ if store.name in self.external_stores():
1477
1467
  raise excs.Error(f'Table `{self._name}` already has an external store with that name: {store.name}')
1478
1468
  _logger.info(f'Linking external store `{store.name}` to table `{self._name}`')
1479
1469
 
@@ -1501,9 +1491,10 @@ class Table(SchemaObject):
1501
1491
  """
1502
1492
  from pixeltable.catalog import Catalog
1503
1493
 
1504
- with Catalog.get().begin_xact(tbl_id=self._id, for_write=True):
1505
- self._check_is_dropped()
1506
- all_stores = self.external_stores
1494
+ if self._tbl_version_path.is_snapshot():
1495
+ return
1496
+ with Catalog.get().begin_xact(tbl=self._tbl_version_path, for_write=True, lock_mutable_tree=False):
1497
+ all_stores = self.external_stores()
1507
1498
 
1508
1499
  if stores is None:
1509
1500
  stores = all_stores
@@ -1540,9 +1531,13 @@ class Table(SchemaObject):
1540
1531
  """
1541
1532
  from pixeltable.catalog import Catalog
1542
1533
 
1543
- with Catalog.get().begin_xact(tbl_id=self._id, for_write=True):
1544
- self._check_is_dropped()
1545
- all_stores = self.external_stores
1534
+ if self._tbl_version_path.is_snapshot():
1535
+ return pxt.io.SyncStatus.empty()
1536
+ # we lock the entire tree starting at the root base table in order to ensure that all synced columns can
1537
+ # have their updates propagated down the tree
1538
+ base_tv = self._tbl_version_path.get_tbl_versions()[-1]
1539
+ with Catalog.get().begin_xact(tbl=TableVersionPath(base_tv), for_write=True, lock_mutable_tree=True):
1540
+ all_stores = self.external_stores()
1546
1541
 
1547
1542
  if stores is None:
1548
1543
  stores = all_stores
@@ -1562,7 +1557,7 @@ class Table(SchemaObject):
1562
1557
  return sync_status
1563
1558
 
1564
1559
  def __dir__(self) -> list[str]:
1565
- return list(super().__dir__()) + list(self._schema.keys())
1560
+ return list(super().__dir__()) + list(self._get_schema().keys())
1566
1561
 
1567
1562
  def _ipython_key_completions_(self) -> list[str]:
1568
- return list(self._schema.keys())
1563
+ return list(self._get_schema().keys())