fix(rosseti): считать координату ключа в SQL тем же double, что в питоне
All checks were successful
CI Trade-In / changes (pull_request) Successful in 12s
CI / changes (pull_request) Successful in 14s
CI Trade-In / backend-tests (pull_request) Has been skipped
CI Trade-In / browser-tests (pull_request) Has been skipped
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Successful in 2m24s
CI / backend-tests (pull_request) Successful in 17m41s
All checks were successful
CI Trade-In / changes (pull_request) Successful in 12s
CI / changes (pull_request) Successful in 14s
CI Trade-In / backend-tests (pull_request) Has been skipped
CI Trade-In / browser-tests (pull_request) Has been skipped
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Successful in 2m24s
CI / backend-tests (pull_request) Successful in 17m41s
Ревью PR #3329. round(ST_X(geom)::numeric * 100000) округляет по кратчайшему десятичному представлению float8, а питон — по двоичному double: расхождение на 0.19% реальных координат (761 из 400000), напр. 64.423605 → питон 6442360 (6442360.499999999), numeric-путь 6442361. Каждое расхождение = вечный дубль ЦП, который сам не зарастёт — миграция применяется один раз (_schema_migrations). Теперь в SQL sign/floor/abs над float8 без каста в numeric: IEEE754 бит в бит как math.floor в питоне. test_coord_e5 брал 60.123455, где двоичное и десятичное округление совпадают — защита, которая не защищает. Добавлено расходящееся значение 64.423605. RAISE WARNING при rows_after > 700 заменён на RAISE EXCEPTION: warning не останавливает прогон, файл помечался бы applied навсегда вместе с дублями. Refs #3322
This commit is contained in:
parent
b35f444d50
commit
6a2d873c70
3 changed files with 33 additions and 10 deletions
|
|
@ -107,7 +107,10 @@ def _coord_e5(value: float | None) -> str:
|
|||
|
||||
Целое, а не форматированный float: ключ обязан совпадать байт-в-байт с SQL-
|
||||
бэкфиллом (99c), а текстовое представление double в питоне и в PG разное.
|
||||
``floor(|v|*1e5 + 0.5)`` со знаком = ``round(numeric)`` в PG (half-away-from-zero).
|
||||
Округление ДВОИЧНОЕ (по значению double, не по десятичному представлению):
|
||||
64.423605*1e5 == 6442360.499999999 → 6442360, хотя «по десятичному» было бы
|
||||
6442361. В 99c та же семантика: floor/abs/sign над float8, БЕЗ каста в numeric
|
||||
(каст округляет по кратчайшему десятичному repr и расходится в 0.19% координат).
|
||||
"""
|
||||
if value is None:
|
||||
return ""
|
||||
|
|
|
|||
|
|
@ -139,8 +139,16 @@ def test_stable_external_id_no_geometry() -> None:
|
|||
assert rw._stable_external_id(f, f["properties"]) == "h:eb91917f35aff23f"
|
||||
|
||||
|
||||
def test_coord_e5_half_away_from_zero() -> None:
|
||||
"""Квантование как round(numeric) в PG: полукругление ОТ нуля, симметрично знаку."""
|
||||
def test_coord_e5_rounds_on_binary_double_not_decimal() -> None:
|
||||
"""Округление по ДВОИЧНОМУ double, не по десятичному представлению.
|
||||
|
||||
64.423605*1e5 == 6442360.499999999 → 6442360; «по десятичному» вышло бы 6442361
|
||||
(так считал бы round(ST_X(geom)::numeric*100000) — расхождение на 0.19% реальных
|
||||
координат). 99c обязана давать те же цифры, поэтому семантика закреплена тестом.
|
||||
"""
|
||||
assert rw._coord_e5(64.423605) == "6442360"
|
||||
assert rw._coord_e5(-64.423605) == "-6442360"
|
||||
# 60.123455*1e5 == ровно 6012345.5 → полукругление ОТ нуля, симметрично знаку.
|
||||
assert rw._coord_e5(60.123455) == "6012346"
|
||||
assert rw._coord_e5(-60.123455) == "-6012346"
|
||||
assert rw._coord_e5(60.6) == "6060000"
|
||||
|
|
|
|||
|
|
@ -23,9 +23,17 @@
|
|||
-- seed = sc_name_norm || '|' || voltage_class || '|' || lon_e5 || '|' || lat_e5
|
||||
-- external_id = 'h:' || left(hex(sha256(utf8(seed))), 16)
|
||||
-- где lon_e5/lat_e5 — координата в единицах 1e-5 градуса (~1 м), округление
|
||||
-- round() = half-away-from-zero (в питоне ровно то же: floor(|v|*1e5+0.5) со знаком);
|
||||
-- пустая строка, если geom отсутствует. Целые, а не форматированный float —
|
||||
-- текстовое представление double в питоне и в PG различается.
|
||||
-- floor(|v|*1e5 + 0.5) со знаком — ДВОИЧНОЕ, ровно как в питоне; пустая строка,
|
||||
-- если geom отсутствует. Целые, а не форматированный float — текстовое
|
||||
-- представление double в питоне и в PG различается.
|
||||
--
|
||||
-- ПОЧЕМУ НЕ round(...::numeric): каст float8→numeric берёт кратчайшее десятичное
|
||||
-- представление, и округление идёт по нему, а не по двоичному double. На реальных
|
||||
-- координатах расходится в 0.19% случаев (замер: 761 из 400000), напр. 64.423605
|
||||
-- → питон 6442360 (двоичное 6442360.499999999), numeric-путь 6442361. Каждое
|
||||
-- расхождение = вечный дубль ЦП, который сам не зарастёт: миграция применяется
|
||||
-- один раз (_schema_migrations). Поэтому в SQL считаем ТЕМ ЖЕ double: floor/abs/
|
||||
-- sign над float8 — это IEEE754, бит в бит как math.floor в питоне.
|
||||
-- sha256, а не sha1: sha256 встроен в PG16, sha1 потребовал бы pgcrypto.
|
||||
--
|
||||
-- Байт-в-байт совпадение с питоном держится на том, что SQL НИЧЕГО не нормализует
|
||||
|
|
@ -68,9 +76,11 @@ BEGIN
|
|||
sc_name_norm
|
||||
|| '|' || coalesce(voltage_class, '')
|
||||
|| '|' || CASE WHEN geom IS NULL THEN ''
|
||||
ELSE round(ST_X(geom)::numeric * 100000)::text END
|
||||
ELSE (sign(ST_X(geom))
|
||||
* floor(abs(ST_X(geom)) * 100000 + 0.5))::bigint::text END
|
||||
|| '|' || CASE WHEN geom IS NULL THEN ''
|
||||
ELSE round(ST_Y(geom)::numeric * 100000)::text END,
|
||||
ELSE (sign(ST_Y(geom))
|
||||
* floor(abs(ST_Y(geom)) * 100000 + 0.5))::bigint::text END,
|
||||
'UTF8'
|
||||
)),
|
||||
'hex'
|
||||
|
|
@ -116,9 +126,11 @@ BEGIN
|
|||
rows_before, names_before, rows_after, names_after, deleted, rekeyed;
|
||||
|
||||
-- 700 — потолок здравого смысла: источник отдаёт ~488 ЦП по области.
|
||||
-- Превышение = формула ключа не схлопнула дубли, смотреть глазами.
|
||||
-- Превышение = формула ключа не схлопнула дубли. EXCEPTION, а не WARNING:
|
||||
-- иначе файл пометится applied навсегда, а дубли останутся. Откат всей
|
||||
-- транзакции ничего не теряет и оставляет миграцию непринятой до разбора.
|
||||
IF rows_after > 700 THEN
|
||||
RAISE WARNING '#3322: после дедупа осталось % строк (ожидалось ~481-488) — проверить формулу ключа', rows_after;
|
||||
RAISE EXCEPTION '#3322: после дедупа осталось % строк (ожидалось ~481-488) — формула ключа не схлопнула дубли, транзакция откачена', rows_after;
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue