Skip to content

Commit 2237505

Browse files
committed
Fix catalog wire 2.2 fixture and generator CI checks
1 parent de501bc commit 2237505

15 files changed

Lines changed: 94 additions & 48 deletions

File tree

‎tests/catalog/test_catalog_publication.py‎

Lines changed: 30 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
from vgi_rpc.external import ClientExternalConfig, Compression, ExternalRef
1919

2020
from vgi._test_fixtures.catalog_contents import CATALOG_PROBE, ContentsProbeCatalog, ContentsProbeWorker
21-
from vgi.catalog import SchemaInfo
21+
from vgi.catalog import AttachOpaqueData, SchemaInfo
2222
from vgi.catalog.contents_codec import decode_collection, decode_inventory, encode_collection
2323
from vgi.catalog.snapshot import (
2424
CatalogSnapshotManager,
@@ -103,6 +103,11 @@ def _policy(repository: Any, **kwargs: Any) -> CatalogSnapshotPolicy:
103103
)
104104

105105

106+
def _close_policy(policy: CatalogSnapshotPolicy) -> None:
107+
assert policy.fetch_config is not None
108+
policy.fetch_config.fetch_config.close()
109+
110+
106111
@pytest.mark.parametrize("compression", [None, Compression(algorithm="gzip"), Compression()])
107112
def test_recovery_and_warm_hits_skip_inventory_and_upload(storage: _Storage, compression: Compression | None) -> None:
108113
"""Restart recovery does one validated GET; subsequent hits do no catalog work."""
@@ -116,15 +121,16 @@ def test_recovery_and_warm_hits_skip_inventory_and_upload(storage: _Storage, com
116121
assert isinstance(ref, ExternalRef)
117122
assert first.statistics == {"builds": 1, "uploads": 1, "fetches": 1}
118123
assert first.resolve(request, lambda: pytest.fail("warm build")) == ref
119-
assert first.resolve(request, lambda: pytest.fail("conditional build"), "generation-1").not_modified
124+
unchanged = first.resolve(request, lambda: pytest.fail("conditional build"), "generation-1")
125+
assert isinstance(unchanged, CatalogContentsResponse) and unchanged.not_modified
120126
assert storage.uploads == 1 and storage.gets == 1
121127
recovered = CatalogSnapshotManager(policy)
122128
assert recovered.resolve(request, lambda: pytest.fail("recovery build")) == ref
123129
assert recovered.statistics == {"builds": 0, "uploads": 0, "fetches": 1}
124130
assert recovered.resolve(request, lambda: pytest.fail("second recovery build")) == ref
125131
assert storage.uploads == 1 and storage.gets == 2
126132
finally:
127-
policy.fetch_config.fetch_config.close()
133+
_close_policy(policy)
128134

129135

130136
def test_scopes_singleflight_and_conditional_invalidation(storage: _Storage) -> None:
@@ -150,7 +156,7 @@ def test_scopes_singleflight_and_conditional_invalidation(storage: _Storage) ->
150156
manager.resolve(request, lambda: response)
151157
assert storage.uploads == 3
152158
finally:
153-
policy.fetch_config.fetch_config.close()
159+
_close_policy(policy)
154160

155161

156162
def test_singleflight_allows_independent_keys_to_build_concurrently(storage: _Storage) -> None:
@@ -174,7 +180,7 @@ def build() -> CatalogContentsResponse:
174180
assert all(isinstance(future.result(), ExternalRef) for future in futures)
175181
assert manager.statistics == {"builds": 2, "uploads": 2, "fetches": 2}
176182
finally:
177-
policy.fetch_config.fetch_config.close()
183+
_close_policy(policy)
178184

179185

180186
def test_failed_readback_never_commits_and_caps_prevent_upload(storage: _Storage) -> None:
@@ -195,7 +201,7 @@ def test_failed_readback_never_commits_and_caps_prevent_upload(storage: _Storage
195201
limited.resolve(request, lambda: response)
196202
assert storage.uploads == 1
197203
finally:
198-
policy.fetch_config.fetch_config.close()
204+
_close_policy(policy)
199205

200206

201207
def test_import_validates_nested_catalog_and_contract(storage: _Storage) -> None:
@@ -217,7 +223,7 @@ def test_import_validates_nested_catalog_and_contract(storage: _Storage) -> None
217223
manager.resolve(other, lambda: malformed)
218224
assert storage.uploads == 1
219225
finally:
220-
policy.fetch_config.fetch_config.close()
226+
_close_policy(policy)
221227

222228

223229
def test_cheap_validator_avoids_all_publication_work(storage: _Storage) -> None:
@@ -227,10 +233,10 @@ def test_cheap_validator_avoids_all_publication_work(storage: _Storage) -> None:
227233
manager = CatalogSnapshotManager(policy)
228234
try:
229235
result = manager.resolve(_request(), lambda: pytest.fail("build"), "fresh")
230-
assert result.not_modified
236+
assert isinstance(result, CatalogContentsResponse) and result.not_modified
231237
assert manager.statistics == {"builds": 0, "uploads": 0, "fetches": 0}
232238
finally:
233-
policy.fetch_config.fetch_config.close()
239+
_close_policy(policy)
234240

235241

236242
def test_worker_auth_precedes_cache_and_fallback_is_explicit(
@@ -263,7 +269,7 @@ def test_worker_auth_precedes_cache_and_fallback_is_explicit(
263269
assert isinstance(response, CatalogContentsResponse)
264270
assert decode_inventory(response)
265271
finally:
266-
policy.fetch_config.fetch_config.close()
272+
_close_policy(policy)
267273

268274

269275
def test_worker_fallback_rejects_generation_change(storage: _Storage, monkeypatch: pytest.MonkeyPatch) -> None:
@@ -294,15 +300,17 @@ def changing_contents(*args: Any, **kwargs: Any) -> Any:
294300
worker.catalog_contents(attach.attach_opaque_data)
295301
assert storage.uploads == 0
296302
finally:
297-
policy.fetch_config.fetch_config.close()
303+
_close_policy(policy)
298304

299305

300306
def test_collections_consume_multiple_batches_and_reject_wrong_schema() -> None:
301307
"""Later rows and batches survive; legacy schema attachment bytes disappear."""
302308
records = [
303-
SchemaInfo(path=[str(i)], comment=None, tags={"x": str(i)}, attach_opaque_data=b"secret") for i in range(7)
309+
SchemaInfo(path=[str(i)], comment=None, tags={"x": str(i)}, attach_opaque_data=AttachOpaqueData(b"secret"))
310+
for i in range(7)
304311
]
305312
encoded = encode_collection(SchemaInfo, records, batch_size=2)
313+
assert encoded is not None
306314
assert decode_collection(SchemaInfo, encoded) == records
307315
assert len(list(pa.ipc.open_stream(encoded))) == 4
308316
assert b"secret" not in encoded
@@ -337,7 +345,7 @@ def put_object(self, *, Bucket: str, Key: str, Body: bytes, **kwargs: Any) -> No
337345
self.store.objects["/" + Key] = (Body, kwargs.get("ContentEncoding"))
338346
self.etags[Key] = str(int(self.etags.get(Key, "0")) + 1)
339347

340-
def generate_presigned_url(self, operation: str, *, Params: Any, ExpiresIn: int) -> str:
348+
def generate_presigned_url(self, operation: str, *, Params: dict[str, str], ExpiresIn: int) -> str:
341349
del operation, ExpiresIn
342350
return self.store.base + "/" + Params["Key"]
343351

@@ -364,7 +372,7 @@ def test_durable_s3_recovery_and_conditional_manifest_tombstone(storage: _Storag
364372
assert second.commit_if_absent(manifest) == manifest
365373
assert second.lookup(key) == manifest
366374
finally:
367-
policy.fetch_config.fetch_config.close()
375+
_close_policy(policy)
368376

369377

370378
def test_s3_renewal_is_persisted_and_preserves_a_concurrent_replacement(
@@ -415,7 +423,7 @@ def concurrent_put(**kwargs: Any) -> None:
415423
assert repository.renew(renewed) is None
416424
assert restarted.lookup(key) == replacement
417425
finally:
418-
policy.fetch_config.fetch_config.close()
426+
_close_policy(policy)
419427

420428

421429
def test_subprocess_client_resolves_published_catalog() -> None:
@@ -441,6 +449,7 @@ class Entry(ArrowSerializableDataclass):
441449
value: Annotated[str, ArrowType(pa.dictionary(pa.int16(), pa.string()))]
442450

443451
encoded = encode_collection(Entry, [Entry("a"), Entry("b"), Entry("c")], batch_size=1)
452+
assert encoded is not None
444453
assert decode_collection(Entry, encoded) == [Entry("a"), Entry("b"), Entry("c")]
445454
with pytest.raises(ValueError, match="trailing bytes"):
446455
decode_collection(Entry, encoded + encoded)
@@ -469,7 +478,7 @@ def test_import_clamps_supplied_fetch_limits(storage: _Storage) -> None:
469478
with pytest.raises((ValueError, RuntimeError), match="decompress|bytes|limit"):
470479
limited.import_publication(request, replace(manifest, raw_size=999))
471480
finally:
472-
policy.fetch_config.fetch_config.close()
481+
_close_policy(policy)
473482

474483

475484
def test_import_checks_actual_outer_size_digest_and_stream_end(storage: _Storage) -> None:
@@ -503,7 +512,7 @@ def test_import_checks_actual_outer_size_digest_and_stream_end(storage: _Storage
503512
with pytest.raises(ValueError, match="trailing bytes"):
504513
CatalogSnapshotManager(policy).import_publication(request, invalid)
505514
finally:
506-
policy.fetch_config.fetch_config.close()
515+
_close_policy(policy)
507516

508517

509518
def test_warm_renewal_rejects_changed_immutable_metadata(storage: _Storage) -> None:
@@ -527,7 +536,7 @@ def renew(self, manifest: Any) -> Any:
527536
with pytest.raises(ValueError, match="immutable"):
528537
manager.resolve(request, lambda: pytest.fail("renewal must not rebuild"))
529538
finally:
530-
policy.fetch_config.fetch_config.close()
539+
_close_policy(policy)
531540

532541

533542
def test_generation_change_does_not_commit(storage: _Storage) -> None:
@@ -555,7 +564,7 @@ def build() -> CatalogContentsResponse:
555564
assert storage.uploads == 0
556565
assert repository.lookup(key) is None
557566
finally:
558-
policy.fetch_config.fetch_config.close()
567+
_close_policy(policy)
559568

560569

561570
@pytest.mark.parametrize("phase", ["lookup", "readback", "commit"])
@@ -593,7 +602,7 @@ def change_generation(*args: Any, **kwargs: Any) -> Any:
593602
with pytest.raises(ValueError, match="generation changed"):
594603
manager.resolve(request, lambda: response, if_none_match=response.etag)
595604
finally:
596-
policy.fetch_config.fetch_config.close()
605+
_close_policy(policy)
597606

598607

599608
@pytest.mark.parametrize("phase", ["readback", "commit"])
@@ -631,4 +640,4 @@ def change_generation(*args: Any, **kwargs: Any) -> Any:
631640
if phase == "readback":
632641
assert repository.lookup(manifest.cache_key) is None
633642
finally:
634-
policy.fetch_config.fetch_config.close()
643+
_close_policy(policy)

‎tests/catalog/test_declarative.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1234,7 +1234,7 @@ def test_schema_to_schema_info(self) -> None:
12341234
info = schema.to_schema_info(attach_opaque_data)
12351235
assert isinstance(info, SchemaInfo)
12361236
assert info.path == ["main"]
1237-
assert info.attach_opaque_data == attach_opaque_data
1237+
assert "attach_opaque_data" not in pa.ipc.open_stream(info.serialize_to_bytes()).schema.names
12381238
assert info.comment == "Main schema"
12391239
assert info.tags == {"type": "core"}
12401240

‎tests/catalog/test_function_info_binary_dictionary.py‎

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
"""Binary metadata dictionaries preserve bytes, typed defaults and batch boundaries."""
22

3+
from __future__ import annotations
4+
35
from dataclasses import replace
6+
from typing import Any
47

58
import pyarrow as pa
69
import pytest
@@ -14,6 +17,12 @@
1417
_BINARY_DICTIONARY = pa.dictionary(pa.int32(), pa.binary())
1518

1619

20+
def _dictionary_column(batch: pa.RecordBatch, name: str) -> pa.DictionaryArray[Any, Any]:
21+
column = batch.column(name)
22+
assert isinstance(column, pa.DictionaryArray)
23+
return column
24+
25+
1726
def _function(name: str, arguments: bytes, defaults: pa.RecordBatch | None = None) -> FunctionInfo:
1827
return FunctionInfo(
1928
name=name,
@@ -56,7 +65,7 @@ def test_collection_dictionary_replacements_preserve_values_and_nulls() -> None:
5665
assert encoded is not None
5766
batches = list(pa.ipc.open_stream(encoded))
5867
assert len(batches) == 3
59-
assert [batch.column("arguments").dictionary.to_pylist() for batch in batches] == [
68+
assert [_dictionary_column(batch, "arguments").dictionary.to_pylist() for batch in batches] == [
6069
[b"a"],
6170
[b"\xff"],
6271
[b"a", b""],
@@ -73,16 +82,16 @@ def test_binary_dictionary_deduplicates_equal_content() -> None:
7382
assert encoded is not None
7483
batch = pa.ipc.open_stream(encoded).read_next_batch()
7584
for name in _FIELDS:
76-
column = batch.column(name)
85+
column = _dictionary_column(batch, name)
7786
assert len(column.dictionary) == 1
7887
assert column.indices.to_pylist() == [0] * 20
7988
plain_schema = pa.schema(
8089
[field.with_type(pa.binary()) if field.name in _FIELDS else field for field in batch.schema]
8190
)
8291
plain = pa.RecordBatch.from_arrays(
8392
[
84-
column.dictionary_decode() if field.name in _FIELDS else column
85-
for column, field in zip(batch, batch.schema, strict=True)
93+
column.cast(pa.binary()) if field.name in _FIELDS else column
94+
for column, field in zip(batch.columns, batch.schema, strict=True)
8695
],
8796
schema=plain_schema,
8897
)

‎vgi/_test_fixtures/narrow_bind/worker.py‎

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -168,7 +168,6 @@ def schemas(
168168
for i, info in enumerate(infos):
169169
if info.path == ["main"]:
170170
infos[i] = SchemaInfo(
171-
attach_opaque_data=info.attach_opaque_data,
172171
path=list(info.path),
173172
comment=info.comment,
174173
tags=info.tags,

‎vgi/_test_fixtures/projection_repro/worker.py‎

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -381,7 +381,6 @@ def schemas(
381381
for i, info in enumerate(infos):
382382
if info.path == ["main"]:
383383
infos[i] = SchemaInfo(
384-
attach_opaque_data=info.attach_opaque_data,
385384
path=list(info.path),
386385
comment=info.comment,
387386
tags=info.tags,

‎vgi/_test_fixtures/schema_reconcile/worker.py‎

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -544,7 +544,6 @@ def schemas(
544544
for i, info in enumerate(infos):
545545
if info.path == [_SCHEMA_NAME]:
546546
infos[i] = SchemaInfo(
547-
attach_opaque_data=info.attach_opaque_data,
548547
path=list(info.path),
549548
comment=info.comment,
550549
tags=info.tags,

‎vgi/_test_fixtures/writable/worker.py‎

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -566,7 +566,6 @@ def schemas(
566566
# default of 1 (unknown / eager-load), which restores correct lookups.
567567
static_schemas = [
568568
SchemaInfo(
569-
attach_opaque_data=s.attach_opaque_data,
570569
path=list(s.path),
571570
comment=s.comment,
572571
tags=s.tags,

‎vgi/catalog/catalog_interface.py‎

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -363,6 +363,9 @@ class SchemaInfo(CatalogObject, ArrowSerializableDataclass):
363363
364364
Attributes:
365365
path: Raw identifier components from the outermost schema to this schema.
366+
attach_opaque_data: Deprecated constructor-only compatibility argument.
367+
Discarded and never serialized; attachment state belongs to
368+
``CatalogAttachResult`` and subsequent request envelopes.
366369
estimated_object_count: Approximate population per object kind, keyed by
367370
the same names the C++ extension uses for its set-cache
368371
instrumentation: ``"table"``, ``"view"``, ``"scalar_function"``,
@@ -805,9 +808,7 @@ class FunctionInfo(CatalogSchemaObject, ArrowSerializableDataclass):
805808

806809
output_schema: Annotated[SerializedSchema, ArrowType(pa.dictionary(pa.int32(), pa.binary()))]
807810

808-
parameter_default_values: Annotated[
809-
pa.RecordBatch | None, ArrowType(pa.dictionary(pa.int32(), pa.binary()))
810-
] = None
811+
parameter_default_values: Annotated[pa.RecordBatch | None, ArrowType(pa.dictionary(pa.int32(), pa.binary()))] = None
811812

812813
stability: FunctionStability | None = None
813814
null_handling: NullHandling | None = None

‎vgi/catalog/descriptors.py‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -899,6 +899,9 @@ class Schema:
899899
def to_schema_info(self, attach_opaque_data: AttachOpaqueData) -> SchemaInfo:
900900
"""Convert to [`SchemaInfo`][] for catalog response.
901901
902+
``attach_opaque_data`` is accepted for compatibility and discarded;
903+
schema metadata does not contain attachment state.
904+
902905
Populates ``estimated_object_count`` from the declared population so
903906
the C++ extension's eager-load gate can choose between bulk
904907
``LoadEntries`` and per-name single-entry RPCs without an extra round
@@ -923,7 +926,6 @@ def to_schema_info(self, attach_opaque_data: AttachOpaqueData) -> SchemaInfo:
923926
for func in self.functions:
924927
function_counts[func.get_metadata().function_type] += 1
925928
return SchemaInfo(
926-
attach_opaque_data=attach_opaque_data,
927929
path=list(self.path),
928930
comment=self.comment,
929931
tags=dict(self.tags),

‎vgi/catalog/snapshot.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -216,7 +216,7 @@ def __init__(
216216
raise ValueError("retention must exceed a positive URL lifetime")
217217
self.client, self.bucket, self.prefix = client, bucket, prefix.rstrip("/") + "/"
218218
self.profile, self.url_ttl_seconds, self.retention_seconds = profile, url_ttl_seconds, retention_seconds
219-
self.storage = self
219+
self.storage: ExternalStorage = self
220220
self._locations: dict[str, PublicationLocation] = {}
221221
self._lock = threading.Lock()
222222

0 commit comments

Comments
 (0)