pixeltable 0.3.6__py3-none-any.whl → 0.3.8__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 (127) hide show
  1. pixeltable/__init__.py +5 -3
  2. pixeltable/__version__.py +2 -2
  3. pixeltable/catalog/__init__.py +1 -0
  4. pixeltable/catalog/catalog.py +335 -128
  5. pixeltable/catalog/column.py +22 -5
  6. pixeltable/catalog/dir.py +19 -6
  7. pixeltable/catalog/insertable_table.py +34 -37
  8. pixeltable/catalog/named_function.py +0 -4
  9. pixeltable/catalog/schema_object.py +28 -42
  10. pixeltable/catalog/table.py +193 -158
  11. pixeltable/catalog/table_version.py +191 -232
  12. pixeltable/catalog/table_version_handle.py +50 -0
  13. pixeltable/catalog/table_version_path.py +49 -33
  14. pixeltable/catalog/view.py +56 -96
  15. pixeltable/config.py +103 -0
  16. pixeltable/dataframe.py +89 -89
  17. pixeltable/env.py +98 -168
  18. pixeltable/exec/aggregation_node.py +5 -4
  19. pixeltable/exec/cache_prefetch_node.py +1 -1
  20. pixeltable/exec/component_iteration_node.py +13 -9
  21. pixeltable/exec/data_row_batch.py +3 -3
  22. pixeltable/exec/exec_context.py +0 -4
  23. pixeltable/exec/exec_node.py +3 -2
  24. pixeltable/exec/expr_eval/schedulers.py +2 -1
  25. pixeltable/exec/in_memory_data_node.py +9 -4
  26. pixeltable/exec/row_update_node.py +1 -2
  27. pixeltable/exec/sql_node.py +20 -16
  28. pixeltable/exprs/__init__.py +2 -0
  29. pixeltable/exprs/arithmetic_expr.py +7 -11
  30. pixeltable/exprs/array_slice.py +1 -1
  31. pixeltable/exprs/column_property_ref.py +3 -3
  32. pixeltable/exprs/column_ref.py +12 -13
  33. pixeltable/exprs/comparison.py +3 -6
  34. pixeltable/exprs/compound_predicate.py +4 -4
  35. pixeltable/exprs/expr.py +31 -22
  36. pixeltable/exprs/expr_dict.py +3 -3
  37. pixeltable/exprs/expr_set.py +1 -1
  38. pixeltable/exprs/function_call.py +110 -80
  39. pixeltable/exprs/globals.py +3 -3
  40. pixeltable/exprs/in_predicate.py +1 -1
  41. pixeltable/exprs/inline_expr.py +3 -3
  42. pixeltable/exprs/is_null.py +1 -1
  43. pixeltable/exprs/json_mapper.py +2 -2
  44. pixeltable/exprs/json_path.py +17 -10
  45. pixeltable/exprs/literal.py +1 -1
  46. pixeltable/exprs/method_ref.py +2 -2
  47. pixeltable/exprs/row_builder.py +8 -17
  48. pixeltable/exprs/rowid_ref.py +21 -10
  49. pixeltable/exprs/similarity_expr.py +5 -5
  50. pixeltable/exprs/sql_element_cache.py +1 -1
  51. pixeltable/exprs/type_cast.py +2 -3
  52. pixeltable/exprs/variable.py +2 -2
  53. pixeltable/ext/__init__.py +2 -0
  54. pixeltable/ext/functions/__init__.py +2 -0
  55. pixeltable/ext/functions/yolox.py +3 -3
  56. pixeltable/func/__init__.py +3 -1
  57. pixeltable/func/aggregate_function.py +9 -9
  58. pixeltable/func/callable_function.py +3 -4
  59. pixeltable/func/expr_template_function.py +6 -16
  60. pixeltable/func/function.py +48 -14
  61. pixeltable/func/function_registry.py +1 -3
  62. pixeltable/func/query_template_function.py +5 -12
  63. pixeltable/func/signature.py +23 -22
  64. pixeltable/func/tools.py +3 -3
  65. pixeltable/func/udf.py +6 -4
  66. pixeltable/functions/__init__.py +2 -0
  67. pixeltable/functions/fireworks.py +7 -4
  68. pixeltable/functions/globals.py +4 -5
  69. pixeltable/functions/huggingface.py +1 -5
  70. pixeltable/functions/image.py +17 -7
  71. pixeltable/functions/llama_cpp.py +1 -1
  72. pixeltable/functions/mistralai.py +1 -1
  73. pixeltable/functions/ollama.py +4 -4
  74. pixeltable/functions/openai.py +19 -19
  75. pixeltable/functions/string.py +23 -30
  76. pixeltable/functions/timestamp.py +11 -6
  77. pixeltable/functions/together.py +14 -12
  78. pixeltable/functions/util.py +1 -1
  79. pixeltable/functions/video.py +5 -4
  80. pixeltable/functions/vision.py +6 -9
  81. pixeltable/functions/whisper.py +3 -3
  82. pixeltable/globals.py +246 -260
  83. pixeltable/index/__init__.py +2 -0
  84. pixeltable/index/base.py +1 -1
  85. pixeltable/index/btree.py +3 -1
  86. pixeltable/index/embedding_index.py +11 -5
  87. pixeltable/io/external_store.py +11 -12
  88. pixeltable/io/label_studio.py +4 -3
  89. pixeltable/io/parquet.py +57 -56
  90. pixeltable/iterators/__init__.py +4 -2
  91. pixeltable/iterators/audio.py +11 -11
  92. pixeltable/iterators/document.py +10 -10
  93. pixeltable/iterators/string.py +1 -2
  94. pixeltable/iterators/video.py +14 -15
  95. pixeltable/metadata/__init__.py +9 -5
  96. pixeltable/metadata/converters/convert_10.py +0 -1
  97. pixeltable/metadata/converters/convert_15.py +0 -2
  98. pixeltable/metadata/converters/convert_23.py +0 -2
  99. pixeltable/metadata/converters/convert_24.py +3 -3
  100. pixeltable/metadata/converters/convert_25.py +1 -1
  101. pixeltable/metadata/converters/convert_27.py +0 -2
  102. pixeltable/metadata/converters/convert_28.py +0 -2
  103. pixeltable/metadata/converters/convert_29.py +7 -8
  104. pixeltable/metadata/converters/util.py +7 -7
  105. pixeltable/metadata/schema.py +27 -19
  106. pixeltable/plan.py +68 -40
  107. pixeltable/share/__init__.py +2 -0
  108. pixeltable/share/packager.py +15 -12
  109. pixeltable/share/publish.py +3 -5
  110. pixeltable/store.py +37 -38
  111. pixeltable/type_system.py +41 -28
  112. pixeltable/utils/coco.py +4 -4
  113. pixeltable/utils/console_output.py +1 -3
  114. pixeltable/utils/description_helper.py +1 -1
  115. pixeltable/utils/documents.py +3 -3
  116. pixeltable/utils/filecache.py +20 -9
  117. pixeltable/utils/formatter.py +2 -3
  118. pixeltable/utils/media_store.py +1 -1
  119. pixeltable/utils/pytorch.py +1 -1
  120. pixeltable/utils/sql.py +4 -4
  121. pixeltable/utils/transactional_directory.py +2 -1
  122. {pixeltable-0.3.6.dist-info → pixeltable-0.3.8.dist-info}/METADATA +1 -1
  123. pixeltable-0.3.8.dist-info/RECORD +174 -0
  124. pixeltable-0.3.6.dist-info/RECORD +0 -172
  125. {pixeltable-0.3.6.dist-info → pixeltable-0.3.8.dist-info}/LICENSE +0 -0
  126. {pixeltable-0.3.6.dist-info → pixeltable-0.3.8.dist-info}/WHEEL +0 -0
  127. {pixeltable-0.3.6.dist-info → pixeltable-0.3.8.dist-info}/entry_points.txt +0 -0
