feat(lapis): support scalar functions in aggregation fields via dot notation - #1780
feat(lapis): support scalar functions in aggregation fields via dot notation#1780fhennig wants to merge 12 commits into
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Code review (high effort)Reviewed 🐞 Correctness bug (CONFIRMED)
Repro: None of the added tests exercise this path — 🧹 Cleanup findings (lower severity)
Note: I also checked whether the dot-notation parsing regresses metadata fields whose names literally contain a 🤖 Generated with Claude Code |
There was a problem hiding this comment.
Pull request overview
Adds support for scalar/computed fields (e.g. date.isoWeek) in the fields parameter for the /aggregated endpoint by introducing a typed Field model, validating scalar functions against a whitelist, generating a pre-groupBy() map() step in SaneQL, and translating internal alias columns back to user-facing keys in the response.
Changes:
- Introduces
Field.PlainvsField.ComputedplusScalarFunctionwhitelist and validation (type-checked). - Extends aggregated SaneQL generation with a
map()step for computed fields and adjusts ordering/response key rewriting. - Adds unit + e2e tests and documentation for computed fields; blocks computed fields in
/details.
Reviewed changes
Copilot reviewed 22 out of 22 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| lapis/src/main/kotlin/org/genspectrum/lapis/request/Field.kt | Introduces Field sealed type and parsing for <field>.<function> computed syntax with type validation. |
| lapis/src/main/kotlin/org/genspectrum/lapis/request/ScalarFunction.kt | Adds scalar function whitelist (currently isoWeek for DATE). |
| lapis/src/main/kotlin/org/genspectrum/lapis/request/OrderByField.kt | Canonicalizes orderBy fields using the same converter as fields (supports computed fields casing). |
| lapis/src/main/kotlin/org/genspectrum/lapis/silo/SiloQuery.kt | Adds computed-field map() step before groupBy() and groups by computed aliases. |
| lapis/src/main/kotlin/org/genspectrum/lapis/model/SiloQueryModel.kt | Splits plain vs computed fields, rewrites computed orderBy to aliases, and renames aliases back in responses. |
| lapis/src/main/kotlin/org/genspectrum/lapis/model/mutationsOverTime/QueriesOverTimeModel.kt | Updates aggregated call site to named parameters after signature expansion. |
| lapis/src/main/kotlin/org/genspectrum/lapis/controller/LapisController.kt | Rejects computed fields for /details requests with a 400. |
| lapis/src/main/kotlin/org/genspectrum/lapis/controller/ControllerDescriptions.kt | Documents computed-field dot notation in aggregated fields OpenAPI description. |
| lapis/src/test/kotlin/org/genspectrum/lapis/silo/SiloQueryToSaneQlTest.kt | Adds SaneQL rendering test for computed fields (map + groupBy alias). |
| lapis/src/test/kotlin/org/genspectrum/lapis/silo/SiloQueryTest.kt | Updates aggregated action tests to named parameters. |
| lapis/src/test/kotlin/org/genspectrum/lapis/request/SequenceFiltersRequestWithFieldsTest.kt | Updates tests to use Field.Plain. |
| lapis/src/test/kotlin/org/genspectrum/lapis/request/ScalarFunctionFieldTest.kt | New tests for computed-field parsing/validation. |
| lapis/src/test/kotlin/org/genspectrum/lapis/request/OrderByFieldConverterTest.kt | Adds coverage for computed fields in orderBy conversion/canonicalization. |
| lapis/src/test/kotlin/org/genspectrum/lapis/model/SiloQueryModelTest.kt | Adds tests for map-step emission, alias renaming, and orderBy alias rewriting. |
| lapis/src/test/kotlin/org/genspectrum/lapis/model/mutationsOverTime/Helpers.kt | Updates aggregated action helper call site to named parameters. |
| lapis/src/test/kotlin/org/genspectrum/lapis/controller/LapisControllerCommonFieldsTest.kt | Updates controller tests to use Field.Plain. |
| lapis/src/test/kotlin/org/genspectrum/lapis/controller/Helpers.kt | Updates request helper to construct Field.Plain. |
| lapis-e2e/test/details.spec.ts | Adds e2e assertion that computed fields are rejected on /details. |
| lapis-e2e/test/aggregated.spec.ts | Adds e2e coverage for computed fields in /aggregated including ordering and error cases. |
| lapis-docs/src/content/docs/references/introduction.mdx | Links to computed fields concept doc. |
| lapis-docs/src/content/docs/concepts/computed-fields.mdx | New documentation page describing computed fields and available functions. |
| lapis-docs/astro.config.mjs | Adds “Computed fields” to docs navigation. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
…otation Users can now specify computed fields like `date.isoWeek` in the `fields` parameter of the `/aggregated` endpoint. LAPIS validates the function against a whitelist (currently `isoWeek` for date fields), generates a `map()` step before `groupBy()` in the SaneQL query, and renames the internal alias columns back to the user-facing `field.function` names in the response. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add a doc comment to ScalarFunction explaining new functions must be whitelisted there, and describe the <field>.<function> syntax in the OpenAPI description for the aggregated endpoint's fields parameter. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Cover stratifying and ordering by a computed field (date.isoWeek), bad requests for unknown functions and wrong field types, and that computed fields are rejected on /details. Verified against a real SILO instance. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Add a concepts page explaining the <field>.<function> dot-notation syntax for the fields parameter (e.g. date.isoWeek), and link it from the references introduction. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
orderBy previously used a plain-field-only cleaner, so a computed field (e.g. "date.isoWeek") with different casing than in `fields` would not be recognized as the same field, silently breaking the alias rewrite and sending an invalid column reference to SILO. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…eries SILO accepts a quoted identifier containing a literal "." as a column name (verified against a live SILO instance), so the computed field's canonical name (e.g. "date.isoWeek") can be used directly as the SaneQL map/groupBy/orderBy column instead of routing through an internal __scalar_<function>_<field> alias. This removes the response-column rename and orderBy-rewrite steps entirely, along with the class of bugs that comes from keeping two names in sync. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- Change Field from a sealed class with inner Plain/Computed subclasses to
a sealed interface with top-level PlainField and ComputedField
- ComputedField exposes outputColumnName (e.g. "date.isoWeek") matching the
SequencePositionField pattern from main, eliminating the need for a
separate internal alias and column-renaming step in SiloQueryModel
- Extract dot-notation parsing into ScalarFunctionFieldConverter in
request/converter/, mirroring main's converter package structure
- Revert SaneQlAssignment to auto-quoting ("name":=value), undoing the
temporary workaround; SaneQlAssignment(field.outputColumnName, ...) now
produces the correct quoted alias directly
- Update all call sites and tests accordingly
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
50e05da to
2092281
Compare
- PlainFieldConverter: reject dot-notation fields with a clear error
("Scalar functions are not supported in fields for this endpoint")
so the /details e2e test gets the expected message
- AggregatedFieldConverterTest: pass scalarFunctionFieldConverter in constructor
- ScalarFunctionFieldTest: use JUnit assertTrue instead of Kotlin assert
(Kotlin assert is a no-op without -ea JVM flag)
- ControllerDescriptions: remove backslash-escapes from raw strings
(\" in triple-quoted strings renders as literal \", not ")
- computed-fields.mdx: apply prettier formatting
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The page was added to the nav but not to the ordered page list used by the next-button and navigation link tests. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
AggregatedAction already has @JsonInclude(NON_EMPTY), so empty lists are suppressed anyway. The JSON serialization is never sent to SILO (it uses SaneQL), so the annotation served no purpose. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
fengelniederhammer
left a comment
There was a problem hiding this comment.
A couple of minor things, looks good otherwise 👍
|
|
||
| :::note | ||
| Computed fields are only supported in the `fields` parameter of `/sample/aggregated` | ||
| (and the analogous endpoints for other sequence types). |
There was a problem hiding this comment.
which other endpoint?
| ## Available functions | ||
|
|
||
| | Function | Applicable field types | Description | | ||
| | --------- | ---------------------- | ------------------------------------ | | ||
| | `isoWeek` | `date` | The ISO 8601 week of the date field. | |
| ``` | ||
| [URL to LAPIS instance]/sample/aggregated?fields=date.isoWeek | ||
| ``` | ||
|
|
||
| ```json | ||
| { | ||
| "fields": ["date.isoWeek"] | ||
| } | ||
| ``` |
There was a problem hiding this comment.
| ``` | |
| [URL to LAPIS instance]/sample/aggregated?fields=date.isoWeek | |
| ``` | |
| ```json | |
| { | |
| "fields": ["date.isoWeek"] | |
| } | |
| ``` | |
| ``` | |
| POST /sample/aggregated?fields=date.isoWeek | |
| { | |
| "fields": ["date.isoWeek"] | |
| } | |
| ``` |
I think other examples also already use a format like this?
| it('should order by a computed field using dot notation', async () => { | ||
| const result = await lapisClient.postAggregated({ | ||
| aggregatedPostRequest: { | ||
| date: '2021-06-05', |
There was a problem hiding this comment.
really filter by date, too? I think the test would be more meaningful without the filter, but still deterministic due to the orderBy.
| @Component | ||
| class ScalarFunctionFieldConverter( | ||
| private val caseInsensitiveFieldsCleaner: CaseInsensitiveFieldsCleaner, | ||
| private val databaseConfig: DatabaseConfig, |
There was a problem hiding this comment.
| private val databaseConfig: DatabaseConfig, | |
| private databaseConfig: DatabaseConfig, |
| @Test | ||
| fun `isoWeek on non-date field throws BadRequestException`() { | ||
| val ex = assertThrows<BadRequestException> { underTest.tryConvert("country.isoWeek") } | ||
| assertTrue(ex.message.orEmpty().contains("STRING"), "Expected error to mention type, got: ${ex.message}") |
There was a problem hiding this comment.
| assertTrue(ex.message.orEmpty().contains("STRING"), "Expected error to mention type, got: ${ex.message}") | |
| assertTrue(ex.message.contains("STRING"), "Expected error to mention type, got: ${ex.message}") |
According to my IDE, this is redundant.
| ), | ||
| ) | ||
|
|
||
| private val underTest = ScalarFunctionFieldConverter( |
There was a problem hiding this comment.
Nit: Usually, in Kotlin and Java, those underTests are declared inside the test class.

resolves #1767
Summary
Users can now specify computed fields like
date.isoWeekin thefieldsparameter of the/aggregatedendpoint. LAPIS validates the function against a whitelist (currentlyisoWeekfor date fields), generates amap()step beforegroupBy()in the SaneQL query, and renames the internal alias columns back to the user-facingfield.functionnames in the response.PR Checklist
llms.txt.