Hey 👋
A small batch of bug fixes. Please report any issues. 🤞😰🤞
Kysely's migrator no longer records timestamps that go backwards when the wall clock jumps back during a migration run (WSL2/VM time sync, an NTP step, an unsmeared leap second). Such timestamps made every later migrateToLatest() fail with "corrupted migrations", even though the migrations ran in the correct order. The migrator now reads the latest recorded timestamp while holding the migration lock, and records each new migration at max(now, latest + 1ms), so the order holds across runs and hosts. Executed migrations with an invalid timestamp now fail with "corrupted migrations" instead of being sorted arbitrarily.
Migration tables that already have out-of-order timestamps need a one-time repair. Run the query for your dialect below. It goes through executed migrations in name order, which is the order the migrator runs them in by default, and moves every timestamp that isn't later than the previous one to 1 millisecond after it. Running it again changes nothing.
Replace kysely_migration with your migration table's name, prefixed with the schema if you set migrationTableSchema. Run it while no migrations are running. Skip this if you use allowUnorderedMigrations: true. Name order isn't the execution order in that mode, and out-of-order timestamps don't block migrations there.
PostgreSQL and PGlite:
with ordered as (
select
name,
(extract(epoch from "timestamp"::timestamptz) * 1000)::bigint as executed_at,
row_number() over (order by name collate "C") as position
from kysely_migration
),
repaired as (
select
name,
executed_at,
position + max(executed_at - position) over (order by position) as repaired_executed_at
from ordered
)
update kysely_migration
set "timestamp" = to_char(
(timestamptz 'epoch' + repaired_executed_at * interval '1 millisecond') at time zone 'UTC',
'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"'
)
from repaired
where kysely_migration.name = repaired.name
and repaired.repaired_executed_at <> repaired.executed_at;MySQL 8.0+:
update kysely_migration
join (
select
name,
executed_at,
position + max(executed_at - position) over (order by position) as repaired_executed_at
from (
select
name,
timestampdiff(
microsecond,
'1970-01-01',
str_to_date(`timestamp`, '%Y-%m-%dT%H:%i:%s.%fZ')
) div 1000 as executed_at,
row_number() over (order by cast(name as binary)) as position
from kysely_migration
) as ordered
) as repaired using (name)
set kysely_migration.`timestamp` = concat(
date_format(
timestamp '1970-01-01 00:00:00' + interval repaired.repaired_executed_at * 1000 microsecond,
'%Y-%m-%dT%H:%i:%s.'
),
lpad(repaired.repaired_executed_at mod 1000, 3, '0'),
'Z'
)
where repaired.repaired_executed_at <> repaired.executed_at;MS SQL Server 2016+:
with ordered as (
select
name,
datediff_big(millisecond, '1970-01-01', convert(datetime2(3), [timestamp], 127)) as executed_at,
row_number() over (order by name collate Latin1_General_BIN2) as position
from kysely_migration
),
repaired as (
select
name,
executed_at,
position + max(executed_at - position) over (order by position rows unbounded preceding) as repaired_executed_at
from ordered
)
update kysely_migration
set [timestamp] = convert(
varchar(19),
dateadd(
millisecond,
repaired.repaired_executed_at % 86400000,
dateadd(day, repaired.repaired_executed_at / 86400000, cast('1970-01-01' as datetime2(3)))
),
126
) + '.' + right('00' + cast(repaired.repaired_executed_at % 1000 as varchar(3)), 3) + 'Z'
from kysely_migration
join repaired on repaired.name = kysely_migration.name
where repaired.repaired_executed_at <> repaired.executed_at;SQLite 3.33+:
with ordered as (
select
name,
cast(round((julianday("timestamp") - 2440587.5) * 86400000) as integer) as executed_at,
row_number() over (order by name collate binary) as position
from kysely_migration
),
repaired as (
select
name,
executed_at,
position + max(executed_at - position) over (order by position) as repaired_executed_at
from ordered
)
update kysely_migration
set "timestamp" = strftime('%Y-%m-%dT%H:%M:%S', repaired_executed_at / 1000, 'unixepoch')
|| printf('.%03dZ', repaired_executed_at % 1000)
from repaired
where kysely_migration.name = repaired.name
and repaired.repaired_executed_at <> repaired.executed_at;The binary collations (collate "C", cast(name as binary), etc.) make the database sort names the same way the migrator does in JavaScript.
Also, PGlite 0.5.15+ commits that roll back aborted transactions now throw AbortedTransactionError, same as PostgreSQL since 0.29.8.
🚀 Features
🐞 Bugfixes
- fix:
withSchemacrashes onfor ... ofandjson_agg/to_jsontables in subqueries. by @igalklebanov in #2227 - fix: inline
key()/at()JSON path references inferunknowninjsonBuildObject. by @igalklebanov in #2228 - fix: migration timestamps go backwards when the clock jumps back. by @igalklebanov in #2248
PGlite 🟨
- fix(pglite): reject commits that roll back aborted transactions by @igalklebanov in #2217
📖 Documentation
- docs(site): add Omnea to proof wall by @igalklebanov in #2220
- docs(site): add ZenStack creator's quote by @igalklebanov in #2230
- docs(site): add Capacities, Disney, Talentir, and Teamspective to proof wall by @igalklebanov in #2229
- docs(site): refresh proof wall permalinks by @igalklebanov in #2231
- docs(site): explain installed-version API guidance for agents by @igalklebanov in #2190
- docs(site): expand raw SQL recipe by @igalklebanov in #2191
- docs(site): compare Kysely and Knex by @igalklebanov in #2184
- docs: document compiled queries and raw execution by @igalklebanov in #2177
- docs: document file migration provider customization by @igalklebanov in #2179
- docs(site): add OpenWork to proof wall by @igalklebanov in #2232
- feat(site): adopt Pagefind 1.6 search options. by @igalklebanov in #2233
- docs(site): raise the weekly downloads fallback to 20M+ by @igalklebanov in #2240
- docs(site): compare Kysely and Drizzle by @igalklebanov in #2186
- docs(site): add OpenPencil to proof wall by @igalklebanov in #2243
- docs(site): compare Kysely and TypeORM by @igalklebanov in #2187
📦 CICD & Tooling
- chore: remove example project. by @igalklebanov in #2241
- chore: bump dependencies. by @igalklebanov in #2250
⚠️ Breaking Changes
key()andat()no longer accept a 2nd type argument for the output type (e.g..key<'url', number>('url')). It was never documented. Use$castTo<T>()to override the output type. (#2228)
🐤 New Contributors
What's Changed
Full Changelog: v0.29.8...v0.29.9