Files
pbf/navsea_merge_navgrid_mysql.py

394 lines
14 KiB
Python

#!/usr/bin/env python3
from __future__ import annotations
import argparse
import json
from dataclasses import dataclass
from pathlib import Path
import pymysql
DB_USER = "root"
DB_PASSWORD = "2chi9ks2"
DB_HOST = "localhost"
DB_SOCKET = "/tmp/mysql.sock"
DEFAULT_TARGET_DB = "navsea_japan_coast_grid"
DEFAULT_SOURCE_DB = "navsea_fukuoka_saga_grid"
DEFAULT_KEEP_LAYERS = ("fish_port_20m", "hazard_50m")
DEFAULT_JOB_NAME = "fish_port_20m_full_mysql_resume"
@dataclass(frozen=True)
class LayerSummary:
layer_name: str
count: int
bbox: tuple[float, float, float, float] | None
source_desc: str | None
export_file: str | None
def mysql_connect(database: str | None = None):
kwargs = {
"host": DB_HOST,
"user": DB_USER,
"password": DB_PASSWORD,
"charset": "utf8mb4",
"autocommit": False,
"cursorclass": pymysql.cursors.Cursor,
}
if DB_SOCKET:
kwargs["unix_socket"] = DB_SOCKET
if database:
kwargs["database"] = database
return pymysql.connect(**kwargs)
def ensure_database(name: str) -> None:
conn = mysql_connect()
try:
with conn.cursor() as cur:
cur.execute(
f"CREATE DATABASE IF NOT EXISTS `{name}` "
"DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci"
)
conn.commit()
finally:
conn.close()
def ensure_target_schema(conn) -> None:
stmts = [
"""
CREATE TABLE IF NOT EXISTS navsea_grid_import_state (
job_name VARCHAR(64) NOT NULL PRIMARY KEY,
layer_name VARCHAR(32) NOT NULL,
source_name VARCHAR(191) NOT NULL,
checkpoint_json LONGTEXT NOT NULL,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
""",
"""
CREATE TABLE IF NOT EXISTS navsea_grid_import_progress (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
job_name VARCHAR(64) NOT NULL,
layer_name VARCHAR(32) NOT NULL,
prc VARCHAR(8) NOT NULL,
status_name VARCHAR(32) NOT NULL,
cluster_count INT NOT NULL,
inserted_cells BIGINT NOT NULL,
land_cells BIGINT NOT NULL,
sea_cells BIGINT NOT NULL,
elapsed_sec DOUBLE NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
KEY idx_job_layer_prc (job_name, layer_name, prc),
KEY idx_job_created (job_name, created_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
""",
"""
CREATE TABLE IF NOT EXISTS navsea_grid_layer_meta (
layer_name VARCHAR(32) NOT NULL PRIMARY KEY,
description VARCHAR(255) NOT NULL,
source_desc TEXT NOT NULL,
cell_size_m DOUBLE NOT NULL,
feature_count BIGINT NOT NULL,
bbox_min_lon DOUBLE NOT NULL,
bbox_min_lat DOUBLE NOT NULL,
bbox_max_lon DOUBLE NOT NULL,
bbox_max_lat DOUBLE NOT NULL,
export_file VARCHAR(255) DEFAULT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
""",
"""
CREATE TABLE IF NOT EXISTS navsea_grid_cell (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
layer_name VARCHAR(32) NOT NULL,
cell_id VARCHAR(64) NOT NULL,
row_idx INT NOT NULL,
col_idx INT NOT NULL,
cell_size_m DOUBLE NOT NULL,
state_name VARCHAR(32) NOT NULL,
class_name VARCHAR(32) NOT NULL,
source_name VARCHAR(191) NOT NULL,
min_lon DOUBLE NOT NULL,
min_lat DOUBLE NOT NULL,
max_lon DOUBLE NOT NULL,
max_lat DOUBLE NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
UNIQUE KEY uniq_layer_cell (layer_name, cell_id),
KEY idx_layer_state (layer_name, state_name),
KEY idx_layer_rowcol (layer_name, row_idx, col_idx)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
""",
]
with conn.cursor() as cur:
for stmt in stmts:
cur.execute(stmt)
conn.commit()
def table_count(conn, table_name: str) -> int:
with conn.cursor() as cur:
cur.execute(f"SELECT COUNT(*) FROM `{table_name}`")
return int(cur.fetchone()[0] or 0)
def fetch_layer_summary(conn, layer_name: str) -> LayerSummary:
with conn.cursor() as cur:
cur.execute(
"""
SELECT COUNT(*), MIN(min_lon), MIN(min_lat), MAX(max_lon), MAX(max_lat)
FROM navsea_grid_cell
WHERE layer_name=%s
""",
(layer_name,),
)
row = cur.fetchone()
count = int(row[0] or 0)
bbox = None if count == 0 else (float(row[1]), float(row[2]), float(row[3]), float(row[4]))
cur.execute(
"""
SELECT description, source_desc, export_file
FROM navsea_grid_layer_meta
WHERE layer_name=%s
""",
(layer_name,),
)
meta_row = cur.fetchone()
if meta_row is None:
source_desc = None
export_file = None
else:
source_desc = str(meta_row[1])
export_file = meta_row[2]
return LayerSummary(layer_name=layer_name, count=count, bbox=bbox, source_desc=source_desc, export_file=export_file)
def clear_target_layers(conn, layers: tuple[str, ...], job_name: str) -> None:
with conn.cursor() as cur:
placeholders = ", ".join(["%s"] * len(layers))
cur.execute(f"DELETE FROM navsea_grid_cell WHERE layer_name IN ({placeholders})", layers)
cur.execute(f"DELETE FROM navsea_grid_layer_meta WHERE layer_name IN ({placeholders})", layers)
cur.execute(
f"DELETE FROM navsea_grid_import_progress WHERE job_name=%s AND layer_name IN ({placeholders})",
(job_name, *layers),
)
cur.execute("DELETE FROM navsea_grid_import_state WHERE job_name=%s", (job_name,))
conn.commit()
def clear_target_rows_by_job(conn, job_name: str) -> None:
with conn.cursor() as cur:
cur.execute("DELETE FROM navsea_grid_import_progress WHERE job_name=%s", (job_name,))
cur.execute("DELETE FROM navsea_grid_import_state WHERE job_name=%s", (job_name,))
conn.commit()
def copy_layer_cells(source_db: str, target_conn, layer_name: str) -> int:
with target_conn.cursor() as cur:
cur.execute("DELETE FROM navsea_grid_cell WHERE layer_name=%s", (layer_name,))
cur.execute(
"""
INSERT INTO navsea_grid_cell
(layer_name, cell_id, row_idx, col_idx, cell_size_m, state_name, class_name, source_name,
min_lon, min_lat, max_lon, max_lat)
SELECT
layer_name, cell_id, row_idx, col_idx, cell_size_m, state_name, class_name, source_name,
min_lon, min_lat, max_lon, max_lat
FROM `{source_db}`.navsea_grid_cell
WHERE layer_name=%s
""".format(source_db=source_db),
(layer_name,),
)
target_conn.commit()
cur.execute("SELECT COUNT(*) FROM navsea_grid_cell WHERE layer_name=%s", (layer_name,))
return int(cur.fetchone()[0] or 0)
def copy_progress_tables(source_db: str, target_conn, job_name: str) -> None:
with target_conn.cursor() as cur:
cur.execute("DELETE FROM navsea_grid_import_progress WHERE job_name=%s", (job_name,))
cur.execute("DELETE FROM navsea_grid_import_state WHERE job_name=%s", (job_name,))
cur.execute(
f"""
INSERT INTO navsea_grid_import_state (job_name, layer_name, source_name, checkpoint_json, updated_at)
SELECT job_name, layer_name, source_name, checkpoint_json, updated_at
FROM `{source_db}`.navsea_grid_import_state
WHERE job_name=%s
""",
(job_name,),
)
cur.execute(
f"""
INSERT INTO navsea_grid_import_progress
(job_name, layer_name, prc, status_name, cluster_count, inserted_cells, land_cells, sea_cells, elapsed_sec, created_at)
SELECT
job_name, layer_name, prc, status_name, cluster_count, inserted_cells, land_cells, sea_cells, elapsed_sec, created_at
FROM `{source_db}`.navsea_grid_import_progress
WHERE job_name=%s
ORDER BY id
""",
(job_name,),
)
target_conn.commit()
def upsert_layer_meta(
target_conn,
*,
layer_name: str,
description: str,
source_desc: str,
cell_size_m: float,
feature_count: int,
bbox: tuple[float, float, float, float],
export_file: str | None,
) -> None:
with target_conn.cursor() as cur:
cur.execute(
"""
INSERT INTO navsea_grid_layer_meta
(layer_name, description, source_desc, cell_size_m, feature_count,
bbox_min_lon, bbox_min_lat, bbox_max_lon, bbox_max_lat, export_file)
VALUES
(%s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
ON DUPLICATE KEY UPDATE
description=VALUES(description),
source_desc=VALUES(source_desc),
cell_size_m=VALUES(cell_size_m),
feature_count=VALUES(feature_count),
bbox_min_lon=VALUES(bbox_min_lon),
bbox_min_lat=VALUES(bbox_min_lat),
bbox_max_lon=VALUES(bbox_max_lon),
bbox_max_lat=VALUES(bbox_max_lat),
export_file=VALUES(export_file)
""",
(
layer_name,
description,
source_desc,
cell_size_m,
feature_count,
bbox[0],
bbox[1],
bbox[2],
bbox[3],
export_file,
),
)
target_conn.commit()
def drop_database(name: str) -> None:
conn = mysql_connect()
try:
with conn.cursor() as cur:
cur.execute(f"DROP DATABASE IF EXISTS `{name}`")
conn.commit()
finally:
conn.close()
def main() -> None:
parser = argparse.ArgumentParser(description="把 navsea_fukuoka_saga_grid 合并进 navsea_japan_coast_grid。")
parser.add_argument("--target-db", default=DEFAULT_TARGET_DB)
parser.add_argument("--source-db", default=DEFAULT_SOURCE_DB)
parser.add_argument("--keep-layers", nargs="+", default=list(DEFAULT_KEEP_LAYERS), help="默认搬运的 layer")
parser.add_argument("--drop-source-db", action="store_true", help="合并完成后删除源数据库")
parser.add_argument("--dry-run", action="store_true", help="只打印将要执行的动作,不真正写入")
args = parser.parse_args()
keep_layers = tuple(args.keep_layers)
ensure_database(args.target_db)
source_conn = mysql_connect(args.source_db)
target_conn = mysql_connect(args.target_db)
try:
ensure_target_schema(target_conn)
print(f"目标库: {args.target_db}")
print(f"源库: {args.source_db}")
print(f"搬运图层: {', '.join(keep_layers)}")
if args.dry_run:
for layer in keep_layers:
summary = fetch_layer_summary(source_conn, layer)
print(f"DRY-RUN {layer}: source_count={summary.count} bbox={summary.bbox}")
return
clear_target_layers(target_conn, keep_layers, DEFAULT_JOB_NAME)
clear_target_rows_by_job(target_conn, DEFAULT_JOB_NAME)
# 先把需要保留的图层从源库搬到全国库。
copied_counts: dict[str, int] = {}
for layer in keep_layers:
with source_conn.cursor() as cur:
cur.execute("SELECT COUNT(*) FROM navsea_grid_cell WHERE layer_name=%s", (layer,))
source_count = int(cur.fetchone()[0] or 0)
if source_count == 0:
print(f"警告: 源库里没有 layer={layer},跳过")
continue
copied = copy_layer_cells(args.source_db, target_conn, layer)
copied_counts[layer] = copied
print(f"已搬运 {layer}: {copied} 条")
# 迁移状态表,方便后续继续 resume / 审计。
if table_count(source_conn, "navsea_grid_import_state") > 0:
copy_progress_tables(args.source_db, target_conn, DEFAULT_JOB_NAME)
print("已搬运 import_state / import_progress")
# 重新写目标库里的图层元信息。
if "hazard_50m" in copied_counts:
hazard_summary = fetch_layer_summary(target_conn, "hazard_50m")
upsert_layer_meta(
target_conn,
layer_name="hazard_50m",
description="福冈/佐贺 PBF 海上障碍 50m 黄格",
source_desc="pbf-delivery-kyushu-reencoded z12",
cell_size_m=50.0,
feature_count=hazard_summary.count,
bbox=hazard_summary.bbox or (0.0, 0.0, 0.0, 0.0),
export_file="src/pbf/coastline-mysql/fukuoka_saga/hazard_50m_grid.geojson",
)
if "fish_port_20m" in copied_counts:
fish_summary = fetch_layer_summary(target_conn, "fish_port_20m")
upsert_layer_meta(
target_conn,
layer_name="fish_port_20m",
description="全国渔港 20m 可航/不可航格",
source_desc="coastline/C09-06.zip + navsea_japan_coast_grid.coast_200m",
cell_size_m=20.0,
feature_count=fish_summary.count,
bbox=fish_summary.bbox or (0.0, 0.0, 0.0, 0.0),
export_file="src/pbf/coastline-mysql/fukuoka_saga/fish_port_20m_grid.geojson",
)
manifest = {
"target_db": args.target_db,
"source_db": args.source_db,
"kept_layers": list(keep_layers),
"layer_counts": {layer: copied_counts.get(layer, 0) for layer in keep_layers},
"completed": True,
}
manifest_path = Path("out/navsea_merge_navgrid_mysql.manifest.json")
manifest_path.parent.mkdir(parents=True, exist_ok=True)
manifest_path.write_text(json.dumps(manifest, ensure_ascii=False, indent=2), encoding="utf-8")
print(f"已写出清单: {manifest_path}")
finally:
target_conn.close()
source_conn.close()
if args.drop_source_db:
drop_database(args.source_db)
print(f"已删除源数据库: {args.source_db}")
if __name__ == "__main__":
main()