Skip to content

Commit 003df47

Browse files
committed
Add database migrations
This'll mean that any existing MapSync database (like the example 4.6GB database) will be migrated to have generated columns.
1 parent 632bca3 commit 003df47

File tree

4 files changed

+167
-75
lines changed

4 files changed

+167
-75
lines changed

server/src/Renderer.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { spawn } from "node:child_process";
22
import { promisify } from "node:util";
3-
import * as database from "./database.ts";
3+
import * as database from "./db/database.ts";
44

55
export async function renderTile(
66
dimension: string,
@@ -25,8 +25,8 @@ export async function renderTile(
2525

2626
const chunkHeaderBuf = Buffer.allocUnsafe(4 + 4 + 2); // reused. 32+32+16 bit
2727
for (const chunk of allChunks) {
28-
chunkHeaderBuf.writeInt32BE(chunk.chunk_x, 0);
29-
chunkHeaderBuf.writeInt32BE(chunk.chunk_z, 4);
28+
chunkHeaderBuf.writeInt32BE(chunk.chunkX, 0);
29+
chunkHeaderBuf.writeInt32BE(chunk.chunkZ, 4);
3030
chunkHeaderBuf.writeUInt16BE(chunk.version, 8);
3131
await write(chunkHeaderBuf);
3232
await write(chunk.data);
Lines changed: 29 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
import { Database as BunSqliteDatabase } from "bun:sqlite";
22

3-
import * as kysely from "kysely";
3+
import { Kysely, type Generated, Migrator } from "kysely";
44
import { BunSqliteDialect } from "kysely-bun-sqlite";
55

6-
import { DATA_FOLDER } from "./metadata.ts";
7-
import { type Pos2D } from "./model.ts";
6+
import { DATA_FOLDER } from "../metadata.ts";
7+
import Migrations from "./migrations.ts";
8+
import { type Pos2D } from "../model.ts";
89

9-
let database: kysely.Kysely<Database> | null = null;
10+
let database: Kysely<Database> | null = null;
1011

1112
export interface Database {
1213
chunk_data: {
@@ -18,17 +19,17 @@ export interface Database {
1819
world: string;
1920
chunk_x: number;
2021
chunk_z: number;
21-
gen_region_x: kysely.Generated<number>;
22-
gen_region_z: kysely.Generated<number>;
23-
gen_region_coord: kysely.Generated<string>;
22+
gen_region_x: Generated<number>;
23+
gen_region_z: Generated<number>;
24+
gen_region_coord: Generated<string>;
2425
uuid: string;
2526
ts: number;
2627
hash: Buffer;
2728
};
2829
}
2930

3031
export function get() {
31-
return (database ??= new kysely.Kysely<Database>({
32+
return (database ??= new Kysely<Database>({
3233
dialect: new BunSqliteDialect({
3334
database: new BunSqliteDatabase(
3435
Bun.env["SQLITE_PATH"] ?? `${DATA_FOLDER}/db.sqlite`,
@@ -41,62 +42,20 @@ export function get() {
4142
}));
4243
}
4344

45+
export function getMigrations(): Migrator {
46+
return new Migrator({
47+
db: get(),
48+
provider: new Migrations(),
49+
});
50+
}
51+
52+
/** Convenience function to migrate to latest */
4453
export async function setup() {
45-
await get()
46-
.transaction()
47-
.execute(async (db) => {
48-
await db.schema
49-
.createTable("chunk_data")
50-
.ifNotExists()
51-
.addColumn("hash", "blob", (col) => col.notNull().primaryKey())
52-
.addColumn("version", "integer", (col) => col.notNull())
53-
.addColumn("data", "blob", (col) => col.notNull())
54-
.execute();
55-
await db.schema
56-
.createTable("player_chunk")
57-
.ifNotExists()
58-
.addColumn("world", "text", (col) => col.notNull())
59-
.addColumn("chunk_x", "integer", (col) => col.notNull())
60-
.addColumn("chunk_z", "integer", (col) => col.notNull())
61-
.addColumn("gen_region_x", "integer", (col) =>
62-
col
63-
.generatedAlwaysAs(
64-
kysely.sql<number>`floor(chunk_x / 32.0)`,
65-
)
66-
.notNull(),
67-
)
68-
.addColumn("gen_region_z", "integer", (col) =>
69-
col
70-
.generatedAlwaysAs(
71-
kysely.sql<number>`floor(chunk_z / 32.0)`,
72-
)
73-
.notNull(),
74-
)
75-
.addColumn("gen_region_coord", "text", (col) => {
76-
return col
77-
.generatedAlwaysAs(
78-
kysely.sql<string>`gen_region_x || '_' || gen_region_z`,
79-
)
80-
.notNull();
81-
})
82-
.addColumn("uuid", "text", (col) => col.notNull())
83-
.addColumn("ts", "bigint", (col) => col.notNull())
84-
.addColumn("hash", "blob", (col) => col.notNull())
85-
.addPrimaryKeyConstraint("PK_coords_and_player", [
86-
"world",
87-
"chunk_x",
88-
"chunk_z",
89-
"uuid",
90-
])
91-
.addForeignKeyConstraint(
92-
"FK_chunk_ref",
93-
["hash"],
94-
"chunk_data",
95-
["hash"],
96-
(fk) => fk.onUpdate("no action").onDelete("no action"),
97-
)
98-
.execute();
99-
});
54+
const results = await getMigrations().migrateToLatest();
55+
if (results.error) {
56+
throw results.error;
57+
}
58+
return results.results ?? [];
10059
}
10160

10261
/**
@@ -178,13 +137,13 @@ export async function storeChunkData(
178137
) {
179138
await get()
180139
.transaction()
181-
.execute(async (db) => {
182-
await db
140+
.execute(async (transaction) => {
141+
await transaction
183142
.insertInto("chunk_data")
184143
.values({ hash, version, data })
185144
.onConflict((oc) => oc.column("hash").doNothing())
186145
.execute();
187-
await db
146+
await transaction
188147
.replaceInto("player_chunk")
189148
.values({
190149
world: dimension,
@@ -210,16 +169,16 @@ export async function getRegionChunks(
210169
.selectFrom("player_chunk")
211170
.innerJoin("chunk_data", "chunk_data.hash", "player_chunk.hash")
212171
.select([
213-
"player_chunk.chunk_x as chunk_x",
214-
"player_chunk.chunk_z as chunk_z",
172+
"player_chunk.chunk_x as chunkX",
173+
"player_chunk.chunk_z as chunkZ",
215174
(eb) => eb.fn.max("player_chunk.ts").as("timestamp"),
216175
"chunk_data.version as version",
217176
"chunk_data.data as data",
218177
])
219178
.where("player_chunk.world", "=", dimension)
220179
.where("player_chunk.gen_region_x", "=", regionX)
221180
.where("player_chunk.gen_region_z", "=", regionZ)
222-
.groupBy(["chunk_x", "chunk_z", "version", "data"])
223-
.orderBy("player_chunk.ts", "desc")
181+
.groupBy(["chunkX", "chunkZ", "version", "data"])
182+
.orderBy("timestamp", "desc")
224183
.execute();
225184
}

server/src/db/migrations.ts

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
import { Kysely, sql, type Migration, type MigrationProvider } from "kysely";
2+
3+
type MigrationRegistry = Record<string, Migration>;
4+
type MigrationClass = { name: string } & (new () => Migration);
5+
6+
export default class Migrations implements MigrationProvider {
7+
public async getMigrations(): Promise<MigrationRegistry> {
8+
return this.generateMigrationRegistry([
9+
Migration_0001_InitialSetup,
10+
Migration_0002_GenerateRegionCoordColumns,
11+
]);
12+
}
13+
14+
private generateMigrationRegistry(
15+
migrations: Array<MigrationClass>,
16+
): MigrationRegistry {
17+
const registry: MigrationRegistry = {};
18+
for (const clazz of migrations) {
19+
registry[clazz.name] = new clazz();
20+
}
21+
return registry;
22+
}
23+
}
24+
25+
// ============================================================
26+
// WARNING FOR WRITING MIGRATIONS!
27+
//
28+
// Kysely does not respect class functions: your "up" and "down" methods MUST
29+
// be fields, not class functions, otherwise your migration will fail!
30+
// ============================================================
31+
32+
export class Migration_0001_InitialSetup implements Migration {
33+
public up = async (db: Kysely<any>) => {
34+
await db.transaction().execute(async (transaction) => {
35+
await transaction.schema
36+
.createTable("chunk_data")
37+
.ifNotExists()
38+
.addColumn("hash", "blob", (col) => col.notNull().primaryKey())
39+
.addColumn("version", "integer", (col) => col.notNull())
40+
.addColumn("data", "blob", (col) => col.notNull())
41+
.execute();
42+
await transaction.schema
43+
.createTable("player_chunk")
44+
.ifNotExists()
45+
.addColumn("world", "text", (col) => col.notNull())
46+
.addColumn("chunk_x", "integer", (col) => col.notNull())
47+
.addColumn("chunk_z", "integer", (col) => col.notNull())
48+
.addColumn("uuid", "text", (col) => col.notNull())
49+
.addColumn("ts", "bigint", (col) => col.notNull())
50+
.addColumn("hash", "blob", (col) => col.notNull())
51+
.addPrimaryKeyConstraint("PK_coords_and_player", [
52+
"world",
53+
"chunk_x",
54+
"chunk_z",
55+
"uuid",
56+
])
57+
.addForeignKeyConstraint(
58+
"FK_chunk_ref",
59+
["hash"],
60+
"chunk_data",
61+
["hash"],
62+
(fk) => fk.onUpdate("no action").onDelete("no action"),
63+
)
64+
.execute();
65+
});
66+
};
67+
// Probably shouldn't define a "down" since that just means an empty db
68+
}
69+
70+
export class Migration_0002_GenerateRegionCoordColumns implements Migration {
71+
public up = async (db: Kysely<any>) => {
72+
await db.transaction().execute(async (transaction) => {
73+
await transaction.schema
74+
.alterTable("player_chunk")
75+
.addColumn("gen_region_x", "integer", (col) => {
76+
return col
77+
.generatedAlwaysAs(sql<number>`floor(chunk_x / 32.0)`)
78+
.notNull();
79+
})
80+
.execute();
81+
await transaction.schema
82+
.alterTable("player_chunk")
83+
.addColumn("gen_region_z", "integer", (col) => {
84+
return col
85+
.generatedAlwaysAs(sql<number>`floor(chunk_z / 32.0)`)
86+
.notNull();
87+
})
88+
.execute();
89+
await transaction.schema
90+
.alterTable("player_chunk")
91+
.addColumn("gen_region_coord", "text", (col) => {
92+
return col
93+
.generatedAlwaysAs(
94+
sql<string>`gen_region_x || '_' || gen_region_z`,
95+
)
96+
.notNull();
97+
})
98+
.execute();
99+
});
100+
};
101+
public down = async (db: Kysely<any>) => {
102+
await db.transaction().execute(async (transaction) => {
103+
await transaction.schema
104+
.alterTable("player_chunk")
105+
.dropColumn("gen_region_coord")
106+
.execute();
107+
await transaction.schema
108+
.alterTable("player_chunk")
109+
.dropColumn("gen_region_x")
110+
.execute();
111+
await transaction.schema
112+
.alterTable("player_chunk")
113+
.dropColumn("gen_region_z")
114+
.execute();
115+
});
116+
};
117+
}

server/src/main.ts

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import "./cli.ts";
2-
import * as database from "./database.ts";
2+
import * as database from "./db/database.ts";
33
import * as metadata from "./metadata.ts";
44
import {
55
type ClientPacket,
@@ -18,7 +18,23 @@ import { isAuthed, OnlineAuth, requireAuth } from "./net/auth.ts";
1818

1919
let config: metadata.Config = null!;
2020
Promise.resolve().then(async () => {
21-
await database.setup();
21+
for (const result of await database.setup()) {
22+
switch (result.status) {
23+
case "Success":
24+
console.info(`Migration [${result.migrationName}] applied!`);
25+
break;
26+
case "Error":
27+
console.error(
28+
`Migration [${result.migrationName}] failed to apply!`,
29+
);
30+
break;
31+
case "NotExecuted":
32+
console.warn(
33+
`Migration [${result.migrationName}] was not applied!`,
34+
);
35+
break;
36+
}
37+
}
2238

2339
config = metadata.getConfig();
2440

0 commit comments

Comments
 (0)