pixeltable 0.4.3__py3-none-any.whl → 0.4.4__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.

@@ -23,10 +23,11 @@ from pixeltable.utils.exception_handler import run_cleanup_on_exception
23
23
  from pixeltable.utils.filecache import FileCache
24
24
  from pixeltable.utils.media_store import MediaStore
25
25
 
26
+ from .tbl_ops import TableOp
27
+
26
28
  if TYPE_CHECKING:
27
29
  from pixeltable.plan import SampleClause
28
30
 
29
-
30
31
  from ..func.globals import resolve_symbol
31
32
  from .column import Column
32
33
  from .globals import _POS_COLUMN_NAME, _ROWID_COLUMN_NAME, MediaValidation, is_valid_identifier
@@ -40,6 +41,19 @@ if TYPE_CHECKING:
40
41
  _logger = logging.getLogger('pixeltable')
41
42
 
42
43
 
44
+ @dataclasses.dataclass(frozen=True)
45
+ class TableVersionMd:
46
+ """
47
+ Complete set of md records for a specific TableVersion instance.
48
+
49
+ TODO: subsume schema.FullTableMd
50
+ """
51
+
52
+ tbl_md: schema.TableMd
53
+ version_md: schema.TableVersionMd
54
+ schema_version_md: schema.TableSchemaVersionMd
55
+
56
+
43
57
  class TableVersion:
44
58
  """
45
59
  TableVersion represents a particular version of a table/view along with its physical representation:
@@ -65,6 +79,7 @@ class TableVersion:
65
79
 
66
80
  # record metadata stored in catalog
67
81
  _tbl_md: schema.TableMd
82
+ _version_md: schema.TableVersionMd
68
83
  _schema_version_md: schema.TableSchemaVersionMd
69
84
 
70
85
  effective_version: Optional[int]
@@ -78,7 +93,7 @@ class TableVersion:
78
93
  num_iterator_cols: int
79
94
 
80
95
  # target for data operation propagation (only set for non-snapshots, and only records non-snapshot views)
81
- mutable_views: set[TableVersionHandle]
96
+ mutable_views: frozenset[TableVersionHandle]
82
97
 
83
98
  # contains complete history of columns, incl dropped ones
84
99
  cols: list[Column]
@@ -92,6 +107,8 @@ class TableVersion:
92
107
  external_stores: dict[str, pxt.io.ExternalStore]
93
108
  store_tbl: Optional['store.StoreBase']
94
109
 
110
+ is_initialized: bool # True if init() has been called
111
+
95
112
  # used by Catalog to invalidate cached instances at the end of a transaction;
96
113
  # True if this instance reflects the state of stored metadata in the context of this transaction and
97
114
  # it is the instance cached in Catalog
@@ -110,6 +127,7 @@ class TableVersion:
110
127
  self,
111
128
  id: UUID,
112
129
  tbl_md: schema.TableMd,
130
+ version_md: schema.TableVersionMd,
113
131
  effective_version: Optional[int],
114
132
  schema_version_md: schema.TableSchemaVersionMd,
115
133
  mutable_views: list[TableVersionHandle],
@@ -117,8 +135,10 @@ class TableVersion:
117
135
  base: Optional[TableVersionHandle] = None,
118
136
  ):
119
137
  self.is_validated = True # a freshly constructed instance is always valid
138
+ self.is_initialized = False
120
139
  self.id = id
121
140
  self._tbl_md = copy.deepcopy(tbl_md)
141
+ self._version_md = copy.deepcopy(version_md)
122
142
  self._schema_version_md = copy.deepcopy(schema_version_md)
123
143
  self.effective_version = effective_version
124
144
  assert not (self.is_view and base is None)
@@ -159,7 +179,7 @@ class TableVersion:
159
179
  self.num_iterator_cols = len(output_schema)
160
180
  assert tbl_md.view_md.iterator_args is not None
161
181
 
162
- self.mutable_views = set(mutable_views)
182
+ self.mutable_views = frozenset(mutable_views)
163
183
  assert self.is_mutable or len(self.mutable_views) == 0
164
184
 
165
185
  self.cols = []
@@ -175,7 +195,9 @@ class TableVersion:
175
195
  """Create a snapshot copy of this TableVersion"""
176
196
  assert not self.is_snapshot
177
197
  base = self.path.base.tbl_version if self.is_view else None
178
- return TableVersion(self.id, self.tbl_md, self.version, self.schema_version_md, mutable_views=[], base=base)
198
+ return TableVersion(
199
+ self.id, self.tbl_md, self.version_md, self.version, self.schema_version_md, mutable_views=[], base=base
200
+ )
179
201
 
180
202
  @property
181
203
  def versioned_name(self) -> str:
@@ -190,6 +212,74 @@ class TableVersion:
190
212
 
191
213
  return TableVersionHandle(self.id, self.effective_version, self)
192
214
 
215
+ @classmethod
216
+ def create_initial_md(
217
+ cls,
218
+ name: str,
219
+ cols: list[Column],
220
+ num_retained_versions: int,
221
+ comment: str,
222
+ media_validation: MediaValidation,
223
+ view_md: Optional[schema.ViewMd] = None,
224
+ ) -> TableVersionMd:
225
+ user = Env.get().user
226
+
227
+ # assign ids
228
+ cols_by_name: dict[str, Column] = {}
229
+ for pos, col in enumerate(cols):
230
+ col.id = pos
231
+ col.schema_version_add = 0
232
+ cols_by_name[col.name] = col
233
+ if col.is_computed:
234
+ col.check_value_expr()
235
+
236
+ timestamp = time.time()
237
+ column_md = cls._create_column_md(cols)
238
+ tbl_id = uuid.uuid4()
239
+ tbl_id_str = str(tbl_id)
240
+ tbl_md = schema.TableMd(
241
+ tbl_id=tbl_id_str,
242
+ name=name,
243
+ user=user,
244
+ is_replica=False,
245
+ current_version=0,
246
+ current_schema_version=0,
247
+ next_col_id=len(cols),
248
+ next_idx_id=0,
249
+ next_row_id=0,
250
+ view_sn=0,
251
+ column_md=column_md,
252
+ index_md={},
253
+ external_stores=[],
254
+ view_md=view_md,
255
+ additional_md={},
256
+ )
257
+
258
+ table_version_md = schema.TableVersionMd(
259
+ tbl_id=tbl_id_str, created_at=timestamp, version=0, schema_version=0, additional_md={}
260
+ )
261
+
262
+ schema_col_md: dict[int, schema.SchemaColumn] = {}
263
+ for pos, col in enumerate(cols):
264
+ md = schema.SchemaColumn(
265
+ pos=pos,
266
+ name=col.name,
267
+ media_validation=col._media_validation.name.lower() if col._media_validation is not None else None,
268
+ )
269
+ schema_col_md[col.id] = md
270
+
271
+ schema_version_md = schema.TableSchemaVersionMd(
272
+ tbl_id=tbl_id_str,
273
+ schema_version=0,
274
+ preceding_schema_version=None,
275
+ columns=schema_col_md,
276
+ num_retained_versions=num_retained_versions,
277
+ comment=comment,
278
+ media_validation=media_validation.name.lower(),
279
+ additional_md={},
280
+ )
281
+ return TableVersionMd(tbl_md, table_version_md, schema_version_md)
282
+
193
283
  @classmethod
194
284
  def create(
195
285
  cls,
@@ -199,8 +289,6 @@ class TableVersion:
199
289
  num_retained_versions: int,
200
290
  comment: str,
201
291
  media_validation: MediaValidation,
202
- # base_path: Optional[pxt.catalog.TableVersionPath] = None,
203
- view_md: Optional[schema.ViewMd] = None,
204
292
  ) -> tuple[UUID, Optional[TableVersion]]:
205
293
  user = Env.get().user
206
294
 
@@ -233,7 +321,7 @@ class TableVersion:
233
321
  column_md=column_md,
234
322
  index_md={},
235
323
  external_stores=[],
236
- view_md=view_md,
324
+ view_md=None,
237
325
  additional_md={},
238
326
  )
239
327
 
@@ -271,47 +359,15 @@ class TableVersion:
271
359
 
272
360
  cat = pxt.catalog.Catalog.get()
273
361
 
274
- # if this is purely a snapshot (it doesn't require any additional storage for columns and it doesn't have a
275
- # predicate to apply at runtime), we don't create a physical table and simply use the base's table version path
276
- if (
277
- view_md is not None
278
- and view_md.is_snapshot
279
- and view_md.predicate is None
280
- and view_md.sample_clause is None
281
- and len(cols) == 0
282
- ):
283
- cat.store_tbl_md(
284
- tbl_id=tbl_id,
285
- dir_id=dir_id,
286
- tbl_md=table_md,
287
- version_md=table_version_md,
288
- schema_version_md=schema_version_md,
289
- )
290
- return tbl_id, None
291
-
292
- # assert (base_path is not None) == (view_md is not None)
293
- is_snapshot = view_md is not None and view_md.is_snapshot
294
- effective_version = 0 if is_snapshot else None
295
- base_path = pxt.catalog.TableVersionPath.from_md(view_md.base_versions) if view_md is not None else None
296
- base = base_path.tbl_version if base_path is not None else None
297
- tbl_version = cls(tbl_id, table_md, effective_version, schema_version_md, [], base_path=base_path, base=base)
362
+ tbl_version = cls(tbl_id, table_md, table_version_md, None, schema_version_md, [])
298
363
  # TODO: break this up, so that Catalog.create_table() registers tbl_version
299
- cat._tbl_versions[tbl_id, effective_version] = tbl_version
364
+ cat._tbl_versions[tbl_id, None] = tbl_version
300
365
  tbl_version.init()
301
366
  tbl_version.store_tbl.create()
302
- is_mutable = not is_snapshot and not table_md.is_replica
303
- if base is not None and base.get().is_mutable and is_mutable:
304
- from .table_version_handle import TableVersionHandle
305
-
306
- handle = TableVersionHandle(tbl_version.id, effective_version)
307
- assert handle not in base.get().mutable_views
308
- base.get().mutable_views.add(handle)
309
-
310
- if view_md is None or not view_md.is_snapshot:
311
- # add default indices, after creating the store table
312
- for col in tbl_version.cols_by_name.values():
313
- status = tbl_version._add_default_index(col)
314
- assert status is None or status.num_excs == 0
367
+ # add default indices, after creating the store table
368
+ for col in tbl_version.cols_by_name.values():
369
+ status = tbl_version._add_default_index(col)
370
+ assert status is None or status.num_excs == 0
315
371
 
316
372
  cat.store_tbl_md(
317
373
  tbl_id=tbl_id,
@@ -322,6 +378,27 @@ class TableVersion:
322
378
  )
323
379
  return tbl_id, tbl_version
324
380
 
381
+ def exec_op(self, op: TableOp) -> None:
382
+ if op.create_store_table_op is not None:
383
+ # don't use Catalog.begin_xact() here, to avoid accidental recursive calls to exec_op()
384
+ with Env.get().begin_xact():
385
+ self.store_tbl.create()
386
+
387
+ elif op.load_view_op is not None:
388
+ from pixeltable.catalog import Catalog
389
+ from pixeltable.plan import Planner
390
+
391
+ from .table_version_path import TableVersionPath
392
+
393
+ # clear out any remaining media files from an aborted previous attempt
394
+ MediaStore.delete(self.id)
395
+ view_path = TableVersionPath.from_dict(op.load_view_op.view_path)
396
+ plan, _ = Planner.create_view_load_plan(view_path)
397
+ _, row_counts = self.store_tbl.insert_rows(plan, v_min=self.version)
398
+ status = UpdateStatus(row_count_stats=row_counts)
399
+ Catalog.get().store_update_status(self.id, self.version, status)
400
+ _logger.debug(f'Loaded view {self.name} with {row_counts.num_rows} rows')
401
+
325
402
  @classmethod
326
403
  def create_replica(cls, md: schema.FullTableMd) -> TableVersion:
327
404
  assert Env.get().in_xact
@@ -331,7 +408,14 @@ class TableVersion:
331
408
  base_path = pxt.catalog.TableVersionPath.from_md(view_md.base_versions) if view_md is not None else None
332
409
  base = base_path.tbl_version if base_path is not None else None
333
410
  tbl_version = cls(
334
- tbl_id, md.tbl_md, md.version_md.version, md.schema_version_md, [], base_path=base_path, base=base
411
+ tbl_id,
412
+ md.tbl_md,
413
+ md.version_md,
414
+ md.version_md.version,
415
+ md.schema_version_md,
416
+ [],
417
+ base_path=base_path,
418
+ base=base,
335
419
  )
336
420
  cat = pxt.catalog.Catalog.get()
337
421
  # We're creating a new TableVersion replica, so we should never have seen this particular
@@ -345,23 +429,18 @@ class TableVersion:
345
429
  return tbl_version
346
430
 
347
431
  def drop(self) -> None:
348
- if self.is_view and self.is_mutable:
349
- # update mutable_views
350
- # TODO: invalidate base to force reload
351
- from .table_version_handle import TableVersionHandle
352
-
353
- assert self.base is not None
354
- if self.base.get().is_mutable:
355
- self.base.get().mutable_views.remove(TableVersionHandle.create(self))
432
+ # if self.is_view and self.is_mutable:
433
+ # # update mutable_views
434
+ # # TODO: invalidate base to force reload
435
+ # from .table_version_handle import TableVersionHandle
436
+ #
437
+ # assert self.base is not None
438
+ # if self.base.get().is_mutable:
439
+ # self.base.get().mutable_views.remove(TableVersionHandle.create(self))
356
440
 
357
- # cat = Catalog.get()
358
- # delete this table and all associated data
359
441
  MediaStore.delete(self.id)
360
442
  FileCache.get().clear(tbl_id=self.id)
361
- # cat.delete_tbl_md(self.id)
362
443
  self.store_tbl.drop()
363
- # de-register table version from catalog
364
- # cat.remove_tbl_version(self)
365
444
 
366
445
  def init(self) -> None:
367
446
  """
@@ -373,11 +452,11 @@ class TableVersion:
373
452
  cat = Catalog.get()
374
453
  assert (self.id, self.effective_version) in cat._tbl_versions
375
454
  self._init_schema()
376
- if not self.is_snapshot:
455
+ if self.is_mutable:
377
456
  cat.record_column_dependencies(self)
378
-
379
457
  # init external stores; this needs to happen after the schema is created
380
458
  self._init_external_stores()
459
+ self.is_initialized = True
381
460
 
382
461
  def _init_schema(self) -> None:
383
462
  # create columns first, so the indices can reference them
@@ -453,7 +532,14 @@ class TableVersion:
453
532
  # instantiate index object
454
533
  cls_name = md.class_fqn.rsplit('.', 1)[-1]
455
534
  cls = getattr(index_module, cls_name)
456
- idx_col = self.path.get_column_by_id(UUID(md.indexed_col_tbl_id), md.indexed_col_id)
535
+ idx_col: Column
536
+ if md.indexed_col_tbl_id == str(self.id):
537
+ # this is a reference to one of our columns: avoid TVP.get_column_by_id() here, because we're not fully
538
+ # initialized yet
539
+ idx_col = self.cols_by_id[md.indexed_col_id]
540
+ else:
541
+ assert self.path.base is not None
542
+ idx_col = self.path.base.get_column_by_id(UUID(md.indexed_col_tbl_id), md.indexed_col_id)
457
543
  idx = cls.from_dict(idx_col, md.init_args)
