fix: 平衡报工归属拆分并约束唯一性

This commit is contained in:
souplearn
2026-07-25 06:33:12 +08:00
parent 6065ab031d
commit 4c0cbc9815
5 changed files with 283 additions and 27 deletions
+100 -19
View File
@@ -49,11 +49,21 @@ REQUIRED_COLUMNS = {
},
}
REQUIRED_INDEXES = {
"idx_report_allocations_report": ("report_id",),
"idx_report_allocations_item": ("report_item_id",),
"idx_report_allocations_date": ("allocation_date",),
"idx_report_allocations_point_date": ("attendance_point_name", "allocation_date"),
"idx_report_allocations_employee_date": ("employee_phone", "allocation_date"),
"uq_report_allocations_report_item_date": {
"columns": ("report_id", "report_item_id", "allocation_date"),
"non_unique": 0,
},
"idx_report_allocations_report": {"columns": ("report_id",), "non_unique": 1},
"idx_report_allocations_item": {"columns": ("report_item_id",), "non_unique": 1},
"idx_report_allocations_date": {"columns": ("allocation_date",), "non_unique": 1},
"idx_report_allocations_point_date": {
"columns": ("attendance_point_name", "allocation_date"),
"non_unique": 1,
},
"idx_report_allocations_employee_date": {
"columns": ("employee_phone", "allocation_date"),
"non_unique": 1,
},
}
REQUIRED_FOREIGN_KEYS = {
"report_id": ("production_reports", "id", "CASCADE"),
@@ -153,22 +163,51 @@ def existing_foreign_keys(conn) -> dict[str, tuple[str, str, str]]:
return {row[0]: (row[1], row[2], row[3]) for row in rows}
def find_equivalent_index(indexes: dict[str, IndexMetadata], columns: tuple[str, ...]) -> str | None:
def required_index_columns(index_name: str) -> tuple[str, ...]:
return REQUIRED_INDEXES[index_name]["columns"]
def required_index_non_unique(index_name: str) -> int:
return int(REQUIRED_INDEXES[index_name]["non_unique"])
def find_equivalent_index(
indexes: dict[str, IndexMetadata],
columns: tuple[str, ...],
non_unique: int,
) -> str | None:
for index_name, metadata in indexes.items():
if index_name != "PRIMARY" and metadata["columns"] == columns and metadata["non_unique"] == 1:
if (
index_name != "PRIMARY"
and metadata["columns"] == columns
and metadata["non_unique"] == non_unique
):
return index_name
return None
def create_index(conn, index_name: str, columns: tuple[str, ...]) -> None:
if index_name not in REQUIRED_INDEXES or REQUIRED_INDEXES[index_name] != columns:
if index_name not in REQUIRED_INDEXES or required_index_columns(index_name) != columns:
raise ValueError(f"unexpected index definition: {index_name} {columns}")
non_unique = required_index_non_unique(index_name)
indexes = existing_indexes(conn)
if index_name in indexes:
if (
index_name in indexes
and indexes[index_name]["columns"] == columns
and indexes[index_name]["non_unique"] == non_unique
):
return
if index_name in indexes:
conn.execute(
text(
f"ALTER TABLE {quote_identifier(TABLE_NAME)} "
f"DROP INDEX {quote_identifier(index_name)}"
)
)
indexes = existing_indexes(conn)
equivalent_index = find_equivalent_index(indexes, columns)
equivalent_index = find_equivalent_index(indexes, columns, non_unique)
if equivalent_index:
conn.execute(
text(
@@ -179,14 +218,51 @@ def create_index(conn, index_name: str, columns: tuple[str, ...]) -> None:
return
column_sql = ", ".join(quote_identifier(column) for column in columns)
unique_sql = "UNIQUE " if non_unique == 0 else ""
conn.execute(
text(
f"CREATE INDEX {quote_identifier(index_name)} "
f"CREATE {unique_sql}INDEX {quote_identifier(index_name)} "
f"ON {quote_identifier(TABLE_NAME)} ({column_sql})"
)
)
def cleanup_duplicate_allocation_keys(conn) -> None:
if not table_exists(conn):
return
conn.execute(
text(
"""
DELETE target
FROM production_report_allocations target
JOIN (
SELECT id
FROM (
SELECT allocation.id
FROM production_report_allocations allocation
JOIN (
SELECT
report_id,
report_item_id,
allocation_date,
MIN(id) AS keep_id
FROM production_report_allocations
WHERE report_item_id IS NOT NULL
GROUP BY report_id, report_item_id, allocation_date
HAVING COUNT(*) > 1
) duplicate_key
ON duplicate_key.report_id = allocation.report_id
AND duplicate_key.report_item_id = allocation.report_item_id
AND duplicate_key.allocation_date = allocation.allocation_date
WHERE allocation.id <> duplicate_key.keep_id
) duplicate_rows
) doomed
ON doomed.id = target.id
"""
)
)
def cleanup_duplicate_fk_indexes(conn) -> None:
indexes = existing_indexes(conn)
duplicates = [
@@ -235,6 +311,8 @@ def ensure_schema(conn) -> None:
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (id),
UNIQUE KEY uq_report_allocations_report_item_date
(report_id, report_item_id, allocation_date),
KEY idx_report_allocations_report (report_id),
KEY idx_report_allocations_item (report_item_id),
KEY idx_report_allocations_date (allocation_date),
@@ -251,8 +329,9 @@ def ensure_schema(conn) -> None:
)
)
for index_name, columns in REQUIRED_INDEXES.items():
create_index(conn, index_name, columns)
cleanup_duplicate_allocation_keys(conn)
for index_name in REQUIRED_INDEXES:
create_index(conn, index_name, required_index_columns(index_name))
cleanup_duplicate_fk_indexes(conn)
@@ -319,19 +398,21 @@ def verify_schema(conn) -> None:
wrong_indexes = [
f"{index_name}({', '.join(indexes[index_name]['columns'])})"
for index_name, columns in REQUIRED_INDEXES.items()
if indexes.get(index_name, {}).get("columns") != columns
for index_name in REQUIRED_INDEXES
if indexes.get(index_name, {}).get("columns") != required_index_columns(index_name)
]
if wrong_indexes:
raise RuntimeError(f"{TABLE_NAME} incompatible indexes: {', '.join(wrong_indexes)}")
unique_required_indexes = [
wrong_index_uniqueness = [
index_name
for index_name in REQUIRED_INDEXES
if indexes[index_name]["non_unique"] != 1
if indexes[index_name]["non_unique"] != required_index_non_unique(index_name)
]
if unique_required_indexes:
raise RuntimeError(f"{TABLE_NAME} required indexes are unique: {', '.join(unique_required_indexes)}")
if wrong_index_uniqueness:
raise RuntimeError(
f"{TABLE_NAME} incompatible index uniqueness: {', '.join(wrong_index_uniqueness)}"
)
duplicate_single_column_indexes = [
index_name