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.
- vgi/_test_fixtures/attach_options.py +34 -3
- vgi/_test_fixtures/http_server.py +5 -14
- vgi/catalog/__init__.py +7 -0
- vgi/catalog/_descriptor_spec.py +39 -2
- vgi/catalog/attach_option.py +110 -2
- vgi/catalog/catalog_interface.py +68 -0
- vgi/client/catalog_mixin.py +53 -3
- vgi/client/client.py +82 -26
- vgi/http/landing.html +260 -18
- vgi/http/landing_page.py +65 -48
- vgi/http/vgi-client.js +23 -0
- vgi/serve.py +3 -10
- {vgi_python-0.26.0.dist-info → vgi_python-0.28.0.dist-info}/METADATA +2 -2
- {vgi_python-0.26.0.dist-info → vgi_python-0.28.0.dist-info}/RECORD +17 -17
- {vgi_python-0.26.0.dist-info → vgi_python-0.28.0.dist-info}/WHEEL +1 -1
- vgi/http/describe_json.py +0 -461
- {vgi_python-0.26.0.dist-info → vgi_python-0.28.0.dist-info}/entry_points.txt +0 -0
- {vgi_python-0.26.0.dist-info → vgi_python-0.28.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -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
|
|
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
|
|
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
|
|
416
|
-
#
|
|
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
|
-
|
|
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
|
]
|
vgi/catalog/_descriptor_spec.py
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
vgi/catalog/attach_option.py
CHANGED
|
@@ -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
|
|
31
|
-
wire
|
|
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]:
|
vgi/catalog/catalog_interface.py
CHANGED
|
@@ -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]
|
vgi/client/catalog_mixin.py
CHANGED
|
@@ -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
|
|
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 =
|
|
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
|
*,
|