pixeltable 0.3.15__py3-none-any.whl → 0.4.0rc2__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 (58) hide show
  1. pixeltable/__version__.py +2 -2
  2. pixeltable/catalog/catalog.py +296 -105
  3. pixeltable/catalog/column.py +10 -8
  4. pixeltable/catalog/dir.py +1 -2
  5. pixeltable/catalog/insertable_table.py +25 -20
  6. pixeltable/catalog/schema_object.py +3 -6
  7. pixeltable/catalog/table.py +261 -189
  8. pixeltable/catalog/table_version.py +333 -202
  9. pixeltable/catalog/table_version_handle.py +15 -2
  10. pixeltable/catalog/table_version_path.py +60 -14
  11. pixeltable/catalog/view.py +38 -6
  12. pixeltable/dataframe.py +196 -18
  13. pixeltable/env.py +4 -4
  14. pixeltable/exec/__init__.py +1 -1
  15. pixeltable/exec/expr_eval/evaluators.py +4 -1
  16. pixeltable/exec/in_memory_data_node.py +1 -1
  17. pixeltable/exec/sql_node.py +171 -22
  18. pixeltable/exprs/column_property_ref.py +15 -6
  19. pixeltable/exprs/column_ref.py +32 -11
  20. pixeltable/exprs/comparison.py +1 -1
  21. pixeltable/exprs/data_row.py +5 -3
  22. pixeltable/exprs/expr.py +7 -0
  23. pixeltable/exprs/literal.py +2 -0
  24. pixeltable/exprs/row_builder.py +4 -6
  25. pixeltable/exprs/rowid_ref.py +8 -0
  26. pixeltable/exprs/similarity_expr.py +1 -0
  27. pixeltable/func/query_template_function.py +1 -1
  28. pixeltable/func/tools.py +1 -1
  29. pixeltable/functions/gemini.py +0 -1
  30. pixeltable/functions/string.py +212 -58
  31. pixeltable/globals.py +12 -4
  32. pixeltable/index/base.py +5 -0
  33. pixeltable/index/btree.py +5 -0
  34. pixeltable/index/embedding_index.py +5 -0
  35. pixeltable/io/external_store.py +8 -29
  36. pixeltable/io/label_studio.py +1 -1
  37. pixeltable/io/parquet.py +2 -2
  38. pixeltable/io/table_data_conduit.py +0 -31
  39. pixeltable/metadata/__init__.py +11 -2
  40. pixeltable/metadata/converters/convert_13.py +2 -2
  41. pixeltable/metadata/converters/convert_30.py +6 -11
  42. pixeltable/metadata/converters/convert_35.py +9 -0
  43. pixeltable/metadata/converters/convert_36.py +38 -0
  44. pixeltable/metadata/converters/util.py +3 -9
  45. pixeltable/metadata/notes.py +2 -0
  46. pixeltable/metadata/schema.py +8 -1
  47. pixeltable/plan.py +221 -14
  48. pixeltable/share/packager.py +137 -13
  49. pixeltable/share/publish.py +2 -2
  50. pixeltable/store.py +19 -13
  51. pixeltable/utils/dbms.py +1 -1
  52. pixeltable/utils/formatter.py +64 -42
  53. pixeltable/utils/sample.py +25 -0
  54. {pixeltable-0.3.15.dist-info → pixeltable-0.4.0rc2.dist-info}/METADATA +2 -1
  55. {pixeltable-0.3.15.dist-info → pixeltable-0.4.0rc2.dist-info}/RECORD +58 -55
  56. {pixeltable-0.3.15.dist-info → pixeltable-0.4.0rc2.dist-info}/LICENSE +0 -0
  57. {pixeltable-0.3.15.dist-info → pixeltable-0.4.0rc2.dist-info}/WHEEL +0 -0
  58. {pixeltable-0.3.15.dist-info → pixeltable-0.4.0rc2.dist-info}/entry_points.txt +0 -0
@@ -1,5 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import copy
3
4
  import dataclasses
4
5
  import importlib
5
6
  import logging
@@ -22,6 +23,10 @@ from pixeltable.utils.exception_handler import run_cleanup_on_exception
22
23
  from pixeltable.utils.filecache import FileCache
23
24
  from pixeltable.utils.media_store import MediaStore
24
25
 
26
+ if TYPE_CHECKING:
27
+ from pixeltable.plan import SampleClause
28
+
29
+
25
30
  from ..func.globals import resolve_symbol
26
31
  from .column import Column
27
32
  from .globals import _POS_COLUMN_NAME, _ROWID_COLUMN_NAME, MediaValidation, UpdateStatus, is_valid_identifier
@@ -50,43 +55,46 @@ class TableVersion:
50
55
 
51
56
  Instances of TableVersion should not be stored as member variables (ie, used across transaction boundaries).
52
57
  Use a TableVersionHandle instead.
