pixeltable 0.4.3__py3-none-any.whl → 0.4.5__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of pixeltable might be problematic. Click here for more details.

Files changed (52) hide show
  1. pixeltable/__version__.py +2 -2
  2. pixeltable/catalog/__init__.py +1 -1
  3. pixeltable/catalog/catalog.py +619 -255
  4. pixeltable/catalog/dir.py +1 -2
  5. pixeltable/catalog/insertable_table.py +9 -9
  6. pixeltable/catalog/path.py +59 -20
  7. pixeltable/catalog/schema_object.py +10 -4
  8. pixeltable/catalog/table.py +51 -53
  9. pixeltable/catalog/table_version.py +216 -156
  10. pixeltable/catalog/table_version_path.py +1 -1
  11. pixeltable/catalog/tbl_ops.py +44 -0
  12. pixeltable/catalog/view.py +63 -65
  13. pixeltable/config.py +12 -4
  14. pixeltable/dataframe.py +75 -6
  15. pixeltable/env.py +46 -17
  16. pixeltable/exec/aggregation_node.py +1 -1
  17. pixeltable/exec/cache_prefetch_node.py +2 -6
  18. pixeltable/exec/component_iteration_node.py +4 -3
  19. pixeltable/exec/data_row_batch.py +10 -51
  20. pixeltable/exec/expr_eval/expr_eval_node.py +2 -2
  21. pixeltable/exec/in_memory_data_node.py +17 -16
  22. pixeltable/exec/sql_node.py +6 -7
  23. pixeltable/exprs/column_ref.py +2 -1
  24. pixeltable/exprs/data_row.py +13 -13
  25. pixeltable/exprs/row_builder.py +16 -4
  26. pixeltable/exprs/string_op.py +1 -1
  27. pixeltable/func/expr_template_function.py +1 -4
  28. pixeltable/functions/date.py +1 -1
  29. pixeltable/functions/gemini.py +4 -4
  30. pixeltable/functions/math.py +1 -1
  31. pixeltable/functions/openai.py +9 -6
  32. pixeltable/functions/timestamp.py +6 -6
  33. pixeltable/functions/video.py +2 -6
  34. pixeltable/globals.py +62 -33
  35. pixeltable/io/datarows.py +2 -1
  36. pixeltable/io/pandas.py +1 -0
  37. pixeltable/io/table_data_conduit.py +12 -13
  38. pixeltable/iterators/audio.py +17 -8
  39. pixeltable/iterators/image.py +5 -2
  40. pixeltable/metadata/schema.py +39 -2
  41. pixeltable/plan.py +5 -14
  42. pixeltable/share/packager.py +13 -13
  43. pixeltable/store.py +31 -7
  44. pixeltable/type_system.py +2 -1
  45. pixeltable/utils/filecache.py +1 -1
  46. pixeltable/utils/http_server.py +2 -3
  47. pixeltable/utils/media_store.py +90 -34
  48. {pixeltable-0.4.3.dist-info → pixeltable-0.4.5.dist-info}/METADATA +1 -1
  49. {pixeltable-0.4.3.dist-info → pixeltable-0.4.5.dist-info}/RECORD +52 -51
  50. {pixeltable-0.4.3.dist-info → pixeltable-0.4.5.dist-info}/LICENSE +0 -0
  51. {pixeltable-0.4.3.dist-info → pixeltable-0.4.5.dist-info}/WHEEL +0 -0
  52. {pixeltable-0.4.3.dist-info → pixeltable-0.4.5.dist-info}/entry_points.txt +0 -0
