233 lines
9.4 KiB
PHP
233 lines
9.4 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
require dirname(__DIR__, 2) . '/vendor/autoload.php';
|
|
|
|
use app\common\service\pharmacy\EjMedicineBootstrapService;
|
|
|
|
$hostname = trim((string) (getenv('ZYT_BOOTSTRAP_TEST_DB_HOST') ?: ''));
|
|
$port = (int) (getenv('ZYT_BOOTSTRAP_TEST_DB_PORT') ?: 3306);
|
|
$username = (string) (getenv('ZYT_BOOTSTRAP_TEST_DB_USER') ?: '');
|
|
$configuredDatabase = trim((string) (getenv('ZYT_BOOTSTRAP_TEST_DB_DATABASE') ?: ''));
|
|
$passwordOverride = getenv('ZYT_BOOTSTRAP_TEST_DB_PASSWORD');
|
|
$password = $passwordOverride === false ? '' : $passwordOverride;
|
|
if ($hostname === '' || $username === '') {
|
|
fwrite(STDOUT, "medicine bootstrap MySQL integration skipped: database credentials unavailable\n");
|
|
exit(0);
|
|
}
|
|
|
|
$databaseName = 'zyt_bootstrap_test_' . bin2hex(random_bytes(8));
|
|
$admin = null;
|
|
$pdo = null;
|
|
$temporaryTables = false;
|
|
$stage = 'connect';
|
|
try {
|
|
$admin = new PDO(
|
|
sprintf('mysql:host=%s;port=%d;charset=utf8mb4', $hostname, $port),
|
|
$username,
|
|
$password,
|
|
[
|
|
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
|
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
|
|
PDO::ATTR_EMULATE_PREPARES => false,
|
|
]
|
|
);
|
|
$stage = 'create_database';
|
|
try {
|
|
$admin->exec("CREATE DATABASE `{$databaseName}` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci");
|
|
} catch (PDOException $exception) {
|
|
if ($configuredDatabase === '' || !in_array((string) $exception->getCode(), ['42000', '1044'], true)) {
|
|
throw $exception;
|
|
}
|
|
$temporaryTables = true;
|
|
$databaseName = $configuredDatabase;
|
|
}
|
|
$stage = 'test';
|
|
$pdo = new PDO(
|
|
sprintf('mysql:host=%s;port=%d;dbname=%s;charset=utf8mb4', $hostname, $port, $databaseName),
|
|
$username,
|
|
$password,
|
|
[
|
|
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
|
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
|
|
PDO::ATTR_EMULATE_PREPARES => false,
|
|
]
|
|
);
|
|
$tableKind = $temporaryTables ? 'CREATE TEMPORARY TABLE' : 'CREATE TABLE';
|
|
$pdo->exec("{$tableKind} projection_catalog (
|
|
medicine_code varchar(32) NOT NULL PRIMARY KEY,
|
|
local_medicine_id bigint unsigned NOT NULL,
|
|
name varchar(120) NOT NULL
|
|
) ENGINE=InnoDB");
|
|
$pdo->exec("{$tableKind} projection_mapping (
|
|
local_medicine_id bigint unsigned NOT NULL PRIMARY KEY,
|
|
medicine_code varchar(32) NOT NULL UNIQUE,
|
|
operator_id bigint unsigned NOT NULL,
|
|
operator_name varchar(80) NOT NULL
|
|
) ENGINE=InnoDB");
|
|
foreach (['submissions', 'callbacks', 'business_links'] as $table) {
|
|
$pdo->exec("{$tableKind} `{$table}` (id bigint unsigned NOT NULL PRIMARY KEY) ENGINE=InnoDB");
|
|
}
|
|
$pdo->exec("INSERT INTO projection_catalog VALUES ('OLD001', 999, '旧投影')");
|
|
$pdo->exec("INSERT INTO projection_mapping VALUES (999, 'OLD001', 8, 'old-operator')");
|
|
|
|
$transaction = static function (callable $operation) use ($pdo): array {
|
|
$pdo->beginTransaction();
|
|
try {
|
|
$result = $operation();
|
|
$pdo->commit();
|
|
return $result;
|
|
} catch (Throwable $exception) {
|
|
if ($pdo->inTransaction()) {
|
|
$pdo->rollBack();
|
|
}
|
|
throw $exception;
|
|
}
|
|
};
|
|
$referenceCounter = static function () use ($pdo): array {
|
|
return [
|
|
'submissions' => (int) $pdo->query('SELECT COUNT(*) FROM submissions')->fetchColumn(),
|
|
'callbacks' => (int) $pdo->query('SELECT COUNT(*) FROM callbacks')->fetchColumn(),
|
|
'business_links' => (int) $pdo->query('SELECT COUNT(*) FROM business_links')->fetchColumn(),
|
|
];
|
|
};
|
|
$referenceLocker = static function () use ($pdo): void {
|
|
foreach (['submissions', 'callbacks', 'business_links'] as $table) {
|
|
$pdo->query("SELECT id FROM `{$table}` ORDER BY id FOR UPDATE")->fetchAll();
|
|
}
|
|
};
|
|
$locker = static function () use ($pdo): void {
|
|
$pdo->query('SELECT medicine_code FROM projection_catalog ORDER BY medicine_code FOR UPDATE')->fetchAll();
|
|
$pdo->query('SELECT local_medicine_id FROM projection_mapping ORDER BY local_medicine_id FOR UPDATE')->fetchAll();
|
|
};
|
|
$verifier = static function () use ($pdo): array {
|
|
return [
|
|
'catalog' => (int) $pdo->query('SELECT COUNT(*) FROM projection_catalog')->fetchColumn(),
|
|
'active_mappings' => (int) $pdo->query('SELECT COUNT(*) FROM projection_mapping')->fetchColumn(),
|
|
'unmapped' => (int) $pdo->query(
|
|
'SELECT COUNT(*) FROM projection_catalog c LEFT JOIN projection_mapping m '
|
|
. 'ON m.medicine_code = c.medicine_code WHERE m.local_medicine_id IS NULL'
|
|
)->fetchColumn(),
|
|
];
|
|
};
|
|
$rows = [
|
|
['local_medicine_id' => 1, 'medicine_code' => 'EJ000001', 'name' => '黄芪'],
|
|
['local_medicine_id' => 2, 'medicine_code' => 'EJ000002', 'name' => '党参'],
|
|
];
|
|
$replacer = static function (array $nextRows) use ($pdo): void {
|
|
$pdo->exec('DELETE FROM projection_mapping');
|
|
$pdo->exec('DELETE FROM projection_catalog');
|
|
$catalog = $pdo->prepare(
|
|
'INSERT INTO projection_catalog (medicine_code,local_medicine_id,name) VALUES (?,?,?)'
|
|
);
|
|
$mapping = $pdo->prepare(
|
|
'INSERT INTO projection_mapping (local_medicine_id,medicine_code,operator_id,operator_name) VALUES (?,?,0,?)'
|
|
);
|
|
foreach ($nextRows as $row) {
|
|
$catalog->execute([$row['medicine_code'], $row['local_medicine_id'], $row['name']]);
|
|
$mapping->execute([$row['local_medicine_id'], $row['medicine_code'], 'system-bootstrap']);
|
|
}
|
|
};
|
|
|
|
$pdo->exec('INSERT INTO submissions VALUES (1)');
|
|
try {
|
|
EjMedicineBootstrapService::replaceProjectionWith(
|
|
$rows,
|
|
$transaction,
|
|
$referenceLocker,
|
|
$referenceCounter,
|
|
$locker,
|
|
$replacer,
|
|
$verifier
|
|
);
|
|
throw new RuntimeException('nonzero MySQL reference gate unexpectedly allowed replacement');
|
|
} catch (RuntimeException $exception) {
|
|
if (!str_contains($exception->getMessage(), '业务引用')) {
|
|
throw $exception;
|
|
}
|
|
}
|
|
$pdo->exec('DELETE FROM submissions');
|
|
if ((string) $pdo->query('SELECT medicine_code FROM projection_catalog')->fetchColumn() !== 'OLD001') {
|
|
throw new RuntimeException('nonzero MySQL reference gate mutated the old projection');
|
|
}
|
|
|
|
$result = EjMedicineBootstrapService::replaceProjectionWith(
|
|
$rows,
|
|
$transaction,
|
|
$referenceLocker,
|
|
$referenceCounter,
|
|
$locker,
|
|
$replacer,
|
|
$verifier
|
|
);
|
|
if ($result !== ['catalog' => 2, 'active_mappings' => 2, 'unmapped' => 0]) {
|
|
throw new RuntimeException('successful MySQL projection replacement did not verify exactly');
|
|
}
|
|
$operators = $pdo->query(
|
|
'SELECT CONCAT(operator_id, ":", operator_name) FROM projection_mapping ORDER BY local_medicine_id'
|
|
)->fetchAll(PDO::FETCH_COLUMN);
|
|
if ($operators !== ['0:system-bootstrap', '0:system-bootstrap']) {
|
|
throw new RuntimeException('bootstrap mappings did not preserve the system operator identity');
|
|
}
|
|
|
|
$pdo->exec('DELETE FROM projection_mapping');
|
|
$pdo->exec('DELETE FROM projection_catalog');
|
|
$pdo->exec("INSERT INTO projection_catalog VALUES ('OLD002', 998, '回滚旧投影')");
|
|
$pdo->exec("INSERT INTO projection_mapping VALUES (998, 'OLD002', 7, 'rollback-operator')");
|
|
try {
|
|
EjMedicineBootstrapService::replaceProjectionWith(
|
|
$rows,
|
|
$transaction,
|
|
$referenceLocker,
|
|
$referenceCounter,
|
|
$locker,
|
|
static function (array $nextRows) use ($replacer): void {
|
|
$replacer($nextRows);
|
|
throw new RuntimeException('forced MySQL replacement failure');
|
|
},
|
|
$verifier
|
|
);
|
|
throw new RuntimeException('forced MySQL replacement failure unexpectedly committed');
|
|
} catch (RuntimeException $exception) {
|
|
if ($exception->getMessage() !== 'forced MySQL replacement failure') {
|
|
throw $exception;
|
|
}
|
|
}
|
|
$rolledBack = $pdo->query(
|
|
'SELECT c.medicine_code,c.local_medicine_id,c.name,m.operator_id,m.operator_name '
|
|
. 'FROM projection_catalog c JOIN projection_mapping m USING (medicine_code)'
|
|
)->fetch();
|
|
if ($rolledBack !== [
|
|
'medicine_code' => 'OLD002',
|
|
'local_medicine_id' => 998,
|
|
'name' => '回滚旧投影',
|
|
'operator_id' => 7,
|
|
'operator_name' => 'rollback-operator',
|
|
]) {
|
|
throw new RuntimeException('MySQL rollback did not restore the old projection exactly');
|
|
}
|
|
|
|
fwrite(STDOUT, $temporaryTables
|
|
? "medicine bootstrap MySQL integration passed: temporary_tables\n"
|
|
: "medicine bootstrap MySQL integration passed: temporary_database\n");
|
|
} catch (PDOException $exception) {
|
|
if ($stage === 'test') {
|
|
throw $exception;
|
|
}
|
|
fwrite(STDOUT, sprintf(
|
|
"medicine bootstrap MySQL integration skipped: temporary database unavailable stage=%s sqlstate=%s\n",
|
|
$stage,
|
|
(string) $exception->getCode()
|
|
));
|
|
exit(0);
|
|
} finally {
|
|
$pdo = null;
|
|
if ($admin instanceof PDO && !$temporaryTables) {
|
|
try {
|
|
$admin->exec("DROP DATABASE IF EXISTS `{$databaseName}`");
|
|
} catch (Throwable) {
|
|
}
|
|
}
|
|
}
|