458
544
 
459
545
  # fix up the sa column type of the index value and undo columns
@@ -478,40 +564,17 @@ class TableVersion:
478
564
  else:
479
565
  self.store_tbl = StoreTable(self)
480
566
 
481
- def _write_md(
482
- self,
483
- new_version: bool,
484
- new_version_ts: float,
485
- new_schema_version: bool,
486
- update_status: Optional[UpdateStatus] = None,
487
- ) -> None:
488
- """Writes table metadata to the database.
489
-
490
- Args:
491
- timestamp: timestamp of the change
492
- update_tbl_version: if `True`, will also write `TableVersion` metadata
493
- preceding_schema_version: if specified, will also write `TableSchemaVersion` metadata, recording the
494
- specified preceding schema version
495
- """
567
+ def _write_md(self, new_version: bool, new_schema_version: bool) -> None:
496
568
  from pixeltable.catalog import Catalog
497
569
 
498
- version_md = self._create_version_md(new_version_ts, update_status=update_status) if new_version else None
499
-
500
570
  Catalog.get().store_tbl_md(
501
- self.id, None, self._tbl_md, version_md, self._schema_version_md if new_schema_version else None
571
+ self.id,
572
+ None,
573
+ self._tbl_md,
574
+ self._version_md if new_version else None,
575
+ self._schema_version_md if new_schema_version else None,
502
576
  )
503
577
 
504
- def _write_md_update_status(self, new_version_ts: float, update_status: UpdateStatus) -> None:
505
- """Writes a new update_status in the table version metadata in the database.
506
-
507
- Args:
508
- timestamp: timestamp of the change
509
- update_status: UpdateStatus to be updated in the database
510
- """
511
- from pixeltable.catalog import Catalog
512
-
513
- Catalog.get().update_tbl_version_md(self._create_version_md(new_version_ts, update_status))
514
-
515
578
  def _store_idx_name(self, idx_id: int) -> str:
516
579
  """Return name of index in the store, which needs to be globally unique"""
517
580
  return f'idx_{self.id.hex}_{idx_id}'
@@ -519,10 +582,10 @@ class TableVersion:
519
582
  def add_index(self, col: Column, idx_name: Optional[str], idx: index.IndexBase) -> UpdateStatus:
520
583
  # we're creating a new schema version
521
584
  self.version += 1
522
- self.preceding_schema_version = self.schema_version
585
+ self.created_at = time.time()
523
586
  self.schema_version = self.version
524
587
  status = self._add_index(col, idx_name, idx)
525
- self._write_md(new_version=True, new_version_ts=time.time(), new_schema_version=True)
588
+ self._write_md(new_version=True, new_schema_version=True)
526
589
  _logger.info(f'Added index {idx_name} on column {col.name} to table {self.name}')
527
590
  return status
528
591
 
@@ -638,12 +701,12 @@ class TableVersion:
638
701
  return status
639
702
 
640
703
  def drop_index(self, idx_id: int) -> None:
641
- assert not self.is_snapshot
704
+ assert self.is_mutable
642
705
  assert idx_id in self._tbl_md.index_md
643
706
 
644
707
  # we're creating a new schema version
645
708
  self.version += 1
646
- self.preceding_schema_version = self.schema_version
709
+ self.created_at = time.time()
647
710
  self.schema_version = self.version
648
711
  idx_md = self._tbl_md.index_md[idx_id]
649
712
  idx_md.schema_version_drop = self.schema_version
@@ -656,14 +719,14 @@ class TableVersion:
656
719
  del self._tbl_md.index_md[idx_id]
657
720
 
658
721
  self._drop_columns([idx_info.val_col, idx_info.undo_col])
659
- self._write_md(new_version=True, new_version_ts=time.time(), new_schema_version=True)
722
+ self._write_md(new_version=True, new_schema_version=True)
660
723
  _logger.info(f'Dropped index {idx_md.name} on table {self.name}')
661
724
 
662
725
  def add_columns(
663
726
  self, cols: Iterable[Column], print_stats: bool, on_error: Literal['abort', 'ignore']
664
727
  ) -> UpdateStatus:
665
728
  """Adds columns to the table."""
666
- assert not self.is_snapshot
729
+ assert self.is_mutable
667
730
  assert all(is_valid_identifier(col.name) for col in cols if col.name is not None)
668
731
  assert all(col.stored is not None for col in cols)
669
732
  assert all(col.name not in self.cols_by_name for col in cols if col.name is not None)
@@ -674,7 +737,7 @@ class TableVersion:
674
737
 
675
738
  # we're creating a new schema version
676
739
  self.version += 1
677
- self.preceding_schema_version = self.schema_version
740
+ self.created_at = time.time()
678
741
  self.schema_version = self.version
679
742
  index_cols: dict[Column, tuple[index.BtreeIndex, Column, Column]] = {}
680
743
  all_cols: list[Column] = []
@@ -691,7 +754,8 @@ class TableVersion:
691
754
  # Create indices and their md records
692
755
  for col, (idx, val_col, undo_col) in index_cols.items():
693
756
  self._create_index(col, val_col, undo_col, idx_name=None, idx=idx)
694
- self._write_md(new_version=True, new_version_ts=time.time(), new_schema_version=True, update_status=status)
757
+ self.update_status = status
758
+ self._write_md(new_version=True, new_schema_version=True)
695
759
  _logger.info(f'Added columns {[col.name for col in cols]} to table {self.name}, new version: {self.version}')
696
760
 