@@ -2,7 +2,6 @@ from __future__ import annotations
2
2
 
3
3
  import dataclasses
4
4
  import importlib
5
- import inspect
6
5
  import logging
7
6
  import time
8
7
  import uuid
@@ -11,12 +10,10 @@ from uuid import UUID
11
10
 
12
11
  import jsonschema.exceptions
13
12
  import sqlalchemy as sql
14
- import sqlalchemy.orm as orm
15
13
 
16
14
  import pixeltable as pxt
17
15
  import pixeltable.exceptions as excs
18
16
  import pixeltable.exprs as exprs
19
- import pixeltable.func as func
20
17
  import pixeltable.index as index
21
18
  import pixeltable.type_system as ts
22
19
  from pixeltable.env import Env
@@ -32,6 +29,8 @@ from .globals import _POS_COLUMN_NAME, _ROWID_COLUMN_NAME, MediaValidation, Upda
32
29
  if TYPE_CHECKING:
33
30
  from pixeltable import exec, store
34
31
 
32
+ from .table_version_handle import TableVersionHandle
33
+
35
34
  _logger = logging.getLogger('pixeltable')
36
35
 
37
36
 
@@ -48,26 +47,27 @@ class TableVersion:
48
47
  * TODO: create a separate hierarchy of objects that records the version-independent tree of tables/views, and
49
48
  have TableVersions reference those
50
49
  - mutable TableVersions record their TableVersionPath, which is needed for expr evaluation in updates