58
+
59
+ Only TableVersion and Catalog interact directly with stored metadata. Everything else needs to go through these
60
+ two classes.
53
61
  """
54
62
 
55
63
  id: UUID
56
- name: str
57
- user: Optional[str]
64
+
65
+ # record metadata stored in catalog
66
+ _tbl_md: schema.TableMd
67
+ _schema_version_md: schema.TableSchemaVersionMd
68
+
58
69
  effective_version: Optional[int]
59
- is_replica: bool
60
- version: int
61
- comment: str
62
- media_validation: MediaValidation
63
- num_retained_versions: int
64
- schema_version: int
65
- view_md: Optional[schema.ViewMd]
66
70
  path: Optional[pxt.catalog.TableVersionPath] # only set for live tables; needed to resolve computed cols
67
71
  base: Optional[TableVersionHandle] # only set for views
68
- next_col_id: int
69
- next_idx_id: int
70
- next_rowid: int
71
72
  predicate: Optional[exprs.Expr]
72
- mutable_views: list[TableVersionHandle] # target for data operation propagation (only set for live tables)
73
+ sample_clause: Optional['SampleClause']
74
+
73
75
  iterator_cls: Optional[type[ComponentIterator]]
74
76
  iterator_args: Optional[exprs.InlineDict]
75
77
  num_iterator_cols: int
76
78
 
79
+ # target for data operation propagation (only set for non-snapshots, and only records non-snapshot views)
80
+ mutable_views: set[TableVersionHandle]
81
+
77
82
  # contains complete history of columns, incl dropped ones
78
83
  cols: list[Column]
79
84
  # contains only user-facing (named) columns visible in this version
80
85
  cols_by_name: dict[str, Column]
81
86
  # contains only columns visible in this version, both system and user
82
87
  cols_by_id: dict[int, Column]
83
- # needed for _create_tbl_md()
84
- idx_md: dict[int, schema.IndexMd]
85
88
  # contains only actively maintained indices
86
89
  idxs_by_name: dict[str, TableVersion.IndexInfo]
87
90
 
88
91
  external_stores: dict[str, pxt.io.ExternalStore]
89
- store_tbl: 'store.StoreBase'
92
+ store_tbl: Optional['store.StoreBase']
93
+
94
+ # used by Catalog to invalidate cached instances at the end of a transaction;
95
+ # True if this instance reflects the state of stored metadata in the context of this transaction and
96
+ # it is the instance cached in Catalog
97
+ is_validated: bool
90
98
 
91
99
  @dataclasses.dataclass
92
100
  class IndexInfo:
@@ -106,21 +114,15 @@ class TableVersion:
106
114
  mutable_views: list[TableVersionHandle],
107
115
  base_path: Optional[pxt.catalog.TableVersionPath] = None,
108
116
  base: Optional[TableVersionHandle] = None,
109
- # base_store_tbl: Optional['store.StoreBase'] = None,
110
117
  ):
118
+ self.is_validated = True # a freshly constructed instance is always valid
111
119
  self.id = id
112
- self.name = tbl_md.name
113
- self.user = tbl_md.user
120
+ self._tbl_md = copy.deepcopy(tbl_md)
121
+ self._schema_version_md = copy.deepcopy(schema_version_md)
114
122
  self.effective_version = effective_version
115
- self.version = tbl_md.current_version if effective_version is None else effective_version
116
- self.is_replica = tbl_md.is_replica
117
- self.comment = schema_version_md.comment
118
- self.num_retained_versions = schema_version_md.num_retained_versions
119
- self.schema_version = schema_version_md.schema_version
120
- self.view_md = tbl_md.view_md # save this as-is, it's needed for _create_md()
121
- self.media_validation = MediaValidation[schema_version_md.media_validation.upper()]
122
123
  assert not (self.is_view and base is None)
123
124
  self.base = base
125
+ self.store_tbl = None
124
126
 
125
127
  # mutable tables need their TableVersionPath for expr eval during updates
126
128
  from .table_version_handle import TableVersionHandle
@@ -134,22 +136,14 @@ class TableVersion:
134
136
  assert base_path is not None
135
137
  self.path = TableVersionPath(self_handle, base=base_path)
136
138
 
137
- if self.is_snapshot:
138
- self.next_col_id = -1
139
- self.next_idx_id = -1 # TODO: can snapshots have separate indices?
140
- self.next_rowid = -1
141
- else:
142
- assert tbl_md.current_version == self.version
143
- self.next_col_id = tbl_md.next_col_id
144
- self.next_idx_id = tbl_md.next_idx_id
145
- self.next_rowid = tbl_md.next_row_id
146
-
147
139
  # view-specific initialization
148
140
  from pixeltable import exprs
141
+ from pixeltable.plan import SampleClause
149
142
 
150
143
  predicate_dict = None if self.view_md is None or self.view_md.predicate is None else self.view_md.predicate
151
144
  self.predicate = exprs.Expr.from_dict(predicate_dict) if predicate_dict is not None else None
152
- self.mutable_views = mutable_views
145
+ sample_dict = None if self.view_md is None or self.view_md.sample_clause is None else self.view_md.sample_clause
146
+ self.sample_clause = SampleClause.from_dict(sample_dict) if sample_dict is not None else None
153
147
 
154
148
  # component view-specific initialization
155
149
  self.iterator_cls = None
@@ -164,22 +158,26 @@ class TableVersion:
164
158
  self.num_iterator_cols = len(output_schema)
165
159
  assert tbl_md.view_md.iterator_args is not None
166
160
 
167
- # register this table version now so that it's available when we're re-creating value exprs
168
- cat = pxt.catalog.Catalog.get()
169
- cat.add_tbl_version(self)
161
+ self.mutable_views = set(mutable_views)
162
+ assert self.is_mutable or len(self.mutable_views) == 0
170
163
 
171
- # init schema after we determined whether we're a component view, and before we create the store table
172
164
  self.cols = []
173
165
  self.cols_by_name = {}
174
166
  self.cols_by_id = {}
175
- self.idx_md = tbl_md.index_md
176
167
  self.idxs_by_name = {}
177
168
  self.external_stores = {}
178
169
 
179
- self._init_schema(tbl_md, schema_version_md)
170
+ def init(self) -> None:
171
+ """
172
+ Initialize schema-related in-memory metadata separately, now that this TableVersion instance is visible
173
+ in Catalog.
174
+ """
175
+ from .catalog import Catalog
180
176
 
181
- # Init external stores (this needs to happen after the schema is created)
182
- self._init_external_stores(tbl_md)
177
+ assert (self.id, self.effective_version) in Catalog.get()._tbl_versions
178
+ self._init_schema()
179
+ # init external stores; this needs to happen after the schema is created
180
+ self._init_external_stores()
183
181
 
184
182
  def __hash__(self) -> int:
185
183
  return hash(self.id)
@@ -188,19 +186,7 @@ class TableVersion:
188
186
  """Create a snapshot copy of this TableVersion"""
189
187
  assert not self.is_snapshot
190
188
  base = self.path.base.tbl_version if self.is_view else None
191
- return TableVersion(
192
- self.id,
193
- self._create_tbl_md(),
194
- self.version,
195
- self._create_schema_version_md(preceding_schema_version=0), # preceding_schema_version: dummy value
196
- mutable_views=[],
197
- base=base,
198
- )
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)
189
+ return TableVersion(self.id, self.tbl_md, self.version, self.schema_version_md, mutable_views=[], base=base)
204
190
 
205
191
  @property
206
192
  def versioned_name(self) -> str:
@@ -292,7 +278,13 @@ class TableVersion:
292
278
 
293
279
  # if this is purely a snapshot (it doesn't require any additional storage for columns and it doesn't have a
294
280
  # predicate to apply at runtime), we don't create a physical table and simply use the base's table version path
295
- if view_md is not None and view_md.is_snapshot and view_md.predicate is None and len(cols) == 0:
281
+ if (
282
+ view_md is not None
283
+ and view_md.is_snapshot
284
+ and view_md.predicate is None
285
+ and view_md.sample_clause is None
286
+ and len(cols) == 0
287
+ ):
296
288
  session.add(tbl_record)
297
289
  session.add(tbl_version_record)
298
290
  session.add(schema_version_record)
@@ -306,8 +298,19 @@ class TableVersion:
306
298
  tbl_version = cls(
307
299
  tbl_record.id, table_md, effective_version, schema_version_md, [], base_path=base_path, base=base
308
300
  )
309
-
301
+ # TODO: break this up, so that Catalog.create_table() registers tbl_version
302
+ cat = pxt.catalog.Catalog.get()
303
+ cat._tbl_versions[tbl_record.id, effective_version] = tbl_version
304
+ tbl_version.init()
310
305
  tbl_version.store_tbl.create()
306
+ is_mutable = not is_snapshot and not table_md.is_replica
307
+ if base is not None and base.get().is_mutable and is_mutable:
308
+ from .table_version_handle import TableVersionHandle
309
+
310
+ handle = TableVersionHandle(tbl_version.id, effective_version)
311
+ assert handle not in base.get().mutable_views
312
+ base.get().mutable_views.add(handle)
313
+
311
314
  if view_md is None or not view_md.is_snapshot:
312
315
  # add default indices, after creating the store table
313
316
  for col in tbl_version.cols_by_name.values():
@@ -315,7 +318,7 @@ class TableVersion:
315
318
  assert status is None or status.num_excs == 0
316
319
 
317
320
  # we re-create the tbl_record here, now that we have new index metadata
318
- tbl_record = schema.Table(id=tbl_id, dir_id=dir_id, md=dataclasses.asdict(tbl_version._create_tbl_md()))
321
+ tbl_record = schema.Table(id=tbl_id, dir_id=dir_id, md=dataclasses.asdict(tbl_version.tbl_md))
319
322
  session.add(tbl_record)
320
323
  session.add(tbl_version_record)
321
324
  session.add(schema_version_record)
@@ -331,6 +334,9 @@ class TableVersion:
331
334
  tbl_version = cls(
332
335
  tbl_id, md.tbl_md, md.version_md.version, md.schema_version_md, [], base_path=base_path, base=base
333
336
  )
337
+ cat = pxt.catalog.Catalog.get()
338
+ cat._tbl_versions[tbl_version.id, tbl_version.effective_version] = tbl_version
339
+ tbl_version.init()
334
340
  tbl_version.store_tbl.create()
335
341
  tbl_version.store_tbl.ensure_columns_exist(col for col in tbl_version.cols if col.is_stored)
336
342
  return tbl_version
@@ -338,6 +344,14 @@ class TableVersion:
338
344
  def drop(self) -> None:
339
345
  from .catalog import Catalog
340
346
 
347
+ if self.is_view and self.is_mutable:
348
+ # update mutable_views
349
+ from .table_version_handle import TableVersionHandle
350
+
351
+ assert self.base is not None
352
+ if self.base.get().is_mutable:
353
+ self.base.get().mutable_views.remove(TableVersionHandle.create(self))
354
+
341
355
  cat = Catalog.get()
342
356
  # delete this table and all associated data
343
357
  MediaStore.delete(self.id)
@@ -347,24 +361,24 @@ class TableVersion:
347
361
  # de-register table version from catalog
348
362
  cat.remove_tbl_version(self)
349
363
 
350
- def _init_schema(self, tbl_md: schema.TableMd, schema_version_md: schema.TableSchemaVersionMd) -> None:
364
+ def _init_schema(self) -> None:
351
365
  # create columns first, so the indices can reference them
352
- self._init_cols(tbl_md, schema_version_md)
366
+ self._init_cols()
353
367
  if not self.is_snapshot:
354
- self._init_idxs(tbl_md)
368
+ self._init_idxs()
355
369
  # create the sa schema only after creating the columns and indices
356
370
  self._init_sa_schema()
357
371
 
358
- def _init_cols(self, tbl_md: schema.TableMd, schema_version_md: schema.TableSchemaVersionMd) -> None:
372
+ def _init_cols(self) -> None:
359
373
  """Initialize self.cols with the columns visible in our effective version"""
360
374
  self.cols = []
361
375
  self.cols_by_name = {}
362
376
  self.cols_by_id = {}
363
377
  # Sort columns in column_md by the position specified in col_md.id to guarantee that all references
364
378
  # point backward.
365
- sorted_column_md = sorted(tbl_md.column_md.values(), key=lambda item: item.id)
379
+ sorted_column_md = sorted(self.tbl_md.column_md.values(), key=lambda item: item.id)
366
380
  for col_md in sorted_column_md:
367
- schema_col_md = schema_version_md.columns.get(col_md.id)
381
+ schema_col_md = self.schema_version_md.columns.get(col_md.id)
368
382
  col_name = schema_col_md.name if schema_col_md is not None else None
369
383
  media_val = (
370
384
  MediaValidation[schema_col_md.media_validation.upper()]
@@ -382,7 +396,7 @@ class TableVersion:
382
396
  schema_version_drop=col_md.schema_version_drop,
383
397
  value_expr_dict=col_md.value_expr,
384
398
  )
385
- col.tbl = self.create_handle()
399
+ col.tbl = self
386
400
  self.cols.append(col)
387
401
 
388
402
  # populate the lookup structures before Expr.from_dict()
@@ -401,12 +415,12 @@ class TableVersion:
401
415
  if not self.is_snapshot and col_md.value_expr is not None:
402
416
  self._record_refd_columns(col)
403
417
 
404
- def _init_idxs(self, tbl_md: schema.TableMd) -> None:
405
- self.idx_md = tbl_md.index_md
418
+ def _init_idxs(self) -> None:
419
+ # self.idx_md = tbl_md.index_md
406
420
  self.idxs_by_name = {}
407
421
  import pixeltable.index as index_module
408
422
 
409
- for md in tbl_md.index_md.values():
423
+ for md in self.tbl_md.index_md.values():
410
424
  if md.schema_version_add > self.schema_version or (
411
425
  md.schema_version_drop is not None and md.schema_version_drop <= self.schema_version
412
426
  ):
@@ -441,28 +455,32 @@ class TableVersion:
441
455
  else:
442
456
  self.store_tbl = StoreTable(self)
443
457
 
444
- def _update_md(
445
- self, timestamp: float, update_tbl_version: bool = True, preceding_schema_version: Optional[int] = None
446
- ) -> None:
458
+ def _write_md(self, new_version: bool, new_version_ts: float, new_schema_version: bool) -> None:
447
459
  """Writes table metadata to the database.
