'INT NULL AFTER unit_id', 'min_stock_level' => 'DECIMAL(15,2) DEFAULT 0.00 AFTER stock_quantity', 'expiry_date' => 'DATE DEFAULT NULL AFTER min_stock_level', 'image_path' => 'VARCHAR(255) DEFAULT NULL AFTER expiry_date', 'vat_rate' => 'DECIMAL(5,2) DEFAULT 0.00 AFTER image_path', 'is_promotion' => 'TINYINT(1) DEFAULT 0 AFTER vat_rate', 'promotion_start' => 'DATE DEFAULT NULL AFTER is_promotion', 'promotion_end' => 'DATE DEFAULT NULL AFTER promotion_start', 'promotion_percent' => 'DECIMAL(5,2) DEFAULT 0.00 AFTER promotion_end', 'outlet_id' => 'INT(11) DEFAULT 1 AFTER promotion_percent', ]; foreach ($columns as $column => $definition) { if (stock_items_schema_sync_20260502_column_exists($pdo, 'stock_items', $column)) { continue; } $statement = sprintf( 'ALTER TABLE stock_items ADD COLUMN %s %s', $column, $definition ); stock_items_schema_sync_20260502_exec($pdo, $statement); } stock_items_schema_sync_20260502_modify_decimal_column($pdo, 'purchase_price', 'DECIMAL(15,3) DEFAULT 0.000'); stock_items_schema_sync_20260502_modify_decimal_column($pdo, 'sale_price', 'DECIMAL(15,3) DEFAULT 0.000'); } function stock_items_schema_sync_20260502_table_exists(PDO $pdo, string $table): bool { $stmt = $pdo->prepare( 'SELECT 1 FROM information_schema.TABLES WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = :table LIMIT 1' ); $stmt->execute(['table' => $table]); return (bool) $stmt->fetchColumn(); } function stock_items_schema_sync_20260502_column_exists(PDO $pdo, string $table, string $column): bool { $stmt = $pdo->prepare( 'SELECT 1 FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = :table AND COLUMN_NAME = :column LIMIT 1' ); $stmt->execute([ 'table' => $table, 'column' => $column, ]); return (bool) $stmt->fetchColumn(); } function stock_items_schema_sync_20260502_exec(PDO $pdo, string $statement): void { try { $pdo->exec($statement); } catch (PDOException $exception) { if (stock_items_schema_sync_20260502_is_ignorable($exception)) { return; } throw $exception; } } function stock_items_schema_sync_20260502_is_ignorable(PDOException $exception): bool { $driverCode = isset($exception->errorInfo[1]) ? (int) $exception->errorInfo[1] : null; $message = strtolower($exception->getMessage()); $ignorableCodes = [1050, 1060, 1061, 1062, 1091, 1826]; $ignorableSnippets = [ 'already exists', 'duplicate column name', 'duplicate key name', 'duplicate entry', 'duplicate foreign key constraint name', 'duplicate key on write or update', 'errno: 121', 'check that column/key exists', ]; if ($driverCode !== null && in_array($driverCode, $ignorableCodes, true)) { return true; } foreach ($ignorableSnippets as $snippet) { if (str_contains($message, $snippet)) { return true; } } return false; } function stock_items_schema_sync_20260502_modify_decimal_column(PDO $pdo, string $column, string $definition): void { if (!stock_items_schema_sync_20260502_column_exists($pdo, 'stock_items', $column)) { return; } $statement = sprintf( 'ALTER TABLE stock_items MODIFY COLUMN %s %s', $column, $definition ); $pdo->exec($statement); } } stock_items_schema_sync_20260502_run(); return true;