Files
api/services/nginx/app/classes/release_manager_schema_bootstrap.php
T

528 lines
27 KiB
PHP

<?php
namespace classes;
/**
* Additive schema bootstrap for application release management.
*
* The legacy API does not have a centralized migration runner, so these
* migrations must be safe to call from request handlers, tests, and cron.
*/
class release_manager_schema_bootstrap
{
private static bool $initialized = false;
private static ?bool $tablesExist = null;
public static function ensureTables(): void
{
if (self::$initialized) {
return;
}
global $db;
$queries = [
"CREATE TABLE IF NOT EXISTS release_channels (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
slug VARCHAR(64) NOT NULL,
name VARCHAR(128) NOT NULL,
description TEXT NULL,
enabled TINYINT(1) NOT NULL DEFAULT 1,
default_channel TINYINT(1) NOT NULL DEFAULT 0,
rollout_percent DECIMAL(5,2) NOT NULL DEFAULT 0.00,
frontend_base_url VARCHAR(512) NULL,
api_base_url VARCHAR(512) NULL,
replay_enabled TINYINT(1) NOT NULL DEFAULT 0,
capture_level VARCHAR(32) NOT NULL DEFAULT 'metadata',
retention_days INT NOT NULL DEFAULT 14,
metadata_json LONGTEXT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
deleted_at DATETIME NULL,
UNIQUE KEY uq_release_channels_slug (slug),
INDEX idx_release_channels_enabled (enabled, deleted_at),
INDEX idx_release_channels_default (default_channel, deleted_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci",
"CREATE TABLE IF NOT EXISTS release_versions (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
app VARCHAR(16) NOT NULL,
repository VARCHAR(255) NULL,
branch VARCHAR(128) NULL,
commit_sha VARCHAR(64) NULL,
tag VARCHAR(128) NULL,
version_label VARCHAR(128) NULL,
build_url VARCHAR(512) NULL,
artifact_url VARCHAR(512) NULL,
deployed_url VARCHAR(512) NULL,
status VARCHAR(32) NOT NULL DEFAULT 'discovered',
metadata_json LONGTEXT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
deployed_at DATETIME NULL,
INDEX idx_release_versions_app_status (app, status),
INDEX idx_release_versions_commit (commit_sha),
INDEX idx_release_versions_repo_branch (repository, branch)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci",
"CREATE TABLE IF NOT EXISTS release_channel_versions (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
channel_id BIGINT UNSIGNED NOT NULL,
frontend_version_id BIGINT UNSIGNED NULL,
api_version_id BIGINT UNSIGNED NULL,
deployment_id BIGINT UNSIGNED NULL,
service_set_id BIGINT UNSIGNED NULL,
bundle_id BIGINT UNSIGNED NULL,
actor_user_id INT NULL,
active TINYINT(1) NOT NULL DEFAULT 1,
activated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
INDEX idx_release_channel_versions_channel_active (channel_id, active),
INDEX idx_release_channel_versions_frontend (frontend_version_id),
INDEX idx_release_channel_versions_api (api_version_id),
INDEX idx_release_channel_versions_deployment (deployment_id),
INDEX idx_release_channel_versions_service_set (service_set_id),
INDEX idx_release_channel_versions_bundle (bundle_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci",
"CREATE TABLE IF NOT EXISTS release_assignments (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
subject_type VARCHAR(16) NOT NULL,
subject_id VARCHAR(64) NOT NULL,
channel_id BIGINT UNSIGNED NOT NULL,
reason VARCHAR(255) NULL,
expires_at DATETIME NULL,
actor_user_id INT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
deleted_at DATETIME NULL,
INDEX idx_release_assignments_subject (subject_type, subject_id, deleted_at, expires_at),
INDEX idx_release_assignments_channel (channel_id, deleted_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci",
"CREATE TABLE IF NOT EXISTS release_deployment_targets (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
channel_id BIGINT UNSIGNED NOT NULL,
app VARCHAR(16) NOT NULL,
coolify_instance_id BIGINT UNSIGNED NULL,
coolify_service_uuid VARCHAR(128) NULL,
repository VARCHAR(255) NOT NULL,
branch VARCHAR(128) NOT NULL DEFAULT 'main',
auto_deploy TINYINT(1) NOT NULL DEFAULT 1,
health_url VARCHAR(512) NULL,
deploy_context_json LONGTEXT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
deleted_at DATETIME NULL,
INDEX idx_release_targets_channel_app (channel_id, app, deleted_at),
INDEX idx_release_targets_repo_branch (repository, branch, auto_deploy, deleted_at),
INDEX idx_release_targets_coolify (coolify_instance_id, coolify_service_uuid)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci",
"CREATE TABLE IF NOT EXISTS release_service_sets (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
channel_id BIGINT UNSIGNED NULL,
name VARCHAR(128) NOT NULL,
slug VARCHAR(64) NOT NULL,
mode VARCHAR(32) NOT NULL DEFAULT 'attach_existing',
source_service_set_id BIGINT UNSIGNED NULL,
frontend_target_id BIGINT UNSIGNED NULL,
api_target_id BIGINT UNSIGNED NULL,
database_coolify_target_id BIGINT UNSIGNED NULL,
redis_coolify_target_id BIGINT UNSIGNED NULL,
minio_coolify_target_id BIGINT UNSIGNED NULL,
status VARCHAR(32) NOT NULL DEFAULT 'needs_configuration',
health_json LONGTEXT NULL,
metadata_json LONGTEXT NULL,
actor_user_id INT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
deleted_at DATETIME NULL,
UNIQUE KEY uq_release_service_sets_slug (slug),
INDEX idx_release_service_sets_channel (channel_id, deleted_at),
INDEX idx_release_service_sets_source (source_service_set_id),
INDEX idx_release_service_sets_status (status, deleted_at),
INDEX idx_release_service_sets_frontend_target (frontend_target_id),
INDEX idx_release_service_sets_api_target (api_target_id),
INDEX idx_release_service_sets_database_target (database_coolify_target_id),
INDEX idx_release_service_sets_redis_target (redis_coolify_target_id),
INDEX idx_release_service_sets_minio_target (minio_coolify_target_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci",
"CREATE TABLE IF NOT EXISTS release_deployments (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
channel_id BIGINT UNSIGNED NOT NULL,
target_id BIGINT UNSIGNED NULL,
version_id BIGINT UNSIGNED NULL,
service_set_id BIGINT UNSIGNED NULL,
bundle_id BIGINT UNSIGNED NULL,
deployment_kind VARCHAR(32) NOT NULL DEFAULT 'single_app',
app VARCHAR(16) NOT NULL,
active_channel_app_key VARCHAR(96) NULL,
provider VARCHAR(32) NOT NULL DEFAULT 'coolify',
repository VARCHAR(255) NULL,
branch VARCHAR(128) NULL,
commit_sha VARCHAR(64) NULL,
status VARCHAR(32) NOT NULL DEFAULT 'queued',
provider_operation_id VARCHAR(128) NULL,
deployment_url VARCHAR(512) NULL,
actor_user_id INT NULL,
requested_payload_json LONGTEXT NULL,
result_json LONGTEXT NULL,
error_message TEXT NULL,
started_at DATETIME NULL,
completed_at DATETIME NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_release_deployments_channel_status (channel_id, status),
INDEX idx_release_deployments_target (target_id),
INDEX idx_release_deployments_version (version_id),
INDEX idx_release_deployments_service_set (service_set_id),
INDEX idx_release_deployments_bundle (bundle_id),
INDEX idx_release_deployments_kind (deployment_kind),
INDEX idx_release_deployments_commit (commit_sha)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci",
"CREATE TABLE IF NOT EXISTS release_bundles (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
channel_id BIGINT UNSIGNED NOT NULL,
service_set_id BIGINT UNSIGNED NOT NULL,
version_label VARCHAR(128) NULL,
frontend_version_id BIGINT UNSIGNED NULL,
api_version_id BIGINT UNSIGNED NULL,
frontend_deployment_id BIGINT UNSIGNED NULL,
api_deployment_id BIGINT UNSIGNED NULL,
frontend_repository VARCHAR(255) NULL,
frontend_branch VARCHAR(128) NULL,
frontend_commit_sha VARCHAR(64) NULL,
api_repository VARCHAR(255) NULL,
api_branch VARCHAR(128) NULL,
api_commit_sha VARCHAR(64) NULL,
status VARCHAR(32) NOT NULL DEFAULT 'draft',
deployment_result_json LONGTEXT NULL,
metadata_json LONGTEXT NULL,
actor_user_id INT NULL,
deployed_at DATETIME NULL,
promoted_at DATETIME NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
deleted_at DATETIME NULL,
INDEX idx_release_bundles_channel_status (channel_id, status, deleted_at),
INDEX idx_release_bundles_service_set (service_set_id, status, deleted_at),
INDEX idx_release_bundles_frontend_version (frontend_version_id),
INDEX idx_release_bundles_api_version (api_version_id),
INDEX idx_release_bundles_frontend_deployment (frontend_deployment_id),
INDEX idx_release_bundles_api_deployment (api_deployment_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci",
"CREATE TABLE IF NOT EXISTS release_replay_targets (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
target_type VARCHAR(16) NOT NULL,
target_id VARCHAR(64) NULL,
channel_id BIGINT UNSIGNED NULL,
capture_level VARCHAR(32) NOT NULL DEFAULT 'full_redacted',
enabled TINYINT(1) NOT NULL DEFAULT 1,
expires_at DATETIME NULL,
actor_user_id INT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
deleted_at DATETIME NULL,
INDEX idx_release_replay_target (target_type, target_id, enabled, deleted_at, expires_at),
INDEX idx_release_replay_channel (channel_id, enabled, deleted_at, expires_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci",
"CREATE TABLE IF NOT EXISTS release_timeline_sessions (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
trace_id VARCHAR(64) NOT NULL,
session_hash VARCHAR(64) NULL,
principal_type VARCHAR(16) NULL,
principal_id VARCHAR(64) NULL,
customer_number INT NULL,
channel_id BIGINT UNSIGNED NULL,
channel_slug VARCHAR(64) NULL,
frontend_version_id BIGINT UNSIGNED NULL,
api_version_id BIGINT UNSIGNED NULL,
device_type VARCHAR(16) NULL,
browser_name VARCHAR(64) NULL,
browser_version VARCHAR(64) NULL,
os_name VARCHAR(64) NULL,
os_version VARCHAR(64) NULL,
viewport_width INT NULL,
viewport_height INT NULL,
device_pixel_ratio DECIMAL(6,3) NULL,
frontend_version_label VARCHAR(128) NULL,
frontend_commit_sha VARCHAR(128) NULL,
api_version_label VARCHAR(128) NULL,
api_commit_sha VARCHAR(128) NULL,
last_route_path VARCHAR(255) NULL,
user_agent VARCHAR(512) NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
last_seen_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
UNIQUE KEY uq_release_timeline_trace (trace_id),
INDEX idx_release_timeline_principal (principal_type, principal_id),
INDEX idx_release_timeline_channel (channel_id, channel_slug),
INDEX idx_release_timeline_device (device_type),
INDEX idx_release_timeline_release (frontend_version_label, api_version_label),
INDEX idx_release_timeline_customer (customer_number),
INDEX idx_release_timeline_last_seen (last_seen_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci",
"CREATE TABLE IF NOT EXISTS release_timeline_events (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
timeline_session_id BIGINT UNSIGNED NULL,
trace_id VARCHAR(64) NOT NULL,
event_type VARCHAR(64) NOT NULL,
severity VARCHAR(16) NOT NULL DEFAULT 'info',
module_key VARCHAR(64) NULL,
route_path VARCHAR(255) NULL,
component VARCHAR(255) NULL,
request_id VARCHAR(128) NULL,
occurred_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
payload_json LONGTEXT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
INDEX idx_release_timeline_events_session (timeline_session_id, occurred_at),
INDEX idx_release_timeline_events_trace (trace_id, occurred_at),
INDEX idx_release_timeline_events_type (event_type, severity),
INDEX idx_release_timeline_events_module (module_key, occurred_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci",
"CREATE TABLE IF NOT EXISTS release_module_health_snapshots (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
channel_id BIGINT UNSIGNED NULL,
module_key VARCHAR(64) NOT NULL,
status VARCHAR(32) NOT NULL,
reason VARCHAR(512) NULL,
payload_json LONGTEXT NULL,
checked_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
INDEX idx_release_module_health_module (module_key, checked_at),
INDEX idx_release_module_health_channel (channel_id, module_key, checked_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci",
"CREATE TABLE IF NOT EXISTS release_operation_runs (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
operation_type VARCHAR(64) NOT NULL,
subject_type VARCHAR(64) NULL,
subject_id VARCHAR(128) NULL,
channel_id BIGINT UNSIGNED NULL,
app VARCHAR(16) NULL,
status VARCHAR(32) NOT NULL DEFAULT 'queued',
title VARCHAR(255) NULL,
summary TEXT NULL,
solution_hint TEXT NULL,
actor_user_id INT NULL,
context_json LONGTEXT NULL,
started_at DATETIME NULL,
completed_at DATETIME NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_release_operation_runs_channel (channel_id, created_at),
INDEX idx_release_operation_runs_subject (subject_type, subject_id, created_at),
INDEX idx_release_operation_runs_type_status (operation_type, status),
INDEX idx_release_operation_runs_created (created_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci",
"CREATE TABLE IF NOT EXISTS release_operation_steps (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
operation_run_id BIGINT UNSIGNED NOT NULL,
step_key VARCHAR(64) NOT NULL,
label VARCHAR(255) NOT NULL,
status VARCHAR(32) NOT NULL DEFAULT 'queued',
message TEXT NULL,
diagnostic TEXT NULL,
solution_hint TEXT NULL,
context_json LONGTEXT NULL,
started_at DATETIME NULL,
completed_at DATETIME NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_release_operation_steps_run (operation_run_id, id),
INDEX idx_release_operation_steps_status (status)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci",
"CREATE TABLE IF NOT EXISTS release_audit_logs (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
channel_id BIGINT UNSIGNED NULL,
deployment_id BIGINT UNSIGNED NULL,
action VARCHAR(64) NOT NULL,
actor_user_id INT NULL,
severity VARCHAR(16) NOT NULL DEFAULT 'info',
context_json LONGTEXT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
INDEX idx_release_audit_channel (channel_id, created_at),
INDEX idx_release_audit_deployment (deployment_id),
INDEX idx_release_audit_action (action)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci",
];
foreach ($queries as $sql) {
$db->query($sql);
}
self::ensureColumn('release_channel_versions', 'service_set_id', 'BIGINT UNSIGNED NULL AFTER deployment_id');
self::ensureColumn('release_channel_versions', 'bundle_id', 'BIGINT UNSIGNED NULL AFTER service_set_id');
self::ensureColumn('release_deployments', 'service_set_id', 'BIGINT UNSIGNED NULL AFTER version_id');
self::ensureColumn('release_deployments', 'bundle_id', 'BIGINT UNSIGNED NULL AFTER service_set_id');
self::ensureColumn('release_deployments', 'deployment_kind', "VARCHAR(32) NOT NULL DEFAULT 'single_app' AFTER bundle_id");
self::ensureColumn('release_deployments', 'active_channel_app_key', 'VARCHAR(96) NULL AFTER app');
self::ensureColumn('release_timeline_sessions', 'device_type', 'VARCHAR(16) NULL AFTER api_version_id');
self::ensureColumn('release_timeline_sessions', 'browser_name', 'VARCHAR(64) NULL AFTER device_type');
self::ensureColumn('release_timeline_sessions', 'browser_version', 'VARCHAR(64) NULL AFTER browser_name');
self::ensureColumn('release_timeline_sessions', 'os_name', 'VARCHAR(64) NULL AFTER browser_version');
self::ensureColumn('release_timeline_sessions', 'os_version', 'VARCHAR(64) NULL AFTER os_name');
self::ensureColumn('release_timeline_sessions', 'viewport_width', 'INT NULL AFTER os_version');
self::ensureColumn('release_timeline_sessions', 'viewport_height', 'INT NULL AFTER viewport_width');
self::ensureColumn('release_timeline_sessions', 'device_pixel_ratio', 'DECIMAL(6,3) NULL AFTER viewport_height');
self::ensureColumn('release_timeline_sessions', 'frontend_version_label', 'VARCHAR(128) NULL AFTER device_pixel_ratio');
self::ensureColumn('release_timeline_sessions', 'frontend_commit_sha', 'VARCHAR(128) NULL AFTER frontend_version_label');
self::ensureColumn('release_timeline_sessions', 'api_version_label', 'VARCHAR(128) NULL AFTER frontend_commit_sha');
self::ensureColumn('release_timeline_sessions', 'api_commit_sha', 'VARCHAR(128) NULL AFTER api_version_label');
self::ensureColumn('release_timeline_sessions', 'last_route_path', 'VARCHAR(255) NULL AFTER api_commit_sha');
self::ensureIndex('release_timeline_sessions', 'idx_release_timeline_device', 'device_type');
self::ensureIndex('release_timeline_sessions', 'idx_release_timeline_release', 'frontend_version_label, api_version_label');
self::ensureUniqueIndex('release_deployments', 'uniq_release_deployments_active_channel_app', 'active_channel_app_key');
self::ensureModuleConfigDefault('ReleaseManager', 'enabled', 'true', 'bool');
self::ensureModuleConfigDefault('ReleaseManager', 'github_webhook_secret', '', 'string');
self::ensureModuleConfigDefault('ReleaseManager', 'release_gate_token', '', 'string');
self::ensureModuleConfigDefault('ReleaseManager', 'release_gate_required_for_promotion', 'true', 'bool');
self::ensureModuleConfigDefault('ReleaseManager', 'github_token', '', 'string');
self::ensureModuleConfigDefault('ReleaseManager', 'github_api_url', 'https://api.github.com', 'string');
self::ensureModuleConfigDefault('ReleaseManager', 'default_retention_days', '14', 'int');
self::ensureDefaultChannels();
self::$initialized = true;
self::$tablesExist = true;
}
public static function tablesExist(): bool
{
if (self::$tablesExist !== null) {
return self::$tablesExist;
}
global $db;
foreach ([
'release_channels',
'release_versions',
'release_channel_versions',
'release_assignments',
'release_service_sets',
'release_deployments',
'release_bundles',
'release_operation_runs',
'release_operation_steps',
'release_timeline_sessions',
'release_timeline_events',
] as $table) {
$tableSql = $db->escape_string($table);
$result = $db->query("SHOW TABLES LIKE '$tableSql'");
if ($result === false || $result->num_rows === 0) {
self::$tablesExist = false;
return false;
}
}
self::$tablesExist = true;
return true;
}
private static function ensureDefaultChannels(): void
{
global $db;
$channels = [
['stable', 'Stable', 'Default production channel.', 1, 1, 'metadata'],
['canary', 'Canary', 'Earliest production validation channel.', 1, 0, 'metadata'],
['beta', 'Beta', 'Broader pre-stable rollout channel.', 1, 0, 'metadata'],
['internal', 'Internal', 'Internal staff and superuser validation channel.', 1, 0, 'metadata'],
];
foreach ($channels as [$slug, $name, $description, $enabled, $default, $captureLevel]) {
$db->query(sprintf(
"INSERT IGNORE INTO release_channels (slug, name, description, enabled, default_channel, capture_level)
VALUES ('%s', '%s', '%s', %d, %d, '%s')",
$db->escape_string($slug),
$db->escape_string($name),
$db->escape_string($description),
(int)$enabled,
(int)$default,
$db->escape_string($captureLevel)
));
}
}
private static function ensureModuleConfigDefault(string $module, string $variable, string $value, string $type): void
{
global $db;
$moduleSql = $db->escape_string($module);
$variableSql = $db->escape_string($variable);
$result = $db->query("SELECT value FROM module_config WHERE module = '$moduleSql' AND variable = '$variableSql' LIMIT 1");
if ($result !== false && $result->num_rows > 0) {
return;
}
$valueSql = $db->escape_string($value);
$typeSql = $db->escape_string($type);
$db->query("INSERT INTO module_config (module, variable, value, type) VALUES ('$moduleSql', '$variableSql', '$valueSql', '$typeSql')");
}
private static function ensureColumn(string $table, string $column, string $definition): void
{
global $db;
$table = preg_replace('/[^a-zA-Z0-9_]/', '', $table);
$column = preg_replace('/[^a-zA-Z0-9_]/', '', $column);
if ($table === '' || $column === '') {
return;
}
$result = $db->query("SHOW COLUMNS FROM `$table` LIKE '$column'");
if ($result !== false && $result->num_rows > 0) {
return;
}
$db->query("ALTER TABLE `$table` ADD COLUMN `$column` $definition");
}
private static function ensureIndex(string $table, string $index, string $columns): void
{
global $db;
$table = preg_replace('/[^a-zA-Z0-9_]/', '', $table);
$index = preg_replace('/[^a-zA-Z0-9_]/', '', $index);
if ($table === '' || $index === '') {
return;
}
$indexSql = $db->escape_string($index);
$result = $db->query("SHOW INDEX FROM `$table` WHERE Key_name = '$indexSql'");
if ($result !== false && $result->num_rows > 0) {
return;
}
$db->query("ALTER TABLE `$table` ADD INDEX `$index` ($columns)");
}
private static function ensureUniqueIndex(string $table, string $index, string $columns): void
{
global $db;
$table = preg_replace('/[^a-zA-Z0-9_]/', '', $table);
$index = preg_replace('/[^a-zA-Z0-9_]/', '', $index);
if ($table === '' || $index === '') {
return;
}
$indexSql = $db->escape_string($index);
$result = $db->query("SHOW INDEX FROM `$table` WHERE Key_name = '$indexSql'");
if ($result !== false && $result->num_rows > 0) {
return;
}
$db->query("ALTER TABLE `$table` ADD UNIQUE KEY `$index` ($columns)");
}
}