d8877b243a
Build & Deploy to K8s / build-and-deploy (push) Successful in 8m33s
5/15 common-code 재설계가 화이트리스트를 8종으로 좁히면서 빠뜨린
운영 DB 데이터 정리. 90787d83 의 화이트리스트 확장 fix 는 회복용
보호막이었고, 본 PR 은 데이터를 표준으로 통합하는 후속 정리.
매핑:
category/select/radio/checkbox/boolean → code
textarea → text
datetime → date
영향: 메타 DB 1,207 row 갱신. 테넌트 DB 들은 비어있어 0 row.
WHERE input_type IN (...) 으로 멱등 (재실행 시 0 row).
화이트리스트 축소는 운영 안정 확인 후 별도 PR.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
371 lines
19 KiB
Java
371 lines
19 KiB
Java
package com.erp.migration;
|
|
|
|
import com.erp.tenant.DbContextHolder;
|
|
import com.erp.tenant.TenantDbSettings;
|
|
import lombok.RequiredArgsConstructor;
|
|
import lombok.extern.slf4j.Slf4j;
|
|
import org.apache.ibatis.session.SqlSession;
|
|
import org.springframework.beans.factory.annotation.Value;
|
|
import org.springframework.boot.context.event.ApplicationReadyEvent;
|
|
import org.springframework.context.event.EventListener;
|
|
import org.springframework.stereotype.Component;
|
|
|
|
import java.sql.Connection;
|
|
import java.sql.DriverManager;
|
|
import java.sql.Statement;
|
|
import java.util.List;
|
|
|
|
/**
|
|
* 앱 부팅 시 idempotent 스키마 마이그레이션 실행.
|
|
*
|
|
* 왜 필요:
|
|
* DB-per-tenant 구조 → 메타 DB 1개 + 활성 테넌트 DB N개 에 동일 ALTER 필요.
|
|
* runbook 문서 (`db/migrations/RUN_*.md`) 로만 남기면 배포 때 사람이 까먹으면 장애.
|
|
* 부팅 때 `IF NOT EXISTS` 로 안전하게 돌려두면 배포 순서/인적 실수와 무관.
|
|
*
|
|
* 원칙:
|
|
* - 각 ALTER 는 반드시 idempotent (`IF NOT EXISTS` / 재실행 안전)
|
|
* - 테넌트 DB 하나 실패해도 다른 DB 는 계속 진행 (ERROR 로그만)
|
|
* - 메타 DB 가 실패하면 앱 시작은 계속되지만 WARN 크게 남김
|
|
*/
|
|
@Component
|
|
@RequiredArgsConstructor
|
|
@Slf4j
|
|
public class StartupSchemaMigrator {
|
|
|
|
private final TenantDbSettings tenantDbSettings;
|
|
private final SqlSession sqlSession;
|
|
|
|
@Value("${spring.datasource.url}")
|
|
private String metaJdbcUrl;
|
|
|
|
private static final List<String> MIGRATIONS = List.of(
|
|
// RUN_082: 첫 로그인 비밀번호 강제 변경 플래그
|
|
"ALTER TABLE USER_INFO ADD COLUMN IF NOT EXISTS FORCE_PASSWORD_CHANGE BOOLEAN DEFAULT FALSE",
|
|
|
|
// V017: 회사 관리 그룹 하위 관리자 메뉴 순서 재배열
|
|
// 조직 계층(회사→부서→사용자) + 권한 체계(메뉴→권한→권한그룹)
|
|
// 메타 DB 는 Flyway V017 로도 적용되지만 프로비저닝된 테넌트 DB 는
|
|
// 회사 생성 시점 스냅샷이 박혀있으므로 부팅 때 모든 활성 DB 에 동기화.
|
|
// SEQ 만 갱신 → 멱등.
|
|
// 타입 주의: SEQ 가 varchar 이므로 THEN 값도 문자열 리터럴로 줄 것
|
|
// (정수 리터럴이면 ELSE SEQ 와 CASE 타입 불일치 42804 발생).
|
|
"""
|
|
UPDATE MENU_INFO
|
|
SET SEQ = CASE MENU_NAME_KOR
|
|
WHEN '회사관리' THEN '100'
|
|
WHEN '부서관리' THEN '200'
|
|
WHEN '사용자관리' THEN '300'
|
|
WHEN '메뉴관리' THEN '400'
|
|
WHEN '권한관리' THEN '500'
|
|
WHEN '권한 그룹관리' THEN '600'
|
|
ELSE SEQ
|
|
END
|
|
WHERE MENU_TYPE = '0'
|
|
AND COMPANY_CODE = '*'
|
|
AND PARENT_OBJ_ID IS NOT NULL
|
|
AND PARENT_OBJ_ID <> '0'
|
|
AND MENU_NAME_KOR IN (
|
|
'회사관리', '부서관리', '사용자관리',
|
|
'메뉴관리', '권한관리', '권한 그룹관리'
|
|
)
|
|
""",
|
|
|
|
// V018 (1) 부서관리 V1 - DEPT_INFO 소프트삭제 컬럼.
|
|
// DELETE 동작이 hard 가 아니라 DELETED_AT = NOW() 로 전환됨.
|
|
// 메타 DB 는 Flyway V018 로도 적용되지만 프로비저닝된 테넌트 DB 는 부팅 때 동기화.
|
|
"ALTER TABLE DEPT_INFO ADD COLUMN IF NOT EXISTS DELETED_AT TIMESTAMP NULL",
|
|
|
|
// V018 (2) DEPT_INFO 활성 부서 부분 인덱스 (DELETED_AT IS NULL 쿼리 가속)
|
|
"CREATE INDEX IF NOT EXISTS IDX_DEPT_INFO_ACTIVE ON DEPT_INFO (COMPANY_CODE, PARENT_DEPT_CODE) WHERE DELETED_AT IS NULL",
|
|
|
|
// V019: 부서관리 V1 - DEPT_INFO 미사용/중복 컬럼 정리.
|
|
// 메타 DB 는 Flyway V019 로도 적용되지만 프로비저닝된 테넌트 DB 는 부팅 때 동기화.
|
|
// DROP IF EXISTS 로 멱등성 보장.
|
|
"ALTER TABLE DEPT_INFO DROP COLUMN IF EXISTS MASTER_SABUN",
|
|
"ALTER TABLE DEPT_INFO DROP COLUMN IF EXISTS MASTER_USER_ID",
|
|
"ALTER TABLE DEPT_INFO DROP COLUMN IF EXISTS ORG_HEAD",
|
|
"ALTER TABLE DEPT_INFO DROP COLUMN IF EXISTS LOCATION_NAME",
|
|
"ALTER TABLE DEPT_INFO DROP COLUMN IF EXISTS SALES_YN",
|
|
"ALTER TABLE DEPT_INFO DROP COLUMN IF EXISTS SHOW_IN_CHART",
|
|
"ALTER TABLE DEPT_INFO DROP COLUMN IF EXISTS ERP_MANAGED",
|
|
"ALTER TABLE DEPT_INFO DROP COLUMN IF EXISTS DATA_TYPE",
|
|
|
|
// V020: 사용자별 메뉴 즐겨찾기 테이블.
|
|
// 메타 DB 는 Flyway V020 으로도 적용되지만 프로비저닝된 테넌트 DB 는 부팅 때 동기화.
|
|
// CREATE IF NOT EXISTS 로 멱등성 보장.
|
|
"""
|
|
CREATE TABLE IF NOT EXISTS USER_MENU_FAVORITES (
|
|
OBJID BIGSERIAL PRIMARY KEY,
|
|
USER_ID VARCHAR(100) NOT NULL,
|
|
MENU_OBJID VARCHAR(50) NOT NULL,
|
|
SORT_ORDER INTEGER NOT NULL DEFAULT 0,
|
|
CREATED_AT TIMESTAMP NOT NULL DEFAULT NOW(),
|
|
CONSTRAINT UQ_USER_MENU_FAVORITES UNIQUE (USER_ID, MENU_OBJID)
|
|
)
|
|
""",
|
|
"CREATE INDEX IF NOT EXISTS IDX_USER_MENU_FAVORITES_USER ON USER_MENU_FAVORITES (USER_ID)",
|
|
|
|
// RUN_086 (1) btree_gist 확장 — USER_SUBSTITUTES 의 EXCLUDE 제약 의존성
|
|
"CREATE EXTENSION IF NOT EXISTS btree_gist",
|
|
|
|
// RUN_086 (2) 대무자(代務者) 관리 테이블
|
|
// self-위임 차단 (CHECK), 같은 쌍 활성 기간 겹침 차단 (EXCLUDE).
|
|
// 재실행 시 IF NOT EXISTS 로 안전. EXCLUDE/CHECK 제약은 첫 생성 때만 적용.
|
|
"""
|
|
CREATE TABLE IF NOT EXISTS USER_SUBSTITUTES (
|
|
SUBSTITUTE_ID BIGSERIAL PRIMARY KEY,
|
|
COMPANY_CODE VARCHAR(50) NOT NULL,
|
|
ORIGINAL_USER_ID VARCHAR(50) NOT NULL,
|
|
PROXY_USER_ID VARCHAR(50) NOT NULL,
|
|
START_DATE DATE NULL,
|
|
END_DATE DATE NOT NULL,
|
|
REASON VARCHAR(500),
|
|
IS_ACTIVE BOOLEAN NOT NULL DEFAULT TRUE,
|
|
CREATED_BY VARCHAR(50),
|
|
CREATED_DATE TIMESTAMP NOT NULL DEFAULT NOW(),
|
|
UPDATED_BY VARCHAR(50),
|
|
UPDATED_DATE TIMESTAMP NOT NULL DEFAULT NOW(),
|
|
CONSTRAINT chk_user_substitutes_self
|
|
CHECK (ORIGINAL_USER_ID <> PROXY_USER_ID),
|
|
CONSTRAINT chk_user_substitutes_date
|
|
CHECK (START_DATE IS NULL OR START_DATE <= END_DATE),
|
|
CONSTRAINT excl_user_substitutes_overlap
|
|
EXCLUDE USING gist (
|
|
COMPANY_CODE WITH =,
|
|
ORIGINAL_USER_ID WITH =,
|
|
PROXY_USER_ID WITH =,
|
|
daterange(START_DATE, END_DATE, '[]') WITH &&
|
|
) WHERE (IS_ACTIVE = TRUE)
|
|
)
|
|
""",
|
|
|
|
// RUN_086 (3) USER_SUBSTITUTES 인덱스 — Filter 핫패스 + 조회 가속
|
|
"CREATE INDEX IF NOT EXISTS idx_user_substitutes_original ON USER_SUBSTITUTES (COMPANY_CODE, ORIGINAL_USER_ID, IS_ACTIVE)",
|
|
"CREATE INDEX IF NOT EXISTS idx_user_substitutes_proxy ON USER_SUBSTITUTES (COMPANY_CODE, PROXY_USER_ID, IS_ACTIVE)",
|
|
|
|
// RUN_086 (4) SYSTEM_AUDIT_LOG — 처리자(actual processor) 분리 기록 컬럼
|
|
"ALTER TABLE SYSTEM_AUDIT_LOG ADD COLUMN IF NOT EXISTS PROCESSOR_ID VARCHAR(50)",
|
|
"ALTER TABLE SYSTEM_AUDIT_LOG ADD COLUMN IF NOT EXISTS PROCESSOR_NAME VARCHAR(100)",
|
|
|
|
// RUN_086 (5) APPROVAL_PROXY_SETTINGS → USER_SUBSTITUTES 1회 데이터 복사 (idempotent)
|
|
// 기존 운영 데이터 보존 + 어댑터 read 경로가 즉시 동작하도록.
|
|
// IS_ACTIVE 매핑: APPROVAL_PROXY_SETTINGS 의 CHAR('Y'/'N') → BOOLEAN.
|
|
// 메타데이터(created/updated) 는 원본 컬럼 의존 없이 'migration_086' + NOW() 로 고정
|
|
// (APPROVAL_PROXY_SETTINGS 의 timestamp 컬럼명이 환경별로 다를 수 있어 안전한 default 채택).
|
|
"""
|
|
INSERT INTO USER_SUBSTITUTES (
|
|
COMPANY_CODE, ORIGINAL_USER_ID, PROXY_USER_ID,
|
|
START_DATE, END_DATE, REASON, IS_ACTIVE,
|
|
CREATED_BY, CREATED_DATE, UPDATED_BY, UPDATED_DATE
|
|
)
|
|
SELECT
|
|
p.COMPANY_CODE, p.ORIGINAL_USER_ID, p.PROXY_USER_ID,
|
|
CAST(NULLIF(p.START_DATE, '') AS DATE),
|
|
CAST(NULLIF(p.END_DATE, '') AS DATE),
|
|
p.REASON,
|
|
CASE WHEN p.IS_ACTIVE = 'Y' THEN TRUE ELSE FALSE END,
|
|
'migration_086', NOW(),
|
|
'migration_086', NOW()
|
|
FROM APPROVAL_PROXY_SETTINGS p
|
|
WHERE NULLIF(p.END_DATE, '') IS NOT NULL
|
|
AND NOT EXISTS (
|
|
SELECT 1 FROM USER_SUBSTITUTES s
|
|
WHERE s.COMPANY_CODE = p.COMPANY_CODE
|
|
AND s.ORIGINAL_USER_ID = p.ORIGINAL_USER_ID
|
|
AND s.PROXY_USER_ID = p.PROXY_USER_ID
|
|
AND s.START_DATE IS NOT DISTINCT FROM CAST(NULLIF(p.START_DATE, '') AS DATE)
|
|
AND s.END_DATE = CAST(NULLIF(p.END_DATE, '') AS DATE)
|
|
)
|
|
""",
|
|
|
|
// V021 / RUN_087: BATCH_MAPPINGS 에 MAPPING_CONFIG JSONB 컬럼 추가.
|
|
// conditional 매핑(when/then/default) 규칙 저장용.
|
|
// direct/fixed 매핑은 NULL. 메타 DB 는 Flyway V021 로도 적용되지만
|
|
// 프로비저닝된 테넌트 DB 는 부팅 때 동기화.
|
|
"ALTER TABLE BATCH_MAPPINGS ADD COLUMN IF NOT EXISTS MAPPING_CONFIG JSONB",
|
|
|
|
// V022 / RUN_088: 부서별 다중 관리자(결재/부서/조직장) 매핑 테이블.
|
|
// 기존 DEPT_INFO.APPROVAL_MANAGER/DEPT_MANAGER 단일 컬럼 → 매핑 테이블로 다중화.
|
|
// role: 'approval' | 'dept' | 'org_leader'. 부서 hard-delete 시 CASCADE 로 정리.
|
|
// 메타 DB 는 Flyway V022 로도 적용되지만 프로비저닝된 테넌트 DB 는 부팅 때 동기화.
|
|
"""
|
|
CREATE TABLE IF NOT EXISTS DEPT_MANAGERS (
|
|
DEPT_CODE VARCHAR(1024) NOT NULL,
|
|
USER_ID VARCHAR(50) NOT NULL,
|
|
ROLE VARCHAR(20) NOT NULL,
|
|
SORT_ORDER INTEGER NOT NULL DEFAULT 1,
|
|
CREATED_AT TIMESTAMP NOT NULL DEFAULT NOW(),
|
|
PRIMARY KEY (DEPT_CODE, USER_ID, ROLE),
|
|
CONSTRAINT chk_dept_managers_role
|
|
CHECK (ROLE IN ('approval', 'dept', 'org_leader')),
|
|
CONSTRAINT fk_dept_managers_dept
|
|
FOREIGN KEY (DEPT_CODE) REFERENCES DEPT_INFO(DEPT_CODE) ON DELETE CASCADE
|
|
)
|
|
""",
|
|
"CREATE INDEX IF NOT EXISTS idx_dept_managers_role ON DEPT_MANAGERS (DEPT_CODE, ROLE, SORT_ORDER)",
|
|
|
|
// V023 / RUN_089: MENU_INFO 에 IS_SOLUTION_ONLY 컬럼 추가.
|
|
// 솔루션 관리 호스트(solution.invyone.com 등) 에서만 노출되는 메뉴 플래그.
|
|
// 테넌트 사이트에선 mapper SQL 단계에서 제외. 메타 DB 는 Flyway V023 으로도 적용되지만
|
|
// 프로비저닝된 테넌트 DB 는 부팅 때 동기화.
|
|
"ALTER TABLE MENU_INFO ADD COLUMN IF NOT EXISTS IS_SOLUTION_ONLY BOOLEAN DEFAULT FALSE NOT NULL",
|
|
|
|
// V023 데이터 동기화: 솔루션 전용 메뉴 마킹.
|
|
// 회사관리 / 회사 프로비저닝 / 감사로그는 관리 호스트에서만 노출돼야 함.
|
|
// 이미 TRUE 인 행은 그대로 두기 위해 false 인 행만 갱신.
|
|
"""
|
|
UPDATE MENU_INFO
|
|
SET IS_SOLUTION_ONLY = TRUE
|
|
WHERE IS_SOLUTION_ONLY = FALSE
|
|
AND MENU_URL IN (
|
|
'/admin/sysMng/subdomainList',
|
|
'/admin/userMng/companyList',
|
|
'/admin/audit-log'
|
|
)
|
|
""",
|
|
|
|
// V024 / RUN_089: TABLE_TYPE_COLUMNS.CODE_CATEGORY → CODE_INFO rename.
|
|
// 5/15 common-code 재설계(commit 2348800e) 가 mapper SQL 의 컬럼 참조명만
|
|
// 바꾸고 DB rename 을 빠뜨려, 테이블타입관리 컬럼 조회 API 가 500 반환.
|
|
// PostgreSQL 은 RENAME COLUMN 에 IF EXISTS 가 없어서 DO 블록으로 멱등 처리:
|
|
// - CODE_CATEGORY 만 있는 기존 테넌트: rename 수행
|
|
// - 이미 CODE_INFO 인 신규 테넌트: no-op
|
|
// - 둘 다 있거나 둘 다 없는 비정상 상태: no-op (방어적)
|
|
"""
|
|
DO $$
|
|
BEGIN
|
|
IF EXISTS (
|
|
SELECT 1 FROM information_schema.columns
|
|
WHERE table_schema = 'public'
|
|
AND table_name = 'table_type_columns'
|
|
AND column_name = 'code_category'
|
|
) AND NOT EXISTS (
|
|
SELECT 1 FROM information_schema.columns
|
|
WHERE table_schema = 'public'
|
|
AND table_name = 'table_type_columns'
|
|
AND column_name = 'code_info'
|
|
) THEN
|
|
ALTER TABLE TABLE_TYPE_COLUMNS
|
|
RENAME COLUMN CODE_CATEGORY TO CODE_INFO;
|
|
END IF;
|
|
END $$
|
|
""",
|
|
|
|
// V025 / RUN_090 (1) TABLE_TYPE_COLUMNS 중복 행 정리.
|
|
// PK 가 id 단일 (varchar) 인데 (TABLE_NAME, COLUMN_NAME, COMPANY_CODE) 에는
|
|
// UNIQUE 가 없어서 같은 키로 row 가 여러 개 INSERT 된 이력이 있음.
|
|
// 메타 DB 실측: 35K rows 중 2 그룹 4 row 가 중복. 그 그룹들은 동일 데이터를
|
|
// updated_date NULL 짜리 옛 row 와 2026-03-16 마지막 갱신 row 가 공존하는 형태.
|
|
// 가장 최근 (updated_date DESC NULLS LAST, id::bigint DESC) 행만 남기고 제거.
|
|
// 테넌트 DB 들은 실측상 중복 없음 → DELETE 0건. 멱등 (재실행해도 변화 없음).
|
|
"""
|
|
DELETE FROM TABLE_TYPE_COLUMNS
|
|
WHERE id IN (
|
|
SELECT id FROM (
|
|
SELECT id,
|
|
ROW_NUMBER() OVER (
|
|
PARTITION BY TABLE_NAME, COLUMN_NAME, COMPANY_CODE
|
|
ORDER BY UPDATED_DATE DESC NULLS LAST,
|
|
id::bigint DESC
|
|
) AS rn
|
|
FROM TABLE_TYPE_COLUMNS
|
|
) r
|
|
WHERE r.rn > 1
|
|
)
|
|
""",
|
|
|
|
// V025 / RUN_090 (2) ON CONFLICT 매칭용 UNIQUE INDEX 추가.
|
|
// mapper 의 upsertColumnSettings / upsertNullable / upsertUnique /
|
|
// upsertColumnInputType 모두 ON CONFLICT (TABLE_NAME, COLUMN_NAME, COMPANY_CODE)
|
|
// 를 쓰는데 DB 엔 매칭 unique 제약이 없어서 모든 쓰기 API 가 500.
|
|
// 인덱스 형태로 등록하면 ON CONFLICT 가 인식하고 ADD CONSTRAINT 식의
|
|
// IF NOT EXISTS 누락 문제도 회피.
|
|
"CREATE UNIQUE INDEX IF NOT EXISTS UX_TABLE_TYPE_COLUMNS_TCC ON TABLE_TYPE_COLUMNS (TABLE_NAME, COLUMN_NAME, COMPANY_CODE)",
|
|
|
|
// V026 / RUN_091: TABLE_TYPE_COLUMNS.INPUT_TYPE legacy → 표준 8종 정리.
|
|
// 5/15 common-code 재설계가 화이트리스트를 8종으로 좁혔지만 운영 DB 의
|
|
// 옛 값(category 886, select 149, textarea 102, checkbox 55, radio 12,
|
|
// datetime 2, boolean 1) 을 정리하는 마이그레이션을 빠뜨림.
|
|
// 매핑:
|
|
// category / select / radio / checkbox / boolean → code (commonCode 통합 의도)
|
|
// textarea → text (single/multi line 구분 손실 — UI 동작 가벼움)
|
|
// datetime → date
|
|
// 메타 DB 1,207 row 갱신. 테넌트 DB 들은 비어있어 영향 0.
|
|
// WHERE 절로 멱등 (재실행 시 0 row).
|
|
"""
|
|
UPDATE TABLE_TYPE_COLUMNS
|
|
SET INPUT_TYPE = CASE INPUT_TYPE
|
|
WHEN 'category' THEN 'code'
|
|
WHEN 'select' THEN 'code'
|
|
WHEN 'radio' THEN 'code'
|
|
WHEN 'checkbox' THEN 'code'
|
|
WHEN 'boolean' THEN 'code'
|
|
WHEN 'textarea' THEN 'text'
|
|
WHEN 'datetime' THEN 'date'
|
|
END,
|
|
UPDATED_DATE = NOW()
|
|
WHERE INPUT_TYPE IN ('category','select','radio','checkbox','boolean','textarea','datetime')
|
|
"""
|
|
);
|
|
|
|
@EventListener(ApplicationReadyEvent.class)
|
|
public void run() {
|
|
log.info("[SchemaMigrator] start — {} migration(s)", MIGRATIONS.size());
|
|
|
|
String metaDb = parseMetaDbName(metaJdbcUrl);
|
|
applyTo(metaDb, "meta");
|
|
|
|
// 테넌트 목록 조회는 반드시 메타 라우팅으로
|
|
DbContextHolder.setMeta();
|
|
List<String> tenantDbs;
|
|
try {
|
|
tenantDbs = sqlSession.selectList("provisioning.listActiveDbNames");
|
|
} catch (Exception e) {
|
|
log.warn("[SchemaMigrator] tenant list query failed — skipping tenants: {}", e.getMessage());
|
|
tenantDbs = List.of();
|
|
} finally {
|
|
DbContextHolder.clear();
|
|
}
|
|
|
|
int ok = 0, fail = 0;
|
|
List<String> failedDbs = new java.util.ArrayList<>();
|
|
for (String db : tenantDbs) {
|
|
if (db == null || db.isBlank() || db.equalsIgnoreCase(metaDb)) continue;
|
|
if (applyTo(db, "tenant")) {
|
|
ok++;
|
|
} else {
|
|
fail++;
|
|
failedDbs.add(db);
|
|
}
|
|
}
|
|
if (!failedDbs.isEmpty()) {
|
|
log.error("[SchemaMigrator] 마이그레이션 실패 테넌트 DB ({}건): {}", failedDbs.size(), failedDbs);
|
|
}
|
|
log.info("[SchemaMigrator] done — meta=done, tenants ok={}, fail={}", ok, fail);
|
|
}
|
|
|
|
private boolean applyTo(String dbName, String kind) {
|
|
String url = tenantDbSettings.buildJdbcUrl(dbName);
|
|
try (Connection c = DriverManager.getConnection(url, tenantDbSettings.username(), tenantDbSettings.password());
|
|
Statement s = c.createStatement()) {
|
|
for (String ddl : MIGRATIONS) {
|
|
s.execute(ddl);
|
|
}
|
|
log.info("[SchemaMigrator] {} db='{}' OK", kind, dbName);
|
|
return true;
|
|
} catch (Exception e) {
|
|
log.error("[SchemaMigrator] {} db='{}' FAILED: {}", kind, dbName, e.getMessage());
|
|
return false;
|
|
}
|
|
}
|
|
|
|
static String parseMetaDbName(String jdbcUrl) {
|
|
int slash = jdbcUrl.lastIndexOf('/');
|
|
if (slash < 0) return "invyone";
|
|
String tail = jdbcUrl.substring(slash + 1);
|
|
int q = tail.indexOf('?');
|
|
return q < 0 ? tail : tail.substring(0, q);
|
|
}
|
|
}
|