1483 lines
68 KiB
PHP
1483 lines
68 KiB
PHP
<?php
|
|
declare(strict_types=1);
|
|
|
|
namespace Modules\MiningChecker\Infrastructure;
|
|
|
|
use App\SqlDataImporter;
|
|
use App\UploadedSqlFile;
|
|
use Modules\MiningChecker\Support\ApiException;
|
|
use PDO;
|
|
|
|
final class SchemaManager
|
|
{
|
|
private PDO $pdo;
|
|
private string $prefix;
|
|
private string $moduleBasePath;
|
|
private string $driver;
|
|
private SqlDataImporter $sqlImporter;
|
|
|
|
public function __construct(PDO $pdo, string $prefix, string $moduleBasePath)
|
|
{
|
|
$this->pdo = $pdo;
|
|
$this->prefix = $prefix;
|
|
$this->moduleBasePath = rtrim($moduleBasePath, '/');
|
|
$this->driver = strtolower((string) $this->pdo->getAttribute(PDO::ATTR_DRIVER_NAME));
|
|
$this->sqlImporter = new SqlDataImporter($this->pdo);
|
|
}
|
|
|
|
public function ensureSchema(): void
|
|
{
|
|
$status = $this->schemaStatus();
|
|
if ($status['all_present']) {
|
|
return;
|
|
}
|
|
|
|
if ($status['present_count'] === 0) {
|
|
$this->importSchema();
|
|
return;
|
|
}
|
|
|
|
throw new ApiException(
|
|
'Mining-Checker Tabellen sind nur teilweise vorhanden. Bitte Migration manuell abschliessen.',
|
|
500,
|
|
[
|
|
'missing_tables' => $status['missing_tables'],
|
|
'present_tables' => $status['present_tables'],
|
|
]
|
|
);
|
|
}
|
|
|
|
public function initializeSchema(bool $dropExisting = false): array
|
|
{
|
|
$before = $this->schemaStatus();
|
|
$droppedTables = [];
|
|
|
|
if ($dropExisting && $before['present_count'] > 0) {
|
|
$droppedTables = $this->dropExistingTables();
|
|
}
|
|
|
|
$statusBeforeImport = $this->schemaStatus();
|
|
$imported = false;
|
|
|
|
if (!$statusBeforeImport['all_present']) {
|
|
if ($statusBeforeImport['present_count'] > 0 && !$dropExisting) {
|
|
throw new ApiException(
|
|
'Mining-Checker Tabellen sind nur teilweise vorhanden. Fuer eine Neuinitialisierung bitte Reset aktivieren.',
|
|
409,
|
|
[
|
|
'missing_tables' => $statusBeforeImport['missing_tables'],
|
|
'present_tables' => $statusBeforeImport['present_tables'],
|
|
]
|
|
);
|
|
}
|
|
|
|
$this->importSchema();
|
|
$imported = true;
|
|
}
|
|
|
|
$after = $this->schemaStatus();
|
|
|
|
return [
|
|
'dropped_existing' => $dropExisting,
|
|
'dropped_tables' => $droppedTables,
|
|
'schema_imported' => $imported,
|
|
'before' => $before,
|
|
'after' => $after,
|
|
'message' => $after['all_present']
|
|
? ($imported
|
|
? 'Mining-Checker Schema wurde erfolgreich angelegt.'
|
|
: 'Mining-Checker Tabellen waren bereits vollstaendig vorhanden.')
|
|
: 'Mining-Checker Schema ist weiterhin unvollstaendig.',
|
|
];
|
|
}
|
|
|
|
public function rebuildSchemaDirect(): array
|
|
{
|
|
$dropped = [];
|
|
foreach ($this->knownTablesInDropOrder() as $table) {
|
|
try {
|
|
if ($this->driver === 'pgsql') {
|
|
$this->pdo->exec('DROP TABLE IF EXISTS ' . $table . ' CASCADE');
|
|
} else {
|
|
$safeTable = str_replace('`', '``', $table);
|
|
$this->pdo->exec('DROP TABLE IF EXISTS `' . $safeTable . '`');
|
|
}
|
|
$dropped[] = $table;
|
|
} catch (\Throwable $exception) {
|
|
throw new ApiException(
|
|
'Mining-Checker Tabellen konnten nicht direkt geloescht werden.',
|
|
500,
|
|
['message' => $exception->getMessage(), 'table' => $table]
|
|
);
|
|
}
|
|
}
|
|
|
|
$this->importSchema();
|
|
|
|
return [
|
|
'dropped_tables' => $dropped,
|
|
'message' => 'Mining-Checker Tabellen wurden geloescht und das Schema neu aufgebaut.',
|
|
];
|
|
}
|
|
|
|
public function schemaStatus(): array
|
|
{
|
|
$requiredTables = $this->knownTablesInCreateOrder();
|
|
|
|
$presentTables = $this->existingTables($requiredTables);
|
|
$missingTables = array_values(array_diff($requiredTables, $presentTables));
|
|
$pendingUpgrades = [];
|
|
if ($missingTables === []) {
|
|
$pendingUpgrades = $this->detectPendingUpgrades($presentTables);
|
|
}
|
|
|
|
return [
|
|
'required_tables' => $requiredTables,
|
|
'present_tables' => $presentTables,
|
|
'missing_tables' => $missingTables,
|
|
'present_count' => count($presentTables),
|
|
'missing_count' => count($missingTables),
|
|
'pending_upgrades' => $pendingUpgrades,
|
|
'pending_upgrade_count' => count($pendingUpgrades),
|
|
'all_present' => $missingTables === [],
|
|
];
|
|
}
|
|
|
|
public function upgradeSchema(): array
|
|
{
|
|
$before = $this->lightweightStatus();
|
|
if ($before['present_count'] === 0) {
|
|
$this->importSchema();
|
|
$after = $this->lightweightStatus();
|
|
return [
|
|
'upgraded' => ['schema_initialized'],
|
|
'before' => $before,
|
|
'after' => $after,
|
|
'message' => 'Mining-Checker Schema war leer und wurde initial angelegt.',
|
|
];
|
|
}
|
|
|
|
if (!$before['core_present']) {
|
|
throw new ApiException(
|
|
'Schema-Upgrade ist nur moeglich, wenn alle Grundtabellen vorhanden sind. Bitte zuerst initialisieren oder resetten.',
|
|
409,
|
|
[
|
|
'missing_tables' => $before['missing_core_tables'],
|
|
'present_tables' => $before['present_tables'],
|
|
]
|
|
);
|
|
}
|
|
|
|
$applied = [];
|
|
|
|
if ($this->tableExists($this->prefix . 'cost_plans')) {
|
|
$requiredColumns = ['mining_speed_value', 'mining_speed_unit', 'bonus_speed_value', 'bonus_speed_unit', 'base_price_amount', 'payment_type'];
|
|
$existingColumns = $this->existingColumns($this->prefix . 'cost_plans', $requiredColumns);
|
|
if (count($existingColumns) !== count($requiredColumns)) {
|
|
$this->upgradeCostPlanColumns();
|
|
$applied[] = 'cost_plan_columns';
|
|
}
|
|
}
|
|
|
|
$settingsColumns = $this->existingColumns($this->prefix . 'settings', ['preferred_currencies', 'report_currency', 'crypto_currency', 'display_timezone', 'fx_max_age_hours', 'module_theme_mode', 'module_theme_accent']);
|
|
if (!in_array('preferred_currencies', $settingsColumns, true) || !in_array('report_currency', $settingsColumns, true) || !in_array('crypto_currency', $settingsColumns, true) || !in_array('display_timezone', $settingsColumns, true) || !in_array('fx_max_age_hours', $settingsColumns, true) || !in_array('module_theme_mode', $settingsColumns, true) || !in_array('module_theme_accent', $settingsColumns, true)) {
|
|
$this->upgradeSettingsPreferredCurrenciesColumn();
|
|
$applied[] = 'settings_preferences';
|
|
}
|
|
if ($this->tableExists($this->prefix . 'measurements') && !$this->columnExists($this->prefix . 'measurements', 'fx_fetch_id')) {
|
|
$this->ensureMeasurementFxReferenceColumn();
|
|
$applied[] = 'measurement_fx_reference';
|
|
}
|
|
if ($this->tableExists($this->prefix . 'measurements') && !$this->columnExists($this->prefix . 'measurements', 'coin_currency')) {
|
|
$this->ensureMeasurementCoinCurrencyColumn();
|
|
$applied[] = 'measurement_coin_currency';
|
|
}
|
|
|
|
if (!$this->tableExists($this->prefix . 'measurement_rates')) {
|
|
$this->ensureMeasurementRatesTable();
|
|
$applied[] = 'measurement_rates_table';
|
|
}
|
|
if (!$this->tableExists($this->prefix . 'payouts')) {
|
|
$this->ensurePayoutsTable();
|
|
$applied[] = 'payouts_table';
|
|
}
|
|
if (!$this->tableExists($this->prefix . 'wallet_snapshots')) {
|
|
$this->ensureWalletSnapshotsTable();
|
|
$applied[] = 'wallet_snapshots_table';
|
|
}
|
|
if (!$this->tableExists($this->prefix . 'fx_fetches') || !$this->tableExists($this->prefix . 'fx_rates')) {
|
|
$this->ensureFxRatesTable();
|
|
$applied[] = 'fx_rates_table';
|
|
}
|
|
if (!$this->tableExists($this->prefix . 'miner_offers')) {
|
|
$this->upgradeMinerOffersTable();
|
|
$applied[] = 'miner_offers_table';
|
|
}
|
|
if (!$this->tableExists($this->prefix . 'purchased_miners')) {
|
|
$this->upgradePurchasedMinersTable();
|
|
$applied[] = 'purchased_miners_table';
|
|
}
|
|
if ($this->tableExists($this->prefix . 'targets') && !$this->columnExists($this->prefix . 'targets', 'miner_offer_id')) {
|
|
$this->upgradeTargetOfferColumn();
|
|
$applied[] = 'target_offer_column';
|
|
}
|
|
if ($this->tableExists($this->prefix . 'miner_offers') && (
|
|
!$this->columnExists($this->prefix . 'miner_offers', 'base_price_amount') ||
|
|
!$this->columnExists($this->prefix . 'miner_offers', 'base_price_currency') ||
|
|
!$this->columnExists($this->prefix . 'miner_offers', 'payment_type') ||
|
|
!$this->columnExists($this->prefix . 'miner_offers', 'auto_renew')
|
|
)) {
|
|
$this->upgradeMinerOfferBasePriceColumns();
|
|
$applied[] = 'miner_offer_base_columns';
|
|
}
|
|
if ($this->tableExists($this->prefix . 'purchased_miners') && (
|
|
!$this->columnExists($this->prefix . 'purchased_miners', 'reference_price_amount') ||
|
|
!$this->columnExists($this->prefix . 'purchased_miners', 'reference_price_currency') ||
|
|
!$this->columnExists($this->prefix . 'purchased_miners', 'auto_renew')
|
|
)) {
|
|
$this->upgradePurchasedMinerReferenceColumns();
|
|
$applied[] = 'purchased_miner_reference_columns';
|
|
}
|
|
if ($this->tableExists($this->prefix . 'targets') && $this->tableExists($this->prefix . 'miner_offers')) {
|
|
$this->ensureTargetOfferForeignKey();
|
|
$applied[] = 'target_offer_foreign_key';
|
|
}
|
|
|
|
$after = $this->lightweightStatus();
|
|
$allApplied = array_values(array_unique($applied));
|
|
|
|
return [
|
|
'upgraded' => $allApplied,
|
|
'before' => $before,
|
|
'after' => $after,
|
|
'message' => $allApplied === []
|
|
? 'Schema ist bereits auf dem neuesten Stand.'
|
|
: 'Schema-Upgrade erfolgreich ausgefuehrt.',
|
|
];
|
|
}
|
|
|
|
public function upgradeSchemaDirect(): array
|
|
{
|
|
$coreTables = [
|
|
$this->prefix . 'projects',
|
|
$this->prefix . 'settings',
|
|
$this->prefix . 'cost_plans',
|
|
$this->prefix . 'measurements',
|
|
$this->prefix . 'targets',
|
|
$this->prefix . 'dashboard_definitions',
|
|
];
|
|
|
|
$presentCoreTables = $this->existingTables($coreTables);
|
|
if ($presentCoreTables === []) {
|
|
$this->importSchema();
|
|
return [
|
|
'upgraded' => ['schema_initialized'],
|
|
'message' => 'Mining-Checker Schema wurde neu angelegt.',
|
|
];
|
|
}
|
|
|
|
if (count($presentCoreTables) !== count($coreTables)) {
|
|
throw new ApiException(
|
|
'Grundtabellen sind nur teilweise vorhanden. Bitte Schema zuerst sauber initialisieren.',
|
|
409,
|
|
['missing_core_tables' => array_values(array_diff($coreTables, $presentCoreTables))]
|
|
);
|
|
}
|
|
|
|
$applied = [];
|
|
$requiredColumns = ['mining_speed_value', 'mining_speed_unit', 'bonus_speed_value', 'bonus_speed_unit', 'base_price_amount', 'payment_type'];
|
|
$existingColumns = $this->existingColumns($this->prefix . 'cost_plans', $requiredColumns);
|
|
if (count($existingColumns) !== count($requiredColumns)) {
|
|
$this->upgradeCostPlanColumns();
|
|
$applied[] = 'cost_plan_columns';
|
|
}
|
|
|
|
$settingsColumns = $this->existingColumns($this->prefix . 'settings', ['preferred_currencies', 'report_currency', 'crypto_currency', 'display_timezone', 'fx_max_age_hours', 'module_theme_mode', 'module_theme_accent']);
|
|
if (!in_array('preferred_currencies', $settingsColumns, true) || !in_array('report_currency', $settingsColumns, true) || !in_array('crypto_currency', $settingsColumns, true) || !in_array('display_timezone', $settingsColumns, true) || !in_array('fx_max_age_hours', $settingsColumns, true) || !in_array('module_theme_mode', $settingsColumns, true) || !in_array('module_theme_accent', $settingsColumns, true)) {
|
|
$this->upgradeSettingsPreferredCurrenciesColumn();
|
|
$applied[] = 'settings_preferences';
|
|
}
|
|
if ($this->tableExists($this->prefix . 'measurements') && !$this->columnExists($this->prefix . 'measurements', 'fx_fetch_id')) {
|
|
$this->ensureMeasurementFxReferenceColumn();
|
|
$applied[] = 'measurement_fx_reference';
|
|
}
|
|
if ($this->tableExists($this->prefix . 'measurements') && !$this->columnExists($this->prefix . 'measurements', 'coin_currency')) {
|
|
$this->ensureMeasurementCoinCurrencyColumn();
|
|
$applied[] = 'measurement_coin_currency';
|
|
}
|
|
|
|
if (!$this->tableExists($this->prefix . 'fx_fetches') || !$this->tableExists($this->prefix . 'fx_rates')) {
|
|
$this->ensureFxRatesTable();
|
|
$applied[] = 'fx_rates_table';
|
|
}
|
|
|
|
if (!$this->tableExists($this->prefix . 'measurement_rates')) {
|
|
$this->ensureMeasurementRatesTable();
|
|
$applied[] = 'measurement_rates_table';
|
|
}
|
|
|
|
if (!$this->tableExists($this->prefix . 'payouts')) {
|
|
$this->ensurePayoutsTable();
|
|
$applied[] = 'payouts_table';
|
|
}
|
|
if (!$this->tableExists($this->prefix . 'wallet_snapshots')) {
|
|
$this->ensureWalletSnapshotsTable();
|
|
$applied[] = 'wallet_snapshots_table';
|
|
}
|
|
|
|
if (!$this->tableExists($this->prefix . 'miner_offers')) {
|
|
$this->upgradeMinerOffersTable();
|
|
$applied[] = 'miner_offers_table';
|
|
}
|
|
|
|
if (!$this->tableExists($this->prefix . 'purchased_miners')) {
|
|
$this->upgradePurchasedMinersTable();
|
|
$applied[] = 'purchased_miners_table';
|
|
}
|
|
if ($this->tableExists($this->prefix . 'targets') && !$this->columnExists($this->prefix . 'targets', 'miner_offer_id')) {
|
|
$this->upgradeTargetOfferColumn();
|
|
$applied[] = 'target_offer_column';
|
|
}
|
|
if ($this->tableExists($this->prefix . 'miner_offers') && (
|
|
!$this->columnExists($this->prefix . 'miner_offers', 'base_price_amount') ||
|
|
!$this->columnExists($this->prefix . 'miner_offers', 'base_price_currency') ||
|
|
!$this->columnExists($this->prefix . 'miner_offers', 'payment_type') ||
|
|
!$this->columnExists($this->prefix . 'miner_offers', 'auto_renew')
|
|
)) {
|
|
$this->upgradeMinerOfferBasePriceColumns();
|
|
$applied[] = 'miner_offer_base_columns';
|
|
}
|
|
if ($this->tableExists($this->prefix . 'purchased_miners') && (
|
|
!$this->columnExists($this->prefix . 'purchased_miners', 'reference_price_amount') ||
|
|
!$this->columnExists($this->prefix . 'purchased_miners', 'reference_price_currency') ||
|
|
!$this->columnExists($this->prefix . 'purchased_miners', 'auto_renew')
|
|
)) {
|
|
$this->upgradePurchasedMinerReferenceColumns();
|
|
$applied[] = 'purchased_miner_reference_columns';
|
|
}
|
|
if ($this->tableExists($this->prefix . 'targets') && $this->tableExists($this->prefix . 'miner_offers')) {
|
|
$this->ensureTargetOfferForeignKey();
|
|
$applied[] = 'target_offer_foreign_key';
|
|
}
|
|
|
|
return [
|
|
'upgraded' => $applied,
|
|
'message' => $applied === []
|
|
? 'Schema ist bereits auf dem neuesten Stand.'
|
|
: 'Direktes Schema-Upgrade erfolgreich ausgefuehrt.',
|
|
];
|
|
}
|
|
|
|
public function importSqlFile(array $uploadedFile): array
|
|
{
|
|
try {
|
|
$file = UploadedSqlFile::read($uploadedFile);
|
|
} catch (\RuntimeException $exception) {
|
|
throw new ApiException($exception->getMessage(), 422);
|
|
}
|
|
|
|
$this->prepareSchemaForSqlImport();
|
|
|
|
$statementCount = $this->executeSqlContent((string) $file['sql'], (string) $file['file']);
|
|
|
|
return [
|
|
'file' => (string) $file['file'],
|
|
'statement_count' => $statementCount,
|
|
'message' => 'SQL-Datei wurde erfolgreich eingespielt.',
|
|
];
|
|
}
|
|
|
|
private function prepareSchemaForSqlImport(): void
|
|
{
|
|
$status = $this->schemaStatus();
|
|
if (!$status['all_present']) {
|
|
$this->initializeSchema(false);
|
|
}
|
|
|
|
$this->upgradeSchemaDirect();
|
|
$this->ensureLegacyImportCompatibility();
|
|
}
|
|
|
|
private function ensureLegacyImportCompatibility(): void
|
|
{
|
|
$this->ensureLegacyMinerOfferImportColumns();
|
|
}
|
|
|
|
private function ensureMeasurementFxReferenceColumn(): void
|
|
{
|
|
$table = $this->prefix . 'measurements';
|
|
if (!$this->tableExists($table)) {
|
|
return;
|
|
}
|
|
|
|
$statements = $this->driver === 'pgsql'
|
|
? [
|
|
'ALTER TABLE ' . $table . ' ADD COLUMN IF NOT EXISTS fx_fetch_id BIGINT',
|
|
'CREATE INDEX IF NOT EXISTS idx_miningcheck_measurements_fx_fetch ON ' . $table . ' (fx_fetch_id)',
|
|
]
|
|
: [
|
|
'ALTER TABLE `' . $table . '` ADD COLUMN fx_fetch_id BIGINT UNSIGNED NULL',
|
|
'ALTER TABLE `' . $table . '` ADD INDEX idx_miningcheck_measurements_fx_fetch (fx_fetch_id)',
|
|
];
|
|
|
|
foreach ($statements as $statement) {
|
|
try {
|
|
$this->executeUpgradeStatements([$statement], 'Messpunkt-FX-Referenz konnte nicht angelegt werden.');
|
|
} catch (\Throwable $exception) {
|
|
$message = strtolower($exception->getMessage());
|
|
if (
|
|
($this->driver === 'mysql' && (str_contains($message, 'duplicate column') || str_contains($message, 'duplicate key name'))) ||
|
|
($this->driver === 'pgsql' && str_contains($message, 'already exists'))
|
|
) {
|
|
continue;
|
|
}
|
|
|
|
throw $exception;
|
|
}
|
|
}
|
|
}
|
|
|
|
private function ensureMeasurementCoinCurrencyColumn(): void
|
|
{
|
|
$table = $this->prefix . 'measurements';
|
|
$settingsTable = $this->prefix . 'settings';
|
|
if (!$this->tableExists($table)) {
|
|
return;
|
|
}
|
|
|
|
$statements = $this->driver === 'pgsql'
|
|
? [
|
|
'ALTER TABLE ' . $table . ' ADD COLUMN IF NOT EXISTS coin_currency VARCHAR(10)',
|
|
'UPDATE ' . $table . ' AS m
|
|
SET coin_currency = COALESCE(NULLIF(BTRIM(s.crypto_currency), \'\'), \'DOGE\')
|
|
FROM ' . $settingsTable . ' AS s
|
|
WHERE s.project_key = m.project_key
|
|
AND (m.coin_currency IS NULL OR BTRIM(m.coin_currency) = \'\')',
|
|
'UPDATE ' . $table . ' SET coin_currency = \'DOGE\' WHERE coin_currency IS NULL OR BTRIM(coin_currency) = \'\'',
|
|
'ALTER TABLE ' . $table . ' ALTER COLUMN coin_currency SET DEFAULT \'DOGE\'',
|
|
'ALTER TABLE ' . $table . ' ALTER COLUMN coin_currency SET NOT NULL',
|
|
]
|
|
: [
|
|
'ALTER TABLE `' . $table . '` ADD COLUMN coin_currency VARCHAR(10) NOT NULL DEFAULT \'DOGE\' AFTER coins_total',
|
|
'UPDATE `' . $table . '` m
|
|
LEFT JOIN `' . $settingsTable . '` s ON s.project_key = m.project_key
|
|
SET m.coin_currency = COALESCE(NULLIF(TRIM(s.crypto_currency), \'\'), \'DOGE\')
|
|
WHERE m.coin_currency IS NULL OR TRIM(m.coin_currency) = \'\'',
|
|
];
|
|
|
|
foreach ($statements as $index => $statement) {
|
|
try {
|
|
if ($this->driver === 'mysql' && $index === 0 && $this->columnExists($table, 'coin_currency')) {
|
|
continue;
|
|
}
|
|
$this->executeUpgradeStatements([$statement], 'Messpunkt-Coin-Waehrung konnte nicht angelegt werden.');
|
|
} catch (\Throwable $exception) {
|
|
$message = strtolower($exception->getMessage());
|
|
if (
|
|
($this->driver === 'mysql' && $index === 0 && str_contains($message, 'duplicate column')) ||
|
|
($this->driver === 'pgsql' && str_contains($message, 'already exists'))
|
|
) {
|
|
continue;
|
|
}
|
|
|
|
throw $exception;
|
|
}
|
|
}
|
|
}
|
|
|
|
private function ensureLegacyMinerOfferImportColumns(): void
|
|
{
|
|
$table = $this->prefix . 'miner_offers';
|
|
if (!$this->tableExists($table)) {
|
|
return;
|
|
}
|
|
|
|
$statements = $this->driver === 'pgsql'
|
|
? [
|
|
'ALTER TABLE ' . $table . ' ADD COLUMN IF NOT EXISTS price_amount NUMERIC(20,8)',
|
|
'ALTER TABLE ' . $table . ' ADD COLUMN IF NOT EXISTS price_currency VARCHAR(10)',
|
|
'ALTER TABLE ' . $table . ' ADD COLUMN IF NOT EXISTS usd_reference_amount NUMERIC(20,8)',
|
|
'ALTER TABLE ' . $table . ' ADD COLUMN IF NOT EXISTS reference_price_amount NUMERIC(20,8)',
|
|
'ALTER TABLE ' . $table . ' ADD COLUMN IF NOT EXISTS reference_price_currency VARCHAR(10)',
|
|
]
|
|
: [
|
|
'ALTER TABLE `' . $table . '` ADD COLUMN price_amount DECIMAL(20,8) NULL',
|
|
'ALTER TABLE `' . $table . '` ADD COLUMN price_currency VARCHAR(10) NULL',
|
|
'ALTER TABLE `' . $table . '` ADD COLUMN usd_reference_amount DECIMAL(20,8) NULL',
|
|
'ALTER TABLE `' . $table . '` ADD COLUMN reference_price_amount DECIMAL(20,8) NULL',
|
|
'ALTER TABLE `' . $table . '` ADD COLUMN reference_price_currency VARCHAR(10) NULL',
|
|
];
|
|
|
|
foreach ($statements as $statement) {
|
|
try {
|
|
$this->executeUpgradeStatements([$statement], 'Import-Kompatibilitaet fuer Miner-Angebote fehlgeschlagen.');
|
|
} catch (\Throwable $exception) {
|
|
if ($this->driver === 'mysql' && str_contains(strtolower($exception->getMessage()), 'duplicate column')) {
|
|
continue;
|
|
}
|
|
|
|
throw $exception;
|
|
}
|
|
}
|
|
}
|
|
|
|
private function tableExists(string $table): bool
|
|
{
|
|
return in_array($table, $this->existingTables([$table]), true);
|
|
}
|
|
|
|
private function importSchema(): void
|
|
{
|
|
$schemaFile = $this->resolveSchemaFile();
|
|
if (!is_file($schemaFile)) {
|
|
throw new ApiException('Schema-Datei fuer Mining-Checker fehlt.', 500, ['schema_file' => $schemaFile]);
|
|
}
|
|
|
|
$sql = (string) file_get_contents($schemaFile);
|
|
$this->executeSqlContent($sql, $schemaFile, 'Schema-Import fuer Mining-Checker fehlgeschlagen.');
|
|
}
|
|
|
|
private function executeSqlContent(string $sql, string $sourceLabel, string $errorMessage = 'SQL-Import fuer Mining-Checker fehlgeschlagen.'): int
|
|
{
|
|
try {
|
|
return $this->sqlImporter->importString($sql);
|
|
} catch (\RuntimeException $exception) {
|
|
$previous = $exception->getPrevious() ?? $exception;
|
|
|
|
throw new ApiException(
|
|
$errorMessage,
|
|
500,
|
|
[
|
|
'message' => $previous->getMessage(),
|
|
'source' => $sourceLabel,
|
|
'statement' => substr($exception->getMessage(), 0, 1000),
|
|
]
|
|
);
|
|
} catch (\Throwable $exception) {
|
|
throw new ApiException(
|
|
$errorMessage,
|
|
500,
|
|
[
|
|
'message' => $exception->getMessage(),
|
|
'source' => $sourceLabel,
|
|
'statement' => null,
|
|
]
|
|
);
|
|
}
|
|
}
|
|
|
|
private function dropExistingTables(): array
|
|
{
|
|
$tables = array_reverse($this->schemaStatus()['present_tables']);
|
|
if ($tables === []) {
|
|
return [];
|
|
}
|
|
|
|
try {
|
|
if ($this->driver !== 'mysql') {
|
|
$this->pdo->beginTransaction();
|
|
} else {
|
|
$this->pdo->exec('SET FOREIGN_KEY_CHECKS = 0');
|
|
}
|
|
|
|
foreach ($tables as $table) {
|
|
if ($this->driver === 'pgsql') {
|
|
$this->pdo->exec('DROP TABLE IF EXISTS ' . $table . ' CASCADE');
|
|
} else {
|
|
$safeTable = str_replace('`', '``', $table);
|
|
$this->pdo->exec('DROP TABLE IF EXISTS `' . $safeTable . '`');
|
|
}
|
|
}
|
|
|
|
if ($this->driver === 'mysql') {
|
|
$this->pdo->exec('SET FOREIGN_KEY_CHECKS = 1');
|
|
} elseif ($this->pdo->inTransaction()) {
|
|
$this->pdo->commit();
|
|
}
|
|
} catch (\Throwable $exception) {
|
|
if ($this->pdo->inTransaction()) {
|
|
$this->pdo->rollBack();
|
|
}
|
|
|
|
try {
|
|
if ($this->driver === 'mysql') {
|
|
$this->pdo->exec('SET FOREIGN_KEY_CHECKS = 1');
|
|
}
|
|
} catch (\Throwable) {
|
|
}
|
|
|
|
throw new ApiException(
|
|
'Vorhandene Mining-Checker Tabellen konnten nicht geloescht werden.',
|
|
500,
|
|
['message' => $exception->getMessage()]
|
|
);
|
|
}
|
|
|
|
return $tables;
|
|
}
|
|
|
|
private function resolveSchemaFile(): string
|
|
{
|
|
$specificFile = $this->moduleBasePath . '/sql/schema.' . $this->driver . '.sql';
|
|
if (is_file($specificFile)) {
|
|
return $specificFile;
|
|
}
|
|
|
|
return $this->moduleBasePath . '/sql/schema.sql';
|
|
}
|
|
|
|
private function detectPendingUpgrades(array $presentTables): array
|
|
{
|
|
$upgrades = [];
|
|
|
|
if (in_array($this->prefix . 'cost_plans', $presentTables, true)) {
|
|
$requiredColumns = [
|
|
'mining_speed_value',
|
|
'mining_speed_unit',
|
|
'bonus_speed_value',
|
|
'bonus_speed_unit',
|
|
'base_price_amount',
|
|
'payment_type',
|
|
];
|
|
|
|
$existingColumns = $this->existingColumns($this->prefix . 'cost_plans', $requiredColumns);
|
|
foreach ($requiredColumns as $column) {
|
|
if (!in_array($column, $existingColumns, true)) {
|
|
$upgrades[] = 'cost_plan_columns';
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if ($this->tableExists($this->prefix . 'settings')) {
|
|
$requiredSettingsColumns = ['preferred_currencies', 'report_currency', 'crypto_currency', 'display_timezone', 'fx_max_age_hours', 'module_theme_mode', 'module_theme_accent'];
|
|
$existingSettingsColumns = $this->existingColumns($this->prefix . 'settings', $requiredSettingsColumns);
|
|
foreach ($requiredSettingsColumns as $column) {
|
|
if (!in_array($column, $existingSettingsColumns, true)) {
|
|
$upgrades[] = 'settings_preferences';
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if ($this->tableExists($this->prefix . 'measurements') && !$this->columnExists($this->prefix . 'measurements', 'fx_fetch_id')) {
|
|
$upgrades[] = 'measurement_fx_reference';
|
|
}
|
|
if ($this->tableExists($this->prefix . 'measurements') && !$this->columnExists($this->prefix . 'measurements', 'coin_currency')) {
|
|
$upgrades[] = 'measurement_coin_currency';
|
|
}
|
|
|
|
if (!$this->tableExists($this->prefix . 'fx_fetches') || !$this->tableExists($this->prefix . 'fx_rates')) {
|
|
$upgrades[] = 'fx_rates_table';
|
|
}
|
|
if (!$this->tableExists($this->prefix . 'measurement_rates')) {
|
|
$upgrades[] = 'measurement_rates_table';
|
|
}
|
|
if (!$this->tableExists($this->prefix . 'payouts')) {
|
|
$upgrades[] = 'payouts_table';
|
|
}
|
|
if (!$this->tableExists($this->prefix . 'wallet_snapshots')) {
|
|
$upgrades[] = 'wallet_snapshots_table';
|
|
}
|
|
if (!$this->tableExists($this->prefix . 'miner_offers')) {
|
|
$upgrades[] = 'miner_offers_table';
|
|
}
|
|
if (!$this->tableExists($this->prefix . 'purchased_miners')) {
|
|
$upgrades[] = 'purchased_miners_table';
|
|
}
|
|
if ($this->tableExists($this->prefix . 'targets') && !$this->columnExists($this->prefix . 'targets', 'miner_offer_id')) {
|
|
$upgrades[] = 'target_offer_column';
|
|
}
|
|
if (
|
|
$this->tableExists($this->prefix . 'targets') &&
|
|
$this->tableExists($this->prefix . 'miner_offers') &&
|
|
!$this->foreignKeyExists('fk_mining_targets_offer')
|
|
) {
|
|
$upgrades[] = 'target_offer_foreign_key';
|
|
}
|
|
|
|
return array_values(array_unique($upgrades));
|
|
}
|
|
|
|
private function columnExists(string $table, string $column): bool
|
|
{
|
|
return in_array($column, $this->existingColumns($table, [$column]), true);
|
|
}
|
|
|
|
private function existingTables(array $tableNames): array
|
|
{
|
|
$tableNames = array_values(array_unique(array_filter($tableNames)));
|
|
if ($tableNames === []) {
|
|
return [];
|
|
}
|
|
|
|
$placeholders = [];
|
|
$params = [];
|
|
foreach ($tableNames as $index => $tableName) {
|
|
$placeholder = ':table_' . $index;
|
|
$placeholders[] = $placeholder;
|
|
$params['table_' . $index] = $tableName;
|
|
}
|
|
|
|
$schemaCondition = $this->driver === 'pgsql'
|
|
? 'table_schema = current_schema()'
|
|
: 'table_schema = DATABASE()';
|
|
|
|
$sql = sprintf(
|
|
'SELECT table_name FROM information_schema.tables WHERE %s AND table_name IN (%s)',
|
|
$schemaCondition,
|
|
implode(', ', $placeholders)
|
|
);
|
|
|
|
$statement = $this->pdo->prepare($sql);
|
|
$statement->execute($params);
|
|
|
|
$rows = $statement->fetchAll(PDO::FETCH_COLUMN) ?: [];
|
|
return array_values(array_intersect($tableNames, array_map('strval', $rows)));
|
|
}
|
|
|
|
private function existingColumns(string $table, array $columnNames): array
|
|
{
|
|
$columnNames = array_values(array_unique(array_filter($columnNames)));
|
|
if ($columnNames === []) {
|
|
return [];
|
|
}
|
|
|
|
$placeholders = [];
|
|
$params = ['table_name' => $table];
|
|
foreach ($columnNames as $index => $columnName) {
|
|
$placeholder = ':column_' . $index;
|
|
$placeholders[] = $placeholder;
|
|
$params['column_' . $index] = $columnName;
|
|
}
|
|
|
|
$schemaCondition = $this->driver === 'pgsql'
|
|
? 'table_schema = current_schema()'
|
|
: 'table_schema = DATABASE()';
|
|
|
|
$sql = sprintf(
|
|
'SELECT column_name FROM information_schema.columns WHERE %s AND table_name = :table_name AND column_name IN (%s)',
|
|
$schemaCondition,
|
|
implode(', ', $placeholders)
|
|
);
|
|
|
|
$statement = $this->pdo->prepare($sql);
|
|
$statement->execute($params);
|
|
|
|
$rows = $statement->fetchAll(PDO::FETCH_COLUMN) ?: [];
|
|
return array_values(array_intersect($columnNames, array_map('strval', $rows)));
|
|
}
|
|
|
|
private function upgradeCostPlanColumns(): void
|
|
{
|
|
$table = $this->prefix . 'cost_plans';
|
|
$columns = $this->driver === 'pgsql'
|
|
? [
|
|
'mining_speed_value' => 'ALTER TABLE ' . $table . ' ADD COLUMN IF NOT EXISTS mining_speed_value NUMERIC(20,4)',
|
|
'mining_speed_unit' => 'ALTER TABLE ' . $table . ' ADD COLUMN IF NOT EXISTS mining_speed_unit VARCHAR(8)',
|
|
'bonus_speed_value' => 'ALTER TABLE ' . $table . ' ADD COLUMN IF NOT EXISTS bonus_speed_value NUMERIC(20,4)',
|
|
'bonus_speed_unit' => 'ALTER TABLE ' . $table . ' ADD COLUMN IF NOT EXISTS bonus_speed_unit VARCHAR(8)',
|
|
'base_price_amount' => 'ALTER TABLE ' . $table . ' ADD COLUMN IF NOT EXISTS base_price_amount NUMERIC(20,8)',
|
|
'payment_type' => 'ALTER TABLE ' . $table . ' ADD COLUMN IF NOT EXISTS payment_type VARCHAR(10) NOT NULL DEFAULT \'fiat\'',
|
|
]
|
|
: [
|
|
'mining_speed_value' => 'ALTER TABLE `' . $table . '` ADD COLUMN mining_speed_value DECIMAL(20,4) NULL',
|
|
'mining_speed_unit' => 'ALTER TABLE `' . $table . '` ADD COLUMN mining_speed_unit VARCHAR(8) NULL',
|
|
'bonus_speed_value' => 'ALTER TABLE `' . $table . '` ADD COLUMN bonus_speed_value DECIMAL(20,4) NULL',
|
|
'bonus_speed_unit' => 'ALTER TABLE `' . $table . '` ADD COLUMN bonus_speed_unit VARCHAR(8) NULL',
|
|
'base_price_amount' => 'ALTER TABLE `' . $table . '` ADD COLUMN base_price_amount DECIMAL(20,8) NULL',
|
|
'payment_type' => 'ALTER TABLE `' . $table . '` ADD COLUMN payment_type VARCHAR(10) NOT NULL DEFAULT \'fiat\'',
|
|
];
|
|
|
|
foreach ($columns as $targetColumn => $statement) {
|
|
try {
|
|
if ($this->driver === 'mysql') {
|
|
if ($this->columnExists($table, $targetColumn)) {
|
|
continue;
|
|
}
|
|
}
|
|
$this->pdo->exec($statement);
|
|
} catch (\Throwable $exception) {
|
|
throw new ApiException(
|
|
'Schema-Upgrade fuer Mining-Checker fehlgeschlagen.',
|
|
500,
|
|
['message' => $exception->getMessage(), 'statement' => $statement]
|
|
);
|
|
}
|
|
}
|
|
|
|
$backfillStatements = $this->driver === 'pgsql'
|
|
? [
|
|
'UPDATE ' . $table . ' SET base_price_amount = total_cost_amount WHERE base_price_amount IS NULL',
|
|
"UPDATE " . $table . " SET payment_type = CASE WHEN UPPER(currency) IN ('ADA','ARB','BNB','BTC','DAI','DOGE','DOT','ETH','LINK','LTC','SOL','USDC','USDT','XRP') THEN 'crypto' ELSE 'fiat' END WHERE payment_type IS NULL OR BTRIM(payment_type) = ''",
|
|
]
|
|
: [
|
|
'UPDATE `' . $table . '` SET base_price_amount = total_cost_amount WHERE base_price_amount IS NULL',
|
|
"UPDATE `" . $table . "` SET payment_type = CASE WHEN UPPER(currency) IN ('ADA','ARB','BNB','BTC','DAI','DOGE','DOT','ETH','LINK','LTC','SOL','USDC','USDT','XRP') THEN 'crypto' ELSE 'fiat' END WHERE payment_type IS NULL OR TRIM(payment_type) = ''",
|
|
];
|
|
$this->executeUpgradeStatements($backfillStatements, 'Backfill fuer Kostenplaene fehlgeschlagen.');
|
|
}
|
|
|
|
public function ensureFxRatesTable(): void
|
|
{
|
|
if ($this->tableExists($this->prefix . 'fx_fetches') && $this->tableExists($this->prefix . 'fx_rates')) {
|
|
return;
|
|
}
|
|
|
|
$this->upgradeFxRatesTable();
|
|
}
|
|
|
|
public function ensureExtendedTables(): void
|
|
{
|
|
$this->ensureFxRatesTable();
|
|
$this->ensureMeasurementRatesTable();
|
|
$this->ensurePayoutsTable();
|
|
$this->ensureMinerTables();
|
|
}
|
|
|
|
public function ensureMeasurementRatesTable(): void
|
|
{
|
|
if ($this->tableExists($this->prefix . 'measurement_rates')) {
|
|
return;
|
|
}
|
|
|
|
$table = $this->prefix . 'measurement_rates';
|
|
$measurementTable = $this->prefix . 'measurements';
|
|
$statements = $this->driver === 'pgsql'
|
|
? [
|
|
'CREATE TABLE IF NOT EXISTS ' . $table . ' (
|
|
id BIGSERIAL PRIMARY KEY,
|
|
measurement_id BIGINT NOT NULL,
|
|
project_key VARCHAR(64) NOT NULL,
|
|
base_currency VARCHAR(10) NOT NULL,
|
|
quote_currency VARCHAR(10) NOT NULL,
|
|
rate NUMERIC(20,10) NOT NULL,
|
|
provider VARCHAR(32) NOT NULL DEFAULT \'derived\',
|
|
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
CONSTRAINT fk_mining_measurement_rates_measurement FOREIGN KEY (measurement_id) REFERENCES ' . $measurementTable . '(id) ON DELETE CASCADE,
|
|
CONSTRAINT uq_mining_measurement_rate_pair UNIQUE (measurement_id, base_currency, quote_currency)
|
|
)',
|
|
'CREATE INDEX IF NOT EXISTS idx_miningcheck_measurement_rates_project_measurement ON ' . $table . ' (project_key, measurement_id)',
|
|
]
|
|
: [
|
|
'CREATE TABLE IF NOT EXISTS `' . $table . '` (
|
|
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
|
|
measurement_id BIGINT UNSIGNED NOT NULL,
|
|
project_key VARCHAR(64) NOT NULL,
|
|
base_currency VARCHAR(10) NOT NULL,
|
|
quote_currency VARCHAR(10) NOT NULL,
|
|
rate DECIMAL(20,10) NOT NULL,
|
|
provider VARCHAR(32) NOT NULL DEFAULT \'derived\',
|
|
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
CONSTRAINT fk_mining_measurement_rates_measurement FOREIGN KEY (measurement_id) REFERENCES `' . $measurementTable . '`(id) ON DELETE CASCADE,
|
|
CONSTRAINT uq_mining_measurement_rate_pair UNIQUE (measurement_id, base_currency, quote_currency),
|
|
KEY idx_miningcheck_measurement_rates_project_measurement (project_key, measurement_id)
|
|
)',
|
|
];
|
|
|
|
$this->executeUpgradeStatements($statements, 'Schema-Upgrade fuer Messwert-Waehrungen fehlgeschlagen.');
|
|
}
|
|
|
|
public function ensurePayoutsTable(): void
|
|
{
|
|
if ($this->tableExists($this->prefix . 'payouts')) {
|
|
return;
|
|
}
|
|
|
|
$table = $this->prefix . 'payouts';
|
|
$projectTable = $this->prefix . 'projects';
|
|
$statements = $this->driver === 'pgsql'
|
|
? [
|
|
'CREATE TABLE IF NOT EXISTS ' . $table . ' (
|
|
id BIGSERIAL PRIMARY KEY,
|
|
project_key VARCHAR(64) NOT NULL,
|
|
payout_at TIMESTAMP NOT NULL,
|
|
coins_amount NUMERIC(20,6) NOT NULL,
|
|
payout_currency VARCHAR(10) NOT NULL DEFAULT \'DOGE\',
|
|
note TEXT,
|
|
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
CONSTRAINT fk_mining_payouts_project FOREIGN KEY (project_key) REFERENCES ' . $projectTable . '(project_key) ON DELETE CASCADE
|
|
)',
|
|
'CREATE INDEX IF NOT EXISTS idx_miningcheck_payouts_project_payout_at ON ' . $table . ' (project_key, payout_at)',
|
|
]
|
|
: [
|
|
'CREATE TABLE IF NOT EXISTS `' . $table . '` (
|
|
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
|
|
project_key VARCHAR(64) NOT NULL,
|
|
payout_at TIMESTAMP NOT NULL,
|
|
coins_amount DECIMAL(20,6) NOT NULL,
|
|
payout_currency VARCHAR(10) NOT NULL DEFAULT \'DOGE\',
|
|
note TEXT,
|
|
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
|
CONSTRAINT fk_mining_payouts_project FOREIGN KEY (project_key) REFERENCES `' . $projectTable . '`(project_key) ON DELETE CASCADE,
|
|
KEY idx_miningcheck_payouts_project_payout_at (project_key, payout_at)
|
|
)',
|
|
];
|
|
|
|
$this->executeUpgradeStatements($statements, 'Schema-Upgrade fuer Auszahlungen fehlgeschlagen.');
|
|
}
|
|
|
|
public function ensureWalletSnapshotsTable(): void
|
|
{
|
|
if ($this->tableExists($this->prefix . 'wallet_snapshots')) {
|
|
return;
|
|
}
|
|
|
|
$table = $this->prefix . 'wallet_snapshots';
|
|
$projectTable = $this->prefix . 'projects';
|
|
$statements = $this->driver === 'pgsql'
|
|
? [
|
|
'CREATE TABLE IF NOT EXISTS ' . $table . ' (
|
|
id BIGSERIAL PRIMARY KEY,
|
|
project_key VARCHAR(64) NOT NULL,
|
|
owner_sub VARCHAR(128) NOT NULL,
|
|
measured_at TIMESTAMP NOT NULL,
|
|
total_value_amount NUMERIC(20,8),
|
|
total_value_currency VARCHAR(10),
|
|
wallet_balance NUMERIC(28,10),
|
|
wallet_currency VARCHAR(10) NOT NULL,
|
|
balances_json JSONB,
|
|
note TEXT,
|
|
source VARCHAR(16) NOT NULL,
|
|
image_path VARCHAR(255),
|
|
ocr_raw_text TEXT,
|
|
ocr_confidence NUMERIC(6,4),
|
|
ocr_flags JSONB,
|
|
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
CONSTRAINT fk_mining_wallet_snapshots_project FOREIGN KEY (project_key) REFERENCES ' . $projectTable . '(project_key) ON DELETE CASCADE
|
|
)',
|
|
'CREATE INDEX IF NOT EXISTS idx_miningcheck_wallet_snapshots_project_measured_at ON ' . $table . ' (project_key, owner_sub, measured_at)',
|
|
]
|
|
: [
|
|
'CREATE TABLE IF NOT EXISTS `' . $table . '` (
|
|
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
|
|
project_key VARCHAR(64) NOT NULL,
|
|
owner_sub VARCHAR(128) NOT NULL,
|
|
measured_at TIMESTAMP NOT NULL,
|
|
total_value_amount DECIMAL(20,8) NULL,
|
|
total_value_currency VARCHAR(10) NULL,
|
|
wallet_balance DECIMAL(28,10) NULL,
|
|
wallet_currency VARCHAR(10) NOT NULL,
|
|
balances_json JSON NULL,
|
|
note TEXT NULL,
|
|
source ENUM(\'manual\', \'image_ocr\', \'seed_import\') NOT NULL,
|
|
image_path VARCHAR(255) NULL,
|
|
ocr_raw_text MEDIUMTEXT NULL,
|
|
ocr_confidence DECIMAL(6,4) NULL,
|
|
ocr_flags JSON NULL,
|
|
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
|
CONSTRAINT fk_mining_wallet_snapshots_project FOREIGN KEY (project_key) REFERENCES `' . $projectTable . '`(project_key) ON DELETE CASCADE,
|
|
KEY idx_miningcheck_wallet_snapshots_project_measured_at (project_key, owner_sub, measured_at)
|
|
)',
|
|
];
|
|
|
|
$this->executeUpgradeStatements($statements, 'Schema-Upgrade fuer Wallet-Snapshots fehlgeschlagen.');
|
|
}
|
|
|
|
public function ensureMinerTables(): void
|
|
{
|
|
if (!$this->tableExists($this->prefix . 'miner_offers')) {
|
|
$this->upgradeMinerOffersTable();
|
|
}
|
|
|
|
if (!$this->tableExists($this->prefix . 'purchased_miners')) {
|
|
$this->upgradePurchasedMinersTable();
|
|
}
|
|
}
|
|
|
|
private function upgradeFxRatesTable(): void
|
|
{
|
|
$fetchTable = $this->prefix . 'fx_fetches';
|
|
$rateTable = $this->prefix . 'fx_rates';
|
|
$statements = $this->driver === 'pgsql'
|
|
? [
|
|
'CREATE TABLE IF NOT EXISTS ' . $fetchTable . ' (
|
|
id BIGSERIAL PRIMARY KEY,
|
|
provider VARCHAR(32) NOT NULL DEFAULT \'currencyapi\',
|
|
base_currency VARCHAR(10) NOT NULL,
|
|
rate_date DATE NOT NULL,
|
|
fetched_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
|
|
)',
|
|
'CREATE INDEX IF NOT EXISTS idx_miningcheck_fx_fetches_base_fetched ON ' . $fetchTable . ' (base_currency, fetched_at)',
|
|
'CREATE TABLE IF NOT EXISTS ' . $rateTable . ' (
|
|
id BIGSERIAL PRIMARY KEY,
|
|
fetch_id BIGINT NOT NULL,
|
|
currency_code VARCHAR(10) NOT NULL,
|
|
current_value NUMERIC(20,10) NOT NULL,
|
|
CONSTRAINT fk_mining_fx_rates_fetch FOREIGN KEY (fetch_id) REFERENCES ' . $fetchTable . '(id) ON DELETE CASCADE
|
|
)',
|
|
'CREATE INDEX IF NOT EXISTS idx_miningcheck_fx_rates_fetch ON ' . $rateTable . ' (fetch_id)',
|
|
'CREATE INDEX IF NOT EXISTS idx_miningcheck_fx_rates_currency ON ' . $rateTable . ' (currency_code)',
|
|
]
|
|
: [
|
|
'CREATE TABLE IF NOT EXISTS `' . $fetchTable . '` (
|
|
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
|
|
provider VARCHAR(32) NOT NULL DEFAULT \'currencyapi\',
|
|
base_currency VARCHAR(10) NOT NULL,
|
|
rate_date DATE NOT NULL,
|
|
fetched_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
KEY idx_miningcheck_fx_fetches_base_fetched (base_currency, fetched_at)
|
|
)',
|
|
'CREATE TABLE IF NOT EXISTS `' . $rateTable . '` (
|
|
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
|
|
fetch_id BIGINT UNSIGNED NOT NULL,
|
|
currency_code VARCHAR(10) NOT NULL,
|
|
current_value DECIMAL(20,10) NOT NULL,
|
|
KEY idx_miningcheck_fx_rates_fetch (fetch_id),
|
|
KEY idx_miningcheck_fx_rates_currency (currency_code),
|
|
CONSTRAINT fk_mining_fx_rates_fetch FOREIGN KEY (fetch_id) REFERENCES `' . $fetchTable . '`(id) ON DELETE CASCADE
|
|
)',
|
|
];
|
|
|
|
foreach ($statements as $statement) {
|
|
$this->executeUpgradeStatements([$statement], 'Schema-Upgrade fuer FX-Kurse fehlgeschlagen.');
|
|
}
|
|
}
|
|
|
|
private function upgradeSettingsPreferredCurrenciesColumn(): void
|
|
{
|
|
$table = $this->prefix . 'settings';
|
|
$statements = $this->driver === 'pgsql'
|
|
? [
|
|
'ALTER TABLE ' . $table . ' ADD COLUMN IF NOT EXISTS preferred_currencies JSONB',
|
|
'ALTER TABLE ' . $table . ' ADD COLUMN IF NOT EXISTS report_currency VARCHAR(10)',
|
|
'ALTER TABLE ' . $table . ' ADD COLUMN IF NOT EXISTS crypto_currency VARCHAR(10)',
|
|
'ALTER TABLE ' . $table . ' ADD COLUMN IF NOT EXISTS display_timezone VARCHAR(64)',
|
|
'ALTER TABLE ' . $table . ' ADD COLUMN IF NOT EXISTS fx_max_age_hours NUMERIC(10,2)',
|
|
'ALTER TABLE ' . $table . ' ADD COLUMN IF NOT EXISTS module_theme_mode VARCHAR(16)',
|
|
'ALTER TABLE ' . $table . ' ADD COLUMN IF NOT EXISTS module_theme_accent VARCHAR(16)',
|
|
]
|
|
: [
|
|
'ALTER TABLE `' . $table . '` ADD COLUMN preferred_currencies JSON NULL',
|
|
'ALTER TABLE `' . $table . '` ADD COLUMN report_currency VARCHAR(10) NULL',
|
|
'ALTER TABLE `' . $table . '` ADD COLUMN crypto_currency VARCHAR(10) NULL',
|
|
'ALTER TABLE `' . $table . '` ADD COLUMN display_timezone VARCHAR(64) NULL',
|
|
'ALTER TABLE `' . $table . '` ADD COLUMN fx_max_age_hours DECIMAL(10,2) NULL',
|
|
'ALTER TABLE `' . $table . '` ADD COLUMN module_theme_mode VARCHAR(16) NULL',
|
|
'ALTER TABLE `' . $table . '` ADD COLUMN module_theme_accent VARCHAR(16) NULL',
|
|
];
|
|
|
|
foreach ($statements as $statement) {
|
|
try {
|
|
$this->executeUpgradeStatements([$statement], 'Schema-Upgrade fuer Settings fehlgeschlagen.');
|
|
} catch (\Throwable $exception) {
|
|
if ($this->driver === 'mysql' && str_contains(strtolower($exception->getMessage()), 'duplicate column')) {
|
|
continue;
|
|
}
|
|
|
|
throw $exception;
|
|
}
|
|
}
|
|
}
|
|
|
|
private function upgradeMinerOffersTable(): void
|
|
{
|
|
$table = $this->prefix . 'miner_offers';
|
|
$projectTable = $this->prefix . 'projects';
|
|
$statements = $this->driver === 'pgsql'
|
|
? [
|
|
'CREATE TABLE IF NOT EXISTS ' . $table . ' (
|
|
id BIGSERIAL PRIMARY KEY,
|
|
project_key VARCHAR(64) NOT NULL,
|
|
label VARCHAR(120) NOT NULL,
|
|
runtime_months INTEGER,
|
|
mining_speed_value NUMERIC(20,4),
|
|
mining_speed_unit VARCHAR(8),
|
|
bonus_speed_value NUMERIC(20,4),
|
|
bonus_speed_unit VARCHAR(8),
|
|
base_price_amount NUMERIC(20,8) NOT NULL,
|
|
base_price_currency VARCHAR(10) NOT NULL,
|
|
payment_type VARCHAR(10) NOT NULL DEFAULT \'fiat\',
|
|
auto_renew BOOLEAN NOT NULL DEFAULT FALSE,
|
|
note TEXT,
|
|
is_active BOOLEAN NOT NULL DEFAULT TRUE,
|
|
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
CONSTRAINT fk_mining_miner_offers_project FOREIGN KEY (project_key) REFERENCES ' . $projectTable . '(project_key) ON DELETE CASCADE
|
|
)',
|
|
]
|
|
: [
|
|
'CREATE TABLE IF NOT EXISTS `' . $table . '` (
|
|
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
|
|
project_key VARCHAR(64) NOT NULL,
|
|
label VARCHAR(120) NOT NULL,
|
|
runtime_months INT NULL,
|
|
mining_speed_value DECIMAL(20,4) NULL,
|
|
mining_speed_unit VARCHAR(8) NULL,
|
|
bonus_speed_value DECIMAL(20,4) NULL,
|
|
bonus_speed_unit VARCHAR(8) NULL,
|
|
base_price_amount DECIMAL(20,8) NOT NULL,
|
|
base_price_currency VARCHAR(10) NOT NULL,
|
|
payment_type VARCHAR(10) NOT NULL DEFAULT \'fiat\',
|
|
auto_renew TINYINT(1) NOT NULL DEFAULT 0,
|
|
note TEXT,
|
|
is_active TINYINT(1) NOT NULL DEFAULT 1,
|
|
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
|
CONSTRAINT fk_mining_miner_offers_project FOREIGN KEY (project_key) REFERENCES `' . $projectTable . '`(project_key) ON DELETE CASCADE
|
|
)',
|
|
];
|
|
|
|
$this->executeUpgradeStatements($statements, 'Schema-Upgrade fuer Miner-Angebote fehlgeschlagen.');
|
|
}
|
|
|
|
private function upgradePurchasedMinersTable(): void
|
|
{
|
|
$table = $this->prefix . 'purchased_miners';
|
|
$projectTable = $this->prefix . 'projects';
|
|
$offerTable = $this->prefix . 'miner_offers';
|
|
$statements = $this->driver === 'pgsql'
|
|
? [
|
|
'CREATE TABLE IF NOT EXISTS ' . $table . ' (
|
|
id BIGSERIAL PRIMARY KEY,
|
|
project_key VARCHAR(64) NOT NULL,
|
|
miner_offer_id BIGINT,
|
|
purchased_at TIMESTAMP NOT NULL,
|
|
label VARCHAR(120) NOT NULL,
|
|
runtime_months INTEGER,
|
|
mining_speed_value NUMERIC(20,4),
|
|
mining_speed_unit VARCHAR(8),
|
|
bonus_speed_value NUMERIC(20,4),
|
|
bonus_speed_unit VARCHAR(8),
|
|
total_cost_amount NUMERIC(20,8) NOT NULL,
|
|
currency VARCHAR(10) NOT NULL,
|
|
usd_reference_amount NUMERIC(20,8),
|
|
reference_price_amount NUMERIC(20,8),
|
|
reference_price_currency VARCHAR(10),
|
|
auto_renew BOOLEAN NOT NULL DEFAULT FALSE,
|
|
note TEXT,
|
|
is_active BOOLEAN NOT NULL DEFAULT TRUE,
|
|
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
CONSTRAINT fk_mining_purchased_miners_project FOREIGN KEY (project_key) REFERENCES ' . $projectTable . '(project_key) ON DELETE CASCADE,
|
|
CONSTRAINT fk_mining_purchased_miners_offer FOREIGN KEY (miner_offer_id) REFERENCES ' . $offerTable . '(id) ON DELETE SET NULL
|
|
)',
|
|
]
|
|
: [
|
|
'CREATE TABLE IF NOT EXISTS `' . $table . '` (
|
|
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
|
|
project_key VARCHAR(64) NOT NULL,
|
|
miner_offer_id BIGINT UNSIGNED NULL,
|
|
purchased_at TIMESTAMP NOT NULL,
|
|
label VARCHAR(120) NOT NULL,
|
|
runtime_months INT NULL,
|
|
mining_speed_value DECIMAL(20,4) NULL,
|
|
mining_speed_unit VARCHAR(8) NULL,
|
|
bonus_speed_value DECIMAL(20,4) NULL,
|
|
bonus_speed_unit VARCHAR(8) NULL,
|
|
total_cost_amount DECIMAL(20,8) NOT NULL,
|
|
currency VARCHAR(10) NOT NULL,
|
|
usd_reference_amount DECIMAL(20,8) NULL,
|
|
reference_price_amount DECIMAL(20,8) NULL,
|
|
reference_price_currency VARCHAR(10) NULL,
|
|
auto_renew TINYINT(1) NOT NULL DEFAULT 0,
|
|
note TEXT,
|
|
is_active TINYINT(1) NOT NULL DEFAULT 1,
|
|
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
|
CONSTRAINT fk_mining_purchased_miners_project FOREIGN KEY (project_key) REFERENCES `' . $projectTable . '`(project_key) ON DELETE CASCADE,
|
|
CONSTRAINT fk_mining_purchased_miners_offer FOREIGN KEY (miner_offer_id) REFERENCES `' . $offerTable . '`(id) ON DELETE SET NULL
|
|
)',
|
|
];
|
|
|
|
$this->executeUpgradeStatements($statements, 'Schema-Upgrade fuer gekaufte Miner fehlgeschlagen.');
|
|
}
|
|
|
|
private function ensureCurrencyForeignKeys(): void
|
|
{
|
|
$this->seedMissingCurrenciesFromReferences();
|
|
|
|
$constraints = [
|
|
['table' => $this->prefix . 'settings', 'column' => 'daily_cost_currency', 'name' => 'fk_mining_settings_daily_cost_currency_currency'],
|
|
['table' => $this->prefix . 'settings', 'column' => 'report_currency', 'name' => 'fk_mining_settings_report_currency_currency'],
|
|
['table' => $this->prefix . 'settings', 'column' => 'crypto_currency', 'name' => 'fk_mining_settings_crypto_currency_currency'],
|
|
['table' => $this->prefix . 'cost_plans', 'column' => 'currency', 'name' => 'fk_mining_cost_plans_currency_currency'],
|
|
['table' => $this->prefix . 'measurements', 'column' => 'price_currency', 'name' => 'fk_mining_measurements_price_currency_currency'],
|
|
['table' => $this->prefix . 'measurement_rates', 'column' => 'base_currency', 'name' => 'fk_mining_measurement_rates_base_currency_currency'],
|
|
['table' => $this->prefix . 'measurement_rates', 'column' => 'quote_currency', 'name' => 'fk_mining_measurement_rates_quote_currency_currency'],
|
|
['table' => $this->prefix . 'payouts', 'column' => 'payout_currency', 'name' => 'fk_mining_payouts_payout_currency_currency'],
|
|
['table' => $this->prefix . 'targets', 'column' => 'currency', 'name' => 'fk_mining_targets_currency_currency'],
|
|
['table' => $this->prefix . 'miner_offers', 'column' => 'base_price_currency', 'name' => 'fk_mining_miner_offers_base_price_currency_currency'],
|
|
['table' => $this->prefix . 'purchased_miners', 'column' => 'currency', 'name' => 'fk_mining_purchased_miners_currency_currency'],
|
|
['table' => $this->prefix . 'purchased_miners', 'column' => 'reference_price_currency', 'name' => 'fk_mining_purchased_miners_reference_price_currency_currency'],
|
|
['table' => $this->prefix . 'fx_fetches', 'column' => 'base_currency', 'name' => 'fk_mining_fx_fetches_base_currency_currency'],
|
|
['table' => $this->prefix . 'fx_rates', 'column' => 'currency_code', 'name' => 'fk_mining_fx_rates_currency_code_currency'],
|
|
];
|
|
|
|
foreach ($constraints as $constraint) {
|
|
if (
|
|
!$this->tableExists($constraint['table']) ||
|
|
!$this->columnExists($constraint['table'], $constraint['column']) ||
|
|
$this->foreignKeyExists($constraint['name'])
|
|
) {
|
|
continue;
|
|
}
|
|
|
|
$statement = $this->driver === 'pgsql'
|
|
? 'ALTER TABLE ' . $constraint['table'] . ' ADD CONSTRAINT ' . $constraint['name'] . ' FOREIGN KEY (' . $constraint['column'] . ') REFERENCES ' . $this->prefix . 'currencies(code)'
|
|
: 'ALTER TABLE `' . $constraint['table'] . '` ADD CONSTRAINT ' . $constraint['name'] . ' FOREIGN KEY (`' . $constraint['column'] . '`) REFERENCES `' . $this->prefix . 'currencies`(`code`)';
|
|
|
|
$this->executeUpgradeStatements([$statement], 'Schema-Upgrade fuer Waehrungs-Referenzen fehlgeschlagen.');
|
|
}
|
|
}
|
|
|
|
private function seedMissingCurrenciesFromReferences(): void
|
|
{
|
|
$sources = [
|
|
[$this->prefix . 'settings', 'daily_cost_currency'],
|
|
[$this->prefix . 'settings', 'report_currency'],
|
|
[$this->prefix . 'settings', 'crypto_currency'],
|
|
[$this->prefix . 'cost_plans', 'currency'],
|
|
[$this->prefix . 'measurements', 'price_currency'],
|
|
[$this->prefix . 'measurement_rates', 'base_currency'],
|
|
[$this->prefix . 'measurement_rates', 'quote_currency'],
|
|
[$this->prefix . 'payouts', 'payout_currency'],
|
|
[$this->prefix . 'targets', 'currency'],
|
|
[$this->prefix . 'miner_offers', 'base_price_currency'],
|
|
[$this->prefix . 'purchased_miners', 'currency'],
|
|
[$this->prefix . 'purchased_miners', 'reference_price_currency'],
|
|
[$this->prefix . 'fx_fetches', 'base_currency'],
|
|
[$this->prefix . 'fx_rates', 'currency_code'],
|
|
];
|
|
|
|
foreach ($sources as [$table, $column]) {
|
|
if (!$this->tableExists($table) || !$this->columnExists($table, $column)) {
|
|
continue;
|
|
}
|
|
|
|
$statement = $this->driver === 'pgsql'
|
|
? 'INSERT INTO ' . $this->prefix . 'currencies (code, name, symbol, is_active, sort_order)
|
|
SELECT DISTINCT src.' . $column . ', src.' . $column . ', src.' . $column . ', TRUE, 1000
|
|
FROM ' . $table . ' src
|
|
LEFT JOIN ' . $this->prefix . 'currencies c ON c.code = src.' . $column . '
|
|
WHERE src.' . $column . ' IS NOT NULL
|
|
AND BTRIM(src.' . $column . ') <> \'\'
|
|
AND c.code IS NULL'
|
|
: 'INSERT INTO `' . $this->prefix . 'currencies` (code, name, symbol, is_active, sort_order)
|
|
SELECT DISTINCT src.`' . $column . '`, src.`' . $column . '`, src.`' . $column . '`, 1, 1000
|
|
FROM `' . $table . '` src
|
|
LEFT JOIN `' . $this->prefix . 'currencies` c ON c.code = src.`' . $column . '`
|
|
WHERE src.`' . $column . '` IS NOT NULL
|
|
AND TRIM(src.`' . $column . '`) <> \'\'
|
|
AND c.code IS NULL';
|
|
|
|
$this->executeUpgradeStatements([$statement], 'Fehlende Waehrungen konnten nicht vorbereitet werden.');
|
|
}
|
|
}
|
|
|
|
private function upgradeCurrenciesClassificationColumns(): void
|
|
{
|
|
$table = $this->prefix . 'currencies';
|
|
$statements = $this->driver === 'pgsql'
|
|
? ['ALTER TABLE ' . $table . ' ADD COLUMN IF NOT EXISTS is_crypto BOOLEAN NOT NULL DEFAULT FALSE']
|
|
: ['ALTER TABLE `' . $table . '` ADD COLUMN is_crypto TINYINT(1) NOT NULL DEFAULT 0'];
|
|
foreach ($statements as $statement) {
|
|
try {
|
|
$this->executeUpgradeStatements([$statement], 'Schema-Upgrade fuer Waehrungs-Klassifikation fehlgeschlagen.');
|
|
} catch (\Throwable $exception) {
|
|
if ($this->driver === 'mysql' && str_contains(strtolower($exception->getMessage()), 'duplicate column')) {
|
|
continue;
|
|
}
|
|
throw $exception;
|
|
}
|
|
}
|
|
}
|
|
|
|
private function upgradeMinerOfferBasePriceColumns(): void
|
|
{
|
|
$table = $this->prefix . 'miner_offers';
|
|
$statements = $this->driver === 'pgsql'
|
|
? [
|
|
'ALTER TABLE ' . $table . ' ADD COLUMN IF NOT EXISTS base_price_amount NUMERIC(20,8)',
|
|
'ALTER TABLE ' . $table . ' ADD COLUMN IF NOT EXISTS base_price_currency VARCHAR(10)',
|
|
'ALTER TABLE ' . $table . ' ADD COLUMN IF NOT EXISTS payment_type VARCHAR(10) NOT NULL DEFAULT \'fiat\'',
|
|
'ALTER TABLE ' . $table . ' ADD COLUMN IF NOT EXISTS auto_renew BOOLEAN NOT NULL DEFAULT FALSE',
|
|
'UPDATE ' . $table . ' SET base_price_amount = COALESCE(base_price_amount, reference_price_amount, usd_reference_amount, price_amount)',
|
|
'UPDATE ' . $table . ' SET base_price_currency = COALESCE(base_price_currency, reference_price_currency, CASE WHEN usd_reference_amount IS NOT NULL THEN \'USD\' ELSE price_currency END)',
|
|
"UPDATE " . $table . " SET payment_type = CASE WHEN payment_type IS NULL OR BTRIM(payment_type) = '' THEN CASE WHEN UPPER(COALESCE(price_currency, '')) IN ('ADA','ARB','BNB','BTC','DAI','DOGE','DOT','ETH','LINK','LTC','SOL','USDC','USDT','XRP') THEN 'crypto' ELSE 'fiat' END ELSE payment_type END",
|
|
]
|
|
: [
|
|
'ALTER TABLE `' . $table . '` ADD COLUMN base_price_amount DECIMAL(20,8) NULL',
|
|
'ALTER TABLE `' . $table . '` ADD COLUMN base_price_currency VARCHAR(10) NULL',
|
|
'ALTER TABLE `' . $table . '` ADD COLUMN payment_type VARCHAR(10) NOT NULL DEFAULT \'fiat\'',
|
|
'ALTER TABLE `' . $table . '` ADD COLUMN auto_renew TINYINT(1) NOT NULL DEFAULT 0',
|
|
'UPDATE `' . $table . '` SET base_price_amount = COALESCE(base_price_amount, reference_price_amount, usd_reference_amount, price_amount)',
|
|
'UPDATE `' . $table . '` SET base_price_currency = COALESCE(base_price_currency, reference_price_currency, CASE WHEN usd_reference_amount IS NOT NULL THEN \'USD\' ELSE price_currency END)',
|
|
"UPDATE `" . $table . "` SET payment_type = CASE WHEN payment_type IS NULL OR TRIM(payment_type) = '' THEN CASE WHEN UPPER(COALESCE(price_currency, '')) IN ('ADA','ARB','BNB','BTC','DAI','DOGE','DOT','ETH','LINK','LTC','SOL','USDC','USDT','XRP') THEN 'crypto' ELSE 'fiat' END ELSE payment_type END",
|
|
];
|
|
foreach ($statements as $statement) {
|
|
try {
|
|
$this->executeUpgradeStatements([$statement], 'Schema-Upgrade fuer Miner-Angebote fehlgeschlagen.');
|
|
} catch (\Throwable $exception) {
|
|
if ($this->driver === 'mysql' && str_contains(strtolower($exception->getMessage()), 'duplicate column')) {
|
|
continue;
|
|
}
|
|
throw $exception;
|
|
}
|
|
}
|
|
}
|
|
|
|
private function upgradePurchasedMinerReferenceColumns(): void
|
|
{
|
|
$table = $this->prefix . 'purchased_miners';
|
|
$statements = $this->driver === 'pgsql'
|
|
? [
|
|
'ALTER TABLE ' . $table . ' ADD COLUMN IF NOT EXISTS reference_price_amount NUMERIC(20,8)',
|
|
'ALTER TABLE ' . $table . ' ADD COLUMN IF NOT EXISTS reference_price_currency VARCHAR(10)',
|
|
'ALTER TABLE ' . $table . ' ADD COLUMN IF NOT EXISTS auto_renew BOOLEAN NOT NULL DEFAULT FALSE',
|
|
]
|
|
: [
|
|
'ALTER TABLE `' . $table . '` ADD COLUMN reference_price_amount DECIMAL(20,8) NULL',
|
|
'ALTER TABLE `' . $table . '` ADD COLUMN reference_price_currency VARCHAR(10) NULL',
|
|
'ALTER TABLE `' . $table . '` ADD COLUMN auto_renew TINYINT(1) NOT NULL DEFAULT 0',
|
|
];
|
|
foreach ($statements as $statement) {
|
|
try {
|
|
$this->executeUpgradeStatements([$statement], 'Schema-Upgrade fuer gekaufte Miner fehlgeschlagen.');
|
|
} catch (\Throwable $exception) {
|
|
if ($this->driver === 'mysql' && str_contains(strtolower($exception->getMessage()), 'duplicate column')) {
|
|
continue;
|
|
}
|
|
throw $exception;
|
|
}
|
|
}
|
|
}
|
|
|
|
private function upgradeTargetOfferColumn(): void
|
|
{
|
|
$table = $this->prefix . 'targets';
|
|
$statements = $this->driver === 'pgsql'
|
|
? ['ALTER TABLE ' . $table . ' ADD COLUMN IF NOT EXISTS miner_offer_id BIGINT']
|
|
: ['ALTER TABLE `' . $table . '` ADD COLUMN miner_offer_id BIGINT UNSIGNED NULL'];
|
|
foreach ($statements as $statement) {
|
|
try {
|
|
$this->executeUpgradeStatements([$statement], 'Schema-Upgrade fuer Ziel-Angebots-Verknuepfung fehlgeschlagen.');
|
|
} catch (\Throwable $exception) {
|
|
if ($this->driver === 'mysql' && str_contains(strtolower($exception->getMessage()), 'duplicate column')) {
|
|
continue;
|
|
}
|
|
throw $exception;
|
|
}
|
|
}
|
|
}
|
|
|
|
private function ensureTargetOfferForeignKey(): void
|
|
{
|
|
$constraintName = 'fk_mining_targets_offer';
|
|
if ($this->foreignKeyExists($constraintName)) {
|
|
return;
|
|
}
|
|
|
|
$targetTable = $this->prefix . 'targets';
|
|
$offerTable = $this->prefix . 'miner_offers';
|
|
$statement = $this->driver === 'pgsql'
|
|
? 'ALTER TABLE ' . $targetTable . ' ADD CONSTRAINT ' . $constraintName . ' FOREIGN KEY (miner_offer_id) REFERENCES ' . $offerTable . '(id) ON DELETE SET NULL'
|
|
: 'ALTER TABLE `' . $targetTable . '` ADD CONSTRAINT ' . $constraintName . ' FOREIGN KEY (miner_offer_id) REFERENCES `' . $offerTable . '`(id) ON DELETE SET NULL';
|
|
|
|
$this->executeUpgradeStatements([$statement], 'Schema-Upgrade fuer Ziel-Angebots-Fremdschluessel fehlgeschlagen.');
|
|
}
|
|
|
|
private function foreignKeyExists(string $constraintName): bool
|
|
{
|
|
$schemaCondition = $this->driver === 'pgsql'
|
|
? 'constraint_schema = current_schema()'
|
|
: 'constraint_schema = DATABASE()';
|
|
|
|
$statement = $this->pdo->prepare(
|
|
'SELECT constraint_name
|
|
FROM information_schema.table_constraints
|
|
WHERE ' . $schemaCondition . '
|
|
AND constraint_type = \'FOREIGN KEY\'
|
|
AND constraint_name = :constraint_name
|
|
LIMIT 1'
|
|
);
|
|
$statement->execute(['constraint_name' => $constraintName]);
|
|
|
|
return (bool) $statement->fetchColumn();
|
|
}
|
|
|
|
private function executeUpgradeStatements(array $statements, string $message): void
|
|
{
|
|
foreach ($statements as $statement) {
|
|
try {
|
|
$this->pdo->exec($statement);
|
|
} catch (\Throwable $exception) {
|
|
throw new ApiException(
|
|
$message,
|
|
500,
|
|
['message' => $exception->getMessage(), 'statement' => $statement]
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
private function lightweightStatus(): array
|
|
{
|
|
$coreTables = $this->coreTables();
|
|
$extraTables = $this->extraTables();
|
|
|
|
$presentTables = $this->existingTables(array_merge($coreTables, $extraTables));
|
|
|
|
return [
|
|
'present_tables' => $presentTables,
|
|
'present_count' => count($presentTables),
|
|
'core_present' => count(array_intersect($coreTables, $presentTables)) === count($coreTables),
|
|
'missing_core_tables' => array_values(array_diff($coreTables, $presentTables)),
|
|
'missing_extra_tables' => array_values(array_diff($extraTables, $presentTables)),
|
|
];
|
|
}
|
|
|
|
private function coreTables(): array
|
|
{
|
|
return [
|
|
$this->prefix . 'projects',
|
|
$this->prefix . 'settings',
|
|
$this->prefix . 'cost_plans',
|
|
$this->prefix . 'measurements',
|
|
$this->prefix . 'targets',
|
|
$this->prefix . 'dashboard_definitions',
|
|
];
|
|
}
|
|
|
|
private function extraTables(): array
|
|
{
|
|
return [
|
|
$this->prefix . 'fx_fetches',
|
|
$this->prefix . 'fx_rates',
|
|
$this->prefix . 'measurement_rates',
|
|
$this->prefix . 'payouts',
|
|
$this->prefix . 'wallet_snapshots',
|
|
$this->prefix . 'miner_offers',
|
|
$this->prefix . 'purchased_miners',
|
|
];
|
|
}
|
|
|
|
private function knownTablesInDropOrder(): array
|
|
{
|
|
return array_reverse($this->knownTablesInCreateOrder());
|
|
}
|
|
|
|
private function knownTablesInCreateOrder(): array
|
|
{
|
|
return [
|
|
$this->prefix . 'projects',
|
|
$this->prefix . 'settings',
|
|
$this->prefix . 'cost_plans',
|
|
$this->prefix . 'measurements',
|
|
$this->prefix . 'measurement_rates',
|
|
$this->prefix . 'payouts',
|
|
$this->prefix . 'wallet_snapshots',
|
|
$this->prefix . 'miner_offers',
|
|
$this->prefix . 'targets',
|
|
$this->prefix . 'dashboard_definitions',
|
|
$this->prefix . 'purchased_miners',
|
|
$this->prefix . 'fx_fetches',
|
|
$this->prefix . 'fx_rates',
|
|
];
|
|
}
|
|
}
|