Skip to content

fix: SPARQL 1.1 numeric arithmetic and expression ORDER BY correctness#1282

Merged
bplatz merged 6 commits into
mainfrom
fix/aggregate-math
Jun 3, 2026
Merged

fix: SPARQL 1.1 numeric arithmetic and expression ORDER BY correctness#1282
bplatz merged 6 commits into
mainfrom
fix/aggregate-math

Conversation

@bplatz

@bplatz bplatz commented Jun 3, 2026

Copy link
Copy Markdown
Contributor

Summary

Fixes three SPARQL 1.1 correctness gaps in aggregate/analytic query evaluation. Each is an independent spec-compliance issue in core expression handling.

1. Mixed numeric-type arithmetic no longer errors

Arithmetic between two different XSD numeric types — e.g. xsd:float(?x) / ?int — failed with a type mismatch. XPath operator mapping requires numeric type promotion: the narrower operand is promoted to the wider type and the operation proceeds. Numeric operands carried as typed literals (string-backed xsd:float, the full integer family including values needing BigInt, xsd:decimal, and the INF/-INF/NaN float lexicals) are now normalized through the canonical core coercion layer before the operation, so promotion applies uniformly instead of falling through to an error.

2. Integer division yields xsd:decimal

xsd:integer / xsd:integer truncated to an integer (10 / 42) — a silent wrong-value deviation. Per XPath op:numeric-divide, integer division yields xsd:decimal (10 / 42.5). Division now returns a decimal, with precision capped at 34 significant digits (IEEE-754 decimal128, matching AVG) so recurring quotients stay bounded. As elsewhere, xsd:decimal serializes as a string to preserve exactness, while xsd:double remains a JSON number.

3. Expression-based ORDER BY is supported

ORDER BY with an expression rather than a bare variable was rejected at lowering time. SPARQL 1.1 §15.1 permits any expression as an order condition. Expression order keys (e.g. ORDER BY DESC(?a / ?b)) are now desugared to a synthetic per-solution bind evaluated after grouping/aggregation and before the sort, so the key may reference GROUP BY keys, aggregate outputs, and SELECT-expression aliases. Inline aggregates in the order condition (ORDER BY DESC(COUNT(?x)), SPARQL 1.1 §18.2.4.1) are hoisted into the grouping phase and deduplicated against any matching SELECT aggregate so the count is computed once.

Scope / limitations

  • Expression-based ORDER BY inside a subquery is intentionally rejected: the subquery operator sorts its projected output and has no stage to evaluate a synthetic sort key. Top-level queries are unaffected.
  • xsd:float arithmetic uses the existing double representation (no distinct 32-bit float type), consistent with how xsd:float/xsd:double already collapse elsewhere in the engine.

bplatz added 5 commits June 3, 2026 10:02
Two related SPARQL 1.1 arithmetic bugs in the expression evaluator
(ArithmeticOp::apply), found by the BSBM Business Intelligence query mix:

- Mixed-type arithmetic such as xsd:float(?x) / ?int errored with a type
  mismatch. xsd:float casts are carried as a String-backed TypedLiteral, so
  the operand never matched a numeric arm. Coerce numeric TypedLiteral
  operands (xsd:float/double/decimal/integer) to their primitive variant
  before applying, letting XPath numeric type promotion proceed.

- xsd:integer / xsd:integer truncated to an integer (10 / 4 -> 2). Per XPath
  op:numeric-divide, integer division yields xsd:decimal (10 / 4 -> 2.5).
  Long/Long and BigInt/BigInt division now return a decimal, capped at 34
  significant digits (IEEE-754 decimal128), mirroring AVG.

Adds unit tests plus SPARQL and JSON-LD parity integration tests.
Addresses review of the prior arithmetic commit. coerce_numeric_operand
hand-rolled only a partial XSD numeric coercion (f64 for float/double, i64
for integer/long/int), so numeric typed literals like xsd:short, large
xsd:integer values needing BigInt, or the INF/-INF/NaN float lexicals fell
through to a TypeMismatch.

Delegate string->numeric parsing to fluree_db_core::coerce_value, which
already covers the full XSD numeric lattice: the integer family (with BigInt
overflow and per-type range validation), xsd:decimal, and xsd:float/double
including special lexicals. Only a numeric result is adopted; non-numeric
datatypes and parse failures keep the literal as-is so it still falls to a
TypeMismatch in apply.

Adds a unit test covering xsd:short, an oversized xsd:integer (BigInt), and
the xsd:float INF lexical.
Expression ORDER BY (e.g. `ORDER BY DESC(xsd:float(?c) / ?n)`) was rejected at
lowering. Desugar each non-trivial order key into a synthetic bind computed once
per solution, carried on a new `Query.order_binds` field and applied as a
dedicated stage after grouping/aggregation/HAVING/post-binds and before the sort.

- Uniform across no-grouping, dedup-only GROUP BY, and aggregating queries.
- Under grouping, validate the order expression references only group keys,
  aggregate outputs, or post-binds (clean error instead of evaluating over a
  Grouped binding).
- Allow ORDER BY on post-aggregation bind outputs (e.g. `ORDER BY ?ceil`).
- Fast paths that sort on `query.ordering` decline when order binds are present;
  the order-bind stage runs only in the generic pipeline.
- Subquery expression ORDER BY is rejected at lowering rather than silently
  mis-sorting on a grabbed variable; bare/bracketed variables still work.

Tests: lowering + execution coverage incl. dedup-only GROUP BY, grouped-var
rejection, the stats count-by-predicate fast path, and the BSBM BI Q5 ratio shape.
(+ ?sal (/ ?sal 10)) now yields an xsd:decimal because xsd:integer /
xsd:integer returns xsd:decimal per XPath op:numeric-divide. The summed
salaries are exact decimals (110, 220) rendered as strings; the untouched
sales employee stays an integer. Adjusts assertions accordingly.
Allow an aggregate call directly inside an ORDER BY expression
(e.g. `ORDER BY DESC(COUNT(?x))` or `DESC(COUNT(?x) * 2)`), which previously
failed at lowering because the order key was lowered before any aggregate alias
map existed. Lowering-only change; the executor already accepts aggregate
outputs as order-bind sort keys.

- Defer aggregate-bearing ORDER BY expressions until aggregate hoisting runs,
  then lower them into `order_binds` with the alias map in scope.
- Share one aggregate-alias map across HAVING and ORDER BY hoisting (rename
  collect_having_aggregates -> collect_inline_aggregates) so synthetic names stay
  unique and an ORDER BY aggregate reuses a matching SELECT alias instead of
  recomputing it.
- Hoisted aggregates feed the aggregate list before the auto-GROUP-BY check, so
  implicit single-group aggregation triggers.
- CONSTRUCT/DESCRIBE reject inline-aggregate ORDER BY (no aggregation stage).

Tests: explicit GROUP BY (deduped with SELECT alias), aggregate not in SELECT,
implicit single group, aggregate inside a compound expression, plus lowering
unit tests for dedup and hoisting.
@bplatz bplatz requested review from aaj3f and zonotope June 3, 2026 16:22

@zonotope zonotope left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🌞

Base automatically changed from feature/count-plan-aggregate-fastpaths to main June 3, 2026 16:55
@bplatz bplatz merged commit 0465727 into main Jun 3, 2026
13 of 14 checks passed
@bplatz bplatz deleted the fix/aggregate-math branch June 3, 2026 17:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants