-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(migrations): add Clickhouse db migration handling
- Loading branch information
Showing
9 changed files
with
339 additions
and
116 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,2 +1,3 @@ | ||
export * from './types'; | ||
export * from './stream-copy'; | ||
export * from './stream-copy'; | ||
export * from './migrations'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,151 @@ | ||
import fs from 'fs'; | ||
import { createHash } from 'crypto'; | ||
import { ClickHouseClient, createClient } from "@clickhouse/client"; | ||
import { sql_queries, sql_sets } from './sql-queries'; | ||
import { NodeClickHouseClientConfigOptions } from '@clickhouse/client/dist/client'; | ||
|
||
export function createDatabase(clickhouse: ClickHouseClient, database: string, { engine = 'Atomic' }: { engine?: string }) { | ||
return clickhouse.exec({ | ||
query: `CREATE DATABASE IF NOT EXISTS ${database} ENGINE = ${engine}`, | ||
clickhouse_settings: { | ||
wait_end_of_query: 1, | ||
}, | ||
}); | ||
} | ||
|
||
export async function initializeMigrationTable(clickhouse: ClickHouseClient) { | ||
const q = `CREATE TABLE IF NOT EXISTS _migrations ( | ||
uid UUID DEFAULT generateUUIDv4(), | ||
version UInt32, | ||
checksum String, | ||
migration_name String, | ||
applied_at DateTime DEFAULT now() | ||
) | ||
ENGINE = MergeTree | ||
ORDER BY tuple(applied_at)`; | ||
|
||
return clickhouse.exec({ | ||
query: q, | ||
clickhouse_settings: { | ||
wait_end_of_query: 1, | ||
}, | ||
}); | ||
} | ||
|
||
interface Migration { | ||
version: number; | ||
filename: string; | ||
commands: string; | ||
} | ||
|
||
interface CompletedMigration { | ||
version: number; | ||
checksum: string; | ||
migration_name: string; | ||
} | ||
|
||
export async function getMigrationsToApply(clickhouse: ClickHouseClient, migrations: Migration[]) { | ||
const alreadyAppliedMigrations = await clickhouse.query({ | ||
query: `SELECT version, checksum, migration_name FROM _migrations ORDER BY version`, | ||
format: 'JSONEachRow', | ||
}) | ||
.then((rz) => rz.json<CompletedMigration[]>()) | ||
.then((rows) => rows.reduce((acc, row) => { | ||
acc[row.version] = row; | ||
return acc; | ||
}, {} as Record<number, CompletedMigration>)); | ||
|
||
Object.values(alreadyAppliedMigrations).forEach((migration) => { | ||
if (!migrations.find((m) => m.version === migration.version)) { | ||
throw new Error(`Migration ${migration.version} has been applied but no longer exists`); | ||
} | ||
}); | ||
|
||
const appliedMigrations = [] as Migration[]; | ||
|
||
for (const migration of migrations) { | ||
const checksum = createHash('md5').update(migration.commands).digest('hex'); | ||
|
||
if (alreadyAppliedMigrations[migration.version]) { | ||
// Check if migration file was not changed after apply. | ||
if (alreadyAppliedMigrations[migration.version].checksum !== checksum) { | ||
throw new Error(`A migration file should't be changed after apply. Please, restore content of the ${alreadyAppliedMigrations[migration.version].migration_name | ||
} migrations.`) | ||
} | ||
|
||
// Skip if a migration is already applied. | ||
continue; | ||
} | ||
appliedMigrations.push(migration); | ||
} | ||
|
||
return appliedMigrations; | ||
} | ||
|
||
export async function applyMigrations(clickhouse: ClickHouseClient, migrations: Migration[]) { | ||
for (const migration of migrations) { | ||
const checksum = createHash('md5').update(migration.commands).digest('hex'); | ||
|
||
// Extract sql from the migration. | ||
const queries = sql_queries(migration.commands); | ||
const sets = sql_sets(migration.commands); | ||
|
||
for (const query of queries) { | ||
try { | ||
await clickhouse.exec({ | ||
query: query, | ||
clickhouse_settings: sets, | ||
}); | ||
} catch (e) { | ||
throw new Error( | ||
`the migrations ${migration.filename} has an error. Please, fix it (be sure that already executed parts of the migration would not be run second time) and re-run migration script. | ||
${(e as Error).message}`); | ||
} | ||
} | ||
|
||
try { | ||
await clickhouse.insert({ | ||
table: '_migrations', | ||
values: [{ version: migration.version, checksum: checksum, migration_name: migration.filename }], | ||
format: 'JSONEachRow', | ||
}); | ||
} catch (e: unknown) { | ||
throw new Error(`can't insert a data into the table _migrations: ${(e as Error).message}`); | ||
} | ||
} | ||
} | ||
|
||
export function getMigrationsInDirectory(directory: string): Migration[] { | ||
const migrations = [] as Migration[]; | ||
|
||
fs.readdirSync(directory).forEach((filename) => { | ||
// Manage only .sql files. | ||
if (!filename.endsWith('.sql')) return; | ||
|
||
const version = Number(filename.split('_')[0]); | ||
const commands = fs.readFileSync(`${directory}/${filename}`, 'utf8'); | ||
|
||
migrations.push({ | ||
version, | ||
filename, | ||
commands, | ||
}); | ||
}); | ||
|
||
return migrations.sort((a, b) => a.version - b.version); | ||
} | ||
|
||
export async function applyMigrationsInDirectory(config: NodeClickHouseClientConfigOptions & { database: string }, directory: string) { | ||
const defaultDb = createClient({ | ||
...config, | ||
database: undefined, | ||
}); | ||
await createDatabase(defaultDb, config.database, { engine: 'Atomic' }); | ||
const targetDb = createClient(config); | ||
await initializeMigrationTable(targetDb); | ||
const migrations = getMigrationsInDirectory(directory); | ||
const toApply = await getMigrationsToApply(targetDb, migrations); | ||
if (toApply.length > 0) { | ||
return applyMigrations(targetDb, migrations); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,53 @@ | ||
// Extract sql queries from migrations. | ||
const sql_queries = (content: string): string[] => { | ||
const queries = content | ||
.replace(/(--|#!|#\s).*(\n|\r\n|\r|$)/gm, '\n') | ||
.replace(/^\s*(SET\s).*(\n|\r\n|\r|$)/gm, '') | ||
.replace(/(\n|\r\n|\r)/gm, ' ') | ||
.replace(/\s+/g, ' ') | ||
.split(';') | ||
.map((el: string) => el.trim()) | ||
.filter((el: string) => el.length != 0); | ||
|
||
return queries; | ||
}; | ||
|
||
// Extract query settings from migrations. | ||
const sql_sets = (content: string) => { | ||
const sets: { [key: string]: string } = {}; | ||
|
||
const sets_arr = content | ||
.replace(/(--|#!|#\s).*(\n|\r\n|\r|$)/gm, '\n') | ||
.replace(/^\s*(?!SET\s).*(\n|\r\n|\r|$)/gm, '') | ||
.replace(/^\s*(SET\s)/gm, '') | ||
.replace(/(\n|\r\n|\r)/gm, ' ') | ||
.replace(/\s+/g, '') | ||
.split(';'); | ||
|
||
sets_arr.forEach((set_full) => { | ||
const set = set_full.split('='); | ||
if (set[0]) { | ||
sets[set[0]] = set[1]; | ||
} | ||
}); | ||
|
||
return sets; | ||
}; | ||
|
||
export { sql_queries, sql_sets }; | ||
|
||
// -- any | ||
// SET allow_experimental_object_type = 1; --set option | ||
// SET allow_experimental_object_new = 1; | ||
// SELECT * FROM events | ||
|
||
// sdfsfdsd | ||
// -- asssss | ||
// --asdf | ||
// sdfsdf | ||
// sdfsfs | ||
// SET a=1 | ||
// asdf | ||
// SET f=22 | ||
|
||
// sdf |
Oops, something went wrong.