50
+
51
+ Instances of TableVersion should not be stored as member variables (ie, used across transaction boundaries).
52
+ Use a TableVersionHandle instead.
51
53
  """
52
54
 
53
55
  id: UUID
54
56
  name: str
57
+ effective_version: Optional[int]
55
58
  version: int
56
59
  comment: str
57
60
  media_validation: MediaValidation
58
61
  num_retained_versions: int
59
62
  schema_version: int
60
63
  view_md: Optional[schema.ViewMd]
61
- is_snapshot: bool
62
- include_base_columns: bool
63
- effective_version: Optional[int]
64
- path: Optional[pxt.catalog.TableVersionPath]
65
- base: Optional[TableVersion]
64
+ path: Optional[pxt.catalog.TableVersionPath] # only set for live tables; needed to resolve computed cols
65
+ base: Optional[TableVersionHandle] # only set for views
66
66
  next_col_id: int
67
67
  next_idx_id: int
68
68
  next_rowid: int
69
69
  predicate: Optional[exprs.Expr]
70
- mutable_views: list[TableVersion]
70
+ mutable_views: list[TableVersionHandle] # target for data operation propagation (only set for live tables)
71
71
  iterator_cls: Optional[type[ComponentIterator]]
72
72
  iterator_args: Optional[exprs.InlineDict]
73
73
  num_iterator_cols: int
@@ -99,37 +99,37 @@ class TableVersion:
99
99
  self,
100
100
  id: UUID,
101
101
  tbl_md: schema.TableMd,
102
- version: int,
102
+ effective_version: Optional[int],
103
103
  schema_version_md: schema.TableSchemaVersionMd,
104
- base: Optional[TableVersion] = None,
104
+ mutable_views: list[TableVersionHandle],
105
105
  base_path: Optional[pxt.catalog.TableVersionPath] = None,
106
- is_snapshot: Optional[bool] = None,
106
+ base: Optional[TableVersionHandle] = None,
107
+ # base_store_tbl: Optional['store.StoreBase'] = None,
107
108
  ):
108
- # only one of base and base_path can be non-None
109
- assert base is None or base_path is None
110
109
  self.id = id
111
110
  self.name = tbl_md.name
112
- self.version = version
111
+ self.effective_version = effective_version
112
+ self.version = tbl_md.current_version if effective_version is None else effective_version
113
113
  self.comment = schema_version_md.comment
114
114
  self.num_retained_versions = schema_version_md.num_retained_versions
115
115
  self.schema_version = schema_version_md.schema_version
116
116
  self.view_md = tbl_md.view_md # save this as-is, it's needed for _create_md()
117
- is_view = tbl_md.view_md is not None
118
- self.is_snapshot = (is_view and tbl_md.view_md.is_snapshot) or bool(is_snapshot)
119
- self.include_base_columns = not is_view or tbl_md.view_md.include_base_columns
120
117
  self.media_validation = MediaValidation[schema_version_md.media_validation.upper()]
121
- # a mutable TableVersion doesn't have a static version
122
- self.effective_version = self.version if self.is_snapshot else None
118
+ assert not (self.is_view and base is None)
119
+ self.base = base
123
120
 
124
121
  # mutable tables need their TableVersionPath for expr eval during updates
122
+ from .table_version_handle import TableVersionHandle
125
123
  from .table_version_path import TableVersionPath
126
124
 
127
125
  if self.is_snapshot:
128
126
  self.path = None
129
127
  else:
130
- self.path = TableVersionPath(self, base=base_path) if base_path is not None else TableVersionPath(self)
128
+ self_handle = TableVersionHandle(id, self.effective_version)
129
+ if self.is_view:
130
+ assert base_path is not None
131
+ self.path = TableVersionPath(self_handle, base=base_path)
131
132
 
132
- self.base = base_path.tbl_version if base_path is not None else base
133
133
  if self.is_snapshot:
134
134
  self.next_col_id = -1
135
135
  self.next_idx_id = -1 # TODO: can snapshots have separate indices?
@@ -143,17 +143,15 @@ class TableVersion:
143
143
  # view-specific initialization
144
144
  from pixeltable import exprs
145
145
 
146
- predicate_dict = None if not is_view or tbl_md.view_md.predicate is None else tbl_md.view_md.predicate
146
+ predicate_dict = None if self.view_md is None or self.view_md.predicate is None else self.view_md.predicate
147
147
  self.predicate = exprs.Expr.from_dict(predicate_dict) if predicate_dict is not None else None
148
- self.mutable_views = [] # targets for update propagation
149
- if self.base is not None and not self.base.is_snapshot and not self.is_snapshot:
150
- self.base.mutable_views.append(self)
148
+ self.mutable_views = mutable_views
151
149
 
152
150
  # component view-specific initialization
153
151
  self.iterator_cls = None
154
152
  self.iterator_args = None
155
153
  self.num_iterator_cols = 0
156
- if is_view and tbl_md.view_md.iterator_class_fqn is not None:
154
+ if self.view_md is not None and self.view_md.iterator_class_fqn is not None:
157
155
  module_name, class_name = tbl_md.view_md.iterator_class_fqn.rsplit('.', 1)
158
156
  module = importlib.import_module(module_name)
159
157
  self.iterator_cls = getattr(module, class_name)
@@ -164,7 +162,7 @@ class TableVersion:
164
162
 
165
163
  # register this table version now so that it's available when we're re-creating value exprs
166
164
  cat = pxt.catalog.Catalog.get()
167
- cat.tbl_versions[(self.id, self.effective_version)] = self
165
+ cat.add_tbl_version(self)
168
166
 
169
167
  # init schema after we determined whether we're a component view, and before we create the store table
170
168
  self.cols = []
@@ -179,42 +177,45 @@ class TableVersion:
179
177
  # Init external stores (this needs to happen after the schema is created)
180
178
  self._init_external_stores(tbl_md)
181
179
 
180
+ # Force column metadata to load, in order to surface any invalid metadata now (as warnings)
181
+ for col in self.cols_by_id.values():
182
+ _ = col.value_expr
183
+
182
184
  def __hash__(self) -> int:
183
185
  return hash(self.id)
184
186
 
185
- def _get_column(self, tbl_id: UUID, col_id: int) -> Column:
186
- if self.id == tbl_id:
187
- return self.cols_by_id[col_id]
188
- else:
189
- if self.base is None:
190
- raise excs.Error(f'Unknown table id: {tbl_id}')
191
- return self.base._get_column(tbl_id, col_id)
192
-
193
187
  def create_snapshot_copy(self) -> TableVersion:
194
188
  """Create a snapshot copy of this TableVersion"""
195
189
  assert not self.is_snapshot
190
+ base = self.path.base.tbl_version if self.is_view else None
196
191
  return TableVersion(
197
192
  self.id,
198
193
  self._create_tbl_md(),
199
194
  self.version,
200
195
  self._create_schema_version_md(preceding_schema_version=0), # preceding_schema_version: dummy value
201
- is_snapshot=True,
202
- base=self.base,
196
+ mutable_views=[],
197
+ base=base,
203
198
  )
204
199
 
200
+ def create_handle(self) -> TableVersionHandle:
201
+ from .table_version_handle import TableVersionHandle
202
+
203
+ return TableVersionHandle(self.id, self.effective_version, tbl_version=self)
204
+
205
205
  @classmethod
206
206
  def create(
207
207
  cls,
208
- session: orm.Session,
209
208
  dir_id: UUID,
210
209
  name: str,
211
210
  cols: list[Column],
212
211
  num_retained_versions: int,
213
212
  comment: str,
214
213
  media_validation: MediaValidation,
215
- base_path: Optional[pxt.catalog.TableVersionPath] = None,
214
+ # base_path: Optional[pxt.catalog.TableVersionPath] = None,
216
215
  view_md: Optional[schema.ViewMd] = None,
217
216
  ) -> tuple[UUID, Optional[TableVersion]]:
217
+ session = Env.get().session
218
+
218
219
  # assign ids
219
220
  cols_by_name: dict[str, Column] = {}
220
221
  for pos, col in enumerate(cols):
@@ -276,7 +277,7 @@ class TableVersion:
276
277
  tbl_id=tbl_record.id, schema_version=0, md=dataclasses.asdict(schema_version_md)
277
278
  )
278
279
 
279
- # if this is purely a snapshot (it doesn't require any additional storage for columns and it # doesn't have a
280
+ # if this is purely a snapshot (it doesn't require any additional storage for columns and it doesn't have a
280
281
  # predicate to apply at runtime), we don't create a physical table and simply use the base's table version path
281
282
  if view_md is not None and view_md.is_snapshot and view_md.predicate is None and len(cols) == 0:
282
283
  session.add(tbl_record)
@@ -284,17 +285,20 @@ class TableVersion:
284
285
  session.add(schema_version_record)
285
286
  return tbl_record.id, None
286
287
 
287
- assert (base_path is not None) == (view_md is not None)
288
- base = base_path.tbl_version if base_path is not None and view_md.is_snapshot else None
289
- base_path = base_path if base_path is not None and not view_md.is_snapshot else None
290
- tbl_version = cls(tbl_record.id, table_md, 0, schema_version_md, base=base, base_path=base_path)
288
+ # assert (base_path is not None) == (view_md is not None)
289
+ is_snapshot = view_md is not None and view_md.is_snapshot
290
+ effective_version = 0 if is_snapshot else None
291
+ base_path = pxt.catalog.TableVersionPath.from_md(view_md.base_versions) if view_md is not None else None
292
+ base = base_path.tbl_version if base_path is not None else None
293
+ tbl_version = cls(
294
+ tbl_record.id, table_md, effective_version, schema_version_md, [], base_path=base_path, base=base
295
+ )
291
296
 
292
- conn = session.connection()
293
- tbl_version.store_tbl.create(conn)
297
+ tbl_version.store_tbl.create()
294
298
  if view_md is None or not view_md.is_snapshot:
295
299
  # add default indices, after creating the store table
296
300
  for col in tbl_version.cols_by_name.values():
297
- status = tbl_version._add_default_index(col, conn=conn)
301
+ status = tbl_version._add_default_index(col)
298
302
  assert status is None or status.num_excs == 0
299
303
 
300
304
  # we re-create the tbl_record here, now that we have new index metadata
@@ -305,30 +309,30 @@ class TableVersion:
305
309
  return tbl_record.id, tbl_version
306
310
 
307
311
  @classmethod
308
- def delete_md(cls, tbl_id: UUID, conn: sql.Connection) -> None:
312
+ def delete_md(cls, tbl_id: UUID) -> None:
313
+ conn = Env.get().conn
309
314
  conn.execute(sql.delete(schema.TableSchemaVersion.__table__).where(schema.TableSchemaVersion.tbl_id == tbl_id))
310
315
  conn.execute(sql.delete(schema.TableVersion.__table__).where(schema.TableVersion.tbl_id == tbl_id))
311
316
  conn.execute(sql.delete(schema.Table.__table__).where(schema.Table.id == tbl_id))
312
317
 
313
318
  def drop(self) -> None:
314
- with Env.get().engine.begin() as conn:
315
- # delete this table and all associated data
316
- MediaStore.delete(self.id)
317
- FileCache.get().clear(tbl_id=self.id)
318
- self.delete_md(self.id, conn)
319
- self.store_tbl.drop(conn)
319
+ # delete this table and all associated data
320
+ MediaStore.delete(self.id)
321
+ FileCache.get().clear(tbl_id=self.id)
322
+ self.delete_md(self.id)
323
+ self.store_tbl.drop()
320
324
 
321
325
  # de-register table version from catalog
322
326
  from .catalog import Catalog
323
327
 
324
328
  cat = Catalog.get()
325
- del cat.tbl_versions[(self.id, self.effective_version)]
326
- # TODO: remove from tbl_dependents
329
+ cat.remove_tbl_version(self)
327
330
 
328
331
  def _init_schema(self, tbl_md: schema.TableMd, schema_version_md: schema.TableSchemaVersionMd) -> None:
329
332
  # create columns first, so the indices can reference them
330
333
  self._init_cols(tbl_md, schema_version_md)
331
- self._init_idxs(tbl_md)
334
+ if not self.is_snapshot:
335
+ self._init_idxs(tbl_md)
332
336
  # create the sa schema only after creating the columns and indices
333
337
  self._init_sa_schema()
334
338
 
@@ -356,7 +360,7 @@ class TableVersion:
356
360
  schema_version_drop=col_md.schema_version_drop,
357
361
  value_expr_dict=col_md.value_expr,
358
362
  )
359
- col.tbl = self
363
+ col.tbl = self.create_handle()
360
364
  self.cols.append(col)
361
365
 
362
366
  # populate the lookup structures before Expr.from_dict()
@@ -373,7 +377,6 @@ class TableVersion:
373
377
  # make sure to traverse columns ordered by position = order in which cols were created;
374
378
  # this guarantees that references always point backwards
375
379
  if col_md.value_expr is not None:
376
- refd_cols = exprs.Expr.get_refd_columns(col_md.value_expr)
377
380
  self._record_refd_columns(col)
378
381
 
379
382
  def _init_idxs(self, tbl_md: schema.TableMd) -> None:
@@ -393,7 +396,7 @@ class TableVersion:
393
396
  # instantiate index object
394
397
  cls_name = md.class_fqn.rsplit('.', 1)[-1]
395
398
  cls = getattr(index_module, cls_name)
396
- idx_col = self._get_column(UUID(md.indexed_col_tbl_id), md.indexed_col_id)
399
+ idx_col = self.path.get_column_by_id(UUID(md.indexed_col_tbl_id), md.indexed_col_id)
397
400
  idx = cls.from_dict(idx_col, md.init_args)
398
401
 
399
402
  # fix up the sa column type of the index value and undo columns
@@ -411,19 +414,15 @@ class TableVersion:
411
414
  # need to record errors
412
415
  from pixeltable.store import StoreComponentView, StoreTable, StoreView
413
416
 
414
- if self.is_component_view():
417
+ if self.is_component_view:
415
418
  self.store_tbl = StoreComponentView(self)
416
- elif self.is_view():
419
+ elif self.is_view:
417
420
  self.store_tbl = StoreView(self)
418
421
  else:
419
422
  self.store_tbl = StoreTable(self)
420
423
 
421
424
  def _update_md(
422
- self,
423
- timestamp: float,
424
- conn: sql.engine.Connection,
425
- update_tbl_version: bool = True,
426
- preceding_schema_version: Optional[int] = None,
425
+ self, timestamp: float, update_tbl_version: bool = True, preceding_schema_version: Optional[int] = None
427
426
  ) -> None:
428
427
  """Writes table metadata to the database.
