Skip to content

[FEATURE](schema) Model axle count as a bounded whole number over VehicleSelector#553

Open
Seth Fitzsimmons (sethfitz) wants to merge 3 commits into
pyspark-expression-codegenfrom
union-type-compat
Open

[FEATURE](schema) Model axle count as a bounded whole number over VehicleSelector#553
Seth Fitzsimmons (sethfitz) wants to merge 3 commits into
pyspark-expression-codegenfrom
union-type-compat

Conversation

@sethfitz

@sethfitz Seth Fitzsimmons (sethfitz) commented Jul 10, 2026

Copy link
Copy Markdown
Collaborator

Closes #472. Rebased onto #569 (pyspark-expression-codegen).

Models VehicleAxleCountSelector.value as a bounded whole-number float64 so the flattened value column across every VehicleSelector arm has one Spark type, and teaches the codegen to carry union arms whose same-name fields diverge only in their constraints.

  • value becomes float64 with Field(ge=1, le=100, multiple_of=1) (was uint8). multiple_of=1 rejects fractional counts; the [1, 100] bound anchors it to real axle counts and rejects the non-finite doubles a bare float64 would admit, so pydantic and the generated Spark checks agree — no custom constraint class, and native keywords emit standard JSON Schema (minimum/maximum/multipleOf).
  • The codegen dispatches native multiple_of to a new check_multiple_of(col, n) PySpark expression, with conformance coverage and markdown prose.
  • A field's lower and upper bounds merge into one check_bounds(ge, le) (also fixes a latent two-sided-bound case like ConfidenceScore).
  • Same-name union fields whose per-arm constraints diverge become separate arm-gated checks rather than being collapsed or widened; every arm must agree on Spark type (raising otherwise); generation fails loudly if two same-name checks land in one arm.

The generated Segment value column was already DoubleType via widening; it is now DoubleType natively, with arm-gated bounds and multiple-of checks.

Why native multiple_of rather than a custom constraint

An earlier iteration added a bespoke IntegerConstraint class to express "whole number." That was unnecessary: pydantic's built-in Field(multiple_of=1) says exactly that — we simply hadn't been using it. Native multiple_of is the cleaner foundation:

  • No custom class, and no annotation-ordering pitfall — one Field carries the bounds and the integrality together, so there is no way to order them wrong (the custom class had to sit after the bound or it leaked a non-standard ge into the JSON Schema instead of minimum).
  • It emits standard JSON Schema natively.
  • It generalizes: the codegen dispatches any multiple_of=n to check_multiple_of(col, n), not just the integral case.

Whole-number semantics that the custom class handled explicitly (rejecting non-finite doubles) are preserved by the field's own [1, 100] bound, which excludes inf/NaN on both the pydantic and Spark sides.

@sethfitz Seth Fitzsimmons (sethfitz) added the change type - minor 🤏 Minor schema change. See https://lf-overturemaps.atlassian.net/wiki/x/GgDa label Jul 10, 2026
@github-actions

github-actions Bot commented Jul 10, 2026

Copy link
Copy Markdown

🗺️ Schema reference docs preview is live!

🌍 Preview https://staging.overturemaps.org/schema/pr/553/schema/index.html
🕐 Updated Jul 20, 2026 20:12 UTC
📝 Commit 68bae42
🔧 env SCHEMA_PREVIEW true

Note

♻️ This preview updates automatically with each push to this PR.

…ression

Recognize pydantic's Field(multiple_of=n) in the codegen and generate a
check_multiple_of(col, n) PySpark check that fires when col % n is
non-zero.  multiple_of=1 is the integral (whole-number) case; testing
the remainder in double space matches pydantic for large integral
doubles, NaN, and infinity.

Wire it through the pipeline: constraint dispatch, the check_multiple_of
runtime expression, conformance invalid-value generation (a non-multiple
derived from the divisor), and the markdown constraint description.

Signed-off-by: Seth Fitzsimmons <seth@mojodna.net>
@sethfitz
Seth Fitzsimmons (sethfitz) changed the base branch from pyspark-codegen to pyspark-expression-codegen July 20, 2026 18:46
@sethfitz Seth Fitzsimmons (sethfitz) changed the title [FEATURE](schema) Add IntegerConstraint; fail codegen on divergent union column types [FEATURE](schema) Model axle count as a bounded whole number over VehicleSelector Jul 20, 2026
…ounds

Field(ge=1, le=100) yields separate Ge and Le constraints, each
dispatched to its own check_bounds. They describe one range on one
column, so they collapse into a single check_bounds(ge=1, le=100): one
violation identity rather than two same-name bounds checks a split union
arm cannot label distinctly. This also folds a two-sided bound like
ConfidenceScore's ge=0.0, le=1.0 into one check instead of two.

Signed-off-by: Seth Fitzsimmons <seth@mojodna.net>
…Selector

VehicleAxleCountSelector.value was uint8; every other VehicleSelector
arm is float64. Flattened to one Spark value column, the arms disagreed
on type and the codegen widened uint8 to DoubleType -- an implicit
compatibility requirement nothing in the model stated.

Model value as float64 with multiple_of=1 (whole number) and a [1, 100]
bound. The upper bound anchors it to real axle counts and, with the
lower bound, rejects the non-finite doubles a bare float64 would admit
-- so pydantic and the generated Spark checks agree without a custom
constraint class.

Extend the codegen to keep same-name union fields whose per-arm
constraints diverge (axle count's bounded whole number vs the other
arms' ge=0) as separate arm-gated checks rather than collapsing or
widening them, require every arm to agree on Spark type (raising
otherwise), and fail generation loudly if two same-name checks ever land
in one arm with indistinguishable violation labels.

Refs #472

Signed-off-by: Seth Fitzsimmons <seth@mojodna.net>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

change type - minor 🤏 Minor schema change. See https://lf-overturemaps.atlassian.net/wiki/x/GgDa

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant