Skip to content

Commit 66f1674

Browse files
committed
Qualify successor protocol tests under strict typing
1 parent 19d873e commit 66f1674

72 files changed

Lines changed: 2427 additions & 1119 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎tests/catalog/column_native_reference.py‎

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,13 +27,14 @@
2727
from vgi.worker import Worker
2828

2929
from .column_paths_reference import ColumnPathsCatalog
30+
from .typing_helpers import present
3031

3132
COUNT = pa.schema([pa.field("count", pa.int64(), nullable=False)])
3233

3334

3435
def _args(params: Any) -> dict[str, Any]:
3536
call = params.bind_call if isinstance(params, BindParams) else params.init_call.bind_call
36-
target = json.loads(call.arguments.positional[0].as_py())
37+
target = json.loads(present(call.arguments.positional[0]).as_py())
3738
return dict(
3839
attach_opaque_data=params.attach_opaque_data,
3940
transaction_opaque_data=params.transaction_opaque_data,
@@ -62,16 +63,20 @@ def on_bind(cls, params: BindParams[None]) -> BindResponse:
6263
if info is None:
6364
raise ValueError("table does not exist")
6465

65-
def data_field(field: pa.Field) -> pa.Field:
66+
def data_field(field: pa.Field[Any]) -> pa.Field[Any]:
6667
# Column definitions live on TableInfo; the scan's data schema must
6768
# not ask generic function consumers to interpret catalog defaults.
6869
field = with_column_expressions(field, ColumnExpressions())
6970
if pa.types.is_struct(field.type):
7071
field = field.with_type(pa.struct([data_field(child) for child in field.type]))
7172
return field
7273

73-
schema = pa.ipc.read_schema(pa.BufferReader(info.columns))
74-
return BindResponse(output_schema=pa.schema([data_field(field) for field in schema], metadata=schema.metadata))
74+
schema = pa.ipc.read_schema(pa.py_buffer(info.columns))
75+
return BindResponse(
76+
output_schema=pa.schema(
77+
[data_field(field) for field in schema], metadata={k: v for k, v in (schema.metadata or {}).items()}
78+
)
79+
)
7580

7681
@classmethod
7782
def on_init(cls, params: InitParams[None]) -> GlobalInitResponse:

‎tests/catalog/column_paths_reference.py‎

Lines changed: 41 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
CONSTRAINT_RECORDS_READ,
4040
CatalogCapabilities,
4141
)
42+
from vgi.catalog.catalog_interface import SerializedSchema
4243
from vgi.catalog.column_metadata import column_metadata
4344
from vgi.catalog.constraints import (
4445
ConstraintDefinition,
@@ -58,7 +59,9 @@
5859
from vgi.column_path import column_definition, column_path_key, replace_column_field, resolve_column_path
5960
from vgi.worker import Worker
6061

61-
from .table_layout_reference import TABLE_PROFILES, TableLayoutCatalog, _table_key, _validate_expression
62+
from .expression_reference import validate_expression as _validate_expression
63+
from .table_layout_reference import TABLE_PROFILES, TableLayoutCatalog, _table_key
64+
from .typing_helpers import present
6265

6366
COLUMN_PROFILES = CatalogCapabilities(
6467
read=sorted([*TABLE_PROFILES.read, CONSTRAINT_RECORDS_READ]),
@@ -86,7 +89,7 @@ def _eval(
8689
path: list[str],
8790
schema: pa.Schema | None = None,
8891
rows: list[dict[str, Any]] | None = None,
89-
) -> pa.Array:
92+
) -> pa.Array[Any]:
9093
expression.validate_owner(path)
9194
with duckdb.connect(config={"enable_external_access": False}) as connection:
9295
connection.execute("SET TimeZone='UTC'")
@@ -176,15 +179,15 @@ class ColumnPathsCatalog(TableLayoutCatalog):
176179
profiles = COLUMN_PROFILES
177180

178181
def table_create(self, **kwargs: Any) -> None:
179-
_fields(pa.ipc.read_schema(pa.BufferReader(kwargs["columns"])), kwargs["schema_path"])
182+
_fields(pa.ipc.read_schema(pa.py_buffer(kwargs["columns"])), kwargs["schema_path"])
180183
super().table_create(**kwargs)
181184

182185
def _create_constraints(self, args: dict[str, Any]) -> tuple[list[bytes], bytes | None]:
183186
table = CatalogObjectRef(kind="table", schema_path=args["schema_path"], name=args["name"])
184187
definitions = constraint_definitions(args["constraint_definitions"], table)
185188
if any(definition.kind != ConstraintKind.NOT_NULL for definition in definitions):
186189
raise NotImplementedError("column reference only enforces NOT_NULL constraints")
187-
validate_constraint_schema(pa.ipc.read_schema(pa.BufferReader(args["columns"])), definitions)
190+
validate_constraint_schema(pa.ipc.read_schema(pa.py_buffer(args["columns"])), definitions)
188191
return [
189192
self._not_null_info(table, definition).serialize_to_bytes() for definition in definitions
190193
], uuid.uuid4().bytes
@@ -201,7 +204,7 @@ def _not_null_info(table: CatalogObjectRef, definition: ConstraintDefinition) ->
201204
)
202205