429
428
 
@@ -436,6 +435,7 @@ class TableVersion:
436
435
  """
437
436
  assert update_tbl_version or preceding_schema_version is None
438
437
 
438
+ conn = Env.get().conn
439
439
  conn.execute(
440
440
  sql.update(schema.Table.__table__)
441
441
  .values({schema.Table.md: dataclasses.asdict(self._create_tbl_md())})
@@ -467,13 +467,12 @@ class TableVersion:
467
467
  self.version += 1
468
468
  preceding_schema_version = self.schema_version
469
469
  self.schema_version = self.version
470
- with Env.get().engine.begin() as conn:
471
- status = self._add_index(col, idx_name, idx, conn)
472
- self._update_md(time.time(), conn, preceding_schema_version=preceding_schema_version)
473
- _logger.info(f'Added index {idx_name} on column {col.name} to table {self.name}')
474
- return status
470
+ status = self._add_index(col, idx_name, idx)
471
+ self._update_md(time.time(), preceding_schema_version=preceding_schema_version)
472
+ _logger.info(f'Added index {idx_name} on column {col.name} to table {self.name}')
473
+ return status
475
474
 
476
- def _add_default_index(self, col: Column, conn: sql.engine.Connection) -> Optional[UpdateStatus]:
475
+ def _add_default_index(self, col: Column) -> Optional[UpdateStatus]:
477
476
  """Add a B-tree index on this column if it has a compatible type"""
478
477
  if not col.stored:
479
478
  # if the column is intentionally not stored, we want to avoid the overhead of an index
@@ -487,12 +486,10 @@ class TableVersion:
487
486
  if col.col_type.is_bool_type():
488
487
  # B-trees on bools aren't useful
489
488
  return None
490
- status = self._add_index(col, idx_name=None, idx=index.BtreeIndex(col), conn=conn)
489
+ status = self._add_index(col, idx_name=None, idx=index.BtreeIndex(col))
491
490
  return status
492
491
 
493
- def _add_index(
494
- self, col: Column, idx_name: Optional[str], idx: index.IndexBase, conn: sql.engine.Connection
495
- ) -> UpdateStatus:
492
+ def _add_index(self, col: Column, idx_name: Optional[str], idx: index.IndexBase) -> UpdateStatus:
496
493
  assert not self.is_snapshot
497
494
  idx_id = self.next_idx_id
498
495
  self.next_idx_id += 1
@@ -513,7 +510,7 @@ class TableVersion:
513
510
  schema_version_drop=None,
514
511
  records_errors=idx.records_value_errors(),
515
512
  )
516
- val_col.tbl = self
513
+ val_col.tbl = self.create_handle()
517
514
  val_col.col_type = val_col.col_type.copy(nullable=True)
518
515
  self.next_col_id += 1
519
516
 
@@ -527,7 +524,7 @@ class TableVersion:
527
524
  schema_version_drop=None,
528
525
  records_errors=False,
529
526
  )
530
- undo_col.tbl = self
527
+ undo_col.tbl = self.create_handle()
531
528
  undo_col.col_type = undo_col.col_type.copy(nullable=True)
532
529
  self.next_col_id += 1
533
530
 
@@ -552,9 +549,9 @@ class TableVersion:
552
549
  # add the columns and update the metadata
553
550
  # TODO support on_error='abort' for indices; it's tricky because of the way metadata changes are entangled
554
551
  # with the database operations
555
- status = self._add_columns([val_col, undo_col], conn, print_stats=False, on_error='ignore')
552
+ status = self._add_columns([val_col, undo_col], print_stats=False, on_error='ignore')
556
553
  # now create the index structure
557
- idx.create_index(self._store_idx_name(idx_id), val_col, conn)
554
+ idx.create_index(self._store_idx_name(idx_id), val_col)
558
555
 
559
556
  return status
560
557
 
@@ -575,10 +572,9 @@ class TableVersion:
575
572
  del self.idxs_by_name[idx_md.name]
576
573
  del self.idx_md[idx_id]
577
574
 
578
- with Env.get().engine.begin() as conn:
579
- self._drop_columns([idx_info.val_col, idx_info.undo_col])
580
- self._update_md(time.time(), conn, preceding_schema_version=preceding_schema_version)
581
- _logger.info(f'Dropped index {idx_md.name} on table {self.name}')
575
+ self._drop_columns([idx_info.val_col, idx_info.undo_col])
576
+ self._update_md(time.time(), preceding_schema_version=preceding_schema_version)
577
+ _logger.info(f'Dropped index {idx_md.name} on table {self.name}')
582
578
 
583
579
  def add_columns(
584
580
  self, cols: Iterable[Column], print_stats: bool, on_error: Literal['abort', 'ignore']
@@ -589,7 +585,7 @@ class TableVersion:
589
585
  assert all(col.stored is not None for col in cols)
590
586
  assert all(col.name not in self.cols_by_name for col in cols)
591
587
  for col in cols:
592
- col.tbl = self
588
+ col.tbl = self.create_handle()
593
589
  col.id = self.next_col_id
594
590
  self.next_col_id += 1
595
591
 
@@ -597,11 +593,10 @@ class TableVersion:
597
593
  self.version += 1
598
594
  preceding_schema_version = self.schema_version
599
595
  self.schema_version = self.version
600
- with Env.get().engine.begin() as conn:
601
- status = self._add_columns(cols, conn, print_stats=print_stats, on_error=on_error)
602
- for col in cols:
603
- _ = self._add_default_index(col, conn)
604
- self._update_md(time.time(), conn, preceding_schema_version=preceding_schema_version)
596
+ status = self._add_columns(cols, print_stats=print_stats, on_error=on_error)
597
+ for col in cols:
598
+ _ = self._add_default_index(col)
599
+ self._update_md(time.time(), preceding_schema_version=preceding_schema_version)
605
600
  _logger.info(f'Added columns {[col.name for col in cols]} to table {self.name}, new version: {self.version}')
606
601
 
607
602
  msg = (
@@ -613,15 +608,11 @@ class TableVersion:
613
608
  return status
614
609
 
615
610
  def _add_columns(
616
- self,
617
- cols: Iterable[Column],
618
- conn: sql.engine.Connection,
619
- print_stats: bool,
620
- on_error: Literal['abort', 'ignore'],
611
+ self, cols: Iterable[Column], print_stats: bool, on_error: Literal['abort', 'ignore']
621
612
  ) -> UpdateStatus:
622
613
  """Add and populate columns within the current transaction"""
623
614
  cols = list(cols)
624
- row_count = self.store_tbl.count(conn=conn)
615
+ row_count = self.store_tbl.count()
625
616
  for col in cols:
626
617
  if not col.col_type.nullable and not col.is_computed:
627
618
  if row_count > 0:
@@ -644,7 +635,7 @@ class TableVersion:
644
635
  self._record_refd_columns(col)
645
636
 
646
637
  if col.is_stored:
647
- self.store_tbl.add_column(col, conn)
638
+ self.store_tbl.add_column(col)
648
639
 
649
640
  if not col.is_computed or not col.is_stored or row_count == 0:
650
641
  continue
@@ -656,10 +647,9 @@ class TableVersion:
656
647
  plan.ctx.num_rows = row_count
657
648
 
658
649
  try:
659
- plan.ctx.set_conn(conn)
660
650
  plan.open()
661
651
  try:
662
- num_excs = self.store_tbl.load_column(col, plan, value_expr_slot_idx, conn, on_error)
652
+ num_excs = self.store_tbl.load_column(col, plan, value_expr_slot_idx, on_error)
663
653
  except sql.exc.DBAPIError as exc:
664
654
  # Wrap the DBAPIError in an excs.Error to unify processing in the subsequent except block
665
655
  raise excs.Error(f'SQL error during execution of computed column `{col.name}`:\n{exc}') from exc
@@ -687,12 +677,11 @@ class TableVersion:
687
677
  num_rows=row_count,
688
678
  num_computed_values=row_count,
689
679
  num_excs=num_excs,
690
- cols_with_excs=[f'{col.tbl.name}.{col.name}' for col in cols_with_excs if col.name is not None],
680
+ cols_with_excs=[f'{col.tbl.get().name}.{col.name}' for col in cols_with_excs if col.name is not None],
691
681
  )
692
682
 
693
683
  def drop_column(self, col: Column) -> None:
694
684
  """Drop a column from the table."""
695
- from pixeltable.catalog import Catalog
696
685
 
697
686
  assert not self.is_snapshot
698
687
 
@@ -701,23 +690,22 @@ class TableVersion:
701
690
  preceding_schema_version = self.schema_version
702
691
  self.schema_version = self.version
703
692
 
704
- with Env.get().engine.begin() as conn:
705
- # drop this column and all dependent index columns and indices
706
- dropped_cols = [col]
707
- dropped_idx_names: list[str] = []
708
- for idx_info in self.idxs_by_name.values():
709
- if idx_info.col != col:
710
- continue
711
- dropped_cols.extend([idx_info.val_col, idx_info.undo_col])
712
- idx_md = self.idx_md[idx_info.id]
713
- idx_md.schema_version_drop = self.schema_version
714
- assert idx_md.name in self.idxs_by_name
715
- dropped_idx_names.append(idx_md.name)
716
- # update idxs_by_name
717
- for idx_name in dropped_idx_names:
718
- del self.idxs_by_name[idx_name]
719
- self._drop_columns(dropped_cols)
720
- self._update_md(time.time(), conn, preceding_schema_version=preceding_schema_version)
693
+ # drop this column and all dependent index columns and indices
694
+ dropped_cols = [col]
695
+ dropped_idx_names: list[str] = []
696
+ for idx_info in self.idxs_by_name.values():
697
+ if idx_info.col != col:
698
+ continue
699
+ dropped_cols.extend([idx_info.val_col, idx_info.undo_col])
700
+ idx_md = self.idx_md[idx_info.id]
701
+ idx_md.schema_version_drop = self.schema_version
702
+ assert idx_md.name in self.idxs_by_name
703
+ dropped_idx_names.append(idx_md.name)
704
+ # update idxs_by_name
705
+ for idx_name in dropped_idx_names:
706
+ del self.idxs_by_name[idx_name]
707
+ self._drop_columns(dropped_cols)
708
+ self._update_md(time.time(), preceding_schema_version=preceding_schema_version)
721
709
  _logger.info(f'Dropped column {col.name} from table {self.name}, new version: {self.version}')
722
710
 
723
711
  def _drop_columns(self, cols: Iterable[Column]) -> None:
@@ -760,8 +748,7 @@ class TableVersion:
760
748
  preceding_schema_version = self.schema_version
761
749
  self.schema_version = self.version
762
750
 
763
- with Env.get().engine.begin() as conn:
764
- self._update_md(time.time(), conn, preceding_schema_version=preceding_schema_version)
751
+ self._update_md(time.time(), preceding_schema_version=preceding_schema_version)
765
752
  _logger.info(f'Renamed column {old_name} to {new_name} in table {self.name}, new version: {self.version}')
766
753
 
767
754
  def set_comment(self, new_comment: Optional[str]):
@@ -781,15 +768,13 @@ class TableVersion:
781
768
  self.version += 1
782
769
  preceding_schema_version = self.schema_version
783
770
  self.schema_version = self.version
784
- with Env.get().engine.begin() as conn:
785
- self._update_md(time.time(), conn, preceding_schema_version=preceding_schema_version)
771
+ self._update_md(time.time(), preceding_schema_version=preceding_schema_version)
786
772
  _logger.info(f'[{self.name}] Updating table schema to version: {self.version}')
787
773
 
788
774
  def insert(
789
775
  self,
790
776
  rows: Optional[list[dict[str, Any]]],
791
777
  df: Optional[pxt.DataFrame],
792
- conn: Optional[sql.engine.Connection] = None,
793
778
  print_stats: bool = False,
794
779
  fail_on_exception: bool = True,
795
780
  ) -> UpdateStatus:
@@ -812,20 +797,11 @@ class TableVersion:
812
797
  self.next_rowid += 1
813
798
  yield rowid
814
799
 
815
- if conn is None:
816
- with Env.get().engine.begin() as conn:
817
- return self._insert(
818
- plan, conn, time.time(), print_stats=print_stats, rowids=rowids(), abort_on_exc=fail_on_exception
819
- )
820
- else:
821
- return self._insert(
822
- plan, conn, time.time(), print_stats=print_stats, rowids=rowids(), abort_on_exc=fail_on_exception
823
- )
800
+ return self._insert(plan, time.time(), print_stats=print_stats, rowids=rowids(), abort_on_exc=fail_on_exception)
824
801
 
825
802
  def _insert(
826
803
  self,
827
804
  exec_plan: 'exec.ExecNode',
828
- conn: sql.engine.Connection,
829
805
  timestamp: float,
830
806
  *,
831
807
  rowids: Optional[Iterator[int]] = None,
@@ -837,20 +813,20 @@ class TableVersion:
837
813
  self.version += 1
838
814
  result = UpdateStatus()
839
815
  num_rows, num_excs, cols_with_excs = self.store_tbl.insert_rows(
840
- exec_plan, conn, v_min=self.version, rowids=rowids, abort_on_exc=abort_on_exc
816
+ exec_plan, v_min=self.version, rowids=rowids, abort_on_exc=abort_on_exc
841
817
  )
842
818
  result.num_rows = num_rows
843
819
  result.num_excs = num_excs
844
820
  result.num_computed_values += exec_plan.ctx.num_computed_exprs * num_rows
845
821
  result.cols_with_excs = [f'{self.name}.{self.cols_by_id[cid].name}' for cid in cols_with_excs]
846
- self._update_md(timestamp, conn)
822
+ self._update_md(timestamp)
847
823
 
848
824
  # update views
849
825
  for view in self.mutable_views:
850
826
  from pixeltable.plan import Planner
851
827
 
852
- plan, _ = Planner.create_view_load_plan(view.path, propagates_insert=True)
853
- status = view._insert(plan, conn, timestamp, print_stats=print_stats)
828
+ plan, _ = Planner.create_view_load_plan(view.get().path, propagates_insert=True)
829
+ status = view.get()._insert(plan, timestamp, print_stats=print_stats)
854
830
  result.num_rows += status.num_rows
855
831
  result.num_excs += status.num_excs
856
832
  result.num_computed_values += status.num_computed_values
@@ -886,22 +862,20 @@ class TableVersion:
886
862
  if analysis_info.filter is not None:
887
863
  raise excs.Error(f'Filter {analysis_info.filter} not expressible in SQL')
888
864
 
889
- with Env.get().engine.begin() as conn:
890
- plan, updated_cols, recomputed_cols = Planner.create_update_plan(self.path, update_spec, [], where, cascade)
891
- from pixeltable.exprs import SqlElementCache
892
-
893
- result = self.propagate_update(
894
- plan,
895
- where.sql_expr(SqlElementCache()) if where is not None else None,
896
- recomputed_cols,
897
- base_versions=[],
898
- conn=conn,
899
- timestamp=time.time(),
900
- cascade=cascade,
901
- show_progress=True,
902
- )
903
- result.updated_cols = updated_cols
904
- return result
865
+ plan, updated_cols, recomputed_cols = Planner.create_update_plan(self.path, update_spec, [], where, cascade)
866
+ from pixeltable.exprs import SqlElementCache
867
+
868
+ result = self.propagate_update(
869
+ plan,
870
+ where.sql_expr(SqlElementCache()) if where is not None else None,
871
+ recomputed_cols,
872
+ base_versions=[],
873
+ timestamp=time.time(),
874
+ cascade=cascade,
875
+ show_progress=True,
876
+ )
877
+ result.updated_cols = updated_cols
878
+ return result
905
879
 
906
880
  def batch_update(
907
881
  self,
@@ -920,33 +894,24 @@ class TableVersion:
920
894
  assert len(rowids) == 0 or len(rowids) == len(batch)
921
895
  cols_with_excs: set[str] = set()
922
896
 
923
- with Env.get().engine.begin() as conn:
924
- from pixeltable.plan import Planner
897
+ from pixeltable.plan import Planner
925
898
 
926
- plan, row_update_node, delete_where_clause, updated_cols, recomputed_cols = (
927
- Planner.create_batch_update_plan(self.path, batch, rowids, cascade=cascade)
928
- )
929
- result = self.propagate_update(
930
- plan,
931
- delete_where_clause,
932
- recomputed_cols,
933
- base_versions=[],
934
- conn=conn,
935
- timestamp=time.time(),
936
- cascade=cascade,
937
- )
938
- result.updated_cols = [c.qualified_name for c in updated_cols]
939
-
940
- unmatched_rows = row_update_node.unmatched_rows()
941
- if len(unmatched_rows) > 0:
942
- if error_if_not_exists:
943
- raise excs.Error(f'batch_update(): {len(unmatched_rows)} row(s) not found')
944
- if insert_if_not_exists:
945
- insert_status = self.insert(
946
- unmatched_rows, None, conn=conn, print_stats=False, fail_on_exception=False
947
- )
948
- result += insert_status
949
- return result
899
+ plan, row_update_node, delete_where_clause, updated_cols, recomputed_cols = Planner.create_batch_update_plan(
900
+ self.path, batch, rowids, cascade=cascade
901
+ )
902
+ result = self.propagate_update(
903
+ plan, delete_where_clause, recomputed_cols, base_versions=[], timestamp=time.time(), cascade=cascade
904
+ )
905
+ result.updated_cols = [c.qualified_name for c in updated_cols]
906
+
907
+ unmatched_rows = row_update_node.unmatched_rows()
908
+ if len(unmatched_rows) > 0:
909
+ if error_if_not_exists:
910
+ raise excs.Error(f'batch_update(): {len(unmatched_rows)} row(s) not found')
911
+ if insert_if_not_exists:
912
+ insert_status = self.insert(unmatched_rows, None, print_stats=False, fail_on_exception=False)
913
+ result += insert_status
914
+ return result
950
915
 
951
916
  def _validate_update_spec(
952
917
  self, value_spec: dict[str, Any], allow_pk: bool, allow_exprs: bool
@@ -1000,7 +965,6 @@ class TableVersion:
1000
965
  where_clause: Optional[sql.ColumnElement],
1001
966
  recomputed_view_cols: list[Column],
1002
967
  base_versions: list[Optional[int]],
1003
- conn: sql.engine.Connection,
1004
968
  timestamp: float,
1005
969
  cascade: bool,
1006
970
  show_progress: bool = True,
@@ -1010,32 +974,26 @@ class TableVersion:
1010
974
  # we're creating a new version
1011
975
  self.version += 1
1012
976
  result.num_rows, result.num_excs, cols_with_excs = self.store_tbl.insert_rows(
1013
- plan, conn, v_min=self.version, show_progress=show_progress
977
+ plan, v_min=self.version, show_progress=show_progress
1014
978
  )
1015
979
  result.cols_with_excs = [f'{self.name}.{self.cols_by_id[cid].name}' for cid in cols_with_excs]
1016
980
  self.store_tbl.delete_rows(
1017
- self.version, base_versions=base_versions, match_on_vmin=True, where_clause=where_clause, conn=conn
981
+ self.version, base_versions=base_versions, match_on_vmin=True, where_clause=where_clause
1018
982
  )
1019
- self._update_md(timestamp, conn)
983
+ self._update_md(timestamp)
1020
984
 
1021
985
  if cascade:
1022
986
  base_versions = [None if plan is None else self.version] + base_versions # don't update in place
1023
987
  # propagate to views
1024
988
  for view in self.mutable_views:
1025
- recomputed_cols = [col for col in recomputed_view_cols if col.tbl is view]
989
+ recomputed_cols = [col for col in recomputed_view_cols if col.tbl == view]
1026
990
  plan = None
1027
991
  if len(recomputed_cols) > 0:
1028
992
  from pixeltable.plan import Planner
1029
993
 
1030
- plan = Planner.create_view_update_plan(view.path, recompute_targets=recomputed_cols)
1031
- status = view.propagate_update(
1032
- plan,
1033
- None,
1034
- recomputed_view_cols,
1035
- base_versions=base_versions,
1036
- conn=conn,
1037
- timestamp=timestamp,
1038
- cascade=True,
994
+ plan = Planner.create_view_update_plan(view.get().path, recompute_targets=recomputed_cols)
995
+ status = view.get().propagate_update(
996
+ plan, None, recomputed_view_cols, base_versions=base_versions, timestamp=timestamp, cascade=True
1039
997
  )
1040
998
  result.num_rows += status.num_rows
1041
999
  result.num_excs += status.num_excs
@@ -1063,18 +1021,13 @@ class TableVersion:
1063
1021
  raise excs.Error(f'Filter {analysis_info.filter} not expressible in SQL')
1064
1022
  sql_where_clause = analysis_info.sql_where_clause
1065
1023
 
1066
- with Env.get().engine.begin() as conn:
1067
- num_rows = self.propagate_delete(sql_where_clause, base_versions=[], conn=conn, timestamp=time.time())
1024
+ num_rows = self.propagate_delete(sql_where_clause, base_versions=[], timestamp=time.time())
1068
1025
 
1069
1026
  status = UpdateStatus(num_rows=num_rows)
1070
1027
  return status
1071
1028
 
1072
1029
  def propagate_delete(
1073
- self,
1074
- where: Optional[exprs.Expr],
1075
- base_versions: list[Optional[int]],
1076
- conn: sql.engine.Connection,
1077
- timestamp: float,
1030
+ self, where: Optional[exprs.Expr], base_versions: list[Optional[int]], timestamp: float
1078
1031
  ) -> int:
1079
1032
  """Delete rows in this table and propagate to views.
