38471-vm/db/migrations/20260502_full_schema_sync.php
2026-05-02 02:47:39 +00:00

314 lines
11 KiB
PHP

<?php
declare(strict_types=1);
require_once __DIR__ . '/../config.php';
if (!function_exists('full_schema_sync_20260502_run')) {
function full_schema_sync_20260502_run(): void
{
$pdo = db();
full_schema_sync_20260502_apply_create_tables_from_file($pdo, __DIR__ . '/../schema.sql');
full_schema_sync_20260502_apply_create_tables_from_file($pdo, __DIR__ . '/20260318_create_outlets_table.sql');
full_schema_sync_20260502_apply_create_tables_from_file($pdo, __DIR__ . '/20260318_multi_outlet_schema.sql');
full_schema_sync_20260502_apply_create_tables_from_file($pdo, __DIR__ . '/20260318_user_outlets_table.sql');
full_schema_sync_20260502_ensure_columns($pdo);
full_schema_sync_20260502_seed_defaults($pdo);
}
function full_schema_sync_20260502_apply_create_tables_from_file(PDO $pdo, string $filePath): void
{
if (!is_file($filePath)) {
return;
}
$sql = file_get_contents($filePath);
if ($sql === false) {
throw new RuntimeException('Unable to read schema source: ' . basename($filePath));
}
foreach (full_schema_sync_20260502_extract_create_table_statements($sql) as $statement) {
$normalized = full_schema_sync_20260502_normalize_create_table_statement($statement);
full_schema_sync_20260502_exec($pdo, $normalized);
}
}
function full_schema_sync_20260502_extract_create_table_statements(string $sql): array
{
if (!preg_match_all('/CREATE\s+TABLE\b.*?;/is', $sql, $matches)) {
return [];
}
return $matches[0] ?? [];
}
function full_schema_sync_20260502_normalize_create_table_statement(string $statement): string
{
$statement = trim($statement);
$statement = preg_replace('/^CREATE\s+TABLE(?:\s+IF\s+NOT\s+EXISTS)?\s+/i', 'CREATE TABLE IF NOT EXISTS ', $statement, 1) ?? $statement;
$lines = preg_split('/\R/', $statement) ?: [];
$kept = [];
foreach ($lines as $line) {
if (preg_match('/^\s*CONSTRAINT\b/i', $line)) {
continue;
}
$kept[] = $line;
}
$statement = implode("\n", $kept);
$statement = preg_replace('/,\s*\)(?=\s*(ENGINE|DEFAULT|COMMENT|CHARSET|COLLATE|;))/is', "\n)", $statement) ?? $statement;
return rtrim($statement, "\n\r\t ;") . ';';
}
function full_schema_sync_20260502_ensure_columns(PDO $pdo): void
{
$columns = [
'role_groups' => [
'permissions' => 'TEXT NULL',
],
'users' => [
'theme' => "VARCHAR(20) DEFAULT 'default'",
'outlet_id' => 'INT(11) DEFAULT NULL',
],
'customers' => [
'outlet_id' => 'INT(11) DEFAULT NULL',
],
'suppliers' => [
'outlet_id' => 'INT(11) DEFAULT 1',
],
'stock_categories' => [
'outlet_id' => 'INT(11) DEFAULT 1',
],
'stock_units' => [
'outlet_id' => 'INT(11) DEFAULT 1',
],
'stock_items' => [
'outlet_id' => 'INT(11) DEFAULT 1',
],
'expenses' => [
'outlet_id' => 'INT(11) DEFAULT NULL',
],
'invoices' => [
'transaction_no' => 'VARCHAR(50) DEFAULT NULL',
'register_session_id' => 'INT(11) DEFAULT NULL',
'is_pos' => 'TINYINT(1) DEFAULT 0',
'discount_amount' => 'DECIMAL(15,3) DEFAULT 0.000',
'loyalty_points_earned' => 'DECIMAL(15,3) DEFAULT 0.000',
'loyalty_points_redeemed' => 'DECIMAL(15,3) DEFAULT 0.000',
'created_by' => 'INT(11) DEFAULT NULL',
'outlet_id' => 'INT(11) DEFAULT NULL',
],
'invoice_items' => [
'vat_amount' => 'DECIMAL(15,3) DEFAULT 0.000',
],
'payments' => [
'outlet_id' => 'INT(11) DEFAULT NULL',
],
'pos_held_carts' => [
'outlet_id' => 'INT(11) DEFAULT NULL',
],
'pos_transactions' => [
'outlet_id' => 'INT(11) DEFAULT NULL',
],
'purchase_payments' => [
'outlet_id' => 'INT(11) DEFAULT NULL',
],
'purchase_returns' => [
'outlet_id' => 'INT(11) DEFAULT NULL',
],
'purchases' => [
'outlet_id' => 'INT(11) DEFAULT 1',
],
'quotations' => [
'outlet_id' => 'INT(11) DEFAULT NULL',
],
'sales_returns' => [
'outlet_id' => 'INT(11) DEFAULT NULL',
],
'lpos' => [
'outlet_id' => 'INT(11) DEFAULT NULL',
],
];
foreach ($columns as $table => $tableColumns) {
if (!full_schema_sync_20260502_table_exists($pdo, $table)) {
continue;
}
foreach ($tableColumns as $column => $definition) {
if (full_schema_sync_20260502_column_exists($pdo, $table, $column)) {
continue;
}
$statement = sprintf(
'ALTER TABLE `%s` ADD COLUMN `%s` %s',
str_replace('`', '', $table),
str_replace('`', '', $column),
$definition
);
full_schema_sync_20260502_exec($pdo, $statement);
}
}
}
function full_schema_sync_20260502_seed_defaults(PDO $pdo): void
{
if (full_schema_sync_20260502_table_exists($pdo, 'outlets')) {
$pdo->exec(
"INSERT INTO outlets (id, name, address, phone, status, created_at) "
. "SELECT 1, 'Main Outlet', 'Head Office', '', 'active', NOW() "
. "WHERE NOT EXISTS (SELECT 1 FROM outlets WHERE id = 1)"
);
}
if (full_schema_sync_20260502_table_exists($pdo, 'role_groups') && full_schema_sync_20260502_column_exists($pdo, 'role_groups', 'permissions')) {
$pdo->exec("UPDATE role_groups SET permissions = 'all' WHERE permissions IS NULL AND LOWER(name) IN ('administrator', 'admin')");
}
if (
full_schema_sync_20260502_table_exists($pdo, 'role_groups')
&& full_schema_sync_20260502_table_exists($pdo, 'role_permissions')
) {
$pdo->exec(
"INSERT INTO role_permissions (role_id, permission) "
. "SELECT rg.id, 'all' "
. "FROM role_groups rg "
. "LEFT JOIN role_permissions rp ON rp.role_id = rg.id AND rp.permission = 'all' "
. "WHERE LOWER(rg.name) IN ('administrator', 'admin') AND rp.id IS NULL"
);
}
$outletTables = [
'users',
'customers',
'suppliers',
'stock_categories',
'stock_units',
'stock_items',
'expenses',
'invoices',
'payments',
'pos_held_carts',
'pos_transactions',
'purchase_payments',
'purchase_returns',
'purchases',
'quotations',
'sales_returns',
'lpos',
];
foreach ($outletTables as $table) {
if (
full_schema_sync_20260502_table_exists($pdo, $table)
&& full_schema_sync_20260502_column_exists($pdo, $table, 'outlet_id')
) {
$pdo->exec(sprintf('UPDATE `%s` SET `outlet_id` = 1 WHERE `outlet_id` IS NULL', $table));
}
}
if (
full_schema_sync_20260502_table_exists($pdo, 'user_outlets')
&& full_schema_sync_20260502_table_exists($pdo, 'users')
&& full_schema_sync_20260502_table_exists($pdo, 'outlets')
) {
$pdo->exec(
'INSERT INTO user_outlets (user_id, outlet_id) '
. 'SELECT u.id, 1 '
. 'FROM users u '
. 'LEFT JOIN user_outlets uo ON uo.user_id = u.id AND uo.outlet_id = 1 '
. 'WHERE uo.user_id IS NULL'
);
}
if (
full_schema_sync_20260502_table_exists($pdo, 'outlet_stock')
&& full_schema_sync_20260502_table_exists($pdo, 'stock_items')
&& full_schema_sync_20260502_table_exists($pdo, 'outlets')
&& full_schema_sync_20260502_column_exists($pdo, 'stock_items', 'stock_quantity')
) {
$pdo->exec(
'INSERT INTO outlet_stock (outlet_id, item_id, quantity) '
. 'SELECT 1, si.id, COALESCE(si.stock_quantity, 0) '
. 'FROM stock_items si '
. 'LEFT JOIN outlet_stock os ON os.outlet_id = 1 AND os.item_id = si.id '
. 'WHERE os.id IS NULL'
);
}
}
function full_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 full_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 full_schema_sync_20260502_exec(PDO $pdo, string $statement): void
{
try {
$pdo->exec($statement);
} catch (PDOException $exception) {
if (full_schema_sync_20260502_is_ignorable($exception)) {
return;
}
throw $exception;
}
}
function full_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;
}
}
full_schema_sync_20260502_run();
return true;