Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 46 additions & 18 deletions adbc_drivers_validation/generate_documentation.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,15 +80,15 @@ class CustomFeatures:
@dataclasses.dataclass(frozen=True)
class TypeTableEntry:
lhs: str
rhs: str
rhs: tuple[str, ...]
result: typing.Literal["passed", "partial", "failed"]
footnotes: tuple[str, ...] = dataclasses.field(default_factory=tuple)
# e.g. for BigQuery, we may have both "ingest (default)" and "ingest (with
# Storage Write API)" for the same type
variant: str | None = None

def render_rhs(self) -> str:
rhs = self.rhs
rhs = ", ".join(self.rhs)
if self.result == "failed":
rhs = "❌"
elif self.result == "partial":
Expand All @@ -97,6 +97,39 @@ def render_rhs(self) -> str:
rhs += footnote
return rhs

def merge(self, other: "TypeTableEntry") -> "TypeTableEntry":
if self.lhs != other.lhs:
raise ValueError(f"Cannot merge lhs {self.lhs} and {other.lhs}")
if self.variant != other.variant:
raise ValueError(f"Cannot merge variant {self.variant} and {other.variant}")

# failed + failed = failed
# failed + (other) = partial
# partial + (anything) or (anything) + partial = partial
# passed + passed = passed
rhs = tuple(sorted(set(self.rhs) | set(other.rhs)))
if self.result == "failed" and other.result == "failed":
result = "failed"
elif self.result == "failed":
result = "partial"
# we don't want the failed side to contribute to the RHS
rhs = other.rhs
elif other.result == "failed":
result = "partial"
rhs = self.rhs
elif self.result == "partial" or other.result == "partial":
result = "partial"
else:
result = "passed"

return TypeTableEntry(
lhs=self.lhs,
rhs=rhs,
result=result,
footnotes=tuple(sorted(set(self.footnotes) | set(other.footnotes))),
variant=self.variant,
)


@dataclasses.dataclass
class DriverTypeTable:
Expand Down Expand Up @@ -235,7 +268,7 @@ def add_table_entry(
vendor_version: str,
category: typing.Literal["select", "bind", "ingest"],
lhs: str,
rhs: str,
rhs: str | list[str],
test_case: dict[str, typing.Any],
*,
extra_caveats: list[str] | None = None,
Expand Down Expand Up @@ -273,7 +306,7 @@ def add_table_entry(
caveats.append(skip_reason)
else:
query_kind = query_name.split("/")[1]
caveats.append(f"{query_kind} is not supported for {rhs}")
caveats.append(f"{query_kind} is not supported")

caveats.extend(extra_caveats or [])

Expand All @@ -288,7 +321,7 @@ def add_table_entry(
getattr(version, f"type_{category}").append(
TypeTableEntry(
lhs=lhs,
rhs=rhs,
rhs=tuple(rhs) if isinstance(rhs, list) else (rhs,),
result=result,
footnotes=footnotes,
variant=variant,
Expand Down Expand Up @@ -489,11 +522,8 @@ def render(
for c in column_order:
entries = columns[c].get(k)
if entries:
# TODO: more sophisticated merging? Or break out columns for
# different (major) versions; or simply hope that we can treat
# different major versions as different "vendors" which will
# break out a column for them
all_cells.append(", ".join(entry.render_rhs() for entry in entries))
entry = functools.reduce(lambda a, b: a.merge(b), entries)
all_cells.append(entry.render_rhs())
else:
all_cells.append("(NA/not tested)")

Expand Down Expand Up @@ -549,8 +579,8 @@ def render(
for c in column_order[v]:
entries = columns[v][c].get(k)
if entries:
# TODO: more sophisticated merging?
all_cells.append(", ".join(entry.render_rhs() for entry in entries))
entry = functools.reduce(lambda a, b: a.merge(b), entries)
all_cells.append(entry.render_rhs())
else:
all_cells.append("(NA/not tested)")

Expand Down Expand Up @@ -997,21 +1027,19 @@ def generate_includes(driver: str, get_quirks: GetQuirks) -> ValidationReport:
)
extra_caveats = []
if len(arrow_type_names) != 1:
arrow_type = ", ".join(sorted(arrow_type_names))
extra_caveats.append(
"Return type is inconsistent depending on how the query was written"
)
else:
arrow_type = next(iter(arrow_type_names))

arrow_type = html.escape(arrow_type)
arrow_types = [
html.escape(arrow_type) for arrow_type in sorted(arrow_type_names)
]
sql_type = html.escape(test_case["sql_type"])
report.add_table_entry(
test_case["vendor"],
test_case["vendor_version"],
"select",
sql_type,
arrow_type,
arrow_types,
test_case,
extra_caveats=extra_caveats,
)
Expand Down
57 changes: 57 additions & 0 deletions tests/test_generate_documentation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
# Copyright (c) 2026 ADBC Drivers Contributors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import pytest

from adbc_drivers_validation.generate_documentation import TypeTableEntry


def test_type_table_entry() -> None:
# fmt:off
entry1 = TypeTableEntry("1", ("a",), "passed", (" [f1]",), None)
entry2 = TypeTableEntry("1", ("b", "c"), "passed", (" [f2]",), None)
entry3 = TypeTableEntry("1", ("d",), "partial", (" [f3]", " [f4]",), None)
entry4 = TypeTableEntry("1", ("e",), "failed", (" [f5]",), None)
entry5 = TypeTableEntry("1", ("b",), "passed", (), None)
entry6 = TypeTableEntry("1", ("b", "d"), "partial", (" [f6]",), None)
entry7 = TypeTableEntry("1", ("a", "b"), "failed", (" [f1]",), None)
entry8 = TypeTableEntry("2", ("a",), "passed", (" [f1]",), None)
entry9 = TypeTableEntry("1", ("b",), "passed", (" [f2]",), "Variant")
# fmt:on

assert entry1.render_rhs() == "a [f1]"
assert entry2.render_rhs() == "b, c [f2]"
assert entry3.render_rhs() == "d ⚠️ [f3] [f4]"
assert entry4.render_rhs() == "❌ [f5]"

assert entry1.merge(entry5).render_rhs() == "a, b [f1]"
assert entry5.merge(entry1).render_rhs() == "a, b [f1]"
assert entry1.merge(entry3).render_rhs() == "a, d ⚠️ [f1] [f3] [f4]"
assert entry3.merge(entry1).render_rhs() == "a, d ⚠️ [f1] [f3] [f4]"
assert entry1.merge(entry4).render_rhs() == "a ⚠️ [f1] [f5]"
assert entry4.merge(entry1).render_rhs() == "a ⚠️ [f1] [f5]"

assert entry2.merge(entry6).render_rhs() == "b, c, d ⚠️ [f2] [f6]"
assert entry6.merge(entry2).render_rhs() == "b, c, d ⚠️ [f2] [f6]"
assert entry2.merge(entry4).render_rhs() == "b, c ⚠️ [f2] [f5]"
assert entry4.merge(entry2).render_rhs() == "b, c ⚠️ [f2] [f5]"

assert entry4.merge(entry7).render_rhs() == "❌ [f1] [f5]"
assert entry7.merge(entry4).render_rhs() == "❌ [f1] [f5]"

with pytest.raises(ValueError, match="1 and 2"):
entry1.merge(entry8)

with pytest.raises(ValueError, match="None and Variant"):
entry1.merge(entry9)