1080
1033
  Args:
@@ -1084,17 +1037,17 @@ class TableVersion:
1084
1037
  """
1085
1038
  sql_where_clause = where.sql_expr(exprs.SqlElementCache()) if where is not None else None
1086
1039
  num_rows = self.store_tbl.delete_rows(
1087
- self.version + 1, base_versions=base_versions, match_on_vmin=False, where_clause=sql_where_clause, conn=conn
1040
+ self.version + 1, base_versions=base_versions, match_on_vmin=False, where_clause=sql_where_clause
1088
1041
  )
1089
1042
  if num_rows > 0:
1090
1043
  # we're creating a new version
1091
1044
  self.version += 1
1092
- self._update_md(timestamp, conn)
1045
+ self._update_md(timestamp)
1093
1046
  else:
1094
1047
  pass
1095
1048
  for view in self.mutable_views:
1096
- num_rows += view.propagate_delete(
1097
- where=None, base_versions=[self.version] + base_versions, conn=conn, timestamp=timestamp
1049
+ num_rows += view.get().propagate_delete(
1050
+ where=None, base_versions=[self.version] + base_versions, timestamp=timestamp
1098
1051
  )
1099
1052
  return num_rows
1100
1053
 
@@ -1103,22 +1056,20 @@ class TableVersion:
1103
1056
  assert not self.is_snapshot
1104
1057
  if self.version == 0:
1105
1058
  raise excs.Error('Cannot revert version 0')
1106
- with orm.Session(Env.get().engine, future=True) as session:
1107
- self._revert(session)
1108
- session.commit()
1059
+ self._revert()
1109
1060
 
1110
- def _delete_column(self, col: Column, conn: sql.engine.Connection) -> None:
1061
+ def _delete_column(self, col: Column) -> None:
1111
1062
  """Physically remove the column from the schema and the store table"""
1112
1063
  if col.is_stored:
1113
- self.store_tbl.drop_column(col, conn)
1064
+ self.store_tbl.drop_column(col)
1114
1065
  self.cols.remove(col)
1115
1066
  if col.name is not None:
1116
1067
  del self.cols_by_name[col.name]
1117
1068
  del self.cols_by_id[col.id]
1118
1069
 
1119
- def _revert(self, session: orm.Session) -> None:
1070
+ def _revert(self) -> None:
1120
1071
  """Reverts this table version and propagates to views"""
1121
- conn = session.connection()
1072
+ conn = Env.get().conn
1122
1073
  # make sure we don't have a snapshot referencing this version
1123
1074
  # (unclear how to express this with sqlalchemy)
1124
1075
  query = (
@@ -1137,7 +1088,6 @@ class TableVersion:
1137
1088
  )
1138
1089
  )
1139
1090
 
1140
- conn = session.connection()
1141
1091
  # delete newly-added data
1142
1092
  MediaStore.delete(self.id, version=self.version)
1143
1093
  conn.execute(sql.delete(self.store_tbl.sa_tbl).where(self.store_tbl.sa_tbl.c.v_min == self.version))
@@ -1158,7 +1108,7 @@ class TableVersion:
1158
1108
  if len(added_cols) > 0:
1159
1109
  next_col_id = min(col.id for col in added_cols)
1160
1110
  for col in added_cols:
1161
- self._delete_column(col, conn)
1111
+ self._delete_column(col)
1162
1112
  self.next_col_id = next_col_id
1163
1113
 
1164
1114
  # remove newly-added indices from the lookup structures
@@ -1181,6 +1131,7 @@ class TableVersion:
1181
1131
  for md in dropped_idx_md:
1182
1132
  md.schema_version_drop = None
1183
1133
 
1134
+ session = Env.get().session
1184
1135
  # we need to determine the preceding schema version and reload the schema
1185
1136
  schema_version_md_dict = (
1186
1137
  session.query(schema.TableSchemaVersion.md)
@@ -1224,7 +1175,7 @@ class TableVersion:
1224
1175
 
1225
1176
  # propagate to views
1226
1177
  for view in self.mutable_views:
1227
- view._revert(session)
1178
+ view.get()._revert()
1228
1179
  _logger.info(f'TableVersion {self.name}: reverted to version {self.version}')
1229
1180
 
1230
1181
  def _init_external_stores(self, tbl_md: schema.TableMd) -> None:
@@ -1235,40 +1186,48 @@ class TableVersion:
1235
1186
  self.external_stores[store.name] = store
1236
1187
 
1237
1188
  def link_external_store(self, store: pxt.io.ExternalStore) -> None:
1238
- with Env.get().engine.begin() as conn:
1239
- store.link(self, conn) # May result in additional metadata changes
1240
- self.external_stores[store.name] = store
1241
- self._update_md(time.time(), conn, update_tbl_version=False)
1189
+ store.link(self) # May result in additional metadata changes
1190
+ self.external_stores[store.name] = store
1191
+ self._update_md(time.time(), update_tbl_version=False)
1242
1192
 
1243
1193
  def unlink_external_store(self, store_name: str, delete_external_data: bool) -> None:
1244
1194
  assert store_name in self.external_stores
1245
1195
  store = self.external_stores[store_name]
1246
- with Env.get().engine.begin() as conn:
1247
- store.unlink(self, conn) # May result in additional metadata changes
1248
- del self.external_stores[store_name]
1249
- self._update_md(time.time(), conn, update_tbl_version=False)
1196
+ store.unlink(self) # May result in additional metadata changes
1197
+ del self.external_stores[store_name]
1198
+ self._update_md(time.time(), update_tbl_version=False)
1250
1199
 
1251
1200
  if delete_external_data and isinstance(store, pxt.io.external_store.Project):
1252
1201
  store.delete()
1253
1202
 
1203
+ @property
1204
+ def is_snapshot(self) -> bool:
1205
+ return self.effective_version is not None
1206
+
1207
+ @property
1254
1208
  def is_view(self) -> bool:
1255
- return self.base is not None
1209
+ return self.view_md is not None
1210
+
1211
+ @property
1212
+ def include_base_columns(self) -> bool:
1213
+ return self.view_md is not None and self.view_md.include_base_columns
1256
1214
 
1215
+ @property
1257
1216
  def is_component_view(self) -> bool:
1258
1217
  return self.iterator_cls is not None
1259
1218
 
1260
1219
  def is_insertable(self) -> bool:
1261
1220
  """Returns True if this corresponds to an InsertableTable"""
1262
- return not self.is_snapshot and not self.is_view()
1221
+ return not self.is_snapshot and not self.is_view
1263
1222
 
1264
1223
  def is_iterator_column(self, col: Column) -> bool:
1265
1224
  """Returns True if col is produced by an iterator"""
1266
1225
  # the iterator columns directly follow the pos column
1267
- return self.is_component_view() and col.id > 0 and col.id < self.num_iterator_cols + 1
1226
+ return self.is_component_view and col.id > 0 and col.id < self.num_iterator_cols + 1
1268
1227
 
1269
1228
  def is_system_column(self, col: Column) -> bool:
1270
1229
  """Return True if column was created by Pixeltable"""
1271
- if col.name == _POS_COLUMN_NAME and self.is_component_view():
1230
+ if col.name == _POS_COLUMN_NAME and self.is_component_view:
1272
1231
  return True
1273
1232
  return False
1274
1233
 
@@ -1282,7 +1241,7 @@ class TableVersion:
1282
1241
 
1283
1242
  def get_required_col_names(self) -> list[str]:
1284
1243
  """Return the names of all columns for which values must be specified in insert()"""
1285
- assert not self.is_view()
1244
+ assert not self.is_view
1286
1245
  names = [c.name for c in self.cols_by_name.values() if not c.is_computed and not c.col_type.nullable]
1287
1246
  return names
1288
1247
 
@@ -1318,8 +1277,8 @@ class TableVersion:
1318
1277
 
1319
1278
  def num_rowid_columns(self) -> int:
1320
1279
  """Return the number of columns of the rowids, without accessing store_tbl"""
1321
- if self.is_component_view():
1322
- return 1 + self.base.num_rowid_columns()
1280
+ if self.is_component_view:
1281
+ return 1 + self.base.get().num_rowid_columns()
1323
1282
  return 1
1324
1283
 
1325
1284
  @classmethod
@@ -1393,4 +1352,4 @@ class TableVersion:
1393
1352
 
1394
1353
  id = UUID(d['id'])
1395
1354
  effective_version = d['effective_version']
1396
- return catalog.Catalog.get().tbl_versions[(id, effective_version)]
1355
+ return catalog.Catalog.get().get_tbl_version(id, effective_version)