1import * as Cloudflare from "@/Cloudflare";
2import { applySqlMigrations } from "@/Cloudflare/Workers/SqlMigrationsApply.ts";
3import * as Drizzle from "@/Drizzle/Cloudflare.ts";
4import * as Effect from "effect/Effect";
5import * as Result from "effect/Result";
6import { relations, users } from "./schema.ts";
7
8export type HistoryRow = {
9 id: number;
10 hash: string;
11 created_at: number | null;
12 name: string;
13 applied_at: string | null;
14};
15
16type State = Cloudflare.DurableObjectState["Service"];
17
18const history = (state: State, table: string) =>
19 state.storage.sql
20 .exec<HistoryRow>(
21 `SELECT id, hash, created_at, name, applied_at FROM "${table}" ORDER BY id`,
22 )
23 .pipe(Effect.flatMap((cursor) => cursor.toArray()));
24
25const tables = (state: State) =>
26 state.storage.sql
27 .exec<{ name: string }>(
28 "SELECT name FROM sqlite_master WHERE type = 'table' ORDER BY name",
29 )
30 .pipe(Effect.flatMap((cursor) => cursor.toArray()));
31
32export class MigratedObject extends Cloudflare.DurableObject<MigratedObject>()(
33 "SqlMigratedObject",
34 Effect.gen(function* () {
35 const snapshot = yield* Cloudflare.SqlMigrations(
36 "./test/Cloudflare/Workers/fixtures/sql-migrations/drizzle",
37 );
38 return Effect.gen(function* () {
39 const state = yield* Cloudflare.DurableObjectState;
40 const db = yield* Drizzle.DurableObject({
41 migrations: snapshot,
42 relations,
43 });
44 const boots = ((yield* state.storage.get<number>("boots")) ?? 0) + 1;
45 yield* state.storage.put("boots", boots);
46
47 const inspect = () =>
48 Effect.gen(function* () {
49 const payload = yield* state.storage.sql
50 .exec<{ length: number }>(
51 "SELECT length(value) AS length FROM payload",
52 )
53 .pipe(Effect.flatMap((cursor) => cursor.one()));
54 const rows = yield* db.query.users.findMany({
55 with: { posts: true },
56 });
57 return {
58 tag: snapshot._tag,
59 table: snapshot.table,
60 records: snapshot.records.map((record) => ({
61 name: record.name,
62 hash: record.hash,
63 created_at: record.createdAtMillis ?? null,
64 })),
65 boots,
66 payloadLength: payload.length,
67 history: yield* history(state, snapshot.table),
68 tables: yield* tables(state),
69 users: rows
70 .map((row) => ({
71 name: row.name,
72 posts: row.posts.map((post) => post.title),
73 }))
74 .sort((left, right) => left.name.localeCompare(right.name)),
75 };
76 });
77
78 return {
79 inspect,
80 addUser: (name: string) =>
81 db.insert(users).values({ name }).pipe(Effect.asVoid),
82 repeat: () =>
83 Effect.gen(function* () {
84 yield* Drizzle.DurableObject({ migrations: snapshot, relations });
85 yield* snapshot.apply();
86 return yield* inspect();
87 }).pipe(Effect.provideService(Cloudflare.DurableObjectState, state)),
88 reset: () =>
89 state.abort("sql migration reactivation", { retryAlarm: false }),
90 };
91 });
92 }),
93) {}
94
95export class CustomMigratedObject extends Cloudflare.DurableObject<CustomMigratedObject>()(
96 "CustomSqlMigratedObject",
97 Effect.gen(function* () {
98 const snapshot = yield* Cloudflare.SqlMigrations({
99 dir: "./test/Cloudflare/Workers/fixtures/sql-migrations/flat",
100 table: "fixture_migrations",
101 });
102 return Effect.gen(function* () {
103 const state = yield* Cloudflare.DurableObjectState;
104 yield* snapshot.apply().pipe(Effect.orDie);
105 const inspect = () =>
106 Effect.gen(function* () {
107 const rows = yield* state.storage.sql
108 .exec<{ value: string }>(
109 "SELECT value FROM flat_values ORDER BY id",
110 )
111 .pipe(Effect.flatMap((cursor) => cursor.toArray()));
112 return {
113 tag: snapshot._tag,
114 table: snapshot.table,
115 names: snapshot.records.map((record) => record.name),
116 history: yield* history(state, snapshot.table),
117 tables: yield* tables(state),
118 values: rows.map((row) => row.value),
119 };
120 });
121 return {
122 inspect,
123 repeat: () =>
124 snapshot
125 .apply()
126 .pipe(
127 Effect.andThen(inspect),
128 Effect.provideService(Cloudflare.DurableObjectState, state),
129 ),
130 };
131 });
132 }),
133) {}
134
135export class MigrationScenarios extends Cloudflare.DurableObject<MigrationScenarios>()(
136 "SqlMigrationScenarios",
137 Effect.gen(function* () {
138 const snapshot = yield* Cloudflare.SqlMigrations({
139 dir: "./test/Cloudflare/Workers/fixtures/sql-migrations/drizzle",
140 });
141 const broken = yield* Cloudflare.SqlMigrations(
142 "./test/Cloudflare/Workers/fixtures/sql-migrations/rollback",
143 );
144 return Effect.gen(function* () {
145 const state = yield* Cloudflare.DurableObjectState;
146 return {
147 rollback: () =>
148 Effect.gen(function* () {
149 const result = yield* broken.apply().pipe(Effect.result);
150 const rows = yield* state.storage.sql
151 .exec<{ value: string }>(
152 "SELECT value FROM stable_values ORDER BY id",
153 )
154 .pipe(Effect.flatMap((cursor) => cursor.toArray()));
155 return {
156 error: Result.isFailure(result) ? result.failure._tag : null,
157 history: yield* history(state, broken.table),
158 tables: yield* tables(state),
159 values: rows.map((row) => row.value),
160 names: broken.records.map((record) => record.name),
161 };
162 }).pipe(Effect.provideService(Cloudflare.DurableObjectState, state)),
163 adopt: () =>
164 Effect.gen(function* () {
165 const first = snapshot.records[0]!;
166 yield* Drizzle.DurableObject({
167 migrations: { migrations: { [first.name]: first.sql } },
168 relations,
169 });
170 const before = yield* history(state, "__drizzle_migrations");
171 yield* snapshot.apply();
172 const adopted = yield* history(state, snapshot.table);
173 yield* snapshot.apply();
174 const db = yield* Drizzle.DurableObject({
175 migrations: snapshot,
176 relations,
177 });
178 return {
179 before,
180 after: yield* history(state, "__drizzle_migrations"),
181 adopted,
182 repeated: yield* history(state, snapshot.table),
183 names: snapshot.records.map((record) => record.name),
184 users: (yield* db.query.users.findMany({
185 with: { posts: true },
186 })).map((row) => ({
187 name: row.name,
188 posts: row.posts.map((post) => post.title),
189 })),
190 };
191 }).pipe(Effect.provideService(Cloudflare.DurableObjectState, state)),
192 conflict: (empty: boolean = false) =>
193 Effect.gen(function* () {
194 const first = snapshot.records[0]!;
195 const db = yield* Drizzle.DurableObject({
196 migrations: { migrations: { [first.name]: first.sql } },
197 relations,
198 });
199 const before = yield* history(state, "__drizzle_migrations");
200 const result = yield* applySqlMigrations({
201 _tag: snapshot._tag,
202 table: snapshot.table,
203 records: empty ? [] : snapshot.records.slice(1),
204 }).pipe(Effect.result);
205 return {
206 error: Result.isFailure(result) ? result.failure._tag : null,
207 before,
208 after: yield* history(state, "__drizzle_migrations"),
209 tables: yield* tables(state),
210 users: (yield* db.select().from(users)).map((row) => row.name),
211 };
212 }).pipe(Effect.provideService(Cloudflare.DurableObjectState, state)),
213 };
214 });
215 }),
216) {}