feat(collections): add created_at and updated_at timestamps to collections and variants tables#1119
feat(collections): add created_at and updated_at timestamps to collections and variants tables#1119
Conversation
…tions and variants tables Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
d06249d to
e213524
Compare
…alization Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ion mismatches Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Adds server-managed createdAt/updatedAt timestamps to collections and variants, persists them in Postgres, and exposes them via the API. This addresses issue #1113 by making timestamps available to API consumers and ensuring updatedAt changes on updates.
Changes:
- Add
created_at/updated_atcolumns tocollections_tableandvariants_tablevia a new DB migration. - Plumb timestamps through Exposed entities/model logic into the
CollectionandVariantAPI responses, and update timestamps on writes. - Add tests around timestamp presence/update behavior and rejecting client attempts to set timestamp fields; configure Jackson to fail on unknown properties.
Reviewed changes
Copilot reviewed 11 out of 11 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| backend/src/test/kotlin/org/genspectrum/dashboardsbackend/controller/CollectionsPutTest.kt | Adds update-time assertions and 400 tests when timestamps are provided in update bodies. |
| backend/src/test/kotlin/org/genspectrum/dashboardsbackend/controller/CollectionsPostTest.kt | Adds creation-time assertions and 400 tests when timestamps are provided in create bodies. |
| backend/src/main/resources/db/migration/V1.2__add_timestamps_to_collections_and_variants.sql | Adds timestamp columns + defaults for collections and variants tables. |
| backend/src/main/resources/application.properties | Enables global Jackson “fail on unknown properties”. |
| backend/src/main/kotlin/org/genspectrum/dashboardsbackend/model/collection/VariantTable.kt | Adds timestamp columns to the Exposed table/entity + maps them to API variants. |
| backend/src/main/kotlin/org/genspectrum/dashboardsbackend/model/collection/CollectionTable.kt | Adds timestamp columns to the Exposed table/entity + maps them to API collections. |
| backend/src/main/kotlin/org/genspectrum/dashboardsbackend/model/collection/CollectionModel.kt | Sets timestamps on create/update and updates updatedAt on collection/variant changes. |
| backend/src/main/kotlin/org/genspectrum/dashboardsbackend/config/InstantSerializer.kt | Adds Jackson (de)serialization for kotlinx.datetime.Instant. |
| backend/src/main/kotlin/org/genspectrum/dashboardsbackend/api/Variant.kt | Exposes createdAt/updatedAt on API variants. |
| backend/src/main/kotlin/org/genspectrum/dashboardsbackend/api/Collection.kt | Exposes createdAt/updatedAt on API collections. |
| backend/build.gradle.kts | Adds exposed-kotlin-datetime dependency for timestamp column support. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| ALTER TABLE collections_table | ||
| ADD COLUMN created_at timestamp without time zone NOT NULL DEFAULT timezone('UTC', CURRENT_TIMESTAMP), | ||
| ADD COLUMN updated_at timestamp without time zone NOT NULL DEFAULT timezone('UTC', CURRENT_TIMESTAMP); | ||
|
|
||
| -- Variants | ||
| ALTER TABLE variants_table | ||
| ADD COLUMN created_at timestamp without time zone NOT NULL DEFAULT timezone('UTC', CURRENT_TIMESTAMP), | ||
| ADD COLUMN updated_at timestamp without time zone NOT NULL DEFAULT timezone('UTC', CURRENT_TIMESTAMP); |
There was a problem hiding this comment.
The column defaults use timezone('UTC', CURRENT_TIMESTAMP), which is not a constant default in Postgres and can trigger a full table rewrite/long lock on large tables when adding the column. It also preserves microsecond precision, which may conflict with the “millisecond precision” guarantee mentioned in the PR. Consider a safer migration pattern (add nullable column, backfill with date_trunc('milliseconds', ...), then set NOT NULL + default), or at least truncate the default to milliseconds.
| ALTER TABLE collections_table | |
| ADD COLUMN created_at timestamp without time zone NOT NULL DEFAULT timezone('UTC', CURRENT_TIMESTAMP), | |
| ADD COLUMN updated_at timestamp without time zone NOT NULL DEFAULT timezone('UTC', CURRENT_TIMESTAMP); | |
| -- Variants | |
| ALTER TABLE variants_table | |
| ADD COLUMN created_at timestamp without time zone NOT NULL DEFAULT timezone('UTC', CURRENT_TIMESTAMP), | |
| ADD COLUMN updated_at timestamp without time zone NOT NULL DEFAULT timezone('UTC', CURRENT_TIMESTAMP); | |
| -- Step 1: add nullable columns without defaults to avoid table rewrite | |
| ALTER TABLE collections_table | |
| ADD COLUMN created_at timestamp without time zone, | |
| ADD COLUMN updated_at timestamp without time zone; | |
| -- Step 2: backfill existing rows with millisecond-precision UTC timestamps | |
| UPDATE collections_table | |
| SET created_at = date_trunc('milliseconds', timezone('UTC', CURRENT_TIMESTAMP)), | |
| updated_at = date_trunc('milliseconds', timezone('UTC', CURRENT_TIMESTAMP)) | |
| WHERE created_at IS NULL | |
| OR updated_at IS NULL; | |
| -- Step 3: enforce NOT NULL and set millisecond-precision defaults | |
| ALTER TABLE collections_table | |
| ALTER COLUMN created_at SET NOT NULL, | |
| ALTER COLUMN created_at SET DEFAULT date_trunc('milliseconds', timezone('UTC', CURRENT_TIMESTAMP)), | |
| ALTER COLUMN updated_at SET NOT NULL, | |
| ALTER COLUMN updated_at SET DEFAULT date_trunc('milliseconds', timezone('UTC', CURRENT_TIMESTAMP)); | |
| -- Variants | |
| -- Step 1: add nullable columns without defaults to avoid table rewrite | |
| ALTER TABLE variants_table | |
| ADD COLUMN created_at timestamp without time zone, | |
| ADD COLUMN updated_at timestamp without time zone; | |
| -- Step 2: backfill existing rows with millisecond-precision UTC timestamps | |
| UPDATE variants_table | |
| SET created_at = date_trunc('milliseconds', timezone('UTC', CURRENT_TIMESTAMP)), | |
| updated_at = date_trunc('milliseconds', timezone('UTC', CURRENT_TIMESTAMP)) | |
| WHERE created_at IS NULL | |
| OR updated_at IS NULL; | |
| -- Step 3: enforce NOT NULL and set millisecond-precision defaults | |
| ALTER TABLE variants_table | |
| ALTER COLUMN created_at SET NOT NULL, | |
| ALTER COLUMN created_at SET DEFAULT date_trunc('milliseconds', timezone('UTC', CURRENT_TIMESTAMP)), | |
| ALTER COLUMN updated_at SET NOT NULL, | |
| ALTER COLUMN updated_at SET DEFAULT date_trunc('milliseconds', timezone('UTC', CURRENT_TIMESTAMP)); |
| ADD COLUMN created_at timestamp without time zone NOT NULL DEFAULT timezone('UTC', CURRENT_TIMESTAMP), | ||
| ADD COLUMN updated_at timestamp without time zone NOT NULL DEFAULT timezone('UTC', CURRENT_TIMESTAMP); | ||
|
|
||
| -- Variants | ||
| ALTER TABLE variants_table | ||
| ADD COLUMN created_at timestamp without time zone NOT NULL DEFAULT timezone('UTC', CURRENT_TIMESTAMP), | ||
| ADD COLUMN updated_at timestamp without time zone NOT NULL DEFAULT timezone('UTC', CURRENT_TIMESTAMP); |
There was a problem hiding this comment.
Same concern as the collections table: adding NOT NULL columns with a non-constant CURRENT_TIMESTAMP-based default can rewrite/lock the table and yields microsecond precision. Consider backfilling and then applying a milliseconds-truncated default to avoid operational risk and ensure consistent timestamp precision.
| ADD COLUMN created_at timestamp without time zone NOT NULL DEFAULT timezone('UTC', CURRENT_TIMESTAMP), | |
| ADD COLUMN updated_at timestamp without time zone NOT NULL DEFAULT timezone('UTC', CURRENT_TIMESTAMP); | |
| -- Variants | |
| ALTER TABLE variants_table | |
| ADD COLUMN created_at timestamp without time zone NOT NULL DEFAULT timezone('UTC', CURRENT_TIMESTAMP), | |
| ADD COLUMN updated_at timestamp without time zone NOT NULL DEFAULT timezone('UTC', CURRENT_TIMESTAMP); | |
| ADD COLUMN created_at timestamp without time zone, | |
| ADD COLUMN updated_at timestamp without time zone; | |
| -- Backfill existing collections with millisecond-truncated UTC timestamps | |
| UPDATE collections_table | |
| SET created_at = COALESCE(created_at, date_trunc('milliseconds', timezone('UTC', now()))), | |
| updated_at = COALESCE(updated_at, date_trunc('milliseconds', timezone('UTC', now()))); | |
| -- Enforce NOT NULL and set millisecond-truncated UTC defaults for collections | |
| ALTER TABLE collections_table | |
| ALTER COLUMN created_at SET NOT NULL, | |
| ALTER COLUMN created_at SET DEFAULT date_trunc('milliseconds', timezone('UTC', now())), | |
| ALTER COLUMN updated_at SET NOT NULL, | |
| ALTER COLUMN updated_at SET DEFAULT date_trunc('milliseconds', timezone('UTC', now())); | |
| -- Variants | |
| ALTER TABLE variants_table | |
| ADD COLUMN created_at timestamp without time zone, | |
| ADD COLUMN updated_at timestamp without time zone; | |
| -- Backfill existing variants with millisecond-truncated UTC timestamps | |
| UPDATE variants_table | |
| SET created_at = COALESCE(created_at, date_trunc('milliseconds', timezone('UTC', now()))), | |
| updated_at = COALESCE(updated_at, date_trunc('milliseconds', timezone('UTC', now()))); | |
| -- Enforce NOT NULL and set millisecond-truncated UTC defaults for variants | |
| ALTER TABLE variants_table | |
| ALTER COLUMN created_at SET NOT NULL, | |
| ALTER COLUMN created_at SET DEFAULT date_trunc('milliseconds', timezone('UTC', now())), | |
| ALTER COLUMN updated_at SET NOT NULL, | |
| ALTER COLUMN updated_at SET DEFAULT date_trunc('milliseconds', timezone('UTC', now())); |
| import kotlinx.datetime.Clock | ||
| import kotlinx.datetime.Instant |
There was a problem hiding this comment.
My IDE claims that these are deprecated and we should use this instead:
| import kotlinx.datetime.Clock | |
| import kotlinx.datetime.Instant | |
| import kotlin.time.Clock | |
| import kotlin.time.Instant |
There was a problem hiding this comment.
Ah, I see, Exposed still needs it...
| private fun createVariantEntity(collectionEntity: CollectionEntity, variantRequest: VariantRequest): VariantEntity { | ||
| val now = now() |
There was a problem hiding this comment.
I wonder whether this method should maybe rather accept a now argument from the caller?
resolves #1113
Summary
Add
created_atandupdated_atfields to collections and variants; they behave as you'd expect.Example timestamp:
2026-03-26T12:59:18.027Z- uses millisecond precision.Adds tests to see that updating works, and that you can't manually set the dates.
Jackson is now configured to always fail parsing on unknown properties.
Example API response object
{ "id": 1, "name": "Covid: Alpha & Beta comparison", "ownedBy": "foobar", "organism": "covid", "description": "Comparing Alpha and Beta variant trajectories", "variants": [ { "type": "query", "id": 1, "collectionId": 1, "name": "Alpha (B.1.1.7)", "description": "Alpha lineage globally", "countQuery": "pangoLineage=B.1.1.7*", "coverageQuery": null, "createdAt": "2026-03-26T11:31:23.404Z", "updatedAt": "2026-03-26T11:31:23.404Z" }, { "type": "query", "id": 2, "collectionId": 1, "name": "Beta (B.1.351)", "description": "Beta lineage globally", "countQuery": "pangoLineage=B.1.351*", "coverageQuery": null, "createdAt": "2026-03-26T11:31:23.404Z", "updatedAt": "2026-03-26T11:31:23.404Z" } ], "createdAt": "2026-03-26T11:31:23.404Z", "updatedAt": "2026-03-26T11:31:23.404Z" }PR Checklist
Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com