Skip to content

Commit 1ec87a0

Browse files
committed
fix(rivetkit): run db migrations before lifecycle hooks
1 parent 66a67ac commit 1ec87a0

4 files changed

Lines changed: 174 additions & 9 deletions

File tree

rivetkit-typescript/packages/rivetkit-napi/src/napi_actor_events.rs

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,17 @@ async fn run_preamble(
198198
) -> Result<RunHandlerSlot> {
199199
let is_new = snapshot.is_none();
200200

201+
// Run database migrations before any user lifecycle hook so `c.db` is
202+
// usable from createState, onCreate, and createVars.
203+
if let Some(callback) = &bindings.on_migrate {
204+
with_timeout(
205+
"onMigrate",
206+
config.on_migrate_timeout,
207+
call_on_migrate(callback, ctx, is_new),
208+
)
209+
.await?;
210+
}
211+
201212
if is_new {
202213
if let Some(callback) = &bindings.create_state {
203214
let bytes = with_timeout(
@@ -240,15 +251,6 @@ async fn run_preamble(
240251
.await?;
241252
}
242253

243-
if let Some(callback) = &bindings.on_migrate {
244-
with_timeout(
245-
"onMigrate",
246-
config.on_migrate_timeout,
247-
call_on_migrate(callback, ctx, is_new),
248-
)
249-
.await?;
250-
}
251-
252254
ctx.init_alarms().await?;
253255
ctx.mark_ready_internal();
254256

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
import { actor } from "rivetkit";
2+
import { db } from "@/common/database/mod";
3+
4+
// Verifies that onMigrate runs before createState/onCreate/createVars so the
5+
// schema is queryable from those lifecycle hooks. The runtime should make
6+
// `c.db` usable as soon as user code can read it.
7+
8+
export const dbInitOrderCreateStateActor = actor({
9+
createState: async (c, _input) => {
10+
const rows = await c.db.execute<{ count: number }>(
11+
"SELECT COUNT(*) as count FROM init_order_items",
12+
);
13+
return { count: rows[0]?.count ?? -1 };
14+
},
15+
db: db({
16+
onMigrate: async (db) => {
17+
await db.execute(`
18+
CREATE TABLE IF NOT EXISTS init_order_items (
19+
id INTEGER PRIMARY KEY AUTOINCREMENT,
20+
name TEXT NOT NULL
21+
)
22+
`);
23+
},
24+
}),
25+
actions: {
26+
getInitialCount: (c) => (c.state as { count: number }).count,
27+
insert: async (c, name: string) => {
28+
await c.db.execute(
29+
"INSERT INTO init_order_items (name) VALUES (?)",
30+
name,
31+
);
32+
},
33+
},
34+
options: {
35+
actionTimeout: 120_000,
36+
sleepTimeout: 100,
37+
},
38+
});
39+
40+
export const dbInitOrderOnCreateActor = actor({
41+
state: { initialCount: -1 },
42+
onCreate: async (c, _input) => {
43+
const rows = await c.db.execute<{ count: number }>(
44+
"SELECT COUNT(*) as count FROM init_order_items",
45+
);
46+
c.state.initialCount = rows[0]?.count ?? -1;
47+
},
48+
db: db({
49+
onMigrate: async (db) => {
50+
await db.execute(`
51+
CREATE TABLE IF NOT EXISTS init_order_items (
52+
id INTEGER PRIMARY KEY AUTOINCREMENT,
53+
name TEXT NOT NULL
54+
)
55+
`);
56+
},
57+
}),
58+
actions: {
59+
getInitialCount: (c) => c.state.initialCount,
60+
},
61+
options: {
62+
actionTimeout: 120_000,
63+
sleepTimeout: 100,
64+
},
65+
});
66+
67+
export const dbInitOrderCreateVarsActor = actor({
68+
state: {},
69+
createVars: async (c) => {
70+
const rows = await c.db.execute<{ count: number }>(
71+
"SELECT COUNT(*) as count FROM init_order_items",
72+
);
73+
return { initialCount: rows[0]?.count ?? -1 };
74+
},
75+
db: db({
76+
onMigrate: async (db) => {
77+
await db.execute(`
78+
CREATE TABLE IF NOT EXISTS init_order_items (
79+
id INTEGER PRIMARY KEY AUTOINCREMENT,
80+
name TEXT NOT NULL
81+
)
82+
`);
83+
},
84+
}),
85+
actions: {
86+
getInitialCount: (c) => (c.vars as { initialCount: number }).initialCount,
87+
},
88+
options: {
89+
actionTimeout: 120_000,
90+
sleepTimeout: 100,
91+
},
92+
});

rivetkit-typescript/packages/rivetkit/fixtures/driver-test-suite/registry-static.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,11 @@ import {
2222
import { dbActorRaw } from "./actor-db-raw";
2323
import { onStateChangeActor } from "./actor-onstatechange";
2424
import { connErrorSerializationActor } from "./conn-error-serialization";
25+
import {
26+
dbInitOrderCreateStateActor,
27+
dbInitOrderCreateVarsActor,
28+
dbInitOrderOnCreateActor,
29+
} from "./db-init-order";
2530
import { dbPragmaMigrationActor } from "./db-pragma-migration";
2631
import { counterWithParams } from "./conn-params";
2732
import { connStateActor } from "./conn-state";
@@ -315,6 +320,10 @@ export const registry = setup({
315320
connErrorSerializationActor,
316321
// From db-pragma-migration.ts
317322
dbPragmaMigrationActor,
323+
// From db-init-order.ts
324+
dbInitOrderCreateStateActor,
325+
dbInitOrderOnCreateActor,
326+
dbInitOrderCreateVarsActor,
318327
// From state-zod-coercion.ts
319328
stateZodCoercionActor,
320329
// From lifecycle-hooks.ts
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import { describe, expect, test } from "vitest";
2+
import { describeDriverMatrix } from "./shared-matrix";
3+
import { setupDriverTest } from "./shared-utils";
4+
5+
const REAL_TIMER_DB_TIMEOUT_MS = 180_000;
6+
7+
describeDriverMatrix("Actor Db Init Order", (driverTestConfig) => {
8+
const dbTestTimeout = driverTestConfig.useRealTimers
9+
? REAL_TIMER_DB_TIMEOUT_MS
10+
: undefined;
11+
12+
describe("onMigrate runs before lifecycle hooks", () => {
13+
test(
14+
"createState can read schema created by onMigrate",
15+
async (c) => {
16+
const { client } = await setupDriverTest(c, driverTestConfig);
17+
const key = `db-init-order-create-state-${crypto.randomUUID()}`;
18+
const actor = client.dbInitOrderCreateStateActor.getOrCreate([
19+
key,
20+
]);
21+
22+
const initialCount = await actor.getInitialCount();
23+
expect(initialCount).toBe(0);
24+
25+
await actor.insert("alpha");
26+
const nextActor = client.dbInitOrderCreateStateActor.getOrCreate(
27+
[key],
28+
);
29+
expect(typeof (await nextActor.getInitialCount())).toBe("number");
30+
},
31+
dbTestTimeout,
32+
);
33+
34+
test(
35+
"onCreate can read schema created by onMigrate",
36+
async (c) => {
37+
const { client } = await setupDriverTest(c, driverTestConfig);
38+
const key = `db-init-order-on-create-${crypto.randomUUID()}`;
39+
const actor = client.dbInitOrderOnCreateActor.getOrCreate([key]);
40+
41+
const initialCount = await actor.getInitialCount();
42+
expect(initialCount).toBe(0);
43+
},
44+
dbTestTimeout,
45+
);
46+
47+
test(
48+
"createVars can read schema created by onMigrate",
49+
async (c) => {
50+
const { client } = await setupDriverTest(c, driverTestConfig);
51+
const key = `db-init-order-create-vars-${crypto.randomUUID()}`;
52+
const actor = client.dbInitOrderCreateVarsActor.getOrCreate([
53+
key,
54+
]);
55+
56+
const initialCount = await actor.getInitialCount();
57+
expect(initialCount).toBe(0);
58+
},
59+
dbTestTimeout,
60+
);
61+
});
62+
});

0 commit comments

Comments
 (0)