mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-21 00:38:14 +08:00
175 lines
5.2 KiB
JavaScript
175 lines
5.2 KiB
JavaScript
const assert = require('node:assert/strict');
|
|
const { test } = require('node:test');
|
|
const {
|
|
PostgresMigrationLeaderUnavailableError,
|
|
PostgresMigrationStreamStore,
|
|
} = require('../dist');
|
|
const { runMigrationStream } = require('@qinglong/runtime-core');
|
|
|
|
const CHECKSUM = 'a'.repeat(64);
|
|
|
|
function createPool(lockResults = [true, true]) {
|
|
const records = new Map();
|
|
const calls = [];
|
|
let released = 0;
|
|
let lockIndex = 0;
|
|
|
|
function selectRecord(values, source) {
|
|
const record = source.get(values[0]);
|
|
return { rows: record ? [{ ...record }] : [] };
|
|
}
|
|
|
|
return {
|
|
calls,
|
|
records,
|
|
get released() {
|
|
return released;
|
|
},
|
|
pool: {
|
|
async query(text, values = []) {
|
|
calls.push({ scope: 'pool', text, values: [...values] });
|
|
if (text.startsWith('SELECT\n stream_id')) {
|
|
if (values.length === 0) {
|
|
return {
|
|
rows: [...records.values()].map((record) => ({ ...record })),
|
|
};
|
|
}
|
|
return selectRecord(values, records);
|
|
}
|
|
throw new Error(`unexpected pool query: ${text}`);
|
|
},
|
|
async connect() {
|
|
let staged = null;
|
|
return {
|
|
async query(text, values = []) {
|
|
calls.push({ scope: 'client', text, values: [...values] });
|
|
if (text === 'BEGIN') {
|
|
staged = new Map(
|
|
[...records].map(([id, record]) => [id, { ...record }]),
|
|
);
|
|
return { rows: [] };
|
|
}
|
|
if (text === 'COMMIT') {
|
|
records.clear();
|
|
for (const [id, record] of staged) records.set(id, record);
|
|
staged = null;
|
|
return { rows: [] };
|
|
}
|
|
if (text === 'ROLLBACK') {
|
|
staged = null;
|
|
return { rows: [] };
|
|
}
|
|
if (text.startsWith('SELECT set_config')) return { rows: [] };
|
|
if (text.startsWith('SELECT pg_try_advisory_xact_lock')) {
|
|
return { rows: [{ acquired: lockResults[lockIndex++] ?? true }] };
|
|
}
|
|
if (text.startsWith('CREATE SCHEMA')) return { rows: [] };
|
|
if (text.startsWith('CREATE TABLE')) return { rows: [] };
|
|
if (text.startsWith('SELECT\n stream_id')) {
|
|
return selectRecord(values, staged);
|
|
}
|
|
if (text.startsWith('INSERT INTO')) {
|
|
const [migrationId, streamId, dialect, checksum, appliedAtMs] =
|
|
values;
|
|
staged.set(migrationId, {
|
|
migrationId,
|
|
streamId,
|
|
dialect,
|
|
checksum,
|
|
appliedAtMs: String(appliedAtMs),
|
|
});
|
|
return { rows: [], rowCount: 1 };
|
|
}
|
|
if (text === 'CREATE TABLE ql3.run_probe(id integer)') {
|
|
return { rows: [] };
|
|
}
|
|
throw new Error(`unexpected client query: ${text}`);
|
|
},
|
|
release() {
|
|
released += 1;
|
|
},
|
|
};
|
|
},
|
|
},
|
|
};
|
|
}
|
|
|
|
function stream(up) {
|
|
return {
|
|
id: 'postgresql-main',
|
|
dialect: 'postgresql',
|
|
migrationIdScheme: 'postgres-prefixed',
|
|
checksumScheme: 'sha256',
|
|
migrations: [
|
|
{
|
|
id: 'pg-0001-schema-history',
|
|
checksum: CHECKSUM,
|
|
up,
|
|
},
|
|
],
|
|
};
|
|
}
|
|
|
|
test('serializes PostgreSQL history bootstrap and migration in advisory-lock transactions', async () => {
|
|
const state = createPool();
|
|
let upCalls = 0;
|
|
await runMigrationStream({
|
|
stream: stream(async (context) => {
|
|
upCalls += 1;
|
|
await context.query('CREATE TABLE ql3.run_probe(id integer)');
|
|
}),
|
|
store: new PostgresMigrationStreamStore(state.pool),
|
|
clock: () => 123,
|
|
});
|
|
|
|
assert.equal(upCalls, 1);
|
|
assert.equal(state.released, 2);
|
|
assert.deepEqual(state.records.get('pg-0001-schema-history'), {
|
|
migrationId: 'pg-0001-schema-history',
|
|
streamId: 'postgresql-main',
|
|
dialect: 'postgresql',
|
|
checksum: CHECKSUM,
|
|
appliedAtMs: '123',
|
|
});
|
|
assert.equal(
|
|
state.calls.filter(({ text }) =>
|
|
text.startsWith('SELECT pg_try_advisory_xact_lock'),
|
|
).length,
|
|
2,
|
|
);
|
|
assert.equal(state.calls.filter(({ text }) => text === 'COMMIT').length, 2);
|
|
});
|
|
|
|
test('fails closed before migration work when another migration leader owns the lock', async () => {
|
|
const state = createPool([true, false]);
|
|
let upCalls = 0;
|
|
await assert.rejects(
|
|
runMigrationStream({
|
|
stream: stream(async () => {
|
|
upCalls += 1;
|
|
}),
|
|
store: new PostgresMigrationStreamStore(state.pool),
|
|
}),
|
|
PostgresMigrationLeaderUnavailableError,
|
|
);
|
|
assert.equal(upCalls, 0);
|
|
assert.equal(state.records.size, 0);
|
|
assert.equal(state.calls.filter(({ text }) => text === 'ROLLBACK').length, 1);
|
|
assert.equal(state.released, 2);
|
|
});
|
|
|
|
test('rolls PostgreSQL migration work and history back together', async () => {
|
|
const state = createPool();
|
|
await assert.rejects(
|
|
runMigrationStream({
|
|
stream: stream(async () => {
|
|
throw new Error('migration work failed');
|
|
}),
|
|
store: new PostgresMigrationStreamStore(state.pool),
|
|
}),
|
|
/migration work failed/,
|
|
);
|
|
assert.equal(state.records.size, 0);
|
|
assert.equal(state.calls.filter(({ text }) => text === 'ROLLBACK').length, 1);
|
|
});
|