448
460
 
449
461
  Args:
450
462
  timestamp: timestamp of the change
451
- conn: database connection to use
452
463
  update_tbl_version: if `True`, will also write `TableVersion` metadata
453
464
  preceding_schema_version: if specified, will also write `TableSchemaVersion` metadata, recording the
454
465
  specified preceding schema version
455
466
  """
456
- assert update_tbl_version or preceding_schema_version is None
457
467
  from pixeltable.catalog import Catalog
458
468
 
459
- tbl_md = self._create_tbl_md()
460
- version_md = self._create_version_md(timestamp) if update_tbl_version else None
461
- schema_version_md = (
462
- self._create_schema_version_md(preceding_schema_version) if preceding_schema_version is not None else None
469
+ version_md: Optional[schema.TableVersionMd] = (
470
+ schema.TableVersionMd(
471
+ tbl_id=str(self.id),
472
+ created_at=new_version_ts,
473
+ version=self.version,
474
+ schema_version=self.schema_version,
475
+ additional_md={},
476
+ )
477
+ if new_version
478
+ else None
463
479
  )
464
480
 
465
- Catalog.get().store_tbl_md(self.id, tbl_md, version_md, schema_version_md)
481
+ Catalog.get().store_tbl_md(
482
+ self.id, self._tbl_md, version_md, self._schema_version_md if new_schema_version else None
483
+ )
466
484
 
467
485
  def ensure_md_loaded(self) -> None:
468
486
  """Ensure that table metadata is loaded."""
@@ -476,10 +494,10 @@ class TableVersion:
476
494
  def add_index(self, col: Column, idx_name: Optional[str], idx: index.IndexBase) -> UpdateStatus:
477
495
  # we're creating a new schema version
478
496
  self.version += 1
479
- preceding_schema_version = self.schema_version
497
+ self.preceding_schema_version = self.schema_version
480
498
  self.schema_version = self.version
481
499
  status = self._add_index(col, idx_name, idx)
482
- self._update_md(time.time(), preceding_schema_version=preceding_schema_version)
500
+ self._write_md(new_version=True, new_version_ts=time.time(), new_schema_version=True)
483
501
  _logger.info(f'Added index {idx_name} on column {col.name} to table {self.name}')
484
502
  return status
485
503
 
@@ -524,7 +542,7 @@ class TableVersion:
524
542
  schema_version_drop=None,
525
543
  records_errors=idx.records_value_errors(),
526
544
  )
527
- val_col.tbl = self.create_handle()
545
+ val_col.tbl = self
528
546
  val_col.col_type = val_col.col_type.copy(nullable=True)
529
547
  self.next_col_id += 1
530
548
 
@@ -538,7 +556,7 @@ class TableVersion:
538
556
  schema_version_drop=None,
539
557
  records_errors=False,
540
558
  )
541
- undo_col.tbl = self.create_handle()
559
+ undo_col.tbl = self
542
560
  undo_col.col_type = undo_col.col_type.copy(nullable=True)
543
561
  self.next_col_id += 1
544
562
  return val_col, undo_col
@@ -553,7 +571,7 @@ class TableVersion:
553
571
  idx_name = f'idx{idx_id}'
554
572
  else:
555
573
  assert is_valid_identifier(idx_name)
556
- assert idx_name not in [i.name for i in self.idx_md.values()]
574
+ assert idx_name not in [i.name for i in self._tbl_md.index_md.values()]
557
575
  # create and register the index metadata
558
576
  idx_cls = type(idx)
559
577
  idx_md = schema.IndexMd(
@@ -569,7 +587,7 @@ class TableVersion:
569
587
  init_args=idx.as_dict(),
570
588
  )
571
589
  idx_info = self.IndexInfo(id=idx_id, name=idx_name, idx=idx, col=col, val_col=val_col, undo_col=undo_col)
572
- self.idx_md[idx_id] = idx_md
590
+ self._tbl_md.index_md[idx_id] = idx_md
573
591
  self.idxs_by_name[idx_name] = idx_info
574
592
  try:
575
593
  idx.create_index(self._store_idx_name(idx_id), val_col)
@@ -578,7 +596,7 @@ class TableVersion:
578
596
  def cleanup_index() -> None:
579
597
  """Delete the newly added in-memory index structure"""
580
598
  del self.idxs_by_name[idx_name]
581
- del self.idx_md[idx_id]
599
+ del self._tbl_md.index_md[idx_id]
582
600
  self.next_idx_id = idx_id
583
601
 
584
602
  # Run cleanup only if there has been an exception; otherwise, skip cleanup.
@@ -596,47 +614,48 @@ class TableVersion:
596
614
 
597
615
  def drop_index(self, idx_id: int) -> None:
598
616
  assert not self.is_snapshot
599
- assert idx_id in self.idx_md
617
+ assert idx_id in self._tbl_md.index_md
600
618
 
601
619
  # we're creating a new schema version
602
620
  self.version += 1
603
- preceding_schema_version = self.schema_version
621
+ self.preceding_schema_version = self.schema_version
604
622
  self.schema_version = self.version
605
- idx_md = self.idx_md[idx_id]
623
+ idx_md = self._tbl_md.index_md[idx_id]
606
624
  idx_md.schema_version_drop = self.schema_version
607
625
  assert idx_md.name in self.idxs_by_name
608
626
  idx_info = self.idxs_by_name[idx_md.name]
609
627
  # remove this index entry from the active indexes (in memory)
610
628
  # and the index metadata (in persistent table metadata)
629
+ # TODO: this is wrong, it breaks revert()
611
630
  del self.idxs_by_name[idx_md.name]
612
- del self.idx_md[idx_id]
631
+ del self._tbl_md.index_md[idx_id]
613
632
 
614
633
  self._drop_columns([idx_info.val_col, idx_info.undo_col])
615
- self._update_md(time.time(), preceding_schema_version=preceding_schema_version)
634
+ self._write_md(new_version=True, new_version_ts=time.time(), new_schema_version=True)
616
635
  _logger.info(f'Dropped index {idx_md.name} on table {self.name}')
617
636
 
618
637
  def add_columns(
619
638
  self, cols: Iterable[Column], print_stats: bool, on_error: Literal['abort', 'ignore']
620
639
  ) -> UpdateStatus:
621
- """Adds a column to the table."""
640
+ """Adds columns to the table."""
622
641
  assert not self.is_snapshot
623
- assert all(is_valid_identifier(col.name) for col in cols)
642
+ assert all(is_valid_identifier(col.name) for col in cols if col.name is not None)
624
643
  assert all(col.stored is not None for col in cols)
625
- assert all(col.name not in self.cols_by_name for col in cols)
644
+ assert all(col.name not in self.cols_by_name for col in cols if col.name is not None)
626
645
  for col in cols:
627
- col.tbl = self.create_handle()
646
+ col.tbl = self
628
647
  col.id = self.next_col_id
629
648
  self.next_col_id += 1
630
649
 
631
650
  # we're creating a new schema version
632
651
  self.version += 1
633
- preceding_schema_version = self.schema_version
652
+ self.preceding_schema_version = self.schema_version
634
653
  self.schema_version = self.version
635
654
  index_cols: dict[Column, tuple[index.BtreeIndex, Column, Column]] = {}
636
655
  all_cols: list[Column] = []
637
656
  for col in cols:
638
657
  all_cols.append(col)
639
- if self._is_btree_indexable(col):
658
+ if col.name is not None and self._is_btree_indexable(col):
640
659
  idx = index.BtreeIndex(col)
641
660
  val_col, undo_col = self._create_index_columns(idx)
642
661
  index_cols[col] = (idx, val_col, undo_col)
@@ -644,10 +663,10 @@ class TableVersion:
644
663
  all_cols.append(undo_col)
645
664
  # Add all columns
646
665
  status = self._add_columns(all_cols, print_stats=print_stats, on_error=on_error)
647
- # Create indices and their mds
666
+ # Create indices and their md records
648
667
  for col, (idx, val_col, undo_col) in index_cols.items():
649
668
  self._create_index(col, val_col, undo_col, idx_name=None, idx=idx)
650
- self._update_md(time.time(), preceding_schema_version=preceding_schema_version)
669
+ self._write_md(new_version=True, new_version_ts=time.time(), new_schema_version=True)
651
670
  _logger.info(f'Added columns {[col.name for col in cols]} to table {self.name}, new version: {self.version}')
652
671
 
653
672
  msg = (
@@ -685,6 +704,23 @@ class TableVersion:
685
704
  col.check_value_expr()
686
705
  self._record_refd_columns(col)
687
706
 
707
+ # also add to stored md
708
+ self._tbl_md.column_md[col.id] = schema.ColumnMd(
709
+ id=col.id,
710
+ col_type=col.col_type.as_dict(),
711
+ is_pk=col.is_pk,
712
+ schema_version_add=col.schema_version_add,
713
+ schema_version_drop=col.schema_version_drop,
714
+ value_expr=col.value_expr.as_dict() if col.value_expr is not None else None,
715
+ stored=col.stored,
716
+ )
717
+ if col.name is not None:
718
+ self._schema_version_md.columns[col.id] = schema.SchemaColumn(
719
+ name=col.name,
720
+ pos=len(self.cols_by_name),
721
+ media_validation=col._media_validation.name.lower() if col._media_validation is not None else None,
722
+ )
723
+
688
724
  if col.is_stored:
689
725
  self.store_tbl.add_column(col)
690
726
 
@@ -731,7 +767,7 @@ class TableVersion:
731
767
  num_rows=row_count,
732
768
  num_computed_values=row_count,
733
769
  num_excs=num_excs,
734
- cols_with_excs=[f'{col.tbl.get().name}.{col.name}' for col in cols_with_excs if col.name is not None],
770
+ cols_with_excs=[f'{col.tbl.name}.{col.name}' for col in cols_with_excs if col.name is not None],
735
771
  )
736
772
 
737
773
  def drop_column(self, col: Column) -> None:
@@ -741,7 +777,7 @@ class TableVersion:
741
777
 
742
778
  # we're creating a new schema version
743
779
  self.version += 1
744
- preceding_schema_version = self.schema_version
780
+ self.preceding_schema_version = self.schema_version
745
781
  self.schema_version = self.version
746
782
 
747
783
  # drop this column and all dependent index columns and indices
@@ -751,15 +787,17 @@ class TableVersion:
751
787
  if idx_info.col != col:
752
788
  continue
753
789
  dropped_cols.extend([idx_info.val_col, idx_info.undo_col])
754
- idx_md = self.idx_md[idx_info.id]
790
+ idx_md = self._tbl_md.index_md[idx_info.id]
755
791
  idx_md.schema_version_drop = self.schema_version
756
792
  assert idx_md.name in self.idxs_by_name
757
793
  dropped_idx_names.append(idx_md.name)
794
+
758
795
  # update idxs_by_name
759
796
  for idx_name in dropped_idx_names:
760
797
  del self.idxs_by_name[idx_name]
798
+
761
799
  self._drop_columns(dropped_cols)
762
- self._update_md(time.time(), preceding_schema_version=preceding_schema_version)
800
+ self._write_md(new_version=True, new_version_ts=time.time(), new_schema_version=True)
763
801
  _logger.info(f'Dropped column {col.name} from table {self.name}, new version: {self.version}')
764
802
 
765
803
  def _drop_columns(self, cols: Iterable[Column]) -> None:
@@ -780,6 +818,14 @@ class TableVersion:
780
818
  del self.cols_by_name[col.name]
781
819
  assert col.id in self.cols_by_id
782
820
  del self.cols_by_id[col.id]
821
+ # update stored md
822
+ self._tbl_md.column_md[col.id].schema_version_drop = col.schema_version_drop
823
+ if col.name is not None:
824
+ del self._schema_version_md.columns[col.id]
825
+
826
+ # update positions
827
+ for pos, schema_col in enumerate(self._schema_version_md.columns.values()):
828
+ schema_col.pos = pos
783
829
 
784
830
  self.store_tbl.create_sa_tbl()
785
831
 
@@ -796,13 +842,14 @@ class TableVersion:
796
842
  del self.cols_by_name[old_name]
797
843
  col.name = new_name
798
844
  self.cols_by_name[new_name] = col
845
+ self._schema_version_md.columns[col.id].name = new_name
799
846
 
800
847
  # we're creating a new schema version
801
848
  self.version += 1
802
- preceding_schema_version = self.schema_version
849
+ self.preceding_schema_version = self.schema_version
803
850
  self.schema_version = self.version
804
851
 
805
- self._update_md(time.time(), preceding_schema_version=preceding_schema_version)
852
+ self._write_md(new_version=True, new_version_ts=time.time(), new_schema_version=True)
806
853
  _logger.info(f'Renamed column {old_name} to {new_name} in table {self.name}, new version: {self.version}')
807
854
 
808
855
  def set_comment(self, new_comment: Optional[str]) -> None:
@@ -821,9 +868,9 @@ class TableVersion:
821
868
  def _create_schema_version(self) -> None:
822
869
  # we're creating a new schema version
823
870
  self.version += 1
824
- preceding_schema_version = self.schema_version
871
+ self.preceding_schema_version = self.schema_version
825
872
  self.schema_version = self.version
826
- self._update_md(time.time(), preceding_schema_version=preceding_schema_version)
873
+ self._write_md(new_version=True, new_version_ts=time.time(), new_schema_version=True)
827
874
  _logger.info(f'[{self.name}] Updating table schema to version: {self.version}')
828
875
 
829
876
  def insert(
@@ -838,7 +885,7 @@ class TableVersion:
838
885
  """
