Commit 93f54f7a authored by platyhouse's avatar platyhouse

이제 모든 staged 변경 사항을 확인했습니다. 커밋 메세지를 작성하겠습니다.

# MySQL 도구 기능 개선 및 ptyMysqlOverwrite 스크립트 추가

## ptyMysqlOverwrite 추가

### ptyMysqlOverwrite.php
- 서로 다른 MySQL 서버/DB 간 테이블 복사 도구 신규 구현
- ptyMysqlBackup.php와 ptyMysqlRestore.php를 내부적으로 활용
- 임시 디렉토리에 백업 → 테이블명 변환(sed) → 복원 순서로 동작
- 소스/대상 테이블명이 다를 경우 SQL 내 테이블명 자동 변환
- --force 옵션으로 확인 없이 실행 가능
- 프로세스 종료 시 임시 파일 자동 정리

## ptyMysqlRestore 기능 개선

### ptyMysqlRestore.php
- --table 옵션 추가: 다른 테이블명으로 복원 가능
- --database 옵션을 --db로 변경 (일관성)
- transformTableName() 함수 추가: sed로 SQL 내 테이블명 스트리밍 변환
- 원본과 다른 테이블명 복원 시 표시 개선 (← 원본DB.원본테이블)
- 임시 변환 파일 자동 정리 로직 추가

## ptyElasticUploadFromMysql 분석기 설정 개선

### ptyElasticUploadFromMysql.php
- MySQL comment의 elastic.analyzer 설정 시 usedAnalyzers에 수집하도록 수정
- getAnalyzerSettings() 메소드 추가: 사용된 analyzer별 설정 자동 생성
- nori 기반 analyzer 자동 인식 및 tokenizer 설정 생성
- 인덱스 생성 시 analysis 설정 동적 구성으로 변경

## 문서 업데이트