697
761
  msg = (
@@ -801,11 +865,11 @@ class TableVersion:
801
865
  def drop_column(self, col: Column) -> None:
802
866
  """Drop a column from the table."""
803
867
 
804
- assert not self.is_snapshot
868
+ assert self.is_mutable
805
869
 
806
870
  # we're creating a new schema version
807
871
  self.version += 1
808
- self.preceding_schema_version = self.schema_version
872
+ self.created_at = time.time()
809
873
  self.schema_version = self.version
810
874
 
811
875
  # drop this column and all dependent index columns and indices
@@ -825,12 +889,12 @@ class TableVersion:
825
889
  del self.idxs_by_name[idx_name]
826
890
 
827
891
  self._drop_columns(dropped_cols)
828
- self._write_md(new_version=True, new_version_ts=time.time(), new_schema_version=True)
892
+ self._write_md(new_version=True, new_schema_version=True)
829
893
  _logger.info(f'Dropped column {col.name} from table {self.name}, new version: {self.version}')
830
894
 
831
895
  def _drop_columns(self, cols: Iterable[Column]) -> None:
832
896
  """Mark columns as dropped"""
833
- assert not self.is_snapshot
897
+ assert self.is_mutable
834
898
 
835
899
  for col in cols:
836
900
  col.schema_version_drop = self.schema_version
@@ -853,7 +917,7 @@ class TableVersion:
853
917
 
854
918
  def rename_column(self, old_name: str, new_name: str) -> None:
855
919
  """Rename a column."""
856
- assert not self.is_snapshot
920
+ assert self.is_mutable
857
921
  if old_name not in self.cols_by_name:
858
922
  raise excs.Error(f'Unknown column: {old_name}')
859
923
  if not is_valid_identifier(new_name):
@@ -868,10 +932,10 @@ class TableVersion:
868
932
 
869
933
  # we're creating a new schema version
870
934
  self.version += 1
871
- self.preceding_schema_version = self.schema_version
935
+ self.created_at = time.time()
872
936
  self.schema_version = self.version
873
937
 
874
- self._write_md(new_version=True, new_version_ts=time.time(), new_schema_version=True)
938
+ self._write_md(new_version=True, new_schema_version=True)
875
939
  _logger.info(f'Renamed column {old_name} to {new_name} in table {self.name}, new version: {self.version}')
876
940
 
877
941
  def set_comment(self, new_comment: Optional[str]) -> None:
@@ -890,9 +954,9 @@ class TableVersion:
890
954
  def _create_schema_version(self) -> None:
891
955
  # we're creating a new schema version
892
956
  self.version += 1
893
- self.preceding_schema_version = self.schema_version
957
+ self.created_at = time.time()
894
958
  self.schema_version = self.version
895
- self._write_md(new_version=True, new_version_ts=time.time(), new_schema_version=True)
959
+ self._write_md(new_version=True, new_schema_version=True)
896
960
  _logger.info(f'[{self.name}] Updating table schema to version: {self.version}')
897
961
 
898
962
  def insert(
@@ -939,6 +1003,7 @@ class TableVersion:
939
1003
  """Insert rows produced by exec_plan and propagate to views"""
940
1004
  # we're creating a new version
941
1005
  self.version += 1
1006
+ self.created_at = timestamp
942
1007
  cols_with_excs, row_counts = self.store_tbl.insert_rows(
943
1008
  exec_plan, v_min=self.version, rowids=rowids, abort_on_exc=abort_on_exc
944
1009
  )
@@ -956,7 +1021,8 @@ class TableVersion:
956
1021
  result += status.to_cascade()
957
1022
 
958
1023
  # Use the net status after all propagations
959
- self._write_md(new_version=True, new_version_ts=timestamp, new_schema_version=False, update_status=result)
1024
+ self.update_status = result
1025
+ self._write_md(new_version=True, new_schema_version=False)
960
1026
  if print_stats:
961
1027
  exec_plan.ctx.profile.print(num_rows=result.num_rows)
962
1028
  _logger.info(f'TableVersion {self.name}: new version {self.version}')
@@ -972,8 +1038,7 @@ class TableVersion:
972
1038
  cascade: if True, also update all computed columns that transitively depend on the updated columns,
973
1039
  including within views.
974
1040
  """
975
- if self.is_snapshot:
976
- raise excs.Error('Cannot update a snapshot')
1041
+ assert self.is_mutable
977
1042
 
978
1043
  from pixeltable.plan import Planner
979
1044
 
@@ -1087,7 +1152,7 @@ class TableVersion:
1087
1152
  return update_targets
1088
1153
 
1089
1154
  def recompute_columns(self, col_names: list[str], errors_only: bool = False, cascade: bool = True) -> UpdateStatus:
1090
- assert not self.is_snapshot
1155
+ assert self.is_mutable
1091
1156
  assert all(name in self.cols_by_name for name in col_names)
1092
1157
  assert len(col_names) > 0
1093
1158
  assert len(col_names) == 1 or not errors_only
@@ -1132,6 +1197,7 @@ class TableVersion:
1132
1197
  create_new_table_version = plan is not None
1133
1198
  if create_new_table_version:
1134
1199
  self.version += 1
1200
+ self.created_at = timestamp
1135
1201
  cols_with_excs, row_counts = self.store_tbl.insert_rows(
1136
1202
  plan, v_min=self.version, show_progress=show_progress
1137
1203
  )
@@ -1158,7 +1224,8 @@ class TableVersion:
1158
1224
  )
1159
1225
  result += status.to_cascade()
1160
1226
  if create_new_table_version:
1161
- self._write_md(new_version=True, new_version_ts=timestamp, new_schema_version=False, update_status=result)
1227
+ self.update_status = result
1228
+ self._write_md(new_version=True, new_schema_version=False)
1162
1229
  return result
1163
1230
 
1164
1231
  def delete(self, where: Optional[exprs.Expr] = None) -> UpdateStatus:
@@ -1212,18 +1279,21 @@ class TableVersion:
1212
1279
  if del_rows > 0:
1213
1280
  # we're creating a new version
1214
1281
  self.version += 1
1282
+ self.created_at = timestamp
1215
1283
  for view in self.mutable_views:
1216
1284
  status = view.get().propagate_delete(
1217
1285
  where=None, base_versions=[self.version, *base_versions], timestamp=timestamp
1218
1286
  )
1219
1287
  result += status.to_cascade()
1288
+ self.update_status = result
1289
+
1220
1290
  if del_rows > 0:
1221
- self._write_md(new_version=True, new_version_ts=timestamp, new_schema_version=False, update_status=result)
1291
+ self._write_md(new_version=True, new_schema_version=False)
1222
1292
  return result
1223
1293
 
1224
1294
  def revert(self) -> None:
1225
1295
  """Reverts the table to the previous version."""
1226
- assert not self.is_snapshot
1296
+ assert self.is_mutable
1227
1297
  if self.version == 0:
1228
1298
  raise excs.Error('Cannot revert version 0')
1229
1299
  self._revert()
@@ -1317,7 +1387,7 @@ class TableVersion:
1317
1387
  )
1318
1388
 
1319
1389
  self.version -= 1
1320
- self._write_md(new_version=False, new_version_ts=0, new_schema_version=False)
1390
+ self._write_md(new_version=False, new_schema_version=False)
1321
1391
 
1322
1392
  # propagate to views
1323
1393
  views_str = ', '.join([str(v.id) for v in self.mutable_views])
@@ -1339,28 +1409,32 @@ class TableVersion:
1339
1409
 
1340
1410
  def link_external_store(self, store: pxt.io.ExternalStore) -> None:
1341
1411
  self.version += 1
1342
- self.preceding_schema_version = self.schema_version
1412
+ self.created_at = time.time()
1343
1413
  self.schema_version = self.version
1344
1414
 
1345
1415
  self.external_stores[store.name] = store
1346
1416
  self._tbl_md.external_stores.append(
1347
1417
  {'class': f'{type(store).__module__}.{type(store).__qualname__}', 'md': store.as_dict()}
1348
1418
  )
1349
- self._write_md(new_version=True, new_version_ts=time.time(), new_schema_version=True)
1419
+ self._write_md(new_version=True, new_schema_version=True)
1350
1420
 
1351
1421
  def unlink_external_store(self, store: pxt.io.ExternalStore) -> None:
1352
1422
  del self.external_stores[store.name]
1353
1423
  self.version += 1
1354
- self.preceding_schema_version = self.schema_version
1424
+ self.created_at = time.time()
1355
1425
  self.schema_version = self.version
1356
1426
  idx = next(i for i, store_md in enumerate(self._tbl_md.external_stores) if store_md['md']['name'] == store.name)
1357
1427
  self._tbl_md.external_stores.pop(idx)
1358
- self._write_md(new_version=True, new_version_ts=time.time(), new_schema_version=True)
1428
+ self._write_md(new_version=True, new_schema_version=True)
1359
1429
 
1360
1430
  @property
1361
1431
  def tbl_md(self) -> schema.TableMd:
1362
1432
  return self._tbl_md
1363
1433
 
1434
+ @property
1435
+ def version_md(self) -> schema.TableVersionMd:
1436
+ return self._version_md
1437
+
1364
1438
  @property
1365
1439
  def schema_version_md(self) -> schema.TableSchemaVersionMd:
1366
1440
  return self._schema_version_md
@@ -1408,6 +1482,16 @@ class TableVersion:
1408
1482
  def version(self, version: int) -> None:
1409
1483
  assert self.effective_version is None
1410
1484
  self._tbl_md.current_version = version
1485
+ self._version_md.version = version
1486
+
1487
+ @property
1488
+ def created_at(self) -> float:
1489
+ return self._version_md.created_at
1490
+
1491
+ @created_at.setter
1492
+ def created_at(self, ts: float) -> None:
1493
+ assert self.effective_version is None
1494
+ self._version_md.created_at = ts
1411
1495
 
1412
1496
  @property
1413
1497
  def schema_version(self) -> int:
@@ -1417,16 +1501,22 @@ class TableVersion:
1417
1501
  def schema_version(self, version: int) -> None:
1418
1502
  assert self.effective_version is None
1419
1503
  self._tbl_md.current_schema_version = version
1504
+ self._version_md.schema_version = version
1505
+ self._schema_version_md.preceding_schema_version = self._schema_version_md.schema_version
1420
1506
  self._schema_version_md.schema_version = version
1421
1507
 
1422
1508
  @property
1423
1509
  def preceding_schema_version(self) -> int:
1424
1510
  return self._schema_version_md.preceding_schema_version
1425
1511
 
1426
- @preceding_schema_version.setter
1427
- def preceding_schema_version(self, v: int) -> None:
1512
+ @property
1513
+ def update_status(self) -> Optional[UpdateStatus]:
1514
+ return self._version_md.update_status
1515
+
1516
+ @update_status.setter
1517
+ def update_status(self, status: UpdateStatus) -> None:
1428
1518
  assert self.effective_version is None
1429
- self._schema_version_md.preceding_schema_version = v
1519
+ self._version_md.update_status = status
1430
1520
 
1431
1521
  @property
1432
1522
  def media_validation(self) -> MediaValidation:
@@ -1482,7 +1572,7 @@ class TableVersion:
1482
1572
  @property
1483
1573
  def is_insertable(self) -> bool:
1484
1574
  """Returns True if this corresponds to an InsertableTable"""
1485
- return not self.is_snapshot and not self.is_view
1575
+ return self.is_mutable and not self.is_view
1486
1576
 
1487
1577
  def is_iterator_column(self, col: Column) -> bool:
1488
1578
  """Returns True if col is produced by an iterator"""
@@ -1560,37 +1650,6 @@ class TableVersion:
1560
1650
  {'class': f'{type(store).__module__}.{type(store).__qualname__}', 'md': store.as_dict()} for store in stores
1561
1651
  ]
1562
1652
 
1563
- def _create_version_md(self, timestamp: float, update_status: Optional[UpdateStatus]) -> schema.TableVersionMd:
1564
- return schema.TableVersionMd(
1565
- tbl_id=str(self.id),
1566
- created_at=timestamp,
1567
- version=self.version,
1568
- schema_version=self.schema_version,
1569
- user=Env.get().user,
1570
- update_status=update_status,
1571
- additional_md={},
1572
- )
1573
-
1574
- def _create_schema_version_md(self, preceding_schema_version: int) -> schema.TableSchemaVersionMd:
1575
- column_md: dict[int, schema.SchemaColumn] = {}
1576
- for pos, col in enumerate(self.cols_by_name.values()):
1577
- column_md[col.id] = schema.SchemaColumn(
1578
- pos=pos,
1579
- name=col.name,
1580
- media_validation=col._media_validation.name.lower() if col._media_validation is not None else None,
1581
- )
1582
- # preceding_schema_version to be set by the caller
1583
- return schema.TableSchemaVersionMd(
1584
- tbl_id=str(self.id),
1585
- schema_version=self.schema_version,
1586
- preceding_schema_version=preceding_schema_version,
1587
- columns=column_md,
1588
- num_retained_versions=self.num_retained_versions,
1589
- comment=self.comment,
1590
- media_validation=self.media_validation.name.lower(),
1591
- additional_md={},
1592
- )
1593
-
1594
1653
  def as_dict(self) -> dict:
1595
1654
  return {'id': str(self.id), 'effective_version': self.effective_version}
1596
1655