feat(M01-B): 增加迁移执行器与旧库只读兼容层
This commit is contained in:
@@ -0,0 +1,66 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import { LegacyReadRepository } from '../dist/db/legacy-read-repository.js';
|
||||
|
||||
const calls = [];
|
||||
const fakePool = {
|
||||
async query(sql, parameters) {
|
||||
calls.push({ sql, parameters });
|
||||
return [[{
|
||||
legacyId: '7',
|
||||
tenantId: '2',
|
||||
parentId: '3',
|
||||
name: 'Room A',
|
||||
code: 'A01',
|
||||
status: 'OPEN',
|
||||
startAt: null,
|
||||
endAt: null
|
||||
}], []];
|
||||
}
|
||||
};
|
||||
|
||||
const repository = new LegacyReadRepository(fakePool);
|
||||
const rooms = await repository.listRooms({ tenantId: 2, parentId: 3, limit: 25 });
|
||||
|
||||
assert.equal(rooms.length, 1);
|
||||
assert.equal(calls.length, 1);
|
||||
assert.match(calls[0].sql, /FROM `member_room_info`/);
|
||||
assert.match(calls[0].sql, /`tenant_id` = \?/);
|
||||
assert.match(calls[0].sql, /`store_id` = \?/);
|
||||
assert.match(calls[0].sql, /ORDER BY `id` ASC LIMIT \?/);
|
||||
assert.deepEqual(calls[0].parameters, [2, 3, 25]);
|
||||
assert.doesNotMatch(calls[0].sql, /\b(?:INSERT|UPDATE|DELETE|REPLACE)\b/i);
|
||||
|
||||
await assert.rejects(
|
||||
() => repository.listStores({ tenantId: 2, limit: 501 }),
|
||||
/between 1 and 500/
|
||||
);
|
||||
|
||||
const unsafeRepository = new LegacyReadRepository(fakePool, {
|
||||
stores: {
|
||||
table: 'member_store_info; DROP TABLE users',
|
||||
idColumn: 'id',
|
||||
tenantColumn: 'tenant_id'
|
||||
},
|
||||
rooms: {
|
||||
table: 'member_room_info',
|
||||
idColumn: 'id',
|
||||
tenantColumn: 'tenant_id'
|
||||
},
|
||||
orders: {
|
||||
table: 'member_order_info',
|
||||
idColumn: 'id',
|
||||
tenantColumn: 'tenant_id'
|
||||
},
|
||||
devices: {
|
||||
table: 'member_device_info',
|
||||
idColumn: 'id',
|
||||
tenantColumn: 'tenant_id'
|
||||
}
|
||||
});
|
||||
|
||||
await assert.rejects(
|
||||
() => unsafeRepository.listStores({ tenantId: 2 }),
|
||||
/Unsafe legacy SQL identifier/
|
||||
);
|
||||
|
||||
console.log('PASS: legacy read-only repository contracts are present.');
|
||||
@@ -0,0 +1,55 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import {
|
||||
executeMigrationPlan,
|
||||
loadMigrationPlan,
|
||||
splitSqlStatements
|
||||
} from '../dist/db/migration-runner.js';
|
||||
|
||||
assert.deepEqual(
|
||||
splitSqlStatements("SELECT 'a;b'; -- comment\nSELECT `semi;colon`;"),
|
||||
["SELECT 'a;b'", 'SELECT `semi;colon`']
|
||||
);
|
||||
|
||||
const plan = await loadMigrationPlan('up');
|
||||
assert.equal(plan.direction, 'up');
|
||||
assert.match(plan.file, /2026061601_m01b_core_schema\.up\.sql$/);
|
||||
assert.match(plan.checksum, /^[a-f0-9]{64}$/);
|
||||
assert.ok(plan.statements.length >= 11);
|
||||
|
||||
const calls = [];
|
||||
const fakePool = {
|
||||
async query(sql) {
|
||||
calls.push(sql);
|
||||
return [{ affectedRows: 1 }, []];
|
||||
}
|
||||
};
|
||||
|
||||
const dryRun = await executeMigrationPlan(fakePool, plan, true);
|
||||
assert.equal(dryRun.executed, false);
|
||||
assert.equal(calls.length, 0);
|
||||
|
||||
const liveResult = await executeMigrationPlan(fakePool, {
|
||||
direction: 'up',
|
||||
file: 'test.sql',
|
||||
checksum: 'test',
|
||||
statements: ['SELECT 1', 'SELECT 2']
|
||||
});
|
||||
assert.equal(liveResult.executed, true);
|
||||
assert.equal(liveResult.affectedRows, 2);
|
||||
assert.deepEqual(calls, ['SELECT 1', 'SELECT 2']);
|
||||
|
||||
await assert.rejects(
|
||||
() => executeMigrationPlan({
|
||||
async query() {
|
||||
return [[], []];
|
||||
}
|
||||
}, {
|
||||
direction: 'verify',
|
||||
file: 'verify.sql',
|
||||
checksum: 'test',
|
||||
statements: ['SELECT table_name']
|
||||
}),
|
||||
/returned fewer than 10 rows/
|
||||
);
|
||||
|
||||
console.log('PASS: migration runner plan, parser and execution contracts are present.');
|
||||
Reference in New Issue
Block a user