vgi-python 0.26.0__py3-none-any.whl → 0.28.0__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.
@@ -31,7 +31,12 @@ from vgi_rpc import ArrowSerializableDataclass
31
31
  from vgi_rpc.rpc import CallContext, OutputCollector
32
32
  from vgi_rpc.utils import deserialize_record_batch, serialize_record_batch_bytes
33
33
 
34
- from vgi.catalog.attach_option import AttachOption, AttachOptionSpec, extract_attach_option_specs
34
+ from vgi.catalog.attach_option import (
35
+ AttachOption,
36
+ AttachOptionSpec,
37
+ extract_attach_option_specs,
38
+ validate_required_attach_options,
39
+ )
35
40
  from vgi.catalog.catalog_interface import (
36
41
  AttachOpaqueData,
37
42
  CatalogAttachResult,
@@ -53,6 +58,10 @@ __all__ = [
53
58
 
54
59
 
55
60
  CATALOG_NAME = "attach_options"
61
+ # A second catalog from the same worker, declaring one option the caller must
62
+ # supply. Kept separate from CATALOG_NAME so the defaults-and-round-trip
63
+ # coverage above can keep attaching with no options at all.
64
+ REQUIRED_CATALOG_NAME = "attach_options_required"
56
65
  _ATTACH_ID_SEP = b"\x00"
57
66
  _UUID_BYTES = 16
58
67
 
@@ -109,9 +118,19 @@ class AttachOptions:
109
118
  ] = {"a": 1, "b": "x"}
110
119
 
111
120
 
121
+ class RequiredAttachOptions:
122
+ """Attach-time options for the catalog that refuses an anonymous ATTACH."""
123
+
124
+ # No class-level assignment: there is no default to fall back on, so the
125
+ # caller has to supply it.
126
+ api_key: Annotated[str, AttachOption(desc="API key", required=True)]
127
+ region: Annotated[str, AttachOption(desc="Region")] = "us-east-1"
128
+
129
+
112
130
  # Resolve once at import time; used both to build the echo function's output schema
113
131
  # and to backfill defaults in catalog_attach.
114
132
  _ATTACH_OPTION_SPECS: list[AttachOptionSpec] = extract_attach_option_specs(AttachOptions)
133
+ _REQUIRED_OPTION_SPECS: list[AttachOptionSpec] = extract_attach_option_specs(RequiredAttachOptions)
115
134
 
116
135
  _ECHO_SCHEMA: pa.Schema = schema({spec.name: spec.type for spec in _ATTACH_OPTION_SPECS})
117
136
 
@@ -244,8 +263,14 @@ class AttachOptionsCatalog(ReadOnlyCatalogInterface):
244
263
  ctx: CallContext | None = None,
245
264
  ) -> CatalogAttachResult:
246
265
  del data_version_spec, implementation_version, ctx
247
- if name != CATALOG_NAME:
248
- raise ValueError(f"Unknown catalog: {name!r}. Available: {CATALOG_NAME}")
266
+ if name not in (CATALOG_NAME, REQUIRED_CATALOG_NAME):
267
+ raise ValueError(f"Unknown catalog: {name!r}. Available: {CATALOG_NAME}, {REQUIRED_CATALOG_NAME}")
268
+
269
+ if name == REQUIRED_CATALOG_NAME:
270
+ # This interface overrides catalog_attach, so the base class's check
271
+ # never runs — an overrider that declares required options calls the
272
+ # shared validator itself.
273
+ validate_required_attach_options(name, _REQUIRED_OPTION_SPECS, options)
249
274
 
250
275
  attach_opaque_data = _encode_attach_opaque_data(options)
251
276
 
@@ -270,6 +295,12 @@ class AttachOptionsCatalog(ReadOnlyCatalogInterface):
270
295
  data_version_spec=None,
271
296
  attach_option_specs=[spec.serialize() for spec in _ATTACH_OPTION_SPECS],
272
297
  ),
298
+ CatalogInfo(
299
+ name=REQUIRED_CATALOG_NAME,
300
+ implementation_version=None,
301
+ data_version_spec=None,
302
+ attach_option_specs=[spec.serialize() for spec in _REQUIRED_OPTION_SPECS],
303
+ ),
273
304
  ]
274
305
 
