feat(M09-D1): 完成商品目录与库存流水底座
This commit is contained in:
@@ -45,7 +45,8 @@ assert.match(plan.file, /2026081002_m08d_content_asset_scope\.up\.sql/);
|
||||
assert.match(plan.file, /2026081003_m08d_franchise_leads\.up\.sql/);
|
||||
assert.match(plan.file, /2026081004_m08d_admin_password_auth\.up\.sql/);
|
||||
assert.match(plan.file, /2026081005_m09b_cleaning_rules\.up\.sql/);
|
||||
assert.match(plan.file, /2026081006_m09c_cleaning_settlement_integrity\.up\.sql$/);
|
||||
assert.match(plan.file, /2026081006_m09c_cleaning_settlement_integrity\.up\.sql/);
|
||||
assert.match(plan.file, /2026081107_m09d1_product_inventory_foundation\.up\.sql$/);
|
||||
assert.match(plan.checksum, /^[a-f0-9]{64}$/);
|
||||
assert.ok(plan.statements.length >= 11);
|
||||
|
||||
@@ -55,10 +56,19 @@ assert.match(verifyPlan.statements[91], /^SELECT index_name/);
|
||||
assert.match(verifyPlan.file, /2026081003_m08d_franchise_leads\.verify\.sql/);
|
||||
assert.match(verifyPlan.file, /2026081004_m08d_admin_password_auth\.verify\.sql/);
|
||||
assert.match(verifyPlan.file, /2026081005_m09b_cleaning_rules\.verify\.sql/);
|
||||
assert.match(verifyPlan.file, /2026081006_m09c_cleaning_settlement_integrity\.verify\.sql$/);
|
||||
assert.match(verifyPlan.file, /2026081006_m09c_cleaning_settlement_integrity\.verify\.sql/);
|
||||
assert.match(
|
||||
verifyPlan.file,
|
||||
/2026081107_m09d1_product_inventory_foundation\.verify\.sql$/
|
||||
);
|
||||
|
||||
const downPlan = await loadMigrationPlan('down');
|
||||
assert.match(downPlan.file, /^database\/migrations\/2026081107_m09d1_product_inventory_foundation\.down\.sql/);
|
||||
assert.match(downPlan.file, /2026081006_m09c_cleaning_settlement_integrity\.down\.sql/);
|
||||
assert.ok(
|
||||
downPlan.file.indexOf('2026081107_m09d1_product_inventory_foundation.down.sql')
|
||||
< downPlan.file.indexOf('2026081006_m09c_cleaning_settlement_integrity.down.sql')
|
||||
);
|
||||
assert.ok(
|
||||
downPlan.file.indexOf('2026081006_m09c_cleaning_settlement_integrity.down.sql')
|
||||
< downPlan.file.indexOf('2026081005_m09b_cleaning_rules.down.sql')
|
||||
@@ -76,6 +86,51 @@ const dryRun = await executeMigrationPlan(fakePool, plan, true);
|
||||
assert.equal(dryRun.executed, false);
|
||||
assert.equal(calls.length, 0);
|
||||
|
||||
const lockCalls = [];
|
||||
let connectionReleased = false;
|
||||
const lockedResult = await executeMigrationPlan({
|
||||
async query() {
|
||||
throw new Error('pool.query must not run while a dedicated migration connection exists');
|
||||
},
|
||||
async getConnection() {
|
||||
return {
|
||||
async query(sql) {
|
||||
lockCalls.push(sql);
|
||||
if (sql.includes('GET_LOCK')) return [[{ acquired: 1 }], []];
|
||||
if (sql.includes('RELEASE_LOCK')) return [[{ released: 1 }], []];
|
||||
return [{ affectedRows: 2 }, []];
|
||||
},
|
||||
release() {
|
||||
connectionReleased = true;
|
||||
}
|
||||
};
|
||||
}
|
||||
}, {
|
||||
direction: 'down', file: 'locked.sql', checksum: 'locked', statements: ['SELECT 1']
|
||||
});
|
||||
assert.equal(lockedResult.affectedRows, 2);
|
||||
assert.equal(connectionReleased, true);
|
||||
assert.match(lockCalls[0], /GET_LOCK/);
|
||||
assert.equal(lockCalls[1], 'SELECT 1');
|
||||
assert.match(lockCalls[2], /RELEASE_LOCK/);
|
||||
|
||||
let timedOutConnectionReleased = false;
|
||||
await assert.rejects(
|
||||
() => executeMigrationPlan({
|
||||
async query() {},
|
||||
async getConnection() {
|
||||
return {
|
||||
async query() { return [[{ acquired: 0 }], []]; },
|
||||
release() { timedOutConnectionReleased = true; }
|
||||
};
|
||||
}
|
||||
}, {
|
||||
direction: 'down', file: 'lock-timeout.sql', checksum: 'lock-timeout', statements: []
|
||||
}),
|
||||
/MIGRATION_LOCK_TIMEOUT/
|
||||
);
|
||||
assert.equal(timedOutConnectionReleased, true);
|
||||
|
||||
const liveResult = await executeMigrationPlan(fakePool, {
|
||||
direction: 'up',
|
||||
file: 'test.sql',
|
||||
@@ -86,6 +141,144 @@ assert.equal(liveResult.executed, true);
|
||||
assert.equal(liveResult.affectedRows, 2);
|
||||
assert.deepEqual(calls, ['SELECT 1', 'SELECT 2']);
|
||||
|
||||
const triggerPlan = {
|
||||
direction: 'up',
|
||||
file: 'trigger.sql',
|
||||
checksum: 'trigger',
|
||||
statements: ['CREATE TRIGGER test_trigger BEFORE UPDATE ON test FOR EACH ROW SET @x = 1']
|
||||
};
|
||||
await assert.rejects(
|
||||
() => executeMigrationPlan({
|
||||
async query() { return [[{ logBin: 1, trustFunctionCreators: 0 }], []]; }
|
||||
}, triggerPlan),
|
||||
/MIGRATION_TRIGGER_PRIVILEGE_REQUIRED/
|
||||
);
|
||||
const triggerCalls = [];
|
||||
await executeMigrationPlan({
|
||||
async query(sql) {
|
||||
triggerCalls.push(sql);
|
||||
if (sql.includes('@@GLOBAL.log_bin')) {
|
||||
return [[{ logBin: 1, trustFunctionCreators: 1 }], []];
|
||||
}
|
||||
return [{ affectedRows: 0 }, []];
|
||||
}
|
||||
}, triggerPlan);
|
||||
assert.equal(triggerCalls.length, 2);
|
||||
|
||||
let indexAttempted = false;
|
||||
const retryResult = await executeMigrationPlan({
|
||||
async query(sql) {
|
||||
if (sql.includes('ADD UNIQUE KEY uq_qipai_stores_tenant_id')) {
|
||||
indexAttempted = true;
|
||||
throw Object.assign(new Error('duplicate index'), { code: 'ER_DUP_KEYNAME' });
|
||||
}
|
||||
if (sql.includes('information_schema.statistics')) {
|
||||
return [[{ nonUnique: 0, columns: 'tenant_id,id' }], []];
|
||||
}
|
||||
return [{ affectedRows: 1 }, []];
|
||||
}
|
||||
}, {
|
||||
direction: 'up', file: 'retry.sql', checksum: 'retry',
|
||||
statements: [
|
||||
'ALTER TABLE qipai_stores ADD UNIQUE KEY uq_qipai_stores_tenant_id (tenant_id, id)',
|
||||
'SELECT 1'
|
||||
]
|
||||
});
|
||||
assert.equal(indexAttempted, true);
|
||||
assert.equal(retryResult.affectedRows, 1);
|
||||
await assert.rejects(
|
||||
() => executeMigrationPlan({
|
||||
async query(sql) {
|
||||
if (sql.includes('ADD UNIQUE KEY')) {
|
||||
throw Object.assign(new Error('wrong duplicate index'), { code: 'ER_DUP_KEYNAME' });
|
||||
}
|
||||
return [[{ nonUnique: 0, columns: 'id,tenant_id' }], []];
|
||||
}
|
||||
}, {
|
||||
direction: 'up', file: 'retry-mismatch.sql', checksum: 'retry-mismatch',
|
||||
statements: [
|
||||
'ALTER TABLE qipai_stores ADD UNIQUE KEY uq_qipai_stores_tenant_id (tenant_id, id)'
|
||||
]
|
||||
}),
|
||||
/wrong duplicate index/
|
||||
);
|
||||
|
||||
const downRetryCalls = [];
|
||||
const downRetryResult = await executeMigrationPlan({
|
||||
async query(sql) {
|
||||
downRetryCalls.push(sql);
|
||||
if (/DROP INDEX uq_qipai_(?:stores|users)_tenant_id/.test(sql)
|
||||
|| /DROP COLUMN checksum/.test(sql)) {
|
||||
throw Object.assign(new Error('already removed'), { code: 'ER_CANT_DROP_FIELD_OR_KEY' });
|
||||
}
|
||||
if (sql.includes('information_schema.statistics')
|
||||
|| sql.includes('information_schema.columns')) return [[], []];
|
||||
return [{ affectedRows: 1 }, []];
|
||||
}
|
||||
}, {
|
||||
direction: 'down', file: 'retry-down.sql', checksum: 'retry-down',
|
||||
statements: [
|
||||
'ALTER TABLE qipai_stores DROP INDEX uq_qipai_stores_tenant_id',
|
||||
'ALTER TABLE qipai_users DROP INDEX uq_qipai_users_tenant_id',
|
||||
'ALTER TABLE qipai_schema_migrations DROP COLUMN checksum',
|
||||
'SELECT 1'
|
||||
]
|
||||
});
|
||||
assert.equal(downRetryResult.affectedRows, 1);
|
||||
assert.equal(downRetryCalls.filter((sql) => sql.includes('information_schema.')).length, 3);
|
||||
await assert.rejects(
|
||||
() => executeMigrationPlan({
|
||||
async query(sql) {
|
||||
if (sql.startsWith('ALTER TABLE')) {
|
||||
throw Object.assign(new Error('drop target still exists'), {
|
||||
code: 'ER_CANT_DROP_FIELD_OR_KEY'
|
||||
});
|
||||
}
|
||||
return [[{ present: 1 }], []];
|
||||
}
|
||||
}, {
|
||||
direction: 'down', file: 'retry-down-mismatch.sql', checksum: 'retry-down-mismatch',
|
||||
statements: ['ALTER TABLE qipai_stores DROP INDEX uq_qipai_stores_tenant_id']
|
||||
}),
|
||||
/drop target still exists/
|
||||
);
|
||||
|
||||
const versionedCalls = [];
|
||||
const versionedPlan = {
|
||||
direction: 'up', file: 'old.sql,new.sql', checksum: 'versioned',
|
||||
statements: ['SELECT old', 'SELECT new'],
|
||||
migrations: [
|
||||
{ version: '1', name: 'old', file: 'old.sql', checksum: 'old-checksum', statements: ['SELECT old'] },
|
||||
{ version: '2', name: 'new', file: 'new.sql', checksum: 'new-checksum', statements: ['SELECT new'] }
|
||||
]
|
||||
};
|
||||
await executeMigrationPlan({
|
||||
async query(sql, params = []) {
|
||||
versionedCalls.push({ sql, params });
|
||||
if (sql.startsWith('SELECT * FROM qipai_schema_migrations')) {
|
||||
return [[{ version: '1', name: 'old', checksum: 'old-checksum' }], []];
|
||||
}
|
||||
if (sql.startsWith('SELECT version, name')) {
|
||||
return [[{ version: '2', name: 'new' }], []];
|
||||
}
|
||||
if (sql.startsWith('UPDATE qipai_schema_migrations')) {
|
||||
return [{ affectedRows: 1 }, []];
|
||||
}
|
||||
return [{ affectedRows: 1 }, []];
|
||||
}
|
||||
}, versionedPlan);
|
||||
assert.equal(versionedCalls.some(({ sql }) => sql === 'SELECT old'), false);
|
||||
assert.equal(versionedCalls.some(({ sql }) => sql === 'SELECT new'), true);
|
||||
|
||||
await assert.rejects(
|
||||
() => executeMigrationPlan({
|
||||
async query() {
|
||||
return [[{ version: '1', name: 'old', checksum: 'changed' }], []];
|
||||
}
|
||||
}, versionedPlan),
|
||||
/MIGRATION_CHECKSUM_MISMATCH/
|
||||
);
|
||||
|
||||
await assert.rejects(
|
||||
() => executeMigrationPlan({
|
||||
async query() {
|
||||
|
||||
Reference in New Issue
Block a user