feat: DB업그레이드에서 기존 상태 검사와 확인 이력 등록 지원

미기록 항목의 실제 실행 조건을 읽기 전용으로 확인해 실행 불필요 상태를 구분한다. 명시적 요청에서 선행 항목부터 확인 이력을 등록하고 등록 버튼을 왼쪽에 배치한다.

부분 적용·게시판별 컬럼·조회 오류와 순차 등록·실패 이력·체크섬·잠금 보호 검증을 추가한다.
This commit is contained in:
whitedot
2026-09-09 07:58:05 +00:00
parent 1da82df500
commit 545e6a504d
6 changed files with 262 additions and 11 deletions
+24 -4
View File
@@ -13,6 +13,7 @@ if ($is_admin != 'super') {
$is_execute = isset($_SERVER['REQUEST_METHOD']) && $_SERVER['REQUEST_METHOD'] === 'POST';
$migration_result = array('success' => true, 'applied' => 0, 'skipped' => 0, 'errors' => array());
$shop_install_result = null;
$action = '';
if ($is_execute) {
check_demo();
@@ -20,6 +21,8 @@ if ($is_execute) {
$action = isset($_POST['action']) ? trim($_POST['action']) : 'migrate';
if ($action === 'install_shop') {
$shop_install_result = g5_shop_install_run();
} elseif ($action === 'record_existing') {
$migration_result = g5_migration_run('', true);
} else {
$migration_id = isset($_POST['migration_id']) ? trim($_POST['migration_id']) : '';
$migration_result = g5_migration_run($migration_id);
@@ -29,8 +32,11 @@ if ($is_execute) {
$migration_statuses = g5_migration_status();
$pending_count = 0;
$unrecorded_count = 0;
foreach ($migration_statuses as $migration_status) {
if (isset($migration_status['error']) || $migration_status['status'] !== 'success' || $migration_status['checksum_changed']) {
if (isset($migration_status['status']) && $migration_status['status'] === 'unrecorded') {
$unrecorded_count++;
} elseif (isset($migration_status['error']) || $migration_status['status'] !== 'success' || $migration_status['checksum_changed']) {
$pending_count++;
}
}
@@ -40,9 +46,11 @@ if ($shop_install_result !== null && $shop_install_result['success']) {
} elseif ($shop_install_result !== null) {
$db_upgrade_msg = '쇼핑몰 설치에 실패했습니다. 오류 내용을 확인해 주십시오.';
} elseif (!$is_execute) {
$db_upgrade_msg = $pending_count ? '적용하지 않은 DB 마이그레이션이 있습니다. 아래 내용을 확인한 뒤 실행해 주십시오.' : 'DB 마이그레이션이 모두 적용되어 있습니다.';
$db_upgrade_msg = $pending_count ? '확인·실행이 필요한 DB 마이그레이션이 있습니다. 아래 내용을 확인해 주십시오.' : ($unrecorded_count ? '현재 실행할 변경은 없지만 기존 상태 확인 이력이 등록되지 않았습니다.' : 'DB 마이그레이션이 모두 적용되어 있습니다.');
} elseif (!$migration_result['success']) {
$db_upgrade_msg = 'DB 마이그레이션에 실패했습니다. 오류 내용을 확인해 주십시오.';
} elseif ($action === 'record_existing') {
$db_upgrade_msg = '기존 상태 확인 이력 ' . (int) $migration_result['skipped'] . '건을 등록했습니다. 선행 항목에 실행이 필요하거나 실패 이력이 있으면 등록을 중단합니다.';
} elseif ($migration_result['applied'] || $migration_result['skipped']) {
$db_upgrade_msg = 'DB 마이그레이션이 완료되었습니다.';
} else {
@@ -56,6 +64,10 @@ $dbupgrade_token = get_admin_token();
<div class="local_desc01 local_desc">
<p><?php echo $db_upgrade_msg; ?></p>
<?php if ($unrecorded_count) { ?>
<p>실행 불필요 <?php echo (int) $unrecorded_count; ?>건: 현재 실행 조건상 건너뛰는 항목입니다. 과거 실행 성공을 의미하지 않으며, 선행 작업 후에는 결과가 달라질 수 있습니다. 기존 상태 등록은 처음부터 연속해서 실행 불필요한 항목만 기록합니다.</p>
<?php } ?>
<p>대기 항목에는 데이터 보정 등 조회만으로 완료를 판단할 수 없는 작업도 포함됩니다. 조회 시 DB 변경이나 이력 등록은 수행하지 않습니다.</p>
</div>
<?php if ($shop_install_result !== null && !$shop_install_result['success']) { ?>
@@ -71,6 +83,13 @@ $dbupgrade_token = get_admin_token();
<?php } ?>
<div class="dbupgrade_all_actions">
<?php if ($unrecorded_count) { ?>
<form class="dbupgrade_existing_action" method="post" action="<?php echo G5_ADMIN_URL; ?>/dbupgrade.php">
<input type="hidden" name="token" value="<?php echo $dbupgrade_token; ?>">
<input type="hidden" name="action" value="record_existing">
<button type="submit" class="btn_submit">기존 상태 확인 이력 등록</button>
</form>
<?php } ?>
<?php if (!defined('G5_USE_SHOP') || !G5_USE_SHOP) { ?>
<form method="post" action="<?php echo G5_ADMIN_URL; ?>/dbupgrade.php" onsubmit="return confirm('쇼핑몰을 설치하시겠습니까? 설치 전 데이터베이스와 data/dbconfig.php 백업을 권장합니다.');">
<input type="hidden" name="token" value="<?php echo $dbupgrade_token; ?>">
@@ -105,8 +124,8 @@ $dbupgrade_token = get_admin_token();
<?php
$current_migration_status = isset($migration_status['status']) ? $migration_status['status'] : 'error';
$migration_succeeded = $current_migration_status === 'success' && !$migration_status['checksum_changed'];
$migration_can_run = $current_migration_status !== 'error' && $current_migration_status !== 'success' && $previous_migrations_ready;
$migration_button_title = $current_migration_status === 'success' ? '이미 성공한 마이그레이션입니다.' : ($previous_migrations_ready ? '' : '선행 마이그레이션을 먼저 실행해야 합니다.');
$migration_can_run = in_array($current_migration_status, array('pending', 'failed'), true) && $previous_migrations_ready;
$migration_button_title = $current_migration_status === 'unrecorded' ? '기존 상태 확인 이력을 등록해 주십시오.' : ($current_migration_status === 'success' ? '이미 성공한 마이그레이션입니다.' : ($previous_migrations_ready ? '' : '선행 마이그레이션을 먼저 실행해야 합니다.'));
?>
<tr>
<?php if (isset($migration_status['error'])) { ?>
@@ -131,6 +150,7 @@ $dbupgrade_token = get_admin_token();
<style>
.dbupgrade_all_actions {display:flex;width:100%;margin:0 0 20px;justify-content:flex-end;gap:5px}
.dbupgrade_all_actions form {margin:0}
.dbupgrade_all_actions .dbupgrade_existing_action {margin-right:auto}
.dbupgrade_all_actions .btn_submit {display:inline-block;float:none;position:static;width:auto;height:30px;margin:0;padding:0 15px;border:0}
.dbupgrade_sort {width:100%; border:0; background:transparent; color:inherit; font:inherit; cursor:pointer}
#dbupgrade_migration_table td.dbupgrade_description {text-align:left}
+95
View File
@@ -0,0 +1,95 @@
#!/usr/bin/env php
<?php
// 실제 DB 대신 SQL 응답을 제공해 이력 등록의 순서와 쓰기 범위를 검증한다.
if (PHP_SAPI !== 'cli') exit("CLI에서만 실행할 수 있습니다.\n");
define('_GNUBOARD_', true);
define('G5_TABLE_PREFIX', 'inspection_test_');
define('G5_TIME_YMDHIS', '2026-09-07 00:00:00');
define('G5_PATH', sys_get_temp_dir() . '/g5-existing-' . uniqid());
mkdir(G5_PATH);
mkdir(G5_PATH . '/migrations');
$g5 = array('config_table' => 'inspection_test_config');
$records = array();
$queries = array();
$columns = array('present');
$lock_available = true;
$history_readable = true;
class MigrationInspectionRows
{
public $rows;
public function __construct($rows) { $this->rows = $rows; }
}
function sql_real_escape_string($value) { return addslashes($value); }
function sql_error_info() { return '검사용 오류'; }
function get_microtime() { return microtime(true); }
function sql_fetch_array($result) { return array_shift($result->rows); }
function sql_num_rows($result) { return count($result->rows); }
function sql_fetch($sql, $error = true) {
$result = sql_query($sql, $error);
return $result ? sql_fetch_array($result) : false;
}
function sql_query($sql, $error = true) {
global $queries, $records, $columns, $lock_available, $history_readable;
$queries[] = $sql;
if (strpos($sql, 'SHOW TABLES') === 0) return new MigrationInspectionRows(array(array('table' => 'inspection_test_migrations')));
if (strpos($sql, 'SHOW COLUMNS') === 0) {
$rows = array();
foreach ($columns as $column) $rows[] = array('Field' => $column);
return new MigrationInspectionRows($rows);
}
if (strpos($sql, 'SELECT migration_id') === 0) return $history_readable ? new MigrationInspectionRows(array_values($records)) : false;
if (strpos($sql, 'SELECT COUNT(*)') === 0) return new MigrationInspectionRows(array(array('total' => count($records))));
if (strpos($sql, 'SELECT GET_LOCK') === 0) return new MigrationInspectionRows(array(array('acquired' => $lock_available ? 1 : 0)));
if (strpos($sql, 'SELECT RELEASE_LOCK') === 0 || strpos($sql, 'CREATE TABLE IF NOT EXISTS `inspection_test_migrations`') === 0) return true;
if (strpos($sql, 'INSERT INTO `inspection_test_migrations`') === 0) {
preg_match("/migration_id = '([^']+)'/", $sql, $id);
preg_match("/checksum = '([^']+)'/", $sql, $checksum);
$records[$id[1]] = array('migration_id' => $id[1], 'checksum' => $checksum[1], 'status' => 'success', 'error_message' => '', 'applied_at' => G5_TIME_YMDHIS);
return true;
}
fwrite(STDERR, '[실패] 허용하지 않은 SQL: ' . $sql . "\n");
exit(1);
}
function inspection_check($condition, $message) {
if (!$condition) { fwrite(STDERR, '[실패] ' . $message . "\n"); exit(1); }
}
function inspection_cleanup() {
foreach (glob(G5_PATH . '/migrations/*') as $file) unlink($file);
rmdir(G5_PATH . '/migrations');
rmdir(G5_PATH);
}
register_shutdown_function('inspection_cleanup');
require dirname(dirname(__FILE__)) . '/lib/migration.lib.php';
$ids = array('20200101_001_existing', '20200102_001_missing', '20200103_001_later');
foreach ($ids as $index => $id) {
$column = $index === 1 ? 'missing' : 'present';
file_put_contents(G5_PATH . '/migrations/' . $id . '.sql', "-- @if-column-missing {{config_table}} $column\nALTER TABLE `{{config_table}}` ADD `$column` int;\n");
}
$statuses = g5_migration_status();
inspection_check($statuses[0]['status'] === 'unrecorded' && $statuses[1]['status'] === 'pending' && $statuses[2]['status'] === 'unrecorded', '기존·미적용 상태 구분');
foreach ($queries as $query) inspection_check(preg_match('/^(SHOW|SELECT) /', $query), '상태 조회에서 DB 쓰기 발생');
$result = g5_migration_run('', true);
inspection_check($result['success'] && $result['skipped'] === 1 && count($records) === 1 && isset($records[$ids[0]]), '미적용 선행 항목을 넘어 등록함');
$result = g5_migration_run('', true);
inspection_check($result['skipped'] === 0 && count($records) === 1, '반복 등록');
$statuses = g5_migration_status();
inspection_check($statuses[0]['status'] === 'success' && $statuses[2]['status'] === 'unrecorded', '부분 이력 이후 검사 누락');
$records[$ids[0]]['checksum'] = 'changed';
$result = g5_migration_run('', true);
inspection_check(!$result['success'] && count($records) === 1, '체크섬 변경 무시');
$records[$ids[0]]['status'] = 'failed';
$columns[] = 'missing';
$result = g5_migration_run('', true);
inspection_check($result['skipped'] === 0 && $records[$ids[0]]['status'] === 'failed', '실패 이력을 덮어씀');
$records = array();
$lock_available = false;
$result = g5_migration_run('', true);
inspection_check(!$result['success'] && !$records, '잠금 실패 후 등록');
$lock_available = true;
$result = g5_migration_run('', true);
inspection_check($result['success'] && $result['skipped'] === 3 && count($records) === 3, '재검사 후 기존 항목 등록 실패');
$history_readable = false;
$result = g5_migration_run('', true);
inspection_check(!$result['success'] && count($records) === 3, '이력 조회 실패 후 덮어씀');
echo "기존 상태 조회·순차 이력 등록·재검사·실패/체크섬/잠금 보호 검사가 통과했습니다.\n";
+31 -2
View File
@@ -96,7 +96,7 @@ if (!sql_query("CREATE TABLE `{$test_content_table}` (
g5_migration_test_fail(sql_error_info());
}
if (!sql_query("CREATE TABLE `{$test_board_table}` (`bo_table` varchar(20) NOT NULL DEFAULT '') ENGINE=MyISAM DEFAULT CHARSET=utf8", false) ||
!sql_query("CREATE TABLE `{$test_write_table}` (`wr_id` int(11) NOT NULL AUTO_INCREMENT, `wr_seo_title` varchar(200) NOT NULL DEFAULT '', PRIMARY KEY (`wr_id`)) ENGINE=MyISAM DEFAULT CHARSET=utf8", false) ||
!sql_query("CREATE TABLE `{$test_write_table}` (`wr_id` int(11) NOT NULL AUTO_INCREMENT, `wr_content` text NOT NULL, `wr_seo_title` varchar(200) NOT NULL DEFAULT '', PRIMARY KEY (`wr_id`)) ENGINE=MyISAM DEFAULT CHARSET=utf8", false) ||
!sql_query("INSERT INTO `{$test_board_table}` SET bo_table = 'sample'", false)) {
g5_migration_test_fail(sql_error_info());
}
@@ -147,9 +147,38 @@ foreach ($isolated_keys as $index => $key) {
}
// 복합 ALTER의 첫 컬럼만 없는 역방향 부분 적용 상태에서도 과거 마이그레이션이 중단되지 않아야 한다.
$inspection_cache = array();
$social_migration = g5_migration_parse_file(G5_PATH . '/migrations/20180330_001_social_login.sql');
if (g5_migration_needs_no_execution($social_migration, $inspection_cache)) {
g5_migration_test_fail('부분 적용된 소셜 스키마를 실행 불필요로 판단했습니다.');
}
g5_migration_test_execute(g5_migration_parse_file(G5_PATH . '/migrations/20140331_001_shop_cart_shipping.sql'));
g5_migration_test_execute(g5_migration_parse_file(G5_PATH . '/migrations/20180330_001_social_login.sql'));
g5_migration_test_execute(g5_migration_parse_file(G5_PATH . '/migrations/20260723_001_inicis_pro.sql'));
$inspection_cache = array();
if (!g5_migration_needs_no_execution($social_migration, $inspection_cache)) {
g5_migration_test_fail('이미 적용된 소셜 스키마를 실행 불필요로 판단하지 못했습니다.');
}
$write_migration = g5_migration_parse_file(G5_PATH . '/migrations/20191202_002_board_write_seo.sql');
if (!g5_migration_needs_no_execution($write_migration, $inspection_cache)) {
g5_migration_test_fail('기존 게시판 SEO 컬럼을 확인하지 못했습니다.');
}
sql_query("ALTER TABLE `{$test_write_table}` DROP COLUMN wr_seo_title", false);
$inspection_cache = array();
if (g5_migration_needs_no_execution($write_migration, $inspection_cache)) {
g5_migration_test_fail('게시판 SEO 컬럼 누락을 실행 불필요로 판단했습니다.');
}
g5_migration_test_execute($write_migration);
$inspection_cache = array();
$unconditional_migration = g5_migration_parse_file(G5_PATH . '/migrations/20260904_002_reconcile_partial_schema.sql');
$backfill_migration = g5_migration_parse_file(G5_PATH . '/migrations/20260904_003_backfill_legacy_data.sql');
if (g5_migration_needs_no_execution($unconditional_migration, $inspection_cache) || g5_migration_needs_no_execution($backfill_migration, $inspection_cache)) {
g5_migration_test_fail('조건 없는 스키마 변경 또는 데이터 보정을 완료로 추정했습니다.');
}
$absent_table = $test_absent_prefix . 'inspection';
if (g5_migration_inspect_object('column', $absent_table, 'missing', $inspection_cache) !== null) {
g5_migration_test_fail('조회 오류를 객체 없음과 구분하지 못했습니다.');
}
if (!g5_migration_column_exists($test_cart_table, 'it_sc_type') ||
!g5_migration_column_exists($test_config_table, 'cf_social_login_use') ||
!g5_migration_column_exists($test_config_table, 'cf_member_img_size') ||
@@ -217,4 +246,4 @@ if (g5_migration_validate_target_dependencies($migrations, $dependency_records,
g5_migration_test_fail('누락된 선행 마이그레이션을 차단하지 못했습니다.');
}
echo "실행 순서·선행 조건·부분 적용 스키마·기존 데이터 보정 검사가 통과했습니다.\n";
echo "기존 상태 읽기 전용 검사·실행 순서·선행 조건·부분 적용 스키마·기존 데이터 보정 검사가 통과했습니다.\n";
+4 -4
View File
@@ -34,13 +34,13 @@ if ($command === 'status') {
exit(0);
}
if ($command !== 'migrate') {
fwrite(STDERR, "사용법: php bin/db-migrate.php status | migrate [마이그레이션_ID]\n");
if ($command !== 'migrate' && $command !== 'record-existing') {
fwrite(STDERR, "사용법: php bin/db-migrate.php status | migrate [마이그레이션_ID] | record-existing\n");
exit(2);
}
$target_id = isset($argv[2]) && $argv[2] !== '--all' ? $argv[2] : '';
$result = g5_migration_run($target_id);
$target_id = $command === 'migrate' && isset($argv[2]) && $argv[2] !== '--all' ? $argv[2] : '';
$result = g5_migration_run($target_id, $command === 'record-existing');
if (!$result['success']) {
foreach ($result['errors'] as $error) {
fwrite(STDERR, '[실패] ' . $error . "\n");
+8
View File
@@ -20,16 +20,22 @@ CLI를 권장한다.
```bash
php bin/db-migrate.php status
php bin/db-migrate.php record-existing
php bin/db-migrate.php migrate
php bin/db-migrate.php migrate 20260416_001_member_auto_login
php bin/check-migration-schema.php
php bin/check-migration-runner.php
php bin/check-migration-existing.php
php bin/check-shop-install.php
php bin/check-runtime-ddl.php
```
웹에서는 최고관리자가 **환경설정 → DB업그레이드**에서 전체 또는 특정 마이그레이션을 명시적으로 실행할 수 있다. 성공한 마이그레이션의 개별 실행 버튼은 비활성화되며, 전체 실행에서도 성공 이력과 체크섬이 일치하는 항목은 건너뛴다. 화면 조회만으로는 DDL이 실행되지 않는다.
기존 설치본처럼 이력이 비어 있으면 상태 조회 시 실제 실행 조건을 읽기 전용으로 검사한다. 일부 이력이 있는 경우에도 미기록 항목은 검사한다. 현재 조건에서 모든 SQL을 건너뛰는 항목은 `실행 불필요 (미기록)`로 표시하며 개별 실행 버튼을 비활성화한다. 이는 과거 실행 성공이나 컬럼 정의 전체의 일치를 보증하는 표시가 아니다. 게시판별 컬럼도 검사하며, 조건 없는 DDL·데이터 보정·검사 오류는 대기로 남긴다. 조회 요청 안에서는 메타데이터를 공유해 중복 조회를 줄이고, 다음 요청에서는 다시 검사하므로 외부 변경이나 선행 작업 후 상태가 반영된다.
`기존 상태 확인 이력 등록` 버튼 또는 CLI `record-existing`은 실행 잠금 아래 다시 검사하고, 처음부터 연속해서 실행 불필요한 미기록 항목만 성공 이력으로 등록한다. 등록 시각은 확인한 시각이며 실제 과거 적용 시각이 아니다. 이 동작은 필요하면 이력 테이블을 생성하지만, 마이그레이션 SQL 자체는 실행하지 않는다. 변경이 필요한 항목이나 실패 이력을 만나면 중단하며, 성공 이력의 체크섬 불일치는 오류로 처리한다. 화면 진입만으로 등록하지 않으며 웹 등록은 최고관리자 권한과 POST 토큰 검증을 거친다. 전체 마이그레이션 실행도 기존처럼 실행 조건을 다시 확인하고 건너뛴 항목의 이력을 함께 기록한다.
전체 실행은 파일명 오름차순, 즉 오래된 마이그레이션부터 처리하며 하나가 실패하면 이후 실행을 중단한다. 개별 실행도 대상보다 앞선 모든 마이그레이션의 성공 이력과 체크섬이 유효해야 한다. 선행 항목이 누락·실패·변경된 경우 대상만 건너뛰기 성공으로 기록하지 않고 오류로 차단한다.
실행기는 `g5_migrations` 테이블에 마이그레이션 ID, 체크섬, 상태, 오류, 실행시간과 적용시각을 기록한다. 프로세스 간 잠금을 사용해 동시 실행을 막고, 실패하면 이후 마이그레이션을 중단한다. 적용이 끝난 파일의 체크섬이 바뀌면 오류로 처리한다.
@@ -38,6 +44,8 @@ php bin/check-runtime-ddl.php
`check-migration-schema.php`는 마이그레이션의 테이블·컬럼·인덱스가 신규 설치 SQL에 포함되는지와 최종 컬럼 정의가 일치하는지 검사한다. `check-migration-runner.php`는 격리된 임시 테이블로 부분 적용 스키마, 쇼핑몰 미설치 건너뛰기, 기존 데이터 보정을 검사한다. 마이그레이션을 추가하거나 설치 SQL을 변경한 뒤 두 검사를 반드시 실행한다.
`check-migration-existing.php`는 실제 DB 대신 SQL 응답을 제공해 조회 시 쓰기 금지, 순차 이력 등록, 재검사, 실패 이력·체크섬·잠금 보호를 검사한다. 기존 상태 검사나 이력 등록 로직을 변경한 뒤 실행한다.
쇼핑몰을 설치하지 않은 사이트에서는 DB 업그레이드 화면의 `쇼핑몰 설치` 버튼으로 최신 쇼핑몰 스키마를 추가할 수 있다. 같은 접두어의 쇼핑몰 테이블이 하나라도 있으면 기존 데이터 훼손을 막기 위해 설치를 중단하며, 기존 `data/dbconfig.php`의 DB 접속 정보와 토큰 키는 유지하고 쇼핑몰 설정만 추가한다. `php bin/check-shop-install.php`로 격리된 접두어에서 후설치, 설정 보존과 중복 실행 차단을 검사할 수 있다.
`check-runtime-ddl.php`는 관리자·게시판·라이브러리·쇼핑몰 등 일반 실행 경로에 DDL 문이 다시 추가되지 않았는지 검사한다. 테이블이나 컬럼 변경은 이 검사를 우회하지 말고 버전형 마이그레이션으로 작성한다.
+100 -1
View File
@@ -322,11 +322,84 @@ function g5_migration_get_records()
return $records;
}
// 검사 실패는 객체 없음과 구분한다. 잘못된 건너뛰기 이력을 만들지 않는다.
function g5_migration_inspect_object($object, $table, $name, &$cache)
{
$key = $object . ':' . $table;
if (!array_key_exists($key, $cache)) {
$quoted = '`' . str_replace('`', '``', $table) . '`';
if ($object === 'table') {
$sql = "SHOW TABLES LIKE '" . sql_real_escape_string($table) . "'";
} elseif ($object === 'column') {
$sql = 'SHOW COLUMNS FROM ' . $quoted;
} else {
$sql = 'SHOW INDEX FROM ' . $quoted;
}
$result = sql_query($sql, false);
$cache[$key] = $result ? array() : null;
if ($result) {
while ($row = sql_fetch_array($result)) {
$value = $object === 'table' ? reset($row) : $row[$object === 'column' ? 'Field' : 'Key_name'];
$cache[$key][$value] = true;
}
}
}
return $cache[$key] === null ? null : isset($cache[$key][$object === 'table' ? $table : $name]);
}
// SQL을 실행하지 않고, 실행기의 모든 문이 건너뛰어질 때만 true를 반환한다.
function g5_migration_needs_no_execution($migration, &$cache)
{
global $g5;
if (isset($migration['error'])) {
return false;
}
if ($migration['skip_table'] && $migration['skip_column'] &&
g5_migration_inspect_object('column', $migration['skip_table'], $migration['skip_column'], $cache) === true) {
return true;
}
foreach ($migration['statements'] as $statement) {
$skip = false;
foreach ($statement['conditions'] as $condition) {
$exists = g5_migration_inspect_object($condition['object'], $condition['table'], $condition['name'], $cache);
if ($exists === null) {
return false;
}
if (($condition['state'] === 'exists') !== $exists) {
$skip = true;
break;
}
}
if ($skip) {
continue;
}
if (!$statement['foreach_write_table']) {
// 데이터 보정 및 조건 없는 DDL은 완료로 추정하지 않는다.
return false;
}
$result = sql_query("SELECT bo_table FROM `" . str_replace('`', '``', $g5['board_table']) . "`", false);
if (!$result) {
return false;
}
while ($board = sql_fetch_array($result)) {
$table = $g5['write_prefix'] . $board['bo_table'];
if (g5_migration_inspect_object('table', $table, '', $cache) !== true ||
$statement['foreach_write_column'] === '' ||
g5_migration_inspect_object('column', $table, $statement['foreach_write_column'], $cache) !== true) {
return false;
}
}
}
return true;
}
function g5_migration_status()
{
$migrations = g5_migration_discover();
$records = g5_migration_get_records();
$status = array();
$inspection_cache = array();
foreach ($migrations as $migration) {
if (isset($migration['error'])) {
@@ -336,6 +409,9 @@ function g5_migration_status()
$record = isset($records[$migration['id']]) ? $records[$migration['id']] : null;
$migration['status'] = $record ? $record['status'] : 'pending';
if (!$record && g5_migration_needs_no_execution($migration, $inspection_cache)) {
$migration['status'] = 'unrecorded';
}
$migration['checksum_changed'] = $record && $record['status'] === 'success' && $record['checksum'] !== $migration['checksum'];
$migration['error_message'] = $record ? $record['error_message'] : '';
$migration['applied_at'] = $record ? $record['applied_at'] : '';
@@ -353,6 +429,7 @@ function g5_migration_status_label($status, $checksum_changed = false)
$labels = array(
'pending' => '대기',
'unrecorded' => '실행 불필요 (미기록)',
'success' => '성공',
'failed' => '실패'
);
@@ -429,7 +506,7 @@ function g5_migration_save_record($migration, $status, $error_message, $executio
return (bool) sql_query($sql, false);
}
function g5_migration_run($target_id = '')
function g5_migration_run($target_id = '', $record_existing = false)
{
$result = array('success' => false, 'applied' => 0, 'skipped' => 0, 'errors' => array());
@@ -449,6 +526,15 @@ function g5_migration_run($target_id = '')
$records = g5_migration_get_records();
$target_found = $target_id === '';
if ($record_existing) {
$history = sql_fetch('SELECT COUNT(*) AS total FROM `' . g5_migration_table_name() . '`', false);
if (!$history || !isset($history['total']) || (int) $history['total'] !== count($records)) {
$result['errors'][] = '기존 마이그레이션 이력을 확인하지 못했습니다.';
sql_query("SELECT RELEASE_LOCK('{$lock_name}')", false);
return $result;
}
}
if ($target_id !== '') {
$dependency_error = g5_migration_validate_target_dependencies($migrations, $records, $target_id);
if ($dependency_error !== '') {
@@ -479,6 +565,19 @@ function g5_migration_run($target_id = '')
}
$started_at = get_microtime();
if ($record_existing) {
$inspection_cache = array();
// 앞선 변경이 후속 실행 조건을 바꿀 수 있으므로 연속된 항목만 등록한다.
if ($record || !g5_migration_needs_no_execution($migration, $inspection_cache)) {
break;
}
if (!g5_migration_save_record($migration, 'success', '', 0)) {
$result['errors'][] = $migration['id'] . ' 기존 상태 확인 이력을 저장하지 못했습니다: ' . sql_error_info();
break;
}
$result['skipped']++;
continue;
}
$skip = $migration['skip_table'] && $migration['skip_column'] && g5_migration_column_exists($migration['skip_table'], $migration['skip_column']);
$migration_error = '';
$executed = false;