839
886
  from pixeltable.plan import Planner
840
887
 
841
- assert self.is_insertable()
888
+ assert self.is_insertable
842
889
  assert (rows is None) != (df is None) # Exactly one must be specified
843
890
  if rows is not None:
844
891
  plan = Planner.create_insert_plan(self, rows, ignore_errors=not fail_on_exception)
@@ -848,8 +895,8 @@ class TableVersion:
848
895
  # this is a base table; we generate rowids during the insert
849
896
  def rowids() -> Iterator[int]:
850
897
  while True:
851
- rowid = self.next_rowid
852
- self.next_rowid += 1
898
+ rowid = self.next_row_id
899
+ self.next_row_id += 1
853
900
  yield rowid
854
901
 
855
902
  return self._insert(plan, time.time(), print_stats=print_stats, rowids=rowids(), abort_on_exc=fail_on_exception)
@@ -874,7 +921,7 @@ class TableVersion:
874
921
  result.num_excs = num_excs
875
922
  result.num_computed_values += exec_plan.ctx.num_computed_exprs * num_rows
876
923
  result.cols_with_excs = [f'{self.name}.{self.cols_by_id[cid].name}' for cid in cols_with_excs]
877
- self._update_md(timestamp)
924
+ self._write_md(new_version=True, new_version_ts=timestamp, new_schema_version=False)
878
925
 