### CLAUDE.md
- ptyElasticUploadFromMysql 상세 문서 추가 (사용법, 파일 구조, 타입 변환 규칙)
- MySQL comment에서 elastic.* 설정 파싱 규칙 문서화
- Nori analyzer 자동 설정 동작 방식 설명
- ptyMysql* 스크립트 작성 규칙 추가
- ptyMysqlInfo, ptyMysqlBackup, ptyMysqlRestore, ptyMysqlOverwrite 사용법 문서화
parent e202048b
......@@ -233,3 +233,390 @@ ptyCronBuild 'ptyRun "cd /home/redmine/git; php git_update.php" --max-run-time=6
- `ptyRun`의 기본값(`--run-duplicate=false`)으로 중복 실행이 자동 차단됨
- `--max-run-time`으로 최대 실행 시간 설정 시, 해당 시간 초과 시 프로세스를 강제 종료하고 재실행
- 예: `--daily-10min``--max-run-time=600`을 함께 사용하면 10분마다 크론이 실행되지만, 이전 실행이 아직 진행 중이면 스킵하고, 10분 이상 실행 중이면 강제 종료 후 재실행
### ptyElasticUploadFromMysql - MySQL → Elasticsearch 업로드
MySQL 테이블을 Elasticsearch 인덱스로 업로드하는 도구입니다.
#### 기본 사용법
```bash
./ptyElasticUploadFromMysql <테이블명> <인덱스명> [옵션]
```
#### CLI 옵션
| 옵션 | 설명 | 기본값 |
|------|------|--------|
| `--mysql=섹션명` | MySQL INI 섹션 | `default` |
| `--elastic=섹션명` | Elasticsearch INI 섹션 | `default` |
| `--batch=N` | 배치 크기 | `100` |
| `--recreate` | 기존 인덱스 삭제 후 재생성 | - |
| `--primary=필드명` | Primary Key 필드명 | `id` |
| `--where='조건'` | WHERE 절 추가 | - |
| `--help` | 도움말 출력 | - |
#### 사용 예시
```bash
# 기본 사용
./ptyElasticUploadFromMysql new_law_items law_items
# 인덱스 재생성 + 배치 크기 500
./ptyElasticUploadFromMysql new_law_items law_items --batch=500 --recreate
# 프로덕션 환경
./ptyElasticUploadFromMysql users users --mysql=production --elastic=production
# 조건부 업로드
./ptyElasticUploadFromMysql users users --where='enabled=1'
```
#### 파일 구조
```
ptyElasticUploadFromMysql.php
├── ConfigLoader (클래스)
│ ├── loadMysqlConfig($section) # ~/.ptyMysqlConfig.ini 로드
│ └── loadElasticConfig($section) # ~/.ptyElasticConfig.ini 로드
├── MySQLToElastic (클래스)
│ ├── 속성
│ │ ├── $pdo # PDO 연결
│ │ ├── $elastic # Elastic 클라이언트
│ │ ├── $excludedColumns # elastic.register=0 컬럼들
│ │ └── $usedAnalyzers # 사용된 analyzer들
│ │
│ ├── 초기화
│ │ ├── initMySQLConnection() # PDO 연결 (버퍼링 비활성화)
│ │ └── initElasticConnection() # Elastic 클라이언트 생성
│ │
│ ├── 매핑 생성
│ │ ├── getTableStructure() # SHOW FULL COLUMNS로 구조 조회
│ │ ├── parseElasticOptions() # comment에서 elastic.* 파싱
│ │ ├── convertMySQLTypeToElastic() # MySQL→ES 타입 변환
│ │ └── getAnalyzerSettings() # analyzer/tokenizer 설정 생성
│ │
│ ├── 인덱스 관리
│ │ └── createIndex($recreate) # 인덱스 생성/재생성
│ │
│ └── 데이터 업로드
│ ├── uploadData($whereClause) # 메인 업로드 루프
│ └── bulkInsert($rows) # Bulk API 호출
└── 메인 실행 코드 (CLI 파싱 및 실행)
```
#### MySQL 타입 → Elasticsearch 타입 변환 규칙
| MySQL 타입 | Elasticsearch 타입 |
|------------|-------------------|
| `tinyint`, `smallint`, `mediumint`, `int` | `integer` |
| `bigint` | `long` |
| `float`, `double`, `decimal` | `float` |
| `datetime`, `timestamp`, `date` | `date` |
| `tinyint(1)` | `boolean` |
| `*text` (text, longtext 등) | `text` |
| `varchar(<=255)`, `char(<=255)` | `keyword` |
| `varchar(>255)`, `char(>255)` | `text` |
| `enum` | `keyword` |
| `json` | `object` |
| 기타 | `keyword` |
#### MySQL 컬럼 COMMENT에서 elastic.* 설정
`SHOW FULL COLUMNS FROM` 명령으로 컬럼 comment를 읽어 다음 설정을 파싱합니다:
| 설정 | 설명 | 예시 |
|------|------|------|
| `elastic.register=0` | 인덱스에서 제외 (매핑 + 데이터 모두) | `내부데이터, elastic.register=0` |
| `elastic.type=text\|keyword` | ES 타입 강제 지정 | `elastic.type=text` |
| `elastic.analyzer=분석기명` | analyzer 설정 | `elastic.analyzer=lw_nori_analyzer` |
**예시 MySQL DDL:**
```sql
CREATE TABLE law_items (
id INT PRIMARY KEY,
title VARCHAR(500) COMMENT '법령.법령명, elastic.type=text, elastic.analyzer=lw_nori_analyzer',
contents LONGTEXT COMMENT '본문, elastic.type=text, elastic.analyzer=lw_nori_analyzer',
internal_memo TEXT COMMENT '내부메모, elastic.register=0',
category_code VARCHAR(10) COMMENT '카테고리코드, elastic.type=keyword'
);
```
#### Nori analyzer 자동 설정
`elastic.analyzer``nori`가 포함된 이름 지정 시 자동 생성:
```json
{
"analysis": {
"analyzer": {
"lw_nori_analyzer": {
"type": "custom",
"tokenizer": "lw_nori_tokenizer",
"char_filter": ["html_strip"],
"filter": ["lowercase", "stop"]
}
},
"tokenizer": {
"lw_nori_tokenizer": {
"type": "nori_tokenizer",
"decompound_mode": "mixed",
"discard_punctuation": "true"
}
}
}
}
```
**tokenizer 이름 생성 규칙:**
- `lw_nori_analyzer``lw_nori_tokenizer` (analyzer → tokenizer 치환)
- `my_nori``my_nori_tokenizer` (_tokenizer 추가)
#### 문서 ID 생성 규칙
1. `serviceName`, `serviceId` 컬럼이 있으면: `{serviceName}_{serviceId}_{primaryKey}`
2. 없으면: `{primaryKey}` 값 사용
#### 메모리 최적화
- 메모리 제한: 2GB (`ini_set('memory_limit', '2048M')`)
- MySQL 버퍼링 비활성화 (`MYSQL_ATTR_USE_BUFFERED_QUERY => false`)
- 배치 처리 후 `gc_collect_cycles()` 호출
- 변수 즉시 `unset()` 처리
#### 에러 처리
| HTTP 코드 | 의미 | 해결 방법 |
|-----------|------|-----------|
| 413 | Request Entity Too Large | `--batch` 값 줄이기 |
| 400 | analyzer not configured | nori 플러그인 설치 확인 |
#### 수정 시 주의사항
- `parseElasticOptions()`: 정규식 패턴 수정 시 기존 comment 형식 호환성 유지
- `getAnalyzerSettings()`: nori 외 analyzer 추가 시 조건 분기 추가
- `bulkInsert()`: `$excludedColumns` 처리 순서 유지 (datetime 변환 후)
- `createIndex()`: 인덱스 존재 시에도 `getTableStructure()` 호출 필수 (excludedColumns 설정)
## ptyMysql* 스크립트
MySQL 관련 CLI 도구 모음입니다. 모든 스크립트는 `~/.ptyMysqlConfig.ini` 설정 파일을 사용합니다.
### 공통 라이브러리: ptyMysqlConfig
`ptyLibrary_PHP/mysql/ptyMysqlConfig.php` - MySQL 설정 로더 클래스
```php
// 설정 로드
$config = ptyMysqlConfig::load('production');
// 반환: ['host', 'username', 'password', 'database', 'charset']
// mysqli 연결 생성
$conn = ptyMysqlConfig::connect('production');
$connection = $conn['connection']; // mysqli 객체
$config = $conn['config']; // 설정 배열
// 사용 가능한 섹션 목록
$sections = ptyMysqlConfig::getSections();
// 설정 파일 예시 출력
echo ptyMysqlConfig::getConfigExample();
```
### ptyMysqlInfo - 서버 정보 조회
MySQL 서버의 상세 정보를 조회합니다.
```bash
./ptyMysqlInfo [--mysql=섹션명]
```
**출력 정보:**
- 연결 정보 (Host, User, Section, Charset)
- 서버 정보 (Version, 포트, 데이터 디렉토리, 문자셋, InnoDB 버퍼 풀 등)
- 서버 상태 (Uptime, 연결 수, 쿼리 수, 슬로우 쿼리 등)
- 데이터베이스 목록 (테이블 수, 크기)
- 사용자 목록 (Locked, PW Expired 상태)
- 현재 프로세스 (SHOW FULL PROCESSLIST)
- InnoDB 상태 (버퍼 풀 히트율)
- 주요 설정 변수
### ptyMysqlBackup - 테이블 백업
mysqldump를 사용하여 테이블을 백업합니다.
```bash
./ptyMysqlBackup <database> <table> [옵션]
./ptyMysqlBackup mydb users # mydb.users.sql 생성
./ptyMysqlBackup mydb * # mydb의 모든 테이블 백업
./ptyMysqlBackup * * # 전체 백업
./ptyMysqlBackup mydb users --output=/backup # 지정 경로에 저장
```
**옵션:**
| 옵션 | 설명 | 기본값 |
|------|------|--------|
| `--mysql=섹션명` | INI 파일 섹션 | `default` |
| `--output=경로` | 출력 디렉토리 | `.` |
| `--verbose` | 상세 로그 출력 | - |
**백업 파일:**
- 파일명 형식: `{database}.{table}.sql`
- 파일 헤더에 백업 시간, 실행 명령어 포함
- `x_` 접두사 DB는 자동 스킵
**mysqldump 옵션:**
```
--default-character-set=utf8mb4
--routines --events
--add-drop-table --add-drop-database
--complete-insert --extended-insert=TRUE
--single-transaction --ssl-mode=DISABLED
--max-allowed-packet=1G
```
### ptyMysqlRestore - 복원
SQL 파일을 MySQL에 복원합니다.
```bash
./ptyMysqlRestore <pattern> [옵션]
./ptyMysqlRestore "*" # 모든 sql 파일 복원
./ptyMysqlRestore "mydb.*.sql" # mydb의 모든 테이블 복원
./ptyMysqlRestore "mydb.users.sql" # 특정 파일 복원
./ptyMysqlRestore "*" --database=mydb_dev # 모든 파일을 mydb_dev DB에 복원
./ptyMysqlRestore "*" --force # 확인 없이 복원
```
**옵션:**
| 옵션 | 설명 | 기본값 |
|------|------|--------|
| `--mysql=섹션명` | INI 파일 섹션 | `default` |
| `--input=경로` | 입력 디렉토리 | `.` |
| `--database=DB명` | 복원 대상 DB (미지정시 파일명에서 추출) | - |
| `--force` | 확인 없이 바로 복원 | - |
| `--verbose` | 상세 로그 출력 | - |
**파일명 형식:** `{database}.{table}.sql`
**복원 전 확인:**
- 서버 정보 표시
- 복원 대상 파일 목록
- 테이블 상태 표시: `[EXISTS]`, `[NEW]`, `[OVERWRITE]`
- `--force` 없으면 사용자 확인 요청
### ptyMysqlOverwrite - 테이블 복사
서로 다른 MySQL 서버/DB 간에 테이블을 복사합니다. 내부적으로 ptyMysqlBackup + ptyMysqlRestore를 사용합니다.
```bash
./ptyMysqlOverwrite --src-mysql=api --dst-mysql=dev \
--src-db=lawtice_db --dst-db=lawtice_dev \
--src-table=new_law_items --dst-table=new_law_items
```
**필수 옵션:**
| 옵션 | 설명 |
|------|------|
| `--src-mysql=섹션명` | 소스 MySQL INI 섹션 |
| `--dst-mysql=섹션명` | 대상 MySQL INI 섹션 |
| `--src-db=DB명` | 소스 데이터베이스명 |
| `--dst-db=DB명` | 대상 데이터베이스명 |
| `--src-table=테이블명` | 소스 테이블명 |
| `--dst-table=테이블명` | 대상 테이블명 |
**선택 옵션:**
| 옵션 | 설명 |
|------|------|
| `--force` | 확인 없이 바로 실행 |
| `--verbose` | 상세 로그 출력 |
**동작 과정:**
1. **Step 1**: 소스 테이블을 임시 디렉토리에 백업 (`ptyMysqlBackup.php` 사용)
2. **Step 2**: 테이블명 변환 (src-table ≠ dst-table인 경우)
3. **Step 3**: 대상 서버에 복원 (`ptyMysqlRestore.php` 사용)
4. 임시 파일 정리
**Step 2 테이블명 변환 상세:**
mysqldump로 생성된 SQL 파일에는 원본 테이블명이 하드코딩되어 있습니다.
`--src-table``--dst-table`이 다를 경우, SQL 파일 내의 테이블명을 변경해야 합니다.
```sql
-- 변환 전 (src-table=users)
DROP TABLE IF EXISTS `users`;
CREATE TABLE `users` (...);
INSERT INTO `users` VALUES (...);
LOCK TABLES `users` WRITE;
-- 변환 후 (dst-table=users_backup)
DROP TABLE IF EXISTS `users_backup`;
CREATE TABLE `users_backup` (...);
INSERT INTO `users_backup` VALUES (...);
LOCK TABLES `users_backup` WRITE;
```
- `sed` 명령어로 스트리밍 처리 (대용량 파일도 메모리 문제 없음)
- 테이블명이 같으면 변환 과정 스킵
**사용 시나리오:**
```bash
# 프로덕션 → 개발 환경 동기화
./ptyMysqlOverwrite --src-mysql=production --dst-mysql=development \
--src-db=prod_db --dst-db=dev_db \
--src-table=users --dst-table=users --force
# 같은 서버에서 테이블 복제
./ptyMysqlOverwrite --src-mysql=default --dst-mysql=default \
--src-db=mydb --dst-db=mydb \
--src-table=users --dst-table=users_backup
```
### ptyMysql* 스크립트 작성 규칙
새로운 MySQL CLI 스크립트 생성 시 다음 패턴을 따릅니다:
```php
#!/usr/bin/env php
<?php
namespace platyFramework;
require_once __DIR__ . '/ptyLibrary_PHP/cli/ptyCliOptionParser.php';
require_once __DIR__ . '/ptyLibrary_PHP/mysql/ptyMysqlConfig.php';
// 인자 파싱
$parsed = ptyCliOptionParser::parse($argv);
$options = $parsed['options'];
$mysqlSection = $options['mysql'] ?? 'default';
// 도움말
if (isset($options['help'])) {
echo "사용법: ...\n";
echo ptyMysqlConfig::getConfigExample() . "\n";
exit(0);
}
try {
// MySQL 연결
$conn = ptyMysqlConfig::connect($mysqlSection);
$connection = $conn['connection'];
$config = $conn['config'];
// ... 비즈니스 로직 ...
mysqli_close($connection);
} catch (\Exception $e) {
echo "Error: " . $e->getMessage() . "\n";
exit(1);
}
```
**필수 옵션:**
- `--mysql=섹션명` - INI 파일 섹션 (기본값: default)
- `--help` - 도움말 출력
......@@ -285,6 +285,10 @@ class MySQLToElastic
// elastic.analyzer가 지정되었으면 analyzer 설정
if ($elasticOptions['analyzer'] !== null) {
$mapping[$field]['analyzer'] = $elasticOptions['analyzer'];
// 사용된 analyzer 수집
if (!in_array($elasticOptions['analyzer'], $this->usedAnalyzers)) {
$this->usedAnalyzers[] = $elasticOptions['analyzer'];
}
}
// text 타입인 경우 keyword 필드도 추가 (정렬, 집계용)
......@@ -310,6 +314,61 @@ class MySQLToElastic
return $mapping;
}
/**
* 사용된 analyzer들의 설정 생성
*
* lw_nori_analyzer 같은 nori 기반 analyzer는 자동으로 설정 생성
* 그 외 analyzer는 기본 설정 사용
*
* @return array ['analyzer' => [...], 'tokenizer' => [...]]
*/
private function getAnalyzerSettings()
{
$analyzerSettings = [];
$tokenizerSettings = [];
foreach ($this->usedAnalyzers as $analyzerName) {
// nori 기반 analyzer인지 확인 (이름에 nori가 포함되어 있으면)
if (stripos($analyzerName, 'nori') !== false) {
// nori tokenizer 이름 생성 (analyzer 이름에서 analyzer를 tokenizer로 변경)
$tokenizerName = str_ireplace('analyzer', 'tokenizer', $analyzerName);
if ($tokenizerName === $analyzerName) {
// analyzer가 이름에 없으면 _tokenizer 추가
$tokenizerName = $analyzerName . '_tokenizer';
}
$analyzerSettings[$analyzerName] = [
'type' => 'custom',
'tokenizer' => $tokenizerName,
'char_filter' => ['html_strip'],
'filter' => ['lowercase', 'stop']
];
$tokenizerSettings[$tokenizerName] = [
'type' => 'nori_tokenizer',
'decompound_mode' => 'mixed',
'discard_punctuation' => 'true'
];
$this->log->info("Nori analyzer 설정 추가: {$analyzerName} (tokenizer: {$tokenizerName})");
} else {
// 기본 analyzer 설정 (standard 기반)
$analyzerSettings[$analyzerName] = [
'type' => 'custom',
'tokenizer' => 'standard',
'filter' => ['lowercase']
];
$this->log->info("기본 analyzer 설정 추가: {$analyzerName}");
}
}
return [
'analyzer' => $analyzerSettings,
'tokenizer' => $tokenizerSettings
];
}
/**
* MySQL 타입을 Elasticsearch 타입으로 변환
*/
......@@ -393,23 +452,39 @@ class MySQLToElastic
}
}
// 테이블 구조 조회
// 테이블 구조 조회 (사용된 analyzer도 수집됨)
$mapping = $this->getTableStructure();
// 사용된 analyzer 설정 가져오기
$analyzerConfig = $this->getAnalyzerSettings();
// analysis 설정 구성
$analysis = [
'analyzer' => [
'korean' => [
'type' => 'custom',
'tokenizer' => 'standard',
'filter' => ['lowercase']
]
]
];
// 사용된 analyzer 추가
if (!empty($analyzerConfig['analyzer'])) {
$analysis['analyzer'] = array_merge($analysis['analyzer'], $analyzerConfig['analyzer']);
}
// 사용된 tokenizer 추가
if (!empty($analyzerConfig['tokenizer'])) {
$analysis['tokenizer'] = $analyzerConfig['tokenizer'];
}
// 인덱스 생성 요청 데이터
$indexData = [
'settings' => [
'number_of_shards' => 1,
'number_of_replicas' => 1,
'analysis' => [
'analyzer' => [
'korean' => [
'type' => 'custom',
'tokenizer' => 'standard',
'filter' => ['lowercase']
]
]
]
'analysis' => $analysis
],
'mappings' => [
'properties' => $mapping
......
#!/usr/bin/env php
<?php
/**
* ptyMysqlOverwrite
*
* MySQL 테이블을 다른 서버/DB로 복사하는 도구
* ptyMysqlBackup.php와 ptyMysqlRestore.php를 내부적으로 사용
*
* 설정 파일: ~/.ptyMysqlConfig.ini
*
* Usage: ./ptyMysqlOverwrite --src-mysql=섹션 --dst-mysql=섹션 --src-db=DB --dst-db=DB --src-table=테이블 --dst-table=테이블
*/
namespace platyFramework;
require_once __DIR__ . '/ptyLibrary_PHP/cli/ptyCliOptionParser.php';
require_once __DIR__ . '/ptyLibrary_PHP/cli/ptyCliLog.php';
// 인자 파싱
$parsed = ptyCliOptionParser::parse($argv);
$options = $parsed['options'];
$srcMysql = $options['src-mysql'] ?? null;
$dstMysql = $options['dst-mysql'] ?? null;
$srcDb = $options['src-db'] ?? null;
$dstDb = $options['dst-db'] ?? null;
$srcTable = $options['src-table'] ?? null;
$dstTable = $options['dst-table'] ?? null;
$verbose = isset($options['verbose']);
$force = isset($options['force']);
// 도움말 또는 필수 인자 확인
if (!$srcMysql || !$dstMysql || !$srcDb || !$dstDb || !$srcTable || !$dstTable || isset($options['help'])) {
echo "사용법: {$argv[0]} [옵션]\n";
echo "\n";
echo "필수 옵션:\n";
echo " --src-mysql=섹션명 소스 MySQL INI 섹션\n";
echo " --dst-mysql=섹션명 대상 MySQL INI 섹션\n";
echo " --src-db=DB명 소스 데이터베이스명\n";
echo " --dst-db=DB명 대상 데이터베이스명\n";
echo " --src-table=테이블명 소스 테이블명\n";
echo " --dst-table=테이블명 대상 테이블명\n";
echo "\n";
echo "선택 옵션:\n";
echo " --force 확인 없이 바로 실행\n";
echo " --verbose 상세 로그 출력\n";
echo " --help 도움말 출력\n";
echo "\n";
echo "예시:\n";
echo " {$argv[0]} --src-mysql=api --dst-mysql=dev \\\n";
echo " --src-db=lawtice_db --dst-db=lawtice_dev \\\n";
echo " --src-table=new_law_items --dst-table=new_law_items\n";
echo "\n";
echo " # 같은 서버 내에서 다른 DB로 복사\n";
echo " {$argv[0]} --src-mysql=default --dst-mysql=default \\\n";
echo " --src-db=prod_db --dst-db=dev_db \\\n";
echo " --src-table=users --dst-table=users\n";
echo "\n";
echo "설정 파일: ~/.ptyMysqlConfig.ini\n";
echo "\n";
echo "[api]\n";
echo "host=api.example.com\n";
echo "username=root\n";
echo "password=\"password\"\n";
echo "database=default_db\n";
echo "\n";
echo "[dev]\n";
echo "host=dev.example.com\n";
echo "username=root\n";
echo "password=\"password\"\n";
echo "database=default_db\n";
echo "\n";
exit(isset($options['help']) ? 0 : 1);
}
// ANSI 색상 코드
$RED = "\033[1;31m";
$GREEN = "\033[1;32m";
$YELLOW = "\033[1;33m";
$CYAN = "\033[1;36m";
$MAGENTA = "\033[1;35m";
$RESET = "\033[0m";
$log = new ptyCliLog(prefix: "OVERWRITE", color: ptyCliLog::COLOR_CYAN);
// 스크립트 경로
$scriptDir = __DIR__;
$backupScript = $scriptDir . '/ptyMysqlBackup.php';
$restoreScript = $scriptDir . '/ptyMysqlRestore.php';
// 스크립트 존재 확인
if (!file_exists($backupScript)) {
$log->error("ptyMysqlBackup.php를 찾을 수 없습니다: $backupScript");
exit(1);
}
if (!file_exists($restoreScript)) {
$log->error("ptyMysqlRestore.php를 찾을 수 없습니다: $restoreScript");
exit(1);
}
// 작업 정보 표시
echo "\n";
echo "{$CYAN}╔══════════════════════════════════════════════════════════════════╗{$RESET}\n";
echo "{$CYAN}║ MySQL 테이블 Overwrite ║{$RESET}\n";
echo "{$CYAN}╚══════════════════════════════════════════════════════════════════╝{$RESET}\n";
echo "\n";
echo "{$YELLOW}[ 소스 ]{$RESET}\n";
echo str_repeat("-", 50) . "\n";
echo " MySQL Section : $srcMysql\n";
echo " Database : $srcDb\n";
echo " Table : $srcTable\n";
echo "\n";
echo "{$YELLOW}[ 대상 ]{$RESET}\n";
echo str_repeat("-", 50) . "\n";
echo " MySQL Section : $dstMysql\n";
echo " Database : $dstDb\n";
echo " Table : $dstTable\n";
echo "\n";
echo "{$RED}╔══════════════════════════════════════════════════════════════════╗{$RESET}\n";
echo "{$RED}║ 주의: 대상 테이블의 기존 데이터가 완전히 삭제됩니다! ║{$RESET}\n";
echo "{$RED}╚══════════════════════════════════════════════════════════════════╝{$RESET}\n";
echo "\n";
// 확인 절차
if (!$force) {
echo "진행하시겠습니까? (y/N): ";
$handle = fopen("php://stdin", "r");
$line = fgets($handle);
fclose($handle);
$answer = strtolower(trim($line));
if ($answer !== 'y' && $answer !== 'yes') {
echo "\n작업이 취소되었습니다.\n";
exit(0);
}
echo "\n";
}
// 임시 디렉토리 생성
$tempDir = sys_get_temp_dir() . '/ptyMysqlOverwrite_' . getmypid() . '_' . time();
if (!mkdir($tempDir, 0755, true)) {
$log->error("임시 디렉토리 생성 실패: $tempDir");
exit(1);
}
$log->info("임시 디렉토리: $tempDir");
// 정리 함수
function cleanup($tempDir) {
if (is_dir($tempDir)) {
$files = glob($tempDir . '/*');
foreach ($files as $file) {
if (is_file($file)) {
unlink($file);
}
}
rmdir($tempDir);
}
}
// 에러 시 정리
register_shutdown_function(function() use ($tempDir) {
cleanup($tempDir);
});
try {
// ============================================
// Step 1: 소스 테이블 백업
// ============================================
$log->info("=== Step 1: 소스 테이블 백업 ===");
$backupCmd = "php " . escapeshellarg($backupScript);
$backupCmd .= " " . escapeshellarg($srcDb);
$backupCmd .= " " . escapeshellarg($srcTable);
$backupCmd .= " --mysql=" . escapeshellarg($srcMysql);
$backupCmd .= " --output=" . escapeshellarg($tempDir);
if ($verbose) {
$backupCmd .= " --verbose";
}
$backupCmd .= " 2>&1";
// 실행 명령어 로그
$log->info("실행: ./ptyMysqlBackup {$srcDb} {$srcTable} --mysql={$srcMysql} --output={$tempDir}");
$output = [];
exec($backupCmd, $output, $returnCode);
if ($verbose) {
foreach ($output as $line) {
echo " $line\n";
}
}
if ($returnCode !== 0) {
$log->error("백업 실패 (exit code: $returnCode)");
foreach ($output as $line) {
echo " $line\n";
}
exit(1);
}
// 백업 파일 확인
$srcBackupFile = $tempDir . "/{$srcDb}.{$srcTable}.sql";
if (!file_exists($srcBackupFile)) {
$log->error("백업 파일을 찾을 수 없습니다: $srcBackupFile");
exit(1);
}
$backupSize = filesize($srcBackupFile);
$log->success("백업 완료: " . formatBytes($backupSize));
// ============================================
// Step 2: 테이블명 변환 (src-table → dst-table)
// ============================================
//
// mysqldump로 생성된 SQL 파일에는 원본 테이블명이 하드코딩되어 있습니다.
// --src-table과 --dst-table이 다를 경우, SQL 파일 내의 테이블명을 변경해야 합니다.
//
// 변환이 필요한 SQL 문:
// - DROP TABLE IF EXISTS `src_table` → DROP TABLE IF EXISTS `dst_table`
// - CREATE TABLE `src_table` → CREATE TABLE `dst_table`
// - INSERT INTO `src_table` → INSERT INTO `dst_table`
// - LOCK TABLES `src_table` → LOCK TABLES `dst_table`
//
// 예시:
// --src-table=users --dst-table=users_backup 인 경우
// "INSERT INTO `users`" → "INSERT INTO `users_backup`" 으로 변환
//
// 주의: sed를 사용하여 스트리밍 처리 (대용량 파일도 메모리 문제 없음)
//
$log->info("=== Step 2: 파일 준비 ===");
$dstBackupFile = $tempDir . "/{$dstDb}.{$dstTable}.sql";
// 테이블명이 다를 경우에만 변환 필요
if ($srcTable !== $dstTable) {
// sed를 사용하여 스트리밍으로 테이블명 변환
// - 파일을 메모리에 전부 로드하지 않고 라인 단위로 처리
// - macOS와 Linux 호환을 위해 sed -i '' 대신 파이프(>) 사용
$sedCmd = "sed";
$sedCmd .= " -e " . escapeshellarg("s/DROP TABLE IF EXISTS \`{$srcTable}\`/DROP TABLE IF EXISTS \`{$dstTable}\`/g");
$sedCmd .= " -e " . escapeshellarg("s/CREATE TABLE \`{$srcTable}\`/CREATE TABLE \`{$dstTable}\`/g");
$sedCmd .= " -e " . escapeshellarg("s/INSERT INTO \`{$srcTable}\`/INSERT INTO \`{$dstTable}\`/g");
$sedCmd .= " -e " . escapeshellarg("s/LOCK TABLES \`{$srcTable}\`/LOCK TABLES \`{$dstTable}\`/g");
$sedCmd .= " " . escapeshellarg($srcBackupFile);
$sedCmd .= " > " . escapeshellarg($dstBackupFile);
if ($verbose) {
$log->info("sed 명령: $sedCmd");
}
exec($sedCmd, $output, $returnCode);
if ($returnCode !== 0) {
$log->error("테이블명 변환 실패");
exit(1);
}
$log->success("테이블명 변환 완료: {$srcTable}{$dstTable}");
} else {
// 테이블명이 같으면 그냥 원본 파일 사용 (복사 불필요)
$dstBackupFile = $srcBackupFile;
$log->info("테이블명 동일 - 변환 불필요");
}
// 복원용 파일명 결정 (Restore 스크립트에서 사용)
$restoreFileName = basename($dstBackupFile);
// ============================================
// Step 3: 대상 서버로 복원
// ============================================
$log->info("=== Step 3: 대상 서버로 복원 ===");
$restoreCmd = "php " . escapeshellarg($restoreScript);
$restoreCmd .= " " . escapeshellarg($restoreFileName);
$restoreCmd .= " --mysql=" . escapeshellarg($dstMysql);
$restoreCmd .= " --input=" . escapeshellarg($tempDir);
$restoreCmd .= " --db=" . escapeshellarg($dstDb);
$restoreCmd .= " --force"; // 이미 확인했으므로 force
if ($verbose) {
$restoreCmd .= " --verbose";
}
$restoreCmd .= " 2>&1";
// 실행 명령어 로그
$log->info("실행: ./ptyMysqlRestore {$restoreFileName} --mysql={$dstMysql} --db={$dstDb} --force");
$output = [];
exec($restoreCmd, $output, $returnCode);
if ($verbose) {
foreach ($output as $line) {
echo " $line\n";
}
}
if ($returnCode !== 0) {
$log->error("복원 실패 (exit code: $returnCode)");
foreach ($output as $line) {
echo " $line\n";
}
exit(1);
}
$log->success("복원 완료");
// ============================================
// 완료
// ============================================
echo "\n";
echo "{$GREEN}╔══════════════════════════════════════════════════════════════════╗{$RESET}\n";
echo "{$GREEN}║ Overwrite 완료! ║{$RESET}\n";
echo "{$GREEN}╚══════════════════════════════════════════════════════════════════╝{$RESET}\n";
echo "\n";
echo " {$srcMysql}:{$srcDb}.{$srcTable}\n";
echo " ↓\n";
echo " {$dstMysql}:{$dstDb}.{$dstTable}\n";
echo "\n";
// 정리
cleanup($tempDir);
$log->success("임시 파일 정리 완료");
} catch (\Exception $e) {
$log->error($e->getMessage());
cleanup($tempDir);
exit(1);
}
// 바이트를 읽기 쉬운 형식으로 변환
function formatBytes($bytes) {
if ($bytes >= 1073741824) {
return number_format($bytes / 1073741824, 2) . ' GB';
} elseif ($bytes >= 1048576) {
return number_format($bytes / 1048576, 2) . ' MB';
} elseif ($bytes >= 1024) {
return number_format($bytes / 1024, 2) . ' KB';
} else {
return $bytes . ' bytes';
}
}
......@@ -24,7 +24,8 @@ $mysqlSection = $options['mysql'] ?? 'default';
$verbose = isset($options['verbose']);
$force = isset($options['force']);
$inputDir = $options['input'] ?? '.';
$targetDatabase = $options['database'] ?? null;
$targetDatabase = $options['db'] ?? null;
$targetTable = $options['table'] ?? null;
// 도움말 또는 필수 인자 확인
if (empty($positionalArgs) || isset($options['help'])) {
......@@ -35,7 +36,8 @@ if (empty($positionalArgs) || isset($options['help'])) {
echo "\n";
echo "옵션:\n";
echo " --mysql=섹션명 INI 파일 섹션 (기본값: default)\n";
echo " --database=DB명 복원할 대상 데이터베이스 (미지정시 파일명에서 추출)\n";
echo " --db=DB명 복원할 대상 데이터베이스 (미지정시 파일명에서 추출)\n";
echo " --table=테이블명 복원할 대상 테이블명 (미지정시 파일명에서 추출)\n";
echo " --input=경로 입력 디렉토리 (기본값: 현재 디렉토리)\n";
echo " --force 확인 없이 바로 복원 실행\n";
echo " --verbose 상세 로그 출력\n";
......@@ -49,7 +51,8 @@ if (empty($positionalArgs) || isset($options['help'])) {
echo " {$argv[0]} \"*\" --input=/backup # /backup 디렉토리에서 복원\n";
echo " {$argv[0]} \"*\" --mysql=production # production 섹션 사용\n";
echo " {$argv[0]} \"*\" --force # 확인 없이 복원\n";
echo " {$argv[0]} \"*\" --database=mydb_dev # 모든 파일을 mydb_dev DB에 복원\n";
echo " {$argv[0]} \"*\" --db=mydb_dev # 모든 파일을 mydb_dev DB에 복원\n";
echo " {$argv[0]} \"mydb.users.sql\" --table=users_backup # users → users_backup 으로 복원\n";
echo "\n";
echo "파일명 형식: <database>.<table>.sql\n";
echo "\n";
......@@ -113,8 +116,48 @@ function extractDbAndTable($filename) {
return [null, null];
}
// ============================================
// SQL 파일 내 테이블명 변환 함수
// ============================================
//
// mysqldump로 생성된 SQL 파일에는 원본 테이블명이 하드코딩되어 있습니다.
// --table 옵션으로 다른 테이블명으로 복원할 경우, SQL 파일 내의 테이블명을 변경해야 합니다.
//
// 변환이 필요한 SQL 문:
// - DROP TABLE IF EXISTS `src_table` → DROP TABLE IF EXISTS `dst_table`
// - CREATE TABLE `src_table` → CREATE TABLE `dst_table`
// - INSERT INTO `src_table` → INSERT INTO `dst_table`
// - LOCK TABLES `src_table` → LOCK TABLES `dst_table`
//
// 주의: sed를 사용하여 스트리밍 처리 (대용량 파일도 메모리 문제 없음)
//
function transformTableName($srcFile, $srcTable, $dstTable, $verbose) {
$dstFile = sys_get_temp_dir() . '/ptyMysqlRestore_' . getmypid() . '_' . time() . '.sql';
$sedCmd = "sed";
$sedCmd .= " -e " . escapeshellarg("s/DROP TABLE IF EXISTS \`{$srcTable}\`/DROP TABLE IF EXISTS \`{$dstTable}\`/g");
$sedCmd .= " -e " . escapeshellarg("s/CREATE TABLE \`{$srcTable}\`/CREATE TABLE \`{$dstTable}\`/g");
$sedCmd .= " -e " . escapeshellarg("s/INSERT INTO \`{$srcTable}\`/INSERT INTO \`{$dstTable}\`/g");
$sedCmd .= " -e " . escapeshellarg("s/LOCK TABLES \`{$srcTable}\`/LOCK TABLES \`{$dstTable}\`/g");
$sedCmd .= " " . escapeshellarg($srcFile);
$sedCmd .= " > " . escapeshellarg($dstFile);
if ($verbose) {
logMessage("테이블명 변환: {$srcTable}{$dstTable}", true);
}
exec($sedCmd, $output, $returnCode);
if ($returnCode !== 0) {
@unlink($dstFile);
return null;
}
return $dstFile;
}
// 복원 실행 함수
function restoreFile($host, $user, $password, $dbName, $sqlFile, $verbose) {
function restoreFile($host, $user, $password, $dbName, $tableName, $sqlFile, $verbose) {
$basename = basename($sqlFile);
$size = filesize($sqlFile);
$sizeStr = formatBytes($size);
......@@ -136,14 +179,14 @@ function restoreFile($host, $user, $password, $dbName, $sqlFile, $verbose) {
exec($cmd, $output, $returnCode);
if ($returnCode !== 0) {
logMessage("ERROR: $basename 복원 실패", true);
logMessage("ERROR: $tableName 복원 실패", true);
if (!empty($output)) {
logMessage(implode("\n", $output), true);
}
return false;
}
logMessage("OK: $basename ($sizeStr)", true);
logMessage("OK: $tableName ($sizeStr)", true);
return true;
}
......@@ -203,15 +246,18 @@ try {
$dbList = [];
foreach ($files as $sqlFile) {
list($origDbName, $tableName) = extractDbAndTable($sqlFile);
list($origDbName, $origTableName) = extractDbAndTable($sqlFile);
if (!$origDbName) {
continue;
}
// --database 옵션이 있으면 해당 DB로, 없으면 파일명에서 추출한 DB 사용
// --db 옵션이 있으면 해당 DB로, 없으면 파일명에서 추출한 DB 사용
$dbName = $targetDatabase ?? $origDbName;
// --table 옵션이 있으면 해당 테이블로, 없으면 파일명에서 추출한 테이블 사용
$tableName = $targetTable ?? $origTableName;
$size = filesize($sqlFile);
$totalSize += $size;
......@@ -220,6 +266,7 @@ try {
}
$dbList[$dbName][] = [
'table' => $tableName,
'origTable' => $origTableName,
'file' => $sqlFile,
'size' => $size,
'origDb' => $origDbName,
......@@ -245,12 +292,20 @@ try {
foreach ($tables as $info) {
$tableName = $info['table'];
$origTableName = $info['origTable'];
$sizeStr = formatBytes($info['size']);
// 원본 DB가 다르면 표시
// 원본 정보 표시 (DB나 테이블이 다를 경우)
$origInfo = "";
$origParts = [];
if ($targetDatabase && $info['origDb'] !== $dbName) {
$origInfo = " {$MAGENTA}{$info['origDb']}{$RESET}";
$origParts[] = $info['origDb'];
}
if ($targetTable && $origTableName !== $tableName) {
$origParts[] = $origTableName;
}
if (!empty($origParts)) {
$origInfo = " {$MAGENTA}← " . implode('.', $origParts) . "{$RESET}";
}
// 테이블 존재 여부 확인
......@@ -313,9 +368,29 @@ try {
foreach ($tables as $info) {
$totalRestores++;
if (restoreFile($config['host'], $config['username'], $config['password'], $dbName, $info['file'], $verbose)) {
$tableName = $info['table'];
$origTableName = $info['origTable'];
$sqlFile = $info['file'];
$tempFile = null;
// 테이블명이 다를 경우 sed로 변환
if ($tableName !== $origTableName) {
$tempFile = transformTableName($sqlFile, $origTableName, $tableName, $verbose);
if ($tempFile === null) {
logMessage("ERROR: 테이블명 변환 실패 - $origTableName$tableName", true);
continue;
}
$sqlFile = $tempFile;
}
if (restoreFile($config['host'], $config['username'], $config['password'], $dbName, $tableName, $sqlFile, $verbose)) {
$successRestores++;
}
// 임시 파일 정리
if ($tempFile !== null && file_exists($tempFile)) {
@unlink($tempFile);
}
}
}
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment