Skip to content

OXY-166: Add scalar aggregate functions (SUM / AVG / MIN / MAX) to oxygen-sql - #307

Open
Kalin-Rudnicki wants to merge 1 commit into
mainfrom
OXY-166
Open

OXY-166: Add scalar aggregate functions (SUM / AVG / MIN / MAX) to oxygen-sql#307
Kalin-Rudnicki wants to merge 1 commit into
mainfrom
OXY-166

Conversation

@Kalin-Rudnicki

Copy link
Copy Markdown
Owner

OXY-166 — Scalar aggregate functions (SUM / AVG / MIN / MAX)

Adds SUM / AVG / MIN / MAX scalar aggregate support to the oxygen-sql query DSL, mirroring the existing COUNT path end-to-end (DSL → parse → fragment → decode). Scalar aggregation over the whole result set — no GROUP BY (that's OXY-100).

DSL

yield sum(p.age)   // Option[Long]
yield avg(p.age)   // Option[BigDecimal]
yield min(p.age)   // Option[Int]
yield max(p.age)   // Option[Int]

Nullability

SUM/AVG/MIN/MAX return SQL NULL over an empty result set, so all four decode to Option[_] (unlike COUNT, which is never null).

Type choices (documented in docs/sql/queries.md)

Follows Postgres' own widening:

Aggregate Column PG type Scala result
sum Short/Int bigint Option[Long]
sum Long/BigDecimal numeric Option[BigDecimal]
sum Float real Option[Float]
sum Double double precision Option[Double]
avg Short/Int/Long/BigDecimal numeric Option[BigDecimal]
avg Float/Double double precision Option[Double]
min/max any orderable A same as A Option[A]

The sum/avg widening is encoded by new SumType/AvgType type-classes, so the query's static type already reflects the widened result. The macro reads the widened Out type off the term and picks the matching optional decoder — the primitive int decoder matches on JDBC runtime class (java.lang.Integer), so decoding SUM(int) (a bigint/Long) without widening would fail; widening is required for correctness, not cosmetic.

Added a decode-only ResultDecoder.bigDecimal (Postgres numericjava.math.BigDecimal). Intentionally no RowRepr[BigDecimal] / Column.Type.Numeric — that would ripple into the migration codecs; a BigDecimal table column is therefore still unsupported, but aggregate results decode to BigDecimal correctly.

Tests

  • New CustomQuerySpec "scalar aggregates" test (real Postgres via TestContainers): SUM/AVG/MIN/MAX over a non-empty group and an empty group (NULL → None), plus SUM widening (→Long) and AVG type (→BigDecimal).
  • Full sql-it suite: 55 passed / 0 failed.

See report/OXY-166.md for the full decision log and confidence score (9/10).

🤖 Generated with Claude Code

https://claude.ai/code/session_011YxWKdsz97QT9BD7AdpSq6

Mirrors the existing COUNT path (DSL -> parse -> fragment -> decode) to add
scalar aggregation over the whole result set (no GROUP BY; that's OXY-100).

- DSL: `Q.sum` / `Q.avg` / `Q.min` / `Q.max` in `Q`.
- Nullability: SUM/AVG/MIN/MAX are SQL NULL over an empty set, so all four
  decode to `Option[_]` (unlike COUNT which is never null).
- Widening follows Postgres: SUM(int)->Long, SUM(bigint/numeric)->BigDecimal,
  AVG(exact)->BigDecimal, AVG(float)->Double; MIN/MAX keep the column type.
  The `sum`/`avg` result type is encoded by the new `SumType`/`AvgType`
  type-classes so the query's static type reflects the widened result.
- Added a decode-only `ResultDecoder.bigDecimal` (Postgres `numeric` ->
  `java.math.BigDecimal`); intentionally no `RowRepr[BigDecimal]` /
  `Column.Type.Numeric` to avoid touching migration codecs.
- Tests: real-Postgres integration test over non-empty AND empty result sets
  (NULL -> None) plus SUM/AVG type correctness.
- Docs: docs/sql/queries.md.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011YxWKdsz97QT9BD7AdpSq6
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.

1 participant