-
Notifications
You must be signed in to change notification settings - Fork 2
/
migrations.ts
159 lines (143 loc) · 5.15 KB
/
migrations.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
/**
* Currently assumes that we will use Prisma migrate's directory structure when it comes to migrations.
*
* Prisma persists migration history in a table called `_prisma_migrations`:
* https://github.com/prisma/prisma-engines/blob/main/migration-engine/ARCHITECTURE.md#the-_prisma_migrations-table
*
* Docs on migration history: https://www.prisma.io/docs/concepts/components/prisma-migrate#migration-history
*/
import crypto from 'crypto'
import fs from 'fs'
import path from 'path'
import { v4 as uuidv4 } from 'uuid'
import { Database, SqliteError } from 'better-sqlite3'
export type Migration = {
id: string
checksum: string
finished_at?: string
migration_name: string
logs?: string
rolled_back_at?: string
started_at?: string
applied_steps_count: number
}
export function migrate(db: Database, migrationsDirPath: string) {
// Determine whether the initial setup has been completed before
const migrationsTableExists =
db
.prepare(
"SELECT EXISTS (SELECT 1 FROM sqlite_master WHERE type IS 'table' AND name IS '_prisma_migrations')"
)
.pluck()
.get() !== 0
if (!migrationsTableExists) {
// Taken from `prisma-engines` with a slight alteration to how we store dates (we want more granular):
// https://github.com/prisma/prisma-engines/blob/863b4a98c22936a01efd27ab814b452f6a62cd73/migration-engine/connectors/sql-migration-connector/src/flavour/sqlite.rs#L61
db.prepare(
`CREATE TABLE "_prisma_migrations" (
"id" TEXT PRIMARY KEY NOT NULL,
"checksum" TEXT NOT NULL,
"finished_at" DATETIME,
"migration_name" TEXT NOT NULL,
"logs" TEXT,
"rolled_back_at" DATETIME,
"started_at" DATETIME NOT NULL DEFAULT (strftime('%Y-%m-%d %H:%M:%f', current_timestamp)),
"applied_steps_count" INTEGER UNSIGNED NOT NULL DEFAULT 0
);`
).run()
}
const mostRecentMigrationName: string | undefined = db
// Note that `finished_at` must have millisecond granularity in order for the ORDER BY to work as expected
// Otherwise, it won't work in the case where multiple migrations are applied in one go
.prepare(
`SELECT migration_name as name FROM '_prisma_migrations'
WHERE finished_at IS NOT NULL AND rolled_back_at IS NULL
ORDER BY finished_at DESC LIMIT 1;`
)
.get()?.name
const migrationsToApply = getUnappliedMigrations(
migrationsDirPath,
mostRecentMigrationName
)
migrationsToApply.forEach((migration) => {
const migrationFile = fs.readFileSync(migration.path, 'utf8')
const migrationId = uuidv4()
db.prepare<{
id: string
checksum: string
migration_name: string
}>(
"INSERT INTO '_prisma_migrations' (id, checksum, migration_name) VALUES (:id, :checksum, :migration_name);"
).run({
id: migrationId,
checksum: crypto
.createHash('sha256')
.update(migrationFile.toString())
.digest('hex'),
migration_name: migration.name,
})
const executeMigration = db.transaction(() => {
db.exec(migrationFile)
})
try {
executeMigration()
// Reference for SQLite date functions: https://www.sqlite.org/lang_datefunc.html
db.prepare<{
id: string
finished_at: number
}>(
`UPDATE '_prisma_migrations'
SET
finished_at = strftime('%Y-%m-%d %H:%M:%f', :finished_at / 1000, 'unixepoch'),
applied_steps_count = 1
WHERE id = :id;`
).run({
id: migrationId,
finished_at: Date.now().valueOf(),
})
} catch (err) {
if (err instanceof SqliteError) {
db.prepare<{
id: string
logs: string
rolled_back_at: number
}>(
`UPDATE '_prisma_migrations'
SET
logs = :logs,
rolled_back_at = strftime('%Y-%m-%d %H:%M:%f', :rolled_back_at / 1000, 'unixepoch')
WHERE id = :id;`
).run({
id: migrationId,
logs: err.message,
rolled_back_at: Date.now().valueOf(),
})
}
throw err
}
})
}
function getUnappliedMigrations(
migrationsDirPath: string,
mostRecentMigrationName?: string
): { path: string; name: string }[] {
const prismaMigrateDirents = fs.readdirSync(migrationsDirPath, {
withFileTypes: true,
})
// Migration directories are already sorted due to using a sequential formatted date as the prefix for the name
// https://github.com/prisma/prisma-engines/blob/6d0d1f6ebabd0497065a8d8e13be1d4dbc2d7c05/migration-engine/connectors/migration-connector/src/migrations_directory.rs#L26
const sortedMigrationDirectories = prismaMigrateDirents.filter((dirent) =>
dirent.isDirectory()
)
const currentMigrationIndex = mostRecentMigrationName
? sortedMigrationDirectories.findIndex(
(dir) => dir.name === mostRecentMigrationName
)
: -1
return sortedMigrationDirectories
.slice(currentMigrationIndex + 1, sortedMigrationDirectories.length)
.map((dirent) => ({
name: dirent.name,
path: path.resolve(migrationsDirPath, dirent.name, 'migration.sql'),
}))
}