@fragno-dev/db 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +17 -0
- package/.turbo/turbo-types$colon$check.log +1 -0
- package/README.md +13 -0
- package/dist/cuid.js +3 -0
- package/dist/mod.d.ts +5 -0
- package/dist/mod.d.ts.map +1 -0
- package/dist/mod.js +6 -0
- package/dist/mod.js.map +1 -0
- package/dist/schema/create.d.ts +282 -0
- package/dist/schema/create.d.ts.map +1 -0
- package/dist/schema/create.js +437 -0
- package/dist/schema/create.js.map +1 -0
- package/package.json +34 -0
- package/src/cuid.ts +3 -0
- package/src/migration-engine/auto-from-schema.test.ts +238 -0
- package/src/migration-engine/auto-from-schema.ts +106 -0
- package/src/migration-engine/create.test.ts +349 -0
- package/src/migration-engine/create.ts +199 -0
- package/src/migration-engine/shared.ts +102 -0
- package/src/mod.ts +1 -0
- package/src/schema/create.test.ts +287 -0
- package/src/schema/create.ts +809 -0
- package/src/shared/config.ts +18 -0
- package/src/shared/prisma.ts +45 -0
- package/src/shared/providers.ts +10 -0
- package/src/util/deep-equal.ts +29 -0
- package/tsconfig.json +10 -0
- package/tsdown.config.ts +7 -0
- package/vitest.config.ts +7 -0
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { column, idColumn, referenceColumn, schema } from "../schema/create";
|
|
3
|
+
import { generateMigrationFromSchema } from "./auto-from-schema";
|
|
4
|
+
|
|
5
|
+
describe("generateMigrationFromSchema", () => {
|
|
6
|
+
it("should generate create-table operation for new tables", () => {
|
|
7
|
+
const mySchema = schema((s) => {
|
|
8
|
+
return s
|
|
9
|
+
.addTable("users", (t) => {
|
|
10
|
+
return t.addColumn("id", idColumn()).addColumn("name", column("string"));
|
|
11
|
+
})
|
|
12
|
+
.addTable("posts", (t) => {
|
|
13
|
+
return t
|
|
14
|
+
.addColumn("id", idColumn())
|
|
15
|
+
.addColumn("title", column("string"))
|
|
16
|
+
.addColumn("content", column("string"));
|
|
17
|
+
});
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
// Version 0 -> 1: users table created
|
|
21
|
+
// Version 1 -> 2: posts table created
|
|
22
|
+
// We want to generate the migration for version 1 -> 2
|
|
23
|
+
const operations = generateMigrationFromSchema(mySchema, 1, 2, {
|
|
24
|
+
provider: "postgresql",
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
expect(operations).toHaveLength(1);
|
|
28
|
+
expect(operations[0]).toMatchObject({
|
|
29
|
+
type: "create-table",
|
|
30
|
+
value: mySchema.tables.posts,
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("should generate multiple table operations in sequence", () => {
|
|
35
|
+
const mySchema = schema((s) => {
|
|
36
|
+
return s
|
|
37
|
+
.addTable("users", (t) => {
|
|
38
|
+
return t.addColumn("id", idColumn()).addColumn("name", column("string"));
|
|
39
|
+
})
|
|
40
|
+
.addTable("posts", (t) => {
|
|
41
|
+
return t.addColumn("id", idColumn()).addColumn("title", column("string"));
|
|
42
|
+
})
|
|
43
|
+
.addTable("comments", (t) => {
|
|
44
|
+
return t.addColumn("id", idColumn()).addColumn("text", column("string"));
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
// Generate migrations from version 0 to 3 (all three tables)
|
|
49
|
+
const operations = generateMigrationFromSchema(mySchema, 0, 3, {
|
|
50
|
+
provider: "postgresql",
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
expect(operations).toHaveLength(3);
|
|
54
|
+
expect(operations[0]).toMatchObject({
|
|
55
|
+
type: "create-table",
|
|
56
|
+
value: mySchema.tables.users,
|
|
57
|
+
});
|
|
58
|
+
expect(operations[1]).toMatchObject({
|
|
59
|
+
type: "create-table",
|
|
60
|
+
value: mySchema.tables.posts,
|
|
61
|
+
});
|
|
62
|
+
expect(operations[2]).toMatchObject({
|
|
63
|
+
type: "create-table",
|
|
64
|
+
value: mySchema.tables.comments,
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("should generate add-foreign-key operation for new foreign keys", () => {
|
|
69
|
+
const mySchema = schema((s) => {
|
|
70
|
+
return s
|
|
71
|
+
.addTable("users", (t) => {
|
|
72
|
+
return t.addColumn("id", idColumn());
|
|
73
|
+
})
|
|
74
|
+
.addTable("posts", (t) => {
|
|
75
|
+
return t.addColumn("id", idColumn()).addColumn("authorId", referenceColumn());
|
|
76
|
+
})
|
|
77
|
+
.addReference("posts", "author", {
|
|
78
|
+
columns: ["authorId"],
|
|
79
|
+
targetTable: "users",
|
|
80
|
+
targetColumns: ["id"],
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
// Version 0 -> 1: users table
|
|
85
|
+
// Version 1 -> 2: posts table
|
|
86
|
+
// Version 2 -> 3: author foreign key
|
|
87
|
+
const operations = generateMigrationFromSchema(mySchema, 2, 3, {
|
|
88
|
+
provider: "postgresql",
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
expect(operations).toHaveLength(1);
|
|
92
|
+
expect(operations[0]).toMatchObject({
|
|
93
|
+
type: "add-foreign-key",
|
|
94
|
+
table: "posts",
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
const fkOp = operations[0];
|
|
98
|
+
if (fkOp.type === "add-foreign-key") {
|
|
99
|
+
expect(fkOp.value).toMatchObject({
|
|
100
|
+
name: "posts_users_author_fk",
|
|
101
|
+
table: "posts",
|
|
102
|
+
referencedTable: "users",
|
|
103
|
+
columns: ["authorId"],
|
|
104
|
+
referencedColumns: ["id"],
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it("should generate add-index operation for indexes defined in createIndex", () => {
|
|
110
|
+
const mySchema = schema((s) => {
|
|
111
|
+
return s.addTable("users", (t) => {
|
|
112
|
+
return t
|
|
113
|
+
.addColumn("id", idColumn())
|
|
114
|
+
.addColumn("email", column("string"))
|
|
115
|
+
.createIndex("idx_email", ["email"], { unique: true });
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
// Version 0 -> 1: users table
|
|
120
|
+
// Version 1 -> 2: email index
|
|
121
|
+
const operations = generateMigrationFromSchema(mySchema, 1, 2, {
|
|
122
|
+
provider: "postgresql",
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
expect(operations).toHaveLength(1);
|
|
126
|
+
expect(operations[0]).toMatchObject({
|
|
127
|
+
type: "add-index",
|
|
128
|
+
table: "users",
|
|
129
|
+
name: "idx_email",
|
|
130
|
+
columns: ["email"],
|
|
131
|
+
unique: true,
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it("should generate mixed operations for tables and foreign keys", () => {
|
|
136
|
+
const mySchema = schema((s) => {
|
|
137
|
+
return s
|
|
138
|
+
.addTable("users", (t) => {
|
|
139
|
+
return t.addColumn("id", idColumn()).addColumn("name", column("string"));
|
|
140
|
+
})
|
|
141
|
+
.addTable("posts", (t) => {
|
|
142
|
+
return t.addColumn("id", idColumn()).addColumn("authorId", referenceColumn());
|
|
143
|
+
})
|
|
144
|
+
.addReference("posts", "author", {
|
|
145
|
+
columns: ["authorId"],
|
|
146
|
+
targetTable: "users",
|
|
147
|
+
targetColumns: ["id"],
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
// Generate all migrations from scratch
|
|
152
|
+
const operations = generateMigrationFromSchema(mySchema, 0, 3, {
|
|
153
|
+
provider: "postgresql",
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
expect(operations).toHaveLength(3);
|
|
157
|
+
expect(operations[0].type).toBe("create-table");
|
|
158
|
+
expect(operations[1].type).toBe("create-table");
|
|
159
|
+
expect(operations[2].type).toBe("add-foreign-key");
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it("should generate mixed operations for tables, indexes, and foreign keys", () => {
|
|
163
|
+
const mySchema = schema((s) => {
|
|
164
|
+
return s
|
|
165
|
+
.addTable("users", (t) => {
|
|
166
|
+
return t
|
|
167
|
+
.addColumn("id", idColumn())
|
|
168
|
+
.addColumn("email", column("string"))
|
|
169
|
+
.createIndex("idx_email", ["email"], { unique: true });
|
|
170
|
+
})
|
|
171
|
+
.addTable("posts", (t) => {
|
|
172
|
+
return t.addColumn("id", idColumn()).addColumn("authorId", referenceColumn());
|
|
173
|
+
})
|
|
174
|
+
.addReference("posts", "author", {
|
|
175
|
+
columns: ["authorId"],
|
|
176
|
+
targetTable: "users",
|
|
177
|
+
targetColumns: ["id"],
|
|
178
|
+
});
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
// Generate all migrations from scratch
|
|
182
|
+
const operations = generateMigrationFromSchema(mySchema, 0, 4, {
|
|
183
|
+
provider: "postgresql",
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
expect(operations).toHaveLength(4);
|
|
187
|
+
expect(operations[0].type).toBe("create-table");
|
|
188
|
+
expect(operations[1].type).toBe("add-index");
|
|
189
|
+
expect(operations[2].type).toBe("create-table");
|
|
190
|
+
expect(operations[3].type).toBe("add-foreign-key");
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
it("should generate no operations when version range is empty", () => {
|
|
194
|
+
const mySchema = schema((s) => {
|
|
195
|
+
return s.addTable("users", (t) => {
|
|
196
|
+
return t.addColumn("id", idColumn()).addColumn("name", column("string"));
|
|
197
|
+
});
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
const operations = generateMigrationFromSchema(mySchema, 1, 1, {
|
|
201
|
+
provider: "postgresql",
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
expect(operations).toHaveLength(0);
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
it("should throw error when fromVersion exceeds schema version", () => {
|
|
208
|
+
const mySchema = schema((s) => s.addTable("users", (t) => t.addColumn("id", idColumn())));
|
|
209
|
+
|
|
210
|
+
expect(() => {
|
|
211
|
+
generateMigrationFromSchema(mySchema, 999, 1000, { provider: "postgresql" });
|
|
212
|
+
}).toThrow("fromVersion (999) exceeds schema version (1)");
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
it("should throw error when toVersion exceeds schema version", () => {
|
|
216
|
+
const mySchema = schema((s) => s.addTable("users", (t) => t.addColumn("id", idColumn())));
|
|
217
|
+
|
|
218
|
+
expect(() => {
|
|
219
|
+
generateMigrationFromSchema(mySchema, 0, 999, { provider: "postgresql" });
|
|
220
|
+
}).toThrow("toVersion (999) exceeds schema version (1)");
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
it("should throw error when trying to migrate backwards", () => {
|
|
224
|
+
const mySchema = schema((s) => s.addTable("users", (t) => t.addColumn("id", idColumn())));
|
|
225
|
+
|
|
226
|
+
expect(() => {
|
|
227
|
+
generateMigrationFromSchema(mySchema, 1, 0, { provider: "postgresql" });
|
|
228
|
+
}).toThrow("Cannot migrate backwards");
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
it("should throw error for negative fromVersion", () => {
|
|
232
|
+
const mySchema = schema((s) => s.addTable("users", (t) => t.addColumn("id", idColumn())));
|
|
233
|
+
|
|
234
|
+
expect(() => {
|
|
235
|
+
generateMigrationFromSchema(mySchema, -1, 1, { provider: "postgresql" });
|
|
236
|
+
}).toThrow("fromVersion cannot be negative");
|
|
237
|
+
});
|
|
238
|
+
});
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { compileForeignKey, type AnySchema } from "../schema/create";
|
|
2
|
+
import type { MigrationOperation } from "./shared";
|
|
3
|
+
import type { Provider } from "../shared/providers";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Generate migration operations from a schema's operation history
|
|
7
|
+
*
|
|
8
|
+
* The schema version number represents the cumulative number of operations applied.
|
|
9
|
+
* This function takes operations from the schema between fromVersion and toVersion,
|
|
10
|
+
* and converts them into migration operations.
|
|
11
|
+
*
|
|
12
|
+
* @param targetSchema - The schema containing the operations history
|
|
13
|
+
* @param fromVersion - The current database version (e.g., 0)
|
|
14
|
+
* @param toVersion - The target version to migrate to (e.g., 5)
|
|
15
|
+
* @param options - Migration generation options
|
|
16
|
+
* @returns Array of migration operations to apply
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* ```ts
|
|
20
|
+
* const mySchema = schema(s => s
|
|
21
|
+
* .addTable("users", t => t.addColumn("id", idColumn())) // version 1
|
|
22
|
+
* .addTable("posts", t => t.addColumn("id", idColumn())) // version 2
|
|
23
|
+
* );
|
|
24
|
+
*
|
|
25
|
+
* // Generate operations from version 0 to 1 (only creates users table)
|
|
26
|
+
* const operations = generateMigrationFromSchema(mySchema, 0, 1, {
|
|
27
|
+
* provider: "postgresql"
|
|
28
|
+
* });
|
|
29
|
+
* ```
|
|
30
|
+
*/
|
|
31
|
+
export function generateMigrationFromSchema(
|
|
32
|
+
targetSchema: AnySchema,
|
|
33
|
+
fromVersion: number,
|
|
34
|
+
toVersion: number,
|
|
35
|
+
_options: {
|
|
36
|
+
provider: Provider;
|
|
37
|
+
},
|
|
38
|
+
): MigrationOperation[] {
|
|
39
|
+
if (fromVersion < 0) {
|
|
40
|
+
throw new Error(`fromVersion cannot be negative: ${fromVersion}`);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (fromVersion > targetSchema.version) {
|
|
44
|
+
throw new Error(
|
|
45
|
+
`fromVersion (${fromVersion}) exceeds schema version (${targetSchema.version})`,
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (toVersion > targetSchema.version) {
|
|
50
|
+
throw new Error(`toVersion (${toVersion}) exceeds schema version (${targetSchema.version})`);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (toVersion < fromVersion) {
|
|
54
|
+
throw new Error(
|
|
55
|
+
`Cannot migrate backwards: toVersion (${toVersion}) < fromVersion (${fromVersion})`,
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Get operations between fromVersion and toVersion
|
|
60
|
+
// Operations are 1-indexed (operation 0 is version 0→1)
|
|
61
|
+
const relevantOperations = targetSchema.operations.slice(fromVersion, toVersion);
|
|
62
|
+
|
|
63
|
+
// Convert schema operations to migration operations
|
|
64
|
+
const migrationOperations: MigrationOperation[] = [];
|
|
65
|
+
|
|
66
|
+
for (const op of relevantOperations) {
|
|
67
|
+
if (op.type === "add-table") {
|
|
68
|
+
migrationOperations.push({
|
|
69
|
+
type: "create-table",
|
|
70
|
+
value: op.table,
|
|
71
|
+
});
|
|
72
|
+
} else if (op.type === "add-reference") {
|
|
73
|
+
const table = targetSchema.tables[op.tableName];
|
|
74
|
+
if (!table) {
|
|
75
|
+
throw new Error(`Table ${op.tableName} not found in schema`);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Find the foreign key that matches this reference
|
|
79
|
+
const foreignKey = table.foreignKeys.find(
|
|
80
|
+
(fk) => fk.name === `${op.tableName}_${op.config.targetTable}_${op.referenceName}_fk`,
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
if (!foreignKey) {
|
|
84
|
+
throw new Error(
|
|
85
|
+
`Foreign key for reference ${op.referenceName} not found in table ${op.tableName}`,
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
migrationOperations.push({
|
|
90
|
+
type: "add-foreign-key",
|
|
91
|
+
table: op.tableName,
|
|
92
|
+
value: compileForeignKey(foreignKey),
|
|
93
|
+
});
|
|
94
|
+
} else if (op.type === "add-index") {
|
|
95
|
+
migrationOperations.push({
|
|
96
|
+
type: "add-index",
|
|
97
|
+
table: op.tableName,
|
|
98
|
+
name: op.name,
|
|
99
|
+
columns: op.columns,
|
|
100
|
+
unique: op.unique,
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return migrationOperations;
|
|
106
|
+
}
|
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from "vitest";
|
|
2
|
+
import { createMigrator, type MigrationEngineOptions } from "./create";
|
|
3
|
+
import { schema, idColumn, column, referenceColumn } from "../schema/create";
|
|
4
|
+
import type { MigrationOperation } from "./shared";
|
|
5
|
+
|
|
6
|
+
describe("createMigrator", () => {
|
|
7
|
+
const testSchema = schema((s) => {
|
|
8
|
+
return s
|
|
9
|
+
.addTable("users", (t) => {
|
|
10
|
+
return t.addColumn("id", idColumn()).addColumn("name", column("string"));
|
|
11
|
+
})
|
|
12
|
+
.addTable("posts", (t) => {
|
|
13
|
+
return t.addColumn("id", idColumn()).addColumn("title", column("string"));
|
|
14
|
+
})
|
|
15
|
+
.addTable("comments", (t) => {
|
|
16
|
+
return t.addColumn("id", idColumn()).addColumn("text", column("string"));
|
|
17
|
+
});
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
function createTestMigrator(currentVersion = 0) {
|
|
21
|
+
const executedOperations: MigrationOperation[][] = [];
|
|
22
|
+
let version = currentVersion;
|
|
23
|
+
|
|
24
|
+
const options: MigrationEngineOptions = {
|
|
25
|
+
schema: testSchema,
|
|
26
|
+
userConfig: {
|
|
27
|
+
provider: "postgresql",
|
|
28
|
+
},
|
|
29
|
+
executor: async (operations) => {
|
|
30
|
+
executedOperations.push(operations);
|
|
31
|
+
},
|
|
32
|
+
settings: {
|
|
33
|
+
getVersion: async () => version,
|
|
34
|
+
updateSettingsInMigration: async (newVersion) => {
|
|
35
|
+
version = newVersion;
|
|
36
|
+
return [
|
|
37
|
+
{
|
|
38
|
+
type: "custom",
|
|
39
|
+
sql: `UPDATE _settings SET version = ${newVersion}`,
|
|
40
|
+
},
|
|
41
|
+
];
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const migrator = createMigrator(options);
|
|
47
|
+
|
|
48
|
+
return { migrator, executedOperations, getVersion: () => version };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
describe("migrate()", () => {
|
|
52
|
+
it("should migrate from version 0 to latest schema version", async () => {
|
|
53
|
+
const { migrator, getVersion } = createTestMigrator(0);
|
|
54
|
+
|
|
55
|
+
const result = await migrator.migrate();
|
|
56
|
+
|
|
57
|
+
expect(result.operations.length).toBeGreaterThan(0);
|
|
58
|
+
expect(getVersion()).toBe(testSchema.version);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("should migrate from intermediate version to latest", async () => {
|
|
62
|
+
const { migrator, getVersion } = createTestMigrator(1);
|
|
63
|
+
|
|
64
|
+
const result = await migrator.migrate();
|
|
65
|
+
|
|
66
|
+
expect(result.operations.length).toBeGreaterThan(0);
|
|
67
|
+
expect(getVersion()).toBe(testSchema.version);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("should return empty operations when already at latest version", async () => {
|
|
71
|
+
const { migrator, getVersion } = createTestMigrator(testSchema.version);
|
|
72
|
+
|
|
73
|
+
const result = await migrator.migrate();
|
|
74
|
+
|
|
75
|
+
expect(result.operations).toEqual([]);
|
|
76
|
+
expect(getVersion()).toBe(testSchema.version);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("should generate correct operations when jumping multiple versions", async () => {
|
|
80
|
+
const { migrator } = createTestMigrator(0);
|
|
81
|
+
|
|
82
|
+
const result = await migrator.migrate();
|
|
83
|
+
|
|
84
|
+
// Should create all 3 tables plus settings update
|
|
85
|
+
const createTableOps = result.operations.filter((op) => op.type === "create-table");
|
|
86
|
+
expect(createTableOps).toHaveLength(3);
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
describe("migrateTo()", () => {
|
|
91
|
+
it("should migrate forward from version 0 to version 2", async () => {
|
|
92
|
+
const { migrator, getVersion } = createTestMigrator(0);
|
|
93
|
+
|
|
94
|
+
const result = await migrator.migrateTo(2);
|
|
95
|
+
|
|
96
|
+
expect(result.operations.length).toBeGreaterThan(0);
|
|
97
|
+
expect(getVersion()).toBe(2);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it("should migrate forward by jumping multiple versions", async () => {
|
|
101
|
+
const { migrator, getVersion } = createTestMigrator(0);
|
|
102
|
+
|
|
103
|
+
const result = await migrator.migrateTo(testSchema.version);
|
|
104
|
+
|
|
105
|
+
expect(getVersion()).toBe(testSchema.version);
|
|
106
|
+
const createTableOps = result.operations.filter((op) => op.type === "create-table");
|
|
107
|
+
expect(createTableOps).toHaveLength(3);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it("should migrate forward one version at a time", async () => {
|
|
111
|
+
const { migrator, getVersion } = createTestMigrator(0);
|
|
112
|
+
|
|
113
|
+
await migrator.migrateTo(1);
|
|
114
|
+
expect(getVersion()).toBe(1);
|
|
115
|
+
|
|
116
|
+
await migrator.migrateTo(2);
|
|
117
|
+
expect(getVersion()).toBe(2);
|
|
118
|
+
|
|
119
|
+
await migrator.migrateTo(3);
|
|
120
|
+
expect(getVersion()).toBe(3);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it("should return empty operations when already at target version", async () => {
|
|
124
|
+
const { migrator, getVersion } = createTestMigrator(2);
|
|
125
|
+
|
|
126
|
+
const result = await migrator.migrateTo(2);
|
|
127
|
+
|
|
128
|
+
expect(result.operations).toEqual([]);
|
|
129
|
+
expect(getVersion()).toBe(2);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it("should throw error when trying to migrate backwards", async () => {
|
|
133
|
+
const { migrator } = createTestMigrator(3);
|
|
134
|
+
|
|
135
|
+
await expect(migrator.migrateTo(1)).rejects.toThrow(
|
|
136
|
+
"Cannot migrate backwards: current version is 3, target is 1. Only forward migrations are supported.",
|
|
137
|
+
);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it("should throw error when migrating to negative version", async () => {
|
|
141
|
+
const { migrator } = createTestMigrator(0);
|
|
142
|
+
|
|
143
|
+
await expect(migrator.migrateTo(-1)).rejects.toThrow(
|
|
144
|
+
"Cannot migrate to negative version: -1",
|
|
145
|
+
);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it("should throw error when migrating beyond schema version", async () => {
|
|
149
|
+
const { migrator } = createTestMigrator(0);
|
|
150
|
+
|
|
151
|
+
await expect(migrator.migrateTo(999)).rejects.toThrow(
|
|
152
|
+
`Cannot migrate to version 999: schema only has version ${testSchema.version}`,
|
|
153
|
+
);
|
|
154
|
+
});
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
describe("getVersion()", () => {
|
|
158
|
+
it("should return current version", async () => {
|
|
159
|
+
const { migrator } = createTestMigrator(0);
|
|
160
|
+
|
|
161
|
+
const version = await migrator.getVersion();
|
|
162
|
+
|
|
163
|
+
expect(version).toBe(0);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it("should return updated version after migration", async () => {
|
|
167
|
+
const { migrator } = createTestMigrator(0);
|
|
168
|
+
|
|
169
|
+
await migrator.migrateTo(2);
|
|
170
|
+
const version = await migrator.getVersion();
|
|
171
|
+
|
|
172
|
+
expect(version).toBe(2);
|
|
173
|
+
});
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
describe("execute()", () => {
|
|
177
|
+
it("should execute migration operations", async () => {
|
|
178
|
+
const { migrator, executedOperations } = createTestMigrator(0);
|
|
179
|
+
|
|
180
|
+
const result = await migrator.migrateTo(1);
|
|
181
|
+
await result.execute();
|
|
182
|
+
|
|
183
|
+
expect(executedOperations).toHaveLength(1);
|
|
184
|
+
expect(executedOperations[0].length).toBeGreaterThan(0);
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it("should not execute operations until execute() is called", async () => {
|
|
188
|
+
const { migrator, executedOperations } = createTestMigrator(0);
|
|
189
|
+
|
|
190
|
+
await migrator.migrateTo(1);
|
|
191
|
+
|
|
192
|
+
expect(executedOperations).toHaveLength(0);
|
|
193
|
+
});
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
describe("transformers", () => {
|
|
197
|
+
it("should apply afterAuto transformer", async () => {
|
|
198
|
+
const executedOperations: MigrationOperation[][] = [];
|
|
199
|
+
let version = 0;
|
|
200
|
+
|
|
201
|
+
const afterAutoSpy = vi.fn((operations) => {
|
|
202
|
+
return [
|
|
203
|
+
...operations,
|
|
204
|
+
{
|
|
205
|
+
type: "custom",
|
|
206
|
+
sql: "-- Added by afterAuto",
|
|
207
|
+
},
|
|
208
|
+
];
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
const options: MigrationEngineOptions = {
|
|
212
|
+
schema: testSchema,
|
|
213
|
+
userConfig: {
|
|
214
|
+
provider: "postgresql",
|
|
215
|
+
},
|
|
216
|
+
executor: async (operations) => {
|
|
217
|
+
executedOperations.push(operations);
|
|
218
|
+
},
|
|
219
|
+
settings: {
|
|
220
|
+
getVersion: async () => version,
|
|
221
|
+
updateSettingsInMigration: async (newVersion) => {
|
|
222
|
+
version = newVersion;
|
|
223
|
+
return [];
|
|
224
|
+
},
|
|
225
|
+
},
|
|
226
|
+
transformers: [
|
|
227
|
+
{
|
|
228
|
+
afterAuto: afterAutoSpy,
|
|
229
|
+
},
|
|
230
|
+
],
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
const migrator = createMigrator(options);
|
|
234
|
+
const result = await migrator.migrateTo(1);
|
|
235
|
+
|
|
236
|
+
expect(afterAutoSpy).toHaveBeenCalledOnce();
|
|
237
|
+
expect(result.operations.some((op) => op.type === "custom")).toBe(true);
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
it("should apply afterAll transformer", async () => {
|
|
241
|
+
const executedOperations: MigrationOperation[][] = [];
|
|
242
|
+
let version = 0;
|
|
243
|
+
|
|
244
|
+
const afterAllSpy = vi.fn((operations) => {
|
|
245
|
+
return [
|
|
246
|
+
...operations,
|
|
247
|
+
{
|
|
248
|
+
type: "custom",
|
|
249
|
+
sql: "-- Added by afterAll",
|
|
250
|
+
},
|
|
251
|
+
];
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
const options: MigrationEngineOptions = {
|
|
255
|
+
schema: testSchema,
|
|
256
|
+
userConfig: {
|
|
257
|
+
provider: "postgresql",
|
|
258
|
+
},
|
|
259
|
+
executor: async (operations) => {
|
|
260
|
+
executedOperations.push(operations);
|
|
261
|
+
},
|
|
262
|
+
settings: {
|
|
263
|
+
getVersion: async () => version,
|
|
264
|
+
updateSettingsInMigration: async (newVersion) => {
|
|
265
|
+
version = newVersion;
|
|
266
|
+
return [];
|
|
267
|
+
},
|
|
268
|
+
},
|
|
269
|
+
transformers: [
|
|
270
|
+
{
|
|
271
|
+
afterAll: afterAllSpy,
|
|
272
|
+
},
|
|
273
|
+
],
|
|
274
|
+
};
|
|
275
|
+
|
|
276
|
+
const migrator = createMigrator(options);
|
|
277
|
+
const result = await migrator.migrateTo(1);
|
|
278
|
+
|
|
279
|
+
expect(afterAllSpy).toHaveBeenCalledOnce();
|
|
280
|
+
expect(result.operations.some((op) => op.type === "custom")).toBe(true);
|
|
281
|
+
});
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
describe("updateSettings option", () => {
|
|
285
|
+
it("should update settings by default", async () => {
|
|
286
|
+
const { migrator } = createTestMigrator(0);
|
|
287
|
+
|
|
288
|
+
const result = await migrator.migrateTo(1);
|
|
289
|
+
|
|
290
|
+
const settingsOp = result.operations.find((op) => op.type === "custom");
|
|
291
|
+
expect(settingsOp).toBeDefined();
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
it("should skip settings update when updateSettings is false", async () => {
|
|
295
|
+
const { migrator } = createTestMigrator(0);
|
|
296
|
+
|
|
297
|
+
const result = await migrator.migrateTo(1, { updateSettings: false });
|
|
298
|
+
|
|
299
|
+
// When updateSettings is false, we shouldn't have the settings update operation
|
|
300
|
+
// Check that we only have create-table operations
|
|
301
|
+
const nonTableOps = result.operations.filter((op) => op.type !== "create-table");
|
|
302
|
+
expect(nonTableOps).toHaveLength(0);
|
|
303
|
+
});
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
describe("with foreign keys", () => {
|
|
307
|
+
it("should handle schema with foreign keys", async () => {
|
|
308
|
+
const fkSchema = schema((s) => {
|
|
309
|
+
return s
|
|
310
|
+
.addTable("users", (t) => {
|
|
311
|
+
return t.addColumn("id", idColumn());
|
|
312
|
+
})
|
|
313
|
+
.addTable("posts", (t) => {
|
|
314
|
+
return t.addColumn("id", idColumn()).addColumn("authorId", referenceColumn());
|
|
315
|
+
})
|
|
316
|
+
.addReference("posts", "author", {
|
|
317
|
+
columns: ["authorId"],
|
|
318
|
+
targetTable: "users",
|
|
319
|
+
targetColumns: ["id"],
|
|
320
|
+
});
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
let version = 0;
|
|
324
|
+
const options: MigrationEngineOptions = {
|
|
325
|
+
schema: fkSchema,
|
|
326
|
+
userConfig: {
|
|
327
|
+
provider: "postgresql",
|
|
328
|
+
},
|
|
329
|
+
executor: async () => {},
|
|
330
|
+
settings: {
|
|
331
|
+
getVersion: async () => version,
|
|
332
|
+
updateSettingsInMigration: async (newVersion) => {
|
|
333
|
+
version = newVersion;
|
|
334
|
+
return [];
|
|
335
|
+
},
|
|
336
|
+
},
|
|
337
|
+
};
|
|
338
|
+
|
|
339
|
+
const migrator = createMigrator(options);
|
|
340
|
+
const result = await migrator.migrate();
|
|
341
|
+
|
|
342
|
+
const createTableOps = result.operations.filter((op) => op.type === "create-table");
|
|
343
|
+
const fkOps = result.operations.filter((op) => op.type === "add-foreign-key");
|
|
344
|
+
|
|
345
|
+
expect(createTableOps).toHaveLength(2);
|
|
346
|
+
expect(fkOps).toHaveLength(1);
|
|
347
|
+
});
|
|
348
|
+
});
|
|
349
|
+
});
|