275
306
  def catalog_version(
@@ -412,25 +412,16 @@ def main() -> None:
412
412
  mw._exempt_prefixes = (*mw._exempt_prefixes, f"{prefix}/__blobs__")
413
413
 
414
414
  if describe:
415
- # Standardized landing surface (shared static page + JSON contract),
416
- # describing the reference ExampleWorker catalog.
417
- from vgi.http.landing_page import (
418
- ColumnsResource,
419
- DescribeJsonResource,
420
- LandingPageResource,
421
- )
415
+ # Standardized landing surface: the shared static page plus the
416
+ # browser client build it reads the catalog with.
417
+ from vgi.http.landing_page import ClientBundleResource, LandingPageResource
422
418
 
423
419
  oauth_active = oauth_metadata is not None and getattr(oauth_metadata, "client_id", None) is not None
424
420
  server_id = getattr(server, "server_id", "")
425
- wsgi_app.add_route(prefix or "/", LandingPageResource(server_id=server_id))
426
- wsgi_app.add_route(
427
- f"{prefix}/describe.json",
428
- DescribeJsonResource(ExampleWorker, oauth=oauth_active, server_id=server_id),
429
- )
430
421
  wsgi_app.add_route(
431
- f"{prefix}/describe/{{catalog}}/{{schema}}/{{table}}.json",
432
- ColumnsResource(ExampleWorker),
422
+ prefix or "/", LandingPageResource(ExampleWorker, server_id=server_id, oauth=oauth_active)
433
423
  )
424
+ wsgi_app.add_route(f"{prefix}/vgi-client.js", ClientBundleResource())
434
425
 
435
426
  # vgi_rpc's make_wsgi_app already advertises VGI-Max-Request-Bytes and
436
427
  # installs a Falcon middleware enforcing it with a structured 413 that
vgi/catalog/__init__.py CHANGED
@@ -16,6 +16,7 @@ from vgi.catalog.catalog_interface import (
16
16
  CatalogInterface,
17
17
  CatalogObject,
18
18
  CatalogSchemaObject,
19
+ ColumnStatistics,
19
20
  CopyFromFormatInfo,
20
21
  FunctionInfo,
21
22
  FunctionType,
@@ -35,6 +36,8 @@ from vgi.catalog.catalog_interface import (
35
36
  TableInfo,
36
37
  TransactionOpaqueData,
37
38
  ViewInfo,
39
+ deserialize_column_statistics,
40
+ serialize_column_statistics,
38
41
  )
39
42
  from vgi.catalog.descriptors import Catalog, ForeignKeyDef, Index, Macro, Schema, Sql, Table, View
40
43
  from vgi.catalog.secret_type import SecretTypeSpec
@@ -60,6 +63,7 @@ __all__ = [
60
63
  "CatalogInfo",
61
64
  "CatalogObject",
62
65
  "CatalogSchemaObject",
66
+ "ColumnStatistics",
63
67
  "IndexInfo",
64
68
  "MacroInfo",
65
69
  "SchemaInfo",
@@ -89,4 +93,7 @@ __all__ = [
89
93
  # Storage
90
94
  "CatalogStorage",
91
95
  "CatalogStorageSqlite",
96
+ # Column statistics wire format
97
+ "serialize_column_statistics",
98
+ "deserialize_column_statistics",
92
99
  ]
@@ -5,9 +5,16 @@
5
5
  [`Setting`][vgi.catalog.setting.Setting] (session-level, resent every call) and
6
6
  [`AttachOption`][vgi.catalog.attach_option.AttachOption] (delivered once at
7
7
  ``catalog_attach``) are declared the same way — an `Annotated`-hint descriptor
8
- plus a serializable spec with an identical Arrow IPC wire format. This module
8
+ plus a serializable spec sharing the same four Arrow IPC columns. This module
9
9
  holds the machinery both share; the two public modules subclass it so their
10
- names, wire format, and behaviour are unchanged.
10
+ names and behaviour are unchanged.
11
+
12
+ A subclass may widen its wire format with additional columns — `AttachOption`
13
+ adds ``required`` — via :meth:`_SpecBase._extra_row` /
14
+ :meth:`_SpecBase._extra_kwargs` and :meth:`_DescriptorBase.extra_spec_kwargs`.
15
+ Because both directions read by column name, the widening is compatible in both
16
+ directions: a reader ignores columns it doesn't know, and a reader whose peer
17
+ never wrote the column falls back to the field default.
11
18
  """
12
19
 
13
20
  from __future__ import annotations
@@ -97,6 +104,24 @@ class _SpecBase:
97
104
  ] # type: ignore[arg-type]
98
105
  )
99
106
 
107
+ def _extra_row(self) -> dict[str, Any]:
108
+ """Extra wire columns this spec contributes beyond the shared four.
109
+
110
+ Subclasses that widen :data:`ARROW_SCHEMA` return the matching values
111
+ here. Keys must exist in the subclass's schema.
112
+ """
113
+ return {}
114
+
115
+ @classmethod
116
+ def _extra_kwargs(cls, row: dict[str, object]) -> dict[str, Any]:
117
+ """Constructor kwargs read from the extra wire columns.
118
+
119
+ ``row`` is built from the *batch's* schema, so a column a peer never
120
+ wrote is simply absent — read with ``.get()`` and supply the default.
121
+ """
122
+ del row
123
+ return {}
124
+
100
125
  def serialize(self) -> bytes:
101
126
  """Serialize to Arrow IPC bytes."""
102
127
  # Serialize type as a single-field schema
@@ -116,6 +141,7 @@ class _SpecBase:
116
141
  "description": self.desc,
117
142
  "type": type_bytes,
118
143
  "default_value": default_bytes,
144
+ **self._extra_row(),
119
145
  }
120
146
  ],
121
147
  schema=self.ARROW_SCHEMA,
@@ -147,6 +173,7 @@ class _SpecBase:
147
173
  desc=cast(str, row["description"]),
148
174
  type=data_type,
149
175
  default=default,
176
+ **cls._extra_kwargs(row),
150
177
  )
151
178
 
152
179
 
@@ -179,6 +206,15 @@ class _DescriptorBase:
179
206
  return self
180
207
  return getattr(type(obj), self._name, None)
181
208
 
209
+ def extra_spec_kwargs(self) -> dict[str, Any]:
210
+ """Extra keyword arguments this descriptor passes to its spec factory.
211
+
212
+ The base declaration carries only ``name``/``desc``/``type``/``default``;
213
+ a subclass adding a field returns it here so ``_extract_specs`` stays
214
+ shared.
215
+ """
216
+ return {}
217
+
182
218
 
183
219
  def _extract_specs[D: _DescriptorBase, S: _SpecBase](
184
220
  declaring_cls: type,
@@ -240,6 +276,7 @@ def _extract_specs[D: _DescriptorBase, S: _SpecBase](
240
276
  desc=descriptor.desc,
241
277
  type=arrow_type,
242
278
  default=default,
279
+ **descriptor.extra_spec_kwargs(),
243
280
  )
244
281
  )
245
282
 
@@ -12,13 +12,18 @@ the machinery in ``vgi.catalog._descriptor_spec``.
12
12
  """
13
13
 
14
14
  from dataclasses import dataclass
15
+ from typing import Any, ClassVar
16
+
17
+ import pyarrow as pa
15
18
 
16
19
  from vgi.catalog._descriptor_spec import _DescriptorBase, _extract_specs, _SpecBase
17
20
 
18
21
  __all__ = [
19
22
  "AttachOption",
20
23
  "AttachOptionSpec",
24
+ "MissingAttachOptionsError",
21
25
  "extract_attach_option_specs",
26
+ "validate_required_attach_options",
22
27
  ]
23
28
 
24
29
 
@@ -27,10 +32,48 @@ class AttachOptionSpec(_SpecBase):
27
32
  """Extracted attach-option metadata for catalog discovery serialization.
28
33
 
29
34
  The resolved form of an `AttachOption`. See
30
- `_SpecBase` (in `vgi.catalog._descriptor_spec`) for the field and
31
- wire-format definition.
35
+ `_SpecBase` (in `vgi.catalog._descriptor_spec`) for the shared fields and
36
+ wire format.
37
+
38
+ Attributes:
39
+ required: The caller must supply this option at ``ATTACH`` time. A
40
+ catalog that cannot be attached without it advertises that fact at
41
+ discovery, so a client can say so before attempting the attach
42
+ rather than surfacing a failure that reads like an empty catalog.
43
+ Mutually exclusive with ``default`` — an option that falls back to
44
+ a value is by definition satisfiable without the caller.
45
+ ARROW_SCHEMA: The shared four columns plus ``required``.
46
+
32
47
  """
33
48
 
49
+ required: bool = False
50
+
51
+ ARROW_SCHEMA: ClassVar[pa.Schema] = pa.schema(
52
+ [
53
+ *_SpecBase.ARROW_SCHEMA,
54
+ # Nullable, and appended last: a peer that predates this column
55
+ # reads the batch by name and simply doesn't see it.
56
+ pa.field("required", pa.bool_(), nullable=True),
57
+ ]
58
+ )
59
+
60
+ def __post_init__(self) -> None:
61
+ """Reject the contradictory ``required`` + ``default`` combination."""
62
+ if self.required and self.default is not None:
63
+ raise ValueError(
64
+ f"Attach option {self.name!r} is required but also declares a default "
65
+ f"({self.default!r}); an option with a default is always satisfiable "
66
+ "without the caller. Drop one."
67
+ )
68
+
69
+ def _extra_row(self) -> dict[str, Any]:
70
+ return {"required": self.required}
71
+
72
+ @classmethod
73
+ def _extra_kwargs(cls, row: dict[str, object]) -> dict[str, Any]:
74
+ # Absent column (older peer) and explicit null both mean "not required".
75
+ return {"required": bool(row.get("required") or False)}
76
+
34
77
 
35
78
  @dataclass
36
79
  class AttachOption(_DescriptorBase):
@@ -41,7 +84,72 @@ class AttachOption(_DescriptorBase):
41
84
  in the Annotated hint. See
42
85
  `_DescriptorBase` (in `vgi.catalog._descriptor_spec`) for the
43
86
  ``desc`` and ``arrow_type`` attributes.
87
+
88
+ Declare a required option by omitting the class-level assignment that would
89
+ otherwise supply its default::
90
+
91
+ class AttachOptions:
92
+ region: Annotated[str, AttachOption(desc="AWS region")] = "us-east-1"
93
+ api_key: Annotated[str, AttachOption(desc="API key", required=True)]
94
+
95
+ Attributes:
96
+ required: The caller must supply this option at ``ATTACH`` time.
97
+
98
+ """
99
+
100
+ required: bool = False
101
+
102
+ def extra_spec_kwargs(self) -> dict[str, Any]:
103
+ """Carry ``required`` through to the `AttachOptionSpec`."""
104
+ return {"required": self.required}
105
+
106
+
107
+ class MissingAttachOptionsError(ValueError):
108
+ """Raised when ``catalog_attach`` omits options declared ``required``.
109
+
110
+ Carries the option names in :attr:`missing` so a caller can act on them
111
+ without parsing the message.
112
+
113
+ Attributes:
114
+ missing: Names of the required options that were not supplied.
115
+
116
+ """
117
+
118
+ missing: list[str]
119
+
120
+ def __init__(self, catalog_name: str, missing: list[str]) -> None:
121
+ """Build the error from the catalog name and the missing option names."""
122
+ self.missing = missing
123
+ joined = ", ".join(repr(name) for name in missing)
124
+ super().__init__(
125
+ f"Catalog {catalog_name!r} cannot be attached without the required "
126
+ f"option{'s' if len(missing) > 1 else ''} {joined}."
127
+ )
128
+
129
+
130
+ def validate_required_attach_options(
131
+ catalog_name: str,
132
+ specs: list[AttachOptionSpec],
133
+ options: dict[str, Any],
134
+ ) -> None:
135
+ """Raise if any ``required`` spec has no corresponding entry in ``options``.
136
+
137
+ Option names are matched case-insensitively, mirroring DuckDB's handling of
138
+ ``ATTACH`` option keys.
139
+
140
+ Args:
141
+ catalog_name: Catalog being attached, for the error message.
142
+ specs: The attach-option specs the catalog declares.
143
+ options: Options the caller supplied at ``ATTACH`` time.
144
+
145
+ Raises:
146
+ MissingAttachOptionsError: If a required option was not supplied.
147
+
44
148
  """
149
+ supplied = {key.lower() for key in options}
150
+ missing = [spec.name for spec in specs if spec.required and spec.name.lower() not in supplied]
151
+ if missing:
152
+ raise MissingAttachOptionsError(catalog_name, missing)
45
153
 
46
154
 
47
155
  def extract_attach_option_specs(options_cls: type) -> list[AttachOptionSpec]:
@@ -1414,6 +1414,71 @@ def serialize_column_statistics(
1414
1414
  return serialize_record_batch_bytes(batch, custom_metadata=custom_metadata)
1415
1415
 
1416
1416
 
1417
+ def _union_child_scalar(scalar: pa.Scalar | None) -> pa.Scalar | None: # type: ignore[type-arg]
1418
+ """Unwrap the typed child scalar from a sparse-union ``min``/``max`` cell.
1419
+
1420
+ A union cell is valid but wraps a null child whenever that row's statistic was
1421
+ unknown, so both levels have to be checked.
1422
+
1423
+ Args:
1424
+ scalar: The union cell to unwrap, or None.
1425
+
1426
+ Returns:
1427
+ The typed child scalar, or None when the cell or its child is null.
1428
+ """
1429
+ if scalar is None or not scalar.is_valid:
1430
+ return None
1431
+ value = getattr(scalar, "value", None)
1432
+ if value is None or not value.is_valid:
1433
+ return None
1434
+ return value # type: ignore[no-any-return]
1435
+
1436
+
1437
+ def deserialize_column_statistics(data: bytes) -> list[ColumnStatistics]:
1438
+ """Deserialize column statistics — the inverse of [`serialize_column_statistics`][].
1439
+
1440
+ Reads the sparse-union wire batch back into typed [`ColumnStatistics`][], so a client
1441
+ can consume the bytes returned by ``catalog_table_column_statistics_get`` without
1442
+ re-implementing the union layout. See
1443
+ [`Client.table_column_statistics`][vgi.client.CatalogClientMixin.table_column_statistics] for the
1444
+ wrapper that fetches and decodes in one call.
1445
+
1446
+ The optional ``cache_max_age_seconds`` travels in the IPC batch's custom metadata
1447
+ rather than in the statistics themselves; read it with ``deserialize_record_batch``
1448
+ if you need it.
1449
+
1450
+ Args:
1451
+ data: IPC-serialized bytes of a statistics RecordBatch.
1452
+
1453
+ Returns:
1454
+ Per-column statistics, in the order the worker serialized them. An empty list
1455
+ when the batch carries no rows.
1456
+ """
1457
+ batch, _ = deserialize_record_batch(data)
1458
+ names = batch.column("column_name")
1459
+ mins = batch.column("min")
1460
+ maxs = batch.column("max")
1461
+ has_null = batch.column("has_null")
1462
+ has_not_null = batch.column("has_not_null")
1463
+ distinct = batch.column("distinct_count")
1464
+ unicode_col = batch.column("contains_unicode")
1465
+ max_len = batch.column("max_string_length")
1466
+
1467
+ return [
1468
+ ColumnStatistics(
1469
+ column_name=names[i].as_py(),
1470
+ min=_union_child_scalar(mins[i]),
1471
+ max=_union_child_scalar(maxs[i]),
1472
+ has_null=bool(has_null[i].as_py()),
1473
+ has_not_null=bool(has_not_null[i].as_py()),
1474
+ distinct_count=distinct[i].as_py(),
1475
+ contains_unicode=unicode_col[i].as_py(),
1476
+ max_string_length=max_len[i].as_py(),
1477
+ )
1478
+ for i in range(batch.num_rows)
1479
+ ]
1480
+
1481
+
1417
1482
  class CatalogInterface(ABC):
1418
1483
  """Provides an interface to manage catalogs, schemas, tables, and views for VGI.
1419
1484
 
@@ -2591,10 +2656,13 @@ class ReadOnlyCatalogInterface(CatalogInterface):
2591
2656
  ctx: "CallContext | None" = None,
2592
2657
  ) -> CatalogAttachResult:
2593
2658
  """Attach to the catalog. Version constraints are ignored by default."""
2659
+ from vgi.catalog.attach_option import validate_required_attach_options
2660
+
2594
2661
  del data_version_spec, implementation_version, ctx
2595
2662
  effective_name = self._effective_catalog_name
2596
2663
  if name != effective_name:
2597
2664
  raise ValueError(f"Unknown catalog: {name!r}. Available: {effective_name}")
2665
+ validate_required_attach_options(effective_name, self.attach_option_specs, options)
2598
2666
 
2599
2667
  # Serialize settings and secret types for the attach result
2600
2668
  serialized_settings = [s.serialize() for s in self.settings]
@@ -53,6 +53,7 @@ from vgi.catalog import (
53
53
  AttachOpaqueData,
54
54
  CatalogAttachResult,
55
55
  CatalogInfo,
56
+ ColumnStatistics,
56
57
  FunctionInfo,
57
58
  MacroInfo,
58
59
  MacroType,
@@ -66,6 +67,7 @@ from vgi.catalog import (
66
67
  TransactionOpaqueData,
67
68
  ViewInfo,
68
69
  )
70
+ from vgi.catalog.catalog_interface import deserialize_column_statistics
69
71
  from vgi.protocol import (
70
72
  CatalogAttachRequest,
71
73
  CatalogCreateRequest,
@@ -100,17 +102,30 @@ class CatalogClientMixin:
100
102
  factory).
101
103
 
102
104
  Attributes:
103
- server_path: Worker shell command used for subprocess transport.
105
+ server_path: Worker command used for subprocess transport — a string to
106
+ be split, or an argv sequence taken as-is.
104
107
  """
105
108
 
106
109
  # Type hints for attributes expected from Client
107
- server_path: str
110
+ server_path: str | Sequence[str]
108
111
  _transport: Literal["subprocess", "http", "tcp"]
109
112
  _base_url: str | None
110
113
  _tcp_host: str | None
111
114
  _tcp_port: int | None
112
115
  _external_location: Any | None
113
116
 
117
+ def _worker_argv(self) -> list[str]:
118
+ """The worker command as an argv list, however ``server_path`` was given.
119
+
120
+ A sequence is taken verbatim; a string is split. ``posix=False`` on Windows
121
+ keeps backslashes in paths from being read as escapes — at the cost of
122
+ leaving quotes attached to the token, which is why an argument carrying
123
+ spaces or quotes should be passed as a sequence element instead.
124
+ """
125
+ if isinstance(self.server_path, str):
126
+ return shlex.split(self.server_path, posix=sys.platform != "win32")
127
+ return list(self.server_path)
128
+
114
129
  def _get_or_create_httpx_client(self) -> Any: # implemented by Client
115
130
  raise NotImplementedError
116
131
 
@@ -156,7 +171,7 @@ class CatalogClientMixin:
156
171
  ) as proxy:
157
172
  yield proxy
158
173
  else:
159
- cmd = shlex.split(self.server_path, posix=sys.platform != "win32")
174
+ cmd = self._worker_argv()
160
175
  with _catalog_pool.connect(VgiProtocol, cmd) as proxy: # type: ignore[type-abstract]
161
176
  yield proxy
162
177
  except RpcError as e:
@@ -572,6 +587,41 @@ class CatalogClientMixin:
572
587
  transaction_opaque_data=transaction_opaque_data,
573
588
  ).to_optional()
574
589
 
590
+ def table_column_statistics(
591
+ self,
592
+ *,
593
+ attach_opaque_data: AttachOpaqueData,
594
+ transaction_opaque_data: TransactionOpaqueData | None = None,
595
+ schema_name: str,
596
+ name: str,
597
+ ) -> list[ColumnStatistics]:
598
+ """Fetch a table's column statistics, decoded.
599
+
600
+ Workers may inline statistics on [`TableInfo`][] (see
601
+ ``TableInfo.column_statistics``) or serve them lazily through this per-table
602
+ call, which is what ``TableInfo.supports_column_statistics`` advertises. Prefer
603
+ the inlined copy when present and fall back to this.
604
+
605
+ Args:
606
+ attach_opaque_data: The attachment ID from catalog_attach.
607
+ transaction_opaque_data: Optional transaction ID for transactional reads.
608
+ schema_name: The schema containing the table.
609
+ name: The table name.
610
+
611
+ Returns:
612
+ Per-column statistics, or an empty list when the worker has none for
613
+ this table.
614
+
615
+ """
616
+ with self._catalog_connect() as proxy:
617
+ data = proxy.catalog_table_column_statistics_get(
618
+ attach_opaque_data=attach_opaque_data,
619
+ schema_name=schema_name,
620
+ name=name,
621
+ transaction_opaque_data=transaction_opaque_data,
622
+ )
623
+ return deserialize_column_statistics(data) if data else []
624
+
575
625
  def table_create(
576
626
  self,
577
627
  *,