Skip to content

Commit ae602f5

Browse files
authored
fix(client-contract): keep vectors servable and judge every column by same_value (#34)
The nested-cells vector's flags column holds one item type, the validator reports a vector column Arrow cannot build, and served_table judges flat columns by same_value as it does nested ones.
1 parent c7a5569 commit ae602f5

3 files changed

Lines changed: 72 additions & 16 deletions

File tree

‎python/client-contract/src/dfo_client_contract/rules/reading-order.json‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1479,7 +1479,7 @@
14791479
1.0
14801480
],
14811481
[
1482-
1
1482+
false
14831483
]
14841484
]
14851485
]
@@ -1497,7 +1497,7 @@
14971497
2020,
14981498
4,
14991499
[
1500-
1
1500+
false
15011501
],
15021502
{
15031503
"b": 2,

‎tools/tests/test_validate_client_contract.py‎

Lines changed: 56 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import json
2222
import os
2323
import shutil
24+
import struct
2425
import sys
2526
from collections.abc import Callable
2627
from pathlib import Path
@@ -176,6 +177,15 @@ def _selector_query_with_cut(doc: Any) -> None:
176177
exchange["request"]["path"] += "&without_party_totals=true"
177178

178179

180+
def _time_period_written_as_text(vector: Any) -> None:
181+
"""The second member serves its years as text, a valid Arrow column of
182+
its own that mixes types with the first member's in the stacked table."""
183+
member = vector["members"][1]
184+
at = member["columns"].index("time_period")
185+
for row in member["rows"]:
186+
row[at] = str(row[at])
187+
188+
179189
def _vector(name: str, change: Callable[[Any], None]) -> Callable[[Any], None]:
180190
return lambda doc: change(next(v for v in doc["vectors"] if v["name"] == name))
181191

@@ -1817,10 +1827,25 @@ def change(doc: Any) -> None:
18171827
(
18181828
"rules/reading-order.json",
18191829
_vector(
1820-
"ties-keep-served-order",
1821-
lambda v: v["members"][0]["rows"][0].__setitem__(4, "2022"),
1830+
"nested-cells-compare-by-value",
1831+
lambda v: v["members"][0]["rows"][1].__setitem__(4, [1]),
1832+
),
1833+
"vector 'nested-cells-compare-by-value' member 0 column 'flags' is no "
1834+
"Arrow column",
1835+
),
1836+
(
1837+
"rules/reading-order.json",
1838+
_vector(
1839+
"nested-cells-compare-by-value",
1840+
lambda v: v["members"][0]["rows"][1].__setitem__(2, {"a": "x", "b": 2}),
18221841
),
1823-
"vector 'ties-keep-served-order' mixes JSON types in column",
1842+
"vector 'nested-cells-compare-by-value' member 0 column 'meta' is no "
1843+
"Arrow column",
1844+
),
1845+
(
1846+
"rules/reading-order.json",
1847+
_vector("stack-crs-and-dac2a", _time_period_written_as_text),
1848+
"vector 'stack-crs-and-dac2a' mixes JSON types in column",
18241849
),
18251850
(
18261851
"scenarios/fetch-ref-expired-file-selector.json",
@@ -2117,6 +2142,8 @@ def change(doc: Any) -> None:
21172142
"reading-order-null-sorted-first",
21182143
"reading-order-without-reading-columns",
21192144
"reading-order-vector-row-ragged",
2145+
"reading-order-vector-list-items-mix-types",
2146+
"reading-order-vector-struct-field-mixes-types",
21202147
"reading-order-vector-column-mixes-types",
21212148
"selector-query-carries-the-cut",
21222149
"unlisted-artifact-selector-query-carries-the-cut",
@@ -2166,6 +2193,32 @@ def _parquet(table: pa.Table) -> bytes:
21662193
return sink.getvalue().to_pybytes()
21672194

21682195

2196+
def _nan(payload: int) -> float:
2197+
return struct.unpack("<d", struct.pack("<Q", 0x7FF8000000000000 | payload))[0]
2198+
2199+
2200+
@pytest.mark.parametrize(
2201+
"cells",
2202+
[[0.0, -0.0], [_nan(0), _nan(1)]],
2203+
ids=["signed-zeros", "nan-payloads"],
2204+
)
2205+
def test_a_vector_and_its_parquet_file_group_columns_alike(cells: list[float]) -> None:
2206+
"""Cells `same_value` holds the same, which Arrow's `unique` tells apart,
2207+
are one value on both paths."""
2208+
rows = [[2021 - i, "x", cell] for i, cell in enumerate(cells)]
2209+
columns = ["time_period", "label", "amount"]
2210+
vector = validator.table_from_rows(columns, rows)
2211+
served = validator.served_table(
2212+
_parquet(
2213+
pa.table({name: [row[i] for row in rows] for i, name in enumerate(columns)})
2214+
)
2215+
)
2216+
assert "amount" in served.same
2217+
assert validator.column_groups([served], []) == validator.column_groups(
2218+
[vector], []
2219+
)
2220+
2221+
21692222
def test_a_served_table_reads_list_struct_and_nan_columns() -> None:
21702223
"""Columns `unique` cannot hash are judged cell by cell, and a NaN column
21712224
holds one value, in one table and over stacked ones."""

‎tools/validate_client_contract.py‎

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1906,17 +1906,13 @@ def served_table(data: bytes) -> ServedTable:
19061906
nested = frozenset(
19071907
field.name for field in table.schema if pa.types.is_nested(field.type)
19081908
)
1909+
# Every column is judged by `same_value`, as `table_from_rows` judges a
1910+
# vector's, so a vector and its Parquet file group their columns alike.
19091911
same = {}
19101912
for name in table.column_names if table.num_rows else ():
1911-
column = table.column(name)
1912-
# Arrow's `unique` counts NaN once and cannot hash a nested type,
1913-
# whose cells `same_value` compares.
1914-
if name in nested:
1915-
values = column.to_pylist()
1916-
if _one_value(values):
1917-
same[name] = values[0]
1918-
elif len(distinct := column.unique()) == 1:
1919-
same[name] = distinct[0].as_py()
1913+
values = table.column(name).to_pylist()
1914+
if _one_value(values):
1915+
same[name] = values[0]
19201916
return ServedTable(tuple(table.column_names), table.num_rows, same, nested)
19211917

19221918

@@ -3774,8 +3770,10 @@ def _json_type(value: Any) -> str:
37743770

37753771
def _unorderable(members: Sequence[Mapping[str, Any]]) -> str | None:
37763772
"""Why a reading-order vector's members cannot be ordered, or None: a
3777-
row whose cells do not match its member's columns, or a column whose
3778-
cells, nulls aside, mix JSON types, which the rule gives no order."""
3773+
row whose cells do not match its member's columns, a member's column
3774+
whose cells cannot form one Arrow column, as a served Parquet file holds
3775+
them, or a column whose cells, nulls aside, mix JSON types across the
3776+
stacked members, which the rule gives no order."""
37793777
types: dict[str, set[str]] = {}
37803778
for index, member in enumerate(members):
37813779
columns = member["columns"]
@@ -3788,6 +3786,11 @@ def _unorderable(members: Sequence[Mapping[str, Any]]) -> str | None:
37883786
for name, cell in zip(columns, row, strict=True):
37893787
if cell is not None:
37903788
types.setdefault(name, set()).add(_json_type(cell))
3789+
for position, name in enumerate(columns):
3790+
try:
3791+
pa.array([row[position] for row in member["rows"]])
3792+
except pa.ArrowException as exc:
3793+
return f"member {index} column {name!r} is no Arrow column: {exc}"
37913794
mixed = sorted(name for name, found in types.items() if len(found) > 1)
37923795
if mixed:
37933796
return f"mixes JSON types in column {mixed[0]!r}"

0 commit comments

Comments
 (0)