- common_util_project_copy.rekey_id_files: 코리도 이름 corridor_{노선 id:04d} → 새 노선 id(겹침 대비 임시 이름 거침) · 짝 없는 옛 코리도 지움 · B06_Section/sampling.json surface_model_id → 새 지표면 id
- _insert_rows · _insert_full 이 (지표면 · 노선) id 짝을 함께 돌려줌 · full 복제 · --replace · --storage-only 셋 다 _fix_files 에서 rekey
- id_maps_by_order: 이미 복제된 두 프로젝트의 id 짝(차례 · 행 수 다르면 ValueError) — storage-only 가 씀
- storage 경로 함수 전수: id 박힌 이름 = 코리도뿐(B08 이정표 그림 이름 = 거래처 상자 id · 추천 저장본 = 해시) · 내용 = sampling.json 지표면 id(초기값 스냅샷 지표면 id 는 종전대로)
- 지킴 시험 test_761_2: 노선 id 짝 단정 · rekey(이름 겹침 · 짝 없음 지움 · sampling)
- 바꿔 넣은 B 13 개 고침(tmp 스크립트 · 차례 짝 + 내용 칸 대조): 코리도 셋씩 새 id · corridor_0370 지움 · sampling 911 → 각 새 id
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
150 lines
6.1 KiB
Python
150 lines
6.1 KiB
Python
"""761-2 복제 도구 자리 바꿔 넣기(--replace) — 순수 부분(DB · 파일 안 씀).
|
|
|
|
- 유지 칸: 대상 id · 이름 · 주인 · 회사 · 종류 · 저장 자리 · 담당자는 대상 값 · 나머지는 원본
|
|
- 지우는 표 목록 = 복제 표 목록(projects · project_members 뺌)과 같음 — 표가 늘면 여기서 빨강
|
|
- id 다시 잇기: 새 행이 새 노선 · 새 지표면 · 새 입력 파일 id 를 가리킴 · 단계 설정 id 도
|
|
- 노선 · 지표면 id 가 박힌 storage 파일(코리도 이름 · sampling.json) → 새 id · 짝 없는 코리도 지움(761-5)
|
|
"""
|
|
|
|
import asyncio
|
|
import json
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
sys.path.insert(0, str(Path(__file__).resolve().parents[2]))
|
|
|
|
from common_util import common_util_project_copy as cp # noqa: E402
|
|
from common_util import common_util_project_copy_replace as rp # noqa: E402
|
|
|
|
|
|
class _Cursor:
|
|
"""execute 를 적어 두고 · INSERT 마다 새 자동증가 id · SELECT 는 projects 만 한 줄."""
|
|
|
|
def __init__(self):
|
|
self.inserts: list[tuple[str, dict]] = []
|
|
self.selects: list[str] = []
|
|
self.lastrowid = 1000
|
|
self._last = ""
|
|
|
|
async def execute(self, sql, args=()):
|
|
self._last = sql
|
|
if sql.startswith("INSERT INTO"):
|
|
table = sql.split("`")[1]
|
|
names = [part.strip("` ") for part in sql.split("(")[1].split(")")[0].split(",")]
|
|
self.inserts.append((table, dict(zip(names, args, strict=True))))
|
|
self.lastrowid += 1
|
|
else:
|
|
self.selects.append(sql)
|
|
return 0
|
|
|
|
async def fetchall(self):
|
|
return [{"id": "p", "name": "x"}] if "FROM projects" in self._last else []
|
|
|
|
|
|
def test_merged_project_row_keeps_target_identity():
|
|
source = {
|
|
"id": "src",
|
|
"name": "가이드",
|
|
"user_id": 3,
|
|
"company_id": 1,
|
|
"project_kind": "ROAD",
|
|
"storage_path": "storage/1/3/src",
|
|
"region": "용화",
|
|
"status": "WF3_ANALYZING",
|
|
"crs_epsg": 5178,
|
|
"pm_user_id": 3,
|
|
"updated_at": "t",
|
|
}
|
|
target = {
|
|
**{key: f"T-{key}" for key in source},
|
|
"id": "dst",
|
|
"storage_path": "storage/1/9/dst",
|
|
}
|
|
merged = rp.merged_project_row(source, target)
|
|
assert "id" not in merged and "updated_at" not in merged
|
|
for key in ("name", "user_id", "company_id", "project_kind", "storage_path", "pm_user_id"):
|
|
assert merged[key] == target[key]
|
|
for key in ("region", "status", "crs_epsg"):
|
|
assert merged[key] == source[key]
|
|
|
|
|
|
def test_delete_tables_match_clone_tables():
|
|
cursor = _Cursor()
|
|
rows = asyncio.run(cp._source_rows(cursor, "p"))
|
|
cloned = set(rows) - {"projects", "project_members"}
|
|
assert set(rp.ROUTE_CHILD_TABLES) | set(rp.PROJECT_TABLES) == cloned
|
|
|
|
|
|
def test_insert_rows_relinks_ids():
|
|
rows = {
|
|
"input_files": [{"id": 1, "project_id": "src", "name": "a.las"}],
|
|
"processed_point_cloud": [{"id": 2, "project_id": "src", "input_file_id": 1}],
|
|
"surface_models": [
|
|
{"id": 3, "project_id": "src", "source_file_id": 1, "processed_cloud_id": 2}
|
|
],
|
|
"terrain_layers": [{"id": 4, "surface_model_id": 3}],
|
|
"routes": [{"id": 5, "project_id": "src", "surface_model_id": 3}],
|
|
"route_points": [{"id": 6, "route_id": 5}],
|
|
"route_statistics": [],
|
|
"longitudinal_sections": [{"id": 7, "route_id": 5, "project_id": "src"}],
|
|
"cross_sections": [],
|
|
"project_workflow_stages": [
|
|
{
|
|
"id": 8,
|
|
"project_id": "src",
|
|
"stage_no": 2,
|
|
"params": json.dumps({"input_file_id": "1", "surface_model_id": 3}),
|
|
}
|
|
],
|
|
**{table: [] for table in cp._PLAN_ALL},
|
|
}
|
|
cursor = _Cursor()
|
|
model_map, route_map = asyncio.run(cp._insert_rows(cursor, rows, "dst"))
|
|
new = {table: row for table, row in cursor.inserts}
|
|
assert [table for table, _ in cursor.inserts][0] == "input_files"
|
|
assert "projects" not in new and "project_members" not in new
|
|
assert all(row.get("project_id", "dst") == "dst" for _, row in cursor.inserts)
|
|
assert all("id" not in row for _, row in cursor.inserts)
|
|
file_id, cloud_id, model_id = 1001, 1002, 1003
|
|
assert new["processed_point_cloud"]["input_file_id"] == file_id
|
|
assert new["surface_models"]["source_file_id"] == file_id
|
|
assert new["surface_models"]["processed_cloud_id"] == cloud_id
|
|
assert model_map == {3: model_id}
|
|
assert new["terrain_layers"]["surface_model_id"] == model_id
|
|
assert new["routes"]["surface_model_id"] == model_id
|
|
route_id = 1005
|
|
assert route_map == {5: route_id}
|
|
assert new["route_points"]["route_id"] == route_id
|
|
assert new["longitudinal_sections"]["route_id"] == route_id
|
|
params = json.loads(new["project_workflow_stages"]["params"])
|
|
assert params == {"input_file_id": str(file_id), "surface_model_id": model_id}
|
|
|
|
|
|
def test_rekey_id_files_moves_route_and_model_ids(tmp_path):
|
|
corridor = tmp_path / "B05_Profile" / "corridor"
|
|
corridor.mkdir(parents=True)
|
|
# 옛 1068 → 새 1069 · 옛 1069 → 새 1091 (이름 겹침) · 0370 = 짝 없음
|
|
for old in (370, 1068, 1069):
|
|
(corridor / f"corridor_{old:04d}.json").write_text(str(old), encoding="utf-8")
|
|
(corridor / "corridor_x.json").write_text("x", encoding="utf-8")
|
|
sampling = tmp_path / "B06_Section" / "sampling.json"
|
|
sampling.parent.mkdir(parents=True)
|
|
sampling.write_text(json.dumps({"filter_key": "classification", "surface_model_id": 911}))
|
|
|
|
report = cp.rekey_id_files(tmp_path, {911: 952}, {1068: 1069, 1069: 1091})
|
|
|
|
names = {path.name: path.read_text(encoding="utf-8") for path in corridor.iterdir()}
|
|
assert names == {
|
|
"corridor_1069.json": "1068",
|
|
"corridor_1091.json": "1069",
|
|
"corridor_x.json": "x",
|
|
}
|
|
assert report["corridor_removed"] == ["corridor_0370.json"]
|
|
assert json.loads(sampling.read_text())["surface_model_id"] == 952
|
|
# 폴더 없음 = 빈 보고
|
|
assert cp.rekey_id_files(tmp_path / "없음", {}, {}) == {
|
|
"corridor": {},
|
|
"corridor_removed": [],
|
|
"sampling": None,
|
|
}
|