879
926
  # update views
880
927
  for view in self.mutable_views:
@@ -1038,13 +1085,13 @@ class TableVersion:
1038
1085
  self.store_tbl.delete_rows(
1039
1086
  self.version, base_versions=base_versions, match_on_vmin=True, where_clause=where_clause
1040
1087
  )
1041
- self._update_md(timestamp)
1088
+ self._write_md(new_version=True, new_version_ts=timestamp, new_schema_version=False)
1042
1089
 
1043
1090
  if cascade:
1044
1091
  base_versions = [None if plan is None else self.version, *base_versions] # don't update in place
1045
1092
  # propagate to views
1046
1093
  for view in self.mutable_views:
1047
- recomputed_cols = [col for col in recomputed_view_cols if col.tbl == view]
1094
+ recomputed_cols = [col for col in recomputed_view_cols if col.tbl.id == view.id]
1048
1095
  plan = None
1049
1096
  if len(recomputed_cols) > 0:
1050
1097
  from pixeltable.plan import Planner
@@ -1065,7 +1112,7 @@ class TableVersion:
1065
1112
  Args:
1066
1113
  where: a predicate to filter rows to delete.
1067
1114
  """
1068
- assert self.is_insertable()
1115
+ assert self.is_insertable
1069
1116
  from pixeltable.exprs import Expr
1070
1117
  from pixeltable.plan import Planner
1071
1118
 
@@ -1093,14 +1140,22 @@ class TableVersion:
1093
1140
  Returns:
1094
1141
  number of deleted rows
1095
1142
  """
