vgi-python 0.8.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/__init__.py +152 -0
- vgi/_duckdb.py +62 -0
- vgi/_storage_profile.py +132 -0
- vgi/_test_fixtures/__init__.py +20 -0
- vgi/_test_fixtures/accumulate/__init__.py +19 -0
- vgi/_test_fixtures/accumulate/worker.py +762 -0
- vgi/_test_fixtures/aggregate/__init__.py +62 -0
- vgi/_test_fixtures/aggregate/_common.py +21 -0
- vgi/_test_fixtures/aggregate/basic.py +232 -0
- vgi/_test_fixtures/aggregate/dynamic.py +409 -0
- vgi/_test_fixtures/aggregate/generic.py +86 -0
- vgi/_test_fixtures/aggregate/listagg.py +71 -0
- vgi/_test_fixtures/aggregate/percentile.py +107 -0
- vgi/_test_fixtures/aggregate/streaming.py +192 -0
- vgi/_test_fixtures/aggregate/varargs.py +75 -0
- vgi/_test_fixtures/aggregate/window.py +380 -0
- vgi/_test_fixtures/attach_options.py +308 -0
- vgi/_test_fixtures/bad_protocol.py +62 -0
- vgi/_test_fixtures/cancellable.py +336 -0
- vgi/_test_fixtures/catalog.py +813 -0
- vgi/_test_fixtures/http_server.py +394 -0
- vgi/_test_fixtures/nest_tensor.py +614 -0
- vgi/_test_fixtures/orchard_catalog.py +47 -0
- vgi/_test_fixtures/projection_repro/__init__.py +6 -0
- vgi/_test_fixtures/projection_repro/worker.py +454 -0
- vgi/_test_fixtures/scalar/__init__.py +116 -0
- vgi/_test_fixtures/scalar/_common.py +69 -0
- vgi/_test_fixtures/scalar/arithmetic.py +321 -0
- vgi/_test_fixtures/scalar/binary.py +120 -0
- vgi/_test_fixtures/scalar/formatting.py +176 -0
- vgi/_test_fixtures/scalar/geo.py +300 -0
- vgi/_test_fixtures/scalar/null_handling.py +107 -0
- vgi/_test_fixtures/scalar/random_demo.py +171 -0
- vgi/_test_fixtures/scalar/settings_secrets.py +102 -0
- vgi/_test_fixtures/scalar/type_info.py +219 -0
- vgi/_test_fixtures/schema_reconcile/__init__.py +29 -0
- vgi/_test_fixtures/schema_reconcile/worker.py +653 -0
- vgi/_test_fixtures/simple_writable.py +793 -0
- vgi/_test_fixtures/table/__init__.py +221 -0
- vgi/_test_fixtures/table/_common.py +162 -0
- vgi/_test_fixtures/table/batch_index.py +283 -0
- vgi/_test_fixtures/table/batch_index_broken.py +200 -0
- vgi/_test_fixtures/table/catalog_scans.py +162 -0
- vgi/_test_fixtures/table/filters.py +1005 -0
- vgi/_test_fixtures/table/late_materialization.py +249 -0
- vgi/_test_fixtures/table/make_series.py +273 -0
- vgi/_test_fixtures/table/misc.py +499 -0
- vgi/_test_fixtures/table/order_modes.py +164 -0
- vgi/_test_fixtures/table/pairs.py +437 -0
- vgi/_test_fixtures/table/partition_columns.py +472 -0
- vgi/_test_fixtures/table/partition_columns_broken.py +304 -0
- vgi/_test_fixtures/table/profiling_example.py +195 -0
- vgi/_test_fixtures/table/required_filters.py +234 -0
- vgi/_test_fixtures/table/sequence.py +710 -0
- vgi/_test_fixtures/table/settings.py +426 -0
- vgi/_test_fixtures/table/transaction_storage.py +162 -0
- vgi/_test_fixtures/table/tt_pushdown.py +191 -0
- vgi/_test_fixtures/table/versioned.py +230 -0
- vgi/_test_fixtures/table_in_out.py +1392 -0
- vgi/_test_fixtures/versioned.py +155 -0
- vgi/_test_fixtures/versioned_tables.py +595 -0
- vgi/_test_fixtures/worker.py +1631 -0
- vgi/_test_fixtures/writable/__init__.py +8 -0
- vgi/_test_fixtures/writable/generic.py +236 -0
- vgi/_test_fixtures/writable/table.py +149 -0
- vgi/_test_fixtures/writable/worker.py +1148 -0
- vgi/aggregate_function.py +607 -0
- vgi/argument_spec.py +472 -0
- vgi/arguments.py +1747 -0
- vgi/auth.py +55 -0
- vgi/catalog/__init__.py +88 -0
- vgi/catalog/attach_option.py +206 -0
- vgi/catalog/catalog_interface.py +2767 -0
- vgi/catalog/descriptors.py +870 -0
- vgi/catalog/duckdb_statistics.py +377 -0
- vgi/catalog/secret_type.py +96 -0
- vgi/catalog/setting.py +253 -0
- vgi/catalog/storage.py +372 -0
- vgi/client/__init__.py +67 -0
- vgi/client/catalog_mixin.py +1251 -0
- vgi/client/cli.py +582 -0
- vgi/client/cli_catalog.py +182 -0
- vgi/client/cli_schema.py +270 -0
- vgi/client/cli_table.py +907 -0
- vgi/client/cli_transaction.py +97 -0
- vgi/client/cli_utils.py +441 -0
- vgi/client/cli_view.py +303 -0
- vgi/client/client.py +2183 -0
- vgi/exceptions.py +205 -0
- vgi/function.py +245 -0
- vgi/function_storage.py +1636 -0
- vgi/function_storage_azure_sql.py +922 -0
- vgi/function_storage_cf_do.py +740 -0
- vgi/http/__init__.py +25 -0
- vgi/http/demo_storage.py +212 -0
- vgi/http/worker_page.py +1252 -0
- vgi/invocation.py +154 -0
- vgi/logging_config.py +93 -0
- vgi/meta_worker.py +661 -0
- vgi/metadata.py +1403 -0
- vgi/otel.py +406 -0
- vgi/protocol.py +2418 -0
- vgi/protocol_version.txt +1 -0
- vgi/py.typed +0 -0
- vgi/scalar_function.py +1211 -0
- vgi/schema_utils.py +234 -0
- vgi/secret_protocol.py +124 -0
- vgi/secret_service.py +238 -0
- vgi/serve.py +769 -0
- vgi/table_buffering_function.py +443 -0
- vgi/table_filter_pushdown.py +1528 -0
- vgi/table_function.py +1130 -0
- vgi/table_in_out_function.py +383 -0
- vgi/transactor/__init__.py +24 -0
- vgi/transactor/_duckdb_compat.py +27 -0
- vgi/transactor/client.py +137 -0
- vgi/transactor/protocol.py +149 -0
- vgi/transactor/server.py +740 -0
- vgi/worker.py +4761 -0
- vgi_python-0.8.0.dist-info/METADATA +735 -0
- vgi_python-0.8.0.dist-info/RECORD +124 -0
- vgi_python-0.8.0.dist-info/WHEEL +4 -0
- vgi_python-0.8.0.dist-info/entry_points.txt +5 -0
- vgi_python-0.8.0.dist-info/licenses/LICENSE +134 -0
|
@@ -0,0 +1,595 @@
|
|
|
1
|
+
# Copyright 2025, 2026 Query Farm LLC - https://query.farm
|
|
2
|
+
|
|
3
|
+
"""Example VGI worker whose exposed tables vary per requested data version.
|
|
4
|
+
|
|
5
|
+
Complements :mod:`vgi._test_fixtures.versioned` (which validates versions but shows
|
|
6
|
+
no tables) by exercising the *payload-shaping* side of ATTACH-time versioning.
|
|
7
|
+
|
|
8
|
+
The catalog advertises data_version_spec ``>=1.0.0,<4.0.0`` and supports four
|
|
9
|
+
concrete versions with distinct table sets::
|
|
10
|
+
|
|
11
|
+
1.0.0 -> animals (name, legs, sound)
|
|
12
|
+
1.1.0 -> animals (with color column) (name, legs, sound, color)
|
|
13
|
+
2.0.0 -> animals + plants
|
|
14
|
+
3.0.0 -> plants
|
|
15
|
+
|
|
16
|
+
Clients can also request a version by spec rather than exact match. The
|
|
17
|
+
resolver accepts:
|
|
18
|
+
|
|
19
|
+
* exact ``X.Y.Z`` (e.g. ``1.0.0``) — must be in the supported set.
|
|
20
|
+
* bare ``X`` (e.g. ``1``) — resolves to the newest ``X.y.z`` supported.
|
|
21
|
+
* bare ``X.Y`` (e.g. ``1.0``) — pins to exact ``X.Y.0``.
|
|
22
|
+
* npm ``^X.Y.Z`` — newest ``X.y.z >= X.Y.Z``.
|
|
23
|
+
* npm ``~X.Y.Z`` — newest ``X.Y.z >= X.Y.Z``.
|
|
24
|
+
|
|
25
|
+
Registered as the ``vgi-fixture-versioned-tables-worker`` entry point.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
from __future__ import annotations
|
|
29
|
+
|
|
30
|
+
import contextlib
|
|
31
|
+
import re
|
|
32
|
+
import uuid
|
|
33
|
+
from dataclasses import dataclass
|
|
34
|
+
from datetime import UTC, datetime
|
|
35
|
+
from typing import TYPE_CHECKING, Any
|
|
36
|
+
|
|
37
|
+
import pyarrow as pa
|
|
38
|
+
from vgi_rpc import ArrowSerializableDataclass
|
|
39
|
+
from vgi_rpc.rpc import OutputCollector
|
|
40
|
+
|
|
41
|
+
from vgi.catalog import (
|
|
42
|
+
AttachOpaqueData,
|
|
43
|
+
CatalogAttachResult,
|
|
44
|
+
CatalogDataVersionRelease,
|
|
45
|
+
CatalogInfo,
|
|
46
|
+
ReadOnlyCatalogInterface,
|
|
47
|
+
ScanFunctionResult,
|
|
48
|
+
SchemaInfo,
|
|
49
|
+
SchemaObjectType,
|
|
50
|
+
SerializedSchema,
|
|
51
|
+
TableInfo,
|
|
52
|
+
TransactionOpaqueData,
|
|
53
|
+
)
|
|
54
|
+
from vgi.invocation import BindResponse
|
|
55
|
+
from vgi.schema_utils import schema
|
|
56
|
+
from vgi.table_function import BindParams, ProcessParams, TableFunctionGenerator, init_single_worker
|
|
57
|
+
from vgi.worker import Worker
|
|
58
|
+
|
|
59
|
+
if TYPE_CHECKING:
|
|
60
|
+
from collections.abc import Sequence
|
|
61
|
+
|
|
62
|
+
from vgi_rpc.rpc import CallContext
|
|
63
|
+
|
|
64
|
+
from vgi.catalog.catalog_interface import FunctionInfo, IndexInfo, MacroInfo, ViewInfo
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
CATALOG_NAME = "versioned_tables"
|
|
68
|
+
DATA_VERSION_SPEC = ">=1.0.0,<4.0.0"
|
|
69
|
+
SUPPORTED_VERSIONS: tuple[str, ...] = ("1.0.0", "1.1.0", "2.0.0", "3.0.0")
|
|
70
|
+
DEFAULT_VERSION = "3.0.0"
|
|
71
|
+
|
|
72
|
+
# Source for `CatalogInfo.source_url` — points at the repo so the describe
|
|
73
|
+
# page (and Cupola) can render a "View source →" link.
|
|
74
|
+
SOURCE_URL = "https://github.com/Query-farm/vgi-python"
|
|
75
|
+
|
|
76
|
+
# Public release manifest. Ordering invariant: newest-first. Uniqueness
|
|
77
|
+
# invariant: each ``version`` appears at most once. The describe page and
|
|
78
|
+
# Cupola consume this verbatim, so adding a release means prepending here.
|
|
79
|
+
DATA_VERSION_RELEASES: tuple[CatalogDataVersionRelease, ...] = (
|
|
80
|
+
CatalogDataVersionRelease(
|
|
81
|
+
version="3.0.0",
|
|
82
|
+
released_at=datetime(2026, 4, 15, tzinfo=UTC),
|
|
83
|
+
summary="Removed deprecated 'animals' table; 'plants' is now the only table.",
|
|
84
|
+
notes_url="https://github.com/Query-farm/vgi-python/releases/tag/data-v3.0.0",
|
|
85
|
+
),
|
|
86
|
+
CatalogDataVersionRelease(
|
|
87
|
+
version="2.0.0",
|
|
88
|
+
released_at=datetime(2026, 2, 1, tzinfo=UTC),
|
|
89
|
+
summary="Added 'plants' table alongside 'animals'.",
|
|
90
|
+
notes_url="https://github.com/Query-farm/vgi-python/releases/tag/data-v2.0.0",
|
|
91
|
+
),
|
|
92
|
+
CatalogDataVersionRelease(
|
|
93
|
+
version="1.1.0",
|
|
94
|
+
released_at=datetime(2026, 1, 10, tzinfo=UTC),
|
|
95
|
+
summary="Added 'sound' column to 'animals'.",
|
|
96
|
+
),
|
|
97
|
+
CatalogDataVersionRelease(
|
|
98
|
+
version="1.0.0",
|
|
99
|
+
released_at=datetime(2026, 1, 1, tzinfo=UTC),
|
|
100
|
+
summary="Initial release.",
|
|
101
|
+
),
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
# Implementation versions use a distinctly different numbering (10.x / 11.x)
|
|
105
|
+
# from data versions (1.x / 2.x / 3.x) so test assertions can't confuse the
|
|
106
|
+
# two dimensions. The advertised implementation_version in vgi_catalogs() is
|
|
107
|
+
# the default (newest) — clients that want an older impl pass a spec.
|
|
108
|
+
SUPPORTED_IMPLEMENTATION_VERSIONS: tuple[str, ...] = ("10.0.0", "10.1.0", "11.0.0")
|
|
109
|
+
DEFAULT_IMPLEMENTATION_VERSION = "11.0.0"
|
|
110
|
+
|
|
111
|
+
STICKY_COOKIE_NAME = "vgi_sticky"
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
# ============================================================================
|
|
115
|
+
# Static table data
|
|
116
|
+
# ============================================================================
|
|
117
|
+
|
|
118
|
+
_ANIMALS_ROWS: dict[str, list[Any]] = {
|
|
119
|
+
"name": ["chicken", "cow", "horse", "pig", "sheep"],
|
|
120
|
+
"legs": [2, 4, 4, 4, 4],
|
|
121
|
+
"sound": ["cluck", "moo", "neigh", "oink", "baa"],
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
# 1.1.0 adds the color column. Rows are kept in the same order for test
|
|
125
|
+
# stability.
|
|
126
|
+
_ANIMALS_COLORS = ["red", "brown", "black", "pink", "white"]
|
|
127
|
+
|
|
128
|
+
_PLANTS_ROWS: dict[str, list[Any]] = {
|
|
129
|
+
"name": ["oak", "pine", "rose", "tomato", "wheat"],
|
|
130
|
+
"kind": ["tree", "tree", "flower", "vegetable", "grass"],
|
|
131
|
+
"height_m": [20.0, 25.0, 0.6, 1.5, 1.0],
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
_ANIMALS_SCHEMA_V1 = schema(name=pa.string(), legs=pa.int64(), sound=pa.string())
|
|
135
|
+
|
|
136
|
+
_ANIMALS_SCHEMA_V1_1 = schema(name=pa.string(), legs=pa.int64(), sound=pa.string(), color=pa.string())
|
|
137
|
+
|
|
138
|
+
_PLANTS_SCHEMA = schema(name=pa.string(), kind=pa.string(), height_m=pa.float64())
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
# ============================================================================
|
|
142
|
+
# Table functions — one per (table, version-variant)
|
|
143
|
+
# ============================================================================
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
@dataclass(slots=True, frozen=True)
|
|
147
|
+
class _NoArgs:
|
|
148
|
+
"""Scan functions here take no arguments."""
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
@dataclass(kw_only=True)
|
|
152
|
+
class _EmitOnceState(ArrowSerializableDataclass):
|
|
153
|
+
done: bool = False
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
@init_single_worker
|
|
157
|
+
class AnimalsScanFunction(TableFunctionGenerator[_NoArgs, _EmitOnceState]):
|
|
158
|
+
"""Scan the 1.0.0 animals table: (name, legs, sound)."""
|
|
159
|
+
|
|
160
|
+
class Meta:
|
|
161
|
+
"""Function metadata."""
|
|
162
|
+
|
|
163
|
+
name = "versioned_tables_animals_scan"
|
|
164
|
+
description = "Animals table for data_version 1.0.0"
|
|
165
|
+
|
|
166
|
+
@classmethod
|
|
167
|
+
def on_bind(cls, params: BindParams[_NoArgs]) -> BindResponse:
|
|
168
|
+
"""Return fixed schema."""
|
|
169
|
+
return BindResponse(output_schema=_ANIMALS_SCHEMA_V1)
|
|
170
|
+
|
|
171
|
+
@classmethod
|
|
172
|
+
def initial_state(cls, params: ProcessParams[_NoArgs]) -> _EmitOnceState:
|
|
173
|
+
"""Fresh state per scan."""
|
|
174
|
+
return _EmitOnceState()
|
|
175
|
+
|
|
176
|
+
@classmethod
|
|
177
|
+
def process(cls, params: ProcessParams[_NoArgs], state: _EmitOnceState, out: OutputCollector) -> None:
|
|
178
|
+
"""Emit rows once, then finish."""
|
|
179
|
+
if state.done:
|
|
180
|
+
out.finish()
|
|
181
|
+
return
|
|
182
|
+
state.done = True
|
|
183
|
+
out.emit(pa.RecordBatch.from_pydict(_ANIMALS_ROWS, schema=params.output_schema))
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
@init_single_worker
|
|
187
|
+
class AnimalsWithColorScanFunction(TableFunctionGenerator[_NoArgs, _EmitOnceState]):
|
|
188
|
+
"""Scan the 1.1.0 animals table: same rows plus a ``color`` column."""
|
|
189
|
+
|
|
190
|
+
class Meta:
|
|
191
|
+
"""Function metadata."""
|
|
192
|
+
|
|
193
|
+
name = "versioned_tables_animals_color_scan"
|
|
194
|
+
description = "Animals table for data_version 1.1.0 (with color)"
|
|
195
|
+
|
|
196
|
+
@classmethod
|
|
197
|
+
def on_bind(cls, params: BindParams[_NoArgs]) -> BindResponse:
|
|
198
|
+
"""Return fixed schema with color."""
|
|
199
|
+
return BindResponse(output_schema=_ANIMALS_SCHEMA_V1_1)
|
|
200
|
+
|
|
201
|
+
@classmethod
|
|
202
|
+
def initial_state(cls, params: ProcessParams[_NoArgs]) -> _EmitOnceState:
|
|
203
|
+
"""Fresh state per scan."""
|
|
204
|
+
return _EmitOnceState()
|
|
205
|
+
|
|
206
|
+
@classmethod
|
|
207
|
+
def process(cls, params: ProcessParams[_NoArgs], state: _EmitOnceState, out: OutputCollector) -> None:
|
|
208
|
+
"""Emit rows once, then finish."""
|
|
209
|
+
if state.done:
|
|
210
|
+
out.finish()
|
|
211
|
+
return
|
|
212
|
+
state.done = True
|
|
213
|
+
rows = {**_ANIMALS_ROWS, "color": _ANIMALS_COLORS}
|
|
214
|
+
out.emit(pa.RecordBatch.from_pydict(rows, schema=params.output_schema))
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
@init_single_worker
|
|
218
|
+
class PlantsScanFunction(TableFunctionGenerator[_NoArgs, _EmitOnceState]):
|
|
219
|
+
"""Scan the plants table: (name, kind, height_m)."""
|
|
220
|
+
|
|
221
|
+
class Meta:
|
|
222
|
+
"""Function metadata."""
|
|
223
|
+
|
|
224
|
+
name = "versioned_tables_plants_scan"
|
|
225
|
+
description = "Plants table for data_version 2.0.0 and 3.0.0"
|
|
226
|
+
|
|
227
|
+
@classmethod
|
|
228
|
+
def on_bind(cls, params: BindParams[_NoArgs]) -> BindResponse:
|
|
229
|
+
"""Return fixed schema."""
|
|
230
|
+
return BindResponse(output_schema=_PLANTS_SCHEMA)
|
|
231
|
+
|
|
232
|
+
@classmethod
|
|
233
|
+
def initial_state(cls, params: ProcessParams[_NoArgs]) -> _EmitOnceState:
|
|
234
|
+
"""Fresh state per scan."""
|
|
235
|
+
return _EmitOnceState()
|
|
236
|
+
|
|
237
|
+
@classmethod
|
|
238
|
+
def process(cls, params: ProcessParams[_NoArgs], state: _EmitOnceState, out: OutputCollector) -> None:
|
|
239
|
+
"""Emit rows once, then finish."""
|
|
240
|
+
if state.done:
|
|
241
|
+
out.finish()
|
|
242
|
+
return
|
|
243
|
+
state.done = True
|
|
244
|
+
out.emit(pa.RecordBatch.from_pydict(_PLANTS_ROWS, schema=params.output_schema))
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
# ============================================================================
|
|
248
|
+
# Per-version table spec
|
|
249
|
+
# ============================================================================
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
@dataclass(frozen=True, slots=True)
|
|
253
|
+
class _VersionedTable:
|
|
254
|
+
"""A (function_name, arrow_schema) tuple for one table variant."""
|
|
255
|
+
|
|
256
|
+
function_name: str
|
|
257
|
+
columns: pa.Schema
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
_ANIMALS_V1 = _VersionedTable(function_name=AnimalsScanFunction.Meta.name, columns=_ANIMALS_SCHEMA_V1)
|
|
261
|
+
_ANIMALS_V1_1 = _VersionedTable(function_name=AnimalsWithColorScanFunction.Meta.name, columns=_ANIMALS_SCHEMA_V1_1)
|
|
262
|
+
_PLANTS = _VersionedTable(function_name=PlantsScanFunction.Meta.name, columns=_PLANTS_SCHEMA)
|
|
263
|
+
|
|
264
|
+
VERSION_TABLES: dict[str, dict[str, _VersionedTable]] = {
|
|
265
|
+
"1.0.0": {"animals": _ANIMALS_V1},
|
|
266
|
+
"1.1.0": {"animals": _ANIMALS_V1_1},
|
|
267
|
+
"2.0.0": {"animals": _ANIMALS_V1, "plants": _PLANTS},
|
|
268
|
+
"3.0.0": {"plants": _PLANTS},
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
# ============================================================================
|
|
273
|
+
# Version spec resolver (npm-ish) — generic over (supported, default, label)
|
|
274
|
+
# ============================================================================
|
|
275
|
+
|
|
276
|
+
_EXACT_RE = re.compile(r"^(\d+)\.(\d+)\.(\d+)$")
|
|
277
|
+
_MAJOR_RE = re.compile(r"^(\d+)$")
|
|
278
|
+
_MAJOR_MINOR_RE = re.compile(r"^(\d+)\.(\d+)$")
|
|
279
|
+
_CARET_RE = re.compile(r"^\^(\d+)\.(\d+)\.(\d+)$")
|
|
280
|
+
_TILDE_RE = re.compile(r"^~(\d+)\.(\d+)\.(\d+)$")
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
def _parse(version: str) -> tuple[int, int, int]:
|
|
284
|
+
"""Parse an exact ``X.Y.Z`` version string into a tuple."""
|
|
285
|
+
m = _EXACT_RE.match(version)
|
|
286
|
+
if not m:
|
|
287
|
+
raise ValueError(f"Not a valid version: {version!r}")
|
|
288
|
+
return (int(m.group(1)), int(m.group(2)), int(m.group(3)))
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
def _resolve_against(spec: str | None, supported: tuple[str, ...], default: str, *, label: str) -> str:
|
|
292
|
+
"""Resolve an npm-style spec to a concrete supported version.
|
|
293
|
+
|
|
294
|
+
Accepts exact ``X.Y.Z``, bare ``X`` (latest in major), bare ``X.Y``
|
|
295
|
+
(pinned to ``X.Y.0``), caret ``^X.Y.Z`` (newest in major with
|
|
296
|
+
``(y,z) >= (Y,Z)``), and tilde ``~X.Y.Z`` (newest in major.minor with
|
|
297
|
+
``z >= Z``). Raises ``ValueError`` with ``label`` in the message when
|
|
298
|
+
nothing matches; the extension surfaces that as the ATTACH failure.
|
|
299
|
+
"""
|
|
300
|
+
if not spec:
|
|
301
|
+
return default
|
|
302
|
+
|
|
303
|
+
sorted_supported: list[tuple[tuple[int, int, int], str]] = sorted((_parse(v), v) for v in supported)
|
|
304
|
+
|
|
305
|
+
# Exact X.Y.Z
|
|
306
|
+
if _EXACT_RE.match(spec):
|
|
307
|
+
if spec in supported:
|
|
308
|
+
return spec
|
|
309
|
+
raise ValueError(f"Unsupported {label} {spec!r}; this worker serves {list(supported)}")
|
|
310
|
+
|
|
311
|
+
# Bare major `X`: latest X.y.z
|
|
312
|
+
m = _MAJOR_RE.match(spec)
|
|
313
|
+
if m:
|
|
314
|
+
major = int(m.group(1))
|
|
315
|
+
candidates = [v for t, v in sorted_supported if t[0] == major]
|
|
316
|
+
if not candidates:
|
|
317
|
+
raise ValueError(f"Unsupported {label} {spec!r}; no major {major} version available")
|
|
318
|
+
return candidates[-1]
|
|
319
|
+
|
|
320
|
+
# Bare major.minor `X.Y`: pinned to X.Y.0
|
|
321
|
+
m = _MAJOR_MINOR_RE.match(spec)
|
|
322
|
+
if m:
|
|
323
|
+
pinned = f"{m.group(1)}.{m.group(2)}.0"
|
|
324
|
+
if pinned in supported:
|
|
325
|
+
return pinned
|
|
326
|
+
raise ValueError(f"Unsupported {label} {spec!r}; {pinned!r} not in {list(supported)}")
|
|
327
|
+
|
|
328
|
+
# Caret `^X.Y.Z`
|
|
329
|
+
m = _CARET_RE.match(spec)
|
|
330
|
+
if m:
|
|
331
|
+
base = (int(m.group(1)), int(m.group(2)), int(m.group(3)))
|
|
332
|
+
candidates = [v for t, v in sorted_supported if t[0] == base[0] and t >= base]
|
|
333
|
+
if not candidates:
|
|
334
|
+
raise ValueError(f"Unsupported {label} {spec!r}; no match in major {base[0]}")
|
|
335
|
+
return candidates[-1]
|
|
336
|
+
|
|
337
|
+
# Tilde `~X.Y.Z`
|
|
338
|
+
m = _TILDE_RE.match(spec)
|
|
339
|
+
if m:
|
|
340
|
+
base = (int(m.group(1)), int(m.group(2)), int(m.group(3)))
|
|
341
|
+
candidates = [v for t, v in sorted_supported if t[0] == base[0] and t[1] == base[1] and t >= base]
|
|
342
|
+
if not candidates:
|
|
343
|
+
raise ValueError(f"Unsupported {label} {spec!r}; no match in {base[0]}.{base[1]}.x")
|
|
344
|
+
return candidates[-1]
|
|
345
|
+
|
|
346
|
+
raise ValueError(f"Unsupported {label} {spec!r}; accepted forms: X.Y.Z, X, X.Y, ^X.Y.Z, ~X.Y.Z")
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
def _resolve_data_version(spec: str | None) -> str:
|
|
350
|
+
return _resolve_against(spec, SUPPORTED_VERSIONS, DEFAULT_VERSION, label="data_version_spec")
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
def _resolve_impl_version(spec: str | None) -> str:
|
|
354
|
+
return _resolve_against(
|
|
355
|
+
spec, SUPPORTED_IMPLEMENTATION_VERSIONS, DEFAULT_IMPLEMENTATION_VERSION, label="implementation_version"
|
|
356
|
+
)
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
# ============================================================================
|
|
360
|
+
# Catalog interface
|
|
361
|
+
# ============================================================================
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
class VersionedTablesCatalog(ReadOnlyCatalogInterface):
|
|
365
|
+
"""Catalog whose visible tables depend on the resolved data version."""
|
|
366
|
+
|
|
367
|
+
catalog_name = CATALOG_NAME
|
|
368
|
+
|
|
369
|
+
# Worker.functions also registers these, but listing them on the catalog
|
|
370
|
+
# interface is harmless and keeps the class self-describing.
|
|
371
|
+
functions = [AnimalsScanFunction, AnimalsWithColorScanFunction, PlantsScanFunction]
|
|
372
|
+
|
|
373
|
+
# The resolved data version is encoded directly into the attach_opaque_data rather
|
|
374
|
+
# than kept in per-instance state. The worker pool can dispatch RPCs for
|
|
375
|
+
# a single catalog across multiple subprocesses, so any state kept on
|
|
376
|
+
# `self` would be invisible to sibling workers. Embedding the version in
|
|
377
|
+
# the attach_opaque_data sidesteps that entirely.
|
|
378
|
+
#
|
|
379
|
+
# Wire format: ``<resolved_version>\x00<uuid16>`` — the null byte splits
|
|
380
|
+
# the decodable version prefix from uuid entropy that keeps attach_opaque_data values
|
|
381
|
+
# unique per call.
|
|
382
|
+
_ATTACH_ID_SEP = b"\x00"
|
|
383
|
+
|
|
384
|
+
# ------------------------------------------------------------------ catalogs
|
|
385
|
+
|
|
386
|
+
def catalogs(self) -> list[CatalogInfo]:
|
|
387
|
+
"""Advertise catalog name, version metadata, and the release manifest.
|
|
388
|
+
|
|
389
|
+
``implementation_version`` advertises the default (newest) impl. Clients
|
|
390
|
+
that want an older impl pass a spec (``^10.0.0``, ``~10.0.0``, etc.)
|
|
391
|
+
via the ATTACH ``implementation_version`` option.
|
|
392
|
+
|
|
393
|
+
``releases`` surfaces ``DATA_VERSION_RELEASES`` so the describe page
|
|
394
|
+
and Cupola can render a discoverable release timeline; ``source_url``
|
|
395
|
+
points back at the repo for further reading.
|
|
396
|
+
"""
|
|
397
|
+
return [
|
|
398
|
+
CatalogInfo(
|
|
399
|
+
name=CATALOG_NAME,
|
|
400
|
+
implementation_version=DEFAULT_IMPLEMENTATION_VERSION,
|
|
401
|
+
data_version_spec=DATA_VERSION_SPEC,
|
|
402
|
+
releases=list(DATA_VERSION_RELEASES),
|
|
403
|
+
source_url=SOURCE_URL,
|
|
404
|
+
),
|
|
405
|
+
]
|
|
406
|
+
|
|
407
|
+
def catalog_attach(
|
|
408
|
+
self,
|
|
409
|
+
*,
|
|
410
|
+
name: str,
|
|
411
|
+
options: dict[str, Any],
|
|
412
|
+
data_version_spec: str | None,
|
|
413
|
+
implementation_version: str | None,
|
|
414
|
+
ctx: CallContext | None = None,
|
|
415
|
+
) -> CatalogAttachResult:
|
|
416
|
+
"""Validate versions, record the resolved data version, return attach result."""
|
|
417
|
+
del options
|
|
418
|
+
if name != CATALOG_NAME:
|
|
419
|
+
raise ValueError(f"Unknown catalog: {name!r}. Available: {CATALOG_NAME}")
|
|
420
|
+
|
|
421
|
+
resolved_impl = _resolve_impl_version(implementation_version)
|
|
422
|
+
resolved = _resolve_data_version(data_version_spec)
|
|
423
|
+
|
|
424
|
+
attach_opaque_data = AttachOpaqueData(resolved.encode("utf-8") + self._ATTACH_ID_SEP + uuid.uuid4().bytes)
|
|
425
|
+
|
|
426
|
+
# Pin HTTP sessions. Ignored by subprocess transport.
|
|
427
|
+
if ctx is not None:
|
|
428
|
+
with contextlib.suppress(RuntimeError):
|
|
429
|
+
ctx.set_cookie(STICKY_COOKIE_NAME, uuid.uuid4().hex)
|
|
430
|
+
|
|
431
|
+
return CatalogAttachResult(
|
|
432
|
+
attach_opaque_data=attach_opaque_data,
|
|
433
|
+
supports_transactions=False,
|
|
434
|
+
supports_time_travel=False,
|
|
435
|
+
catalog_version_frozen=True,
|
|
436
|
+
catalog_version=1,
|
|
437
|
+
attach_opaque_data_required=True,
|
|
438
|
+
default_schema="main",
|
|
439
|
+
resolved_data_version=resolved,
|
|
440
|
+
resolved_implementation_version=resolved_impl,
|
|
441
|
+
)
|
|
442
|
+
|
|
443
|
+
# ------------------------------------------------------------------ helpers
|
|
444
|
+
|
|
445
|
+
def _tables_for(self, attach_opaque_data: AttachOpaqueData) -> dict[str, _VersionedTable]:
|
|
446
|
+
raw = bytes(attach_opaque_data)
|
|
447
|
+
sep = raw.find(self._ATTACH_ID_SEP)
|
|
448
|
+
if sep <= 0:
|
|
449
|
+
return {}
|
|
450
|
+
version = raw[:sep].decode("utf-8", errors="replace")
|
|
451
|
+
return VERSION_TABLES.get(version, {})
|
|
452
|
+
|
|
453
|
+
@staticmethod
|
|
454
|
+
def _make_table_info(name: str, table: _VersionedTable) -> TableInfo:
|
|
455
|
+
return TableInfo(
|
|
456
|
+
comment=None,
|
|
457
|
+
tags={},
|
|
458
|
+
name=name,
|
|
459
|
+
schema_name="main",
|
|
460
|
+
columns=SerializedSchema(table.columns.serialize().to_pybytes()),
|
|
461
|
+
not_null_constraints=[],
|
|
462
|
+
unique_constraints=[],
|
|
463
|
+
check_constraints=[],
|
|
464
|
+
)
|
|
465
|
+
|
|
466
|
+
# ------------------------------------------------------------------ schemas / tables
|
|
467
|
+
|
|
468
|
+
def schemas(
|
|
469
|
+
self, *, attach_opaque_data: AttachOpaqueData, transaction_opaque_data: TransactionOpaqueData | None
|
|
470
|
+
) -> list[SchemaInfo]:
|
|
471
|
+
"""Single ``main`` schema, regardless of version."""
|
|
472
|
+
del transaction_opaque_data
|
|
473
|
+
return [SchemaInfo(attach_opaque_data=attach_opaque_data, name="main", comment=None, tags={})]
|
|
474
|
+
|
|
475
|
+
def schema_get(
|
|
476
|
+
self,
|
|
477
|
+
*,
|
|
478
|
+
attach_opaque_data: AttachOpaqueData,
|
|
479
|
+
transaction_opaque_data: TransactionOpaqueData | None,
|
|
480
|
+
name: str,
|
|
481
|
+
) -> SchemaInfo | None:
|
|
482
|
+
"""Return the ``main`` schema only."""
|
|
483
|
+
del transaction_opaque_data
|
|
484
|
+
if name.lower() != "main":
|
|
485
|
+
return None
|
|
486
|
+
return SchemaInfo(attach_opaque_data=attach_opaque_data, name="main", comment=None, tags={})
|
|
487
|
+
|
|
488
|
+
def schema_contents( # type: ignore[override]
|
|
489
|
+
self,
|
|
490
|
+
*,
|
|
491
|
+
attach_opaque_data: AttachOpaqueData,
|
|
492
|
+
transaction_opaque_data: TransactionOpaqueData | None,
|
|
493
|
+
name: str,
|
|
494
|
+
type: SchemaObjectType,
|
|
495
|
+
) -> Sequence[TableInfo | ViewInfo | FunctionInfo | MacroInfo | IndexInfo]:
|
|
496
|
+
"""List objects in the schema — tables filtered by attach's resolved version."""
|
|
497
|
+
del transaction_opaque_data
|
|
498
|
+
if name.lower() != "main":
|
|
499
|
+
return []
|
|
500
|
+
if type == SchemaObjectType.TABLE:
|
|
501
|
+
return [self._make_table_info(n, t) for n, t in sorted(self._tables_for(attach_opaque_data).items())]
|
|
502
|
+
return []
|
|
503
|
+
|
|
504
|
+
def table_get(
|
|
505
|
+
self,
|
|
506
|
+
*,
|
|
507
|
+
attach_opaque_data: AttachOpaqueData,
|
|
508
|
+
transaction_opaque_data: TransactionOpaqueData | None,
|
|
509
|
+
schema_name: str,
|
|
510
|
+
name: str,
|
|
511
|
+
at_unit: str | None = None,
|
|
512
|
+
at_value: str | None = None,
|
|
513
|
+
) -> TableInfo | None:
|
|
514
|
+
"""Return table info only if it exists at this attach's resolved version."""
|
|
515
|
+
del transaction_opaque_data, at_unit, at_value
|
|
516
|
+
if schema_name.lower() != "main":
|
|
517
|
+
return None
|
|
518
|
+
table = self._tables_for(attach_opaque_data).get(name.lower())
|
|
519
|
+
if table is None:
|
|
520
|
+
return None
|
|
521
|
+
return self._make_table_info(name.lower(), table)
|
|
522
|
+
|
|
523
|
+
def view_get(
|
|
524
|
+
self,
|
|
525
|
+
*,
|
|
526
|
+
attach_opaque_data: AttachOpaqueData,
|
|
527
|
+
transaction_opaque_data: TransactionOpaqueData | None,
|
|
528
|
+
schema_name: str,
|
|
529
|
+
name: str,
|
|
530
|
+
) -> None:
|
|
531
|
+
"""No views exposed."""
|
|
532
|
+
del attach_opaque_data, transaction_opaque_data, schema_name, name
|
|
533
|
+
return None
|
|
534
|
+
|
|
535
|
+
def table_scan_function_get(
|
|
536
|
+
self,
|
|
537
|
+
*,
|
|
538
|
+
attach_opaque_data: AttachOpaqueData,
|
|
539
|
+
transaction_opaque_data: TransactionOpaqueData | None,
|
|
540
|
+
schema_name: str,
|
|
541
|
+
name: str,
|
|
542
|
+
at_unit: str | None,
|
|
543
|
+
at_value: str | None,
|
|
544
|
+
) -> ScanFunctionResult:
|
|
545
|
+
"""Dispatch to the function backing this table at the attach's version."""
|
|
546
|
+
del transaction_opaque_data, at_unit, at_value
|
|
547
|
+
if schema_name.lower() != "main":
|
|
548
|
+
raise ValueError(f"Unknown schema: {schema_name}")
|
|
549
|
+
table = self._tables_for(attach_opaque_data).get(name.lower())
|
|
550
|
+
if table is None:
|
|
551
|
+
raise ValueError(f"Table {schema_name}.{name} not visible at this data version")
|
|
552
|
+
return ScanFunctionResult(
|
|
553
|
+
function_name=table.function_name,
|
|
554
|
+
positional_arguments=[],
|
|
555
|
+
named_arguments={},
|
|
556
|
+
required_extensions=[],
|
|
557
|
+
)
|
|
558
|
+
|
|
559
|
+
def catalog_version(
|
|
560
|
+
self,
|
|
561
|
+
*,
|
|
562
|
+
attach_opaque_data: AttachOpaqueData,
|
|
563
|
+
transaction_opaque_data: TransactionOpaqueData | None,
|
|
564
|
+
ctx: CallContext | None = None,
|
|
565
|
+
) -> int:
|
|
566
|
+
"""Assert cookie stickiness on HTTP and return a constant version."""
|
|
567
|
+
del transaction_opaque_data
|
|
568
|
+
if ctx is not None and ctx.cookies and STICKY_COOKIE_NAME not in ctx.cookies:
|
|
569
|
+
raise ValueError(
|
|
570
|
+
f"expected cookie {STICKY_COOKIE_NAME!r} on follow-up request; got {sorted(ctx.cookies)}",
|
|
571
|
+
)
|
|
572
|
+
# Non-zero int; table set only changes across ATTACHes, not within one.
|
|
573
|
+
return 1
|
|
574
|
+
|
|
575
|
+
|
|
576
|
+
# ============================================================================
|
|
577
|
+
# Worker + entry point
|
|
578
|
+
# ============================================================================
|
|
579
|
+
|
|
580
|
+
|
|
581
|
+
class VersionedTablesWorker(Worker):
|
|
582
|
+
"""Worker exposing :class:`VersionedTablesCatalog`."""
|
|
583
|
+
|
|
584
|
+
catalog_interface = VersionedTablesCatalog
|
|
585
|
+
catalog_name = CATALOG_NAME
|
|
586
|
+
functions = [AnimalsScanFunction, AnimalsWithColorScanFunction, PlantsScanFunction]
|
|
587
|
+
|
|
588
|
+
|
|
589
|
+
def main() -> None:
|
|
590
|
+
"""Run the versioned-tables worker process."""
|
|
591
|
+
VersionedTablesWorker.main()
|
|
592
|
+
|
|
593
|
+
|
|
594
|
+
if __name__ == "__main__":
|
|
595
|
+
main()
|