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.
Files changed (124) hide show
  1. vgi/__init__.py +152 -0
  2. vgi/_duckdb.py +62 -0
  3. vgi/_storage_profile.py +132 -0
  4. vgi/_test_fixtures/__init__.py +20 -0
  5. vgi/_test_fixtures/accumulate/__init__.py +19 -0
  6. vgi/_test_fixtures/accumulate/worker.py +762 -0
  7. vgi/_test_fixtures/aggregate/__init__.py +62 -0
  8. vgi/_test_fixtures/aggregate/_common.py +21 -0
  9. vgi/_test_fixtures/aggregate/basic.py +232 -0
  10. vgi/_test_fixtures/aggregate/dynamic.py +409 -0
  11. vgi/_test_fixtures/aggregate/generic.py +86 -0
  12. vgi/_test_fixtures/aggregate/listagg.py +71 -0
  13. vgi/_test_fixtures/aggregate/percentile.py +107 -0
  14. vgi/_test_fixtures/aggregate/streaming.py +192 -0
  15. vgi/_test_fixtures/aggregate/varargs.py +75 -0
  16. vgi/_test_fixtures/aggregate/window.py +380 -0
  17. vgi/_test_fixtures/attach_options.py +308 -0
  18. vgi/_test_fixtures/bad_protocol.py +62 -0
  19. vgi/_test_fixtures/cancellable.py +336 -0
  20. vgi/_test_fixtures/catalog.py +813 -0
  21. vgi/_test_fixtures/http_server.py +394 -0
  22. vgi/_test_fixtures/nest_tensor.py +614 -0
  23. vgi/_test_fixtures/orchard_catalog.py +47 -0
  24. vgi/_test_fixtures/projection_repro/__init__.py +6 -0
  25. vgi/_test_fixtures/projection_repro/worker.py +454 -0
  26. vgi/_test_fixtures/scalar/__init__.py +116 -0
  27. vgi/_test_fixtures/scalar/_common.py +69 -0
  28. vgi/_test_fixtures/scalar/arithmetic.py +321 -0
  29. vgi/_test_fixtures/scalar/binary.py +120 -0
  30. vgi/_test_fixtures/scalar/formatting.py +176 -0
  31. vgi/_test_fixtures/scalar/geo.py +300 -0
  32. vgi/_test_fixtures/scalar/null_handling.py +107 -0
  33. vgi/_test_fixtures/scalar/random_demo.py +171 -0
  34. vgi/_test_fixtures/scalar/settings_secrets.py +102 -0
  35. vgi/_test_fixtures/scalar/type_info.py +219 -0
  36. vgi/_test_fixtures/schema_reconcile/__init__.py +29 -0
  37. vgi/_test_fixtures/schema_reconcile/worker.py +653 -0
  38. vgi/_test_fixtures/simple_writable.py +793 -0
  39. vgi/_test_fixtures/table/__init__.py +221 -0
  40. vgi/_test_fixtures/table/_common.py +162 -0
  41. vgi/_test_fixtures/table/batch_index.py +283 -0
  42. vgi/_test_fixtures/table/batch_index_broken.py +200 -0
  43. vgi/_test_fixtures/table/catalog_scans.py +162 -0
  44. vgi/_test_fixtures/table/filters.py +1005 -0
  45. vgi/_test_fixtures/table/late_materialization.py +249 -0
  46. vgi/_test_fixtures/table/make_series.py +273 -0
  47. vgi/_test_fixtures/table/misc.py +499 -0
  48. vgi/_test_fixtures/table/order_modes.py +164 -0
  49. vgi/_test_fixtures/table/pairs.py +437 -0
  50. vgi/_test_fixtures/table/partition_columns.py +472 -0
  51. vgi/_test_fixtures/table/partition_columns_broken.py +304 -0
  52. vgi/_test_fixtures/table/profiling_example.py +195 -0
  53. vgi/_test_fixtures/table/required_filters.py +234 -0
  54. vgi/_test_fixtures/table/sequence.py +710 -0
  55. vgi/_test_fixtures/table/settings.py +426 -0
  56. vgi/_test_fixtures/table/transaction_storage.py +162 -0
  57. vgi/_test_fixtures/table/tt_pushdown.py +191 -0
  58. vgi/_test_fixtures/table/versioned.py +230 -0
  59. vgi/_test_fixtures/table_in_out.py +1392 -0
  60. vgi/_test_fixtures/versioned.py +155 -0
  61. vgi/_test_fixtures/versioned_tables.py +595 -0
  62. vgi/_test_fixtures/worker.py +1631 -0
  63. vgi/_test_fixtures/writable/__init__.py +8 -0
  64. vgi/_test_fixtures/writable/generic.py +236 -0
  65. vgi/_test_fixtures/writable/table.py +149 -0
  66. vgi/_test_fixtures/writable/worker.py +1148 -0
  67. vgi/aggregate_function.py +607 -0
  68. vgi/argument_spec.py +472 -0
  69. vgi/arguments.py +1747 -0
  70. vgi/auth.py +55 -0
  71. vgi/catalog/__init__.py +88 -0
  72. vgi/catalog/attach_option.py +206 -0
  73. vgi/catalog/catalog_interface.py +2767 -0
  74. vgi/catalog/descriptors.py +870 -0
  75. vgi/catalog/duckdb_statistics.py +377 -0
  76. vgi/catalog/secret_type.py +96 -0
  77. vgi/catalog/setting.py +253 -0
  78. vgi/catalog/storage.py +372 -0
  79. vgi/client/__init__.py +67 -0
  80. vgi/client/catalog_mixin.py +1251 -0
  81. vgi/client/cli.py +582 -0
  82. vgi/client/cli_catalog.py +182 -0
  83. vgi/client/cli_schema.py +270 -0
  84. vgi/client/cli_table.py +907 -0
  85. vgi/client/cli_transaction.py +97 -0
  86. vgi/client/cli_utils.py +441 -0
  87. vgi/client/cli_view.py +303 -0
  88. vgi/client/client.py +2183 -0
  89. vgi/exceptions.py +205 -0
  90. vgi/function.py +245 -0
  91. vgi/function_storage.py +1636 -0
  92. vgi/function_storage_azure_sql.py +922 -0
  93. vgi/function_storage_cf_do.py +740 -0
  94. vgi/http/__init__.py +25 -0
  95. vgi/http/demo_storage.py +212 -0
  96. vgi/http/worker_page.py +1252 -0
  97. vgi/invocation.py +154 -0
  98. vgi/logging_config.py +93 -0
  99. vgi/meta_worker.py +661 -0
  100. vgi/metadata.py +1403 -0
  101. vgi/otel.py +406 -0
  102. vgi/protocol.py +2418 -0
  103. vgi/protocol_version.txt +1 -0
  104. vgi/py.typed +0 -0
  105. vgi/scalar_function.py +1211 -0
  106. vgi/schema_utils.py +234 -0
  107. vgi/secret_protocol.py +124 -0
  108. vgi/secret_service.py +238 -0
  109. vgi/serve.py +769 -0
  110. vgi/table_buffering_function.py +443 -0
  111. vgi/table_filter_pushdown.py +1528 -0
  112. vgi/table_function.py +1130 -0
  113. vgi/table_in_out_function.py +383 -0
  114. vgi/transactor/__init__.py +24 -0
  115. vgi/transactor/_duckdb_compat.py +27 -0
  116. vgi/transactor/client.py +137 -0
  117. vgi/transactor/protocol.py +149 -0
  118. vgi/transactor/server.py +740 -0
  119. vgi/worker.py +4761 -0
  120. vgi_python-0.8.0.dist-info/METADATA +735 -0
  121. vgi_python-0.8.0.dist-info/RECORD +124 -0
  122. vgi_python-0.8.0.dist-info/WHEEL +4 -0
  123. vgi_python-0.8.0.dist-info/entry_points.txt +5 -0
  124. 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()