203206
def _changed_constraints(
204-
self, info: TableInfo, schema: pa.Schema, operation: str, path: list[str], replacement: pa.Field | None
207+
self, info: TableInfo, schema: pa.Schema, operation: str, path: list[str], replacement: pa.Field[Any] | None
205208
) -> list[bytes]:
206209
existing = {}
207210
for constraint in info.constraint_infos():
@@ -234,7 +237,7 @@ def collect(fields: Any, prefix: list[str]) -> None:
234237
return result
235238

236239
def _arrange(self, rows: list[dict[str, Any]], info: TableInfo) -> list[dict[str, Any]]:
237-
schema = pa.ipc.read_schema(pa.BufferReader(info.columns))
240+
schema = pa.ipc.read_schema(pa.py_buffer(info.columns))
238241
# Resolve defaults from the same locked snapshot that stores the rows.
239242
# Reading table metadata before _change would race concurrent DDL.
240243
rows = [_defaulted(schema, row, info.schema_path) for row in rows]
@@ -250,7 +253,7 @@ def _column_change(self, operation: str, args: dict[str, Any]) -> ConstraintSnap
250253
version, data = self._snapshot(db, args["attach_opaque_data"], args["transaction_opaque_data"])
251254
if key not in data:
252255
if args.get("ignore_not_found"):
253-
return
256+
return None
254257
raise ValueError("table does not exist")
255258
info = self._info(data[key])
256259
request = args.get("constraint_request")
@@ -264,12 +267,14 @@ def _column_change(self, operation: str, args: dict[str, Any]) -> ConstraintSnap
264267
raise ValueError("NOT_NULL constraint already exists")
265268
operation = "not_null_set"
266269
else:
267-
matches = [item for item in info.constraint_infos() if item.ref.key() == change.ref.key()]
268-
if len(matches) != 1 or matches[0].definition.kind != ConstraintKind.NOT_NULL:
270+
constraint_matches = [
271+
item for item in info.constraint_infos() if item.ref.key() == change.ref.key()
272+
]
273+
if len(constraint_matches) != 1 or constraint_matches[0].definition.kind != ConstraintKind.NOT_NULL:
269274
raise ValueError("unknown NOT_NULL constraint identity")
270-
path = matches[0].definition.column_paths[0]
275+
path = constraint_matches[0].definition.column_paths[0]
271276
operation = "not_null_drop"
272-
schema = pa.ipc.read_schema(pa.BufferReader(info.columns))
277+
schema = pa.ipc.read_schema(pa.py_buffer(info.columns))
273278
parent_fields = schema if len(path) == 1 else resolve_column_path(schema, path[:-1]).type
274279
if not isinstance(parent_fields, pa.Schema) and not pa.types.is_struct(parent_fields):
275280
raise ValueError("column path has a non-struct parent")
@@ -279,11 +284,11 @@ def _column_change(self, operation: str, args: dict[str, Any]) -> ConstraintSnap
279284
existing = matches[0] if matches else None
280285
if operation == "add" and existing is not None:
281286
if args.get("if_column_not_exists"):
282-
return
287+
return None
283288
raise ValueError("column already exists")
284289
if operation != "add" and existing is None:
285290
if operation == "drop" and args.get("if_column_exists"):
286-
return
291+
return None
287292
raise ValueError("column does not exist")
288293
if (
289294
operation in ("drop", "rename", "type")
@@ -303,7 +308,7 @@ def _column_change(self, operation: str, args: dict[str, Any]) -> ConstraintSnap
303308
parent = _parent(row, path)
304309
if parent is not None:
305310
parent[path[-1]] = (
306-
pc.cast(_eval(default, info.schema_path), replacement.type, safe=True)[0].as_py()
311+
pc.cast(_eval(default, info.schema_path), present(replacement).type, safe=True)[0].as_py()
307312
if default is not None
308313
else None
309314
)
@@ -327,7 +332,12 @@ def _column_change(self, operation: str, args: dict[str, Any]) -> ConstraintSnap
327332
# annotations when replacing the primitive physical type.
328333
metadata = dict(existing.metadata or {})
329334
metadata.update(target.metadata or {})
330-
replacement = existing.with_type(target.type).with_metadata(metadata or None)
335+
replacement = existing.with_type(target.type)
336+
replacement = (
337+
replacement.with_metadata({k: v for k, v in metadata.items()})
338+
if metadata
339+
else replacement.remove_metadata()
340+
)
331341
_fields([replacement], info.schema_path)
332342
if args["expression"] is not None:
333343
# Bind before changes even if the table has no rows.
@@ -363,13 +373,17 @@ def _column_change(self, operation: str, args: dict[str, Any]) -> ConstraintSnap
363373
else:
364374
raise AssertionError(operation)
365375
assert replacement is not None
366-
replacement = replacement.with_metadata(metadata or None)
376+
replacement = (
377+
replacement.with_metadata({k: v for k, v in metadata.items()})
378+
if metadata
379+
else replacement.remove_metadata()
380+
)
367381
updated = replace_column_field(schema, path, replacement, add=operation == "add")
368382
constraints = self._changed_constraints(info, updated, operation, path, replacement)
369383
changed = not updated.equals(schema, check_metadata=True) or constraints != info.constraints
370384
info = replace(
371385
info,
372-
columns=updated.serialize().to_pybytes(),
386+
columns=SerializedSchema(updated.serialize().to_pybytes()),
373387
constraints=constraints,
374388
definition_revision=uuid.uuid4().bytes
375389
if changed and operation != "comment"
@@ -392,7 +406,9 @@ def _column_change(self, operation: str, args: dict[str, Any]) -> ConstraintSnap
392406
db.execute("UPDATE transactions SET version=?, data=? WHERE id=?", (version + 1, json.dumps(data), tx))
393407
if request is not None:
394408
return ConstraintSnapshot(
395-
table=request.table, definition_revision=info.definition_revision, constraints=info.constraints
409+
table=request.table,
410+
definition_revision=present(info.definition_revision),
411+
constraints=info.constraints,
396412
)
397413
return None
398414

@@ -406,8 +422,8 @@ def table_constraints_apply(self, request: ConstraintsApplyRequest) -> Constrain
406422
if change.operation == ConstraintOperation.ADD:
407423
definition = change.definition
408424
if (
409-
definition.kind != ConstraintKind.NOT_NULL
410-
or definition.declared_name is not None
425+
present(definition).kind != ConstraintKind.NOT_NULL
426+
or present(definition).declared_name is not None
411427
or change.enforcement != ConstraintEnforcement.ENFORCED
412428
or change.validation != ConstraintValidation.ALL_ROWS
413429
):
@@ -458,4 +474,9 @@ def table_column_comment_set(self, **kwargs: Any) -> None:
458474
class ColumnPathsWorker(Worker):
459475
"""Expose the durable column reference through the normal protocol worker."""
460476

477+
def _get_catalog(self) -> ColumnPathsCatalog:
478+
catalog = super()._get_catalog()
479+
assert isinstance(catalog, ColumnPathsCatalog)
480+
return catalog
481+
461482
catalog_interface = ColumnPathsCatalog

‎tests/catalog/constraints_protocol_reference.py‎

Lines changed: 65 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,21 +13,26 @@
1313
import os
1414
import sqlite3
1515
import uuid
16+
from collections.abc import Sequence
1617
from contextlib import closing
1718
from dataclasses import replace
18-
from typing import Any, ClassVar
19+
from typing import Any, ClassVar, Literal, overload
1920

2021
import pyarrow as pa
2122

2223
from vgi.catalog import (
2324
AttachOpaqueData,
2425
CatalogAttachResult,
26+
FunctionInfo,
27+
IndexInfo,
28+
MacroInfo,
2529
OnConflict,
2630
ReadOnlyCatalogInterface,
2731
SchemaInfo,
2832
SchemaObjectType,
2933
TableInfo,
3034
TransactionOpaqueData,
35+
ViewInfo,
3136
)
3237
from vgi.catalog.capabilities import (
3338
CONSTRAINT_CREATE,
@@ -38,7 +43,7 @@
3843
TRANSACTION_DDL,
3944
CatalogCapabilities,
4045
)
41-
from vgi.catalog.catalog_interface import CatalogContentsResult, SchemaContentsInfo
46+
from vgi.catalog.catalog_interface import CatalogContentsResult, SchemaContentsInfo, SerializedSchema
4247
from vgi.catalog.constraints import (
4348
ConstraintChange,
4449
ConstraintDefinition,
@@ -50,6 +55,7 @@
5055
ConstraintValidation,
5156
)
5257
from vgi.catalog.dependencies import CatalogObjectRef
58+
from vgi.schema_path import SchemaPath
5359
from vgi.worker import Worker
5460

5561
from .constraints_reference import ConstraintCatalog, _schema
@@ -111,7 +117,7 @@ def _info(record: dict[str, Any]) -> TableInfo:
111117
return TableInfo(
112118
name=snapshot.table.name,
113119
schema_path=snapshot.table.schema_path,
114-
columns=_schema(record).serialize().to_pybytes(),
120+
columns=SerializedSchema(_schema(record).serialize().to_pybytes()),
115121
constraints=snapshot.constraints,
116122
definition_revision=snapshot.definition_revision,
117123
comment=None,
@@ -144,14 +150,68 @@ def table_get(
144150
return None
145151
return self._info(record)
146152

153+
@overload
154+
def schema_contents(
155+
self,
156+
*,
157+
attach_opaque_data: AttachOpaqueData,
158+
transaction_opaque_data: TransactionOpaqueData | None,
159+
path: SchemaPath,
160+
type: Literal[SchemaObjectType.TABLE],
161+
) -> Sequence[TableInfo]: ...
162+
163+
@overload
164+
def schema_contents(
165+
self,
166+
*,
167+
attach_opaque_data: AttachOpaqueData,
168+
transaction_opaque_data: TransactionOpaqueData | None,
169+
path: SchemaPath,
170+
type: Literal[SchemaObjectType.VIEW],
171+
) -> Sequence[ViewInfo]: ...
172+
173+
@overload
174+
def schema_contents(
175+
self,
176+
*,
177+
attach_opaque_data: AttachOpaqueData,
178+
transaction_opaque_data: TransactionOpaqueData | None,
179+
path: SchemaPath,
180+
type: Literal[
181+
SchemaObjectType.SCALAR_FUNCTION,
182+
SchemaObjectType.TABLE_FUNCTION,
183+
SchemaObjectType.AGGREGATE_FUNCTION,
184+
],
185+
) -> Sequence[FunctionInfo]: ...
186+
187+
@overload
188+
def schema_contents(
189+
self,
190+
*,
191+
attach_opaque_data: AttachOpaqueData,
192+
transaction_opaque_data: TransactionOpaqueData | None,
193+
path: SchemaPath,
194+
type: Literal[SchemaObjectType.SCALAR_MACRO, SchemaObjectType.TABLE_MACRO],
195+
) -> Sequence[MacroInfo]: ...
196+
197+
@overload
198+
def schema_contents(
199+
self,
200+
*,
201+
attach_opaque_data: AttachOpaqueData,
202+
transaction_opaque_data: TransactionOpaqueData | None,
203+
path: SchemaPath,
204+
type: Literal[SchemaObjectType.INDEX],
205+
) -> Sequence[IndexInfo]: ...
206+
147207
def schema_contents(
148208
self,
149209
*,
150210
attach_opaque_data: bytes,
151211
transaction_opaque_data: bytes | None,
152212
path: list[str],
153213
type: SchemaObjectType,
154-
) -> list[TableInfo]:
214+
) -> Sequence[TableInfo | ViewInfo | FunctionInfo | MacroInfo | IndexInfo]:
155215
_, infos = self._snapshot_infos(attach_opaque_data, transaction_opaque_data)
156216
return [info for info in infos if info.schema_path == path] if type == SchemaObjectType.TABLE else []
157217

@@ -188,7 +248,7 @@ def table_create(
188248
self.authority.create(
189249
attach_opaque_data,
190250
CatalogObjectRef(kind="table", schema_path=schema_path, name=name),
191-
pa.ipc.read_schema(pa.BufferReader(columns)),
251+
pa.ipc.read_schema(pa.py_buffer(columns)),
192252
[ConstraintDefinition.deserialize_from_bytes(value) for value in constraint_definitions],
193253
tx=transaction_opaque_data,
194254
)

‎tests/catalog/constraints_reference.py‎

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ def _table_ref(record: dict[str, Any]) -> CatalogObjectRef:
8888

8989

9090
def _base_schema(record: dict[str, Any]) -> pa.Schema:
91-
return pa.ipc.read_schema(pa.BufferReader(_unb64(record["schema"])))
91+
return pa.ipc.read_schema(pa.py_buffer(_unb64(record["schema"])))
9292

9393

9494
def _rows(record: dict[str, Any]) -> pa.Table:
@@ -104,7 +104,10 @@ def _schema(record: dict[str, Any]) -> pa.Schema:
104104
for path in info.definition.column_paths
105105
}
106106
base = _base_schema(record)
107-
return pa.schema([field.with_nullable(field.name not in required) for field in base], metadata=base.metadata)
107+
return pa.schema(
108+
[field.with_nullable(field.name not in required) for field in base],
109+
metadata=None if base.metadata is None else {key: value for key, value in (base.metadata).items()},
110+
)
108111

109112

110113
def _touch(record: dict[str, Any]) -> None:
@@ -203,6 +206,7 @@ def _lookup(data: dict[str, Any], table: CatalogObjectRef) -> dict[str, Any]:
203206
record = data.get(_table_key(table))
204207
if record is None:
205208
raise ValueError("table does not exist")
209+
assert isinstance(record, dict)
206210
return record
207211

208212
@staticmethod
@@ -322,7 +326,12 @@ def create(
322326
key = _table_key(table)
323327
if key in data:
324328
raise ValueError("table already exists")
325-
base = pa.schema([field.with_nullable(True) for field in columns], metadata=columns.metadata)
329+
base = pa.schema(
330+
[field.with_nullable(True) for field in columns],
331+
metadata=None
332+
if columns.metadata is None
333+
else {key: value for key, value in (columns.metadata).items()},
334+
)
326335
record = {
327336
"table": _b64(table.serialize_to_bytes()),
328337
"schema": _b64(base.serialize().to_pybytes()),

0 commit comments

Comments
 (0)