@@ -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 = (
@@ -709,6 +773,7 @@ class TableVersion:
709
773
  cols_to_add = list(cols)
710
774
  row_count = self.store_tbl.count()
711
775
  for col in cols_to_add:
776
+ assert col.tbl is self
712
777
  if not col.col_type.nullable and not col.is_computed and row_count > 0:
713
778
  raise excs.Error(
714
779
  f'Cannot add non-nullable column {col.name!r} to table {self.name!r} with existing rows'
@@ -801,11 +866,11 @@ class TableVersion:
801
866
  def drop_column(self, col: Column) -> None:
802
867
  """Drop a column from the table."""
803
868
 
804
- assert not self.is_snapshot
869
+ assert self.is_mutable
805
870
 
806
871
  # we're creating a new schema version
807
872
  self.version += 1
808
- self.preceding_schema_version = self.schema_version
873
+ self.created_at = time.time()
809
874
  self.schema_version = self.version
810
875
 
811
876
  # drop this column and all dependent index columns and indices
@@ -825,12 +890,12 @@ class TableVersion:
825
890
  del self.idxs_by_name[idx_name]
826
891
 
827
892
  self._drop_columns(dropped_cols)
828
- self._write_md(new_version=True, new_version_ts=time.time(), new_schema_version=True)
893
+ self._write_md(new_version=True, new_schema_version=True)
829
894
  _logger.info(f'Dropped column {col.name} from table {self.name}, new version: {self.version}')
830
895
 
831
896
  def _drop_columns(self, cols: Iterable[Column]) -> None:
832
897
  """Mark columns as dropped"""
833
- assert not self.is_snapshot
898
+ assert self.is_mutable
834
899
 
835
900
  for col in cols:
836
901
  col.schema_version_drop = self.schema_version
@@ -853,7 +918,7 @@ class TableVersion:
853
918
 
854
919
  def rename_column(self, old_name: str, new_name: str) -> None:
855
920
  """Rename a column."""
856
- assert not self.is_snapshot
921
+ assert self.is_mutable
857
922
  if old_name not in self.cols_by_name:
858
923
  raise excs.Error(f'Unknown column: {old_name}')
859
924
  if not is_valid_identifier(new_name):
@@ -868,10 +933,10 @@ class TableVersion:
868
933
 
869
934
  # we're creating a new schema version
870
935
  self.version += 1
871
- self.preceding_schema_version = self.schema_version
936
+ self.created_at = time.time()
872
937
  self.schema_version = self.version
873
938
 
874
- self._write_md(new_version=True, new_version_ts=time.time(), new_schema_version=True)
939
+ self._write_md(new_version=True, new_schema_version=True)
875
940
  _logger.info(f'Renamed column {old_name} to {new_name} in table {self.name}, new version: {self.version}')
876
941
 
877
942
  def set_comment(self, new_comment: Optional[str]) -> None:
@@ -890,9 +955,9 @@ class TableVersion:
890
955
  def _create_schema_version(self) -> None:
891
956
  # we're creating a new schema version
892
957
  self.version += 1
893
- self.preceding_schema_version = self.schema_version
958
+ self.created_at = time.time()
894
959
  self.schema_version = self.version
895
- self._write_md(new_version=True, new_version_ts=time.time(), new_schema_version=True)
960
+ self._write_md(new_version=True, new_schema_version=True)
896
961
  _logger.info(f'[{self.name}] Updating table schema to version: {self.version}')
897
962
 
898
963
  def insert(
@@ -939,6 +1004,7 @@ class TableVersion:
939
1004
  """Insert rows produced by exec_plan and propagate to views"""
940
1005
  # we're creating a new version
941
1006
  self.version += 1
1007
+ self.created_at = timestamp
942
1008
  cols_with_excs, row_counts = self.store_tbl.insert_rows(
943
1009
  exec_plan, v_min=self.version, rowids=rowids, abort_on_exc=abort_on_exc
944
1010
  )
@@ -956,7 +1022,8 @@ class TableVersion:
956
1022
  result += status.to_cascade()
957
1023
 
958
1024
  # 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)
1025
+ self.update_status = result
1026
+ self._write_md(new_version=True, new_schema_version=False)
960
1027
  if print_stats:
961
1028
  exec_plan.ctx.profile.print(num_rows=result.num_rows)
962
1029
  _logger.info(f'TableVersion {self.name}: new version {self.version}')
@@ -972,8 +1039,7 @@ class TableVersion:
972
1039
  cascade: if True, also update all computed columns that transitively depend on the updated columns,
973
1040
  including within views.
974
1041
  """
975
- if self.is_snapshot:
976
- raise excs.Error('Cannot update a snapshot')
1042
+ assert self.is_mutable
977
1043
 
978
1044
  from pixeltable.plan import Planner
979
1045
 
@@ -1087,7 +1153,7 @@ class TableVersion:
1087
1153
  return update_targets
1088
1154
 
1089
1155
  def recompute_columns(self, col_names: list[str], errors_only: bool = False, cascade: bool = True) -> UpdateStatus:
1090
- assert not self.is_snapshot
1156
+ assert self.is_mutable
1091
1157
  assert all(name in self.cols_by_name for name in col_names)
1092
1158
  assert len(col_names) > 0
1093
1159
  assert len(col_names) == 1 or not errors_only
@@ -1132,6 +1198,7 @@ class TableVersion:
1132
1198
  create_new_table_version = plan is not None
1133
1199
  if create_new_table_version:
1134
1200
  self.version += 1
1201
+ self.created_at = timestamp
1135
1202
  cols_with_excs, row_counts = self.store_tbl.insert_rows(
1136
1203
  plan, v_min=self.version, show_progress=show_progress
1137
1204
  )
@@ -1158,7 +1225,8 @@ class TableVersion:
1158
1225
  )
1159
1226
  result += status.to_cascade()
1160
1227
  if create_new_table_version:
1161
- self._write_md(new_version=True, new_version_ts=timestamp, new_schema_version=False, update_status=result)
1228
+ self.update_status = result
1229
+ self._write_md(new_version=True, new_schema_version=False)
1162
1230
  return result
1163
1231
 
1164
1232
  def delete(self, where: Optional[exprs.Expr] = None) -> UpdateStatus:
@@ -1212,18 +1280,21 @@ class TableVersion:
1212
1280
  if del_rows > 0:
1213
1281
  # we're creating a new version
1214
1282
  self.version += 1
1283
+ self.created_at = timestamp
1215
1284
  for view in self.mutable_views:
1216
1285
  status = view.get().propagate_delete(
1217
1286
  where=None, base_versions=[self.version, *base_versions], timestamp=timestamp
1218
1287
  )
1219
1288
  result += status.to_cascade()
1289
+ self.update_status = result
1290
+
1220
1291
  if del_rows > 0:
1221
- self._write_md(new_version=True, new_version_ts=timestamp, new_schema_version=False, update_status=result)
1292
+ self._write_md(new_version=True, new_schema_version=False)
1222
1293
  return result
1223
1294
 
1224
1295
  def revert(self) -> None:
1225
1296
  """Reverts the table to the previous version."""
1226
- assert not self.is_snapshot
1297
+ assert self.is_mutable
1227
1298
  if self.version == 0:
1228
1299
  raise excs.Error('Cannot revert version 0')
1229
1300
  self._revert()
@@ -1255,7 +1326,7 @@ class TableVersion:
1255
1326
  )
1256
1327
 
1257
1328
  # delete newly-added data
1258
- MediaStore.delete(self.id, version=self.version)
1329
+ MediaStore.delete(self.id, tbl_version=self.version)
1259
1330
  conn.execute(sql.delete(self.store_tbl.sa_tbl).where(self.store_tbl.sa_tbl.c.v_min == self.version))
1260
1331
 
1261
1332
  # revert new deletions
@@ -1317,7 +1388,7 @@ class TableVersion:
1317
1388
  )
1318
1389
 
1319
1390
  self.version -= 1
1320
- self._write_md(new_version=False, new_version_ts=0, new_schema_version=False)
1391
+ self._write_md(new_version=False, new_schema_version=False)
1321
1392
 
1322
1393
  # propagate to views
1323
1394
  views_str = ', '.join([str(v.id) for v in self.mutable_views])
@@ -1339,28 +1410,32 @@ class TableVersion:
1339
1410
 
1340
1411
  def link_external_store(self, store: pxt.io.ExternalStore) -> None:
1341
1412
  self.version += 1
1342
- self.preceding_schema_version = self.schema_version
1413
+ self.created_at = time.time()
1343
1414
  self.schema_version = self.version
1344
1415
 
1345
1416
  self.external_stores[store.name] = store
1346
1417
  self._tbl_md.external_stores.append(
1347
1418
  {'class': f'{type(store).__module__}.{type(store).__qualname__}', 'md': store.as_dict()}
1348
1419
  )
1349
- self._write_md(new_version=True, new_version_ts=time.time(), new_schema_version=True)
1420
+ self._write_md(new_version=True, new_schema_version=True)
1350
1421
 
1351
1422
  def unlink_external_store(self, store: pxt.io.ExternalStore) -> None:
1352
1423
  del self.external_stores[store.name]
1353
1424
  self.version += 1
1354
- self.preceding_schema_version = self.schema_version
1425
+ self.created_at = time.time()
1355
1426
  self.schema_version = self.version
1356
1427
  idx = next(i for i, store_md in enumerate(self._tbl_md.external_stores) if store_md['md']['name'] == store.name)
1357
1428
  self._tbl_md.external_stores.pop(idx)
1358
- self._write_md(new_version=True, new_version_ts=time.time(), new_schema_version=True)
1429
+ self._write_md(new_version=True, new_schema_version=True)
1359
1430
 
1360
1431
  @property
1361
1432
  def tbl_md(self) -> schema.TableMd:
1362
1433
  return self._tbl_md
1363
1434
 
1435
+ @property
1436
+ def version_md(self) -> schema.TableVersionMd:
1437
+ return self._version_md
1438
+
1364
1439
  @property
1365
1440
  def schema_version_md(self) -> schema.TableSchemaVersionMd:
1366
1441
  return self._schema_version_md
@@ -1408,6 +1483,16 @@ class TableVersion:
1408
1483
  def version(self, version: int) -> None:
1409
1484
  assert self.effective_version is None
1410
1485
  self._tbl_md.current_version = version
1486
+ self._version_md.version = version
1487
+
1488
+ @property
1489
+ def created_at(self) -> float:
1490
+ return self._version_md.created_at
1491
+
1492
+ @created_at.setter
1493
+ def created_at(self, ts: float) -> None:
1494
+ assert self.effective_version is None
1495
+ self._version_md.created_at = ts
1411
1496
 
1412
1497
  @property
1413
1498
  def schema_version(self) -> int:
@@ -1417,16 +1502,22 @@ class TableVersion:
1417
1502
  def schema_version(self, version: int) -> None:
1418
1503
  assert self.effective_version is None
1419
1504
  self._tbl_md.current_schema_version = version
1505
+ self._version_md.schema_version = version
1506
+ self._schema_version_md.preceding_schema_version = self._schema_version_md.schema_version
1420
1507
  self._schema_version_md.schema_version = version
1421
1508
 
1422
1509
  @property
1423
1510
  def preceding_schema_version(self) -> int:
1424
1511
  return self._schema_version_md.preceding_schema_version
1425
1512
 
1426
- @preceding_schema_version.setter
1427
- def preceding_schema_version(self, v: int) -> None:
1513
+ @property
1514
+ def update_status(self) -> Optional[UpdateStatus]:
1515
+ return self._version_md.update_status
1516
+
1517
+ @update_status.setter
1518
+ def update_status(self, status: UpdateStatus) -> None:
1428
1519
  assert self.effective_version is None
1429
- self._schema_version_md.preceding_schema_version = v
1520
+ self._version_md.update_status = status
1430
1521
 
1431
1522
  @property
1432
1523
  def media_validation(self) -> MediaValidation:
@@ -1482,7 +1573,7 @@ class TableVersion:
1482
1573
  @property
1483
1574
  def is_insertable(self) -> bool:
1484
1575
  """Returns True if this corresponds to an InsertableTable"""
1485
- return not self.is_snapshot and not self.is_view
1576
+ return self.is_mutable and not self.is_view
1486
1577
 
1487
1578
  def is_iterator_column(self, col: Column) -> bool:
1488
1579
  """Returns True if col is produced by an iterator"""
@@ -1560,37 +1651,6 @@ class TableVersion:
1560
1651
  {'class': f'{type(store).__module__}.{type(store).__qualname__}', 'md': store.as_dict()} for store in stores
1561
1652
  ]
1562
1653
 
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
1654
  def as_dict(self) -> dict:
1595
1655
  return {'id': str(self.id), 'effective_version': self.effective_version}
1596
1656