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
This diff is collapsed.
......@@ -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,15 +452,14 @@ class MySQLToElastic
}
}
// 테이블 구조 조회
// 테이블 구조 조회 (사용된 analyzer도 수집됨)
$mapping = $this->getTableStructure();
// 인덱스 생성 요청 데이터
$indexData = [
'settings' => [
'number_of_shards' => 1,
'number_of_replicas' => 1,
'analysis' => [
// 사용된 analyzer 설정 가져오기
$analyzerConfig = $this->getAnalyzerSettings();
// analysis 설정 구성
$analysis = [
'analyzer' => [
'korean' => [
'type' => 'custom',
......@@ -409,7 +467,24 @@ class MySQLToElastic
'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' => $analysis
],
'mappings' => [
'properties' => $mapping
......
This diff is collapsed.
......@@ -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