Conversation
| # KIND, either express or implied. See the License for the | ||
| # specific language governing permissions and limitations | ||
| # under the License. | ||
| """Validate the conformance fixtures. |
There was a problem hiding this comment.
I know this is in draft
I had pictured using JSON schema for doing the JSON validation so we didn't have to roll our own. It wouldn't handle the unique id, but it would do everything else. What are your thoughts?
There was a problem hiding this comment.
Agreed. Thanks for the early feedback. Still iterating. I'll move the per-case validation to a JSON Schema and keep a thin script only for the unique-id check (and the clause/spec_ref requirement, which is types-only for this iteration then expandable).
| @@ -0,0 +1,52 @@ | |||
| { | |||
There was a problem hiding this comment.
I know this is what I had initially committed in the POC, but I began to think that maybe type string serialization is too granular to enforce.
I moved the test surface level up to the schema level here: #3
PTAL
There was a problem hiding this comment.
Agreed type-string serialization is the finest grain and shouldn't gate anyone on its own. We're keeping types as its own surface, separate from the schema surface in #3 , so an impl can opt into it or skip it - the manifest plus the --surface filter in the next revision make that opt-out real, and the quoted-CRS and whitespace cases stay marked recommended, not required.
There was a problem hiding this comment.
I think I'm okay with this either way, as long as the tests cover different edge cases in different surfaces, and the subscription path makes sense
sungwy
left a comment
There was a problem hiding this comment.
Thanks @nssalian, I think the direction is good and the POC makes the trade-offs concrete.
As I reviewed this implementation, I had a few thoughts around those trade-offs I wanted to enumerate. These are the trade-offs between having runner-based execution owned by iceberg-verification vs. a submodule subscription model owned by each implementation. Not mutually exclusive, but they optimize for different things.
| Axis | Runner-based | Submodule subscription |
|---|---|---|
| When divergence is caught | Nightly, post-merge | Pre-merge, on the implementation's PR |
| Who sees the failure | This repo's CI, then relayed | The contributor who caused it |
iceberg-verification complexity |
High: one runner per language per surface | Low: fixtures plus a documented format |
| Implementation-side complexity | None | Medium: one submodule based integration, kept current by a dependabot |
| Assertion semantics authored by | This repo | The implementation that owns them |
My main concern with the runners is that the runner has to reimplement assertion semantics per language and per surface. The example we have with type strings are supposed to be an easy case. Once this reaches manifests, deletion vectors, snapshot lineage or partition evolution, each surface has its assertion, and we'd be writing that multiple times, by people who don't maintain those language implementations.
I think it would be natural for a contributing on the implementation repo to want to reach for committed fixtures to test against, when they are developing within their PR. For example, if we had fixtures and references helping verify V4 manifest deletion vector handling, I can't see why contributors wouldn't want to fetch the fixtures to verify their code.
I don't think we drop runners, but these trade-offs makes me wonder if we want to use them for different levels of conformance checks. I think keeping runners would be best for nightly runs to verify a small set of high level, cross-cutting assertions where we can verify if there anything wrong with a specific implementation that should be looked into. A V4 reference-table assertion across a few reference tables is roughly the altitude I mean. Anything with low-level surface semantics feels like a better fit for the submodule path.
WDYT?
| if "input" not in case: | ||
| errors.append(f"{where}: missing 'input'") | ||
|
|
||
| if valid is True and "decoded" not in case: | ||
| errors.append(f"{where}: valid case must have 'decoded'") | ||
| if valid is False and "decoded" in case: | ||
| errors.append(f"{where}: invalid case must not have 'decoded'") |
There was a problem hiding this comment.
It looks like we are being prescriptive of the input and expected values format of the fixtures. Do we anticipate that it'll be the same shape in all of our cases, or is this validation script narrowly targeting the type checks?
There was a problem hiding this comment.
Good point - it shouldn't be one fixed shape. In the next revision I want to move the validation to JSON Schema with one schema per surface: the types schema owns the type-string and decoded shape and requires clause/spec_ref, and the schema surface will declare its own.
There was a problem hiding this comment.
I think POCing this, so that the runner adoption can scale across different surfaces would be an important pre-requisite
| // decodedShape maps an iceberg-go type to the fixture's language-neutral | ||
| // `decoded` shape. Numbers are float64 to match json-decoded expectations. | ||
| // supported=false means iceberg-go has no such type. | ||
| func decodedShape(t iceberg.Type) (map[string]any, bool) { |
There was a problem hiding this comment.
The need for applying language implementation specific wrapper methods to map the input and decoded values makes me want to take a step back from this approach.
This type of handling will be necessary whether that be through a iceberg-verification based runner executed model, or through a submodule based model where each implementation chooses the surfaces they want to test.
I understand why we'd want to try to keep the verification code away from the implementations, and in this repository, but I think maintaining different language implementations in a non-language repository feels like an considerable amount of overhead to me.
There was a problem hiding this comment.
Yeah I agree this is a bit of an overhead. It doesn't disappear in the submodule model though. The same object-to-shape mapping still gets written, just in the impl's repo. The reason to keep it here for a surface like types is that the mapping is small and needs no impl internals, so writing it centrally means an impl gets checked without wiring anything up. For deep surfaces where the mapping needs impl internals, I agree it belongs with the impl.
| set +e | ||
| mkdir -p /tmp/status | ||
| out=/tmp/status/iceberg-java.txt | ||
| if ! ./gradlew installDist --quiet -PicebergVersion=1.12.0-SNAPSHOT ; then |
There was a problem hiding this comment.
good use of SNAPSHOT artifact here. Is there a way to deduce the current version, instead of pinning it so that it reduces the maintenance overhead?
There was a problem hiding this comment.
I used this directly since it was already built by the nightly and I've scheduled it such that it runs after the iceberg nightly. For the others it's the latest commit hash at the time. Let me figure out how to make the version more explicit.
|
Thanks for taking a look @sungwy. I put this up to help start the conversation. I think hybrid shape is right - we shouldn't stand up a runner for every surface. But I'd split by what the assertion needs, not by pre-merge vs post-merge. Where the answer is spec-derivable with no implementation-specific knowledge, the central runner is cheap and, more to the point, needs zero buy-in from an implementation to produce a signal - an impl that never wires up a submodule still gets checked. Types is the clear case: it comes straight out of the Primitive Types table and Appendix C, so I'd keep it here as a standalone surface. Where the assertion genuinely needs impl internals (manifests, deletion vectors, snapshot lineage), I agree the impl should own the check and the submodule/pin path is the right home. Two things I'd push on before leaning hard on the submodule model for the low-level surfaces.
To make the subscribe path concrete, the next revision I plan to put in adds a surfaces manifest, a |
|
The PR has a large surface area to cover. It's best to split this. I'll open smaller PRs to capture this one. I'll leave this one open until those split are ready so if anyone hasn't taken a look, can chime in here. |
|
I'll close this one so it's not distracting for reviews. I've begun splitting the work into the types, then the runners, etc |
Rationale for this change
Iceberg implementations each parse the spec on their own and drift. Tests kept inside each implementation cannot catch a shared misreading, because each checks its own reading against itself. This adds the first conformance surface: spec-derived type fixtures (a single answer key from
format/spec.md), one runner per language, and CI that reports where an implementation diverges.Contents:
table-spec/types/- fixtures forprimitive,variant,nested, andgeospatial(onecases.jsonper subdirectory), plustable-spec/manifest.jsonlisting the surfaces a consumer can subscribe to. The contract is intable-spec/types/README.md:parse(input) == decoded,validmarks accept vs reject, and an optionalcanonicalrequires byte-exact re-serialization.runners/{go,rust,python,java}/- one runner per language; each parsesinputwith the implementation's own type parser and reports PASS, FAIL, or UNSUPPORTED. A--surface <name>flag (orCONFORMANCE_SURFACES) runs a subset.apache/mainHEAD; Java tracks the latest publishediceberg-apiSNAPSHOT (nightly). Reporting is informational: a divergence is recorded as an annotation, and only a runner build or setup error marks a check failed. A nightly workflow re-runs all four and renders an aggregate matrix.dev/validate-fixtures.py- validates eachcases.jsonagainst a per-surface JSON Schema (dev/schema/), plus globally-unique ids and manifest consistency.CONTRIBUTING.md- repository layout, the two-tier model (central runners for spec-derivable surfaces; per-implementation submodule subscription for surfaces that need implementation internals), the fixture format, and how to subscribe.Are these changes tested?
Yes. A fork PR run exercised all four language lanes plus RAT and fixture validation: Go and Java report no divergences; pyiceberg and Rust each surface real spec divergences (pyiceberg requires a quoted geospatial CRS; Rust accepts
decimalprecision above 38 and an unterminatedfixed[), reported as annotations. Each runner was also run locally against the implementation'sapache/main, anddev/validate-fixtures.pyis exercised with both valid and deliberately-broken fixtures.Are there any user-facing changes?
No changes to any existing implementation. This adds a new, opt-in repository surface: an implementation subscribes to the fixtures it wants and runs them, and nothing here blocks another project's CI.
AI Disclosure
--surfacerunner filter, and a CONTRIBUTING guide; make CI track each implementation's dev HEAD and report divergences without blocking.