mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-20 16:07:11 +08:00
feat(ql3): establish 3.0 incubation baseline
This commit is contained in:
@@ -0,0 +1,178 @@
|
||||
require('ts-node/register/transpile-only');
|
||||
|
||||
const assert = require('node:assert/strict');
|
||||
const { test } = require('node:test');
|
||||
const {
|
||||
PostgresMigrationLeaderUnavailableError,
|
||||
PostgresMigrationStreamStore,
|
||||
} = require('../../back/migrations/adapters/postgresMigrationStreamStore');
|
||||
const {
|
||||
runMigrationStream,
|
||||
} = require('../../back/migrations/core/migrationStream');
|
||||
|
||||
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);
|
||||
});
|
||||
Reference in New Issue
Block a user