1143
+ # print(f'calling sql_expr()')
1096
1144
  sql_where_clause = where.sql_expr(exprs.SqlElementCache()) if where is not None else None
1145
+ # #print(f'sql_where_clause={str(sql_where_clause) if sql_where_clause is not None else None}')
1146
+ # sql_cols: list[sql.Column] = []
1147
+ # def collect_cols(col) -> None:
1148
+ # sql_cols.append(col)
1149
+ # sql.sql.visitors.traverse(sql_where_clause, {}, {'column': collect_cols})
1150
+ # x = [f'{str(c)}:{hash(c)}:{id(c.table)}' for c in sql_cols]
1151
+ # print(f'where_clause cols: {x}')
1097
1152
  num_rows = self.store_tbl.delete_rows(
1098
1153
  self.version + 1, base_versions=base_versions, match_on_vmin=False, where_clause=sql_where_clause
1099
1154
  )
1100
1155
  if num_rows > 0:
1101
1156
  # we're creating a new version
1102
1157
  self.version += 1
1103
- self._update_md(timestamp)
1158
+ self._write_md(new_version=True, new_version_ts=timestamp, new_schema_version=False)
1104
1159
  for view in self.mutable_views:
1105
1160
  num_rows += view.get().propagate_delete(
1106
1161
  where=None, base_versions=[self.version, *base_versions], timestamp=timestamp
@@ -1114,17 +1169,13 @@ class TableVersion:
1114
1169
  raise excs.Error('Cannot revert version 0')
1115
1170
  self._revert()
1116
1171
 
1117
- def _delete_column(self, col: Column) -> None:
1118
- """Physically remove the column from the schema and the store table"""
1119
- if col.is_stored:
1120
- self.store_tbl.drop_column(col)
1121
- self.cols.remove(col)
1122
- if col.name is not None:
1123
- del self.cols_by_name[col.name]
1124
- del self.cols_by_id[col.id]
1125
-
1126
1172
  def _revert(self) -> None:
1127
- """Reverts this table version and propagates to views"""
1173
+ """
1174
+ Reverts the stored metadata for this table version and propagates to views.
1175
+
1176
+ Doesn't attempt to revert the in-memory metadata, but instead invalidates this TableVersion instance
1177
+ and relies on Catalog to reload it
1178
+ """
1128
1179
  conn = Env.get().conn
1129
1180
  # make sure we don't have a snapshot referencing this version
1130
1181
  # (unclear how to express this with sqlalchemy)
@@ -1157,109 +1208,206 @@ class TableVersion:
1157
1208
  stmt = sql.update(self.store_tbl.sa_tbl).values(set_clause).where(self.store_tbl.sa_tbl.c.v_max == self.version)
1158
1209
  conn.execute(stmt)
1159
1210
 
1160
- # revert schema changes
1211
+ # revert schema changes:
1212
+ # - undo changes to self._tbl_md and write that back
1213
+ # - delete newly-added TableVersion/TableSchemaVersion records
1161
1214
  if self.version == self.schema_version:
1162
- # delete newly-added columns
1215
+ # physically delete newly-added columns and remove them from the stored md
1163
1216
  added_cols = [col for col in self.cols if col.schema_version_add == self.schema_version]
1164
1217
  if len(added_cols) > 0:
1165
- next_col_id = min(col.id for col in added_cols)
1218
+ self._tbl_md.next_col_id = min(col.id for col in added_cols)
1166
1219
  for col in added_cols:
1167
- self._delete_column(col)
1168
- self.next_col_id = next_col_id
1220
+ if col.is_stored:
1221
+ self.store_tbl.drop_column(col)
1222
+ del self._tbl_md.column_md[col.id]
1169
1223
 
1170
1224
  # remove newly-added indices from the lookup structures
1171
1225
  # (the value and undo columns got removed in the preceding step)
1172
- added_idx_md = [md for md in self.idx_md.values() if md.schema_version_add == self.schema_version]
1226
+ added_idx_md = [md for md in self._tbl_md.index_md.values() if md.schema_version_add == self.schema_version]
1173
1227
  if len(added_idx_md) > 0:
1174
- next_idx_id = min(md.id for md in added_idx_md)
1228
+ self._tbl_md.next_idx_id = min(md.id for md in added_idx_md)
1175
1229
  for md in added_idx_md:
1176
- del self.idx_md[md.id]
1177
- del self.idxs_by_name[md.name]
1178
- self.next_idx_id = next_idx_id
1230
+ # TODO: drop the index
1231
+ del self._tbl_md.index_md[md.id]
1179
1232
 
1180
1233
  # make newly-dropped columns visible again
1181
- dropped_cols = [col for col in self.cols if col.schema_version_drop == self.schema_version]
1182
- for col in dropped_cols:
1183
- col.schema_version_drop = None
1234
+ dropped_col_md = [
1235
+ md for md in self._tbl_md.column_md.values() if md.schema_version_drop == self.schema_version
1236
+ ]
1237
+ for col_md in dropped_col_md:
1238
+ col_md.schema_version_drop = None
1184
1239
 
1185
1240
  # make newly-dropped indices visible again
1186
- dropped_idx_md = [md for md in self.idx_md.values() if md.schema_version_drop == self.schema_version]
1187
- for md in dropped_idx_md:
1188
- md.schema_version_drop = None
1189
-
1190
- session = Env.get().session
1191
- # we need to determine the preceding schema version and reload the schema
1192
- schema_version_md_dict = (
1193
- session.query(schema.TableSchemaVersion.md)
1194
- .where(schema.TableSchemaVersion.tbl_id == self.id)
1195
- .where(schema.TableSchemaVersion.schema_version == self.schema_version)
1196
- .scalar()
1197
- )
1198
- preceding_schema_version = schema_version_md_dict['preceding_schema_version']
1199
- preceding_schema_version_md_dict = (
1200
- session.query(schema.TableSchemaVersion.md)
1201
- .where(schema.TableSchemaVersion.tbl_id == self.id)
1202
- .where(schema.TableSchemaVersion.schema_version == preceding_schema_version)
1203
- .scalar()
1204
- )
1205
- preceding_schema_version_md = schema.md_from_dict(
1206
- schema.TableSchemaVersionMd, preceding_schema_version_md_dict
1207
- )
1208
- tbl_md = self._create_tbl_md()
1209
- self._init_schema(tbl_md, preceding_schema_version_md)
1241
+ dropped_idx_md = [
1242
+ md for md in self._tbl_md.index_md.values() if md.schema_version_drop == self.schema_version
1243
+ ]
1244
+ for idx_md in dropped_idx_md:
1245
+ idx_md.schema_version_drop = None
1210
1246
 
1211
1247
  conn.execute(
1212
1248
  sql.delete(schema.TableSchemaVersion.__table__)
1213
1249
  .where(schema.TableSchemaVersion.tbl_id == self.id)
1214
1250
  .where(schema.TableSchemaVersion.schema_version == self.schema_version)
1215
1251
  )
1216
- self.schema_version = preceding_schema_version
1217
- self.comment = preceding_schema_version_md.comment
1218
- self.num_retained_versions = preceding_schema_version_md.num_retained_versions
1252
+ self._tbl_md.current_schema_version = self._schema_version_md.preceding_schema_version
1219
1253
 
1220
1254
  conn.execute(
1221
1255
  sql.delete(schema.TableVersion.__table__)
1222
1256
  .where(schema.TableVersion.tbl_id == self.id)
1223
1257
  .where(schema.TableVersion.version == self.version)
1224
1258
  )
1259
+
1225
1260
  self.version -= 1
1226
- conn.execute(
1227
- sql.update(schema.Table.__table__)
1228
- .values({schema.Table.md: dataclasses.asdict(self._create_tbl_md())})
1229
- .where(schema.Table.id == self.id)
1230
- )
1261
+ self._write_md(new_version=False, new_version_ts=0, new_schema_version=False)
1231
1262
 
1232
1263
  # propagate to views
1264
+ views_str = ', '.join([str(v.id) for v in self.mutable_views])
1265
+ print(f'revert(): mutable_views={views_str}')
1233
1266
  for view in self.mutable_views:
1234
1267
  view.get()._revert()
1268
+
1269
+ # force reload on next operation
1270
+ self.is_validated = False
1271
+ pxt.catalog.Catalog.get().remove_tbl_version(self)
1235
1272
  _logger.info(f'TableVersion {self.name}: reverted to version {self.version}')
1236
1273
 
1237
- def _init_external_stores(self, tbl_md: schema.TableMd) -> None:
1238
- for store_md in tbl_md.external_stores:
1274
+ def _init_external_stores(self) -> None:
1275
+ for store_md in self.tbl_md.external_stores:
1239
1276
  store_cls = resolve_symbol(store_md['class'])
1240
1277
  assert isinstance(store_cls, type) and issubclass(store_cls, pxt.io.ExternalStore)
1241
1278
  store = store_cls.from_dict(store_md['md'])
1242
1279
  self.external_stores[store.name] = store
1243
1280
 
1244
1281
  def link_external_store(self, store: pxt.io.ExternalStore) -> None:
1245
- store.link(self) # May result in additional metadata changes
1282
+ self.version += 1
1283
+ self.preceding_schema_version = self.schema_version
1284
+ self.schema_version = self.version
1285
+
1246
1286
  self.external_stores[store.name] = store
1247
- self._update_md(time.time(), update_tbl_version=False)
1287
+ self._tbl_md.external_stores.append(
1288
+ {'class': f'{type(store).__module__}.{type(store).__qualname__}', 'md': store.as_dict()}
1289
+ )
1290
+ self._write_md(new_version=True, new_version_ts=time.time(), new_schema_version=True)
1291
+
1292
+ def unlink_external_store(self, store: pxt.io.ExternalStore) -> None:
1293
+ del self.external_stores[store.name]
1294
+ self.version += 1
1295
+ self.preceding_schema_version = self.schema_version
1296
+ self.schema_version = self.version
1297
+ idx = next(i for i, store_md in enumerate(self._tbl_md.external_stores) if store_md['md']['name'] == store.name)
1298
+ self._tbl_md.external_stores.pop(idx)
1299
+ self._write_md(new_version=True, new_version_ts=time.time(), new_schema_version=True)
1300
+
1301
+ @property
1302
+ def tbl_md(self) -> schema.TableMd:
1303
+ return self._tbl_md
1304
+
1305
+ @property
1306
+ def schema_version_md(self) -> schema.TableSchemaVersionMd:
1307
+ return self._schema_version_md
1308
+
1309
+ @property
1310
+ def view_md(self) -> Optional[schema.ViewMd]:
1311
+ return self._tbl_md.view_md
1312
+
1313
+ @property
1314
+ def name(self) -> str:
1315
+ return self._tbl_md.name
1316
+
1317
+ @property
1318
+ def user(self) -> Optional[str]:
1319
+ return self._tbl_md.user
1320
+
1321
+ @property
1322
+ def is_replica(self) -> bool:
1323
+ return self._tbl_md.is_replica
1324
+
1325
+ @property
1326
+ def comment(self) -> str:
1327
+ return self._schema_version_md.comment
1328
+
1329
+ @comment.setter
1330
+ def comment(self, c: str) -> None:
1331
+ assert self.effective_version is None
1332
+ self._schema_version_md.comment = c
1333
+
1334
+ @property
1335
+ def num_retained_versions(self) -> int:
1336
+ return self._schema_version_md.num_retained_versions
1337
+
1338
+ @num_retained_versions.setter
1339
+ def num_retained_versions(self, n: int) -> None:
1340
+ assert self.effective_version is None
1341
+ self._schema_version_md.num_retained_versions = n
1342
+
1343
+ @property
1344
+ def version(self) -> int:
1345
+ # if this is a snapshot instance, we need to ignore current_version
1346
+ return self._tbl_md.current_version if self.effective_version is None else self.effective_version
1347
+
1348
+ @version.setter
1349
+ def version(self, version: int) -> None:
1350
+ assert self.effective_version is None
1351
+ self._tbl_md.current_version = version
1352
+
1353
+ @property
1354
+ def schema_version(self) -> int:
1355
+ return self._schema_version_md.schema_version
1248
1356
 
1249
- def unlink_external_store(self, store_name: str, delete_external_data: bool) -> None:
1250
- assert store_name in self.external_stores
1251
- store = self.external_stores[store_name]
1252
- store.unlink(self) # May result in additional metadata changes
1253
- del self.external_stores[store_name]
1254
- self._update_md(time.time(), update_tbl_version=False)
1357
+ @schema_version.setter
1358
+ def schema_version(self, version: int) -> None:
1359
+ assert self.effective_version is None
1360
+ self._tbl_md.current_schema_version = version
1361
+ self._schema_version_md.schema_version = version
1362
+
1363
+ @property
1364
+ def preceding_schema_version(self) -> int:
1365
+ return self._schema_version_md.preceding_schema_version
1366
+
1367
+ @preceding_schema_version.setter
1368
+ def preceding_schema_version(self, v: int) -> None:
1369
+ assert self.effective_version is None
1370
+ self._schema_version_md.preceding_schema_version = v
1371
+
1372
+ @property
1373
+ def media_validation(self) -> MediaValidation:
1374
+ return MediaValidation[self._schema_version_md.media_validation.upper()]
1375
+
1376
+ @property
1377
+ def next_col_id(self) -> int:
1378
+ return self._tbl_md.next_col_id
1255
1379
 
1256
- if delete_external_data and isinstance(store, pxt.io.external_store.Project):
1257
- store.delete()
1380
+ @next_col_id.setter
1381
+ def next_col_id(self, id: int) -> None:
1382
+ assert self.effective_version is None
1383
+ self._tbl_md.next_col_id = id
1384
+
1385
+ @property
1386
+ def next_idx_id(self) -> int:
1387
+ return self._tbl_md.next_idx_id
1388
+
1389
+ @next_idx_id.setter
1390
+ def next_idx_id(self, id: int) -> None:
1391
+ assert self.effective_version is None
1392
+ self._tbl_md.next_idx_id = id
1393
+
1394
+ @property
1395
+ def next_row_id(self) -> int:
1396
+ return self._tbl_md.next_row_id
1397
+
1398
+ @next_row_id.setter
1399
+ def next_row_id(self, id: int) -> None:
1400
+ assert self.effective_version is None
1401
+ self._tbl_md.next_row_id = id
1258
1402
 
1259
1403
  @property
1260
1404
  def is_snapshot(self) -> bool:
1261
1405
  return self.effective_version is not None
1262
1406
 
1407
+ @property
1408
+ def is_mutable(self) -> bool:
1409
+ return not self.is_snapshot and not self.is_replica
1410
+
1263
1411
  @property
1264
1412
  def is_view(self) -> bool:
1265
1413
  return self.view_md is not None
@@ -1272,6 +1420,7 @@ class TableVersion:
1272
1420
  def is_component_view(self) -> bool:
1273
1421
  return self.iterator_cls is not None
1274
1422
 
1423
+ @property
1275
1424
  def is_insertable(self) -> bool:
1276
1425
  """Returns True if this corresponds to an InsertableTable"""
1277
1426
  return not self.is_snapshot and not self.is_view
@@ -1363,24 +1512,6 @@ class TableVersion:
1363
1512
  {'class': f'{type(store).__module__}.{type(store).__qualname__}', 'md': store.as_dict()} for store in stores
1364
1513
  ]
1365
1514
 
1366
- def _create_tbl_md(self) -> schema.TableMd:
1367
- return schema.TableMd(
1368
- tbl_id=str(self.id),
1369
- name=self.name,
1370
- user=self.user,
1371
- is_replica=self.is_replica,
1372
- current_version=self.version,
1373
- current_schema_version=self.schema_version,
1374
- next_col_id=self.next_col_id,
1375
- next_idx_id=self.next_idx_id,
1376
- next_row_id=self.next_rowid,
1377
- column_md=self._create_column_md(self.cols),
1378
- index_md=self.idx_md,
1379
- external_stores=self._create_stores_md(self.external_stores.values()),
1380
- view_md=self.view_md,
1381
- additional_md={},
1382
- )
1383
-
1384
1515
  def _create_version_md(self, timestamp: float) -> schema.TableVersionMd:
1385
1516
  return schema.TableVersionMd(
1386
1517
  tbl_id=str(self.id),